feat(ui): ship topology and trust admin cutover

This commit is contained in:
master
2026-03-08 10:12:13 +02:00
parent 8b1fe49f35
commit 56143d12b7
19 changed files with 985 additions and 70 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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`

View File

@@ -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<string, string>;
queryParams: Record<string, string>;
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' },
],
},

View File

@@ -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',
},

View File

@@ -50,7 +50,7 @@ interface TopoLink extends d3.SimulationLinkDatum<TopoNode> {
<section class="setup-home">
<header class="setup-home__header">
<h1 class="setup-home__title">Platform Setup</h1>
<p class="setup-home__subtitle">Configure inventory, promotion, workflow, policy. Explore the topology graph below.</p>
<p class="setup-home__subtitle">Configure canonical setup inventory, promotion, workflow, policy, and trust handoffs. Explore the topology graph below.</p>
</header>
@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<TopoNodeKind, string> = {

View File

@@ -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',
},
];

View File

@@ -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<string, string>;
queryParams: Record<string, string>;
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',

View File

@@ -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' },
];

View File

@@ -68,6 +68,30 @@ function redirectToEvidence(path: string) {
};
}
function redirectToSetup(path: string) {
return ({
params,
queryParams,
fragment,
}: {
params: Record<string, string>;
queryParams: Record<string, string>;
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

View File

@@ -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',

View File

@@ -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();
}
});
});

View File

@@ -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, string> = {}): 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',
);
});
});

View File

@@ -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<string | null> {
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');
});
});

View File

@@ -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' },
]);
});
});

View File

@@ -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<void> {
await route.fulfill({
status,
contentType: 'application/json',
body: JSON.stringify(body),
});
}
async function setupHarness(page: Page): Promise<void> {
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();
});