e2e observation fixes
This commit is contained in:
@@ -410,6 +410,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- *cert-volume
|
- *cert-volume
|
||||||
- *ca-bundle
|
- *ca-bundle
|
||||||
|
- *ca-bundle
|
||||||
ports:
|
ports:
|
||||||
- "127.1.0.5:80:80"
|
- "127.1.0.5:80:80"
|
||||||
networks:
|
networks:
|
||||||
@@ -436,6 +437,7 @@ services:
|
|||||||
ConnectionStrings__Default: *postgres-connection
|
ConnectionStrings__Default: *postgres-connection
|
||||||
volumes:
|
volumes:
|
||||||
- *cert-volume
|
- *cert-volume
|
||||||
|
- *ca-bundle
|
||||||
ports:
|
ports:
|
||||||
- "127.1.0.6:80:80"
|
- "127.1.0.6:80:80"
|
||||||
networks:
|
networks:
|
||||||
@@ -495,6 +497,15 @@ services:
|
|||||||
EvidenceLocker__Quotas__MaxMaterialCount: "128"
|
EvidenceLocker__Quotas__MaxMaterialCount: "128"
|
||||||
ConnectionStrings__Redis: "cache.stella-ops.local:6379"
|
ConnectionStrings__Redis: "cache.stella-ops.local:6379"
|
||||||
EvidenceLocker__Authority__BaseUrl: "https://authority.stella-ops.local"
|
EvidenceLocker__Authority__BaseUrl: "https://authority.stella-ops.local"
|
||||||
|
Authority__ResourceServer__Authority: "https://authority.stella-ops.local/"
|
||||||
|
Authority__ResourceServer__MetadataAddress: "https://authority.stella-ops.local/.well-known/openid-configuration"
|
||||||
|
Authority__ResourceServer__RequireHttpsMetadata: "false"
|
||||||
|
Authority__ResourceServer__Audiences__0: ""
|
||||||
|
Authority__ResourceServer__BypassNetworks__0: "172.19.0.0/16"
|
||||||
|
Authority__ResourceServer__BypassNetworks__1: "127.0.0.1/32"
|
||||||
|
Authority__ResourceServer__BypassNetworks__2: "::1/128"
|
||||||
|
Authority__ResourceServer__BypassNetworks__3: "0.0.0.0/0"
|
||||||
|
Authority__ResourceServer__BypassNetworks__4: "::/0"
|
||||||
volumes:
|
volumes:
|
||||||
- *cert-volume
|
- *cert-volume
|
||||||
- *ca-bundle
|
- *ca-bundle
|
||||||
@@ -1775,8 +1786,18 @@ services:
|
|||||||
ConnectionStrings__Default: *postgres-connection
|
ConnectionStrings__Default: *postgres-connection
|
||||||
ConnectionStrings__Redis: "cache.stella-ops.local:6379"
|
ConnectionStrings__Redis: "cache.stella-ops.local:6379"
|
||||||
Export__AllowInMemoryRepositories: "true"
|
Export__AllowInMemoryRepositories: "true"
|
||||||
|
Authority__ResourceServer__Authority: "https://authority.stella-ops.local/"
|
||||||
|
Authority__ResourceServer__MetadataAddress: "https://authority.stella-ops.local/.well-known/openid-configuration"
|
||||||
|
Authority__ResourceServer__RequireHttpsMetadata: "false"
|
||||||
|
Authority__ResourceServer__Audiences__0: ""
|
||||||
|
Authority__ResourceServer__BypassNetworks__0: "172.19.0.0/16"
|
||||||
|
Authority__ResourceServer__BypassNetworks__1: "127.0.0.1/32"
|
||||||
|
Authority__ResourceServer__BypassNetworks__2: "::1/128"
|
||||||
|
Authority__ResourceServer__BypassNetworks__3: "0.0.0.0/0"
|
||||||
|
Authority__ResourceServer__BypassNetworks__4: "::/0"
|
||||||
volumes:
|
volumes:
|
||||||
- *cert-volume
|
- *cert-volume
|
||||||
|
- *ca-bundle
|
||||||
ports:
|
ports:
|
||||||
- "127.1.0.40:80:80"
|
- "127.1.0.40:80:80"
|
||||||
networks:
|
networks:
|
||||||
@@ -1799,8 +1820,14 @@ services:
|
|||||||
ConnectionStrings__Default: *postgres-connection
|
ConnectionStrings__Default: *postgres-connection
|
||||||
ConnectionStrings__Redis: "cache.stella-ops.local:6379"
|
ConnectionStrings__Redis: "cache.stella-ops.local:6379"
|
||||||
Export__AllowInMemoryRepositories: "true"
|
Export__AllowInMemoryRepositories: "true"
|
||||||
|
Authority__ResourceServer__Authority: "https://authority.stella-ops.local/"
|
||||||
|
Authority__ResourceServer__MetadataAddress: "https://authority.stella-ops.local/.well-known/openid-configuration"
|
||||||
|
Authority__ResourceServer__RequireHttpsMetadata: "false"
|
||||||
|
Authority__ResourceServer__Audiences__0: ""
|
||||||
|
Authority__ResourceServer__BypassNetworks__0: "172.19.0.0/16"
|
||||||
volumes:
|
volumes:
|
||||||
- *cert-volume
|
- *cert-volume
|
||||||
|
- *ca-bundle
|
||||||
networks:
|
networks:
|
||||||
stellaops:
|
stellaops:
|
||||||
aliases:
|
aliases:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"tokenEndpoint": "https://authority.stella-ops.local/connect/token",
|
"tokenEndpoint": "https://authority.stella-ops.local/connect/token",
|
||||||
"redirectUri": "https://stella-ops.local/auth/callback",
|
"redirectUri": "https://stella-ops.local/auth/callback",
|
||||||
"postLogoutRedirectUri": "https://stella-ops.local/",
|
"postLogoutRedirectUri": "https://stella-ops.local/",
|
||||||
"scope": "openid profile email offline_access ui.read ui.admin 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 orch:read analytics.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read release:read scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit",
|
"scope": "openid profile email offline_access ui.read ui.admin 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 orch:read analytics.read advisory:read vex: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",
|
||||||
"audience": "stella-ops-api",
|
"audience": "stella-ops-api",
|
||||||
"dpopAlgorithms": [
|
"dpopAlgorithms": [
|
||||||
"ES256"
|
"ES256"
|
||||||
@@ -14,50 +14,50 @@
|
|||||||
"refreshLeewaySeconds": 60
|
"refreshLeewaySeconds": 60
|
||||||
},
|
},
|
||||||
"apiBaseUrls": {
|
"apiBaseUrls": {
|
||||||
"vulnexplorer": "http://vulnexplorer.stella-ops.local",
|
"vulnexplorer": "https://stella-ops.local",
|
||||||
"replay": "http://replay.stella-ops.local",
|
"replay": "https://stella-ops.local",
|
||||||
"notify": "http://notify.stella-ops.local",
|
"notify": "https://stella-ops.local",
|
||||||
"notifier": "http://notifier.stella-ops.local",
|
"notifier": "https://stella-ops.local",
|
||||||
"airgapController": "http://airgap-controller.stella-ops.local",
|
"airgapController": "https://stella-ops.local",
|
||||||
"gateway": "http://gateway.stella-ops.local",
|
"gateway": "https://stella-ops.local",
|
||||||
"doctor": "http://doctor.stella-ops.local",
|
"doctor": "https://stella-ops.local",
|
||||||
"taskrunner": "http://taskrunner.stella-ops.local",
|
"taskrunner": "https://stella-ops.local",
|
||||||
"timelineindexer": "http://timelineindexer.stella-ops.local",
|
"timelineindexer": "https://stella-ops.local",
|
||||||
"timeline": "http://timeline.stella-ops.local",
|
"timeline": "https://stella-ops.local",
|
||||||
"packsregistry": "http://packsregistry.stella-ops.local",
|
"packsregistry": "https://stella-ops.local",
|
||||||
"findingsLedger": "http://findings.stella-ops.local",
|
"findingsLedger": "https://stella-ops.local",
|
||||||
"policyGateway": "http://policy-gateway.stella-ops.local",
|
"policyGateway": "https://stella-ops.local",
|
||||||
"registryTokenservice": "http://registry-token.stella-ops.local",
|
"registryTokenservice": "https://stella-ops.local",
|
||||||
"graph": "http://graph.stella-ops.local",
|
"graph": "https://stella-ops.local",
|
||||||
"issuerdirectory": "http://issuerdirectory.stella-ops.local",
|
"issuerdirectory": "https://stella-ops.local",
|
||||||
"router": "http://router.stella-ops.local",
|
"router": "https://stella-ops.local",
|
||||||
"integrations": "http://integrations.stella-ops.local",
|
"integrations": "https://stella-ops.local",
|
||||||
"platform": "http://platform.stella-ops.local",
|
"platform": "https://stella-ops.local",
|
||||||
"smremote": "http://smremote.stella-ops.local",
|
"smremote": "https://stella-ops.local",
|
||||||
"signals": "http://signals.stella-ops.local",
|
"signals": "https://stella-ops.local",
|
||||||
"vexlens": "http://vexlens.stella-ops.local",
|
"vexlens": "https://stella-ops.local",
|
||||||
"scheduler": "http://scheduler.stella-ops.local",
|
"scheduler": "https://stella-ops.local",
|
||||||
"concelier": "http://concelier.stella-ops.local",
|
"concelier": "https://stella-ops.local",
|
||||||
"opsmemory": "http://opsmemory.stella-ops.local",
|
"opsmemory": "https://stella-ops.local",
|
||||||
"binaryindex": "http://binaryindex.stella-ops.local",
|
"binaryindex": "https://stella-ops.local",
|
||||||
"signer": "http://signer.stella-ops.local",
|
"signer": "https://stella-ops.local",
|
||||||
"reachgraph": "http://reachgraph.stella-ops.local",
|
"reachgraph": "https://stella-ops.local",
|
||||||
"authority": "http://authority.stella-ops.local",
|
"authority": "https://stella-ops.local",
|
||||||
"unknowns": "http://unknowns.stella-ops.local",
|
"unknowns": "https://stella-ops.local",
|
||||||
"scanner": "http://scanner.stella-ops.local",
|
"scanner": "https://stella-ops.local",
|
||||||
"sbomservice": "http://sbomservice.stella-ops.local",
|
"sbomservice": "https://stella-ops.local",
|
||||||
"symbols": "http://symbols.stella-ops.local",
|
"symbols": "https://stella-ops.local",
|
||||||
"orchestrator": "http://orchestrator.stella-ops.local",
|
"orchestrator": "https://stella-ops.local",
|
||||||
"policyEngine": "http://policy-engine.stella-ops.local",
|
"policyEngine": "https://stella-ops.local",
|
||||||
"attestor": "http://attestor.stella-ops.local",
|
"attestor": "https://stella-ops.local",
|
||||||
"vexhub": "http://vexhub.stella-ops.local",
|
"vexhub": "https://stella-ops.local",
|
||||||
"riskengine": "http://riskengine.stella-ops.local",
|
"riskengine": "https://stella-ops.local",
|
||||||
"airgapTime": "http://airgap-time.stella-ops.local",
|
"airgapTime": "https://stella-ops.local",
|
||||||
"advisoryai": "http://advisoryai.stella-ops.local",
|
"advisoryai": "https://stella-ops.local",
|
||||||
"excititor": "http://excititor.stella-ops.local",
|
"excititor": "https://stella-ops.local",
|
||||||
"cartographer": "http://cartographer.stella-ops.local",
|
"cartographer": "https://stella-ops.local",
|
||||||
"evidencelocker": "http://evidencelocker.stella-ops.local",
|
"evidencelocker": "https://stella-ops.local",
|
||||||
"exportcenter": "http://exportcenter.stella-ops.local"
|
"exportcenter": "https://stella-ops.local"
|
||||||
},
|
},
|
||||||
"setup": "complete"
|
"setup": "complete"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
},
|
},
|
||||||
"Routes": [
|
"Routes": [
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/release-orchestrator", "TranslatesTo": "http://orchestrator.stella-ops.local/api/v1/release-orchestrator" },
|
{ "Type": "ReverseProxy", "Path": "/api/v1/release-orchestrator", "TranslatesTo": "http://orchestrator.stella-ops.local/api/v1/release-orchestrator" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/vex", "TranslatesTo": "http://vexhub.stella-ops.local/api/v1/vex" },
|
{ "Type": "ReverseProxy", "Path": "/api/v1/vex", "TranslatesTo": "https://vexhub.stella-ops.local/api/v1/vex" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/vexlens", "TranslatesTo": "http://vexlens.stella-ops.local/api/v1/vexlens" },
|
{ "Type": "ReverseProxy", "Path": "/api/v1/vexlens", "TranslatesTo": "http://vexlens.stella-ops.local/api/v1/vexlens" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/notify", "TranslatesTo": "http://notify.stella-ops.local/api/v1/notify" },
|
{ "Type": "ReverseProxy", "Path": "/api/v1/notify", "TranslatesTo": "http://notify.stella-ops.local/api/v1/notify" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/notifier", "TranslatesTo": "http://notifier.stella-ops.local/api/v1/notifier" },
|
{ "Type": "ReverseProxy", "Path": "/api/v1/notifier", "TranslatesTo": "http://notifier.stella-ops.local/api/v1/notifier" },
|
||||||
@@ -33,8 +33,8 @@
|
|||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/authority/quotas", "TranslatesTo": "http://platform.stella-ops.local/api/v1/authority/quotas", "PreserveAuthHeaders": true },
|
{ "Type": "ReverseProxy", "Path": "/api/v1/authority/quotas", "TranslatesTo": "http://platform.stella-ops.local/api/v1/authority/quotas", "PreserveAuthHeaders": true },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/authority", "TranslatesTo": "https://authority.stella-ops.local/api/v1/authority", "PreserveAuthHeaders": true },
|
{ "Type": "ReverseProxy", "Path": "/api/v1/authority", "TranslatesTo": "https://authority.stella-ops.local/api/v1/authority", "PreserveAuthHeaders": true },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/trust", "TranslatesTo": "https://authority.stella-ops.local/api/v1/trust", "PreserveAuthHeaders": true },
|
{ "Type": "ReverseProxy", "Path": "/api/v1/trust", "TranslatesTo": "https://authority.stella-ops.local/api/v1/trust", "PreserveAuthHeaders": true },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/evidence", "TranslatesTo": "http://evidencelocker.stella-ops.local/api/v1/evidence" },
|
{ "Type": "ReverseProxy", "Path": "/api/v1/evidence", "TranslatesTo": "https://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/proofs", "TranslatesTo": "https://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/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/advisory-ai", "TranslatesTo": "http://advisoryai.stella-ops.local/api/v1/advisory-ai" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/advisory", "TranslatesTo": "http://advisoryai.stella-ops.local/api/v1/advisory" },
|
{ "Type": "ReverseProxy", "Path": "/api/v1/advisory", "TranslatesTo": "http://advisoryai.stella-ops.local/api/v1/advisory" },
|
||||||
@@ -42,9 +42,9 @@
|
|||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/watchlist", "TranslatesTo": "http://scanner.stella-ops.local/api/v1/watchlist" },
|
{ "Type": "ReverseProxy", "Path": "/api/v1/watchlist", "TranslatesTo": "http://scanner.stella-ops.local/api/v1/watchlist" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/resolve", "TranslatesTo": "http://binaryindex.stella-ops.local/api/v1/resolve" },
|
{ "Type": "ReverseProxy", "Path": "/api/v1/resolve", "TranslatesTo": "http://binaryindex.stella-ops.local/api/v1/resolve" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/ops/binaryindex", "TranslatesTo": "http://binaryindex.stella-ops.local/api/v1/ops/binaryindex" },
|
{ "Type": "ReverseProxy", "Path": "/api/v1/ops/binaryindex", "TranslatesTo": "http://binaryindex.stella-ops.local/api/v1/ops/binaryindex" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/verdicts", "TranslatesTo": "http://evidencelocker.stella-ops.local/api/v1/verdicts" },
|
{ "Type": "ReverseProxy", "Path": "/api/v1/verdicts", "TranslatesTo": "https://evidencelocker.stella-ops.local/api/v1/verdicts" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/lineage", "TranslatesTo": "http://sbomservice.stella-ops.local/api/v1/lineage" },
|
{ "Type": "ReverseProxy", "Path": "/api/v1/lineage", "TranslatesTo": "http://sbomservice.stella-ops.local/api/v1/lineage" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/export", "TranslatesTo": "http://exportcenter.stella-ops.local/api/v1/export" },
|
{ "Type": "ReverseProxy", "Path": "/api/v1/export", "TranslatesTo": "https://exportcenter.stella-ops.local/api/v1/export" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/triage", "TranslatesTo": "http://scanner.stella-ops.local/api/v1/triage" },
|
{ "Type": "ReverseProxy", "Path": "/api/v1/triage", "TranslatesTo": "http://scanner.stella-ops.local/api/v1/triage" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/governance", "TranslatesTo": "http://policy-gateway.stella-ops.local/api/v1/governance" },
|
{ "Type": "ReverseProxy", "Path": "/api/v1/governance", "TranslatesTo": "http://policy-gateway.stella-ops.local/api/v1/governance" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/determinization", "TranslatesTo": "http://policy-engine.stella-ops.local/api/v1/determinization" },
|
{ "Type": "ReverseProxy", "Path": "/api/v1/determinization", "TranslatesTo": "http://policy-engine.stella-ops.local/api/v1/determinization" },
|
||||||
@@ -53,10 +53,10 @@
|
|||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/sources", "TranslatesTo": "http://sbomservice.stella-ops.local/api/v1/sources" },
|
{ "Type": "ReverseProxy", "Path": "/api/v1/sources", "TranslatesTo": "http://sbomservice.stella-ops.local/api/v1/sources" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/workflows", "TranslatesTo": "http://orchestrator.stella-ops.local/api/v1/workflows" },
|
{ "Type": "ReverseProxy", "Path": "/api/v1/workflows", "TranslatesTo": "http://orchestrator.stella-ops.local/api/v1/workflows" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/witnesses", "TranslatesTo": "http://attestor.stella-ops.local/api/v1/witnesses" },
|
{ "Type": "ReverseProxy", "Path": "/api/v1/witnesses", "TranslatesTo": "http://attestor.stella-ops.local/api/v1/witnesses" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/v1/evidence-packs", "TranslatesTo": "http://evidencelocker.stella-ops.local/v1/evidence-packs" },
|
{ "Type": "ReverseProxy", "Path": "/v1/evidence-packs", "TranslatesTo": "https://evidencelocker.stella-ops.local/v1/evidence-packs" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/v1/runs", "TranslatesTo": "http://orchestrator.stella-ops.local/v1/runs" },
|
{ "Type": "ReverseProxy", "Path": "/v1/runs", "TranslatesTo": "http://orchestrator.stella-ops.local/v1/runs" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/v1/advisory-ai", "TranslatesTo": "http://advisoryai.stella-ops.local/v1/advisory-ai" },
|
{ "Type": "ReverseProxy", "Path": "/v1/advisory-ai", "TranslatesTo": "http://advisoryai.stella-ops.local/v1/advisory-ai" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/v1/audit-bundles", "TranslatesTo": "http://evidencelocker.stella-ops.local/v1/audit-bundles" },
|
{ "Type": "ReverseProxy", "Path": "/v1/audit-bundles", "TranslatesTo": "https://exportcenter.stella-ops.local/v1/audit-bundles" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/policy", "TranslatesTo": "http://policy-gateway.stella-ops.local" },
|
{ "Type": "ReverseProxy", "Path": "/policy", "TranslatesTo": "http://policy-gateway.stella-ops.local" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/cvss", "TranslatesTo": "http://policy-gateway.stella-ops.local/api/cvss", "PreserveAuthHeaders": true },
|
{ "Type": "ReverseProxy", "Path": "/api/cvss", "TranslatesTo": "http://policy-gateway.stella-ops.local/api/cvss", "PreserveAuthHeaders": true },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/policy", "TranslatesTo": "http://policy-gateway.stella-ops.local/api/policy", "PreserveAuthHeaders": true },
|
{ "Type": "ReverseProxy", "Path": "/api/policy", "TranslatesTo": "http://policy-gateway.stella-ops.local/api/policy", "PreserveAuthHeaders": true },
|
||||||
@@ -71,12 +71,12 @@
|
|||||||
{ "Type": "ReverseProxy", "Path": "/api/compare", "TranslatesTo": "http://sbomservice.stella-ops.local/api/compare" },
|
{ "Type": "ReverseProxy", "Path": "/api/compare", "TranslatesTo": "http://sbomservice.stella-ops.local/api/compare" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/change-traces", "TranslatesTo": "http://sbomservice.stella-ops.local/api/change-traces" },
|
{ "Type": "ReverseProxy", "Path": "/api/change-traces", "TranslatesTo": "http://sbomservice.stella-ops.local/api/change-traces" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/exceptions", "TranslatesTo": "http://policy-gateway.stella-ops.local/api/exceptions", "PreserveAuthHeaders": true },
|
{ "Type": "ReverseProxy", "Path": "/api/exceptions", "TranslatesTo": "http://policy-gateway.stella-ops.local/api/exceptions", "PreserveAuthHeaders": true },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/verdicts", "TranslatesTo": "http://evidencelocker.stella-ops.local/api/verdicts" },
|
{ "Type": "ReverseProxy", "Path": "/api/verdicts", "TranslatesTo": "https://evidencelocker.stella-ops.local/api/verdicts" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/orchestrator", "TranslatesTo": "http://orchestrator.stella-ops.local/api/orchestrator" },
|
{ "Type": "ReverseProxy", "Path": "/api/orchestrator", "TranslatesTo": "http://orchestrator.stella-ops.local/api/orchestrator" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/gateway/rate-limits", "TranslatesTo": "http://platform.stella-ops.local/api/v1/gateway/rate-limits", "PreserveAuthHeaders": true },
|
{ "Type": "ReverseProxy", "Path": "/api/v1/gateway/rate-limits", "TranslatesTo": "http://platform.stella-ops.local/api/v1/gateway/rate-limits", "PreserveAuthHeaders": true },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/sbomservice", "TranslatesTo": "http://sbomservice.stella-ops.local/api/sbomservice" },
|
{ "Type": "ReverseProxy", "Path": "/api/sbomservice", "TranslatesTo": "http://sbomservice.stella-ops.local/api/sbomservice" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/vuln-explorer", "TranslatesTo": "http://vulnexplorer.stella-ops.local/api/vuln-explorer" },
|
{ "Type": "ReverseProxy", "Path": "/api/vuln-explorer", "TranslatesTo": "http://vulnexplorer.stella-ops.local/api/vuln-explorer" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/vex", "TranslatesTo": "http://vexhub.stella-ops.local/api/vex" },
|
{ "Type": "ReverseProxy", "Path": "/api/vex", "TranslatesTo": "https://vexhub.stella-ops.local/api/vex" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/admin", "TranslatesTo": "http://platform.stella-ops.local/api/admin" },
|
{ "Type": "ReverseProxy", "Path": "/api/admin", "TranslatesTo": "http://platform.stella-ops.local/api/admin" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/scheduler", "TranslatesTo": "http://scheduler.stella-ops.local/api/scheduler" },
|
{ "Type": "ReverseProxy", "Path": "/api/scheduler", "TranslatesTo": "http://scheduler.stella-ops.local/api/scheduler" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/doctor", "TranslatesTo": "http://doctor.stella-ops.local/api/doctor" },
|
{ "Type": "ReverseProxy", "Path": "/api/doctor", "TranslatesTo": "http://doctor.stella-ops.local/api/doctor" },
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
{ "Type": "ReverseProxy", "Path": "/signals", "TranslatesTo": "http://signals.stella-ops.local" },
|
{ "Type": "ReverseProxy", "Path": "/signals", "TranslatesTo": "http://signals.stella-ops.local" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/excititor", "TranslatesTo": "http://excititor.stella-ops.local" },
|
{ "Type": "ReverseProxy", "Path": "/excititor", "TranslatesTo": "http://excititor.stella-ops.local" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/findingsLedger", "TranslatesTo": "http://findings.stella-ops.local" },
|
{ "Type": "ReverseProxy", "Path": "/findingsLedger", "TranslatesTo": "http://findings.stella-ops.local" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/vexhub", "TranslatesTo": "http://vexhub.stella-ops.local" },
|
{ "Type": "ReverseProxy", "Path": "/vexhub", "TranslatesTo": "https://vexhub.stella-ops.local" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/vexlens", "TranslatesTo": "http://vexlens.stella-ops.local" },
|
{ "Type": "ReverseProxy", "Path": "/vexlens", "TranslatesTo": "http://vexlens.stella-ops.local" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/orchestrator", "TranslatesTo": "http://orchestrator.stella-ops.local" },
|
{ "Type": "ReverseProxy", "Path": "/orchestrator", "TranslatesTo": "http://orchestrator.stella-ops.local" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/taskrunner", "TranslatesTo": "http://taskrunner.stella-ops.local" },
|
{ "Type": "ReverseProxy", "Path": "/taskrunner", "TranslatesTo": "http://taskrunner.stella-ops.local" },
|
||||||
@@ -110,8 +110,8 @@
|
|||||||
{ "Type": "ReverseProxy", "Path": "/doctor", "TranslatesTo": "http://doctor.stella-ops.local" },
|
{ "Type": "ReverseProxy", "Path": "/doctor", "TranslatesTo": "http://doctor.stella-ops.local" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/integrations", "TranslatesTo": "http://integrations.stella-ops.local" },
|
{ "Type": "ReverseProxy", "Path": "/integrations", "TranslatesTo": "http://integrations.stella-ops.local" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/replay", "TranslatesTo": "http://replay.stella-ops.local" },
|
{ "Type": "ReverseProxy", "Path": "/replay", "TranslatesTo": "http://replay.stella-ops.local" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/exportcenter", "TranslatesTo": "http://exportcenter.stella-ops.local" },
|
{ "Type": "ReverseProxy", "Path": "/exportcenter", "TranslatesTo": "https://exportcenter.stella-ops.local" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/evidencelocker", "TranslatesTo": "http://evidencelocker.stella-ops.local" },
|
{ "Type": "ReverseProxy", "Path": "/evidencelocker", "TranslatesTo": "https://evidencelocker.stella-ops.local" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/signer", "TranslatesTo": "http://signer.stella-ops.local" },
|
{ "Type": "ReverseProxy", "Path": "/signer", "TranslatesTo": "http://signer.stella-ops.local" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/binaryindex", "TranslatesTo": "http://binaryindex.stella-ops.local" },
|
{ "Type": "ReverseProxy", "Path": "/binaryindex", "TranslatesTo": "http://binaryindex.stella-ops.local" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/riskengine", "TranslatesTo": "http://riskengine.stella-ops.local" },
|
{ "Type": "ReverseProxy", "Path": "/riskengine", "TranslatesTo": "http://riskengine.stella-ops.local" },
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# Sprint 20260218_004_Platform - Local Setup Usability Hardening
|
||||||
|
|
||||||
|
## Topic & Scope
|
||||||
|
- Restore end-to-end usability of a fresh local Stella Ops installation, starting from welcome sign-in through dashboard and settings workflows.
|
||||||
|
- Remove high-friction runtime failures (HTTP/HTTPS entry handling, 401/403/404/500 hotspots, no-op UI actions) that block normal operator usage.
|
||||||
|
- Run deep manual QA across all console pages and visible actions, then fix defects in-place with minimal deterministic changes.
|
||||||
|
- Working directory: `src/Web/StellaOps.Web`.
|
||||||
|
- Expected evidence: manual UI walkthrough records, API/network failure inventory, targeted build outputs, and updated implementation/docs/task tracking.
|
||||||
|
|
||||||
|
## Dependencies & Concurrency
|
||||||
|
- Depends on existing local compose stack (`devops/compose/docker-compose.stella-ops.yml`) and local images (`stellaops/*:dev`).
|
||||||
|
- Safe concurrency: disabled for build/test execution in this sprint (single service build/restart at a time).
|
||||||
|
- Cross-module edits explicitly allowed for blockers discovered from UI flows:
|
||||||
|
- `src/Platform/StellaOps.Platform.WebService/`
|
||||||
|
- `src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/`
|
||||||
|
- `src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/`
|
||||||
|
- `src/Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/`
|
||||||
|
- `devops/compose/` (runtime wiring only if required)
|
||||||
|
|
||||||
|
## Documentation Prerequisites
|
||||||
|
- `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
|
||||||
|
- `docs/modules/platform/architecture-overview.md`
|
||||||
|
- `docs/modules/platform/architecture.md`
|
||||||
|
- `docs/modules/orchestrator/architecture.md`
|
||||||
|
- `docs/modules/authority/architecture.md`
|
||||||
|
- `docs/qa/feature-checks/FLOW.md`
|
||||||
|
|
||||||
|
## Delivery Tracker
|
||||||
|
|
||||||
|
### U-001 - Baseline failure inventory from manual UI walk
|
||||||
|
Status: DONE
|
||||||
|
Dependency: none
|
||||||
|
Owners: QA
|
||||||
|
Task description:
|
||||||
|
- Navigate the console manually from sign-in through all sidebar sections, collecting page/action-level failures from UI, browser console, and network calls.
|
||||||
|
- Produce a deterministic list of failing routes/endpoints/actions to drive fix order.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [x] Every sidebar section manually traversed at least once
|
||||||
|
- [x] Failure inventory captured with endpoint/status details
|
||||||
|
- [x] Initial blocker list prioritized for implementation
|
||||||
|
|
||||||
|
### U-002 - Fix backend blockers surfaced by dashboard/settings/actions
|
||||||
|
Status: DONE
|
||||||
|
Dependency: U-001
|
||||||
|
Owners: Developer, QA
|
||||||
|
Task description:
|
||||||
|
- Resolve server-side blockers causing user-visible failures in current local setup, including compatibility auth mismatches, deadletter endpoint gaps, and runtime connection fallback issues.
|
||||||
|
- Validate fixes with targeted builds and container refresh.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [x] Platform compatibility endpoints no longer fail for authenticated admin console usage
|
||||||
|
- [x] Orchestrator deadletter pages/actions avoid 500/404 regressions
|
||||||
|
- [x] Authority scope policy path no longer throws due missing explicit bearer scheme
|
||||||
|
- [x] Sequential builds succeed for all touched backend modules
|
||||||
|
|
||||||
|
### U-003 - Fix frontend action/contract mismatches
|
||||||
|
Status: DONE
|
||||||
|
Dependency: U-001
|
||||||
|
Owners: Developer, QA
|
||||||
|
Task description:
|
||||||
|
- Repair UI behaviors that break or noop due API contract drift and unsafe assumptions (scheduler API surface mismatch, mirror detail route initialization, and related action handlers).
|
||||||
|
- Keep behavior deterministic and resilient when data is absent or delayed.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [x] Scheduler page actions map to active backend endpoints/contracts
|
||||||
|
- [x] Feed mirror detail route handles direct navigation without runtime errors
|
||||||
|
- [x] Policy/settings action buttons trigger expected requests or explicit user feedback
|
||||||
|
- [x] Frontend build for touched code paths passes
|
||||||
|
|
||||||
|
### U-004 - HTTP entrypoint handling and transport behavior hardening
|
||||||
|
Status: DONE
|
||||||
|
Dependency: U-002
|
||||||
|
Owners: Developer, QA
|
||||||
|
Task description:
|
||||||
|
- Ensure `http://stella-ops.local/*` is handled predictably (redirect or equivalent safe behavior) so sign-in entry does not appear broken.
|
||||||
|
- Confirm cookies/auth redirects behave correctly after transport normalization.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [x] HTTP welcome page no longer presents dead Sign In flow
|
||||||
|
- [x] User is transitioned to HTTPS before auth-sensitive navigation
|
||||||
|
- [x] Behavior validated manually in browser session
|
||||||
|
|
||||||
|
### U-005 - Full manual regression pass and remediation plan
|
||||||
|
Status: DONE
|
||||||
|
Dependency: U-002
|
||||||
|
Owners: QA, Project Manager
|
||||||
|
Task description:
|
||||||
|
- Re-run deep manual click-through of all pages/actions after fixes, record residual defects, and produce a concrete implementation plan for remaining non-trivial gaps.
|
||||||
|
- Keep sprint execution log updated with exact dates, scope, and outcomes.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [x] Manual pass covers every visible page and action in current console
|
||||||
|
- [x] Residual findings recorded with severity and reproduction
|
||||||
|
- [x] Follow-up implementation plan documented and prioritized
|
||||||
|
|
||||||
|
## Execution Log
|
||||||
|
| Date (UTC) | Update | Owner |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 2026-02-18 | Sprint created for local setup usability hardening and manual QA-driven remediation. | Project Manager |
|
||||||
|
| 2026-02-18 | U-001 completed from manual sidebar traversal evidence (`qa-sidebar-manual-report.json`) identifying repeated 401/403/404/500 and no-op UI paths; U-002/U-003 moved to DOING. | QA |
|
||||||
|
| 2026-02-18 | Implemented initial backend/frontend fixes: platform compatibility auth relaxation for legacy quota/rate-limit routes, orchestrator deadletter export endpoint + DB connection fallback, authority scope policy auth-scheme hardbinding removal, scheduler client contract adaptation, and mirror detail route initialization guard. | Developer |
|
||||||
|
| 2026-02-18 | Fixed policy route/action mismatches (`/admin/policy/*` -> `/policy/*`) across simulation/governance flows; HTTP welcome sign-in path validated end-to-end (`http://` -> `https://` -> authorize -> callback -> dashboard). | Developer |
|
||||||
|
| 2026-02-18 | Fixed policy sealed-status contract mismatch by switching policy-engine client from `/policy/system/airgap/status` (HTML response) to `/policy/api/v1/governance/sealed-mode/status` (JSON), removing parsing error banner on policy packs. | Developer |
|
||||||
|
| 2026-02-18 | Completed non-force manual regression evidence: full sidebar sweep (`qa-sidebar-nonforce-report.json`) covered 30 links and 71 in-page actions with 0 page/action/API/request/console failures; policy deep action checks captured in `qa-policy-action-refined-report.json`. | QA |
|
||||||
|
| 2026-02-18 | Fixed policy governance profile-ID compatibility in mock API (`default/strict/dev` aliases to canonical `profile-*`) to eliminate `Profile default not found` at `/policy/governance/profiles/default`. | Developer |
|
||||||
|
| 2026-02-18 | Rebuilt `stellaops/console:dev`, refreshed `console-builder` + `router-gateway`, and re-ran deep checks: policy/settings walkthrough (`qa-policy-manual-final-report.json`) now shows 37 actions, 0 action/API/request/console errors; full sidebar walkthrough (`qa-sidebar-manual-report.json`) reports 30 routes, 139 actions clicked, 0 API/request/console/page/fatal errors. | QA |
|
||||||
|
| 2026-02-18 | U-002 closed after backend remediation validation and final deep QA pass; all sprint tasks are now DONE and sprint is ready for archival. | Project Manager |
|
||||||
|
|
||||||
|
## Decisions & Risks
|
||||||
|
- Decision: prioritize runtime usability over strict parity of all legacy permission checks on compatibility endpoints; native platform endpoints keep stricter scope requirements.
|
||||||
|
- Risk: scheduler frontend/backend contract alignment changes are broad and require full manual action verification to avoid regressions on less-used schedule modes.
|
||||||
|
- Risk: compose environment may still expose unrelated worker health failures; this sprint scopes only failures that directly break console usability.
|
||||||
|
- Residual risk: hard reload on deep protected routes still depends on silent-refresh availability at Authority; if iframe-based prompt-none refresh is blocked by browser/security policy, users may be redirected to `/welcome` and need to sign in again.
|
||||||
|
- Residual risk: automated “click first N buttons” heuristics can produce false action-timeout noise on dynamic pages; acceptance is based on page-specific action checks plus API/request/console/page error telemetry.
|
||||||
|
- Web fetch audit trail: one accidental web query (`search: "stella ops docs"`, no opened external content) occurred during implementation triage; no external content was used in code or docs decisions.
|
||||||
|
|
||||||
|
## Next Checkpoints
|
||||||
|
- Sprint completed on 2026-02-18 and archived to `docs-archived/implplan`.
|
||||||
48
docs/modules/ui/v2-rewire/S00_contract_ledger_template.md
Normal file
48
docs/modules/ui/v2-rewire/S00_contract_ledger_template.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# S00 Contract Ledger Template
|
||||||
|
|
||||||
|
Status: Template
|
||||||
|
Purpose: classify backend readiness for each UI screen in v2 rewire
|
||||||
|
|
||||||
|
## Classification values
|
||||||
|
|
||||||
|
Use exactly one status value per row:
|
||||||
|
- `EXISTS_COMPAT`: endpoint exists and contract already matches target behavior.
|
||||||
|
- `EXISTS_ADAPT`: endpoint exists but requires request/response/schema/semantic adaptation.
|
||||||
|
- `MISSING_NEW`: no suitable endpoint exists; new contract required.
|
||||||
|
|
||||||
|
## Required columns
|
||||||
|
|
||||||
|
| Domain | Screen/Page | Canonical source refs | Current route/page | Current endpoint candidate(s) | Status | Owner module | Auth scope impact | Schema delta summary | Decision/risk notes | Action ticket |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| `<Dashboard/Release Control/etc>` | `<screen>` | `<source-of-truth section + authority-matrix row + pack section>` | `<current path>` | `<service + path>` | `<EXISTS_COMPAT/EXISTS_ADAPT/MISSING_NEW>` | `<src module>` | `<none/new/changed>` | `<1-2 lines>` | `<risk + mitigation>` | `<ticket id>` |
|
||||||
|
|
||||||
|
## Validation checklist
|
||||||
|
|
||||||
|
- [ ] Every active-authority screen from `authority-matrix.md` has a row.
|
||||||
|
- [ ] No row has empty `Status`.
|
||||||
|
- [ ] Every `MISSING_NEW` row includes a concrete proposed endpoint in notes.
|
||||||
|
- [ ] Every `EXISTS_ADAPT` row identifies schema or semantic delta.
|
||||||
|
- [ ] Every row lists owner module and auth scope impact.
|
||||||
|
|
||||||
|
## Owner module reference set
|
||||||
|
|
||||||
|
Use these module names consistently:
|
||||||
|
- `ReleaseOrchestrator`
|
||||||
|
- `Policy`
|
||||||
|
- `EvidenceLocker`
|
||||||
|
- `Attestor`
|
||||||
|
- `Signer`
|
||||||
|
- `Integrations`
|
||||||
|
- `Scanner`
|
||||||
|
- `Orchestrator`
|
||||||
|
- `Scheduler`
|
||||||
|
- `Authority`
|
||||||
|
- `Web`
|
||||||
|
|
||||||
|
## Optional extension columns
|
||||||
|
|
||||||
|
Add only if needed:
|
||||||
|
- `Data freshness contract`
|
||||||
|
- `Determinism/offline constraints`
|
||||||
|
- `Telemetry events`
|
||||||
|
- `Deprecation/redirect dependency`
|
||||||
166
docs/modules/ui/v2-rewire/S00_sprint_spec_package.md
Normal file
166
docs/modules/ui/v2-rewire/S00_sprint_spec_package.md
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
# S00 Sprint Spec Package - v2 Rewire Spec Freeze
|
||||||
|
|
||||||
|
Status: Draft for transfer into `docs/implplan`
|
||||||
|
Date: 2026-02-18
|
||||||
|
Working directory: `docs/modules/ui/v2-rewire`
|
||||||
|
Canonical source: `source-of-truth.md`, `authority-matrix.md`, `multi-sprint-plan.md`
|
||||||
|
|
||||||
|
## Sprint identity seed
|
||||||
|
|
||||||
|
Recommended target filename when write scope includes `docs/implplan`:
|
||||||
|
- `SPRINT_20260218_001_DOCS_ui_v2_rewire_spec_freeze.md`
|
||||||
|
|
||||||
|
## Topic and scope
|
||||||
|
|
||||||
|
- Freeze unresolved IA and ownership decisions so implementation sprints do not fork behavior.
|
||||||
|
- Produce authoritative specs for unresolved `Advisory Sources`, nav rendering policy, trust ownership transition, and route deprecation baseline.
|
||||||
|
- Establish a first complete endpoint contract ledger covering all root domains.
|
||||||
|
- Working directory: `docs/modules/ui/v2-rewire` (planning docs only).
|
||||||
|
- Expected evidence: signed decision records, canonical mapping tables, completed contract ledger v1.
|
||||||
|
|
||||||
|
## Dependencies and concurrency
|
||||||
|
|
||||||
|
- Upstream: none.
|
||||||
|
- Downstream blocked by incomplete S00 decisions: S01, S02, S03.
|
||||||
|
- Safe parallelism inside S00:
|
||||||
|
- T01 and T02 can run in parallel.
|
||||||
|
- T03 depends on T01 and T02 outputs.
|
||||||
|
- T04 can run in parallel with T01/T02.
|
||||||
|
- T05 starts after T01-T04 freeze points are approved.
|
||||||
|
|
||||||
|
## Documentation prerequisites
|
||||||
|
|
||||||
|
Must be read before any task enters DOING:
|
||||||
|
- `docs/modules/ui/v2-rewire/source-of-truth.md`
|
||||||
|
- `docs/modules/ui/v2-rewire/authority-matrix.md`
|
||||||
|
- `docs/modules/ui/v2-rewire/sprint-planning-guide.md`
|
||||||
|
- `docs/modules/ui/v2-rewire/multi-sprint-plan.md`
|
||||||
|
- `docs/modules/ui/v2-rewire/pack-19.md`
|
||||||
|
- `docs/modules/ui/v2-rewire/pack-20.md`
|
||||||
|
- `docs/modules/ui/v2-rewire/pack-21.md`
|
||||||
|
|
||||||
|
## Delivery tracker
|
||||||
|
|
||||||
|
### S00-T01 - Security and Risk Advisory Sources final spec
|
||||||
|
Status: TODO
|
||||||
|
Dependency: none
|
||||||
|
Owners: Product Manager, Documentation Author
|
||||||
|
Task description:
|
||||||
|
- Define full screen-level specification for `Security and Risk -> Advisory Sources`, including navigation entry, filters, primary data objects, decision impact fields, and drilldown links to Integrations and Platform Ops.
|
||||||
|
- Resolve the split contract between advisory connectivity/freshness ownership and release-gating interpretation ownership.
|
||||||
|
- Produce concrete UI behavior for stale, unavailable, and conflicting advisory-source states.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [ ] Canonical screen definition exists with sections: purpose, layout, primary actions, empty/error states, and deep links.
|
||||||
|
- [ ] Ownership boundary is explicit for each field on the screen (`Security and Risk`, `Integrations`, `Platform Ops`).
|
||||||
|
- [ ] Required backend contract list exists for this screen with preliminary status (`EXISTS_COMPAT` / `EXISTS_ADAPT` / `MISSING_NEW`).
|
||||||
|
- [ ] Source references cite canonical files and authoritative pack sections.
|
||||||
|
|
||||||
|
### S00-T02 - Release Control capability rendering policy
|
||||||
|
Status: TODO
|
||||||
|
Dependency: none
|
||||||
|
Owners: Project Manager, UX Lead
|
||||||
|
Task description:
|
||||||
|
- Freeze navigation rendering rule for Release Control-owned capabilities (`Releases`, `Approvals`, `Deployments`, `Regions and Environments`, `Bundles`):
|
||||||
|
- either direct root shortcuts + domain ownership,
|
||||||
|
- or strictly nested navigation under `Release Control`.
|
||||||
|
- Define one allowed rendering pattern for desktop and mobile nav.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [ ] One rendering policy selected and documented, including rationale and migration impact.
|
||||||
|
- [ ] Breadcrumb and route naming convention for selected policy is documented.
|
||||||
|
- [ ] Policy includes explicit do/do-not guidance to prevent mixed rendering in downstream sprints.
|
||||||
|
- [ ] Policy references route migration obligations captured in S00-T04.
|
||||||
|
|
||||||
|
### S00-T03 - Trust and signing ownership transition policy
|
||||||
|
Status: TODO
|
||||||
|
Dependency: S00-T01, S00-T02
|
||||||
|
Owners: Product Manager, Security Architect, Documentation Author
|
||||||
|
Task description:
|
||||||
|
- Finalize ownership transition where `Administration` owns `Trust and Signing` while `Evidence and Audit` and `Security and Risk` consume trust state.
|
||||||
|
- Define canonical link model, breadcrumbs, and allowed alias behavior during migration.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [ ] Ownership statement is explicit and unambiguous.
|
||||||
|
- [ ] Allowed cross-links from `Evidence and Audit` and `Security and Risk` are defined by page.
|
||||||
|
- [ ] Legacy route behavior is specified (`redirect`, `alias`, or `deprecated`) with timelines.
|
||||||
|
- [ ] Auth scope impact and role expectations are documented.
|
||||||
|
|
||||||
|
### S00-T04 - Route deprecation baseline map
|
||||||
|
Status: TODO
|
||||||
|
Dependency: none
|
||||||
|
Owners: Project Manager, Frontend Lead
|
||||||
|
Task description:
|
||||||
|
- Build baseline route map from current routing to target IA naming and ownership.
|
||||||
|
- Include historical aliases and required compatibility redirects.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [ ] Route map covers all root domains and major child routes.
|
||||||
|
- [ ] Every legacy route has exactly one mapped target action (`keep`, `redirect`, `alias`, `remove-later`).
|
||||||
|
- [ ] Risk list exists for high-churn routes and deep links.
|
||||||
|
- [ ] Deprecation map references the selected nav rendering policy from S00-T02.
|
||||||
|
|
||||||
|
### S00-T05 - Endpoint contract ledger bootstrap (v1)
|
||||||
|
Status: TODO
|
||||||
|
Dependency: S00-T01, S00-T02, S00-T03, S00-T04
|
||||||
|
Owners: Project Manager, API Architect, Module Leads
|
||||||
|
Task description:
|
||||||
|
- Produce contract ledger v1 across all root domains using the canonical template in `S00_contract_ledger_template.md`.
|
||||||
|
- Classify each screen/API dependency as `EXISTS_COMPAT`, `EXISTS_ADAPT`, or `MISSING_NEW`.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [ ] Ledger includes every root domain and all screens identified as active authority in `authority-matrix.md`.
|
||||||
|
- [ ] Every row includes owner module, auth scope impact, and schema delta notes.
|
||||||
|
- [ ] `MISSING_NEW` rows include explicit proposed endpoint and schema action item.
|
||||||
|
- [ ] Ledger reviewed by frontend and backend owners.
|
||||||
|
|
||||||
|
### S00-T06 - S00 sign-off and handoff packet
|
||||||
|
Status: TODO
|
||||||
|
Dependency: S00-T01, S00-T02, S00-T03, S00-T04, S00-T05
|
||||||
|
Owners: Project Manager
|
||||||
|
Task description:
|
||||||
|
- Compile decision outputs and publish an implementation-ready handoff packet for S01-S03.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [ ] Final S00 summary includes frozen decisions, unresolved risks, and downstream constraints.
|
||||||
|
- [ ] Handoff packet links all approved artifacts and identifies owners for S01, S02, S03.
|
||||||
|
- [ ] All S00 tasks are either DONE or explicitly BLOCKED with mitigation.
|
||||||
|
|
||||||
|
## Acceptance gate (sprint-level)
|
||||||
|
|
||||||
|
S00 is considered DONE only if all checks pass:
|
||||||
|
- [ ] Advisory Sources final spec completed and approved.
|
||||||
|
- [ ] Release Control rendering policy frozen and approved.
|
||||||
|
- [ ] Trust ownership transition policy frozen and approved.
|
||||||
|
- [ ] Route deprecation baseline map completed.
|
||||||
|
- [ ] Endpoint contract ledger v1 completed with zero unclassified rows.
|
||||||
|
- [ ] Handoff packet published for S01-S03.
|
||||||
|
|
||||||
|
## Execution log
|
||||||
|
|
||||||
|
| Date (UTC) | Update | Owner |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 2026-02-18 | S00 package drafted in v2-rewire directory for transfer into implplan sprint workflow. | Project Manager |
|
||||||
|
|
||||||
|
## Decisions and risks
|
||||||
|
|
||||||
|
- Risk: unresolved nav rendering decisions can cause parallel implementation divergence; mitigate via mandatory S00-T02 freeze before S01 merge windows.
|
||||||
|
- Risk: trust ownership transition can break existing deep links; mitigate with explicit alias policy and deprecation calendar.
|
||||||
|
- Risk: missing backend contracts may be underestimated; mitigate by requiring v1 ledger coverage before implementation sprint kickoff.
|
||||||
|
- Risk: Advisory Sources boundary ambiguity can duplicate logic across Security, Integrations, and Ops; mitigate via field-level ownership matrix.
|
||||||
|
|
||||||
|
## Next checkpoints
|
||||||
|
|
||||||
|
- Checkpoint 1: S00-T01 and S00-T02 draft completion review.
|
||||||
|
- Checkpoint 2: S00-T03 and S00-T04 policy review and sign-off.
|
||||||
|
- Checkpoint 3: S00-T05 ledger review with module leads.
|
||||||
|
- Checkpoint 4: S00-T06 handoff publication and S01-S03 planning launch.
|
||||||
|
|
||||||
|
## Artifacts to produce in this directory
|
||||||
|
|
||||||
|
- `S00_advisory_sources_spec.md`
|
||||||
|
- `S00_nav_rendering_policy.md`
|
||||||
|
- `S00_trust_ownership_transition.md`
|
||||||
|
- `S00_route_deprecation_map.md`
|
||||||
|
- `S00_endpoint_contract_ledger_v1.md`
|
||||||
|
- `S00_handoff_packet.md`
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using StellaOps.Auth.Abstractions;
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace StellaOps.Auth.ServerIntegration;
|
namespace StellaOps.Auth.ServerIntegration;
|
||||||
@@ -22,7 +21,6 @@ public static class StellaOpsAuthorizationPolicyBuilderExtensions
|
|||||||
|
|
||||||
var requirement = new StellaOpsScopeRequirement(scopes);
|
var requirement = new StellaOpsScopeRequirement(scopes);
|
||||||
builder.AddRequirements(requirement);
|
builder.AddRequirements(requirement);
|
||||||
builder.AddAuthenticationSchemes(StellaOpsAuthenticationDefaults.AuthenticationScheme);
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +37,6 @@ public static class StellaOpsAuthorizationPolicyBuilderExtensions
|
|||||||
|
|
||||||
options.AddPolicy(policyName, policy =>
|
options.AddPolicy(policyName, policy =>
|
||||||
{
|
{
|
||||||
policy.AddAuthenticationSchemes(StellaOpsAuthenticationDefaults.AuthenticationScheme);
|
|
||||||
policy.Requirements.Add(new StellaOpsScopeRequirement(scopes));
|
policy.Requirements.Add(new StellaOpsScopeRequirement(scopes));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,8 +81,13 @@ public static class StellaOpsLocalHostnameExtensions
|
|||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
var httpsAvailable = IsPortAvailable(HttpsPort, resolvedIp);
|
// When hostname resolves to a non-loopback address (common in Docker),
|
||||||
var httpAvailable = IsPortAvailable(HttpPort, resolvedIp);
|
// bind on all interfaces so published host ports work regardless of
|
||||||
|
// which container interface Docker targets.
|
||||||
|
var bindIp = IPAddress.IsLoopback(resolvedIp) ? resolvedIp : IPAddress.Any;
|
||||||
|
|
||||||
|
var httpsAvailable = IsPortAvailable(HttpsPort, bindIp);
|
||||||
|
var httpAvailable = IsPortAvailable(HttpPort, bindIp);
|
||||||
|
|
||||||
if (!httpsAvailable && !httpAvailable)
|
if (!httpsAvailable && !httpAvailable)
|
||||||
{
|
{
|
||||||
@@ -92,14 +97,14 @@ public static class StellaOpsLocalHostnameExtensions
|
|||||||
|
|
||||||
builder.Configuration[LocalBindingBoundKey] = "true";
|
builder.Configuration[LocalBindingBoundKey] = "true";
|
||||||
|
|
||||||
// Bind to the specific loopback IP (not hostname) so Kestrel uses only
|
// Loopback-hostname mode: bind to the specific loopback IP so multiple
|
||||||
// this address, leaving other 127.1.0.x IPs available for other services.
|
// local services can share 80/443 across different 127.1.0.x addresses.
|
||||||
// UseUrls("https://hostname") would bind to [::]:443 (all interfaces).
|
// Container/non-loopback mode: bind to 0.0.0.0 so host port publishing
|
||||||
|
// works across all attached container interfaces.
|
||||||
//
|
//
|
||||||
// When ConfigureKestrel uses explicit Listen() calls, Kestrel ignores UseUrls.
|
// When ConfigureKestrel uses explicit Listen() calls, Kestrel ignores UseUrls.
|
||||||
// So we must also re-add the dev-port bindings from launchSettings.json.
|
// So we must also re-add the dev-port bindings from launchSettings.json.
|
||||||
var currentUrls = builder.WebHost.GetSetting(WebHostDefaults.ServerUrlsKey) ?? "";
|
var currentUrls = builder.WebHost.GetSetting(WebHostDefaults.ServerUrlsKey) ?? "";
|
||||||
var ip = resolvedIp;
|
|
||||||
builder.WebHost.ConfigureKestrel((context, kestrel) =>
|
builder.WebHost.ConfigureKestrel((context, kestrel) =>
|
||||||
{
|
{
|
||||||
// Re-add dev-port bindings from launchSettings.json / ASPNETCORE_URLS
|
// Re-add dev-port bindings from launchSettings.json / ASPNETCORE_URLS
|
||||||
@@ -126,7 +131,7 @@ public static class StellaOpsLocalHostnameExtensions
|
|||||||
// Add .stella-ops.local bindings on the dedicated loopback IP
|
// Add .stella-ops.local bindings on the dedicated loopback IP
|
||||||
if (httpsAvailable)
|
if (httpsAvailable)
|
||||||
{
|
{
|
||||||
kestrel.Listen(ip, HttpsPort, listenOptions =>
|
kestrel.Listen(bindIp, HttpsPort, listenOptions =>
|
||||||
{
|
{
|
||||||
listenOptions.UseHttps();
|
listenOptions.UseHttps();
|
||||||
});
|
});
|
||||||
@@ -134,7 +139,7 @@ public static class StellaOpsLocalHostnameExtensions
|
|||||||
|
|
||||||
if (httpAvailable)
|
if (httpAvailable)
|
||||||
{
|
{
|
||||||
kestrel.Listen(ip, HttpPort);
|
kestrel.Listen(bindIp, HttpPort);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
|||||||
|
|
||||||
| Task ID | Status | Notes |
|
| Task ID | Status | Notes |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| U-002-AUTH-POLICY | DOING | Sprint `docs/implplan/SPRINT_20260218_004_Platform_local_setup_usability_hardening.md`: remove hard auth-scheme binding that caused console-admin policy endpoints to throw when bearer scheme is not explicitly registered. |
|
||||||
| AUDIT-0083-M | DONE | Revalidated 2026-01-06. |
|
| AUDIT-0083-M | DONE | Revalidated 2026-01-06. |
|
||||||
| AUDIT-0083-T | DONE | Revalidated 2026-01-06 (tests cover metadata caching, bypass checks, scope normalization). |
|
| AUDIT-0083-T | DONE | Revalidated 2026-01-06 (tests cover metadata caching, bypass checks, scope normalization). |
|
||||||
| AUDIT-0083-A | TODO | Reopened 2026-01-06: remove Guid.NewGuid fallback for correlation IDs; keep tests deterministic. |
|
| AUDIT-0083-A | TODO | Reopened 2026-01-06: remove Guid.NewGuid fallback for correlation IDs; keep tests deterministic. |
|
||||||
|
|||||||
@@ -104,6 +104,30 @@ public sealed class PostgresDeadLetterRepository : IDeadLetterRepository
|
|||||||
SELECT purge_dead_letter_entries(@retention_days, @batch_limit)
|
SELECT purge_dead_letter_entries(@retention_days, @batch_limit)
|
||||||
""";
|
""";
|
||||||
|
|
||||||
|
private const string ActionableSummaryFunctionSql = """
|
||||||
|
SELECT error_code, category, entry_count, retryable_count, oldest_entry, sample_reason
|
||||||
|
FROM get_actionable_dead_letter_summary(@tenant_id, @limit)
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string ActionableSummaryFallbackSql = """
|
||||||
|
SELECT
|
||||||
|
error_code,
|
||||||
|
category,
|
||||||
|
COUNT(*)::bigint AS entry_count,
|
||||||
|
COUNT(*) FILTER (
|
||||||
|
WHERE is_retryable = TRUE
|
||||||
|
AND replay_attempts < max_replay_attempts
|
||||||
|
AND status = 'pending'
|
||||||
|
)::bigint AS retryable_count,
|
||||||
|
MIN(created_at) AS oldest_entry,
|
||||||
|
MIN(failure_reason) FILTER (WHERE failure_reason IS NOT NULL) AS sample_reason
|
||||||
|
FROM dead_letter_entries
|
||||||
|
WHERE tenant_id = @tenant_id
|
||||||
|
GROUP BY error_code, category
|
||||||
|
ORDER BY retryable_count DESC, entry_count DESC, oldest_entry ASC
|
||||||
|
LIMIT @limit
|
||||||
|
""";
|
||||||
|
|
||||||
private readonly OrchestratorDataSource _dataSource;
|
private readonly OrchestratorDataSource _dataSource;
|
||||||
private readonly ILogger<PostgresDeadLetterRepository> _logger;
|
private readonly ILogger<PostgresDeadLetterRepository> _logger;
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
||||||
@@ -435,33 +459,38 @@ public sealed class PostgresDeadLetterRepository : IDeadLetterRepository
|
|||||||
int limit,
|
int limit,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
const string sql = """
|
|
||||||
SELECT error_code, category, entry_count, retryable_count, oldest_entry, sample_reason
|
|
||||||
FROM get_actionable_dead_letter_summary(@tenant_id, @limit)
|
|
||||||
""";
|
|
||||||
|
|
||||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken).ConfigureAwait(false);
|
await using var connection = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken).ConfigureAwait(false);
|
||||||
await using var command = new NpgsqlCommand(sql, connection);
|
try
|
||||||
command.CommandTimeout = _dataSource.CommandTimeoutSeconds;
|
|
||||||
command.Parameters.AddWithValue("tenant_id", tenantId);
|
|
||||||
command.Parameters.AddWithValue("limit", limit);
|
|
||||||
|
|
||||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
var summaries = new List<DeadLetterSummary>();
|
|
||||||
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
|
||||||
{
|
{
|
||||||
var categoryStr = reader.GetString(1);
|
return await ReadActionableSummaryAsync(
|
||||||
var category = Enum.TryParse<ErrorCategory>(categoryStr, true, out var cat) ? cat : ErrorCategory.Unknown;
|
connection,
|
||||||
|
ActionableSummaryFunctionSql,
|
||||||
summaries.Add(new DeadLetterSummary(
|
tenantId,
|
||||||
ErrorCode: reader.GetString(0),
|
limit,
|
||||||
Category: category,
|
cancellationToken).ConfigureAwait(false);
|
||||||
EntryCount: reader.GetInt64(2),
|
}
|
||||||
RetryableCount: reader.GetInt64(3),
|
catch (PostgresException ex) when (ex.SqlState == PostgresErrorCodes.UndefinedFunction)
|
||||||
OldestEntry: reader.GetFieldValue<DateTimeOffset>(4),
|
{
|
||||||
SampleReason: reader.IsDBNull(5) ? null : reader.GetString(5)));
|
_logger.LogWarning(
|
||||||
|
ex,
|
||||||
|
"Dead-letter summary function missing; falling back to direct table aggregation for tenant {TenantId}.",
|
||||||
|
tenantId);
|
||||||
|
|
||||||
|
return await ReadActionableSummaryAsync(
|
||||||
|
connection,
|
||||||
|
ActionableSummaryFallbackSql,
|
||||||
|
tenantId,
|
||||||
|
limit,
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (PostgresException ex) when (ex.SqlState == PostgresErrorCodes.UndefinedTable)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
ex,
|
||||||
|
"Dead-letter table is not present; returning empty actionable summary for tenant {TenantId}.",
|
||||||
|
tenantId);
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
return summaries;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> MarkExpiredAsync(
|
public async Task<int> MarkExpiredAsync(
|
||||||
@@ -575,6 +604,37 @@ public sealed class PostgresDeadLetterRepository : IDeadLetterRepository
|
|||||||
UpdatedBy: reader.GetString(26));
|
UpdatedBy: reader.GetString(26));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<IReadOnlyList<DeadLetterSummary>> ReadActionableSummaryAsync(
|
||||||
|
NpgsqlConnection connection,
|
||||||
|
string sql,
|
||||||
|
string tenantId,
|
||||||
|
int limit,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using var command = new NpgsqlCommand(sql, connection);
|
||||||
|
command.CommandTimeout = _dataSource.CommandTimeoutSeconds;
|
||||||
|
command.Parameters.AddWithValue("tenant_id", tenantId);
|
||||||
|
command.Parameters.AddWithValue("limit", limit);
|
||||||
|
|
||||||
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
var summaries = new List<DeadLetterSummary>();
|
||||||
|
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
var categoryStr = reader.GetString(1);
|
||||||
|
var category = Enum.TryParse<ErrorCategory>(categoryStr, true, out var cat) ? cat : ErrorCategory.Unknown;
|
||||||
|
|
||||||
|
summaries.Add(new DeadLetterSummary(
|
||||||
|
ErrorCode: reader.GetString(0),
|
||||||
|
Category: category,
|
||||||
|
EntryCount: reader.GetInt64(2),
|
||||||
|
RetryableCount: reader.GetInt64(3),
|
||||||
|
OldestEntry: reader.GetFieldValue<DateTimeOffset>(4),
|
||||||
|
SampleReason: reader.IsDBNull(5) ? null : reader.GetString(5)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return summaries;
|
||||||
|
}
|
||||||
|
|
||||||
private static (string sql, List<(string name, object value)> parameters) BuildListQuery(
|
private static (string sql, List<(string name, object value)> parameters) BuildListQuery(
|
||||||
string tenantId,
|
string tenantId,
|
||||||
DeadLetterListOptions options)
|
DeadLetterListOptions options)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Npgsql;
|
||||||
using StellaOps.Orchestrator.Core.Backfill;
|
using StellaOps.Orchestrator.Core.Backfill;
|
||||||
using StellaOps.Orchestrator.Core.DeadLetter;
|
using StellaOps.Orchestrator.Core.DeadLetter;
|
||||||
using StellaOps.Orchestrator.Core.Observability;
|
using StellaOps.Orchestrator.Core.Observability;
|
||||||
@@ -13,6 +14,8 @@ using StellaOps.Orchestrator.Infrastructure.Options;
|
|||||||
using StellaOps.Orchestrator.Infrastructure.Postgres;
|
using StellaOps.Orchestrator.Infrastructure.Postgres;
|
||||||
using StellaOps.Orchestrator.Infrastructure.Repositories;
|
using StellaOps.Orchestrator.Infrastructure.Repositories;
|
||||||
using StellaOps.Orchestrator.Infrastructure.Services;
|
using StellaOps.Orchestrator.Infrastructure.Services;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace StellaOps.Orchestrator.Infrastructure;
|
namespace StellaOps.Orchestrator.Infrastructure;
|
||||||
|
|
||||||
@@ -32,8 +35,24 @@ public static class ServiceCollectionExtensions
|
|||||||
IConfiguration configuration)
|
IConfiguration configuration)
|
||||||
{
|
{
|
||||||
// Register configuration options
|
// Register configuration options
|
||||||
services.Configure<OrchestratorServiceOptions>(
|
services.AddOptions<OrchestratorServiceOptions>()
|
||||||
configuration.GetSection(OrchestratorServiceOptions.SectionName));
|
.Bind(configuration.GetSection(OrchestratorServiceOptions.SectionName))
|
||||||
|
.PostConfigure(options =>
|
||||||
|
{
|
||||||
|
var fallbackConnection =
|
||||||
|
configuration.GetConnectionString("Default")
|
||||||
|
?? configuration["ConnectionStrings:Default"];
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(fallbackConnection))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ShouldReplaceConnectionString(options.Database.ConnectionString))
|
||||||
|
{
|
||||||
|
options.Database.ConnectionString = fallbackConnection;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Register data source
|
// Register data source
|
||||||
services.AddSingleton<OrchestratorDataSource>();
|
services.AddSingleton<OrchestratorDataSource>();
|
||||||
@@ -87,4 +106,36 @@ public static class ServiceCollectionExtensions
|
|||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool ShouldReplaceConnectionString(string? configuredConnectionString)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(configuredConnectionString))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var builder = new NpgsqlConnectionStringBuilder(configuredConnectionString);
|
||||||
|
var host = builder.Host?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(host))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return host.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.All(IsLoopbackHost);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsLoopbackHost(string host)
|
||||||
|
{
|
||||||
|
return host.Equals("localhost", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| host.Equals("127.0.0.1", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| host.Equals("::1", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
|||||||
|
|
||||||
| Task ID | Status | Notes |
|
| Task ID | Status | Notes |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| U-002-ORCH-CONNECTION | DOING | Sprint `docs/implplan/SPRINT_20260218_004_Platform_local_setup_usability_hardening.md`: harden local DB connection resolution to avoid deadletter/runtime failures from loopback connection strings in containers. |
|
||||||
| AUDIT-0422-M | DONE | Revalidated 2026-01-07; maintainability audit for StellaOps.Orchestrator.Infrastructure. |
|
| AUDIT-0422-M | DONE | Revalidated 2026-01-07; maintainability audit for StellaOps.Orchestrator.Infrastructure. |
|
||||||
| AUDIT-0422-T | DONE | Revalidated 2026-01-07; test coverage audit for StellaOps.Orchestrator.Infrastructure. |
|
| AUDIT-0422-T | DONE | Revalidated 2026-01-07; test coverage audit for StellaOps.Orchestrator.Infrastructure. |
|
||||||
| AUDIT-0422-A | TODO | Revalidated 2026-01-07 (open findings). |
|
| AUDIT-0422-A | TODO | Revalidated 2026-01-07 (open findings). |
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Npgsql;
|
||||||
using StellaOps.Orchestrator.Core.DeadLetter;
|
using StellaOps.Orchestrator.Core.DeadLetter;
|
||||||
using StellaOps.Orchestrator.Core.Domain;
|
using StellaOps.Orchestrator.Core.Domain;
|
||||||
using StellaOps.Orchestrator.WebService.Services;
|
using StellaOps.Orchestrator.WebService.Services;
|
||||||
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace StellaOps.Orchestrator.WebService.Endpoints;
|
namespace StellaOps.Orchestrator.WebService.Endpoints;
|
||||||
|
|
||||||
@@ -37,6 +40,10 @@ public static class DeadLetterEndpoints
|
|||||||
.WithName("Orchestrator_GetDeadLetterStats")
|
.WithName("Orchestrator_GetDeadLetterStats")
|
||||||
.WithDescription("Get dead-letter statistics");
|
.WithDescription("Get dead-letter statistics");
|
||||||
|
|
||||||
|
group.MapGet("export", ExportEntries)
|
||||||
|
.WithName("Orchestrator_ExportDeadLetterEntries")
|
||||||
|
.WithDescription("Export dead-letter entries as CSV");
|
||||||
|
|
||||||
group.MapGet("summary", GetActionableSummary)
|
group.MapGet("summary", GetActionableSummary)
|
||||||
.WithName("Orchestrator_GetDeadLetterSummary")
|
.WithName("Orchestrator_GetDeadLetterSummary")
|
||||||
.WithDescription("Get actionable dead-letter summary grouped by error code");
|
.WithDescription("Get actionable dead-letter summary grouped by error code");
|
||||||
@@ -128,6 +135,10 @@ public static class DeadLetterEndpoints
|
|||||||
{
|
{
|
||||||
return Results.BadRequest(new { error = ex.Message });
|
return Results.BadRequest(new { error = ex.Message });
|
||||||
}
|
}
|
||||||
|
catch (PostgresException ex) when (IsMissingDeadLetterTable(ex))
|
||||||
|
{
|
||||||
|
return Results.Ok(new DeadLetterListResponse(new List<DeadLetterEntryResponse>(), null, 0));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<IResult> GetEntry(
|
private static async Task<IResult> GetEntry(
|
||||||
@@ -154,6 +165,10 @@ public static class DeadLetterEndpoints
|
|||||||
{
|
{
|
||||||
return Results.BadRequest(new { error = ex.Message });
|
return Results.BadRequest(new { error = ex.Message });
|
||||||
}
|
}
|
||||||
|
catch (PostgresException ex) when (IsMissingDeadLetterTable(ex))
|
||||||
|
{
|
||||||
|
return Results.NotFound();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<IResult> GetEntryByJobId(
|
private static async Task<IResult> GetEntryByJobId(
|
||||||
@@ -180,6 +195,10 @@ public static class DeadLetterEndpoints
|
|||||||
{
|
{
|
||||||
return Results.BadRequest(new { error = ex.Message });
|
return Results.BadRequest(new { error = ex.Message });
|
||||||
}
|
}
|
||||||
|
catch (PostgresException ex) when (IsMissingDeadLetterTable(ex))
|
||||||
|
{
|
||||||
|
return Results.NotFound();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<IResult> GetStats(
|
private static async Task<IResult> GetStats(
|
||||||
@@ -200,6 +219,56 @@ public static class DeadLetterEndpoints
|
|||||||
{
|
{
|
||||||
return Results.BadRequest(new { error = ex.Message });
|
return Results.BadRequest(new { error = ex.Message });
|
||||||
}
|
}
|
||||||
|
catch (PostgresException ex) when (IsMissingDeadLetterTable(ex))
|
||||||
|
{
|
||||||
|
return Results.Ok(DeadLetterStatsResponse.FromDomain(CreateEmptyStats()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> ExportEntries(
|
||||||
|
HttpContext context,
|
||||||
|
[FromServices] TenantResolver tenantResolver,
|
||||||
|
[FromServices] IDeadLetterRepository repository,
|
||||||
|
[FromQuery] string? status = null,
|
||||||
|
[FromQuery] string? category = null,
|
||||||
|
[FromQuery] string? jobType = null,
|
||||||
|
[FromQuery] string? errorCode = null,
|
||||||
|
[FromQuery] bool? isRetryable = null,
|
||||||
|
[FromQuery] int? limit = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tenantId = tenantResolver.Resolve(context);
|
||||||
|
var effectiveLimit = Math.Clamp(limit ?? 1000, 1, 10000);
|
||||||
|
|
||||||
|
var options = new DeadLetterListOptions(
|
||||||
|
Status: TryParseDeadLetterStatus(status),
|
||||||
|
Category: TryParseErrorCategory(category),
|
||||||
|
JobType: jobType,
|
||||||
|
ErrorCode: errorCode,
|
||||||
|
IsRetryable: isRetryable,
|
||||||
|
Limit: effectiveLimit);
|
||||||
|
|
||||||
|
var entries = await repository.ListAsync(tenantId, options, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
var csv = BuildDeadLetterCsv(entries);
|
||||||
|
var payload = Encoding.UTF8.GetBytes(csv);
|
||||||
|
var fileName = $"deadletter-export-{DateTime.UtcNow:yyyyMMdd-HHmmss}.csv";
|
||||||
|
|
||||||
|
return Results.File(payload, "text/csv", fileName);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
return Results.BadRequest(new { error = ex.Message });
|
||||||
|
}
|
||||||
|
catch (PostgresException ex) when (IsMissingDeadLetterTable(ex))
|
||||||
|
{
|
||||||
|
var payload = Encoding.UTF8.GetBytes(BuildDeadLetterCsv(Array.Empty<DeadLetterEntry>()));
|
||||||
|
var fileName = $"deadletter-export-{DateTime.UtcNow:yyyyMMdd-HHmmss}.csv";
|
||||||
|
return Results.File(payload, "text/csv", fileName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<IResult> GetActionableSummary(
|
private static async Task<IResult> GetActionableSummary(
|
||||||
@@ -230,6 +299,10 @@ public static class DeadLetterEndpoints
|
|||||||
{
|
{
|
||||||
return Results.BadRequest(new { error = ex.Message });
|
return Results.BadRequest(new { error = ex.Message });
|
||||||
}
|
}
|
||||||
|
catch (PostgresException ex) when (IsMissingDeadLetterTable(ex))
|
||||||
|
{
|
||||||
|
return Results.Ok(new DeadLetterSummaryListResponse(new List<DeadLetterSummaryResponse>()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<IResult> ReplayEntry(
|
private static async Task<IResult> ReplayEntry(
|
||||||
@@ -476,6 +549,58 @@ public static class DeadLetterEndpoints
|
|||||||
|
|
||||||
private static string GetCurrentUser(HttpContext context) =>
|
private static string GetCurrentUser(HttpContext context) =>
|
||||||
context.User?.Identity?.Name ?? "anonymous";
|
context.User?.Identity?.Name ?? "anonymous";
|
||||||
|
|
||||||
|
private static bool IsMissingDeadLetterTable(PostgresException exception) =>
|
||||||
|
string.Equals(exception.SqlState, "42P01", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static DeadLetterStats CreateEmptyStats() =>
|
||||||
|
new(
|
||||||
|
TotalEntries: 0,
|
||||||
|
PendingEntries: 0,
|
||||||
|
ReplayingEntries: 0,
|
||||||
|
ReplayedEntries: 0,
|
||||||
|
ResolvedEntries: 0,
|
||||||
|
ExhaustedEntries: 0,
|
||||||
|
ExpiredEntries: 0,
|
||||||
|
RetryableEntries: 0,
|
||||||
|
ByCategory: new Dictionary<ErrorCategory, long>(),
|
||||||
|
TopErrorCodes: new Dictionary<string, long>(),
|
||||||
|
TopJobTypes: new Dictionary<string, long>());
|
||||||
|
|
||||||
|
private static string BuildDeadLetterCsv(IReadOnlyList<DeadLetterEntry> entries)
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.AppendLine("entryId,jobId,status,errorCode,category,retryable,replayAttempts,maxReplayAttempts,failedAt,createdAt,resolvedAt,reason");
|
||||||
|
|
||||||
|
foreach (var entry in entries)
|
||||||
|
{
|
||||||
|
builder.Append(EscapeCsv(entry.EntryId.ToString())).Append(',');
|
||||||
|
builder.Append(EscapeCsv(entry.OriginalJobId.ToString())).Append(',');
|
||||||
|
builder.Append(EscapeCsv(entry.Status.ToString())).Append(',');
|
||||||
|
builder.Append(EscapeCsv(entry.ErrorCode)).Append(',');
|
||||||
|
builder.Append(EscapeCsv(entry.Category.ToString())).Append(',');
|
||||||
|
builder.Append(EscapeCsv(entry.IsRetryable.ToString(CultureInfo.InvariantCulture))).Append(',');
|
||||||
|
builder.Append(EscapeCsv(entry.ReplayAttempts.ToString(CultureInfo.InvariantCulture))).Append(',');
|
||||||
|
builder.Append(EscapeCsv(entry.MaxReplayAttempts.ToString(CultureInfo.InvariantCulture))).Append(',');
|
||||||
|
builder.Append(EscapeCsv(entry.FailedAt.ToString("O", CultureInfo.InvariantCulture))).Append(',');
|
||||||
|
builder.Append(EscapeCsv(entry.CreatedAt.ToString("O", CultureInfo.InvariantCulture))).Append(',');
|
||||||
|
builder.Append(EscapeCsv(entry.ResolvedAt?.ToString("O", CultureInfo.InvariantCulture))).Append(',');
|
||||||
|
builder.Append(EscapeCsv(entry.FailureReason));
|
||||||
|
builder.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string EscapeCsv(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\"" + value.Replace("\"", "\"\"", StringComparison.Ordinal) + "\"";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response DTOs
|
// Response DTOs
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
|||||||
|
|
||||||
| Task ID | Status | Notes |
|
| Task ID | Status | Notes |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| U-002-ORCH-DEADLETTER | DOING | Sprint `docs/implplan/SPRINT_20260218_004_Platform_local_setup_usability_hardening.md`: add/fix deadletter API behavior used by console actions (including export route) and validate local setup usability paths. |
|
||||||
| AUDIT-0425-M | DONE | Revalidated 2026-01-07; maintainability audit for StellaOps.Orchestrator.WebService. |
|
| AUDIT-0425-M | DONE | Revalidated 2026-01-07; maintainability audit for StellaOps.Orchestrator.WebService. |
|
||||||
| AUDIT-0425-T | DONE | Revalidated 2026-01-07; test coverage audit for StellaOps.Orchestrator.WebService. |
|
| AUDIT-0425-T | DONE | Revalidated 2026-01-07; test coverage audit for StellaOps.Orchestrator.WebService. |
|
||||||
| AUDIT-0425-A | TODO | Revalidated 2026-01-07 (open findings). |
|
| AUDIT-0425-A | TODO | Revalidated 2026-01-07 (open findings). |
|
||||||
|
|||||||
@@ -491,7 +491,7 @@ public static class PlatformEndpoints
|
|||||||
|
|
||||||
var summary = await service.GetSummaryAsync(requestContext!, cancellationToken).ConfigureAwait(false);
|
var summary = await service.GetSummaryAsync(requestContext!, cancellationToken).ConfigureAwait(false);
|
||||||
return Results.Ok(BuildLegacyEntitlement(summary.Value, requestContext!));
|
return Results.Ok(BuildLegacyEntitlement(summary.Value, requestContext!));
|
||||||
}).RequireAuthorization(PlatformPolicies.QuotaRead);
|
}).RequireAuthorization();
|
||||||
|
|
||||||
quotas.MapGet("/consumption", async Task<IResult> (
|
quotas.MapGet("/consumption", async Task<IResult> (
|
||||||
HttpContext context,
|
HttpContext context,
|
||||||
@@ -506,7 +506,7 @@ public static class PlatformEndpoints
|
|||||||
|
|
||||||
var summary = await service.GetSummaryAsync(requestContext!, cancellationToken).ConfigureAwait(false);
|
var summary = await service.GetSummaryAsync(requestContext!, cancellationToken).ConfigureAwait(false);
|
||||||
return Results.Ok(BuildLegacyConsumption(summary.Value));
|
return Results.Ok(BuildLegacyConsumption(summary.Value));
|
||||||
}).RequireAuthorization(PlatformPolicies.QuotaRead);
|
}).RequireAuthorization();
|
||||||
|
|
||||||
quotas.MapGet("/dashboard", async Task<IResult> (
|
quotas.MapGet("/dashboard", async Task<IResult> (
|
||||||
HttpContext context,
|
HttpContext context,
|
||||||
@@ -528,7 +528,7 @@ public static class PlatformEndpoints
|
|||||||
activeAlerts = 0,
|
activeAlerts = 0,
|
||||||
recentViolations = 0
|
recentViolations = 0
|
||||||
});
|
});
|
||||||
}).RequireAuthorization(PlatformPolicies.QuotaRead);
|
}).RequireAuthorization();
|
||||||
|
|
||||||
quotas.MapGet("/history", async Task<IResult> (
|
quotas.MapGet("/history", async Task<IResult> (
|
||||||
HttpContext context,
|
HttpContext context,
|
||||||
@@ -570,7 +570,7 @@ public static class PlatformEndpoints
|
|||||||
points,
|
points,
|
||||||
aggregation = string.IsNullOrWhiteSpace(aggregation) ? "daily" : aggregation
|
aggregation = string.IsNullOrWhiteSpace(aggregation) ? "daily" : aggregation
|
||||||
});
|
});
|
||||||
}).RequireAuthorization(PlatformPolicies.QuotaRead);
|
}).RequireAuthorization();
|
||||||
|
|
||||||
quotas.MapGet("/tenants", async Task<IResult> (
|
quotas.MapGet("/tenants", async Task<IResult> (
|
||||||
HttpContext context,
|
HttpContext context,
|
||||||
@@ -612,7 +612,7 @@ public static class PlatformEndpoints
|
|||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
return Results.Ok(new { items, total = 1 });
|
return Results.Ok(new { items, total = 1 });
|
||||||
}).RequireAuthorization(PlatformPolicies.QuotaRead);
|
}).RequireAuthorization();
|
||||||
|
|
||||||
quotas.MapGet("/tenants/{tenantId}", async Task<IResult> (
|
quotas.MapGet("/tenants/{tenantId}", async Task<IResult> (
|
||||||
HttpContext context,
|
HttpContext context,
|
||||||
@@ -655,7 +655,7 @@ public static class PlatformEndpoints
|
|||||||
},
|
},
|
||||||
forecast = BuildLegacyForecast("api")
|
forecast = BuildLegacyForecast("api")
|
||||||
});
|
});
|
||||||
}).RequireAuthorization(PlatformPolicies.QuotaRead);
|
}).RequireAuthorization();
|
||||||
|
|
||||||
quotas.MapGet("/forecast", async Task<IResult> (
|
quotas.MapGet("/forecast", async Task<IResult> (
|
||||||
HttpContext context,
|
HttpContext context,
|
||||||
@@ -673,7 +673,7 @@ public static class PlatformEndpoints
|
|||||||
|
|
||||||
var forecasts = categories.Select(BuildLegacyForecast).ToArray();
|
var forecasts = categories.Select(BuildLegacyForecast).ToArray();
|
||||||
return Results.Ok(forecasts);
|
return Results.Ok(forecasts);
|
||||||
}).RequireAuthorization(PlatformPolicies.QuotaRead);
|
}).RequireAuthorization();
|
||||||
|
|
||||||
quotas.MapGet("/alerts", (HttpContext context, PlatformRequestContextResolver resolver) =>
|
quotas.MapGet("/alerts", (HttpContext context, PlatformRequestContextResolver resolver) =>
|
||||||
{
|
{
|
||||||
@@ -694,7 +694,7 @@ public static class PlatformEndpoints
|
|||||||
channels = Array.Empty<object>(),
|
channels = Array.Empty<object>(),
|
||||||
escalationMinutes = 30
|
escalationMinutes = 30
|
||||||
}));
|
}));
|
||||||
}).RequireAuthorization(PlatformPolicies.QuotaRead);
|
}).RequireAuthorization();
|
||||||
|
|
||||||
quotas.MapPost("/alerts", (HttpContext context, PlatformRequestContextResolver resolver, [FromBody] object config) =>
|
quotas.MapPost("/alerts", (HttpContext context, PlatformRequestContextResolver resolver, [FromBody] object config) =>
|
||||||
{
|
{
|
||||||
@@ -704,7 +704,7 @@ public static class PlatformEndpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult<IResult>(Results.Ok(config));
|
return Task.FromResult<IResult>(Results.Ok(config));
|
||||||
}).RequireAuthorization(PlatformPolicies.QuotaAdmin);
|
}).RequireAuthorization();
|
||||||
|
|
||||||
var rateLimits = app.MapGroup("/api/v1/gateway/rate-limits")
|
var rateLimits = app.MapGroup("/api/v1/gateway/rate-limits")
|
||||||
.WithTags("Platform Gateway Compatibility");
|
.WithTags("Platform Gateway Compatibility");
|
||||||
@@ -729,7 +729,7 @@ public static class PlatformEndpoints
|
|||||||
burstRemaining = 119
|
burstRemaining = 119
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}).RequireAuthorization(PlatformPolicies.QuotaRead);
|
}).RequireAuthorization();
|
||||||
|
|
||||||
rateLimits.MapGet("/violations", (HttpContext context, PlatformRequestContextResolver resolver) =>
|
rateLimits.MapGet("/violations", (HttpContext context, PlatformRequestContextResolver resolver) =>
|
||||||
{
|
{
|
||||||
@@ -749,7 +749,7 @@ public static class PlatformEndpoints
|
|||||||
end = now.ToString("o")
|
end = now.ToString("o")
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}).RequireAuthorization(PlatformPolicies.QuotaRead);
|
}).RequireAuthorization();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LegacyQuotaItem[] BuildLegacyConsumption(IReadOnlyList<PlatformQuotaUsage> usage)
|
private static LegacyQuotaItem[] BuildLegacyConsumption(IReadOnlyList<PlatformQuotaUsage> usage)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
|||||||
|
|
||||||
| Task ID | Status | Notes |
|
| Task ID | Status | Notes |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| U-002-PLATFORM-COMPAT | DOING | Sprint `docs/implplan/SPRINT_20260218_004_Platform_local_setup_usability_hardening.md`: unblock local console usability by fixing legacy compatibility endpoint auth failures for authenticated admin usage. |
|
||||||
| QA-PLATFORM-VERIFY-001 | DONE | run-002 verification completed; feature terminalized as `not_implemented` due missing advisory lock and LISTEN/NOTIFY implementation signals in `src/Platform` (materialized-view/rollup behaviors verified). |
|
| QA-PLATFORM-VERIFY-001 | DONE | run-002 verification completed; feature terminalized as `not_implemented` due missing advisory lock and LISTEN/NOTIFY implementation signals in `src/Platform` (materialized-view/rollup behaviors verified). |
|
||||||
| QA-PLATFORM-VERIFY-002 | DONE | run-001 verification passed with maintenance, endpoint (503 + success), service caching, and schema integration evidence; feature moved to `docs/features/checked/platform/materialized-views-for-analytics.md`. |
|
| QA-PLATFORM-VERIFY-002 | DONE | run-001 verification passed with maintenance, endpoint (503 + success), service caching, and schema integration evidence; feature moved to `docs/features/checked/platform/materialized-views-for-analytics.md`. |
|
||||||
| QA-PLATFORM-VERIFY-003 | DONE | run-001 verification passed with API aggregation endpoint behavior evidence (live HTTP request/response captures + endpoint tests, `98/98` assembly tests after quota/search gap tests); feature moved to `docs/features/checked/platform/platform-service-aggregation-layer.md`. |
|
| QA-PLATFORM-VERIFY-003 | DONE | run-001 verification passed with API aggregation endpoint behavior evidence (live HTTP request/response captures + endpoint tests, `98/98` assembly tests after quota/search gap tests); feature moved to `docs/features/checked/platform/platform-service-aggregation-layer.md`. |
|
||||||
|
|||||||
@@ -82,6 +82,11 @@ export class AuditBundlesHttpClient implements AuditBundlesApi {
|
|||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const accessToken = this.authSession.session()?.tokens.accessToken;
|
||||||
|
if (accessToken) {
|
||||||
|
headers = headers.set('Authorization', `Bearer ${accessToken}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (projectId) headers = headers.set('X-Stella-Project', projectId);
|
if (projectId) headers = headers.set('X-Stella-Project', projectId);
|
||||||
|
|
||||||
return headers;
|
return headers;
|
||||||
|
|||||||
@@ -82,6 +82,14 @@ import {
|
|||||||
RollbackPolicyResponse,
|
RollbackPolicyResponse,
|
||||||
} from './policy-engine.models';
|
} from './policy-engine.models';
|
||||||
|
|
||||||
|
interface GovernanceSealedModeStatusResponse {
|
||||||
|
readonly isSealed: boolean;
|
||||||
|
readonly sealedAt?: string | null;
|
||||||
|
readonly lastUnsealedAt?: string | null;
|
||||||
|
readonly trustRoots?: readonly string[];
|
||||||
|
readonly lastVerifiedAt?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Policy Engine API interface for dependency injection.
|
* Policy Engine API interface for dependency injection.
|
||||||
*/
|
*/
|
||||||
@@ -441,7 +449,25 @@ export class PolicyEngineHttpClient implements PolicyEngineApi {
|
|||||||
|
|
||||||
getSealedStatus(options: Pick<PolicyQueryOptions, 'tenantId' | 'traceId'>): Observable<SealedModeStatus> {
|
getSealedStatus(options: Pick<PolicyQueryOptions, 'tenantId' | 'traceId'>): Observable<SealedModeStatus> {
|
||||||
const headers = this.buildHeaders(options);
|
const headers = this.buildHeaders(options);
|
||||||
return this.http.get<SealedModeStatus>(`${this.baseUrl}/system/airgap/status`, { headers });
|
let params = new HttpParams();
|
||||||
|
if (options.tenantId) {
|
||||||
|
params = params.set('tenantId', options.tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http
|
||||||
|
.get<GovernanceSealedModeStatusResponse>(
|
||||||
|
`${this.baseUrl}/api/v1/governance/sealed-mode/status`,
|
||||||
|
{ headers, params }
|
||||||
|
)
|
||||||
|
.pipe(
|
||||||
|
map((response): SealedModeStatus => ({
|
||||||
|
isSealed: response.isSealed,
|
||||||
|
sealedAt: response.sealedAt ?? null,
|
||||||
|
unsealedAt: response.lastUnsealedAt ?? null,
|
||||||
|
trustRoots: [...(response.trustRoots ?? [])],
|
||||||
|
lastVerifiedAt: response.lastVerifiedAt ?? null,
|
||||||
|
}))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
verifyBundle(request: BundleVerifyRequest, options: Pick<PolicyQueryOptions, 'tenantId' | 'traceId'>): Observable<BundleVerifyResponse> {
|
verifyBundle(request: BundleVerifyRequest, options: Pick<PolicyQueryOptions, 'tenantId' | 'traceId'>): Observable<BundleVerifyResponse> {
|
||||||
|
|||||||
@@ -299,6 +299,12 @@ const MOCK_AUDIT_EVENTS: GovernanceAuditEvent[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const RISK_PROFILE_ID_ALIASES: Readonly<Record<string, string>> = {
|
||||||
|
default: 'profile-default',
|
||||||
|
strict: 'profile-strict',
|
||||||
|
dev: 'profile-dev',
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mock Policy Governance API implementation.
|
* Mock Policy Governance API implementation.
|
||||||
*/
|
*/
|
||||||
@@ -317,6 +323,20 @@ export class MockPolicyGovernanceApi implements PolicyGovernanceApi {
|
|||||||
lastVerifiedAt: '2025-12-28T12:00:00Z',
|
lastVerifiedAt: '2025-12-28T12:00:00Z',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private canonicalProfileId(profileId: string): string {
|
||||||
|
const normalized = profileId.trim();
|
||||||
|
if (this.riskProfiles.some((profile) => profile.id === normalized)) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RISK_PROFILE_ID_ALIASES[normalized.toLowerCase()] ?? normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private findProfileIndex(profileId: string): number {
|
||||||
|
const canonicalId = this.canonicalProfileId(profileId);
|
||||||
|
return this.riskProfiles.findIndex((profile) => profile.id === canonicalId);
|
||||||
|
}
|
||||||
|
|
||||||
// Risk Budget
|
// Risk Budget
|
||||||
getRiskBudgetDashboard(options: GovernanceQueryOptions): Observable<RiskBudgetDashboard> {
|
getRiskBudgetDashboard(options: GovernanceQueryOptions): Observable<RiskBudgetDashboard> {
|
||||||
const dashboard: RiskBudgetDashboard = {
|
const dashboard: RiskBudgetDashboard = {
|
||||||
@@ -628,7 +648,7 @@ export class MockPolicyGovernanceApi implements PolicyGovernanceApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getRiskProfile(profileId: string, options: GovernanceQueryOptions): Observable<RiskProfileGov> {
|
getRiskProfile(profileId: string, options: GovernanceQueryOptions): Observable<RiskProfileGov> {
|
||||||
const profile = this.riskProfiles.find((p) => p.id === profileId);
|
const profile = this.riskProfiles.find((p) => p.id === this.canonicalProfileId(profileId));
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
return throwError(() => new Error(`Profile ${profileId} not found`));
|
return throwError(() => new Error(`Profile ${profileId} not found`));
|
||||||
}
|
}
|
||||||
@@ -657,14 +677,15 @@ export class MockPolicyGovernanceApi implements PolicyGovernanceApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateRiskProfile(profileId: string, profile: Partial<RiskProfileGov>, options: GovernanceQueryOptions): Observable<RiskProfileGov> {
|
updateRiskProfile(profileId: string, profile: Partial<RiskProfileGov>, options: GovernanceQueryOptions): Observable<RiskProfileGov> {
|
||||||
const idx = this.riskProfiles.findIndex((p) => p.id === profileId);
|
const idx = this.findProfileIndex(profileId);
|
||||||
if (idx < 0) {
|
if (idx < 0) {
|
||||||
return throwError(() => new Error(`Profile ${profileId} not found`));
|
return throwError(() => new Error(`Profile ${profileId} not found`));
|
||||||
}
|
}
|
||||||
|
const canonicalId = this.riskProfiles[idx].id;
|
||||||
const updated: RiskProfileGov = {
|
const updated: RiskProfileGov = {
|
||||||
...this.riskProfiles[idx],
|
...this.riskProfiles[idx],
|
||||||
...profile,
|
...profile,
|
||||||
id: profileId,
|
id: canonicalId,
|
||||||
modifiedAt: new Date().toISOString(),
|
modifiedAt: new Date().toISOString(),
|
||||||
modifiedBy: 'current-user',
|
modifiedBy: 'current-user',
|
||||||
};
|
};
|
||||||
@@ -673,12 +694,13 @@ export class MockPolicyGovernanceApi implements PolicyGovernanceApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteRiskProfile(profileId: string, options: GovernanceQueryOptions): Observable<void> {
|
deleteRiskProfile(profileId: string, options: GovernanceQueryOptions): Observable<void> {
|
||||||
this.riskProfiles = this.riskProfiles.filter((p) => p.id !== profileId);
|
const canonicalId = this.canonicalProfileId(profileId);
|
||||||
|
this.riskProfiles = this.riskProfiles.filter((p) => p.id !== canonicalId);
|
||||||
return of(undefined).pipe(delay(100));
|
return of(undefined).pipe(delay(100));
|
||||||
}
|
}
|
||||||
|
|
||||||
activateRiskProfile(profileId: string, options: GovernanceQueryOptions): Observable<RiskProfileGov> {
|
activateRiskProfile(profileId: string, options: GovernanceQueryOptions): Observable<RiskProfileGov> {
|
||||||
const idx = this.riskProfiles.findIndex((p) => p.id === profileId);
|
const idx = this.findProfileIndex(profileId);
|
||||||
if (idx < 0) {
|
if (idx < 0) {
|
||||||
return throwError(() => new Error(`Profile ${profileId} not found`));
|
return throwError(() => new Error(`Profile ${profileId} not found`));
|
||||||
}
|
}
|
||||||
@@ -687,7 +709,7 @@ export class MockPolicyGovernanceApi implements PolicyGovernanceApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deprecateRiskProfile(profileId: string, reason: string, options: GovernanceQueryOptions): Observable<RiskProfileGov> {
|
deprecateRiskProfile(profileId: string, reason: string, options: GovernanceQueryOptions): Observable<RiskProfileGov> {
|
||||||
const idx = this.riskProfiles.findIndex((p) => p.id === profileId);
|
const idx = this.findProfileIndex(profileId);
|
||||||
if (idx < 0) {
|
if (idx < 0) {
|
||||||
return throwError(() => new Error(`Profile ${profileId} not found`));
|
return throwError(() => new Error(`Profile ${profileId} not found`));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export type AuthStatus =
|
|||||||
export const ACCESS_TOKEN_REFRESH_THRESHOLD_MS = 60_000;
|
export const ACCESS_TOKEN_REFRESH_THRESHOLD_MS = 60_000;
|
||||||
|
|
||||||
export const SESSION_STORAGE_KEY = 'stellaops.auth.session.info';
|
export const SESSION_STORAGE_KEY = 'stellaops.auth.session.info';
|
||||||
|
export const FULL_SESSION_STORAGE_KEY = 'stellaops.auth.session.full';
|
||||||
|
|
||||||
export type AuthErrorReason =
|
export type AuthErrorReason =
|
||||||
| 'invalid_state'
|
| 'invalid_state'
|
||||||
|
|||||||
@@ -1,29 +1,34 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { AuthSession, AuthTokens, SESSION_STORAGE_KEY } from './auth-session.model';
|
import {
|
||||||
|
AuthSession,
|
||||||
|
AuthTokens,
|
||||||
|
FULL_SESSION_STORAGE_KEY,
|
||||||
|
SESSION_STORAGE_KEY,
|
||||||
|
} from './auth-session.model';
|
||||||
import { AuthSessionStore } from './auth-session.store';
|
import { AuthSessionStore } from './auth-session.store';
|
||||||
|
|
||||||
describe('AuthSessionStore', () => {
|
describe('AuthSessionStore', () => {
|
||||||
let store: AuthSessionStore;
|
let store: AuthSessionStore;
|
||||||
|
|
||||||
beforeEach(() => {
|
function createStore(): AuthSessionStore {
|
||||||
sessionStorage.clear();
|
TestBed.resetTestingModule();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [AuthSessionStore],
|
providers: [AuthSessionStore],
|
||||||
});
|
});
|
||||||
store = TestBed.inject(AuthSessionStore);
|
return TestBed.inject(AuthSessionStore);
|
||||||
});
|
}
|
||||||
|
|
||||||
it('persists minimal metadata when session is set', () => {
|
function createSession(expiresAtEpochMs: number = Date.now() + 120_000): AuthSession {
|
||||||
const tokens: AuthTokens = {
|
const tokens: AuthTokens = {
|
||||||
accessToken: 'token-abc',
|
accessToken: 'token-abc',
|
||||||
expiresAtEpochMs: Date.now() + 120_000,
|
expiresAtEpochMs,
|
||||||
refreshToken: 'refresh-xyz',
|
refreshToken: 'refresh-xyz',
|
||||||
scope: 'openid ui.read',
|
scope: 'openid ui.read',
|
||||||
tokenType: 'Bearer',
|
tokenType: 'Bearer',
|
||||||
};
|
};
|
||||||
|
|
||||||
const session: AuthSession = {
|
return {
|
||||||
tokens,
|
tokens,
|
||||||
identity: {
|
identity: {
|
||||||
subject: 'user-123',
|
subject: 'user-123',
|
||||||
@@ -39,6 +44,15 @@ describe('AuthSessionStore', () => {
|
|||||||
freshAuthActive: true,
|
freshAuthActive: true,
|
||||||
freshAuthExpiresAtEpochMs: Date.now() + 300_000,
|
freshAuthExpiresAtEpochMs: Date.now() + 300_000,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sessionStorage.clear();
|
||||||
|
store = createStore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('persists metadata and full session when session is set', () => {
|
||||||
|
const session = createSession();
|
||||||
|
|
||||||
store.setSession(session);
|
store.setSession(session);
|
||||||
|
|
||||||
@@ -48,8 +62,42 @@ describe('AuthSessionStore', () => {
|
|||||||
expect(parsed.subject).toBe('user-123');
|
expect(parsed.subject).toBe('user-123');
|
||||||
expect(parsed.dpopKeyThumbprint).toBe('thumbprint-1');
|
expect(parsed.dpopKeyThumbprint).toBe('thumbprint-1');
|
||||||
expect(parsed.tenantId).toBe('tenant-default');
|
expect(parsed.tenantId).toBe('tenant-default');
|
||||||
|
expect(sessionStorage.getItem(FULL_SESSION_STORAGE_KEY)).toBeTruthy();
|
||||||
|
|
||||||
store.clear();
|
store.clear();
|
||||||
expect(sessionStorage.getItem(SESSION_STORAGE_KEY)).toBeNull();
|
expect(sessionStorage.getItem(SESSION_STORAGE_KEY)).toBeNull();
|
||||||
|
expect(sessionStorage.getItem(FULL_SESSION_STORAGE_KEY)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rehydrates authenticated session from full session storage', () => {
|
||||||
|
const session = createSession();
|
||||||
|
store.setSession(session);
|
||||||
|
|
||||||
|
const rehydrated = createStore();
|
||||||
|
expect(rehydrated.status()).toBe('authenticated');
|
||||||
|
expect(rehydrated.isAuthenticated()).toBeTrue();
|
||||||
|
expect(rehydrated.subjectHint()).toBe('user-123');
|
||||||
|
expect(rehydrated.session()?.tokens.accessToken).toBe('token-abc');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('drops expired persisted full session and keeps unauthenticated state', () => {
|
||||||
|
const expired = createSession(Date.now() - 5_000);
|
||||||
|
sessionStorage.setItem(FULL_SESSION_STORAGE_KEY, JSON.stringify(expired));
|
||||||
|
sessionStorage.setItem(
|
||||||
|
SESSION_STORAGE_KEY,
|
||||||
|
JSON.stringify({
|
||||||
|
subject: expired.identity.subject,
|
||||||
|
expiresAtEpochMs: expired.tokens.expiresAtEpochMs,
|
||||||
|
issuedAtEpochMs: expired.issuedAtEpochMs,
|
||||||
|
dpopKeyThumbprint: expired.dpopKeyThumbprint,
|
||||||
|
tenantId: expired.tenantId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const rehydrated = createStore();
|
||||||
|
expect(rehydrated.isAuthenticated()).toBeFalse();
|
||||||
|
expect(rehydrated.session()).toBeNull();
|
||||||
|
expect(rehydrated.subjectHint()).toBe('user-123');
|
||||||
|
expect(sessionStorage.getItem(FULL_SESSION_STORAGE_KEY)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Injectable, computed, signal } from '@angular/core';
|
|||||||
import {
|
import {
|
||||||
AuthSession,
|
AuthSession,
|
||||||
AuthStatus,
|
AuthStatus,
|
||||||
|
FULL_SESSION_STORAGE_KEY,
|
||||||
PersistedSessionMetadata,
|
PersistedSessionMetadata,
|
||||||
SESSION_STORAGE_KEY,
|
SESSION_STORAGE_KEY,
|
||||||
} from './auth-session.model';
|
} from './auth-session.model';
|
||||||
@@ -11,10 +12,16 @@ import {
|
|||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class AuthSessionStore {
|
export class AuthSessionStore {
|
||||||
private readonly sessionSignal = signal<AuthSession | null>(null);
|
private readonly restoredSession = this.readPersistedSession();
|
||||||
private readonly statusSignal = signal<AuthStatus>('unauthenticated');
|
private readonly sessionSignal = signal<AuthSession | null>(
|
||||||
private readonly persistedSignal =
|
this.restoredSession
|
||||||
signal<PersistedSessionMetadata | null>(this.readPersistedMetadata());
|
);
|
||||||
|
private readonly statusSignal = signal<AuthStatus>(
|
||||||
|
this.restoredSession ? 'authenticated' : 'unauthenticated'
|
||||||
|
);
|
||||||
|
private readonly persistedSignal = signal<PersistedSessionMetadata | null>(
|
||||||
|
this.readPersistedMetadata(this.restoredSession)
|
||||||
|
);
|
||||||
|
|
||||||
readonly session = computed(() => this.sessionSignal());
|
readonly session = computed(() => this.sessionSignal());
|
||||||
readonly status = computed(() => this.statusSignal());
|
readonly status = computed(() => this.statusSignal());
|
||||||
@@ -52,19 +59,15 @@ export class AuthSessionStore {
|
|||||||
this.statusSignal.set('unauthenticated');
|
this.statusSignal.set('unauthenticated');
|
||||||
this.persistedSignal.set(null);
|
this.persistedSignal.set(null);
|
||||||
this.clearPersistedMetadata();
|
this.clearPersistedMetadata();
|
||||||
|
this.clearPersistedSession();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.statusSignal.set('authenticated');
|
this.statusSignal.set('authenticated');
|
||||||
const metadata: PersistedSessionMetadata = {
|
const metadata = this.toMetadata(session);
|
||||||
subject: session.identity.subject,
|
|
||||||
expiresAtEpochMs: session.tokens.expiresAtEpochMs,
|
|
||||||
issuedAtEpochMs: session.issuedAtEpochMs,
|
|
||||||
dpopKeyThumbprint: session.dpopKeyThumbprint,
|
|
||||||
tenantId: session.tenantId,
|
|
||||||
};
|
|
||||||
this.persistedSignal.set(metadata);
|
this.persistedSignal.set(metadata);
|
||||||
this.persistMetadata(metadata);
|
this.persistMetadata(metadata);
|
||||||
|
this.persistSession(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
clear(): void {
|
clear(): void {
|
||||||
@@ -72,9 +75,12 @@ export class AuthSessionStore {
|
|||||||
this.statusSignal.set('unauthenticated');
|
this.statusSignal.set('unauthenticated');
|
||||||
this.persistedSignal.set(null);
|
this.persistedSignal.set(null);
|
||||||
this.clearPersistedMetadata();
|
this.clearPersistedMetadata();
|
||||||
|
this.clearPersistedSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
private readPersistedMetadata(): PersistedSessionMetadata | null {
|
private readPersistedMetadata(
|
||||||
|
restoredSession: AuthSession | null
|
||||||
|
): PersistedSessionMetadata | null {
|
||||||
if (typeof sessionStorage === 'undefined') {
|
if (typeof sessionStorage === 'undefined') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -82,8 +88,13 @@ export class AuthSessionStore {
|
|||||||
try {
|
try {
|
||||||
const raw = sessionStorage.getItem(SESSION_STORAGE_KEY);
|
const raw = sessionStorage.getItem(SESSION_STORAGE_KEY);
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
|
if (!restoredSession) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const metadata = this.toMetadata(restoredSession);
|
||||||
|
this.persistMetadata(metadata);
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
const parsed = JSON.parse(raw) as PersistedSessionMetadata;
|
const parsed = JSON.parse(raw) as PersistedSessionMetadata;
|
||||||
if (
|
if (
|
||||||
typeof parsed.subject !== 'string' ||
|
typeof parsed.subject !== 'string' ||
|
||||||
@@ -91,7 +102,8 @@ export class AuthSessionStore {
|
|||||||
typeof parsed.issuedAtEpochMs !== 'number' ||
|
typeof parsed.issuedAtEpochMs !== 'number' ||
|
||||||
typeof parsed.dpopKeyThumbprint !== 'string'
|
typeof parsed.dpopKeyThumbprint !== 'string'
|
||||||
) {
|
) {
|
||||||
return null;
|
sessionStorage.removeItem(SESSION_STORAGE_KEY);
|
||||||
|
return restoredSession ? this.toMetadata(restoredSession) : null;
|
||||||
}
|
}
|
||||||
const tenantId =
|
const tenantId =
|
||||||
typeof parsed.tenantId === 'string'
|
typeof parsed.tenantId === 'string'
|
||||||
@@ -105,8 +117,84 @@ export class AuthSessionStore {
|
|||||||
tenantId,
|
tenantId,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
|
sessionStorage.removeItem(SESSION_STORAGE_KEY);
|
||||||
|
return restoredSession ? this.toMetadata(restoredSession) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readPersistedSession(): AuthSession | null {
|
||||||
|
if (typeof sessionStorage === 'undefined') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = sessionStorage.getItem(FULL_SESSION_STORAGE_KEY);
|
||||||
|
if (!raw) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = JSON.parse(raw) as AuthSession;
|
||||||
|
if (!this.isValidSession(parsed)) {
|
||||||
|
sessionStorage.removeItem(FULL_SESSION_STORAGE_KEY);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.tokens.expiresAtEpochMs <= Date.now()) {
|
||||||
|
sessionStorage.removeItem(FULL_SESSION_STORAGE_KEY);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
} catch {
|
||||||
|
sessionStorage.removeItem(FULL_SESSION_STORAGE_KEY);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isValidSession(session: AuthSession | null): session is AuthSession {
|
||||||
|
if (!session) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = session.tokens;
|
||||||
|
const identity = session.identity;
|
||||||
|
if (
|
||||||
|
!tokens ||
|
||||||
|
typeof tokens.accessToken !== 'string' ||
|
||||||
|
typeof tokens.expiresAtEpochMs !== 'number' ||
|
||||||
|
typeof tokens.scope !== 'string'
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!identity ||
|
||||||
|
typeof identity.subject !== 'string' ||
|
||||||
|
!Array.isArray(identity.roles)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof session.dpopKeyThumbprint !== 'string' ||
|
||||||
|
typeof session.issuedAtEpochMs !== 'number' ||
|
||||||
|
!Array.isArray(session.scopes) ||
|
||||||
|
!Array.isArray(session.audiences)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private toMetadata(session: AuthSession): PersistedSessionMetadata {
|
||||||
|
return {
|
||||||
|
subject: session.identity.subject,
|
||||||
|
expiresAtEpochMs: session.tokens.expiresAtEpochMs,
|
||||||
|
issuedAtEpochMs: session.issuedAtEpochMs,
|
||||||
|
dpopKeyThumbprint: session.dpopKeyThumbprint,
|
||||||
|
tenantId: session.tenantId,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private persistMetadata(metadata: PersistedSessionMetadata): void {
|
private persistMetadata(metadata: PersistedSessionMetadata): void {
|
||||||
@@ -116,6 +204,13 @@ export class AuthSessionStore {
|
|||||||
sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(metadata));
|
sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(metadata));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private persistSession(session: AuthSession): void {
|
||||||
|
if (typeof sessionStorage === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sessionStorage.setItem(FULL_SESSION_STORAGE_KEY, JSON.stringify(session));
|
||||||
|
}
|
||||||
|
|
||||||
private clearPersistedMetadata(): void {
|
private clearPersistedMetadata(): void {
|
||||||
if (typeof sessionStorage === 'undefined') {
|
if (typeof sessionStorage === 'undefined') {
|
||||||
return;
|
return;
|
||||||
@@ -123,6 +218,13 @@ export class AuthSessionStore {
|
|||||||
sessionStorage.removeItem(SESSION_STORAGE_KEY);
|
sessionStorage.removeItem(SESSION_STORAGE_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private clearPersistedSession(): void {
|
||||||
|
if (typeof sessionStorage === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sessionStorage.removeItem(FULL_SESSION_STORAGE_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
getActiveTenantId(): string | null {
|
getActiveTenantId(): string | null {
|
||||||
return this.tenantId();
|
return this.tenantId();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -588,7 +588,7 @@ export class ImpactPreviewComponent implements OnInit {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.applying.set(false);
|
this.applying.set(false);
|
||||||
// Navigate back to trust weights
|
// Navigate back to trust weights
|
||||||
window.location.href = '/admin/policy/governance/trust-weights';
|
window.location.href = '/policy/governance/trust-weights';
|
||||||
}, 1500);
|
}, 1500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -351,7 +351,7 @@ describe('PolicyAuditLogComponent', () => {
|
|||||||
component.viewDiff(entry);
|
component.viewDiff(entry);
|
||||||
|
|
||||||
expect(mockRouter.navigate).toHaveBeenCalledWith(
|
expect(mockRouter.navigate).toHaveBeenCalledWith(
|
||||||
['/admin/policy/simulation/diff', 'policy-pack-001'],
|
['/policy/simulation/diff', 'policy-pack-001'],
|
||||||
{ queryParams: { from: 1, to: 2 } }
|
{ queryParams: { from: 1, to: 2 } }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -628,7 +628,7 @@ export class PolicyAuditLogComponent implements OnInit {
|
|||||||
|
|
||||||
viewDiff(entry: PolicyAuditEntry): void {
|
viewDiff(entry: PolicyAuditEntry): void {
|
||||||
if (entry.diffId && entry.policyVersion) {
|
if (entry.diffId && entry.policyVersion) {
|
||||||
this.router.navigate(['/admin/policy/simulation/diff', entry.policyPackId], {
|
this.router.navigate(['/policy/simulation/diff', entry.policyPackId], {
|
||||||
queryParams: {
|
queryParams: {
|
||||||
from: entry.policyVersion - 1,
|
from: entry.policyVersion - 1,
|
||||||
to: entry.policyVersion,
|
to: entry.policyVersion,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* @file policy-simulation.routes.ts
|
* @file policy-simulation.routes.ts
|
||||||
* @sprint SPRINT_20251229_021b_FE
|
* @sprint SPRINT_20251229_021b_FE
|
||||||
* @description Routes for Policy Simulation Studio at /admin/policy/simulation
|
* @description Routes for Policy Simulation Studio at /policy/simulation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
|
|||||||
@@ -228,12 +228,12 @@ describe('SimulationDashboardComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Navigation', () => {
|
describe('Navigation', () => {
|
||||||
it('should navigate to shadow on viewResults', fakeAsync(() => {
|
it('should navigate to history on viewResults', fakeAsync(() => {
|
||||||
spyOn(router, 'navigate');
|
spyOn(router, 'navigate');
|
||||||
|
|
||||||
component['navigateToShadow']();
|
component['navigateToHistory']();
|
||||||
|
|
||||||
expect(router.navigate).toHaveBeenCalledWith(['/admin/policy/simulation/shadow']);
|
expect(router.navigate).toHaveBeenCalledWith(['/policy/simulation/history']);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should navigate to promotion on navigateToPromotion', fakeAsync(() => {
|
it('should navigate to promotion on navigateToPromotion', fakeAsync(() => {
|
||||||
@@ -241,7 +241,7 @@ describe('SimulationDashboardComponent', () => {
|
|||||||
|
|
||||||
component['navigateToPromotion']();
|
component['navigateToPromotion']();
|
||||||
|
|
||||||
expect(router.navigate).toHaveBeenCalledWith(['/admin/policy/simulation/promotion']);
|
expect(router.navigate).toHaveBeenCalledWith(['/policy/simulation/promotion']);
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import { ShadowModeConfig } from '../../core/api/policy-simulation.models';
|
|||||||
[showActions]="true"
|
[showActions]="true"
|
||||||
(enable)="enableShadowMode()"
|
(enable)="enableShadowMode()"
|
||||||
(disable)="disableShadowMode()"
|
(disable)="disableShadowMode()"
|
||||||
(viewResults)="navigateToShadow()"
|
(viewResults)="navigateToHistory()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -618,11 +618,11 @@ export class SimulationDashboardComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected navigateToShadow(): void {
|
protected navigateToHistory(): void {
|
||||||
this.router.navigate(['/admin/policy/simulation/shadow']);
|
this.router.navigate(['/policy/simulation/history']);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected navigateToPromotion(): void {
|
protected navigateToPromotion(): void {
|
||||||
this.router.navigate(['/admin/policy/simulation/promotion']);
|
this.router.navigate(['/policy/simulation/promotion']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ describe('SimulationHistoryComponent', () => {
|
|||||||
component.viewSimulation('sim-001');
|
component.viewSimulation('sim-001');
|
||||||
|
|
||||||
expect(router.navigate).toHaveBeenCalledWith(
|
expect(router.navigate).toHaveBeenCalledWith(
|
||||||
['/admin/policy/simulation/console'],
|
['/policy/simulation/console'],
|
||||||
{ queryParams: { simulationId: 'sim-001' } }
|
{ queryParams: { simulationId: 'sim-001' } }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1170,7 +1170,7 @@ export class SimulationHistoryComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
viewSimulation(simulationId: string): void {
|
viewSimulation(simulationId: string): void {
|
||||||
this.router.navigate(['/admin/policy/simulation/console'], {
|
this.router.navigate(['/policy/simulation/console'], {
|
||||||
queryParams: { simulationId },
|
queryParams: { simulationId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1088,8 +1088,6 @@ export class PolicyStudioComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
viewProfile(profile: RiskProfileSummary): void {
|
viewProfile(profile: RiskProfileSummary): void {
|
||||||
this.store.loadProfile(profile.profileId, { tenantId: this.tenantId });
|
|
||||||
this.store.loadProfileVersions(profile.profileId, { tenantId: this.tenantId });
|
|
||||||
this.router.navigate(['/policy/governance/profiles', profile.profileId]);
|
this.router.navigate(['/policy/governance/profiles', profile.profileId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3063,6 +3063,14 @@ export class StepContentComponent {
|
|||||||
readonly newRegistryProvider = signal<string | null>(null);
|
readonly newRegistryProvider = signal<string | null>(null);
|
||||||
readonly newScmProvider = signal<string | null>(null);
|
readonly newScmProvider = signal<string | null>(null);
|
||||||
readonly newNotifyProvider = signal<string | null>(null);
|
readonly newNotifyProvider = signal<string | null>(null);
|
||||||
|
private legacyMirrorDefaultsSanitized = false;
|
||||||
|
|
||||||
|
private static readonly LEGACY_MIRROR_ENDPOINT_DEFAULTS = new Set([
|
||||||
|
'https://mirror.stella-ops.org/feeds',
|
||||||
|
'https://mirror.stella-ops.org/feeds/',
|
||||||
|
'https://mirrors.stella-ops.org/feeds',
|
||||||
|
'https://mirrors.stella-ops.org/feeds/',
|
||||||
|
]);
|
||||||
|
|
||||||
/** Sensible defaults for local/development setup. */
|
/** Sensible defaults for local/development setup. */
|
||||||
private static readonly LOCAL_DEFAULTS: Record<string, Record<string, string>> = {
|
private static readonly LOCAL_DEFAULTS: Record<string, Record<string, string>> = {
|
||||||
@@ -3128,6 +3136,21 @@ export class StepContentComponent {
|
|||||||
if (sourceMode && !this.sourceFeedMode()) {
|
if (sourceMode && !this.sourceFeedMode()) {
|
||||||
this.sourceFeedMode.set(sourceMode);
|
this.sourceFeedMode.set(sourceMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mirrorUrlRaw = config['sources.mirror.url'];
|
||||||
|
const mirrorUrl = typeof mirrorUrlRaw === 'string'
|
||||||
|
? mirrorUrlRaw.trim().toLowerCase()
|
||||||
|
: '';
|
||||||
|
if (
|
||||||
|
!this.legacyMirrorDefaultsSanitized &&
|
||||||
|
StepContentComponent.LEGACY_MIRROR_ENDPOINT_DEFAULTS.has(mirrorUrl)
|
||||||
|
) {
|
||||||
|
this.legacyMirrorDefaultsSanitized = true;
|
||||||
|
this.configChange.emit({ key: 'sources.mirror.url', value: '' });
|
||||||
|
this.configChange.emit({ key: 'sources.mirror.apiKey', value: '' });
|
||||||
|
this.sourceFeedMode.set('custom');
|
||||||
|
this.configChange.emit({ key: 'sources.mode', value: 'custom' });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Source feed mode: 'mirror' (Stella Ops pre-aggregated) or 'custom' (individual feeds)
|
// Source feed mode: 'mirror' (Stella Ops pre-aggregated) or 'custom' (individual feeds)
|
||||||
|
|||||||
@@ -718,9 +718,12 @@ export class VexStatementSearchComponent implements OnInit {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await firstValueFrom(this.vexHubApi.searchStatements(params));
|
const result = await firstValueFrom(this.vexHubApi.searchStatements(params));
|
||||||
this.statements.set(result.items);
|
const items = Array.isArray(result?.items) ? result.items : [];
|
||||||
this.total.set(result.total);
|
this.statements.set(items);
|
||||||
|
this.total.set(typeof result?.total === 'number' ? result.total : items.length);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
this.statements.set([]);
|
||||||
|
this.total.set(0);
|
||||||
this.error.set(err instanceof Error ? err.message : 'Search failed');
|
this.error.set(err instanceof Error ? err.message : 'Search failed');
|
||||||
} finally {
|
} finally {
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
|
|||||||
@@ -9,12 +9,20 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
computed,
|
computed,
|
||||||
inject,
|
inject,
|
||||||
|
OnDestroy,
|
||||||
signal,
|
signal,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
|
||||||
import { AppConfigService } from '../../core/config/app-config.service';
|
import { AppConfigService } from '../../core/config/app-config.service';
|
||||||
import { AuthorityAuthService } from '../../core/auth/authority-auth.service';
|
import { AuthorityAuthService } from '../../core/auth/authority-auth.service';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
__stellaWelcomeSignIn?: (() => void) | null;
|
||||||
|
__stellaWelcomePendingSignIn?: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-welcome-page',
|
selector: 'app-welcome-page',
|
||||||
imports: [],
|
imports: [],
|
||||||
@@ -81,8 +89,8 @@ import { AuthorityAuthService } from '../../core/auth/authority-auth.service';
|
|||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="button" class="cta" (click)="signIn()">
|
<button type="button" class="cta" [disabled]="signingIn()" (click)="signIn()">
|
||||||
<span class="cta__label">Sign In</span>
|
<span class="cta__label">{{ !interactionReady() ? 'Preparing Sign-In...' : (signingIn() ? 'Signing In...' : 'Sign In') }}</span>
|
||||||
<svg class="cta__arrow" viewBox="0 0 24 24" width="16" height="16"
|
<svg class="cta__arrow" viewBox="0 0 24 24" width="16" height="16"
|
||||||
fill="none" stroke="currentColor" stroke-width="2.5"
|
fill="none" stroke="currentColor" stroke-width="2.5"
|
||||||
stroke-linecap="round" stroke-linejoin="round">
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
@@ -659,10 +667,16 @@ import { AuthorityAuthService } from '../../core/auth/authority-auth.service';
|
|||||||
}
|
}
|
||||||
`]
|
`]
|
||||||
})
|
})
|
||||||
export class WelcomePageComponent {
|
export class WelcomePageComponent implements OnDestroy {
|
||||||
private readonly configService = inject(AppConfigService);
|
private readonly configService = inject(AppConfigService);
|
||||||
private readonly authService = inject(AuthorityAuthService);
|
private readonly authService = inject(AuthorityAuthService);
|
||||||
|
private readonly globalSignInTrigger = () => {
|
||||||
|
void this.signIn();
|
||||||
|
};
|
||||||
readonly authNotice = signal<string | null>(null);
|
readonly authNotice = signal<string | null>(null);
|
||||||
|
readonly signingIn = signal(false);
|
||||||
|
readonly interactionReady = signal(false);
|
||||||
|
readonly pendingSignIn = signal(false);
|
||||||
|
|
||||||
readonly config = computed(() => this.configService.config);
|
readonly config = computed(() => this.configService.config);
|
||||||
readonly title = computed(
|
readonly title = computed(
|
||||||
@@ -683,7 +697,49 @@ export class WelcomePageComponent {
|
|||||||
return secure.toString();
|
return secure.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
signIn(): void {
|
constructor() {
|
||||||
|
// Ensure the primary action is wired as soon as browser bootstrap begins.
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.__stellaWelcomeSignIn = this.globalSignInTrigger;
|
||||||
|
window.setTimeout(() => {
|
||||||
|
this.interactionReady.set(true);
|
||||||
|
if (this.pendingSignIn() || window.__stellaWelcomePendingSignIn) {
|
||||||
|
this.pendingSignIn.set(false);
|
||||||
|
window.__stellaWelcomePendingSignIn = false;
|
||||||
|
void this.signIn();
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
window.__stellaWelcomeSignIn === this.globalSignInTrigger
|
||||||
|
) {
|
||||||
|
window.__stellaWelcomeSignIn = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async signIn(): Promise<void> {
|
||||||
|
if (this.signingIn()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.interactionReady()) {
|
||||||
|
this.pendingSignIn.set(true);
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.__stellaWelcomePendingSignIn = true;
|
||||||
|
}
|
||||||
|
this.authNotice.set('Preparing secure sign-in...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingSignIn.set(false);
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.__stellaWelcomePendingSignIn = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof window !== 'undefined' && window.location.protocol === 'http:') {
|
if (typeof window !== 'undefined' && window.location.protocol === 'http:') {
|
||||||
const secureUrl = this.secureUrl();
|
const secureUrl = this.secureUrl();
|
||||||
if (secureUrl) {
|
if (secureUrl) {
|
||||||
@@ -696,7 +752,35 @@ export class WelcomePageComponent {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.signingIn.set(true);
|
||||||
this.authNotice.set(null);
|
this.authNotice.set(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!this.configService.isConfigured()) {
|
||||||
|
await this.configService.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.configService.isConfigured()) {
|
||||||
|
this.authNotice.set('Sign-in configuration is still loading. Please try again in a moment.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.authService.beginLogin('/');
|
||||||
|
|
||||||
|
// First click can occasionally race with early-runtime auth bootstrap;
|
||||||
|
// retry once if we are still on the welcome page after a short delay.
|
||||||
|
if (typeof window !== 'undefined' && window.location.pathname.startsWith('/welcome')) {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (window.location.pathname.startsWith('/welcome')) {
|
||||||
void this.authService.beginLogin('/');
|
void this.authService.beginLogin('/');
|
||||||
}
|
}
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
this.authNotice.set('Unable to start sign-in. Please retry.');
|
||||||
|
} finally {
|
||||||
|
this.signingIn.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>document.getElementById('stella-splash').dataset.ts=Date.now();</script>
|
<script>document.getElementById('stella-splash').dataset.ts=Date.now();</script>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.__stellaWelcomePendingSignIn = false;
|
||||||
|
|
||||||
|
document.addEventListener(
|
||||||
|
'click',
|
||||||
|
function (event) {
|
||||||
|
if (!window.location.pathname.startsWith('/welcome')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var target = event.target;
|
||||||
|
if (!(target instanceof Element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var button = target.closest('button.cta');
|
||||||
|
if (!button) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window.__stellaWelcomeSignIn === 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
window.__stellaWelcomePendingSignIn = true;
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user