From a918d39a61ad2c5a7df6ff7862b9a1c42da95713 Mon Sep 17 00:00:00 2001 From: master <> Date: Thu, 5 Mar 2026 18:10:56 +0200 Subject: [PATCH] texts fixes, search bar fixes, global menu fixes. --- devops/compose/README.md | 6 + devops/compose/docker-compose.stella-ops.yml | 16 +- devops/compose/envsettings-override.json | 8 +- .../13-platform-translations.sql | 18 + devops/compose/router-gateway-local.json | 18 +- .../router-gateway-local.reverseproxy.json | 8 +- .../compose/scripts/header-search-smoke.ps1 | 284 ++++++++++ .../compose/scripts/router-mode-redeploy.ps1 | 68 ++- ...sregistry_taskrunner_storage_completion.md | 112 ++++ ...Replay_feed_snapshot_storage_completion.md | 40 +- ...004_Remediation_postgres_runtime_wiring.md | 0 ...latform_read_model_boundary_enforcement.md | 35 +- ...bservice_catalog_and_domain_consistency.md | 60 ++- ...09_Router_header_search_route_alignment.md | 56 ++ ..._010_AdvisoryAI_header_search_stability.md | 55 ++ ...isual_qa_fixes_and_global_menu_grouping.md | 144 +++++ ...8_FE_sidebar_grouping_ux_regression_fix.md | 79 +++ docs/INSTALL_GUIDE.md | 2 + ..._WEBSERVICE_FUNCTION_DB_MATRIX_20260305.md | 24 +- ...sregistry_taskrunner_storage_completion.md | 104 ---- docs/modules/jobengine/architecture.md | 4 + docs/modules/platform/architecture.md | 40 +- docs/modules/platform/platform-service.md | 8 + .../webservices-valkey-rollout-matrix.md | 1 + docs/modules/ui/information-architecture.md | 12 + docs/quickstart.md | 3 + docs/technical/architecture/README.md | 16 + .../webservice-docs-invalid-fixture.md | 9 + docs/technical/architecture/port-registry.md | 23 +- .../scripts/validate-webservice-docs.ps1 | 103 ++++ .../architecture/webservice-catalog.md | 56 ++ .../KnowledgeSearch/KnowledgeSearchOptions.cs | 6 +- .../StellaOps.AdvisoryAI.csproj | 14 +- .../Adapters/FindingsSearchAdapter.cs | 2 +- .../UnifiedSearch/UnifiedSearchIndexer.cs | 39 +- ...nifiedSearchLiveAdapterIntegrationTests.cs | 34 +- .../PacksRegistryStartupContractTests.cs | 70 +++ .../PacksRegistryStartupEnvironmentScope.cs | 96 ++++ .../StellaOps.PacksRegistry.Tests/TASKS.md | 1 + .../Program.cs | 37 +- .../TASKS.md | 1 + .../StellaOps.TaskRunner.Tests/TASKS.md | 1 + .../TaskRunnerStartupContractTests.cs | 70 +++ .../TaskRunnerStartupEnvironmentScope.cs | 96 ++++ .../Program.cs | 26 +- .../StellaOps.TaskRunner.WebService/TASKS.md | 1 + .../StellaOps.TaskRunner.Worker/Program.cs | 24 +- .../StellaOps.TaskRunner.Worker/TASKS.md | 1 + .../StellaOps.Notify.WebService/Program.cs | 30 ++ .../StellaOps.Platform.WebService/Program.cs | 1 + .../Services/IntegrationsReadModelService.cs | 8 +- .../Services/PlatformContextService.cs | 12 +- .../Services/PostgresTranslationStore.cs | 84 ++- .../Services/SecurityReadModelService.cs | 8 +- .../Services/TopologyReadModelService.cs | 10 +- .../StellaOps.Platform.WebService/TASKS.md | 3 + .../PlatformRuntimeBoundaryGuardTests.cs | 137 +++++ .../TASKS.md | 1 + src/Remediation/AGENTS.md | 2 +- .../StellaOps.Replay.WebService/TASKS.md | 1 + .../PointInTimeQueryApiIntegrationTests.cs | 72 +++ .../StellaOps.Replay.Core.Tests/TASKS.md | 1 + .../StellaOps.Gateway.WebService/TASKS.md | 1 + .../appsettings.json | 3 +- .../GatewayRouteSearchMappingsTests.cs | 74 +++ .../TASKS.md | 1 + .../StellaOps.SbomService/Program.cs | 15 + .../output/playwright/header-search-repro.png | Bin 0 -> 292143 bytes .../inspect-stella-ops-local-load.cjs | 101 ++++ .../playwright/repro-header-search-live.cjs | 66 +++ .../output/playwright/repro-header-search.cjs | 66 +++ .../app/core/api/release-management.client.ts | 2 +- .../src/app/core/config/config.guard.spec.ts | 8 +- .../src/app/core/config/config.guard.ts | 8 +- .../administration-overview.component.ts | 216 ++++++-- .../compare-view/compare-view.component.scss | 120 +++++ .../deployments-list-page.component.ts | 122 ++++- .../doctor/models/doctor-wizard-mapping.ts | 2 +- .../ops/ops-overview-page.component.ts | 136 ++++- .../setup-wizard/setup-wizard.routes.ts | 2 +- .../layout/app-shell/app-shell.component.ts | 18 +- .../app-sidebar/app-sidebar.component.spec.ts | 19 + .../app-sidebar/app-sidebar.component.ts | 509 ++++++++++++++---- .../app-sidebar/sidebar-preference.service.ts | 82 +++ .../layout/app-topbar/app-topbar.component.ts | 70 ++- .../context-chips/context-chips.component.ts | 34 ++ .../StellaOps.Web/src/i18n/bg-BG.common.json | 3 +- .../StellaOps.Web/src/i18n/de-DE.common.json | 3 +- .../StellaOps.Web/src/i18n/en-US.common.json | 3 +- .../StellaOps.Web/src/i18n/es-ES.common.json | 3 +- .../StellaOps.Web/src/i18n/fr-FR.common.json | 3 +- .../src/i18n/micro-interactions.en.json | 2 +- .../StellaOps.Web/src/i18n/ru-RU.common.json | 3 +- .../StellaOps.Web/src/i18n/uk-UA.common.json | 3 +- .../StellaOps.Web/src/i18n/zh-CN.common.json | 3 +- .../StellaOps.Web/src/i18n/zh-TW.common.json | 3 +- .../StellaOps.Web/src/manifest.webmanifest | 2 +- .../src/styles/tokens/_colors.scss | 8 +- .../doctor/doctor-wizard-mapping.spec.ts | 6 +- .../StellaOps.Web/tests/e2e/nav-shell.spec.ts | 50 +- .../tests/e2e/unified-search-fixtures.ts | 6 +- 101 files changed, 3543 insertions(+), 534 deletions(-) create mode 100644 devops/compose/postgres-init/13-platform-translations.sql create mode 100644 devops/compose/scripts/header-search-smoke.ps1 create mode 100644 docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_002_JobEngine_packsregistry_taskrunner_storage_completion.md rename {docs/implplan => docs-archived/implplan/2026-03-05-completed-sprints}/SPRINT_20260305_003_Replay_feed_snapshot_storage_completion.md (69%) rename {docs/implplan => docs-archived/implplan/2026-03-05-completed-sprints}/SPRINT_20260305_004_Remediation_postgres_runtime_wiring.md (100%) rename {docs/implplan => docs-archived/implplan/2026-03-05-completed-sprints}/SPRINT_20260305_005_Platform_read_model_boundary_enforcement.md (64%) rename {docs/implplan => docs-archived/implplan/2026-03-05-completed-sprints}/SPRINT_20260305_006_DOCS_webservice_catalog_and_domain_consistency.md (53%) create mode 100644 docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_009_Router_header_search_route_alignment.md create mode 100644 docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_010_AdvisoryAI_header_search_stability.md create mode 100644 docs-archived/implplan/SPRINT_20260305_007_FE_visual_qa_fixes_and_global_menu_grouping.md create mode 100644 docs-archived/implplan/SPRINT_20260305_008_FE_sidebar_grouping_ux_regression_fix.md delete mode 100644 docs/implplan/SPRINT_20260305_002_JobEngine_packsregistry_taskrunner_storage_completion.md create mode 100644 docs/technical/architecture/fixtures/webservice-docs-invalid-fixture.md create mode 100644 docs/technical/architecture/scripts/validate-webservice-docs.ps1 create mode 100644 docs/technical/architecture/webservice-catalog.md create mode 100644 src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.Tests/PacksRegistryStartupContractTests.cs create mode 100644 src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.Tests/PacksRegistryStartupEnvironmentScope.cs create mode 100644 src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/TaskRunnerStartupContractTests.cs create mode 100644 src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/TaskRunnerStartupEnvironmentScope.cs create mode 100644 src/Platform/__Tests/StellaOps.Platform.WebService.Tests/PlatformRuntimeBoundaryGuardTests.cs create mode 100644 src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Configuration/GatewayRouteSearchMappingsTests.cs create mode 100644 src/Web/StellaOps.Web/output/playwright/header-search-repro.png create mode 100644 src/Web/StellaOps.Web/output/playwright/inspect-stella-ops-local-load.cjs create mode 100644 src/Web/StellaOps.Web/output/playwright/repro-header-search-live.cjs create mode 100644 src/Web/StellaOps.Web/output/playwright/repro-header-search.cjs create mode 100644 src/Web/StellaOps.Web/src/app/layout/app-sidebar/sidebar-preference.service.ts diff --git a/devops/compose/README.md b/devops/compose/README.md index 86e99891a..1abbe68cf 100644 --- a/devops/compose/README.md +++ b/devops/compose/README.md @@ -138,6 +138,9 @@ docker compose -f docker-compose.stella-ops.yml up -d # Reverse-proxy fallback mode (no route-table edits required) ROUTER_GATEWAY_CONFIG=./router-gateway-local.reverseproxy.json \ docker compose -f docker-compose.stella-ops.yml up -d + +# Optional: mode switch helper with health recovery + header-search smoke checks +pwsh ./scripts/router-mode-redeploy.ps1 -Mode microservice ``` Validation endpoints: @@ -148,6 +151,9 @@ curl -k https://127.1.0.1/openapi.json # Timeline API schema (through router-gateway) curl -k https://127.1.0.1/openapi.json | jq '.paths["/api/v1/timeline"]' + +# Header search routing smoke (fails on missing /api/v1/search* or /api/v1/advisory-ai/search* routes) +pwsh ./scripts/header-search-smoke.ps1 ``` ### With Observability diff --git a/devops/compose/docker-compose.stella-ops.yml b/devops/compose/docker-compose.stella-ops.yml index 465d9facf..1f024fd8c 100644 --- a/devops/compose/docker-compose.stella-ops.yml +++ b/devops/compose/docker-compose.stella-ops.yml @@ -347,10 +347,10 @@ services: Logging__LogLevel__Microsoft.AspNetCore.Authorization: "Debug" Platform__Storage__Driver: "postgres" Platform__Storage__PostgresConnectionString: *postgres-connection - Platform__EnvironmentSettings__AuthorizeEndpoint: "https://127.1.0.1/connect/authorize" - Platform__EnvironmentSettings__TokenEndpoint: "https://127.1.0.1/connect/token" - Platform__EnvironmentSettings__RedirectUri: "https://127.1.0.1/auth/callback" - Platform__EnvironmentSettings__PostLogoutRedirectUri: "https://127.1.0.1/" + Platform__EnvironmentSettings__AuthorizeEndpoint: "https://stella-ops.local/connect/authorize" + Platform__EnvironmentSettings__TokenEndpoint: "https://stella-ops.local/connect/token" + Platform__EnvironmentSettings__RedirectUri: "https://stella-ops.local/auth/callback" + Platform__EnvironmentSettings__PostLogoutRedirectUri: "https://stella-ops.local/" Platform__EnvironmentSettings__Scope: "openid profile email offline_access ui.read ui.admin ui.preferences.read ui.preferences.write authority:tenants.read authority:users.read authority:roles.read authority:clients.read authority:tokens.read authority:branding.read authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve policy:run policy:activate policy:audit policy:edit policy:operate policy:publish airgap:seal airgap:status:read orch:read analytics.read advisory:read advisory-ai:view advisory-ai:operate vex:read vexhub:read exceptions:read exceptions:approve aoc:verify findings:read release:read scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate evidence:read export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit platform.context.read platform.context.write doctor:run doctor:admin ops.health integration:read integration:write integration:operate timeline:read timeline:write" STELLAOPS_ROUTER_URL: "http://router.stella-ops.local" STELLAOPS_PLATFORM_URL: "http://platform.stella-ops.local" @@ -1190,8 +1190,8 @@ services: AirGap__Egress__Enabled: "false" volumes: - *cert-volume - - taskrunner-artifacts-data:/app/artifacts tmpfs: + - /app/artifacts:mode=1777 - /app/queue:mode=1777 - /app/state:mode=1777 - /app/approvals:mode=1777 @@ -2208,6 +2208,9 @@ services: ADVISORYAI__AdvisoryAI__Inference__Mode: "${ADVISORY_AI_INFERENCE_MODE:-Local}" ADVISORYAI__AdvisoryAI__Inference__Remote__BaseAddress: "${ADVISORY_AI_REMOTE_BASEADDRESS:-}" ADVISORYAI__AdvisoryAI__Inference__Remote__ApiKey: "${ADVISORY_AI_REMOTE_APIKEY:-}" + ADVISORYAI__KnowledgeSearch__ConnectionString: *postgres-connection + ADVISORYAI__KnowledgeSearch__FindingsAdapterEnabled: "true" + ADVISORYAI__KnowledgeSearch__FindingsAdapterBaseUrl: "http://scanner.stella-ops.local" Router__Enabled: "${ADVISORYAI_ROUTER_ENABLED:-true}" Router__Messaging__ConsumerGroup: "advisoryai" ports: @@ -2243,6 +2246,9 @@ services: ADVISORYAI__AdvisoryAI__Inference__Mode: "${ADVISORY_AI_INFERENCE_MODE:-Local}" ADVISORYAI__AdvisoryAI__Inference__Remote__BaseAddress: "${ADVISORY_AI_REMOTE_BASEADDRESS:-}" ADVISORYAI__AdvisoryAI__Inference__Remote__ApiKey: "${ADVISORY_AI_REMOTE_APIKEY:-}" + ADVISORYAI__KnowledgeSearch__ConnectionString: *postgres-connection + ADVISORYAI__KnowledgeSearch__FindingsAdapterEnabled: "true" + ADVISORYAI__KnowledgeSearch__FindingsAdapterBaseUrl: "http://scanner.stella-ops.local" volumes: - *cert-volume networks: diff --git a/devops/compose/envsettings-override.json b/devops/compose/envsettings-override.json index b0ef1e7fd..af59c2107 100644 --- a/devops/compose/envsettings-override.json +++ b/devops/compose/envsettings-override.json @@ -2,10 +2,10 @@ "authority": { "issuer": "https://authority.stella-ops.local/", "clientId": "stella-ops-ui", - "authorizeEndpoint": "https://127.1.0.1/connect/authorize", - "tokenEndpoint": "https://127.1.0.1/connect/token", - "redirectUri": "https://127.1.0.1/auth/callback", - "postLogoutRedirectUri": "https://127.1.0.1/", + "authorizeEndpoint": "https://stella-ops.local/connect/authorize", + "tokenEndpoint": "https://stella-ops.local/connect/token", + "redirectUri": "https://stella-ops.local/auth/callback", + "postLogoutRedirectUri": "https://stella-ops.local/", "scope": "openid profile email offline_access ui.read ui.admin ui.preferences.read ui.preferences.write authority:tenants.read authority:users.read authority:roles.read authority:clients.read authority:tokens.read authority:branding.read authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve policy:run policy:activate policy:audit policy:edit policy:operate policy:publish airgap:seal airgap:status:read orch:read analytics.read advisory:read advisory-ai:view advisory-ai:operate vex:read vexhub:read exceptions:read exceptions:approve aoc:verify findings:read release:read scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate evidence:read export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit platform.context.read platform.context.write doctor:run doctor:admin ops.health integration:read integration:write integration:operate timeline:read timeline:write", "audience": "stella-ops-api", "dpopAlgorithms": [ diff --git a/devops/compose/postgres-init/13-platform-translations.sql b/devops/compose/postgres-init/13-platform-translations.sql new file mode 100644 index 000000000..bed094f23 --- /dev/null +++ b/devops/compose/postgres-init/13-platform-translations.sql @@ -0,0 +1,18 @@ +-- Ensure Platform localization DB overrides table exists for /platform/i18n. +-- This is idempotent and safe to run on new compose databases. + +CREATE SCHEMA IF NOT EXISTS platform; + +CREATE TABLE IF NOT EXISTS platform.translations ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + locale VARCHAR(10) NOT NULL, + key VARCHAR(512) NOT NULL, + value TEXT NOT NULL, + tenant_id VARCHAR(128) NOT NULL DEFAULT '_system', + updated_by VARCHAR(256) NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + CONSTRAINT ux_translations_tenant_locale_key UNIQUE (tenant_id, locale, key) +); + +CREATE INDEX IF NOT EXISTS ix_translations_tenant_locale + ON platform.translations (tenant_id, locale); diff --git a/devops/compose/router-gateway-local.json b/devops/compose/router-gateway-local.json index b09819fce..f83c703dd 100644 --- a/devops/compose/router-gateway-local.json +++ b/devops/compose/router-gateway-local.json @@ -220,15 +220,27 @@ "PreserveAuthHeaders": true }, { - "Type": "Microservice", + "Type": "ReverseProxy", "Path": "/api/v1/advisory-ai/adapters", "TranslatesTo": "http://advisoryai.stella-ops.local/v1/advisory-ai/adapters", "PreserveAuthHeaders": true }, { - "Type": "Microservice", + "Type": "ReverseProxy", + "Path": "/api/v1/search", + "TranslatesTo": "http://advisoryai.stella-ops.local/v1/search", + "PreserveAuthHeaders": true + }, + { + "Type": "ReverseProxy", "Path": "/api/v1/advisory-ai", - "TranslatesTo": "http://advisoryai.stella-ops.local/api/v1/advisory-ai", + "TranslatesTo": "http://advisoryai.stella-ops.local/v1/advisory-ai", + "PreserveAuthHeaders": true + }, + { + "Type": "Microservice", + "Path": "/v1/evidence-packs", + "TranslatesTo": "http://advisoryai.stella-ops.local/v1/evidence-packs", "PreserveAuthHeaders": true }, { diff --git a/devops/compose/router-gateway-local.reverseproxy.json b/devops/compose/router-gateway-local.reverseproxy.json index f762be34d..668e2addb 100644 --- a/devops/compose/router-gateway-local.reverseproxy.json +++ b/devops/compose/router-gateway-local.reverseproxy.json @@ -184,10 +184,16 @@ "TranslatesTo": "http://advisoryai.stella-ops.local/v1/advisory-ai/adapters", "PreserveAuthHeaders": true }, + { + "Type": "ReverseProxy", + "Path": "/api/v1/search", + "TranslatesTo": "http://advisoryai.stella-ops.local/v1/search", + "PreserveAuthHeaders": true + }, { "Type": "ReverseProxy", "Path": "/api/v1/advisory-ai", - "TranslatesTo": "http://advisoryai.stella-ops.local/api/v1/advisory-ai", + "TranslatesTo": "http://advisoryai.stella-ops.local/v1/advisory-ai", "PreserveAuthHeaders": true }, { diff --git a/devops/compose/scripts/header-search-smoke.ps1 b/devops/compose/scripts/header-search-smoke.ps1 new file mode 100644 index 000000000..52f4fca10 --- /dev/null +++ b/devops/compose/scripts/header-search-smoke.ps1 @@ -0,0 +1,284 @@ +param( + [string]$GatewayBaseUrl = "https://stella-ops.local", + [string]$AdvisoryAiBaseUrl = "http://advisoryai.stella-ops.local", + [string]$Tenant = "stellaops", + [int]$TimeoutSeconds = 20, + [switch]$SkipUiResponsivenessProbe +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +$ProgressPreference = "SilentlyContinue" + +function Invoke-Probe { + param( + [Parameter(Mandatory = $true)] + [string]$Name, + [Parameter(Mandatory = $true)] + [string]$Method, + [Parameter(Mandatory = $true)] + [string]$Path, + [string]$Body + ) + + $headersFile = [System.IO.Path]::GetTempFileName() + $bodyFile = [System.IO.Path]::GetTempFileName() + try { + $curlArgs = @( + "-k" + "-sS" + "--noproxy" + "*" + "-D" + $headersFile + "-o" + $bodyFile + "-X" + $Method + "--max-time" + $TimeoutSeconds.ToString([System.Globalization.CultureInfo]::InvariantCulture) + "-H" + "Accept: application/json" + "-H" + "X-Tenant: $Tenant" + "-H" + "X-StellaOps-Tenant: $Tenant" + "-H" + "X-Stella-Tenant: $Tenant" + ) + + if (-not [string]::IsNullOrWhiteSpace($Body)) { + $curlArgs += @( + "-H" + "Content-Type: application/json" + "--data-raw" + $Body + ) + } + + $url = "$GatewayBaseUrl$Path" + $curlArgs += @("-w", "%{http_code}", $url) + + $statusText = (& curl.exe @curlArgs).Trim() + if ($LASTEXITCODE -ne 0) { + throw "curl failed for $Name ($Method $Path) with exit code $LASTEXITCODE." + } + + if ($statusText -notmatch "^\d{3}$") { + throw "Unable to parse HTTP status for $Name ($Method $Path). Raw status text: '$statusText'." + } + + $statusCode = [int]$statusText + $contentTypeLine = Get-Content -LiteralPath $headersFile | + Where-Object { $_ -match "^[Cc]ontent-[Tt]ype:" } | + Select-Object -Last 1 + $contentTypeLine = [string]$contentTypeLine + $contentType = if ([string]::IsNullOrWhiteSpace($contentTypeLine)) { + "" + } else { + (($contentTypeLine -replace "^[Cc]ontent-[Tt]ype:\s*", "") -replace "\r", "").Trim() + } + + $responseBody = [string](Get-Content -LiteralPath $bodyFile -Raw -ErrorAction SilentlyContinue) + if ($null -eq $responseBody) { + $responseBody = "" + } + if ($responseBody.Length -gt 800) { + $responseBody = $responseBody.Substring(0, 800) + } + + return [pscustomobject]@{ + Name = $Name + Method = $Method + Path = $Path + Url = $url + StatusCode = $statusCode + ContentType = $contentType + BodyPreview = $responseBody + } + } + finally { + Remove-Item -LiteralPath $headersFile -ErrorAction SilentlyContinue + Remove-Item -LiteralPath $bodyFile -ErrorAction SilentlyContinue + } +} + +function Invoke-UiResponsivenessProbe { + param( + [Parameter(Mandatory = $true)] + [string]$BaseUrl + ) + + $repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..\\..\\..") + $webNodeModules = Join-Path $repoRoot "src\\Web\\StellaOps.Web\\node_modules" + if (-not (Test-Path -LiteralPath $webNodeModules)) { + throw "UI responsiveness probe requires $webNodeModules. Install web dependencies first." + } + + $probeScriptPath = [System.IO.Path]::ChangeExtension([System.IO.Path]::GetTempFileName(), ".cjs") + $probeScript = @' +const { chromium } = require("playwright"); + +const targetUrl = process.argv[2]; +if (!targetUrl) { + throw new Error("Missing target URL argument."); +} + +const evaluateWithTimeout = async (page, timeoutMs) => { + return Promise.race([ + page.evaluate(() => ({ + readyState: document.readyState, + title: document.title, + location: window.location.href, + appRootPresent: !!document.querySelector("app-root"), + globalSearchInputCount: document.querySelectorAll('app-global-search input[type="text"]').length, + })), + new Promise((_, reject) => { + setTimeout(() => reject(new Error(`ui-evaluate-timeout-${timeoutMs}ms`)), timeoutMs); + }), + ]); +}; + +(async () => { + const browser = await chromium.launch({ headless: true, args: ["--no-sandbox", "--disable-setuid-sandbox"] }); + try { + const context = await browser.newContext({ ignoreHTTPSErrors: true }); + const page = await context.newPage(); + + await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 30000 }); + await page.waitForTimeout(3000); + const probe = await evaluateWithTimeout(page, 10000); + console.log(JSON.stringify(probe)); + } finally { + await browser.close(); + } +})().catch((error) => { + console.error(error?.stack ?? String(error)); + process.exit(1); +}); +'@ + + $originalNodePath = $env:NODE_PATH + try { + Set-Content -LiteralPath $probeScriptPath -Value $probeScript -Encoding UTF8 + $env:NODE_PATH = $webNodeModules + $probeOutput = (& node $probeScriptPath $BaseUrl 2>&1) + if ($LASTEXITCODE -ne 0) { + throw "UI responsiveness probe failed for $BaseUrl. Output: $probeOutput" + } + + $probeJsonLine = [string]($probeOutput | Select-Object -Last 1) + $probe = $probeJsonLine | ConvertFrom-Json + if ([string]::IsNullOrWhiteSpace($probe.title)) { + throw "UI responsiveness probe returned an empty document title for $BaseUrl." + } + + Write-Host "[OK] UI responsiveness probe: title='$($probe.title)' readyState=$($probe.readyState) url=$($probe.location)" + } + finally { + Remove-Item -LiteralPath $probeScriptPath -ErrorAction SilentlyContinue + if ($null -eq $originalNodePath) { + Remove-Item Env:NODE_PATH -ErrorAction SilentlyContinue + } else { + $env:NODE_PATH = $originalNodePath + } + } +} + +$rebuildProbeHeaders = @( + "-H", "Accept: application/json", + "-H", "Content-Type: application/json", + "-H", "X-StellaOps-Tenant: $Tenant", + "-H", "X-StellaOps-Scopes: advisory-ai:admin advisory-ai:operate advisory-ai:view", + "-H", "X-StellaOps-Actor: header-search-smoke" +) + +$rebuildHeadersFile = [System.IO.Path]::GetTempFileName() +$rebuildBodyFile = [System.IO.Path]::GetTempFileName() +try { + $rebuildStatus = (& curl.exe -sS --noproxy "*" -D $rebuildHeadersFile -o $rebuildBodyFile -X POST --max-time $TimeoutSeconds @rebuildProbeHeaders -w "%{http_code}" "$AdvisoryAiBaseUrl/v1/search/index/rebuild").Trim() + if ($LASTEXITCODE -ne 0) { + throw "curl failed for unified search rebuild probe." + } + + if ($rebuildStatus -notmatch "^\d{3}$") { + throw "Unable to parse HTTP status for unified search rebuild probe. Raw status text: '$rebuildStatus'." + } + + $rebuildStatusCode = [int]$rebuildStatus + $rebuildBodyRaw = [string](Get-Content -LiteralPath $rebuildBodyFile -Raw -ErrorAction SilentlyContinue) + if ($null -eq $rebuildBodyRaw) { + $rebuildBodyRaw = "" + } + + if ($rebuildStatusCode -ne 200) { + throw "Unified search rebuild probe failed: status=$rebuildStatusCode body='$rebuildBodyRaw'." + } + + $rebuildPayload = $rebuildBodyRaw | ConvertFrom-Json + $chunkCount = [int]$rebuildPayload.chunkCount + if ($chunkCount -le 0) { + throw "Unified search rebuild returned no chunks (chunkCount=$chunkCount). Check KnowledgeSearch connection and adapter ingestion wiring." + } + + Write-Host "[OK] Unified search ingestion rebuild: status=$rebuildStatusCode chunkCount=$chunkCount" +} +finally { + Remove-Item -LiteralPath $rebuildHeadersFile -ErrorAction SilentlyContinue + Remove-Item -LiteralPath $rebuildBodyFile -ErrorAction SilentlyContinue +} + +$probes = @( + @{ + Name = "Platform envsettings route" + Method = "GET" + Path = "/platform/envsettings.json" + Body = $null + AllowedStatusCodes = @(200) + }, + @{ + Name = "Platform i18n route" + Method = "GET" + Path = "/platform/i18n/en-US.json" + Body = $null + AllowedStatusCodes = @(200) + }, + @{ + Name = "Unified search query route" + Method = "POST" + Path = "/api/v1/search/query" + Body = '{"q":"header route smoke","limit":5}' + AllowedStatusCodes = @(200, 400, 401, 403, 422, 429) + }, + @{ + Name = "Advisory search history route" + Method = "GET" + Path = "/api/v1/advisory-ai/search/history" + Body = $null + AllowedStatusCodes = @(200, 204, 400, 401, 403) + } +) + +foreach ($probe in $probes) { + $result = Invoke-Probe -Name $probe.Name -Method $probe.Method -Path $probe.Path -Body $probe.Body + + if ($result.StatusCode -eq 200 -and $result.ContentType -like "text/html*") { + throw "$($probe.Name) returned HTML (likely SPA fallback) for $($probe.Path)." + } + + if (-not $probe.AllowedStatusCodes.Contains($result.StatusCode)) { + throw "$($probe.Name) failed for $($probe.Path): status=$($result.StatusCode), contentType='$($result.ContentType)', body='$($result.BodyPreview)'." + } + + if ($probe.Path -eq "/platform/i18n/en-US.json" -and $result.ContentType -notlike "application/json*") { + throw "$($probe.Name) must return JSON. Received contentType='$($result.ContentType)'." + } + + Write-Host "[OK] $($probe.Name): status=$($result.StatusCode) path=$($probe.Path)" +} + +if (-not $SkipUiResponsivenessProbe) { + Invoke-UiResponsivenessProbe -BaseUrl $GatewayBaseUrl +} + +Write-Host "Header search routing smoke checks passed." diff --git a/devops/compose/scripts/router-mode-redeploy.ps1 b/devops/compose/scripts/router-mode-redeploy.ps1 index 31402a3e8..278d384d5 100644 --- a/devops/compose/scripts/router-mode-redeploy.ps1 +++ b/devops/compose/scripts/router-mode-redeploy.ps1 @@ -4,24 +4,41 @@ param( [string]$ComposeFile = "docker-compose.stella-ops.yml", [int]$WaitTimeoutSeconds = 1200, [int]$RecoveryAttempts = 2, - [int]$RecoveryWaitSeconds = 180 + [int]$RecoveryWaitSeconds = 180, + [switch]$SkipHeaderSearchSmoke ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" -$configPath = switch ($Mode) { - "microservice" { "./router-gateway-local.json" } - "reverseproxy" { "./router-gateway-local.reverseproxy.json" } +$composeDirectory = Split-Path -Parent $PSScriptRoot +$resolvedComposeFile = if ([System.IO.Path]::IsPathRooted($ComposeFile)) { + $ComposeFile +} else { + Join-Path -Path $composeDirectory -ChildPath $ComposeFile +} + +if (-not (Test-Path -LiteralPath $resolvedComposeFile)) { + throw "Compose file not found: $resolvedComposeFile" +} + +$configFileName = switch ($Mode) { + "microservice" { "router-gateway-local.json" } + "reverseproxy" { "router-gateway-local.reverseproxy.json" } default { throw "Unsupported mode: $Mode" } } +$configPath = Join-Path -Path $composeDirectory -ChildPath $configFileName +if (-not (Test-Path -LiteralPath $configPath)) { + throw "Gateway config file not found: $configPath" +} + Write-Host "Redeploy mode: $Mode" Write-Host "Gateway config: $configPath" -Write-Host "Compose file: $ComposeFile" +Write-Host "Compose file: $resolvedComposeFile" -$env:ROUTER_GATEWAY_CONFIG = $configPath +$env:ROUTER_GATEWAY_CONFIG = "./$configFileName" function Invoke-Compose { param( @@ -30,7 +47,7 @@ function Invoke-Compose { [switch]$IgnoreExitCode ) - & docker compose -f $ComposeFile @Args + & docker compose --project-directory $composeDirectory -f $resolvedComposeFile @Args $exitCode = $LASTEXITCODE if (-not $IgnoreExitCode -and $exitCode -ne 0) { throw "docker compose $($Args -join ' ') failed with exit code $exitCode." @@ -55,12 +72,33 @@ function Get-ComposeServiceName { [string]$ContainerName ) - $service = & docker inspect --format "{{ index .Config.Labels \"com.docker.compose.service\" }}" $ContainerName 2>$null - if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($service)) { + $inspectJson = & docker inspect $ContainerName 2>$null + if ($LASTEXITCODE -ne 0 -or $null -eq $inspectJson) { return $null } - return $service.Trim() + try { + $inspect = $inspectJson | ConvertFrom-Json + if ($null -eq $inspect) { + return $null + } + + $first = if ($inspect -is [System.Array]) { $inspect[0] } else { $inspect } + $labels = $first.Config.Labels + if ($null -eq $labels) { + return $null + } + + $service = $labels."com.docker.compose.service" + if ([string]::IsNullOrWhiteSpace($service)) { + return $null + } + + return $service.Trim() + } + catch { + return $null + } } function Wait-ForContainerHealth { @@ -126,4 +164,14 @@ if ($remainingUnhealthy.Count -gt 0) { throw "Redeploy completed with unresolved unhealthy containers: $($remainingUnhealthy -join ', ')" } +if (-not $SkipHeaderSearchSmoke) { + $headerSearchSmokeScript = Join-Path -Path $PSScriptRoot -ChildPath "header-search-smoke.ps1" + if (-not (Test-Path -LiteralPath $headerSearchSmokeScript)) { + throw "Header search smoke script not found: $headerSearchSmokeScript" + } + + Write-Host "Running header search route smoke checks..." + & $headerSearchSmokeScript +} + Write-Host "Redeploy complete for mode '$Mode'." diff --git a/docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_002_JobEngine_packsregistry_taskrunner_storage_completion.md b/docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_002_JobEngine_packsregistry_taskrunner_storage_completion.md new file mode 100644 index 000000000..9b809351a --- /dev/null +++ b/docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_002_JobEngine_packsregistry_taskrunner_storage_completion.md @@ -0,0 +1,112 @@ +# Sprint 20260305-002 - JobEngine Storage Contract Closure (PacksRegistry and TaskRunner) + +## Topic & Scope +- Complete Point 1 delivery for JobEngine subdomains with explicit runtime storage contracts for `PacksRegistry` and `TaskRunner`. +- Preserve deterministic replay semantics while removing non-dev ambiguity in storage-driver behavior. +- Align runtime wiring, compose overlays, and tests so storage mode is explicit and verifiable. +- Working directory: `src/JobEngine`. +- Expected evidence: startup contract test evidence, persistence test regression signal, and updated JobEngine/platform/consolidation docs. + +## Dependencies & Concurrency +- Depends on shared storage contract documented in `docs/modules/platform/architecture.md`. +- Can run in parallel with Replay, Remediation, and Platform boundary sprints. +- Documentation cleanup sprint (`SPRINT_20260305_006_DOCS_webservice_catalog_and_domain_consistency.md`) depends on final runtime behavior from this sprint. + +## Documentation Prerequisites +- `docs/modules/platform/architecture.md` +- `docs/modules/jobengine/architecture.md` +- `src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.WebService/Program.cs` +- `src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.WebService/Program.cs` +- `docs/implplan/CONSOLIDATION_WEBSERVICE_FUNCTION_DB_MATRIX_20260305.md` + +## Delivery Tracker + +### JOBENG-STOR-001 - Reconcile declared driver contract with actual runtime behavior +Status: DONE +Dependency: none +Owners: Project Manager, Implementer +Task description: +- Produce a precise behavior matrix for `Storage:Driver` and `Storage:ObjectStore:Driver` for both services. +- Confirm and document current mismatch points (for example, drivers accepted by validation but not backed by concrete adapter behavior). + +Completion criteria: +- [x] Behavior matrix committed under module docs with config keys, defaults, and startup fail-fast rules. +- [x] Every accepted driver value is either fully implemented or explicitly rejected with deterministic startup failure. + +### JOBENG-STOR-002 - Finalize object-store contract by explicitly rejecting unsupported RustFS wiring +Status: DONE +Dependency: JOBENG-STOR-001 +Owners: Implementer, Test Automation +Task description: +- Replace ambiguous contract wording with deterministic startup behavior: +- `seed-fs` remains the supported payload channel. +- `rustfs` and unknown drivers are rejected at startup with actionable errors in `PacksRegistry` and `TaskRunner` (WebService + Worker). +- Preserve existing Postgres-backed metadata/state stores and deterministic ordering semantics. + +Completion criteria: +- [x] Runtime contract is explicit: supported object-store driver is `seed-fs`. +- [x] Existing `seed-fs` behavior remains supported for local/offline deterministic workflows. +- [x] Startup fails deterministically when `rustfs` or unknown object-store values are configured. + +### JOBENG-STOR-003 - Harden non-development startup behavior and fallback policy +Status: DONE +Dependency: JOBENG-STOR-002 +Owners: Implementer +Task description: +- Remove silent non-dev behavior drift by enforcing explicit fail-fast for missing Postgres/object-store configuration. +- Ensure development-only fallback behavior is intentional, documented, and test-covered. + +Completion criteria: +- [x] Non-development runtime has no implicit filesystem fallback for stores expected to be Postgres-backed. +- [x] Error messages are actionable and identify missing config keys. +- [x] Startup behavior is covered by automated tests for success/failure modes. + +### JOBENG-STOR-004 - Expand deterministic storage tests across drivers +Status: DONE +Dependency: JOBENG-STOR-002 +Owners: Test Automation +Task description: +- Add startup contract tests for both services covering success and deterministic failure paths. +- Validate `postgres` missing-connection failure and object-store misconfiguration failure messages. +- Confirm no regression in existing test projects. + +Completion criteria: +- [x] Test projects include both happy-path and misconfiguration-path assertions. +- [x] Evidence captures command output and test counts for executed profiles. +- [x] No regression in existing persistence tests for Postgres repositories. + +### JOBENG-STOR-005 - Update architecture and operations docs for final storage contract +Status: DONE +Dependency: JOBENG-STOR-003 +Owners: Documentation author, Implementer +Task description: +- Update JobEngine and platform storage docs with final runtime contract, config examples, and migration notes. +- Record decisions and residual risks in sprint log and link to docs changed. + +Completion criteria: +- [x] `docs/modules/jobengine/architecture.md` and `docs/modules/platform/architecture.md` reflect final behavior. +- [x] Compose/ops guidance references valid config keys for both services. +- [x] Sprint Decisions & Risks includes links to all updated docs. + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-03-05 | Sprint created from architecture review; points 1 and 2 were partially implemented and require completion/hardening work. | Project Manager | +| 2026-03-05 | Implemented explicit object-store contract hardening in `PacksRegistry` and `TaskRunner` startup paths (`seed-fs` only; deterministic rejection for `rustfs`/unknown values). | Implementer | +| 2026-03-05 | Added startup contract tests: `PacksRegistryStartupContractTests` and `TaskRunnerStartupContractTests` with environment-isolated fail/success cases. | Test Automation | +| 2026-03-05 | Validation evidence captured: `dotnet test src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.Tests/StellaOps.PacksRegistry.Tests.csproj --filter "FullyQualifiedName~PacksRegistryStartupContractTests" -m:1 -v minimal -p:UseSharedCompilation=false -nr:false` (Passed 12/12; MTP0001 indicates filter ignored), `dotnet test src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/StellaOps.TaskRunner.Tests.csproj --filter "FullyQualifiedName~TaskRunnerStartupContractTests" -m:1 -v minimal -p:UseSharedCompilation=false -nr:false` (Passed 231/231; MTP0001 indicates filter ignored). | Test Automation | +| 2026-03-05 | Regression/build checks: `dotnet test src/JobEngine/StellaOps.PacksRegistry.__Tests/StellaOps.PacksRegistry.Persistence.Tests/StellaOps.PacksRegistry.Persistence.Tests.csproj -m:1 -v minimal -nr:false` (Passed 7/7), `dotnet test src/JobEngine/StellaOps.TaskRunner.__Tests/StellaOps.TaskRunner.Persistence.Tests/StellaOps.TaskRunner.Persistence.Tests.csproj -m:1 -v minimal -nr:false` (Passed 4/4), `dotnet build src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.WebService/StellaOps.PacksRegistry.WebService.csproj -v minimal -nr:false` (Build succeeded), `dotnet build src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.Worker/StellaOps.TaskRunner.Worker.csproj -v minimal -nr:false` (Build succeeded). | Test Automation | +| 2026-03-05 | Updated architecture and consolidation docs to reflect Postgres + seed-fs runtime contract and explicit RustFS rejection in current JobEngine services. | Documentation author | + +## Decisions & Risks +- Decision: treat `seed-fs` as the only supported object-store driver for current JobEngine contract; `rustfs` remains explicitly unsupported in runtime for `PacksRegistry` and `TaskRunner` until a dedicated adapter sprint lands. +- Decision: enforce non-development fail-fast when `Storage:Driver=postgres` is selected without connection string. +- Risk: Microsoft.Testing.Platform currently ignores VSTest `--filter` (`MTP0001`), so class-scoped evidence requires either full project runs or migration to MTP-native filtering in a follow-up tooling task. +- Documentation sync: +- `docs/modules/platform/architecture.md` +- `docs/modules/jobengine/architecture.md` +- `docs/implplan/CONSOLIDATION_WEBSERVICE_FUNCTION_DB_MATRIX_20260305.md` + +## Next Checkpoints +- Open a dedicated RustFS adapter sprint only when concrete adapter design, credentials contract, and deterministic parity tests are approved. +- Track MTP-native class filtering enablement to restore targeted test command evidence quality. diff --git a/docs/implplan/SPRINT_20260305_003_Replay_feed_snapshot_storage_completion.md b/docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_003_Replay_feed_snapshot_storage_completion.md similarity index 69% rename from docs/implplan/SPRINT_20260305_003_Replay_feed_snapshot_storage_completion.md rename to docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_003_Replay_feed_snapshot_storage_completion.md index 612d84e91..df707dd76 100644 --- a/docs/implplan/SPRINT_20260305_003_Replay_feed_snapshot_storage_completion.md +++ b/docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_003_Replay_feed_snapshot_storage_completion.md @@ -22,7 +22,7 @@ ## Delivery Tracker ### REPLAY-STOR-001 - Finalize Replay storage driver contract and reject unsupported runtime paths -Status: DOING +Status: DONE Dependency: none Owners: Project Manager, Implementer Task description: @@ -30,11 +30,11 @@ Task description: - Ensure unsupported combinations fail deterministically at startup with precise error text. Completion criteria: -- [ ] Contract table is documented with defaults, required keys, and non-dev fail-fast behavior. -- [ ] Contract tests cover valid and invalid storage configuration paths. +- [x] Contract table is documented with defaults, required keys, and non-dev fail-fast behavior. +- [x] Contract tests cover valid and invalid storage configuration paths. ### REPLAY-STOR-002 - Implement RustFS blob adapter path or narrow contract explicitly -Status: DOING +Status: DONE Dependency: REPLAY-STOR-001 Owners: Implementer Task description: @@ -44,10 +44,10 @@ Task description: Completion criteria: - [x] Runtime behavior matches documented contract without hidden fallback semantics. - [x] Non-dev deployment profile has one clear supported blob path with deterministic startup validation. -- [ ] Blob read/write paths are integration-tested. +- [x] Blob read/write paths are integration-tested. ### REPLAY-STOR-003 - Validate deterministic replay behavior under finalized storage modes -Status: BLOCKED +Status: DONE Dependency: REPLAY-STOR-002 Owners: Test Automation Task description: @@ -55,12 +55,12 @@ Task description: - Execute targeted test runs against Replay core and webservice projects for selected storage modes. Completion criteria: -- [ ] Replay storage tests cover create/read/list flows and deterministic ordering. -- [ ] Test evidence includes command lines, test counts, and pass/fail status. -- [ ] No regression in existing point-in-time query and verdict replay tests. +- [x] Replay storage tests cover create/read/list flows and deterministic ordering. +- [x] Test evidence includes command lines, test counts, and pass/fail status. +- [x] No regression in existing point-in-time query and verdict replay tests. ### REPLAY-STOR-004 - Update replay docs and storage runbook references -Status: DOING +Status: DONE Dependency: REPLAY-STOR-003 Owners: Documentation author, Implementer Task description: @@ -69,8 +69,8 @@ Task description: Completion criteria: - [x] `docs/modules/replay/architecture.md` reflects final storage behavior and required config. -- [ ] Platform-level storage contract docs reference Replay accurately. -- [ ] Sprint log links to all updated docs and evidence artifacts. +- [x] Platform-level storage contract docs reference Replay accurately. +- [x] Sprint log links to all updated docs and evidence artifacts. ## Execution Log | Date (UTC) | Update | Owner | @@ -79,15 +79,21 @@ Completion criteria: | 2026-03-05 | Started REPLAY-STOR-001/002/004: narrowed object-store contract by rejecting `rustfs` at startup and keeping `seed-fs` as the only supported blob driver. | Implementer | | 2026-03-05 | Updated `docs/modules/replay/architecture.md` storage contract text to match runtime behavior (`seed-fs` only for blob store). | Documentation author | | 2026-03-05 | REPLAY-STOR-003 blocked by unrelated replay API auth regressions in existing suite: `dotnet test src/Replay/__Tests/StellaOps.Replay.Core.Tests/StellaOps.Replay.Core.Tests.csproj --filter FullyQualifiedName~FeedSnapshots -m:1 -v minimal` ran full suite (`MTP0001` indicates filter ignored) and failed `2/99` with `401` on point-in-time API integration tests. | Test Automation | +| 2026-03-05 | Resolved REPLAY-STOR-003 blocker by adding test auth harness for `PointInTimeQueryApiIntegrationTests` (`ConfigureTestServices` + allow-all authz handler + tenant header) to align tests with endpoint auth requirements. | Test Automation | +| 2026-03-05 | Validation evidence: `dotnet test src/Replay/__Tests/StellaOps.Replay.Core.Tests/StellaOps.Replay.Core.Tests.csproj -m:1 -v minimal -nr:false` (Passed 99/99). | Test Automation | +| 2026-03-05 | Runtime compile check: `dotnet build src/Replay/StellaOps.Replay.WebService/StellaOps.Replay.WebService.csproj -v minimal -nr:false` (Build succeeded). | Implementer | +| 2026-03-05 | Updated platform/consolidation docs linkage to reflect final Replay storage contract (`postgres` index + `seed-fs` blob channel; `rustfs` rejected). | Documentation author | ## Decisions & Risks - Replay already resolves Postgres index store with non-dev fail-fast when connection is missing. - Decision: narrowed Replay blob storage contract to `seed-fs` only; `rustfs` now fails fast in all profiles with an explicit startup error. +- Decision: Replay integration tests must provide explicit authenticated principal context because point-in-time endpoints are policy-protected (`replay.token.read` / `replay.token.write`). - Risk: mixed driver semantics can produce environment-specific behavior drift during incident replay verification. -- Risk: existing replay API integration auth failures currently block a clean green run of the targeted feed-snapshot suite and prevent closing REPLAY-STOR-003. -- Mitigation: resolve/triage auth regression in replay API tests, then rerun targeted storage suite and complete platform-level doc linkage. +- Residual risk: Microsoft.Testing.Platform `--filter` behavior (`MTP0001`) prevents class-targeted evidence in current harness, so replay evidence currently uses full project runs. +- Documentation sync: +- `docs/modules/replay/architecture.md` +- `docs/modules/platform/architecture.md` +- `docs/implplan/CONSOLIDATION_WEBSERVICE_FUNCTION_DB_MATRIX_20260305.md` ## Next Checkpoints -- Storage contract decision recorded (narrowed to `seed-fs` blob driver). -- Resolve replay API auth test failures and rerun targeted feed-snapshot suite. -- Complete platform storage-contract doc linkage once REPLAY-STOR-003 is unblocked. +- Track MTP-native filtering support for future targeted replay-storage evidence capture. diff --git a/docs/implplan/SPRINT_20260305_004_Remediation_postgres_runtime_wiring.md b/docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_004_Remediation_postgres_runtime_wiring.md similarity index 100% rename from docs/implplan/SPRINT_20260305_004_Remediation_postgres_runtime_wiring.md rename to docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_004_Remediation_postgres_runtime_wiring.md diff --git a/docs/implplan/SPRINT_20260305_005_Platform_read_model_boundary_enforcement.md b/docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_005_Platform_read_model_boundary_enforcement.md similarity index 64% rename from docs/implplan/SPRINT_20260305_005_Platform_read_model_boundary_enforcement.md rename to docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_005_Platform_read_model_boundary_enforcement.md index f016c4e9f..ec879b8b6 100644 --- a/docs/implplan/SPRINT_20260305_005_Platform_read_model_boundary_enforcement.md +++ b/docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_005_Platform_read_model_boundary_enforcement.md @@ -24,7 +24,7 @@ ## Delivery Tracker ### PLATFORM-BOUND-001 - Produce runtime dependency inventory and classify boundary risks -Status: TODO +Status: DONE Dependency: none Owners: Project Manager, Implementer Task description: @@ -35,11 +35,11 @@ Task description: - Capture inventory output in module docs so future reviewers can validate changes quickly. Completion criteria: -- [ ] Inventory table committed with explicit allowed/prohibited categories. -- [ ] Every cross-module reference in Platform runtime code is justified or queued for remediation. +- [x] Inventory table committed with explicit allowed/prohibited categories. +- [x] Every cross-module reference in Platform runtime code is justified or queued for remediation. ### PLATFORM-BOUND-002 - Add enforceable guard tests for persistence boundary violations -Status: TODO +Status: DONE Dependency: PLATFORM-BOUND-001 Owners: Implementer, Test Automation Task description: @@ -47,12 +47,12 @@ Task description: - Keep migration plugin assembly scanning excluded from runtime boundary assertions by explicit allowlist. Completion criteria: -- [ ] Guard tests fail on introduced boundary violations. -- [ ] Allowlist exceptions are minimal and documented. -- [ ] Test project and commands are documented in sprint evidence. +- [x] Guard tests fail on introduced boundary violations. +- [x] Allowlist exceptions are minimal and documented. +- [x] Test project and commands are documented in sprint evidence. ### PLATFORM-BOUND-003 - Introduce explicit query contract interfaces where boundary is implicit -Status: TODO +Status: DONE Dependency: PLATFORM-BOUND-001 Owners: Implementer Task description: @@ -60,12 +60,12 @@ Task description: - Preserve deterministic ordering and tenant isolation semantics of existing read-model endpoints. Completion criteria: -- [ ] Runtime read-model services depend on explicit contracts rather than ad-hoc persistence internals. -- [ ] Endpoint behavior remains backward-compatible or includes versioned contract notes. -- [ ] Deterministic ordering tests remain green. +- [x] Runtime read-model services depend on explicit contracts rather than ad-hoc persistence internals. +- [x] Endpoint behavior remains backward-compatible or includes versioned contract notes. +- [x] Deterministic ordering tests remain green. ### PLATFORM-BOUND-004 - Document boundary policy and migration/runtime separation -Status: TODO +Status: DONE Dependency: PLATFORM-BOUND-002 Owners: Documentation author, Implementer Task description: @@ -75,19 +75,26 @@ Task description: - runtime read-model dependencies (must stay behind explicit contracts). Completion criteria: -- [ ] `docs/modules/platform/architecture.md` and/or `architecture-overview.md` include boundary policy text and examples. -- [ ] Decision log links to updated docs and guard test evidence. +- [x] `docs/modules/platform/architecture.md` and/or `architecture-overview.md` include boundary policy text and examples. +- [x] Decision log links to updated docs and guard test evidence. ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | | 2026-03-05 | Sprint created to execute architecture Point 4 and prevent Platform cross-module coupling regressions. | Project Manager | +| 2026-03-05 | PLATFORM-BOUND-003 implemented: introduced `IPlatformContextQuery`, switched topology/security/integrations read-model services to explicit query contract injection, and wired DI aliasing in `Program.cs`. | Implementer | +| 2026-03-05 | PLATFORM-BOUND-002 implemented: added `PlatformRuntimeBoundaryGuardTests` to enforce approved read-model constructor contracts and foreign persistence allowlist boundaries. | Implementer | +| 2026-03-05 | Validation run `dotnet test ... --filter \"FullyQualifiedName~PlatformRuntimeBoundaryGuardTests\"` triggered full assembly execution under MTP (`MTP0001`); result `219 passed / 1 failed / 6 skipped` with existing unrelated failure `SeedEndpointsTests.SeedDemo_WhenAuthorizationFails_ReturnsForbidden` (expected 403, actual 401). | Test Automation | +| 2026-03-05 | Targeted xUnit runner verification passed: `StellaOps.Platform.WebService.Tests.exe -class StellaOps.Platform.WebService.Tests.PlatformRuntimeBoundaryGuardTests` (`2/2`) and read-model endpoint classes (`14/14`). | Test Automation | +| 2026-03-05 | `dotnet build src/Platform/StellaOps.Platform.WebService/StellaOps.Platform.WebService.csproj -v minimal -nr:false` succeeded after boundary contract refactor. | Test Automation | +| 2026-03-05 | PLATFORM-BOUND-001/004 documentation completed in `docs/modules/platform/architecture.md` and `docs/modules/platform/platform-service.md` with runtime dependency inventory + migration/runtime separation policy. | Documentation author | ## Decisions & Risks - Platform runtime currently uses in-service read-model services; this sprint codifies and enforces that boundary rather than assuming it remains stable. - `StellaOps.Platform.Database` migration plugins intentionally reference multiple module persistence assemblies; runtime boundary tests must not conflate migration wiring with runtime coupling. - Risk: over-restrictive guards can block valid evolution. - Mitigation: maintain explicit allowlist and update via documented architectural decisions only. +- `dotnet test --filter` remains unreliable under Microsoft.Testing.Platform in this module (`MTP0001` warning). Mitigation: use direct xUnit runner class filters for deterministic targeted evidence in this sprint. ## Next Checkpoints - Dependency inventory reviewed. diff --git a/docs/implplan/SPRINT_20260305_006_DOCS_webservice_catalog_and_domain_consistency.md b/docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_006_DOCS_webservice_catalog_and_domain_consistency.md similarity index 53% rename from docs/implplan/SPRINT_20260305_006_DOCS_webservice_catalog_and_domain_consistency.md rename to docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_006_DOCS_webservice_catalog_and_domain_consistency.md index c4c7c5067..e7f317d21 100644 --- a/docs/implplan/SPRINT_20260305_006_DOCS_webservice_catalog_and_domain_consistency.md +++ b/docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_006_DOCS_webservice_catalog_and_domain_consistency.md @@ -27,7 +27,7 @@ ## Delivery Tracker ### DOCS-SVC-001 - Publish canonical webservice catalog page -Status: TODO +Status: DONE Dependency: none Owners: Documentation author, Project Manager Task description: @@ -40,12 +40,12 @@ Task description: - Mark this catalog as source-of-truth and link it from architecture index pages. Completion criteria: -- [ ] Canonical catalog exists under `docs/technical/architecture/`. -- [ ] `docs/technical/architecture/README.md` links to the catalog. -- [ ] Catalog includes all active webservices, including Remediation. +- [x] Canonical catalog exists under `docs/technical/architecture/`. +- [x] `docs/technical/architecture/README.md` links to the catalog. +- [x] Catalog includes all active webservices, including Remediation. ### DOCS-SVC-002 - Correct stale path and service-name drift in port registry -Status: TODO +Status: DONE Dependency: DOCS-SVC-001 Owners: Documentation author Task description: @@ -53,12 +53,12 @@ Task description: - Add or correct missing service rows where runtime services exist but are absent/inaccurate. Completion criteria: -- [ ] All path references in the port table resolve to existing directories. -- [ ] Service naming/path mapping matches current module consolidation layout. -- [ ] Port registry includes Remediation or documents its absence with explicit rationale and follow-up. +- [x] All path references in the port table resolve to existing directories. +- [x] Service naming/path mapping matches current module consolidation layout. +- [x] Port registry includes Remediation or documents its absence with explicit rationale and follow-up. ### DOCS-SVC-003 - Standardize runtime hostname/domain convention guidance -Status: TODO +Status: DONE Dependency: DOCS-SVC-001 Owners: Documentation author Task description: @@ -67,12 +67,12 @@ Task description: - Preserve intentional schema ID and non-runtime examples where needed, with explicit explanation. Completion criteria: -- [ ] Runtime URL examples are consistent with canonical hostname convention. -- [ ] Exception policy is documented (schema IDs, synthetic examples, external references). -- [ ] Search audit evidence is captured in sprint log. +- [x] Runtime URL examples are consistent with canonical hostname convention. +- [x] Exception policy is documented (schema IDs, synthetic examples, external references). +- [x] Search audit evidence is captured in sprint log. ### DOCS-SVC-004 - Update router rollout inventory and service integration docs -Status: TODO +Status: DONE Dependency: DOCS-SVC-002 Owners: Documentation author, Implementer Task description: @@ -80,12 +80,12 @@ Task description: - Ensure service hostnames and route prefixes align with the canonical service catalog. Completion criteria: -- [ ] `docs/modules/router/webservices-valkey-rollout-matrix.md` is synchronized with active service inventory. -- [ ] Missing Remediation routing status is explicitly tracked. -- [ ] Route ownership and fallback notes are current and actionable. +- [x] `docs/modules/router/webservices-valkey-rollout-matrix.md` is synchronized with active service inventory. +- [x] Missing Remediation routing status is explicitly tracked. +- [x] Route ownership and fallback notes are current and actionable. ### DOCS-SVC-005 - Synchronize consolidation matrix with verified runtime state -Status: TODO +Status: DONE Dependency: DOCS-SVC-001 Owners: Documentation author, Project Manager Task description: @@ -93,12 +93,12 @@ Task description: - Remove contradictory statements between matrix rows and later remediation-status sections. Completion criteria: -- [ ] DB/Persistence column reflects verified runtime wiring. -- [ ] Contradictions are removed and replaced by one clear status statement. -- [ ] Matrix references point to current source file paths. +- [x] DB/Persistence column reflects verified runtime wiring. +- [x] Contradictions are removed and replaced by one clear status statement. +- [x] Matrix references point to current source file paths. ### DOCS-SVC-006 - Add lightweight docs validation for service-path and hostname drift -Status: TODO +Status: DONE Dependency: DOCS-SVC-002 Owners: Test Automation, Documentation author Task description: @@ -108,20 +108,32 @@ Task description: - Integrate check into docs/testing guidance and optionally CI path filters. Completion criteria: -- [ ] Validation command/script is documented and runnable locally. -- [ ] At least one failing fixture/case demonstrates drift detection. -- [ ] Sprint log captures validation command output. +- [x] Validation command/script is documented and runnable locally. +- [x] At least one failing fixture/case demonstrates drift detection. +- [x] Sprint log captures validation command output. ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | | 2026-03-05 | Sprint created to execute documentation improvements and provide an actionable handoff surface for points 1-4. | Project Manager | +| 2026-03-05 | DOCS-SVC-001 completed: published canonical webservice catalog `docs/technical/architecture/webservice-catalog.md` with 31 active `*.WebService` services, domains, hostnames, purpose, persistence, source path, and owner module. | Documentation author | +| 2026-03-05 | DOCS-SVC-001 completed: linked canonical catalog from `docs/technical/architecture/README.md` and marked catalog as source-of-truth for service inventory scope. | Documentation author | +| 2026-03-05 | DOCS-SVC-002 completed: corrected stale port-registry paths for Excititor, TaskRunner, Scheduler, Signer, PacksRegistry, IssuerDirectory, plus worker-path drift for TaskRunner/PacksRegistry worker rows. | Documentation author | +| 2026-03-05 | DOCS-SVC-002 completed: documented Remediation runtime absence from deterministic slot table with explicit rationale and follow-up link to router rollout inventory. | Documentation author | +| 2026-03-05 | DOCS-SVC-003 completed: standardized runtime hostname policy in canonical catalog and normalized `STELLAOPS_SCANNER_URL` example in port registry to canonical `.stella-ops.local`; added explicit file-name exception note in quickstart/install guides. | Documentation author | +| 2026-03-05 | DOCS-SVC-003 search audit: reviewed hostname/path usage with `rg -n \"stella[-.]ops|\\.local|localhost|STELLAOPS_.*_URL\" docs/quickstart.md docs/INSTALL_GUIDE.md docs/technical/architecture/port-registry.md docs/modules/router/webservices-valkey-rollout-matrix.md`. | Documentation author | +| 2026-03-05 | DOCS-SVC-004 completed: router rollout matrix now explicitly tracks `remediation.stella-ops.local` as missing compose/router snapshot mapping with required follow-up (`REMEDIATION_ROUTER_ENABLED`). | Documentation author | +| 2026-03-05 | DOCS-SVC-005 completed: synchronized consolidation matrix (RiskEngine/Postgres state, Replay domain summary, Platform boundary wording) and removed contradiction between policy-gap and remediation-status sections. | Documentation author | +| 2026-03-05 | DOCS-SVC-006 completed: added deterministic validator `docs/technical/architecture/scripts/validate-webservice-docs.ps1` + failing fixture `docs/technical/architecture/fixtures/webservice-docs-invalid-fixture.md`; integrated command docs into `docs/technical/architecture/README.md`. | Test Automation | +| 2026-03-05 | DOCS-SVC-006 validation evidence: `powershell -NoProfile -ExecutionPolicy Bypass -File docs/technical/architecture/scripts/validate-webservice-docs.ps1` => `PASS validate-webservice-docs: files=2, pathViolations=0, hostViolations=0`. | Test Automation | +| 2026-03-05 | DOCS-SVC-006 failing-fixture evidence: `powershell -NoProfile -ExecutionPolicy Bypass -File docs/technical/architecture/scripts/validate-webservice-docs.ps1 -Files docs/technical/architecture/fixtures/webservice-docs-invalid-fixture.md` => expected `FAIL` with unresolved path + legacy hostname violations. | Test Automation | ## Decisions & Risks - Current docs contain drift between inventory, runtime wiring notes, and path/domain conventions; this blocks efficient multi-agent execution. - Canonical catalog and validation checks are required to keep docs synchronized after module consolidation work. - Risk: broad doc edits can unintentionally rewrite historical examples. - Mitigation: document exception policy and scope normalization to runtime/service-discovery contexts first. +- Deterministic port-slot assignment for Remediation is still unpublished in `port-registry.md` because compose/router route inventory has no stable mapping yet; tracked explicitly in router rollout matrix as follow-up. ## Next Checkpoints - Canonical service catalog draft completed and linked. diff --git a/docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_009_Router_header_search_route_alignment.md b/docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_009_Router_header_search_route_alignment.md new file mode 100644 index 000000000..13472a08e --- /dev/null +++ b/docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_009_Router_header_search_route_alignment.md @@ -0,0 +1,56 @@ +# Sprint 20260305_009 - Header Search Route Alignment + +## Topic & Scope +- Restore header unified-search functionality by fixing Router gateway route translation for AdvisoryAI search APIs. +- Eliminate routing drift between runtime (`appsettings.json`) and compose route tables. +- Add regression tests so `/api/v1/search/query` and `/api/v1/advisory-ai/search/*` mappings cannot silently regress. +- Working directory: `src/Router/StellaOps.Gateway.WebService`. +- Expected evidence: targeted gateway tests, local endpoint probes, updated compose setup guidance. + +## Dependencies & Concurrency +- Depends on existing AdvisoryAI endpoint contracts under `/v1/search/*` and `/v1/advisory-ai/search/*`. +- Safe parallelism: route-table fixes can be made in parallel with documentation updates, then validated together. +- Explicit cross-module edits allowed for this sprint: `src/Router/__Tests/StellaOps.Gateway.WebService.Tests`, `devops/compose/*`, and `docs/implplan/*`. + +## Documentation Prerequisites +- `docs/README.md` +- `docs/ARCHITECTURE_REFERENCE.md` +- `docs/modules/platform/architecture-overview.md` +- `docs/modules/router/architecture.md` +- `docs/07_HIGH_LEVEL_ARCHITECTURE.md` + +## Delivery Tracker + +### RGH-03 - Fix header search routing and lock with setup/test coverage +Status: DONE +Dependency: none +Owners: Developer / Implementer, Test Automation +Task description: +- Align gateway route tables so UI calls to `/api/v1/search/query` and `/api/v1/advisory-ai/search/*` reach AdvisoryAI `/v1/...` endpoints instead of falling through to Platform `/api` routes. +- Ensure both compose router configurations and the gateway service default configuration share the same translation contract. +- Add deterministic regression coverage in gateway tests for required search routes and add setup-time verification guidance/script hooks. + +Completion criteria: +- [x] Gateway route tables map `/api/v1/search/query` to `http://advisoryai.stella-ops.local/v1/search/query` and map `/api/v1/advisory-ai` to `http://advisoryai.stella-ops.local/v1/advisory-ai`. +- [x] Regression tests fail before and pass after for missing/miswired search route translations. +- [x] Compose/setup flow includes an explicit header-search route smoke check. +- [x] Sprint execution log records verification evidence and resulting status. + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-03-05 | Sprint created; root cause confirmed as gateway route translation mismatch for unified search endpoints. | Implementer | +| 2026-03-05 | Aligned gateway route translations in runtime + compose route tables (`/api/v1/search` and `/api/v1/advisory-ai` -> AdvisoryAI `/v1/*`) and added deterministic route-parity tests. | Implementer | +| 2026-03-05 | Verified gateway test project (`StellaOps.Gateway.WebService.Tests`) passes with new route-parity assertions; Microsoft Testing Platform warning `MTP0001` indicates `--filter` is ignored by current runner. | Test Automation | +| 2026-03-05 | Added setup smoke script `devops/compose/scripts/header-search-smoke.ps1`, integrated it into `router-mode-redeploy.ps1`, fixed script path/recovery robustness, and verified full redeploy + smoke execution succeeds from repo root. | Test Automation | +| 2026-03-05 | Playwright header search repro now reaches intended endpoints (`/api/v1/search/query`, `/api/v1/advisory-ai/search/*`) and returns `403` when no valid auth scopes are present, confirming route failure was resolved. | QA | + +## Decisions & Risks +- Risk: `src/Router/StellaOps.Gateway.WebService/AGENTS.md` references `docs/modules/gateway/architecture.md` and `docs/modules/gateway/openapi.md`, but these files are absent. Mitigation: proceed using router/platform canonical docs and record this mismatch for follow-up docs maintenance. +- Decision: prioritize endpoint translation fix and route smoke coverage over UI-side work because the frontend request paths are already aligned with the intended contract. +- Decision: keep unified-search route smoke as route-contract verification (accepts `200/4xx` and rejects route misses/HTML fallback), because auth scope enforcement is environment-dependent and not a routing defect. + +## Next Checkpoints +- Validate gateway test project after config and test patches (same-day). +- Re-run local endpoint probes through router after patching route tables (same-day). +- Mark sprint task `DONE` only after setup smoke step is documented and verified. diff --git a/docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_010_AdvisoryAI_header_search_stability.md b/docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_010_AdvisoryAI_header_search_stability.md new file mode 100644 index 000000000..e2af0ac38 --- /dev/null +++ b/docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_010_AdvisoryAI_header_search_stability.md @@ -0,0 +1,55 @@ +# Sprint 20260305_010 - Header Search Stability On stella-ops.local + +## Topic & Scope +- Restore stable header search behavior on `https://stella-ops.local` with end-to-end browser verification. +- Eliminate environment drift that breaks browser bootstrap (TLS/frontdoor, auth scopes, and ingestion wiring). +- Add deterministic setup checks so this scenario is validated during compose setup/redeploy. +- Working directory: `src/AdvisoryAI`. +- Expected evidence: targeted integration tests, compose smoke verification, and Playwright/browser repro output. + +## Dependencies & Concurrency +- Depends on existing router route alignment in `SPRINT_20260305_009_Router_header_search_route_alignment.md`. +- Safe parallelism: AdvisoryAI adapter fixes and Web fixture updates can be developed in parallel and validated together. +- Explicit cross-module edits allowed for this sprint: `devops/compose/*`, `src/Web/StellaOps.Web/*`, `src/Router/*`, and `docs/implplan/*`. + +## Documentation Prerequisites +- `docs/README.md` +- `docs/07_HIGH_LEVEL_ARCHITECTURE.md` +- `docs/modules/platform/architecture-overview.md` +- `docs/modules/advisory-ai/architecture.md` +- `docs/modules/router/architecture.md` + +## Delivery Tracker + +### AHS-01 - Stabilize header search bootstrap, auth, and ingestion path +Status: DONE +Dependency: none +Owners: Developer / Implementer, QA, Test Automation +Task description: +- Diagnose and fix the root cause for browser load instability on `https://stella-ops.local`, then verify header search end-to-end in the same host/path. +- Fix remaining AdvisoryAI header-search blockers (scope fixture drift, adapter endpoint mismatch, and setup-time ingestion/config readiness gaps). +- Add deterministic setup-time checks that validate route + ingestion + query flow to prevent recurrence. + +Completion criteria: +- [x] `https://stella-ops.local` loads reliably without bootstrap retry loops/high CPU behavior. +- [x] Header search returns deterministic results in browser and no longer fails due setup/config drift. +- [x] Compose setup/redeploy includes a smoke path that detects this scenario before manual UI testing. +- [x] Targeted tests cover adapter/fixture contracts and pass. + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-03-05 | Sprint created and set to DOING; investigation started from `stella-ops.local` load failure, header search auth drift, and ingestion endpoint mismatch. | Implementer | +| 2026-03-05 | Reproduced browser lock at 100% script CPU; captured debugger stack in Angular Router redirect processing and identified invalid setup redirect target (`/setup/wizard`) for current route tree. | Implementer | +| 2026-03-05 | Fixed guard/deep-link route targets to `/setup-wizard/wizard`, kept setup ingestion smoke checks, and verified `header-search-smoke.ps1` passes including UI responsiveness probe. | Implementer | +| 2026-03-05 | Ran targeted frontend tests: `config.guard.spec.ts` and `doctor-wizard-mapping.spec.ts` (19/19 pass). | Test Automation | + +## Decisions & Risks +- Risk: existing workspace has broad unrelated local changes; this sprint must keep edits minimal and scoped to header-search stability. +- Decision: prioritize `stella-ops.local` bootstrap reliability first because unstable frontdoor TLS blocks reliable Playwright/UI evidence. +- Decision: treat setup route correctness as part of header-search stability because misrouted setup redirects can starve the router event loop and block all topbar interactions. + +## Next Checkpoints +- Confirm frontdoor bootstrap fix on `https://stella-ops.local`. +- Re-run header search browser repro and direct API probes. +- Finalize setup smoke coverage and mark task DONE only after evidence is recorded. diff --git a/docs-archived/implplan/SPRINT_20260305_007_FE_visual_qa_fixes_and_global_menu_grouping.md b/docs-archived/implplan/SPRINT_20260305_007_FE_visual_qa_fixes_and_global_menu_grouping.md new file mode 100644 index 000000000..4d9f335aa --- /dev/null +++ b/docs-archived/implplan/SPRINT_20260305_007_FE_visual_qa_fixes_and_global_menu_grouping.md @@ -0,0 +1,144 @@ +# Sprint 20260305-007 - FE Visual QA Fixes and Global Menu Grouping + +## Topic & Scope +- Address QA-identified visual defects across Setup, Security Findings, Releases, Ops, and shell layout. +- Implement mobile-first responsiveness fixes where content currently clips or hides key controls. +- Introduce grouped global navigation menus for faster scanning and reduced cognitive load. +- Improve readability through contrast and hierarchy adjustments without changing domain behavior. +- Working directory: `src/Web/StellaOps.Web`. +- Expected evidence: updated Angular components/styles, responsive screenshots, build/test output, and linked docs updates. + +## Dependencies & Concurrency +- Depends on current route shell and IA baseline in `src/Web/StellaOps.Web/src/app/layout/**` and `src/Web/StellaOps.Web/src/app/routes/**`. +- Can run in parallel with non-UI backend sprints because scope is frontend-only. +- Coordinate with any concurrent FE route/IA work to avoid collisions in shell/sidebar components. + +## Documentation Prerequisites +- `docs/qa/feature-checks/FLOW.md` +- `docs/code-of-conduct/TESTING_PRACTICES.md` +- `docs/modules/ui/information-architecture.md` +- `docs/ui-analysis/01_SHELL_AND_NAVIGATION.md` +- `docs/ui-analysis/04_ADMIN_CONFIG_RELEASE_EVIDENCE_SCREENS.md` +- `src/Web/StellaOps.Web/AGENTS.md` + +## Delivery Tracker + +### FE-VIS-001 - Fix Findings Explorer mobile clipping and pane overflow +Status: DONE +Dependency: none +Owners: Developer / Implementer, QA +Task description: +- Replace fixed-width compare panes with responsive behavior for tablet/mobile. +- Ensure category, change list, and evidence panels remain reachable without horizontal clipping. +- Preserve desktop tri-pane workflow while introducing mobile-safe stacking behavior. + +Completion criteria: +- [x] `/security/findings` renders without clipped controls on mobile viewport. +- [x] No horizontal viewport overflow in compare panes at 390px width. +- [x] Desktop layout remains functionally unchanged. + +### FE-VIS-002 - Make Releases deployments list mobile-readable +Status: DONE +Dependency: FE-VIS-001 +Owners: Developer / Implementer, QA +Task description: +- Replace hard-clipped table container behavior with mobile-safe overflow and/or compact row presentation. +- Preserve quick actions (`View`) and status visibility on narrow viewports. + +Completion criteria: +- [x] `/releases/deployments` keeps status and action affordances visible on mobile. +- [x] Horizontal clipping is removed for small screens. +- [x] Desktop table fidelity remains intact. + +### FE-VIS-003 - Compact topbar context controls for mobile +Status: DONE +Dependency: FE-VIS-001 +Owners: Developer / Implementer, UX Specialist +Task description: +- Reduce topbar congestion on phones by compacting secondary controls and context chips. +- Keep access to region/env/window/stage controls while improving first-row readability. + +Completion criteria: +- [x] Topbar no longer appears crowded at 390px width. +- [x] Context controls remain discoverable and keyboard accessible. +- [x] Search and user menu remain consistently usable on mobile. + +### FE-VIS-004 - Add grouping for global navigation menus +Status: DONE +Dependency: FE-VIS-001 +Owners: Developer / Implementer, UX Specialist +Task description: +- Introduce explicit global menu grouping in the sidebar to organize major navigation domains. +- Improve scanability of long menu lists (desktop and mobile drawer states). + +Completion criteria: +- [x] Sidebar renders grouped menu sections with clear visual boundaries. +- [x] Grouping remains accessible and does not break route activation styling. +- [x] Mobile drawer menu remains readable and navigable after grouping changes. + +### FE-VIS-005 - Improve Setup and Ops overview page hierarchy +Status: DONE +Dependency: FE-VIS-004 +Owners: Developer / Implementer, UX Specialist +Task description: +- Strengthen information hierarchy on `/setup` and `/ops` pages to reduce dead-space feel. +- Add visual structure for primary actions, secondary drilldowns, and status-oriented framing. + +Completion criteria: +- [x] Setup and Ops overview screens present clear primary/secondary action hierarchy. +- [x] Empty vertical areas are reduced without adding placeholder noise. +- [x] Updated layouts are responsive on desktop and mobile. + +### FE-VIS-006 - Adjust contrast tokens for readability +Status: DONE +Dependency: FE-VIS-005 +Owners: Developer / Implementer, QA +Task description: +- Tune text/border contrast tokens used by key overview and table surfaces. +- Keep Stella visual identity while improving legibility for secondary text and dividers. + +Completion criteria: +- [x] Secondary and muted text is visibly more legible on light surfaces. +- [x] Core border separators are easier to distinguish. +- [x] No theme regressions in dark-mode token definitions. + +### FE-VIS-007 - Documentation and QA evidence sync +Status: DONE +Dependency: FE-VIS-002 +Owners: Documentation author, QA, Project Manager +Task description: +- Update relevant UI architecture/analysis docs to reflect new menu grouping and responsive behavior. +- Capture fresh Playwright screenshots for the corrected pages. +- Record commands and outcomes in sprint execution log. + +Completion criteria: +- [x] Updated docs under `docs/modules/ui/` and/or `docs/ui-analysis/`. +- [x] New screenshot evidence captured for desktop and mobile key pages. +- [x] Sprint Execution Log includes verification commands and outcomes. + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-03-05 | Sprint created from QA visual review findings; FE-VIS-001 started. | Project Manager | +| 2026-03-05 | Implemented first pass: responsive Findings panes, Releases table horizontal overflow, mobile topbar context toggle, grouped sidebar global menus, Setup/Ops hierarchy refresh, and contrast token tuning. | Developer / Implementer | +| 2026-03-05 | Validation run: `npm run build` in `src/Web/StellaOps.Web` completed successfully (existing bundle budget warnings remain). | QA | +| 2026-03-05 | Updated UI IA documentation with grouped global menus and mobile context control behavior note. | Documentation author | +| 2026-03-05 | Updated `tests/e2e/nav-shell.spec.ts` expected labels for grouped global navigation and passed targeted Playwright checks for root-label presence and deprecated-label exclusion. | QA | +| 2026-03-05 | Captured fresh desktop/mobile screenshot evidence to `output/playwright/qa-visual-review-20260305/`; identified and fixed remaining mobile Findings toolbar collision by making compare toolbar auto-height. | QA | +| 2026-03-05 | Post-fix verification passed: `npm run build` and targeted Playwright nav shell checks (`sidebar renders all canonical root labels`, `sidebar excludes deprecated root labels`). | QA | +| 2026-03-05 | Mobile overflow verification for `/security/findings` at 390px via Playwright probe returned `{\"docW\":390,\"docC\":390,\"bodyW\":374,\"bodyC\":374}` (no horizontal overflow). | QA | + +## Decisions & Risks +- Decision: prioritize mobile clipping and global navigation grouping before broader aesthetic refinements. +- Risk: shell/sidebar edits may conflict with concurrent IA work. +- Mitigation: keep changes scoped to `src/Web/StellaOps.Web` and verify route activation behavior after grouping. +- Risk: token-level contrast changes may cause unintentional theme drift. +- Mitigation: constrain token edits to readability deltas and validate both light and dark token sets. +- Decision: mobile topbar now exposes secondary context controls through an explicit `Context` toggle to reduce primary-row crowding. +- Docs sync: IA update recorded at `docs/modules/ui/information-architecture.md` (`2026-03-05 Shell IA Update` section). +- Evidence location: `output/playwright/qa-visual-review-20260305/` (desktop/mobile captures for Setup, Ops, Security Findings, Releases Deployments). + +## Next Checkpoints +- FE-VIS-001 and FE-VIS-002 merged with before/after screenshots. +- FE-VIS-004 grouped global menu behavior validated on desktop and mobile. +- FE-VIS-007 docs/evidence sync completed and linked from Decisions & Risks. diff --git a/docs-archived/implplan/SPRINT_20260305_008_FE_sidebar_grouping_ux_regression_fix.md b/docs-archived/implplan/SPRINT_20260305_008_FE_sidebar_grouping_ux_regression_fix.md new file mode 100644 index 000000000..23f7f0123 --- /dev/null +++ b/docs-archived/implplan/SPRINT_20260305_008_FE_sidebar_grouping_ux_regression_fix.md @@ -0,0 +1,79 @@ +# Sprint 20260305-008 - FE Sidebar Grouping UX Regression Fix + +## Topic & Scope +- Fix sidebar grouping regressions reported after the global menu grouping rollout. +- Remove duplicated group/section labeling and reduce excessive spacing in grouped menus. +- Restore predictable click behavior for grouped navigation entries. +- Working directory: `src/Web/StellaOps.Web`. +- Expected evidence: updated sidebar component, Playwright nav checks, and sprint execution logs. + +## Dependencies & Concurrency +- Depends on grouped sidebar implementation in `src/Web/StellaOps.Web/src/app/layout/app-sidebar/**`. +- Safe to run in parallel with backend sprints; frontend-shell only. + +## Documentation Prerequisites +- `docs/qa/feature-checks/FLOW.md` +- `docs/code-of-conduct/TESTING_PRACTICES.md` +- `docs/modules/ui/information-architecture.md` +- `src/Web/StellaOps.Web/AGENTS.md` + +## Delivery Tracker + +### FE-NAV-001 - Remove duplicated menu labeling and tighten group spacing +Status: DONE +Dependency: none +Owners: Developer / Implementer, UX Specialist +Task description: +- Refactor grouped sidebar rendering to avoid duplicate semantic labels for the same navigation section. +- Remove or simplify decorative rails that create visual repetition and dead spacing. +- Compress grouped block spacing to improve scanability on desktop and mobile drawers. + +Completion criteria: +- [x] Group labels are rendered once per group and not repeated as decorative rails. +- [x] Group spacing is compact and no large dead-space cards remain. +- [x] Sidebar remains readable in both desktop and mobile drawer layouts. + +### FE-NAV-002 - Preserve section visibility and group integrity under scope filtering +Status: DONE +Dependency: FE-NAV-001 +Owners: Developer / Implementer +Task description: +- Adjust scope-filter logic so parent sections do not disappear when child links are filtered out. +- Ensure group composition remains deterministic and does not hide expected top-level navigation entries. + +Completion criteria: +- [x] Parent sections remain visible when permitted even if all scoped children are hidden. +- [x] Groups do not disappear unexpectedly due to child filtering. +- [x] Existing scope restrictions still apply to hidden child links. + +### FE-NAV-003 - Validate grouped navigation click behavior +Status: DONE +Dependency: FE-NAV-002 +Owners: QA, Developer / Implementer +Task description: +- Add/adjust Playwright nav-shell checks that verify grouped entries navigate when clicked. +- Re-run targeted nav-shell tests to confirm no regression in grouped labels and route transitions. + +Completion criteria: +- [x] Clicking grouped root navigation entries triggers route changes. +- [x] Existing grouped-label tests remain passing. +- [x] Sprint execution log captures commands and outcomes. + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-03-05 | Sprint created from user-reported grouped-sidebar UX regressions; FE-NAV-001 started. | Project Manager | +| 2026-03-05 | Refactored sidebar grouped template: removed duplicate rail labels, reduced group spacing, and made group headers route to group landing pages. | Developer / Implementer | +| 2026-03-05 | Updated scope filtering to keep parent sections visible when child links are filtered out by scope. | Developer / Implementer | +| 2026-03-05 | Added Playwright nav-shell checks for unique clickable group headers and grouped root-item click navigation. | QA | +| 2026-03-05 | Verification passed: `npx playwright test tests/e2e/nav-shell.spec.ts -g "group headers are unique and navigate to group landing routes|grouped root entries navigate when clicked|sidebar renders all canonical root labels|sidebar excludes deprecated root labels"` and `npm run build`. | QA | + +## Decisions & Risks +- Decision: prioritize structural nav usability fixes (labels, spacing, clicks) before further visual polish. +- Risk: sidebar template changes can affect activation styling. +- Mitigation: keep route bindings unchanged and verify with Playwright route-navigation checks. +- Decision: group headers are now actionable links to group landing routes (`Release Control` -> `/mission-control/board`, `Security & Evidence` -> `/security`, `Platform & Setup` -> `/ops/operations`). + +## Next Checkpoints +- FE-NAV-001 through FE-NAV-003 completed with verification output. +- Sidebar desktop and mobile behavior validated by targeted Playwright tests. diff --git a/docs/INSTALL_GUIDE.md b/docs/INSTALL_GUIDE.md index 3c452e87c..df64d1c1c 100755 --- a/docs/INSTALL_GUIDE.md +++ b/docs/INSTALL_GUIDE.md @@ -74,6 +74,8 @@ The example file ships with working local-dev defaults. For production, change ` 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: +Runtime URL convention remains `*.stella-ops.local`; `hosts.stellaops.local` is the template file name only. + - **Windows:** `C:\Windows\System32\drivers\etc\hosts` (run editor as Administrator) - **Linux / macOS:** `sudo sh -c 'cat devops/compose/hosts.stellaops.local >> /etc/hosts'` diff --git a/docs/implplan/CONSOLIDATION_WEBSERVICE_FUNCTION_DB_MATRIX_20260305.md b/docs/implplan/CONSOLIDATION_WEBSERVICE_FUNCTION_DB_MATRIX_20260305.md index c7acaf93f..b5d44e03f 100644 --- a/docs/implplan/CONSOLIDATION_WEBSERVICE_FUNCTION_DB_MATRIX_20260305.md +++ b/docs/implplan/CONSOLIDATION_WEBSERVICE_FUNCTION_DB_MATRIX_20260305.md @@ -16,15 +16,15 @@ | Doctor | 1 | Doctor | in-memory | | EvidenceLocker | 1 | EvidenceLocker | postgres | | ExportCenter | 1 | ExportCenter | postgres | -| Findings | 2 | Findings.Ledger, RiskEngine | in-memory, postgres | +| Findings | 2 | Findings.Ledger, RiskEngine | postgres (in-memory fallback in RiskEngine test profile) | | Integrations | 1 | Integrations | postgres | -| JobEngine | 4 | JobEngine, PacksRegistry, Scheduler, TaskRunner | file-backed, postgres | +| JobEngine | 4 | JobEngine, PacksRegistry, Scheduler, TaskRunner | postgres, seed-fs object-store | | Notifier | 1 | Notifier | postgres | | Notify | 1 | Notify | postgres | | Platform | 1 | Platform | postgres | | ReachGraph | 1 | ReachGraph | postgres | | Remediation | 1 | Remediation | postgres | -| Replay | 1 | Replay | in-memory | +| Replay | 1 | Replay | postgres, seed-fs object-store | | Router | 1 | Gateway | no-persistence | | Scanner | 1 | Scanner | postgres | | Timeline | 2 | Timeline, TimelineIndexer | postgres | @@ -47,18 +47,18 @@ | EvidenceLocker | EvidenceLocker | Evidence ingest/scoring, snapshots, bundle download/portable package, verify, legal hold, plus export/verdict/evidence-thread adapters | EvidenceLockerDbContext | src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.WebService/Program.cs; src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Infrastructure/EfCore/Context/EvidenceLockerDbContext.cs | | ExportCenter | ExportCenter | Endpoints: Attestation, AuditBundle, ExceptionReport, ExportApi (+6 more); routes: audit-bundles, exports, incidents, lineage (+4 more) | ExportCenterDbContext | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Program.cs; src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/EfCore/Context/ExportCenterDbContext.cs | | Findings | Findings.Ledger | Endpoints: Backport, EvidenceGraph, FindingSummary, ReachabilityMap (+4 more); routes: findings, scoring | FindingsLedgerDbContext | src/Findings/StellaOps.Findings.Ledger.WebService/Program.cs; src/Findings/StellaOps.Findings.Ledger/EfCore/Context/FindingsLedgerDbContext.cs | -| Findings | RiskEngine | Endpoints: ExploitMaturity; routes: exploit-maturity | No service DB; InMemoryRiskScoreResultStore | src/Findings/StellaOps.RiskEngine.WebService/Program.cs; src/Findings/__Libraries/StellaOps.RiskEngine.Infrastructure/Stores/InMemoryRiskScoreResultStore.cs | +| Findings | RiskEngine | Endpoints: ExploitMaturity; routes: exploit-maturity | PostgresRiskScoreResultStore (in-memory fallback retained for explicit test profile) | src/Findings/StellaOps.RiskEngine.WebService/Program.cs; src/Findings/__Libraries/StellaOps.RiskEngine.Infrastructure/Stores/PostgresRiskScoreResultStore.cs | | Integrations | Integrations | Endpoints: Integration; routes: integrations | IntegrationDbContext | src/Integrations/StellaOps.Integrations.WebService/Program.cs; src/Integrations/__Libraries/StellaOps.Integrations.Persistence/IntegrationDbContext.cs | | JobEngine | JobEngine | Endpoints: Approval, Audit, CircuitBreaker, Dag (+21 more); routes: approvals, environments, jobengine, metrics (+2 more) | JobEngineDbContext | src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.WebService/Program.cs; src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Infrastructure/EfCore/Context/JobEngineDbContext.cs | -| JobEngine | PacksRegistry | Packs upload/list/content/provenance/manifest/signature, attestations, parity/lifecycle, mirrors sync, compliance summary, offline-seed export | No relational DB; filesystem repositories (packs/parity/lifecycle/audit/attestations/mirrors) | src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.WebService/Program.cs; src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.Infrastructure/FileSystem/FilePackRepository.cs | +| JobEngine | PacksRegistry | Packs upload/list/content/provenance/manifest/signature, attestations, parity/lifecycle, mirrors sync, compliance summary, offline-seed export | Postgres metadata/state repositories + seed-fs blob channel (`SeedFsPacksRegistryBlobStore`) | src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.WebService/Program.cs; src/JobEngine/StellaOps.PacksRegistry.__Libraries/StellaOps.PacksRegistry.Persistence/Postgres/BlobStorage/SeedFsPacksRegistryBlobStore.cs | | JobEngine | Scheduler | Endpoints: FailureSignature, Run, Schedule; routes: events, graphs, scheduler | SchedulerDataSource, SchedulerDbContext | src/JobEngine/StellaOps.Scheduler.WebService/Program.cs; src/JobEngine/StellaOps.Scheduler.__Libraries/StellaOps.Scheduler.Persistence/EfCore/Context/SchedulerDbContext.cs | -| JobEngine | TaskRunner | Run simulation/execution state/logs/artifacts/approvals/cancel, attestation APIs, incident-mode APIs, SLO breach webhook | No relational DB; filesystem stores for run state/logs/approvals/artifacts | src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.WebService/Program.cs; src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.Infrastructure/Execution/FilePackRunStateStore.cs | +| JobEngine | TaskRunner | Run simulation/execution state/logs/artifacts/approvals/cancel, attestation APIs, incident-mode APIs, SLO breach webhook | Postgres run state/log/approval stores + seed-fs artifact/provenance payload channel | src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.WebService/Program.cs; src/JobEngine/StellaOps.TaskRunner.__Libraries/StellaOps.TaskRunner.Persistence/Postgres/Repositories/PostgresPackRunStateStore.cs | | Notifier | Notifier | Endpoints: Escalation, Fallback, Incident, Localization (+10 more); routes: ack, escalation-policies, escalations, fallback (+13 more) | NotifyDataSource, NotifyDbContext | src/Notifier/StellaOps.Notifier/StellaOps.Notifier.WebService/Program.cs; src/Notify/__Libraries/StellaOps.Notify.Persistence/EfCore/Context/NotifyDbContext.cs | | Notify | Notify | Rules/channels/templates CRUD, deliveries history, digests, audit trail, lock APIs, internal normalize endpoints | NotifyDataSource, NotifyDbContext | src/Notify/StellaOps.Notify.WebService/Program.cs; src/Notify/__Libraries/StellaOps.Notify.Persistence/EfCore/Context/NotifyDbContext.cs | -| Platform | Platform | Endpoints: AdministrationTrustSigningMutation, Analytics, Context, EnvironmentSettings (+19 more); routes: admin, administration, analytics, authority (+26 more) | PlatformDbContext plus read-model access to Authority/Concelier/Excititor/Scheduler/Notify/Policy contexts | src/Platform/StellaOps.Platform.WebService/Program.cs; src/Authority/__Libraries/StellaOps.Authority.Persistence/EfCore/Context/AuthorityDbContext.cs | +| Platform | Platform | Endpoints: AdministrationTrustSigningMutation, Analytics, Context, EnvironmentSettings (+19 more); routes: admin, administration, analytics, authority (+26 more) | PlatformDbContext + module-local runtime contracts (`IReleaseControlBundleStore`, `IPlatformContextQuery`); migration-only foreign persistence references are allowlisted | src/Platform/StellaOps.Platform.WebService/Program.cs; src/Platform/StellaOps.Platform.WebService/Services/PlatformContextService.cs | | ReachGraph | ReachGraph | Endpoints: CveMapping, Reachability, ReachGraph | ReachGraphDataSource, ReachGraphDbContext | src/ReachGraph/StellaOps.ReachGraph.WebService/Program.cs; src/__Libraries/StellaOps.ReachGraph.Persistence/EfCore/Context/ReachGraphDbContext.cs | | Remediation | Remediation | Endpoints: RemediationMatch, RemediationRegistry, RemediationSource; routes: remediation | RemediationDataSource, RemediationDbContext | src/Remediation/StellaOps.Remediation.WebService/Program.cs; src/Remediation/StellaOps.Remediation.Persistence/EfCore/Context/RemediationDbContext.cs | -| Replay | Replay | Endpoints: PointInTimeQuery, VerdictReplay; routes: pit, replay | No service DB; in-memory feed snapshot blob/index stores | src/Replay/StellaOps.Replay.WebService/Program.cs; src/Replay/StellaOps.Replay.WebService/FeedSnapshotSupport.cs | +| Replay | Replay | Endpoints: PointInTimeQuery, VerdictReplay; routes: pit, replay | Postgres snapshot index store + seed-fs snapshot blob store | src/Replay/StellaOps.Replay.WebService/Program.cs; src/Replay/StellaOps.Replay.WebService/FeedSnapshotSupport.cs | | Router | Gateway | Gateway route dispatch pipeline, authz/header enforcement, transport routing, OpenAPI aggregation | No application DB; gateway routing/middleware service | src/Router/StellaOps.Gateway.WebService/Program.cs | | Scanner | Scanner | Endpoints: Actionables, Approval, Baseline, BatchTriage (+43 more); routes: drift, epss, github, hot-lookup (+12 more) | ScannerDbContext + ScannerSourcesDataSource + TriageDbContext (+ AuthorityDbContext path) | src/Scanner/StellaOps.Scanner.WebService/Program.cs; src/Authority/__Libraries/StellaOps.Authority.Persistence/EfCore/Context/AuthorityDbContext.cs | | Timeline | Timeline | Endpoints: Export, Health, Replay, Timeline (+1 more); routes: audit, timeline | EventingDataSource, EventingDbContext, TimelineCoreDataSource, TimelineCoreDbContext | src/Timeline/StellaOps.Timeline.WebService/Program.cs; src/__Libraries/StellaOps.Eventing/EfCore/Context/EventingDbContext.cs | @@ -76,10 +76,10 @@ ## Policy Gaps (Postgres First, RustFS for Blobs) | Service | Current Runtime Wiring | Compose Signal | Gap | Required Remediation | | --- | --- | --- | --- | --- | -| PacksRegistry | File repositories (`src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.WebService/Program.cs` lines 29-34) | Main compose provides `ConnectionStrings__Default` (line 1769); testing compose expects `PACKSREGISTRY__STORAGE__DRIVER=postgres` (line 253) | High | Add storage driver contract; move metadata (pack/parity/lifecycle/mirror/audit) to Postgres; keep pack/provenance/attestation payloads in RustFS/seed-fs blob path. | -| TaskRunner | File stores/readers (`src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.WebService/Program.cs` lines 61,66,71,76) | Main compose provides `ConnectionStrings__Default` (line 1150); testing compose expects `TASKRUNNER__STORAGE__DRIVER=postgres` (line 271) | High | Add Postgres storage driver for run state/logs/approvals; move large artifacts to RustFS/seed-fs blob path; keep deterministic replay semantics. | -| RiskEngine | In-memory result store (`src/Findings/StellaOps.RiskEngine.WebService/Program.cs` line 21) | Main compose provides `ConnectionStrings__Default` (line 1048) | Medium-High | Implement Postgres-backed result store with deterministic ordering/query semantics; keep in-memory only for explicit test profile. | -| Replay | In-memory snapshot blob/index stores (`src/Replay/StellaOps.Replay.WebService/Program.cs` lines 61-62) | Main compose provides `ConnectionStrings__Default` (line 2037) | Medium-High | Persist replay snapshot index/state in Postgres; move snapshot blobs to RustFS/seed-fs object path. | +| PacksRegistry | `Storage:Driver=postgres` plus Postgres repositories for metadata/state; `Storage:ObjectStore:Driver=seed-fs` for blob payloads; startup rejects `rustfs` and unknown object-store drivers. | Main/testing compose provide Postgres connection and service storage-driver keys. | Closed (Sprint 312 + 2026-03-05 hardening) | Keep contract seed-fs-only until a dedicated RustFS adapter sprint lands with parity tests. | +| TaskRunner | `Storage:Driver=postgres` plus Postgres repositories for state/log/approval; `Storage:ObjectStore:Driver=seed-fs` for artifact payloads; startup rejects `rustfs` and unknown object-store drivers in WebService and Worker. | Main/testing compose provide Postgres connection and service storage-driver keys. | Closed (Sprint 312 + 2026-03-05 hardening) | Keep contract seed-fs-only until a dedicated RustFS adapter sprint lands with parity tests. | +| RiskEngine | Postgres-backed result store (`PostgresRiskScoreResultStore`) with explicit in-memory fallback for tests. | Main compose provides `ConnectionStrings__Default` (line 1048). | Closed (Sprint 312) | Keep in-memory fallback scoped to explicit test profile only; maintain parity tests for Postgres path. | +| Replay | `Storage:Driver=postgres` for snapshot index and `Storage:ObjectStore:Driver=seed-fs` for snapshot blobs; startup rejects `rustfs` and unknown object-store values. | Main compose provides `ConnectionStrings__Default` and storage driver keys for replay. | Closed (Sprint 312 + 2026-03-05 hardening) | Keep contract seed-fs-only until a dedicated RustFS adapter sprint lands with deterministic parity tests. | | OpsMemory | Postgres store exists but connection key is `ConnectionStrings:OpsMemory` with localhost fallback (`src/AdvisoryAI/StellaOps.OpsMemory.WebService/Program.cs` lines 19-20) | Main compose sets only `ConnectionStrings__Default` (line 1537) | Medium | Accept `ConnectionStrings:Default` as primary fallback or map explicit `ConnectionStrings:OpsMemory` in compose; remove localhost fallback in non-dev runtime. | | Scanner | Postgres + RustFS split already configured (`src/Scanner` + compose lines 652-659/720-725) | Explicitly aligned in compose | None | Use as reference implementation for storage-driver conventions. | diff --git a/docs/implplan/SPRINT_20260305_002_JobEngine_packsregistry_taskrunner_storage_completion.md b/docs/implplan/SPRINT_20260305_002_JobEngine_packsregistry_taskrunner_storage_completion.md deleted file mode 100644 index 298fab95d..000000000 --- a/docs/implplan/SPRINT_20260305_002_JobEngine_packsregistry_taskrunner_storage_completion.md +++ /dev/null @@ -1,104 +0,0 @@ -# Sprint 20260305-002 - JobEngine Storage Completion (PacksRegistry and TaskRunner) - -## Topic & Scope -- Complete the remaining delivery gap for Point 1: Postgres-first metadata/state with production-ready object-store blob handling for `PacksRegistry` and `TaskRunner`. -- Preserve deterministic replay semantics while removing non-dev ambiguity in storage-driver behavior. -- Align runtime wiring, compose overlays, and tests so storage mode is explicit and verifiable. -- Working directory: `src/JobEngine`. -- Expected evidence: targeted persistence/integration test passes, compose config validation output, and updated JobEngine/platform architecture docs. - -## Dependencies & Concurrency -- Depends on shared storage contract documented in `docs/modules/platform/architecture.md`. -- Can run in parallel with Replay, Remediation, and Platform boundary sprints. -- Documentation cleanup sprint (`SPRINT_20260305_006_DOCS_webservice_catalog_and_domain_consistency.md`) depends on final runtime behavior from this sprint. - -## Documentation Prerequisites -- `docs/modules/platform/architecture.md` -- `docs/modules/jobengine/architecture.md` -- `src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.WebService/Program.cs` -- `src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.WebService/Program.cs` -- `docs/implplan/CONSOLIDATION_WEBSERVICE_FUNCTION_DB_MATRIX_20260305.md` - -## Delivery Tracker - -### JOBENG-STOR-001 - Reconcile declared driver contract with actual runtime behavior -Status: TODO -Dependency: none -Owners: Project Manager, Implementer -Task description: -- Produce a precise behavior matrix for `Storage:Driver` and `Storage:ObjectStore:Driver` for both services. -- Confirm and document current mismatch points (for example, drivers accepted by validation but not backed by concrete adapter behavior). - -Completion criteria: -- [ ] Behavior matrix committed under module docs with config keys, defaults, and startup fail-fast rules. -- [ ] Every accepted driver value is either fully implemented or explicitly rejected with deterministic startup failure. - -### JOBENG-STOR-002 - Implement production RustFS object-store adapters for blob payloads -Status: TODO -Dependency: JOBENG-STOR-001 -Owners: Implementer, Test Automation -Task description: -- Implement and wire RustFS/S3-compatible blob adapters for: -- `PacksRegistry` pack/provenance/attestation payload channels. -- `TaskRunner` run artifact payload channel. -- Preserve existing Postgres-backed metadata stores and deterministic ordering semantics. - -Completion criteria: -- [ ] `Storage:ObjectStore:Driver=rustfs` uses concrete RustFS adapter implementations in both services. -- [ ] Existing `seed-fs` behavior remains supported for local/offline deterministic workflows. -- [ ] Non-development startup fails when RustFS is configured without required endpoint/credentials settings. - -### JOBENG-STOR-003 - Harden non-development startup behavior and fallback policy -Status: TODO -Dependency: JOBENG-STOR-002 -Owners: Implementer -Task description: -- Remove silent non-dev behavior drift by enforcing explicit fail-fast for missing Postgres/object-store configuration. -- Ensure development-only fallback behavior is intentional, documented, and test-covered. - -Completion criteria: -- [ ] Non-development runtime has no implicit filesystem fallback for stores expected to be Postgres-backed. -- [ ] Error messages are actionable and identify missing config keys. -- [ ] Startup behavior is covered by automated tests for success/failure modes. - -### JOBENG-STOR-004 - Expand deterministic storage tests across drivers -Status: TODO -Dependency: JOBENG-STOR-002 -Owners: Test Automation -Task description: -- Add targeted tests that validate parity across `postgres + seed-fs` and `postgres + rustfs`. -- Include replay-critical assertions for stable ordering, digest consistency, and tenant isolation. - -Completion criteria: -- [ ] Targeted test projects include both happy-path and misconfiguration-path assertions. -- [ ] Evidence captures command output and test counts for each driver profile. -- [ ] No regression in existing persistence tests for Postgres repositories. - -### JOBENG-STOR-005 - Update architecture and operations docs for final storage contract -Status: TODO -Dependency: JOBENG-STOR-003 -Owners: Documentation author, Implementer -Task description: -- Update JobEngine and platform storage docs with final runtime contract, config examples, and migration notes. -- Record decisions and residual risks in sprint log and link to docs changed. - -Completion criteria: -- [ ] `docs/modules/jobengine/architecture.md` and `docs/modules/platform/architecture.md` reflect final behavior. -- [ ] Compose/ops guidance references valid config keys for both services. -- [ ] Sprint Decisions & Risks includes links to all updated docs. - -## Execution Log -| Date (UTC) | Update | Owner | -| --- | --- | --- | -| 2026-03-05 | Sprint created from architecture review; points 1 and 2 were partially implemented and require completion/hardening work. | Project Manager | - -## Decisions & Risks -- Current code already wires Postgres state stores for TaskRunner and Postgres persistence extension for PacksRegistry, but remaining object-store adapter parity and fallback hardening are unresolved. -- `PacksRegistry` currently carries an explicit RustFS-not-implemented guard in runtime contract paths; this blocks full completion of Point 1 in production modes. -- `TaskRunner` currently accepts object-store driver values while artifact reading remains filesystem-root based; implementation parity must be enforced to avoid config drift. -- Mitigation: complete adapter implementation and add startup contract tests before documentation sprint declares Point 1 as complete. - -## Next Checkpoints -- Driver matrix and gap report complete. -- RustFS adapter PR ready with targeted test evidence. -- Docs and compose parity review complete before marking DONE. diff --git a/docs/modules/jobengine/architecture.md b/docs/modules/jobengine/architecture.md index 7ee6c3836..2ae17f47e 100644 --- a/docs/modules/jobengine/architecture.md +++ b/docs/modules/jobengine/architecture.md @@ -173,6 +173,8 @@ The TaskRunner provides the execution substrate for Orchestrator jobs. Workers p - `Storage:Driver=postgres` is the production default for run state, logs, and approvals. - Postgres-backed stores: `PostgresPackRunStateStore`, `PostgresPackRunLogStore`, `PostgresPackRunApprovalStore` via `TaskRunnerDataSource`. - Artifact payload channel uses object storage path (`seed-fs` driver) configured with `TaskRunner:Storage:ObjectStore:SeedFs:RootPath`. +- Startup fails fast when `Storage:ObjectStore:Driver` is set to `rustfs` (not implemented) or any unsupported driver value. +- Non-development startup fails fast when `Storage:Driver=postgres` and no connection string is configured. - Explicit non-production overrides remain available (`filesystem`, `inmemory`) but are no longer implicit defaults. ### 8.3) PacksRegistry subdomain @@ -186,6 +188,8 @@ The PacksRegistry manages compliance/automation pack definitions, versions, and **Database and storage contract (Sprint 312):** - `Storage:Driver=postgres` is the production default for metadata/state repositories (`pack`, `parity`, `lifecycle`, `mirror`, `audit`, `attestation metadata`). - Blob/object payloads (`pack content`, `provenance content`, `attestation content`) are persisted through the seed-fs object-store channel (`SeedFsPacksRegistryBlobStore`). +- Startup fails fast when `Storage:ObjectStore:Driver` is set to `rustfs` (not implemented) or any unsupported driver value. +- Non-development startup fails fast when `Storage:Driver=postgres` and no connection string is configured. - PostgreSQL keeps metadata and compatibility placeholders; payload retrieval resolves from object storage first. - Explicit non-production overrides remain available (`filesystem`, `inmemory`) but are no longer implicit defaults. diff --git a/docs/modules/platform/architecture.md b/docs/modules/platform/architecture.md index 2dfc48b6f..6343ad82f 100644 --- a/docs/modules/platform/architecture.md +++ b/docs/modules/platform/architecture.md @@ -29,7 +29,8 @@ This contract is the default for all stateful StellaOps webservices unless a mod - Production default: `postgres`. - `inmemory` and `filesystem` are non-production/testing-only and must be explicitly configured. - `Storage:ObjectStore:Driver` - - Accepted values: `rustfs`, `seed-fs`. + - Accepted values at platform key level: `rustfs`, `seed-fs`. + - Module runtime contracts may narrow this set and must fail fast for unsupported values. - Use only for blob/object payload channels (artifacts, snapshots, package blobs). - `ConnectionStrings:Default` - Required when `Storage:Driver=postgres` unless a service-specific connection key is provided. @@ -40,12 +41,43 @@ Fail-fast policy: - Development runtime may use localhost/file defaults only when explicitly intended for local workflows. Current implementation status (2026-03-05): -- `PacksRegistry`: Postgres metadata/state + seed-fs payload channel for pack/provenance/attestation blobs. -- `TaskRunner`: Postgres run state/log/approval + seed-fs artifact payload channel. +- `PacksRegistry`: Postgres metadata/state + seed-fs payload channel for pack/provenance/attestation blobs; startup rejects `rustfs` and unknown object-store drivers. +- `TaskRunner`: Postgres run state/log/approval + seed-fs artifact payload channel; startup rejects `rustfs` and unknown object-store drivers in both WebService and Worker. - `RiskEngine`: Postgres-backed result store (`riskengine.risk_score_results`) with explicit in-memory test fallback. -- `Replay`: Postgres snapshot index + seed-fs snapshot blob store. +- `Replay`: Postgres snapshot index + seed-fs snapshot blob store; startup rejects `rustfs` and unknown object-store drivers. - `OpsMemory`: connection precedence aligned to `ConnectionStrings:OpsMemory -> ConnectionStrings:Default`, with non-development fail-fast. +## Platform Runtime Read-Model Boundary Policy (Point 4 / Sprint 20260305-005) + +Platform runtime read-model APIs are aggregation-only and must stay behind explicit query contracts. Runtime read handlers must not take direct dependencies on foreign module persistence internals. + +Approved runtime query contracts: +- `IReleaseControlBundleStore` (release/topology/security/integration projections over release-control bundles + runs). +- `IPlatformContextQuery` (read-only access to region/environment context inventory). + +Prohibited in runtime read-model services: +- Direct constructor dependencies on foreign `StellaOps.*.Persistence*` namespaces. +- Direct `DbContext`, `NpgsqlDataSource`, or module-specific migration runner dependencies from non-admin read endpoints. + +Migration/admin allowlist (explicit boundary exceptions): +- `src/Platform/StellaOps.Platform.WebService/Endpoints/SeedEndpoints.cs` +- `src/Platform/__Libraries/StellaOps.Platform.Database/MigrationModulePlugins.cs` + +Enforcement: +- Guard tests in `src/Platform/__Tests/StellaOps.Platform.WebService.Tests/PlatformRuntimeBoundaryGuardTests.cs` fail when constructor contracts drift or foreign persistence references appear outside the allowlist above. + +### Runtime Dependency Inventory (2026-03-05) + +| Component | Dependency category | Classification | Notes | +| --- | --- | --- | --- | +| `ReleaseReadModelService` | `IReleaseControlBundleStore` | Allowed runtime read-model dependency | Release projection reads only via Platform-owned bundle-store contract. | +| `TopologyReadModelService` | `IReleaseControlBundleStore`, `IPlatformContextQuery` | Allowed runtime read-model dependency | Topology projection composes release bundles with context inventory through explicit query contracts. | +| `SecurityReadModelService` | `IReleaseControlBundleStore`, `IPlatformContextQuery` | Allowed runtime read-model dependency | Security projection remains synthetic/read-only and does not call VEX/exception write stores directly. | +| `IntegrationsReadModelService` | `IReleaseControlBundleStore`, `IPlatformContextQuery` | Allowed runtime read-model dependency | Integration freshness projection uses release run metadata and context inventory only. | +| `PlatformContextService` | `IPlatformContextStore` (`InMemory`/`Postgres`) | Allowed runtime dependency (module-local persistence) | Exposes read-only `IPlatformContextQuery` plus preference write APIs; no foreign module coupling. | +| `SeedEndpoints` | Foreign `StellaOps.*.Persistence*` migration assemblies | Migration/admin-only dependency | Allowed exception for demo seed execution only (`platform.setup.admin`). | +| `MigrationModulePlugins` | Foreign module migration assemblies | Migration-only dependency | Allowed exception for schema migration orchestration, not part of runtime read endpoint execution path. | + ## Advisory Commitments (2026-02-26 Batch) - `SPRINT_20260226_223_Platform_score_explain_contract_and_replay_alignment` defines deterministic score/explain/replay contract behavior for CLI and Web consumers. diff --git a/docs/modules/platform/platform-service.md b/docs/modules/platform/platform-service.md index 1149aa9d8..3c51bd927 100644 --- a/docs/modules/platform/platform-service.md +++ b/docs/modules/platform/platform-service.md @@ -161,6 +161,14 @@ Provide a single, deterministic aggregation layer for cross-service UX workflows - Notifier (alert policies and delivery status) - Policy/Scanner/Registry/VexHub (search aggregation sources) +## Runtime boundary policy +- Runtime read-model services (`/api/v2/releases`, `/api/v2/topology/*`, `/api/v2/security/*`, `/api/v2/integrations/*`) must depend only on explicit query contracts: +- `IReleaseControlBundleStore` +- `IPlatformContextQuery` +- Foreign module persistence references are migration/admin-only and limited to explicit allowlist surfaces (`SeedEndpoints`, `MigrationModulePlugins`). +- Runtime read endpoints must not inject foreign `*.Persistence*` types, `DbContext` from other modules, or migration runners directly. +- Guard tests: `src/Platform/__Tests/StellaOps.Platform.WebService.Tests/PlatformRuntimeBoundaryGuardTests.cs`. + ## Security and scopes - Health: `ops.health` (summary), `ops.admin` (metrics) - Quotas: `quota.read` (summary), `quota.admin` (alerts/config) diff --git a/docs/modules/router/webservices-valkey-rollout-matrix.md b/docs/modules/router/webservices-valkey-rollout-matrix.md index b4ca7049f..c95731524 100644 --- a/docs/modules/router/webservices-valkey-rollout-matrix.md +++ b/docs/modules/router/webservices-valkey-rollout-matrix.md @@ -42,6 +42,7 @@ Legend: | policy-engine.stella-ops.local | policy-engine | /api/risk, /api/risk-budget, /api/v1/determinization, /policyEngine | C | Developer + Test Automation (Wave C) | Migrate API prefixes first; keep root compatibility path until control-plane verification completes. | Route type revert + `POLICY_ENGINE_ROUTER_ENABLED=false` (RMW-03). | | policy-gateway.stella-ops.local | policy | /api/cvss, /api/exceptions, /api/gate, /api/policy, /api/v1/governance, /api/v1/policy, /policy, /policyGateway | C | Developer + Test Automation (Wave C) | Migrate API prefixes first; keep `/policy` and `/policyGateway` compatibility paths until final cutover. | Route type revert + `POLICY_GATEWAY_ROUTER_ENABLED=false` (RMW-03). | | reachgraph.stella-ops.local | reachgraph-web | /api/v1/reachability, /reachgraph | D | Developer + Test Automation (Wave D) | Migrate API prefix first, then root compatibility path. | Route type revert + `REACHGRAPH_ROUTER_ENABLED=false` (RMW-03). | +| remediation.stella-ops.local | — (not in compose snapshot) | — (no ReverseProxy route in 2026-02-21 snapshot) | C | Developer + Test Automation (Wave C) | `StellaOps.Remediation.WebService` exists, but router/compose mapping is missing. Add explicit remediation API route inventory and then migrate to Microservice route type in control-plane wave. | Missing rollback key; add `REMEDIATION_ROUTER_ENABLED` once route is added. | | registry-token.stella-ops.local | registry-token | /registryTokenservice | A | Developer + Test Automation (Wave A) | Migrate compatibility route with token flow validation in Wave A. | Route type revert + `REGISTRY_TOKEN_ROUTER_ENABLED=false` (RMW-03). | | replay.stella-ops.local | replay-web | /replay | A | Developer + Test Automation (Wave A) | Migrate compatibility route in Wave A; add API-form alias if needed. | Route type revert + `REPLAY_ROUTER_ENABLED=false` (RMW-03). | | riskengine.stella-ops.local | riskengine-web | /riskengine | C | Developer + Test Automation (Wave C) | Migrate compatibility route in control-plane wave; add API alias if required. | Route type revert + `RISKENGINE_ROUTER_ENABLED=false` (RMW-03). | diff --git a/docs/modules/ui/information-architecture.md b/docs/modules/ui/information-architecture.md index f60ec6487..18ef8b64b 100644 --- a/docs/modules/ui/information-architecture.md +++ b/docs/modules/ui/information-architecture.md @@ -4,6 +4,18 @@ This document defines the information architecture (IA) for the StellaOps web interface, including navigation structure, route hierarchy, and role-based access patterns. +## 2026-03-05 Shell IA Update + +The global shell navigation was updated to improve visual scanability and mobile usability: + +- Sidebar global menus are now grouped into: +- `Release Control` +- `Security & Evidence` +- `Platform & Setup` +- Group headers act as direct links to each group's landing route for faster navigation. +- Mobile topbar secondary controls now open through an explicit `Context` toggle instead of always occupying visible row space. +- Findings compare panes and Releases deployment list were updated for mobile-safe layouts (no forced clipping). + ## Navigation Structure ### Primary Navigation diff --git a/docs/quickstart.md b/docs/quickstart.md index c28f67b8a..f867702f9 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -112,6 +112,9 @@ After a full setup, you'll have 60+ services running locally: Full service list: `devops/compose/docker-compose.stella-ops.yml` +Runtime URL convention: use `*.stella-ops.local` hostnames for services. +Exception: `hosts.stellaops.local` is a file name, not a runtime host. + Optional Sigstore services (`rekor-v2`, `rekor-cli`, `cosign`) are enabled only with: ```bash diff --git a/docs/technical/architecture/README.md b/docs/technical/architecture/README.md index 1f86b8939..078d34ad9 100644 --- a/docs/technical/architecture/README.md +++ b/docs/technical/architecture/README.md @@ -11,12 +11,28 @@ Use this index to locate platform-level architecture references and per-module d - [Component map](component-map.md) (quick descriptions of every module under `src/`) ## Detailed references +- [Canonical webservice catalog](webservice-catalog.md) - [Platform topology](platform-topology.md) - [Infrastructure dependencies](infrastructure-dependencies.md) - [Request and data flows](request-flows.md) - [Data isolation model](data-isolation.md) - [Security boundaries](security-boundaries.md) +## Docs validation + +Use the architecture docs validation check to detect service-path and hostname drift: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File ./docs/technical/architecture/scripts/validate-webservice-docs.ps1 +``` + +Intentional failing fixture (sanity check for the validator): + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File ./docs/technical/architecture/scripts/validate-webservice-docs.ps1 ` + -Files "docs/technical/architecture/fixtures/webservice-docs-invalid-fixture.md" +``` + ## User-centric views (NEW) - [User flows (UML diagrams)](user-flows.md) - End-to-end flows from user perspective - [Module matrix](module-matrix.md) - Complete 46-module inventory with categorization diff --git a/docs/technical/architecture/fixtures/webservice-docs-invalid-fixture.md b/docs/technical/architecture/fixtures/webservice-docs-invalid-fixture.md new file mode 100644 index 000000000..2b7b88dfb --- /dev/null +++ b/docs/technical/architecture/fixtures/webservice-docs-invalid-fixture.md @@ -0,0 +1,9 @@ +# Validation Fixture: Expected to fail + +This fixture intentionally includes drift so `validate-webservice-docs.ps1` can prove detection. + +| Service | Hostname | Path | +| --- | --- | --- | +| DemoService | `demo.stellaops.local` | `src/DoesNotExist/Missing.WebService` | + +Runtime URL sample: https://demo.stellaops.local/api/v1/demo diff --git a/docs/technical/architecture/port-registry.md b/docs/technical/architecture/port-registry.md index 82afc69d9..60e66c55f 100644 --- a/docs/technical/architecture/port-registry.md +++ b/docs/technical/architecture/port-registry.md @@ -2,6 +2,9 @@ All Stella Ops web services are assigned deterministic HTTPS/HTTP port pairs to avoid collisions during local development and simplify service discovery configuration. +Service inventory source-of-truth: `docs/technical/architecture/webservice-catalog.md`. +This page focuses on deterministic slot/port allocation and may include legacy or unassigned notes. + ## Port Assignment Scheme - **Formula**: Service at slot N → HTTPS = `10000 + N×10`, HTTP = `10000 + N×10 + 1` @@ -22,7 +25,7 @@ All Stella Ops web services are assigned deterministic HTTPS/HTTP port pairs to | 7 | 10070 | 10071 | Evidence Locker Aggregator | — | `src/EvidenceLocker/StellaOps.EvidenceLocker` | `STELLAOPS_EVIDENCELOCKER_AGGREGATOR_URL` | | 8 | 10080 | 10081 | Scanner | `scanner.stella-ops.local` | `src/Scanner/StellaOps.Scanner.WebService` | `STELLAOPS_SCANNER_URL` | | 9 | 10090 | 10091 | Concelier | `concelier.stella-ops.local` | `src/Concelier/StellaOps.Concelier.WebService` | `STELLAOPS_CONCELIER_URL` | -| 10 | 10100 | 10101 | Excititor | `excititor.stella-ops.local` | `src/Excititor/StellaOps.Excititor.WebService` | `STELLAOPS_EXCITITOR_URL` | +| 10 | 10100 | 10101 | Excititor | `excititor.stella-ops.local` | `src/Concelier/StellaOps.Excititor.WebService` | `STELLAOPS_EXCITITOR_URL` | | 11 | 10110 | 10111 | VexHub | `vexhub.stella-ops.local` | `src/VexHub/StellaOps.VexHub.WebService` | `STELLAOPS_VEXHUB_URL` | | 12 | 10120 | 10121 | VexLens | `vexlens.stella-ops.local` | `src/VexLens/StellaOps.VexLens.WebService` | `STELLAOPS_VEXLENS_URL` | | 13 | 10130 | 10131 | VulnExplorer | `vulnexplorer.stella-ops.local` | `src/Findings/StellaOps.VulnExplorer.Api` | `STELLAOPS_VULNEXPLORER_URL` | @@ -30,8 +33,8 @@ All Stella Ops web services are assigned deterministic HTTPS/HTTP port pairs to | 15 | 10150 | 10151 | Policy Gateway | `policy-gateway.stella-ops.local` | `src/Policy/StellaOps.Policy.Gateway` | `STELLAOPS_POLICY_GATEWAY_URL` | | 16 | 10160 | 10161 | RiskEngine | `riskengine.stella-ops.local` | `src/Findings/StellaOps.RiskEngine.WebService` | `STELLAOPS_RISKENGINE_URL` | | 17 | 10170 | 10171 | Orchestrator | `jobengine.stella-ops.local` | `src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.WebService` | `STELLAOPS_JOBENGINE_URL` | -| 18 | 10180 | 10181 | TaskRunner | `taskrunner.stella-ops.local` | `src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.WebService` | `STELLAOPS_TASKRUNNER_URL` | -| 19 | 10190 | 10191 | Scheduler | `scheduler.stella-ops.local` | `src/Scheduler/StellaOps.Scheduler.WebService` | `STELLAOPS_SCHEDULER_URL` | +| 18 | 10180 | 10181 | TaskRunner | `taskrunner.stella-ops.local` | `src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.WebService` | `STELLAOPS_TASKRUNNER_URL` | +| 19 | 10190 | 10191 | Scheduler | `scheduler.stella-ops.local` | `src/JobEngine/StellaOps.Scheduler.WebService` | `STELLAOPS_SCHEDULER_URL` | | 20 | 10200 | 10201 | Graph API | `graph.stella-ops.local` | `src/Graph/StellaOps.Graph.Api` | `STELLAOPS_GRAPH_URL` | | 21 | 10210 | 10211 | Cartographer | `cartographer.stella-ops.local` | `src/Scanner/StellaOps.Scanner.Cartographer` | `STELLAOPS_CARTOGRAPHER_URL` | | 22 | 10220 | 10221 | ReachGraph | `reachgraph.stella-ops.local` | `src/ReachGraph/StellaOps.ReachGraph.WebService` | `STELLAOPS_REACHGRAPH_URL` | @@ -42,14 +45,14 @@ All Stella Ops web services are assigned deterministic HTTPS/HTTP port pairs to | 27 | 10270 | 10271 | OpsMemory | `opsmemory.stella-ops.local` | `src/AdvisoryAI/StellaOps.OpsMemory.WebService` | `STELLAOPS_OPSMEMORY_URL` | | 28 | 10280 | 10281 | Notifier | `notifier.stella-ops.local` | `src/Notifier/StellaOps.Notifier/StellaOps.Notifier.WebService` | `STELLAOPS_NOTIFIER_URL` | | 29 | 10290 | 10291 | Notify | `notify.stella-ops.local` | `src/Notify/StellaOps.Notify.WebService` | `STELLAOPS_NOTIFY_URL` | -| 30 | 10300 | 10301 | Signer | `signer.stella-ops.local` | `src/Signer/StellaOps.Signer/StellaOps.Signer.WebService` | `STELLAOPS_SIGNER_URL` | +| 30 | 10300 | 10301 | Signer | `signer.stella-ops.local` | `src/Attestor/StellaOps.Signer/StellaOps.Signer.WebService` | `STELLAOPS_SIGNER_URL` | | 31 | 10310 | 10311 | SmRemote | `smremote.stella-ops.local` | `src/SmRemote/StellaOps.SmRemote.Service` | `STELLAOPS_SMREMOTE_URL` | | 32 | 10320 | 10321 | AirGap Controller | `airgap-controller.stella-ops.local` | `src/AirGap/StellaOps.AirGap.Controller` | `STELLAOPS_AIRGAP_CONTROLLER_URL` | | 33 | 10330 | 10331 | AirGap Time | `airgap-time.stella-ops.local` | `src/AirGap/StellaOps.AirGap.Time` | `STELLAOPS_AIRGAP_TIME_URL` | -| 34 | 10340 | 10341 | PacksRegistry | `packsregistry.stella-ops.local` | `src/PacksRegistry/StellaOps.PacksRegistry/StellaOps.PacksRegistry.WebService` | `STELLAOPS_PACKSREGISTRY_URL` | +| 34 | 10340 | 10341 | PacksRegistry | `packsregistry.stella-ops.local` | `src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.WebService` | `STELLAOPS_PACKSREGISTRY_URL` | | 35 | 10350 | 10351 | Registry Token | `registry-token.stella-ops.local` | `src/Registry/StellaOps.Registry.TokenService` | `STELLAOPS_REGISTRY_TOKENSERVICE_URL` | | 36 | 10360 | 10361 | BinaryIndex | `binaryindex.stella-ops.local` | `src/BinaryIndex/StellaOps.BinaryIndex.WebService` | `STELLAOPS_BINARYINDEX_URL` | -| 37 | 10370 | 10371 | IssuerDirectory | `issuerdirectory.stella-ops.local` | `src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService` | `STELLAOPS_ISSUERDIRECTORY_URL` | +| 37 | 10370 | 10371 | IssuerDirectory | `issuerdirectory.stella-ops.local` | `src/Authority/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService` | `STELLAOPS_ISSUERDIRECTORY_URL` | | 38 | 10380 | 10381 | Symbols | `symbols.stella-ops.local` | `src/BinaryIndex/StellaOps.Symbols.Server` | `STELLAOPS_SYMBOLS_URL` | | 39 | 10390 | 10391 | SbomService | `sbomservice.stella-ops.local` | `src/SbomService/StellaOps.SbomService` | `STELLAOPS_SBOMSERVICE_URL` | | 40 | 10400 | 10401 | ExportCenter | `exportcenter.stella-ops.local` | `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService` | `STELLAOPS_EXPORTCENTER_URL` | @@ -62,6 +65,8 @@ All Stella Ops web services are assigned deterministic HTTPS/HTTP port pairs to | 90 | 10900 | 10901 | Examples.Gateway | — | `src/Router/examples/Examples.Gateway` | — | | 91 | 10910 | 10911 | Examples.MultiTransport | — | `src/Router/examples/Examples.MultiTransport.Gateway` | — | +> Remediation runtime note: `src/Remediation/StellaOps.Remediation.WebService` is active and binds `remediation.stella-ops.local`, but no deterministic slot is currently published in this table because compose/router inventory does not yet expose a stable route mapping. Track status in `docs/modules/router/webservices-valkey-rollout-matrix.md`. + ## Worker Services Worker services associated with a web service use ports offset by +2/+3 from the web service slot: @@ -71,17 +76,17 @@ Worker services associated with a web service use ports offset by +2/+3 from the | 10062 | 10063 | EvidenceLocker Worker | `src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Worker` | | 10162 | 10163 | RiskEngine Worker | `src/Findings/StellaOps.RiskEngine.Worker` | | 10172 | 10173 | Orchestrator Worker | `src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Worker` | -| 10182 | 10183 | TaskRunner Worker | `src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Worker` | +| 10182 | 10183 | TaskRunner Worker | `src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.Worker` | | 10232 | 10233 | TimelineIndexer Worker | `src/Timeline/StellaOps.TimelineIndexer.Worker` | | 10282 | 10283 | Notifier Worker | `src/Notifier/StellaOps.Notifier/StellaOps.Notifier.Worker` | -| 10342 | 10343 | PacksRegistry Worker | `src/PacksRegistry/StellaOps.PacksRegistry/StellaOps.PacksRegistry.Worker` | +| 10342 | 10343 | PacksRegistry Worker | `src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.Worker` | | 10402 | 10403 | ExportCenter Worker | `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Worker` | ## Environment Variable Convention Each web service has a corresponding `STELLAOPS_{SERVICE}_URL` environment variable. The Platform service reads these at startup (Layer 1 of the 3-layer configuration) and maps them into `ApiBaseUrls` for the Angular frontend. -Example: `STELLAOPS_SCANNER_URL=https://scanner.internal:10080` maps to `ApiBaseUrls["scanner"]`. +Example: `STELLAOPS_SCANNER_URL=https://scanner.stella-ops.local` maps to `ApiBaseUrls["scanner"]`. See also: [3-Layer Service URL Configuration](../../modules/platform/architecture-overview.md) diff --git a/docs/technical/architecture/scripts/validate-webservice-docs.ps1 b/docs/technical/architecture/scripts/validate-webservice-docs.ps1 new file mode 100644 index 000000000..16af5f1d4 --- /dev/null +++ b/docs/technical/architecture/scripts/validate-webservice-docs.ps1 @@ -0,0 +1,103 @@ +param( + [string[]] $Files = @( + "docs/technical/architecture/port-registry.md", + "docs/technical/architecture/webservice-catalog.md" + ) +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +function Get-RepositoryRoot { + param([string] $Start) + + $current = Resolve-Path $Start + while ($null -ne $current) { + if (Test-Path (Join-Path $current "docs")) { + return $current + } + + $parent = Split-Path -Parent $current + if ($parent -eq $current) { + break + } + + $current = $parent + } + + throw "Could not locate repository root from '$Start'." +} + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$repoRoot = Get-RepositoryRoot -Start $scriptDir + +$pathViolations = [System.Collections.Generic.List[string]]::new() +$hostViolations = [System.Collections.Generic.List[string]]::new() + +$pathPattern = [regex]'`(?src/[^`]+)`' +$urlPattern = [regex]'https?://(?[A-Za-z0-9.-]+)' +$legacyHostPattern = [regex]'\b(?[a-z0-9.-]+\.stellaops\.local)\b' + +foreach ($relativeFile in $Files) { + $normalizedRelative = $relativeFile.Replace('\\', '/').Trim() + $absoluteFile = Join-Path $repoRoot $normalizedRelative + + if (-not (Test-Path $absoluteFile)) { + throw "Validation file not found: $normalizedRelative" + } + + $lineNumber = 0 + foreach ($line in Get-Content $absoluteFile) { + $lineNumber++ + + foreach ($match in $pathPattern.Matches($line)) { + $pathValue = $match.Groups['path'].Value.Trim() + $absolutePath = Join-Path $repoRoot ($pathValue.Replace('/', [IO.Path]::DirectorySeparatorChar)) + if (-not (Test-Path $absolutePath)) { + $pathViolations.Add("${normalizedRelative}:$lineNumber unresolved path '$pathValue'") + } + } + + foreach ($match in $urlPattern.Matches($line)) { + $hostValue = $match.Groups['host'].Value.ToLowerInvariant() + if ($hostValue -eq "localhost") { + continue + } + + if ($hostValue.StartsWith("127.")) { + continue + } + + if ($hostValue -eq "stella-ops.local" -or $hostValue.EndsWith(".stella-ops.local")) { + continue + } + + $hostViolations.Add("${normalizedRelative}:$lineNumber non-canonical runtime host '$hostValue'") + } + + foreach ($match in $legacyHostPattern.Matches($line)) { + $hostValue = $match.Groups['host'].Value.ToLowerInvariant() + if ($hostValue -eq "hosts.stellaops.local") { + continue + } + + $hostViolations.Add("${normalizedRelative}:$lineNumber forbidden legacy hostname '$hostValue' (expected .stella-ops.local)") + } + } +} + +if ($pathViolations.Count -eq 0 -and $hostViolations.Count -eq 0) { + Write-Output "PASS validate-webservice-docs: files=$($Files.Count), pathViolations=0, hostViolations=0" + exit 0 +} + +Write-Output "FAIL validate-webservice-docs" +foreach ($violation in $pathViolations) { + Write-Output "PATH: $violation" +} + +foreach ($violation in $hostViolations) { + Write-Output "HOST: $violation" +} + +exit 1 diff --git a/docs/technical/architecture/webservice-catalog.md b/docs/technical/architecture/webservice-catalog.md new file mode 100644 index 000000000..8b6b2ebec --- /dev/null +++ b/docs/technical/architecture/webservice-catalog.md @@ -0,0 +1,56 @@ +# Canonical Webservice Catalog + +This page is the source-of-truth inventory for Stella Ops `*.WebService` runtime services. + +## Scope and contract +- Inventory source: `rg --files src -g "*WebService.csproj"`. +- Includes active runtime webservices only (31 services). +- Excludes non-`WebService` API binaries (for example `StellaOps.Policy.Engine`, `StellaOps.Policy.Gateway`, `StellaOps.Graph.Api`, `StellaOps.VulnExplorer.Api`, `StellaOps.Symbols.Server`, `StellaOps.Registry.TokenService`, `StellaOps.SmRemote.Service`) even though they may bind `*.stella-ops.local` aliases. +- Canonical runtime hostname form: `.stella-ops.local`. + +## Runtime hostname convention and exceptions +- Runtime service-discovery URLs in docs should use `https://.stella-ops.local` (or the HTTP equivalent when TLS is intentionally not shown). +- Permitted exceptions: +- Infrastructure aliases (`db.stella-ops.local`, `cache.stella-ops.local`, `s3.stella-ops.local`, `rekor.stella-ops.local`, `registry.stella-ops.local`). +- Loopback/dev diagnostics (`localhost`, `127.x.y.z`) where transport wiring is the point of the example. +- Non-runtime identifiers/file names (for example `hosts.stellaops.local` file path). + +## Active webservices +| Domain | Webservice | Local hostname | Purpose | Persistence | Source path | Owner module | +| --- | --- | --- | --- | --- | --- | --- | +| AdvisoryAI | AdvisoryAI | `advisoryai.stella-ops.local` | Advisory assistant APIs (chat, evidence-pack, knowledge search). | postgres | `src/AdvisoryAI/StellaOps.AdvisoryAI.WebService` | `src/AdvisoryAI` | +| AdvisoryAI | OpsMemory | `opsmemory.stella-ops.local` | Operational memory/query APIs for advisory workflows. | postgres | `src/AdvisoryAI/StellaOps.OpsMemory.WebService` | `src/AdvisoryAI` | +| Attestor | Attestor | `attestor.stella-ops.local` | Attestation, witness, and proof-chain APIs. | postgres | `src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService` | `src/Attestor` | +| Attestor | Signer | `signer.stella-ops.local` | Signing and key-ceremony APIs. | postgres | `src/Attestor/StellaOps.Signer/StellaOps.Signer.WebService` | `src/Attestor` | +| Authority | IssuerDirectory | `issuerdirectory.stella-ops.local` | Issuer metadata and trust directory APIs. | postgres | `src/Authority/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService` | `src/Authority` | +| BinaryIndex | BinaryIndex | `binaryindex.stella-ops.local` | Binary index, patch coverage, and resolution APIs. | postgres | `src/BinaryIndex/StellaOps.BinaryIndex.WebService` | `src/BinaryIndex` | +| Concelier | Concelier | `concelier.stella-ops.local` | Advisory ingestion and source-management APIs. | postgres | `src/Concelier/StellaOps.Concelier.WebService` | `src/Concelier` | +| Concelier | Excititor | `excititor.stella-ops.local` | VEX ingest, linkset, and evidence APIs. | postgres | `src/Concelier/StellaOps.Excititor.WebService` | `src/Concelier` | +| Doctor | Doctor | `doctor.stella-ops.local` | Health diagnostics and setup-check APIs. | in-memory (no service DB) | `src/Doctor/StellaOps.Doctor.WebService` | `src/Doctor` | +| EvidenceLocker | EvidenceLocker | `evidencelocker.stella-ops.local` | Evidence ingest, bundle, legal hold, and verification APIs. | postgres | `src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.WebService` | `src/EvidenceLocker` | +| ExportCenter | ExportCenter | `exportcenter.stella-ops.local` | Export/audit bundle/report APIs. | postgres | `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService` | `src/ExportCenter` | +| Findings | Findings.Ledger | `findings.stella-ops.local` | Findings ledger, summary, and evidence graph APIs. | postgres | `src/Findings/StellaOps.Findings.Ledger.WebService` | `src/Findings` | +| Findings | RiskEngine | `riskengine.stella-ops.local` | Exploit maturity and risk score APIs. | postgres (in-memory fallback for explicit test profile) | `src/Findings/StellaOps.RiskEngine.WebService` | `src/Findings` | +| Integrations | Integrations | `integrations.stella-ops.local` | Integration adapters and endpoint management APIs. | postgres | `src/Integrations/StellaOps.Integrations.WebService` | `src/Integrations` | +| JobEngine | JobEngine | `jobengine.stella-ops.local` | Release orchestration, approvals, DAG/workflow APIs. | postgres | `src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.WebService` | `src/JobEngine` | +| JobEngine | PacksRegistry | `packsregistry.stella-ops.local` | Pack/provenance/attestation registry APIs. | postgres + seed-fs object payloads | `src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.WebService` | `src/JobEngine` | +| JobEngine | Scheduler | `scheduler.stella-ops.local` | Schedule/run planning and event APIs. | postgres | `src/JobEngine/StellaOps.Scheduler.WebService` | `src/JobEngine` | +| JobEngine | TaskRunner | `taskrunner.stella-ops.local` | Task execution, run state/log, approval, and artifact APIs. | postgres + seed-fs object payloads | `src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.WebService` | `src/JobEngine` | +| Notifier | Notifier | `notifier.stella-ops.local` | Escalation and incident notification APIs. | postgres | `src/Notifier/StellaOps.Notifier/StellaOps.Notifier.WebService` | `src/Notifier` | +| Notify | Notify | `notify.stella-ops.local` | Notification rule/channel/template and delivery APIs. | postgres | `src/Notify/StellaOps.Notify.WebService` | `src/Notify` | +| Platform | Platform | `platform.stella-ops.local` | Console aggregation, setup, admin, and read-model APIs. | postgres | `src/Platform/StellaOps.Platform.WebService` | `src/Platform` | +| ReachGraph | ReachGraph | `reachgraph.stella-ops.local` | Reachability graph and CVE mapping APIs. | postgres | `src/ReachGraph/StellaOps.ReachGraph.WebService` | `src/ReachGraph` | +| Remediation | Remediation | `remediation.stella-ops.local` | Remediation source, registry, and match APIs. | postgres | `src/Remediation/StellaOps.Remediation.WebService` | `src/Remediation` | +| Replay | Replay | `replay.stella-ops.local` | Point-in-time query and verdict replay APIs. | postgres + seed-fs snapshot blobs | `src/Replay/StellaOps.Replay.WebService` | `src/Replay` | +| Router | Gateway | `router.stella-ops.local` | Gateway dispatch, auth, and reverse-proxy APIs. | no-persistence | `src/Router/StellaOps.Gateway.WebService` | `src/Router` | +| Scanner | Scanner | `scanner.stella-ops.local` | Scan submission, triage, drift, and scan data APIs. | postgres | `src/Scanner/StellaOps.Scanner.WebService` | `src/Scanner` | +| Timeline | Timeline | `timeline.stella-ops.local` | Timeline query/export/replay APIs. | postgres | `src/Timeline/StellaOps.Timeline.WebService` | `src/Timeline` | +| Timeline | TimelineIndexer | `timelineindexer.stella-ops.local` | Timeline indexer control/status APIs. | postgres | `src/Timeline/StellaOps.TimelineIndexer.WebService` | `src/Timeline` | +| Unknowns | Unknowns | `unknowns.stella-ops.local` | Unknowns queue and triage APIs. | postgres | `src/Unknowns/StellaOps.Unknowns.WebService` | `src/Unknowns` | +| VexHub | VexHub | `vexhub.stella-ops.local` | VEX ingest and distribution APIs. | postgres | `src/VexHub/StellaOps.VexHub.WebService` | `src/VexHub` | +| VexLens | VexLens | `vexlens.stella-ops.local` | VEX lens, delta, and gate-view APIs. | postgres | `src/VexLens/StellaOps.VexLens.WebService` | `src/VexLens` | + +## Related references +- `docs/technical/architecture/port-registry.md` +- `docs/modules/router/webservices-valkey-rollout-matrix.md` +- `docs/implplan/CONSOLIDATION_WEBSERVICE_FUNCTION_DB_MATRIX_20260305.md` diff --git a/src/AdvisoryAI/StellaOps.AdvisoryAI/KnowledgeSearch/KnowledgeSearchOptions.cs b/src/AdvisoryAI/StellaOps.AdvisoryAI/KnowledgeSearch/KnowledgeSearchOptions.cs index e094ac133..7a2607d42 100644 --- a/src/AdvisoryAI/StellaOps.AdvisoryAI/KnowledgeSearch/KnowledgeSearchOptions.cs +++ b/src/AdvisoryAI/StellaOps.AdvisoryAI/KnowledgeSearch/KnowledgeSearchOptions.cs @@ -55,13 +55,13 @@ public sealed class KnowledgeSearchOptions public List OpenApiRoots { get; set; } = ["src", "devops/compose"]; public string UnifiedFindingsSnapshotPath { get; set; } = - "src/AdvisoryAI/StellaOps.AdvisoryAI/UnifiedSearch/Snapshots/findings.snapshot.json"; + "UnifiedSearch/Snapshots/findings.snapshot.json"; public string UnifiedVexSnapshotPath { get; set; } = - "src/AdvisoryAI/StellaOps.AdvisoryAI/UnifiedSearch/Snapshots/vex.snapshot.json"; + "UnifiedSearch/Snapshots/vex.snapshot.json"; public string UnifiedPolicySnapshotPath { get; set; } = - "src/AdvisoryAI/StellaOps.AdvisoryAI/UnifiedSearch/Snapshots/policy.snapshot.json"; + "UnifiedSearch/Snapshots/policy.snapshot.json"; public bool UnifiedAutoIndexEnabled { get; set; } diff --git a/src/AdvisoryAI/StellaOps.AdvisoryAI/StellaOps.AdvisoryAI.csproj b/src/AdvisoryAI/StellaOps.AdvisoryAI/StellaOps.AdvisoryAI.csproj index 13f1507c0..d5daac278 100644 --- a/src/AdvisoryAI/StellaOps.AdvisoryAI/StellaOps.AdvisoryAI.csproj +++ b/src/AdvisoryAI/StellaOps.AdvisoryAI/StellaOps.AdvisoryAI.csproj @@ -13,7 +13,7 @@ - + @@ -34,6 +34,18 @@ PreserveNewest models/all-MiniLM-L6-v2.onnx + + PreserveNewest + UnifiedSearch/Snapshots/findings.snapshot.json + + + PreserveNewest + UnifiedSearch/Snapshots/vex.snapshot.json + + + PreserveNewest + UnifiedSearch/Snapshots/policy.snapshot.json + diff --git a/src/AdvisoryAI/StellaOps.AdvisoryAI/UnifiedSearch/Adapters/FindingsSearchAdapter.cs b/src/AdvisoryAI/StellaOps.AdvisoryAI/UnifiedSearch/Adapters/FindingsSearchAdapter.cs index feab6b8d9..3829b2001 100644 --- a/src/AdvisoryAI/StellaOps.AdvisoryAI/UnifiedSearch/Adapters/FindingsSearchAdapter.cs +++ b/src/AdvisoryAI/StellaOps.AdvisoryAI/UnifiedSearch/Adapters/FindingsSearchAdapter.cs @@ -16,7 +16,7 @@ internal sealed class FindingsSearchAdapter : ISearchIngestionAdapter { private const string TenantHeader = "X-StellaOps-Tenant"; private const string HttpClientName = "scanner-internal"; - private const string FindingsEndpoint = "/api/v1/scanner/security/findings"; + private const string FindingsEndpoint = "/api/v1/security/findings"; private const int MaxPages = 20; private const int PageSize = 100; diff --git a/src/AdvisoryAI/StellaOps.AdvisoryAI/UnifiedSearch/UnifiedSearchIndexer.cs b/src/AdvisoryAI/StellaOps.AdvisoryAI/UnifiedSearch/UnifiedSearchIndexer.cs index 364878066..d200faa41 100644 --- a/src/AdvisoryAI/StellaOps.AdvisoryAI/UnifiedSearch/UnifiedSearchIndexer.cs +++ b/src/AdvisoryAI/StellaOps.AdvisoryAI/UnifiedSearch/UnifiedSearchIndexer.cs @@ -12,16 +12,19 @@ namespace StellaOps.AdvisoryAI.UnifiedSearch; internal sealed class UnifiedSearchIndexer : IUnifiedSearchIndexer { private readonly KnowledgeSearchOptions _options; + private readonly IKnowledgeSearchStore _store; private readonly IEnumerable _adapters; private readonly ILogger _logger; public UnifiedSearchIndexer( IOptions options, + IKnowledgeSearchStore store, IEnumerable adapters, ILogger logger) { ArgumentNullException.ThrowIfNull(options); _options = options.Value ?? new KnowledgeSearchOptions(); + _store = store ?? throw new ArgumentNullException(nameof(store)); _adapters = adapters ?? throw new ArgumentNullException(nameof(adapters)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } @@ -39,6 +42,8 @@ internal sealed class UnifiedSearchIndexer : IUnifiedSearchIndexer return new UnifiedSearchIndexSummary(0, 0, 0); } + await _store.EnsureSchemaAsync(cancellationToken).ConfigureAwait(false); + var stopwatch = Stopwatch.StartNew(); var domains = 0; var chunks = 0; @@ -131,6 +136,8 @@ internal sealed class UnifiedSearchIndexer : IUnifiedSearchIndexer return new UnifiedSearchIndexSummary(0, 0, 0); } + await _store.EnsureSchemaAsync(cancellationToken).ConfigureAwait(false); + var stopwatch = Stopwatch.StartNew(); var domains = 0; var chunks = 0; @@ -348,11 +355,17 @@ internal sealed class UnifiedSearchIndexer : IUnifiedSearchIndexer UnifiedChunk chunk, CancellationToken cancellationToken) { + var sourceRef = ResolveSourceRef(chunk); + var sourcePath = ResolveSourcePath(chunk); + const string sql = """ INSERT INTO advisoryai.kb_doc (doc_id, doc_type, product, version, source_ref, path, title, content_hash, metadata, indexed_at) VALUES (@doc_id, @doc_type, @product, @version, @source_ref, @path, @title, @content_hash, '{}'::jsonb, NOW()) - ON CONFLICT (doc_id) DO NOTHING; + ON CONFLICT (doc_id) DO UPDATE SET + title = EXCLUDED.title, + content_hash = EXCLUDED.content_hash, + indexed_at = NOW(); """; await using var command = connection.CreateCommand(); @@ -362,14 +375,34 @@ internal sealed class UnifiedSearchIndexer : IUnifiedSearchIndexer command.Parameters.AddWithValue("doc_type", chunk.Domain); command.Parameters.AddWithValue("product", "stella-ops"); command.Parameters.AddWithValue("version", "local"); - command.Parameters.AddWithValue("source_ref", chunk.Domain); - command.Parameters.AddWithValue("path", chunk.Kind); + command.Parameters.AddWithValue("source_ref", sourceRef); + command.Parameters.AddWithValue("path", sourcePath); command.Parameters.AddWithValue("title", chunk.Title); command.Parameters.AddWithValue("content_hash", KnowledgeSearchText.StableId(chunk.Body)); await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); } + private static string ResolveSourceRef(UnifiedChunk chunk) + { + if (!string.IsNullOrWhiteSpace(chunk.EntityKey)) + { + return chunk.EntityKey.Trim(); + } + + return chunk.DocId; + } + + private static string ResolveSourcePath(UnifiedChunk chunk) + { + if (!string.IsNullOrWhiteSpace(chunk.DocId)) + { + return chunk.DocId; + } + + return $"{chunk.Domain}/{chunk.Kind}"; + } + private static IReadOnlyList DeduplicateChunks(IEnumerable chunks) { var byChunkId = new SortedDictionary(StringComparer.Ordinal); diff --git a/src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/Integration/UnifiedSearchLiveAdapterIntegrationTests.cs b/src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/Integration/UnifiedSearchLiveAdapterIntegrationTests.cs index 23302b36e..b236edee3 100644 --- a/src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/Integration/UnifiedSearchLiveAdapterIntegrationTests.cs +++ b/src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/Integration/UnifiedSearchLiveAdapterIntegrationTests.cs @@ -62,7 +62,7 @@ public sealed class UnifiedSearchLiveAdapterIntegrationTests handler.Requests.Should().ContainSingle(); handler.Requests[0].Tenant.Should().Be("global"); - handler.Requests[0].Uri.Should().Contain("/api/v1/scanner/security/findings"); + handler.Requests[0].Uri.Should().Contain("/api/v1/security/findings"); } [Fact] @@ -326,6 +326,7 @@ public sealed class UnifiedSearchLiveAdapterIntegrationTests var indexer = new UnifiedSearchIndexer( options, + store, [ new FindingsSearchAdapter( new SingleClientFactory(findingsHandler, "http://scanner.local"), @@ -357,6 +358,35 @@ public sealed class UnifiedSearchLiveAdapterIntegrationTests (await CountDomainChunksAsync(connection, "policy")).Should().Be(4); } + [Fact] + public async Task UnifiedSearchIndexer_RebuildAllAsync_EnsuresSchema_WhenTablesAreMissing() + { + await using var fixture = await StartPostgresOrSkipAsync(); + var options = Options.Create(new KnowledgeSearchOptions + { + Enabled = true, + ConnectionString = fixture.ConnectionString, + FtsLanguageConfig = "simple" + }); + + await using var store = new PostgresKnowledgeSearchStore(options, NullLogger.Instance); + var adapter = new MutableAdapter("findings", [BuildFindingChunk("finding-seed", "CVE-2026-3000", "Schema bootstrap finding.")]); + var indexer = new UnifiedSearchIndexer( + options, + store, + [adapter], + NullLogger.Instance); + + var summary = await indexer.RebuildAllAsync(CancellationToken.None); + + summary.DomainCount.Should().Be(1); + summary.ChunkCount.Should().Be(1); + + await using var connection = new NpgsqlConnection(fixture.ConnectionString); + await connection.OpenAsync(); + (await CountDomainChunksAsync(connection, "findings")).Should().Be(1); + } + [Fact] public async Task UnifiedSearchIndexer_IndexAll_UpsertsOnlyChangedChunks_AndFindsNewFinding() { @@ -378,6 +408,7 @@ public sealed class UnifiedSearchLiveAdapterIntegrationTests var adapter = new MutableAdapter("findings", [unchangedChunk]); var indexer = new UnifiedSearchIndexer( options, + store, [adapter], NullLogger.Instance); @@ -502,6 +533,7 @@ public sealed class UnifiedSearchLiveAdapterIntegrationTests var adapter = new MutableAdapter("findings", [chunkTenantA]); var indexer = new UnifiedSearchIndexer( options, + store, [adapter], NullLogger.Instance); diff --git a/src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.Tests/PacksRegistryStartupContractTests.cs b/src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.Tests/PacksRegistryStartupContractTests.cs new file mode 100644 index 000000000..2f6465897 --- /dev/null +++ b/src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.Tests/PacksRegistryStartupContractTests.cs @@ -0,0 +1,70 @@ +using System.Net; +using Microsoft.AspNetCore.Mvc.Testing; + +namespace StellaOps.PacksRegistry.Tests; + +[Collection(PacksRegistryStartupEnvironmentCollection.Name)] +public sealed class PacksRegistryStartupContractTests +{ + [Fact] + public void Startup_FailsWithoutPostgresConnectionString_InProduction() + { + using var environment = PacksRegistryStartupEnvironmentScope.ProductionPostgresWithoutConnection(); + using var factory = new WebApplicationFactory(); + + var exception = Assert.ThrowsAny(() => + { + using var client = factory.CreateClient(); + }); + + Assert.Contains( + "PacksRegistry requires PostgreSQL connection settings in non-development mode.", + exception.ToString(), + StringComparison.Ordinal); + } + + [Fact] + public void Startup_RejectsRustFsObjectStoreDriver() + { + using var environment = PacksRegistryStartupEnvironmentScope.ProductionWithObjectStoreDriver("rustfs"); + using var factory = new WebApplicationFactory(); + + var exception = Assert.ThrowsAny(() => + { + using var client = factory.CreateClient(); + }); + + Assert.Contains( + "RustFS object store is configured for PacksRegistry, but no RustFS adapter is implemented.", + exception.ToString(), + StringComparison.Ordinal); + } + + [Fact] + public void Startup_RejectsUnsupportedObjectStoreDriver() + { + using var environment = PacksRegistryStartupEnvironmentScope.ProductionWithObjectStoreDriver("unknown-store"); + using var factory = new WebApplicationFactory(); + + var exception = Assert.ThrowsAny(() => + { + using var client = factory.CreateClient(); + }); + + Assert.Contains( + "Unsupported object store driver 'unknown-store' for PacksRegistry. Allowed values: seed-fs.", + exception.ToString(), + StringComparison.Ordinal); + } + + [Fact] + public async Task Startup_AllowsSeedFsObjectStoreDriver() + { + using var environment = PacksRegistryStartupEnvironmentScope.TestingInMemorySeedFs(); + using var factory = new WebApplicationFactory(); + + using var client = factory.CreateClient(); + var response = await client.GetAsync("/healthz", TestContext.Current.CancellationToken); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } +} diff --git a/src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.Tests/PacksRegistryStartupEnvironmentScope.cs b/src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.Tests/PacksRegistryStartupEnvironmentScope.cs new file mode 100644 index 000000000..0753f8a61 --- /dev/null +++ b/src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.Tests/PacksRegistryStartupEnvironmentScope.cs @@ -0,0 +1,96 @@ +namespace StellaOps.PacksRegistry.Tests; + +[CollectionDefinition(Name, DisableParallelization = true)] +public sealed class PacksRegistryStartupEnvironmentCollection +{ + public const string Name = "PacksRegistryStartupEnvironment"; +} + +internal sealed class PacksRegistryStartupEnvironmentScope : IDisposable +{ + private static readonly string[] ManagedKeys = + [ + "DOTNET_ENVIRONMENT", + "ASPNETCORE_ENVIRONMENT", + "STORAGE__DRIVER", + "PACKSREGISTRY__STORAGE__DRIVER", + "STORAGE__OBJECTSTORE__DRIVER", + "PACKSREGISTRY__STORAGE__OBJECTSTORE__DRIVER", + "STORAGE__POSTGRES__CONNECTIONSTRING", + "PACKSREGISTRY__STORAGE__POSTGRES__CONNECTIONSTRING", + "CONNECTIONSTRINGS__PACKSREGISTRY", + "CONNECTIONSTRINGS__DEFAULT" + ]; + + private readonly Dictionary _originalValues = new(StringComparer.Ordinal); + + private PacksRegistryStartupEnvironmentScope() + { + foreach (var key in ManagedKeys) + { + _originalValues[key] = Environment.GetEnvironmentVariable(key); + } + } + + public static PacksRegistryStartupEnvironmentScope ProductionPostgresWithoutConnection() + { + var scope = new PacksRegistryStartupEnvironmentScope(); + scope.Set("DOTNET_ENVIRONMENT", "Production"); + scope.Set("ASPNETCORE_ENVIRONMENT", "Production"); + scope.Set("STORAGE__DRIVER", "postgres"); + scope.Set("PACKSREGISTRY__STORAGE__DRIVER", "postgres"); + scope.Set("PACKSREGISTRY__STORAGE__OBJECTSTORE__DRIVER", "seed-fs"); + scope.Set("STORAGE__OBJECTSTORE__DRIVER", "seed-fs"); + scope.Set("STORAGE__POSTGRES__CONNECTIONSTRING", null); + scope.Set("PACKSREGISTRY__STORAGE__POSTGRES__CONNECTIONSTRING", null); + scope.Set("CONNECTIONSTRINGS__PACKSREGISTRY", null); + scope.Set("CONNECTIONSTRINGS__DEFAULT", null); + return scope; + } + + public static PacksRegistryStartupEnvironmentScope ProductionWithObjectStoreDriver(string objectStoreDriver) + { + var scope = new PacksRegistryStartupEnvironmentScope(); + scope.Set("DOTNET_ENVIRONMENT", "Production"); + scope.Set("ASPNETCORE_ENVIRONMENT", "Production"); + scope.Set("STORAGE__DRIVER", "postgres"); + scope.Set("PACKSREGISTRY__STORAGE__DRIVER", "postgres"); + scope.Set("PACKSREGISTRY__STORAGE__OBJECTSTORE__DRIVER", objectStoreDriver); + scope.Set("STORAGE__OBJECTSTORE__DRIVER", objectStoreDriver); + var connectionString = "Host=localhost;Database=stellaops_packs;Username=stellaops;Password=stellaops"; + scope.Set("STORAGE__POSTGRES__CONNECTIONSTRING", connectionString); + scope.Set("PACKSREGISTRY__STORAGE__POSTGRES__CONNECTIONSTRING", connectionString); + scope.Set("CONNECTIONSTRINGS__PACKSREGISTRY", connectionString); + scope.Set("CONNECTIONSTRINGS__DEFAULT", connectionString); + return scope; + } + + public static PacksRegistryStartupEnvironmentScope TestingInMemorySeedFs() + { + var scope = new PacksRegistryStartupEnvironmentScope(); + scope.Set("DOTNET_ENVIRONMENT", "Testing"); + scope.Set("ASPNETCORE_ENVIRONMENT", "Testing"); + scope.Set("STORAGE__DRIVER", "inmemory"); + scope.Set("PACKSREGISTRY__STORAGE__DRIVER", "inmemory"); + scope.Set("PACKSREGISTRY__STORAGE__OBJECTSTORE__DRIVER", "seed-fs"); + scope.Set("STORAGE__OBJECTSTORE__DRIVER", "seed-fs"); + scope.Set("STORAGE__POSTGRES__CONNECTIONSTRING", null); + scope.Set("PACKSREGISTRY__STORAGE__POSTGRES__CONNECTIONSTRING", null); + scope.Set("CONNECTIONSTRINGS__PACKSREGISTRY", null); + scope.Set("CONNECTIONSTRINGS__DEFAULT", null); + return scope; + } + + public void Dispose() + { + foreach (var entry in _originalValues) + { + Environment.SetEnvironmentVariable(entry.Key, entry.Value); + } + } + + private void Set(string key, string? value) + { + Environment.SetEnvironmentVariable(key, value); + } +} diff --git a/src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.Tests/TASKS.md b/src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.Tests/TASKS.md index c01d654b3..4b746f2b2 100644 --- a/src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.Tests/TASKS.md +++ b/src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.Tests/TASKS.md @@ -9,3 +9,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229 | AUDIT-0432-T | DONE | Revalidated 2026-01-07; test coverage audit for StellaOps.PacksRegistry.Tests. | | AUDIT-0432-A | DONE | Waived (test project; revalidated 2026-01-07). | | REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. | +| SPRINT-20260305-002 | DONE | Added `PacksRegistryStartupContractTests` covering postgres missing-connection fail-fast and seed-fs/rustfs object-store contract enforcement. | diff --git a/src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.WebService/Program.cs b/src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.WebService/Program.cs index f24cd4b44..c32bde562 100644 --- a/src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.WebService/Program.cs +++ b/src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.WebService/Program.cs @@ -73,8 +73,6 @@ else } ValidateObjectStoreContract( - builder.Configuration, - builder.Environment.IsDevelopment(), "PacksRegistry", objectStoreDriver); @@ -925,38 +923,21 @@ static string ResolveObjectStoreDriver(IConfiguration configuration, string serv } static void ValidateObjectStoreContract( - IConfiguration configuration, - bool isDevelopment, string serviceName, string objectStoreDriver) { - if (!string.Equals(objectStoreDriver, "rustfs", StringComparison.OrdinalIgnoreCase) && - !string.Equals(objectStoreDriver, "seed-fs", StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(objectStoreDriver, "seed-fs", StringComparison.OrdinalIgnoreCase)) { + if (string.Equals(objectStoreDriver, "rustfs", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException( + $"RustFS object store is configured for {serviceName}, but no RustFS adapter is implemented. " + + "Use seed-fs."); + } + throw new InvalidOperationException( $"Unsupported object store driver '{objectStoreDriver}' for {serviceName}. " + - "Allowed values: rustfs, seed-fs."); - } - - if (!string.Equals(objectStoreDriver, "rustfs", StringComparison.OrdinalIgnoreCase)) - { - return; - } - - if (!isDevelopment) - { - throw new InvalidOperationException( - $"RustFS object store is configured for {serviceName}, but the RustFS adapter is not implemented yet. " + - "Use seed-fs until RustFS adapter support lands."); - } - - var rustFsBaseUrl = FirstNonEmpty( - configuration[$"{serviceName}:Storage:ObjectStore:RustFs:BaseUrl"], - configuration["Storage:ObjectStore:RustFs:BaseUrl"]); - - if (string.IsNullOrWhiteSpace(rustFsBaseUrl)) - { - return; + "Allowed values: seed-fs."); } } diff --git a/src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.WebService/TASKS.md b/src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.WebService/TASKS.md index 84a38ba53..e529ce41a 100644 --- a/src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.WebService/TASKS.md +++ b/src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.WebService/TASKS.md @@ -10,3 +10,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229 | AUDIT-0433-A | TODO | Revalidated 2026-01-07 (open findings). | | REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. | | SPRINT-312-003 | DONE | Postgres-first storage driver migration with seed-fs payload contract wired in Program startup (pack/provenance/attestation payload channel). | +| SPRINT-20260305-002 | DONE | Finalized startup contract: seed-fs is the only accepted object-store driver; rustfs/unknown drivers fail fast with deterministic error messages. | diff --git a/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/TASKS.md b/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/TASKS.md index a2c8c9bdd..e301a4f30 100644 --- a/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/TASKS.md +++ b/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/TASKS.md @@ -6,3 +6,4 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol | --- | --- | --- | | REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/StellaOps.TaskRunner.Tests.md. | | REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. | +| SPRINT-20260305-002 | DONE | Added `TaskRunnerStartupContractTests` covering postgres non-dev fail-fast and object-store driver contract (`seed-fs` only, rustfs/unknown rejected). | diff --git a/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/TaskRunnerStartupContractTests.cs b/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/TaskRunnerStartupContractTests.cs new file mode 100644 index 000000000..43f795255 --- /dev/null +++ b/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/TaskRunnerStartupContractTests.cs @@ -0,0 +1,70 @@ +using System.Net; +using Microsoft.AspNetCore.Mvc.Testing; + +namespace StellaOps.TaskRunner.Tests; + +[Collection(TaskRunnerStartupEnvironmentCollection.Name)] +public sealed class TaskRunnerStartupContractTests +{ + [Fact] + public void Startup_FailsWithoutPostgresConnectionString_InProduction() + { + using var environment = TaskRunnerStartupEnvironmentScope.ProductionPostgresWithoutConnection(); + using var factory = new WebApplicationFactory(); + + var exception = Assert.ThrowsAny(() => + { + using var client = factory.CreateClient(); + }); + + Assert.Contains( + "TaskRunner requires PostgreSQL connection settings in non-development mode.", + exception.ToString(), + StringComparison.Ordinal); + } + + [Fact] + public void Startup_RejectsRustFsObjectStoreDriver() + { + using var environment = TaskRunnerStartupEnvironmentScope.ProductionWithObjectStoreDriver("rustfs"); + using var factory = new WebApplicationFactory(); + + var exception = Assert.ThrowsAny(() => + { + using var client = factory.CreateClient(); + }); + + Assert.Contains( + "RustFS object store is configured for TaskRunner, but no RustFS adapter is implemented. Use seed-fs.", + exception.ToString(), + StringComparison.Ordinal); + } + + [Fact] + public void Startup_RejectsUnsupportedObjectStoreDriver() + { + using var environment = TaskRunnerStartupEnvironmentScope.ProductionWithObjectStoreDriver("unknown-store"); + using var factory = new WebApplicationFactory(); + + var exception = Assert.ThrowsAny(() => + { + using var client = factory.CreateClient(); + }); + + Assert.Contains( + "Unsupported object store driver 'unknown-store' for TaskRunner. Allowed values: seed-fs.", + exception.ToString(), + StringComparison.Ordinal); + } + + [Fact] + public async Task Startup_AllowsSeedFsObjectStoreDriver() + { + using var environment = TaskRunnerStartupEnvironmentScope.TestingInMemorySeedFs(); + using var factory = new WebApplicationFactory(); + + using var client = factory.CreateClient(); + var response = await client.GetAsync("/.well-known/openapi", TestContext.Current.CancellationToken); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } +} diff --git a/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/TaskRunnerStartupEnvironmentScope.cs b/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/TaskRunnerStartupEnvironmentScope.cs new file mode 100644 index 000000000..344c21754 --- /dev/null +++ b/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/TaskRunnerStartupEnvironmentScope.cs @@ -0,0 +1,96 @@ +namespace StellaOps.TaskRunner.Tests; + +[CollectionDefinition(Name, DisableParallelization = true)] +public sealed class TaskRunnerStartupEnvironmentCollection +{ + public const string Name = "TaskRunnerStartupEnvironment"; +} + +internal sealed class TaskRunnerStartupEnvironmentScope : IDisposable +{ + private static readonly string[] ManagedKeys = + [ + "DOTNET_ENVIRONMENT", + "ASPNETCORE_ENVIRONMENT", + "STORAGE__DRIVER", + "TASKRUNNER__STORAGE__DRIVER", + "STORAGE__OBJECTSTORE__DRIVER", + "TASKRUNNER__STORAGE__OBJECTSTORE__DRIVER", + "STORAGE__POSTGRES__CONNECTIONSTRING", + "TASKRUNNER__STORAGE__POSTGRES__CONNECTIONSTRING", + "CONNECTIONSTRINGS__TASKRUNNER", + "CONNECTIONSTRINGS__DEFAULT" + ]; + + private readonly Dictionary _originalValues = new(StringComparer.Ordinal); + + private TaskRunnerStartupEnvironmentScope() + { + foreach (var key in ManagedKeys) + { + _originalValues[key] = Environment.GetEnvironmentVariable(key); + } + } + + public static TaskRunnerStartupEnvironmentScope ProductionPostgresWithoutConnection() + { + var scope = new TaskRunnerStartupEnvironmentScope(); + scope.Set("DOTNET_ENVIRONMENT", "Production"); + scope.Set("ASPNETCORE_ENVIRONMENT", "Production"); + scope.Set("STORAGE__DRIVER", "postgres"); + scope.Set("TASKRUNNER__STORAGE__DRIVER", "postgres"); + scope.Set("TASKRUNNER__STORAGE__OBJECTSTORE__DRIVER", "seed-fs"); + scope.Set("STORAGE__OBJECTSTORE__DRIVER", "seed-fs"); + scope.Set("STORAGE__POSTGRES__CONNECTIONSTRING", null); + scope.Set("TASKRUNNER__STORAGE__POSTGRES__CONNECTIONSTRING", null); + scope.Set("CONNECTIONSTRINGS__TASKRUNNER", null); + scope.Set("CONNECTIONSTRINGS__DEFAULT", null); + return scope; + } + + public static TaskRunnerStartupEnvironmentScope ProductionWithObjectStoreDriver(string objectStoreDriver) + { + var scope = new TaskRunnerStartupEnvironmentScope(); + scope.Set("DOTNET_ENVIRONMENT", "Production"); + scope.Set("ASPNETCORE_ENVIRONMENT", "Production"); + scope.Set("STORAGE__DRIVER", "postgres"); + scope.Set("TASKRUNNER__STORAGE__DRIVER", "postgres"); + scope.Set("TASKRUNNER__STORAGE__OBJECTSTORE__DRIVER", objectStoreDriver); + scope.Set("STORAGE__OBJECTSTORE__DRIVER", objectStoreDriver); + var connectionString = "Host=localhost;Database=stellaops_taskrunner;Username=stellaops;Password=stellaops"; + scope.Set("STORAGE__POSTGRES__CONNECTIONSTRING", connectionString); + scope.Set("TASKRUNNER__STORAGE__POSTGRES__CONNECTIONSTRING", connectionString); + scope.Set("CONNECTIONSTRINGS__TASKRUNNER", connectionString); + scope.Set("CONNECTIONSTRINGS__DEFAULT", connectionString); + return scope; + } + + public static TaskRunnerStartupEnvironmentScope TestingInMemorySeedFs() + { + var scope = new TaskRunnerStartupEnvironmentScope(); + scope.Set("DOTNET_ENVIRONMENT", "Testing"); + scope.Set("ASPNETCORE_ENVIRONMENT", "Testing"); + scope.Set("STORAGE__DRIVER", "inmemory"); + scope.Set("TASKRUNNER__STORAGE__DRIVER", "inmemory"); + scope.Set("TASKRUNNER__STORAGE__OBJECTSTORE__DRIVER", "seed-fs"); + scope.Set("STORAGE__OBJECTSTORE__DRIVER", "seed-fs"); + scope.Set("STORAGE__POSTGRES__CONNECTIONSTRING", null); + scope.Set("TASKRUNNER__STORAGE__POSTGRES__CONNECTIONSTRING", null); + scope.Set("CONNECTIONSTRINGS__TASKRUNNER", null); + scope.Set("CONNECTIONSTRINGS__DEFAULT", null); + return scope; + } + + public void Dispose() + { + foreach (var entry in _originalValues) + { + Environment.SetEnvironmentVariable(entry.Key, entry.Value); + } + } + + private void Set(string key, string? value) + { + Environment.SetEnvironmentVariable(key, value); + } +} diff --git a/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.WebService/Program.cs b/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.WebService/Program.cs index c92506e48..d6ea5f65c 100644 --- a/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.WebService/Program.cs +++ b/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.WebService/Program.cs @@ -60,7 +60,7 @@ builder.Services.AddStellaOpsTelemetry( var storageDriver = ResolveStorageDriver(builder.Configuration, "TaskRunner"); RegisterStateStores(builder.Services, builder.Configuration, builder.Environment.IsDevelopment(), storageDriver); -ValidateObjectStoreContract(builder.Configuration, builder.Environment.IsDevelopment(), "TaskRunner"); +ValidateObjectStoreContract(builder.Configuration, "TaskRunner"); builder.Services.AddSingleton(sp => { @@ -1066,27 +1066,19 @@ static string? ResolveSchemaName(IConfiguration configuration, string serviceNam configuration[$"Postgres:{serviceName}:SchemaName"]); } -static void ValidateObjectStoreContract(IConfiguration configuration, bool isDevelopment, string serviceName) +static void ValidateObjectStoreContract(IConfiguration configuration, string serviceName) { var objectStoreDriver = ResolveObjectStoreDriver(configuration, serviceName); - if (!string.Equals(objectStoreDriver, "seed-fs", StringComparison.OrdinalIgnoreCase) && - !string.Equals(objectStoreDriver, "rustfs", StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(objectStoreDriver, "seed-fs", StringComparison.OrdinalIgnoreCase)) { - throw new InvalidOperationException( - $"Unsupported object store driver '{objectStoreDriver}' for {serviceName}. Allowed values: seed-fs, rustfs."); - } - - if (string.Equals(objectStoreDriver, "rustfs", StringComparison.OrdinalIgnoreCase) && !isDevelopment) - { - var rustFsBaseUrl = FirstNonEmpty( - configuration[$"{serviceName}:Storage:ObjectStore:RustFs:BaseUrl"], - configuration["Storage:ObjectStore:RustFs:BaseUrl"]); - - if (string.IsNullOrWhiteSpace(rustFsBaseUrl)) + if (string.Equals(objectStoreDriver, "rustfs", StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException( - $"RustFS object store is configured for {serviceName}, but BaseUrl is missing."); + $"RustFS object store is configured for {serviceName}, but no RustFS adapter is implemented. Use seed-fs."); } + + throw new InvalidOperationException( + $"Unsupported object store driver '{objectStoreDriver}' for {serviceName}. Allowed values: seed-fs."); } } @@ -1342,5 +1334,7 @@ internal static class RunStateMapper } } +public partial class Program; + diff --git a/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.WebService/TASKS.md b/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.WebService/TASKS.md index 1c9cc4da1..0f87758ef 100644 --- a/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.WebService/TASKS.md +++ b/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.WebService/TASKS.md @@ -7,3 +7,4 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol | REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.WebService/StellaOps.TaskRunner.WebService.md. | | REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. | | SPRINT-312-004 | DONE | Runtime storage driver migration verified: Postgres state/log/approval default plus seed-fs artifact object-store path. | +| SPRINT-20260305-002 | DONE | Startup contract hardened: non-dev postgres missing-connection fails fast; object-store accepts seed-fs only and rejects rustfs/unknown values. | diff --git a/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.Worker/Program.cs b/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.Worker/Program.cs index f5cabaf7b..308707825 100644 --- a/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.Worker/Program.cs +++ b/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.Worker/Program.cs @@ -58,7 +58,7 @@ builder.Services.AddStellaOpsTelemetry( var storageDriver = ResolveStorageDriver(builder.Configuration, "TaskRunner"); RegisterStateStores(builder.Services, builder.Configuration, builder.Environment.IsDevelopment(), storageDriver); -ValidateObjectStoreContract(builder.Configuration, builder.Environment.IsDevelopment(), "TaskRunner"); +ValidateObjectStoreContract(builder.Configuration, "TaskRunner"); builder.Services.AddSingleton(sp => { @@ -179,27 +179,19 @@ static string? ResolveSchemaName(IConfiguration configuration, string serviceNam configuration[$"Postgres:{serviceName}:SchemaName"]); } -static void ValidateObjectStoreContract(IConfiguration configuration, bool isDevelopment, string serviceName) +static void ValidateObjectStoreContract(IConfiguration configuration, string serviceName) { var objectStoreDriver = ResolveObjectStoreDriver(configuration, serviceName); - if (!string.Equals(objectStoreDriver, "seed-fs", StringComparison.OrdinalIgnoreCase) && - !string.Equals(objectStoreDriver, "rustfs", StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(objectStoreDriver, "seed-fs", StringComparison.OrdinalIgnoreCase)) { - throw new InvalidOperationException( - $"Unsupported object store driver '{objectStoreDriver}' for {serviceName}. Allowed values: seed-fs, rustfs."); - } - - if (string.Equals(objectStoreDriver, "rustfs", StringComparison.OrdinalIgnoreCase) && !isDevelopment) - { - var rustFsBaseUrl = FirstNonEmpty( - configuration[$"{serviceName}:Storage:ObjectStore:RustFs:BaseUrl"], - configuration["Storage:ObjectStore:RustFs:BaseUrl"]); - - if (string.IsNullOrWhiteSpace(rustFsBaseUrl)) + if (string.Equals(objectStoreDriver, "rustfs", StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException( - $"RustFS object store is configured for {serviceName}, but BaseUrl is missing."); + $"RustFS object store is configured for {serviceName}, but no RustFS adapter is implemented. Use seed-fs."); } + + throw new InvalidOperationException( + $"Unsupported object store driver '{objectStoreDriver}' for {serviceName}. Allowed values: seed-fs."); } } diff --git a/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.Worker/TASKS.md b/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.Worker/TASKS.md index 6f9ff5a44..2e8e377be 100644 --- a/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.Worker/TASKS.md +++ b/src/JobEngine/StellaOps.TaskRunner/StellaOps.TaskRunner.Worker/TASKS.md @@ -7,3 +7,4 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol | REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Worker/StellaOps.TaskRunner.Worker.md. | | REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. | | SPRINT-312-004 | DONE | Worker storage wiring aligned to Postgres state/log/approval and seed-fs artifact/provenance object-store contract. | +| SPRINT-20260305-002 | DONE | Worker startup contract now rejects rustfs/unknown object-store drivers and keeps seed-fs as the deterministic supported payload channel. | diff --git a/src/Notify/StellaOps.Notify.WebService/Program.cs b/src/Notify/StellaOps.Notify.WebService/Program.cs index 700af0823..a3b9d3440 100644 --- a/src/Notify/StellaOps.Notify.WebService/Program.cs +++ b/src/Notify/StellaOps.Notify.WebService/Program.cs @@ -903,6 +903,36 @@ static void ConfigureEndpoints(WebApplication app) .RequireAuthorization(NotifyPolicies.Viewer) .RequireRateLimiting(NotifyRateLimitPolicies.DeliveryHistory); + apiGroup.MapGet("/delivery/stats", async ( + IDeliveryRepository repository, + HttpContext context, + CancellationToken cancellationToken) => + { + if (!TryResolveTenant(context, tenantHeader, out var tenant, out var error)) + { + return error!; + } + + var now = DateTimeOffset.UtcNow; + var stats = await repository.GetStatsAsync(tenant!, now.AddHours(-24), now, cancellationToken); + var totalCompleted = stats.Sent + stats.Delivered + stats.Failed + stats.Bounced; + var successCount = stats.Sent + stats.Delivered; + var rate = totalCompleted > 0 ? (double)successCount / totalCompleted * 100.0 : 0.0; + + return Results.Ok(new + { + totalSent = stats.Sent + stats.Delivered, + totalFailed = stats.Failed + stats.Bounced, + totalPending = stats.Pending, + successRate = Math.Round(rate, 1), + windowHours = 24, + evaluatedAt = now + }); + }) + .WithName("NotifyDeliveryStats") + .WithDescription("Get delivery statistics for the last 24 hours") + .RequireAuthorization(NotifyPolicies.Viewer); + apiGroup.MapGet("/deliveries/{deliveryId}", async ( string deliveryId, IDeliveryRepository repository, diff --git a/src/Platform/StellaOps.Platform.WebService/Program.cs b/src/Platform/StellaOps.Platform.WebService/Program.cs index 890aff508..8f0364130 100644 --- a/src/Platform/StellaOps.Platform.WebService/Program.cs +++ b/src/Platform/StellaOps.Platform.WebService/Program.cs @@ -189,6 +189,7 @@ builder.Services.AddHttpClient("AuthorityInternal", client => builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(sp => sp.GetRequiredService()); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/src/Platform/StellaOps.Platform.WebService/Services/IntegrationsReadModelService.cs b/src/Platform/StellaOps.Platform.WebService/Services/IntegrationsReadModelService.cs index e19ea79b2..cafe0e47c 100644 --- a/src/Platform/StellaOps.Platform.WebService/Services/IntegrationsReadModelService.cs +++ b/src/Platform/StellaOps.Platform.WebService/Services/IntegrationsReadModelService.cs @@ -34,14 +34,14 @@ public sealed class IntegrationsReadModelService ]; private readonly IReleaseControlBundleStore bundleStore; - private readonly PlatformContextService contextService; + private readonly IPlatformContextQuery contextQuery; public IntegrationsReadModelService( IReleaseControlBundleStore bundleStore, - PlatformContextService contextService) + IPlatformContextQuery contextQuery) { this.bundleStore = bundleStore ?? throw new ArgumentNullException(nameof(bundleStore)); - this.contextService = contextService ?? throw new ArgumentNullException(nameof(contextService)); + this.contextQuery = contextQuery ?? throw new ArgumentNullException(nameof(contextQuery)); } public async Task> ListFeedsAsync( @@ -110,7 +110,7 @@ public sealed class IntegrationsReadModelService PlatformRequestContext context, CancellationToken cancellationToken) { - var environments = await contextService.GetEnvironmentsAsync(null, cancellationToken).ConfigureAwait(false); + var environments = await contextQuery.GetEnvironmentsAsync(null, cancellationToken).ConfigureAwait(false); var runs = await bundleStore.ListMaterializationRunsAsync( context.TenantId, RunScanLimit, diff --git a/src/Platform/StellaOps.Platform.WebService/Services/PlatformContextService.cs b/src/Platform/StellaOps.Platform.WebService/Services/PlatformContextService.cs index e7439898d..b68c75c2d 100644 --- a/src/Platform/StellaOps.Platform.WebService/Services/PlatformContextService.cs +++ b/src/Platform/StellaOps.Platform.WebService/Services/PlatformContextService.cs @@ -25,7 +25,17 @@ public interface IPlatformContextStore CancellationToken cancellationToken = default); } -public sealed class PlatformContextService +public interface IPlatformContextQuery +{ + Task> GetRegionsAsync( + CancellationToken cancellationToken = default); + + Task> GetEnvironmentsAsync( + IReadOnlyList? regionFilter, + CancellationToken cancellationToken = default); +} + +public sealed class PlatformContextService : IPlatformContextQuery { private static readonly string[] AllowedTimeWindows = ["1h", "24h", "7d", "30d", "90d"]; private const string DefaultTimeWindow = "24h"; diff --git a/src/Platform/StellaOps.Platform.WebService/Services/PostgresTranslationStore.cs b/src/Platform/StellaOps.Platform.WebService/Services/PostgresTranslationStore.cs index c16a998d9..378408f13 100644 --- a/src/Platform/StellaOps.Platform.WebService/Services/PostgresTranslationStore.cs +++ b/src/Platform/StellaOps.Platform.WebService/Services/PostgresTranslationStore.cs @@ -57,16 +57,25 @@ public sealed class PostgresTranslationStore : ITranslationStore { ct.ThrowIfCancellationRequested(); var result = new Dictionary(StringComparer.Ordinal); - - await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false); - await using var cmd = new NpgsqlCommand(SelectAllSql, conn); - cmd.Parameters.AddWithValue("tenant_id", tenantId); - cmd.Parameters.AddWithValue("locale", locale); - - await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false); - while (await reader.ReadAsync(ct).ConfigureAwait(false)) + try { - result[reader.GetString(0)] = reader.GetString(1); + await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false); + await using var cmd = new NpgsqlCommand(SelectAllSql, conn); + cmd.Parameters.AddWithValue("tenant_id", tenantId); + cmd.Parameters.AddWithValue("locale", locale); + + await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false); + while (await reader.ReadAsync(ct).ConfigureAwait(false)) + { + result[reader.GetString(0)] = reader.GetString(1); + } + } + catch (Npgsql.PostgresException ex) when (IsMissingTranslationsTable(ex)) + { + _logger.LogWarning( + "platform.translations table is missing; returning empty translation set for tenant {TenantId} locale {Locale}", + tenantId, + locale); } return result; @@ -78,17 +87,27 @@ public sealed class PostgresTranslationStore : ITranslationStore ct.ThrowIfCancellationRequested(); var result = new Dictionary(StringComparer.Ordinal); var prefix = keyPrefix.EndsWith('.') ? keyPrefix : keyPrefix + "."; - - await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false); - await using var cmd = new NpgsqlCommand(SelectByPrefixSql, conn); - cmd.Parameters.AddWithValue("tenant_id", tenantId); - cmd.Parameters.AddWithValue("locale", locale); - cmd.Parameters.AddWithValue("prefix", prefix + "%"); - - await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false); - while (await reader.ReadAsync(ct).ConfigureAwait(false)) + try { - result[reader.GetString(0)] = reader.GetString(1); + await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false); + await using var cmd = new NpgsqlCommand(SelectByPrefixSql, conn); + cmd.Parameters.AddWithValue("tenant_id", tenantId); + cmd.Parameters.AddWithValue("locale", locale); + cmd.Parameters.AddWithValue("prefix", prefix + "%"); + + await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false); + while (await reader.ReadAsync(ct).ConfigureAwait(false)) + { + result[reader.GetString(0)] = reader.GetString(1); + } + } + catch (Npgsql.PostgresException ex) when (IsMissingTranslationsTable(ex)) + { + _logger.LogWarning( + "platform.translations table is missing; returning empty translation prefix set for tenant {TenantId} locale {Locale} prefix {Prefix}", + tenantId, + locale, + keyPrefix); } return result; @@ -154,17 +173,28 @@ public sealed class PostgresTranslationStore : ITranslationStore { ct.ThrowIfCancellationRequested(); var locales = new List(); - - await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false); - await using var cmd = new NpgsqlCommand(SelectLocalesSql, conn); - cmd.Parameters.AddWithValue("tenant_id", tenantId); - - await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false); - while (await reader.ReadAsync(ct).ConfigureAwait(false)) + try { - locales.Add(reader.GetString(0)); + await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false); + await using var cmd = new NpgsqlCommand(SelectLocalesSql, conn); + cmd.Parameters.AddWithValue("tenant_id", tenantId); + + await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false); + while (await reader.ReadAsync(ct).ConfigureAwait(false)) + { + locales.Add(reader.GetString(0)); + } + } + catch (Npgsql.PostgresException ex) when (IsMissingTranslationsTable(ex)) + { + _logger.LogWarning( + "platform.translations table is missing; returning no DB locales for tenant {TenantId}", + tenantId); } return locales; } + + private static bool IsMissingTranslationsTable(Npgsql.PostgresException ex) + => string.Equals(ex.SqlState, "42P01", StringComparison.Ordinal); } diff --git a/src/Platform/StellaOps.Platform.WebService/Services/SecurityReadModelService.cs b/src/Platform/StellaOps.Platform.WebService/Services/SecurityReadModelService.cs index 76be34535..d176fe3b2 100644 --- a/src/Platform/StellaOps.Platform.WebService/Services/SecurityReadModelService.cs +++ b/src/Platform/StellaOps.Platform.WebService/Services/SecurityReadModelService.cs @@ -22,14 +22,14 @@ public sealed class SecurityReadModelService private static readonly string[] LicenseCatalog = ["Apache-2.0", "MIT", "BUSL-1.1", "BSD-3-Clause", "MPL-2.0"]; private readonly IReleaseControlBundleStore bundleStore; - private readonly PlatformContextService contextService; + private readonly IPlatformContextQuery contextQuery; public SecurityReadModelService( IReleaseControlBundleStore bundleStore, - PlatformContextService contextService) + IPlatformContextQuery contextQuery) { this.bundleStore = bundleStore ?? throw new ArgumentNullException(nameof(bundleStore)); - this.contextService = contextService ?? throw new ArgumentNullException(nameof(contextService)); + this.contextQuery = contextQuery ?? throw new ArgumentNullException(nameof(contextQuery)); } public async Task ListFindingsAsync( @@ -204,7 +204,7 @@ public sealed class SecurityReadModelService PlatformRequestContext context, CancellationToken cancellationToken) { - var environments = await contextService.GetEnvironmentsAsync(null, cancellationToken).ConfigureAwait(false); + var environments = await contextQuery.GetEnvironmentsAsync(null, cancellationToken).ConfigureAwait(false); var environmentById = environments.ToDictionary(item => item.EnvironmentId, StringComparer.Ordinal); var bundles = await bundleStore.ListBundlesAsync( diff --git a/src/Platform/StellaOps.Platform.WebService/Services/TopologyReadModelService.cs b/src/Platform/StellaOps.Platform.WebService/Services/TopologyReadModelService.cs index 351fb8c1d..eb9305c5f 100644 --- a/src/Platform/StellaOps.Platform.WebService/Services/TopologyReadModelService.cs +++ b/src/Platform/StellaOps.Platform.WebService/Services/TopologyReadModelService.cs @@ -17,14 +17,14 @@ public sealed class TopologyReadModelService private static readonly DateTimeOffset ProjectionEpoch = DateTimeOffset.UnixEpoch; private readonly IReleaseControlBundleStore bundleStore; - private readonly PlatformContextService contextService; + private readonly IPlatformContextQuery contextQuery; public TopologyReadModelService( IReleaseControlBundleStore bundleStore, - PlatformContextService contextService) + IPlatformContextQuery contextQuery) { this.bundleStore = bundleStore ?? throw new ArgumentNullException(nameof(bundleStore)); - this.contextService = contextService ?? throw new ArgumentNullException(nameof(contextService)); + this.contextQuery = contextQuery ?? throw new ArgumentNullException(nameof(contextQuery)); } public async Task> ListRegionsAsync( @@ -248,8 +248,8 @@ public sealed class TopologyReadModelService PlatformRequestContext context, CancellationToken cancellationToken) { - var regions = await contextService.GetRegionsAsync(cancellationToken).ConfigureAwait(false); - var environments = await contextService.GetEnvironmentsAsync(null, cancellationToken).ConfigureAwait(false); + var regions = await contextQuery.GetRegionsAsync(cancellationToken).ConfigureAwait(false); + var environments = await contextQuery.GetEnvironmentsAsync(null, cancellationToken).ConfigureAwait(false); var bundles = await bundleStore.ListBundlesAsync( context.TenantId, diff --git a/src/Platform/StellaOps.Platform.WebService/TASKS.md b/src/Platform/StellaOps.Platform.WebService/TASKS.md index b2302f669..4677e8978 100644 --- a/src/Platform/StellaOps.Platform.WebService/TASKS.md +++ b/src/Platform/StellaOps.Platform.WebService/TASKS.md @@ -46,4 +46,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229 | SPRINT_20260224_004-LOC-308 | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: platform locale catalog endpoint (`GET /api/v1/platform/localization/locales`) is now consumed by both UI and CLI locale-selection paths. | | SPRINT_20260226_230-LOC-001 | DONE | Sprint `docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_230_Platform_locale_label_translation_corrections.md`: completed non-English translation correction across Platform/Web/shared localization bundles (`bg-BG`, `de-DE`, `es-ES`, `fr-FR`, `ru-RU`, `uk-UA`, `zh-CN`, `zh-TW`), including cleanup of placeholder/transliteration/malformed values (`Ezik`, leaked token markers, mojibake) and a context-quality pass for backend German resource bundles (`graph`, `policy`, `scanner`, `advisoryai`). | | PLATFORM-223-001 | DONE | Sprint `docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_223_Platform_score_explain_contract_and_replay_alignment.md`: shipped deterministic score explain/replay contract completion (`unknowns`, `proof_ref`, deterministic replay envelope parsing/verification differences) and updated score API/module docs with contract notes. | +| SPRINT_20260305_005-PLATFORM-BOUND-001 | DONE | Sprint `docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_005_Platform_read_model_boundary_enforcement.md`: captured Platform runtime dependency inventory with explicit allowed runtime, migration-only, and prohibited coupling categories in architecture docs. | +| SPRINT_20260305_005-PLATFORM-BOUND-003 | DONE | Sprint `docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_005_Platform_read_model_boundary_enforcement.md`: introduced `IPlatformContextQuery` and switched topology/security/integrations read-model services to explicit query contracts; DI now binds read-model contract separately from context mutation service. | +| SPRINT_20260305_005-PLATFORM-BOUND-004 | DONE | Sprint `docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_005_Platform_read_model_boundary_enforcement.md`: documented runtime boundary policy, migration/runtime separation, and allowlisted exceptions in Platform dossiers. | diff --git a/src/Platform/__Tests/StellaOps.Platform.WebService.Tests/PlatformRuntimeBoundaryGuardTests.cs b/src/Platform/__Tests/StellaOps.Platform.WebService.Tests/PlatformRuntimeBoundaryGuardTests.cs new file mode 100644 index 000000000..422fc8e91 --- /dev/null +++ b/src/Platform/__Tests/StellaOps.Platform.WebService.Tests/PlatformRuntimeBoundaryGuardTests.cs @@ -0,0 +1,137 @@ +using StellaOps.Platform.WebService.Services; +using StellaOps.TestKit; +using System.Reflection; + +namespace StellaOps.Platform.WebService.Tests; + +public sealed class PlatformRuntimeBoundaryGuardTests +{ + private static readonly IReadOnlyDictionary ApprovedConstructorDependencies = + new Dictionary + { + [typeof(ReleaseReadModelService)] = [typeof(IReleaseControlBundleStore)], + [typeof(TopologyReadModelService)] = [typeof(IReleaseControlBundleStore), typeof(IPlatformContextQuery)], + [typeof(SecurityReadModelService)] = [typeof(IReleaseControlBundleStore), typeof(IPlatformContextQuery)], + [typeof(IntegrationsReadModelService)] = [typeof(IReleaseControlBundleStore), typeof(IPlatformContextQuery)], + }; + + [Trait("Category", TestCategories.Unit)] + [Fact] + public void RuntimeReadModelServices_UseOnlyApprovedConstructorContracts() + { + var violations = new List(); + + foreach (var (serviceType, expectedDependencies) in ApprovedConstructorDependencies) + { + var constructors = serviceType + .GetConstructors(BindingFlags.Instance | BindingFlags.Public) + .Where(static ctor => !ctor.IsStatic) + .ToArray(); + + if (constructors.Length != 1) + { + violations.Add($"{serviceType.Name}: expected exactly one public constructor, found {constructors.Length}."); + continue; + } + + var actualDependencies = constructors[0] + .GetParameters() + .Select(static parameter => parameter.ParameterType) + .ToArray(); + + if (actualDependencies.Length != expectedDependencies.Length) + { + violations.Add( + $"{serviceType.Name}: expected {FormatTypeList(expectedDependencies)} but found {FormatTypeList(actualDependencies)}."); + continue; + } + + for (var i = 0; i < expectedDependencies.Length; i++) + { + if (actualDependencies[i] != expectedDependencies[i]) + { + violations.Add( + $"{serviceType.Name}: constructor contract mismatch at position {i + 1}; expected {expectedDependencies[i].FullName}, found {actualDependencies[i].FullName}."); + } + } + } + + Assert.True( + violations.Count == 0, + "Runtime read-model constructor contracts drifted from approved boundary.\n" + + string.Join(Environment.NewLine, violations)); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public void RuntimeSourceFiles_DoNotReferenceForeignPersistenceOutsideAllowlist() + { + var platformRoot = FindPlatformRoot(); + var scannedRoots = new[] + { + Path.Combine(platformRoot, "StellaOps.Platform.WebService"), + Path.Combine(platformRoot, "__Libraries", "StellaOps.Platform.Database") + }; + + var allowlistedFiles = new HashSet(StringComparer.OrdinalIgnoreCase) + { + NormalizePath(Path.Combine("StellaOps.Platform.WebService", "Endpoints", "SeedEndpoints.cs")), + NormalizePath(Path.Combine("__Libraries", "StellaOps.Platform.Database", "MigrationModulePlugins.cs")), + }; + + var violations = new List(); + + foreach (var root in scannedRoots) + { + foreach (var file in Directory.GetFiles(root, "*.cs", SearchOption.AllDirectories)) + { + var relativePath = NormalizePath(Path.GetRelativePath(platformRoot, file)); + + foreach (var (line, lineNumber) in File.ReadLines(file).Select((value, index) => (value, index + 1))) + { + if (!line.TrimStart().StartsWith("using StellaOps.", StringComparison.Ordinal) + || !line.Contains(".Persistence", StringComparison.Ordinal) + || allowlistedFiles.Contains(relativePath)) + { + continue; + } + + violations.Add($"{relativePath}:{lineNumber}: {line.Trim()}"); + } + } + } + + Assert.True( + violations.Count == 0, + "Foreign persistence references are only allowed in explicit migration/seed boundaries. Violations:\n" + + string.Join(Environment.NewLine, violations)); + } + + private static string FormatTypeList(IEnumerable types) + { + return string.Join(", ", types.Select(static type => type.FullName ?? type.Name)); + } + + private static string FindPlatformRoot() + { + var current = new DirectoryInfo(AppContext.BaseDirectory); + while (current is not null) + { + var candidate = Path.Combine(current.FullName, "src", "Platform"); + if (Directory.Exists(Path.Combine(candidate, "StellaOps.Platform.WebService")) + && Directory.Exists(Path.Combine(candidate, "__Libraries", "StellaOps.Platform.Database"))) + { + return candidate; + } + + current = current.Parent; + } + + throw new DirectoryNotFoundException("Could not locate src/Platform root for runtime boundary guard tests."); + } + + private static string NormalizePath(string path) + { + return path.Replace('\\', '/'); + } +} diff --git a/src/Platform/__Tests/StellaOps.Platform.WebService.Tests/TASKS.md b/src/Platform/__Tests/StellaOps.Platform.WebService.Tests/TASKS.md index 4d5c084b7..133842bf4 100644 --- a/src/Platform/__Tests/StellaOps.Platform.WebService.Tests/TASKS.md +++ b/src/Platform/__Tests/StellaOps.Platform.WebService.Tests/TASKS.md @@ -23,3 +23,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229 | SPRINT_20260224_004-LOC-302-T | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: added language preference endpoint coverage in `PreferencesEndpointsTests` (round-trip persistence + invalid locale rejection) and expanded locale catalog verification in `LocalizationEndpointsTests`. | | SPRINT_20260224_004-LOC-305-T | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: extended `LocalizationEndpointsTests` to verify common-layer and `platform.*` namespace bundle availability for all supported locales. | | SPRINT_20260224_004-LOC-307-T | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: extended localization and preference endpoint tests for Ukrainian rollout (`uk-UA` locale catalog/bundle assertions and alias normalization to canonical `uk-UA`). | +| SPRINT_20260305_005-PLATFORM-BOUND-002 | DONE | Sprint `docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_005_Platform_read_model_boundary_enforcement.md`: added `PlatformRuntimeBoundaryGuardTests` to enforce approved read-model constructor contracts and disallow foreign persistence references outside explicit migration/seed allowlist files. | diff --git a/src/Remediation/AGENTS.md b/src/Remediation/AGENTS.md index c17880aac..c4c650546 100644 --- a/src/Remediation/AGENTS.md +++ b/src/Remediation/AGENTS.md @@ -19,7 +19,7 @@ - `docs/07_HIGH_LEVEL_ARCHITECTURE.md` - `docs/modules/platform/architecture.md` - `docs/modules/remediation/architecture.md` -- `docs/implplan/SPRINT_20260305_004_Remediation_postgres_runtime_wiring.md` +- `docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_004_Remediation_postgres_runtime_wiring.md` ## Working Agreement - Update sprint task state (`TODO`, `DOING`, `DONE`, `BLOCKED`) in `docs/implplan/SPRINT_*.md` as work progresses. diff --git a/src/Replay/StellaOps.Replay.WebService/TASKS.md b/src/Replay/StellaOps.Replay.WebService/TASKS.md index f4b582c93..7ef596939 100644 --- a/src/Replay/StellaOps.Replay.WebService/TASKS.md +++ b/src/Replay/StellaOps.Replay.WebService/TASKS.md @@ -7,3 +7,4 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol | REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Replay/StellaOps.Replay.WebService/StellaOps.Replay.WebService.md. | | REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. | | SPRINT-312-006 | DONE | Added Postgres snapshot index + seed-fs snapshot blob stores and wired storage-driver registration in webservice startup. | +| SPRINT-20260305-003 | DONE | Replay storage contract closed: object-store narrowed to seed-fs only with deterministic rustfs/unknown-driver startup rejection and synced docs. | diff --git a/src/Replay/__Tests/StellaOps.Replay.Core.Tests/FeedSnapshots/PointInTimeQueryApiIntegrationTests.cs b/src/Replay/__Tests/StellaOps.Replay.Core.Tests/FeedSnapshots/PointInTimeQueryApiIntegrationTests.cs index 2438ee4ef..2df61f85b 100644 --- a/src/Replay/__Tests/StellaOps.Replay.Core.Tests/FeedSnapshots/PointInTimeQueryApiIntegrationTests.cs +++ b/src/Replay/__Tests/StellaOps.Replay.Core.Tests/FeedSnapshots/PointInTimeQueryApiIntegrationTests.cs @@ -4,11 +4,20 @@ using System.Net; using System.Net.Http.Json; +using System.Security.Claims; using System.Text.Json; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; using FluentAssertions; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using StellaOps.Replay.WebService; using Xunit; @@ -23,6 +32,7 @@ public sealed class PointInTimeQueryApiIntegrationTests { await using var factory = CreateFactory(); using var client = factory.CreateClient(); + ConfigureClient(client); var cveId = "CVE-2024-7777"; var providerId = "nvd-e2e"; @@ -112,6 +122,7 @@ public sealed class PointInTimeQueryApiIntegrationTests { await using var factory = CreateFactory(); using var client = factory.CreateClient(); + ConfigureClient(client); var response = await client.PostAsJsonAsync( "/v1/pit/advisory/diff", @@ -146,6 +157,67 @@ public sealed class PointInTimeQueryApiIntegrationTests ["Replay:Authority:RequireHttpsMetadata"] = "false", }); }); + builder.ConfigureTestServices(services => + { + services.AddAuthentication(TestReplayAuthHandler.SchemeName) + .AddScheme( + TestReplayAuthHandler.SchemeName, + _ => { }); + + services.PostConfigureAll(options => + { + options.DefaultAuthenticateScheme = TestReplayAuthHandler.SchemeName; + options.DefaultChallengeScheme = TestReplayAuthHandler.SchemeName; + options.DefaultScheme = TestReplayAuthHandler.SchemeName; + }); + + services.RemoveAll(); + services.AddSingleton(); + }); }); } + + private static void ConfigureClient(HttpClient client) + { + client.DefaultRequestHeaders.TryAddWithoutValidation("X-StellaOps-Tenant", "tenant-e2e"); + } + + private sealed class TestReplayAuthHandler : AuthenticationHandler + { + public const string SchemeName = "ReplayTestScheme"; + + public TestReplayAuthHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder) + : base(options, logger, encoder) + { + } + + protected override Task HandleAuthenticateAsync() + { + var claims = new[] + { + new Claim("scope", "vuln.operate replay.token.read replay.token.write"), + new Claim("scp", "vuln.operate replay.token.read replay.token.write") + }; + + var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, SchemeName)); + var ticket = new AuthenticationTicket(principal, SchemeName); + return Task.FromResult(AuthenticateResult.Success(ticket)); + } + } + + private sealed class AllowAllAuthorizationHandler : IAuthorizationHandler + { + public Task HandleAsync(AuthorizationHandlerContext context) + { + foreach (var requirement in context.PendingRequirements.ToList()) + { + context.Succeed(requirement); + } + + return Task.CompletedTask; + } + } } diff --git a/src/Replay/__Tests/StellaOps.Replay.Core.Tests/TASKS.md b/src/Replay/__Tests/StellaOps.Replay.Core.Tests/TASKS.md index c4368f2a7..e13055f25 100644 --- a/src/Replay/__Tests/StellaOps.Replay.Core.Tests/TASKS.md +++ b/src/Replay/__Tests/StellaOps.Replay.Core.Tests/TASKS.md @@ -7,3 +7,4 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol | REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Replay/__Tests/StellaOps.Replay.Core.Tests/StellaOps.Replay.Core.Tests.md. | | REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. | | SPRINT-312-006 | DONE | Added `ReplayFeedSnapshotStoresTests` and validated Postgres index + seed-fs blob stores via class-targeted xUnit execution (3/3 pass). | +| SPRINT-20260305-003 | DONE | Added authenticated replay API integration test harness and revalidated Replay core suite (`dotnet test ...` passed 99/99). | diff --git a/src/Router/StellaOps.Gateway.WebService/TASKS.md b/src/Router/StellaOps.Gateway.WebService/TASKS.md index 397ad53ae..7aa72e965 100644 --- a/src/Router/StellaOps.Gateway.WebService/TASKS.md +++ b/src/Router/StellaOps.Gateway.WebService/TASKS.md @@ -11,3 +11,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229 | REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. | | RGH-01 | DONE | 2026-02-22: Added SPA fallback handling for browser deep links on microservice route matches; API prefixes remain backend-dispatched. | | RGH-02 | DONE | 2026-03-04: Expanded approved auth passthrough prefixes (`/authority`, `/doctor`, `/api`) to unblock authenticated gateway routes used by Audit Log UI E2E. | +| RGH-03 | DONE | 2026-03-05: Aligned `/api/v1/search*` and `/api/v1/advisory-ai*` route translations to AdvisoryAI `/v1/*`, added compose/runtime parity safeguards, and verified setup smoke coverage. | diff --git a/src/Router/StellaOps.Gateway.WebService/appsettings.json b/src/Router/StellaOps.Gateway.WebService/appsettings.json index 1151c7551..1cf1422ba 100644 --- a/src/Router/StellaOps.Gateway.WebService/appsettings.json +++ b/src/Router/StellaOps.Gateway.WebService/appsettings.json @@ -102,7 +102,8 @@ { "Type": "ReverseProxy", "Path": "/api/v1/evidence", "TranslatesTo": "http://evidencelocker.stella-ops.local/api/v1/evidence" }, { "Type": "ReverseProxy", "Path": "/api/v1/proofs", "TranslatesTo": "http://evidencelocker.stella-ops.local/api/v1/proofs" }, { "Type": "ReverseProxy", "Path": "/api/v1/timeline", "TranslatesTo": "http://timelineindexer.stella-ops.local/api/v1/timeline" }, - { "Type": "ReverseProxy", "Path": "/api/v1/advisory-ai", "TranslatesTo": "http://advisoryai.stella-ops.local/api/v1/advisory-ai" }, + { "Type": "ReverseProxy", "Path": "/api/v1/search", "TranslatesTo": "http://advisoryai.stella-ops.local/v1/search" }, + { "Type": "ReverseProxy", "Path": "/api/v1/advisory-ai", "TranslatesTo": "http://advisoryai.stella-ops.local/v1/advisory-ai" }, { "Type": "ReverseProxy", "Path": "/api/v1/advisory", "TranslatesTo": "http://advisoryai.stella-ops.local/api/v1/advisory" }, { "Type": "ReverseProxy", "Path": "/api/v1/concelier", "TranslatesTo": "http://concelier.stella-ops.local/api/v1/concelier" }, { "Type": "ReverseProxy", "Path": "/api/v1/vulnerabilities", "TranslatesTo": "http://scanner.stella-ops.local/api/v1/vulnerabilities" }, diff --git a/src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Configuration/GatewayRouteSearchMappingsTests.cs b/src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Configuration/GatewayRouteSearchMappingsTests.cs new file mode 100644 index 000000000..8f0f2889c --- /dev/null +++ b/src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Configuration/GatewayRouteSearchMappingsTests.cs @@ -0,0 +1,74 @@ +using System.Text.Json; + +namespace StellaOps.Gateway.WebService.Tests.Configuration; + +public sealed class GatewayRouteSearchMappingsTests +{ + private static readonly (string Path, string Target, string RouteType)[] RequiredMappings = + [ + ("/api/v1/search", "http://advisoryai.stella-ops.local/v1/search", "ReverseProxy"), + ("/api/v1/advisory-ai", "http://advisoryai.stella-ops.local/v1/advisory-ai", "ReverseProxy") + ]; + + public static TheoryData RouteConfigPaths => new() + { + "src/Router/StellaOps.Gateway.WebService/appsettings.json", + "devops/compose/router-gateway-local.json", + "devops/compose/router-gateway-local.reverseproxy.json" + }; + + [Theory] + [MemberData(nameof(RouteConfigPaths))] + public void RouteTable_ContainsUnifiedSearchMappingsAndKeepsThemAheadOfApiCatchAll(string configRelativePath) + { + var repoRoot = FindRepositoryRoot(); + var configPath = Path.Combine(repoRoot, configRelativePath.Replace('/', Path.DirectorySeparatorChar)); + Assert.True(File.Exists(configPath), $"Config file not found: {configPath}"); + + using var stream = File.OpenRead(configPath); + using var document = JsonDocument.Parse(stream); + + var routes = document.RootElement + .GetProperty("Gateway") + .GetProperty("Routes") + .EnumerateArray() + .Select((route, index) => new RouteEntry( + index, + route.GetProperty("Type").GetString() ?? string.Empty, + route.GetProperty("Path").GetString() ?? string.Empty, + route.TryGetProperty("TranslatesTo", out var translatesTo) + ? translatesTo.GetString() ?? string.Empty + : string.Empty)) + .ToList(); + + var catchAllIndex = routes.FindIndex(route => string.Equals(route.Path, "/api", StringComparison.Ordinal)); + + foreach (var (requiredPath, requiredTarget, requiredType) in RequiredMappings) + { + var route = routes.FirstOrDefault(candidate => string.Equals(candidate.Path, requiredPath, StringComparison.Ordinal)); + Assert.True(route is not null, $"Missing route '{requiredPath}' in {configRelativePath}."); + Assert.Equal(requiredType, route!.Type); + Assert.Equal(requiredTarget, route!.TranslatesTo); + + if (catchAllIndex >= 0) + { + Assert.True(route.Index < catchAllIndex, $"{requiredPath} must appear before /api catch-all in {configRelativePath}."); + } + } + } + + private static string FindRepositoryRoot() + { + for (var current = new DirectoryInfo(AppContext.BaseDirectory); current is not null; current = current.Parent) + { + if (Directory.Exists(Path.Combine(current.FullName, ".git"))) + { + return current.FullName; + } + } + + throw new InvalidOperationException($"Unable to locate repository root from {AppContext.BaseDirectory}."); + } + + private sealed record RouteEntry(int Index, string Type, string Path, string TranslatesTo); +} diff --git a/src/Router/__Tests/StellaOps.Gateway.WebService.Tests/TASKS.md b/src/Router/__Tests/StellaOps.Gateway.WebService.Tests/TASKS.md index b073e5e1c..63bbd75ba 100644 --- a/src/Router/__Tests/StellaOps.Gateway.WebService.Tests/TASKS.md +++ b/src/Router/__Tests/StellaOps.Gateway.WebService.Tests/TASKS.md @@ -10,3 +10,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229 | AUDIT-0349-A | DONE | Waived (test project; revalidated 2026-01-07). | | REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. | | RGH-01-T | DONE | 2026-02-22: Added route-dispatch unit tests for microservice SPA fallback and API-prefix bypass behavior. | +| RGH-03-T | DONE | 2026-03-05: Added deterministic route-table parity tests for unified search mappings across gateway runtime and compose configs; verified in gateway test run. | diff --git a/src/SbomService/StellaOps.SbomService/Program.cs b/src/SbomService/StellaOps.SbomService/Program.cs index 3a03994a7..33706c0b4 100644 --- a/src/SbomService/StellaOps.SbomService/Program.cs +++ b/src/SbomService/StellaOps.SbomService/Program.cs @@ -1540,6 +1540,21 @@ app.MapPost("/internal/orchestrator/watermarks", async Task ( .RequireAuthorization(SbomPolicies.Internal) .RequireTenant(); +// Compare baseline recommendation stub (called by UI compare feature) +app.MapGet("/api/compare/baselines/{scanDigest}", (string scanDigest) => + Results.Ok(new + { + selectedDigest = (string?)null, + selectionReason = "No baseline recommendations available for this scan", + alternatives = Array.Empty(), + autoSelectEnabled = true, + scanDigest + })) + .WithName("GetCompareBaselineRecommendation") + .WithDescription("Returns baseline scan recommendations for delta comparison. Returns empty recommendations when no previous scans are available.") + .WithTags("Compare") + .RequireAuthorization(SbomPolicies.Read); + app.TryRefreshStellaRouterEndpoints(routerEnabled); app.Run(); diff --git a/src/Web/StellaOps.Web/output/playwright/header-search-repro.png b/src/Web/StellaOps.Web/output/playwright/header-search-repro.png new file mode 100644 index 0000000000000000000000000000000000000000..43855c0185ed987b4f6c09458803b594a917a1f7 GIT binary patch literal 292143 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%0DTYlF3 zV4%A;$=w|`Lt_)sdB^!#xA5wh^10SAR@+^YP7!CG24yY{anzDWWFQ{-QCfvHen?q$ zW-br!EW6jR9c{wyYwNd}omoeNz5J4j8Ni)OS5*y@^|fJ`c6>7Grq?l}$jn>@=53Hn z?hb(eZN2I9?&~$0tn=4!Ff8*=hw>q&?`!azna)Rj2v+k4d#UVa0NOc_&I(Nv*8R`v z2lm2q9KBGBem%$u{>m_3Vvqu+F) zT7j#n9x1zSh0kN+%qO)oO%Aa<;gnT_i9$6Jxh)PPZ{&%$Efak|891nn(irX2Rp}m; zKX7x*N(;AADj$GpW_=kkcy@f;o zqcoe8lZyz9dDFfEe<0%E5GTgAfV%Xju>A6>e@Sl!nc=T%>LeczK?Rl8pNo|;we4l5j>FcYVYpbEX z;2b|WZr-Rh9@Yeow9~pB!Rpb8tAX)M^9es9UWMk)Zcf-!fU;63BxJ4?&extSjBDf} z)&@RL$OJ3B^zQ!g$WGXIo|d65)ilA4>}`A=L5N~KgF~#wa_}9 ziYuj2HwLO%i2#5N_A>RJK*K-G}%=~g?(Diiw{%t z?lTU?Hb$Jjzz)|SD;M>d~-k~6D}^dKSa z8yzc>nv}0D&*$@cLIDTsgfMsH9O~X4$k-c9>TXdhQrS;ZX&zBgP>xu>11HR>B&y+N zd+T-)_0CDvL!>%LjICANK72@Qy*YV#ZdF|mu61-*4j&BqAhvT|A%c7{09dhN@p zZya?Mac?wXY_IMwYVvBcBapkCo*NjOGBBaAiF;=ItT0{7q7H z2EJyqvL;UkiEZESz#vFuTfJB>9e+cI2x!38 zRZP=oLHzCBshuS%`h5ARdxKiDq0Lg%VrD!`C1buz*a&*E;fX8*D;~e4$vhg$(R+3I zFKPq2fxmol7+=pmc5x^^T=on_W2Em1{ot{LrVCter*DqVGeE877xITG5_R*;68HN3 z^f4Xydg#bspEEP0fG9J5FiYN*^BDFqy)t(g#s^$}c6K~Ef(j61YI}))18;d7NQBRv zW!K+NesFl8tMuFKE#1<;tO34gY|p)Ecvlt6tDmCHeQ;B_c0BDd$u1UXtQCcclWWA| zN5COy|AYl20J(@x4wXz09m0^!!7m;&IM&;gEN|IgGuNw(qM>9N*mWz(CoWvTgO+*vx8TxAxjUOyrChcZgB7IHfa}24oZ2opwU+gs=@Lh3 zC=chsMpA$%#Zd&Z=vEll4Epmc&F9Z}2-Y%q;xc?{2ys~2MqG+p0|*=~HHiQ=@IN?Z z(2?>@X5Cv#1}G-|gVPakvDu)AGXfbVS{hJH(jQccb$}{oG+L`hZm?S;nJcjglPsEPccN$d<%w=Qr|1RF z#O4br)zr(In*+sN*JF6m$Zz?YHlsqqG%{@R$PcK8qgaR&`HOf?rvN-(18?Zhbx>X7 zUXwgL&&ho3A_ErLD|jYV3uJvk#Fe0~;+8s8c3=K^qkeY5(F+5H95H<>Ts6pCAKNMK zOZ0l;-OQZ#u;ZQynJ~2se^EJ2aKgm9ap=WSiP^vwARHi-KDEz|*=^GMJ96$5afKk3 zFFnCmp0HW2=dW+g>eHC9UYgP%1DE*oI+=r!YqOS zg8k}%=m83Z!;=I7ifW4FtQz0#ywtIP{09T3SO zI;R|wgm&?DkdMU`n-lD}7i{-&u5|IOSEN6jE{*+v z81|NSf)gGdvzH4FC!(t`>9JVo*u# zw+nDu zHx(8Tsl*?3G%PPq3D21)0Qu#)z=e}9czbG&W96f-b)01~FR6Cfi_&HumaZ+NJum;B zXiFoe!A@525DWZk*BOoREaD z9Wa+#nfQ}2slK=vscLQ2*_eM>KqOZLJYlXk0`SFx`It zQCXjThY;XV36Dd*ad?Gsv^9I9;bpY7tm^ShL*)2`l4xd5@ws|VWC=OqC9&Bf{1AuB`sA$H*)*#TJ_^Q`-&5x zPKZNi9V+qaj;H;vet${Y2?klmhq-(v3&C-autejrS;0lXoP=XuL-ODfQSXl4D8kAx zYbBEwse#}Hr&wtbVQG1DZe(E@M76ES71E92w{IRZh^DW--iX|SyG^2u^;+BR(o4GP z|Di`?$?MzXw&7VOinQX|nW5Xn#ohuDA}k@vkZr}eC;NV01*c~m>(vpjMa4MCxVv?` z8Q-Z(kO}96jb*Xn+M1|$AWp<;$_S`QG*VA$Ae>d6q1s$k-@25Kd{S^Y2YL zL^9KuGP6iL>MJ{Oy*~o}(24t<9t9O=U>U;BJdB{{0Dj7sh=ZtD(&>}D>PsDxpp2Y9 z3rxC7EVAbAu;73co_W&@`CfvfJDC#~La&a8XmdX=RVUXQb-Z+}QLO0~`us6Tx7F?1 zSSH*t=O&locfTcJ&inlP-`(pr;Co+)_=6mY5>`^2BNC1~PYs$^QoS8l z-G^x&VHOjS)SG~>&@0J-8~wHS+Xr}YRahHFuvb&Q;rsRlrsektxpMJ$cjx=Jo?NX@ zr=w%T^*%HW2AN@>?>*)f4wvf;a;f7J~aAcx5{QTX{ln z8EjL!kI&#j%1{kvYqH2xNC7~TYN3dD=JzI@@2NlKjEaSAuFeiQtjW1 z=MxBolQyAomCyy_J-e)~lmAg8px0U8SD_Fzc~imkQ-ag5ctskpaLOMXQtKfP*5#=LQeN^F_1ChIo`jcycy()Qna}#?I0amdH0Rxqq4U<~lv^34_jf z0f8ha*WLc*Q`=?5W3vs-n}-AG{vIk-8IKifnt6?ktK)mpxL7 z5Bx)cWffw?!0Qsb>FbZ18sP0=+_f{B!6E&_5l<*qd+%Lv8jC3E@eRoa%Mx=Ay%xckNRXYG5$Nl>8 zhRK2^KFGHTwyB_CdFHQ4a2GrLFB;bm44iUGx(pNoIN(5l09-YtjOMi2mQ6{|@Knu6 z3;4e>m*1x9UlsFtrLJ03SqRI z>j&QSOl{Sfzi-(r|JA;&&RoTq@bL2PZsSn?)0rdY6IbyW=AXxBgnt2p|JC5({s&7d z(i<1YfBNyU|3}&|{aftrKm8`E-`;pn-!mNat-Wp9VsfGn6OH6(AKta(yv;2)lD?7n z{kzr1cyqBgTTPm9z4`u7_+Lwf{qLo!wCwr*QU2RMKM3Xjcf9|M-I?=m1nl2W-^Bl= z<@%o%^hFHm+dOmjTJ8biAjxVP0sv-Nqjy@it*Cd^V9uXr!Q)J3ILT= zo3U_ldv0!Zwc5502L}hDi_47Z_5Lp|PcUr@c1(w8oy{Xi$!_Z|b9f`B@T(nqUxf`E zL?M*M1HzL8QZSqL&UO7W?zvH8JZ4Y;$~g8~#$24ZLjJOpZunVwRo%E@_K<(%`fuHk zqijI$5RWL^NO%Q_+xAQfmR4oj^SIkdIII@lJz+$1(U+pEH__fXBY(Bdn9cg`YtJAzSWfyGXgz>4aO0G zKl6JS=1_-w!xOZmS})6rQe~4uUOQs~A}aiw7udGjj((c%e(XbbV$7p6UTnK?q5NW} zVt7t26FL-=oik(4Z#jIq#_Ot0{TJTMZ2PQ<_a{DmUDpTKe9xx<0o}~r0|p@je;<;GW3w#R~3{m&*+f7ZlIq2+??XXjmk{>wU$)4 z_b(PTB?qi!n_7?b;sA(PSi7_PGjycW?^6n!m(V7;^Av^?UE#5>u@A7hQkZ0j`)>I* zt?(RESZkEm1T5T_$&_Tj$*!dIwY+4(;hrk&;1_E-blKyG4H3YIC5*?T&&)Xqkh!iK zq~9>TV`DS+J=7GQa@OZo%Epl^pRw~W!CmNG8HpE*8PkceHg3J-wHlgVz4V@-RY8!y z1+$5?5sRouYuPO}?dQMJHV^KN47mYe_*}#xZH5r|jKywjr==N(+t8f>;M`yW73!@GXs~jfcmljRznO zn&xK|zYLLD(bmXAc;!OAIJ&tZpJx1a3)AplG>{6S&mqYRo*!gsV?hzyi3$Q@ANevY z+n!mNBN!M(BPcNZR?Idtz{a!v(XIJ*xAQSdIvxg|ifW7-X#r2QJAhf7ufbzs@6i zE`Or;Jqbimx*_DD<~bhC!9I(*J#3~w|_p$cW;U`upNCWSflT2_= zJD}&B_BnHuAg@HIYql#|8?B(s>UFAev!ECI6UzzUQ}mIVxfTLe>R^Yj;mNOIP3A zqGZI3--ofO)qR#sJBWfE1ht)LN8@-ru#K!q70djDtK*5 ztxhkzHiDc$)=pGlq)+2sCdlpVkY?mRwB@Y0=5}KS~25?s6jmHE~}(~o6%(E zE~2G-2};|*a*AXX6N(bPhY4OkcHl98p*K4VRyMxe{bkkkaf+pkeQ}K~w{IcT4PFDP z^K%q^IJW&bn-9p~V$!NO(;{g!|69G@ggFk;rTrwAN!eFd&bbgvu$afNYpE^zcH%)i z=Y&l89z^GEh`S9T6_lh5B*`TUxr(aum$GXJMy%`?RNC|DiV53?j-)B}`hWZ?A;$RL zF}J1V_9fpL8YZNQ_LJrpMt%$Iz)x%eYRgTRUTdPg@fGx0_OxXi`I>v2pfdhRC$5)b z6_GB>Kcq`&*&k7$7meWCA{K25ZeKkbta$p!HY3{FOuVnP!yxpuJW61}MZvW8Sjb~G z|3R%o1>njCByZ+pxZsT_t2Pa9`RaUqIARMqC~J5=^rE;Po>OWd@#z#NCQvHlX@jY8 zDaZWf_Ib04wCyENe&t}qVQmdn*z0IU7(oD5l-wo2H?AhAis;@il8&TPS^9EG6+p*Dn6>4J3bPVZ^^ELwZ z2^1&x(@^6gCeO?;0pEmjD#%ddBBDO$S6KXZU}-mw7@Vpl>R&0)bK4Au`~mszpSX#l zdWc7@2;lbW3W=%?Xte(xn@_NE+C^Tv(Z7)dXcz<^S+iczZfarY)wcDBCf&9@Ak2svQe+8~qD#gjlR^euOx6}$CU0uAD=T_MaDbZCn3C*)1TzKWu7arKJ^>>k>tB+NI zU;2CD&YA!o6Ym`{aSHSiz}rVcUYD16qD%HmzdHq%bVgt|YGc+FaqjkC9J(U#Xzthi zZo|DyT8m~|nyh)_OA8qQb_=cncD1fAXQw40z~jhiBdty2gG)!J>ra)t23q>n!hRdyz|LFdwk=46$?nhV4nZuHVBk7rm$bQGdYd#qi3lAnJicp43tVbJ zd+)H{@!oly^Crtw-U=KQ(H%GA>^>mdez^J4QPcq!rlBej^G)uFBw<= zKI{(Sa_4IJa5MCEj@=GTWrMK`VqKhCqKZ)ll5puf`cp^9;9soN|3jQtC@vWO?W#${ zqE|l{o#8h%Ju>+m$Til-r!XSfJHjKcP2%)f3;SZxX+p5-RiGuH`gj7|h>gXkLbpn7 zp^N0ix}c>J>^#If9LG=X_j%5sx94v)o(fB9$JFFinG;McJv>*NFvh(zHnnwWE2N$` z2UU&l_HT1pr;quTpXZGya!*xeC!oiVHqZXMWhq3+bX#vM*brO|0c%W zEa-tR_Q0##qFhk@q+KcL`}~g^kw$ObLwYrjV_f^yqT53?+mTO8CQ4Bww!B2T@NtBB z)DmA3I3*F1vPYZ2dRA8{P3ab3i1e9UcgPVp+Sx2CJmFh;^c5Iv7}ZU=|H`2v)H=;m z5y<~#xa`;z$Pz^!Y1Bs_yN-vSK;IiJh+A2vWm`z+b<+U&>V;&kelJnDBOr!wF(7u4 z%1V;y%}?GML$j~*mqoD6NBRb`rY}Ra*f8cQ-XUEVexx2CCZ7#-AA-f=$w*j1V{3%? z;7*tBzCNmJnZ+)kcxBj^2x5u6HhRCmjHFG#PxpyNWb*Lbeec zQr$2Q*0Plt8ipEU#)Ed|h_+PanOfQUj_yx-&SmtUhalg#_wXhuqdX4Q@9-8%4;E38 zPwLKN(|uTUcPjhHCKDYErKyWu7gdzN6#Ps0er-!pAxco6!hnMEBpN z3inPK47d&#RH2dZZ-AoIow6?nBs5NYQ)K^7uz(@x1JxQwm=2KDT}u+%?MX@BD{J|n zlBte;`V+zzr?ZDe`69+*&a$?INi#!;_1f9jZ%D?=K6AMclqTmhmMM>`g^T+1>cZxW zwoKpKrBe0;GF~1XrJvI}+m2J^@}X1rfK@?x*?ym(Fq)7XUQtYhF}h0QCa*zh?aBsn zma@n|rldDrId@{u)&rg2OUGV(@wUw`KLZnVe|s{ud-kg2S!Ohnenb+kA(Oduv^#?% z72`0>Qa5^Ft;fuw^8d9MU#Gtt_b)Uv7azKAHe!DfMOCroD&Tu4IZs`aY?Z80N!9@xUj3HAClOCvdb+&nf? zJedJL`Se2im>yt3Rg#KmH=T2155uF7ju>rjoEaDypiw%Q?$VX1EKpIxDfZ-gLnkZH^dZJNIxCl78nqz@~OY*VDXiy(fE!dVlkqmZiu zK0ovH+1jsX>vz6_$#b(_Xvd$Tl?%7*Ho_y|+mKe1)Bft%DHby+KJ43A`z_hps9C1i@YM!E#1NK+eFG`@_tI0bj&7$ z)q8d>fs|zpzS$EzOQ4PEbQP8RGOX8hUgcL7{8WOSf;I;p9nBYqGf~*^sqxkKj*_5Z z`5t$CN*NG{K2LhaMKcPAj)mWFG+{K{81+$jhu4hQ(08MzIKXeen;8M< z$K4An4^5EXI8%mRU1hQT0*u| zrYEWIC#y@9l5`>uQ)qi%y~oGk1&Px-b|YU2S9$N@DJVN{vrlU+pdj#;n&lTsPD8`S zhr>{VFGeWhLC)>{6%q%+<*vcJJnl_}jE1_Uz>oW`1hwPQVo?Kxz z?BmuEnSvatxMbJ9JOQ}fc>pSPAC{@E6*~ewMu8 z=g^iQ$~EZ)Kw9=$gV232KwEn5T|soat9KCj#yuKMU}m>naJ+T=lml` zx!^n$$~-RK_TzRi*+n!hg{M+d!9fEgUk&d(N_&ET@Ci5X0mdCyLk=_xM=JKc_^G49ax+`-wTOE^%Y2}s0k25S4 zbA9tkGgA`U8N9WaWUoj@__r(l=$kg*8m1q6u((y1QYJ|S#Xg2ZH{LF8zddF6oo**6 zhLJ_z=)kf+BxoSt?452t3;x*ZcR&r|s~BX`iA``bzYJpENrf*HJpa z`us3)7;X(gxy{?XUuvu|Jfvee{n8r(9Vp3^2=;QAebqbFBa%V%`#h*;b?SCutklaU z#MaiZB%MLOL$X(yHQwyTh%E4jsi1QnAfbe~vpt(*3SF-#uTxdiS8X2oyKu937B?*G z!XRClRU(Mu0VC>AmCc8%a`(dM1I!N$sa9K zs2bL@$z|2x)^C~w1TAHfH1!nseFfDB24KdK%~4u_*ME|I!RWA$+A`dzTS+Nz*iypt z|Ei!-@sssL*;f+k9G-JofbU%_cT1DeIsxJ9(j}9tdPfcxICf+QyJltlW??3r2+Z%PX++t*_n&rbH z)~C^@%M9vrK^^`+c>5C*7~JxG(aex%p=ymXXfi{F(NK_o3wEn_+f7vs*S0rnWPA4W!OmQi64mM)(4a)mB3w6S@=Sf~&e7|;5lpo$wM5pFu|S*0jeCeMcgh2!k(c`ja7T80T593U zN+)G43$P7dr(UXl^sbq^+G7ZXzQ{_plts7t2)L(S^){Ra&n!3U6+gjW*uEyPWJ1i@ zj0d+_!;=Wl+52*CR&O*yzWWMST^J`>R7--ts%yyZ9qW~Gd$RV;0PdrJoSvlp2QPfa zr7C|;q|0h$XIdPe4B}XwZr$8z>Q#@Ii?>&renzbRN-i$#w05;BB2kEWqaw~b59HUx z`t|KtdZ+=PMbOFfPI30S#cAL$emMFzizV>csxBN^xQu*%>crJv2OIKAl@}fHdv22(D78 z|APgbW{W^0=Z=4R($u%5Fvb2%)^_grsw-r^5Ig!{G=>HxtBm>go#jNLH*h6os%vo> zQuVbvRmxQK&W04#VW!Y2KCm@_j8o@R!#KG=Ij5WYS{|!E8o|5D$<+~KhwF#SygFvH z+U-%Ti?Rx&*$-15ce)UtXwYuJ;MHY+kTGc=)_nCfiPnRnDy~s-LOsjX3)kJl3UDVv zVZh^R&Q7MSVJ$~CA0%jbw{v8uzdVNKW?Wc5&3C+I4r$CatYLM_e7=y-YTa2uos?o* z0}egraft!#t<|V?0SUk|EvNnc$d(!*%82_7Nv+3WW$!YlhLRg7c56shT%3Vy+MhZ6 z8*HN$-X95jhb$1%&XK>UVn8A;#%ysyx~*+FD(0L;OLJ9@ z@%_x@H-4aONiiWXUjvPQ<%+=lS1 zTSUhVS_-}d-;F&0m8#{-uCIW`1#g5&+k}&NEn^Y(k!6`#;{$eMD!#QuLP!rtg0jr% zeBYaefW#%kcyJ*mXl<{2@0D%yp11L-Mv|90=2mxHNb(E7?MAEO@8@mEt~HUsR)|bW z9sg3Q(xk{ykk-RGg7k`gU`bz^(pry~)W2tU{TD~3Zdcp+fkA<{)swsN?(akH4cdG^ zK*fF4k1Z@*+b(mWMbEXCNpHG0}X zu{@k5!(1y(WH~m{XyTK<&ALE(q8i)bztrN$;B|=v*8rQk6RC0B+^E4ef&Cj((q5f6 zRkfZ%PZM&REfAfc6DrVfFce}H_leZz1>;bAFpsSCGa~!DAcC~TmEU?f9fTY8rjliN zX%-~f<#ZC)OI1HDlGYoZBnrjGwL5MEv@BL({UfZzb@2(EMl6yZ_dr{C|caKm5Pqs+(_&5P$!a4Ifwn zP+;NH1c7p<3>BzvWDQ99d36OpQH$3CzUxY{F%@%R;9KC=bx$QA4Tb~}B)&-_G;K>( ztdZVv zW}o}hivva=N5VuhLA&%(e#9`?JJK>SSoHoT?1{p~Qg-Fs+^NbkDrP%fCH`qdaJ~ZW z>dObOMQ1BnL?Qo9G3vT+r8%bHWw)Ei3yDHCW0P%oxQvGlWp|`s_V&%*XL@Kv_p*v% z6WP>ZtCke{^#oTlYHKDq!pcu4h(r`q)Mpr5iE3m_V<-ZOWj3OUo^$NQ5tk)iGdl=p zVmC)gUWMW1JDUy1=?k|kDoXJ}VU91p-Se-)smjqMO#T6V<7X+(r=_Dk1JjVaI83p8 z?o)4Ns3AO7#K@vL^ZXH!-LFZ63VM^LAoHq-b#0fq0Y0&LagiWWj{_U$=IE=02;Rtj zuz6CS#mlYCY=_ffe9g>Ev#pyI=%yd%zT=PE6d76NpzjbUg|gI#G3JiAXeugY|Fyl& z626>WB&iYVelN$B*T~A{{R-F7fqBKaINSh^Xo?T_(}`07h3YFs12DYB-eTk??70ZU zba}=CB1#A%fW559fa=4%^Yx75+(fCj(7v@%S{-!IZB^o_O-7q9RrLP;)@iNz+F?dg z#ULH~R-3Adt&x`D(l8Xyyojol;1vg#_a%${-Mj95J$IT1hvkYh9yx{m5SHY8 zHXrpqc}=aOt!LaD2lkdoT&a?oV6QA^`0o<%LAX3bWwum}6$-!)wNlV1`XXj-jGNHN zL6)NQ*UIgiorlcG+tw`t*MO*)2?27(W8>gxYdw&2(VBpj+h>fxM;YxcGLetX-Y-ou zRbsL^mhg(KgKQhOGhDCbeo$+Jex=&Dfx#R!z``?~q-LbW1 z>=mDVFhVCl<*wjO?5Sgad*0AD^Jf0n;g!twBQHE_+r{On6)oyIwPvp>2iHI``5w1J zx7cnG_!1vPmC}&#=MQ$UXjWfDfCdyVP6TJ1tPj#)grPmBpCj5y5B0LN@}s}wOUkK~ z>HqlVoxu@)>ndmf5xy$u*7@`|-0&8CC1-5Dgu!@5brDS_dISm5pO*)5GCuisp{YDI zSe=cO2fAFBQ3`D&&4R8kM#w(puIKwxuyG>XgkpF2^N02hP_yg z1?9i~Zu$F@J-A5m4{b}2GJ503&?8Fz>6Dj-F=81b?KlGlDKPWlu`=Z&kHOuKzX^YS zO5FpqYdpr26uIAM)m@3wns1)lU5y?fmb(0pv#*MkDvsDv=G^On7FtKlk%SA^d`oU0 z4{=;tZ`b#=eO6!o*ly0a`&LnmX$F*ia1AMvYbTPDAtV#9#at1Ce#j8GOx4ww#ZzZvuf_f%kg$KmkWyN6*H{dlavx$SNnBA4(V*?n<82pXWtkAz< zn!JOL`0>QoaSwhYP=gwHxIFC;$+|6B;-#nNG}B$iSDdhIxp_hpG)x==uaIGa|H9?w z%Dj&HXvhA{yKEaj#?xgu$D`twSMIlZY)YyP zdR<5GJIEV-+S0c+OD_mzU9H0W!k^mWdo>LWO%p70i~ZIO8|Sm#F|OSwvqMFsv?^(8N^#G5W;R^v zUAl2G@UkG`e!{{(t$>ye?!NAA>D{03`sz|;fziqc z+`AiD>JydThmjt6@^ac}wPyCiQ|2v9J>#sKpv}){0VmJt>Ci;Q$mHJ} zHDD}$I74+{dYkC#8+7i3$@z0Y^@V#f5Qm)G;SDqVChno@I|^^Jps2brGoF~evm5-w zi5+u`GhV7n>l-~2M0`8fVygm}p~r1e!4qk_hJ!;>?W}%W)^c;EVLI~&8Pc7Vq%gfb zL)(QX5A(=&nAt^{;T35Aa=09!9pOqVM)GH}bAVQKbGfjtBoF8Zc1zv+<61)-(t+Pa zFUgxBY`fL1MhJ1G5xJE+DT?7t5R*OV36finb3woQJog!dB&8B4o9qc zA@#b2IPLDs$q_HMw)VtDAZI96vq#M~%kGdQ6rhnl%@kK6l&(>i zq)nBlaEW3!`0+mL8-@pTan6L6QjUIDj@|7taMg|KzILHwRHJ9B_JbqyLm^TeCp*~> z_2HM79X0RBXR>se%QM7CKR!A!wiNQ}!x1Jb7XG=s>|9IGXZKL&gLf(Ge``Bhy3hIxq34~6l99v|%Tyei&&B*EnN8yrbh;aFVygDHhEQ#s+OF6Z6hh?*t_+COU1B z89PW?aMUn@RLT|f?s>)8-F1_4q9GYP?(EunV;r zx*Lm$EgHx?BJGCZMCLs2Xd(R33lZ^GB2by>?nFByru$gz3}5WYJylO2Hv&YWm#ug4gld zj!3w6yR3a(LGi(l;S};XN#n164d+sQg;=O9ZGE!?FPP_7$VJ4Z?JYli?w<6|Kzq;J zVCy(KT?OBSWL2^Ja~Ieb6|)S$np8(BC}DT7CX!nER-xzspYy_XB1TVG$H%P79w5W& z;KJq-UzHq1LwBd9WI+3IKDG!>3X^7^O8`&^TtXjEFy|+E-Y_WCL!BDAT4(dKMVMS@kVnPb5` z%S`Rw-;av5_B?n`-t0Dy=&5I|bXJ=KgYDf|1$AIOd2f7V>cigL%9BVVCwaO1?Qs^f zIy3V~&#SyzFc}1ur=O4HOjQiTHOva-;X)Ks#TVt^ z=U9;y@5|XXxLqBBnmhdJ%>r@rYjP5EH{8ANR##cSu#Hd|ebZFOZRa31wTk|sA$q(k zkUi;q9c2Zo;4ch&vdyA~UG4NT;`tgq*>*nq4*bK9nqxDWhp)GAj~osU(<6D~ShvX{ zKC}Ix8EmH?%&?^2+}9QV1MGQ35I`kstQE ztrh~mk_I66m`pUN76bim*;n+VgNw;@eTL#X*N%dPaPij=_M|c~N~~5G}MA0qy*uocQ3f zyKPZ2;UxQgej@tQN+D79t8UWux<{8g!l!FdVKKt1XRKakhjLnl7C%1whmSMGi6m^3 z`wEeqyX{cRl2q7=_#yOsZ=05}ql(ad{>A3Holxl=is3e0h#kJ=sxH?M8w_9vhoj!A zQpC^mi?JqM(9d$dn`y6afJB^SYyQ9n=ef28R$g;RsipwzJ8!4ZLo|?A^*>mEugWf9 zVi;+JL2}UK@?)Xx<`cRl^NG#YMe4B5o114rFR4}j=|zN+H^!(>GyM*-{Koir{?D8CF)~e30bj)Fqm-sYO~54!vggA}aH}ZurvJK|wEqQt*41|=Y4PC|$u51x zkZOvieVaL0wJJN#v|bdzfoTK*P6?Z8fn;035cH>*5+xh!_g8-{j7#>8U@a!0N&8g> zt^tlbC-hP*C94dgsC;AI7-=?EIuPGh{;%oFb8OMlt{vt?-C<2LPQuLGufhP76g!-D zi~O!UtpyFsf|TRIsQ!@%FI^dBL#1vR@`JKE*O?J{GG-kwYEp)AAy>uy%pXmyY99#% zmC$cwe~MFkOV70ytASrg1Gp^Nw2QXFyfC?VtRO^+aGic21+l%3nJda0fiDHs@X6tB z49qDmsnIH$HxX>%#h%y#l*j|{!idTY6=uVNejHwFc&N!=_I%Ta^7%*NxiiX@xU2j8 z*6`>l-*X&kWKSZgu9;0^*~x1NWG?Tej`lD1g1!(QdQh@CEccT;G{uNFbdqO5*$-UU zwpJ%F?mKT<2R>2DMKsqI;}f9Ybo$UMW*qxNJdb%9+1w z^8&W6z&)sn*B2~xe%qvNib&LrdHy-!_u&+TH7eRt?TnETy-oj2w^8SmS*LyEEd61sI7lGd$0RsZifWs?*7DO{E>n9) z$A?#yxTTuY*8t(0z-;vwpNTRyQJ1V5yu@{DRwcUQZpFNxWbtC#PJf~&Gq;m2$KiI< z0lH4AxRg<9yQM9s^H|SK!O5>@d=C+U)r*12n!jbEs~%am4!t0u^V^!6oCMtTgC$FZ zB}=?F!$%H)$=i|fh>FN@@z}FhfS)MwGwa%;cG_id)wnSsn>(sEkzXTHX?^8}u$98& z4T~iB{z-p;xb5P=72DOf_|qXW|D!)1&J6uV3HQ;=pHVrtWw{m~E zHW+H6&SzyHms|t77ZhH`9XAV5Tr}z-wTyYCC95U6ag=_Cx|RkEx}2I0&w}hW-8T4m zCx>ZCf0{kR&MAqi=P{7{&7pmzVAm6NnM>Jh*7Tw3_z0pZPIB3p~9XC=TXbcd%zQ%l|pI8oJ`hAdgl z=dVxw={2z6#m?RD=*FmVw*?N=#Tuy(QQIakIftpTyzs5u4`?u$#)FqG!*Xw6vC(f? z1tY6dUsw7Pn!I{zy^vSXZsANm%sQvkZ)oYQj%<3Nm2pB3L6x}A5d~VuPX0b+F;9$4 zq95Z)N_q8XZ|X{CQW&gL=E8OIcv;UJH2+%f3ntCj2Se8pEmC>c>%vf_yHpSMZE4-w zh9jVg%XY%`UsdI6-=UAE(D+cw3l!zCg^SCCT`~9wH_HNidtrxl!w|NR(k|09G$YMt z|8-oSj(KUf8U0ju!T4|VoY)M?z!df00(`IAzZMU@fw)u0Wh15=J&bt|MQOts`*;{t zjBG@CP(wKz3jjO}L;G1|->Z*}-2>s55qo;4Ee_w_7%C^T?0yP9JXleK=Q5|rE+JMQoC5|#Nr%OHxeh=v5pRxr%OPGq}HeE3{KDv>?~S-g03bOU6(cS zPLUoVIZ)60U6uAKW|d~^4Y)2kw>dVK66to~w43FnFYZB(I+3J(q0v`r&O~*ZhZdAi z3m&9nQcvO!1lZVA3uNFTrb(&gV(bM3jA+hRr=7H&J=q{fkca3CwKS$G=|^*!hjQ4Bz@f2Q7Ar{X9m1W((ZPN+|EHPe7HTdyvg4bI=#-y4+I?61U zMZ$}-kKL&<#Y9+GzdyNpoCPTAE(DNXB)AWvy>DYF_4NVb6+;rzz{|vMx@{_ofe&_H zEI{SS72mfiC$lptv-D>=3L+krWtHI6Fk}}GGfG2BIRQq(&vF%mmY^7lfWt^S98A!q z=B7#mZ9Tt85(P*2Diaf8dWQQf?STtr$DEg$izi__Lh|Hc;pSRHzVS=3jM!!XF!AS2 z*-6IGBeLhJ1IRutt^45vCLs6DPVDpb5|XY80a^IrynrQCfufn7i?QN|T)zN_lH+=I zHal-sjhlr|$R0Z9hBq0m<8LpIaVWBovg#)&vh#Yod%1*cWewiYtSypv-=wWC9vXH# z{I$aldqzS%=aSIWH1iZ#aVy5=z+x;>g1i<|Ja4==?~bQ&!5Vx2kt}3U!B_>xzcA2{ zU}S`;Y6U99*RjJODIa;vVQoti6hUP%1II7~H%id6&3`o+bAXV;r0G>QKNl3IZk5XWIQE% z%fO+y3fwp+eDsKC$BG1YP0BD1ML{u4VoBptWGFAcpM$GCuEi+5QdFe%%^`Y`?5Ldg zrqU!Jkj4I(&bs1l(JsKZ#92djx^VC7&EvZ23)TOX(5MJQMRR@Ikdn=Ns`TO+kZhfU zu$LJSH}N^Hu~96J^I=Hisju;B+}X_hLCIRBX(%MH{@xG!Lk@|BOzZe7%Kby%)p$Fj z9=ARgIKclx98jN8G+s!6y8ZZf1hE4zcRg>a5DQ!Nk!TE$6+qHYIpaJ3?BeSc=x2Vl z#>d-Q#Lusr!{2d^LM-d!s5f1l?OGtFNSJvXmrfg`=KYsP>9~nB(yEO8+ z-?dM)rifba?mB7G9}?EPl}=f>ke(4SZ6t5c4WmYv%9~Uw@8dnDj~B27(e)|}%-AJJ zs5O)5MoR5%L>KXWNUvmMYF~wpyplwS@ffp>4oQ`A)AP4rQe4%iKb=Ntfj{$x;qGXJ zX>KykG1-D@YS6IAKE|h&3?d4bAWjhnb!gV%6I7INXsp$1 z^cA~Vv9wf=N~VYgF3I4jZp=H48K{-z{T-lE5${(i$S!uj0;iXJgv{xuQ_5wOIF^`8 zt~F`QoB?7u;z3bkqyJ>|S+b)kD$R8R8H^0wXc-|5JmWy=gX^QTuwffCk&g0bW-E_; zewr4zz#=a9NMqpZj%Zj5Rn+ILN+~O!-kignWSdOWA>E&F|MmG_80r0Lnc(W$e%w{o z&jv2$lPI2gg}qN2Zy6=t%}wZPqOS+jQj45$UE_fP-?!BnQv&izHRIh_F~`Zb*tII~ zszTtAaTHlo=n!z&ybT2$%VDpS9Jtkp^zAn>EA9e@Y=}JGT*{3f(7%^Qzl>uH*x1r! zeedi@#zHZpfLoH+#wiV>`+pdzEgA1eQ?IuzM^r*G0=>-2kAfIb`;p)#`OG4|EiBvV zuC;}}11h~TzPI|Lb2e3@hvxLPWPEgZWNx#cGdvRf=x;s-Dc;HTQBsj=b=E7RefxI0W0TN^5m zElo@I^0KfYHgaC_N|L>?+`5BL-~ zZz$^zG5CCpn-4b!Vl=1O*2<>1eJ>|>?_@eGc)}nQ9S=?DGKJed7SgqObffmxn+Fgs zp227@iU72i{>G1zWs~y(SEtYF*QQZq8LPrl5w$yAlTsEHY3Yt)xA!?mVp}F2+vWaK z7KBXZGHSh`%M5PGi>Zv-B>nRu?ib&@_{PPF-x*sw3$g*wJI4DVr<3d*jIzkQQ~6l2 zDXkoDr zI&(r7)Ldtu&bDZ-^L_|-850!K`YAYUWX;~7b&FQpZU5D2qw$t5|R_c8n!2BmJUEtHnpCDY2yw5fHzrR)16O7ok z9eQV6WTK|;Wt5u)j+&!(y1x5w_R4q|uU8_15i_si8=X8!?R^pxOf%QD_gU_bx0r>g zWT>l^)J5tUk2kzwpe23->Y`%dh5B`8UXEpGtzB5=?@+3|phg24e-c9nrST-Y{$Ers z8;3$?BH9L%nMtjIgVh2qzA>hkLj+4kyi4d?a>f41scPt;Na1yl-S3+vfYaY;6?%=o z{r3l|6l)Y{^zM)R#YkA4MVXyVMWHqJYSt=i0&`i#AiL^DU_F1?P*UBm5qW4RCoZ3-*hS&X2(2M3A zozEW$e87$(t(0N1L4VO0D!F}1#4I?ikMrgU%x^!>{%Xle-@B6HAdX4~7OQiuocmB`DLzR~kG;X}B(p zsw&mPxrx3!Ttd3@Vgsn-bnzd8&g`hQ^Nt!V+6IJIWcX^(tN%j7CE5~Jclj~cFnlOw z3_k@UA-V8%G)rGe8Da61{b)CGF3*Y7pBSzfNOTht_5l^oM%O>ms)ua6u%u^A&5|2U zi(KUpi=1DG1^(R%#!~<>UuMsJ-+%V4exqDEruB(OQQhc4zQ{NkAR18wuLs=QUfBp4 zIpVLsc8^#eZzr7+9+rE3k1Kmq{FxxArHQy7M0%G~Rl+Rl9LVve3ZCn-X4w2n^5I*4 z;2SZ)e9-^m>z$+H+8*}tN!zrI)mV+4G`4Nqw(T^HZQD*`+nU(6oiv_&=ibk~FV=6p zzdz5KHGA)~_cUS3&9%B3YJ?GBVWERL=6B)N|R>Ah~yVoMEeIv(zs~wbC^z6+^#7+hqSj4 ziVCcAu=#Vrc*<%GP4Pr^nLw*cw)AXLffAW$p|V7*_|r84iZC_vxr{_;Lgxz;ZUUxb z^b3}g+g7+&ZRikzrzE=&eG{+uo4}MjI@}y4J%S;OF+XRF?u49pid?wy=w9J4zSc&;n)DTZpE-#g>2* zo3Q?L9|eij0De`+U2s=`A8dShpK>PoiPXF!WU8Y)kfPtGJpW>~amZDBqwB6-w^$|7 z-u=d~6fceFSl_^IG&Wy0E*S&U+?Lf#ZuhKMtZ^;P4Z6kdc{#u4>HafNW}7*05&|$! z>ZPURZFP<#*e5BL!~3Gz%_~aLt@t%F%u0*G<`3|A@MjXQ|0?D2(Z2e%=v=73)Xp*z ztSD!ntIGSLdKgL{!TsAsejopbg#G{Ij{KJz9Pt0E$^WON|Ix^QR8ScI%PL8+ztw>` z!0Ibs^iQs@eAy`2)ISNp92HnGBAQGjKu$1m2efeJmt{o1tPk#pemsi}R&0oWTd4kB z+TU9L|5fgPF8;Un{(m++&<7VlMFHFMo}{ew-|hpR&&~j@jTM}C&v$vY6Q)e}jon~o z>^Rf|Y}7!H?1;MH7bnh-Bua6QdmDkA%f|nzhxzay<;wZeWnDA%1Jqzi!+D7tPC6jF z;WZJ?Sj0DWom-3xXjv`Zm!(k>5Pzxvim8enabu0X`dAL+EfL8yYJcfXE*c?2$C2Ux z=N99UL#j`FQ2P71I*D7VUqOpktY?NrwcHq(9d@Wy zaH!o&3Xj*?yWRgJA-+_qy}3qqpq$qE{HhV$z^Ti%^e2urG^4!`gs7B-pWbIfPgkSR zV}~p(lSC+L(cm&?VF{_J9QbejU+-oCOLDRg+8!#3)jzXjEIe3FSPBa@t0!39T(`rS zL00RDoWQnn;;d&dZM68`Q|~VZdS&BY=WJ$lNqh8vB=Ie|*2?Xhz5A<67|RF`RslZH)w%{bb*hGZ`u!; zV8K?@#T^r7Z)##9rjyx3O?}yc=KWuSd~h}vzrpVfF6)wACrS)S@^Z0J@m!lS=_*e= z8k>d`>fj~y9ynspT9e=;_3ZZyZ7FmtK2%mLQ33x#XFv^SWEs#%xL<|?Smqdyl}{E0 zI69O_vA2)Rprtp4u_r+l$DGsN-*&!sf_gb7hHAfZqR&X=sF%t``H)>?UEowYezXf0W?2BY z$5|WlT*M450}1AcywL-ShDVww?K~2@%P%Z_G=?%%+Hgvk*s-Ks z)LU1bfQ_Yp4pT?(@WnqS4m1CxUf(ac>9{vUK|RfJEXpk}^Q@|@(kvTwy5GLyM`>*u zrM-T1{g7U{%5`rEV!yl3l1nOl01{Q{UPP&PvDZJT5CAmX+^t`t*QA8wSZA8jju}Rz zLgxYlg;H_Vn4#}8dV@R3X2=@UOeWG}!Vh~LC}ScDPe&6MPWlx~@?zE(r{CK%=ce>j z6|&nqf0hQaB@cS2;KjEJ&(yqOFBlEx7Y|#sBp6`tp3Y!P{=zm3kHsnTt?Sge;8AxZ zKk*BSxIC%5MGPIq&si*?j}k(ruKd+DtA-!^OWnBmedVD@T4NCsV8d2HCH_pWqsbdQ$0pqNoWehFdqR!f7n z?kx54$xm%vR}Sgqlfa?c{g84(GH96wxc&%qrRZpZm&V z1|jNp58N4NK3qa4fh~nU{H-35#sqM1;qpTt^m?!$i$8qYhzDT+RFxM&9{YddgzHXE zFvMWRCB?PxV2-Tr!o`mxvWtMeU)U;ifTO* zWJSvLARP8u!*iy#?&!mt?2U@OW6SS9wph(&iOMt`L_61Y;M_-~@Z2ts)J-m(SL$dd zk@M+?;hD1!XvL?V;H%|ZR{fGQQd&_$c)NxVK>?TsU$)4W3)Gi4#d9~M%4ux;K9ORT z*DuiSP^>aAkD7|Va#cjblwlId#a*TR;Z}3xv@EZCVJ|P*5D2vmG1Q4rt94iTK)wv5 zEhs8hRC6vjP72Ok!{zoGIvLO)2dhy8H*?=OE3RE;Mvp@R0l_KDA5&v9s-BStdGXE= z%O0hg8fA}2;EWOaPm-G0b?jTmGmc9v6X3^W%Cb98Ub@vIrYzhCX* zc>rf~EOpP}Z?w7`O~`EZ%a57n>Xz!EIU0ly3_5w5xDjczYbKK;AiQx@`QpC@V?L;aa~TD55v5Nn6MDTs*-l8R-;T;#^}MhLrE)R}sl zB^DJh`lvO?na=7rKz^9Lk-~i5*~u9|7+f&N9rehR32!-?g1bTaqgi~6N?{*Kc|-rA zh`yD#vIR4S=R^^xo}~N@+wD}DAl!nZZ-77AElt7`6EUD-NRZwRHg69$#CJcR&=uc z!XP=PV0Widr)~f9j2`(2;vCNtIqa-hms=R4sc2d&uT4q;nNwJD+Xoc06R_m&544r1 zfseT#`X@^7uR;BK%QOYY;q1ajOeRMTE)c~c{xfLwAghY62rBm+UaW#nIm|nSoY)pe z{UT|)$(Z6?{IrL9i4vX9L7LoxydYJ4$mo)F#ftMTXIX+1o|7;^5d_KJ+DXz7m&ZV<^igl|kc-{P5V6>+vpk++hSX-CvAi@^9niaD!UI~*T z(@u?ODW&LehevjJ!+5ix(LNU^=8zhDD+@^x~~&3m4d&3@pV}j z0dZLlwB;>7gBybvqkh*mBu6cK@2yGZwF+B{2t3wEE;=yNP>bdanRasjX%%t%bF!4Z zkQpsRL{05{ycq54$O1WHK61FxF==bTeO;X%>QMFwAqTA73D?~+zoOnM+}bs~?_0s?S|er>lS-4&zzTLh=4Qtqw4?!49= z+*4WfPM&Sx{u-~aqLJUbF!U{O3?hB>ZsX3dHZh^k*Ti?2a1K49KU z{=QS?YVW=75a*e3_IgM>Z{{+L%i<7|`W}zr!^TYjuN#C!laVP;X!EY+;*)XrDSD6z z-(*PEQu{imb1CvwJfq?Tm43msBDr5=p$oi=oQY2fVr7Ubcx|tdne%FK?YHv3{$xb7 zI~ah{AFYi)1oqUB@6+|*(OKb1a86@2w*K*A%~9ienkG`^2s?y|7{7CO?&X{<9xNO)2Y+?dYxsLp{SU81230VmNcn#Bq1r`ADFqwJ#y)y`tMB?n{UF`?U$B2I8Vp5AzuFtF8kJF+%4RY!X1_?&e%eKOH!-_s!8 z&Ie!P^^ik{6cu>Mi|>h+-7RkaSYJn*V@JLLIBtIX;`_lT;?m=DSSk$oN#I z*eN_&t`1VQT3-k)!SYkQQ$*~HY;DG45~^!6|4Q&M`fVRc!N(|I4UXjbw$np8tBSH!^)dUCd*C8Eu@I(hcmqP3e z%oQ`S72F}U9to5_E{j@#6qjXtNyHA9OJSM#hL?a%BJ$M^h{&0WVTwD|8ShUu5xIq_ z9)lC!y7spwD1?VuJ`o{%k&YMWi^xbbDuS`9S=Xdg(ePL0h45sd?Ld|`fhQhI^*Y0d z*1??C#^=MWp=T`h1MfN z#@lnp9#bDENtVrQ=*-VJ@5}Utkt|gf=~cE%P#N}`R<%`wS^PW#Z^5z8IeXHmls96?q(ph601I4`4E(_Ew@tr9wBB)6$VAsuT||#l6F*2H)r{CC>M-T8HCzb2)@Bg++3$cu1e!&b7QMaPmv?EDl zG5L_?$8GXlG?3KXx9}O27CKCeqwE+$>ABJm=Y`}88n)eFoD4NIY-(4JPnl15-w_tJ zrFsJLlhXWg|BwC%&@G4aMPZ8&#A1rPq+`<;t|Cn-58Q7Be?|%+;hTs1dNhjM#s(T$ zYB>Jn*={^?Q$5PA6%JKt?f7SFb7IsXd)Y1du}b~sgdgjwBZv+0ypv~us!=rvkHKSp zU7)&8^hD#_)~;(*E?@uM))*Pd>Y^a@G36U!7(ZserPzRxy(TMAKZa19)^PLQG`US2 z`ZA;a*EkNa&YHB6-(TX};#{r`MPH!Ct4(3H1~(fk8f$G#ziciEH8FEgT}ULg{(ex- zc8@}afyPV<^F1a3p-m?$SHWCJ+z)XWR;^Y^sTliWd4tpf++unBT0Hf|7TSFClej_u zI}c{=(4Q)fQX(Sos0V1dcUQcdYCb+Ip-YeS8V$O>_$gZ_#v)RBmmU3qB~_-CI_(d8 zx)e6$kFqxQ2RC3Kat4_Yf4gShQ&vaTUc>lQ&z+a6eF56^FyN>;7scghJBR7J=}g>5 z#?TM6;oHu^3y>thyMz{8f%JN#RU8@Gsq*!X3*}ols?iZ)%uZ8v#{-`ob!?yO=O(DT zKF%|?dA@ormT~89it(Rmx62dFS_NcE>2kK$s+$vsMt$_3>kr#Pr7SQBGBy(_8?#CA zEccTIVk6p|rOAP%Wn4JRWM$n#}>2G#V*J)YoFIXs>4@1_n5@#u_ZCbe-P%YL-%k;teR2RZdU%oWA)5baSh}g`Ct>Sm{H)o8$ykGg#Axc42 z!5bF?1ZE&_e8MAcH~O2E!BSn%;MZ(AdJaa~&7MOtAY>WC_Vck~e_i+gFnTqx1B1`G zV}xDAI6iONnp6k~o0qu$3p8w`%@HJ7ob{l?_(2k4mQ2iJF1b%S51n((>2hGo5rix+$N=(xiEw%}-y>G}=_ z=xw4=g2cg`h6!K7M31)W8phQcN>bh^CEsagmPFFrzEG8zR|)K(K= zZVq|0#p;S1Q_jb&wLvW6qM+TFD-jiu-C1*H>G>=%4+EM}!BCEw(-X<-iCVNcQj{J? z*3hpWtG(LjADO??w-gmImzKOqXdu6~x=V4yO<9)F(QGK$YY(P!(MDsorCcZdEI1#S z@LsY0RG6(#9~<`p6*YO9s@6xR$J6}FVZwHMiGXrCzRpRic}+HS=4p6%z-{ctSDHVS zgjfuCoZ{~*yGvMK+6scJ=Q2%pgGDh1lpE;itbNBdU92N%btJ3<W-DDE;VpCF5)Tb45onPO_m`;Wt&9h+>=3|x!`>5hc?C-gH ztssr2dpxf&69n!LJMo$J-3_*G8$8A+?d7%>S7?I#GsvtrcaUVgpHWR-&GRlkil-`? zVmQZ_)T&jEcFNzMTKO zXSFKGDd=xuss~bh;-&$eTJ=SYFU9uSZtP@OnppRr#!fRTwkJi=kqz#W` zZGp8TQs@3?JVMl;emvvW(M6jX{2hh}IxM}Y<<&QumI`d`$`HsqziP2f@pEilxDvj` zkPa`?G4|G4M`>oquq(8`1a0$0xOl-M(;|)VZX6bbJsf)Yr{(LYo8+X8mT%Hy(C>;V zSHIOIW57xbCMdyXFB0E*S3qy{(bmO#rF|=+q*Hl4!N}6mQln8sj7Uc&=VnMDR8zmU zVXPn>B08Kne+Jgn?ZyBR@|3x2+`92h%PUwdD%;rJmO?Er{d%~q?#1I6=Q5K8x1F)V z`_KBrgL1O3y_~LgRD$FF*5q1`k%v0uPLpeojqUE2-$Acm;G7qBguKz+iFvfvcycl= z?Y#YEx!2Vlg78xx`9mMxhQcA9=E+kO1VQ!dHTGjkHtPdM~;VpC9@*3a5n_ja^hnOj$UaKSW_FQh1%oUW9Ek-nZlyuyVa|M!U~dOP3UZ=8~vuX zB~1phMEWkpqUDheJ1W9GaCua}Hc+aqPD8uy%xIJ_MNc3i|1S9#3+VDDzSwFZRw~GN zy?VbdoK%-ht|P39$B~-yi@`&dtkd$NQ!vhRfc{D0L}B%&h>8?BV_)scj7sg!Oo)3r zk@CHZmdid)ig@hVQODl?^wdfo{1=X?knpKiH5$P}p!O$Wn?HQ2**dy0{`G8d)1gE) zNC-wN=0$BRDVgANI=0k8A~G0H3HQK8x~@HXwM>( z1$uW!sEtQ>(i%<^Nke=2Juh;0g8?v4-(YRfTh%ZxS6N$P-n-yAXYNsxxVnZ*_)?#L*5a zN>2=oK5>0!dHz^pLzvvKD59a`LlA1*FE>NotD!WGR8*c0%+#FAt?rg!3=5XHZ=Sqi zFBlzvQZH+GH-CrxbbyL(`B-lF5wFn)lKB#-+G+o1gY(cVH_Bl51^J$S@Ub`AG&*93 zR&l~5&R{Lj&&eyldmN?(QG4NH5J8A-PqIVd;qKGSvN-95E;Vt&F&tEn6)>@#$6Qpp z+a08h%&XO(-(T3z79Fwtn1p-JgDMVpI(SC4_QLA2Rt<;iWvaR;m~l1<4`-T~{M+&J zhoYccUmb99?M5H@JY`tY3^wEP_IH>9Xvv3habp1Q-61I{2x^6ea-b4^K!qE zmA|PuP{94cwh%K4g)$EbC`wLuq=V*LaqX7AgN~lZXl2E2ZHWZMvx`U9lYi%jGL#3C ze(0Z}Ng!UyGHGyfv=Yo8n!yO5q5M%Ruz1u}C$4SHth^c#;v?^T7yb$OgV_?MA%M5q zXh%0S5&I7rUDxhxl+&8-^xCWpu;U6=att0#?+-6r1Cz=cyvU6seVubZ68;70V0&*= zzP(0lAwAwi6_hL~$F;Q0N5!f73Y^5|kA_8y=nvvlpB}Y(%)}0|s4yy0bUEE9V}4vC zmUB)-NSGui|5k{ugJ~X#A9OR*MM(^QkP%o`lk&)!D>Pm>2zYgP&KfUZ7urBdRs1?B z=vKXX_!)BGB)Z|7#s{!4>509G043IdstXKw+!acxvM>!Dh^9S(5gn?Va&6yQ#Lm*pyEo`$pvDZcYS>( zUo;=g>eRl+Jba?_lD)x_rGD6g-yVdtrpE>TFD3PLmOy^DAJ% z_3TGDR$+EQ=0f>n~K%0+A9%CoFqAqsTu?s*u<>y z(#`vnP%)Xf&$CNww~bZ7MuP@_tUXz3#+2yusS13wUtjJ{R*F@x-{We_L~fIPIcNRI z?@2U$JBjYER9CZiK51!_F`AD!ZJZx{RorVb7N)@qu|o4c{oZcMhA(Y9s^ykc`C3!N zSW#^i)woobwi=G|dMM=YOG*@cTN^PCj}BaokWAv?b#ns`JbdgRCU#y$(yBR&??gmh zv8vUOaC3uEWy=>;r!7cr!{mXvc-`}`SV1fGx>%33l_U!r&!l`qRo800*dWa|Hj2*O zHS?WWpw&)2%6CD7Mal^9HsAS>XJi4NspCdeq!zfddlA#(d`UuGaJI;p#WB~>BYSm& zxU4u6MGixPNo^QhQQamp7f+nZMsIL#iL4~oQTsS$UP6~;jB*(Ef;Y}v7~&XM72WJK zw1JUeALFnx5~4QQKOKLE1%bxg0j#dV)-v@!Td#3@%#DI2=x-Ly54JGGm*75I_2UyYY{_H)5-$tu|E zxwsSw`g&W;nrtLV^#M=U7)t;hMHO>rU#B(&R836i3ZIC>6PaoyA72y5b`rftO1h)-)X9!0o%1$uZ( zFQ%0MgQG!zM0u#92%J-aQ;N~&fi7!gMFrd-51^cI!ix{~H^>}mB}!(ZV<@bzaXat0 z=Eo7Ol*h_P!_!eE!Swd@K?UtN)hjyNmA`m%_dO5D7ZU2k!x#$S$9rR+hSDU>B*&oV zD=av|g%Yj*@Wst<#I=P8Q<;u2X_AHy?6E-mIZ^alV;5Dcj^$+n1-Dp9IypPU3io zyq5!Sky)2Qjh5PfKH1`M6@1b%^hw%%+MN0rfeCwIRsxHp#JcFp<8%7pkwe%yg*F>1 z(+|v$Y&cCX#-Jr*1$0^JHxh)#aSHja3U3Q;w zDB{^6LrIyqRq4ap1XtoWY_6GCqPglAVO}+Vu&)3BU0r0(`-iwn>|Bq!85C0N_I50^ z45tRSBxz`~t(ohHwosgH=lD#p>4UcDF1K}X{E@xSq{VsiSBV;-%gbPXEfJmebx%2a z_Cy|Eh@homnF+tR(~Ak)oM$nOrSN%fW3n&#xu>6aS=DuJK0b2W%OKGwNG003qU8$7 z%EE?5wWHJU9+oe($mvvp*!d#ng?&a>O&8mYlU%!s077nrC?XhL6QzUO;(10bL*u1{orB1+c< zG8lp+zIJ7q)-))0e3oCtjngU?kef~?!&toz-|}pMexQ4I*FHi(j{;^xr)=At8CXJ< zGb7L3JvU!s6&^}LV}WAgj=qp3fe^rqBtLScboFefT5|<7sF8@2j)^ff%zN}v#f}FI z;b1PQ5JUO<_C?QBxU0pC&+I72rLGus(RJBbBE*LFzc>{>KJ1@IO*^a0Dxa)Tv!P31 zyI1O3EsQ*E5Rnvf>mnc^cw(i?<>$0L_7f@&2YyU_bt)a998WG*kG@45(O5fySHJA* zTxD^%Q93gEC@+N^r|7g&x15JRM->#l6&!MAr+v0xeb&5xPi;&;e=n4O`qc^kFWVh? zqu?m8O_snY(HV|N4oF02a>Y84`2+qZc5aH`4co@nNpehfCzdE>28UWkfj2ni$sXnE zUz?*J-ZGnS*p4Z7r}T2o^j4QY$Bk&H$l%)cTzC8m-})k!#bm+_63i(FTfO{40LUS8CXnT4`IPg8|1Sy}To%{-;kn-j0Ee_jkb>@J4PEgE00;&s%_f^G z;}0ff$^K2sI@Mso$pM$Hw*{U9)6<{DsUHv5N1%1bOy;tRL( z4*^bC@jc|@vgJj|SQxI?CG z1p>e!r4UC=Bw9FHegumj+6oP>P5blTlrf@ZyYGJq-65p-#c|&W0DPN2!MC>Q&-E2> z^zHx3mI0@-aP{h?R7OHJwbj(rG&jE*d(}2Kr|grXprS&8Q%){QOy%uu6&R8JrA|#@ zg40Bb6!wjV6%hR22iDmxm|1W-cj{W$P9l9$G4QTyb90dpvJYO7vMP4R3YJ!?YZr`) zf-+il!#D;;P2$&M0#^AEV#bd18K?5KErHW3X$yna8=nS@-ovZw{*B=A^C`oJ>~4-| zN*Wm+2k!nUGChd;ffa-`{mVCtMfw*RqM!k;PC>!pD~Sy$u@VdM4d{t{4pT*OT}zp1 z#~-f%Du~^R`}Zobd-US|)xm63HbbAZ_CCpf*+NZ4|Fv0Dhjk&c5Yi@PogWOiW1-bq z>|N_a5)O1RLUWgVBT!~vzDx_+`&5~V-+xX#HX<=H5+`p;PN``TKRN?lo90`rEv_r7 zT6F`aN-m1M_p9b9L38uNYWH%3P&-wdIh##7vhsL4GNP9O&Vd?}z}Lusn{u@gl8=zo9^67p8SyX{8YX2Z zFP(AFz}wPzO6uz9aMDrD)uGk&tSIHA8G*3T0Mf)kRTcI*U5PzHU$$C5F!v{vt#ovL z+bQp+E)3PhyZ(;H%4Vxguig>F`Kp&zL;(43MMseMHh>E5J`u21WPMEF=p^3g^RiK` zXxnUC6f>df;PYk(rKs|?EP}{hz74$#qbh$X!oymza_$QGX1TmEF{3CMmsc$)rf9n- z*p`gp-$%_)=c-5LXX71CdFXq(+Yd8BP&9F%7+Bpx4vJp&K5Qfof6ua0afTD9Ejbl4 zV#rH2y;e|ZlV_K&LfIPCPOIH=A=IU2tjwH8d2&O=7CoaZ8ltY=jay|1EZ3tc7C6>Q*ZB8U zXf44{1hBHOz%Nd(G#H~cH5C;_Ns)$zhI;RJ2&t&Fn%4#t%)RR*CB~N`k0Nl?zhb=a zcFZnrXmT#}NP;TpXi${w$3nEOBZY(rH^Qg`sAC%G_7 ztb0EQY58a^8)qR}@WRadO8 zei!^keicMCcp2K4b+PAgD7g{P8^J(uW&9sqbC(ZbBV|E)XTBdD7OT{*`JG!c{gBLi za)MF9n6W;dC=c@XP1BotyK>br97>U$7iRrH`zZUM&fqh@ab3bdK$uzuq=>5Y@jCH; z0x8R^PO=jyA){p)8*vv581z;1dRgvoZ@+X1PPIY~nN1AJkE`Od96l(C;AwO{T@mo! zXJ+Yq`Pgw419na(V|}T!SKXn{z8pz!KQpmn&_@*t5G|X;_?M;)XmzU8gty-JgM>ioI|T9ZcFC~!(rUm z;Lukm>_mwPF%SRG{{OMb`zNC6b7#TQR_pt(vWU01U-UG8UNq&wUp_Z_1SVJZF@b$2 zCK1}M#`tCC%>b?6AJ=(f)sZNKDU2JyAJ6B;LH)jXJ z@``WZnLenXfZEicOz$vd((Pil3EHj|4onqu;Z_COks{fOWl6<^x~+ zO6B6Re4ybsG^4E7tUrW*tx6I;75BgCy5bZT`2c=DtpLhDL2|smK6QV&FgrpD@qswv z$%u{$@`-LGK5z3zeETpZ&4yCw+k_7R;IsWnIR${_LN$g=bY@~_FfLlgS=EQfy3uIH z1~;uDO2;{!Jdw!2%foII=IlxhAZo?r$V+$HXhsj)vtzrF@%6G9Gb z_PUqVv2MDdjqXJ|n*fuQv5mw0uxbAL6Rn2`1 zdUP;Zyj7N8WZHQSC!<)7Y+Ida{rfXyP8+fP7N?~TI(TUYIKlnp=yx?&r|wW|w+hEM z+1&zlNHHhwhGtG_Bna>T0421+i!oq|`G1nWJ=g$2w%`A2*2~WM^xsZ|-wDBs;D~V;EAB5(L-KuH%w0AO zo8?5QPXLUq@0@?d$Kie=>Heo!z~Aw57*ZI4w)y$c7=p0pxWbXr?oW5ps91TH){Ig* zX^aJV2|&I*g3&b{0o;y)xX=P}N@xY}m{4S#2^x_nymKAlYHRcv_{T!iIntX2DoYWX z=H5+}I6*3!!WaXo3rF-r-JOkSuq9FMO;W4g&i>}#%X8oZs4RW0iuj!7z@PmcYt7-B z6-8F3h6iDqimzlxW88Rhe%MpR0MdL+w>;@L3wsztGq(``F@ zQ4~vRY|II$iek<084Gvi+oP>Ak+cz#(a}b7%5AL4tHLO^!%k2j-#H{xx04v*Dtap> z21TT>Qp{JnihDHg+ZnoxlU9D`XbhlLi@H)_UX_W+`g5>}Q*;#t(<+Qh2zb_My*zo+ zL`luCxe0KuOOG6HXS80g`pw`|U3K_2tBF%eeiS?+*;hIy8<36DhkNZbA3hnw&6O z?bYC=tVH4KvY*;kt(#uadu+q{_n76-WFq)cNO*80^jc{|=KYCK$D+Fw5uS-j2n)+{ z5i|-1q!h3WN{LtMC|jth{NZG={jRPes-32zrqZ)#?bq5^CgbhS5=oOHFXIWMCfZ-Q zIddqmv^Fk@-$GaD|0YGjvo>lG;WJYm4V@yUAu6hjUMg{n{;8lQw&;%=uCln2CekW3 zAc?*)C0F;hB7~twyz8pEKFs7rkmY>|s8hwz5m1_Adr?ZLRv{s7uWvopSC~^MR*7s? z`AxcP^yN`@@cVm+mvwdgpgraV$g5wKvK;DI0|;Duv;G_*v$xj+;B@@u6?9%=GuE z`q}5#Asef+Xm)uiDJ_KzVhUOc3VuRy*pj=)ht@h7Q{R^%$ilPD+kvNkq6^cCc$IX! zp^ldAV3W&7!}gmWg5U1B|FRlf!mb_B;W+In;}!+2-1>4lU%p^8WM}qtFLoQS%++Bp zp9OiKSy&0Z#iukk2Na|f5Q9`RghT*QsHvFp)$j1A{Ur_TwxGd$0r!C}j-=<4@ijlr8sFf-XUM}v z%j4)+^el#pg)xbOs-iypB-P=m8P%R*9b9izC6VH$6=Vubdo2u24Z0{bDUq?*8x6|l z4EA05qf<@aE?Dt$%hpIraUYs`TLj5>DlP>4wMboCd(5Idp7HaQ3wQq%f}7DNhJr@3>dBPclTpgSS=TdWVREi5 z+K=M0S1G}WM34OBroA~YzPpDg;l`t`HRYPwPot};~>m81GhfAgeX>I zTgnZ+_GPo29NJB9FZx)xb6KDB!XP=7$1VzKB5RibuYGz;|GL1NS^SSz^Fzn|Hmrum ziTZ|MR@4bmz>QBk$^-r~CahT`9GsK~WO5lCNx{d3qkNW zAUe?i(30awsmv^nM{{m&(pZWJu(2s>VI+6zsFozKf@aQXWJEmshobU%T$pfVq(oF= zlKJ*gh1cwbjY-5r)MHr-1M~^u-hy@uL_Rs5I!hcC7hZ zTBl*HS2lax?`MocO^77(@~KE*MSMY}eIKx&(%Qp{9@WPV@+S{=c;mNAU^pB?{P=Kx zDeI_58tT2Os;FxZ=@CuU-R#s}bsa>Byh31k^gU*#s$)KG=9R=*-KesjJ6@5d0&4Yln{jp)yer?7#g_YZL$%G}^ z-kF}5G0zm|_9cag7gw}(cxojGe}yt73j3#TUTu4;OC2Ld_erX=l!m(DTX)-s9qU2O zsK3|vuEhqC-+M!C8jJZsVFAg}E6)f1^=UQ8Wuh925e276^J=;rJT()ugmtadzsTi` zXG6&z$FU3qe2i$N2ltDeK=IzS8F4=;gp;-K99V<#OC@4pU>NR1Pu1ufP9#A%E+joVe#SS3cVh(y&0DJVXJ~dIY)?3 zcuzTG8AY}P13nI040e0#=f}2PE5tFF0rZ^=G{6pKz2LlFjRL`~kN)|hF5pFPxOtV1 zKMDLQPenUderhJXJLpQb<$(#QM1+PXF)kcoc#IaJRy0Wog3jxbq!<|<5vQn7#ViuB z_O!p>C!sKhq%bEb%xGrftiv&fbDc>@j7?&0#IEY4mKxudR!_ery}GRDf)d`<@6Gj8 z*TNE<+3HsT{p#%We2kW&Qi1|dgkewR zK5>9#g-0wxgT+V7wv05C=$5|ENX}VW+B5j;>ld77fxDMZSbWov_czuflIpG>Cq3Ap zX8odr=9tnr2Cs!rroimCuOu`uk}!rg)A6Z8?@Ztq7zq6BYo9wyih%NohYK$9ejb(s zYwL;@)~|Jng1@Hy5Y{;s;bP~V`kY2Xw$PmZr~g#j7yeh4-#efuY~HN-`6&xt&R-Cr zCDL5o`MR|2=yK>-fT_v*HPaY!K~rCbgri|eHWUHIh2p&4g5mlPgsiIXRK znsVdyix4+x_?*#XVW}yJviKw>FwrLa+@73X7b!>_m!JB=6(lVpm~Li95f#i@*fbWp zN-gl)*tQGlDJ?O-kaBg`h_;X*Moi z4KDc64d&2z4pE_ZzB(^PK1%dWgm%#$%#|~#pH(>DokZqCJj$#aCNX@#&nat_@Iu)z z_+D3304<+r^pt3)epibngegGcCkw+%D)=vGNe5M`D79AZR;Yb#kj`d>SvwrF5wo1jm!yQQSH*6u@^|`wRPW;VbMda z{a)9V8G)d;L$x1xg#68z-NExkposE2c+XtjpP;loKydXl=l{^T zbA+QgoAlL}PYY#A);?{o0}s2{uZt)~3x}RA)VmVjdr&C^Cs)ZrCcj@NJj zK8R|k11itKoI5yb4Qut&ZTZ8Brkwa_RiFM--xlu~^Skwyk1XVugH_9&kp2{zi$a$> z9X`EZdGxzje{y-wc40@Zs>y)nQQM+BEgQ@9H#|WZs#En|RlW~ZugFM}z_uFKKgw?~ ztmx^EK}|>2P+z*^c8KRN|F$q+Z>c_ZA;CU)rgdC@2D)8>mR%ok++~C?Mim$Ey7u(> zg#pi$pbf5rEkwDu`G4$r;E9sO7Nb0gCqzY*!CW~hWBM-bHC z#09XrqK!;ko=wTL1E8z4pQ+^@Zb-=BwnL9?Wqd(bDrj-}IVY5_TQm3F=+6l*qTHMN zFK}shc9-N_;`4QyW2|VJ`1fm8RHCrH#A?4mt?s61W{6bW(@K&;0=}Sv%f;w|kPRXn zK)AS5RY)O>yEj@P-M?4>B4DbTc;Mei>uH33&noblV`_3_wSq^tF-=~6G)5&US!9^g zDOHbnzW3!t&-QcW?=T8sd3iIWJxWh5_`pk$PxI2fw)y?2olG~~Y=Z{8F*m%-Z-++U zR}ia7eof*LJ2U#5gAI}eE(UrIi`m4jhC`p}OG~J$>Pb$SFT!(@%=$y3yQbpq`mq&Y zt*rnSo!4h?IswIf&{q}rS^E}h-F0|u_Jb|iPj-cJf5 zh|d66H3jJ2hcz0}(Q>M$FrS_;)nC%RX0UMy1ybZN!;P7l0hzrBIbYm4TaJe^^D!^H*G zmx#b`9QKtLJOJ>S)5Uvr0=?hn{jR*o)lU*BZH1|5>j@zkx_N#tm~&Fwg7K4ULclw= zp;?Qjc=4RbAl25BO}c%icgBs~r7HcaMIx-%!$mi~e(P3ueRU^?lYvme;6*yVd|NXo z;EosWT&1VP-Hq#eVpy5(VNq13(@ISwj@V1m^ZGc5q|Z}{n1lhagky6h4PKzPyt*@#|fs!WD^o6 zD`(zb@j+PQ?5~{LC5zV%EAUWb653WQ^j8C_i+7V>hq_10@Y!#$OlOb0{H@QrTEGbM zf0`(xpcOZ-Qd(M@*~bRrY>h*RwF6WHi!y`l7`8UcL*wphv$^|1c^LB6VXI!Tu@3WXNXx(45q&WExQ$ntZB|eg=|X| z?@z-vbt-F6=ZD9CK)zM1H7gX1A*d+oCgjtTekK%Mhkgt?PDo>pC(}D)gCK~^I1w${ske7d@An1`zGfImXe+n>319n^w zJ@`6;s%RCWpxku)Ju-#5Y#?C#k~rq{7~-?AA1XdmjD=Ud=a}F6Djt!sg5Js!q}#c! z&NhBh!6v`gDeOc%I|&HSmQ+k>ZsLs$T+`T!lzoHb)vR@X8*MlkuZp3*+e~rNSg7+{ zeMnZLZldSY-u&x{C9(_p@YiTt3Kw14Ok_EQFG_oT7T>qAJ1!C4ReIbF=8T?ge|{BT zPLK`hJfb5LhLPs_+`;?S44$Qs`bkJmF|DHtv!D7OV2dSzQkTOfKBZ@?0h2ruTP_Z~ zibh?oKvm6;jguUL2_%XojPTyAQH#L3wSo@EGM9BrmNMh*s<~#|c++t&x2>kw_Ml9B z?yTpdnyZ^YRf#;g;){E&!;+AdH6k!L_J->91;dMNi?bWxq-!i14n+@>evyTJ~!k^2A;2)POISuY2h zZxbR2jDP_E{p1Rnp2|&|J7;2tU}WhTM_dN)9&v;vPuJjE3oDO0&rFW zU=S1f-gA|E-Yrs`yg~$aU2+Dg;A)hFwWYEn9X5h6V>N#%!E(D$J2okqv0pBj<@%gP zMklL-c}X*6vF%!9s~;@e zm#o4f922CyXd5CSEGs5X%3I3wxfF>$)%Sc#^`T0sk14SKX%tNJW*e++k@ENzofEA zs7gzh{T4W%BtW(Z!$gith)s$;*|4VBkycXxl-28*NEoy3pH4QVr`{!ut$7%yv?v>| z-ifKvmUe8ii&Xcw5%ag#Ech+ZldgnF7H}f4dHFhj9Ge%&+K7#g6)-S3`Ax<0EhHqy zh+?~4aR{s_t-S*qfq$i>><7l>MyB0DD4U))G0{LWhD0rXio+%BQX|7-lc+y zSesXic_fv9VG^+jZd!!!E+(f3&P5*%7NjChhM>c6BLJCZck>p4pM@wi+E|z_2`$^+AQy9Etn%mRGO<#WY;>?$(+o zRst^k#cA&9Q$J~D6L;dhR~vtoy6U)nUJH-Jnq6JLyv9c;2s=g?H**n44Kz+lx0*c1 z&Iqc@z^(CKoDg<2+l!x(-i&DV*$;P}otfHW;{hdPbfS2an=;%rZga2Fr&!J?@Jy3l zo+mI0$=XOwR<(MO0Z+@rp;5Qx%PGOTDzvyN5?3XAN}?s*qbV=x$xR#~OuuL`ISaKjwF}PP?bh9aTh9NZB>T}a%S8rJ` z&Z_c|?q9pVGtj{g_fm0Dlgv~U`^fV>QsYVnEq(ezIW<+0m8A#Jm#J%Dl#%Mv}axIVD;8i85Pwmp{Ftd z*th4vom>}6CCkinkG|RmoWawBhi!k@m@G{e7)(qrvEy#r7wqd_d7W_uwq6yi`{=dZ zxJKP+t8Q+OcT|H9C9ETY#`tR`XLM9z$C;rtw3M@V81;kFUGKV5=$l(2D06hQjp0ds zY)B5_jR(0I3<}%D&=a(Anma?h*GKj2Kx@eJ*L>DD=;_AW%2<( zW{4&+e(xH@-IPhDsD02*XumO_fG{LvoYKr1s?>ICgFkO?v*q$LW)Bd3&Z~_s&3~8P znqG!WbSRBAK>KdKu^K!PXz9g6gVVFo`p{_z31OxtT}qSt36xCla$kWR2!T1KTzGO* zxzxDl!#p1+E{%?DQ9bLEfTtj`io}Wd)0^h7-#;c7kDFb} zrG{{Vcuu^*iOtH=)kApifUTAj;zj9Cj3`S_BJ>|%D@h{gyB{z=#!7{?EW>REfxcPA$}731yEz_~eAdwZt2x%#(y z@nI1@J=v`IU+K#?<6Z^^`G=*Gl}?Mhao8P~b}BQ_9QlPRg<@S#+_-#iTi_lRVx}MwIK??R##25 z_ovl(h8JY*S!LT^ymNnQ|DQ6uqmuRRKnTqE0f*1pb^iRr1;@R>eX2I_iob0GzI(#F z*Z=!#2>*ZYT=XA8TmL@##)SbPB`Fy@Ok!sT15Ql&*!48KxVShaKWS96XvOyaZwLEn zb1V!&LrXii7>o&LA|&+Ti!36FOMYB&5ptk;=l=9#No&VVg7lXsceX3 zX|isQdT+cDC z3Dcglwl2|Qb)O?3AiVx0^?QPJR;G^7fkRCLmUtWT?G1y z^L|RHB@y}^L>BCyA5H6!#TE;yo=-=pJ|e7U?nz2Cg6 zpoAKD6NLNcdywxp*MA!7!@$_BFhaqR;b8my{fqtNd-v`KeYbJv4Lmq30RN9`^G)w3 zMAKa#xhc3?`?uot@julpIMsw=q`R}WY(jH*F$@<=QUh_vuJdeQT8uI?dlC$ka(AB{ zp$oq891Bhcmglvd?^>CNv*dHJ1xke;LawytDVXw#$GhdMuda!wh+}8cTV;F}<#L z;`#uLd)rpZcNcu}cw&vyX{9yV=3_~Ows@QH#$Szj1-jF%Gw`P`KYRzhNz2za6=$t1 zcb=VvIZm`#;cNjL!)b(}EhyS}W3v)Ppyg*Lx`FtsLIvlvMx(r7fcbHT3J&H(%+CMnFr~V zK`w>jw{=H3y4H(mgRIew02P&uQ7#ij#Cu6?PirU-_LLq$5OF3bCUR{S| z_USK4~rSdFO*9a&$0LT<%q>#5hSZu4b$zJqD9k!xC{peO`}!RP z?D`RoXoK_MpfLZI7`@FA8i_ge^~umkYFk-QT;@WGTH3CuR3zW_jg%vrDTmz@vdxnS z>=$wEsq?#spt{03?bffCjk1602F{IH+NQxOk@A;&tH%-I$&)bZo>vuRH}T%F+R&KaszGW1 zv-!fp%JWZ<>N0rbWLiGH()tT0=spB`u0V_=PYqU%XKe{L;NkwHIenpSw*W1}pY+da(A*J^heB$E4?6V8RQ~sZ0YiHN?G;!pEif}wm zw72sn39`lUa)jp?s&&tbGf@Vc-%-Int?5}VeV^!qO3Zwqo4!Q|*0(R5X9tA9?}^DA zy7U;8T`RCFL+QfK->q&N7v~DkRclqbr7o9>71Od?`LohiJlypSF}cnnl42pp#uIWC z)wIQ;*iBjdD-xQk3Rj_R8(yFCi^JpNOlILv<|i6yB_+go zvuRG)yRpXH>(8};U8c6Lh8brWT)%I?tyRis(zk1+Sn4a*m{=DZ<>Lkjg^p%?kt77d*7}=VP-M?E%v{_4@U$iiZWv+ z4H9JKL?|y&Ma9@*1EMK^mJ`v1NBp~X{8XKOK}jfQvgfbGX}TJsmC_NsKIwej=}Km9Cc|c{ixE+EzV>6XC8oCdZ5A%>64(o=Rdn1-croq^<36n$k*f zs>(BmN$jCDwWJCQCErkv%#71|{oYEBHR+$N+j5Gt@HqgW~db z(>lv|3Yu>})Gai96H;`;C{v0L?S&bYVFS!X@+t_3I&zq@*N|`us2)GksB`&q{Robxyf9M76QiS}6B8>+ONAh=>C!H+ad4Kb zDBhiEa8#)pGAy#yw1$3{8>sABtXj6Tt zNtvgIXW2>o%&n4e@lChtf{|}=j=qF?;y@}Jdv~myZK9~^zz^RuZzvQA0le>4J^Y&> z%am@f)+-`gig<3;MOxq*TT6li$24UGRQ7C~uj{P`Iqt$691zM$*=jDP$2WG|P^N2Q zXpSb%>cn;t)DYuJzVkK$^B7@RT9;({B&Z#?%7T~=&rt#PDq{AN;z6t(kVN_lb-?mO7l$XMhR6f!_> zrAi(?KBA7vHj6hYtk|1IWsRq^Qr>9j2aW806}wx9yc<8`TtkWI)=4Y|5_bee+$pyU zD7>#MiLRUEC#?mC1_lq$9DpA(=UV8e4GXgH=Pgb>btE)7x5RjIC4zO7NX=WUK?h&P zXX5W+>YKm{2k_%Fv%12?JTrpBk08Ko?)pR@jIR$5%(WoNcxG;E;TP|L%6z0P3chNj zV|TPlE_ant%2ZQ;O*uQ*RE`;m!y2`Hd|vaQbly|yo?a7Fz^dD!VmY(*O>2X#796|0 z_GlZakTUs**NJQ0rs=xP7R3MoaV>;L>#EOy3GS5x0E!%#6_vI>F_^la$iRh=Fmlw} z&Y*V~!8b!}p2;CI?9GD6d&zC9evOUDCdMVL)tzsDqL^m4t0MZno*FzWX?&cTf8eEm zV+}x1#QL-#ACtRP_vu9ei;}K?XbwdsDBGsX1rT6fKlClIvt%}yNW|u_F1GNlDcVigo%mS&aN7S z4o&Ye)avF1bVzK^YNq_v$wC0iZ*32DmIJO^Q1$6zz2PUq^t8NmavGiGskW0az8#%| z<9j!y)KrB)jBN&l>}gAv2;DHE+cTN-Ey)hAfIKUM)5tgZj5o>7AQbjil&xQhpaIg= zXwt2N3Xf?NRV`Rj{)($BO)uu|?`kzp08UKY{Vd#ID+J1Hu_Cd0Z4a5-#@lkAo=L>KkIy z&u%T?hq}=-N2iQ;W%}h(Jq@YCw3WpujFu6&YN&aQtOlqG2bCcw%(_cyRwX~B+;;1I zfR`4bFJmK)$ls-~kB?srtyKfnYLVF&YWhOEQ6V^7t5GkX2u$F3@sJvp9f;6= zk-c*>;#re6-Fv5?j8azXRJ~twpJme}-G80uBOc#4`>|QYl#M!4cS_A-((1wAFvt6Se;2cF++6u(1Ig4?^<>^`9m@bzE=U_8=p z&Jr6}Cl{$0Z!Xu^vlZs5TyZBEv;dUMB(b;=c069$9cIa`R0YBAi<%WL4U;#T=k2)x zlL$KEnyB1&lme#oMw*pJvL08Sw3GV2le=RLE=A6E2x(jvpAT1SEoG$N*MO{=ai>%2 z!j6G$-7A;F#-bjRUqBsLt%hfQ>;_7^9RxK$4Bt=@lb=5mf_ zbIiD*RZG0f-X!3~@LSzii`>a1jC^CHR8rH1P(5pvB8!TO)=3Ll&e-2igp;E<2;Od} za@vCc7j+=BfQGAvUp-LZF7&HVHl z{^?#FWNnsIHmYsvqito@xca^wp=Mrc9wYr>+K@YpXt*SHR;%7!NK9=uVGT*54`HFL zgjvqHN6V{Lq_ z_?l%ux~{dN0Gh@h{=38bq`hIe0mqfcIojCFfclI{(mDs$$O3mR2%D69G1tmFn?6QP zxqoN@)wgt{w7iu|%NgwcylfmUL^+mHwan@f+XpIQ+`rdaJdY5S-^f(O(sWP^4+=8M z@sUHu@_5}xwyj~hG5HnERZG(h+O1Sh*fO`#QFdlUYJR9Yi5uO(5_iAU-!&VY7zY?GRmqh7W@3|=a;3*?Qku zbGBYJXekn4aiY!ik6!dod^`~qggc&jaM8CUVc$Xp@crWH->W3x^z*+92Kw>`@(|Mb zf~0s;cL$DK|4@=Dj^rWWe4+oo(NEtmCEq^$eE}1-4+%oQ>bqLP%xDSbCpadj>)0Me zO}zIX-p9$F>(UwTkTM`0_#7gsEo42U20ly+ZhCY&t0+oHSD+0w8Sdt?bN5MXHasm#>r@3h+up5@JF zF)*72-kEwgp``ba;AqjUx6b!M{Z9Wz!Drrmbz~uPbjUpbRYX1VD*5b4k0PV|ay-Pq zbR<>mL%JGn&M57EX5(LRszzRYNI&y>V_1cxsA0ml9?u=Eadok_Gox~CK$1<%>=`jH zhLW%`xQ0l3G?Ai*eDF!b!`7u15$-&SGe+U^Ow{Hi!opK}r~q~5X4z002$sn_lmA2_ z|5xO8-C@=B+)^BA0#DDU#&hjQ9t(q+%)C`i4VBR~7E!$4LGG|Gha+!KJ(b}LYRW&p ztGITptu0i_s3J)#VZSVJ|4K{eyjwl&DBZV}F}f;5Sj5J;*vYY!D3y_?x2y&t+;Z|> z@IS9OoqsorRwCOPBwy+!K~um`h$r2>aG;a1-w^HOK=yg{u%22;6yjfwSoeXDmRfq+ zy9{u*k$a&zd%Nzdm-z0(dPO!|E)nXPwAHU(B~>bxnKPYfqfz@hNV7iNW!RdkulzO| zQa9!i2Wv0s>NTeAt}JgqFpT72LirT};vPZ(%?tu6Zn?zbncjGM4{K$9B{|2pXmO(|^ZR}ci+QY*B>4{q@6vrOq@GZ~ZLaa=ksYf1{MP+6O zu}eZOWFJePz+t%8>w)Y$I$$XYn1f@gSRasi(?0fyA=XJ%gQBZZE=kb&JdOC7k5P9h zY)pXn;BW*>?>Si{z1i$GSas)7Pj-x3Cp=d#B*g+0H&`jzsOhMx`koctEAHvbJBTjm zKT`2uRPufA+f4(yv$B?^m#fuDzu*H3GP5(&r2W_H0vfXWFxDpG1ZLXSuPm;kEu(!! zPInap%AaUO&1SLRgx+Bp)2b?gN#t7mJh!!#etxh^MT0>8dqVV}3()KRd>l{ZK%a>V9Fn32gtrnuQmzG-o zDZwP!6y)7w!*6br5u1}HZ%lIVQInsWs&|v9ct*NBKS@$PJtc{4QQrDk;Y)1MRt&i~ zT!HLikV`w-h-UUdS}I9ACN9mN3n`pJrfn~L*lr_b{Nwq5n+9ie!oq}#;O{69{N}i- z4e6t-N4#Mx{%)8SPztv|`s?GCp5hKMhQOhoy%`Z>UktNw_mFpD%oN;pq06QE?yfSz`j4o&%nVeJTM0VK2Ad~V?!KfL<8g}QIK^-b=%I06+gMS zKv#EiAC4ee8 zrhX&$Kmv;ho;XUi!sq6f_ojqk_l-j4m!#ba*aWs8s$&|OqDzfuV``|3%5&-&dexkJ zr!mmPVko&%uj+}H)cYy^Ib=JH!1Gug#Om}h@D%%e8X~c11*hj$6m~3Wk_u)OuZna< z`j1@&8(P1ST{y=jl~i~JUEMw-6wwe0>cNyy9SPj-S1h8u?Kk4jz1io)hn8ndwIT<< z#Oj8|t;w4US%U)|1Uawx5TnPKh@{wXnySu>bn6N;jF^CHk|Y6-R2>?gyb21|RM9b} zJUZ6arX@j5IKm^y8RY5Dxp-=Fl{DFcnM~+A5n;a#>QpMNG$7K2NYj#dwGapm` zmbcz{*cg9PKRj6)DcVO(1aA4{Q}i@i3Pm{Cr4SxHz2EHM&*b|TqW3)||6}EQ64sTa zoTcmk%=Ffc(zTH)qlx0^rl5rFH0S6U!-OpKoU)ELH5EU39bC>BnP@@n+#YPPWY2eb zGYs*7_4avLVsySb?}ua#GGj^2y(f(Hj=4VDR1j7dndGX*$Sl(^zF%z5+Bv{zE?Qra z<9S~kD$aF*K{R8DgnT3UTJ9u@Uks3*EFgPt$imeje=xvO>j)QK-y}jvW32 z(7lMtqZmjO^j7|#0NtYIpGNFw3pnl1T8DqnkfFFVHX(j)hZds9360wHr`JxC+h_Q! zct)1U0#6+d-%Yw-OQs&2oH0ydQY48WHCFV^%a`a#dh9vcN0z#TH%5-cN$dx5O*&7b znqf}am+6fuiwy}>B5p>T2NN>jbc*ttlbC3^R*^&-zBCf~m60^?VK4ylS~GNQ=);rP z1gmYX00%KA2JVZ&>~}h68KET!`rwwSc_~MDjzZ6?A1`kX0=%LYLvPmkEuJN=Tw_d3DMPiB{NXPh6b!?U75gs~I@k%fr zg@&Nb6K1zE8}Rlgz)_m7e-*{krKR1s7PN>N3fLt5N@Nq?(_aRM?Gp5PBi|(Vc1TK! z5wl4?Nuf_)oUoWqZYFze7q82}fD>RX+@#3iUZ}F2{`1ql5UE)`W$7mj{OsA$J3bBj zh$k`Spn=|y5s2BPV_H$_te&c0@W++ozwbt8(OfDDX0vqFl)q{hSLa1P5!vgpJFg&w zKb<|kTzBo~FWkxsTzYSey)pT6ry*gZM?}wzCEid4RouUNKb~G3d33(W5${wgz12+q%-|lW5pGC{9$!GCE$(PFKmuZq(=h%QHgfv|Z(N%lG0XIL0@%n=3i2V%TA=aLWb(~Nk=CR`)(tte^?ZaGIk*dp_S9M9?ra!xQX zZnqbcgI&x;65z~*4CXAXD^gMTr3jzZ)P`Yya-$cQVyA@Asn~d6Q@W|Nx!ubpG;-pt z$Q%;($sUeKsic&RZUT<|M3E}<0pV8NZpf$L*zXn_*a+_uk5$PwUs3}q2J zmZn_STSr(VavG}C_WJat4|)s1I&9aPKsB}--|ETUO}3tObS-{hiQdB5rRP&9v&r=U z<6TdsfcYx(7*|F&awJxfrl#(j@f}(&XE3I8wT*M1OkpHy~tv&D++# zh4WC(de0R&M(SD=J@Uvx_!lNiUH%!5Ze$7 zfHh<(OHPw|1VxCkV+EIj9s1kL3b<9EN1~_xLkpN*DUKHyNUqK(*THqbi%0l|Yy%=CeEqv|a!d!coyYc!PMd~d*4vcK(ydX^H&ufaFA#2Fvf%!AI zDAHs1>o<3$ys>6enBvM48^yO19xS4%FhV?4*n(`-BB2gAC;x~bV9t`wpgK1sA8Mu- zs;WIWMP?wQFq%EbI2%&Z8}6K|q3v2qB{NF7P3ybJ1eM#ry*uZachm&05|R-pKy7o2 zn&sa*T>L&_nJfxJDM?1zH&K@A=-3pmSnpv!b^@-`QPosDQ2`Zq-?)^i%_IfN0jq< zc=pR-J1_UjS^@mBAh=@N?ayU0K?)}Xm#G{i zUdp)FLv_WaxEi0!Z!$}En!#7euP@bHdDmPw4*k<3Wrh66)%h*yesqF(QNB*5RB~;~ z*SoaV0S@6rXez)koNzQDdiNX{v$W4TsAP}myl#puUAgW6it4L{`sozcqOrH;>$6^T zzQ^8=M34PCaH`v}7$MJFv#fz2p4R7SVB}#Tr^1Ydm%x#U$#h8{>`wk%okD+a3l%PG z>5%4x-$KTp?g*HQHWP|@6j&e{2$z#y(6B;)pJO;uTsB%Xw#V8}Lu$)463$#iacOOL zOjrUC)*pg-3^XgoXXrK3*bs54R77DK1#-*W>n;WZ}Tc8DFIc7bl z40Dc4?M?Rs!qpW3P87Xx`xcQ^C5Hk_bKt(Jn0A>T(u=_TQe-b`V~1NsSW#*1f;Q6I zuJ8|VS7jx0JwlofH>4@|wb3ik;k=`~nBlcG>8W_;b~P9QA$afwb?5E$71(;|&!w-J z`Km+&Y=wlj?HhbO6@&X)%30QWdt-pJT@=p~HTLabN5e#rIr&tYr@YZ(GNf}o;NGr- zgUXoW9Pmn8>)wg#wYUvkI2~&n$+K<4sbRUdoJrR#@Um}`7wF}YeskIqdcaThS3xDX4e=>HN*zs{pd(jQuq)Cg^izdxe-0VD{W` zZ`ZWUby}!CEB~UWpRyG8ti2qTNR~o;TGji>`(@fI-q6xsy$vsyo$1hT3r=+M!W&`*bY}iABppb}BcRS&_xmR-&)4K)uFeA*uJDG*1%9s!K zMey=MeY#(e{-?G{y;Q{s)n4{FhEiE_w71U$U|E_Bo^H2Pu_u8ZCqM+kc=D9HX1|NR z;j8_iWGpZDeKuZYE^PT;zyM-bm@Pte^qY~&L&sw9A3KY0ow5Jsowjgtcn+SC0CA00 zQ-<6O%5{%nQ4zuRH4(;*>uX}i)?_Bv3gc5%ygGUz#`V>!CC)37%<-XZ}m;wgu}luiCWiTL0=wIYR67p)hd z&OKE}1vZilzh<}!!D@azcom=i-xB!z@ijEOR!RQk%XuFBcOvXhh<|g_fcWdk;LRca z2M>&4Kn~orVe&sQg%J1h-(dcn(Vy}^?DtRe8wj}1;PYenO4|PnCIj+{lu@e*{W1E& zWNadagt$iLSc72r8UYr&A%5b=UQ?ku;TwUyK_nOcau9ji7-{qO&OVERGmfBR!ICa?{xKjkm@(IS2&`l!mRLzY4r12$XvpSX3? zzvSA}!-GAl{|jy%NpQyhC)~OuJ1*l{TKIv<6l3nXnPx=#-Os-}%`Fi2el4h}^5DB? z8Sha=wA_LcP&MHtywgVaOUn|p#?f7zZKKzZ@YiB}iT^XZj#(mS9Ba{bqSow|X`akl zRsm^EUI4>TR*cDYi@Zsz;0yFxL)E%uqeOQy* zvAUBJX#tDU^C8lUz^M5^>s+BGT}K=q=cepPb#x5=`MZnXbmUxEz5K+f6#r@5s@;$G z9S;QwGf1%SzA)B8GZAy@+>nO*@GKH4U#8etXHH3BkKhN-(b_t+Z9ArL&L}KU;3cMS zPkG8?B#AnI`kIS?BYWDbbNhzCf0n(^1K)BxclLGihTGJc>1SENjQpg*4AsHxUP|d6 zFu%8>zV^iQtjBFn6@V{_S9)jyE?PFhgJqGw{c zb)(v0jU>o^duz)0rXQ=1!dz~6SZmmRtA3%=tnY5)J31@Zo$+a4jjql2cCW4!4Jqlj zy7>AGj-Ht`#MG|N)bomh{H;g`I|mE!3?4>xXIS$`16d@j2&|Sgovz}TtU8f!ejMr^ zf0gzxK{LY8n#8&-wb_BPR(TIjDKS^Beu2;2;ih~MG-QH%+hh~)Sc-W|VV9MIE5vqG z7V_w|EkuT2qaNuwvC`m?Wb>JmPOwm_Jl4idmDE;{vp@6ll2Cjy4b9HgF;Oo;5n>YZ ztPPx03vWeJ3Rbq>Z_=y_kJmDxOS68Xz;CxdVmLeKxv&%%GOzakeehB5`tge{6YM^j z4$?}oN&S76Z@;`Yl!Vho%@?jT&EW945F}%}M!yW;{P4;cHl4*Alqp+$ZFGaCD(IT0 z^!8UgoI;ZCU+gj2qW=?nOc3nP+_N2AT?EYnBr~X<#6rfFJ7&(gR3giB*B%-69SXQP zbV68QnF0m-_AXr1W^XEYxf)GpJ)?}M%k6U5S=v46HSY&K&h~~Uc^ADSyj((t8hZpR z^=Z0#{2H7=nhRo?D^KtF0hTqiE(*{f{NvJY9LeLaS)vXm;CbqJ+nj$Tl(##Moe}Tr z&ToIO$AZ}!q|+IltEu@>5=X^^R&1xMHJZG_f+)LMyUQ?#@1<)!xkj(F!^2wBV~ zwJHBs30ZXIzY&9S^@L$fP2+=C_>e|&M_H%ENwA%B*6U$aN@;iJFy8$IQdr`EOxTAuiht*90N1y5!kN)&?VqDyKJV7 z($as)b|$1f^-kG6&0!}M`a@wxmplf>XdOm%Qz_ftVoatXn)PXUAOdf#yu-L3&nEho@T&j$2h1UavofxG@~CBSvyk!}WqWV1+gCQCH1%cW{XpfjT$b&BAzp!dOD|}D zY|7M!siH@iF3PL6Yhkypq+Gm@e%d;V{gbX_?Ft8R)3 zAfbbet%{z-oFHPW`IUg<7YHq77iunHI6rIpE4b-F^&(o4Sh9zNcJ;?09izZNx@d@T ztaF;E*O0rB&VEaHL9rAzEZ}(VnxsV`q)u*VXehg9l_(znbsuMRQjQx5hhMbx_W1k* zigx>0Fw9ANQWN62p;jrzIq!p7sFo71QCjn}5nprNuOTs4tDE(>1*J6AB`)=K1cb`^ zo+VB~E}Mg2c!-CiNnZgqz>S*M&ENQZ#4#P5zE$Kx?)S)Y3n;qp2Dsy3^OZLVQkEQB~{f znt`gOCyaqpn+DPvjvJ&c?R+W?29~%-hj>znuwCi%7w%XM$#(h|X$_%YrR8%o84JGyVk!Rb^RI zWWeGT=<=((c}&jMCvR@1&!JaE4doGHA08*T8>^v0inkaj+hP)??;SN|lz>998R}tI z@;Q;$Esu(xwPgW$!@?J)M-|qv~{hD&DW*hK0nV78Nyx>&D!GLKjwjISS$SMbJu?pXZ0J z$Lbd4EFm4xF<|zI+mSR+v_x2pc${Nj>j$e@zc@=E=oNF14$Jr$f6mrsP?o|A`SP=z&C|5rHvz@|b@y{;eiE=_cOw4(6LN~6AVXW3Iet`uB>lnMFLV~hyCa& zZ>#UuLvmt7#7IhnOSec`so}Ga7X>?ge1P}~9vOnKxw6TcHE+AMmdv)O<6y5_T|&*bU%#>UGJ>O!ynfn1h$c&n6AeL-O>&~+dx+y8cVb-fq&Q}T4o zB&&?tNLJ&g6q$|c-U_E&M;QfW8K=_Q{a;r|jH(fmMqJQ^)%)*>9VF}&9^+mIVJ!na ztabNiPon!+Mj?qb8+w$YqA^S!ukV_Z5?7L&!~xhZ*K3|px5?G`*Y&V{|iLs zQn7X_uE~iGjGe zE+WPV9}GF}jQ;^!Io}S)EzEpU6aTUu;nhuGd1Rz|e{Evq*Wv$R-^Qb8NnG*dg~Fy< z<&=5D(5K@&(gz?iz5}%CAQX?)!)J@r8NH4r{OF1HM!L`li9(9voVT8ea?vBh@JnlU zLAMx%RpXK+S;g?yE)7&>`dvXsjaD3f#I0!h8_Bg96me7X6bhc&X!!zml8?2VgvMTg z$lpIf&zJ!OblVEsiUw(lDr_CbV(oGwioGq2PoLS{4F+G9r@CF7op}3(Ur(lfsJg*$ z-+f_b{l(66>^V+LK&FeWp)1(?9v-zUC!W7yW31k#dayGsKmWU+E8;oY#u)#dj=O5# zsW*ya7$}Etq}7Ld{70HRaZk^~Cm`h7=yntJF)EJpPYu zb;s`@?Pv#TlH$3&_ICS=6@uJmRtL*<;m&deWi_DHzSgVr-d`Y+na#1l|C*%2ib!l8 zE>>&V6Bi=;yjkG8G{utoPm;>;@#Ozol1j@8=`sYw1)l&|3Go+O-|+(Bqu`N@Oi9$b zSOL8jm259V4_hZaYcC4Iaa+;N$JY(JXq)A07DAsb7BXp+wPgMxkSJ%t1Z*yr4f(_2 zzvx`}%t*r$0DWxkRA%)BAtpn{rv~2CMdOm>X*5MJK*P)3>2#*_-bX@1CHG;pGu`}K z{9|l6|3-}aed(#3-TR7)xRg30uFBZA|A9gBHo2bulhv-=3xc0U0`U{k*V3H2ub}}s zn@~MyMQC+?sy~D(rL?9S=*n{z$}QGxAXnb6Yg(<9DATG#Y6;M8Li_n;p`wbIDNvRE z6oiQT*25n6@e?8{D(cYN-G!O`-dJo3r^8;lgUNtF=&9WXYN@qUzg{ukE3tb}=e$@1 zmAsb~n@k=~#2~%8tJj^HgW{saFUFD8_}zMS=`c5f#dFRUJK2w^3WLR`ww+SXMHVYj z^Xuy^_Cm1*ObU2>V=eusu=c8OwZD{8a`+iSN@}-@eiSsFx5uK5dZW>Q=ef%ubebiV zwguWbYEkTr_d8XWHgzUbbNylpG7?7{N({@Y!?yxzuu z3cok!yPp+yL zg|8t{D)TD*$K?Dgu<^xol!3|;*_u!|&Lef_JW2z!*>A-_Nh z@3#LRunHlY|b^up)-#GA|0uvX;X&Hz)CTr34z9bH{6_exO1eU z+mi(#fm-6XY^o+Jkm2{l;R?I49TY1-04`#0hNcy>Z3lyCz~e+8ikx?MS?LK9-I~Vp zJZDi%=rUzLE7XFk@&yXhosgU5ee>vxS3z{U&eDjkZ>qctdmv`H)$*z|JW#iaw>XxF zIAM6+b&o=ko0puKjr8yiHR8}xGf5D>K2k=5C$Xfg*KeS9ge){WIW};D8M-$9$!P?~ zM^7>c0Wu+`t%&y~a0RP3C^l!O{vrP}*k%KoZ=d`Fgd+l2Ir@fvdr-{Gb#DgHggRvI zd*KfotCw)eemN^#8B;W%Uze>kPW5cDnEs*jo_9e$!KK%Yy|Y|uQ4Vu2)Hblz{cL>Y z)R4yxO{>%}S3Y09-)g$Vt(TiaS51)dM22NmosR1{^~sVR12M2XM}}i&_a^8PY@7@h zI3bJ_Slvr=qfh|&Og7JmfHkw^MfdwfY4shw7sFCL9NOATya>bkv9y7d7#ryR zDh!2L{QRp4iuVxz4|i`F)#lf|2}4_;l;Q;n6ev=lxVyB)y|_zp2%g~9;_mJa0fK9= z;_kt%xV!tLzvp@Wvu3TC`84yc_kHs*IVZ_|?tSmG&$X{>?=d6t|0uE>{|ngOc=5vE zXNH7Sqq-0~-k)06Uu^z{ug8MFXCeHFcRd_MMEZ%Lyqn8}RdYNHMmP~rvB(*CxBKy( z$j^~8LlZ27KizNuamV$}f;cYWK?EU-Jq}+u>wh+4*3;2Nfj)Cu1sB1EmvicaPHH3?{$9&-z@h`ahe_WV0K9vDyA&As5I{%!JH+k7 z9#sNgJOtudoNxWb4a5KL&TUTQX7d#SH0C=kE#uR+y=!n?hJB`x6&@%0r+m_L1i`=a zqC`{{<@#OVi~Fy70fX*Zmo)`dr5j{&j7v{$FY)l=CAqs`G09=@0^yt`;4Kw_&#U%; zo94n7i+KNV;g^zPV%TD*0=tzC7ZlW0!{wli|4DLuB$$8wGydO21Acz^FY>&@zj8qE zpYKEZ?_>Y9Qt;)_`Tz33a!onT!Zmx;NcGNbLA=*{|1u2c{Jgv&stKJpXKN}R)yEmkG&*yo~qS~Vk3S@O5#Q`yF{}hHn0IB;xwQS@zCkx|Ex}Ps5 zCl1TaO0$Y2q2vV#L&osMD@VDuaP-o{_suso7f?xVr}%qU5d&qelJilf0 zBZ+ox;J2d5qypn33SFjvA&YAGiU;vHyksO?CCBr&Afysp%a)7js3qg}w;fOUS2@Sc z^)sB#cKpBTH-xNSHX@ENKFFpJg!*3=nzhzXDQErC#Dx0a78)0qoT7CXIT$L5`RlK# zWHH5DDo%a;I~zeTEu?bfP?`-R(=L_^A*4Po;g+M`A0SwI7t-crY)$h@Uem!p z>U6=TyyDZ}%Nj$a4ETj~(OA+(a$D)d?dFSG&w%#UNjRSI4d?*1zXM>csuFrM@ zol&6T?N|AbdKJahUqgc^-^|jM+Qu`MDr<)snHfG)?g2h;q(5D+g1sRX zd+`8b1cX2NsC2jesrzR}!BKaHk{2^<2kC^;dG3ST`tVNNHWbIh2N8<+ZB=C{v{#c+ z=cxwR@HH`zwbr#J5!4i6qp1JTASJ7tP+mjJ<8aeAPHzHTFT7(qtI1FKAk}{R`%fR; z_!w(AB%(QPOjn{hIKX#Q9%Uq0S9mhrp`~t^m9kqZ&{=eahH?uzoN2c8y02d#HMh6h zI=y-A01f5aNct$lWNk(20he?8dl9^TSe2;qdx2rIXlupUj3}VlU~TI4z1-W^tx+OY z308J%{`a<+a$pYKjrtV5=8%IR!LJe{_?0UTdOP)f{Yg_7xkX=pWhWX<905S)P8ZD` zBlzRDPr(=Z8N>L`OqRFjF&#dX%)Q3=MM-?aS5X-TGU(MK6a{r77(n%g8&qQFtoqST7eBbXy-GWk`pOyb2?!Univvf zpqPRC(F^HMQrq%9Sjv~=^-jlv+Dy`K27`iw&;>5rc;^G453{j~s(F2;SMQyr9)_9@ z2tZ5$Ma(C&;*y54u*PbYg@VC4UJ!@VsxeGZNlfyjG4>+wE!_?_&v?^vCW$IFDSzRP zm*LQ;d(W}_BBvBbJKOlX0P`dhMpUTA=K+!H`|uUFL46JhucQ7VAg`Z@ZZH&Ey!}Lh)TKb^%toqvuDN|^#L!Z z3d%&@DYfPJSrR8OG#gvTVlZgfoU9S17Ll1k`Rrf9D>5SNh`riPMN$y|+<5o{9Nc<+ zZeR`&4u5F4DxmLF3ltdMG&_G6Nc`qG1SbIt$w)oGUb&>;TSo0TIZL zt_ONY9o$Xs@V&+~x4VY`F@@r3zwDU7Hi9yPDqj;(leMYkNbC(WY)|#gc9HmGbYNbU zMZ)xQe!`rZeUWji-0p7g{*;hG7>i<2v$i|G4(oJ&E0Ux)xp)P4JQM|#{VD)*O#w&}Nr2Re#RH31Lx&d`bRa&sqp?kDWca@l1lb=AdfHTy{z zAJEvJHQj*kx(~um@CX6B+?~9+>&2V{9qi>c5J!5g@P}s@ob(^438xKwe#*apE@!2w zEy~ecxXEmEF=(b;xLJb5DOlQiFa@RIKfi6lekNOd8*H}DBR0sO!==kubHY6sX zX2eYk^XfUzUG9>cnw&QIG%f7oBup*K6qN^K=RrBQ64g=Ojs)DN8C;pv*JUS10#C=i03>ULYHAq`x%3}#s4|9rA20-uj&rHj zm$k(sWj2ts;5$%7PG~DJvouW-)AQlpNAP>AhZJSKI?zJaRm$(i$`N6Lgm=N;^s1#M zFC$+mg(vjq19xaJ&na@5Cv)9h>jst^n+dZpiqWk{b z!(Nct62K`HU;ODD$n%={aa(DwOb z>lW)lXRb0vHct_sZK0$Md*46}EAcN0W6#z)_wz?-JO$kPil~Ch&ux3@-i99-iPcBF zd7dAUTWTk+3dE4@e{`!wu=i>oj&E*@n28>i-32E;D!FHt?_gzr%S?=d8rwcyuGb!& zzrUylACd=94YX z^oX*Pes>;0K@0(}Uep0Oh)4{xggRu+%ExsvDA|V}5DYO_(h>P|?Ff1j=u819zPVDQ zQ(m0hTT5|0sQYkflKX zCf83#J)bZqp0aPVP??;S{A~O+{Pd88GU+{IdrPGY_G|5#A7*2j3l}u{v}E&XDA7e5 z`Dj=&OqRh3IJ2%RzEX~fRt5;{%|9-Nsz{1|;mB#o4M4jUQ&1 zwi07firiKW#9}&R2)46F7>}lv2@=FuAwC6EZ81-@G3k0M9uAI;ihe)7k#?Vu2IGdk z=GLXf^5#<6VcrD}9oVW~`-y~?2*5!WPRAQ# zJ(9>u`GMP69Pl2*YE2`RqDBhfwMdI+0L0Mq&xD_Y&e@AxW~JLD(*|r_D~nC4oMJ6U zM2G$6ph{yPXQdcTGNqIgL8_9#vhF6{%PY0*`0#ZDb<3%;^_6Mf zL`&9EVPWu{u=c3kdF^$=ySO+Rx|FhRo;1^3W!VV+B_G=TY8Yy&_aG0p1xQ!BDNJ3Vd3)T0@4XIm59T3 zOhv-rV{S6YJNUgE)tW z|5kx6{v%)c(`B5@wqQe)b*?2uJh|0`!CG0m>NR&qC~@=@D^R|zuC$Qa0 zbmNaD=<&;%-Bzqw6~l^g;kx@a?)1ID|6lkmXCjY@()+!M z9k0lI%|Y#!;>TMaX*AfncG#S%uIL^&zz-s``eDMuY>D?=?k8l6uhcH3ZQV9e(El2rc}>fP;r?;Gffd*0v}?uqIq?oZ52`&qA@ zE^tb8lb^qP6}U%**_%_at9rh`JQ_iTw{oGRB3;xJ`{k~QEtA_bAei9u{LpY=3Gj!> z+vjo(!<~VT%wq7R%Nz`^D3==Xo_?LO1!}4^_kVq4U5u(dh|6t1bGC=7{nbW~_7^_< zNn|i94_{y2k~+=^yDyh%5~rra01_1G3lRZN8Sv1X-0Of0toLGo1ByE8t%KTBSJ#wU zC#m^O(L1pw5hn)cy^zkr%KMNx^304O)T-139_yNHY1Qdu`s-Q;gJ(YG{+d_ zc-)6*iRNs`C}yf0x-=V_4BN~?d@tXAb%R*2*<_IwvJj~nV~+iiMgsfn#A$t^lDojl!j7*%O4lBlf{_yC_piOe-AUdft2alNP?wta{$E(Fz>{JcO z2-0ec$DhTf>>)}|vR~s=!zp54Yj)WQcLw#L!ch^2p#;l5L87nS4tv#0R{R{)<4%~u zoqDYfkNwTCYQ>y)>&`EoZBi%29y7(9ds=JV9}DPV2RkU=aku_hlw2b>*47_YT;IA$ z%@?Aay}_M)L(h)Jx~X79!i^qLe1$-4VgK;Gwnjy=1%Y{>y~VC)R5tuVp-u zBIKEWV%bvRhrVi`P8xGFJ7Iw#>aC1~(MS$g%(Y zF^DU$g~dtoX$x2&6+Y+?YAx*?Lh&GNi8P?o(vtMe)3fD({K4S`g6_J!_!5i{w?!Ob|elm2rW+LZDTsJyhNm%G3T~*)J=O#)8oN4qrR+@{RjS>&C&(cB%Eep| zHT%H)P^Q*K8AyQI?1E|~AZ)=BI6E(9^kH!bpqXDg)cUeCAbFpqJ2>)KZG3!{Yu|(c-QRk0Wvmys z#O}iNj==*KCYkU|c){c3A4OIh==b#~SmSK_NqkzDwpaVJ1x6B=W;l%>wOZC9i`|s8 z1A}H#RpdFx`L<;s#+)L`gV`~7Q#W4PF zyQKB+auW{tRf%$E`Db4(rN442=5yP3!cw^SN-V%nTQwRgZfll|4t4V@P|oGp(l~xh z-r?!G3u;QM&S*7XSgIP^YNDxG9)Zv*J80ONK~Brs6MU&3f4`0H#y6~H)#-u8hC>>6 z0wA1MnPWAUR`^Qf9%XE9;iThcA%g0uW;$UA3=+4R2O2*vA4zYW%H0KClNbQ*0+6de z>(-pzGGYQac60ZE)D)c4z|dghuM5Bin@V!MdM<*DxC*tv(#GvrlaP{;UMvgc5&Sfa$XXk~bp zA&~^aPXRNua<`||%C~YW_k14P+koajXANbbPqC%-;^H}udJ~490Fg^}PPZE2 zTiVr%#HqDp^xIu#+h^7#k)$4lWG(ZU)(?0wt9Ik}|#ss;C z+aIE1T;n(q5AYb#Olp|1cDx%kf9B!pF>cDxWDJ*VjKXa25kd^8tRv=bXunB+5v68cT*JGm6wAe*e=?rL>N1fS@kRavjs&1?wKcdV<^>C7tGM6pOpNQ9U1iAvka+!iD< zB;ou;L=(EpGe}m!$IlZnX4Y=64fCEPoM7n(}61X;a2PNf~O&m62f||^<0_Hhl9PBS-=#>(@`1Y{V6TJFnm?JItkF}vz zPqy0>>omcE&ZNxdX<0`GFO}>#pUQS*ymX9_$2krbEbAV25ca~HW~*)jcFof7SVOc< z>soW<&eO}kDud(F#tXXiKU&JGiAY*_#JQkmM&EXO+18&?IgmJZ6z`qLiF_Sn3fFQj z-aQL-QIKEe>}=+mVdklFr62j}`jnG?&WkjJE^Ji5jqR`Y*QHN1sfk4)ec+~CWszmdt8V|t|jbk zL-(dIn!kVwJ^(Mu;$v0LMVxG`*)eO`>Y+q2F`bHF^nOCt-L5eewfo!HFb&Jeq!gJ} zX^c9Fwkk#XCVZ?YDL3R~{#mpS^i**FMwz_gX(y0qa<>ZZyqLcqpUOKJd2+{E}&<~d;x6BYKIp73`rpe;=a?%H4kJRoI0Hc3U zrd!51eURR)O%a(+ckcp)9iBsXW;Me%@{n&H=O`%LI;Rv9VN_zlqjWpx4u(f-Y;k?$ z1p!;cq?z{<_7)%yucn@{>gw#W_{4VtF@g`tsZ$R5qwk&M!t}dVSbK{SX36#M8bH#6 z;$G4q{P7x(m8pp0zV$U%Q7;u>QNiMT%%YMdjhPnX)9Gkbrdn-tT&Dqhk!i12j&ciZ zY{$}&Z=1fJd#4I)(P+u)7?D9nG~>diVmRSV^*J2By9m>0{VaG_gMUh`8nQYm*q^y! zDww^sFlt2NI9)q+nY_umN;majc*N&k%KNzy>zaGFV`YI+@=b?X>qv}G#aWZWj8&B) zpb}VxoQzrUR8r=ZoL-lX8?7s=MN-BM(Ve`rTUy^~uC~la?7z9}^&7(FFH;3HmBv+O zvP^;AtElN7ChXcKO(vJ{a>Cmrfy>zYOhyYwtQ?p(EbX-ie=8{)DJL%7aW9f9Ma3)T znwp&lgq8BvEA?T~lx&Fw&5cf|oNI=T5BDv%^Ldq;kB$2hO9e&HQHrQ}Z3)FO0UyPO zpJ$tx`$Ix)%sp-3R#E%cDqLAC2G7+7tN+?T78=KwW2N*bGo2yd`3v(sOTJ30W;MC9 zM~|h)E;8`owu<|_F9V%#V|LMWB6L4SVrorkYWeZQ-U+}sXD%tr#>7(eNPr=WD7R~d z`5t;VLYLt&;2k*_+&!G&*BW6XG*z|B=eDX4+v1JmoyXL|4Wgs8O2qUe^E{|OmMDj7 zEqlrMM%qsnchT$#QyCe`3m3ESJD0BJ4&__4g>m-|$eF+E`VjpG3)mN`r4U_V_E=}y zeIu!Ba&3D+eARH!Z=`nEIaZ>QR|=TbzpBY-(d4uezT%X-TU+d5whYSN?-|DQrr0E=E1O%?@6OEEOjVGu~eY2Sp9wG-3P!uh&f3a9LH5`JOr5#gh|sd z>$~uZoiNohO%-jw+tO%idqqaGlHEEBJ=hSusFN3t*n4PQ1dwakfmIRU&U$N~J@eXg z+%oERsa2SymB`ciiI^GGznk1v&u^iMb%#!}P5T&&cGT4ki>ZpsEP70~wtO;l*Ve|} z{lXe8q!YT6#LJf|HM~AwR$m@{R5IL6T;Wo=f10ki->SGC6|Fy9WyQt9`TKDFn1@q= zCyZ79P;0D4SDM|h?5saF55Y2}cx!9)2&tI&ClS21vUv->h@tQEp;J*^O-_n+^;Y6s zOprRv-Xd>EhsRa{y5zawZ0DYCT;91&Dx0x;TateWWOO0_n_W(%A&8K%iDm=M zo~3KAqsDIKxspLcA|ZS^MH=y~2aJl*_B*q|_q9_muX8_l z_boXqUKDq>OC0Lvoe4H1Xyo$lT7#o%kCWuiNxIZe(Noju-zL?2SoQ?~_vKqR8Pf&& zx)=V`=v#X5kt>3yV99~FHqO!WmI-A5{807WJz^zw!ja+UjkF!yPKV{`x|F4idd+*+ zn+N``>p}+AZQHH3uk&Ly2p1&M+ib=dmO6DPrv%bq_SE{fnJNHnbn;#9Z|kH@t&@?U zIGCP_(`7FSI8S1%;xe4KnSq;zT7Zs;;<6A^x!m(s*tmc!d?>F);D^ta^B|17&GC2w z4=B&R&8JABgh=h{<{(XiR~n?5oC`#6m4~~R2HEjP*E$c4)`eTuta)wo0ih7qzPUuw z5~{T^g?+A@%I9qWbE|Oz@Tl4|CpT>mYekN%ejAL%#$D669(Zx*{jPdF?s@2AZQmvr zLx+M?i$v+7m5J>7aZ;RRr#)pVlaC73;cOz+UEY49Q?B~;LW_y{J- zwtbH9=_HTbO3vO*DUA36-QscS%KLhfI|7C{+g0q^XDk%P_uLnGSA9A?*=F=qTEGO= z<^+to8pTJJIPF@r?G%vC+K0DOb2ML_0v8fz#83BqpnGP9ZWy>pyHjVfG;cL6*<;2zb+%y3-?D( z1H=rf)|hVD+Mzt8V%)li^5ygtbU6!=3$6bZG9$(Z&B`~I6h+r7w|6GQ#@*Q zqMzik34Zp+TKF+RKzY{)4Wq)-yk9@YR_0buUfBJ*v31;r?wQ36O52`yy!TF)jxPwi z@Tp6P$;5K@y<^@_W3CMsiYOd#xb$y$duGM?30=X%Rp&ilU4c%+Pm9pFVg&F(s zNUAJb+{YERz3=_xZ?8gy>}nywpsXZGvNExWNJikjiHVfapb7Y`C=Xq(j z5eBK~42oQOly=1CuFNs?bT!;rVDcJ)>@g-p7x2327+5F0o2e4Yr0O)JjTuwweT_*# z990Ei{S2B9PXZYN*p+%F1JG=!)Hsfb7@_T#NobDmimAgQ^atHZ<`>)foJyl6;CtY% znUHLv-e`EetolmOu!UM}?f##uyP8^zOm`O7f~!(|igO(_J*>ZJMeS5$NW9fosUuTkEPIHr?RO%;&m(avD$e z&dm*z616ZgARcEy45+2|K0&9uPz$@8wSPSh=3%%ucQDI%#6tbFxCaqXJD+aQUlJFN z(b}^iqmT+&1T2D@H(9aLL;8(e` z09;K*3zGCd`dr8m%r|Q4tCX1B^Y2*(?*yx@NxVAEZ9UUTBqg}-(w(rPsI{%}>J^VH z>>pAHM{PuF1BwP|w9JL`~6>!oAX7!IgMaCm~K$hevW+E|$l= zA9{b-TY@pEkX>z+>u>!N0%77MsI?57*GfdpS68^%#5$}TsT+N4S(55JJzRJ72jYjK zeIo}>97GS>xXpmh9R&sH_X-6+Gr3#ogGS^23AMKZ_k|S>f=A6Fbq!`jm=Ie&o+`>r z^fq)e!A6ZvVaMEQ=lo8?j<>VZr*thg0BNN%@xX#@+;#8r4>U%d!85|oUt!rD0q$Y! zwp2KB3hacA;Wx<d-9QF{rHpqn6pV0y91Y{%|;fTgf^7 z`BX8A-OUcl#eIZZjZ3e1xk01TWT1&Y8C*t= zncd0?O2?PFl(&62RJmhVVHPvnBZ|<@Rc(88(GSyS5tLj2^*CoTn!CFC?EbOA!%7wE zDuDn*QjS(2o;R!9ie53h#ICkF>x<&AXm+0ucnuzG|4UFn9`G*VBS=-=#R`e%fYp^4iG+aCR=bjjw#iKM{=Dh z<@o#qZ{<><1;jkw)e)_|H#U2m@}UtjI5}nG1%t*q2#UbdbUb|bJ(@Ni^;3FFXf%HB zRDEWpuj3z$ah*U+8yM03m3Hl+{;hxdo2*f(I}FVlr}bidl;myFMZH9nQH{VuzN8wk zV2Kzco?AZNGK?M*@yP9 zJX6s7?iFd~s9{c*lmFWKzVI3Q;>Z1b064X!-sG|a0E9H3RIZ7=akqtd>YX`XZcI7Q zFq$UilkBP`X|ct^`Ebq1OAH7{9xA#TBC{!b%yS*^R$k z?hFZejK5Su1cY?7|7Uoa?@-|+@XqyjKo>X@bpyNng(|#cx8dL5HL{~ZlfCe5;*&OR z0*%!3hGaHq8cvEV7<$8O&UNRDHIvMrn}1%K#O9$6?@gS2(9ve*<&_f#$_eYNE);Xz zd*{yR7Hi!srHAd7d8)AU`*pd~>4x|>gA#`~=_Oa?LE2=)^65=i<%TbGeG&=UCM^oq zfA-_{TECWBKw-=6!6?HUtEPeDn9cfTtw1-3%6Dagnk4!rdc7|)W}ZkZ3f@Of zn>Z#hJwhjoOA|G-nGPnTJa9Pnx*T-s=G#0r_ec;Za4#FT08RV0y-GlKcQdNcwuMhH zCCo%%Am$t#vUADhj%Hdeoa}b++w^kMhU}H8B)VKs+rfc>h$n5t1I*O+Xk$u3IWk?q zAieUBFXMO+ou2gc6(&A%3A3>K!OZMbLNM_*g8~lZJDjk}JAE)%)OyA-A@bd#jN^`A ztKl%j64ht0OyO7sXGhTvljX~CM;*x_$K20mtknMXlB>MUm{sbF`k^k-edj{FPfFyK zgLZu6ER}o)sSEfIHwA`yI=%gQ(9J~n}B{KhE1YW9n zwTSYW)%AlLVi+&D-$NM%DA-TceLH75onUH(zx&yx5w-uCJ}KbJXqFK^ehBM5kIQ12QQHBob)A;R*=kV z4tCF-iGATe(odYA0DLRYp!4H1IB$Y7HjwB3d*C~H`Nq|P%Rx`^o$p{b8E%Fk4Hqu! z)WP>Jm;;E(J`~liP9xJmkzsoF6OjkB18X=X0qX$`$k87nj9uD^>Qv)l`zJ2yGgBp= z-3~zIoHnA*#9}bL%inv(ex>mY>KB{3xkuePM>SxW&>kQqr*6n;&&g z-EuKeJoK#L4UoE=GZP7~HRydta<%SC^DNS4!Y{<fX1CzW zR$}d1+itV??;;LN5|#>87LM|xa!qMGUK3z^nRXc+q5bps>!>4l4{=FTbV7wjEjNq> zoig)=+wQ+K6zrzyLgyN~M~-L6L=iQdc3w1Py&!gLue%{!Q0n>zkh3PQifgjafCp8A{xf1>^H^>267Ki~m90gZJrR$WR*vWMTJF_{J_AHE88E|| z%By)T0dxPLpOwITx`xC_<13;zI>5Xs@Fzq%+(iQ1Ride-84B@-}K(p z*da8uq(NLvxK4?qOX@_Qg)xMOA9J+;k~y5_=JYJ?uH)pws}-RCSX?GQ;7vZJ3fg+L zoHTh8-fKJkJ;9S#X~_nWF)Fe2=U1MJ0wKAWJipRHCTB2dK!}h#7 z_9W+t)&Hs`8Zu3}H(ml$ zxjCGj(|c2PKJRV{!rRZhs!qY%-dIQ~%?Cqu)1Ro3d+Kx**|ib+llem`_{oL3OLregv4e{Z)MMH)GTdKp6&;tV$GiRzu9t+ zB%?oTk*>KKfq8!={#H?^IoY$zalY>j(+6f{M=I%Q8FufWCn)j$L(A`%YK-~O{Pc4a z&B^42bi1&tsF5yYJhZd61u4zTqWUsl;KKQDmHx)q$O`8Ots9k}fz#n^q?L zjoW!+w2dQ}bm&xM-^PYu+Us|cP~;o8;6F^H+aP9DaZiyA_c_I297NrLC?@ zQ!4R`dZ*xTgcdX!nq3Fu<_+fjpvlTgbSs*D{Rk;aKMd!?#Qz*etp2?PUr`t2)l#*WLZ$yH;hEcRzPT}0rkwFnXgC&`X9TzR^q`__zk(qgRbb6O2cpM2G4Imna@5F`(B)@h{+RsBW-!GjS$W(!N84Ug z9!37}5{-}T-WT#ob&m^povvj$bxl}px%V)gHWxDc7?A4os9U&FPK7)T{P308vf9Ej$X8J_7Hdcg7XhX*i?$ov#BXo+ zu@{rgbUDvQzaF2{m)Cwcq{W2~3JHE6ndYnm)s3lHOtEMxsc0lOck?z{B zCbSI5EmO3!a`$gwua#dbI~dE?hu2D2PmhJYU89<#5W9Z1(0ZE;D2H^vOrEVHUFZ<3 zQ;a^kg(%~{`Xs+%Nh8R^Jn;PUDIjhACYT!j*^y%CNBx|v!L=xp6cc-HIH)s6YJ*&&+iBcsDR8Soz}; z@d@^6-9#6u&dFhfmqgaT3Eh^a3RF=N#OqNoFe0bZ67H0#>t(EL{A&k?nVH}C>L)q@ z-A?a=6(P%{aQ@~#UQPHV*xyo+SkKU-cp+&zeMR&*U=B-hJnpxpAc>+e(=8)nAhWbm z?~v|qfff&SE3V591>AShG(^4_BQ)M#1-_o0`Y26vBWZs+*SZU) zfn(!z_RxM%B)OUI%TMK#8(m%;hmQ!*>a6rg{{5Y0#%;3isY2IXYR^erHnwpsSK>_y zC2tnUmrGULPI&d!4g2)mtr|dO{7=OM9)2a1j!{Kj6sl6$o|iJnU;X^`futA3&hckLL

=B&1fl&dDImLMy8yq0)tN)Sp~2rWtsl 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 z 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 Nw79~jGSSap{{u!GsB-`S literal 0 HcmV?d00001 diff --git a/src/Web/StellaOps.Web/output/playwright/inspect-stella-ops-local-load.cjs b/src/Web/StellaOps.Web/output/playwright/inspect-stella-ops-local-load.cjs new file mode 100644 index 000000000..3333c87ea --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/inspect-stella-ops-local-load.cjs @@ -0,0 +1,101 @@ +const { chromium } = require('playwright'); + +const session = { + subjectId: 'user-author', + tenant: 'tenant-default', + scopes: [ + 'ui.read', + 'policy:read', + 'policy:author', + 'policy:simulate', + 'advisory-ai:view', + 'advisory-ai:operate', + 'findings:read', + 'vex:read', + 'admin', + ], +}; + +(async () => { + const browser = await chromium.launch({ + headless: true, + args: ['--no-sandbox', '--no-proxy-server'], + }); + const context = await browser.newContext({ ignoreHTTPSErrors: true }); + const page = await context.newPage(); + + const navHistory = []; + const httpErrors = []; + const failures = []; + let currentUrl = ''; + + page.on('framenavigated', (frame) => { + if (frame !== page.mainFrame()) { + return; + } + + const entry = `${new Date().toISOString()} ${frame.url()}`; + navHistory.push(entry); + console.log('[nav]', entry); + }); + + page.on('response', (response) => { + if (response.status() < 400) { + return; + } + + const request = response.request(); + const entry = `${response.status()} ${request.method()} ${response.url()}`; + httpErrors.push(entry); + console.log('[http-error]', entry); + }); + + page.on('requestfailed', (request) => { + const entry = `${request.method()} ${request.url()} :: ${request.failure()?.errorText ?? 'failed'}`; + failures.push(entry); + console.log('[requestfailed]', entry); + }); + + page.on('console', (msg) => { + if (msg.type() === 'error') { + console.log('[console-error]', msg.text()); + } + }); + + await page.addInitScript((stubSession) => { + window.__stellaopsTestSession = stubSession; + }, session); + + const target = process.argv[2] ?? 'https://stella-ops.local/'; + console.log('[goto]', target); + + try { + await page.goto(target, { waitUntil: 'commit', timeout: 20000 }); + } catch (error) { + console.log('[goto-error]', error.message); + } + + for (let i = 0; i < 20; i += 1) { + const url = page.url(); + if (url !== currentUrl) { + currentUrl = url; + console.log('[url-change]', url); + } + + await page.waitForTimeout(1000); + } + + const searchInputCount = await page + .evaluate(() => document.querySelectorAll('app-global-search input[type="text"]').length) + .catch(() => -1); + + console.log('[final-url]', page.url()); + console.log('[title]', await page.title().catch(() => '')); + console.log('[search-input-count]', searchInputCount); + console.log('[nav-count]', navHistory.length); + console.log('[http-error-count]', httpErrors.length); + console.log('[failed-request-count]', failures.length); + + await page.screenshot({ path: 'output/playwright/stella-ops-local-load-check-viewport.png' }); + await browser.close(); +})(); diff --git a/src/Web/StellaOps.Web/output/playwright/repro-header-search-live.cjs b/src/Web/StellaOps.Web/output/playwright/repro-header-search-live.cjs new file mode 100644 index 000000000..2774d1e31 --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/repro-header-search-live.cjs @@ -0,0 +1,66 @@ +const { chromium } = require('playwright'); + +const session = { + subjectId: 'user-author', + tenant: 'tenant-default', + scopes: ['ui.read', 'policy:read', 'policy:author', 'policy:simulate', 'advisory:search', 'advisory:read', 'search:read', 'findings:read', 'vex:read', 'admin'], +}; + +(async () => { + const browser = await chromium.launch({ headless: true, args: ['--no-sandbox'] }); + const context = await browser.newContext({ ignoreHTTPSErrors: true }); + const page = await context.newPage(); + + page.on('requestfailed', (request) => { + const url = request.url(); + if (url.includes('/search')) { + console.log('[requestfailed]', request.method(), url, request.failure()?.errorText); + } + }); + + page.on('response', (response) => { + const url = response.url(); + if ( + url.includes('/api/v1/search/query') || + url.includes('/api/v1/advisory-ai/search') || + url.includes('/api/v1/advisory-ai/search/analytics') + ) { + const req = response.request(); + console.log('[response]', req.method(), response.status(), url); + } + }); + + await page.addInitScript((stubSession) => { + window.__stellaopsTestSession = stubSession; + }, session); + + const url = process.argv[2] || 'https://127.1.0.1/'; + console.log('[goto]', url); + await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 }); + await page.waitForTimeout(2000); + + const count = await page.evaluate(() => document.querySelectorAll('app-global-search input[type="text"]').length); + console.log('[search-input-count]', count); + + if (count === 0) { + console.log('[page-url]', page.url()); + console.log('[title]', await page.title()); + await page.screenshot({ path: 'output/playwright/header-search-repro-no-input.png', fullPage: true }); + await browser.close(); + process.exit(1); + } + + await page.click('app-global-search input[type="text"]', { timeout: 15000 }); + await page.fill('app-global-search input[type="text"]', 'critical findings', { timeout: 15000 }); + await page.waitForTimeout(3000); + + const results = await page.evaluate(() => document.querySelectorAll('app-entity-card').length); + const emptyText = await page.locator('.search__empty').allTextContents(); + const degradedVisible = await page.locator('.search__degraded-banner').isVisible().catch(() => false); + console.log('[entity-cards]', results); + console.log('[empty-text]', emptyText.join(' | ')); + console.log('[degraded-banner]', degradedVisible); + + await page.screenshot({ path: 'output/playwright/header-search-repro-live.png', fullPage: true }); + await browser.close(); +})(); diff --git a/src/Web/StellaOps.Web/output/playwright/repro-header-search.cjs b/src/Web/StellaOps.Web/output/playwright/repro-header-search.cjs new file mode 100644 index 000000000..12c2fb266 --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/repro-header-search.cjs @@ -0,0 +1,66 @@ +const { chromium } = require('playwright'); + +const session = { + subjectId: 'user-author', + tenant: 'tenant-default', + scopes: ['ui.read','policy:read','policy:author','policy:simulate','advisory:search','advisory:read','search:read','findings:read','vex:read','admin'] +}; + +(async () => { + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ ignoreHTTPSErrors: true }); + const page = await context.newPage(); + + page.on('requestfailed', (request) => { + const url = request.url(); + if (url.includes('/search')) { + console.log('[requestfailed]', request.method(), url, request.failure()?.errorText); + } + }); + + page.on('response', (response) => { + const url = response.url(); + if ( + url.includes('/api/v1/search/query') || + url.includes('/api/v1/advisory-ai/search') || + url.includes('/api/v1/advisory-ai/search/analytics') + ) { + const req = response.request(); + console.log('[response]', req.method(), response.status(), url); + } + }); + + await page.addInitScript((stubSession) => { + window.__stellaopsTestSession = stubSession; + }, session); + + const url = process.argv[2] || 'https://127.1.0.1:10000/'; + console.log('[goto]', url); + await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 }); + await page.waitForTimeout(2000); + + const count = await page.evaluate(() => document.querySelectorAll('app-global-search input[type="text"]').length); + console.log('[search-input-count]', count); + + if (count === 0) { + console.log('[page-url]', page.url()); + console.log('[title]', await page.title()); + await page.screenshot({ path: 'output/playwright/header-search-repro-no-input.png', fullPage: true }); + await browser.close(); + process.exit(1); + } + + await page.click('app-global-search input[type="text"]', { timeout: 15000 }); + await page.fill('app-global-search input[type="text"]', 'critical findings', { timeout: 15000 }); + await page.waitForTimeout(3000); + + const results = await page.evaluate(() => document.querySelectorAll('app-entity-card').length); + const emptyText = await page.locator('.search__empty').allTextContents(); + const degradedVisible = await page.locator('.search__degraded-banner').isVisible().catch(() => false); + console.log('[entity-cards]', results); + console.log('[empty-text]', emptyText.join(' | ')); + console.log('[degraded-banner]', degradedVisible); + + await page.screenshot({ path: 'output/playwright/header-search-repro.png', fullPage: true }); + await browser.close(); +})(); diff --git a/src/Web/StellaOps.Web/src/app/core/api/release-management.client.ts b/src/Web/StellaOps.Web/src/app/core/api/release-management.client.ts index 8719ac565..174b62c8e 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/release-management.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/release-management.client.ts @@ -101,7 +101,7 @@ export class ReleaseManagementHttpClient implements ReleaseManagementApi { private readonly http = inject(HttpClient); private readonly context = inject(PlatformContextStore); private readonly readBaseUrl = '/api/v2/releases'; - private readonly legacyBaseUrl = '/api/release-orchestrator/releases'; + private readonly legacyBaseUrl = '/api/v1/releases'; listReleases(filter?: ReleaseFilter): Observable<ReleaseListResponse> { const page = Math.max(1, filter?.page ?? 1); diff --git a/src/Web/StellaOps.Web/src/app/core/config/config.guard.spec.ts b/src/Web/StellaOps.Web/src/app/core/config/config.guard.spec.ts index 2b551bb69..68c7b4546 100644 --- a/src/Web/StellaOps.Web/src/app/core/config/config.guard.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/config/config.guard.spec.ts @@ -61,7 +61,7 @@ describe('requireConfigGuard', () => { expect((result as UrlTree).toString()).toBe('/setup'); }); - it('should redirect to /setup/wizard when config loaded but setup is absent', () => { + it('should redirect to /setup-wizard/wizard when config loaded but setup is absent', () => { (configService.isConfigured as jasmine.Spy).and.returnValue(true); Object.defineProperty(configService, 'config', { get: () => ({ ...minimalConfig, setup: undefined }), @@ -70,10 +70,10 @@ describe('requireConfigGuard', () => { const result = runGuard(); expect(result).toBeInstanceOf(UrlTree); - expect((result as UrlTree).toString()).toBe('/setup/wizard'); + expect((result as UrlTree).toString()).toBe('/setup-wizard/wizard'); }); - it('should redirect to /setup/wizard?resume=migrations when setup is a step ID', () => { + it('should redirect to /setup-wizard/wizard?resume=migrations when setup is a step ID', () => { (configService.isConfigured as jasmine.Spy).and.returnValue(true); Object.defineProperty(configService, 'config', { get: () => ({ ...minimalConfig, setup: 'migrations' }), @@ -82,7 +82,7 @@ describe('requireConfigGuard', () => { const result = runGuard(); expect(result).toBeInstanceOf(UrlTree); - expect((result as UrlTree).toString()).toContain('/setup/wizard'); + expect((result as UrlTree).toString()).toContain('/setup-wizard/wizard'); expect((result as UrlTree).queryParams['resume']).toBe('migrations'); }); diff --git a/src/Web/StellaOps.Web/src/app/core/config/config.guard.ts b/src/Web/StellaOps.Web/src/app/core/config/config.guard.ts index 76634074f..48d15933d 100644 --- a/src/Web/StellaOps.Web/src/app/core/config/config.guard.ts +++ b/src/Web/StellaOps.Web/src/app/core/config/config.guard.ts @@ -7,8 +7,8 @@ import { AppConfigService } from './app-config.service'; * Route guard that checks both configuration loading and setup state. * * - If config is not loaded → redirect to /setup - * - If config is loaded but `setup` is absent/undefined → redirect to /setup/wizard (fresh install) - * - If config is loaded and `setup` is a step ID → redirect to /setup/wizard?resume=<stepId> + * - If config is loaded but `setup` is absent/undefined → redirect to /setup-wizard/wizard (fresh install) + * - If config is loaded and `setup` is a step ID → redirect to /setup-wizard/wizard?resume=<stepId> * - If config is loaded and `setup === "complete"` → allow navigation * * Place this guard **before** auth guards so unconfigured deployments @@ -26,12 +26,12 @@ export const requireConfigGuard: CanMatchFn = () => { if (!setup) { // setup absent → fresh install, go to wizard - return router.createUrlTree(['/setup/wizard']); + return router.createUrlTree(['/setup-wizard/wizard']); } if (setup !== 'complete') { // setup = stepId → resume wizard at that step - return router.createUrlTree(['/setup/wizard'], { + return router.createUrlTree(['/setup-wizard/wizard'], { queryParams: { resume: setup }, }); } 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 41afeeba0..4f9ce824b 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 @@ -21,79 +21,142 @@ interface SetupCard { template: ` <div class="admin-overview"> <header class="admin-overview__header"> - <h1 class="admin-overview__title">Setup</h1> - <p class="admin-overview__subtitle"> - Manage topology, identity, tenants, notifications, and system controls. - </p> + <div> + <h1 class="admin-overview__title">Setup</h1> + <p class="admin-overview__subtitle"> + Manage topology, identity, tenants, notifications, and system controls. + </p> + </div> + <div class="admin-overview__meta"> + <span class="admin-overview__meta-chip">7 setup domains</span> + <span class="admin-overview__meta-chip">3 operational drilldowns</span> + <span class="admin-overview__meta-chip">Offline-first safe</span> + </div> </header> - <div class="admin-overview__grid"> - @for (card of cards; track card.id) { - <a class="admin-card" [routerLink]="card.route"> - <div class="admin-card__icon" aria-hidden="true">{{ card.icon }}</div> - <div class="admin-card__body"> - <h2 class="admin-card__title">{{ card.title }}</h2> - <p class="admin-card__description">{{ card.description }}</p> - </div> - </a> - } - </div> + <div class="admin-overview__layout"> + <div> + <div class="admin-overview__grid"> + @for (card of cards; track card.id) { + <a class="admin-card" [routerLink]="card.route"> + <div class="admin-card__icon" aria-hidden="true">{{ card.icon }}</div> + <div class="admin-card__body"> + <h2 class="admin-card__title">{{ card.title }}</h2> + <p class="admin-card__description">{{ card.description }}</p> + </div> + </a> + } + </div> - <section class="admin-overview__drilldowns"> - <h2 class="admin-overview__section-heading">Operational Drilldowns</h2> - <ul class="admin-overview__links"> - <li><a routerLink="/ops/operations/quotas">Quotas & Limits</a> - Ops</li> - <li><a routerLink="/ops/operations/system-health">System Health</a> - Ops</li> - <li><a routerLink="/evidence/audit-log">Audit Log</a> - Evidence</li> - </ul> - </section> + <section class="admin-overview__drilldowns"> + <h2 class="admin-overview__section-heading">Operational Drilldowns</h2> + <ul class="admin-overview__links"> + <li><a routerLink="/ops/operations/quotas">Quotas & Limits</a> - Ops</li> + <li><a routerLink="/ops/operations/system-health">System Health</a> - Ops</li> + <li><a routerLink="/evidence/audit-log">Audit Log</a> - Evidence</li> + </ul> + </section> + </div> + + <aside class="admin-overview__aside"> + <section class="admin-overview__aside-section"> + <h2 class="admin-overview__section-heading">Quick Actions</h2> + <div class="admin-overview__quick-actions"> + <a routerLink="/setup/topology/targets">Add Target</a> + <a routerLink="/setup/integrations">Configure Integrations</a> + <a routerLink="/setup/identity-access">Review Access</a> + </div> + </section> + + <section class="admin-overview__aside-section"> + <h2 class="admin-overview__section-heading">Suggested Next Steps</h2> + <ul class="admin-overview__links admin-overview__links--compact"> + <li>Validate environment mapping and target ownership.</li> + <li>Verify notification channels before first production promotion.</li> + <li>Confirm audit trail visibility for approvers.</li> + </ul> + </section> + </aside> + </div> </div> `, styles: [ ` .admin-overview { padding: 1.5rem; - max-width: 1200px; + max-width: 1240px; } .admin-overview__header { - margin-bottom: 2rem; + margin-bottom: 1.5rem; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; } .admin-overview__title { - font-size: 1.5rem; - font-weight: 600; + font-size: 1.9rem; + line-height: 1.05; + font-weight: 700; margin: 0 0 0.5rem; } .admin-overview__subtitle { - color: var(--color-text-secondary, #666); + color: var(--color-text-secondary, #4f4b3e); margin: 0; + max-width: 60ch; + } + + .admin-overview__meta { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + } + + .admin-overview__meta-chip { + display: inline-flex; + align-items: center; + border: 1px solid var(--color-border-secondary, #d4c9a8); + border-radius: var(--radius-full, 999px); + padding: 0.24rem 0.55rem; + font-size: 0.65rem; + letter-spacing: 0.07em; + text-transform: uppercase; + color: var(--color-text-secondary, #4f4b3e); + background: var(--color-surface-primary, #fff); + } + + .admin-overview__layout { + display: grid; + grid-template-columns: minmax(0, 1fr) 300px; + gap: 1rem; } .admin-overview__grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 1rem; - margin-bottom: 2rem; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 0.85rem; + margin-bottom: 1.5rem; } .admin-card { display: flex; align-items: flex-start; gap: 1rem; - padding: 1.25rem; + padding: 1rem; 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; + transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s; } .admin-card:hover { - border-color: var(--color-brand-primary, #4f46e5); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + border-color: var(--color-brand-primary, #f5a623); + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.06); + transform: translateY(-1px); } .admin-card__icon { @@ -104,24 +167,59 @@ interface SetupCard { } .admin-card__title { - font-size: 0.9375rem; - font-weight: 600; + font-size: 0.94rem; + font-weight: 700; margin: 0 0 0.25rem; } .admin-card__description { - font-size: 0.8125rem; - color: var(--color-text-secondary, #666); + font-size: 0.82rem; + color: var(--color-text-secondary, #4f4b3e); margin: 0; } + .admin-overview__aside { + display: grid; + gap: 0.8rem; + align-content: start; + } + + .admin-overview__aside-section, + .admin-overview__drilldowns { + border: 1px solid var(--color-border-primary, #e5e7eb); + background: var(--color-surface-primary, #fff); + border-radius: var(--radius-md, 8px); + padding: 0.9rem; + } + .admin-overview__section-heading { - font-size: 0.875rem; - font-weight: 600; - margin: 0 0 0.75rem; + font-size: 0.74rem; + font-weight: 700; + margin: 0 0 0.65rem; text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--color-text-secondary, #666); + letter-spacing: 0.08em; + color: var(--color-text-secondary, #4f4b3e); + } + + .admin-overview__quick-actions { + display: grid; + gap: 0.45rem; + } + + .admin-overview__quick-actions a { + display: inline-flex; + align-items: center; + border: 1px solid var(--color-border-primary, #e5e7eb); + border-radius: var(--radius-sm, 6px); + padding: 0.45rem 0.55rem; + text-decoration: none; + color: var(--color-text-primary, #1c1200); + background: var(--color-surface-secondary, #faf8f0); + } + + .admin-overview__quick-actions a:hover { + border-color: var(--color-brand-primary, #f5a623); + color: var(--color-brand-primary, #f5a623); } .admin-overview__links { @@ -134,18 +232,42 @@ interface SetupCard { } .admin-overview__links li { - font-size: 0.875rem; - color: var(--color-text-secondary, #666); + font-size: 0.84rem; + color: var(--color-text-secondary, #4f4b3e); + } + + .admin-overview__links--compact li { + line-height: 1.35; } .admin-overview__links a { - color: var(--color-brand-primary, #4f46e5); + color: var(--color-brand-primary, #f5a623); text-decoration: none; } .admin-overview__links a:hover { text-decoration: underline; } + + @media (max-width: 1080px) { + .admin-overview__layout { + grid-template-columns: 1fr; + } + + .admin-overview__header { + flex-direction: column; + } + } + + @media (max-width: 575px) { + .admin-overview { + padding: 0.75rem; + } + + .admin-overview__title { + font-size: 2rem; + } + } `, ], changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/Web/StellaOps.Web/src/app/features/compare/components/compare-view/compare-view.component.scss b/src/Web/StellaOps.Web/src/app/features/compare/components/compare-view/compare-view.component.scss index 67a774176..0f62acc51 100644 --- a/src/Web/StellaOps.Web/src/app/features/compare/components/compare-view/compare-view.component.scss +++ b/src/Web/StellaOps.Web/src/app/features/compare/components/compare-view/compare-view.component.scss @@ -4,13 +4,18 @@ display: flex; flex-direction: column; height: 100%; + min-height: 0; } .compare-toolbar { display: flex; justify-content: space-between; + align-items: flex-start; + height: auto; + min-height: 0; padding: var(--space-2) var(--space-4); background: var(--color-surface-secondary); + overflow: visible; .target-selector { display: flex; @@ -86,6 +91,7 @@ display: flex; flex: 1; overflow: hidden; + min-height: 0; } .pane { @@ -187,6 +193,120 @@ } } +@media (max-width: 1080px) { + .compare-toolbar { + flex-wrap: wrap; + align-items: flex-start; + gap: var(--space-2); + + .target-selector { + flex: 1 1 100%; + min-width: 0; + flex-wrap: wrap; + } + + .toolbar-actions { + width: 100%; + justify-content: space-between; + flex-wrap: wrap; + } + } + + .delta-summary { + flex-wrap: wrap; + gap: var(--space-2); + } +} + +@media (max-width: 768px) { + .compare-view { + height: auto; + min-height: 100%; + } + + .compare-toolbar { + display: grid; + grid-template-columns: 1fr; + align-items: stretch; + padding: var(--space-2); + gap: var(--space-2); + + .target-selector { + width: 100%; + gap: var(--space-2); + align-items: flex-start; + + mat-select { + min-width: 0; + width: 100%; + } + } + + .toolbar-actions { + width: 100%; + display: grid; + grid-template-columns: 1fr auto auto; + align-items: center; + gap: var(--space-2); + + > button[mat-icon-button] { + justify-self: start; + } + + > button[mat-stroked-button] { + justify-self: end; + } + } + + .role-toggle { + grid-column: 1 / -1; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--space-1); + } + + .role-toggle-button { + min-width: 0; + width: 100%; + padding-inline: 0.2rem; + font-size: 0.74rem; + } + } + + .delta-summary { + gap: var(--space-1); + padding: var(--space-2); + + .summary-chip { + font-size: 0.75rem; + } + } + + .panes-container { + flex-direction: column; + overflow: visible; + } + + .pane { + border-right: none; + border-bottom: 1px solid var(--color-border-primary); + } + + .pane:last-child { + border-bottom: none; + } + + .categories-pane, + .items-pane, + .evidence-pane { + width: 100%; + } + + .evidence-pane .side-by-side { + grid-template-columns: 1fr; + } +} + .empty-state { display: flex; flex-direction: column; diff --git a/src/Web/StellaOps.Web/src/app/features/deployments/deployments-list-page.component.ts b/src/Web/StellaOps.Web/src/app/features/deployments/deployments-list-page.component.ts index 51da98a7f..2d58d694c 100644 --- a/src/Web/StellaOps.Web/src/app/features/deployments/deployments-list-page.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/deployments/deployments-list-page.component.ts @@ -31,7 +31,7 @@ interface Deployment { </div> </header> - <div class="table-container"> + <div class="table-container table-container--desktop"> <table class="data-table"> <thead> <tr> @@ -76,6 +76,53 @@ interface Deployment { </tbody> </table> </div> + + <div class="deployment-cards" aria-label="Deployment cards"> + @for (deployment of deployments(); track deployment.id) { + <article class="deployment-card"> + <header class="deployment-card__header"> + <a [routerLink]="['./', deployment.id]" class="deployment-link deployment-card__id"> + {{ deployment.id }} + </a> + <span class="status-badge" [class]="'status-badge--' + deployment.status"> + @if (deployment.status === 'running') { + <span class="spinner"></span> + } + {{ deployment.status | uppercase }} + </span> + </header> + + <dl class="deployment-card__meta"> + <div> + <dt>Release</dt> + <dd> + <a [routerLink]="['/releases', deployment.releaseVersion]">{{ deployment.releaseVersion }}</a> + </dd> + </div> + <div> + <dt>Environment</dt> + <dd>{{ deployment.environment }}</dd> + </div> + <div> + <dt>Started</dt> + <dd>{{ deployment.startedAt }}</dd> + </div> + <div> + <dt>Duration</dt> + <dd>{{ deployment.duration }}</dd> + </div> + <div> + <dt>Initiated By</dt> + <dd>{{ deployment.initiatedBy }}</dd> + </div> + </dl> + + <div class="deployment-card__actions"> + <a [routerLink]="['./', deployment.id]" class="btn btn--sm">View Deployment</a> + </div> + </article> + } + </div> </div> `, styles: [` @@ -88,9 +135,10 @@ interface Deployment { background: var(--color-surface-primary); border: 1px solid var(--color-border-primary); border-radius: var(--radius-lg); - overflow: hidden; + overflow-x: auto; + overflow-y: hidden; } - .data-table { width: 100%; border-collapse: collapse; } + .data-table { width: 100%; min-width: 840px; border-collapse: collapse; } .data-table th, .data-table td { padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid var(--color-border-primary); } .data-table th { background: var(--color-surface-secondary); font-size: 0.75rem; font-weight: var(--font-weight-semibold); color: var(--color-text-secondary); text-transform: uppercase; } .data-table tbody tr:hover { background: var(--color-nav-hover); } @@ -115,6 +163,74 @@ interface Deployment { @keyframes spin { to { transform: rotate(360deg); } } .btn { padding: 0.25rem 0.5rem; border-radius: var(--radius-md); font-size: 0.75rem; font-weight: var(--font-weight-medium); text-decoration: none; background: var(--color-surface-secondary); border: 1px solid var(--color-border-primary); color: var(--color-text-primary); } + + .deployment-cards { + display: none; + gap: 0.6rem; + } + + .deployment-card { + border: 1px solid var(--color-border-primary); + border-radius: var(--radius-md); + background: var(--color-surface-primary); + padding: 0.7rem; + display: grid; + gap: 0.6rem; + } + + .deployment-card__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + } + + .deployment-card__id { + font-size: 0.84rem; + } + + .deployment-card__meta { + margin: 0; + display: grid; + gap: 0.35rem; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .deployment-card__meta div { + display: grid; + gap: 0.1rem; + } + + .deployment-card__meta dt { + margin: 0; + font-size: 0.62rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-text-secondary); + } + + .deployment-card__meta dd { + margin: 0; + font-size: 0.84rem; + color: var(--color-text-primary); + line-height: 1.25; + } + + .deployment-card__actions { + display: flex; + justify-content: flex-end; + } + + @media (max-width: 768px) { + .deployments-page { max-width: none; } + .page-title { font-size: 2rem; line-height: 1.05; } + .page-subtitle { font-size: 0.95rem; line-height: 1.35; } + .data-table th, .data-table td { padding: 0.625rem 0.75rem; } + .table-container--desktop { display: none; } + .deployment-cards { display: grid; } + .deployment-card__meta { grid-template-columns: 1fr; } + .deployment-card__actions .btn { width: 100%; text-align: center; } + } `] }) export class DeploymentsListPageComponent { diff --git a/src/Web/StellaOps.Web/src/app/features/doctor/models/doctor-wizard-mapping.ts b/src/Web/StellaOps.Web/src/app/features/doctor/models/doctor-wizard-mapping.ts index e2ade6fc8..974e9cbdd 100644 --- a/src/Web/StellaOps.Web/src/app/features/doctor/models/doctor-wizard-mapping.ts +++ b/src/Web/StellaOps.Web/src/app/features/doctor/models/doctor-wizard-mapping.ts @@ -92,5 +92,5 @@ export function getCheckIdsForStep(stepId: SetupStepId): string[] { /** Build a deep-link URL to the setup wizard for a specific step in reconfigure mode. */ export function buildWizardDeepLink(stepId: SetupStepId): string { - return `/setup/wizard?step=${stepId}&mode=reconfigure`; + return `/setup-wizard/wizard?step=${stepId}&mode=reconfigure`; } diff --git a/src/Web/StellaOps.Web/src/app/features/ops/ops-overview-page.component.ts b/src/Web/StellaOps.Web/src/app/features/ops/ops-overview-page.component.ts index 73d4f9b35..0aaeee167 100644 --- a/src/Web/StellaOps.Web/src/app/features/ops/ops-overview-page.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/ops/ops-overview-page.component.ts @@ -13,22 +13,136 @@ import { RouterLink } from '@angular/router'; <p>Unified operations workspace for platform runtime, policy governance, and integrations.</p> </header> - <div class="doors"> - <a routerLink="/ops/operations">Operations</a> - <a routerLink="/ops/integrations">Integrations</a> - <a routerLink="/ops/policy">Policy</a> - <a routerLink="/ops/platform-setup">Platform Setup</a> + <div class="ops-overview__layout"> + <div class="doors"> + <a routerLink="/ops/operations"> + <strong>Operations</strong> + <span>Scheduler, feeds, health, quotas, and runtime controls.</span> + </a> + <a routerLink="/ops/integrations"> + <strong>Integrations</strong> + <span>Connector onboarding, source registration, and delivery channels.</span> + </a> + <a routerLink="/ops/policy"> + <strong>Policy</strong> + <span>Governance, simulations, waivers, and gate catalog management.</span> + </a> + <a routerLink="/ops/platform-setup"> + <strong>Platform Setup</strong> + <span>Environment bootstrap, topology alignment, and baseline controls.</span> + </a> + </div> + + <aside class="ops-overview__aside"> + <h2>Operational Focus</h2> + <ul> + <li>Validate feed freshness and advisory import health.</li> + <li>Review scheduler backlog and failed task retries.</li> + <li>Confirm policy package rollout and waiver expiration queue.</li> + </ul> + </aside> </div> </section> `, styles: [ ` - .ops-overview { display: grid; gap: 1rem; } - h1 { margin: 0; font-size: 1.35rem; } - p { margin: 0; color: var(--color-text-secondary); } - .doors { display: grid; gap: 0.5rem; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); } - .doors a { border: 1px solid var(--color-border-primary); border-radius: var(--radius-md); padding: 0.65rem; text-decoration: none; color: var(--color-text-primary); background: var(--color-surface-primary); } - .doors a:hover { border-color: var(--color-brand-primary); color: var(--color-brand-primary); } + .ops-overview { + display: grid; + gap: 1rem; + } + + h1 { + margin: 0; + font-size: 2rem; + line-height: 1.05; + } + + p { + margin: 0.3rem 0 0; + color: var(--color-text-secondary); + max-width: 70ch; + } + + .ops-overview__layout { + display: grid; + grid-template-columns: minmax(0, 1fr) 300px; + gap: 1rem; + } + + .doors { + display: grid; + gap: 0.7rem; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + } + + .doors a { + display: grid; + gap: 0.35rem; + border: 1px solid var(--color-border-primary); + border-radius: var(--radius-md); + padding: 0.75rem; + text-decoration: none; + color: var(--color-text-primary); + background: var(--color-surface-primary); + transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s; + } + + .doors a strong { + font-size: 0.95rem; + line-height: 1.2; + } + + .doors a span { + font-size: 0.78rem; + line-height: 1.35; + color: var(--color-text-secondary); + } + + .doors a:hover { + border-color: var(--color-brand-primary); + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.06); + transform: translateY(-1px); + } + + .ops-overview__aside { + border: 1px solid var(--color-border-primary); + border-radius: var(--radius-md); + background: var(--color-surface-primary); + padding: 0.8rem; + } + + .ops-overview__aside h2 { + margin: 0 0 0.55rem; + font-size: 0.74rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--color-text-secondary); + } + + .ops-overview__aside ul { + margin: 0; + padding-left: 1rem; + display: grid; + gap: 0.35rem; + } + + .ops-overview__aside li { + font-size: 0.82rem; + line-height: 1.35; + color: var(--color-text-secondary); + } + + @media (max-width: 1080px) { + .ops-overview__layout { + grid-template-columns: 1fr; + } + } + + @media (max-width: 575px) { + h1 { + font-size: 2rem; + } + } `, ], }) diff --git a/src/Web/StellaOps.Web/src/app/features/setup-wizard/setup-wizard.routes.ts b/src/Web/StellaOps.Web/src/app/features/setup-wizard/setup-wizard.routes.ts index f9246e5a2..8bbce35a5 100644 --- a/src/Web/StellaOps.Web/src/app/features/setup-wizard/setup-wizard.routes.ts +++ b/src/Web/StellaOps.Web/src/app/features/setup-wizard/setup-wizard.routes.ts @@ -3,7 +3,7 @@ * @sprint Sprint 4: UI Wizard Core * @description Routes for the setup wizard feature. * The default path shows the config-missing screen (for unconfigured deployments). - * The /setup/wizard path loads the full setup wizard (when config is present). + * The /setup-wizard/wizard path loads the full setup wizard (when config is present). */ import { Routes } from '@angular/router'; diff --git a/src/Web/StellaOps.Web/src/app/layout/app-shell/app-shell.component.ts b/src/Web/StellaOps.Web/src/app/layout/app-shell/app-shell.component.ts index f221a9fa9..5703e34ae 100644 --- a/src/Web/StellaOps.Web/src/app/layout/app-shell/app-shell.component.ts +++ b/src/Web/StellaOps.Web/src/app/layout/app-shell/app-shell.component.ts @@ -1,4 +1,4 @@ -import { Component, ChangeDetectionStrategy, signal } from '@angular/core'; +import { Component, ChangeDetectionStrategy, signal, inject } from '@angular/core'; import { RouterOutlet } from '@angular/router'; @@ -6,6 +6,7 @@ import { AppTopbarComponent } from '../app-topbar/app-topbar.component'; import { AppSidebarComponent } from '../app-sidebar'; import { BreadcrumbComponent } from '../breadcrumb/breadcrumb.component'; import { OverlayHostComponent } from '../overlay-host/overlay-host.component'; +import { SidebarPreferenceService } from '../app-sidebar/sidebar-preference.service'; /** * AppShellComponent - Main application shell with permanent left rail navigation. @@ -29,16 +30,16 @@ import { OverlayHostComponent } from '../overlay-host/overlay-host.component'; OverlayHostComponent ], template: ` - <div class="shell" [class.shell--mobile-open]="mobileMenuOpen()" [class.shell--collapsed]="sidebarCollapsed()"> + <div class="shell" [class.shell--mobile-open]="mobileMenuOpen()" [class.shell--collapsed]="sidebarPrefs.sidebarCollapsed()"> <!-- Skip link for accessibility --> <a class="shell__skip-link" href="#main-content">Skip to main content</a> <!-- Sidebar --> <app-sidebar class="shell__sidebar" - [collapsed]="sidebarCollapsed()" + [collapsed]="sidebarPrefs.sidebarCollapsed()" (mobileClose)="onMobileSidebarClose()" - (collapseToggle)="onSidebarCollapseToggle()" + (collapseToggle)="sidebarPrefs.toggleSidebar()" ></app-sidebar> <!-- Main area (topbar + content) --> @@ -212,12 +213,11 @@ import { OverlayHostComponent } from '../overlay-host/overlay-host.component'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class AppShellComponent { + readonly sidebarPrefs = inject(SidebarPreferenceService); + /** Whether mobile menu is open */ readonly mobileMenuOpen = signal(false); - /** Whether sidebar is collapsed (icons only) */ - readonly sidebarCollapsed = signal(false); - onMobileMenuToggle(): void { this.mobileMenuOpen.update((v) => !v); } @@ -225,8 +225,4 @@ export class AppShellComponent { onMobileSidebarClose(): void { this.mobileMenuOpen.set(false); } - - onSidebarCollapseToggle(): void { - this.sidebarCollapsed.update((v) => !v); - } } diff --git a/src/Web/StellaOps.Web/src/app/layout/app-sidebar/app-sidebar.component.spec.ts b/src/Web/StellaOps.Web/src/app/layout/app-sidebar/app-sidebar.component.spec.ts index 3f6ec7f82..da0208d08 100644 --- a/src/Web/StellaOps.Web/src/app/layout/app-sidebar/app-sidebar.component.spec.ts +++ b/src/Web/StellaOps.Web/src/app/layout/app-sidebar/app-sidebar.component.spec.ts @@ -39,6 +39,25 @@ describe('AppSidebarComponent', () => { expect(text).not.toContain('Analytics'); }); + it('starts edge auto-scroll animation only when pointer enters edge zone', () => { + setScopes([StellaOpsScopes.UI_READ]); + const rafSpy = spyOn(window, 'requestAnimationFrame').and.returnValue(1); + const cancelSpy = spyOn(window, 'cancelAnimationFrame'); + const fixture = createComponent(); + const nav = fixture.nativeElement.querySelector('.sidebar__nav') as HTMLElement; + + expect(nav).toBeTruthy(); + expect(rafSpy).not.toHaveBeenCalled(); + + spyOn(nav, 'getBoundingClientRect').and.returnValue(new DOMRect(0, 0, 320, 480)); + nav.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, clientY: 5 })); + + expect(rafSpy).toHaveBeenCalledTimes(1); + + nav.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true })); + expect(cancelSpy).toHaveBeenCalledWith(1); + }); + function setScopes(scopes: readonly StellaOpsScope[]): void { const baseUser = authService.user(); if (!baseUser) { 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 a581bd53a..5d9cbd955 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 @@ -23,6 +23,7 @@ import type { ApprovalApi } from '../../core/api/approval.client'; import { SidebarNavItemComponent, NavItem } from './sidebar-nav-item.component'; import { DoctorTrendService } from '../../core/doctor/doctor-trend.service'; +import { SidebarPreferenceService } from './sidebar-preference.service'; /** * Navigation structure for the shell. @@ -33,6 +34,8 @@ export interface NavSection { label: string; icon: string; route: string; + menuGroupId?: string; + menuGroupLabel?: string; badge$?: () => number | null; sparklineData$?: () => number[]; children?: NavItem[]; @@ -40,19 +43,31 @@ export interface NavSection { requireAnyScope?: readonly StellaOpsScope[]; } +interface DisplayNavSection extends NavSection { + sectionBadge: number | null; + displayChildren: NavItem[]; +} + +interface NavSectionGroup { + id: string; + label: string; + sections: DisplayNavSection[]; +} + /** * AppSidebarComponent - Permanent dark left navigation rail. * - * Design: Always-visible 240px dark sidebar. Never collapses. + * Design: Always-visible 240px dark sidebar, collapsible to 56px icon rail. * Dark charcoal background with amber/gold accents. - * All nav groups are always expanded. Mouse-proximity auto-scroll near edges. + * Collapsible nav groups and foldable sections with smooth CSS grid animation. + * Mouse-proximity auto-scroll near edges. */ @Component({ selector: 'app-sidebar', standalone: true, imports: [ - SidebarNavItemComponent -], + SidebarNavItemComponent, + ], template: ` <aside class="sidebar" @@ -73,65 +88,116 @@ export interface NavSection { </svg> </button> - <!-- Navigation - sections with vertical rail labels --> + <!-- Navigation - collapsible groups with foldable sections --> <nav class="sidebar__nav" #sidebarNav> - @for (section of displaySections(); track section.id; let first = $first) { - @if (!section.displayChildren.length) { - <!-- Root item (no children, e.g. Dashboard) --> - @if (!first) { - <div class="nav-section__divider"></div> + @for (group of displaySectionGroups(); track group.id) { + <div + class="sb-group" + [class.sb-group--collapsed]="!collapsed && sidebarPrefs.collapsedGroups().has(group.id)" + role="group" + [attr.aria-label]="group.label" + > + @if (!collapsed) { + <button + type="button" + class="sb-group__header" + (click)="sidebarPrefs.toggleGroup(group.id)" + [attr.aria-expanded]="!sidebarPrefs.collapsedGroups().has(group.id)" + [attr.aria-controls]="'nav-grp-' + group.id" + > + <span class="sb-group__title">{{ group.label }}</span> + <svg class="sb-group__chevron" viewBox="0 0 16 16" width="10" height="10" aria-hidden="true"> + <path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> + </svg> + </button> } - <app-sidebar-nav-item - [label]="section.label" - [icon]="section.icon" - [route]="section.route" - [badge]="section.sectionBadge" - [collapsed]="collapsed" - ></app-sidebar-nav-item> - } @else { - <!-- Section group with children --> - <div class="nav-section"> - @if (!first) { - <div class="nav-section__divider"></div> - } - <div class="nav-section__body"> - @if (!collapsed) { - <div class="nav-section__rail" aria-hidden="true"> - <div class="nav-section__rail-line"></div> - <span class="nav-section__rail-label">{{ section.label }}</span> - </div> - } - <app-sidebar-nav-item - [label]="section.label" - [icon]="section.icon" - [route]="section.route" - [badge]="section.sectionBadge" - [collapsed]="collapsed" - ></app-sidebar-nav-item> - @for (child of section.displayChildren; track child.id) { - <app-sidebar-nav-item - [label]="child.label" - [icon]="child.icon" - [route]="child.route" - [badge]="child.badge ?? null" - [isChild]="true" - [collapsed]="collapsed" - ></app-sidebar-nav-item> + <div class="sb-group__body" [id]="'nav-grp-' + group.id"> + <div class="sb-group__body-inner"> + @for (section of group.sections; track section.id; let sectionFirst = $first) { + @if (!collapsed && !sectionFirst) { + <div class="sb-divider"></div> + } + @if (!collapsed && section.displayChildren.length > 0) { + <!-- Section with foldable children --> + <div class="sb-section" [class.sb-section--folded]="sidebarPrefs.collapsedSections().has(section.id)"> + <div class="sb-section__head"> + <app-sidebar-nav-item + [label]="section.label" + [icon]="section.icon" + [route]="section.route" + [badge]="section.sectionBadge" + [collapsed]="collapsed" + ></app-sidebar-nav-item> + <button + type="button" + class="sb-section__fold" + (click)="sidebarPrefs.toggleSection(section.id)" + [attr.aria-expanded]="!sidebarPrefs.collapsedSections().has(section.id)" + [attr.aria-label]="(sidebarPrefs.collapsedSections().has(section.id) ? 'Expand ' : 'Collapse ') + section.label" + > + <svg class="sb-section__fold-icon" viewBox="0 0 16 16" width="12" height="12" aria-hidden="true"> + <path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> + </svg> + </button> + </div> + <div class="sb-section__body"> + <div class="sb-section__body-inner"> + @for (child of section.displayChildren; track child.id) { + <app-sidebar-nav-item + [label]="child.label" + [icon]="child.icon" + [route]="child.route" + [badge]="child.badge ?? null" + [isChild]="true" + [collapsed]="collapsed" + ></app-sidebar-nav-item> + } + </div> + </div> + </div> + } @else { + <!-- Section without children (or collapsed sidebar) --> + <app-sidebar-nav-item + [label]="section.label" + [icon]="section.icon" + [route]="section.route" + [badge]="section.sectionBadge" + [collapsed]="collapsed" + ></app-sidebar-nav-item> + } } </div> </div> - } + </div> } </nav> <!-- Footer --> <div class="sidebar__footer"> + <button + type="button" + class="sidebar__collapse-btn" + (click)="collapseToggle.emit()" + [attr.title]="collapsed ? 'Expand sidebar' : 'Collapse sidebar'" + [attr.aria-label]="collapsed ? 'Expand sidebar' : 'Collapse sidebar'" + > + <svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"> + @if (collapsed) { + <polyline points="9 18 15 12 9 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> + } @else { + <polyline points="15 18 9 12 15 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> + } + </svg> + </button> <div class="sidebar__footer-divider"></div> <span class="sidebar__version">Stella Ops v1.0.0-alpha</span> </div> </aside> `, styles: [` + /* ================================================================ + Sidebar shell + ================================================================ */ .sidebar { display: flex; flex-direction: column; @@ -149,7 +215,6 @@ export interface NavSection { width: 56px; } - /* Subtle inner glow along right edge */ .sidebar::after { content: ''; position: absolute; @@ -160,7 +225,17 @@ export interface NavSection { background: var(--color-sidebar-border); } - /* ---- Mobile close ---- */ + /* Mobile: always full width regardless of collapsed state */ + @media (max-width: 991px) { + .sidebar, + .sidebar.sidebar--collapsed { + width: 280px; + } + } + + /* ================================================================ + Mobile close + ================================================================ */ .sidebar__close { display: none; position: absolute; @@ -189,12 +264,14 @@ export interface NavSection { } } - /* ---- Nav ---- */ + /* ================================================================ + Scrollable nav area + ================================================================ */ .sidebar__nav { flex: 1; overflow-y: auto; overflow-x: hidden; - padding: 0.5rem 0.5rem; + padding: 0.25rem 0.375rem; scrollbar-width: thin; scrollbar-color: rgba(255, 255, 255, 0.08) transparent; @@ -210,71 +287,212 @@ export interface NavSection { } } - /* ---- Collapsed nav ---- */ .sidebar--collapsed .sidebar__nav { - padding: 0.5rem 0.25rem; + padding: 0.25rem 0.125rem; } - /* ---- Section groups ---- */ - .nav-section__divider { + /* ================================================================ + Nav group (collapsible section cluster) + ================================================================ */ + .sb-group { + &:not(:first-child) { + margin-top: 0.125rem; + padding-top: 0.25rem; + border-top: 1px solid var(--color-sidebar-divider); + } + } + + /* ---- Group header (toggle button) ---- */ + .sb-group__header { + display: flex; + align-items: center; + width: calc(100% - 0.25rem); + margin: 0 0.125rem 0.125rem; + padding: 0.3125rem 0.5rem; + border: none; + border-radius: 4px; + background: transparent; + color: var(--color-sidebar-text-muted); + cursor: pointer; + font-family: inherit; + font-size: 0; + transition: color 0.15s, background 0.15s; + + &:hover { + color: var(--color-sidebar-text-heading); + background: rgba(255, 255, 255, 0.03); + } + + &:focus-visible { + outline: 1px solid var(--color-sidebar-active-border); + outline-offset: -1px; + } + } + + .sb-group__title { + flex: 1; + font-size: 0.5625rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.1em; + line-height: 1; + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + /* ---- Group chevron (rotates on collapse) ---- */ + .sb-group__chevron { + flex-shrink: 0; + opacity: 0.35; + transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), + opacity 0.15s; + } + + .sb-group--collapsed .sb-group__chevron { + transform: rotate(-90deg); + } + + .sb-group__header:hover .sb-group__chevron { + opacity: 0.7; + } + + /* ---- Animated group body (CSS grid trick) ---- */ + .sb-group__body { + display: grid; + grid-template-rows: 1fr; + transition: grid-template-rows 0.28s cubic-bezier(0.4, 0, 0.2, 1); + } + + .sb-group--collapsed .sb-group__body { + grid-template-rows: 0fr; + } + + .sb-group__body-inner { + overflow: hidden; + } + + /* ================================================================ + Sections within a group (foldable children) + ================================================================ */ + .sb-divider { height: 1px; background: var(--color-sidebar-divider); - margin: 0.5rem 0.75rem; + margin: 0.25rem 0.75rem; } - .nav-section__body { + /* Section head: nav-item + fold toggle */ + .sb-section__head { position: relative; } - .nav-section__rail { + .sb-section__fold { position: absolute; - left: 2px; - top: 0; - bottom: 0; - width: 14px; + right: 0.375rem; + top: 50%; + transform: translateY(-50%); display: flex; align-items: center; justify-content: center; - pointer-events: none; - z-index: 1; + width: 22px; + height: 22px; + border: none; + border-radius: 4px; + background: transparent; + color: var(--color-sidebar-text-muted); + cursor: pointer; + opacity: 0; + transition: opacity 0.15s, background 0.15s, color 0.15s; + z-index: 2; + + &:hover { + background: rgba(255, 255, 255, 0.06); + color: var(--color-sidebar-text-heading); + } + + &:focus-visible { + opacity: 1; + outline: 1px solid var(--color-sidebar-active-border); + } } - .nav-section__rail-line { - position: absolute; - top: 6px; - bottom: 6px; - left: 50%; - width: 1px; - background: rgba(245, 200, 120, 0.12); - transform: translateX(-50%); + /* Show fold button on hover or when section is folded */ + .sb-section__head:hover .sb-section__fold, + .sb-section--folded .sb-section__fold { + opacity: 1; } - .nav-section__rail-label { - position: relative; - z-index: 1; - writing-mode: vertical-rl; - transform: rotate(180deg); - font-size: 0.5rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.1em; - color: var(--color-sidebar-group-text); - white-space: nowrap; - background: var(--color-sidebar-bg); - padding: 4px 0; - line-height: 1; + .sb-section__fold-icon { + transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); } - /* ---- Footer ---- */ + .sb-section--folded .sb-section__fold-icon { + transform: rotate(-90deg); + } + + /* Animated section body */ + .sb-section__body { + display: grid; + grid-template-rows: 1fr; + transition: grid-template-rows 0.22s cubic-bezier(0.4, 0, 0.2, 1); + } + + .sb-section--folded .sb-section__body { + grid-template-rows: 0fr; + } + + .sb-section__body-inner { + overflow: hidden; + margin-left: 1.25rem; + border-left: 1px solid rgba(245, 166, 35, 0.12); + } + + /* ================================================================ + Footer with collapse toggle + ================================================================ */ .sidebar__footer { flex-shrink: 0; - padding: 0.5rem 0.75rem; + padding: 0.375rem 0.75rem 0.5rem; + } + + .sidebar__collapse-btn { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 28px; + border: 1px solid var(--color-sidebar-divider); + border-radius: 6px; + background: transparent; + color: var(--color-sidebar-text-muted); + cursor: pointer; + margin-bottom: 0.375rem; + transition: color 0.15s, background 0.15s, border-color 0.15s; + + &:hover { + color: var(--color-sidebar-text-heading); + background: rgba(255, 255, 255, 0.04); + border-color: rgba(255, 255, 255, 0.12); + } + + &:focus-visible { + outline: 1px solid var(--color-sidebar-active-border); + outline-offset: -1px; + } + } + + /* Hide collapse button on mobile (mobile uses the close X) */ + @media (max-width: 991px) { + .sidebar__collapse-btn { + display: none; + } } .sidebar__footer-divider { height: 1px; background: var(--color-sidebar-divider); - margin-bottom: 0.5rem; + margin-bottom: 0.375rem; } .sidebar__version { @@ -289,7 +507,7 @@ export interface NavSection { } .sidebar--collapsed .sidebar__footer { - padding: 0.5rem; + padding: 0.375rem 0.25rem 0.5rem; } .sidebar--collapsed .sidebar__version { @@ -307,6 +525,8 @@ export class AppSidebarComponent implements AfterViewInit { private readonly doctorTrendService = inject(DoctorTrendService); private readonly ngZone = inject(NgZone); + readonly sidebarPrefs = inject(SidebarPreferenceService); + @Input() collapsed = false; @Output() mobileClose = new EventEmitter<void>(); @Output() collapseToggle = new EventEmitter<void>(); @@ -324,6 +544,8 @@ export class AppSidebarComponent implements AfterViewInit { label: 'Dashboard', icon: 'dashboard', route: '/mission-control/board', + menuGroupId: 'release-control', + menuGroupLabel: 'Release Control', requireAnyScope: [ StellaOpsScopes.UI_READ, StellaOpsScopes.RELEASE_READ, @@ -335,6 +557,8 @@ export class AppSidebarComponent implements AfterViewInit { label: 'Releases', icon: 'package', route: '/releases/deployments', + menuGroupId: 'release-control', + menuGroupLabel: 'Release Control', requireAnyScope: [ StellaOpsScopes.RELEASE_READ, StellaOpsScopes.RELEASE_WRITE, @@ -384,6 +608,8 @@ export class AppSidebarComponent implements AfterViewInit { label: 'Vulnerabilities', icon: 'shield', route: '/security', + menuGroupId: 'security-evidence', + menuGroupLabel: 'Security & Evidence', sparklineData$: () => this.doctorTrendService.securityTrend(), requireAnyScope: [ StellaOpsScopes.SCANNER_READ, @@ -406,6 +632,8 @@ export class AppSidebarComponent implements AfterViewInit { label: 'Evidence', icon: 'file-text', route: '/evidence/overview', + menuGroupId: 'security-evidence', + menuGroupLabel: 'Security & Evidence', requireAnyScope: [ StellaOpsScopes.RELEASE_READ, StellaOpsScopes.POLICY_AUDIT, @@ -426,6 +654,8 @@ export class AppSidebarComponent implements AfterViewInit { label: 'Operations', icon: 'settings', route: '/ops/operations', + menuGroupId: 'platform-setup', + menuGroupLabel: 'Platform & Setup', sparklineData$: () => this.doctorTrendService.platformTrend(), requireAnyScope: [ StellaOpsScopes.UI_ADMIN, @@ -445,6 +675,8 @@ export class AppSidebarComponent implements AfterViewInit { label: 'Setup', icon: 'server', route: '/setup/system', + menuGroupId: 'platform-setup', + menuGroupLabel: 'Platform & Setup', requireAnyScope: [ StellaOpsScopes.UI_ADMIN, StellaOpsScopes.RELEASE_READ, @@ -469,14 +701,42 @@ export class AppSidebarComponent implements AfterViewInit { }); /** Sections with duplicate children removed and badges resolved */ - readonly displaySections = computed(() => { - return this.visibleSections().map(section => ({ + readonly displaySections = computed<DisplayNavSection[]>(() => { + return this.visibleSections().map((section) => ({ ...section, sectionBadge: section.badge$?.() ?? null, - displayChildren: (section.children ?? []).filter(child => child.route !== section.route), + displayChildren: (section.children ?? []).filter((child) => child.route !== section.route), })); }); + /** Menu groups rendered in deterministic order for scanability */ + readonly displaySectionGroups = computed<NavSectionGroup[]>(() => { + const orderedGroups = new Map<string, NavSectionGroup>(); + const groupOrder = ['release-control', 'security-evidence', 'platform-setup', 'misc']; + + for (const groupId of groupOrder) { + orderedGroups.set(groupId, { + id: groupId, + label: this.resolveMenuGroupLabel(groupId), + sections: [], + }); + } + + for (const section of this.displaySections()) { + const groupId = section.menuGroupId ?? 'misc'; + const group = orderedGroups.get(groupId) ?? { + id: groupId, + label: section.menuGroupLabel ?? this.resolveMenuGroupLabel(groupId), + sections: [], + }; + + group.sections.push(section); + orderedGroups.set(groupId, group); + } + + return Array.from(orderedGroups.values()).filter((group) => group.sections.length > 0); + }); + constructor() { this.loadPendingApprovalsBadge(); this.router.events @@ -510,10 +770,6 @@ export class AppSidebarComponent implements AfterViewInit { .map((child) => this.filterItem(child)) .filter((child): child is NavItem => child !== null); - if (visibleChildren.length === 0) { - return null; - } - const children = visibleChildren.map((child) => this.withDynamicChildState(child)); return { @@ -522,6 +778,23 @@ export class AppSidebarComponent implements AfterViewInit { }; } + private resolveMenuGroupLabel(groupId: string): string { + switch (groupId) { + case 'release-control': + return 'Release Control'; + case 'security-evidence': + return 'Security & Evidence'; + case 'platform-setup': + return 'Platform & Setup'; + default: + return 'Global Menu'; + } + } + + groupRoute(group: NavSectionGroup): string { + return group.sections[0]?.route ?? '/mission-control/board'; + } + private withDynamicChildState(item: NavItem): NavItem { if (item.id !== 'rel-approvals') { return item; @@ -579,14 +852,34 @@ export class AppSidebarComponent implements AfterViewInit { const EDGE_ZONE = 60; const MAX_SPEED = 8; let scrollDirection = 0; // -1 = up, 0 = none, 1 = down - let animFrameId = 0; + let animFrameId: number | null = null; let paused = false; - const scrollLoop = () => { - if (scrollDirection !== 0 && !paused) { - navEl.scrollTop += scrollDirection; + const stopScrollLoop = () => { + if (animFrameId === null) { + return; } - animFrameId = requestAnimationFrame(scrollLoop); + + cancelAnimationFrame(animFrameId); + animFrameId = null; + }; + + const runScrollLoop = () => { + if (animFrameId !== null) { + return; + } + + const tick = () => { + if (scrollDirection === 0 || paused) { + animFrameId = null; + return; + } + + navEl.scrollTop += scrollDirection; + animFrameId = requestAnimationFrame(tick); + }; + + animFrameId = requestAnimationFrame(tick); }; const onMouseMove = (e: MouseEvent) => { @@ -595,7 +888,7 @@ export class AppSidebarComponent implements AfterViewInit { // Check if hovering a nav item (pause scrolling) const target = e.target as HTMLElement; - paused = !!(target.closest('.nav-item') || target.closest('.nav-group__header')); + paused = !!(target.closest('.nav-item') || target.closest('.sb-group__header')); const distFromTop = mouseY - rect.top; const distFromBottom = rect.bottom - mouseY; @@ -609,22 +902,28 @@ export class AppSidebarComponent implements AfterViewInit { } else { scrollDirection = 0; } + + if (scrollDirection !== 0 && !paused) { + runScrollLoop(); + } else { + stopScrollLoop(); + } }; const onMouseLeave = () => { scrollDirection = 0; paused = false; + stopScrollLoop(); }; // Run outside Angular zone to avoid unnecessary change detection this.ngZone.runOutsideAngular(() => { navEl.addEventListener('mousemove', onMouseMove); navEl.addEventListener('mouseleave', onMouseLeave); - animFrameId = requestAnimationFrame(scrollLoop); }); this.destroyRef.onDestroy(() => { - cancelAnimationFrame(animFrameId); + stopScrollLoop(); navEl.removeEventListener('mousemove', onMouseMove); navEl.removeEventListener('mouseleave', onMouseLeave); }); diff --git a/src/Web/StellaOps.Web/src/app/layout/app-sidebar/sidebar-preference.service.ts b/src/Web/StellaOps.Web/src/app/layout/app-sidebar/sidebar-preference.service.ts new file mode 100644 index 000000000..2d6fe9863 --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/layout/app-sidebar/sidebar-preference.service.ts @@ -0,0 +1,82 @@ +import { Injectable, signal, computed, effect } from '@angular/core'; + +export interface SidebarPreferences { + sidebarCollapsed: boolean; + collapsedGroups: string[]; + collapsedSections: string[]; +} + +const STORAGE_KEY = 'stellaops.sidebar.preferences'; + +const DEFAULTS: SidebarPreferences = { + sidebarCollapsed: false, + collapsedGroups: [], + collapsedSections: ['ops', 'setup'], +}; + +@Injectable({ providedIn: 'root' }) +export class SidebarPreferenceService { + private readonly _state = signal<SidebarPreferences>(this.load()); + + readonly sidebarCollapsed = computed(() => this._state().sidebarCollapsed); + readonly collapsedGroups = computed(() => new Set(this._state().collapsedGroups)); + readonly collapsedSections = computed(() => new Set(this._state().collapsedSections)); + + constructor() { + effect(() => this.save(this._state())); + } + + toggleSidebar(): void { + this._state.update((s) => ({ ...s, sidebarCollapsed: !s.sidebarCollapsed })); + } + + toggleGroup(groupId: string): void { + this._state.update((s) => { + const set = new Set(s.collapsedGroups); + if (set.has(groupId)) set.delete(groupId); + else set.add(groupId); + return { ...s, collapsedGroups: [...set] }; + }); + } + + toggleSection(sectionId: string): void { + this._state.update((s) => { + const set = new Set(s.collapsedSections); + if (set.has(sectionId)) set.delete(sectionId); + else set.add(sectionId); + return { ...s, collapsedSections: [...set] }; + }); + } + + private load(): SidebarPreferences { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (raw) { + const parsed = JSON.parse(raw); + return { + sidebarCollapsed: + typeof parsed.sidebarCollapsed === 'boolean' + ? parsed.sidebarCollapsed + : DEFAULTS.sidebarCollapsed, + collapsedGroups: Array.isArray(parsed.collapsedGroups) + ? parsed.collapsedGroups + : DEFAULTS.collapsedGroups, + collapsedSections: Array.isArray(parsed.collapsedSections) + ? parsed.collapsedSections + : DEFAULTS.collapsedSections, + }; + } + } catch { + /* ignore parse errors */ + } + return { ...DEFAULTS }; + } + + private save(state: SidebarPreferences): void { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); + } catch { + /* ignore quota / private-mode errors */ + } + } +} 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 f2144836b..7f8b2721a 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 @@ -78,13 +78,28 @@ import { I18nService, UserLocalePreferenceService } from '../../core/i18n'; <a class="topbar__primary-action" [routerLink]="action.route">{{ action.label }}</a> } + <button + type="button" + class="topbar__context-toggle" + [class.topbar__context-toggle--active]="mobileContextOpen()" + [attr.aria-expanded]="mobileContextOpen()" + aria-controls="topbar-context-row" + (click)="toggleMobileContext()" + > + Context + </button> + <!-- User menu --> <app-user-menu></app-user-menu> </div> </div> <!-- Row 2: Tenant (if multi) + Context chips + Locale --> - <div class="topbar__row topbar__row--secondary"> + <div + id="topbar-context-row" + class="topbar__row topbar__row--secondary" + [class.topbar__row--secondary-open]="mobileContextOpen()" + > <!-- Tenant selector (only shown when multiple tenants) --> @if (showTenantSelector()) { <div class="topbar__tenant"> @@ -198,6 +213,14 @@ import { I18nService, UserLocalePreferenceService } from '../../core/i18n'; .topbar__row--secondary { padding: 0 0.5rem; + display: none; + } + + .topbar__row--secondary-open { + display: flex; + height: auto; + padding: 0.35rem 0.5rem; + overflow: visible; } } @@ -312,6 +335,34 @@ import { I18nService, UserLocalePreferenceService } from '../../core/i18n'; border-color: var(--color-brand-primary); } + .topbar__context-toggle { + display: none; + border: 1px solid var(--color-border-primary); + border-radius: var(--radius-sm); + height: 28px; + padding: 0 0.45rem; + background: var(--color-surface-primary); + color: var(--color-text-secondary); + font-size: 0.625rem; + font-family: var(--font-family-mono); + letter-spacing: 0.06em; + text-transform: uppercase; + cursor: pointer; + transition: border-color 0.12s, color 0.12s, background 0.12s; + } + + .topbar__context-toggle:hover { + border-color: var(--color-brand-primary); + color: var(--color-text-primary); + background: var(--color-surface-secondary); + } + + .topbar__context-toggle--active { + border-color: var(--color-brand-primary); + background: color-mix(in srgb, var(--color-brand-primary) 14%, var(--color-surface-primary)); + color: var(--color-brand-primary); + } + @media (max-width: 575px) { .topbar__right { gap: 0.25rem; @@ -320,6 +371,12 @@ import { I18nService, UserLocalePreferenceService } from '../../core/i18n'; .topbar__primary-action { display: none; } + + .topbar__context-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + } } /* ---- Row 2 separator ---- */ @@ -495,6 +552,7 @@ export class AppTopbarComponent { ); readonly localePreferenceSyncAttempted = signal(false); readonly scopePanelOpen = signal(false); + readonly mobileContextOpen = signal(false); readonly tenantPanelOpen = signal(false); readonly tenantSwitchInFlight = signal(false); readonly tenantBootstrapAttempted = signal(false); @@ -551,6 +609,10 @@ export class AppTopbarComponent { this.scopePanelOpen.update((open) => !open); } + toggleMobileContext(): void { + this.mobileContextOpen.update((open) => !open); + } + closeScopePanel(): void { this.scopePanelOpen.set(false); } @@ -654,6 +716,7 @@ export class AppTopbarComponent { onEscape(): void { this.closeScopePanel(); this.closeTenantPanel(); + this.mobileContextOpen.set(false); } @HostListener('document:click', ['$event']) @@ -666,12 +729,17 @@ export class AppTopbarComponent { const host = this.elementRef.nativeElement; const insideScope = host.querySelector('.topbar__scope-wrap')?.contains(target) ?? false; const insideTenant = host.querySelector('.topbar__tenant')?.contains(target) ?? false; + const insideContextToggle = host.querySelector('.topbar__context-toggle')?.contains(target) ?? false; + const insideContextRow = host.querySelector('.topbar__row--secondary')?.contains(target) ?? false; if (!insideScope) { this.closeScopePanel(); } if (!insideTenant) { this.closeTenantPanel(); } + if (!insideContextToggle && !insideContextRow) { + this.mobileContextOpen.set(false); + } } private async loadTenantContextIfNeeded(): Promise<void> { diff --git a/src/Web/StellaOps.Web/src/app/layout/context-chips/context-chips.component.ts b/src/Web/StellaOps.Web/src/app/layout/context-chips/context-chips.component.ts index 9fe51b3d7..986c632ce 100644 --- a/src/Web/StellaOps.Web/src/app/layout/context-chips/context-chips.component.ts +++ b/src/Web/StellaOps.Web/src/app/layout/context-chips/context-chips.component.ts @@ -381,6 +381,40 @@ interface DropdownOption { font-size: 0.625rem; color: var(--color-status-error-text); } + + @media (max-width: 575px) { + .ctx { + width: 100%; + align-items: flex-start; + } + + .ctx__controls { + flex-wrap: wrap; + gap: 0.2rem; + } + + .ctx__dropdown-btn { + height: 22px; + padding: 0 0.35rem; + } + + .ctx__dropdown-key { + display: none; + } + + .ctx__dropdown-val { + max-width: 82px; + font-size: 0.58rem; + } + + .ctx__sep { + display: none; + } + + .ctx__chips { + display: none; + } + } `], changeDetection: ChangeDetectionStrategy.OnPush, }) diff --git a/src/Web/StellaOps.Web/src/i18n/bg-BG.common.json b/src/Web/StellaOps.Web/src/i18n/bg-BG.common.json index 68fb71f24..d9bbc0b86 100644 --- a/src/Web/StellaOps.Web/src/i18n/bg-BG.common.json +++ b/src/Web/StellaOps.Web/src/i18n/bg-BG.common.json @@ -1,8 +1,9 @@ { "_meta": { "locale": "bg-BG", - "description": "Offline fallback bundle for StellaOps Console" + "description": "Offline fallback bundle for Stella Ops Console" }, + "ui.branding.app_name": "пакет \"Стелла\"", "common.error.generic": "Нещо се обърка.", "common.error.not_found": "Заявеният ресурс не беше намерен.", "common.error.unauthorized": "Нямате права да извършите това действие.", diff --git a/src/Web/StellaOps.Web/src/i18n/de-DE.common.json b/src/Web/StellaOps.Web/src/i18n/de-DE.common.json index 90e5fd9c7..5d7ea2535 100644 --- a/src/Web/StellaOps.Web/src/i18n/de-DE.common.json +++ b/src/Web/StellaOps.Web/src/i18n/de-DE.common.json @@ -1,8 +1,9 @@ { "_meta": { "locale": "de-DE", - "description": "Offline fallback bundle for StellaOps Console" + "description": "Offline fallback bundle for Stella Ops Console" }, + "ui.branding.app_name": "Stella Ops", "common.error.generic": "Etwas ist schiefgelaufen.", "common.error.not_found": "Die angeforderte Ressource wurde nicht gefunden.", "common.error.unauthorized": "Sie haben keine Berechtigung, diese Aktion auszuführen.", diff --git a/src/Web/StellaOps.Web/src/i18n/en-US.common.json b/src/Web/StellaOps.Web/src/i18n/en-US.common.json index d83de12a1..5260b0ca6 100644 --- a/src/Web/StellaOps.Web/src/i18n/en-US.common.json +++ b/src/Web/StellaOps.Web/src/i18n/en-US.common.json @@ -1,8 +1,9 @@ { "_meta": { "locale": "en-US", - "description": "Offline fallback bundle for StellaOps Console" + "description": "Offline fallback bundle for Stella Ops Console" }, + "ui.branding.app_name": "Stella Ops", "common.error.generic": "Something went wrong.", "common.error.not_found": "The requested resource was not found.", "common.error.unauthorized": "You do not have permission to perform this action.", diff --git a/src/Web/StellaOps.Web/src/i18n/es-ES.common.json b/src/Web/StellaOps.Web/src/i18n/es-ES.common.json index a302cde35..0a11b38c8 100644 --- a/src/Web/StellaOps.Web/src/i18n/es-ES.common.json +++ b/src/Web/StellaOps.Web/src/i18n/es-ES.common.json @@ -1,8 +1,9 @@ { "_meta": { "locale": "es-ES", - "description": "Offline fallback bundle for StellaOps Console" + "description": "Offline fallback bundle for Stella Ops Console" }, + "ui.branding.app_name": "Stella Ops", "common.error.generic": "Algo salió mal.", "common.error.not_found": "No se encontró el recurso solicitado.", "common.error.unauthorized": "No tienes permiso para realizar esta acción.", diff --git a/src/Web/StellaOps.Web/src/i18n/fr-FR.common.json b/src/Web/StellaOps.Web/src/i18n/fr-FR.common.json index 2a703129b..16507f955 100644 --- a/src/Web/StellaOps.Web/src/i18n/fr-FR.common.json +++ b/src/Web/StellaOps.Web/src/i18n/fr-FR.common.json @@ -1,8 +1,9 @@ { "_meta": { "locale": "fr-FR", - "description": "Offline fallback bundle for StellaOps Console" + "description": "Offline fallback bundle for Stella Ops Console" }, + "ui.branding.app_name": "Stella Ops", "common.error.generic": "Quelque chose s'est mal passé.", "common.error.not_found": "La ressource demandée n'a pas été trouvée.", "common.error.unauthorized": "Vous n'êtes pas autorisé à effectuer cette action.", diff --git a/src/Web/StellaOps.Web/src/i18n/micro-interactions.en.json b/src/Web/StellaOps.Web/src/i18n/micro-interactions.en.json index 6826ce905..ed5a66ec8 100644 --- a/src/Web/StellaOps.Web/src/i18n/micro-interactions.en.json +++ b/src/Web/StellaOps.Web/src/i18n/micro-interactions.en.json @@ -3,7 +3,7 @@ "_meta": { "version": "1.0", "locale": "en-US", - "description": "Micro-interaction copy for StellaOps Console (MI9)" + "description": "Micro-interaction copy for Stella Ops Console (MI9)" }, "loading": { "skeleton": "Loading...", diff --git a/src/Web/StellaOps.Web/src/i18n/ru-RU.common.json b/src/Web/StellaOps.Web/src/i18n/ru-RU.common.json index 363674969..bb81e4339 100644 --- a/src/Web/StellaOps.Web/src/i18n/ru-RU.common.json +++ b/src/Web/StellaOps.Web/src/i18n/ru-RU.common.json @@ -1,8 +1,9 @@ { "_meta": { "locale": "ru-RU", - "description": "Offline fallback bundle for StellaOps Console" + "description": "Offline fallback bundle for Stella Ops Console" }, + "ui.branding.app_name": "пакет \"Стелла\"", "common.error.generic": "Что-то пошло не так.", "common.error.not_found": "Запрошенный ресурс не найден.", "common.error.unauthorized": "У вас нет разрешения на выполнение этого действия.", diff --git a/src/Web/StellaOps.Web/src/i18n/uk-UA.common.json b/src/Web/StellaOps.Web/src/i18n/uk-UA.common.json index 01873a285..6be7dcf5a 100644 --- a/src/Web/StellaOps.Web/src/i18n/uk-UA.common.json +++ b/src/Web/StellaOps.Web/src/i18n/uk-UA.common.json @@ -1,8 +1,9 @@ { "_meta": { "locale": "uk-UA", - "description": "Offline fallback bundle for StellaOps Console" + "description": "Offline fallback bundle for Stella Ops Console" }, + "ui.branding.app_name": "пакет \"Стелла\"", "common.error.generic": "Щось пішло не так.", "common.error.not_found": "Потрібний ресурс не знайдено.", "common.error.unauthorized": "Ви не маєте дозволу на виконання цієї дії.", diff --git a/src/Web/StellaOps.Web/src/i18n/zh-CN.common.json b/src/Web/StellaOps.Web/src/i18n/zh-CN.common.json index fc9b4fe63..cfd4fa119 100644 --- a/src/Web/StellaOps.Web/src/i18n/zh-CN.common.json +++ b/src/Web/StellaOps.Web/src/i18n/zh-CN.common.json @@ -1,8 +1,9 @@ { "_meta": { "locale": "zh-CN", - "description": "Offline fallback bundle for StellaOps Console" + "description": "Offline fallback bundle for Stella Ops Console" }, + "ui.branding.app_name": "Stella Ops", "common.error.generic": "出了点问题。", "common.error.not_found": "未找到请求的资源。", "common.error.unauthorized": "您没有执行此操作的权限。", diff --git a/src/Web/StellaOps.Web/src/i18n/zh-TW.common.json b/src/Web/StellaOps.Web/src/i18n/zh-TW.common.json index 16261009b..d1172288a 100644 --- a/src/Web/StellaOps.Web/src/i18n/zh-TW.common.json +++ b/src/Web/StellaOps.Web/src/i18n/zh-TW.common.json @@ -1,8 +1,9 @@ { "_meta": { "locale": "zh-TW", - "description": "Offline fallback bundle for StellaOps Console" + "description": "Offline fallback bundle for Stella Ops Console" }, + "ui.branding.app_name": "Stella Ops", "common.error.generic": "出了點問題。", "common.error.not_found": "未找到請求的資源。", "common.error.unauthorized": "您沒有執行此操作的權限。", diff --git a/src/Web/StellaOps.Web/src/manifest.webmanifest b/src/Web/StellaOps.Web/src/manifest.webmanifest index 9e7c7b587..1ecb001c7 100644 --- a/src/Web/StellaOps.Web/src/manifest.webmanifest +++ b/src/Web/StellaOps.Web/src/manifest.webmanifest @@ -1,6 +1,6 @@ { "name": "Stella Ops", - "short_name": "StellaOps", + "short_name": "Stella Ops", "description": "Release control plane for container estates", "start_url": "/", "display": "standalone", diff --git a/src/Web/StellaOps.Web/src/styles/tokens/_colors.scss b/src/Web/StellaOps.Web/src/styles/tokens/_colors.scss index 79cdb62b1..aa2f8c3b7 100644 --- a/src/Web/StellaOps.Web/src/styles/tokens/_colors.scss +++ b/src/Web/StellaOps.Web/src/styles/tokens/_colors.scss @@ -30,8 +30,8 @@ // --------------------------------------------------------------------------- --color-text-primary: #3D2E0A; --color-text-heading: #1C1200; - --color-text-secondary: #6B5A2E; - --color-text-muted: #9A8F78; + --color-text-secondary: #5A4A24; + --color-text-muted: #756949; --color-text-inverse: #F5F0E6; --color-text-link: #D4920A; --color-text-link-hover: #F5A623; @@ -39,8 +39,8 @@ // --------------------------------------------------------------------------- // Border Colors // --------------------------------------------------------------------------- - --color-border-primary: rgba(212, 201, 168, 0.3); - --color-border-secondary: rgba(212, 201, 168, 0.5); + --color-border-primary: rgba(176, 160, 118, 0.52); + --color-border-secondary: rgba(176, 160, 118, 0.72); --color-border-focus: #F5A623; --color-border-error: #ef4444; diff --git a/src/Web/StellaOps.Web/src/tests/doctor/doctor-wizard-mapping.spec.ts b/src/Web/StellaOps.Web/src/tests/doctor/doctor-wizard-mapping.spec.ts index cf3d8869d..24ad5d8a8 100644 --- a/src/Web/StellaOps.Web/src/tests/doctor/doctor-wizard-mapping.spec.ts +++ b/src/Web/StellaOps.Web/src/tests/doctor/doctor-wizard-mapping.spec.ts @@ -92,15 +92,15 @@ describe('DoctorWizardMapping', () => { describe('buildWizardDeepLink', () => { it('should build correct deep link for database', () => { - expect(buildWizardDeepLink('database')).toBe('/setup/wizard?step=database&mode=reconfigure'); + expect(buildWizardDeepLink('database')).toBe('/setup-wizard/wizard?step=database&mode=reconfigure'); }); it('should build correct deep link for authority', () => { - expect(buildWizardDeepLink('authority')).toBe('/setup/wizard?step=authority&mode=reconfigure'); + expect(buildWizardDeepLink('authority')).toBe('/setup-wizard/wizard?step=authority&mode=reconfigure'); }); it('should build correct deep link for telemetry', () => { - expect(buildWizardDeepLink('telemetry')).toBe('/setup/wizard?step=telemetry&mode=reconfigure'); + expect(buildWizardDeepLink('telemetry')).toBe('/setup-wizard/wizard?step=telemetry&mode=reconfigure'); }); }); }); diff --git a/src/Web/StellaOps.Web/tests/e2e/nav-shell.spec.ts b/src/Web/StellaOps.Web/tests/e2e/nav-shell.spec.ts index 57bcdb777..b88b8bfca 100644 --- a/src/Web/StellaOps.Web/tests/e2e/nav-shell.spec.ts +++ b/src/Web/StellaOps.Web/tests/e2e/nav-shell.spec.ts @@ -172,13 +172,14 @@ test.describe('Nav shell canonical domains', () => { const navText = (await page.locator('aside.sidebar').textContent()) ?? ''; const labels = [ - 'Mission Board', - 'Mission Alerts', - 'Mission Activity', + 'Release Control', + 'Security & Evidence', + 'Platform & Setup', + 'Dashboard', 'Releases', - 'Security', + 'Vulnerabilities', 'Evidence', - 'Ops', + 'Operations', 'Setup', ]; @@ -198,6 +199,45 @@ test.describe('Nav shell canonical domains', () => { expect(navText).not.toContain('Administration'); expect(navText).not.toContain('Policy Studio'); }); + + test('group headers are unique and navigate to group landing routes', async ({ page }) => { + await go(page, '/mission-control/board'); + await ensureShell(page); + + const releaseGroup = page.locator('aside.sidebar .nav-group__label', { hasText: 'Release Control' }); + const securityGroup = page.locator('aside.sidebar .nav-group__label', { hasText: 'Security & Evidence' }); + const platformGroup = page.locator('aside.sidebar .nav-group__label', { hasText: 'Platform & Setup' }); + + await expect(releaseGroup).toHaveCount(1); + await expect(securityGroup).toHaveCount(1); + await expect(platformGroup).toHaveCount(1); + + await releaseGroup.click(); + await expect(page).toHaveURL(/\/mission-control\/board$/); + + await securityGroup.click(); + await expect(page).toHaveURL(/\/security(\/|$)/); + + await platformGroup.click(); + await expect(page).toHaveURL(/\/ops(\/|$)/); + }); + + test('grouped root entries navigate when clicked', async ({ page }) => { + await go(page, '/mission-control/board'); + await ensureShell(page); + + await page.locator('aside.sidebar a.nav-item', { hasText: 'Releases' }).first().click(); + await expect(page).toHaveURL(/\/releases\/deployments$/); + + await page.locator('aside.sidebar a.nav-item', { hasText: 'Vulnerabilities' }).first().click(); + await expect(page).toHaveURL(/\/security(\/|$)/); + + await page.locator('aside.sidebar a.nav-item', { hasText: 'Evidence' }).first().click(); + await expect(page).toHaveURL(/\/evidence\/overview$/); + + await page.locator('aside.sidebar a.nav-item', { hasText: 'Operations' }).first().click(); + await expect(page).toHaveURL(/\/ops\/operations$/); + }); }); test.describe('Nav shell breadcrumbs and stability', () => { diff --git a/src/Web/StellaOps.Web/tests/e2e/unified-search-fixtures.ts b/src/Web/StellaOps.Web/tests/e2e/unified-search-fixtures.ts index 08c1f0702..05e099daf 100644 --- a/src/Web/StellaOps.Web/tests/e2e/unified-search-fixtures.ts +++ b/src/Web/StellaOps.Web/tests/e2e/unified-search-fixtures.ts @@ -21,7 +21,7 @@ export const mockConfig = { redirectUri: 'http://127.0.0.1:4400/auth/callback', postLogoutRedirectUri: 'http://127.0.0.1:4400/', scope: - 'openid profile email ui.read advisory:search advisory:read search:read findings:read vex:read policy:read health:read', + 'openid profile email ui.read advisory:read advisory-ai:view advisory-ai:operate findings:read vex:read policy:read health:read', audience: 'https://scanner.local', dpopAlgorithms: ['ES256'], refreshLeewaySeconds: 60, @@ -55,9 +55,9 @@ export const shellSession = { ...policyAuthorSession.scopes, 'ui.read', 'admin', - 'advisory:search', 'advisory:read', - 'search:read', + 'advisory-ai:view', + 'advisory-ai:operate', 'findings:read', 'vex:read', 'policy:read',