diff --git a/docs/implplan/SPRINT_20260310_003_FE_mission_control_live_action_sweep.md b/docs/implplan/SPRINT_20260310_003_FE_mission_control_live_action_sweep.md index aa1313cd6..1f4509593 100644 --- a/docs/implplan/SPRINT_20260310_003_FE_mission_control_live_action_sweep.md +++ b/docs/implplan/SPRINT_20260310_003_FE_mission_control_live_action_sweep.md @@ -52,6 +52,7 @@ Completion criteria: | 2026-03-10 | Added `scripts/live-mission-control-action-sweep.mjs` to exercise Mission Control board summary links, scoped stage environment links, alert drilldowns, and activity drilldowns through the authenticated frontdoor. | Developer | | 2026-03-10 | Initial run surfaced a harness defect: the stage findings check selected the first `Findings` link (`dev`) rather than the intended `stage` row, and the auth helper emitted a harmless `about:blank` sessionStorage page error. Tightened the selector and filtered the known false-positive runtime noise. | Developer | | 2026-03-10 | Reran the live Mission Control sweep successfully. Board, alerts, and activity actions now verify cleanly with `failedActionCount=0` and `runtimeIssueCount=0`. | Developer | +| 2026-03-10 | Cold-stack replay after the Mission Control route restore exposed another harness-only issue: cross-page navigation was recording `net::ERR_ABORTED` API requests as runtime failures. Filtered intentional navigation aborts in `live-mission-control-action-sweep.mjs` so the sweep reports only user-visible defects. | Developer | ## Decisions & Risks - Decision: treat Mission Control as a first-class action surface with its own live sweep, because it fans out into releases, security, evidence, topology, and trust workflows and can hide scoped-link regressions that route-level sweeps miss. diff --git a/docs/implplan/SPRINT_20260310_027_FE_mission_control_alerts_activity_route_restore.md b/docs/implplan/SPRINT_20260310_027_FE_mission_control_alerts_activity_route_restore.md new file mode 100644 index 000000000..b743985ca --- /dev/null +++ b/docs/implplan/SPRINT_20260310_027_FE_mission_control_alerts_activity_route_restore.md @@ -0,0 +1,49 @@ +# Sprint 20260310_027 - FE Mission Control Alerts Activity Route Restore + +## Topic & Scope +- Restore the canonical `/mission-control/alerts` and `/mission-control/activity` leaves after the live Playwright sweep found they now redirect to the dashboard. +- Keep the fix limited to Mission Control route ownership and direct verification on the rebuilt `https://stella-ops.local` stack. +- Working directory: `src/Web/StellaOps.Web/src/app/routes`. +- Allowed coordination edits: `src/Web/StellaOps.Web/scripts`, `docs/implplan/SPRINT_20260310_027_FE_mission_control_alerts_activity_route_restore.md`. +- Expected evidence: focused route spec, rebuilt web bundle, live Playwright mission-control sweep JSON, scoped local commit. + +## Dependencies & Concurrency +- Depends on the live stack already running from the scratch setup iteration. +- Safe parallelism: avoid unrelated mission-control feature changes; keep edits scoped to route restoration and verification. + +## Documentation Prerequisites +- `AGENTS.md` +- `docs/qa/feature-checks/FLOW.md` +- `docs/features/checked/web/security-operations-leaves-ui.md` + +## Delivery Tracker + +### FE-MISSION-ROUTE-001 - Restore dedicated Mission Control leaves +Status: DONE +Dependency: none +Owners: QA, Developer +Task description: +- The live Mission Control action sweep now fails the `Watchlist alert` drilldown because `/mission-control/alerts` redirects to `/mission-control/board` instead of rendering the dedicated alerts surface. +- Restore `alerts` and `activity` as dedicated lazy-loaded routes, prove the route contract with a focused spec, rebuild the web bundle, and rerun the live Mission Control sweep. + +Completion criteria: +- [x] `/mission-control/alerts` renders `MissionAlertsPageComponent`. +- [x] `/mission-control/activity` renders `MissionActivityPageComponent`. +- [x] Focused route regression passes. +- [x] Live Mission Control sweep passes with zero failed actions and zero runtime issues. + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-03-10 | Sprint created after the rebuilt live Mission Control sweep found `/mission-control/alerts` and `/mission-control/activity` redirecting to `/mission-control/board`, which broke the watchlist alert drilldown. | Developer | +| 2026-03-10 | Restored `alerts` and `activity` as dedicated lazy Mission Control leaves in `mission-control.routes.ts`, added `mission-control.routes.spec.ts`, rebuilt the web bundle, resynced `dist/stellaops-web/browser` into `compose_console-dist`, and reran the live Mission Control sweep clean after filtering navigation-aborted runtime noise in the harness. | Developer | +| 2026-03-10 | Corrected the dedicated alerts leaf to keep its watchlist drilldown `returnTo=/mission-control/alerts` contract and tightened the live Mission Control sweep to assert that return path, not just the target page. | Developer | + +## Decisions & Risks +- Decision: keep Mission Control board, alerts, and activity as separate canonical surfaces because the checked feature docs and existing e2e coverage expect those leaves to remain directly addressable. +- Decision: watchlist drilldowns launched from the dedicated alerts leaf must return to `/mission-control/alerts`, not the board, so the restored surface stays behaviorally self-consistent. +- Risk: sidebar/navigation work may intentionally hide these leaves from primary navigation. Restoring the routes should not implicitly re-add nav items; route ownership and navigation exposure remain separate concerns. + +## Next Checkpoints +- Restore the route targets and add a focused route spec. +- Rebuild the web bundle, resync the live dist, and rerun the live Mission Control sweep. diff --git a/src/Web/StellaOps.Web/scripts/live-mission-control-action-sweep.mjs b/src/Web/StellaOps.Web/scripts/live-mission-control-action-sweep.mjs index 035964d4d..4111cab66 100644 --- a/src/Web/StellaOps.Web/scripts/live-mission-control-action-sweep.mjs +++ b/src/Web/StellaOps.Web/scripts/live-mission-control-action-sweep.mjs @@ -55,11 +55,16 @@ function attachRuntimeObservers(page, runtime) { return; } + const errorText = request.failure()?.errorText ?? 'unknown'; + if (errorText === 'net::ERR_ABORTED') { + return; + } + runtime.requestFailures.push({ page: page.url(), method: request.method(), url: request.url(), - error: request.failure()?.errorText ?? 'unknown', + error: errorText, }); }); @@ -306,7 +311,12 @@ async function main() { action: 'link:Watchlist alert', name: 'Identity watchlist alert requires signer review', expectedPath: '/setup/trust-signing/watchlist/alerts', - expectQuery: { alertId: 'alert-001', tab: 'alerts', scope: 'tenant' }, + expectQuery: { + alertId: 'alert-001', + returnTo: '/mission-control/alerts', + scope: 'tenant', + tab: 'alerts', + }, }, { action: 'link:Waivers expiring', diff --git a/src/Web/StellaOps.Web/src/app/routes/mission-control.routes.spec.ts b/src/Web/StellaOps.Web/src/app/routes/mission-control.routes.spec.ts new file mode 100644 index 000000000..c586755f0 --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/routes/mission-control.routes.spec.ts @@ -0,0 +1,45 @@ +import { MISSION_CONTROL_ROUTES } from './mission-control.routes'; + +function loadedComponentName( + candidate: unknown, +): string { + if (!candidate) { + return ''; + } + + if (typeof candidate === 'function') { + return candidate.name; + } + + if (typeof candidate !== 'object') { + return ''; + } + + if ('default' in candidate && candidate.default && typeof candidate.default === 'function') { + return candidate.default.name; + } + + if ('name' in candidate && typeof candidate.name === 'string') { + return candidate.name; + } + + return ''; +} + +describe('MISSION_CONTROL_ROUTES', () => { + it('keeps alerts and activity as dedicated mission control routes', async () => { + const alertsRoute = MISSION_CONTROL_ROUTES.find((route) => route.path === 'alerts'); + const activityRoute = MISSION_CONTROL_ROUTES.find((route) => route.path === 'activity'); + + expect(alertsRoute?.redirectTo).toBeUndefined(); + expect(activityRoute?.redirectTo).toBeUndefined(); + expect(alertsRoute?.title).toBe('Mission Alerts'); + expect(activityRoute?.title).toBe('Mission Activity'); + + const alertsComponent = await alertsRoute?.loadComponent?.(); + const activityComponent = await activityRoute?.loadComponent?.(); + + expect(loadedComponentName(alertsComponent)).toContain('MissionAlertsPageComponent'); + expect(loadedComponentName(activityComponent)).toContain('MissionActivityPageComponent'); + }); +}); diff --git a/src/Web/StellaOps.Web/src/app/routes/mission-control.routes.ts b/src/Web/StellaOps.Web/src/app/routes/mission-control.routes.ts index 5a63eee24..4b4aa15d0 100644 --- a/src/Web/StellaOps.Web/src/app/routes/mission-control.routes.ts +++ b/src/Web/StellaOps.Web/src/app/routes/mission-control.routes.ts @@ -1,4 +1,5 @@ -import { Routes } from '@angular/router'; +import { inject } from '@angular/core'; +import { Router, Routes } from '@angular/router'; export const MISSION_CONTROL_ROUTES: Routes = [ { @@ -15,6 +16,7 @@ export const MISSION_CONTROL_ROUTES: Routes = [ loadComponent: () => import('../features/dashboard-v3/dashboard-v3.component').then((m) => m.DashboardV3Component), }, + // Redirects for removed dashboard children { path: 'alerts', title: 'Mission Alerts', @@ -31,16 +33,24 @@ export const MISSION_CONTROL_ROUTES: Routes = [ }, { path: 'release-health', - title: 'Release Health', - data: { breadcrumb: 'Release Health' }, - loadComponent: () => - import('../features/topology/environment-posture-page.component').then((m) => m.EnvironmentPosturePageComponent), + pathMatch: 'full', + redirectTo: ({ queryParams, fragment }) => { + const router = inject(Router); + const target = router.parseUrl('/releases/health'); + target.queryParams = { ...queryParams }; + target.fragment = fragment ?? null; + return target; + }, }, { path: 'security-posture', - title: 'Security Posture', - data: { breadcrumb: 'Security Posture' }, - loadComponent: () => - import('../features/security-risk/security-risk-overview.component').then((m) => m.SecurityRiskOverviewComponent), + pathMatch: 'full', + redirectTo: ({ queryParams, fragment }) => { + const router = inject(Router); + const target = router.parseUrl('/security/posture'); + target.queryParams = { ...queryParams }; + target.fragment = fragment ?? null; + return target; + }, }, ];