diff --git a/docs-archived/implplan/SPRINT_20260308_005_FE_topology_trust_admin_cutover.md b/docs-archived/implplan/SPRINT_20260308_005_FE_topology_trust_admin_cutover.md new file mode 100644 index 000000000..761bdc813 --- /dev/null +++ b/docs-archived/implplan/SPRINT_20260308_005_FE_topology_trust_admin_cutover.md @@ -0,0 +1,108 @@ +# Sprint 20260308_005_FE - Topology And Trust Administration Cutover + +## Topic & Scope +- Complete the `Setup` cutover for `Topology` and `Trust & Signing` so the canonical setup surfaces are fully usable and old settings or admin entry points no longer strand operators on placeholder pages or broken links. +- Replace stale `/platform/setup/*`, `/settings/trust*`, `/administration/trust*`, and `/admin/*` trust or setup links with mounted canonical routes while preserving bookmark compatibility where practical. +- Finish the missing workflow exposure for topology inventory and trust administration so preserved pages are actually reachable from the shell instead of hiding behind weak-route drift. +- Working directory: `src/Web/StellaOps.Web/`. +- Expected evidence: targeted Angular tests, Playwright setup/trust cutover coverage, shipped UI docs, and archived sprint notes. + +## Dependencies & Concurrency +- Depends on the shipped `Platform Ops Consolidation`, `Watchlist`, and `Execution Operations` cutovers already archived in `docs-archived/implplan/`. +- Safe parallelism: backend APIs are out of scope; this sprint is limited to frontend code, frontend docs, and verification assets. + +## Documentation Prerequisites +- `AGENTS.md` +- `docs/modules/ui/AGENTS.md` +- `src/Web/StellaOps.Web/AGENTS.md` +- `docs/modules/ui/README.md` +- `docs/modules/ui/architecture.md` +- `docs/modules/ui/implementation_plan.md` +- `docs/modules/ui/component-preservation-map/RESTORATION_PRIORITIES.md` +- `docs/modules/ui/component-preservation-map/components/weak-route/topology/README.md` +- `docs/modules/ui/component-preservation-map/components/weak-route/trust-admin/README.md` + +## Delivery Tracker + +### FE-TTA-001 - Freeze canonical setup owner and alias contract +Status: DONE +Dependency: none +Owners: Developer / Implementer +Task description: +- Make `Setup > Topology` and `Setup > Trust & Signing` the canonical owners for setup inventory and trust administration workflows. +- Standardize redirect behavior for stale `platform/setup`, `settings/trust`, `administration/trust`, and old `admin/*` trust-related entry points so they land on mounted canonical pages without dropping context. + +Completion criteria: +- [x] Canonical route helpers and alias policy are defined for topology and trust setup pages. +- [x] Stale platform or trust entry points land on mounted canonical pages. +- [x] Active navigation no longer points trust administration at nonexistent `/admin/*` roots. + +### FE-TTA-002 - Complete topology shell exposure and platform setup handoffs +Status: DONE +Dependency: FE-TTA-001 +Owners: Developer / Implementer +Task description: +- Expose the preserved topology pages through the active setup shell so operators can reach regions, environments, promotion graph, workflows, gate profiles, and related detail flows without relying on typed URLs. +- Repair `Platform Setup` quick links and topology drill-ins so they hand off into the canonical `Setup > Topology` subtree instead of stale or broken `platform/setup` routes. + +Completion criteria: +- [x] Topology shell navigation exposes the preserved topology pages that are already mounted. +- [x] Platform Setup handoffs point to canonical setup or topology routes. +- [x] Topology overview and setup entry points use working route-backed drill-ins. + +### FE-TTA-003 - Merge legacy trust settings and issuer entry points into usable trust administration +Status: DONE +Dependency: FE-TTA-001 +Owners: Developer / Implementer +Task description: +- Replace the placeholder `TrustSettingsPageComponent` routes with the real trust-administration shell and merge remaining legacy issuer or trust entry points into that shell. +- Keep watchlist, keys, issuers, certificates, audit, air-gap, incidents, and analytics accessible from the canonical trust workspace while preserving operator context from old bookmarks where possible. + +Completion criteria: +- [x] Live trust routes no longer render the placeholder trust settings page. +- [x] Legacy issuer and trust entry points hand off into the canonical trust workspace. +- [x] Trust navigation and summary state remain usable from both setup and legacy entry paths. + +### FE-TTA-004 - Verify cutover, sync docs, and archive +Status: DONE +Dependency: FE-TTA-002, FE-TTA-003 +Owners: Developer / Implementer, QA +Task description: +- Add focused tests for the setup alias contract and repaired topology or trust workflows, then run targeted Angular and Playwright verification. +- Record the shipped setup cutover in checked-feature docs and archive the sprint only after every delivery task is done. + +Completion criteria: +- [x] Targeted Angular tests cover redirect contracts and repaired topology or trust workflows. +- [x] Playwright verifies at least one end-to-end setup journey across topology and trust handoffs. +- [x] UI docs and checked-feature notes reflect the shipped behavior. +- [x] Sprint moved to `docs-archived/implplan/` only after all tasks are marked DONE. + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-03-08 | Sprint created and moved to DOING for the topology and trust administration cutover. | Codex | +| 2026-03-08 | Repaired canonical setup alias helpers, old admin and settings trust bookmarks, navigation targets, and platform-setup handoffs into Topology and Trust & Signing. | Developer | +| 2026-03-08 | Expanded topology shell exposure and removed live placeholder trust ownership in favor of the mounted trust-admin workspace. | Developer | +| 2026-03-08 | Verified targeted Angular coverage with `npm run test -- --watch=false --include src/tests/platform/platform-setup-routes.spec.ts --include src/tests/topology/topology-routes.spec.ts --include src/tests/topology/topology-shell.component.spec.ts --include src/tests/setup/setup-topology-trust-cutover.spec.ts --include src/tests/trust_admin/trust-scoring-dashboard-ui.behavior.spec.ts`: 20 tests passed across 5 files. | QA | +| 2026-03-08 | Verified browser cutover flow with `npx playwright test --config playwright.config.ts tests/e2e/topology-trust-admin-cutover.spec.ts --workers=1`: 1 scenario passed. | QA | +| 2026-03-08 | Production build passed via `npm run build`; existing bundle budget warnings remain unchanged from the baseline. | QA | +| 2026-03-08 | Synced topology and trust administration docs, checked-feature evidence, and task-board status for archive. | Documentation author | + +## Decisions & Risks +- Risk: the current setup area has two overlapping trust surfaces, and one of them is only a placeholder shell. +- Mitigation: make the trust-admin shell canonical and route old trust settings entry points into it in the same sprint. +- Risk: `Platform Setup` still carries stale quick links and duplicate setup concepts that may drift from the canonical topology shell. +- Mitigation: keep topology ownership in `Setup` and repair old handoffs instead of growing another setup product tree. +- Risk: some old `/admin/*` links may have no top-level route owner anymore. +- Mitigation: either retarget them to canonical setup destinations or add explicit alias redirects so bookmarks still resolve. +- Delivery rule: this sprint is only complete when canonical setup routes are mounted, stale trust and topology entry points are repaired, and the core operator journeys are verified end to end. +- Reference design note: `docs/modules/ui/topology-trust-administration/README.md`. +- Docs synced: + - `docs/modules/ui/topology-trust-administration/README.md` + - `docs/features/checked/web/topology-trust-administration-ui.md` + - `docs/modules/ui/README.md` + - `docs/modules/ui/implementation_plan.md` + - `docs/modules/ui/TASKS.md` + +## Next Checkpoints +- 2026-03-08: archived after implementation, verification, and docs sync completed. diff --git a/docs/features/checked/web/topology-trust-administration-ui.md b/docs/features/checked/web/topology-trust-administration-ui.md new file mode 100644 index 000000000..88926977c --- /dev/null +++ b/docs/features/checked/web/topology-trust-administration-ui.md @@ -0,0 +1,66 @@ +# Topology And Trust Administration UI + +## Module +Web + +## Status +VERIFIED + +## Description +Shipped the canonical `Setup > Topology` and `Setup > Trust & Signing` cutover so operators land on mounted setup shells instead of stale `settings`, `administration`, `admin`, or `platform/setup` routes. The topology shell now exposes the preserved setup pages directly, and the trust workspace replaces the live placeholder settings page. + +## Implementation Details +- **Feature directories**: + - `src/Web/StellaOps.Web/src/app/features/topology/` + - `src/Web/StellaOps.Web/src/app/features/trust-admin/` + - `src/Web.StellaOps.Web/src/app/features/platform/setup/` + - `src/Web.StellaOps.Web/src/app/features/settings/` +- **Primary components**: + - `topology-shell` (`src/Web.StellaOps.Web/src/app/features/topology/topology-shell.component.ts`) + - `trust-admin` (`src/Web.StellaOps.Web/src/app/features/trust-admin/trust-admin.component.ts`) + - `platform-setup-home` (`src/Web.StellaOps.Web/src/app/features/platform/setup/platform-setup-home.component.ts`) +- **Canonical routes**: + - `/setup/topology/overview` + - `/setup/topology/regions` + - `/setup/topology/promotion-graph` + - `/setup/topology/workflows` + - `/setup/topology/gate-profiles` + - `/setup/trust-signing` + - `/setup/trust-signing/issuers` + - `/setup/trust-signing/watchlist` + - `/setup/trust-signing/analytics` +- **Legacy aliases**: + - `/platform/setup/*` + - `/settings/trust*` + - `/administration/trust*` + - `/admin/trust*` + - `/admin/issuers` +- **Secondary entry points**: + - `Ops > Platform Setup` + - `Setup > Topology` + - `Setup > Trust & Signing` + - admin navigation bookmarks for trust and notifications + +## E2E Test Plan +- **Setup**: + - [x] Start the local Angular test server with `npm run serve:test`. + - [x] Use a test session with setup and trust-read scopes. +- **Core verification**: + - [x] Open `/settings/trust` and verify redirect into the canonical trust shell. + - [x] Open the `Trusted Issuers` tab and verify issuer data renders. + - [x] Open `/admin/trust` and verify bookmark compatibility into the same trust shell. + - [x] Open `/ops/platform-setup`, use the setup quick-link handoff, and verify navigation into canonical topology routes. + +## Verification +- Run: + - `npm run test -- --watch=false --include src/tests/platform/platform-setup-routes.spec.ts --include src/tests/topology/topology-routes.spec.ts --include src/tests/topology/topology-shell.component.spec.ts --include src/tests/setup/setup-topology-trust-cutover.spec.ts --include src/tests/trust_admin/trust-scoring-dashboard-ui.behavior.spec.ts` + - `npx playwright test --config playwright.config.ts tests/e2e/topology-trust-admin-cutover.spec.ts --workers=1` + - `npm run build` +- Tier 0 (source): pass +- Tier 1 (build/tests): pass +- Tier 2 (behavior): pass +- Notes: + - Angular targeted tests passed: `5` files, `20` tests. + - Playwright passed: `1` topology/trust cutover scenario. + - Production build passed; existing bundle-budget warnings remain unchanged from the baseline. +- Verified on (UTC): 2026-03-08T08:06:30Z diff --git a/docs/modules/ui/README.md b/docs/modules/ui/README.md index 0fd65356d..2d9d45415 100644 --- a/docs/modules/ui/README.md +++ b/docs/modules/ui/README.md @@ -9,6 +9,8 @@ The Console presents operator dashboards for scans, policies, VEX evidence, runtime posture, and admin workflows. ## Latest updates (2026-03-08) +- Shipped the canonical `Setup > Topology` and `Setup > Trust & Signing` cutover, including repaired legacy trust bookmarks, fixed `Platform Setup` handoffs, and expanded topology shell exposure. +- Added checked-feature verification for topology and trust administration at `../../features/checked/web/topology-trust-administration-ui.md`. - Shipped the execution-operations cutover for canonical JobEngine, Scheduler, Dead-Letter, and companion Scanner Ops workflows under `Ops > Operations`. - Added checked-feature verification for execution operations at `../../features/checked/web/execution-operations-ui.md`. @@ -80,6 +82,7 @@ The Console presents operator dashboards for scans, policies, VEX evidence, runt - ./offline-operations/README.md - ./quota-health-aoc-operations/README.md - ./execution-operations/README.md +- ./topology-trust-administration/README.md - ./triage-explainability-workspace/README.md - ./workflow-visualization-replay/README.md - ./contextual-actions-patterns/README.md diff --git a/docs/modules/ui/TASKS.md b/docs/modules/ui/TASKS.md index db6c517b4..316079918 100644 --- a/docs/modules/ui/TASKS.md +++ b/docs/modules/ui/TASKS.md @@ -100,6 +100,10 @@ - [DONE] FE-EXO-002 Complete JobEngine and scheduler operator workflows - [DONE] FE-EXO-003 Complete dead-letter and scanner-ops supporting workflows - [DONE] FE-EXO-004 Verify cutover, sync docs, and archive +- [DONE] FE-TTA-001 Freeze canonical setup owner and alias contract +- [DONE] FE-TTA-002 Complete topology shell exposure and platform setup handoffs +- [DONE] FE-TTA-003 Merge legacy trust settings and issuer entry points into usable trust administration +- [DONE] FE-TTA-004 Verify cutover, sync docs, and archive - [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 diff --git a/docs/modules/ui/implementation_plan.md b/docs/modules/ui/implementation_plan.md index fcd92b42e..49afc7084 100644 --- a/docs/modules/ui/implementation_plan.md +++ b/docs/modules/ui/implementation_plan.md @@ -31,11 +31,13 @@ Provide a living plan for UI deliverables, dependencies, and evidence. - `docs/features/checked/web/offline-operations-ui.md` - shipped verification note for the canonical Offline Kit and Feeds & Airgap owner routes, repaired stale aliases, and completed offline shell actions. - `docs/features/checked/web/quota-health-aoc-operations-ui.md` - shipped verification note for canonical quota, health, and AOC owner routes, repaired deep links, route-backed filters, and completed operator actions. - `docs/features/checked/web/execution-operations-ui.md` - shipped verification note for canonical execution routes, repaired jobengine and scheduler aliases, completed dead-letter actions, and usable scanner-support workflows. +- `docs/features/checked/web/topology-trust-administration-ui.md` - shipped verification note for canonical topology and trust setup shells, repaired settings/admin/platform aliases, and platform-setup handoffs. - `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/offline-operations/README.md` - detailed owner-shell contract for Offline Kit, Feeds & Airgap, Evidence handoffs, and stale alias policy. - `docs/modules/ui/quota-health-aoc-operations/README.md` - canonical owner-shell contract for quota, health, and AOC operations cutover plus alias and action rules. - `docs/modules/ui/execution-operations/README.md` - canonical execution owner-shell contract for JobEngine, Scheduler, Dead-Letter, and companion Scanner Ops workflows. +- `docs/modules/ui/topology-trust-administration/README.md` - canonical setup owner contract for topology inventory, trust administration, legacy trust redirects, and platform-setup handoffs. - `docs/modules/ui/triage-explainability-workspace/README.md` - detailed artifact workspace and audit-bundle UX dossier. - `docs/modules/ui/workflow-visualization-replay/README.md` - detailed run-detail graph, timeline, replay, and evidence UX dossier. - `docs/modules/ui/contextual-actions-patterns/README.md` - shared placement contract for stray actions, pages, drawers, and tabs. diff --git a/docs/modules/ui/topology-trust-administration/README.md b/docs/modules/ui/topology-trust-administration/README.md new file mode 100644 index 000000000..149369507 --- /dev/null +++ b/docs/modules/ui/topology-trust-administration/README.md @@ -0,0 +1,85 @@ +# Topology And Trust Administration + +## Purpose +- Make `Setup > Topology` and `Setup > Trust & Signing` the canonical owners for environment inventory and trust administration. +- Keep legacy `settings`, `administration`, `admin`, and `platform/setup` entry points usable without preserving the old split-product shells. + +## Canonical Owner +- Owner shells: + - `Setup > Topology` + - `Setup > Trust & Signing` +- Primary routes: + - `/setup/topology/overview` + - `/setup/topology/map` + - `/setup/topology/regions` + - `/setup/topology/targets` + - `/setup/topology/hosts` + - `/setup/topology/agents` + - `/setup/topology/promotion-graph` + - `/setup/topology/workflows` + - `/setup/topology/gate-profiles` + - `/setup/topology/connectivity` + - `/setup/topology/runtime-drift` + - `/setup/trust-signing` + - `/setup/trust-signing/keys` + - `/setup/trust-signing/issuers` + - `/setup/trust-signing/certificates` + - `/setup/trust-signing/watchlist` + - `/setup/trust-signing/watchlist/entries` + - `/setup/trust-signing/watchlist/alerts` + - `/setup/trust-signing/watchlist/tuning` + - `/setup/trust-signing/audit` + - `/setup/trust-signing/airgap` + - `/setup/trust-signing/incidents` + - `/setup/trust-signing/analytics` +- Secondary handoff route: + - `/ops/platform-setup` + +## Legacy Alias Policy +- Preserve stale bookmarks and old links by redirecting: + - `/platform/setup` + - `/platform/setup/regions-environments` + - `/platform/setup/promotion-paths` + - `/platform/setup/workflows-gates` + - `/platform/setup/gate-profiles` + - `/platform/setup/trust-signing` + - `/platform/setup/trust-signing/:page` + - `/settings/trust` + - `/settings/trust/issuers` + - `/settings/trust/:page` + - `/administration/trust` + - `/administration/trust/issuers` + - `/administration/trust/:page` + - `/admin/trust` + - `/admin/trust/:page` + - `/admin/issuers` +- Redirects must preserve query params and fragments so tenant, region, environment, and tab context survive the handoff. + +## UX Rules +- `Platform Setup` is a setup overview and handoff page, not the owner of topology or trust subtrees. +- `Topology` owns region, environment, target, agent, promotion, workflow, gate-profile, connectivity, and runtime-drift navigation. +- `Trust & Signing` owns keys, issuers, certificates, watchlist, audit, air-gap trust posture, incidents, and analytics. +- Legacy settings or admin trust URLs should land directly on the live trust shell instead of placeholder pages. + +## Preserved Value +- Keep: + - topology inventory and graph drill-ins + - promotion, workflow, and gate-profile setup + - trust summary, issuer management, certificate inventory, and watchlist + - trust audit, incident, analytics, and air-gap administration +- Why: + - these are core release-setup capabilities, not experimental side branches + - the product issue was weak wiring and stale route ownership, not missing product value + +## Shipped In This Cut +- Canonical setup alias helpers for trust and platform-setup handoffs. +- Top-level `/admin/*` compatibility redirects for trust and notification bookmarks. +- Expanded `Topology` shell tabs so preserved mounted pages are reachable from the live setup shell. +- Fixed `Platform Setup` quick links so they hand off into canonical `Setup` routes. +- Retired live trust-placeholder ownership in favor of the real `Trust Management` shell. + +## Related Docs +- `docs/features/checked/web/topology-trust-administration-ui.md` +- `docs/modules/ui/watchlist-operations/README.md` +- `docs/modules/ui/platform-ops-consolidation/README.md` +- `docs/modules/ui/component-preservation-map/RESTORATION_PRIORITIES.md` diff --git a/src/Web/StellaOps.Web/src/app/app.routes.ts b/src/Web/StellaOps.Web/src/app/app.routes.ts index bbad2ab1f..4f2b7f014 100644 --- a/src/Web/StellaOps.Web/src/app/app.routes.ts +++ b/src/Web/StellaOps.Web/src/app/app.routes.ts @@ -1,4 +1,5 @@ -import { Routes } from '@angular/router'; +import { inject } from '@angular/core'; +import { Router, Routes } from '@angular/router'; import { requireAnyScopeGuard, @@ -78,6 +79,30 @@ const requireSetupGuard = requireAnyScopeGuard( '/console/profile', ); +function preserveAppRedirect(template: string) { + return ({ + params, + queryParams, + fragment, + }: { + params: Record; + queryParams: Record; + fragment?: string | null; + }) => { + const router = inject(Router); + let targetPath = template; + + for (const [name, value] of Object.entries(params ?? {})) { + targetPath = targetPath.replaceAll(`:${name}`, encodeURIComponent(value)); + } + + const target = router.parseUrl(targetPath); + target.queryParams = { ...queryParams }; + target.fragment = fragment ?? null; + return target; + }; +} + export const routes: Routes = [ { path: '', @@ -170,6 +195,29 @@ export const routes: Routes = [ data: { breadcrumb: 'Console Admin' }, loadChildren: () => import('./features/console-admin/console-admin.routes').then((m) => m.consoleAdminRoutes), }, + { + path: 'admin', + children: [ + { path: '', redirectTo: '/administration', pathMatch: 'full' }, + { path: 'notifications', redirectTo: preserveAppRedirect('/setup/notifications'), pathMatch: 'full' }, + { path: 'notifications/:page', redirectTo: preserveAppRedirect('/setup/notifications/:page'), pathMatch: 'full' }, + { path: 'trust', redirectTo: preserveAppRedirect('/setup/trust-signing'), pathMatch: 'full' }, + { path: 'trust/:page', redirectTo: preserveAppRedirect('/setup/trust-signing/:page'), pathMatch: 'full' }, + { + path: 'trust/:page/:child', + redirectTo: preserveAppRedirect('/setup/trust-signing/:page/:child'), + pathMatch: 'full', + }, + { path: 'issuers', redirectTo: preserveAppRedirect('/setup/trust-signing/issuers'), pathMatch: 'full' }, + { + path: 'issuers/:page', + redirectTo: preserveAppRedirect('/setup/trust-signing/issuers'), + pathMatch: 'full', + }, + { path: 'registries', redirectTo: preserveAppRedirect('/setup/integrations'), pathMatch: 'full' }, + { path: '**', redirectTo: '/administration' }, + ], + }, { path: 'platform-ops', loadChildren: () => import('./routes/platform-ops.routes').then((m) => m.PLATFORM_OPS_ROUTES), @@ -192,8 +240,43 @@ export const routes: Routes = [ 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: 'setup', redirectTo: '/ops/platform-setup', pathMatch: 'full' }, + { + path: 'setup/regions-environments', + redirectTo: preserveAppRedirect('/setup/topology/regions'), + pathMatch: 'full', + }, + { + path: 'setup/promotion-paths', + redirectTo: preserveAppRedirect('/setup/topology/promotion-graph'), + pathMatch: 'full', + }, + { + path: 'setup/workflows-gates', + redirectTo: preserveAppRedirect('/setup/topology/workflows'), + pathMatch: 'full', + }, + { + path: 'setup/gate-profiles', + redirectTo: preserveAppRedirect('/setup/topology/gate-profiles'), + pathMatch: 'full', + }, + { + path: 'setup/trust-signing', + redirectTo: preserveAppRedirect('/setup/trust-signing'), + pathMatch: 'full', + }, + { + path: 'setup/trust-signing/:page', + redirectTo: preserveAppRedirect('/setup/trust-signing/:page'), + pathMatch: 'full', + }, + { + path: 'setup/trust-signing/:page/:child', + redirectTo: preserveAppRedirect('/setup/trust-signing/:page/:child'), + pathMatch: 'full', + }, + { path: 'setup/:rest', redirectTo: preserveAppRedirect('/ops/platform-setup/:rest'), pathMatch: 'full' }, { path: '**', redirectTo: '/ops' }, ], }, diff --git a/src/Web/StellaOps.Web/src/app/core/navigation/navigation.config.ts b/src/Web/StellaOps.Web/src/app/core/navigation/navigation.config.ts index 776ad8c5a..f3f08d0dd 100644 --- a/src/Web/StellaOps.Web/src/app/core/navigation/navigation.config.ts +++ b/src/Web/StellaOps.Web/src/app/core/navigation/navigation.config.ts @@ -588,14 +588,14 @@ export const NAVIGATION_GROUPS: NavGroup[] = [ { id: 'admin-notifications', label: 'Notification Admin', - route: '/admin/notifications', + route: '/setup/notifications', icon: 'bell-config', tooltip: 'Configure notification rules, channels, and templates', }, { id: 'admin-trust', label: 'Trust Management', - route: '/admin/trust', + route: '/setup/trust-signing', icon: 'certificate', tooltip: 'Manage signing keys, issuers, and certificates', }, @@ -623,7 +623,7 @@ export const NAVIGATION_GROUPS: NavGroup[] = [ { id: 'issuer-trust', label: 'Issuer Directory', - route: '/admin/issuers', + route: '/setup/trust-signing/issuers', icon: 'shield-check', tooltip: 'Manage issuer trust and key lifecycle', }, diff --git a/src/Web/StellaOps.Web/src/app/features/platform/setup/platform-setup-home.component.ts b/src/Web/StellaOps.Web/src/app/features/platform/setup/platform-setup-home.component.ts index 5201d844a..3508a6912 100644 --- a/src/Web/StellaOps.Web/src/app/features/platform/setup/platform-setup-home.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/platform/setup/platform-setup-home.component.ts @@ -50,7 +50,7 @@ interface TopoLink extends d3.SimulationLinkDatum {

