From b9115378703bedee5115afb96d84b62bf854be7f Mon Sep 17 00:00:00 2001 From: master <> Date: Sat, 21 Feb 2026 16:21:33 +0200 Subject: [PATCH] ui fixes --- devops/compose/env/stellaops.env.example | 20 +- devops/compose/hosts.stellaops.local | 62 + docs/INSTALL_GUIDE.md | 132 +- docs/dev/DEV_ENVIRONMENT_SETUP.md | 76 +- ...221_041_FE_prealpha_ia_ops_setup_rewire.md | 198 +++ docs/modules/ui/TASKS.md | 18 + .../ui/v2-rewire/S00_route_deprecation_map.md | 130 +- docs/modules/ui/v2-rewire/source-of-truth.md | 44 +- docs/quickstart.md | 132 +- scripts/setup.ps1 | 110 +- scripts/setup.sh | 87 +- .../src/app/app.component.spec.ts | 2 + .../StellaOps.Web/src/app/app.component.ts | 27 +- src/Web/StellaOps.Web/src/app/app.config.ts | 6 +- src/Web/StellaOps.Web/src/app/app.routes.ts | 972 +------------ .../src/app/core/api/aoc.client.ts | 501 +------ .../src/app/core/api/search.client.ts | 8 +- .../src/app/core/api/search.models.ts | 16 +- .../auth/authority-auth-adapter.service.ts | 8 +- .../app/core/auth/authority-auth.service.ts | 29 +- .../platform-context-url-sync.service.ts | 34 +- .../core/context/platform-context.store.ts | 36 +- .../app/core/doctor/doctor-trend.service.ts | 8 +- .../admin-notifications.routes.ts | 25 +- .../administration-overview.component.ts | 242 ++-- .../features/analytics/analytics.routes.ts | 23 +- .../app/features/bundles/bundles.routes.ts | 5 - .../console-admin/console-admin.routes.ts | 4 +- .../dashboard-v3/dashboard-v3.component.ts | 22 +- .../evidence-export/evidence-export.routes.ts | 8 +- .../feed-mirror/feed-mirror.routes.ts | 5 +- .../integration-hub/integration-hub.routes.ts | 51 +- .../issuer-trust/issuer-trust.routes.ts | 6 +- .../mission-activity-page.component.ts | 48 + .../mission-alerts-page.component.ts | 34 + .../offline-kit/offline-kit.routes.ts | 5 +- .../features/operations/operations.routes.ts | 11 +- .../ops/ops-overview-page.component.ts | 35 + .../platform/setup/platform-setup.routes.ts | 11 +- .../policy-governance.routes.ts | 4 +- .../policy-simulation.routes.ts | 7 +- .../src/app/features/policy/policy.routes.ts | 114 +- .../registry-admin/registry-admin.routes.ts | 6 +- .../environments/environments.routes.ts | 20 - .../release-detail.component.ts | 25 +- .../releases/hotfix-create-page.component.ts | 53 + .../releases/hotfix-detail-page.component.ts | 37 + .../release-ops-overview-page.component.ts | 37 + .../scanner-ops/scanner-ops.routes.ts | 6 +- .../scheduler-ops/scheduler-ops.routes.ts | 7 +- .../secret-detection.routes.ts | 6 +- .../advisory-sources.component.ts | 7 +- .../security-risk-overview.component.ts | 5 +- ...ecurity-component-detail-page.component.ts | 35 + .../security-disposition-page.component.ts | 6 +- ...-environment-risk-detail-page.component.ts | 35 + .../security-reports-page.component.ts | 34 + .../app/features/security/security.routes.ts | 5 +- .../vulnerability-detail-page.component.ts | 3 +- .../app/features/settings/settings.routes.ts | 6 +- .../environment-posture-page.component.ts | 2 + ...ology-agent-group-detail-page.component.ts | 22 + .../topology-agents-page.component.ts | 14 +- .../topology-connectivity-page.component.ts | 18 + .../topology/topology-data.service.ts | 2 + ...ology-environment-detail-page.component.ts | 6 +- .../topology-host-detail-page.component.ts | 22 + .../topology/topology-hosts-page.component.ts | 8 +- .../topology-inventory-page.component.ts | 2 + .../topology/topology-map-page.component.ts | 21 + .../topology-overview-page.component.ts | 20 +- ...topology-promotion-paths-page.component.ts | 4 +- ...ogy-regions-environments-page.component.ts | 12 +- .../topology-runtime-drift-page.component.ts | 18 + .../topology-target-detail-page.component.ts | 22 + .../topology-targets-page.component.ts | 8 +- .../app/features/topology/topology.models.ts | 2 + .../trust-admin/trust-admin.routes.ts | 7 +- .../app-sidebar/app-sidebar.component.ts | 120 +- .../layout/app-topbar/app-topbar.component.ts | 79 + .../context-chips/context-chips.component.ts | 21 +- .../src/app/layout/context-chips/index.ts | 1 + .../live-event-stream-chip.component.ts | 70 + .../src/app/routes/evidence-audit.routes.ts | 136 +- .../src/app/routes/evidence.routes.ts | 133 +- .../src/app/routes/legacy-redirects.routes.ts | 277 +--- .../src/app/routes/mission-control.routes.ts | 32 + .../src/app/routes/operations.routes.ts | 53 - .../src/app/routes/ops.routes.ts | 37 + .../src/app/routes/platform-ops.routes.ts | 309 +--- .../src/app/routes/platform.routes.ts | 55 +- .../src/app/routes/release-control.routes.ts | 246 +--- .../src/app/routes/releases.routes.ts | 128 +- .../src/app/routes/security.routes.ts | 172 +-- .../src/app/routes/setup.routes.ts | 56 + .../src/app/routes/topology.routes.ts | 114 +- src/Web/StellaOps.Web/src/index.html | 15 +- .../unified-audit-log-viewer.behavior.spec.ts | 13 +- .../platform-context-url-sync.service.spec.ts | 13 +- .../dashboard/dashboard-route-aliases.spec.ts | 18 +- .../evidence-audit-routes.spec.ts | 168 +-- .../tests/navigation/legacy-redirects.spec.ts | 210 +-- .../platform-ops/platform-ops-routes.spec.ts | 262 +--- .../platform/platform-setup-routes.spec.ts | 16 +- .../release-control-routes.spec.ts | 214 +-- .../tests/e2e/accessibility.spec.ts | 75 +- .../tests/e2e/analytics-sbom-lake.spec.ts | 13 +- src/Web/StellaOps.Web/tests/e2e/auth.spec.ts | 25 +- .../tests/e2e/critical-path.spec.ts | 264 +--- .../tests/e2e/doctor-registry.spec.ts | 748 ++-------- .../tests/e2e/graph-platform-client.spec.ts | 11 +- .../tests/e2e/ia-v2-a11y-regression.spec.ts | 99 +- .../StellaOps.Web/tests/e2e/nav-shell.spec.ts | 245 ++-- .../e2e/pack-conformance.scratch.spec.ts | 89 +- .../e2e/prealpha-canonical-full-sweep.spec.ts | 1276 +++++++++++++++++ src/Web/StellaOps.Web/tests/e2e/smoke.spec.ts | 596 ++------ 116 files changed, 4365 insertions(+), 5903 deletions(-) create mode 100644 devops/compose/hosts.stellaops.local create mode 100644 docs/implplan/SPRINT_20260221_041_FE_prealpha_ia_ops_setup_rewire.md create mode 100644 docs/modules/ui/TASKS.md create mode 100644 src/Web/StellaOps.Web/src/app/features/mission-control/mission-activity-page.component.ts create mode 100644 src/Web/StellaOps.Web/src/app/features/mission-control/mission-alerts-page.component.ts create mode 100644 src/Web/StellaOps.Web/src/app/features/ops/ops-overview-page.component.ts create mode 100644 src/Web/StellaOps.Web/src/app/features/releases/hotfix-create-page.component.ts create mode 100644 src/Web/StellaOps.Web/src/app/features/releases/hotfix-detail-page.component.ts create mode 100644 src/Web/StellaOps.Web/src/app/features/releases/release-ops-overview-page.component.ts create mode 100644 src/Web/StellaOps.Web/src/app/features/security/security-component-detail-page.component.ts create mode 100644 src/Web/StellaOps.Web/src/app/features/security/security-environment-risk-detail-page.component.ts create mode 100644 src/Web/StellaOps.Web/src/app/features/security/security-reports-page.component.ts create mode 100644 src/Web/StellaOps.Web/src/app/features/topology/topology-agent-group-detail-page.component.ts create mode 100644 src/Web/StellaOps.Web/src/app/features/topology/topology-connectivity-page.component.ts create mode 100644 src/Web/StellaOps.Web/src/app/features/topology/topology-host-detail-page.component.ts create mode 100644 src/Web/StellaOps.Web/src/app/features/topology/topology-map-page.component.ts create mode 100644 src/Web/StellaOps.Web/src/app/features/topology/topology-runtime-drift-page.component.ts create mode 100644 src/Web/StellaOps.Web/src/app/features/topology/topology-target-detail-page.component.ts create mode 100644 src/Web/StellaOps.Web/src/app/layout/context-chips/live-event-stream-chip.component.ts create mode 100644 src/Web/StellaOps.Web/src/app/routes/mission-control.routes.ts create mode 100644 src/Web/StellaOps.Web/src/app/routes/ops.routes.ts create mode 100644 src/Web/StellaOps.Web/src/app/routes/setup.routes.ts create mode 100644 src/Web/StellaOps.Web/tests/e2e/prealpha-canonical-full-sweep.spec.ts diff --git a/devops/compose/env/stellaops.env.example b/devops/compose/env/stellaops.env.example index 879c8294e..386dbc6b2 100644 --- a/devops/compose/env/stellaops.env.example +++ b/devops/compose/env/stellaops.env.example @@ -16,7 +16,7 @@ # PostgreSQL Database POSTGRES_USER=stellaops -POSTGRES_PASSWORD=REPLACE_WITH_STRONG_PASSWORD +POSTGRES_PASSWORD=stellaops # Change for production POSTGRES_DB=stellaops_platform POSTGRES_PORT=5432 @@ -31,12 +31,12 @@ RUSTFS_HTTP_PORT=8080 # ============================================================================= # Authority (OAuth2/OIDC) -AUTHORITY_ISSUER=https://authority.example.com +AUTHORITY_ISSUER=https://authority.stella-ops.local/ AUTHORITY_PORT=8440 AUTHORITY_OFFLINE_CACHE_TOLERANCE=00:30:00 # Signer -SIGNER_POE_INTROSPECT_URL=https://licensing.example.com/introspect +SIGNER_POE_INTROSPECT_URL=http://authority.stella-ops.local/.well-known/openid-configuration SIGNER_PORT=8441 # Attestor @@ -62,18 +62,18 @@ UI_PORT=8443 SCANNER_WEB_PORT=8444 # Queue configuration (Valkey only - NATS removed) -SCANNER__QUEUE__BROKER=valkey://valkey:6379 +SCANNER__QUEUE__BROKER=valkey://cache.stella-ops.local:6379 # Event streaming SCANNER_EVENTS_ENABLED=false SCANNER_EVENTS_DRIVER=valkey -SCANNER_EVENTS_DSN=valkey:6379 +SCANNER_EVENTS_DSN=cache.stella-ops.local:6379 SCANNER_EVENTS_STREAM=stella.events SCANNER_EVENTS_PUBLISH_TIMEOUT_SECONDS=5 SCANNER_EVENTS_MAX_STREAM_LENGTH=10000 # Surface cache configuration -SCANNER_SURFACE_FS_ENDPOINT=http://rustfs:8080 +SCANNER_SURFACE_FS_ENDPOINT=http://s3.stella-ops.local SCANNER_SURFACE_FS_BUCKET=surface-cache SCANNER_SURFACE_CACHE_ROOT=/var/lib/stellaops/surface SCANNER_SURFACE_CACHE_QUOTA_MB=4096 @@ -102,8 +102,8 @@ SCANNER_OFFLINEKIT_REKOR_SNAPSHOT_HOST_PATH=./offline/rekor-snapshot # Queue configuration (Valkey only - NATS removed) SCHEDULER__QUEUE__KIND=Valkey -SCHEDULER__QUEUE__VALKEY__URL=valkey:6379 -SCHEDULER_SCANNER_BASEADDRESS=http://scanner-web:8444 +SCHEDULER__QUEUE__VALKEY__URL=cache.stella-ops.local:6379 +SCHEDULER_SCANNER_BASEADDRESS=http://scanner.stella-ops.local # ============================================================================= # REKOR / SIGSTORE CONFIGURATION @@ -121,7 +121,7 @@ REKOR_TILES_IMAGE=ghcr.io/sigstore/rekor-tiles:latest # ============================================================================= ADVISORY_AI_WEB_PORT=8448 -ADVISORY_AI_SBOM_BASEADDRESS=http://scanner-web:8444 +ADVISORY_AI_SBOM_BASEADDRESS=http://scanner.stella-ops.local ADVISORY_AI_INFERENCE_MODE=Local ADVISORY_AI_REMOTE_BASEADDRESS= ADVISORY_AI_REMOTE_APIKEY= @@ -135,7 +135,7 @@ STELLAOPS_CRYPTO_PROFILE=default # Enable crypto simulation (for testing) STELLAOPS_CRYPTO_ENABLE_SIM=0 -STELLAOPS_CRYPTO_SIM_URL=http://sim-crypto:8080 +STELLAOPS_CRYPTO_SIM_URL=http://crypto-sim.stella-ops.local:8080 # CryptoPro (Russia only) - requires EULA acceptance CRYPTOPRO_PORT=18080 diff --git a/devops/compose/hosts.stellaops.local b/devops/compose/hosts.stellaops.local new file mode 100644 index 000000000..504d725c2 --- /dev/null +++ b/devops/compose/hosts.stellaops.local @@ -0,0 +1,62 @@ +# Stella Ops local development hostnames +# Each service gets a unique loopback IP so all can bind :443/:80 simultaneously. +# +# Source of truth: devops/compose/hosts.stellaops.local +# Install automatically via: scripts/setup.ps1 (Windows) or scripts/setup.sh (Linux/macOS) +# Manual install: append this file to your hosts file +# Windows: C:\Windows\System32\drivers\etc\hosts (run editor as Administrator) +# Linux/macOS: /etc/hosts (use sudo) + +# ── Platform services ──────────────────────────────────────────────────────── +127.1.0.1 stella-ops.local +127.1.0.2 router.stella-ops.local +127.1.0.3 platform.stella-ops.local +127.1.0.4 authority.stella-ops.local +127.1.0.5 gateway.stella-ops.local +127.1.0.6 attestor.stella-ops.local +127.1.0.7 evidencelocker.stella-ops.local +127.1.0.8 scanner.stella-ops.local +127.1.0.9 concelier.stella-ops.local +127.1.0.10 excititor.stella-ops.local +127.1.0.11 vexhub.stella-ops.local +127.1.0.12 vexlens.stella-ops.local +127.1.0.13 vulnexplorer.stella-ops.local +127.1.0.14 policy-engine.stella-ops.local +127.1.0.15 policy-gateway.stella-ops.local +127.1.0.16 riskengine.stella-ops.local +127.1.0.17 orchestrator.stella-ops.local +127.1.0.18 taskrunner.stella-ops.local +127.1.0.19 scheduler.stella-ops.local +127.1.0.20 graph.stella-ops.local +127.1.0.21 cartographer.stella-ops.local +127.1.0.22 reachgraph.stella-ops.local +127.1.0.23 timelineindexer.stella-ops.local +127.1.0.24 timeline.stella-ops.local +127.1.0.25 findings.stella-ops.local +127.1.0.26 doctor.stella-ops.local +127.1.0.27 opsmemory.stella-ops.local +127.1.0.28 notifier.stella-ops.local +127.1.0.29 notify.stella-ops.local +127.1.0.30 signer.stella-ops.local +127.1.0.31 smremote.stella-ops.local +127.1.0.32 airgap-controller.stella-ops.local +127.1.0.33 airgap-time.stella-ops.local +127.1.0.34 packsregistry.stella-ops.local +127.1.0.35 registry-token.stella-ops.local +127.1.0.36 binaryindex.stella-ops.local +127.1.0.37 issuerdirectory.stella-ops.local +127.1.0.38 symbols.stella-ops.local +127.1.0.39 sbomservice.stella-ops.local +127.1.0.40 exportcenter.stella-ops.local +127.1.0.41 replay.stella-ops.local +127.1.0.42 integrations.stella-ops.local +127.1.0.43 signals.stella-ops.local +127.1.0.44 advisoryai.stella-ops.local +127.1.0.45 unknowns.stella-ops.local + +# ── Infrastructure (local dev containers) ──────────────────────────────────── +127.1.1.1 db.stella-ops.local +127.1.1.2 cache.stella-ops.local +127.1.1.3 s3.stella-ops.local +127.1.1.4 rekor.stella-ops.local +127.1.1.5 registry.stella-ops.local diff --git a/docs/INSTALL_GUIDE.md b/docs/INSTALL_GUIDE.md index c0ddc2af2..7fcf27d47 100755 --- a/docs/INSTALL_GUIDE.md +++ b/docs/INSTALL_GUIDE.md @@ -1,47 +1,118 @@ -# Installation guide (Docker Compose + air-gap) +# Installation Guide -This guide explains how to run StellaOps from this repository using deterministic deployment bundles under `deploy/`. +How to run Stella Ops from this repository using Docker Compose. ## Prerequisites -- Docker Engine with Compose v2. -- Enough disk for container images plus scan artifacts (SBOMs, logs, caches). -- For production-style installs, plan for persistent volumes (PostgreSQL + object storage) and a secrets provider. -## Connected host (dev / evaluation) +- Docker Engine with Compose v2 (`docker compose version`) +- Enough disk for container images plus scan artifacts (SBOMs, logs, caches) +- For production-style installs, plan for persistent volumes (PostgreSQL + object storage) and a secrets provider -StellaOps ships reproducible Compose profiles pinned to immutable digests. +## Quick path (automated setup scripts) -```bash -cd deploy/compose -cp env/dev.env.example dev.env -docker compose --env-file dev.env -f docker-compose.dev.yaml config -docker compose --env-file dev.env -f docker-compose.dev.yaml up -d +The fastest way to get running. The setup scripts validate prerequisites, configure the environment, start infrastructure, build solutions, build Docker images, and launch the full platform. + +**Windows (PowerShell 7):** + +```powershell +.\scripts\setup.ps1 # full setup +.\scripts\setup.ps1 -InfraOnly # infrastructure only (PostgreSQL, Valkey, RustFS, Rekor, Zot) ``` -Verify: +**Linux / macOS:** ```bash -docker compose --env-file dev.env -f docker-compose.dev.yaml ps +./scripts/setup.sh # full setup +./scripts/setup.sh --infra-only # infrastructure only ``` -Defaults are defined by the selected env file. For the dev profile, the UI listens on `https://localhost:8443` by default; see `devops/compose/env/dev.env.example` for the full port map. +The scripts will: +1. Check prerequisites (dotnet 10.x, node 20+, docker, git) +2. Offer to install hosts file entries automatically +3. Copy `env/stellaops.env.example` to `.env` if needed (works out of the box) +4. Start infrastructure and wait for healthy containers +5. Build .NET solutions and Docker images +6. Launch the full platform with health checks -## Air-gapped host (Compose profile) +Open **https://stella-ops.local** when setup completes. -Use the air-gap profile to avoid outbound hostnames and to align defaults with offline operation: +## Manual path (step by step) + +### 1. Environment file ```bash -cd deploy/compose -cp env/airgap.env.example airgap.env -docker compose --env-file airgap.env -f docker-compose.airgap.yaml config -docker compose --env-file airgap.env -f docker-compose.airgap.yaml up -d +cd devops/compose +cp env/stellaops.env.example .env ``` -For offline bundles, imports, and update workflows, use: +The example file ships with working local-dev defaults. For production, change `POSTGRES_PASSWORD` and review all values. + +### 2. Hosts file + +Stella Ops services bind to unique loopback IPs so all can use port 443 without collisions. Add the entries from `devops/compose/hosts.stellaops.local` to your hosts file: + +- **Windows:** `C:\Windows\System32\drivers\etc\hosts` (run editor as Administrator) +- **Linux / macOS:** `sudo sh -c 'cat devops/compose/hosts.stellaops.local >> /etc/hosts'` + +### 3. Start infrastructure + +```bash +cd devops/compose +docker compose -f docker-compose.dev.yml up -d +docker compose -f docker-compose.dev.yml ps # verify all healthy +``` + +### 4. Start the full platform + +```bash +docker compose -f docker-compose.stella-ops.yml up -d +``` + +Optional overlays: + +```bash +# With Sigstore transparency log +docker compose -f docker-compose.stella-ops.yml --profile sigstore up -d + +# With telemetry stack (Prometheus, Tempo, Loki) +docker compose -f docker-compose.stella-ops.yml -f docker-compose.telemetry.yml up -d +``` + +### 5. Verify + +```bash +docker compose -f docker-compose.stella-ops.yml ps +curl -k https://stella-ops.local # should return the Angular UI +``` + +## Air-gapped deployments + +For offline/air-gapped environments, use the sealed CI compose file and offline telemetry overlay: + +```bash +# Sealed CI environment (authority, signer, attestor in isolation) +docker compose -f docker-compose.sealed-ci.yml up -d + +# Offline observability (no external endpoints) +docker compose -f docker-compose.stella-ops.yml -f docker-compose.telemetry-offline.yml up -d + +# Tile proxy for air-gapped Sigstore verification +docker compose -f docker-compose.stella-ops.yml -f docker-compose.tile-proxy.yml up -d +``` + +For offline bundles, imports, and update workflows, see: - `docs/OFFLINE_KIT.md` - `docs/modules/airgap/guides/overview.md` -- `docs/modules/airgap/guides/importer.md` -- `docs/modules/airgap/guides/controller.md` + +## Regional compliance overlays + +| Region | Testing | Production | +|--------|---------|------------| +| China (SM2/SM3/SM4) | `docker-compose.compliance-china.yml` + `docker-compose.crypto-sim.yml` | `docker-compose.compliance-china.yml` + `docker-compose.sm-remote.yml` | +| Russia (GOST) | `docker-compose.compliance-russia.yml` + `docker-compose.crypto-sim.yml` | `docker-compose.compliance-russia.yml` + `docker-compose.cryptopro.yml` | +| EU (eIDAS) | `docker-compose.compliance-eu.yml` + `docker-compose.crypto-sim.yml` | `docker-compose.compliance-eu.yml` | + +See `devops/compose/README.md` for detailed compliance deployment instructions. ## Hardening: require Authority for Concelier job triggers @@ -54,14 +125,9 @@ CONCELIER_AUTHORITY__ALLOWANONYMOUSFALLBACK=false Store the client secret outside source control (Docker secrets, mounted file, or Kubernetes Secret). For audit fields and alerting guidance, see `docs/modules/concelier/operations/authority-audit-runbook.md`. -## Quota / licensing (optional) - -Quota enforcement is configuration-driven. For the current posture and operational implications, see: -- `docs/QUOTA_OVERVIEW.md` -- `docs/QUOTA_ENFORCEMENT_FLOW.md` -- `docs/license-jwt-quota.md` - ## Next steps -- Quick start: `docs/quickstart.md` + +- Quickstart: `docs/quickstart.md` +- Developer setup details: `docs/dev/DEV_ENVIRONMENT_SETUP.md` - Architecture overview: `docs/ARCHITECTURE_OVERVIEW.md` -- Detailed technical index: `docs/technical/README.md` +- Compose profiles reference: `devops/compose/README.md` diff --git a/docs/dev/DEV_ENVIRONMENT_SETUP.md b/docs/dev/DEV_ENVIRONMENT_SETUP.md index 416d142b6..c0ed29190 100644 --- a/docs/dev/DEV_ENVIRONMENT_SETUP.md +++ b/docs/dev/DEV_ENVIRONMENT_SETUP.md @@ -65,67 +65,25 @@ The scripts will check for required tools (dotnet 10.x, node 20+, npm 10+, docke Each service binds to a unique loopback IP so all can use ports 443/80 without collisions. Full details: [`docs/technical/architecture/port-registry.md`](../technical/architecture/port-registry.md). -Add the block below to your hosts file: +### Automated (recommended) -- **Windows:** `C:\Windows\System32\drivers\etc\hosts` (run editor as Administrator) -- **Linux / macOS:** `/etc/hosts` (use `sudo`) +The setup scripts (`scripts/setup.ps1` / `scripts/setup.sh`) will detect missing entries and offer to install them automatically. -``` -# Stella Ops local development hostnames -# Each service gets a unique loopback IP so all can bind :443/:80 simultaneously. -127.1.0.1 stella-ops.local -127.1.0.2 router.stella-ops.local -127.1.0.3 platform.stella-ops.local -127.1.0.4 authority.stella-ops.local -127.1.0.5 gateway.stella-ops.local -127.1.0.6 attestor.stella-ops.local -127.1.0.7 evidencelocker.stella-ops.local -127.1.0.8 scanner.stella-ops.local -127.1.0.9 concelier.stella-ops.local -127.1.0.10 excititor.stella-ops.local -127.1.0.11 vexhub.stella-ops.local -127.1.0.12 vexlens.stella-ops.local -127.1.0.13 vulnexplorer.stella-ops.local -127.1.0.14 policy-engine.stella-ops.local -127.1.0.15 policy-gateway.stella-ops.local -127.1.0.16 riskengine.stella-ops.local -127.1.0.17 orchestrator.stella-ops.local -127.1.0.18 taskrunner.stella-ops.local -127.1.0.19 scheduler.stella-ops.local -127.1.0.20 graph.stella-ops.local -127.1.0.21 cartographer.stella-ops.local -127.1.0.22 reachgraph.stella-ops.local -127.1.0.23 timelineindexer.stella-ops.local -127.1.0.24 timeline.stella-ops.local -127.1.0.25 findings.stella-ops.local -127.1.0.26 doctor.stella-ops.local -127.1.0.27 opsmemory.stella-ops.local -127.1.0.28 notifier.stella-ops.local -127.1.0.29 notify.stella-ops.local -127.1.0.30 signer.stella-ops.local -127.1.0.31 smremote.stella-ops.local -127.1.0.32 airgap-controller.stella-ops.local -127.1.0.33 airgap-time.stella-ops.local -127.1.0.34 packsregistry.stella-ops.local -127.1.0.35 registry-token.stella-ops.local -127.1.0.36 binaryindex.stella-ops.local -127.1.0.37 issuerdirectory.stella-ops.local -127.1.0.38 symbols.stella-ops.local -127.1.0.39 sbomservice.stella-ops.local -127.1.0.40 exportcenter.stella-ops.local -127.1.0.41 replay.stella-ops.local -127.1.0.42 integrations.stella-ops.local -127.1.0.43 signals.stella-ops.local -127.1.0.44 advisoryai.stella-ops.local -127.1.0.45 unknowns.stella-ops.local +### Manual -# Stella Ops infrastructure (local dev containers) -127.1.1.1 db.stella-ops.local -127.1.1.2 cache.stella-ops.local -127.1.1.3 s3.stella-ops.local -127.1.1.4 rekor.stella-ops.local -127.1.1.5 registry.stella-ops.local -``` +Append the contents of [`devops/compose/hosts.stellaops.local`](../../devops/compose/hosts.stellaops.local) to your hosts file: + +- **Windows:** Run an elevated PowerShell and run: + ```powershell + Get-Content devops\compose\hosts.stellaops.local | Add-Content C:\Windows\System32\drivers\etc\hosts + ``` +- **Linux / macOS:** + ```bash + sudo sh -c 'cat devops/compose/hosts.stellaops.local >> /etc/hosts' + ``` + +The file contains ~50 entries mapping services to unique loopback IPs (127.1.0.1 through 127.1.1.5). +See the file for the full list. --- @@ -133,7 +91,7 @@ Add the block below to your hosts file: ```bash cd devops/compose -cp env/stellaops.env.example .env # edit POSTGRES_PASSWORD at minimum +cp env/stellaops.env.example .env # works out of the box; change POSTGRES_PASSWORD for production docker compose -f docker-compose.dev.yml up -d docker compose -f docker-compose.dev.yml ps ``` diff --git a/docs/implplan/SPRINT_20260221_041_FE_prealpha_ia_ops_setup_rewire.md b/docs/implplan/SPRINT_20260221_041_FE_prealpha_ia_ops_setup_rewire.md new file mode 100644 index 000000000..1c039851a --- /dev/null +++ b/docs/implplan/SPRINT_20260221_041_FE_prealpha_ia_ops_setup_rewire.md @@ -0,0 +1,198 @@ +# Sprint 20260221_041 - Pre-Alpha IA Rewire (Ops + Setup) + +## Topic & Scope +- Rewire the Web UI to pre-alpha IA where root workspaces are Mission Control, Releases, Security, Evidence, Ops, and Setup. +- Consolidate Platform + Policy under Ops, move Topology under Setup, and remove compatibility redirects/alias routing. +- Complete missing distinct surfaces called out in advisory gap review (release/hotfix split, security detail surfaces, topology map/connectivity/drift, evidence overview default, global header contracts). +- Working directory: `src/Web/StellaOps.Web/`. +- Expected evidence: route/nav contract changes, distinct page surfaces, updated docs, and targeted frontend tests. + +## Dependencies & Concurrency +- Depends on current Pack-22/23 contracts in `docs/modules/ui/v2-rewire/` and existing pre-alpha frontend branch state. +- Safe parallelism: frontend-only changes in `src/Web/StellaOps.Web/`; docs updates in `docs/modules/ui/v2-rewire/` can run in parallel if contracts stay consistent. + +## Documentation Prerequisites +- `docs/modules/ui/README.md` +- `docs/modules/ui/architecture.md` +- `docs/modules/ui/implementation_plan.md` +- `docs/modules/platform/architecture-overview.md` +- `docs/modules/ui/v2-rewire/source-of-truth.md` + +## Delivery Tracker + +### 041-T1 - Root IA/nav rewrite (Mission Control + Ops + Setup) +Status: DONE +Dependency: none +Owners: Developer (FE) +Task description: +- Rewrite shell navigation roots to: Mission Control, Releases, Security, Evidence, Ops, Setup. +- Remove Topology as root; place topology entries under Setup. +- Remove Platform and Policy roots; expose their capabilities under Ops. + +Completion criteria: +- [x] Sidebar root groups match target IA and scope gates +- [x] No root menu points to legacy Platform/Policy/Administration/Topology roots + +### 041-T2 - Canonical route tree rebuild (no alias redirects) +Status: DONE +Dependency: 041-T1 +Owners: Developer (FE) +Task description: +- Rebuild `app.routes.ts` and child route trees to pre-alpha canonical paths. +- Remove legacy alias windows and redirect maps used for backward compatibility. +- Keep only canonical pre-alpha route entries needed for first-version UX. + +Completion criteria: +- [x] `LEGACY_REDIRECT_ROUTES` not used by router +- [x] Canonical route trees are reachable without alias redirects + +### 041-T3 - Mission Control menu expansion +Status: DONE +Dependency: 041-T1 +Owners: Developer (FE) +Task description: +- Split Mission Control into distinct root-visible surfaces (Mission Board, Alerts, Activity). +- Ensure breadcrumb/title metadata is distinct for each surface. + +Completion criteria: +- [x] Mission Control has dedicated routes for board/alerts/activity +- [x] Sidebar children and route metadata are aligned + +### 041-T4 - Ops consolidation (Platform + Policy + Integrations) +Status: DONE +Dependency: 041-T2 +Owners: Developer (FE) +Task description: +- Move Policy governance/simulation/profile surfaces under Ops. +- Move Platform ops/integrations/setup controls under Ops taxonomy. +- Provide one canonical Ops route tree with explicit sub-areas. + +Completion criteria: +- [x] Policy capabilities are routed under `/ops/**` +- [x] Platform capabilities are routed under `/ops/**` + +### 041-T5 - Setup consolidation (Administration renamed + Topology moved) +Status: DONE +Dependency: 041-T2 +Owners: Developer (FE) +Task description: +- Rename Administration workspace to Setup. +- Move topology surfaces (overview/map/targets/hosts/agents/connectivity/drift/details) into Setup. +- Keep IAM/tenant/notifications/usage/system in Setup. + +Completion criteria: +- [x] Setup route tree includes admin + topology surfaces +- [x] Topology is no longer a root route domain + +### 041-T6 - Releases completion and full hotfix split +Status: DONE +Dependency: 041-T2 +Owners: Developer (FE) +Task description: +- Add missing Releases surfaces: overview, promotion queue, environments inventory, environment detail, deployment history. +- Fully split hotfix flows into list/create/detail screens. +- Keep run-centric detail as source of truth while exposing distinct list-level surfaces. + +Completion criteria: +- [x] Releases menu exposes all required pre-alpha screens +- [x] Hotfix has separate list/create/detail routes + +### 041-T7 - Security split completion and distinct reports +Status: DONE +Dependency: 041-T2 +Owners: Developer (FE) +Task description: +- Add first-class Advisories & VEX and Supply-Chain Data surfaces. +- Add dedicated detail routes for CVE/component/artifact/environment-risk views. +- Implement distinct Security Reports page/component (not reused disposition view). + +Completion criteria: +- [x] Security detail routes exist and are linked from triage +- [x] `/security/reports` is a distinct screen/component + +### 041-T8 - Evidence default and topology detail gaps +Status: DONE +Dependency: 041-T2 +Owners: Developer (FE) +Task description: +- Make Evidence Overview the default landing page. +- Add missing topology surfaces: map/connectivity/runtime-drift/detail pages where absent. +- Preserve evidence capsule/replay/export/audit flows with distinct route contracts. + +Completion criteria: +- [x] `/evidence` lands on overview +- [x] Setup topology includes map/connectivity/runtime-drift/detail routes + +### 041-T9 - Global header contract completion +Status: DONE +Dependency: 041-T1 +Owners: Developer (FE) +Task description: +- Add contextual primary action slot/button in top bar by active workspace. +- Add stage selector to global scope controls. +- Add live event stream status chip. + +Completion criteria: +- [x] Topbar renders contextual primary action per workspace +- [x] Scope controls include Stage +- [x] Context chips include Live Event Stream state + +### 041-T10 - Integrations consolidation for advisory + VEX +Status: DONE +Dependency: 041-T4 +Owners: Developer (FE) +Task description: +- Replace split advisory feeds + VEX source menus with unified Advisory & VEX Sources surface. +- Keep internal tabs for provider families while preserving integration management capabilities. + +Completion criteria: +- [x] Unified Advisory & VEX sources route exists under Ops Integrations +- [x] Old split menu entries removed from canonical nav + +### 041-T11 - Docs sync for new pre-alpha IA +Status: DONE +Dependency: 041-T1, 041-T2, 041-T4, 041-T5 +Owners: Documentation author, Developer (FE) +Task description: +- Update v2-rewire source-of-truth docs and route map docs to reflect new canonical IA. +- Record rationale and removed redirect policy for pre-alpha first release. + +Completion criteria: +- [x] Docs describe canonical roots and route tree accurately +- [x] Sprint decisions reference updated doc paths + +### 041-T12 - Targeted tests and verification evidence +Status: DONE +Dependency: 041-T6, 041-T7, 041-T8, 041-T9, 041-T10 +Owners: Developer (FE), QA +Task description: +- Update/create targeted tests for nav roots, route contracts, scope chips, and canonical pages. +- Execute targeted test command set and log results. + +Completion criteria: +- [x] Targeted tests for changed contracts are passing +- [x] Execution Log contains command and summary evidence + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-02-21 | Sprint created; 041-T1 moved to DOING; implementation started for pre-alpha IA root/nav and route consolidation. | Developer (FE) | +| 2026-02-21 | Completed canonical IA rewire: roots switched to Mission Control/Releases/Security/Evidence/Ops/Setup, Topology moved under Setup, Platform+Policy consolidated under Ops, and legacy redirect route trees retired. Updated docs `docs/modules/ui/v2-rewire/source-of-truth.md` and `docs/modules/ui/v2-rewire/S00_route_deprecation_map.md`. | Developer (FE) | +| 2026-02-21 | Verification evidence: `npm run build` succeeded; targeted tests passed: `npm run test -- --watch=false --include=\"src/app/app.component.spec.ts\" --include=\"src/tests/navigation/legacy-redirects.spec.ts\" --include=\"src/tests/platform-ops/platform-ops-routes.spec.ts\" --include=\"src/tests/release-control/release-control-routes.spec.ts\" --include=\"src/tests/evidence-audit/evidence-audit-routes.spec.ts\" --include=\"src/tests/platform/platform-setup-routes.spec.ts\" --include=\"src/tests/dashboard/dashboard-route-aliases.spec.ts\" --include=\"src/tests/context/platform-context-url-sync.service.spec.ts\" --include=\"src/tests/audit_log/unified-audit-log-viewer.behavior.spec.ts\"`. | Developer (FE) | +| 2026-02-21 | End-user Playwright validation completed for canonical IA pages: `npm run test:e2e -- tests/e2e/prealpha-canonical-full-sweep.spec.ts` passed (`105/105`). Additional interaction/a11y surfaces were stabilized and verified with `npm run test:e2e -- tests/e2e/accessibility.spec.ts tests/e2e/witness-drawer.spec.ts tests/e2e/witness-viewer.spec.ts tests/e2e/workflow-time-travel.spec.ts tests/e2e/graph-platform-client.spec.ts` (`36 passed, 8 intentionally skipped`). | QA / Developer (FE) | +| 2026-02-21 | Full-suite Playwright rerun after stabilization: `npm run test:e2e` now reports `166 passed`, `195 skipped`, `53 failed`, `33 did not run` (down from prior 80 failures). Remaining failures are concentrated in legacy-route/old-workbench contracts (`web-checked-feature-recheck`, old login/auth-smoke assumptions, pack-conformance legacy expectations). | QA / Developer (FE) | + +## Decisions & Risks +- Decision: pre-alpha policy removes backward-compatibility aliases/redirects; canonical routes only. +- Decision: Ops is the single operational root for former Platform + Policy + Integrations surfaces. +- Decision: Setup is the single configuration/governance root for former Administration + Topology. +- Risk: existing tests and bookmarks built for alias windows will fail/break by design; mitigated by updating tests and docs in this sprint. +- Risk: broad route refactor may cause temporary dead links during implementation; mitigated by staged task completion and verification passes. +- Cross-module note: docs updates are required under `docs/modules/ui/v2-rewire/` to keep code/docs contracts aligned. +- Residual risk: full `npm run test -- --watch=false` still reports numerous unrelated failing suites in this branch baseline; sprint verification therefore uses targeted route/nav contract tests for changed surfaces. +- Residual risk: full `npm run test:e2e` still includes many legacy/unchecked suites asserting removed redirect-era routes or older workbench contracts; canonical pre-alpha route suite is green and should be treated as release gate for this IA cut. + +## Next Checkpoints +- 2026-02-21 UTC: Root IA/nav + route tree merged to canonical pre-alpha shape. +- 2026-02-22 UTC: Releases/Security/Evidence/Setup surface completion + docs sync. +- 2026-02-22 UTC: Targeted test verification and sprint status review. diff --git a/docs/modules/ui/TASKS.md b/docs/modules/ui/TASKS.md new file mode 100644 index 000000000..3884a38f6 --- /dev/null +++ b/docs/modules/ui/TASKS.md @@ -0,0 +1,18 @@ +# UI Task Board + +## Active Sprint Links +- `docs/implplan/SPRINT_20260221_041_FE_prealpha_ia_ops_setup_rewire.md` + +## Delivery Tasks +- [DONE] 041-T1 Root IA/nav rewrite (Mission Control + Ops + Setup) +- [DONE] 041-T2 Canonical route tree rebuild (no alias redirects) +- [DONE] 041-T3 Mission Control menu expansion +- [DONE] 041-T4 Ops consolidation (Platform + Policy + Integrations) +- [DONE] 041-T5 Setup consolidation (Administration renamed + Topology moved) +- [DONE] 041-T6 Releases completion and full hotfix split +- [DONE] 041-T7 Security split completion and distinct reports +- [DONE] 041-T8 Evidence default and topology detail gaps +- [DONE] 041-T9 Global header contract completion +- [DONE] 041-T10 Integrations consolidation for advisory + VEX +- [DONE] 041-T11 Docs sync for new pre-alpha IA +- [DONE] 041-T12 Targeted tests and verification evidence diff --git a/docs/modules/ui/v2-rewire/S00_route_deprecation_map.md b/docs/modules/ui/v2-rewire/S00_route_deprecation_map.md index 96d91eaeb..b1649530b 100644 --- a/docs/modules/ui/v2-rewire/S00_route_deprecation_map.md +++ b/docs/modules/ui/v2-rewire/S00_route_deprecation_map.md @@ -1,109 +1,59 @@ -# S00 Route Deprecation Map (Pack 22/23 Canonical) +# S00 Route Retirement Map (Pre-Alpha Canonical) Status: Active -Date: 2026-02-20 +Date: 2026-02-21 Working directory: `docs/modules/ui/v2-rewire` -Canonical source: `source-of-truth.md`, `pack-22.md` +Canonical source: `source-of-truth.md`, `pack-22.md`, `pack-23.md` ## Purpose -Define deterministic route migration from pre-Pack22 root families to Pack22/23 canonical IA: +Define the pre-alpha routing contract where only canonical roots are supported and legacy families are retired. -- `/dashboard` (Mission Control) -- `/releases` (run-centric subroots under `/releases/versions*` and `/releases/runs*`) -- `/security` (workspace subroots under `/security/posture`, `/security/triage`, `/security/disposition`, `/security/sbom/*`, `/security/reachability`) -- `/evidence` (capsule-first subroots under `/evidence/capsules`, `/evidence/exports`, `/evidence/verification/*`, `/evidence/audit-log`) -- `/topology` -- `/platform` (ops/integrations/setup canonical root; legacy `/operations`, `/integrations`, `/administration` are alias-window routes) +Canonical root families: + +- `/` and `/mission-control` +- `/releases` +- `/security` +- `/evidence` +- `/ops` +- `/setup` ## Action definitions | Action | Meaning | | --- | --- | -| `canonical` | Route family is authoritative and must be used by nav and breadcrumbs. | -| `redirect` | Legacy route redirects to canonical route. | -| `alias-window` | Legacy route remains temporarily available and is tracked via alias telemetry. | +| `canonical` | Route family is authoritative and must be used by nav, breadcrumbs, and deep links. | +| `retired` | Legacy route family is removed from active router trees. No compatibility redirects or alias windows. | -## Root family mapping +## Root family status -| Legacy root family | Canonical target | Action | +| Legacy root family | Canonical replacement | Action | | --- | --- | --- | -| `/release-control/*` | split between `/releases/*` and `/topology/*` | `redirect` + `alias-window` | -| `/security-risk/*` | `/security/*` | `redirect` + `alias-window` | -| `/evidence-audit/*` | `/evidence/*` | `redirect` + `alias-window` | -| `/platform-ops/*` | `/platform/ops/*` | `redirect` + `alias-window` | -| `/operations/*` (old ops shell) | `/platform/ops/*` | `redirect` + `alias-window` | -| `/integrations/*` (legacy root) | `/platform/integrations/*` | `redirect` + `alias-window` | -| `/administration/*` (legacy root) | `/platform/setup/*` | `redirect` + `alias-window` | -| `/settings/release-control/*` | `/topology/promotion-graph`, `/topology/regions`, `/topology/targets`, `/topology/agents`, `/topology/workflows` | `redirect` | +| `/release-control/*` | `/releases/*` and `/setup/topology/*` | `retired` | +| `/security-risk/*` | `/security/*` | `retired` | +| `/evidence-audit/*` | `/evidence/*` | `retired` | +| `/platform-ops/*` | `/ops/operations/*` | `retired` | +| `/platform/*` | `/ops/*` | `retired` | +| `/policy*` (root-level variants) | `/ops/policy/*` | `retired` | +| `/topology/*` (root-level) | `/setup/topology/*` | `retired` | +| `/administration/*` | `/setup/*` | `retired` | +| `/operations/*` (legacy root) | `/ops/operations/*` | `retired` | +| `/integrations/*` (legacy root) | `/ops/integrations/*` | `retired` | -## Release Control decomposition +## Canonical ownership map -| Legacy path | Canonical target | Action | -| --- | --- | --- | -| `/release-control/releases` | `/releases/runs` | `redirect` | -| `/release-control/releases/:id` | `/releases/runs/:id/timeline` | `redirect` | -| `/release-control/approvals` | `/releases/approvals` | `redirect` | -| `/release-control/runs` | `/releases/runs` | `redirect` | -| `/release-control/deployments` | `/releases/runs` | `redirect` | -| `/release-control/promotions` | `/releases/runs` | `redirect` | -| `/release-control/hotfixes` | `/releases/hotfix` | `redirect` | -| `/release-control/regions` | `/topology/regions` | `redirect` | -| `/release-control/setup` | `/topology/promotion-graph` | `redirect` | -| `/release-control/setup/environments-paths` | `/topology/promotion-graph` | `redirect` | -| `/release-control/setup/targets-agents` | `/topology/targets` | `redirect` | -| `/release-control/setup/workflows` | `/topology/workflows` | `redirect` | +| Workspace | Scope | +| --- | --- | +| `Mission Control` | Mission board, alerts, activity | +| `Releases` | Versions, runs, approvals, hotfix lane, promotions, environments, deployment history | +| `Security` | Posture, triage, advisories/VEX, supply-chain data, reachability, reports | +| `Evidence` | Overview, capsules, verify/replay, proofs, exports, audit log | +| `Ops` | Operations, data integrity, integrations, policy, platform setup | +| `Setup` | Administration surfaces + topology (overview/map/targets/hosts/agents/connectivity/drift) | -## Settings alias decomposition +## Enforcement checkpoints -| Legacy path | Canonical target | Action | -| --- | --- | --- | -| `/settings/release-control` | `/topology/promotion-graph` | `redirect` | -| `/settings/release-control/environments` | `/topology/regions` | `redirect` | -| `/settings/release-control/targets` | `/topology/targets` | `redirect` | -| `/settings/release-control/agents` | `/topology/agents` | `redirect` | -| `/settings/release-control/workflows` | `/topology/workflows` | `redirect` | - -## Security consolidation - -| Legacy path | Canonical target | Action | -| --- | --- | --- | -| `/security-risk` | `/security/posture` | `redirect` | -| `/security-risk/findings*` | `/security/triage*` | `redirect` | -| `/security-risk/vulnerabilities*` | `/security/triage*` | `redirect` | -| `/security-risk/vex` | `/security/disposition` | `redirect` | -| `/security-risk/exceptions` | `/security/disposition` | `redirect` | -| `/security-risk/sbom` | `/security/sbom/graph` | `redirect` | -| `/security-risk/sbom-lake` | `/security/sbom/lake` | `redirect` | -| `/security-risk/advisory-sources` | `/platform/integrations/feeds` | `redirect` | -| `/sbom-sources` | `/platform/integrations/sbom-sources` | `redirect` | - -## Evidence and Operations renames - -| Legacy path | Canonical target | Action | -| --- | --- | --- | -| `/evidence-audit` | `/evidence/capsules` | `redirect` | -| `/evidence-audit/packs*` | `/evidence/capsules*` | `redirect` | -| `/evidence-audit/audit-log` | `/evidence/audit-log` | `redirect` | -| `/evidence-audit/replay` | `/evidence/verification/replay` | `redirect` | -| `/platform-ops` | `/platform/ops` | `redirect` | -| `/platform-ops/data-integrity` | `/platform/ops/data-integrity` | `redirect` | -| `/platform-ops/orchestrator*` | `/platform/ops/orchestrator*` | `redirect` | -| `/platform-ops/agents` | `/topology/agents` | `redirect` | - -## Telemetry expectations - -- Legacy alias hits must emit deterministic `legacy_route_hit` telemetry with: - - `oldPath`, - - `newPath`, - - tenant/user context metadata. -- Legacy detection and expected target resolution are derived from `LEGACY_REDIRECT_ROUTE_TEMPLATES` to prevent drift between redirect behavior and telemetry mapping. -- Alias telemetry must remain active until Pack22 cutover approval. - -## Cutover checkpoint - -Before alias removal: - -- Legacy hit rate for `/release-control/*`, `/security-risk/*`, `/evidence-audit/*`, `/platform-ops/*` is reviewed. -- Route-to-endpoint matrix in `docs/qa/` confirms canonical routes are using Pack22 endpoints. -- Sprint closure notes record alias telemetry evidence and final removal plan. +- App-level route declarations must not include legacy redirect route maps. +- No `redirectTo` entries are permitted in active pre-alpha route trees. +- Search shortcuts, contextual primary actions, and sidebar links must target canonical roots only. +- QA route tests must assert retired legacy trees are empty and canonical trees are present. diff --git a/docs/modules/ui/v2-rewire/source-of-truth.md b/docs/modules/ui/v2-rewire/source-of-truth.md index 166a61d20..88d9b8251 100644 --- a/docs/modules/ui/v2-rewire/source-of-truth.md +++ b/docs/modules/ui/v2-rewire/source-of-truth.md @@ -1,7 +1,7 @@ # UI v2 Rewire Source of Truth Status: Active -Date: 2026-02-20 +Date: 2026-02-21 Working directory: `docs/modules/ui/v2-rewire` ## 1) Hard rules @@ -15,6 +15,7 @@ Working directory: `docs/modules/ui/v2-rewire` 4. Canonical planning references must come from this file plus `authority-matrix.md`, not raw packs alone. 5. `pack-23.md` is the active Platform IA override for all conflicts with `pack-22.md` and lower packs. 6. `pack-22.md` remains authority for non-Platform areas unless `pack-23.md` explicitly overrides them. +7. Pre-alpha policy is canonical-only routing: no legacy redirects and no alias windows. ## 2) Canonical IA (v3) @@ -26,8 +27,8 @@ Canonical top-level modules are: - `Releases` - `Security` - `Evidence` -- `Topology` -- `Platform` +- `Ops` +- `Setup` ### 2.2 Global context @@ -39,7 +40,8 @@ Required global context controls: - Region multi-select - Environment multi-select scoped to Region selection - Time window selector -- Status indicators (offline/feed/policy/evidence) +- Stage selector +- Status indicators (offline/feed/policy/evidence/live event stream) ### 2.3 Ownership decisions resolved by precedence @@ -47,16 +49,17 @@ These are authoritative for planning and replace older conflicting placements: - `Release Control` root is decomposed: - release lifecycle surfaces move to `Releases`, - - inventory/setup surfaces move to `Topology`. + - inventory/topology surfaces move to `Setup -> Topology`. - `Bundle` is deprecated in operator IA and renamed to `Release Version`. - `Runs`, `Deployments`, `Promotions`, and `Hotfixes` are lifecycle views inside `Releases` and not top-level modules. - `VEX` and `Exceptions` remain distinct data models, but are exposed in one operator workspace: - `Security -> Disposition Center` tabs (`VEX Statements`, `Exceptions`, `Expiring`), - - feeds/source configuration lives in `Platform -> Integrations -> Feeds`. + - feeds/source configuration lives in `Ops -> Integrations -> Advisory & VEX Sources`. - SBOM Graph/Lake are one `Security -> SBOM` workspace with mode tabs. - Reachability is a first-class surface under `Security -> Reachability`. -- `Policy Governance` remains administration-owned under `Platform -> Setup`. -- Trust posture is visible in `Evidence`, while signing/trust mutation stays in `Platform -> Setup -> Trust & Signing`. +- Topology ownership is setup-owned under `Setup -> Topology`. +- Policy and former Platform ownership are consolidated under `Ops`. +- Trust posture is visible in `Evidence`, while signing/trust mutation stays under `Ops` setup/policy surfaces. ## 3) Canonical screen authorities @@ -95,11 +98,12 @@ Superseded: - Standalone menu treatment from earlier packs where runs/deployments/promotions/hotfixes were separate roots. -### 3.4 Topology +### 3.4 Setup + Topology Authoritative packs: -- `pack-22.md` for module ownership and taxonomy. +- `pack-22.md` for topology taxonomy and environment detail structure. +- `pack-23.md` for platform ownership moves now consolidated under `Ops`. - `pack-18.md` for environment detail shell standards reused inside topology-aware views. ### 3.5 Security @@ -120,11 +124,11 @@ Authoritative packs: - `pack-22.md` for evidence navigation framing and release linkage expectations. - `pack-20.md` for evidence chain structure (packs/export/proof/replay/audit). -### 3.7 Operations +### 3.7 Ops Authoritative packs: -- `pack-23.md` for Platform Ops placement and workflow prioritization. +- `pack-23.md` for Platform + Policy + Integrations consolidation under one root. - `pack-15.md` for data integrity operating model. - `pack-10.md` for feeds/airgap operational detail where still valid. @@ -135,7 +139,7 @@ Authoritative packs: - `pack-23.md` for Platform Integrations placement and topology ownership split. - `pack-10.md` and `pack-21.md` for connector detail flows where not overridden. -### 3.9 Platform Administration +### 3.9 Setup Administration Authoritative packs: @@ -153,10 +157,12 @@ Use these terms in sprint tickets/specs: - `Security & Risk` -> `Security` - `Evidence & Audit` -> `Evidence` - `Evidence Pack/Bundle` -> `Decision Capsule` -- `Platform Ops` -> `Platform -> Ops` -- `Integrations` root -> `Platform -> Integrations` (alias-window only at `/integrations`) -- `Setup` root -> `Platform -> Setup` (includes administration-owned setup/governance) -- `Regions & Environments` menu -> `Topology` module + global context switchers +- `Platform Ops` -> `Ops` +- `Policy` -> `Ops -> Policy` +- `Integrations` -> `Ops -> Integrations` +- `Administration` -> `Setup` +- `Topology` root -> `Setup -> Topology` +- `Regions & Environments` menu -> `Setup -> Topology` + global context switchers ## 5) Planning gaps to schedule first @@ -164,6 +170,6 @@ Create first-wave dependency sprints for: - backend global context contracts and persistence (`Region/Environment` top-bar model), - releases read-model contracts for list/detail/activity/approvals queue, -- topology inventory contracts and synchronization, +- setup-owned topology inventory contracts and synchronization, - security disposition aggregation contracts (VEX + Exceptions UX join), -- route deprecation map from `/release-control/*`, `/security-risk/*`, `/evidence-audit/*`, `/platform-ops/*` to canonical paths. +- route retirement cleanup from legacy families to canonical pre-alpha roots with no redirect compatibility layer. diff --git a/docs/quickstart.md b/docs/quickstart.md index 4b2a1ecb6..512136b45 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -1,98 +1,90 @@ -# Quickstart – First Scan in Five Minutes +# Quickstart -- Local Dev Environment in Minutes -> **Status:** public α image ships late 2025 (`registry.stella-ops.org/stella-ops/stella-ops:0.1.0-alpha`). Commands below are ready the moment the tag lands. +Get Stella Ops running locally for development and evaluation. -## 0. Prerequisites (1 min) +> This guide is for **local development**. For production deployment, see the [Installation Guide](INSTALL_GUIDE.md). -| Requirement | Minimum | Notes | -|-------------|---------|-------| -| OS | Ubuntu 22.04 LTS / Alma 9 | x86‑64 or arm64 | -| Docker | Engine 25 + Compose v2 | `docker -v` | -| Resources | 2 vCPU / 2 GiB RAM / 10 GiB SSD | Fits developer laptops | -| TLS trust | Built-in self-signed or your own certs | Replace `/certs` before production | +## Prerequisites -Keep Valkey and PostgreSQL bundled unless you already operate managed instances. +| Requirement | Minimum | Verify | +|-------------|---------|--------| +| OS | Windows 10+, macOS 12+, Ubuntu 22.04+ | x86-64 or arm64 | +| Docker | Engine 20.10+ with Compose v2 | `docker compose version` | +| .NET SDK | 10.x | `dotnet --version` | +| Node.js | 20+ | `node --version` | +| RAM | 16 GB (32 GB recommended) | | +| Disk | 50 GB free | | -## 1. Download the signed bundles (1 min) +## 1. Clone the repository ```bash -curl -LO https://get.stella-ops.org/docker-compose.infrastructure.yml -curl -LO https://get.stella-ops.org/docker-compose.infrastructure.yml.sig -curl -LO https://get.stella-ops.org/docker-compose.stella-ops.yml -curl -LO https://get.stella-ops.org/docker-compose.stella-ops.yml.sig - -cosign verify-blob \ - --key https://stella-ops.org/keys/cosign.pub \ - --signature docker-compose.infrastructure.yml.sig \ - docker-compose.infrastructure.yml - -cosign verify-blob \ - --key https://stella-ops.org/keys/cosign.pub \ - --signature docker-compose.stella-ops.yml.sig \ - docker-compose.stella-ops.yml +git clone /stella-ops/stella-ops.git +cd stella-ops ``` -*Air-gapped?* The [Offline Update Kit](OFFLINE_KIT.md) ships these files plus feeds and plug-ins. +## 2. Run the setup script -## 2. Configure `.env` (1 min) +**Windows (PowerShell 7):** -Create `.env` with the essentials: - -```dotenv -STELLA_OPS_COMPANY_NAME="Acme Corp" -STELLA_OPS_DEFAULT_ADMIN_USERNAME="admin" -STELLA_OPS_DEFAULT_ADMIN_PASSWORD="change-me!" -POSTGRES_USER=stella_admin -POSTGRES_PASSWORD=$(openssl rand -base64 18) -POSTGRES_HOST=postgres -VALKEY_PASSWORD=$(openssl rand -base64 18) -VALKEY_URL=valkey +```powershell +.\scripts\setup.ps1 ``` -Use existing Valkey/PostgreSQL endpoints by setting `POSTGRES_HOST` and `VALKEY_URL`. Keep credentials scoped to Stella Ops; Valkey counters enforce the transparent quota (`{{ quota_token }}` scans/day). - -## 3. Launch services (1 min) +**Linux / macOS:** ```bash -docker compose --env-file .env -f docker-compose.infrastructure.yml up -d -docker compose --env-file .env -f docker-compose.stella-ops.yml up -d +./scripts/setup.sh ``` -- `StellaOps.Authority` issues short-lived OpToks for CLI/UI. -- `StellaOps.Scanner` hosts `/scan`, queues work to Workers. -- `StellaOps.Policy.Engine` and `StellaOps.Concelier` start with seeded policies, feeds sync in the background. +The script will: +- Verify all prerequisites are installed +- Offer to add hosts file entries (50 services need unique loopback IPs) +- Create `.env` from the example template (works out of the box, no editing needed) +- Start infrastructure (PostgreSQL, Valkey, RustFS) +- Build .NET solutions and Docker images +- Launch the full platform +- Run health checks and report status -## 4. Run your first scan (1 min) +### Infrastructure only (faster) -```bash -stella auth login \ - --device-code \ - --audiences scanner,attestor \ - --scopes attestor.verify,attestor.read -stella scan image \ - --image registry.stella-ops.org/demo/juice-shop:latest \ - --sbom-type cyclonedx-json +To skip builds and just start infrastructure: + +```powershell +.\scripts\setup.ps1 -InfraOnly # Windows +./scripts/setup.sh --infra-only # Linux/macOS ``` -- Expect `<5 s` warm scans once the Delta SBOM cache is primed. -- CLI exits non-zero if lattice policy blocks the image; use `stella policy explain --last` for context. -- Headers `X-Stella-Quota-Remaining` and the UI banner keep quota usage transparent. +## 3. Open the platform -> Need to inspect attestations only? Swap `attestor.verify` for `attestor.read`. Submission endpoints still need `attestor.write`. +Once setup completes, open **https://stella-ops.local** in your browser. -## 5. Verify & explore (1 min) +Accept the self-signed certificate warning on first visit. -- Check the Console (`https://localhost:8443`) to view findings, VEX evidence, and deterministic replay manifests. -- Export the DSSE bundle: `stella export run --format dsse`. -- Capture evidence for audit: `stella attest bundle --output demo.dsse.json`. +## What's running -### Sovereign mode in one click +After a full setup, you'll have 45+ services running locally: -- Import the Offline Update Kit (`stella offline-kit import ./stella-ouk-2025-alpha.tar.gz`) to replace every external feed. -- Apply a CryptoProfile (`stella authority crypto apply ./profiles/fips.yaml`) to swap signing algorithms without rebuilding. +| Service | URL | Purpose | +|---------|-----|---------| +| Web UI | https://stella-ops.local | Angular console | +| Authority | https://authority.stella-ops.local | OAuth2/OIDC | +| Scanner | https://scanner.stella-ops.local | SBOM/vulnerability scanning | +| Concelier | https://concelier.stella-ops.local | Advisory aggregation | +| PostgreSQL | db.stella-ops.local:5432 | Primary database | +| Valkey | cache.stella-ops.local:6379 | Cache and messaging | -### Next steps +Full service list: `devops/compose/docker-compose.stella-ops.yml` -- Harden the deployment with [SECURITY_HARDENING_GUIDE.md](SECURITY_HARDENING_GUIDE.md). -- Explore feature highlights in [`key-features.md`](key-features.md). -- Plan the rollout using the [evaluation checklist](onboarding/evaluation-checklist.md). +## Troubleshooting + +**"stella-ops.local not found"** -- The hosts file entries are missing. Re-run the setup script and accept the hosts file installation, or manually append `devops/compose/hosts.stellaops.local` to your hosts file. + +**Containers unhealthy** -- Check logs with `docker compose -f devops/compose/docker-compose.stella-ops.yml logs `. + +**Port conflicts** -- Override ports in `devops/compose/.env`. See `devops/compose/env/stellaops.env.example` for available port variables. + +## Next steps + +- [Developer setup details](dev/DEV_ENVIRONMENT_SETUP.md) -- manual steps, hybrid debugging, building individual modules +- [Installation Guide](INSTALL_GUIDE.md) -- production deployment, air-gap, regional compliance +- [Architecture overview](ARCHITECTURE_OVERVIEW.md) -- how the platform fits together diff --git a/scripts/setup.ps1 b/scripts/setup.ps1 index b9bcda152..6100fe459 100644 --- a/scripts/setup.ps1 +++ b/scripts/setup.ps1 @@ -141,22 +141,57 @@ function Test-Prerequisites { } } -# ─── 2. Check hosts file ─────────────────────────────────────────────────── +# ─── 2. Check and install hosts file ───────────────────────────────────── function Test-HostsFile { Write-Step 'Checking hosts file for stella-ops.local entries' $hostsPath = 'C:\Windows\System32\drivers\etc\hosts' - if (Test-Path $hostsPath) { - $content = Get-Content $hostsPath -Raw - if ($content -match 'stella-ops\.local') { - Write-Ok 'stella-ops.local entries found in hosts file' + $hostsSource = Join-Path $Root 'devops/compose/hosts.stellaops.local' + + if (-not (Test-Path $hostsPath)) { + Write-Warn "Cannot read hosts file at $hostsPath" + return + } + + $content = Get-Content $hostsPath -Raw + if ($content -match 'stella-ops\.local') { + Write-Ok 'stella-ops.local entries found in hosts file' + return + } + + Write-Warn 'stella-ops.local entries NOT found in hosts file.' + + if (-not (Test-Path $hostsSource)) { + Write-Warn "Hosts source file not found at $hostsSource" + Write-Host ' Add the hosts block from docs/dev/DEV_ENVIRONMENT_SETUP.md section 2' -ForegroundColor Yellow + return + } + + # Check if running as Administrator + $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + + if ($isAdmin) { + Write-Host '' + Write-Host ' Stella Ops needs ~50 hosts file entries for local development.' -ForegroundColor Yellow + Write-Host ' Source: devops/compose/hosts.stellaops.local' -ForegroundColor Yellow + Write-Host '' + $answer = Read-Host ' Add entries to hosts file now? (Y/n)' + if ($answer -eq '' -or $answer -match '^[Yy]') { + $hostsBlock = Get-Content $hostsSource -Raw + Add-Content -Path $hostsPath -Value "`n$hostsBlock" + Write-Ok 'Hosts entries added successfully' } else { - Write-Warn 'stella-ops.local entries NOT found in hosts file.' - Write-Host ' Add the hosts block from docs/dev/DEV_ENVIRONMENT_SETUP.md section 2' -ForegroundColor Yellow - Write-Host ' to C:\Windows\System32\drivers\etc\hosts (run editor as Administrator)' -ForegroundColor Yellow + Write-Warn 'Skipped. Add them manually before accessing the platform.' + Write-Host " Copy from: $hostsSource" -ForegroundColor Yellow } } else { - Write-Warn "Cannot read hosts file at $hostsPath" + Write-Host '' + Write-Host ' Stella Ops needs ~50 hosts file entries for local development.' -ForegroundColor Yellow + Write-Host ' To install them, run this command in an elevated (Administrator) PowerShell:' -ForegroundColor Yellow + Write-Host '' + Write-Host " Get-Content '$hostsSource' | Add-Content '$hostsPath'" -ForegroundColor White + Write-Host '' + Write-Host ' Or re-run this script as Administrator to install them automatically.' -ForegroundColor Yellow } } @@ -172,7 +207,7 @@ function Initialize-EnvFile { } elseif (Test-Path $envExample) { Copy-Item $envExample $envFile Write-Ok "Copied $envExample -> $envFile" - Write-Warn 'Review .env and change POSTGRES_PASSWORD at minimum.' + Write-Warn 'For production, change POSTGRES_PASSWORD in .env.' } else { Write-Fail "Neither .env nor env/stellaops.env.example found in $ComposeDir" exit 1 @@ -280,6 +315,8 @@ function Start-Platform { function Test-Smoke { Write-Step 'Running smoke tests' + + # Infrastructure checks $endpoints = @( @{ Name = 'PostgreSQL'; Cmd = { docker exec stellaops-dev-postgres pg_isready -U stellaops 2>$null; $LASTEXITCODE -eq 0 } }, @{ Name = 'Valkey'; Cmd = { $r = docker exec stellaops-dev-valkey valkey-cli ping 2>$null; $r -eq 'PONG' } } @@ -293,6 +330,59 @@ function Test-Smoke { Write-Warn "$($ep.Name) check failed: $_" } } + + # Platform container health summary + Write-Step 'Container health summary' + Push-Location $ComposeDir + try { + $composeFiles = @('docker-compose.dev.yml', 'docker-compose.stella-ops.yml') + $totalContainers = 0 + $healthyContainers = 0 + $unhealthyNames = @() + + foreach ($cf in $composeFiles) { + if (-not (Test-Path $cf)) { continue } + $ps = docker compose -f $cf ps --format json 2>$null + if (-not $ps) { continue } + foreach ($line in $ps -split "`n") { + $line = $line.Trim() + if (-not $line) { continue } + try { + $svc = $line | ConvertFrom-Json + $totalContainers++ + if (-not $svc.Health -or $svc.Health -eq 'healthy') { + $healthyContainers++ + } else { + $unhealthyNames += $svc.Name + } + } catch {} + } + } + + if ($totalContainers -gt 0) { + if ($healthyContainers -eq $totalContainers) { + Write-Ok "$healthyContainers/$totalContainers containers healthy" + } else { + Write-Warn "$healthyContainers/$totalContainers containers healthy" + foreach ($name in $unhealthyNames) { + Write-Warn " Unhealthy: $name" + } + } + } + + # Platform endpoint check + try { + $tcp = New-Object System.Net.Sockets.TcpClient + $tcp.Connect('stella-ops.local', 443) + $tcp.Close() + Write-Ok 'Platform accessible at https://stella-ops.local' + } catch { + Write-Warn 'Platform not yet accessible at https://stella-ops.local (may still be starting)' + } + } + finally { + Pop-Location + } } # ─── Main ─────────────────────────────────────────────────────────────────── diff --git a/scripts/setup.sh b/scripts/setup.sh index 6d104a135..5481a25d2 100644 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -123,16 +123,51 @@ check_prerequisites() { fi } -# ─── 2. Check hosts file ─────────────────────────────────────────────────── +# ─── 2. Check and install hosts file ───────────────────────────────────── check_hosts() { step 'Checking hosts file for stella-ops.local entries' + local hosts_source="${ROOT}/devops/compose/hosts.stellaops.local" + if grep -q 'stella-ops\.local' /etc/hosts 2>/dev/null; then ok 'stella-ops.local entries found in /etc/hosts' - else - warn 'stella-ops.local entries NOT found in /etc/hosts.' + return + fi + + warn 'stella-ops.local entries NOT found in /etc/hosts.' + + if [[ ! -f "$hosts_source" ]]; then + warn "Hosts source file not found at $hosts_source" echo ' Add the hosts block from docs/dev/DEV_ENVIRONMENT_SETUP.md section 2' echo ' to /etc/hosts (use sudo).' + return + fi + + echo '' + echo ' Stella Ops needs ~50 hosts file entries for local development.' + echo " Source: devops/compose/hosts.stellaops.local" + echo '' + printf ' Add entries to /etc/hosts now? (Y/n) ' + read -r answer + + if [[ -z "$answer" || "$answer" =~ ^[Yy] ]]; then + if [[ "$(id -u)" -eq 0 ]]; then + printf '\n' >> /etc/hosts + cat "$hosts_source" >> /etc/hosts + ok 'Hosts entries added successfully' + else + echo '' + echo ' Adding hosts entries requires sudo...' + if sudo sh -c "printf '\n' >> /etc/hosts && cat '$hosts_source' >> /etc/hosts"; then + ok 'Hosts entries added successfully' + else + warn 'Failed to add hosts entries. Add them manually:' + echo " sudo sh -c 'cat $hosts_source >> /etc/hosts'" + fi + fi + else + warn 'Skipped. Add them manually before accessing the platform:' + echo " sudo sh -c 'cat $hosts_source >> /etc/hosts'" fi } @@ -148,7 +183,7 @@ ensure_env() { elif [[ -f "$env_example" ]]; then cp "$env_example" "$env_file" ok "Copied $env_example -> $env_file" - warn 'Review .env and change POSTGRES_PASSWORD at minimum.' + warn 'For production, change POSTGRES_PASSWORD in .env.' else fail "Neither .env nor env/stellaops.env.example found in $COMPOSE_DIR" exit 1 @@ -235,6 +270,7 @@ start_platform() { smoke_test() { step 'Running smoke tests' + # Infrastructure checks if docker exec stellaops-dev-postgres pg_isready -U stellaops &>/dev/null; then ok 'PostgreSQL' else @@ -247,6 +283,49 @@ smoke_test() { else warn 'Valkey not responding' fi + + # Platform container health summary + step 'Container health summary' + cd "$COMPOSE_DIR" + + local total=0 + local healthy=0 + local unhealthy_names="" + + for cf in docker-compose.dev.yml docker-compose.stella-ops.yml; do + [[ ! -f "$cf" ]] && continue + while IFS= read -r line; do + [[ -z "$line" ]] && continue + local name; name=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin).get('Name',''))" 2>/dev/null || true) + local h; h=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin).get('Health',''))" 2>/dev/null || true) + total=$((total + 1)) + if [[ -z "$h" || "$h" == "healthy" ]]; then + healthy=$((healthy + 1)) + else + unhealthy_names="${unhealthy_names} Unhealthy: ${name}\n" + fi + done < <(docker compose -f "$cf" ps --format json 2>/dev/null) + done + + if (( total > 0 )); then + if (( healthy == total )); then + ok "$healthy/$total containers healthy" + else + warn "$healthy/$total containers healthy" + [[ -n "$unhealthy_names" ]] && printf " \033[0;33m%b\033[0m" "$unhealthy_names" + fi + fi + + # Platform endpoint check + if curl -sk --connect-timeout 5 -o /dev/null -w '' https://stella-ops.local 2>/dev/null; then + ok 'Platform accessible at https://stella-ops.local' + elif bash -c "echo >/dev/tcp/stella-ops.local/443" 2>/dev/null; then + ok 'Platform listening on https://stella-ops.local (TLS handshake pending)' + else + warn 'Platform not yet accessible at https://stella-ops.local (may still be starting)' + fi + + cd "$ROOT" } # ─── Main ─────────────────────────────────────────────────────────────────── diff --git a/src/Web/StellaOps.Web/src/app/app.component.spec.ts b/src/Web/StellaOps.Web/src/app/app.component.spec.ts index 9d36619f9..733fbc8dc 100644 --- a/src/Web/StellaOps.Web/src/app/app.component.spec.ts +++ b/src/Web/StellaOps.Web/src/app/app.component.spec.ts @@ -14,6 +14,7 @@ import { OfflineModeService } from './core/services/offline-mode.service'; import { PolicyPackStore } from './features/policy-studio/services/policy-pack.store'; import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { signal } from '@angular/core'; +import { DOCTOR_API, MockDoctorClient } from './features/doctor/services/doctor.client'; class AuthorityAuthServiceStub { beginLogin = jasmine.createSpy('beginLogin'); @@ -52,6 +53,7 @@ describe('AppComponent', () => { }, }, { provide: OfflineModeService, useClass: OfflineModeServiceStub }, + { provide: DOCTOR_API, useClass: MockDoctorClient }, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting(), ] diff --git a/src/Web/StellaOps.Web/src/app/app.component.ts b/src/Web/StellaOps.Web/src/app/app.component.ts index eeed6ed40..955ef4ef2 100644 --- a/src/Web/StellaOps.Web/src/app/app.component.ts +++ b/src/Web/StellaOps.Web/src/app/app.component.ts @@ -46,7 +46,7 @@ import { PlatformContextUrlSyncService } from './core/context/platform-context-u }) export class AppComponent { private static readonly SHELL_EXCLUDED_ROUTES = [ - '/setup', + '/setup-wizard', '/welcome', '/callback', '/silent-refresh', @@ -65,6 +65,16 @@ export class AppComponent { private readonly destroyRef = inject(DestroyRef); constructor() { + const removeSplash = () => { + const splash = document.getElementById('stella-splash'); + if (!splash) { + return; + } + splash.style.opacity = '0'; + splash.style.transition = 'opacity 0.3s ease-out'; + setTimeout(() => splash.remove(), 350); + }; + // Remove the inline splash screen once the first route resolves. // This keeps the splash visible while route guards (e.g. backend probe) // are still pending, avoiding a blank screen. @@ -74,14 +84,11 @@ export class AppComponent { take(1), takeUntilDestroyed(this.destroyRef), ) - .subscribe(() => { - const splash = document.getElementById('stella-splash'); - if (splash) { - splash.style.opacity = '0'; - splash.style.transition = 'opacity 0.3s ease-out'; - setTimeout(() => splash.remove(), 350); - } - }); + .subscribe(() => removeSplash()); + + // Defensive fallback: if first navigation never settles (e.g. test/misconfigured + // backend), remove splash so the shell remains interactive. + setTimeout(() => removeSplash(), 5000); // Initialize branding on app start this.brandingService.fetchBranding().subscribe(); @@ -145,7 +152,7 @@ export class AppComponent { /** Setup wizard gets a completely chrome-free viewport. */ readonly isFullPageRoute = computed(() => { const url = this.currentUrl(); - return url === '/setup' || url.startsWith('/setup/'); + return url === '/setup-wizard' || url.startsWith('/setup-wizard/'); }); /** Hide navigation on setup/auth pages and when not authenticated. */ diff --git a/src/Web/StellaOps.Web/src/app/app.config.ts b/src/Web/StellaOps.Web/src/app/app.config.ts index b11cbce78..52571e10e 100644 --- a/src/Web/StellaOps.Web/src/app/app.config.ts +++ b/src/Web/StellaOps.Web/src/app/app.config.ts @@ -218,7 +218,7 @@ import { AnalyticsHttpClient, MockAnalyticsClient } from './core/api/analytics.c import { FEED_MIRROR_API, FEED_MIRROR_API_BASE_URL, FeedMirrorHttpClient } from './core/api/feed-mirror.client'; import { ATTESTATION_CHAIN_API, AttestationChainHttpClient } from './core/api/attestation-chain.client'; import { CONSOLE_SEARCH_API, ConsoleSearchHttpClient } from './core/api/console-search.client'; -import { POLICY_GOVERNANCE_API, MockPolicyGovernanceApi } from './core/api/policy-governance.client'; +import { POLICY_GOVERNANCE_API, HttpPolicyGovernanceApi } from './core/api/policy-governance.client'; import { POLICY_GATES_API, POLICY_GATES_API_BASE_URL, PolicyGatesHttpClient } from './core/api/policy-gates.client'; import { RELEASE_API, ReleaseHttpClient } from './core/api/release.client'; import { TRIAGE_EVIDENCE_API, TriageEvidenceHttpClient } from './core/api/triage-evidence.client'; @@ -850,10 +850,10 @@ export const appConfig: ApplicationConfig = { useExisting: ConsoleSearchHttpClient, }, // Policy Governance API - MockPolicyGovernanceApi, + HttpPolicyGovernanceApi, { provide: POLICY_GOVERNANCE_API, - useExisting: MockPolicyGovernanceApi, + useExisting: HttpPolicyGovernanceApi, }, // Policy Gates API (Policy Gateway backend) { diff --git a/src/Web/StellaOps.Web/src/app/app.routes.ts b/src/Web/StellaOps.Web/src/app/app.routes.ts index 06571a408..9018025e1 100644 --- a/src/Web/StellaOps.Web/src/app/app.routes.ts +++ b/src/Web/StellaOps.Web/src/app/app.routes.ts @@ -1,23 +1,12 @@ import { Routes } from '@angular/router'; import { - requireAuthGuard, requireAnyScopeGuard, - requireOrchViewerGuard, - requireOrchOperatorGuard, - requirePolicyAuthorGuard, - requirePolicySimulatorGuard, - requirePolicyReviewerGuard, - requirePolicyApproverGuard, - requirePolicyReviewOrApproveGuard, - requirePolicyViewerGuard, - requireAnalyticsViewerGuard, + requireAuthGuard, StellaOpsScopes, } from './core/auth'; - -import { requireConfigGuard } from './core/config/config.guard'; import { requireBackendsReachableGuard } from './core/config/backends-reachable.guard'; -import { LEGACY_REDIRECT_ROUTES } from './routes/legacy-redirects.routes'; +import { requireConfigGuard } from './core/config/config.guard'; const requireMissionControlGuard = requireAnyScopeGuard( [ @@ -62,991 +51,106 @@ const requireEvidenceGuard = requireAnyScopeGuard( '/console/profile', ); -const requireTopologyGuard = requireAnyScopeGuard( +const requireOpsGuard = requireAnyScopeGuard( [ - StellaOpsScopes.RELEASE_READ, + StellaOpsScopes.UI_ADMIN, StellaOpsScopes.ORCH_READ, StellaOpsScopes.ORCH_OPERATE, - StellaOpsScopes.UI_ADMIN, + StellaOpsScopes.HEALTH_READ, + StellaOpsScopes.NOTIFY_VIEWER, + StellaOpsScopes.POLICY_READ, + StellaOpsScopes.POLICY_AUTHOR, + StellaOpsScopes.POLICY_REVIEW, + StellaOpsScopes.POLICY_APPROVE, + StellaOpsScopes.POLICY_SIMULATE, ], '/console/profile', ); -const requirePlatformGuard = requireAnyScopeGuard( +const requireSetupGuard = requireAnyScopeGuard( [ StellaOpsScopes.UI_ADMIN, StellaOpsScopes.ORCH_READ, - StellaOpsScopes.HEALTH_READ, - StellaOpsScopes.NOTIFY_VIEWER, StellaOpsScopes.ORCH_OPERATE, + StellaOpsScopes.RELEASE_READ, ], '/console/profile', ); export const routes: Routes = [ - // ======================================================================== - // V2 CANONICAL DOMAIN ROUTES - // Canonical operator roots per source-of-truth: - // Mission Control, Releases, Security, Evidence, Topology, Platform. - // Legacy roots (/operations, /integrations, /administration, etc.) remain alias-window routes. - // ======================================================================== - - // Domain 1: Mission Control (path remains /dashboard) { path: '', pathMatch: 'full', title: 'Mission Control', canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard, requireMissionControlGuard], data: { breadcrumb: 'Mission Control' }, - loadChildren: () => - import('./routes/dashboard.routes').then( - (m) => m.DASHBOARD_ROUTES - ), + loadChildren: () => import('./routes/mission-control.routes').then((m) => m.MISSION_CONTROL_ROUTES), }, { - path: 'dashboard', + path: 'mission-control', title: 'Mission Control', canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard, requireMissionControlGuard], data: { breadcrumb: 'Mission Control' }, - loadChildren: () => - import('./routes/dashboard.routes').then( - (m) => m.DASHBOARD_ROUTES - ), + loadChildren: () => import('./routes/mission-control.routes').then((m) => m.MISSION_CONTROL_ROUTES), }, - { - path: 'control-plane', - pathMatch: 'full', - redirectTo: '/', - }, - - // Domain 2: Releases { path: 'releases', title: 'Releases', canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard, requireReleasesGuard], data: { breadcrumb: 'Releases' }, - loadChildren: () => - import('./routes/releases.routes').then( - (m) => m.RELEASES_ROUTES - ), + loadChildren: () => import('./routes/releases.routes').then((m) => m.RELEASES_ROUTES), }, - - // Domain 3: Security { path: 'security', title: 'Security', canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard, requireSecurityGuard], data: { breadcrumb: 'Security' }, - loadChildren: () => - import('./routes/security.routes').then( - (m) => m.SECURITY_ROUTES - ), + loadChildren: () => import('./routes/security.routes').then((m) => m.SECURITY_ROUTES), }, - - // Domain 4: Evidence { path: 'evidence', title: 'Evidence', canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard, requireEvidenceGuard], data: { breadcrumb: 'Evidence' }, - loadChildren: () => - import('./routes/evidence.routes').then( - (m) => m.EVIDENCE_ROUTES - ), - }, - - // Domain 5: Topology - { - path: 'topology', - title: 'Topology', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard, requireTopologyGuard], - data: { breadcrumb: 'Topology' }, - loadChildren: () => - import('./routes/topology.routes').then( - (m) => m.TOPOLOGY_ROUTES - ), - }, - - // Domain 6: Platform - { - path: 'platform', - title: 'Platform', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard, requirePlatformGuard], - data: { breadcrumb: 'Platform' }, - loadChildren: () => - import('./routes/platform.routes').then( - (m) => m.PLATFORM_ROUTES - ), - }, - - // Legacy root alias: Administration - { - path: 'administration', - pathMatch: 'full', - redirectTo: '/platform/setup', - }, - - // Legacy root alias: Operations - { - path: 'operations', - title: 'Operations', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard, requirePlatformGuard], - data: { breadcrumb: 'Operations' }, - loadChildren: () => - import('./routes/operations.routes').then( - (m) => m.OPERATIONS_ROUTES - ), - }, - - // Legacy deep-link compatibility surface: Administration - { - path: 'administration', - title: 'Administration', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard, requirePlatformGuard], - data: { breadcrumb: 'Administration' }, - loadChildren: () => - import('./routes/administration.routes').then( - (m) => m.ADMINISTRATION_ROUTES - ), - }, - - // ======================================================================== - // V1 ALIAS ROUTES (SPRINT_20260218_006) - // These serve v1 canonical paths during the migration alias window defined in - // docs/modules/ui/v2-rewire/S00_route_deprecation_map.md. - // They load the same content as canonical routes to maintain backward compatibility. - // Convert to redirects and remove at SPRINT_20260218_016 after confirming traffic. - // ======================================================================== - - // Releases domain aliases - { - path: 'approvals', - pathMatch: 'full', - redirectTo: '/releases/approvals', + loadChildren: () => import('./routes/evidence.routes').then((m) => m.EVIDENCE_ROUTES), }, { - path: 'environments', - pathMatch: 'full', - redirectTo: '/topology/environments', + path: 'ops', + title: 'Ops', + canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard, requireOpsGuard], + data: { breadcrumb: 'Ops' }, + loadChildren: () => import('./routes/ops.routes').then((m) => m.OPS_ROUTES), }, { - path: 'release-control', - pathMatch: 'full', - redirectTo: '/releases', - }, - { - path: 'deployments', - pathMatch: 'full', - redirectTo: '/releases/runs', - }, - - // Legacy Security alias - { - path: 'security-risk', - pathMatch: 'full', - redirectTo: '/security', - }, - - // Analytics alias (served under Security in v3) - { - path: 'analytics', - title: 'Analytics', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAnalyticsViewerGuard], - loadChildren: () => - import('./features/analytics/analytics.routes').then((m) => m.ANALYTICS_ROUTES), - }, - - // Legacy Evidence alias - { - path: 'evidence-audit', - pathMatch: 'full', - redirectTo: '/evidence', - }, - - // Legacy Operations aliases - { - path: 'platform-ops', - title: 'Operations', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard, requirePlatformGuard], - data: { breadcrumb: 'Operations' }, - loadChildren: () => - import('./routes/operations.routes').then( - (m) => m.OPERATIONS_ROUTES - ), - }, - - // Administration domain alias — policy - { - path: 'policy', - pathMatch: 'full', - redirectTo: '/administration/policy-governance', - }, - - // Legacy setup aliases moved under Release Control -> Setup. - { - path: 'settings/release-control', - pathMatch: 'full', - redirectTo: '/topology/promotion-graph', - }, - { - path: 'settings/release-control/environments', - pathMatch: 'full', - redirectTo: '/topology/regions', - }, - { - path: 'settings/release-control/targets', - pathMatch: 'full', - redirectTo: '/topology/targets', - }, - { - path: 'settings/release-control/agents', - pathMatch: 'full', - redirectTo: '/topology/agents', - }, - { - path: 'settings/release-control/workflows', - pathMatch: 'full', - redirectTo: '/topology/workflows', - }, - - // Administration domain alias — settings - { - path: 'settings', - pathMatch: 'full', - redirectTo: '/administration', - }, - - // ========================================================================== - // LEGACY REDIRECT ROUTES - // Redirects for renamed/consolidated paths before legacy aliases/components. - // ========================================================================== - ...LEGACY_REDIRECT_ROUTES, - - // ======================================================================== - // LEGACY ROUTES (to be migrated/removed in future sprints) - // ======================================================================== - - // Legacy Home Dashboard - redirects or will be removed - { - path: 'home', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/home/home-dashboard.component').then( - (m) => m.HomeDashboardComponent - ), - }, - { - path: 'dashboard/sources', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/dashboard/sources-dashboard.component').then( - (m) => m.SourcesDashboardComponent - ), - }, - { - path: 'console/profile', - title: 'Profile', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/console/console-profile.component').then( - (m) => m.ConsoleProfileComponent - ), - }, - { - path: 'console/status', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/console/console-status.component').then( - (m) => m.ConsoleStatusComponent - ), - }, - - // Console Admin routes - gated by ui.admin scope - { - path: 'console/admin', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/console-admin/console-admin.routes').then( - (m) => m.consoleAdminRoutes - ), - }, - // Orchestrator routes - gated by orch:read scope (UI-ORCH-32-001) - { - path: 'orchestrator', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireOrchViewerGuard], - loadComponent: () => - import('./features/orchestrator/orchestrator-dashboard.component').then( - (m) => m.OrchestratorDashboardComponent - ), - }, - { - path: 'orchestrator/jobs', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireOrchViewerGuard], - loadComponent: () => - import('./features/orchestrator/orchestrator-jobs.component').then( - (m) => m.OrchestratorJobsComponent - ), - }, - { - path: 'orchestrator/jobs/:jobId', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireOrchViewerGuard], - loadComponent: () => - import('./features/orchestrator/orchestrator-job-detail.component').then( - (m) => m.OrchestratorJobDetailComponent - ), - }, - { - path: 'orchestrator/quotas', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireOrchOperatorGuard], - loadComponent: () => - import('./features/orchestrator/orchestrator-quotas.component').then( - (m) => m.OrchestratorQuotasComponent - ), - }, - // Release Orchestrator - Dashboard and management UI (SPRINT_20260110_111_001) - { - path: 'release-orchestrator', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/release-orchestrator/dashboard/dashboard.routes').then( - (m) => m.DASHBOARD_ROUTES - ), - }, - { - path: 'policy-studio/packs', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requirePolicyViewerGuard], - loadComponent: () => - import('./features/policy-studio/workspace/policy-workspace.component').then( - (m) => m.PolicyWorkspaceComponent - ), - }, - { - path: 'policy-studio/packs/:packId/editor', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requirePolicyAuthorGuard], - loadComponent: () => - import('./features/policy-studio/editor/policy-editor.component').then( - (m) => m.PolicyEditorComponent - ), - }, - { - path: 'policy-studio/packs/:packId/yaml', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requirePolicyAuthorGuard], - loadComponent: () => - import('./features/policy-studio/yaml/policy-yaml-editor.component').then( - (m) => m.PolicyYamlEditorComponent - ), - }, - { - path: 'policy-studio/packs/:packId/simulate', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requirePolicySimulatorGuard], - loadComponent: () => - import('./features/policy-studio/simulation/policy-simulation.component').then( - (m) => m.PolicySimulationComponent - ), - }, - { - path: 'policy-studio/packs/:packId/approvals', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requirePolicyReviewOrApproveGuard], - loadComponent: () => - import('./features/policy-studio/approvals/policy-approvals.component').then( - (m) => m.PolicyApprovalsComponent - ), - }, - { - path: 'policy-studio/packs/:packId/rules', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requirePolicyAuthorGuard], - loadComponent: () => - import('./features/policy-studio/rule-builder/policy-rule-builder.component').then( - (m) => m.PolicyRuleBuilderComponent - ), - }, - { - path: 'policy-studio/packs/:packId/explain/:runId', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requirePolicyViewerGuard], - loadComponent: () => - import('./features/policy-studio/explain/policy-explain.component').then( - (m) => m.PolicyExplainComponent - ), - }, - { - path: 'policy-studio/packs/:packId/dashboard', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requirePolicyViewerGuard], - loadComponent: () => - import('./features/policy-studio/dashboard/policy-dashboard.component').then( - (m) => m.PolicyDashboardComponent - ), - }, - { - path: 'concelier/trivy-db-settings', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/trivy-db-settings/trivy-db-settings-page.component').then( - (m) => m.TrivyDbSettingsPageComponent - ), - }, - { - path: 'scans/:scanId', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/scans/scan-detail-page.component').then( - (m) => m.ScanDetailPageComponent - ), + path: 'setup', + title: 'Setup', + canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard, requireSetupGuard], + data: { breadcrumb: 'Setup' }, + loadChildren: () => import('./routes/setup.routes').then((m) => m.SETUP_ROUTES), }, { path: 'welcome', - canMatch: [requireConfigGuard, requireBackendsReachableGuard], + title: 'Welcome', loadComponent: () => - import('./features/welcome/welcome-page.component').then( - (m) => m.WelcomePageComponent - ), - }, - { - path: 'risk', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/risk/risk-dashboard.component').then( - (m) => m.RiskDashboardComponent - ), - }, - { - path: 'graph', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/graph/graph-explorer.component').then( - (m) => m.GraphExplorerComponent - ), - }, - { - path: 'lineage', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/lineage/lineage.routes').then((m) => m.lineageRoutes), - }, - { - path: 'reachability', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/reachability/reachability-center.component').then( - (m) => m.ReachabilityCenterComponent - ), - }, - { - path: 'timeline', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/timeline/timeline.routes').then((m) => m.TIMELINE_ROUTES), - }, - { - path: 'evidence-thread', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/evidence-thread/evidence-thread.routes').then((m) => m.EVIDENCE_THREAD_ROUTES), - }, - { - path: 'vulnerabilities', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/vulnerabilities/vulnerability-explorer.component').then( - (m) => m.VulnerabilityExplorerComponent - ), - }, - { - path: 'vulnerabilities/triage', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/vulnerabilities/components/vuln-triage-dashboard/vuln-triage-dashboard.component').then( - (m) => m.VulnTriageDashboardComponent - ), - }, - // Findings container with diff-first default (SPRINT_1227_0005_0001) - { - path: 'findings', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/findings/container/findings-container.component').then( - (m) => m.FindingsContainerComponent - ), - }, - { - path: 'findings/:scanId', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/findings/container/findings-container.component').then( - (m) => m.FindingsContainerComponent - ), - }, - { - path: 'triage', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/triage/components/triage-canvas/triage-canvas.component').then( - (m) => m.TriageCanvasComponent - ), - }, - { - path: 'triage/artifacts', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/triage/triage-artifacts.component').then( - (m) => m.TriageArtifactsComponent - ), - }, - { - path: 'triage/inbox', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/triage-inbox/triage-inbox.component').then( - (m) => m.TriageInboxComponent - ), - }, - { - path: 'triage/artifacts/:artifactId', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/triage/triage-workspace.component').then( - (m) => m.TriageWorkspaceComponent - ), - }, - { - path: 'triage/audit-bundles', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/triage/triage-audit-bundles.component').then( - (m) => m.TriageAuditBundlesComponent - ), - }, - { - path: 'triage/audit-bundles/new', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/triage/triage-audit-bundle-new.component').then( - (m) => m.TriageAuditBundleNewComponent - ), - }, - { - path: 'triage/ai-recommendations', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/triage/ai-recommendation-workbench.component').then( - (m) => m.AiRecommendationWorkbenchComponent - ), - }, - { - path: 'triage/quiet-lane', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/triage/quiet-lane-workbench.component').then( - (m) => m.QuietLaneWorkbenchComponent - ), - }, - { - path: 'audit/reasons', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/triage/reason-capsule-workbench.component').then( - (m) => m.ReasonCapsuleWorkbenchComponent - ), - }, - { - path: 'qa/web-recheck', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/qa/web-feature-recheck-workbench.component').then( - (m) => m.WebFeatureRecheckWorkbenchComponent - ), - }, - { - path: 'qa/sbom-component-detail', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/sbom/pages/component-detail/component-detail.page').then( - (m) => m.ComponentDetailPage - ), - }, - { - path: 'ops/binary-index', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/binary-index/binary-index-ops.component').then( - (m) => m.BinaryIndexOpsComponent - ), - }, - { - path: 'settings/determinization-config', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/settings/determinization-config-pane.component').then( - (m) => m.DeterminizationConfigPaneComponent - ), - }, - { - path: 'compare/:currentId', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/compare/components/compare-view/compare-view.component').then( - (m) => m.CompareViewComponent - ), - }, - { - path: 'proofs/:subjectDigest', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/proof-chain/proof-chain.component').then( - (m) => m.ProofChainComponent - ), - }, - { - path: 'vulnerabilities/:vulnId', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/vulnerabilities/vulnerability-detail.component').then( - (m) => m.VulnerabilityDetailComponent - ), - }, - { - path: 'cvss/receipts/:receiptId', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/cvss/cvss-receipt.component').then((m) => m.CvssReceiptComponent), - }, - { - path: 'notify', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/notify/notify-panel.component').then( - (m) => m.NotifyPanelComponent - ), - }, - // Admin - VEX Hub (SPRINT_20251229_018a) - { - path: 'admin/vex-hub', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/vex-hub/vex-hub.routes').then((m) => m.vexHubRoutes), - }, - // Admin - Notifications (SPRINT_20251229_018b) - { - path: 'admin/notifications', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/admin-notifications/admin-notifications.routes').then((m) => m.adminNotificationsRoutes), - }, - // Admin - Trust Management (SPRINT_20251229_018c) - { - path: 'admin/trust', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/trust-admin/trust-admin.routes').then((m) => m.trustAdminRoutes), - }, - // Ops - Feed Mirror (SPRINT_20251229_020) - { - path: 'ops/feeds', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/feed-mirror/feed-mirror.routes').then((m) => m.feedMirrorRoutes), - }, - // Ops - Signals Runtime Dashboard (SPRINT_20260208_072) - { - path: 'ops/signals', - title: 'Signals Runtime Dashboard', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/signals/signals.routes').then((m) => m.SIGNALS_ROUTES), - }, - // Ops - Pack Registry Browser (SPRINT_20260208_068) - { - path: 'ops/packs', - title: 'Pack Registry Browser', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireOrchViewerGuard], - loadChildren: () => - import('./features/pack-registry/pack-registry.routes').then((m) => m.PACK_REGISTRY_ROUTES), - }, - { - path: 'sbom-sources', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/sbom-sources/sbom-sources.routes').then((m) => m.SBOM_SOURCES_ROUTES), - }, - // Admin - Policy Governance (SPRINT_20251229_021a) - { - path: 'admin/policy/governance', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/policy-governance/policy-governance.routes').then((m) => m.policyGovernanceRoutes), - }, - // Admin - Policy Simulation (SPRINT_20251229_021b) - { - path: 'admin/policy/simulation', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/policy-simulation/policy-simulation.routes').then((m) => m.policySimulationRoutes), - }, - // Scheduler Ops (SPRINT_20251229_017) - { - path: 'scheduler', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/scheduler-ops/scheduler-ops.routes').then((m) => m.schedulerOpsRoutes), + import('./features/welcome/welcome-page.component').then((m) => m.WelcomePageComponent), }, { path: 'auth/callback', - loadComponent: () => - import('./features/auth/auth-callback.component').then( - (m) => m.AuthCallbackComponent - ), + loadComponent: () => import('./features/auth/auth-callback.component').then((m) => m.AuthCallbackComponent), }, { path: 'auth/silent-refresh', loadComponent: () => - import('./features/auth/silent-refresh.component').then( - (m) => m.SilentRefreshComponent - ), - }, - // Exceptions route - { - path: 'exceptions', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/triage/triage-artifacts.component').then( - (m) => m.TriageArtifactsComponent - ), - }, - // Integration Hub (SPRINT_20251229_011_FE_integration_hub_ui) - { - path: 'integrations', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard, requirePlatformGuard], - loadChildren: () => - import('./features/integration-hub/integration-hub.routes').then((m) => m.integrationHubRoutes), - }, - // Admin - Registry Token Service (SPRINT_20251229_023) - { - path: 'admin/registries', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/registry-admin/registry-admin.routes').then((m) => m.registryAdminRoutes), - }, - // Admin - Issuer Trust (SPRINT_20251229_024) - { - path: 'admin/issuers', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/issuer-trust/issuer-trust.routes').then((m) => m.issuerTrustRoutes), - }, - // Ops - Scanner Operations (SPRINT_20251229_025) - { - path: 'ops/scanner', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/scanner-ops/scanner-ops.routes').then((m) => m.scannerOpsRoutes), - }, - // Ops - Offline Kit Management (SPRINT_20251229_026) - { - path: 'ops/offline-kit', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/offline-kit/offline-kit.routes').then((m) => m.offlineKitRoutes), - }, - // Ops - AOC Compliance Dashboard (SPRINT_20251229_027) - { - path: 'ops/aoc', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/aoc-compliance/aoc-compliance.routes').then((m) => m.AOC_COMPLIANCE_ROUTES), - }, - // Admin - Unified Audit Log (SPRINT_20251229_028) - { - path: 'admin/audit', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/audit-log/audit-log.routes').then((m) => m.auditLogRoutes), - }, - // Ops - Quota Dashboard (SPRINT_20251229_029) - { - path: 'ops/quotas', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/quota-dashboard/quota.routes').then((m) => m.quotaRoutes), - }, - // Ops - Dead-Letter Management (SPRINT_20251229_030) - { - path: 'ops/orchestrator/dead-letter', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/deadletter/deadletter.routes').then((m) => m.deadletterRoutes), - }, - // Ops - SLO Burn Rate Monitoring (SPRINT_20251229_031) - { - path: 'ops/orchestrator/slo', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/slo-monitoring/slo.routes').then((m) => m.sloRoutes), - }, - // Ops - Platform Health Dashboard (SPRINT_20251229_032) - { - path: 'ops/health', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/platform-health/platform-health.routes').then((m) => m.platformHealthRoutes), - }, - // Ops - Doctor Diagnostics (SPRINT_20260112_001_008) - { - path: 'ops/doctor', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/doctor/doctor.routes').then((m) => m.DOCTOR_ROUTES), - }, - // Ops - Agent Fleet (SPRINT_20260118_023_FE) - { - path: 'ops/agents', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/agents/agents.routes').then((m) => m.AGENTS_ROUTES), - }, - // Analyze - Unknowns Tracking (SPRINT_20251229_033) - { - path: 'analyze/unknowns', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/unknowns-tracking/unknowns.routes').then((m) => m.unknownsRoutes), - }, - // Analyze - Patch Map Explorer (SPRINT_20260103_003_FE_patch_map_explorer) - { - path: 'analyze/patch-map', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/binary-index/patch-map.component').then( - (m) => m.PatchMapComponent - ), - }, - // Evidence Packs (SPRINT_20260109_011_005) - { - path: 'evidence-packs', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/evidence-pack/evidence-pack-list.component').then( - (m) => m.EvidencePackListComponent - ), + import('./features/auth/silent-refresh.component').then((m) => m.SilentRefreshComponent), }, { - path: 'evidence-packs/:packId', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/evidence-pack/evidence-pack-viewer.component').then( - (m) => m.EvidencePackViewerComponent - ), + path: 'setup-wizard', + loadChildren: () => import('./features/setup-wizard/setup-wizard.routes').then((m) => m.setupWizardRoutes), }, - // Advisory AI Autofix workbench (strict Tier 2 UI verification surface) - { - path: 'ai/autofix', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/advisory-ai/autofix-workbench.component').then( - (m) => m.AutofixWorkbenchComponent - ), - }, - { - path: 'aoc/verify', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/aoc/aoc-verification-workbench.component').then( - (m) => m.AocVerificationWorkbenchComponent - ), - }, - { - path: 'ai/chat', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/advisory-ai/chat/chat.component').then( - (m) => m.ChatComponent - ), - }, - { - path: 'ai/chips', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/advisory-ai/chip-showcase.component').then( - (m) => m.ChipShowcaseComponent - ), - }, - // AI Runs (SPRINT_20260109_011_003) - { - path: 'ai-runs', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/ai-runs/ai-runs-list.component').then( - (m) => m.AiRunsListComponent - ), - }, - { - path: 'ai-runs/:runId', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadComponent: () => - import('./features/ai-runs/ai-run-viewer.component').then( - (m) => m.AiRunViewerComponent - ), - }, - // Change Trace (SPRINT_20260112_200_007) - { - path: 'change-trace', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/change-trace/change-trace.routes').then((m) => m.changeTraceRoutes), - }, - // Setup Wizard (Sprint 4: UI Wizard Core) — NO config guard (must work without config) - { - path: 'setup', - loadChildren: () => - import('./features/setup-wizard/setup-wizard.routes').then((m) => m.setupWizardRoutes), - }, - // Configuration Pane (Sprint 6: Configuration Pane) - { - path: 'console/configuration', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/configuration-pane/configuration-pane.routes').then((m) => m.CONFIGURATION_PANE_ROUTES), - }, - // SBOM Diff View (SPRINT_0127_0001_FE - FE-PERSONA-02) - { - path: 'sbom/diff', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/sbom-diff/sbom-diff.routes').then((m) => m.SBOM_DIFF_ROUTES), - }, - // Deploy Diff View (SPRINT_20260125_006_FE_ab_deploy_diff_panel) - { - path: 'deploy/diff', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/deploy-diff/deploy-diff.routes').then((m) => m.DEPLOY_DIFF_ROUTES), - }, - // VEX Timeline (SPRINT_0127_0001_FE - FE-PERSONA-03) - { - path: 'vex/timeline', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/vex-timeline/vex-timeline.routes').then((m) => m.VEX_TIMELINE_ROUTES), - }, - // Developer Workspace (SPRINT_0127_0001_FE - FE-PERSONA-04) - { - path: 'workspace/dev', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/workspaces/developer/developer-workspace.routes').then( - (m) => m.DEVELOPER_WORKSPACE_ROUTES - ), - }, - // Auditor Workspace (SPRINT_0127_0001_FE - FE-PERSONA-05) - { - path: 'workspace/audit', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard], - loadChildren: () => - import('./features/workspaces/auditor/auditor-workspace.routes').then( - (m) => m.AUDITOR_WORKSPACE_ROUTES - ), - }, - // Fallback for unknown routes { path: '**', - redirectTo: '', + canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard, requireMissionControlGuard], + data: { breadcrumb: 'Mission Control' }, + loadChildren: () => import('./routes/mission-control.routes').then((m) => m.MISSION_CONTROL_ROUTES), }, ]; diff --git a/src/Web/StellaOps.Web/src/app/core/api/aoc.client.ts b/src/Web/StellaOps.Web/src/app/core/api/aoc.client.ts index 47d752751..734a5b8a9 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/aoc.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/aoc.client.ts @@ -168,122 +168,62 @@ export class AocHttpClient implements AocApi { export class AocClient { private readonly http = inject(HttpClient); private readonly config = inject(AppConfigService); + private readonly authSession = inject(AuthSessionStore); + + private get baseUrl(): string { + const gatewayBase = this.config.config.apiBaseUrls.gateway ?? this.config.config.apiBaseUrls.attestor; + const normalized = gatewayBase.endsWith('/') ? gatewayBase.slice(0, -1) : gatewayBase; + return `${normalized}/api/v1/aoc`; + } + + private buildHeaders(): HttpHeaders { + const tenantId = this.authSession.getActiveTenantId() || ''; + const traceId = generateTraceId(); + return new HttpHeaders({ + 'X-StellaOps-Tenant': tenantId, + 'X-Stella-Trace-Id': traceId, + Accept: 'application/json', + }); + } /** * Gets AOC metrics for the dashboard. */ getMetrics(tenantId: string, windowMinutes = 1440): Observable { - // TODO: Replace with real API call when available - // return this.http.get( - // this.config.apiBaseUrl + '/aoc/metrics', - // { params: { tenantId, windowMinutes: windowMinutes.toString() } } - // ); - - // Mock data for development - return of(this.getMockMetrics()).pipe(delay(300)); + const params = new HttpParams() + .set('tenantId', tenantId) + .set('windowMinutes', windowMinutes.toString()); + return this.http.get(`${this.baseUrl}/metrics`, { + params, + headers: this.buildHeaders(), + }); } /** * Triggers verification of documents within a time window. */ verify(request: AocVerificationRequest): Observable { - // TODO: Replace with real API call when available - // return this.http.post( - // this.config.apiBaseUrl + '/aoc/verify', - // request - // ); - - // Mock verification result - return of(this.getMockVerificationResult()).pipe(delay(500)); + return this.http.post(`${this.baseUrl}/verify`, request, { + headers: this.buildHeaders(), + }); } - private getMockMetrics(): AocMetrics { - const now = new Date(); - const dayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); - - return { - passCount: 12847, - failCount: 23, - totalCount: 12870, - passRate: 99.82, - recentViolations: [ - { - code: 'AOC-PROV-001', - description: 'Missing provenance attestation', - count: 12, - severity: 'high', - lastSeen: new Date(now.getTime() - 15 * 60 * 1000).toISOString(), - }, - { - code: 'AOC-DIGEST-002', - description: 'Digest mismatch in manifest', - count: 7, - severity: 'critical', - lastSeen: new Date(now.getTime() - 45 * 60 * 1000).toISOString(), - }, - { - code: 'AOC-SCHEMA-003', - description: 'Schema validation failed', - count: 4, - severity: 'medium', - lastSeen: new Date(now.getTime() - 2 * 60 * 60 * 1000).toISOString(), - }, - ], - ingestThroughput: { - docsPerMinute: 8.9, - avgLatencyMs: 145, - p95LatencyMs: 312, - queueDepth: 3, - errorRate: 0.18, - }, - timeWindow: { - start: dayAgo.toISOString(), - end: now.toISOString(), - durationMinutes: 1440, - }, - }; - } - - private getMockVerificationResult(): AocVerificationResult { - const verifyId = 'verify-' + Date.now().toString(); - return { - verificationId: verifyId, - status: 'passed', - checkedCount: 1523, - passedCount: 1520, - failedCount: 3, - violations: [ - { - documentId: 'doc-abc123', - violationCode: 'AOC-PROV-001', - field: 'attestation.provenance', - expected: 'present', - actual: 'missing', - provenance: { - sourceId: 'source-registry-1', - ingestedAt: new Date().toISOString(), - digest: 'sha256:abc123...', - }, - }, - ], - completedAt: new Date().toISOString(), - }; - } - - // ========================================================================== - // Sprint 027: AOC Compliance Dashboard Methods - // ========================================================================== - /** * Gets AOC compliance dashboard data including metrics, violations, and ingestion flow. */ getComplianceDashboard(filters?: AocDashboardFilters): Observable { - // TODO: Replace with real API call - // return this.http.get( - // this.config.apiBaseUrl + '/aoc/compliance/dashboard', - // { params: this.buildFilterParams(filters) } - // ); - return of(this.getMockComplianceDashboard()).pipe(delay(300)); + let params = new HttpParams(); + if (filters?.dateRange) { + params = params.set('startDate', filters.dateRange.start); + params = params.set('endDate', filters.dateRange.end); + } + if (filters?.sources?.length) params = params.set('sources', filters.sources.join(',')); + if (filters?.modules?.length) params = params.set('modules', filters.modules.join(',')); + if (filters?.violationReasons?.length) params = params.set('violationReasons', filters.violationReasons.join(',')); + return this.http.get(`${this.baseUrl}/compliance/dashboard`, { + params, + headers: this.buildHeaders(), + }); } /** @@ -294,16 +234,28 @@ export class AocClient { pageSize = 20, filters?: AocDashboardFilters ): Observable { - // TODO: Replace with real API call - return of(this.getMockGuardViolations(page, pageSize)).pipe(delay(300)); + let params = new HttpParams() + .set('page', page.toString()) + .set('pageSize', pageSize.toString()); + if (filters?.dateRange) { + params = params.set('startDate', filters.dateRange.start); + params = params.set('endDate', filters.dateRange.end); + } + if (filters?.sources?.length) params = params.set('sources', filters.sources.join(',')); + if (filters?.modules?.length) params = params.set('modules', filters.modules.join(',')); + return this.http.get(`${this.baseUrl}/compliance/violations`, { + params, + headers: this.buildHeaders(), + }); } /** * Gets ingestion flow metrics from Concelier and Excititor. */ getIngestionFlow(): Observable { - // TODO: Replace with real API call - return of(this.getMockIngestionFlow()).pipe(delay(300)); + return this.http.get(`${this.baseUrl}/ingestion/flow`, { + headers: this.buildHeaders(), + }); } /** @@ -313,347 +265,32 @@ export class AocClient { inputType: 'advisory_id' | 'finding_id' | 'cve_id', inputValue: string ): Observable { - // TODO: Replace with real API call - return of(this.getMockProvenanceChain(inputType, inputValue)).pipe(delay(500)); + return this.http.post(`${this.baseUrl}/provenance/validate`, { + inputType, + inputValue, + }, { + headers: this.buildHeaders(), + }); } /** * Generates compliance report for export. */ generateComplianceReport(request: ComplianceReportRequest): Observable { - // TODO: Replace with real API call - return of(this.getMockComplianceReport(request)).pipe(delay(800)); + return this.http.post(`${this.baseUrl}/compliance/reports`, request, { + headers: this.buildHeaders(), + }); } /** * Retries a failed ingestion (guard violation). */ retryIngestion(violationId: string): Observable<{ success: boolean; message: string }> { - // TODO: Replace with real API call - return of({ success: true, message: 'Ingestion retry queued' }).pipe(delay(300)); - } - - private getMockComplianceDashboard(): AocComplianceDashboardData { - const now = new Date(); - const dayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); - - return { - metrics: { - guardViolations: { - count: 23, - percentage: 0.18, - byReason: { - schema_invalid: 8, - untrusted_source: 6, - duplicate: 5, - missing_required_fields: 4, - }, - trend: 'down', - }, - provenanceCompleteness: { - percentage: 100, - recordsWithValidHash: 12847, - totalRecords: 12847, - trend: 'stable', - }, - deduplicationRate: { - percentage: 94.2, - duplicatesDetected: 1180, - totalIngested: 12527, - trend: 'up', - }, - ingestionLatency: { - p50Ms: 850, - p95Ms: 2100, - p99Ms: 4500, - meetsSla: true, - slaTargetP95Ms: 5000, - }, - supersedesDepth: { - maxDepth: 7, - avgDepth: 2.3, - distribution: [ - { depth: 0, count: 8500 }, - { depth: 1, count: 2800 }, - { depth: 2, count: 1100 }, - { depth: 3, count: 320 }, - { depth: 4, count: 95 }, - { depth: 5, count: 28 }, - { depth: 6, count: 3 }, - { depth: 7, count: 1 }, - ], - }, - periodStart: dayAgo.toISOString(), - periodEnd: now.toISOString(), - }, - recentViolations: this.getMockGuardViolations(1, 5).items, - ingestionFlow: this.getMockIngestionFlow(), - }; - } - - private getMockGuardViolations(page: number, pageSize: number): GuardViolationsPagedResponse { - const now = new Date(); - const violations: GuardViolation[] = [ - { - id: 'viol-001', - timestamp: new Date(now.getTime() - 15 * 60 * 1000).toISOString(), - source: 'NVD', - reason: 'schema_invalid', - message: 'Advisory JSON does not match expected CVE 5.0 schema', - payloadSample: '{"cve": {"id": "CVE-2024-1234", "containers": ...}}', - module: 'concelier', - canRetry: true, - }, - { - id: 'viol-002', - timestamp: new Date(now.getTime() - 45 * 60 * 1000).toISOString(), - source: 'GHSA', - reason: 'untrusted_source', - message: 'Source not in allowlist: ghsa-mirror-staging.example.com', - module: 'concelier', - canRetry: false, - }, - { - id: 'viol-003', - timestamp: new Date(now.getTime() - 2 * 60 * 60 * 1000).toISOString(), - source: 'VEX-Mirror', - reason: 'duplicate', - message: 'Document with upstream_hash sha256:abc123 already exists', - module: 'excititor', - canRetry: false, - }, - { - id: 'viol-004', - timestamp: new Date(now.getTime() - 3 * 60 * 60 * 1000).toISOString(), - source: 'Red Hat', - reason: 'malformed_timestamp', - message: 'Timestamp "2024-13-45T99:00:00Z" is not valid ISO-8601', - payloadSample: '{"published": "2024-13-45T99:00:00Z"}', - module: 'concelier', - canRetry: true, - }, - { - id: 'viol-005', - timestamp: new Date(now.getTime() - 5 * 60 * 60 * 1000).toISOString(), - source: 'Internal VEX', - reason: 'missing_required_fields', - message: 'VEX statement missing required field: product.cpe', - module: 'excititor', - canRetry: true, - }, - ]; - - const start = (page - 1) * pageSize; - const items = violations.slice(start, start + pageSize); - - return { - items, - totalCount: violations.length, - page, - pageSize, - hasMore: start + pageSize < violations.length, - }; - } - - private getMockIngestionFlow(): IngestionFlowSummary { - const now = new Date(); - - const sources: IngestionSourceMetrics[] = [ - { - sourceId: 'nvd', - sourceName: 'NVD', - module: 'concelier', - throughputPerMinute: 23, - latencyP50Ms: 720, - latencyP95Ms: 1200, - latencyP99Ms: 2100, - errorRate: 0.02, - backlogDepth: 12, - lastIngestionAt: new Date(now.getTime() - 2 * 60 * 1000).toISOString(), - status: 'healthy', - }, - { - sourceId: 'ghsa', - sourceName: 'GHSA', - module: 'concelier', - throughputPerMinute: 45, - latencyP50Ms: 480, - latencyP95Ms: 800, - latencyP99Ms: 1500, - errorRate: 0.01, - backlogDepth: 5, - lastIngestionAt: new Date(now.getTime() - 1 * 60 * 1000).toISOString(), - status: 'healthy', - }, - { - sourceId: 'redhat', - sourceName: 'Red Hat', - module: 'concelier', - throughputPerMinute: 12, - latencyP50Ms: 1850, - latencyP95Ms: 3100, - latencyP99Ms: 5200, - errorRate: 0.05, - backlogDepth: 28, - lastIngestionAt: new Date(now.getTime() - 5 * 60 * 1000).toISOString(), - status: 'degraded', - }, - { - sourceId: 'vex-mirror', - sourceName: 'VEX Mirror', - module: 'excititor', - throughputPerMinute: 8, - latencyP50Ms: 1200, - latencyP95Ms: 2500, - latencyP99Ms: 4200, - errorRate: 0.03, - backlogDepth: 3, - lastIngestionAt: new Date(now.getTime() - 3 * 60 * 1000).toISOString(), - status: 'healthy', - }, - { - sourceId: 'upstream-vex', - sourceName: 'Upstream VEX', - module: 'excititor', - throughputPerMinute: 3, - latencyP50Ms: 2100, - latencyP95Ms: 4200, - latencyP99Ms: 6800, - errorRate: 0.08, - backlogDepth: 1, - lastIngestionAt: new Date(now.getTime() - 8 * 60 * 1000).toISOString(), - status: 'healthy', - }, - ]; - - return { - sources, - totalThroughput: sources.reduce((sum, s) => sum + s.throughputPerMinute, 0), - avgLatencyP95Ms: Math.round(sources.reduce((sum, s) => sum + s.latencyP95Ms, 0) / sources.length), - overallErrorRate: sources.reduce((sum, s) => sum + s.errorRate, 0) / sources.length, - lastUpdatedAt: now.toISOString(), - }; - } - - private getMockProvenanceChain( - inputType: 'advisory_id' | 'finding_id' | 'cve_id', - inputValue: string - ): ProvenanceChain { - const now = new Date(); - - const steps: ProvenanceStep[] = [ - { - stepType: 'source', - label: 'NVD Published', - timestamp: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), - hash: 'sha256:nvd-original-hash-abc123', - status: 'valid', - details: { source: 'NVD', originalId: inputValue }, - }, - { - stepType: 'advisory_raw', - label: 'Concelier Stored', - timestamp: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000 + 5 * 60 * 1000).toISOString(), - hash: 'sha256:concelier-raw-hash-def456', - linkedFromHash: 'sha256:nvd-original-hash-abc123', - status: 'valid', - details: { table: 'advisory_raw', recordId: 'adv-12345' }, - }, - { - stepType: 'normalized', - label: 'Policy Engine Normalized', - timestamp: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000 + 10 * 60 * 1000).toISOString(), - hash: 'sha256:normalized-hash-ghi789', - linkedFromHash: 'sha256:concelier-raw-hash-def456', - status: 'valid', - details: { affectedRanges: 3, products: 5 }, - }, - { - stepType: 'vex_decision', - label: 'VEX Consensus Applied', - timestamp: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), - hash: 'sha256:vex-consensus-hash-jkl012', - linkedFromHash: 'sha256:normalized-hash-ghi789', - status: 'valid', - details: { status: 'affected', justification: 'vulnerable_code_not_in_execute_path' }, - }, - { - stepType: 'finding', - label: 'Finding Generated', - timestamp: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString(), - hash: 'sha256:finding-hash-mno345', - linkedFromHash: 'sha256:vex-consensus-hash-jkl012', - status: 'valid', - details: { findingId: 'finding-67890', severity: 'high', cvss: 8.1 }, - }, - { - stepType: 'policy_verdict', - label: 'Policy Evaluated', - timestamp: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(), - hash: 'sha256:verdict-hash-pqr678', - linkedFromHash: 'sha256:finding-hash-mno345', - status: 'valid', - details: { verdict: 'fail', policyHash: 'sha256:policy-v2.1' }, - }, - { - stepType: 'attestation', - label: 'Attestation Signed', - timestamp: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000).toISOString(), - hash: 'sha256:attestation-hash-stu901', - linkedFromHash: 'sha256:verdict-hash-pqr678', - status: 'valid', - details: { dsseEnvelope: 'dsse://...', rekorLogIndex: 12345678 }, - }, - ]; - - return { - inputType, - inputValue, - steps, - isComplete: true, - validationErrors: [], - validatedAt: now.toISOString(), - }; - } - - private getMockComplianceReport(request: ComplianceReportRequest): ComplianceReportSummary { - const now = new Date(); - - return { - reportId: 'report-' + Date.now(), - generatedAt: now.toISOString(), - period: { start: request.startDate, end: request.endDate }, - guardViolationSummary: { - total: 147, - bySource: { NVD: 45, GHSA: 32, 'Red Hat': 28, 'VEX Mirror': 42 }, - byReason: { - schema_invalid: 52, - untrusted_source: 28, - duplicate: 35, - malformed_timestamp: 18, - missing_required_fields: 14, - }, - }, - provenanceCompliance: { - percentage: 99.97, - bySource: { NVD: 100, GHSA: 100, 'Red Hat': 99.8, 'VEX Mirror': 100 }, - }, - deduplicationMetrics: { - rate: 94.2, - bySource: { NVD: 92.1, GHSA: 96.5, 'Red Hat': 91.8, 'VEX Mirror': 97.3 }, - }, - latencyMetrics: { - p50Ms: 850, - p95Ms: 2100, - p99Ms: 4500, - bySource: { - NVD: { p50: 720, p95: 1200, p99: 2100 }, - GHSA: { p50: 480, p95: 800, p99: 1500 }, - 'Red Hat': { p50: 1850, p95: 3100, p99: 5200 }, - 'VEX Mirror': { p50: 1200, p95: 2500, p99: 4200 }, - }, - }, - }; + return this.http.post<{ success: boolean; message: string }>( + `${this.baseUrl}/compliance/violations/${encodeURIComponent(violationId)}/retry`, + {}, + { headers: this.buildHeaders() }, + ); } } diff --git a/src/Web/StellaOps.Web/src/app/core/api/search.client.ts b/src/Web/StellaOps.Web/src/app/core/api/search.client.ts index 633802bdd..f6cef8c6d 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/search.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/search.client.ts @@ -160,7 +160,7 @@ export class SearchClient { type: 'policy' as SearchEntityType, title: item.name, subtitle: item.description, - route: `/policy-studio/packs/${item.id}/editor`, + route: `/ops/policy/baselines?packId=${encodeURIComponent(item.id)}`, matchScore: 100, })) ), @@ -182,7 +182,7 @@ export class SearchClient { title: `job-${item.id.substring(0, 8)}`, subtitle: `${item.type} (${item.status})`, description: item.artifactRef, - route: `/platform/ops/orchestrator/jobs/${item.id}`, + route: `/ops/operations/orchestrator/jobs/${item.id}`, matchScore: 100, })) ), @@ -209,7 +209,7 @@ export class SearchClient { type: 'finding' as SearchEntityType, title: item.cveId, subtitle: item.artifactRef, - route: `/findings?cve=${item.cveId}`, + route: `/security/triage?cve=${encodeURIComponent(item.cveId)}`, severity: item.severity?.toLowerCase() as SearchResult['severity'], matchScore: 100, })) @@ -259,7 +259,7 @@ export class SearchClient { type: 'integration' as SearchEntityType, title: item.name, subtitle: `${item.type} (${item.status})`, - route: `/platform/integrations/${item.id}`, + route: `/ops/integrations/${item.id}`, matchScore: 100, })) ), diff --git a/src/Web/StellaOps.Web/src/app/core/api/search.models.ts b/src/Web/StellaOps.Web/src/app/core/api/search.models.ts index f43794a21..980086fb9 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/search.models.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/search.models.ts @@ -100,7 +100,7 @@ export const DEFAULT_QUICK_ACTIONS: QuickAction[] = [ shortcut: '>scan', description: 'Opens artifact scan dialog', icon: 'scan', - route: '/findings', + route: '/security/triage', keywords: ['scan', 'artifact', 'analyze'], }, { @@ -109,7 +109,7 @@ export const DEFAULT_QUICK_ACTIONS: QuickAction[] = [ shortcut: '>vex', description: 'Open VEX creation workflow', icon: 'shield-check', - route: '/admin/vex-hub', + route: '/security/advisories-vex', keywords: ['vex', 'create', 'statement'], }, { @@ -118,7 +118,7 @@ export const DEFAULT_QUICK_ACTIONS: QuickAction[] = [ shortcut: '>policy', description: 'Create new policy pack', icon: 'shield', - route: '/policy-studio/packs', + route: '/ops/policy/baselines', keywords: ['policy', 'new', 'pack', 'create'], }, { @@ -127,7 +127,7 @@ export const DEFAULT_QUICK_ACTIONS: QuickAction[] = [ shortcut: '>jobs', description: 'Navigate to job list', icon: 'workflow', - route: '/platform/ops/jobs-queues', + route: '/ops/operations/jobs-queues', keywords: ['jobs', 'orchestrator', 'list'], }, { @@ -136,7 +136,7 @@ export const DEFAULT_QUICK_ACTIONS: QuickAction[] = [ shortcut: '>findings', description: 'Navigate to findings list', icon: 'alert-triangle', - route: '/findings', + route: '/security/triage', keywords: ['findings', 'vulnerabilities', 'list'], }, { @@ -145,7 +145,7 @@ export const DEFAULT_QUICK_ACTIONS: QuickAction[] = [ shortcut: '>settings', description: 'Navigate to settings', icon: 'settings', - route: '/platform/setup', + route: '/setup', keywords: ['settings', 'config', 'preferences'], }, { @@ -154,7 +154,7 @@ export const DEFAULT_QUICK_ACTIONS: QuickAction[] = [ shortcut: '>health', description: 'View platform health status', icon: 'heart-pulse', - route: '/platform/ops/system-health', + route: '/ops/operations/system-health', keywords: ['health', 'status', 'platform', 'ops', 'doctor', 'system'], }, { @@ -179,7 +179,7 @@ export const DEFAULT_QUICK_ACTIONS: QuickAction[] = [ shortcut: '>integrations', description: 'View and manage integrations', icon: 'plug', - route: '/platform/integrations', + route: '/ops/integrations', keywords: ['integrations', 'connect', 'manage'], }, ]; diff --git a/src/Web/StellaOps.Web/src/app/core/auth/authority-auth-adapter.service.ts b/src/Web/StellaOps.Web/src/app/core/auth/authority-auth-adapter.service.ts index cd4d1dc56..a03fac8ef 100644 --- a/src/Web/StellaOps.Web/src/app/core/auth/authority-auth-adapter.service.ts +++ b/src/Web/StellaOps.Web/src/app/core/auth/authority-auth-adapter.service.ts @@ -2,6 +2,8 @@ import { Injectable, computed, effect, + inject, + Injector, signal, } from '@angular/core'; @@ -26,6 +28,8 @@ const KNOWN_SCOPE_SET = new Set( @Injectable({ providedIn: 'root' }) export class AuthorityAuthAdapterService implements AuthService { + private readonly injector = inject(Injector); + readonly isAuthenticated = signal(false); readonly user = signal(null); @@ -35,7 +39,6 @@ export class AuthorityAuthAdapterService implements AuthService { }); constructor( - private readonly authorityAuth: AuthorityAuthService, private readonly sessionStore: AuthSessionStore, private readonly consoleSessionStore: ConsoleSessionStore ) { @@ -135,7 +138,8 @@ export class AuthorityAuthAdapterService implements AuthService { } logout(): void { - void this.authorityAuth.logout(); + const authorityAuth = this.injector.get(AuthorityAuthService); + void authorityAuth.logout(); } private toAuthUser(): AuthUser | null { diff --git a/src/Web/StellaOps.Web/src/app/core/auth/authority-auth.service.ts b/src/Web/StellaOps.Web/src/app/core/auth/authority-auth.service.ts index 5ae0f621d..b343d7a08 100644 --- a/src/Web/StellaOps.Web/src/app/core/auth/authority-auth.service.ts +++ b/src/Web/StellaOps.Web/src/app/core/auth/authority-auth.service.ts @@ -1,5 +1,5 @@ -import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import { HttpBackend, HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http'; +import { inject, Injectable, Injector } from '@angular/core'; import { firstValueFrom } from 'rxjs'; import { AppConfigService } from '../config/app-config.service'; @@ -55,6 +55,9 @@ interface AccessTokenMetadata { providedIn: 'root', }) export class AuthorityAuthService { + private readonly http: HttpClient; + private readonly injector = inject(Injector); + private static readonly SILENT_REFRESH_TIMEOUT_MS = 10_000; private refreshTimer: ReturnType | null = null; @@ -63,13 +66,15 @@ export class AuthorityAuthService { private lastError: AuthErrorReason | null = null; constructor( - private readonly http: HttpClient, + httpBackend: HttpBackend, private readonly config: AppConfigService, private readonly sessionStore: AuthSessionStore, private readonly storage: AuthStorageService, - private readonly dpop: DpopService, - private readonly consoleSession: ConsoleSessionService - ) {} + private readonly dpop: DpopService + ) { + // Use a raw client to avoid interceptor recursion (AuthHttpInterceptor -> AuthorityAuthService). + this.http = new HttpClient(httpBackend); + } get error(): AuthErrorReason | null { return this.lastError; @@ -255,7 +260,7 @@ export class AuthorityAuthService { } catch (error) { this.lastError = 'token_exchange_failed'; this.sessionStore.clear(); - this.consoleSession.clear(); + this.getConsoleSession().clear(); throw error; } } @@ -311,7 +316,7 @@ export class AuthorityAuthService { .catch((error) => { this.lastError = 'refresh_failed'; this.sessionStore.clear(); - this.consoleSession.clear(); + this.getConsoleSession().clear(); throw error; }) .finally(() => { @@ -325,7 +330,7 @@ export class AuthorityAuthService { const session = this.sessionStore.session(); this.cancelRefreshTimer(); this.sessionStore.clear(); - this.consoleSession.clear(); + this.getConsoleSession().clear(); await this.dpop.setNonce(null); const authority = this.config.authority; @@ -462,10 +467,14 @@ export class AuthorityAuthService { freshAuthExpiresAtEpochMs: accessMetadata.freshAuthExpiresAtEpochMs, }; this.sessionStore.setSession(session); - void this.consoleSession.loadConsoleContext(); + void this.getConsoleSession().loadConsoleContext(); this.scheduleRefresh(tokens, this.config.authority); } + private getConsoleSession(): ConsoleSessionService { + return this.injector.get(ConsoleSessionService); + } + private toAuthTokens(payload: TokenResponse): AuthTokens { const expiresAtEpochMs = Date.now() + payload.expires_in * 1000; return { diff --git a/src/Web/StellaOps.Web/src/app/core/context/platform-context-url-sync.service.ts b/src/Web/StellaOps.Web/src/app/core/context/platform-context-url-sync.service.ts index 7b0aea83f..90eaae9ab 100644 --- a/src/Web/StellaOps.Web/src/app/core/context/platform-context-url-sync.service.ts +++ b/src/Web/StellaOps.Web/src/app/core/context/platform-context-url-sync.service.ts @@ -39,7 +39,7 @@ export class PlatformContextUrlSyncService { return; } - const currentUrl = this.router.url; + const currentUrl = this.resolveCurrentUrl(); if (!this.isScopeManagedPath(currentUrl)) { return; } @@ -53,9 +53,11 @@ export class PlatformContextUrlSyncService { return; } + const nextTree = this.router.parseUrl(currentUrl); + nextTree.queryParams = nextQuery; + this.syncingToUrl = true; - void this.router.navigate([], { - queryParams: nextQuery, + void this.router.navigateByUrl(nextTree, { replaceUrl: true, }).finally(() => { this.syncingToUrl = false; @@ -70,7 +72,7 @@ export class PlatformContextUrlSyncService { return; } - const currentUrl = this.router.url; + const currentUrl = this.resolveCurrentUrl(); if (!this.isScopeManagedPath(currentUrl)) { return; } @@ -137,7 +139,7 @@ export class PlatformContextUrlSyncService { const path = url.split('?')[0].toLowerCase(); if ( - path.startsWith('/setup') + path.startsWith('/setup-wizard') || path.startsWith('/auth/') || path.startsWith('/welcome') || path.startsWith('/console/') @@ -147,15 +149,25 @@ export class PlatformContextUrlSyncService { return ( path === '/' - || path.startsWith('/dashboard') + || path.startsWith('/mission-control') || path.startsWith('/releases') || path.startsWith('/security') || path.startsWith('/evidence') - || path.startsWith('/topology') - || path.startsWith('/platform') - || path.startsWith('/operations') - || path.startsWith('/integrations') - || path.startsWith('/administration') + || path.startsWith('/ops') + || path.startsWith('/setup') ); } + + private resolveCurrentUrl(): string { + const routerUrl = this.router.url || '/'; + const browserUrl = `${window.location.pathname}${window.location.search || ''}${window.location.hash || ''}`; + + // During lazy-route startup Router can transiently report "/" even when the + // browser is already at a deep route; preserve the concrete browser location. + if ((routerUrl === '/' || routerUrl === '') && browserUrl && browserUrl !== '/') { + return browserUrl; + } + + return routerUrl; + } } diff --git a/src/Web/StellaOps.Web/src/app/core/context/platform-context.store.ts b/src/Web/StellaOps.Web/src/app/core/context/platform-context.store.ts index 5ee0045e6..127590c54 100644 --- a/src/Web/StellaOps.Web/src/app/core/context/platform-context.store.ts +++ b/src/Web/StellaOps.Web/src/app/core/context/platform-context.store.ts @@ -24,19 +24,23 @@ export interface PlatformContextPreferences { regions: string[]; environments: string[]; timeWindow: string; + stage?: string; updatedAt: string; updatedBy: string; } const DEFAULT_TIME_WINDOW = '24h'; +const DEFAULT_STAGE = 'all'; const REGION_QUERY_KEYS = ['regions', 'region']; const ENVIRONMENT_QUERY_KEYS = ['environments', 'environment', 'env']; const TIME_WINDOW_QUERY_KEYS = ['timeWindow', 'time']; +const STAGE_QUERY_KEYS = ['stage']; interface PlatformContextQueryState { regions: string[]; environments: string[]; timeWindow: string; + stage: string; } @Injectable({ providedIn: 'root' }) @@ -51,6 +55,7 @@ export class PlatformContextStore { readonly selectedRegions = signal([]); readonly selectedEnvironments = signal([]); readonly timeWindow = signal(DEFAULT_TIME_WINDOW); + readonly stage = signal(DEFAULT_STAGE); readonly loading = signal(false); readonly initialized = signal(false); @@ -162,6 +167,17 @@ export class PlatformContextStore { this.bumpContextVersion(); } + setStage(stage: string): void { + const normalized = (stage || DEFAULT_STAGE).trim().toLowerCase(); + if (normalized === this.stage()) { + return; + } + + this.stage.set(normalized); + this.persistPreferences(); + this.bumpContextVersion(); + } + scopeQueryPatch(): Record { const regions = this.selectedRegions(); const environments = this.selectedEnvironments(); @@ -171,6 +187,7 @@ export class PlatformContextStore { regions: regions.length > 0 ? regions.join(',') : null, environments: environments.length > 0 ? environments.join(',') : null, timeWindow: timeWindow !== DEFAULT_TIME_WINDOW ? timeWindow : null, + stage: this.stage() !== DEFAULT_STAGE ? this.stage() : null, }; } @@ -187,8 +204,10 @@ export class PlatformContextStore { const allowedRegions = this.regions().map((item) => item.regionId); const nextRegions = this.normalizeIds(queryState.regions, allowedRegions); const nextTimeWindow = queryState.timeWindow || DEFAULT_TIME_WINDOW; + const nextStage = queryState.stage || DEFAULT_STAGE; const regionsChanged = !this.arraysEqual(nextRegions, this.selectedRegions()); const timeChanged = nextTimeWindow !== this.timeWindow(); + const stageChanged = nextStage !== this.stage(); const preferredEnvironmentIds = queryState.environments.length > 0 ? queryState.environments @@ -197,6 +216,7 @@ export class PlatformContextStore { if (regionsChanged) { this.selectedRegions.set(nextRegions); this.timeWindow.set(nextTimeWindow); + this.stage.set(nextStage); this.loadEnvironments(nextRegions, preferredEnvironmentIds, true); return; } @@ -210,16 +230,18 @@ export class PlatformContextStore { if (environmentsChanged) { this.selectedEnvironments.set(nextEnvironments); } - if (timeChanged || environmentsChanged) { + if (timeChanged || environmentsChanged || stageChanged) { this.timeWindow.set(nextTimeWindow); + this.stage.set(nextStage); this.persistPreferences(); this.bumpContextVersion(); } return; } - if (timeChanged) { + if (timeChanged || stageChanged) { this.timeWindow.set(nextTimeWindow); + this.stage.set(nextStage); this.persistPreferences(); this.bumpContextVersion(); } @@ -235,6 +257,7 @@ export class PlatformContextStore { regions: prefs?.regions ?? [], environments: prefs?.environments ?? [], timeWindow: (prefs?.timeWindow ?? DEFAULT_TIME_WINDOW).trim() || DEFAULT_TIME_WINDOW, + stage: (prefs?.stage ?? DEFAULT_STAGE).trim().toLowerCase() || DEFAULT_STAGE, }; const hydrated = this.mergeWithInitialQueryOverride(preferenceState); const preferredRegions = this.normalizeIds( @@ -243,6 +266,7 @@ export class PlatformContextStore { ); this.selectedRegions.set(preferredRegions); this.timeWindow.set(hydrated.timeWindow); + this.stage.set(hydrated.stage); this.loadEnvironments(preferredRegions, hydrated.environments, false); }, error: () => { @@ -251,6 +275,7 @@ export class PlatformContextStore { regions: [], environments: [], timeWindow: DEFAULT_TIME_WINDOW, + stage: DEFAULT_STAGE, }); const preferredRegions = this.normalizeIds( fallbackState.regions, @@ -259,6 +284,7 @@ export class PlatformContextStore { this.selectedRegions.set(preferredRegions); this.selectedEnvironments.set([]); this.timeWindow.set(fallbackState.timeWindow); + this.stage.set(fallbackState.stage); this.loadEnvironments(preferredRegions, fallbackState.environments, false); }, }); @@ -327,6 +353,7 @@ export class PlatformContextStore { regions: this.selectedRegions(), environments: this.selectedEnvironments(), timeWindow: this.timeWindow(), + stage: this.stage(), }; this.http @@ -355,6 +382,7 @@ export class PlatformContextStore { regions: override.regions.length > 0 ? override.regions : baseState.regions, environments: override.environments.length > 0 ? override.environments : baseState.environments, timeWindow: override.timeWindow || baseState.timeWindow, + stage: override.stage || baseState.stage, }; } @@ -383,8 +411,9 @@ export class PlatformContextStore { const regions = this.readQueryList(queryParams, REGION_QUERY_KEYS); const environments = this.readQueryList(queryParams, ENVIRONMENT_QUERY_KEYS); const timeWindow = this.readQueryValue(queryParams, TIME_WINDOW_QUERY_KEYS); + const stage = this.readQueryValue(queryParams, STAGE_QUERY_KEYS); - if (regions.length === 0 && environments.length === 0 && !timeWindow) { + if (regions.length === 0 && environments.length === 0 && !timeWindow && !stage) { return null; } @@ -392,6 +421,7 @@ export class PlatformContextStore { regions, environments, timeWindow: (timeWindow || DEFAULT_TIME_WINDOW).trim() || DEFAULT_TIME_WINDOW, + stage: (stage || DEFAULT_STAGE).trim().toLowerCase() || DEFAULT_STAGE, }; } diff --git a/src/Web/StellaOps.Web/src/app/core/doctor/doctor-trend.service.ts b/src/Web/StellaOps.Web/src/app/core/doctor/doctor-trend.service.ts index a5df718f8..cdbc8164a 100644 --- a/src/Web/StellaOps.Web/src/app/core/doctor/doctor-trend.service.ts +++ b/src/Web/StellaOps.Web/src/app/core/doctor/doctor-trend.service.ts @@ -10,7 +10,7 @@ import { DoctorTrendResponse } from './doctor-trend.models'; */ @Injectable({ providedIn: 'root' }) export class DoctorTrendService { - private readonly api = inject(DOCTOR_API); + private readonly api = inject(DOCTOR_API, { optional: true }); private readonly destroyRef = inject(DestroyRef); private intervalId: ReturnType | null = null; @@ -33,6 +33,12 @@ export class DoctorTrendService { } private fetchTrends(): void { + if (!this.api) { + this.securityTrend.set([]); + this.platformTrend.set([]); + return; + } + this.api.getTrends?.(['security', 'platform'], 12) ?.pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ diff --git a/src/Web/StellaOps.Web/src/app/features/admin-notifications/admin-notifications.routes.ts b/src/Web/StellaOps.Web/src/app/features/admin-notifications/admin-notifications.routes.ts index e448ca282..9669b18d4 100644 --- a/src/Web/StellaOps.Web/src/app/features/admin-notifications/admin-notifications.routes.ts +++ b/src/Web/StellaOps.Web/src/app/features/admin-notifications/admin-notifications.routes.ts @@ -15,8 +15,10 @@ export const adminNotificationsRoutes: Routes = [ children: [ { path: '', - redirectTo: 'rules', - pathMatch: 'full', + loadComponent: () => + import('./components/notification-rule-list.component').then( + (m) => m.NotificationRuleListComponent + ), }, // Rules routes { @@ -115,8 +117,10 @@ export const adminNotificationsRoutes: Routes = [ children: [ { path: '', - redirectTo: 'quiet-hours', - pathMatch: 'full', + loadComponent: () => + import('./components/quiet-hours-config.component').then( + (m) => m.QuietHoursConfigComponent + ), }, { path: 'quiet-hours', @@ -148,16 +152,19 @@ export const adminNotificationsRoutes: Routes = [ }, ], }, - // Legacy routes for backward compatibility { path: 'quiet-hours', - redirectTo: 'config/quiet-hours', - pathMatch: 'full', + loadComponent: () => + import('./components/quiet-hours-config.component').then( + (m) => m.QuietHoursConfigComponent + ), }, { path: 'overrides', - redirectTo: 'config/overrides', - pathMatch: 'full', + loadComponent: () => + import('./components/operator-override-management.component').then( + (m) => m.OperatorOverrideManagementComponent + ), }, ], }, diff --git a/src/Web/StellaOps.Web/src/app/features/administration/administration-overview.component.ts b/src/Web/StellaOps.Web/src/app/features/administration/administration-overview.component.ts index dd07901eb..41afeeba0 100644 --- a/src/Web/StellaOps.Web/src/app/features/administration/administration-overview.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/administration/administration-overview.component.ts @@ -1,16 +1,12 @@ /** - * Administration Overview (A0) - * Sprint: SPRINT_20260218_007_FE_ui_v2_rewire_administration_foundation (A2-01) - * - * Root overview for the Administration domain. - * Provides summary cards for all A1-A7 capability areas with direct navigation links. - * Ownership labels explicitly reference canonical IA (docs/modules/ui/v2-rewire/source-of-truth.md). + * Setup Overview (A0 + Topology move) + * Sprint: SPRINT_20260221_041_FE_prealpha_ia_ops_setup_rewire */ import { Component, ChangeDetectionStrategy } from '@angular/core'; import { RouterLink } from '@angular/router'; -interface AdminCard { +interface SetupCard { id: string; title: string; description: string; @@ -25,9 +21,9 @@ interface AdminCard { template: `
-

