texts fixes, search bar fixes, global menu fixes.

This commit is contained in:
master
2026-03-05 18:10:56 +02:00
parent 8e1cb9448d
commit a918d39a61
101 changed files with 3543 additions and 534 deletions

View File

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

View File

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

View File

@@ -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": [

View File

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

View File

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

View File

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

View File

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

View File

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