From a3f532359bd1c5f0c860818d5b6d2ac8b98c7e26 Mon Sep 17 00:00:00 2001
From: master <>
Date: Sat, 7 Mar 2026 20:31:32 +0200
Subject: [PATCH] feat(ui): ship consolidated operations shell
---
...60307_026_FE_platform_ops_consolidation.md | 51 +-
.../web/operations-consolidation-ui.md | 62 ++
docs/modules/ui/README.md | 4 +
docs/modules/ui/TASKS.md | 13 +-
docs/modules/ui/implementation_plan.md | 2 +-
.../ui/platform-ops-consolidation/README.md | 19 +
src/Web/StellaOps.Web/src/app/app.routes.ts | 19 +-
.../data-integrity-overview.component.ts | 30 +-
.../dlq-replays-page.component.ts | 8 +-
.../feeds-freshness-page.component.ts | 6 +-
...integration-connectivity-page.component.ts | 2 +-
.../job-run-detail-page.component.ts | 4 +-
.../nightly-ops-report-page.component.ts | 8 +-
...achability-ingest-health-page.component.ts | 4 +-
.../scan-pipeline-health-page.component.ts | 5 +-
.../features/platform/ops/operations-paths.ts | 43 ++
.../platform-feeds-airgap-page.component.ts | 8 +-
.../platform-jobs-queues-page.component.ts | 24 +-
.../platform-ops-overview-page.component.html | 153 +++++
.../platform-ops-overview-page.component.scss | 258 ++++++++
.../platform-ops-overview-page.component.ts | 604 +++++++++---------
.../src/app/routes/legacy-redirects.routes.ts | 63 +-
.../src/app/routes/operations.routes.ts | 20 +
.../src/app/routes/platform-ops.routes.ts | 103 ++-
.../tests/navigation/legacy-redirects.spec.ts | 44 +-
.../platform-ops/data-integrity-pages.spec.ts | 16 +-
...atform-ops-overview-page.component.spec.ts | 67 ++
.../platform-ops/platform-ops-routes.spec.ts | 105 +--
.../e2e/operations-consolidation.spec.ts | 156 +++++
29 files changed, 1447 insertions(+), 454 deletions(-)
rename {docs => docs-archived}/implplan/SPRINT_20260307_026_FE_platform_ops_consolidation.md (76%)
create mode 100644 docs/features/checked/web/operations-consolidation-ui.md
create mode 100644 src/Web/StellaOps.Web/src/app/features/platform/ops/operations-paths.ts
create mode 100644 src/Web/StellaOps.Web/src/app/features/platform/ops/platform-ops-overview-page.component.html
create mode 100644 src/Web/StellaOps.Web/src/app/features/platform/ops/platform-ops-overview-page.component.scss
create mode 100644 src/Web/StellaOps.Web/src/tests/platform-ops/platform-ops-overview-page.component.spec.ts
create mode 100644 src/Web/StellaOps.Web/tests/e2e/operations-consolidation.spec.ts
diff --git a/docs/implplan/SPRINT_20260307_026_FE_platform_ops_consolidation.md b/docs-archived/implplan/SPRINT_20260307_026_FE_platform_ops_consolidation.md
similarity index 76%
rename from docs/implplan/SPRINT_20260307_026_FE_platform_ops_consolidation.md
rename to docs-archived/implplan/SPRINT_20260307_026_FE_platform_ops_consolidation.md
index 99deb8379..199414889 100644
--- a/docs/implplan/SPRINT_20260307_026_FE_platform_ops_consolidation.md
+++ b/docs-archived/implplan/SPRINT_20260307_026_FE_platform_ops_consolidation.md
@@ -30,7 +30,7 @@
## Delivery Tracker
### FE-PO-001 - Rebuild the Operations overview and submenu in the live shell
-Status: TODO
+Status: DONE
Dependency: none
Owners: Product Manager, FE Architect
Task description:
@@ -38,12 +38,12 @@ Task description:
- Ensure the overview and submenu expose the real operator concerns rather than leaving them as a paper design.
Completion criteria:
-- [ ] The live shell exposes the intended overview groups and submenu entries.
-- [ ] Overview group labels and child-route labels are wired in code.
-- [ ] No parallel `platform-ops` owner tree is required for operator access.
+- [x] The live shell exposes the intended overview groups and submenu entries.
+- [x] Overview group labels and child-route labels are wired in code.
+- [x] No parallel `platform-ops` owner tree is required for operator access.
### FE-PO-002 - Ship the overview page and grouped blocking cards
-Status: TODO
+Status: DONE
Dependency: FE-PO-001
Owners: Developer, FE Architect
Task description:
@@ -51,12 +51,12 @@ Task description:
- Make the overview page usable as the real operator landing page rather than a placeholder around child routes.
Completion criteria:
-- [ ] Blocking strip renders the required signals in the live shell.
-- [ ] Grouped cards route into working child pages.
-- [ ] Overview sections follow one consistent shipped pattern.
+- [x] Blocking strip renders the required signals in the live shell.
+- [x] Grouped cards route into working child pages.
+- [x] Overview sections follow one consistent shipped pattern.
### FE-PO-003 - Absorb high-value legacy widgets and pages
-Status: TODO
+Status: DONE
Dependency: FE-PO-001
Owners: Developer, Documentation author
Task description:
@@ -64,12 +64,12 @@ Task description:
- Retire only the obsolete duplicates after the missing operational functionality is actually present in the live shell.
Completion criteria:
-- [ ] High-value legacy widgets are visible in the active shell or child routes.
-- [ ] Missing operational views are actually shipped into target pages.
-- [ ] Only genuinely duplicated legacy routes remain candidates for retirement.
+- [x] High-value legacy widgets are visible in the active shell or child routes.
+- [x] Missing operational views are actually shipped into target pages.
+- [x] Only genuinely duplicated legacy routes remain candidates for retirement.
### FE-PO-004 - Cut over routes and legacy aliases
-Status: TODO
+Status: DONE
Dependency: FE-PO-001
Owners: FE Architect, Developer
Task description:
@@ -77,12 +77,12 @@ Task description:
- Ensure old bookmarks and legacy route trees redirect into working pages without reviving a second shell.
Completion criteria:
-- [ ] Canonical route family is active in the router.
-- [ ] Legacy aliases redirect into working operations pages.
-- [ ] Sidebar, overview cards, and breadcrumb labels match in the shipped shell.
+- [x] Canonical route family is active in the router.
+- [x] Legacy aliases redirect into working operations pages.
+- [x] Sidebar, overview cards, and breadcrumb labels match in the shipped shell.
### FE-PO-005 - Complete Setup boundary and topology deep links
-Status: TODO
+Status: DONE
Dependency: FE-PO-002
Owners: Product Manager, FE Architect
Task description:
@@ -90,12 +90,12 @@ Task description:
- Wire deep links between `Ops > Operations` and `Setup > Topology` where operators need to cross that boundary.
Completion criteria:
-- [ ] Agent fleet and topology ownership remain under Setup in the live UI.
-- [ ] Operations-to-Setup deep links work from shipped pages.
-- [ ] No duplicated topology management remains exposed under Ops.
+- [x] Agent fleet and topology ownership remain under Setup in the live UI.
+- [x] Operations-to-Setup deep links work from shipped pages.
+- [x] No duplicated topology management remains exposed under Ops.
### FE-PO-006 - Verify, document, and cut over the consolidated shell
-Status: TODO
+Status: DONE
Dependency: FE-PO-004
Owners: QA, Documentation author
Task description:
@@ -103,14 +103,17 @@ Task description:
- Update UI docs and operational runbooks so the consolidated shell ships as the usable owner surface.
Completion criteria:
-- [ ] Verification covers overview groups and target routes.
-- [ ] Legacy alias redirects are included in testing.
-- [ ] Docs reflect the consolidated and shipped owner shell.
+- [x] Verification covers overview groups and target routes.
+- [x] Legacy alias redirects are included in testing.
+- [x] Docs reflect the consolidated and shipped owner shell.
## Execution Log
| Date (UTC) | Update | Owner |
| --- | --- | --- |
| 2026-03-07 | Sprint created to ship a consolidated Operations shell with grouped overview cards, stable child routes, legacy-widget absorption, and explicit Setup boundaries. | Project Manager |
+| 2026-03-07 | Implementation started. Current `Ops > Operations` routes exist, but the overview and several child pages still link to dead `/platform/ops/*` paths. Consolidation work will replace those with canonical `/ops/operations/*` routes and add legacy redirect coverage. | Developer |
+| 2026-03-07 | Shipped the consolidated overview taxonomy, canonical `/ops/operations/*` links, legacy alias redirects, and Setup-boundary deep links. | Developer |
+| 2026-03-07 | Verified with targeted Angular and Playwright coverage, updated module docs, and archived the sprint. | QA |
## Decisions & Risks
- Decision: `Ops > Operations` remains the one operator shell; legacy `platform-ops` becomes migration input only.
diff --git a/docs/features/checked/web/operations-consolidation-ui.md b/docs/features/checked/web/operations-consolidation-ui.md
new file mode 100644
index 000000000..23466a704
--- /dev/null
+++ b/docs/features/checked/web/operations-consolidation-ui.md
@@ -0,0 +1,62 @@
+# Operations Consolidation UI
+
+## Module
+Web
+
+## Status
+VERIFIED
+
+## Description
+Shipped the canonical `Ops > Operations` owner shell with grouped overview cards, blocking-strip signals, canonical `/ops/operations/*` child routes, absorbed legacy data-integrity and queue links, and explicit deep links into `Setup > Topology` for agent-fleet ownership. Legacy `platform-ops/*` and `platform/ops/*` aliases now resolve into the consolidated shell instead of requiring a parallel product tree.
+
+## Implementation Details
+- **Feature directory**: `src/Web/StellaOps.Web/src/app/features/platform/ops/`
+- **Primary components**:
+ - `platform-ops-overview-page` (`src/Web/StellaOps.Web/src/app/features/platform/ops/platform-ops-overview-page.component.ts`)
+ - `platform-feeds-airgap-page` (`src/Web/StellaOps.Web/src/app/features/platform/ops/platform-feeds-airgap-page.component.ts`)
+ - `platform-jobs-queues-page` (`src/Web/StellaOps.Web/src/app/features/platform/ops/platform-jobs-queues-page.component.ts`)
+- **Canonical routes**:
+ - `/ops/operations`
+ - `/ops/operations/data-integrity`
+ - `/ops/operations/jobs-queues`
+ - `/ops/operations/health-slo`
+ - `/ops/operations/feeds-airgap`
+ - `/ops/operations/offline-kit`
+ - `/ops/operations/quotas`
+ - `/ops/operations/aoc`
+ - `/ops/operations/doctor`
+ - `/ops/operations/signals`
+ - `/ops/operations/packs`
+ - `/ops/operations/notifications`
+- **Legacy aliases**:
+ - `/platform-ops/*`
+ - `/platform/ops/*`
+- **Secondary entry points**:
+ - `Mission Control`
+ - `Setup > Topology`
+ - `Evidence`
+ - `Releases`
+
+## E2E Test Plan
+- **Setup**:
+ - [ ] Log in with a user that can access `Ops` and `Setup`.
+ - [ ] Navigate to `/ops/operations`.
+ - [ ] Ensure doctor trend and approvals fixtures or seeded data exist.
+- **Core verification**:
+ - [ ] Verify `Blocking`, `Execution`, `Health`, `Supply And Airgap`, and `Capacity And Setup Boundary` groups render.
+ - [ ] Verify overview cards drill into the intended canonical child routes.
+ - [ ] Verify Setup-boundary links send agent and topology work to `Setup > Topology`.
+- **Legacy verification**:
+ - [ ] Verify `platform-ops/*` and `platform/ops/*` aliases land in canonical `/ops/operations/*` routes.
+ - [ ] Verify query-string state survives redirect into tabbed child pages.
+ - [ ] Verify data-integrity drill-ins and job detail routes keep working after alias cutover.
+
+## Verification
+- Run:
+ - `npx ng test --watch=false --include src/tests/platform-ops/platform-ops-routes.spec.ts --include src/tests/platform-ops/platform-ops-overview-page.component.spec.ts --include src/tests/platform-ops/platform-feeds-airgap-page.component.spec.ts --include src/tests/platform-ops/data-integrity-pages.spec.ts --include src/tests/navigation/legacy-redirects.spec.ts`
+ - `PLAYWRIGHT_PORT=4410 npx playwright test tests/e2e/operations-consolidation.spec.ts --workers=1`
+- Tier 0 (source): pass
+- Tier 1 (build/tests): pass
+- Tier 2 (behavior): pass
+- Note: under the local Angular dev server, `/platform*` first-request URLs are intercepted by `proxy.conf.json`, so the Playwright legacy-alias check uses client-side navigation after app bootstrap to validate Angular redirect behavior deterministically.
+- Verified on (UTC): 2026-03-07T18:28:16Z
diff --git a/docs/modules/ui/README.md b/docs/modules/ui/README.md
index 802e24509..b10ae9331 100644
--- a/docs/modules/ui/README.md
+++ b/docs/modules/ui/README.md
@@ -16,8 +16,12 @@ The Console presents operator dashboards for scans, policies, VEX evidence, runt
- Added restoration topic shape notes at `restoration-topics/README.md` for Watchlist, Reachability Witnessing, Platform Ops, Triage explainability, and Workflow Visualization placement.
- Added implementation-ready UX dossiers for Watchlist, Reachability Witnessing, Platform Ops Consolidation, Triage Explainability Workspace, Workflow Visualization and Replay, and shared contextual action patterns.
- Added FE sprint files for the five accepted restoration topics plus a shared sprint for single actions, drawers, tabs, and stray-page placement patterns.
+- Shipped the canonical `Setup > Trust & Signing` watchlist shell, including entries, alerts, tuning, and Mission Control or Notifications deep links.
+- Added checked-feature verification for watchlist management at `../../features/checked/web/identity-watchlist-management-ui.md`.
- Shipped the canonical `Security > Reachability` witness and proof-of-exposure shell, including cross-shell handoffs from findings, triage, evidence replay, and release detail.
- Added checked-feature verification for reachability witnessing at `../../features/checked/web/reachability-witnessing-ui.md`.
+- Shipped the consolidated `Ops > Operations` shell with grouped overview cards, canonical `/ops/operations/*` routes, and legacy `platform-ops` alias cutover.
+- Added checked-feature verification for operations consolidation at `../../features/checked/web/operations-consolidation-ui.md`.
## Latest updates (2026-02-21)
- Runtime mock cutover completed for policy simulation history/conflict/batch flows and graph explorer data loading in `src/Web/StellaOps.Web/src/app/**`.
diff --git a/docs/modules/ui/TASKS.md b/docs/modules/ui/TASKS.md
index 5d8a16475..9e9372dde 100644
--- a/docs/modules/ui/TASKS.md
+++ b/docs/modules/ui/TASKS.md
@@ -9,7 +9,6 @@
- `docs/implplan/SPRINT_20260307_009_DOCS_ui_component_preservation_map.md`
- `docs/implplan/SPRINT_20260307_022_FE_policy_vex_release_decisioning_studio.md`
- `docs/implplan/SPRINT_20260307_023_DOCS_ui_restoration_topic_shapes.md`
-- `docs/implplan/SPRINT_20260307_026_FE_platform_ops_consolidation.md`
- `docs/implplan/SPRINT_20260307_027_FE_triage_explainability_workspace.md`
- `docs/implplan/SPRINT_20260307_028_FE_workflow_visualization_replay.md`
- `docs/implplan/SPRINT_20260307_029_FE_contextual_actions_and_stray_surfaces.md`
@@ -82,12 +81,12 @@
- [DONE] FE-RW-004 Cross-product deep links and release-context use for reachability proofs
- [DONE] FE-RW-005 Supporting evidence and export surfaces for witness UX
- [DONE] FE-RW-006 QA, rollout, and docs sync for reachability witnessing
-- [TODO] FE-PO-001 Freeze Operations overview taxonomy and submenu structure
-- [TODO] FE-PO-002 Overview page regrouping and blocking-card contract
-- [TODO] FE-PO-003 Legacy widget absorption matrix for Platform Ops
-- [TODO] FE-PO-004 Route cleanup and alias migration contract for Operations
-- [TODO] FE-PO-005 Setup boundary and deep-link contract for Operations
-- [TODO] FE-PO-006 QA, rollout, and docs sync for Platform Ops consolidation
+- [DONE] FE-PO-001 Freeze Operations overview taxonomy and submenu structure
+- [DONE] FE-PO-002 Overview page regrouping and blocking-card contract
+- [DONE] FE-PO-003 Legacy widget absorption matrix for Platform Ops
+- [DONE] FE-PO-004 Route cleanup and alias migration contract for Operations
+- [DONE] FE-PO-005 Setup boundary and deep-link contract for Operations
+- [DONE] FE-PO-006 QA, rollout, and docs sync for Platform Ops consolidation
- [TODO] FE-TX-001 Freeze artifact workspace route, lane, and panel contract
- [TODO] FE-TX-002 List-lane segmentation slice for Artifact Workspace
- [TODO] FE-TX-003 Detail-side explainability rail slice
diff --git a/docs/modules/ui/implementation_plan.md b/docs/modules/ui/implementation_plan.md
index 4a81bb205..ce7ec9733 100644
--- a/docs/modules/ui/implementation_plan.md
+++ b/docs/modules/ui/implementation_plan.md
@@ -13,7 +13,6 @@ Provide a living plan for UI deliverables, dependencies, and evidence.
- `SPRINT_20260307_009_DOCS_ui_component_preservation_map.md` - per-component preservation dossiers for unused and weakly surfaced console UI components.
- `SPRINT_20260307_022_FE_policy_vex_release_decisioning_studio.md` - canonical Decisioning Studio shell to unify policy, simulation, VEX decisioning, and release-context gate explanation.
- `SPRINT_20260307_023_DOCS_ui_restoration_topic_shapes.md` - documentation prerequisite for shell/menu/tab placements; not a product-delivery sprint by itself.
-- `SPRINT_20260307_026_FE_platform_ops_consolidation.md` - ship one Operations shell with grouped overview cards, legacy widget absorption, and legacy redirects.
- `SPRINT_20260307_027_FE_triage_explainability_workspace.md` - ship the artifact workspace lane model, explainability panels, and audit-bundle flows.
- `SPRINT_20260307_028_FE_workflow_visualization_replay.md` - ship run-detail graph, timeline, replay, and evidence tabs plus bounded workflow-editor preview reuse.
- `SPRINT_20260307_029_FE_contextual_actions_and_stray_surfaces.md` - ship the shared tabs, drawers, right rails, split views, and contextual detail primitives adopted by the restoration features.
@@ -27,6 +26,7 @@ Provide a living plan for UI deliverables, dependencies, and evidence.
- `docs/modules/ui/watchlist-operations/README.md` - detailed watchlist UX dossier and owner-shell contract.
- `docs/features/checked/web/reachability-witnessing-ui.md` - shipped verification note for the canonical Reachability witness and PoE shell.
- `docs/features/checked/web/identity-watchlist-management-ui.md` - shipped verification note for the Trust & Signing watchlist shell and its Mission Control / Notifications handoffs.
+- `docs/features/checked/web/operations-consolidation-ui.md` - shipped verification note for the canonical Operations shell, overview grouping, and legacy alias cutover.
- `docs/modules/ui/reachability-witnessing/README.md` - detailed witness and proof UX dossier plus cross-shell deep-link contract.
- `docs/modules/ui/platform-ops-consolidation/README.md` - detailed Operations overview taxonomy and legacy absorption plan.
- `docs/modules/ui/triage-explainability-workspace/README.md` - detailed artifact workspace and audit-bundle UX dossier.
diff --git a/docs/modules/ui/platform-ops-consolidation/README.md b/docs/modules/ui/platform-ops-consolidation/README.md
index 56b2531d7..10a93735c 100644
--- a/docs/modules/ui/platform-ops-consolidation/README.md
+++ b/docs/modules/ui/platform-ops-consolidation/README.md
@@ -1,5 +1,7 @@
# Platform Ops Consolidation
+**Status:** Shipped on 2026-03-07
+
## Recommendation
Keep one consolidated operator shell under `Ops > Operations` and absorb the useful legacy `platform-ops` surfaces into it.
@@ -10,6 +12,14 @@ Keep one consolidated operator shell under `Ops > Operations` and absorb the use
This is not a restoration of an abandoned product. It is a consolidation of operator navigation, overview grouping, and missing widgets into the existing route tree.
+## Shipped Scope
+
+- Mounted one grouped `Ops > Operations` overview with blocking-strip status, quick submenu chips, pending-action callouts, and an explicit Setup ownership boundary.
+- Standardized canonical route helpers under `/ops/operations/*` and rewired active overview and child-page links away from dead `/platform/ops/*` paths.
+- Added legacy alias redirects for both `/platform-ops/*` and `/platform/ops/*` into the live Operations shell.
+- Preserved agent-fleet and topology ownership under `Setup > Topology` while adding direct Operations handoffs where runtime monitoring needs to cross that boundary.
+- Added focused route, component, and Playwright verification plus a checked-feature note at `docs/features/checked/web/operations-consolidation-ui.md`.
+
## Why This Is The Right Shape
- The current app already routes operators through `/ops/operations`.
@@ -115,6 +125,15 @@ Prefer the current route family and tighten it rather than creating new paths.
- duplicate overview routes should be retired once card parity is reached
- child-route labels should match the sidebar and overview cards exactly
+## Verification
+
+- Angular:
+ - `npx ng test --watch=false --include src/tests/platform-ops/platform-ops-routes.spec.ts --include src/tests/platform-ops/platform-ops-overview-page.component.spec.ts --include src/tests/platform-ops/platform-feeds-airgap-page.component.spec.ts --include src/tests/platform-ops/data-integrity-pages.spec.ts --include src/tests/navigation/legacy-redirects.spec.ts`
+- Playwright:
+ - `PLAYWRIGHT_PORT=4410 npx playwright test tests/e2e/operations-consolidation.spec.ts --workers=1`
+- Checked feature record:
+ - `docs/features/checked/web/operations-consolidation-ui.md`
+
## What To Merge
### Preserve as the main shell
diff --git a/src/Web/StellaOps.Web/src/app/app.routes.ts b/src/Web/StellaOps.Web/src/app/app.routes.ts
index 5299b99bf..4fc5e6e4f 100644
--- a/src/Web/StellaOps.Web/src/app/app.routes.ts
+++ b/src/Web/StellaOps.Web/src/app/app.routes.ts
@@ -7,6 +7,7 @@ import {
} from './core/auth';
import { requireBackendsReachableGuard } from './core/config/backends-reachable.guard';
import { requireConfigGuard } from './core/config/config.guard';
+import { LEGACY_REDIRECT_ROUTES } from './routes/legacy-redirects.routes';
const requireMissionControlGuard = requireAnyScopeGuard(
[
@@ -83,6 +84,7 @@ export const routes: Routes = [
pathMatch: 'full',
redirectTo: 'mission-control/board',
},
+ ...LEGACY_REDIRECT_ROUTES,
{
path: 'mission-control',
title: 'Mission Control',
@@ -104,6 +106,13 @@ export const routes: Routes = [
data: { breadcrumb: 'Security' },
loadChildren: () => import('./routes/security-risk.routes').then((m) => m.SECURITY_RISK_ROUTES),
},
+ {
+ path: 'triage',
+ title: 'Triage',
+ canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard],
+ data: { breadcrumb: 'Triage' },
+ loadChildren: () => import('./routes/triage.routes').then((m) => m.TRIAGE_ROUTES),
+ },
{
path: 'evidence',
title: 'Evidence',
@@ -161,11 +170,17 @@ export const routes: Routes = [
data: { breadcrumb: 'Console Admin' },
loadChildren: () => import('./features/console-admin/console-admin.routes').then((m) => m.consoleAdminRoutes),
},
+ {
+ path: 'platform-ops',
+ loadChildren: () => import('./routes/platform-ops.routes').then((m) => m.PLATFORM_OPS_ROUTES),
+ },
{
path: 'platform',
children: [
- { path: 'ops', redirectTo: '/ops', pathMatch: 'full' },
- { path: 'ops/:rest', redirectTo: '/ops/:rest' },
+ {
+ path: 'ops',
+ loadChildren: () => import('./routes/platform-ops.routes').then((m) => m.PLATFORM_OPS_ROUTES),
+ },
{ path: 'setup', redirectTo: '/setup', pathMatch: 'full' },
{ path: 'setup/:rest', redirectTo: '/setup/:rest' },
{ path: '**', redirectTo: '/ops' },
diff --git a/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity-overview.component.ts b/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity-overview.component.ts
index b6acb819d..07991033a 100644
--- a/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity-overview.component.ts
+++ b/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity-overview.component.ts
@@ -123,13 +123,13 @@ interface FailureItem {
@@ -325,7 +325,7 @@ export class DataIntegrityOverviewComponent {
state: 'warn',
impact: 'BLOCKING',
detail: 'NVD feed stale by 3h 12m',
- route: '/platform/ops/data-integrity/feeds-freshness',
+ route: '/ops/operations/data-integrity/feeds-freshness',
},
{
id: 'scan',
@@ -333,7 +333,7 @@ export class DataIntegrityOverviewComponent {
state: 'ok',
impact: 'INFO',
detail: 'Nightly rescan completed',
- route: '/platform/ops/data-integrity/scan-pipeline',
+ route: '/ops/operations/data-integrity/scan-pipeline',
},
{
id: 'reachability',
@@ -341,7 +341,7 @@ export class DataIntegrityOverviewComponent {
state: 'warn',
impact: 'DEGRADED',
detail: 'Runtime backlog elevated',
- route: '/platform/ops/data-integrity/reachability-ingest',
+ route: '/ops/operations/data-integrity/reachability-ingest',
},
{
id: 'integrations',
@@ -349,7 +349,7 @@ export class DataIntegrityOverviewComponent {
state: 'ok',
impact: 'INFO',
detail: 'Core connectors are reachable',
- route: '/platform/ops/data-integrity/integration-connectivity',
+ route: '/ops/operations/data-integrity/integration-connectivity',
},
{
id: 'dlq',
@@ -357,7 +357,7 @@ export class DataIntegrityOverviewComponent {
state: 'warn',
impact: 'DEGRADED',
detail: '3 items pending replay',
- route: '/platform/ops/data-integrity/dlq',
+ route: '/ops/operations/data-integrity/dlq',
},
];
@@ -379,19 +379,19 @@ export class DataIntegrityOverviewComponent {
id: 'failure-nvd',
title: 'NVD sync lag',
detail: 'Feed lag exceeds SLA for release-critical path.',
- route: '/platform/ops/data-integrity/feeds-freshness',
+ route: '/ops/operations/data-integrity/feeds-freshness',
},
{
id: 'failure-runtime',
title: 'Runtime ingest backlog',
detail: 'Runtime source queue depth is increasing.',
- route: '/platform/ops/data-integrity/reachability-ingest',
+ route: '/ops/operations/data-integrity/reachability-ingest',
},
{
id: 'failure-dlq',
title: 'DLQ replay queue',
detail: 'Pending replay items block confidence for approvals.',
- route: '/platform/ops/data-integrity/dlq',
+ route: '/ops/operations/data-integrity/dlq',
},
];
diff --git a/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/dlq-replays-page.component.ts b/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/dlq-replays-page.component.ts
index 407b406eb..5a59dbef9 100644
--- a/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/dlq-replays-page.component.ts
+++ b/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/dlq-replays-page.component.ts
@@ -60,9 +60,9 @@ interface DlqItem {
{{ item.payload }}
{{ item.age }}
- Replay
- View
- Link job
+ Replay
+ View
+ Link job
} @empty {
@@ -76,7 +76,7 @@ interface DlqItem {
`,
diff --git a/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/feeds-freshness-page.component.ts b/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/feeds-freshness-page.component.ts
index 69182ec01..19779ae0b 100644
--- a/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/feeds-freshness-page.component.ts
+++ b/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/feeds-freshness-page.component.ts
@@ -48,11 +48,11 @@ interface FeedRow {
diff --git a/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/integration-connectivity-page.component.ts b/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/integration-connectivity-page.component.ts
index c2ac2c113..e6e043e26 100644
--- a/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/integration-connectivity-page.component.ts
+++ b/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/integration-connectivity-page.component.ts
@@ -43,7 +43,7 @@ interface ConnectorRow {
Open Detail
Test
- View dependent jobs
+ View dependent jobs
View impacted approvals
diff --git a/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/job-run-detail-page.component.ts b/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/job-run-detail-page.component.ts
index 6d4c84092..d4f6d1577 100644
--- a/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/job-run-detail-page.component.ts
+++ b/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/job-run-detail-page.component.ts
@@ -46,8 +46,8 @@ interface AffectedItem {
`,
diff --git a/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/nightly-ops-report-page.component.ts b/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/nightly-ops-report-page.component.ts
index d1176ba2e..1f5279e2b 100644
--- a/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/nightly-ops-report-page.component.ts
+++ b/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/nightly-ops-report-page.component.ts
@@ -67,11 +67,11 @@ interface NightlyJobRow {
{{ row.impact }}
- View Run
- Open Scheduler
- Open JobEngine
+ View Run
+ Open Scheduler
+ Open JobEngine
Open Integration
- Open DLQ
+ Open DLQ
}
diff --git a/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/reachability-ingest-health-page.component.ts b/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/reachability-ingest-health-page.component.ts
index bfe3de082..2d37c5b48 100644
--- a/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/reachability-ingest-health-page.component.ts
+++ b/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/reachability-ingest-health-page.component.ts
@@ -53,8 +53,8 @@ interface IngestRow {
diff --git a/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/scan-pipeline-health-page.component.ts b/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/scan-pipeline-health-page.component.ts
index 5d1a62bd8..6cf4a9653 100644
--- a/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/scan-pipeline-health-page.component.ts
+++ b/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/scan-pipeline-health-page.component.ts
@@ -42,8 +42,8 @@ interface Stage {
@@ -173,4 +173,3 @@ export class ScanPipelineHealthPageComponent {
readonly affectedEnvironments = 3;
readonly blockedApprovals = 2;
}
-
diff --git a/src/Web/StellaOps.Web/src/app/features/platform/ops/operations-paths.ts b/src/Web/StellaOps.Web/src/app/features/platform/ops/operations-paths.ts
new file mode 100644
index 000000000..414f32a51
--- /dev/null
+++ b/src/Web/StellaOps.Web/src/app/features/platform/ops/operations-paths.ts
@@ -0,0 +1,43 @@
+export const OPERATIONS_ROOT = '/ops/operations';
+
+export const OPERATIONS_PATHS = {
+ overview: OPERATIONS_ROOT,
+ dataIntegrity: `${OPERATIONS_ROOT}/data-integrity`,
+ jobsQueues: `${OPERATIONS_ROOT}/jobs-queues`,
+ healthSlo: `${OPERATIONS_ROOT}/health-slo`,
+ feedsAirgap: `${OPERATIONS_ROOT}/feeds-airgap`,
+ offlineKit: `${OPERATIONS_ROOT}/offline-kit`,
+ quotas: `${OPERATIONS_ROOT}/quotas`,
+ aoc: `${OPERATIONS_ROOT}/aoc`,
+ doctor: `${OPERATIONS_ROOT}/doctor`,
+ signals: `${OPERATIONS_ROOT}/signals`,
+ packs: `${OPERATIONS_ROOT}/packs`,
+ notifications: `${OPERATIONS_ROOT}/notifications`,
+ status: `${OPERATIONS_ROOT}/status`,
+ scheduler: `${OPERATIONS_ROOT}/scheduler`,
+ schedulerRuns: `${OPERATIONS_ROOT}/scheduler/runs`,
+ schedulerSchedules: `${OPERATIONS_ROOT}/scheduler/schedules`,
+ schedulerWorkers: `${OPERATIONS_ROOT}/scheduler/workers`,
+ deadLetter: `${OPERATIONS_ROOT}/dead-letter`,
+ jobEngine: `${OPERATIONS_ROOT}/jobengine`,
+ jobEngineJobs: `${OPERATIONS_ROOT}/jobengine/jobs`,
+ jobEngineQuotas: `${OPERATIONS_ROOT}/jobengine/quotas`,
+} as const;
+
+export const OPERATIONS_SETUP_PATHS = {
+ topologyOverview: '/setup/topology/overview',
+ topologyAgents: '/setup/topology/agents',
+ feedPolicy: '/ops/platform-setup/feed-policy',
+} as const;
+
+export const OPERATIONS_INTEGRATION_PATHS = {
+ advisorySources: '/ops/integrations/advisory-vex-sources',
+} as const;
+
+export function dataIntegrityPath(section?: string): string {
+ return section ? `${OPERATIONS_PATHS.dataIntegrity}/${section}` : OPERATIONS_PATHS.dataIntegrity;
+}
+
+export function jobEngineJobPath(jobId?: string): string {
+ return jobId ? `${OPERATIONS_PATHS.jobEngineJobs}/${jobId}` : OPERATIONS_PATHS.jobEngineJobs;
+}
diff --git a/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-feeds-airgap-page.component.ts b/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-feeds-airgap-page.component.ts
index 5b5131a42..fe75ebe38 100644
--- a/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-feeds-airgap-page.component.ts
+++ b/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-feeds-airgap-page.component.ts
@@ -20,7 +20,7 @@ type FeedsOfflineTab = 'feed-mirrors' | 'airgap-bundles' | 'version-locks';
@@ -90,15 +90,15 @@ type FeedsOfflineTab = 'feed-mirrors' | 'airgap-bundles' | 'version-locks';
@if (tab() === 'airgap-bundles') {
Offline import/export workflows and bundle verification controls.
}
@if (tab() === 'version-locks') {
Freeze upstream feed inputs used by promotion gates and replay evidence.
}
diff --git a/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-jobs-queues-page.component.ts b/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-jobs-queues-page.component.ts
index e759afe8c..ec7af7a27 100644
--- a/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-jobs-queues-page.component.ts
+++ b/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-jobs-queues-page.component.ts
@@ -65,8 +65,8 @@ interface WorkerRow {
@@ -133,8 +133,8 @@ interface WorkerRow {
{{ row.lastRun }}
{{ row.health }}
- View
- Run Now
+ View
+ Run Now
}
@@ -167,7 +167,7 @@ interface WorkerRow {
{{ row.duration }}
{{ row.impact }}
- View
+ View
Copy CorrID
@@ -197,8 +197,8 @@ interface WorkerRow {
{{ row.nextRun }}
{{ row.lastStatus }}
- Edit
- Pause
+ Edit
+ Pause
}
@@ -229,7 +229,7 @@ interface WorkerRow {
{{ row.retryable }}
{{ row.impact }}
- Replay
+ Replay
Copy CorrID
@@ -261,8 +261,8 @@ interface WorkerRow {
{{ row.capacity }}
{{ row.heartbeat }}
- View
- Drain
+ View
+ Drain
}
@@ -288,14 +288,14 @@ interface WorkerRow {
@if (tab() === 'dead-letters') {
Dead-letter triage is linked to release impact.
- Open DLQ & Replays
+ Open DLQ & Replays
}
@if (tab() === 'workers') {
Worker capacity and health affect queue latency and decision freshness SLAs.
}
diff --git a/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-ops-overview-page.component.html b/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-ops-overview-page.component.html
new file mode 100644
index 000000000..858de532f
--- /dev/null
+++ b/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-ops-overview-page.component.html
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+
+ Blocking subsystems
+ 3
+ Release affecting
+
+
+ Degraded surfaces
+ 5
+ Operator follow-up
+
+
+ Setup-owned handoffs
+ 2
+ Topology boundary
+
+
+ Queued actions
+ {{ pendingActions.length }}
+ Needs review
+
+
+
+
+
+
+
+ @for (group of overviewGroups; track group.id) {
+
+ }
+
+
+
+ @if (refreshedAt()) {
+
+ Snapshot refreshed at {{ refreshedAt() }}.
+
+ }
+
diff --git a/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-ops-overview-page.component.scss b/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-ops-overview-page.component.scss
new file mode 100644
index 000000000..d19bf296b
--- /dev/null
+++ b/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-ops-overview-page.component.scss
@@ -0,0 +1,258 @@
+:host {
+ display: block;
+}
+
+.ops-overview {
+ display: grid;
+ gap: 1rem;
+}
+
+.ops-overview__header {
+ display: flex;
+ justify-content: space-between;
+ gap: 1rem;
+ align-items: flex-start;
+}
+
+.ops-overview__title-group h1 {
+ margin: 0;
+ font-size: 1.55rem;
+}
+
+.ops-overview__eyebrow {
+ margin: 0 0 0.2rem;
+ color: var(--color-brand-primary);
+ font-size: 0.76rem;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.ops-overview__subtitle {
+ margin: 0.3rem 0 0;
+ max-width: 72ch;
+ color: var(--color-text-secondary);
+ font-size: 0.84rem;
+ line-height: 1.5;
+}
+
+.ops-overview__actions {
+ display: flex;
+ gap: 0.4rem;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+}
+
+.ops-overview__actions a,
+.ops-overview__actions button,
+.ops-overview__submenu-link,
+.blocking-card,
+.ops-card,
+.ops-overview__boundary-links a {
+ text-decoration: none;
+}
+
+.ops-overview__actions a,
+.ops-overview__actions button {
+ border: 1px solid var(--color-border-primary);
+ border-radius: var(--radius-md);
+ background: var(--color-surface-primary);
+ color: var(--color-text-primary);
+ padding: 0.38rem 0.66rem;
+ font-size: 0.74rem;
+ cursor: pointer;
+}
+
+.ops-overview__submenu {
+ display: flex;
+ gap: 0.45rem;
+ flex-wrap: wrap;
+}
+
+.ops-overview__submenu-link {
+ border: 1px solid var(--color-border-primary);
+ border-radius: var(--radius-full);
+ background: var(--color-surface-primary);
+ color: var(--color-text-secondary);
+ padding: 0.2rem 0.62rem;
+ font-size: 0.72rem;
+}
+
+.ops-overview__summary {
+ display: grid;
+ gap: 0.7rem;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+}
+
+.ops-overview__summary article,
+.ops-overview__panel {
+ border: 1px solid var(--color-border-primary);
+ border-radius: var(--radius-lg);
+ background: var(--color-surface-primary);
+ padding: 0.85rem;
+ display: grid;
+ gap: 0.25rem;
+}
+
+.ops-overview__summary strong {
+ font-size: 1.45rem;
+}
+
+.ops-overview__summary-label,
+.ops-overview__section-header p,
+.ops-card p,
+.ops-overview__panel p,
+.ops-overview__panel li span {
+ color: var(--color-text-secondary);
+}
+
+.ops-overview__summary-label {
+ font-size: 0.74rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.ops-overview__section-header {
+ display: flex;
+ justify-content: space-between;
+ gap: 0.75rem;
+ align-items: flex-start;
+}
+
+.ops-overview__section-header h2,
+.ops-overview__panel h2 {
+ margin: 0;
+ font-size: 1rem;
+}
+
+.ops-overview__section-header p,
+.ops-card p,
+.ops-overview__panel p,
+.ops-overview__panel li span {
+ margin: 0.25rem 0 0;
+ font-size: 0.8rem;
+ line-height: 1.45;
+}
+
+.ops-overview__blocking-grid,
+.ops-overview__group-grid,
+.ops-overview__footer-grid {
+ display: grid;
+ gap: 0.8rem;
+}
+
+.ops-overview__blocking-grid,
+.ops-overview__group-grid {
+ grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
+}
+
+.ops-overview__footer-grid {
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+}
+
+.blocking-card,
+.ops-card {
+ border: 1px solid var(--color-border-primary);
+ border-radius: var(--radius-lg);
+ background: var(--color-surface-primary);
+ color: inherit;
+ padding: 0.85rem;
+ display: grid;
+ gap: 0.4rem;
+}
+
+.blocking-card__topline,
+.ops-card__header {
+ display: flex;
+ justify-content: space-between;
+ gap: 0.5rem;
+ align-items: center;
+}
+
+.blocking-card__route,
+.ops-card__owner {
+ font-size: 0.68rem;
+ font-weight: var(--font-weight-semibold);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--color-text-secondary);
+}
+
+.ops-card__owner--setup {
+ color: var(--color-brand-primary);
+}
+
+.blocking-card h3,
+.ops-card h3 {
+ margin: 0;
+ font-size: 0.96rem;
+}
+
+.impact {
+ width: fit-content;
+ border-radius: var(--radius-full);
+ padding: 0.12rem 0.48rem;
+ font-size: 0.68rem;
+ font-weight: var(--font-weight-semibold);
+}
+
+.impact--blocking {
+ background: var(--color-status-error-bg);
+ color: var(--color-status-error-text);
+}
+
+.impact--degraded {
+ background: var(--color-status-warning-bg);
+ color: var(--color-status-warning-text);
+}
+
+.impact--info {
+ background: var(--color-status-info-bg);
+ color: var(--color-status-info-text);
+}
+
+.ops-overview__panel ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: grid;
+ gap: 0.65rem;
+}
+
+.ops-overview__panel li {
+ display: grid;
+ gap: 0.16rem;
+}
+
+.ops-overview__panel li a,
+.ops-overview__boundary-links a {
+ color: var(--color-brand-primary);
+}
+
+.ops-overview__panel li strong {
+ font-size: 0.7rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.ops-overview__boundary-links {
+ display: flex;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+ margin-top: 0.6rem;
+}
+
+.ops-overview__note {
+ margin: 0;
+ font-size: 0.75rem;
+ color: var(--color-text-secondary);
+}
+
+@media (max-width: 900px) {
+ .ops-overview__header {
+ flex-direction: column;
+ }
+
+ .ops-overview__actions {
+ justify-content: flex-start;
+ }
+}
diff --git a/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-ops-overview-page.component.ts b/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-ops-overview-page.component.ts
index 88cd98bf3..e309cf781 100644
--- a/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-ops-overview-page.component.ts
+++ b/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-ops-overview-page.component.ts
@@ -1,330 +1,316 @@
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { RouterLink } from '@angular/router';
-import { DoctorChecksInlineComponent } from '../../doctor/components/doctor-checks-inline/doctor-checks-inline.component';
-interface WorkflowCard {
- id: string;
- title: string;
- description: string;
- route: string;
- impact: 'BLOCKING' | 'DEGRADED' | 'INFO';
+import { DoctorChecksInlineComponent } from '../../doctor/components/doctor-checks-inline/doctor-checks-inline.component';
+import {
+ OPERATIONS_INTEGRATION_PATHS,
+ OPERATIONS_PATHS,
+ OPERATIONS_SETUP_PATHS,
+ dataIntegrityPath,
+} from './operations-paths';
+
+type OpsImpact = 'blocking' | 'degraded' | 'info';
+
+interface OverviewNavItem {
+ readonly id: string;
+ readonly label: string;
+ readonly route: string;
+}
+
+interface BlockingCard {
+ readonly id: string;
+ readonly title: string;
+ readonly detail: string;
+ readonly metric: string;
+ readonly impact: OpsImpact;
+ readonly route: string;
+}
+
+interface OperationsCard {
+ readonly id: string;
+ readonly title: string;
+ readonly detail: string;
+ readonly metric: string;
+ readonly impact: OpsImpact;
+ readonly route: string;
+ readonly owner: 'Ops' | 'Setup';
+}
+
+interface OperationsGroup {
+ readonly id: string;
+ readonly title: string;
+ readonly description: string;
+ readonly cards: readonly OperationsCard[];
+}
+
+interface PendingAction {
+ readonly id: string;
+ readonly title: string;
+ readonly detail: string;
+ readonly route: string;
+ readonly owner: 'Ops' | 'Setup';
}
@Component({
selector: 'app-platform-ops-overview-page',
standalone: true,
imports: [RouterLink, DoctorChecksInlineComponent],
+ templateUrl: './platform-ops-overview-page.component.html',
+ styleUrls: ['./platform-ops-overview-page.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
- template: `
-
-
-
-
-
- Data Trust Score
- 87
- WARN
-
-
- Platform Health
- 2
- WARN services
-
-
- Dead Letter Queue
- 3
- DEGRADED
-
-
-
-
-
-
-
-
- Secondary Operator Tools
-
-
-
-
- Recent Operator Alerts
-
-
- NVD feed stale 3h12m
- Impact: BLOCKING
- Open
-
-
- Runtime ingest backlog
- Impact: DEGRADED
- Open
-
-
- DLQ replay queue pending
- Impact: DEGRADED
- Open
-
-
-
-
- @if (refreshed()) {
- Snapshot refreshed for current scope.
- }
-
- `,
- styles: [`
- .ops-overview {
- display: grid;
- gap: 0.9rem;
- }
-
- .ops-overview__header {
- display: flex;
- justify-content: space-between;
- gap: 0.9rem;
- align-items: start;
- }
-
- .ops-overview__header h1 {
- margin: 0;
- font-size: 1.4rem;
- }
-
- .ops-overview__header p {
- margin: 0.2rem 0 0;
- color: var(--color-text-secondary);
- font-size: 0.82rem;
- max-width: 66ch;
- }
-
- .ops-overview__actions {
- display: flex;
- gap: 0.35rem;
- flex-wrap: wrap;
- }
-
- .ops-overview__actions a,
- .ops-overview__actions button {
- border: 1px solid var(--color-border-primary);
- border-radius: var(--radius-md);
- padding: 0.35rem 0.6rem;
- background: var(--color-surface-primary);
- text-decoration: none;
- color: var(--color-text-primary);
- font-size: 0.74rem;
- cursor: pointer;
- }
-
- .ops-overview__kpis {
- display: grid;
- gap: 0.6rem;
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
- }
-
- .ops-overview__kpis article {
- border: 1px solid var(--color-border-primary);
- border-radius: var(--radius-lg);
- background: var(--color-surface-primary);
- padding: 0.65rem;
- display: grid;
- gap: 0.2rem;
- }
-
- .ops-overview__kpis h2 {
- margin: 0;
- font-size: 0.74rem;
- color: var(--color-text-secondary);
- text-transform: uppercase;
- letter-spacing: 0.03em;
- }
-
- .ops-overview__kpis p {
- margin: 0;
- font-size: 1.15rem;
- font-weight: var(--font-weight-semibold);
- color: var(--color-text-heading);
- }
-
- .pill {
- width: fit-content;
- border-radius: var(--radius-full);
- padding: 0.1rem 0.45rem;
- font-size: 0.66rem;
- font-weight: var(--font-weight-semibold);
- }
-
- .pill--warn {
- background: var(--color-status-warning-bg);
- color: var(--color-status-warning-text);
- }
-
- .pill--degraded {
- background: var(--color-status-info-bg);
- color: var(--color-status-info-text);
- }
-
- .ops-overview__primary h2,
- .ops-overview__secondary h2,
- .ops-overview__alerts h2 {
- margin: 0 0 0.4rem;
- font-size: 0.95rem;
- }
-
- .ops-overview__grid {
- display: grid;
- gap: 0.6rem;
- grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
- }
-
- .ops-card {
- border: 1px solid var(--color-border-primary);
- border-radius: var(--radius-lg);
- background: var(--color-surface-primary);
- padding: 0.7rem;
- text-decoration: none;
- color: inherit;
- display: grid;
- gap: 0.25rem;
- }
-
- .ops-card h3 {
- margin: 0;
- font-size: 0.88rem;
- }
-
- .ops-card p {
- margin: 0;
- font-size: 0.74rem;
- color: var(--color-text-secondary);
- }
-
- .impact {
- width: fit-content;
- border-radius: var(--radius-full);
- padding: 0.1rem 0.45rem;
- font-size: 0.65rem;
- font-weight: var(--font-weight-semibold);
- }
-
- .impact--blocking {
- background: var(--color-status-error-bg);
- color: var(--color-status-error-text);
- }
-
- .impact--degraded {
- background: var(--color-status-warning-bg);
- color: var(--color-status-warning-text);
- }
-
- .impact--info {
- background: var(--color-status-info-bg);
- color: var(--color-status-info-text);
- }
-
- .ops-overview__links {
- display: flex;
- flex-wrap: wrap;
- gap: 0.4rem;
- }
-
- .ops-overview__links a {
- border: 1px solid var(--color-border-primary);
- border-radius: var(--radius-md);
- background: var(--color-surface-primary);
- padding: 0.25rem 0.45rem;
- font-size: 0.74rem;
- color: var(--color-brand-primary);
- text-decoration: none;
- }
-
- .ops-overview__alerts ul {
- list-style: none;
- margin: 0;
- padding: 0;
- display: grid;
- gap: 0.35rem;
- }
-
- .ops-overview__alerts li {
- border: 1px solid var(--color-border-primary);
- border-radius: var(--radius-md);
- background: var(--color-surface-primary);
- padding: 0.45rem 0.55rem;
- display: flex;
- gap: 0.45rem;
- flex-wrap: wrap;
- align-items: center;
- font-size: 0.76rem;
- }
-
- .ops-overview__alerts a {
- color: var(--color-brand-primary);
- text-decoration: none;
- }
-
- .ops-overview__note {
- margin: 0;
- font-size: 0.74rem;
- color: var(--color-text-secondary);
- }
- `],
})
export class PlatformOpsOverviewPageComponent {
- readonly refreshed = signal(false);
+ readonly OPERATIONS_PATHS = OPERATIONS_PATHS;
+ readonly OPERATIONS_SETUP_PATHS = OPERATIONS_SETUP_PATHS;
+ readonly refreshedAt = signal(null);
- readonly primaryWorkflows: WorkflowCard[] = [
+ readonly quickNav: readonly OverviewNavItem[] = [
+ { id: 'overview', label: 'Overview', route: OPERATIONS_PATHS.overview },
+ { id: 'data-integrity', label: 'Data Integrity', route: OPERATIONS_PATHS.dataIntegrity },
+ { id: 'jobs-queues', label: 'Jobs & Queues', route: OPERATIONS_PATHS.jobsQueues },
+ { id: 'health-slo', label: 'Health & SLO', route: OPERATIONS_PATHS.healthSlo },
+ { id: 'feeds-airgap', label: 'Feeds & Airgap', route: OPERATIONS_PATHS.feedsAirgap },
+ { id: 'offline-kit', label: 'Offline Kit', route: OPERATIONS_PATHS.offlineKit },
+ { id: 'quotas', label: 'Quotas & Limits', route: OPERATIONS_PATHS.quotas },
+ { id: 'aoc', label: 'AOC Compliance', route: OPERATIONS_PATHS.aoc },
+ { id: 'doctor', label: 'Diagnostics', route: OPERATIONS_PATHS.doctor },
+ { id: 'signals', label: 'Signals', route: OPERATIONS_PATHS.signals },
+ { id: 'packs', label: 'Pack Registry', route: OPERATIONS_PATHS.packs },
+ { id: 'notifications', label: 'Notifications', route: OPERATIONS_PATHS.notifications },
+ ];
+
+ readonly blockingCards: readonly BlockingCard[] = [
{
- id: 'data-integrity',
- title: 'Data Integrity',
- description: 'Trust signals, blocked decisions, and freshness recovery actions.',
- route: '/platform/ops/data-integrity',
- impact: 'BLOCKING',
+ id: 'feeds',
+ title: 'Feed freshness blocking releases',
+ detail: 'NVD mirror is stale and approvals are pinned to the last-known-good snapshot.',
+ metric: '3h 12m stale',
+ impact: 'blocking',
+ route: dataIntegrityPath('feeds-freshness'),
},
{
- id: 'jobs-queues',
- title: 'Jobs & Queues',
- description: 'Unified orchestration runs, schedules, dead letters, and workers.',
- route: '/platform/ops/jobs-queues',
- impact: 'DEGRADED',
+ id: 'dlq',
+ title: 'Replay backlog degrading confidence',
+ detail: 'Dead-letter replay queue is elevated for reachability and evidence exports.',
+ metric: '3 queued replays',
+ impact: 'degraded',
+ route: dataIntegrityPath('dlq'),
},
{
- id: 'health-slo',
- title: 'Health & SLO',
- description: 'Service/dependency health and incident timelines with SLO context.',
- route: '/platform/ops/health-slo',
- impact: 'INFO',
+ id: 'aoc',
+ title: 'AOC compliance needs operator review',
+ detail: 'Recent provenance violations require a compliance drilldown before promotion.',
+ metric: '4 open violations',
+ impact: 'blocking',
+ route: `${OPERATIONS_PATHS.aoc}/violations`,
},
];
+
+ readonly overviewGroups: readonly OperationsGroup[] = [
+ {
+ id: 'blocking',
+ title: 'Blocking',
+ description: 'Operator-first paths for issues that can block release, evidence, or trust decisions.',
+ cards: [
+ {
+ id: 'data-integrity',
+ title: 'Data Integrity',
+ detail: 'Feeds freshness, scan pipeline health, integration reachability, and DLQ replay safety.',
+ metric: '5 trust signals',
+ impact: 'blocking',
+ route: OPERATIONS_PATHS.dataIntegrity,
+ owner: 'Ops',
+ },
+ {
+ id: 'aoc',
+ title: 'AOC Compliance',
+ detail: 'Control attestation, provenance validation, and violation triage.',
+ metric: '4 violations',
+ impact: 'blocking',
+ route: OPERATIONS_PATHS.aoc,
+ owner: 'Ops',
+ },
+ {
+ id: 'notifications',
+ title: 'Notifications',
+ detail: 'Critical operator alerts, escalation channels, and delivery status.',
+ metric: '2 paging alerts',
+ impact: 'degraded',
+ route: OPERATIONS_PATHS.notifications,
+ owner: 'Ops',
+ },
+ ],
+ },
+ {
+ id: 'execution',
+ title: 'Execution',
+ description: 'Execution control for queues, workers, schedules, runtime signals, and replay flow.',
+ cards: [
+ {
+ id: 'jobs-queues',
+ title: 'Jobs & Queues',
+ detail: 'Orchestrator jobs, dead-letter posture, worker fleet, and immediate drill-ins.',
+ metric: '1 blocking run',
+ impact: 'degraded',
+ route: OPERATIONS_PATHS.jobsQueues,
+ owner: 'Ops',
+ },
+ {
+ id: 'scheduler',
+ title: 'Scheduler',
+ detail: 'Run inventory, schedules, and worker coordination windows.',
+ metric: '19 active schedules',
+ impact: 'info',
+ route: OPERATIONS_PATHS.schedulerRuns,
+ owner: 'Ops',
+ },
+ {
+ id: 'signals',
+ title: 'Signals',
+ detail: 'Runtime signal freshness, event volume, and missing telemetry indicators.',
+ metric: '97% fresh',
+ impact: 'info',
+ route: OPERATIONS_PATHS.signals,
+ owner: 'Ops',
+ },
+ ],
+ },
+ {
+ id: 'health',
+ title: 'Health',
+ description: 'System posture, diagnostics, and operational evidence for degraded subsystems.',
+ cards: [
+ {
+ id: 'health-slo',
+ title: 'Health & SLO',
+ detail: 'Service health, burn-rate posture, and subsystem incident context.',
+ metric: '2 degraded services',
+ impact: 'degraded',
+ route: OPERATIONS_PATHS.healthSlo,
+ owner: 'Ops',
+ },
+ {
+ id: 'doctor',
+ title: 'Diagnostics',
+ detail: 'Interactive doctor checks, dependency probes, and operator troubleshooting.',
+ metric: '11 checks',
+ impact: 'info',
+ route: OPERATIONS_PATHS.doctor,
+ owner: 'Ops',
+ },
+ {
+ id: 'status',
+ title: 'System Status',
+ detail: 'Global status, service heartbeat, and cross-product availability view.',
+ metric: '1 regional incident',
+ impact: 'degraded',
+ route: OPERATIONS_PATHS.status,
+ owner: 'Ops',
+ },
+ ],
+ },
+ {
+ id: 'supply-airgap',
+ title: 'Supply And Airgap',
+ description: 'Feed sourcing, offline delivery, and pack distribution in one operator view.',
+ cards: [
+ {
+ id: 'feeds-airgap',
+ title: 'Feeds & Airgap',
+ detail: 'Mirror freshness, version locks, airgap bundle flows, and source configuration.',
+ metric: '1 degraded mirror',
+ impact: 'blocking',
+ route: OPERATIONS_PATHS.feedsAirgap,
+ owner: 'Ops',
+ },
+ {
+ id: 'offline-kit',
+ title: 'Offline Kit',
+ detail: 'Offline import/export operations, evidence bundle movement, and sealed-mode transfers.',
+ metric: '3 queued exports',
+ impact: 'info',
+ route: OPERATIONS_PATHS.offlineKit,
+ owner: 'Ops',
+ },
+ {
+ id: 'packs',
+ title: 'Pack Registry',
+ detail: 'Pack distribution, bundle integrity, and package availability for workflows.',
+ metric: '14 active packs',
+ impact: 'info',
+ route: OPERATIONS_PATHS.packs,
+ owner: 'Ops',
+ },
+ ],
+ },
+ {
+ id: 'capacity-boundary',
+ title: 'Capacity And Setup Boundary',
+ description: 'Capacity remains in Operations, while topology and agent inventory stay owned by Setup.',
+ cards: [
+ {
+ id: 'quotas',
+ title: 'Quotas & Limits',
+ detail: 'JobEngine quotas, operator limits, and burst-capacity review.',
+ metric: '1 tenant near limit',
+ impact: 'degraded',
+ route: OPERATIONS_PATHS.quotas,
+ owner: 'Ops',
+ },
+ {
+ id: 'topology',
+ title: 'Topology Overview',
+ detail: 'Infrastructure placement, regions, environments, and promotion-path ownership.',
+ metric: 'Setup owned',
+ impact: 'info',
+ route: OPERATIONS_SETUP_PATHS.topologyOverview,
+ owner: 'Setup',
+ },
+ {
+ id: 'agents',
+ title: 'Agent Fleet',
+ detail: 'Agent enrollment, placement, and health remain under Setup > Topology.',
+ metric: 'Setup owned',
+ impact: 'info',
+ route: OPERATIONS_SETUP_PATHS.topologyAgents,
+ owner: 'Setup',
+ },
+ ],
+ },
+ ];
+
+ readonly pendingActions: readonly PendingAction[] = [
+ {
+ id: 'retry-feed-sync',
+ title: 'Recover stale feed source',
+ detail: 'Open the feeds freshness lens and review version-lock posture before retry.',
+ route: dataIntegrityPath('feeds-freshness'),
+ owner: 'Ops',
+ },
+ {
+ id: 'replay-dlq',
+ title: 'Drain replay backlog',
+ detail: 'Open DLQ and replay blockers before the next promotion window.',
+ route: dataIntegrityPath('dlq'),
+ owner: 'Ops',
+ },
+ {
+ id: 'review-agent-placement',
+ title: 'Review agent placement',
+ detail: 'Agent fleet issues route to Setup because topology ownership stays out of Operations.',
+ route: OPERATIONS_SETUP_PATHS.topologyAgents,
+ owner: 'Setup',
+ },
+ {
+ id: 'configure-advisory-sources',
+ title: 'Review advisory source config',
+ detail: 'Use Integrations for source configuration; Operations remains the monitoring shell.',
+ route: OPERATIONS_INTEGRATION_PATHS.advisorySources,
+ owner: 'Ops',
+ },
+ ];
+
+ refreshSnapshot(): void {
+ this.refreshedAt.set(new Date().toISOString());
+ }
}
diff --git a/src/Web/StellaOps.Web/src/app/routes/legacy-redirects.routes.ts b/src/Web/StellaOps.Web/src/app/routes/legacy-redirects.routes.ts
index 754cb0e60..099a1cc31 100644
--- a/src/Web/StellaOps.Web/src/app/routes/legacy-redirects.routes.ts
+++ b/src/Web/StellaOps.Web/src/app/routes/legacy-redirects.routes.ts
@@ -1,4 +1,5 @@
-import { Routes } from '@angular/router';
+import { inject } from '@angular/core';
+import { Params, Router, Routes } from '@angular/router';
export interface LegacyRedirectRouteTemplate {
path: string;
@@ -6,10 +7,58 @@ export interface LegacyRedirectRouteTemplate {
pathMatch?: 'prefix' | 'full';
}
-/**
- * Pre-alpha route policy: no legacy redirect map.
- * Keep exports as empty collections so historical imports compile without enabling aliases.
- */
-export const LEGACY_REDIRECT_ROUTE_TEMPLATES: readonly LegacyRedirectRouteTemplate[] = [];
+export const LEGACY_REDIRECT_ROUTE_TEMPLATES: readonly LegacyRedirectRouteTemplate[] = [
+ {
+ path: 'ops/health',
+ redirectTo: '/ops/operations/health-slo',
+ pathMatch: 'full',
+ },
+ {
+ path: 'release-orchestrator/environments',
+ redirectTo: '/topology/regions',
+ pathMatch: 'full',
+ },
+ {
+ path: 'triage/artifacts',
+ redirectTo: '/security/artifacts',
+ pathMatch: 'full',
+ },
+ {
+ path: 'triage/artifacts/:artifactId',
+ redirectTo: '/security/artifacts/:artifactId',
+ pathMatch: 'full',
+ },
+ {
+ path: 'triage/findings',
+ redirectTo: '/security/findings',
+ pathMatch: 'full',
+ },
+ {
+ path: 'triage/findings/:findingId',
+ redirectTo: '/security/findings/:findingId',
+ pathMatch: 'full',
+ },
+];
-export const LEGACY_REDIRECT_ROUTES: Routes = [];
+export const LEGACY_REDIRECT_ROUTES: Routes = LEGACY_REDIRECT_ROUTE_TEMPLATES.map((template) => ({
+ path: template.path,
+ redirectTo: ({ params, queryParams, fragment }) => {
+ const router = inject(Router);
+ const target = router.parseUrl(interpolateRedirectTarget(template.redirectTo, params));
+ target.queryParams = { ...queryParams };
+ target.fragment = fragment ?? null;
+ return target;
+ },
+ pathMatch: template.pathMatch ?? 'full',
+}));
+
+function interpolateRedirectTarget(template: string, params: Params): string {
+ let target = template;
+
+ for (const [name, rawValue] of Object.entries(params ?? {})) {
+ const value = Array.isArray(rawValue) ? rawValue.join('/') : String(rawValue);
+ target = target.replaceAll(`:${name}`, encodeURIComponent(value));
+ }
+
+ return target;
+}
diff --git a/src/Web/StellaOps.Web/src/app/routes/operations.routes.ts b/src/Web/StellaOps.Web/src/app/routes/operations.routes.ts
index 75229070f..6d6b41234 100644
--- a/src/Web/StellaOps.Web/src/app/routes/operations.routes.ts
+++ b/src/Web/StellaOps.Web/src/app/routes/operations.routes.ts
@@ -77,6 +77,26 @@ export const OPERATIONS_ROUTES: Routes = [
(m) => m.JobEngineJobsComponent,
),
},
+ {
+ path: 'jobengine/jobs/:jobId',
+ title: 'Job Detail',
+ data: { breadcrumb: 'Job Detail' },
+ canMatch: [requireOrchViewerGuard],
+ loadComponent: () =>
+ import('../features/jobengine/jobengine-job-detail.component').then(
+ (m) => m.JobEngineJobDetailComponent,
+ ),
+ },
+ {
+ path: 'jobengine/jobs/:jobId/dag',
+ title: 'Job DAG',
+ data: { breadcrumb: 'Job DAG' },
+ canMatch: [requireOrchViewerGuard],
+ loadComponent: () =>
+ import('../features/jobengine/jobengine-job-detail.component').then(
+ (m) => m.JobEngineJobDetailComponent,
+ ),
+ },
{
path: 'orchestrator/jobs/:jobId',
title: 'Job Detail',
diff --git a/src/Web/StellaOps.Web/src/app/routes/platform-ops.routes.ts b/src/Web/StellaOps.Web/src/app/routes/platform-ops.routes.ts
index 14eb4a11b..ee474defb 100644
--- a/src/Web/StellaOps.Web/src/app/routes/platform-ops.routes.ts
+++ b/src/Web/StellaOps.Web/src/app/routes/platform-ops.routes.ts
@@ -1,4 +1,101 @@
-import { Routes } from '@angular/router';
+import { inject } from '@angular/core';
+import { Params, Router, Routes } from '@angular/router';
-/** Legacy Platform Ops tree retired in pre-alpha IA. */
-export const PLATFORM_OPS_ROUTES: Routes = [];
+import {
+ OPERATIONS_PATHS,
+ OPERATIONS_SETUP_PATHS,
+} from '../features/platform/ops/operations-paths';
+
+interface LegacyPlatformOpsRedirect {
+ path: string;
+ redirectTo: string;
+}
+
+const LEGACY_PLATFORM_OPS_REDIRECTS: readonly LegacyPlatformOpsRedirect[] = [
+ { path: '', redirectTo: OPERATIONS_PATHS.overview },
+ { path: 'data-integrity', redirectTo: OPERATIONS_PATHS.dataIntegrity },
+ {
+ path: 'data-integrity/nightly-ops/:runId',
+ redirectTo: `${OPERATIONS_PATHS.dataIntegrity}/nightly-ops/:runId`,
+ },
+ {
+ path: 'data-integrity/:section',
+ redirectTo: `${OPERATIONS_PATHS.dataIntegrity}/:section`,
+ },
+ { path: 'jobs-queues', redirectTo: OPERATIONS_PATHS.jobsQueues },
+ { path: 'feeds', redirectTo: OPERATIONS_PATHS.feedsAirgap },
+ { path: 'feeds-airgap', redirectTo: OPERATIONS_PATHS.feedsAirgap },
+ { path: 'offline-kit', redirectTo: OPERATIONS_PATHS.offlineKit },
+ { path: 'health', redirectTo: OPERATIONS_PATHS.healthSlo },
+ { path: 'health-slo', redirectTo: OPERATIONS_PATHS.healthSlo },
+ { path: 'doctor', redirectTo: OPERATIONS_PATHS.doctor },
+ { path: 'quotas', redirectTo: OPERATIONS_PATHS.quotas },
+ { path: 'aoc', redirectTo: OPERATIONS_PATHS.aoc },
+ { path: 'signals', redirectTo: OPERATIONS_PATHS.signals },
+ { path: 'packs', redirectTo: OPERATIONS_PATHS.packs },
+ { path: 'notifications', redirectTo: OPERATIONS_PATHS.notifications },
+ { path: 'status', redirectTo: OPERATIONS_PATHS.status },
+ { path: 'agents', redirectTo: OPERATIONS_SETUP_PATHS.topologyAgents },
+ { path: 'jobengine', redirectTo: OPERATIONS_PATHS.jobEngine },
+ { path: 'jobengine/jobs', redirectTo: OPERATIONS_PATHS.jobEngineJobs },
+ {
+ path: 'jobengine/jobs/:jobId/dag',
+ redirectTo: `${OPERATIONS_PATHS.jobEngineJobs}/:jobId`,
+ },
+ {
+ path: 'jobengine/jobs/:jobId',
+ redirectTo: `${OPERATIONS_PATHS.jobEngineJobs}/:jobId`,
+ },
+ { path: 'jobengine/quotas', redirectTo: OPERATIONS_PATHS.jobEngineQuotas },
+ { path: 'scheduler', redirectTo: OPERATIONS_PATHS.schedulerRuns },
+ { path: 'scheduler/runs', redirectTo: OPERATIONS_PATHS.schedulerRuns },
+ { path: 'scheduler/runs/:runId', redirectTo: OPERATIONS_PATHS.schedulerRuns },
+ {
+ path: 'scheduler/runs/:runId/stream',
+ redirectTo: OPERATIONS_PATHS.schedulerRuns,
+ },
+ {
+ path: 'scheduler/schedules',
+ redirectTo: OPERATIONS_PATHS.schedulerSchedules,
+ },
+ { path: 'scheduler/workers', redirectTo: OPERATIONS_PATHS.schedulerWorkers },
+ { path: 'dead-letter', redirectTo: OPERATIONS_PATHS.deadLetter },
+];
+
+export const PLATFORM_OPS_ROUTES: Routes = [
+ ...LEGACY_PLATFORM_OPS_REDIRECTS.map((template) => ({
+ path: template.path,
+ redirectTo: ({
+ params,
+ queryParams,
+ fragment,
+ }: {
+ params: Params;
+ queryParams: Params;
+ fragment?: string | null;
+ }) => {
+ const router = inject(Router);
+ const target = router.parseUrl(interpolateLegacyTarget(template.redirectTo, params));
+ target.queryParams = { ...queryParams };
+ target.fragment = fragment ?? null;
+ return target;
+ },
+ pathMatch: 'full' as const,
+ })),
+ {
+ path: '**',
+ redirectTo: OPERATIONS_PATHS.overview,
+ pathMatch: 'full',
+ },
+];
+
+function interpolateLegacyTarget(template: string, params: Params): string {
+ let target = template;
+
+ for (const [name, rawValue] of Object.entries(params ?? {})) {
+ const value = Array.isArray(rawValue) ? rawValue.join('/') : String(rawValue);
+ target = target.replaceAll(`:${name}`, encodeURIComponent(value));
+ }
+
+ return target;
+}
diff --git a/src/Web/StellaOps.Web/src/tests/navigation/legacy-redirects.spec.ts b/src/Web/StellaOps.Web/src/tests/navigation/legacy-redirects.spec.ts
index 23c28c587..b68fd590a 100644
--- a/src/Web/StellaOps.Web/src/tests/navigation/legacy-redirects.spec.ts
+++ b/src/Web/StellaOps.Web/src/tests/navigation/legacy-redirects.spec.ts
@@ -3,12 +3,46 @@ import {
LEGACY_REDIRECT_ROUTES,
} from '../../app/routes/legacy-redirects.routes';
-describe('Legacy redirect policy (pre-alpha)', () => {
- it('keeps redirect templates empty', () => {
- expect(LEGACY_REDIRECT_ROUTE_TEMPLATES).toEqual([]);
+describe('Legacy redirect policy', () => {
+ it('publishes the active legacy redirect templates still emitted by the shell', () => {
+ expect(LEGACY_REDIRECT_ROUTE_TEMPLATES).toEqual(
+ jasmine.arrayContaining([
+ jasmine.objectContaining({
+ path: 'ops/health',
+ redirectTo: '/ops/operations/health-slo',
+ }),
+ jasmine.objectContaining({
+ path: 'release-orchestrator/environments',
+ redirectTo: '/topology/regions',
+ }),
+ jasmine.objectContaining({
+ path: 'triage/artifacts/:artifactId',
+ redirectTo: '/security/artifacts/:artifactId',
+ }),
+ jasmine.objectContaining({
+ path: 'triage/findings/:findingId',
+ redirectTo: '/security/findings/:findingId',
+ }),
+ ]),
+ );
});
- it('keeps redirect route map empty', () => {
- expect(LEGACY_REDIRECT_ROUTES).toEqual([]);
+ it('materializes a router redirect for every legacy template', () => {
+ expect(LEGACY_REDIRECT_ROUTES.length).toBe(LEGACY_REDIRECT_ROUTE_TEMPLATES.length);
+ expect(LEGACY_REDIRECT_ROUTES).toEqual(
+ jasmine.arrayContaining([
+ jasmine.objectContaining({
+ path: 'triage/artifacts',
+ pathMatch: 'full',
+ }),
+ jasmine.objectContaining({
+ path: 'triage/findings',
+ pathMatch: 'full',
+ }),
+ ]),
+ );
+ for (const route of LEGACY_REDIRECT_ROUTES) {
+ expect(typeof route.redirectTo).toBe('function');
+ }
});
});
diff --git a/src/Web/StellaOps.Web/src/tests/platform-ops/data-integrity-pages.spec.ts b/src/Web/StellaOps.Web/src/tests/platform-ops/data-integrity-pages.spec.ts
index 46d9079c0..47805e0b5 100644
--- a/src/Web/StellaOps.Web/src/tests/platform-ops/data-integrity-pages.spec.ts
+++ b/src/Web/StellaOps.Web/src/tests/platform-ops/data-integrity-pages.spec.ts
@@ -40,11 +40,11 @@ describe('DataIntegrityOverviewComponent (platform-ops)', () => {
it('provides deep-link routes for all trust signals and drilldowns', () => {
expect(component.trustSignals.map((signal) => signal.route)).toEqual([
- '/platform/ops/data-integrity/feeds-freshness',
- '/platform/ops/data-integrity/scan-pipeline',
- '/platform/ops/data-integrity/reachability-ingest',
- '/platform/ops/data-integrity/integration-connectivity',
- '/platform/ops/data-integrity/dlq',
+ '/ops/operations/data-integrity/feeds-freshness',
+ '/ops/operations/data-integrity/scan-pipeline',
+ '/ops/operations/data-integrity/reachability-ingest',
+ '/ops/operations/data-integrity/integration-connectivity',
+ '/ops/operations/data-integrity/dlq',
]);
const drilldownLinks = fixture.nativeElement.querySelectorAll('.drilldowns a');
@@ -52,7 +52,7 @@ describe('DataIntegrityOverviewComponent (platform-ops)', () => {
});
it('uses canonical platform ops routes for top failures', () => {
- expect(component.topFailures.every((item) => item.route.startsWith('/platform/ops/'))).toBeTrue();
+ expect(component.topFailures.every((item) => item.route.startsWith('/ops/operations/'))).toBeTrue();
});
});
@@ -122,7 +122,7 @@ describe('FeedsFreshnessPageComponent (platform-ops)', () => {
it('links to canonical feeds and offline page and stays read-only', () => {
const links = fixture.nativeElement.querySelectorAll('footer.links a');
expect(links.length).toBe(3);
- expect((links[0] as HTMLAnchorElement).getAttribute('href')).toContain('/platform/ops/feeds-airgap');
+ expect((links[0] as HTMLAnchorElement).getAttribute('href')).toContain('/ops/operations/feeds-airgap');
expect((links[1] as HTMLAnchorElement).getAttribute('href')).toContain('tab=version-locks');
expect((links[2] as HTMLAnchorElement).getAttribute('href')).toContain('tab=feed-mirrors');
expect(fixture.nativeElement.querySelectorAll('input, textarea').length).toBe(0);
@@ -234,7 +234,7 @@ describe('DlqReplaysPageComponent (platform-ops)', () => {
it('links to canonical dead-letter page', () => {
const link = fixture.nativeElement.querySelector('footer.links a') as HTMLAnchorElement | null;
expect(link).toBeTruthy();
- expect(link?.getAttribute('href')).toContain('/platform/ops/dead-letter');
+ expect(link?.getAttribute('href')).toContain('/ops/operations/dead-letter');
});
});
diff --git a/src/Web/StellaOps.Web/src/tests/platform-ops/platform-ops-overview-page.component.spec.ts b/src/Web/StellaOps.Web/src/tests/platform-ops/platform-ops-overview-page.component.spec.ts
new file mode 100644
index 000000000..8c173e3ad
--- /dev/null
+++ b/src/Web/StellaOps.Web/src/tests/platform-ops/platform-ops-overview-page.component.spec.ts
@@ -0,0 +1,67 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { provideRouter } from '@angular/router';
+
+import {
+ DOCTOR_API,
+ MockDoctorClient,
+} from '../../app/features/doctor/services/doctor.client';
+import { PlatformOpsOverviewPageComponent } from '../../app/features/platform/ops/platform-ops-overview-page.component';
+
+describe('PlatformOpsOverviewPageComponent', () => {
+ let fixture: ComponentFixture;
+ let component: PlatformOpsOverviewPageComponent;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [PlatformOpsOverviewPageComponent],
+ providers: [provideRouter([]), { provide: DOCTOR_API, useClass: MockDoctorClient }],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(PlatformOpsOverviewPageComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('renders grouped operations sections and canonical quick-nav links', () => {
+ const text = fixture.nativeElement.textContent as string;
+ expect(text).toContain('Platform Ops');
+ expect(text).toContain('Blocking');
+ expect(text).toContain('Supply And Airgap');
+ expect(text).toContain('Capacity And Setup Boundary');
+
+ const navLinks = fixture.nativeElement.querySelectorAll('[data-testid^="operations-nav-"]');
+ expect(navLinks.length).toBe(component.quickNav.length);
+
+ const dataIntegrityLink = fixture.nativeElement.querySelector(
+ '[data-testid="operations-nav-data-integrity"]'
+ ) as HTMLAnchorElement | null;
+ expect(dataIntegrityLink?.getAttribute('href')).toContain('/ops/operations/data-integrity');
+ });
+
+ it('routes cards into canonical operations pages and setup boundary pages', () => {
+ const dataIntegrityCard = fixture.nativeElement.querySelector(
+ '[data-testid="operations-card-data-integrity"]'
+ ) as HTMLAnchorElement | null;
+ const agentFleetCard = fixture.nativeElement.querySelector(
+ '[data-testid="operations-card-agents"]'
+ ) as HTMLAnchorElement | null;
+
+ expect(dataIntegrityCard?.getAttribute('href')).toContain('/ops/operations/data-integrity');
+ expect(agentFleetCard?.getAttribute('href')).toContain('/setup/topology/agents');
+ });
+
+ it('emits a refresh note when the operator refreshes the snapshot', () => {
+ const refreshButton = fixture.nativeElement.querySelector(
+ '[data-testid="operations-refresh-btn"]'
+ ) as HTMLButtonElement | null;
+
+ expect(refreshButton).toBeTruthy();
+ refreshButton?.click();
+ fixture.detectChanges();
+
+ expect(component.refreshedAt()).not.toBeNull();
+ expect(
+ fixture.nativeElement.querySelector('[data-testid="operations-refresh-note"]')
+ ).toBeTruthy();
+ });
+});
diff --git a/src/Web/StellaOps.Web/src/tests/platform-ops/platform-ops-routes.spec.ts b/src/Web/StellaOps.Web/src/tests/platform-ops/platform-ops-routes.spec.ts
index 3c78064d1..8a2dbbcfc 100644
--- a/src/Web/StellaOps.Web/src/tests/platform-ops/platform-ops-routes.spec.ts
+++ b/src/Web/StellaOps.Web/src/tests/platform-ops/platform-ops-routes.spec.ts
@@ -1,22 +1,45 @@
+import { routes as APP_ROUTES } from '../../app/app.routes';
import { OPS_ROUTES } from '../../app/routes/ops.routes';
import { OPERATIONS_ROUTES } from '../../app/routes/operations.routes';
-import { integrationHubRoutes } from '../../app/features/integration-hub/integration-hub.routes';
+import { PLATFORM_OPS_ROUTES } from '../../app/routes/platform-ops.routes';
-describe('OPS_ROUTES (pre-alpha)', () => {
- it('contains canonical top-level ops paths', () => {
+describe('Platform and Operations route contracts', () => {
+ it('mounts legacy platform-ops redirects in the app router', () => {
+ const appPaths = APP_ROUTES.map((route) => route.path);
+ expect(appPaths).toContain('platform-ops');
+ expect(appPaths).toContain('platform');
+
+ const platformRoute = APP_ROUTES.find((route) => route.path === 'platform');
+ const platformChildren = platformRoute?.children?.map((route) => route.path) ?? [];
+ expect(platformChildren).toContain('ops');
+ });
+
+ it('keeps canonical Operations navigation under /ops', () => {
const paths = OPS_ROUTES.map((route) => route.path);
- expect(paths).toEqual(['', 'operations', 'integrations', 'policy', 'platform-setup']);
+ expect(paths).toContain('operations');
+ expect(paths).toContain('integrations');
+ expect(paths).toContain('policy');
+ expect(paths).toContain('platform-setup');
+ expect(paths).toContain('scanner-ops');
+ expect(paths).toContain('agents');
});
- it('has no redirects', () => {
- for (const route of OPS_ROUTES) {
- expect(route.redirectTo).toBeUndefined();
- }
+ it('keeps shorthand ops aliases pointed at the operations shell', () => {
+ const aliases = OPS_ROUTES.filter((route) => route.redirectTo);
+ expect(aliases.map((route) => route.path)).toEqual([
+ 'feeds',
+ 'feeds-airgap',
+ 'airgap',
+ 'health-slo',
+ 'signals',
+ 'scheduler',
+ 'offline-kit',
+ 'quotas',
+ 'packs',
+ ]);
});
-});
-describe('OPERATIONS_ROUTES (pre-alpha)', () => {
- it('includes required operations surfaces', () => {
+ it('includes required canonical operations surfaces', () => {
const paths = OPERATIONS_ROUTES.map((route) => route.path);
const expected = [
'',
@@ -28,6 +51,8 @@ describe('OPERATIONS_ROUTES (pre-alpha)', () => {
'jobengine',
'jobengine/jobs',
'jobengine/jobs/:jobId',
+ 'jobengine/jobs/:jobId/dag',
+ 'orchestrator/jobs/:jobId',
'jobengine/quotas',
'scheduler',
'quotas',
@@ -48,40 +73,44 @@ describe('OPERATIONS_ROUTES (pre-alpha)', () => {
}
});
- it('has no redirects', () => {
- for (const route of OPERATIONS_ROUTES) {
- expect(route.redirectTo).toBeUndefined();
- }
- });
-});
-
-describe('integrationHubRoutes (pre-alpha)', () => {
- it('contains canonical integrations surfaces under ops', () => {
- const paths = integrationHubRoutes.map((route) => route.path);
+ it('keeps legacy platform ops redirects available for old bookmarks', () => {
+ const paths = PLATFORM_OPS_ROUTES.map((route) => route.path);
const expected = [
'',
- 'onboarding',
- 'onboarding/:type',
- 'registries',
- 'scm',
- 'ci',
- 'runtime-hosts',
- 'advisory-vex-sources',
- 'secrets',
+ 'data-integrity',
+ 'data-integrity/nightly-ops/:runId',
+ 'data-integrity/:section',
+ 'jobs-queues',
+ 'feeds',
+ 'feeds-airgap',
+ 'offline-kit',
+ 'health',
+ 'health-slo',
+ 'doctor',
+ 'quotas',
+ 'aoc',
+ 'signals',
+ 'packs',
'notifications',
- 'sbom-sources',
- 'activity',
- ':integrationId',
+ 'status',
+ 'agents',
+ 'jobengine',
+ 'jobengine/jobs',
+ 'jobengine/jobs/:jobId',
+ 'jobengine/jobs/:jobId/dag',
+ 'jobengine/quotas',
+ 'scheduler',
+ 'scheduler/runs',
+ 'scheduler/runs/:runId',
+ 'scheduler/runs/:runId/stream',
+ 'scheduler/schedules',
+ 'scheduler/workers',
+ 'dead-letter',
+ '**',
];
for (const path of expected) {
expect(paths).toContain(path);
}
});
-
- it('has no redirects', () => {
- for (const route of integrationHubRoutes) {
- expect(route.redirectTo).toBeUndefined();
- }
- });
});
diff --git a/src/Web/StellaOps.Web/tests/e2e/operations-consolidation.spec.ts b/src/Web/StellaOps.Web/tests/e2e/operations-consolidation.spec.ts
new file mode 100644
index 000000000..f2159d076
--- /dev/null
+++ b/src/Web/StellaOps.Web/tests/e2e/operations-consolidation.spec.ts
@@ -0,0 +1,156 @@
+import { expect, test, type Page, type Route } from '@playwright/test';
+
+import type { StubAuthSession } from '../../src/app/testing/auth-fixtures';
+
+const adminSession: StubAuthSession = {
+ subjectId: 'operations-e2e-user',
+ tenant: 'tenant-default',
+ scopes: [
+ 'admin',
+ 'ui.read',
+ 'ui.admin',
+ 'orch:read',
+ 'orch:operate',
+ 'health:read',
+ 'notify.viewer',
+ 'policy:read',
+ ],
+};
+
+const mockConfig = {
+ authority: {
+ issuer: '/authority',
+ clientId: 'stella-ops-ui',
+ authorizeEndpoint: '/authority/connect/authorize',
+ tokenEndpoint: '/authority/connect/token',
+ logoutEndpoint: '/authority/connect/logout',
+ redirectUri: 'https://127.0.0.1:4400/auth/callback',
+ postLogoutRedirectUri: 'https://127.0.0.1:4400/',
+ scope: 'openid profile email ui.read',
+ audience: '/gateway',
+ dpopAlgorithms: ['ES256'],
+ refreshLeewaySeconds: 60,
+ },
+ apiBaseUrls: {
+ authority: '/authority',
+ scanner: '/scanner',
+ policy: '/policy',
+ concelier: '/concelier',
+ attestor: '/attestor',
+ gateway: '/gateway',
+ },
+ quickstartMode: true,
+ setup: 'complete',
+};
+
+async function fulfillJson(route: Route, body: unknown): Promise {
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify(body),
+ });
+}
+
+async function navigateClientSide(page: Page, target: string): Promise {
+ await page.evaluate((url) => {
+ window.history.pushState({}, '', url);
+ window.dispatchEvent(new PopStateEvent('popstate', { state: window.history.state }));
+ }, target);
+}
+
+async function setupHarness(page: Page): Promise {
+ await page.addInitScript((session) => {
+ (window as { __stellaopsTestSession?: unknown }).__stellaopsTestSession = session;
+ }, adminSession);
+
+ await page.route('**/platform/envsettings.json', (route) => fulfillJson(route, mockConfig));
+ await page.route('**/config.json', (route) => fulfillJson(route, mockConfig));
+ await page.route('**/.well-known/openid-configuration', (route) =>
+ fulfillJson(route, {
+ issuer: 'https://127.0.0.1:4400/authority',
+ authorization_endpoint: 'https://127.0.0.1:4400/authority/connect/authorize',
+ token_endpoint: 'https://127.0.0.1:4400/authority/connect/token',
+ jwks_uri: 'https://127.0.0.1:4400/authority/.well-known/jwks.json',
+ response_types_supported: ['code'],
+ subject_types_supported: ['public'],
+ id_token_signing_alg_values_supported: ['RS256'],
+ })
+ );
+ await page.route('**/authority/.well-known/jwks.json', (route) => fulfillJson(route, { keys: [] }));
+ await page.route('**/console/profile**', (route) =>
+ fulfillJson(route, {
+ subjectId: adminSession.subjectId,
+ username: 'operations-e2e',
+ displayName: 'Operations E2E',
+ tenant: adminSession.tenant,
+ roles: ['admin'],
+ scopes: adminSession.scopes,
+ })
+ );
+ await page.route('**/console/token/introspect**', (route) =>
+ fulfillJson(route, {
+ active: true,
+ tenant: adminSession.tenant,
+ subject: adminSession.subjectId,
+ scopes: adminSession.scopes,
+ })
+ );
+ await page.route('**/api/v2/context/regions', (route) =>
+ fulfillJson(route, [{ regionId: 'eu-west', displayName: 'EU West', sortOrder: 1, enabled: true }])
+ );
+ await page.route('**/api/v2/context/environments**', (route) =>
+ fulfillJson(route, [
+ {
+ environmentId: 'prod',
+ regionId: 'eu-west',
+ environmentType: 'prod',
+ displayName: 'Prod',
+ sortOrder: 1,
+ enabled: true,
+ },
+ ])
+ );
+ await page.route('**/api/v2/context/preferences', (route) =>
+ fulfillJson(route, {
+ tenantId: adminSession.tenant,
+ actorId: adminSession.subjectId,
+ regions: ['eu-west'],
+ environments: ['prod'],
+ timeWindow: '24h',
+ stage: 'all',
+ updatedAt: '2026-03-07T12:00:00Z',
+ updatedBy: adminSession.subjectId,
+ })
+ );
+ await page.route('**/doctor/api/v1/doctor/trends**', (route) => fulfillJson(route, []));
+ await page.route('**/api/v1/approvals**', (route) => fulfillJson(route, []));
+}
+
+test.beforeEach(async ({ page }) => {
+ await setupHarness(page);
+});
+
+test('operations overview routes grouped cards into canonical child pages', async ({ page }) => {
+ await page.goto('/ops/operations', { waitUntil: 'networkidle' });
+
+ await expect(page.getByTestId('operations-overview')).toBeVisible();
+ await expect(page.getByTestId('operations-card-data-integrity')).toBeVisible();
+
+ await page.getByTestId('operations-card-data-integrity').click();
+ await expect(page).toHaveURL(/\/ops\/operations\/data-integrity$/);
+ await expect(page.getByText('Data Trust Score')).toBeVisible();
+});
+
+test('legacy platform ops redirects preserve canonical routes and query state', async ({ page }) => {
+ await page.goto('/ops/operations', {
+ waitUntil: 'networkidle',
+ });
+
+ await navigateClientSide(page, '/platform/ops/feeds-airgap?tab=version-locks');
+ await expect(page).toHaveURL(/\/ops\/operations\/feeds-airgap\?tab=version-locks$/);
+ await expect(page.getByRole('button', { name: 'Version Locks' })).toHaveClass(/active/);
+
+ await navigateClientSide(page, '/platform-ops/data-integrity/feeds-freshness');
+ await expect(page).toHaveURL(/\/ops\/operations\/data-integrity\/feeds-freshness$/);
+ await expect(page.getByRole('heading', { name: 'Feeds Freshness' })).toBeVisible();
+});