Skip to content

feat(devices): fleet migration posture report (#3244) - #3264

Open
ToddHebebrand wants to merge 2 commits into
mainfrom
feat/3244-fleet-posture-report
Open

feat(devices): fleet migration posture report (#3244)#3264
ToddHebebrand wants to merge 2 commits into
mainfrom
feat/3244-fleet-posture-report

Conversation

@ToddHebebrand

Copy link
Copy Markdown
Collaborator

Closes #3244 (epic #3249).

Surfaces the management-posture detection Breeze already collects (agent/internal/mgmtdetect/devices.management_posture jsonb) as a fleet-level migration/decommission report. Surfacing only: no agent changes, no new tables/columns — the jsonb stays the single source of truth.

Spec: docs/superpowers/specs/onboarding-signup/2026-08-08-fleet-migration-posture-report-design.md · Plan: docs/superpowers/plans/open/2026-08-08-fleet-migration-posture-report.md (both on the #3250 docs branch).

API

  • services/managementPostureReport.tstwo aggregate queries, per the plan's critical note:
    • Detections: CROSS JOIN LATERAL jsonb_array_elements(...), count(DISTINCT devices.id) per (org, product, status), plus a fresh-count FILTER on collectedAt.
    • Coverage: per-org denominators with no lateral jointotalDevices, neverScanned (posture IS NULL), stale, scannedNoneDetected (empty array OR absent key), detectedDevices, freshDetectedDevices.
    • Deliberately not one GROUP BY with a LEFT JOIN LATERAL: that collapses never-scanned / category-absent / empty-array into one (NULL, NULL) group, making a never-scanned device read as verified-clean — the exact failure that strands endpoints in Phase 6 of a migration.
    • The summary assembler never emits a detection count without its coverage denominators (throws on the impossible orphan case rather than dropping it).
    • category is validated against the ingest enum (MANAGEMENT_POSTURE_CATEGORIES, now exported from routes/agents/schemas.ts as the single source of truth) and rides a bind param with an explicit ::text cast — never string interpolation.
  • routes/devices/posture.tsGET /devices/management-posture/summary (orgId?, category?=rmm, stalenessDays?=7) and GET /devices/management-posture/devices (product, category?, status?, pagination). Scoping mirrors GET /devices/stats: authMiddleware + requireScope + DEVICES_READ, auth.orgCondition, 403 on inaccessible ?orgId, site-allowlist narrowing (empty allowlist ⇒ empty report), decommissioned + ephemeral devices excluded. Mounted before coreRoutes so /management-posture/* isn't eaten by the /:id matcher.

Web

  • components/devices/FleetPostureReport.tsx + pages/devices/posture.astro + sidebar entry (Reporting → Fleet Posture, gated on devices:read). Org narrowing rides the global org selector (/devices/* is org-or-all; fetchWithAuth injects orgId).
  • Posture age next to every count (fresh of total on every row; stale/never-scanned cards); a bare zero is never rendered — zero detections with unknown/stale devices shows an explicit amber caveat.
  • Orphaned remote-access agents called out as a security finding (separate red callout fed by a remoteAccess summary fetch — ScreenConnect/Splashtop survive their RMM's uninstall).
  • Per-org migration progress: enrolled / running both (mid-migration) / Breeze-only (verified clean) / unknown.
  • CSV export via toCsv + downloadBlob (injection-neutralized), for customer-facing migration evidence.
  • Category selection persists in window.location.hash per the repo's URL-state convention.
  • i18n: fleetPosture.* (devices ns) + nav.fleetPosture added to all 7 locales with real translations; all four i18n guard suites pass.

Tests

  • services/managementPostureReport.test.ts (unit, mocked db.execute): asserts two separate queries (lateral join in detections only, none in coverage), scope propagation, assembly, numeric coercion, orphan-detection throw, category validation.
  • routes/devices/posture.test.ts (unit): defaults, category 400 (incl. SQL-injection-shaped input), inaccessible-org 403, org/site narrowing, empty-allowlist short-circuit, pagination.
  • services/managementPostureReport.integration.test.ts (real Postgres, the load-bearing one): one org seeded with all four populations at once — never-scanned, stale-with-detection, fresh empty-array, fresh absent-key, fresh-detected, duplicate-product, decommissioned, plus a second partner's org. Asserts the partition neverScanned + stale + freshClean + freshDetected == totalDevices, empty-array/absent-key land in scannedNoneDetected (not neverScanned), duplicate product counts once, active/installed/unknown kept separate, fresh <= total, org/partner scoping, drill-down, staleness-window boundary. Registered in vitest.integration.config.ts (and excluded from the unit runner). A single-population fixture passes against the broken one-query form — this fixture is what actually guards the design.
  • components/devices/FleetPostureReport.test.tsx: render, zero-caveat, orphan callout, drill-down fetch, error state.
  • No RLS/cascade/export registration needed: no new table/column; devices is already registered and management_posture is already excludedOpen.

Deviations from the plan (flagged, not silent)

  • Plan SQL used deleted_at IS NULL; devices has no such column — used the repo's actual liveness convention from GET /devices/stats (status != 'decommissioned' AND is_ephemeral = false).
  • Site filter (Task 4): deferred. The spec's §3 endpoint contract has no site parameter and site-restricted users are already auto-narrowed via allowedSiteIds; adding a free-form site dropdown would grow the API surface beyond the spec. Can follow as a small increment if wanted.
  • Task 5 (perf against a 10k seeded fleet): not run — no Postgres (or Docker) on this machine. The query shapes match the spec's cost analysis (per-org rides the org_id index; partner-wide roll-up is an on-demand full scan). Needs a one-off measurement on a seeded stack before/after merge; happy to record numbers on [API][Web] Fleet migration/decommission report — Management Posture already detects 11 competing RMMs and nothing consumes it #3244 from an environment with a DB.
  • Task 6 toolkit Recipe 5 / Known Rough Edges: apps/docs/.../migration/toolkit.mdx only exists on the unmerged docs(migration): RMM-to-Breeze migration guides and toolkit #3250 docs branch, not on main — cannot be edited from this PR. Should be a follow-up commit on docs(migration): RMM-to-Breeze migration guides and toolkit #3250 (or after it merges). features/management-posture.mdx (on main) is updated here with the fleet view + both endpoints.

Verification

  • vitest run (single-fork): service unit 10/10, route unit 10/10, agents security.test.ts + devices stats.test.ts 12/12, web component 5/5, i18n guards + sidebar structural + no-silent-mutations 182/182 — all green.
  • tsc --noEmit: clean for apps/api and apps/web. (astro check cannot run locally — Node 20 vs Astro's >=22.12 — the new 13-line .astro page mirrors compare.astro; CI covers it.)
  • The integration suite runs under the Integration Tests CI job (no local DB available); it is skipped (it.runIf(DATABASE_URL)) elsewhere.

🤖 Generated with Claude Code

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 8, 2026

Copy link
Copy Markdown

Deploying breeze with  Cloudflare Pages  Cloudflare Pages

Latest commit: 7fc9889
Status: ✅  Deploy successful!
Preview URL: https://537c649a.breeze-9te.pages.dev
Branch Preview URL: https://feat-3244-fleet-posture-repo.breeze-9te.pages.dev

View logs

ToddHebebrand and others added 2 commits August 8, 2026 15:21
Two aggregate queries over devices.management_posture (detections via
CROSS JOIN LATERAL; coverage denominators separately, no lateral join)
behind GET /devices/management-posture/summary and /devices, plus the
Fleet Posture web page, CSV export, orphaned remote-access callout,
docs, and the mixed-fixture integration test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Progress chips now PARTITION the fleet from fresh counts (stale-clean
  no longer reads as verified-clean; both/Breeze-only derive from
  freshDetectedDevices).
- Drill-down pins the org whose count it explains (orgId param) so
  All-organizations mode cannot mix orgs under one heading.
- Best-effort remoteAccess fetch: rejection no longer sinks the main
  report; missing findings surface an explicit note.
- Orphaned-RA callout restricted to RMM-bundled survivors
  (ScreenConnect, Splashtop) instead of every RA detection.
- categoryFromHash own-property check (#toString no longer wedges page).
- fetchSummary out-of-order guard; drill-down load-more errors keep
  loaded rows and offer retry.
- Extracted shared buildDeviceScope (posture + stats routes).
- Hoisted web posture category constants to lib/postureCategories.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ToddHebebrand
ToddHebebrand force-pushed the feat/3244-fleet-posture-report branch from e921153 to 7fc9889 Compare August 8, 2026 15:23
@ToddHebebrand

Copy link
Copy Markdown
Collaborator Author

Review run: /code-review 3264 high (multi-finder review; findings converged and were verified against the diff and the plan/spec text).

Findings: 10 raised → 8 fixed in 7fc9889; 2 deliberately deferred:

  • UI partition bug (stale-clean rendered as "verified clean", chips could sum past totalDevices) — fixed: chips now derive from fresh counts and partition the fleet.
  • Drill-down missing orgId (All-orgs mode listed every org's devices under one org's heading) — fixed: the drill-down pins the org whose count it explains.
  • Best-effort remoteAccess fetch was fatal on rejection / silent on failure — fixed: .catch + explicit "findings unavailable" note.
  • Orphan banner flagged every RA detection — narrowed to the plan's RMM-bundled survivors (ScreenConnect, Splashtop); a true per-device orphan check (RA present AND parent RMM absent) needs a server-side cross-category query — noted as follow-up.
  • raw in CATEGORY_LABELS prototype-key wedge, fetch race on rapid filter switches, load-more error wiping loaded rows, scoping logic duplicated with stats.ts (extracted buildDeviceScope, stats.ts refactored onto it), 3rd/4th copy of the category list (hoisted to lib/postureCategories.ts) — all fixed.
  • Deferred (flagged in the PR body): site filter (spec §3 defines no site param; site-restricted users are already auto-narrowed) and the 10k-device perf measurement (no local Postgres — needs a seeded stack; will record numbers on [API][Web] Fleet migration/decommission report — Management Posture already detects 11 competing RMMs and nothing consumes it #3244).

Tests: all single-fork, package-local vitest —

  • API unit: posture.test.ts 10/10, managementPostureReport.test.ts 10/10, stats.test.ts + agents/security.test.ts 12/12 (re-run green after rebase onto latest main).
  • Web: FleetPostureReport.test.tsx 6/6; i18n guards (keyUsage, localeParity, translationCoverage) + Sidebar.nav.test.tsx 81/81.
  • Mixed-fixture integration test (managementPostureReport.integration.test.ts, 6 cases): needs real Postgres — not runnable on this machine; registered in vitest.integration.config.ts, runs in the Integration Tests CI job. This is the suite that guards the two-query split (never-scanned ≠ verified-clean).
  • Typecheck: tsc --noEmit clean for all files in this PR (api + web). astro check cannot run locally (Node 20 vs Astro ≥22.12); the 13-line page mirrors existing pages, CI covers it. Remaining local tsc noise is a pre-existing env artifact (@breeze/extension-sdk unresolvable in the partial node_modules setup), untouched by this PR.

Status: review-clean (0 outstanding findings), rebased onto latest main, awaiting maintainer merge. Watch the Integration Tests job for the mixed-fixture suite's first real-DB run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[API][Web] Fleet migration/decommission report — Management Posture already detects 11 competing RMMs and nothing consumes it

1 participant