Platform Setup

-

Configure inventory, promotion, workflow, policy. Explore the topology graph below.

+

Configure canonical setup inventory, promotion, workflow, policy, and trust handoffs. Explore the topology graph below.

@if (error()) { @@ -370,13 +370,14 @@ export class PlatformSetupHomeComponent implements AfterViewInit, OnDestroy { private allLinks: TopoLink[] = []; readonly quickLinks = [ - { title: 'Regions & Environments', description: 'Region-first setup and risk tiers.', route: '/platform/setup/regions-environments' }, - { title: 'Promotion Paths', description: 'Promotion flow graph and rules.', route: '/platform/setup/promotion-paths' }, - { title: 'Workflows & Gates', description: 'Workflow and gate profile mapping.', route: '/platform/setup/workflows-gates' }, - { title: 'Gate Profiles', description: 'Strict, risk-aware, and expedited lanes.', route: '/platform/setup/gate-profiles' }, - { title: 'Release Templates', description: 'Release template and evidence defaults.', route: '/platform/setup/release-templates' }, - { title: 'Feed Policy', description: 'Freshness thresholds and staleness.', route: '/platform/setup/feed-policy' }, - { title: 'Defaults & Guardrails', description: 'Policy impact labels and degraded-mode.', route: '/platform/setup/defaults-guardrails' }, + { title: 'Regions & Environments', description: 'Region-first setup and risk tiers.', route: '/setup/topology/regions' }, + { title: 'Promotion Paths', description: 'Promotion flow graph and rules.', route: '/setup/topology/promotion-graph' }, + { title: 'Workflows & Gates', description: 'Workflow inventory and gate bindings.', route: '/setup/topology/workflows' }, + { title: 'Gate Profiles', description: 'Strict, risk-aware, and expedited lanes.', route: '/setup/topology/gate-profiles' }, + { title: 'Release Templates', description: 'Release template and evidence defaults.', route: '/ops/platform-setup/release-templates' }, + { title: 'Policy Bindings', description: 'Freshness thresholds and feed-policy bindings.', route: '/ops/platform-setup/policy-bindings' }, + { title: 'Defaults & Guardrails', description: 'Policy impact labels and degraded-mode.', route: '/ops/platform-setup/defaults-guardrails' }, + { title: 'Trust & Signing', description: 'Keys, issuers, watchlist, and trust audit workflows.', route: '/setup/trust-signing' }, ]; private readonly nodeColors: Record = { diff --git a/src/Web/StellaOps.Web/src/app/features/platform/setup/platform-setup.routes.ts b/src/Web/StellaOps.Web/src/app/features/platform/setup/platform-setup.routes.ts index 8a56cb112..fa185cdc1 100644 --- a/src/Web/StellaOps.Web/src/app/features/platform/setup/platform-setup.routes.ts +++ b/src/Web/StellaOps.Web/src/app/features/platform/setup/platform-setup.routes.ts @@ -12,28 +12,22 @@ export const PLATFORM_SETUP_ROUTES: Routes = [ path: 'regions-environments', title: 'Setup Regions & Environments', data: { breadcrumb: 'Regions & Environments' }, - loadComponent: () => - import('./platform-setup-regions-environments-page.component').then( - (m) => m.PlatformSetupRegionsEnvironmentsPageComponent, - ), + redirectTo: '/setup/topology/regions', + pathMatch: 'full', }, { path: 'promotion-paths', title: 'Setup Promotion Paths', data: { breadcrumb: 'Promotion Paths' }, - loadComponent: () => - import('./platform-setup-promotion-paths-page.component').then( - (m) => m.PlatformSetupPromotionPathsPageComponent, - ), + redirectTo: '/setup/topology/promotion-graph', + pathMatch: 'full', }, { path: 'workflows-gates', title: 'Setup Workflows & Gates', data: { breadcrumb: 'Workflows & Gates' }, - loadComponent: () => - import('./platform-setup-workflows-gates-page.component').then( - (m) => m.PlatformSetupWorkflowsGatesPageComponent, - ), + redirectTo: '/setup/topology/workflows', + pathMatch: 'full', }, { path: 'release-templates', @@ -57,10 +51,8 @@ export const PLATFORM_SETUP_ROUTES: Routes = [ path: 'gate-profiles', title: 'Gate Profiles', data: { breadcrumb: 'Gate Profiles' }, - loadComponent: () => - import('./platform-setup-gate-profiles-page.component').then( - (m) => m.PlatformSetupGateProfilesPageComponent, - ), + redirectTo: '/setup/topology/gate-profiles', + pathMatch: 'full', }, { path: 'defaults-guardrails', @@ -75,9 +67,7 @@ export const PLATFORM_SETUP_ROUTES: Routes = [ path: 'trust-signing', title: 'Trust & Signing', data: { breadcrumb: 'Trust & Signing' }, - loadComponent: () => - import('../../settings/trust/trust-settings-page.component').then( - (m) => m.TrustSettingsPageComponent, - ), + redirectTo: '/setup/trust-signing', + pathMatch: 'full', }, ]; diff --git a/src/Web/StellaOps.Web/src/app/features/settings/settings.routes.ts b/src/Web/StellaOps.Web/src/app/features/settings/settings.routes.ts index d4be8c5d3..2da39d0a5 100644 --- a/src/Web/StellaOps.Web/src/app/features/settings/settings.routes.ts +++ b/src/Web/StellaOps.Web/src/app/features/settings/settings.routes.ts @@ -3,7 +3,32 @@ * Sprint: SPRINT_20260118_002_FE_settings_consolidation */ -import { Routes } from '@angular/router'; +import { inject } from '@angular/core'; +import { Router, Routes } from '@angular/router'; + +function redirectToCanonicalSetup(path: string) { + return ({ + params, + queryParams, + fragment, + }: { + params: Record; + queryParams: Record; + fragment?: string | null; + }) => { + const router = inject(Router); + let targetPath = path; + + for (const [name, value] of Object.entries(params ?? {})) { + targetPath = targetPath.replaceAll(`:${name}`, encodeURIComponent(value)); + } + + const target = router.parseUrl(targetPath); + target.queryParams = { ...queryParams }; + target.fragment = fragment ?? null; + return target; + }; +} export const SETTINGS_ROUTES: Routes = [ { @@ -51,16 +76,45 @@ export const SETTINGS_ROUTES: Routes = [ { path: 'trust', title: 'Trust & Signing', - loadComponent: () => - import('./trust/trust-settings-page.component').then(m => m.TrustSettingsPageComponent), - data: { breadcrumb: 'Trust & Signing' }, + redirectTo: redirectToCanonicalSetup('/setup/trust-signing'), + pathMatch: 'full' as const, + }, + { + path: 'trust/issuers', + title: 'Trust & Signing', + redirectTo: redirectToCanonicalSetup('/setup/trust-signing/issuers'), + pathMatch: 'full' as const, + }, + { + path: 'trust/:page/:child', + title: 'Trust & Signing', + redirectTo: redirectToCanonicalSetup('/setup/trust-signing/:page/:child'), + pathMatch: 'full' as const, }, { path: 'trust/:page', title: 'Trust & Signing', - loadComponent: () => - import('./trust/trust-settings-page.component').then(m => m.TrustSettingsPageComponent), + redirectTo: redirectToCanonicalSetup('/setup/trust-signing/:page'), + pathMatch: 'full' as const, + }, + { + path: 'trust-signing', + title: 'Trust & Signing', + redirectTo: redirectToCanonicalSetup('/setup/trust-signing'), + pathMatch: 'full' as const, + }, + { + path: 'trust-signing/:page', + title: 'Trust & Signing', + redirectTo: redirectToCanonicalSetup('/setup/trust-signing/:page'), + pathMatch: 'full' as const, + }, + { + path: 'trust-signing/:page/:child', + title: 'Trust & Signing', + redirectTo: redirectToCanonicalSetup('/setup/trust-signing/:page/:child'), data: { breadcrumb: 'Trust & Signing' }, + pathMatch: 'full' as const, }, { path: 'security-data', diff --git a/src/Web/StellaOps.Web/src/app/features/topology/topology-shell.component.ts b/src/Web/StellaOps.Web/src/app/features/topology/topology-shell.component.ts index 185cdf48c..bd6477e16 100644 --- a/src/Web/StellaOps.Web/src/app/features/topology/topology-shell.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/topology/topology-shell.component.ts @@ -52,9 +52,13 @@ export class TopologyShellComponent { readonly tabs: TabItem[] = [ { id: 'overview', label: 'Overview', route: 'overview' }, { id: 'map', label: 'Map', route: 'map' }, + { id: 'regions', label: 'Regions & Environments', route: 'regions' }, { id: 'targets', label: 'Targets', route: 'targets' }, { id: 'hosts', label: 'Hosts', route: 'hosts' }, { id: 'agents', label: 'Agents', route: 'agents' }, + { id: 'promotion', label: 'Promotion Graph', route: 'promotion-graph' }, + { id: 'workflows', label: 'Workflows', route: 'workflows' }, + { id: 'gate-profiles', label: 'Gate Profiles', route: 'gate-profiles' }, { id: 'connectivity', label: 'Connectivity', route: 'connectivity' }, { id: 'drift', label: 'Runtime Drift', route: 'runtime-drift' }, ]; diff --git a/src/Web/StellaOps.Web/src/app/routes/administration.routes.ts b/src/Web/StellaOps.Web/src/app/routes/administration.routes.ts index 04816ae67..8b41fc818 100644 --- a/src/Web/StellaOps.Web/src/app/routes/administration.routes.ts +++ b/src/Web/StellaOps.Web/src/app/routes/administration.routes.ts @@ -68,6 +68,30 @@ function redirectToEvidence(path: string) { }; } +function redirectToSetup(path: string) { + return ({ + params, + queryParams, + fragment, + }: { + params: Record; + queryParams: Record; + fragment?: string | null; + }) => { + const router = inject(Router); + let targetPath = path; + + for (const [name, value] of Object.entries(params ?? {})) { + targetPath = targetPath.replaceAll(`:${name}`, encodeURIComponent(value)); + } + + const target = router.parseUrl(targetPath); + target.queryParams = { ...queryParams }; + target.fragment = fragment ?? null; + return target; + }; +} + export const ADMINISTRATION_ROUTES: Routes = [ // A0 — Administration overview { @@ -294,38 +318,37 @@ export const ADMINISTRATION_ROUTES: Routes = [ path: 'trust-signing', title: 'Trust & Signing', data: { breadcrumb: 'Trust & Signing' }, - loadChildren: () => - import('../features/trust-admin/trust-admin.routes').then( - (m) => m.trustAdminRoutes - ), + redirectTo: redirectToSetup('/setup/trust-signing'), + pathMatch: 'full', }, // Legacy trust sub-paths (formerly /admin/trust/*) { path: 'trust', title: 'Trust & Signing', data: { breadcrumb: 'Trust & Signing' }, - loadComponent: () => - import('../features/settings/trust/trust-settings-page.component').then( - (m) => m.TrustSettingsPageComponent - ), - }, - { - path: 'trust/:page', - title: 'Trust & Signing', - data: { breadcrumb: 'Trust & Signing' }, - loadComponent: () => - import('../features/settings/trust/trust-settings-page.component').then( - (m) => m.TrustSettingsPageComponent - ), + redirectTo: redirectToSetup('/setup/trust-signing'), + pathMatch: 'full', }, { path: 'trust/issuers', title: 'Issuers', data: { breadcrumb: 'Issuers' }, - loadChildren: () => - import('../features/issuer-trust/issuer-trust.routes').then( - (m) => m.issuerTrustRoutes - ), + redirectTo: redirectToSetup('/setup/trust-signing/issuers'), + pathMatch: 'full', + }, + { + path: 'trust/:page/:child', + title: 'Trust & Signing', + data: { breadcrumb: 'Trust & Signing' }, + redirectTo: redirectToSetup('/setup/trust-signing/:page/:child'), + pathMatch: 'full', + }, + { + path: 'trust/:page', + title: 'Trust & Signing', + data: { breadcrumb: 'Trust & Signing' }, + redirectTo: redirectToSetup('/setup/trust-signing/:page'), + pathMatch: 'full', }, // Legacy alias: /administration/identity-providers → /settings/identity-providers diff --git a/src/Web/StellaOps.Web/src/app/routes/topology.routes.ts b/src/Web/StellaOps.Web/src/app/routes/topology.routes.ts index d51d0d336..5f84949ce 100644 --- a/src/Web/StellaOps.Web/src/app/routes/topology.routes.ts +++ b/src/Web/StellaOps.Web/src/app/routes/topology.routes.ts @@ -47,6 +47,11 @@ export const TOPOLOGY_ROUTES: Routes = [ (m) => m.TopologyRegionsEnvironmentsPageComponent, ), }, + { + path: 'regions-environments', + redirectTo: 'regions', + pathMatch: 'full', + }, { path: 'environments', title: 'Environments', @@ -180,6 +185,11 @@ export const TOPOLOGY_ROUTES: Routes = [ (m) => m.TopologyPromotionPathsPageComponent, ), }, + { + path: 'promotion-paths', + redirectTo: 'promotion-graph', + pathMatch: 'full', + }, { path: 'workflows', title: 'Workflows', @@ -194,6 +204,11 @@ export const TOPOLOGY_ROUTES: Routes = [ (m) => m.TopologyInventoryPageComponent, ), }, + { + path: 'workflows-gates', + redirectTo: 'workflows', + pathMatch: 'full', + }, { path: 'gate-profiles', title: 'Gate Profiles', diff --git a/src/Web/StellaOps.Web/src/tests/platform/platform-setup-routes.spec.ts b/src/Web/StellaOps.Web/src/tests/platform/platform-setup-routes.spec.ts index 6c4c5beb2..68bd1f5c0 100644 --- a/src/Web/StellaOps.Web/src/tests/platform/platform-setup-routes.spec.ts +++ b/src/Web/StellaOps.Web/src/tests/platform/platform-setup-routes.spec.ts @@ -1,23 +1,40 @@ import { PLATFORM_SETUP_ROUTES } from '../../app/features/platform/setup/platform-setup.routes'; describe('PLATFORM_SETUP_ROUTES (pre-alpha)', () => { - it('uses policy-bindings as canonical policy setup page', () => { + it('keeps policy-bindings as canonical policy setup page', () => { const route = PLATFORM_SETUP_ROUTES.find((item) => item.path === 'policy-bindings'); expect(route).toBeDefined(); expect(route?.loadComponent).toBeDefined(); }); - it('includes gate profiles and defaults guardrails pages', () => { - const gateProfiles = PLATFORM_SETUP_ROUTES.find((item) => item.path === 'gate-profiles'); + it('keeps release templates and defaults guardrails as mounted pages', () => { + const releaseTemplates = PLATFORM_SETUP_ROUTES.find((item) => item.path === 'release-templates'); const defaults = PLATFORM_SETUP_ROUTES.find((item) => item.path === 'defaults-guardrails'); - expect(gateProfiles?.loadComponent).toBeDefined(); + expect(releaseTemplates?.loadComponent).toBeDefined(); expect(defaults?.loadComponent).toBeDefined(); }); - it('contains no redirect aliases', () => { - for (const route of PLATFORM_SETUP_ROUTES) { - expect(route.redirectTo).toBeUndefined(); + it('redirects absorbed topology and trust pages into canonical setup owners', () => { + const regions = PLATFORM_SETUP_ROUTES.find((item) => item.path === 'regions-environments'); + const promotion = PLATFORM_SETUP_ROUTES.find((item) => item.path === 'promotion-paths'); + const workflows = PLATFORM_SETUP_ROUTES.find((item) => item.path === 'workflows-gates'); + const gateProfiles = PLATFORM_SETUP_ROUTES.find((item) => item.path === 'gate-profiles'); + const trustSigning = PLATFORM_SETUP_ROUTES.find((item) => item.path === 'trust-signing'); + + expect(regions?.redirectTo).toBe('/setup/topology/regions'); + expect(promotion?.redirectTo).toBe('/setup/topology/promotion-graph'); + expect(workflows?.redirectTo).toBe('/setup/topology/workflows'); + expect(gateProfiles?.redirectTo).toBe('/setup/topology/gate-profiles'); + expect(trustSigning?.redirectTo).toBe('/setup/trust-signing'); + }); + + it('retains mounted routes for the remaining platform setup surfaces', () => { + const mountedPaths = ['policy-bindings', 'release-templates', 'defaults-guardrails']; + + for (const path of mountedPaths) { + const route = PLATFORM_SETUP_ROUTES.find((item) => item.path === path); + expect(route?.loadComponent).toBeDefined(); } }); }); diff --git a/src/Web/StellaOps.Web/src/tests/setup/setup-topology-trust-cutover.spec.ts b/src/Web/StellaOps.Web/src/tests/setup/setup-topology-trust-cutover.spec.ts new file mode 100644 index 000000000..5eaa12043 --- /dev/null +++ b/src/Web/StellaOps.Web/src/tests/setup/setup-topology-trust-cutover.spec.ts @@ -0,0 +1,143 @@ +import { TestBed } from '@angular/core/testing'; +import { provideRouter, Route, Router } from '@angular/router'; + +import { NAVIGATION_GROUPS } from '../../app/core/navigation/navigation.config'; +import { SETTINGS_ROUTES } from '../../app/features/settings/settings.routes'; +import { routes } from '../../app/app.routes'; +import { ADMINISTRATION_ROUTES } from '../../app/routes/administration.routes'; + +function resolveRedirect(route: Route | undefined, params: Record = {}): string | undefined { + const redirect = route?.redirectTo; + if (typeof redirect === 'string') { + return redirect; + } + + if (typeof redirect !== 'function') { + return undefined; + } + + return TestBed.runInInjectionContext(() => { + const router = TestBed.inject(Router); + const target = redirect({ + params, + queryParams: {}, + fragment: null, + } as never) as unknown; + + return typeof target === 'string' ? target : router.serializeUrl(target as never); + }); +} + +describe('setup topology trust cutover contract', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideRouter([])], + }); + }); + + it('redirects legacy settings trust routes to canonical setup trust-signing pages', () => { + const root = SETTINGS_ROUTES[0]; + const children = root.children ?? []; + + expect(resolveRedirect(children.find((route) => route.path === 'trust'))).toBe('/setup/trust-signing'); + expect(resolveRedirect(children.find((route) => route.path === 'trust/issuers'))).toBe( + '/setup/trust-signing/issuers', + ); + expect(resolveRedirect(children.find((route) => route.path === 'trust/:page'), { page: 'analytics' })).toBe( + '/setup/trust-signing/analytics', + ); + expect( + resolveRedirect(children.find((route) => route.path === 'trust/:page/:child'), { + page: 'watchlist', + child: 'alerts', + }), + ).toBe('/setup/trust-signing/watchlist/alerts'); + expect(resolveRedirect(children.find((route) => route.path === 'trust-signing'))).toBe('/setup/trust-signing'); + }); + + it('redirects administration trust routes into canonical setup trust-signing pages', () => { + expect(resolveRedirect(ADMINISTRATION_ROUTES.find((route) => route.path === 'trust-signing'))).toBe( + '/setup/trust-signing', + ); + expect(resolveRedirect(ADMINISTRATION_ROUTES.find((route) => route.path === 'trust'))).toBe('/setup/trust-signing'); + expect(resolveRedirect(ADMINISTRATION_ROUTES.find((route) => route.path === 'trust/issuers'))).toBe( + '/setup/trust-signing/issuers', + ); + expect( + resolveRedirect(ADMINISTRATION_ROUTES.find((route) => route.path === 'trust/:page'), { page: 'analytics' }), + ).toBe('/setup/trust-signing/analytics'); + expect( + resolveRedirect(ADMINISTRATION_ROUTES.find((route) => route.path === 'trust/:page/:child'), { + page: 'watchlist', + child: 'tuning', + }), + ).toBe('/setup/trust-signing/watchlist/tuning'); + }); + + it('provides top-level admin aliases for notification and trust bookmarks', () => { + const admin = routes.find((route) => route.path === 'admin'); + expect(admin).toBeDefined(); + + const childPaths = (admin?.children ?? []).map((route) => route.path); + expect(childPaths).toEqual([ + '', + 'notifications', + 'notifications/:page', + 'trust', + 'trust/:page', + 'trust/:page/:child', + 'issuers', + 'issuers/:page', + 'registries', + '**', + ]); + }); + + it('retargets active admin navigation links to mounted setup destinations', () => { + const adminGroup = NAVIGATION_GROUPS.find((group) => group.id === 'admin'); + expect(adminGroup).toBeDefined(); + + const itemById = new Map((adminGroup?.items ?? []).map((item) => [item.id, item.route])); + + expect(itemById.get('admin-notifications')).toBe('/setup/notifications'); + expect(itemById.get('admin-trust')).toBe('/setup/trust-signing'); + expect(itemById.get('issuer-trust')).toBe('/setup/trust-signing/issuers'); + }); + + it('preserves top-level platform setup aliases for topology and trust destinations', () => { + const platform = routes.find((route) => route.path === 'platform'); + expect(platform).toBeDefined(); + + const children = platform?.children ?? []; + + expect(resolveRedirect(children.find((route) => route.path === 'setup/regions-environments'))).toBe( + '/setup/topology/regions', + ); + expect(resolveRedirect(children.find((route) => route.path === 'setup/promotion-paths'))).toBe( + '/setup/topology/promotion-graph', + ); + expect(resolveRedirect(children.find((route) => route.path === 'setup/workflows-gates'))).toBe( + '/setup/topology/workflows', + ); + expect(resolveRedirect(children.find((route) => route.path === 'setup/gate-profiles'))).toBe( + '/setup/topology/gate-profiles', + ); + expect(resolveRedirect(children.find((route) => route.path === 'setup/trust-signing'))).toBe('/setup/trust-signing'); + expect( + resolveRedirect(children.find((route) => route.path === 'setup/trust-signing/:page'), { page: 'issuers' }), + ).toBe( + '/setup/trust-signing/issuers', + ); + expect( + resolveRedirect(children.find((route) => route.path === 'setup/trust-signing/:page/:child'), { + page: 'watchlist', + child: 'alerts', + }), + ).toBe( + '/setup/trust-signing/watchlist/alerts', + ); + expect(resolveRedirect(children.find((route) => route.path === 'setup/:rest'), { rest: 'policy-bindings' })).toBe( + '/ops/platform-setup/policy-bindings', + ); + }); +}); diff --git a/src/Web/StellaOps.Web/src/tests/topology/topology-routes.spec.ts b/src/Web/StellaOps.Web/src/tests/topology/topology-routes.spec.ts index d39e7b249..91cbb59c1 100644 --- a/src/Web/StellaOps.Web/src/tests/topology/topology-routes.spec.ts +++ b/src/Web/StellaOps.Web/src/tests/topology/topology-routes.spec.ts @@ -1,8 +1,11 @@ import { TOPOLOGY_ROUTES } from '../../app/routes/topology.routes'; describe('TOPOLOGY_ROUTES dedicated pages', () => { + const topologyRoot = TOPOLOGY_ROUTES.find((item) => item.path === ''); + const childRoutes = topologyRoot?.children ?? []; + async function loadComponentName(path: string): Promise { - const route = TOPOLOGY_ROUTES.find((item) => item.path === path); + const route = childRoutes.find((item) => item.path === path); expect(route).toBeDefined(); expect(route?.loadComponent).toBeDefined(); const component = await route!.loadComponent!(); @@ -32,7 +35,12 @@ describe('TOPOLOGY_ROUTES dedicated pages', () => { }); it('keeps promotion-paths as an alias redirect', () => { - const alias = TOPOLOGY_ROUTES.find((item) => item.path === 'promotion-paths'); + const alias = childRoutes.find((item) => item.path === 'promotion-paths'); expect(alias?.redirectTo).toBe('promotion-graph'); }); + + it('keeps regions-environments and workflows-gates as canonical aliases', () => { + expect(childRoutes.find((item) => item.path === 'regions-environments')?.redirectTo).toBe('regions'); + expect(childRoutes.find((item) => item.path === 'workflows-gates')?.redirectTo).toBe('workflows'); + }); }); diff --git a/src/Web/StellaOps.Web/src/tests/topology/topology-shell.component.spec.ts b/src/Web/StellaOps.Web/src/tests/topology/topology-shell.component.spec.ts new file mode 100644 index 000000000..61826200d --- /dev/null +++ b/src/Web/StellaOps.Web/src/tests/topology/topology-shell.component.spec.ts @@ -0,0 +1,22 @@ +import { TopologyShellComponent } from '../../app/features/topology/topology-shell.component'; + +describe('TopologyShellComponent', () => { + it('exposes canonical tabs for the preserved topology pages', () => { + const component = new TopologyShellComponent(); + + expect(component.tabs).toEqual([ + { id: 'overview', label: 'Overview', route: 'overview' }, + { id: 'map', label: 'Map', route: 'map' }, + { id: 'regions', label: 'Regions & Environments', route: 'regions' }, + { id: 'targets', label: 'Targets', route: 'targets' }, + { id: 'hosts', label: 'Hosts', route: 'hosts' }, + { id: 'agents', label: 'Agents', route: 'agents' }, + { id: 'promotion', label: 'Promotion Graph', route: 'promotion-graph' }, + { id: 'workflows', label: 'Workflows', route: 'workflows' }, + { id: 'gate-profiles', label: 'Gate Profiles', route: 'gate-profiles' }, + { id: 'connectivity', label: 'Connectivity', route: 'connectivity' }, + { id: 'drift', label: 'Runtime Drift', route: 'runtime-drift' }, + ]); + }); +}); + diff --git a/src/Web/StellaOps.Web/tests/e2e/topology-trust-admin-cutover.spec.ts b/src/Web/StellaOps.Web/tests/e2e/topology-trust-admin-cutover.spec.ts new file mode 100644 index 000000000..71657e111 --- /dev/null +++ b/src/Web/StellaOps.Web/tests/e2e/topology-trust-admin-cutover.spec.ts @@ -0,0 +1,287 @@ +import { expect, test, type Page, type Route } from '@playwright/test'; + +import type { StubAuthSession } from '../../src/app/testing/auth-fixtures'; + +const operatorSession: StubAuthSession = { + subjectId: 'setup-e2e-user', + tenant: 'tenant-default', + scopes: [ + 'admin', + 'ui.read', + 'ui.admin', + 'orch:read', + 'orch:operate', + 'release:read', + 'signer: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', +}; + +const trustDashboardSummary = { + keys: { + total: 4, + active: 3, + expiringSoon: 1, + expired: 0, + revoked: 0, + pendingRotation: 0, + }, + issuers: { + total: 2, + fullTrust: 1, + partialTrust: 1, + minimalTrust: 0, + untrusted: 0, + blocked: 0, + averageTrustScore: 89.5, + }, + certificates: { + total: 3, + valid: 3, + expiringSoon: 0, + expired: 0, + revoked: 0, + invalidChains: 0, + }, + recentEvents: [], + expiryAlerts: [], +}; + +const trustIssuers = { + items: [ + { + issuerId: 'issuer-001', + tenantId: 'tenant-default', + name: 'github-security-advisories', + displayName: 'GitHub Security Advisories', + description: 'Official GitHub advisory issuer', + issuerType: 'csaf_publisher', + trustLevel: 'full', + trustScore: 95, + publicKeyFingerprints: ['SHA256:issuer-001'], + documentCount: 1200, + verificationCount: 1188, + weights: { + baseWeight: 80, + recencyFactor: 10, + verificationBonus: 15, + volumePenalty: 2, + manualAdjustment: 0, + }, + validFrom: '2025-01-01T00:00:00Z', + lastVerifiedAt: '2026-03-08T06:00:00Z', + isActive: true, + createdAt: '2025-01-01T00:00:00Z', + updatedAt: '2026-03-08T06:00:00Z', + }, + ], + pageNumber: 1, + pageSize: 20, + totalCount: 1, + totalPages: 1, +}; + +const topologyRegions = [ + { regionId: 'eu-west', displayName: 'EU West', environmentCount: 1, targetCount: 2 }, +]; + +const topologyEnvironments = [ + { + environmentId: 'prod', + regionId: 'eu-west', + environmentType: 'prod', + displayName: 'Production', + targetCount: 2, + }, +]; + +const topologyTargets = [ + { + targetId: 'target-001', + environmentId: 'prod', + regionId: 'eu-west', + name: 'Gateway', + targetType: 'vm', + healthStatus: 'healthy', + agentId: 'agent-001', + }, +]; + +const topologyAgents = [ + { + agentId: 'agent-001', + agentName: 'Agent One', + regionId: 'eu-west', + environmentId: 'prod', + status: 'active', + assignedTargetCount: 2, + }, +]; + +const promotionPaths = [ + { + pathId: 'path-001', + regionId: 'eu-west', + sourceEnvironmentId: 'stage', + targetEnvironmentId: 'prod', + status: 'running', + requiredApprovals: 1, + }, +]; + +async function fulfillJson(route: Route, body: unknown, status = 200): Promise { + await route.fulfill({ + status, + contentType: 'application/json', + body: JSON.stringify(body), + }); +} + +async function setupHarness(page: Page): Promise { + await page.addInitScript((session) => { + (window as { __stellaopsTestSession?: unknown }).__stellaopsTestSession = session; + }, operatorSession); + + await page.route('**/api/**', (route) => fulfillJson(route, {})); + await page.route('**/platform/envsettings.json', (route) => fulfillJson(route, mockConfig)); + await page.route('**/platform/i18n/*.json', (route) => fulfillJson(route, {})); + 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/branding**', (route) => + fulfillJson(route, { + tenantId: operatorSession.tenant, + appName: 'Stella Ops', + logoUrl: null, + cssVariables: {}, + }), + ); + await page.route('**/console/profile**', (route) => + fulfillJson(route, { + subjectId: operatorSession.subjectId, + username: 'setup-e2e', + displayName: 'Setup E2E', + tenant: operatorSession.tenant, + roles: ['platform-admin'], + scopes: operatorSession.scopes, + }), + ); + await page.route('**/console/token/introspect**', (route) => + fulfillJson(route, { + active: true, + tenant: operatorSession.tenant, + subject: operatorSession.subjectId, + scopes: operatorSession.scopes, + }), + ); + await page.route('**/authority/console/tenants**', (route) => + fulfillJson(route, { + tenants: [ + { + tenantId: operatorSession.tenant, + displayName: 'Default Tenant', + isDefault: true, + isActive: true, + }, + ], + }), + ); + 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: 'Production', + sortOrder: 1, + enabled: true, + }, + ]), + ); + await page.route('**/api/v2/context/preferences**', (route) => + fulfillJson(route, { + tenantId: operatorSession.tenant, + actorId: operatorSession.subjectId, + regions: ['eu-west'], + environments: ['prod'], + timeWindow: '24h', + stage: 'all', + updatedAt: '2026-03-08T07:00:00Z', + updatedBy: operatorSession.subjectId, + }), + ); + + await page.route(/\/api\/v1\/trust\/dashboard(?:\?.*)?$/, (route) => fulfillJson(route, trustDashboardSummary)); + await page.route(/\/api\/v1\/trust\/issuers(?:\?.*)?$/, (route) => fulfillJson(route, trustIssuers)); + await page.route(/\/api\/v2\/topology\/regions(?:\?.*)?$/, (route) => fulfillJson(route, topologyRegions)); + await page.route(/\/api\/v2\/topology\/environments(?:\?.*)?$/, (route) => + fulfillJson(route, topologyEnvironments), + ); + await page.route(/\/api\/v2\/topology\/targets(?:\?.*)?$/, (route) => fulfillJson(route, topologyTargets)); + await page.route(/\/api\/v2\/topology\/agents(?:\?.*)?$/, (route) => fulfillJson(route, topologyAgents)); + await page.route(/\/api\/v2\/topology\/promotion-paths(?:\?.*)?$/, (route) => + fulfillJson(route, promotionPaths), + ); +} + +test.beforeEach(async ({ page }) => { + await setupHarness(page); +}); + +test('topology and trust cutover keeps setup handoffs and legacy trust entry points usable', async ({ page }) => { + await page.goto('/settings/trust', { waitUntil: 'networkidle' }); + await expect(page).toHaveURL(/\/setup\/trust-signing(?:\?.*)?$/); + await expect(page.getByRole('heading', { name: 'Trust Management' })).toBeVisible(); + await page.getByRole('tab', { name: 'Trusted Issuers' }).click(); + await expect(page).toHaveURL(/\/setup\/trust-signing\/issuers(?:\?.*)?$/); + await expect(page.getByText('GitHub Security Advisories')).toBeVisible(); + + await page.goto('/admin/trust', { waitUntil: 'networkidle' }); + await expect(page).toHaveURL(/\/setup\/trust-signing(?:\?.*)?$/); + await expect(page.getByRole('tab', { name: 'Watchlist' })).toBeVisible(); + + await page.goto('/ops/platform-setup', { waitUntil: 'networkidle' }); + const regionsCard = page.locator('.setup-home__card').filter({ hasText: 'Regions & Environments' }); + await regionsCard.getByRole('link', { name: 'Open' }).click(); + await expect(page).toHaveURL(/\/setup\/topology\/regions(?:\?.*)?$/); + await expect(page.getByRole('heading', { name: 'Topology' })).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Regions & Environments' })).toBeVisible(); +});