Administration

+

Setup

- Manage identity, tenants, notifications, policy, trust, and system controls. + Manage topology, identity, tenants, notifications, and system controls.

@@ -46,162 +42,164 @@ interface AdminCard {

Operational Drilldowns

`, - styles: [` - .admin-overview { - padding: 1.5rem; - max-width: 1200px; - } + styles: [ + ` + .admin-overview { + padding: 1.5rem; + max-width: 1200px; + } - .admin-overview__header { - margin-bottom: 2rem; - } + .admin-overview__header { + margin-bottom: 2rem; + } - .admin-overview__title { - font-size: 1.5rem; - font-weight: 600; - margin: 0 0 0.5rem; - } + .admin-overview__title { + font-size: 1.5rem; + font-weight: 600; + margin: 0 0 0.5rem; + } - .admin-overview__subtitle { - color: var(--color-text-secondary, #666); - margin: 0; - } + .admin-overview__subtitle { + color: var(--color-text-secondary, #666); + margin: 0; + } - .admin-overview__grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 1rem; - margin-bottom: 2rem; - } + .admin-overview__grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1rem; + margin-bottom: 2rem; + } - .admin-card { - display: flex; - align-items: flex-start; - gap: 1rem; - padding: 1.25rem; - background: var(--color-surface, #fff); - border: 1px solid var(--color-border, #e5e7eb); - border-radius: var(--radius-md, 8px); - text-decoration: none; - color: inherit; - transition: border-color 0.15s, box-shadow 0.15s; - } + .admin-card { + display: flex; + align-items: flex-start; + gap: 1rem; + padding: 1.25rem; + background: var(--color-surface, #fff); + border: 1px solid var(--color-border, #e5e7eb); + border-radius: var(--radius-md, 8px); + text-decoration: none; + color: inherit; + transition: border-color 0.15s, box-shadow 0.15s; + } - .admin-card:hover { - border-color: var(--color-brand-primary, #4f46e5); - box-shadow: 0 2px 8px rgba(0,0,0,0.08); - } + .admin-card:hover { + border-color: var(--color-brand-primary, #4f46e5); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + } - .admin-card__icon { - font-size: 1.5rem; - flex-shrink: 0; - width: 2.5rem; - text-align: center; - } + .admin-card__icon { + font-size: 1.2rem; + flex-shrink: 0; + width: 2.5rem; + text-align: center; + } - .admin-card__title { - font-size: 0.9375rem; - font-weight: 600; - margin: 0 0 0.25rem; - } + .admin-card__title { + font-size: 0.9375rem; + font-weight: 600; + margin: 0 0 0.25rem; + } - .admin-card__description { - font-size: 0.8125rem; - color: var(--color-text-secondary, #666); - margin: 0; - } + .admin-card__description { + font-size: 0.8125rem; + color: var(--color-text-secondary, #666); + margin: 0; + } - .admin-overview__section-heading { - font-size: 0.875rem; - font-weight: 600; - margin: 0 0 0.75rem; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--color-text-secondary, #666); - } + .admin-overview__section-heading { + font-size: 0.875rem; + font-weight: 600; + margin: 0 0 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-text-secondary, #666); + } - .admin-overview__links { - list-style: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - gap: 0.5rem; - } + .admin-overview__links { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.5rem; + } - .admin-overview__links li { - font-size: 0.875rem; - color: var(--color-text-secondary, #666); - } + .admin-overview__links li { + font-size: 0.875rem; + color: var(--color-text-secondary, #666); + } - .admin-overview__links a { - color: var(--color-brand-primary, #4f46e5); - text-decoration: none; - } + .admin-overview__links a { + color: var(--color-brand-primary, #4f46e5); + text-decoration: none; + } - .admin-overview__links a:hover { - text-decoration: underline; - } - `], + .admin-overview__links a:hover { + text-decoration: underline; + } + `, + ], changeDetection: ChangeDetectionStrategy.OnPush, }) export class AdministrationOverviewComponent { - readonly cards: AdminCard[] = [ + readonly cards: SetupCard[] = [ + { + id: 'topology-overview', + title: 'Topology Overview', + description: 'Regions, environments, targets, hosts, and agent fleet posture.', + route: '/setup/topology/overview', + icon: 'TOPO', + }, + { + id: 'topology-map', + title: 'Topology Map', + description: 'Environment and target map with run and evidence correlation.', + route: '/setup/topology/map', + icon: 'MAP', + }, { id: 'identity-access', title: 'Identity & Access', description: 'Users, roles, clients, tokens, and scope management.', - route: '/administration/identity-access', - icon: '👤', + route: '/setup/identity-access', + icon: 'IAM', }, { id: 'tenant-branding', title: 'Tenant & Branding', description: 'Tenant configuration, logo, color scheme, and white-label settings.', - route: '/administration/tenant-branding', - icon: '🎨', + route: '/setup/tenant-branding', + icon: 'UI', }, { id: 'notifications', title: 'Notifications', description: 'Notification rules, channels, and delivery templates.', - route: '/administration/notifications', - icon: '🔔', + route: '/setup/notifications', + icon: 'NTF', }, { id: 'usage', title: 'Usage & Limits', - description: 'Subscription usage, quota policies, and resource ceilings.', - route: '/administration/usage', - icon: '📊', - }, - { - id: 'policy-governance', - title: 'Policy Governance', - description: 'Policy packs, baselines, simulation, exceptions, and approval workflows.', - route: '/administration/policy-governance', - icon: '📋', - }, - { - id: 'trust-signing', - title: 'Trust & Signing', - description: 'Keys, issuers, certificates, transparency log, and trust scoring.', - route: '/administration/trust-signing', - icon: '🔐', + description: 'Subscription usage, quotas, and resource ceilings.', + route: '/setup/usage', + icon: 'QTA', }, { id: 'system', - title: 'System', + title: 'System Settings', description: 'System configuration, diagnostics, offline settings, and security data.', - route: '/administration/system', - icon: '⚙️', + route: '/setup/system', + icon: 'SYS', }, ]; } diff --git a/src/Web/StellaOps.Web/src/app/features/analytics/analytics.routes.ts b/src/Web/StellaOps.Web/src/app/features/analytics/analytics.routes.ts index 3648cec3e..cd00249f7 100644 --- a/src/Web/StellaOps.Web/src/app/features/analytics/analytics.routes.ts +++ b/src/Web/StellaOps.Web/src/app/features/analytics/analytics.routes.ts @@ -7,13 +7,26 @@ import { Routes } from '@angular/router'; export const ANALYTICS_ROUTES: Routes = [ { path: '', - redirectTo: '/security-risk/sbom-lake', - pathMatch: 'full', - data: { breadcrumb: 'Analytics' }, + loadComponent: () => + import('../security/security-sbom-explorer-page.component').then((m) => m.SecuritySbomExplorerPageComponent), + data: { breadcrumb: 'Analytics', mode: 'lake' }, }, { path: 'sbom-lake', - pathMatch: 'full', - redirectTo: '/security-risk/sbom-lake', + loadComponent: () => + import('../security/security-sbom-explorer-page.component').then((m) => m.SecuritySbomExplorerPageComponent), + data: { breadcrumb: 'Analytics', mode: 'lake' }, + }, + { + path: 'sbom-graph', + loadComponent: () => + import('../security/security-sbom-explorer-page.component').then((m) => m.SecuritySbomExplorerPageComponent), + data: { breadcrumb: 'Analytics', mode: 'graph' }, + }, + { + path: 'reachability', + loadComponent: () => + import('../reachability/reachability-center.component').then((m) => m.ReachabilityCenterComponent), + data: { breadcrumb: 'Analytics' }, }, ]; diff --git a/src/Web/StellaOps.Web/src/app/features/bundles/bundles.routes.ts b/src/Web/StellaOps.Web/src/app/features/bundles/bundles.routes.ts index e10ff2458..3c5334e58 100644 --- a/src/Web/StellaOps.Web/src/app/features/bundles/bundles.routes.ts +++ b/src/Web/StellaOps.Web/src/app/features/bundles/bundles.routes.ts @@ -56,9 +56,4 @@ export const BUNDLE_ROUTES: Routes = [ loadComponent: () => import('./bundle-version-detail.component').then((m) => m.BundleVersionDetailComponent), }, - { - path: ':bundleId/:version', - pathMatch: 'full', - redirectTo: ':bundleId/versions/:version', - }, ]; diff --git a/src/Web/StellaOps.Web/src/app/features/console-admin/console-admin.routes.ts b/src/Web/StellaOps.Web/src/app/features/console-admin/console-admin.routes.ts index bb0a38217..a306cdd24 100644 --- a/src/Web/StellaOps.Web/src/app/features/console-admin/console-admin.routes.ts +++ b/src/Web/StellaOps.Web/src/app/features/console-admin/console-admin.routes.ts @@ -20,8 +20,8 @@ export const consoleAdminRoutes: Routes = [ children: [ { path: '', - redirectTo: 'tenants', - pathMatch: 'full' + loadComponent: () => import('./tenants/tenants-list.component').then(m => m.TenantsListComponent), + data: { requiredScopes: [StellaOpsScopes.AUTHORITY_TENANTS_READ] } }, { path: 'tenants', diff --git a/src/Web/StellaOps.Web/src/app/features/dashboard-v3/dashboard-v3.component.ts b/src/Web/StellaOps.Web/src/app/features/dashboard-v3/dashboard-v3.component.ts index b12abb682..0884a8335 100644 --- a/src/Web/StellaOps.Web/src/app/features/dashboard-v3/dashboard-v3.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/dashboard-v3/dashboard-v3.component.ts @@ -1,4 +1,4 @@ -/** +/** * Dashboard V3 - Mission Board * Sprint: SPRINT_20260218_012_FE_ui_v2_rewire_dashboard_v3_mission_board (D7-01 through D7-05) * @@ -116,7 +116,7 @@ interface MissionSummary { {{ summary().dataIntegrityStatus | titlecase }}
Data Integrity
- Ops detail + Ops detail @@ -124,7 +124,7 @@ interface MissionSummary {

Regional Pipeline

- All environments + All environments
@@ -174,7 +174,7 @@ interface MissionSummary {
@@ -118,7 +118,7 @@ interface SearchHit { · Offline {{ agentHealth().offline }}

Targets under non-active agents: {{ impactedTargetsByAgentHealth() }}

- Open Agents + Open Agents
@@ -129,7 +129,7 @@ interface SearchHit { · Failed {{ promotionSummary().failed }}

Manual approvals required: {{ promotionSummary().manualApprovalCount }}

- Open Promotion Paths + Open Promotion Paths
@@ -535,27 +535,27 @@ export class TopologyOverviewPageComponent { } if (kind === 'env') { - void this.router.navigate(['/topology/environments', id, 'posture']); + void this.router.navigate(['/setup/topology/environments', id, 'posture']); return; } if (kind === 'target') { - void this.router.navigate(['/topology/targets'], { queryParams: { targetId: id } }); + void this.router.navigate(['/setup/topology/targets'], { queryParams: { targetId: id } }); return; } if (kind === 'host') { - void this.router.navigate(['/topology/hosts'], { queryParams: { hostId: id } }); + void this.router.navigate(['/setup/topology/hosts'], { queryParams: { hostId: id } }); return; } if (kind === 'agent') { - void this.router.navigate(['/topology/agents'], { queryParams: { agentId: id } }); + void this.router.navigate(['/setup/topology/agents'], { queryParams: { agentId: id } }); } } openTarget(targetId: string): void { - void this.router.navigate(['/topology/targets'], { queryParams: { targetId } }); + void this.router.navigate(['/setup/topology/targets'], { queryParams: { targetId } }); } private load(): void { @@ -651,3 +651,5 @@ export class TopologyOverviewPageComponent { return 4; } } + + diff --git a/src/Web/StellaOps.Web/src/app/features/topology/topology-promotion-paths-page.component.ts b/src/Web/StellaOps.Web/src/app/features/topology/topology-promotion-paths-page.component.ts index f6193c1ca..a90e827f3 100644 --- a/src/Web/StellaOps.Web/src/app/features/topology/topology-promotion-paths-page.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/topology/topology-promotion-paths-page.component.ts @@ -129,7 +129,7 @@ interface PathRow extends TopologyPromotionPath { {{ entry.inbound }} {{ entry.outbound }} - Open + Open } @empty { @@ -395,3 +395,5 @@ export class TopologyPromotionPathsPageComponent { }); } } + + diff --git a/src/Web/StellaOps.Web/src/app/features/topology/topology-regions-environments-page.component.ts b/src/Web/StellaOps.Web/src/app/features/topology/topology-regions-environments-page.component.ts index b1cb32c5d..e0c24a916 100644 --- a/src/Web/StellaOps.Web/src/app/features/topology/topology-regions-environments-page.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/topology/topology-regions-environments-page.component.ts @@ -92,7 +92,7 @@ type RegionsView = 'region-first' | 'flat' | 'graph'; {{ environmentHealthLabel(env.environmentId) }} {{ env.targetCount }} - Open + Open } @empty { @@ -126,7 +126,7 @@ type RegionsView = 'region-first' | 'flat' | 'graph'; {{ env.environmentType }} {{ environmentHealthLabel(env.environmentId) }} {{ env.targetCount }} - Open + Open } @empty { @@ -158,9 +158,9 @@ type RegionsView = 'region-first' | 'flat' | 'graph'; · targets {{ selectedEnvironmentTargetCount() }}

@@ -558,3 +558,5 @@ export class TopologyRegionsEnvironmentsPageComponent { return values.some((value) => value.toLowerCase().includes(query)); } } + + diff --git a/src/Web/StellaOps.Web/src/app/features/topology/topology-runtime-drift-page.component.ts b/src/Web/StellaOps.Web/src/app/features/topology/topology-runtime-drift-page.component.ts new file mode 100644 index 000000000..76d729712 --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/features/topology/topology-runtime-drift-page.component.ts @@ -0,0 +1,18 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'app-topology-runtime-drift-page', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+
+

Runtime Drift

+

Expected versus observed runtime posture, agent mismatches, and drift risk indicators.

+
+
+ `, +}) +export class TopologyRuntimeDriftPageComponent {} + + diff --git a/src/Web/StellaOps.Web/src/app/features/topology/topology-target-detail-page.component.ts b/src/Web/StellaOps.Web/src/app/features/topology/topology-target-detail-page.component.ts new file mode 100644 index 000000000..b060acd9f --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/features/topology/topology-target-detail-page.component.ts @@ -0,0 +1,22 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +@Component({ + selector: 'app-topology-target-detail-page', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+
+

Target {{ targetId }}

+

Health, deployments, connectivity, and evidence links for the selected target.

+
+
+ `, +}) +export class TopologyTargetDetailPageComponent { + private readonly route = inject(ActivatedRoute); + readonly targetId = this.route.snapshot.paramMap.get('targetId') ?? 'unknown'; +} + + diff --git a/src/Web/StellaOps.Web/src/app/features/topology/topology-targets-page.component.ts b/src/Web/StellaOps.Web/src/app/features/topology/topology-targets-page.component.ts index 1d053f5b0..98ec57c02 100644 --- a/src/Web/StellaOps.Web/src/app/features/topology/topology-targets-page.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/topology/topology-targets-page.component.ts @@ -98,9 +98,9 @@ import { TopologyAgent, TopologyHost, TopologyTarget } from './topology.models';

Host: {{ selectedHostName() }}

Agent: {{ selectedAgentName() }}

} @else {

Select a target row to view its topology mapping details.

@@ -409,3 +409,5 @@ export class TopologyTargetsPageComponent { }); } } + + diff --git a/src/Web/StellaOps.Web/src/app/features/topology/topology.models.ts b/src/Web/StellaOps.Web/src/app/features/topology/topology.models.ts index eba959909..daafdfdd9 100644 --- a/src/Web/StellaOps.Web/src/app/features/topology/topology.models.ts +++ b/src/Web/StellaOps.Web/src/app/features/topology/topology.models.ts @@ -104,3 +104,5 @@ export interface EvidenceCapsuleRow { status: string; updatedAt: string; } + + diff --git a/src/Web/StellaOps.Web/src/app/features/trust-admin/trust-admin.routes.ts b/src/Web/StellaOps.Web/src/app/features/trust-admin/trust-admin.routes.ts index b3af37e77..60c8fc0f0 100644 --- a/src/Web/StellaOps.Web/src/app/features/trust-admin/trust-admin.routes.ts +++ b/src/Web/StellaOps.Web/src/app/features/trust-admin/trust-admin.routes.ts @@ -29,8 +29,11 @@ export const trustAdminRoutes: Routes = [ children: [ { path: '', - redirectTo: 'keys', - pathMatch: 'full', + loadComponent: () => + import('./signing-key-dashboard.component').then( + (m) => m.SigningKeyDashboardComponent + ), + data: { requiredScopes: [StellaOpsScopes.SIGNER_READ] }, }, { path: 'keys', diff --git a/src/Web/StellaOps.Web/src/app/layout/app-sidebar/app-sidebar.component.ts b/src/Web/StellaOps.Web/src/app/layout/app-sidebar/app-sidebar.component.ts index 056ffceea..e18e468c1 100644 --- a/src/Web/StellaOps.Web/src/app/layout/app-sidebar/app-sidebar.component.ts +++ b/src/Web/StellaOps.Web/src/app/layout/app-sidebar/app-sidebar.component.ts @@ -281,19 +281,41 @@ export class AppSidebarComponent { private readonly pendingApprovalsCount = signal(0); - /** Track which groups are expanded - default open: Releases, Security, Platform. */ - readonly expandedGroups = signal>(new Set(['releases', 'security', 'platform'])); + /** Track which groups are expanded - default open: Releases, Security, Ops. */ + readonly expandedGroups = signal>(new Set(['releases', 'security', 'ops'])); /** - * Navigation sections - canonical IA. - * Root modules: Mission Control, Releases, Security, Evidence, Topology, Platform. + * Navigation sections - pre-alpha canonical IA. + * Root modules: Mission Control, Releases, Security, Evidence, Ops, Setup. */ readonly navSections: NavSection[] = [ { - id: 'dashboard', - label: 'Mission Control', + id: 'mission-board', + label: 'Mission Board', icon: 'dashboard', - route: '/dashboard', + route: '/mission-control/board', + requireAnyScope: [ + StellaOpsScopes.UI_READ, + StellaOpsScopes.RELEASE_READ, + StellaOpsScopes.SCANNER_READ, + ], + }, + { + id: 'mission-alerts', + label: 'Mission Alerts', + icon: 'alert-triangle', + route: '/mission-control/alerts', + requireAnyScope: [ + StellaOpsScopes.UI_READ, + StellaOpsScopes.RELEASE_READ, + StellaOpsScopes.SCANNER_READ, + ], + }, + { + id: 'mission-activity', + label: 'Mission Activity', + icon: 'clock', + route: '/mission-control/activity', requireAnyScope: [ StellaOpsScopes.UI_READ, StellaOpsScopes.RELEASE_READ, @@ -311,6 +333,7 @@ export class AppSidebarComponent { StellaOpsScopes.RELEASE_PUBLISH, ], children: [ + { id: 'rel-overview', label: 'Overview', route: '/releases/overview', icon: 'chart' }, { id: 'rel-versions', label: 'Release Versions', @@ -338,7 +361,10 @@ export class AppSidebarComponent { StellaOpsScopes.EXCEPTION_APPROVE, ], }, - { id: 'rel-hotfix', label: 'Hotfix Lane', route: '/releases/hotfix', icon: 'zap' }, + { id: 'rel-hotfix-list', label: 'Hotfixes', route: '/releases/hotfixes', icon: 'zap' }, + { id: 'rel-promotion-queue', label: 'Promotion Queue', route: '/releases/promotion-queue', icon: 'git-merge' }, + { id: 'rel-envs', label: 'Environments', route: '/releases/environments', icon: 'globe' }, + { id: 'rel-deployments', label: 'Deployment History', route: '/releases/deployments', icon: 'activity' }, { id: 'rel-create', label: 'Create Version', @@ -366,8 +392,8 @@ export class AppSidebarComponent { children: [ { id: 'sec-overview', label: 'Posture', route: '/security/posture', icon: 'chart' }, { id: 'sec-triage', label: 'Triage', route: '/security/triage', icon: 'list' }, - { id: 'sec-disposition', label: 'Disposition Center', route: '/security/disposition', icon: 'shield-off' }, - { id: 'sec-sbom', label: 'SBOM', route: '/security/sbom/lake', icon: 'graph' }, + { id: 'sec-advisories-vex', label: 'Advisories & VEX', route: '/security/advisories-vex', icon: 'shield-off' }, + { id: 'sec-supply-chain', label: 'Supply-Chain Data', route: '/security/supply-chain-data', icon: 'graph' }, { id: 'sec-reachability', label: 'Reachability', route: '/security/reachability', icon: 'cpu' }, { id: 'sec-reports', label: 'Reports', route: '/security/reports', icon: 'book-open' }, ], @@ -385,39 +411,18 @@ export class AppSidebarComponent { StellaOpsScopes.VEX_EXPORT, ], children: [ + { id: 'ev-overview', label: 'Overview', route: '/evidence/overview', icon: 'home' }, { id: 'ev-capsules', label: 'Decision Capsules', route: '/evidence/capsules', icon: 'archive' }, - { id: 'ev-verify', label: 'Replay & Verify', route: '/evidence/verification/replay', icon: 'refresh' }, + { id: 'ev-verify', label: 'Replay & Verify', route: '/evidence/verify-replay', icon: 'refresh' }, { id: 'ev-exports', label: 'Export Center', route: '/evidence/exports', icon: 'download' }, { id: 'ev-audit', label: 'Audit Log', route: '/evidence/audit-log', icon: 'book-open' }, - { id: 'ev-trust', label: 'Trust & Signing', route: '/platform/setup/trust-signing', icon: 'shield' }, ], }, { - id: 'topology', - label: 'Topology', - icon: 'server', - route: '/topology', - requireAnyScope: [ - StellaOpsScopes.RELEASE_READ, - StellaOpsScopes.ORCH_READ, - StellaOpsScopes.ORCH_OPERATE, - StellaOpsScopes.UI_ADMIN, - ], - children: [ - { id: 'top-overview', label: 'Overview', route: '/topology/overview', icon: 'chart' }, - { id: 'top-regions', label: 'Regions & Environments', route: '/topology/regions', icon: 'globe' }, - { id: 'top-environments', label: 'Environment Posture', route: '/topology/environments', icon: 'list' }, - { id: 'top-targets', label: 'Targets / Runtimes', route: '/topology/targets', icon: 'package' }, - { id: 'top-hosts', label: 'Hosts', route: '/topology/hosts', icon: 'hard-drive' }, - { id: 'top-agents', label: 'Agents', route: '/topology/agents', icon: 'cpu' }, - { id: 'top-paths', label: 'Promotion Graph', route: '/topology/promotion-graph', icon: 'git-merge' }, - ], - }, - { - id: 'platform', - label: 'Platform', + id: 'ops', + label: 'Ops', icon: 'settings', - route: '/platform', + route: '/ops', sparklineData$: () => this.doctorTrendService.platformTrend(), requireAnyScope: [ StellaOpsScopes.UI_ADMIN, @@ -425,17 +430,42 @@ export class AppSidebarComponent { StellaOpsScopes.ORCH_OPERATE, StellaOpsScopes.HEALTH_READ, StellaOpsScopes.NOTIFY_VIEWER, + StellaOpsScopes.POLICY_READ, ], children: [ - { id: 'plat-home', label: 'Overview', route: '/platform', icon: 'home' }, - { id: 'plat-ops', label: 'Ops', route: '/platform/ops', icon: 'activity' }, - { id: 'plat-jobs', label: 'Jobs & Queues', route: '/platform/ops/jobs-queues', icon: 'play' }, - { id: 'plat-integrity', label: 'Data Integrity', route: '/platform/ops/data-integrity', icon: 'shield' }, - { id: 'plat-system-health', label: 'System Health', route: '/platform/ops/system-health', icon: 'heart' }, - { id: 'plat-feeds', label: 'Feeds & Airgap', route: '/platform/ops/feeds-airgap', icon: 'rss' }, - { id: 'plat-quotas', label: 'Quotas & Limits', route: '/platform/ops/quotas', icon: 'bar-chart' }, - { id: 'plat-integrations', label: 'Integrations', route: '/platform/integrations', icon: 'plug' }, - { id: 'plat-setup', label: 'Setup', route: '/platform/setup', icon: 'cog' }, + { id: 'ops-overview', label: 'Overview', route: '/ops', icon: 'home' }, + { id: 'ops-operations', label: 'Operations', route: '/ops/operations', icon: 'activity' }, + { id: 'ops-integrations', label: 'Integrations', route: '/ops/integrations', icon: 'plug' }, + { id: 'ops-advisory-vex', label: 'Advisory & VEX Sources', route: '/ops/integrations/advisory-vex-sources', icon: 'rss' }, + { id: 'ops-policy', label: 'Policy', route: '/ops/policy', icon: 'shield' }, + { id: 'ops-platform-setup', label: 'Platform Setup', route: '/ops/platform-setup', icon: 'cog' }, + ], + }, + { + id: 'setup', + label: 'Setup', + icon: 'server', + route: '/setup', + requireAnyScope: [ + StellaOpsScopes.UI_ADMIN, + StellaOpsScopes.RELEASE_READ, + StellaOpsScopes.ORCH_READ, + StellaOpsScopes.ORCH_OPERATE, + ], + children: [ + { id: 'setup-overview', label: 'Overview', route: '/setup', icon: 'home' }, + { id: 'setup-topology-overview', label: 'Topology Overview', route: '/setup/topology/overview', icon: 'chart' }, + { id: 'setup-topology-map', label: 'Topology Map', route: '/setup/topology/map', icon: 'globe' }, + { id: 'setup-topology-targets', label: 'Targets', route: '/setup/topology/targets', icon: 'package' }, + { id: 'setup-topology-hosts', label: 'Hosts', route: '/setup/topology/hosts', icon: 'hard-drive' }, + { id: 'setup-topology-agents', label: 'Agent Fleet', route: '/setup/topology/agents', icon: 'cpu' }, + { id: 'setup-topology-connectivity', label: 'Connectivity', route: '/setup/topology/connectivity', icon: 'rss' }, + { id: 'setup-topology-drift', label: 'Runtime Drift', route: '/setup/topology/runtime-drift', icon: 'activity' }, + { id: 'setup-iam', label: 'Identity & Access', route: '/setup/identity-access', icon: 'user' }, + { id: 'setup-branding', label: 'Tenant & Branding', route: '/setup/tenant-branding', icon: 'paintbrush' }, + { id: 'setup-notifications', label: 'Notifications', route: '/setup/notifications', icon: 'bell' }, + { id: 'setup-usage', label: 'Usage & Limits', route: '/setup/usage', icon: 'bar-chart' }, + { id: 'setup-system', label: 'System Settings', route: '/setup/system', icon: 'settings' }, ], }, ]; diff --git a/src/Web/StellaOps.Web/src/app/layout/app-topbar/app-topbar.component.ts b/src/Web/StellaOps.Web/src/app/layout/app-topbar/app-topbar.component.ts index c7f248d5c..db55e892e 100644 --- a/src/Web/StellaOps.Web/src/app/layout/app-topbar/app-topbar.component.ts +++ b/src/Web/StellaOps.Web/src/app/layout/app-topbar/app-topbar.component.ts @@ -3,11 +3,16 @@ import { ChangeDetectionStrategy, Output, EventEmitter, + computed, + DestroyRef, inject, ElementRef, HostListener, signal, } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { NavigationEnd, Router, RouterLink } from '@angular/router'; +import { filter } from 'rxjs/operators'; import { AuthSessionStore } from '../../core/auth/auth-session.store'; import { ConsoleSessionStore } from '../../core/console/console-session.store'; @@ -30,6 +35,7 @@ import { UserMenuComponent } from '../../shared/components/user-menu/user-menu.c imports: [ GlobalSearchComponent, ContextChipsComponent, + RouterLink, UserMenuComponent ], template: ` @@ -60,6 +66,10 @@ import { UserMenuComponent } from '../../shared/components/user-menu/user-menu.c
+ @if (primaryAction(); as action) { + {{ action.label }} + } +
{{ context.regionSummary() }} {{ context.environmentSummary() }} + Stage: {{ context.stage() }}
@@ -94,6 +112,7 @@ import { PlatformContextStore } from '../../core/context/platform-context.store' +
@if (context.error()) { diff --git a/src/Web/StellaOps.Web/src/app/layout/context-chips/index.ts b/src/Web/StellaOps.Web/src/app/layout/context-chips/index.ts index 5c6dc55a0..673d75efa 100644 --- a/src/Web/StellaOps.Web/src/app/layout/context-chips/index.ts +++ b/src/Web/StellaOps.Web/src/app/layout/context-chips/index.ts @@ -3,3 +3,4 @@ export { OfflineStatusChipComponent } from './offline-status-chip.component'; export { FeedSnapshotChipComponent } from './feed-snapshot-chip.component'; export { PolicyBaselineChipComponent } from './policy-baseline-chip.component'; export { EvidenceModeChipComponent } from './evidence-mode-chip.component'; +export { LiveEventStreamChipComponent } from './live-event-stream-chip.component'; diff --git a/src/Web/StellaOps.Web/src/app/layout/context-chips/live-event-stream-chip.component.ts b/src/Web/StellaOps.Web/src/app/layout/context-chips/live-event-stream-chip.component.ts new file mode 100644 index 000000000..8ae565699 --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/layout/context-chips/live-event-stream-chip.component.ts @@ -0,0 +1,70 @@ +import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; +import { PlatformContextStore } from '../../core/context/platform-context.store'; + +@Component({ + selector: 'app-live-event-stream-chip', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + + Events: {{ status() === 'connected' ? 'CONNECTED' : 'DEGRADED' }} + + `, + styles: [ + ` + .chip { + display: inline-flex; + align-items: center; + gap: 0.22rem; + border-radius: var(--radius-full); + border: 1px solid var(--color-border-primary); + background: var(--color-surface-primary); + padding: 0.14rem 0.45rem; + font-size: 0.625rem; + font-family: var(--font-family-mono); + letter-spacing: 0.04em; + text-transform: uppercase; + } + + .chip--ok { + border-color: color-mix(in srgb, var(--color-status-ok) 45%, var(--color-border-primary)); + color: var(--color-status-ok); + } + + .chip--degraded { + border-color: color-mix(in srgb, var(--color-status-warn) 45%, var(--color-border-primary)); + color: var(--color-status-warn); + } + + .chip__icon { + flex-shrink: 0; + } + + .chip__label { + white-space: nowrap; + } + `, + ], +}) +export class LiveEventStreamChipComponent { + private readonly context = inject(PlatformContextStore); + + readonly status = computed<'connected' | 'degraded'>(() => { + if (this.context.loading()) { + return 'degraded'; + } + + return this.context.error() ? 'degraded' : 'connected'; + }); +} diff --git a/src/Web/StellaOps.Web/src/app/routes/evidence-audit.routes.ts b/src/Web/StellaOps.Web/src/app/routes/evidence-audit.routes.ts index 65cc0cce6..220a7ff1a 100644 --- a/src/Web/StellaOps.Web/src/app/routes/evidence-audit.routes.ts +++ b/src/Web/StellaOps.Web/src/app/routes/evidence-audit.routes.ts @@ -1,136 +1,4 @@ -/** - * Evidence & Audit Domain Routes - * Sprint: SPRINT_20260218_015_FE_ui_v2_rewire_evidence_audit_consolidation (V10-01 through V10-05) - */ import { Routes } from '@angular/router'; -export const EVIDENCE_AUDIT_ROUTES: Routes = [ - { - path: '', - title: 'Evidence & Audit', - data: { breadcrumb: 'Evidence & Audit' }, - loadComponent: () => - import('../features/evidence-audit/evidence-audit-overview.component').then( - (m) => m.EvidenceAuditOverviewComponent - ), - }, - { - path: 'home', - pathMatch: 'full', - redirectTo: '', - }, - { - path: 'packs', - title: 'Evidence Packs', - data: { breadcrumb: 'Evidence Packs' }, - loadComponent: () => - import('../features/evidence-pack/evidence-pack-list.component').then( - (m) => m.EvidencePackListComponent - ), - }, - { - path: 'packs/:packId', - title: 'Evidence Pack', - data: { breadcrumb: 'Evidence Pack' }, - loadComponent: () => - import('../features/evidence-pack/evidence-pack-viewer.component').then( - (m) => m.EvidencePackViewerComponent - ), - }, - { - path: 'bundles', - title: 'Evidence Bundles', - data: { breadcrumb: 'Evidence Bundles' }, - loadComponent: () => - import('../features/evidence-export/evidence-bundles.component').then( - (m) => m.EvidenceBundlesComponent - ), - }, - { - path: 'proofs', - title: 'Proof Chains', - data: { breadcrumb: 'Proof Chains' }, - loadComponent: () => - import('../features/proof-chain/proof-chain.component').then( - (m) => m.ProofChainComponent - ), - }, - { - path: 'proofs/:subjectDigest', - title: 'Proof Chain', - data: { breadcrumb: 'Proof Chain' }, - loadComponent: () => - import('../features/proof-chain/proof-chain.component').then( - (m) => m.ProofChainComponent - ), - }, - { - path: 'timeline', - title: 'Timeline', - data: { breadcrumb: 'Timeline' }, - loadChildren: () => - import('../features/timeline/timeline.routes').then((m) => m.TIMELINE_ROUTES), - }, - { - path: 'replay', - title: 'Replay & Verify', - data: { breadcrumb: 'Replay & Verify' }, - loadComponent: () => - import('../features/evidence-export/replay-controls.component').then( - (m) => m.ReplayControlsComponent - ), - }, - { - path: 'receipts/cvss/:receiptId', - title: 'CVSS Receipt', - data: { breadcrumb: 'CVSS Receipt' }, - loadComponent: () => - import('../features/cvss/cvss-receipt.component').then((m) => m.CvssReceiptComponent), - }, - { - path: 'audit-log', - title: 'Audit Log', - data: { breadcrumb: 'Audit Log' }, - loadChildren: () => - import('../features/audit-log/audit-log.routes').then((m) => m.auditLogRoutes), - }, - { - path: 'audit', - pathMatch: 'full', - redirectTo: 'audit-log', - }, - { - path: 'change-trace', - title: 'Change Trace', - data: { breadcrumb: 'Change Trace' }, - loadChildren: () => - import('../features/change-trace/change-trace.routes').then((m) => m.changeTraceRoutes), - }, - { - path: 'trust-signing', - title: 'Trust & Signing', - data: { breadcrumb: 'Trust & Signing' }, - loadComponent: () => - import('../features/settings/trust/trust-settings-page.component').then( - (m) => m.TrustSettingsPageComponent - ), - }, - { - path: 'trust-signing/:page', - title: 'Trust & Signing', - data: { breadcrumb: 'Trust & Signing' }, - loadComponent: () => - import('../features/settings/trust/trust-settings-page.component').then( - (m) => m.TrustSettingsPageComponent - ), - }, - { - path: 'evidence', - title: 'Export Center', - data: { breadcrumb: 'Export Center' }, - loadChildren: () => - import('../features/evidence-export/evidence-export.routes').then( - (m) => m.evidenceExportRoutes - ), - }, -]; +/** Legacy Evidence & Audit tree retired in pre-alpha IA. */ +export const EVIDENCE_AUDIT_ROUTES: Routes = []; diff --git a/src/Web/StellaOps.Web/src/app/routes/evidence.routes.ts b/src/Web/StellaOps.Web/src/app/routes/evidence.routes.ts index b98e53130..e46afaaa5 100644 --- a/src/Web/StellaOps.Web/src/app/routes/evidence.routes.ts +++ b/src/Web/StellaOps.Web/src/app/routes/evidence.routes.ts @@ -3,8 +3,12 @@ import { Routes } from '@angular/router'; export const EVIDENCE_ROUTES: Routes = [ { path: '', - pathMatch: 'full', - redirectTo: 'capsules', + title: 'Evidence Overview', + data: { breadcrumb: 'Overview' }, + loadComponent: () => + import('../features/evidence-audit/evidence-audit-overview.component').then( + (m) => m.EvidenceAuditOverviewComponent, + ), }, { path: 'overview', @@ -15,23 +19,12 @@ export const EVIDENCE_ROUTES: Routes = [ (m) => m.EvidenceAuditOverviewComponent, ), }, - { - path: 'search', - title: 'Evidence Search', - data: { breadcrumb: 'Search' }, - loadComponent: () => - import('../features/evidence-pack/evidence-pack-list.component').then( - (m) => m.EvidencePackListComponent, - ), - }, { path: 'capsules', title: 'Decision Capsules', data: { breadcrumb: 'Capsules' }, loadComponent: () => - import('../features/evidence-pack/evidence-pack-list.component').then( - (m) => m.EvidencePackListComponent, - ), + import('../features/evidence-pack/evidence-pack-list.component').then((m) => m.EvidencePackListComponent), }, { path: 'capsules/:capsuleId', @@ -42,6 +35,19 @@ export const EVIDENCE_ROUTES: Routes = [ (m) => m.EvidencePackViewerComponent, ), }, + { + path: 'verify-replay', + title: 'Verify & Replay', + data: { breadcrumb: 'Verify & Replay' }, + loadComponent: () => + import('../features/evidence-export/replay-controls.component').then((m) => m.ReplayControlsComponent), + }, + { + path: 'proofs', + title: 'Proof Chains', + data: { breadcrumb: 'Proof Chains' }, + loadComponent: () => import('../features/proof-chain/proof-chain.component').then((m) => m.ProofChainComponent), + }, { path: 'exports', title: 'Evidence Exports', @@ -49,107 +55,10 @@ export const EVIDENCE_ROUTES: Routes = [ loadChildren: () => import('../features/evidence-export/evidence-export.routes').then((m) => m.evidenceExportRoutes), }, - { - path: 'verification', - pathMatch: 'full', - redirectTo: 'verification/replay', - }, - { - path: 'verification/replay', - title: 'Replay & Verify', - data: { breadcrumb: 'Replay & Verify' }, - loadComponent: () => - import('../features/evidence-export/replay-controls.component').then( - (m) => m.ReplayControlsComponent, - ), - }, - { - path: 'verification/proofs', - title: 'Proof Explorer', - data: { breadcrumb: 'Proof Explorer' }, - loadComponent: () => - import('../features/proof-chain/proof-chain.component').then((m) => m.ProofChainComponent), - }, - { - path: 'verification/offline', - title: 'Offline Verify', - data: { breadcrumb: 'Offline Verify' }, - loadComponent: () => - import('../features/settings/trust/trust-settings-page.component').then( - (m) => m.TrustSettingsPageComponent, - ), - }, - { - path: 'verify-replay', - pathMatch: 'full', - redirectTo: 'verification/replay', - }, { path: 'audit-log', title: 'Audit Log', data: { breadcrumb: 'Audit Log' }, - loadChildren: () => - import('../features/audit-log/audit-log.routes').then((m) => m.auditLogRoutes), - }, - { - path: 'trust-status', - title: 'Trust Status', - data: { breadcrumb: 'Trust Status' }, - loadComponent: () => - import('../features/settings/trust/trust-settings-page.component').then( - (m) => m.TrustSettingsPageComponent, - ), - }, - - // Legacy aliases. - { - path: 'packs', - pathMatch: 'full', - redirectTo: 'capsules', - }, - { - path: 'packs/:packId', - pathMatch: 'full', - redirectTo: 'capsules/:packId', - }, - { - path: 'bundles', - pathMatch: 'full', - redirectTo: 'exports', - }, - { - path: 'evidence', - pathMatch: 'full', - redirectTo: 'exports', - }, - { - path: 'proofs', - pathMatch: 'full', - redirectTo: 'verification/proofs', - }, - { - path: 'proofs/:subjectDigest', - pathMatch: 'full', - redirectTo: 'verification/proofs', - }, - { - path: 'replay', - pathMatch: 'full', - redirectTo: 'verification/replay', - }, - { - path: 'timeline', - pathMatch: 'full', - redirectTo: 'audit-log', - }, - { - path: 'change-trace', - pathMatch: 'full', - redirectTo: 'audit-log', - }, - { - path: 'trust-signing', - pathMatch: 'full', - redirectTo: '/platform/setup/trust-signing', + loadChildren: () => import('../features/audit-log/audit-log.routes').then((m) => m.auditLogRoutes), }, ]; diff --git a/src/Web/StellaOps.Web/src/app/routes/legacy-redirects.routes.ts b/src/Web/StellaOps.Web/src/app/routes/legacy-redirects.routes.ts index cd3665828..754cb0e60 100644 --- a/src/Web/StellaOps.Web/src/app/routes/legacy-redirects.routes.ts +++ b/src/Web/StellaOps.Web/src/app/routes/legacy-redirects.routes.ts @@ -1,274 +1,15 @@ -/** - * Legacy Route Redirects - * Sprint: SPRINT_20260118_009_FE_route_migration_shared_components (ROUTE-001) - * Updated: SPRINT_20260218_006_FE_ui_v2_rewire_navigation_shell_route_migration (N1-04) - * - * Redirects pre-v1 paths to v2 canonical domain paths per: - * docs/modules/ui/v2-rewire/S00_route_deprecation_map.md - * - * v1 alias routes (/releases, /security, /operations, /settings, etc.) are kept - * as active loadChildren entries in app.routes.ts and are NOT redirected here. - * They will be converted to redirects at SPRINT_20260218_016 cutover. - */ +import { Routes } from '@angular/router'; -import { RedirectFunction, Routes } from '@angular/router'; - -interface LegacyRedirectTemplate { +export interface LegacyRedirectRouteTemplate { path: string; redirectTo: string; - pathMatch: 'full'; + pathMatch?: 'prefix' | 'full'; } -function preserveQueryAndFragment(targetTemplate: string): RedirectFunction { - return ({ params, queryParams, fragment }) => { - let target = targetTemplate; +/** + * Pre-alpha route policy: no legacy redirect map. + * Keep exports as empty collections so historical imports compile without enabling aliases. + */ +export const LEGACY_REDIRECT_ROUTE_TEMPLATES: readonly LegacyRedirectRouteTemplate[] = []; - for (const [key, value] of Object.entries(params ?? {})) { - target = target.replace(`:${key}`, String(value)); - } - - const search = new URLSearchParams(); - for (const [key, value] of Object.entries(queryParams ?? {})) { - if (Array.isArray(value)) { - for (const item of value) { - search.append(key, String(item)); - } - } else if (value !== null && value !== undefined) { - search.set(key, String(value)); - } - } - - const querySuffix = search.toString(); - const fragmentSuffix = fragment ? `#${fragment}` : ''; - if (querySuffix.length > 0) { - return `${target}?${querySuffix}${fragmentSuffix}`; - } - - return `${target}${fragmentSuffix}`; - }; -} - -export const LEGACY_REDIRECT_ROUTE_TEMPLATES: readonly LegacyRedirectTemplate[] = [ - // =========================================== - // Home & Dashboard - // =========================================== - { path: 'dashboard/sources', redirectTo: '/platform/ops/feeds-airgap', pathMatch: 'full' }, - { path: 'home', redirectTo: '/', pathMatch: 'full' }, - - // =========================================== - // Pack 22 root migration aliases - // =========================================== - { path: 'release-control', redirectTo: '/releases', pathMatch: 'full' }, - { path: 'release-control/releases', redirectTo: '/releases', pathMatch: 'full' }, - { path: 'release-control/releases/:id', redirectTo: '/releases/runs/:id/timeline', pathMatch: 'full' }, - { path: 'release-control/approvals', redirectTo: '/releases/approvals', pathMatch: 'full' }, - { path: 'release-control/approvals/:id', redirectTo: '/releases/approvals/:id', pathMatch: 'full' }, - { path: 'release-control/runs', redirectTo: '/releases/runs', pathMatch: 'full' }, - { path: 'release-control/deployments', redirectTo: '/releases/runs', pathMatch: 'full' }, - { path: 'release-control/promotions', redirectTo: '/releases/runs', pathMatch: 'full' }, - { path: 'release-control/hotfixes', redirectTo: '/releases/hotfix', pathMatch: 'full' }, - { path: 'release-control/regions', redirectTo: '/topology/regions', pathMatch: 'full' }, - { path: 'release-control/regions/:region', redirectTo: '/topology/regions', pathMatch: 'full' }, - { path: 'release-control/regions/:region/environments/:env', redirectTo: '/topology/environments/:env/posture', pathMatch: 'full' }, - { path: 'release-control/setup', redirectTo: '/topology/promotion-graph', pathMatch: 'full' }, - { path: 'release-control/setup/environments-paths', redirectTo: '/topology/promotion-graph', pathMatch: 'full' }, - { path: 'release-control/setup/targets-agents', redirectTo: '/topology/targets', pathMatch: 'full' }, - { path: 'release-control/setup/workflows', redirectTo: '/topology/workflows', pathMatch: 'full' }, - { path: 'release-control/governance', redirectTo: '/topology/workflows', pathMatch: 'full' }, - - { path: 'security-risk', redirectTo: '/security', pathMatch: 'full' }, - { path: 'security-risk/findings', redirectTo: '/security/triage', pathMatch: 'full' }, - { path: 'security-risk/findings/:findingId', redirectTo: '/security/triage/:findingId', pathMatch: 'full' }, - { path: 'security-risk/vulnerabilities', redirectTo: '/security/triage', pathMatch: 'full' }, - { path: 'security-risk/vulnerabilities/:vulnId', redirectTo: '/security/triage', pathMatch: 'full' }, - { path: 'security-risk/sbom', redirectTo: '/security/sbom/graph', pathMatch: 'full' }, - { path: 'security-risk/sbom-lake', redirectTo: '/security/sbom/lake', pathMatch: 'full' }, - { path: 'security-risk/vex', redirectTo: '/security/disposition', pathMatch: 'full' }, - { path: 'security-risk/exceptions', redirectTo: '/security/disposition', pathMatch: 'full' }, - { path: 'security-risk/advisory-sources', redirectTo: '/platform/integrations/feeds', pathMatch: 'full' }, - - { path: 'evidence-audit', redirectTo: '/evidence/capsules', pathMatch: 'full' }, - { path: 'evidence-audit/packs', redirectTo: '/evidence/capsules', pathMatch: 'full' }, - { path: 'evidence-audit/packs/:packId', redirectTo: '/evidence/capsules/:packId', pathMatch: 'full' }, - { path: 'evidence-audit/bundles', redirectTo: '/evidence/exports', pathMatch: 'full' }, - { path: 'evidence-audit/evidence', redirectTo: '/evidence/exports', pathMatch: 'full' }, - { path: 'evidence-audit/proofs', redirectTo: '/evidence/verification/proofs', pathMatch: 'full' }, - { path: 'evidence-audit/replay', redirectTo: '/evidence/verification/replay', pathMatch: 'full' }, - { path: 'evidence-audit/audit-log', redirectTo: '/evidence/audit-log', pathMatch: 'full' }, - { path: 'evidence-audit/change-trace', redirectTo: '/evidence/audit-log', pathMatch: 'full' }, - { path: 'evidence-audit/timeline', redirectTo: '/evidence/audit-log', pathMatch: 'full' }, - { path: 'evidence-audit/trust-signing', redirectTo: '/platform/setup/trust-signing', pathMatch: 'full' }, - - { path: 'platform-ops', redirectTo: '/platform/ops', pathMatch: 'full' }, - { path: 'platform-ops/data-integrity', redirectTo: '/platform/ops/data-integrity', pathMatch: 'full' }, - { path: 'platform-ops/orchestrator', redirectTo: '/platform/ops/orchestrator', pathMatch: 'full' }, - { path: 'platform-ops/orchestrator/jobs', redirectTo: '/platform/ops/jobs-queues', pathMatch: 'full' }, - { path: 'platform-ops/health', redirectTo: '/platform/ops/health-slo', pathMatch: 'full' }, - { path: 'platform-ops/quotas', redirectTo: '/platform/ops/quotas', pathMatch: 'full' }, - { path: 'platform-ops/feeds', redirectTo: '/platform/ops/feeds-airgap', pathMatch: 'full' }, - { path: 'platform-ops/offline-kit', redirectTo: '/platform/ops/offline-kit', pathMatch: 'full' }, - { path: 'platform-ops/doctor', redirectTo: '/platform/ops/doctor', pathMatch: 'full' }, - { path: 'platform-ops/aoc', redirectTo: '/platform/ops/aoc', pathMatch: 'full' }, - { path: 'platform-ops/agents', redirectTo: '/topology/agents', pathMatch: 'full' }, - - // =========================================== - // Analyze -> Security & Risk - // =========================================== - { path: 'findings', redirectTo: '/security/triage', pathMatch: 'full' }, - { path: 'findings/:scanId', redirectTo: '/security/scans/:scanId', pathMatch: 'full' }, - { path: 'security/sbom', redirectTo: '/security/sbom/graph', pathMatch: 'full' }, - { path: 'security/vex', redirectTo: '/security/disposition', pathMatch: 'full' }, - { path: 'security/exceptions', redirectTo: '/security/disposition', pathMatch: 'full' }, - { path: 'security/advisory-sources', redirectTo: '/platform/integrations/feeds', pathMatch: 'full' }, - { path: 'scans/:scanId', redirectTo: '/security/scans/:scanId', pathMatch: 'full' }, - { path: 'vulnerabilities', redirectTo: '/security/triage', pathMatch: 'full' }, - { path: 'vulnerabilities/:vulnId', redirectTo: '/security/triage', pathMatch: 'full' }, - { path: 'graph', redirectTo: '/security/sbom/graph', pathMatch: 'full' }, - { path: 'lineage', redirectTo: '/security/lineage', pathMatch: 'full' }, - { path: 'lineage/:artifact/compare', redirectTo: '/security/lineage/:artifact/compare', pathMatch: 'full' }, - { path: 'lineage/compare', redirectTo: '/security/lineage/compare', pathMatch: 'full' }, - { path: 'compare/:currentId', redirectTo: '/security/lineage/compare/:currentId', pathMatch: 'full' }, - { path: 'reachability', redirectTo: '/security/reachability', pathMatch: 'full' }, - { path: 'analyze/unknowns', redirectTo: '/security/unknowns', pathMatch: 'full' }, - { path: 'analyze/patch-map', redirectTo: '/security/patch-map', pathMatch: 'full' }, - { path: 'analytics', redirectTo: '/security/sbom/lake', pathMatch: 'full' }, - { path: 'analytics/sbom-lake', redirectTo: '/security/sbom/lake', pathMatch: 'full' }, - { path: 'cvss/receipts/:receiptId', redirectTo: '/evidence/receipts/cvss/:receiptId', pathMatch: 'full' }, - - // =========================================== - // Triage -> Security & Risk + Administration - // =========================================== - { path: 'triage/artifacts', redirectTo: '/security/artifacts', pathMatch: 'full' }, - { path: 'triage/artifacts/:artifactId', redirectTo: '/security/artifacts/:artifactId', pathMatch: 'full' }, - { path: 'triage/audit-bundles', redirectTo: '/evidence/overview', pathMatch: 'full' }, - { path: 'triage/audit-bundles/new', redirectTo: '/evidence/overview', pathMatch: 'full' }, - { path: 'exceptions', redirectTo: '/administration/policy/exceptions', pathMatch: 'full' }, - { path: 'exceptions/:id', redirectTo: '/administration/policy/exceptions/:id', pathMatch: 'full' }, - { path: 'risk', redirectTo: '/security/risk', pathMatch: 'full' }, - - // =========================================== - // Policy Studio -> Administration - // =========================================== - { path: 'policy-studio/packs', redirectTo: '/administration/policy/packs', pathMatch: 'full' }, - { path: 'policy-studio/packs/:packId', redirectTo: '/administration/policy/packs/:packId', pathMatch: 'full' }, - { path: 'policy-studio/packs/:packId/:page', redirectTo: '/administration/policy/packs/:packId/:page', pathMatch: 'full' }, - - // =========================================== - // VEX Hub -> Security & Risk - // =========================================== - { path: 'admin/vex-hub', redirectTo: '/security/vex', pathMatch: 'full' }, - { path: 'admin/vex-hub/search', redirectTo: '/security/vex/search', pathMatch: 'full' }, - { path: 'admin/vex-hub/search/detail/:id', redirectTo: '/security/vex/search/detail/:id', pathMatch: 'full' }, - { path: 'admin/vex-hub/stats', redirectTo: '/security/vex/stats', pathMatch: 'full' }, - { path: 'admin/vex-hub/consensus', redirectTo: '/security/vex/consensus', pathMatch: 'full' }, - { path: 'admin/vex-hub/explorer', redirectTo: '/security/vex/explorer', pathMatch: 'full' }, - { path: 'admin/vex-hub/:page', redirectTo: '/security/vex/:page', pathMatch: 'full' }, - - // =========================================== - // Orchestrator -> Platform Ops - // =========================================== - { path: 'orchestrator', redirectTo: '/platform/ops/orchestrator', pathMatch: 'full' }, - { path: 'orchestrator/:page', redirectTo: '/platform/ops/orchestrator', pathMatch: 'full' }, - { path: 'scheduler/:page', redirectTo: '/platform/ops/scheduler/:page', pathMatch: 'full' }, - - // =========================================== - // Ops -> Platform Ops - // =========================================== - { path: 'ops/quotas', redirectTo: '/platform/ops/quotas', pathMatch: 'full' }, - { path: 'ops/quotas/:page', redirectTo: '/platform/ops/quotas', pathMatch: 'full' }, - { path: 'ops/orchestrator/dead-letter', redirectTo: '/platform/ops/dead-letter', pathMatch: 'full' }, - { path: 'ops/orchestrator/slo', redirectTo: '/platform/ops/health-slo', pathMatch: 'full' }, - { path: 'ops/health', redirectTo: '/platform/ops/health-slo', pathMatch: 'full' }, - { path: 'ops/feeds', redirectTo: '/platform/ops/feeds-airgap', pathMatch: 'full' }, - { path: 'ops/feeds/:page', redirectTo: '/platform/ops/feeds-airgap', pathMatch: 'full' }, - { path: 'ops/offline-kit', redirectTo: '/platform/ops/offline-kit', pathMatch: 'full' }, - { path: 'ops/aoc', redirectTo: '/platform/ops/aoc', pathMatch: 'full' }, - { path: 'ops/doctor', redirectTo: '/platform/ops/doctor', pathMatch: 'full' }, - - // =========================================== - // Console -> Administration - // =========================================== - { path: 'console/profile', redirectTo: '/administration/profile', pathMatch: 'full' }, - { path: 'console/status', redirectTo: '/platform/ops/status', pathMatch: 'full' }, - { path: 'console/configuration', redirectTo: '/administration/configuration-pane', pathMatch: 'full' }, - { path: 'console/admin/tenants', redirectTo: '/administration/admin/tenants', pathMatch: 'full' }, - { path: 'console/admin/users', redirectTo: '/administration/admin/users', pathMatch: 'full' }, - { path: 'console/admin/roles', redirectTo: '/administration/admin/roles', pathMatch: 'full' }, - { path: 'console/admin/clients', redirectTo: '/administration/admin/clients', pathMatch: 'full' }, - { path: 'console/admin/tokens', redirectTo: '/administration/admin/tokens', pathMatch: 'full' }, - { path: 'console/admin/branding', redirectTo: '/administration/admin/branding', pathMatch: 'full' }, - { path: 'console/admin/:page', redirectTo: '/administration/admin/:page', pathMatch: 'full' }, - - // =========================================== - // Admin -> Administration - // =========================================== - { path: 'admin/trust', redirectTo: '/platform/setup/trust-signing', pathMatch: 'full' }, - { path: 'admin/trust/:page', redirectTo: '/platform/setup/trust-signing', pathMatch: 'full' }, - { path: 'admin/registries', redirectTo: '/platform/integrations/registries', pathMatch: 'full' }, - { path: 'admin/issuers', redirectTo: '/platform/setup/trust-signing', pathMatch: 'full' }, - { path: 'admin/notifications', redirectTo: '/administration/notifications', pathMatch: 'full' }, - { path: 'admin/audit', redirectTo: '/evidence/audit-log', pathMatch: 'full' }, - { path: 'admin/policy/governance', redirectTo: '/administration/policy/governance', pathMatch: 'full' }, - { path: 'concelier/trivy-db-settings', redirectTo: '/platform/setup/feed-policy', pathMatch: 'full' }, - - // =========================================== - // Integrations -> Integrations - // =========================================== - { path: 'sbom-sources', redirectTo: '/platform/integrations/sbom-sources', pathMatch: 'full' }, - - // =========================================== - // Settings -> canonical v2 domains - // =========================================== - { path: 'settings/integrations', redirectTo: '/platform/integrations', pathMatch: 'full' }, - { path: 'settings/integrations/:id', redirectTo: '/platform/integrations/:id', pathMatch: 'full' }, - { path: 'settings/release-control', redirectTo: '/topology/promotion-graph', pathMatch: 'full' }, - { path: 'settings/release-control/environments', redirectTo: '/topology/regions', pathMatch: 'full' }, - { path: 'settings/release-control/targets', redirectTo: '/topology/targets', pathMatch: 'full' }, - { path: 'settings/release-control/agents', redirectTo: '/topology/agents', pathMatch: 'full' }, - { path: 'settings/release-control/workflows', redirectTo: '/topology/workflows', pathMatch: 'full' }, - { path: 'settings/trust', redirectTo: '/platform/setup/trust-signing', pathMatch: 'full' }, - { path: 'settings/trust/:page', redirectTo: '/platform/setup/trust-signing', pathMatch: 'full' }, - { path: 'settings/policy', redirectTo: '/administration/policy-governance', pathMatch: 'full' }, - { path: 'settings/admin', redirectTo: '/administration/identity-access', pathMatch: 'full' }, - { path: 'settings/branding', redirectTo: '/administration/tenant-branding', pathMatch: 'full' }, - { path: 'settings/notifications', redirectTo: '/administration/notifications', pathMatch: 'full' }, - { path: 'settings/usage', redirectTo: '/administration/usage', pathMatch: 'full' }, - { path: 'settings/system', redirectTo: '/administration/system', pathMatch: 'full' }, - { path: 'settings/offline', redirectTo: '/administration/offline', pathMatch: 'full' }, - { path: 'settings/security-data', redirectTo: '/platform/integrations/feeds', pathMatch: 'full' }, - - // =========================================== - // Release Orchestrator -> Release Control - // =========================================== - { path: 'release-orchestrator', redirectTo: '/', pathMatch: 'full' }, - { path: 'release-orchestrator/environments', redirectTo: '/topology/regions', pathMatch: 'full' }, - { path: 'release-orchestrator/releases', redirectTo: '/releases', pathMatch: 'full' }, - { path: 'release-orchestrator/approvals', redirectTo: '/releases/approvals', pathMatch: 'full' }, - { path: 'release-orchestrator/deployments', redirectTo: '/releases/runs', pathMatch: 'full' }, - { path: 'release-orchestrator/workflows', redirectTo: '/topology/workflows', pathMatch: 'full' }, - { path: 'release-orchestrator/evidence', redirectTo: '/evidence/capsules', pathMatch: 'full' }, - - // =========================================== - // Evidence -> Evidence & Audit - // =========================================== - { path: 'evidence/export', redirectTo: '/evidence/exports', pathMatch: 'full' }, - { path: 'evidence/proof-chains', redirectTo: '/evidence/verification/proofs', pathMatch: 'full' }, - { path: 'evidence-packs', redirectTo: '/evidence/capsules', pathMatch: 'full' }, - { path: 'evidence-packs/:packId', redirectTo: '/evidence/capsules/:packId', pathMatch: 'full' }, - // Keep /proofs/* as permanent short alias for convenience - { path: 'proofs/:subjectDigest', redirectTo: '/evidence/verification/proofs', pathMatch: 'full' }, - - // =========================================== - // Other - // =========================================== - { path: 'ai-runs', redirectTo: '/platform/ops/ai-runs', pathMatch: 'full' }, - { path: 'ai-runs/:runId', redirectTo: '/platform/ops/ai-runs/:runId', pathMatch: 'full' }, - { path: 'change-trace', redirectTo: '/evidence/audit-log', pathMatch: 'full' }, - { path: 'notify', redirectTo: '/platform/ops/notifications', pathMatch: 'full' }, -]; - -export const LEGACY_REDIRECT_ROUTES: Routes = LEGACY_REDIRECT_ROUTE_TEMPLATES.map((route) => ({ - path: route.path, - pathMatch: route.pathMatch, - redirectTo: preserveQueryAndFragment(route.redirectTo), -})); +export const LEGACY_REDIRECT_ROUTES: Routes = []; diff --git a/src/Web/StellaOps.Web/src/app/routes/mission-control.routes.ts b/src/Web/StellaOps.Web/src/app/routes/mission-control.routes.ts new file mode 100644 index 000000000..7d48b8c37 --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/routes/mission-control.routes.ts @@ -0,0 +1,32 @@ +import { Routes } from '@angular/router'; + +export const MISSION_CONTROL_ROUTES: Routes = [ + { + path: '', + title: 'Mission Board', + data: { breadcrumb: 'Mission Board' }, + loadComponent: () => + import('../features/dashboard-v3/dashboard-v3.component').then((m) => m.DashboardV3Component), + }, + { + path: 'board', + title: 'Mission Board', + data: { breadcrumb: 'Mission Board' }, + loadComponent: () => + import('../features/dashboard-v3/dashboard-v3.component').then((m) => m.DashboardV3Component), + }, + { + path: 'alerts', + title: 'Mission Alerts', + data: { breadcrumb: 'Alerts' }, + loadComponent: () => + import('../features/mission-control/mission-alerts-page.component').then((m) => m.MissionAlertsPageComponent), + }, + { + path: 'activity', + title: 'Mission Activity', + data: { breadcrumb: 'Activity' }, + loadComponent: () => + import('../features/mission-control/mission-activity-page.component').then((m) => m.MissionActivityPageComponent), + }, +]; diff --git a/src/Web/StellaOps.Web/src/app/routes/operations.routes.ts b/src/Web/StellaOps.Web/src/app/routes/operations.routes.ts index 2ad9e146b..f4be4e8dd 100644 --- a/src/Web/StellaOps.Web/src/app/routes/operations.routes.ts +++ b/src/Web/StellaOps.Web/src/app/routes/operations.routes.ts @@ -138,11 +138,6 @@ export const OPERATIONS_ROUTES: Routes = [ loadChildren: () => import('../features/doctor/doctor.routes').then((m) => m.DOCTOR_ROUTES), }, - { - path: 'diagnostics', - pathMatch: 'full', - redirectTo: 'doctor', - }, { path: 'signals', title: 'Signals', @@ -186,52 +181,4 @@ export const OPERATIONS_ROUTES: Routes = [ import('../features/console/console-status.component').then((m) => m.ConsoleStatusComponent), }, - // Ownership move in Pack 22: agents belong to Topology. - { - path: 'agents', - pathMatch: 'full', - redirectTo: '/topology/agents', - }, - - // Legacy aliases within ops surface. - { - path: 'jobs', - pathMatch: 'full', - redirectTo: 'jobs-queues', - }, - { - path: 'runs', - pathMatch: 'full', - redirectTo: 'jobs-queues', - }, - { - path: 'schedules', - pathMatch: 'full', - redirectTo: 'jobs-queues', - }, - { - path: 'workers', - pathMatch: 'full', - redirectTo: 'jobs-queues', - }, - { - path: 'feeds', - pathMatch: 'full', - redirectTo: 'feeds-airgap', - }, - { - path: 'feeds-offline', - pathMatch: 'full', - redirectTo: 'feeds-airgap', - }, - { - path: 'health', - pathMatch: 'full', - redirectTo: 'health-slo', - }, - { - path: 'slo', - pathMatch: 'full', - redirectTo: 'health-slo', - }, ]; diff --git a/src/Web/StellaOps.Web/src/app/routes/ops.routes.ts b/src/Web/StellaOps.Web/src/app/routes/ops.routes.ts new file mode 100644 index 000000000..abbdec352 --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/routes/ops.routes.ts @@ -0,0 +1,37 @@ +import { Routes } from '@angular/router'; + +export const OPS_ROUTES: Routes = [ + { + path: '', + title: 'Ops', + data: { breadcrumb: 'Ops' }, + loadComponent: () => + import('../features/ops/ops-overview-page.component').then((m) => m.OpsOverviewPageComponent), + }, + { + path: 'operations', + title: 'Operations', + data: { breadcrumb: 'Operations' }, + loadChildren: () => import('./operations.routes').then((m) => m.OPERATIONS_ROUTES), + }, + { + path: 'integrations', + title: 'Integrations', + data: { breadcrumb: 'Integrations' }, + loadChildren: () => + import('../features/integration-hub/integration-hub.routes').then((m) => m.integrationHubRoutes), + }, + { + path: 'policy', + title: 'Policy', + data: { breadcrumb: 'Policy' }, + loadChildren: () => import('../features/policy/policy.routes').then((m) => m.POLICY_ROUTES), + }, + { + path: 'platform-setup', + title: 'Platform Setup', + data: { breadcrumb: 'Platform Setup' }, + loadChildren: () => + import('../features/platform/setup/platform-setup.routes').then((m) => m.PLATFORM_SETUP_ROUTES), + }, +]; diff --git a/src/Web/StellaOps.Web/src/app/routes/platform-ops.routes.ts b/src/Web/StellaOps.Web/src/app/routes/platform-ops.routes.ts index 2e8cecb4d..14eb4a11b 100644 --- a/src/Web/StellaOps.Web/src/app/routes/platform-ops.routes.ts +++ b/src/Web/StellaOps.Web/src/app/routes/platform-ops.routes.ts @@ -1,309 +1,4 @@ -/** - * Platform Ops Domain Routes - * Sprint: SPRINT_20260218_008_FE_ui_v2_rewire_integrations_platform_ops_data_integrity (I3-02, I3-04) - * - * Canonical Platform Ops IA v2 route tree. - * Sub-area ownership per docs/modules/ui/v2-rewire/source-of-truth.md: - * P0 Overview - * P1 Orchestrator & Jobs - * P2 Scheduler - * P3 Quotas & Limits - * P4 Feeds & Mirrors - * P5 Offline Kit & AirGap - * P6 Data Integrity (feeds freshness, scan health, DLQ, SLOs) - * P7 Health & Diagnostics - * P8 AOC Compliance - * P9 Agents & Signals - * - * Security Data: connectivity/freshness is owned here; decision impact consumed by Security & Risk. - */ - import { Routes } from '@angular/router'; -import { - requireOrchViewerGuard, - requireOrchOperatorGuard, -} from '../core/auth'; - -export const PLATFORM_OPS_ROUTES: Routes = [ - // P0 — Platform Ops overview - { - path: '', - title: 'Platform Ops', - data: { breadcrumb: 'Platform Ops' }, - loadComponent: () => - import('../features/platform-ops/platform-ops-overview.component').then( - (m) => m.PlatformOpsOverviewComponent - ), - }, - - // P1 — Orchestrator & Jobs - { - path: 'orchestrator', - title: 'Orchestrator', - data: { breadcrumb: 'Orchestrator' }, - canMatch: [requireOrchViewerGuard], - loadComponent: () => - import('../features/orchestrator/orchestrator-dashboard.component').then( - (m) => m.OrchestratorDashboardComponent - ), - }, - { - path: 'orchestrator/jobs', - title: 'Jobs', - data: { breadcrumb: 'Jobs' }, - canMatch: [requireOrchViewerGuard], - loadComponent: () => - import('../features/orchestrator/orchestrator-jobs.component').then( - (m) => m.OrchestratorJobsComponent - ), - }, - { - path: 'orchestrator/jobs/:jobId', - title: 'Job Detail', - data: { breadcrumb: 'Job Detail' }, - canMatch: [requireOrchViewerGuard], - loadComponent: () => - import('../features/orchestrator/orchestrator-job-detail.component').then( - (m) => m.OrchestratorJobDetailComponent - ), - }, - { - path: 'orchestrator/quotas', - title: 'Orchestrator Quotas', - data: { breadcrumb: 'Orchestrator Quotas' }, - canMatch: [requireOrchOperatorGuard], - loadComponent: () => - import('../features/orchestrator/orchestrator-quotas.component').then( - (m) => m.OrchestratorQuotasComponent - ), - }, - - // P2 — Scheduler - { - path: 'scheduler', - title: 'Scheduler', - data: { breadcrumb: 'Scheduler' }, - loadChildren: () => - import('../features/scheduler-ops/scheduler-ops.routes').then( - (m) => m.schedulerOpsRoutes - ), - }, - { - path: 'scheduler/:page', - title: 'Scheduler', - data: { breadcrumb: 'Scheduler' }, - loadChildren: () => - import('../features/scheduler-ops/scheduler-ops.routes').then( - (m) => m.schedulerOpsRoutes - ), - }, - - // P3 — Quotas - { - path: 'quotas', - title: 'Quotas & Limits', - data: { breadcrumb: 'Quotas & Limits' }, - loadChildren: () => - import('../features/quota-dashboard/quota.routes').then((m) => m.quotaRoutes), - }, - { - path: 'quotas/:page', - title: 'Quotas & Limits', - data: { breadcrumb: 'Quotas & Limits' }, - loadChildren: () => - import('../features/quota-dashboard/quota.routes').then((m) => m.quotaRoutes), - }, - - // P4 — Feeds & Mirrors - { - path: 'feeds', - title: 'Feeds & Mirrors', - data: { breadcrumb: 'Feeds & Mirrors' }, - loadChildren: () => - import('../features/feed-mirror/feed-mirror.routes').then((m) => m.feedMirrorRoutes), - }, - { - path: 'feeds/:page', - title: 'Feeds & Mirrors', - data: { breadcrumb: 'Feeds & Mirrors' }, - loadChildren: () => - import('../features/feed-mirror/feed-mirror.routes').then((m) => m.feedMirrorRoutes), - }, - - // P5 — Offline Kit & AirGap - { - path: 'offline-kit', - title: 'Offline Kit', - data: { breadcrumb: 'Offline Kit' }, - loadChildren: () => - import('../features/offline-kit/offline-kit.routes').then((m) => m.offlineKitRoutes), - }, - - // P6 — Data Integrity (feeds freshness, scan pipeline health, DLQ, SLOs) - { - path: 'data-integrity', - title: 'Data Integrity', - data: { breadcrumb: 'Data Integrity' }, - loadChildren: () => - import('../features/platform-ops/data-integrity/data-integrity.routes').then( - (m) => m.dataIntegrityRoutes - ), - }, - { - path: 'dead-letter', - title: 'Dead-Letter Queue', - data: { breadcrumb: 'Dead-Letter Queue' }, - loadChildren: () => - import('../features/deadletter/deadletter.routes').then((m) => m.deadletterRoutes), - }, - { - path: 'slo', - title: 'SLO Monitoring', - data: { breadcrumb: 'SLO Monitoring' }, - loadChildren: () => - import('../features/slo-monitoring/slo.routes').then((m) => m.sloRoutes), - }, - - // P7 — Health & Diagnostics - { - path: 'health', - title: 'Platform Health', - data: { breadcrumb: 'Platform Health' }, - loadChildren: () => - import('../features/platform-health/platform-health.routes').then( - (m) => m.platformHealthRoutes - ), - }, - { - path: 'doctor', - title: 'Diagnostics', - data: { breadcrumb: 'Diagnostics' }, - loadChildren: () => - import('../features/doctor/doctor.routes').then((m) => m.DOCTOR_ROUTES), - }, - { - path: 'status', - title: 'System Status', - data: { breadcrumb: 'System Status' }, - loadComponent: () => - import('../features/console/console-status.component').then( - (m) => m.ConsoleStatusComponent - ), - }, - - // P8 — AOC Compliance - { - path: 'aoc', - title: 'AOC Compliance', - data: { breadcrumb: 'AOC Compliance' }, - loadChildren: () => - import('../features/aoc-compliance/aoc-compliance.routes').then( - (m) => m.AOC_COMPLIANCE_ROUTES - ), - }, - - // P9 — Agents, Signals, AI Runs - { - path: 'agents', - title: 'Agent Fleet', - data: { breadcrumb: 'Agent Fleet' }, - loadChildren: () => - import('../features/agents/agents.routes').then((m) => m.AGENTS_ROUTES), - }, - { - path: 'signals', - title: 'Signals', - data: { breadcrumb: 'Signals' }, - loadChildren: () => - import('../features/signals/signals.routes').then((m) => m.SIGNALS_ROUTES), - }, - { - path: 'packs', - title: 'Pack Registry', - data: { breadcrumb: 'Pack Registry' }, - loadChildren: () => - import('../features/pack-registry/pack-registry.routes').then((m) => m.PACK_REGISTRY_ROUTES), - }, - { - path: 'ai-runs', - title: 'AI Runs', - data: { breadcrumb: 'AI Runs' }, - loadComponent: () => - import('../features/ai-runs/ai-runs-list.component').then( - (m) => m.AiRunsListComponent - ), - }, - { - path: 'ai-runs/:runId', - title: 'AI Run Detail', - data: { breadcrumb: 'AI Run Detail' }, - loadComponent: () => - import('../features/ai-runs/ai-run-viewer.component').then( - (m) => m.AiRunViewerComponent - ), - }, - { - path: 'notifications', - title: 'Notifications', - data: { breadcrumb: 'Notifications' }, - loadComponent: () => - import('../features/notify/notify-panel.component').then( - (m) => m.NotifyPanelComponent - ), - }, - - // P10 — Federated Telemetry - { - path: 'federation-telemetry', - title: 'Federation', - data: { breadcrumb: 'Federation' }, - loadComponent: () => - import('../features/platform-ops/federation-telemetry/federation-overview.component').then( - (m) => m.FederationOverviewComponent - ), - }, - { - path: 'federation-telemetry/consent', - title: 'Consent Management', - data: { breadcrumb: 'Consent' }, - loadComponent: () => - import('../features/platform-ops/federation-telemetry/consent-management.component').then( - (m) => m.ConsentManagementComponent - ), - }, - { - path: 'federation-telemetry/bundles', - title: 'Bundle Explorer', - data: { breadcrumb: 'Bundles' }, - loadComponent: () => - import('../features/platform-ops/federation-telemetry/bundle-explorer.component').then( - (m) => m.BundleExplorerComponent - ), - }, - { - path: 'federation-telemetry/intelligence', - title: 'Intelligence Viewer', - data: { breadcrumb: 'Intelligence' }, - loadComponent: () => - import('../features/platform-ops/federation-telemetry/intelligence-viewer.component').then( - (m) => m.IntelligenceViewerComponent - ), - }, - { - path: 'federation-telemetry/privacy', - title: 'Privacy Budget', - data: { breadcrumb: 'Privacy' }, - loadComponent: () => - import('../features/platform-ops/federation-telemetry/privacy-budget-monitor.component').then( - (m) => m.PrivacyBudgetMonitorComponent - ), - }, - - // Alias for dead-letter alternative path format - { - path: 'deadletter', - redirectTo: 'dead-letter', - pathMatch: 'full', - }, -]; +/** Legacy Platform Ops tree retired in pre-alpha IA. */ +export const PLATFORM_OPS_ROUTES: Routes = []; diff --git a/src/Web/StellaOps.Web/src/app/routes/platform.routes.ts b/src/Web/StellaOps.Web/src/app/routes/platform.routes.ts index 59a9c8278..834c82a90 100644 --- a/src/Web/StellaOps.Web/src/app/routes/platform.routes.ts +++ b/src/Web/StellaOps.Web/src/app/routes/platform.routes.ts @@ -1,55 +1,4 @@ import { Routes } from '@angular/router'; -export const PLATFORM_ROUTES: Routes = [ - { - path: '', - title: 'Platform', - data: { breadcrumb: 'Platform' }, - loadComponent: () => - import('../features/platform/platform-home-page.component').then( - (m) => m.PlatformHomePageComponent, - ), - }, - { - path: 'home', - pathMatch: 'full', - redirectTo: '', - }, - { - path: 'ops', - title: 'Platform Ops', - data: { breadcrumb: 'Ops' }, - loadChildren: () => - import('./operations.routes').then((m) => m.OPERATIONS_ROUTES), - }, - { - path: 'integrations', - title: 'Platform Integrations', - data: { breadcrumb: 'Integrations' }, - loadChildren: () => - import('../features/integration-hub/integration-hub.routes').then( - (m) => m.integrationHubRoutes, - ), - }, - { - path: 'setup', - title: 'Platform Setup', - data: { breadcrumb: 'Setup' }, - loadChildren: () => - import('../features/platform/setup/platform-setup.routes').then( - (m) => m.PLATFORM_SETUP_ROUTES, - ), - }, - - // Interim aliases used by sprint 021 before setup naming finalized. - { - path: 'administration', - pathMatch: 'full', - redirectTo: 'setup', - }, - { - path: 'administration/:page', - pathMatch: 'full', - redirectTo: 'setup/:page', - }, -]; +/** Legacy Platform tree retired in pre-alpha IA. */ +export const PLATFORM_ROUTES: Routes = []; diff --git a/src/Web/StellaOps.Web/src/app/routes/release-control.routes.ts b/src/Web/StellaOps.Web/src/app/routes/release-control.routes.ts index 4d6607821..f916d59ef 100644 --- a/src/Web/StellaOps.Web/src/app/routes/release-control.routes.ts +++ b/src/Web/StellaOps.Web/src/app/routes/release-control.routes.ts @@ -1,246 +1,4 @@ -/** - * Release Control Canonical Domain Routes - * Sprint: SPRINT_20260218_006 (initial), SPRINT_20260218_009 (bundles), SPRINT_20260218_010 (promotions) - * - * Canonical path prefix: /release-control - * Domain owner: Release Control - */ - import { Routes } from '@angular/router'; -export const RELEASE_CONTROL_ROUTES: Routes = [ - { - path: '', - redirectTo: 'control-plane', - pathMatch: 'full', - }, - - // Control plane entry under Release Control - { - path: 'control-plane', - title: 'Control Plane', - data: { breadcrumb: 'Control Plane' }, - loadComponent: () => - import('../features/control-plane/control-plane-dashboard.component').then( - (m) => m.ControlPlaneDashboardComponent - ), - }, - - // Setup hub and setup child pages (Pack 21 migration from Settings -> Release Control) - { - path: 'setup', - title: 'Setup', - data: { breadcrumb: 'Setup' }, - loadComponent: () => - import('../features/release-control/setup/release-control-setup-home.component').then( - (m) => m.ReleaseControlSetupHomeComponent - ), - }, - { - path: 'setup/environments-paths', - title: 'Environments and Promotion Paths', - data: { breadcrumb: 'Environments and Promotion Paths' }, - loadComponent: () => - import('../features/release-control/setup/setup-environments-paths.component').then( - (m) => m.SetupEnvironmentsPathsComponent - ), - }, - { - path: 'setup/targets-agents', - title: 'Targets and Agents', - data: { breadcrumb: 'Targets and Agents' }, - loadComponent: () => - import('../features/release-control/setup/setup-targets-agents.component').then( - (m) => m.SetupTargetsAgentsComponent - ), - }, - { - path: 'setup/workflows', - title: 'Workflows', - data: { breadcrumb: 'Workflows' }, - loadComponent: () => - import('../features/release-control/setup/setup-workflows.component').then( - (m) => m.SetupWorkflowsComponent - ), - }, - { - path: 'setup/bundle-templates', - title: 'Bundle Templates', - data: { breadcrumb: 'Bundle Templates' }, - loadComponent: () => - import('../features/release-control/setup/setup-bundle-templates.component').then( - (m) => m.SetupBundleTemplatesComponent - ), - }, - { - path: 'setup/environments', - redirectTo: 'setup/environments-paths', - pathMatch: 'full', - }, - { - path: 'setup/targets', - redirectTo: 'setup/targets-agents', - pathMatch: 'full', - }, - { - path: 'setup/agents', - redirectTo: 'setup/targets-agents', - pathMatch: 'full', - }, - { - path: 'setup/templates', - redirectTo: 'setup/bundle-templates', - pathMatch: 'full', - }, - - // Releases (B5: list, create, detail, run timeline) - { - path: 'releases', - title: 'Releases', - data: { breadcrumb: 'Releases' }, - loadChildren: () => - import('../features/release-orchestrator/releases/releases.routes').then( - (m) => m.RELEASE_ROUTES - ), - }, - - // Approvals — decision cockpit (SPRINT_20260218_011) - { - path: 'approvals', - title: 'Approvals', - data: { breadcrumb: 'Approvals' }, - loadChildren: () => - import('../features/approvals/approvals.routes').then( - (m) => m.APPROVALS_ROUTES - ), - }, - - // Regions & Environments (region-first canonical structure) - { - path: 'regions', - title: 'Regions & Environments', - data: { breadcrumb: 'Regions & Environments' }, - loadComponent: () => - import('../features/release-control/regions/regions-overview.component').then( - (m) => m.RegionsOverviewComponent - ), - }, - { - path: 'regions/:region', - title: 'Region Detail', - data: { breadcrumb: 'Region Detail' }, - loadComponent: () => - import('../features/release-control/regions/region-detail.component').then( - (m) => m.RegionDetailComponent - ), - }, - { - path: 'regions/:region/environments/:env', - title: 'Environment Detail', - data: { breadcrumb: 'Environment Detail' }, - loadComponent: () => - import('../features/release-orchestrator/environments/environment-detail/environment-detail.component').then( - (m) => m.EnvironmentDetailComponent - ), - }, - { - path: 'regions/:region/environments/:env/settings', - title: 'Environment Detail', - data: { breadcrumb: 'Environment Detail', initialTab: 'inputs' }, - loadComponent: () => - import('../features/release-orchestrator/environments/environment-detail/environment-detail.component').then( - (m) => m.EnvironmentDetailComponent - ), - }, - { - path: 'environments', - pathMatch: 'full', - redirectTo: 'regions', - }, - { - path: 'environments/:region/:env', - pathMatch: 'full', - redirectTo: 'regions/:region/environments/:env', - }, - { - path: 'environments/:region/:env/settings', - pathMatch: 'full', - redirectTo: 'regions/:region/environments/:env/settings', - }, - { - path: 'environments/:id', - pathMatch: 'full', - redirectTo: 'regions/global/environments/:id', - }, - { - path: 'environments/:id/settings', - pathMatch: 'full', - redirectTo: 'regions/global/environments/:id/settings', - }, - - // Deployments - { - path: 'deployments', - title: 'Deployments', - data: { breadcrumb: 'Deployments' }, - loadChildren: () => - import('../features/release-orchestrator/deployments/deployments.routes').then( - (m) => m.DEPLOYMENT_ROUTES - ), - }, - - // Bundles — bundle organizer lifecycle (SPRINT_20260218_009) - { - path: 'bundles', - title: 'Bundles', - data: { breadcrumb: 'Bundles' }, - loadChildren: () => - import('../features/bundles/bundles.routes').then( - (m) => m.BUNDLE_ROUTES - ), - }, - - // Promotions — bundle-version anchored promotions (SPRINT_20260218_010) - { - path: 'promotions', - title: 'Promotions', - data: { breadcrumb: 'Promotions' }, - loadChildren: () => - import('../features/promotions/promotions.routes').then( - (m) => m.PROMOTION_ROUTES - ), - }, - - // Run timeline — pipeline run history - { - path: 'runs', - title: 'Run Timeline', - data: { breadcrumb: 'Run Timeline' }, - loadChildren: () => - import('../features/release-orchestrator/runs/runs.routes').then( - (m) => m.PIPELINE_RUN_ROUTES - ), - }, - - // Governance & Policy hub - { - path: 'governance', - title: 'Governance', - data: { breadcrumb: 'Governance' }, - loadChildren: () => - import('../features/release-control/governance/release-control-governance.routes').then( - (m) => m.RELEASE_CONTROL_GOVERNANCE_ROUTES - ), - }, - - // Dedicated hotfix queue - { - path: 'hotfixes', - title: 'Hotfixes', - data: { breadcrumb: 'Hotfixes' }, - loadComponent: () => - import('../features/release-control/hotfixes/hotfixes-queue.component').then( - (m) => m.HotfixesQueueComponent - ), - }, -]; +/** Legacy Release Control tree retired in pre-alpha IA. */ +export const RELEASE_CONTROL_ROUTES: Routes = []; diff --git a/src/Web/StellaOps.Web/src/app/routes/releases.routes.ts b/src/Web/StellaOps.Web/src/app/routes/releases.routes.ts index f630a5acc..a017c896f 100644 --- a/src/Web/StellaOps.Web/src/app/routes/releases.routes.ts +++ b/src/Web/StellaOps.Web/src/app/routes/releases.routes.ts @@ -1,10 +1,19 @@ -import { Routes } from '@angular/router'; +import { Routes } from '@angular/router'; export const RELEASES_ROUTES: Routes = [ { path: '', - pathMatch: 'full', - redirectTo: 'runs', + title: 'Release Ops Overview', + data: { breadcrumb: 'Overview' }, + loadComponent: () => + import('../features/releases/release-ops-overview-page.component').then((m) => m.ReleaseOpsOverviewPageComponent), + }, + { + path: 'overview', + title: 'Release Ops Overview', + data: { breadcrumb: 'Overview' }, + loadComponent: () => + import('../features/releases/release-ops-overview-page.component').then((m) => m.ReleaseOpsOverviewPageComponent), }, { path: 'versions', @@ -26,8 +35,12 @@ export const RELEASES_ROUTES: Routes = [ }, { path: 'versions/:versionId', - pathMatch: 'full', - redirectTo: 'versions/:versionId/overview', + title: 'Release Version Detail', + data: { breadcrumb: 'Release Version', semanticObject: 'version' }, + loadComponent: () => + import('../features/release-orchestrator/releases/release-detail/release-detail.component').then( + (m) => m.ReleaseDetailComponent, + ), }, { path: 'versions/:versionId/:tab', @@ -47,8 +60,12 @@ export const RELEASES_ROUTES: Routes = [ }, { path: 'runs/:runId', - pathMatch: 'full', - redirectTo: 'runs/:runId/timeline', + title: 'Release Run Detail', + data: { breadcrumb: 'Release Run', semanticObject: 'run' }, + loadComponent: () => + import('../features/release-orchestrator/releases/release-detail/release-detail.component').then( + (m) => m.ReleaseDetailComponent, + ), }, { path: 'runs/:runId/:tab', @@ -59,76 +76,63 @@ export const RELEASES_ROUTES: Routes = [ (m) => m.ReleaseDetailComponent, ), }, - { - path: 'hotfix', - title: 'Hotfix Run Lane', - data: { breadcrumb: 'Hotfix Lane', semanticObject: 'run', defaultLane: 'hotfix' }, - loadComponent: () => - import('../features/releases/releases-activity.component').then((m) => m.ReleasesActivityComponent), - }, { path: 'approvals', title: 'Run Approvals Queue', - data: { breadcrumb: 'Run Approvals', semanticObject: 'run' }, - loadChildren: () => - import('../features/approvals/approvals.routes').then((m) => m.APPROVALS_ROUTES), - }, - - // Legacy aliases. - { - path: 'new', - pathMatch: 'full', - redirectTo: 'versions/new', + data: { breadcrumb: 'Approvals', semanticObject: 'run' }, + loadChildren: () => import('../features/approvals/approvals.routes').then((m) => m.APPROVALS_ROUTES), }, { - path: 'create', - pathMatch: 'full', - redirectTo: 'versions/new', - }, - { - path: 'activity', - pathMatch: 'full', - redirectTo: 'runs', - }, - { - path: 'deployments', - pathMatch: 'full', - redirectTo: 'runs', - }, - { - path: 'promotions', - pathMatch: 'full', - redirectTo: 'runs', + path: 'promotion-queue', + title: 'Promotion Queue', + data: { breadcrumb: 'Promotion Queue' }, + loadComponent: () => + import('../features/promotions/promotions-list.component').then((m) => m.PromotionsListComponent), }, { path: 'hotfixes', - pathMatch: 'full', - redirectTo: 'hotfix', - }, - - { - path: ':releaseId/runs', - pathMatch: 'full', - redirectTo: 'runs/:releaseId/timeline', + title: 'Hotfixes', + data: { breadcrumb: 'Hotfixes' }, + loadComponent: () => + import('../features/release-control/hotfixes/hotfixes-queue.component').then((m) => m.HotfixesQueueComponent), }, { - path: ':releaseId/promotions', - pathMatch: 'full', - redirectTo: 'runs/:releaseId/gate-decision', + path: 'hotfixes/new', + title: 'Create Hotfix', + data: { breadcrumb: 'Create Hotfix' }, + loadComponent: () => + import('../features/releases/hotfix-create-page.component').then((m) => m.HotfixCreatePageComponent), }, { - path: ':releaseId/deployments', - pathMatch: 'full', - redirectTo: 'runs/:releaseId/deployments', + path: 'hotfixes/:hotfixId', + title: 'Hotfix Detail', + data: { breadcrumb: 'Hotfix Detail' }, + loadComponent: () => + import('../features/releases/hotfix-detail-page.component').then((m) => m.HotfixDetailPageComponent), }, { - path: ':releaseId', - pathMatch: 'full', - redirectTo: 'runs/:releaseId/timeline', + path: 'environments', + title: 'Environments Inventory', + data: { breadcrumb: 'Environments' }, + loadComponent: () => + import('../features/topology/topology-regions-environments-page.component').then( + (m) => m.TopologyRegionsEnvironmentsPageComponent, + ), }, { - path: ':releaseId/:tab', - pathMatch: 'full', - redirectTo: 'runs/:releaseId/:tab', + path: 'environments/:environmentId', + title: 'Environment Detail', + data: { breadcrumb: 'Environment Detail' }, + loadComponent: () => + import('../features/topology/topology-environment-detail-page.component').then( + (m) => m.TopologyEnvironmentDetailPageComponent, + ), + }, + { + path: 'deployments', + title: 'Deployment History', + data: { breadcrumb: 'Deployments' }, + loadComponent: () => + import('../features/deployments/deployments-list-page.component').then((m) => m.DeploymentsListPageComponent), }, ]; diff --git a/src/Web/StellaOps.Web/src/app/routes/security.routes.ts b/src/Web/StellaOps.Web/src/app/routes/security.routes.ts index 70cbcd225..ede83a65d 100644 --- a/src/Web/StellaOps.Web/src/app/routes/security.routes.ts +++ b/src/Web/StellaOps.Web/src/app/routes/security.routes.ts @@ -3,8 +3,12 @@ import { Routes } from '@angular/router'; export const SECURITY_ROUTES: Routes = [ { path: '', - pathMatch: 'full', - redirectTo: 'posture', + title: 'Security Posture', + data: { breadcrumb: 'Posture' }, + loadComponent: () => + import('../features/security-risk/security-risk-overview.component').then( + (m) => m.SecurityRiskOverviewComponent, + ), }, { path: 'posture', @@ -33,6 +37,15 @@ export const SECURITY_ROUTES: Routes = [ (m) => m.FindingDetailPageComponent, ), }, + { + path: 'advisories-vex', + title: 'Advisories & VEX', + data: { breadcrumb: 'Advisories & VEX' }, + loadComponent: () => + import('../features/security/security-disposition-page.component').then( + (m) => m.SecurityDispositionPageComponent, + ), + }, { path: 'disposition', title: 'Disposition Center', @@ -43,9 +56,22 @@ export const SECURITY_ROUTES: Routes = [ ), }, { - path: 'sbom', - pathMatch: 'full', - redirectTo: 'sbom/lake', + path: 'supply-chain-data', + title: 'Supply-Chain Data', + data: { breadcrumb: 'Supply-Chain Data' }, + loadComponent: () => + import('../features/security/security-sbom-explorer-page.component').then( + (m) => m.SecuritySbomExplorerPageComponent, + ), + }, + { + path: 'supply-chain-data/:mode', + title: 'Supply-Chain Data', + data: { breadcrumb: 'Supply-Chain Data' }, + loadComponent: () => + import('../features/security/security-sbom-explorer-page.component').then( + (m) => m.SecuritySbomExplorerPageComponent, + ), }, { path: 'sbom/:mode', @@ -56,37 +82,6 @@ export const SECURITY_ROUTES: Routes = [ (m) => m.SecuritySbomExplorerPageComponent, ), }, - { - path: 'reports', - title: 'Security Reports', - data: { breadcrumb: 'Reports' }, - loadComponent: () => - import('../features/security/security-disposition-page.component').then( - (m) => m.SecurityDispositionPageComponent, - ), - }, - - // Canonical compatibility aliases. - { - path: 'overview', - pathMatch: 'full', - redirectTo: 'posture', - }, - { - path: 'findings', - pathMatch: 'full', - redirectTo: 'triage', - }, - { - path: 'findings/:findingId', - pathMatch: 'full', - redirectTo: 'triage/:findingId', - }, - { - path: 'advisories-vex', - pathMatch: 'full', - redirectTo: 'disposition', - }, { path: 'reachability', title: 'Reachability', @@ -97,70 +92,55 @@ export const SECURITY_ROUTES: Routes = [ ), }, { - path: 'vex', - pathMatch: 'full', - redirectTo: 'disposition', - }, - { - path: 'exceptions', - pathMatch: 'full', - redirectTo: 'disposition', - }, - { - path: 'advisory-sources', - pathMatch: 'full', - redirectTo: '/platform/integrations/feeds', - }, - - // Legacy deep links. - { - path: 'vulnerabilities', - pathMatch: 'full', - redirectTo: 'triage', - }, - { - path: 'vulnerabilities/:cveId', - pathMatch: 'full', - redirectTo: 'triage', - }, - { - path: 'sbom-explorer', - pathMatch: 'full', - redirectTo: 'sbom/lake', - }, - { - path: 'sbom-explorer/:mode', - pathMatch: 'full', - redirectTo: 'sbom/:mode', - }, - { - path: 'supply-chain-data', - pathMatch: 'full', - redirectTo: 'sbom/lake', - }, - { - path: 'supply-chain-data/:mode', - pathMatch: 'full', - redirectTo: 'sbom/:mode', - }, - - { - path: 'scans/:scanId', - title: 'Scan Detail', - data: { breadcrumb: 'Scan Detail' }, + path: 'findings/:findingId', + title: 'Finding Detail', + data: { breadcrumb: 'Finding Detail' }, loadComponent: () => - import('../features/scans/scan-detail-page.component').then((m) => m.ScanDetailPageComponent), + import('../features/security-risk/finding-detail-page.component').then( + (m) => m.FindingDetailPageComponent, + ), }, { - path: 'lineage', - title: 'Lineage', - data: { breadcrumb: 'Lineage' }, - loadChildren: () => - import('../features/lineage/lineage.routes').then((m) => m.lineageRoutes), + path: 'cves/:cveId', + title: 'CVE Detail', + data: { breadcrumb: 'CVE Detail' }, + loadComponent: () => + import('../features/security/vulnerability-detail-page.component').then( + (m) => m.VulnerabilityDetailPageComponent, + ), }, { - path: 'risk', - pathMatch: 'full', - redirectTo: 'overview', + path: 'components/:componentId', + title: 'Component Detail', + data: { breadcrumb: 'Component Detail' }, + loadComponent: () => + import('../features/security/security-component-detail-page.component').then( + (m) => m.SecurityComponentDetailPageComponent, + ), + }, + { + path: 'artifacts/:artifactId', + title: 'Artifact Detail', + data: { breadcrumb: 'Artifact Detail' }, + loadComponent: () => + import('../features/security/artifact-detail-page.component').then( + (m) => m.ArtifactDetailPageComponent, + ), + }, + { + path: 'environments/:environmentId/risk', + title: 'Environment Risk Detail', + data: { breadcrumb: 'Environment Risk Detail' }, + loadComponent: () => + import('../features/security/security-environment-risk-detail-page.component').then( + (m) => m.SecurityEnvironmentRiskDetailPageComponent, + ), + }, + { + path: 'reports', + title: 'Security Reports', + data: { breadcrumb: 'Reports' }, + loadComponent: () => + import('../features/security/security-reports-page.component').then((m) => m.SecurityReportsPageComponent), }, ]; diff --git a/src/Web/StellaOps.Web/src/app/routes/setup.routes.ts b/src/Web/StellaOps.Web/src/app/routes/setup.routes.ts new file mode 100644 index 000000000..8a462449d --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/routes/setup.routes.ts @@ -0,0 +1,56 @@ +import { Routes } from '@angular/router'; + +export const SETUP_ROUTES: Routes = [ + { + path: '', + title: 'Setup Overview', + data: { breadcrumb: 'Setup' }, + loadComponent: () => + import('../features/administration/administration-overview.component').then( + (m) => m.AdministrationOverviewComponent, + ), + }, + { + path: 'identity-access', + title: 'Identity & Access', + data: { breadcrumb: 'Identity & Access' }, + loadComponent: () => + import('../features/settings/admin/admin-settings-page.component').then((m) => m.AdminSettingsPageComponent), + }, + { + path: 'tenant-branding', + title: 'Tenant & Branding', + data: { breadcrumb: 'Tenant & Branding' }, + loadComponent: () => + import('../features/settings/branding/branding-settings-page.component').then( + (m) => m.BrandingSettingsPageComponent, + ), + }, + { + path: 'notifications', + title: 'Notifications', + data: { breadcrumb: 'Notifications' }, + loadChildren: () => + import('../features/admin-notifications/admin-notifications.routes').then((m) => m.adminNotificationsRoutes), + }, + { + path: 'usage', + title: 'Usage & Limits', + data: { breadcrumb: 'Usage & Limits' }, + loadComponent: () => + import('../features/settings/usage/usage-settings-page.component').then((m) => m.UsageSettingsPageComponent), + }, + { + path: 'system', + title: 'System Settings', + data: { breadcrumb: 'System Settings' }, + loadComponent: () => + import('../features/settings/system/system-settings-page.component').then((m) => m.SystemSettingsPageComponent), + }, + { + path: 'topology', + title: 'Topology', + data: { breadcrumb: 'Topology' }, + loadChildren: () => import('./topology.routes').then((m) => m.TOPOLOGY_ROUTES), + }, +]; 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 de949f889..bd6aa32a4 100644 --- a/src/Web/StellaOps.Web/src/app/routes/topology.routes.ts +++ b/src/Web/StellaOps.Web/src/app/routes/topology.routes.ts @@ -3,8 +3,16 @@ import { Routes } from '@angular/router'; export const TOPOLOGY_ROUTES: Routes = [ { path: '', - pathMatch: 'full', - redirectTo: 'overview', + title: 'Topology Overview', + data: { + breadcrumb: 'Overview', + title: 'Topology Overview', + description: 'Operator mission map for region posture, targets, agents, and promotion flow health.', + }, + loadComponent: () => + import('../features/topology/topology-overview-page.component').then( + (m) => m.TopologyOverviewPageComponent, + ), }, { path: 'overview', @@ -19,9 +27,16 @@ export const TOPOLOGY_ROUTES: Routes = [ (m) => m.TopologyOverviewPageComponent, ), }, + { + path: 'map', + title: 'Environment & Target Map', + data: { breadcrumb: 'Map' }, + loadComponent: () => + import('../features/topology/topology-map-page.component').then((m) => m.TopologyMapPageComponent), + }, { path: 'regions', - title: 'Topology Regions & Environments', + title: 'Regions & Environments', data: { breadcrumb: 'Regions & Environments', title: 'Regions & Environments', @@ -35,7 +50,7 @@ export const TOPOLOGY_ROUTES: Routes = [ }, { path: 'environments', - title: 'Topology Environments', + title: 'Environments', data: { breadcrumb: 'Environments', title: 'Environments', @@ -49,12 +64,20 @@ export const TOPOLOGY_ROUTES: Routes = [ }, { path: 'environments/:environmentId', - pathMatch: 'full', - redirectTo: 'environments/:environmentId/posture', + title: 'Environment Detail', + data: { + breadcrumb: 'Environment Detail', + title: 'Environment Detail', + description: 'Topology-first tabs for targets, deployments, agents, security, and evidence.', + }, + loadComponent: () => + import('../features/topology/topology-environment-detail-page.component').then( + (m) => m.TopologyEnvironmentDetailPageComponent, + ), }, { path: 'environments/:environmentId/posture', - title: 'Topology Environment Detail', + title: 'Environment Detail', data: { breadcrumb: 'Environment Detail', title: 'Environment Detail', @@ -67,7 +90,7 @@ export const TOPOLOGY_ROUTES: Routes = [ }, { path: 'targets', - title: 'Topology Targets', + title: 'Targets', data: { breadcrumb: 'Targets', title: 'Targets', @@ -78,35 +101,76 @@ export const TOPOLOGY_ROUTES: Routes = [ (m) => m.TopologyTargetsPageComponent, ), }, + { + path: 'targets/:targetId', + title: 'Target Detail', + data: { breadcrumb: 'Target Detail' }, + loadComponent: () => + import('../features/topology/topology-target-detail-page.component').then( + (m) => m.TopologyTargetDetailPageComponent, + ), + }, { path: 'hosts', - title: 'Topology Hosts', + title: 'Hosts', data: { breadcrumb: 'Hosts', title: 'Hosts', description: 'Host runtime inventory and topology placement.', }, loadComponent: () => - import('../features/topology/topology-hosts-page.component').then( - (m) => m.TopologyHostsPageComponent, + import('../features/topology/topology-hosts-page.component').then((m) => m.TopologyHostsPageComponent), + }, + { + path: 'hosts/:hostId', + title: 'Host Detail', + data: { breadcrumb: 'Host Detail' }, + loadComponent: () => + import('../features/topology/topology-host-detail-page.component').then( + (m) => m.TopologyHostDetailPageComponent, ), }, { path: 'agents', - title: 'Topology Agents', + title: 'Agent Fleet', data: { - breadcrumb: 'Agents', - title: 'Agents', + breadcrumb: 'Agent Fleet', + title: 'Agent Fleet', description: 'Agent fleet status and assignments by region and environment.', }, loadComponent: () => - import('../features/topology/topology-agents-page.component').then( - (m) => m.TopologyAgentsPageComponent, + import('../features/topology/topology-agents-page.component').then((m) => m.TopologyAgentsPageComponent), + }, + { + path: 'agents/:agentGroupId', + title: 'Agent Group Detail', + data: { breadcrumb: 'Agent Group Detail' }, + loadComponent: () => + import('../features/topology/topology-agent-group-detail-page.component').then( + (m) => m.TopologyAgentGroupDetailPageComponent, + ), + }, + { + path: 'connectivity', + title: 'Connectivity', + data: { breadcrumb: 'Connectivity' }, + loadComponent: () => + import('../features/topology/topology-connectivity-page.component').then( + (m) => m.TopologyConnectivityPageComponent, + ), + }, + { + path: 'runtime-drift', + title: 'Runtime Drift', + data: { breadcrumb: 'Runtime Drift' }, + loadComponent: () => + import('../features/topology/topology-runtime-drift-page.component').then( + (m) => m.TopologyRuntimeDriftPageComponent, ), }, { path: 'promotion-graph', - title: 'Topology Promotion Graph', + title: 'Promotion Graph', data: { breadcrumb: 'Promotion Graph', title: 'Promotion Graph', @@ -117,14 +181,9 @@ export const TOPOLOGY_ROUTES: Routes = [ (m) => m.TopologyPromotionPathsPageComponent, ), }, - { - path: 'promotion-paths', - pathMatch: 'full', - redirectTo: 'promotion-graph', - }, { path: 'workflows', - title: 'Topology Workflows', + title: 'Workflows', data: { breadcrumb: 'Workflows', title: 'Workflows', @@ -138,7 +197,7 @@ export const TOPOLOGY_ROUTES: Routes = [ }, { path: 'gate-profiles', - title: 'Topology Gate Profiles', + title: 'Gate Profiles', data: { breadcrumb: 'Gate Profiles', title: 'Gate Profiles', @@ -150,11 +209,4 @@ export const TOPOLOGY_ROUTES: Routes = [ (m) => m.TopologyInventoryPageComponent, ), }, - - // Legacy placement aliases. - { - path: 'targets-hosts', - pathMatch: 'full', - redirectTo: 'targets', - }, ]; diff --git a/src/Web/StellaOps.Web/src/index.html b/src/Web/StellaOps.Web/src/index.html index 856ac6aaf..26d65c9af 100644 --- a/src/Web/StellaOps.Web/src/index.html +++ b/src/Web/StellaOps.Web/src/index.html @@ -13,19 +13,20 @@