Web UI Architecture
Stella Ops Web UI (Angular Console; baseline 2025Q4). Consolidates Console requirements documented across the Policy, Graph, Vulnerability Explorer, Advisory AI, Orchestrator, and Notifications module guides and implementation plans.
Scope. Implementation-ready architecture for the Angular SPA that operators and developers use to drive Stella Ops. This document defines UX surfaces, module boundaries, data flows, auth, RBAC, real-time updates, performance targets, i18n/a11y, security headers, testing and deployment. The UI is a consumer of backend APIs (Scanner, Policy, Excititor, Concelier, Attestor, Authority) and never performs scanning, merging, or signing on its own.
0) Mission & non-goals
Mission. Provide a fast, explainable console for:
- CTO deployment/compliance posture: deployed-as-expected, policy drift, open exceptions, and evidence export.
- DevOps release control: topology, promotion paths, deployments, approvals, and operational recovery.
- Security manager triage: findings, security risk, VEX, reachability, and exception impact.
- Developer self-service: owned microservice image candidates, allowed environment updates, and promotion requests.
- Scans (status, SBOMs, diffs, EntryTrace, attestation).
- Policy management (rules, exemptions, VEX consumption view).
- Vulnerability intel (Concelier status), VEX consensus exploration (Excititor).
- Runtime posture (Zastava observer + admission).
- Admin operations (tenants, tokens, quotas, licensing posture).
Non-goals. No client-side crypto signing; no Docker/CRI access; no direct registry access beyond fetching static assets or OCI referrer summaries exposed by backend.
1) Technology baseline
- Framework: Angular 21 (standalone APIs / Signals), TypeScript 5.9 (
package.json:@angular/* ^21.1.2,typescript ~5.9.3). - Styling: SCSS with a custom design-token system (
src/styles.scss,src/styles/_*.scss,src/styles/tokens/); CSS variables for theming. (Tailwind is not used — there is no Tailwind dependency or config in the project.) - Charts / visualization:
d3(force/SVG),@viz-js/viz(Graphviz/WASM for DAG layouts), andmermaidfor diagram rendering; all imported on demand. - State: Angular Signals + hand-rolled signal stores (
*.store.ts, e.g.core/api/risk.store.ts,auth-session.store.ts). (@ngrx/signalsis not a dependency.) - Transport: Angular
HttpClient+ RxJS; SSE (EventSource) for progress/stream channels (e.g.policy-streaming.client.ts,console-status.client.ts,export-center.client.ts). - Editors:
monaco-editor0.52.0 (lazy-loaded for Policy Studio), plusyaml,marked, andngx-markdownfor YAML/Markdown surfaces. - Build: Angular CLI with the
@angular/build:application(esbuild/Vite-based) builder. - Testing: Vitest +
@angular/cdktesting harnesses, Storybook (@storybook/angular), and Playwright (@playwright/test,@axe-core/playwright) for e2e/a11y. - Packaging / serving: production assets are served by the platform gateway (the SPA is bind/volume-mounted into the gateway container; see
core/config/app-config.service.tsnormalizeApiBaseUrls), not by a UI-local NGINX container. Assets are content-hashed and immutable. - Observability docs: runbook + Grafana JSON stub in
operations/observability.mdandoperations/dashboards/console-ui-observability.json(offline import).
2) High-level module map
Conceptual map — not the literal directory tree. The implementation is organised as
src/app/core/(bootstrap, config, auth, http, i18n, api clients),src/app/shared/,src/app/features/<feature>/(one folder per feature, each lazy-loaded), andsrc/app/routes/(route aggregators that compose features into top-level shells:mission-control,releases,security-risk,triage,evidence,compliance,ops,setup,topology,platform-ops). The grouping below is the domain view; map it tofeatures/+routes/when navigating source.
core/ # bootstrap, config, auth (PKCE/DPoP), http interceptors, i18n, api/ clients
shared/ # UI kit (tables, code-viewers, badges), pipes
features/ # one lazy-loaded folder per surface, e.g.:
dashboard-v3/ # home dashboard (persona lens-card registry)
scanner-ops/ # scanner operations
security/ + findings/ + image-security/ # security & findings surfaces
policy/ + policy-studio/ + policy-governance/ + policy-simulation/ # policy authoring/eval
vex-hub/ + vex-timeline/ + console-vex # VEX explorer/consensus
release-orchestrator/ (dashboard/runs/approvals/deployments/...) # release control
evidence/ + evidence-export/ + evidence-thread/ # attestation/evidence
feed-mirror/ + sbom-sources/ # source/feed health
console-admin/ + settings/ + setup-wizard/ # admin/setup
integration-hub/ # integrations + wizard
routes/ # top-level shells that lazy-load and guard the feature folders
Each feature folder builds as a standalone route (lazy loaded via loadChildren / loadComponent). HTTP shapes live in core/api/*.client.ts + *.models.ts.
3) Navigation & key views
Navigation note. The live top-level shells (declared in
app.routes.ts) are: home Dashboard (/→dashboard-v3), Mission Control (/mission-control), Environments (/environments), Releases (/releases), Developer Services (/developer), Security (/security), Vulnerabilities (/triage), Evidence (/evidence), Compliance (/compliance, alias/assurance), Asset Inventory (/inventory), Operations (/ops), Setup (/setup), Settings (/settings), Console Admin (/console-admin), Notifications (/notifications), and Documentation (/docs). Several subsections below (Scans, Runtime/Zastava, Excititor, Concelier, Attest) describe domain capabilities by their original product names; in the current build these surface as routes under those shells (e.g. scanner operations under/ops→scanner-ops, runtime evidence as a tab inside finding detail, VEX undervex-hub/console-vex, attestation/evidence under/evidence) rather than as the literal top-level routes named in the headings. Treat the per-domain bullets as capability specs.Route shells ≠ sidebar sections. Those
app.routes.tsshells are not what the operator sees in the rail. The sidebar renders five pillars —home · risk · releases · proof · operate(src/Web/StellaOps.Web/src/app/core/navigation/navigation.config.ts:49NAVIGATION_GROUPS, rendered as Home · Vulnerabilities · Deployment · Proof · Operate) — and the route shells are re-parented under them (Phase A, sprint20260707_006).navigation.config.tsis the source of truth for the grouping; a secondnavSectionsarray inapp-sidebar.component.tsis a test/mirror artifact, not the rendered nav.
3.1 Dashboard
- Tiles: “New criticals (24h)”, “VEX suppressions applied”, “Attested SBOMs (7d)”, “Feed age per provider”, “Scanner queue depth”, “Admission events”.
- Trends: sparkline for vulns/day, pass/fail rates, attestation throughput.
- Persona overview: the home dashboard renders from the lens-card registry, filtered by the active persona and token scopes. Operations starts with promotion readiness and pending actions, Security starts with critical risk and triage, Service contributors start with their work/build lenses, and Compliance starts with assurance/evidence/compliance cards. Hidden or out-of-scope lenses are not rendered into the grid. The registry also carries deterministic persona-specific span hints so compact cards fill desktop rows instead of leaving avoidable whitespace, while metric groups collapse to single-column mobile layouts. Fresh tenants still render the scoped overview first; the bootstrap guide appears below it.
3.1a Developer Services
- Route:
/developer/services, lazy-loaded fromsrc/app/features/developer-services/. - Owned services: service name, repository, image namespace, current release/deployment state per visible environment, and last deployment/update from Release Control.
- Fail-closed projection: the page reads the profile before tenant release/deployment/finding data. Missing ownership claims render an explicit configuration state with zero personal rows and no broad downstream reads; visible counts use the filtered canonical service set, duplicate aliases collapse to one service, and deployment-derived environments remain inside
environmentIds/maxEnvironmentType. - Candidate images: immutable OCI digest from the latest release projection, with release version, evidence posture, policy gate, and findings roll-up.
- Allowed updates: direct update controls appear only when the token has explicit
release:write, service ownership, environment constraints,maxEnvironmentType,directUpdateAllowed=true, a non-blocking gate, and an immutable digest. Release Orchestrator repeats these ABAC checks onPOST /api/v1/release-orchestrator/deployments. - Approval fallback: developer-originated image update requests call
POST /api/v1/release-orchestrator/approvals/developer-requests; the backend persists an approval row linked to source type, actor, service, candidate digest, and target environment, and it rejects wrong-service/wrong-environment/tag-only requests. The page accepts a pastedsha256:digest for a newly built image when no release projection has surfaced it yet, but still evaluates the same service/environment ABAC before enabling the button. - Why blocked: show missing ABAC claims, missing digest, failed policy gate, finding risk, out-of-scope environment, stale scan/feed state, incomplete SBOM evidence posture, or unavailable approval API without falling back to mock success. Links to findings, policy simulation, SBOM inventory, and deployment activity carry the service/release context as query parameters.
- Scoped security context: security findings and the SBOM explorer accept the developer’s service context, apply a read-only “my service” filter, and keep security-manager-only actions out of the developer path.
- Operator context: approvals and deployments label developer-originated service updates with source type, service, target environment, and digest metadata. The deployments route includes service, developer, source, and digest filters plus a read-only developer-originated request panel backed by the approvals API. Developer users may inspect their own request/run state, while approve/reject controls require operator or approver scopes.
- Admin setup: Identity & Access user editing exposes service-release ABAC attributes for
role/service-release-contributor; the legacyrole/release-developerbundle maps to the same scoped contributor model. Attributes includeserviceIds,repositoryIds,imageNamespaces,environmentIds,maxEnvironmentType, anddirectUpdateAllowed. The UI requires explicit acknowledgement before saving a user that combines this role with broad admin/operator/bypass/publisher/approver roles.
3.1b Registry Images
- Routes:
/security/image-inventoryis the cross-registry Images destination;/setup/integrations/registry-browser/:registryIdis the per-registry browser. Both consume the Integrations registry-image projection and link to the Image Security dossier. - One readiness rule: both routes use the shared
registry-image-readiness.tshelper. Scanner v2readinessis authoritative when present; legacysbomReadiness/lastScanAtis a compatibility fallback only when connected to an older server that omits the v2 field. - Separate truths: durable readiness (
absent,ready,stale), latest attempt (scanStatus,lastAttemptAt), and policy verdict are independent. A failed latest attempt does not turn a durable prior success into unscanned and does not render as a policy block. - Time display and actions: tables label the primary timestamp Last successful scan and may show latest attempt status/time as compact secondary text. Coverage, stale counts, per-registry unevaluated filters, scheduled-scan affordances, and bulk-scan targeting all use the same durable readiness helper.
- Responsive table containment: the wide Registry Browser posture table stays inside the page width and exposes a named, keyboard-focusable internal horizontal scroller. Narrow layouts show an explicit scroll hint; columns and row actions remain present and reachable rather than being clipped or silently hidden. Route shells that project intrinsically wide content through page tabs must use a zero-minimum track (
minmax(0, 1fr)) and contain the tabs host to the visible route width.
3.2 Scans
Scan list with status, image digest, repo, time, artifacts, attestation badge.
Scan detail:
- SBOM viewer: Inventory/Usage toggle; component table (virtualized), filters by package type, severity, source.
- Diff by layer: A→B change grid (added/removed/upgraded), grouped by introducing/removing layer; tooltips show provenance and links to layer SBOM fragment.
- EntryTrace: shell chain with file:line breadcrumbs; jump-to source viewer (read-only, hexdump fallback).
- Report attestation: consumes Scanner’s optional
attestationprojection onGET /api/v1/scans/{scanId}and shows the backend status, transparency entry/log index, and signing key when recorded. A typedtransparency_anchor_failedgap is fail-visible through the canonical expanded verdict with the server-owned consequence, cause, fix, and owner; rejected evidence is not mislabeled as a self-signed outage. - Export: download buttons (CycloneDX JSON, Protobuf, SPDX JSON); size shown; SHA-256 inline.
3.3 Runtime (Zastava)
- Observer timeline: container start/stop, drift, policy violations; faceted by namespace/owner.
- Live process view: top N processes, loaded libs summary vs Usage SBOM.
- Admission decisions: per-namespace rules, allow/deny events, cache TTL, reasons.
3.4 Policy
Policy bundles: active vs staged; diff viewer with change summary.
Editors:
- YAML rules (ignore lists, thresholds, vendor precedence overrides) via Monaco.
- Rego blocks (advanced gates). Not implemented as a client-side WASM sandbox. Policy evaluation/preview is performed server-side through
core/api/policy-interop.service.ts→POST /api/v1/policy/interop(export/import/validate/evaluate); there is no in-browser OPA-WASM evaluator (no.wasmasset or OPA-WASM dependency exists). This aligns with the Frontend Truthfulness Contract below, which forbids locally simulating policy verdicts.
VEX inclusion controls: weight sliders (visualization only), provider allow/deny toggles.
Preview: select SBOM (or image digest) → show verdict under staged policy.
3.5 Excititor
- Claims explorer: search by vulnId/productKey/provider; show raw claim (status, justification, evidence).
- Consensus view: rollup per (vuln, product) with accepted/rejected sources, weights, timestamps.
- Conflicts: grid of top conflicts; filters for justification gates failed.
3.6 Concelier
- Sources table: staleness, last run, errors.
- Advisory search: by CVE/alias; show normalized affected ranges.
- Exports: trigger full/delta JSON/Trivy DB; show manifest digests and Rekor link if attested.
3.7 Attest
- Proofs list: last 7 days Rekor entries; filter by kind (sbom/report/vex).
- Verification: paste UUID or upload bundle → verify; result with explanations (chain, Merkle path).
3.8 Admin
- Tenants/Installations: view/edit, isolation hints.
- Clients & roles: Authority clients, role→scope mapping, rotation hints.
- Quotas: per license plan, counters, throttle events.
- Licensing posture: last PoE introspection snapshot (redacted), release window.
- Branding: tenant logo, title, and theme tokens with preview/apply (fresh-auth).
- Crypto providers (
Setup -> Crypto Providers, route/setup/crypto-providers):- Provider table — discover/probe SmRemote, CryptoPro, Crypto-Sim and set the per-tenant active provider preference (scope
crypto:admin). - Compliance profile selector (Sprint 503_007) — pick the active
world | fips | gost | sm | kcmvp | eidasprofile via a typed-confirmation dialog that renders the algorithm delta and warns on incompatible (profile, provider) pairs. CallsPUT /api/v1/admin/crypto-providers/compliance-profileon Platform under a distinct scopecrypto:profile:admin; emitsplatform.update_compliance_profileaudit with before/after.
- Provider table — discover/probe SmRemote, CryptoPro, Crypto-Sim and set the per-tenant active provider preference (scope
3.9 Vulnerability triage (VEX-first)
- Routes:
/triage/artifacts,/triage/artifacts/:artifactId,/triage/audit-bundles,/triage/audit-bundles/new. - Workspace: artifact-first split layout (finding cards on the left; explainability tabs on the right: Overview, Reachability, Policy, Attestations).
- VEX decisions: evidence-first VEX modal with scope + validity + evidence links; bulk apply supported; uses
/v1/vex-decisions. - Audit bundles: “Create immutable audit bundle” UX to build and download an evidence pack; uses
/v1/audit-bundles. Audit Trail exposes and mounts its Export Center and Evidence bundles tabs only when the session hasexport.viewer; timeline-only sessions do not issue this read eagerly. - Schemas:
docs/modules/vuln-explorer/schemas/vex-decision.schema.json,docs/modules/attestor/schemas/attestation-vuln-scan.schema.json,docs/modules/evidence-locker/schemas/audit-bundle-index.schema.json. - Reference:
docs-archive/product/advisories/27-Nov-2025-superseded/28-Nov-2025 - Vulnerability Triage UX & VEX-First Decisioning.md(superseded advisory; retained for provenance).
3.10 Integration Hub (Sprint 011)
- Routes (as implemented): the Integration Hub shell (
features/integration-hub/integration-hub.routes.ts) is type-segmented —onboarding,onboarding/:type,registries,scm,ci,runtime-hosts,feeds,advisory-vex-sources— rather than the flat/integrations,/integrations/:id,/integrations/activityoriginally specified below. The list/detail/activity descriptions that follow remain the intended capability surface. - Navigation placement: Under Ops for operators; advanced settings under Admin > Integrations.
- Integration types: SCM (GitHub/GitLab/Gitea), CI (GitHub Actions/GitLab CI/Jenkins), Registry (Docker Hub/Harbor/ECR/ACR/GCR/GHCR), Hosts (Zastava observer), Feeds (Concelier/Excititor mirrors), Artifacts (SBOM/VEX uploads).
- List view:
- KPI strip: total integrations, active, degraded, failed.
- Filters: type chips, status, provider, owner, search.
- Table columns: name, provider, type, status badge, last sync, owner, actions.
- CTA: “Add Integration” button.
- Detail view:
- Summary header: status badge, type, provider, last test timestamp.
- Tabs: Overview, Health, Activity, Permissions, Secrets (AuthRef), Webhooks, Inventory.
- Actions: Test Connection, Edit, Pause/Resume, Delete.
- Activity view:
- Chronological timeline of all integration events.
- Filters: event type, integration, date range.
- Event types: created, updated, deleted, test_success, test_failure, health_ok, health_degraded, health_failed, paused, resumed, credential_rotated, sync_started, sync_completed, sync_failed.
- Stats: total events, success count, warning count, failure count.
- Auto-refresh every 30 seconds.
- Role gating: the canonical integration scopes are
integration:read(list/detail),integration:write(CRUD), andintegration:operate(test connections / run operations) — seeStellaOpsScopes.cs(IntegrationRead/IntegrationWrite/IntegrationOperate). (The earlierintegrations.read/integrations.adminnames do not exist in the catalog.) - API backend:
src/Integrations/StellaOps.Integrations.WebServiceproviding CRUD, test, trigger, pause/resume endpoints. - Credentials: All secrets via AuthRef URIs only; no raw credentials stored in UI state.
3.11 Integration Wizard (Sprint 014)
- Routes: Wizard is modal-based, launched from Integration Hub via “Add Integration” CTA.
- Location:
src/Web/StellaOps.Web/src/app/features/integrations/integration-wizard.component.ts. - Wizard steps:
- Provider selection: Choose provider from type-specific lists (registry, SCM, CI, host).
- Authentication: Configure auth method with AuthRef-managed credentials.
- Scope: Define repository/branch/namespace filters.
- Schedule: Set sync schedule (manual, interval, cron).
- Preflight checks: Run connection tests with detailed failure states.
- Review: Summary and create confirmation.
- Provider profiles:
- Registry: Docker Hub, Harbor, ECR, ACR, GCR, GHCR with type-specific auth (basic, token, IAM).
- SCM: GitHub, GitLab, Gitea with OAuth apps or PAT auth.
- CI: GitHub Actions, GitLab CI, Gitea Actions with webhook configuration.
- Host: Kubernetes, VM, Baremetal with agent install templates.
- Auth methods: Token, OAuth, Service Account, API Key depending on provider.
- Copy-safe UX:
- Webhook URLs and secrets are copy-button enabled.
- Secret fields use
type="password"with reveal toggle. - Setup instructions are Markdown-formatted and copy-safe.
- Optional compliance metadata:
- The scope step can attach DORA RoI metadata to an integration profile when that module is in use.
- Criticality and contract dates are emitted with operator-filled provenance.
- Subcontracting rank 1 may use auto-captured direct-provider provenance; deeper ranks are forced to operator-filled provenance.
- Preflight checks:
- Network connectivity validation.
- Credential verification.
- Permission/scope sufficiency checks.
- Provider-specific health probes.
- Host wizard additions (Sprint 014 extension):
- Kernel/privilege preflight checks for eBPF/ETW observers.
- Helm and systemd install templates.
- Agent download and registration flow.
- Models:
integration.models.tsdefinesIntegrationDraft,IntegrationProvider,WizardStep,PreflightCheck,AuthMethod, and provider constants.
3.12 Advisor (Ask Stella)
- Chat panel scoped to the current artifact, CVE, or release, with citations and evidence chips.
- Citations and Evidence drawer lists object refs (SBOM, VEX, scan IDs) and hashes.
- Action confirmation modal required for any tool action; disabled when policy denies.
- Budget indicators show quota or token budget exhaustion with retry hints.
3.13 Global Search and Assistant Bridge
- Search-first shell: the top-bar search field is the primary operator entry. AdvisoryAI is opened from a compact secondary icon beside search or from grounded answer/result actions, not as a competing route-first workflow.
- Shell-level assistant drawer: deeper-help opens in a global drawer and keeps the operator on the current page route; focus is restored back to search when the drawer closes.
- Assistant -> search return: assistant responses can return the user back into global search with populated query context and deterministic
chat_*action metadata. - Closed assistant dock: the closed Stella helper renders as a compact 44px touch target near the viewport edge; it expands back to the larger mascot and bubble only when the helper is opened so dense dashboards and mobile cards do not have actionable controls covered.
- Zero-learning empty state: focused empty-state search renders only current-page context, successful history, and executable starter chips/questions. Domain-teaching cards, scope toggles, and recovery panels are intentionally absent from the primary flow.
- Automatic page-open suggestions:
AmbientContextServicetracks router navigation and updates starter chips/placeholders automatically for every opened page without requiring manual refresh. - Context rail: empty-state search renders the current page title plus compact tokens for scope and last meaningful action on the same page scope when available.
- Last-action follow-up suggestions: the same service keeps a per-route scoped recent-action history with deterministic TTL bounds; surfaced starters can prepend
follow up: ...chips when that improves relevance. - Implicit route weighting: global search always prefers current-page evidence first and renders cross-scope overflow as a quiet secondary section only when it materially improves the answer.
- Answer-first search: every non-empty search renders a visible answer panel before raw cards; the panel resolves to
grounded,clarify, orinsufficientand never leaves the operator with a blank result area. - Page-owned self-serve questions: priority pages define common questions and clarifying prompts in the shared search context registry; empty-state search uses those as starter questions and answer states reuse them as follow-up or clarification buttons.
- Priority route rollout: integration activity, export center, offline-kit bundle management, setup topology, configuration pane, policy governance conflicts, and image security now require live route rechecks against mounted flows. Mock-only journey coverage is no longer sufficient for those routes.
- Suggestion executability gate: contextual/page starters preflight through backend viability signals before render so dead suggestions are suppressed instead of being taught to the user.
- Ambient payload activation: each global search request sends ambient context (
currentRoute,visibleEntityKeys,recentSearches,sessionId, optionallastAction) so AdvisoryAI can apply contextual ranking and answer shaping. - Contract governance: contextual chips follow
docs/modules/ui/search-chip-context-contract.md, while self-serve questions, rollout ownership, and fallback states followdocs/modules/ui/search-self-serve-contract.md; both are implemented insearch-context.registry.ts. - Optional telemetry markers: global search may emit client-side
search_self_serve_*markers (gap,reformulation,recovery,suggestion_suppressed) for backlog review, but search behavior must remain unchanged when telemetry is disabled or sinks fail. - Fallback transparency: when unified search drops to legacy fallback, global search displays an explicit degraded banner and emits enter/exit telemetry markers for operator visibility.
4) Auth, sessions & RBAC
4.1 OIDC flow
- Authorization Code + PKCE to Authority (
core/auth/pkce.util.ts,authority-auth.service.ts); silent-refresh redirect handled at/auth/silent-refresh, callback at/auth/callback. - ID Token for UX identity; Access Token (OpTok) for APIs (2–5 min TTL).
- DPoP (browser): an ECDSA WebCrypto keypair (default
ES256/P-256, optionallyES384/P-384;EdDSAnot yet supported) is generated and persisted in IndexedDB (core/auth/dpop/dpop-key-store.ts, DBstellaops-auth). Keys are currently createdextractable: trueso the JWK can be re-imported across sessions (not non-exportable). The access token bindscnf.jkt; each API call adds aDPoPproof and the interceptor (auth-http.interceptor.ts) handles nonce challenges automatically. - Refresh: optional DPoP-bound refresh tokens with rotation; otherwise silent renew.
4.2 RBAC
- Authorization is scope-based, not role-based. The UI mirrors the scope catalog in
core/auth/scopes.ts(StellaOpsScopes) and gates routes withrequireAuthGuard/requireScopesGuard/requireAnyScopeGuarddeclared inapp.routes.ts(e.g. Mission Control requires any ofui.read/release:read/scanner:read/sbom:read; Console Admin requiresui.admin). Scopes are read from the issued token; anadminscope is treated as a wildcard (hasScope/hasAnyScope/hasAllScopes). - Failing a guard redirects to
/console/profile(the “insufficient permissions” surface) rather than hard-blocking. - Source inconsistency (refreshed 2026-07-12): a few scopes named in
core/auth/scopes.tsare UI-local conveniences not in the canonical Authority catalog (StellaOpsScopes.cs) —risk:read,tenant:admin,policy:evaluate(verified: 0 catalog hits each). Token issuance may not honour them. Corrections to the prior version of this note:health:readandvex:exportwere listed here but no longer exist inscopes.tsat all (they are phantom scopes — the guards useops.healthandvex:readrespectively); andrelease.policy.readis in the canonical catalog (StellaOpsScopes.cs:799), so it is not an inconsistency. Theadmin/ui.adminscopes are real catalog scopes that additionally act as guard wildcards (see §4.1 and thewebdossier).
4.3 Session storage
- Access tokens & DPoP key material live in memory / IndexedDB; only minimal session metadata is persisted in
sessionStorage(core/auth/auth-storage.service.ts, with a 10-minute sweep for stale entries). Raw JWTs are never written tolocalStorage. The config-source cookie (stellaops_env_settings_url) usesSameSite=Strict.
5) HTTP layer & API clients
core/http/+core/config/app-config.service.tscentralize:- Per-service base URLs resolved from runtime config (
apiBaseUrls), normalized so Docker-internal hostnames (*.stella-ops.local) collapse to gateway-relative paths and avoid CORS (normalizeApiBaseUrls). - Problem+JSON parsing surfaced as uniform error state with correlation IDs.
- SSE helpers (EventSource) per stream client (
core/api/policy-streaming.client.ts,console-status.client.ts,export-center.client.ts,first-signal.client.ts). - DPoP proof injection & nonce handling via
core/auth/auth-http.interceptor.ts(keys generated/persisted bycore/auth/dpop/dpop-key-store.ts). - Tenant header injection via
core/auth/tenant-http.interceptor.ts.
- Per-service base URLs resolved from runtime config (
Typed API clients follow a per-domain
*.client.tsconvention undercore/api/, with DTOs in matching*.models.tsfiles (there is no singlecore/api/models.ts). Representative clients:vulnerability.client.ts,policy-engine.client.ts,vex-consensus.client.ts/console-vex.client.ts,advisories.client.ts,attestor-transparency.client.ts,authority-console.client.ts/authority-admin.client.ts,release-management.client.ts,evidence-pack.client.ts,notify.client.ts. (See the full client roster insrc/Web/StellaOps.Web/src/app/core/api/.)Scanner-owned vulnerability inventory/detail reads in
vulnerability-http.client.tspreflight the canonicalscanner:readscope. The current/api/v1/vulnerabilities*Scanner routes expose only an explicitly enabled non-production fixture and return typed404 scanner.vulnerabilities.not_availableresponses otherwise; production dashboards therefore never consume their statistics route. Dashboard and Security Posture summaries use the tenant/scoped Findings.SecurityGET /api/risk/aggregated-statuscontract underfindings:read, including its authoritativetotal, severity buckets, andcriticalOpencount.Offline-first UX: Ops dashboards must display a “data as of” banner with staleness thresholds when serving cached snapshots.
DTO examples (abbrev):
export type ImageDigest = `sha256:${string}`;
export interface ScanSummary {
imageDigest: ImageDigest; createdAt: string;
artifacts: { view: 'inventory'|'usage'; format: 'cdx-json'|'cdx-pb'|'spdx-json'; sha256: string; size: number }[];
status: 'queued'|'running'|'completed'|'error';
rekor?: { uuid: string; index?: number; url?: string };
}
export interface DiffEntry {
key: string; change: 'added'|'removed'|'upgraded'|'downgraded';
fromVersion?: string; toVersion?: string; layer: string; usedByEntrypoint?: boolean;
}
export interface VexConsensus {
vulnId: string; productKey: string; rollupStatus: 'affected'|'not_affected'|'fixed'|'under_investigation';
sources: { providerId: string; status: string; weight: number; accepted: boolean; reason: string }[];
}
Notifier delivery history: notify.client.ts reads delivery history from <notify-base>/deliveries and deserialises a NotifyDeliveriesResponse envelope. The illustrative shape below is representative; field names are normalized through the client and may differ from the wire DTO — treat this as a design sketch rather than the authoritative contract.
// Illustrative — see notify.client.ts / notify.models.ts for the live shape.
export interface NotifyDeliveryListResponse {
items: NotifyDelivery[];
count: number;
continuationToken?: string;
}
export interface NotifyDelivery {
deliveryId: string;
ruleId: string;
actionId: string;
status: 'pending'|'sent'|'failed'|'throttled'|'digested'|'dropped';
rendered: NotifyDeliveryRendered;
metadata: Record<string, string>; // includes channelType, target, previewProvider, traceId, and provider-specific entries
createdAt: string;
sentAt?: string;
completedAt?: string;
}
6) State, caching & real-time
Per-page stores (Signals) for list filters, pagination, and selected entities.
Normalized caches keyed by
(imageDigest, view, format); artifacts are downloaded via pre-signed URLs from Scanner and streamed; SHA-256 verified client-side before exposing “verified” badge.SSE channels:
/scans/{id}/events→ progress log./runtime/events/stream(optional) → live drift/admission feed (rate-limited).
Cache invalidation on job completion or explicit “refresh”.
7) SBOM viewing & diff UX
- Huge tables rendered with virtual scrolling (CDK Virtual Scroll); sort/filter performed client-side for ≤ 20k rows; beyond that, server-side queries via BOM-Index endpoints.
- Component row shows purl, version, origin (OS pkg / metadata / linker / attested), licenses, and used badge (Usage view).
- Diff: compact heatmap per layer; clicking opens a right-pane with evidence: introducing paths, file hashes, VEX notes (from Excititor consensus) and links to advisories (Concelier).
8) Policy editor & VEX integration
- YAML editor: Monaco-based (
features/policy-studio/editor/monaco-loader.service.ts) with schema hints; previews show which rules matched. - Rego editor: Monaco. Note (orphaned design): the “WASM-OPA sandbox for client-side evaluation” is not implemented — evaluation is server-side via
/api/v1/policy/interop(see §3.4). - VEX toggles: per-provider enable/disable; “explain” drawer shows why a claim was accepted/rejected (justification gate, signature state, weight).
- Staged → Active promotion is a two-step flow with confirmation and automatic policy digest computation.
9) Accessibility, i18n & theming
- A11y: WCAG 2.2 AA; keyboard navigation, focus management, ARIA roles; color-contrast tokens verified by unit tests.
- I18n: runtime translation loader from Platform (
/platform/i18n/{locale}.json) with embedded offline fallback bundles (en-US,de-DE,bg-BG,ru-RU,es-ES,fr-FR,uk-UA,zh-TW,zh-CN); locale selectors in topbar and/settings/languageuse Platform locale catalog (GET /api/v1/platform/localization/locales) with local fallback when catalog lookup is unavailable; fallback selection prefers the requested locale family first, thenen-US; dates/numbers localized viaIntl. Authenticated locale changes from both topbar selector and dedicated settings route (/settings/language) are written to Platform user preferences (GET/PUT /api/v1/platform/preferences/language) so preference is shared across Web and CLI sessions, with localStorage used as offline fallback. - Languages: English default; German, Bulgarian, Russian, Spanish, French, Ukrainian, Chinese (Traditional), Chinese (Simplified).
- Theming: dark/light via CSS variables; persisted in
prefers-color-schemeaware store. Interactive and semantic UI must use the shared status/action/button tokens (--color-status-*,--color-action-link-*,--color-btn-primary-*) rather than hard-coded hue values so local appearance and tenant branding controls can tune them. - Branding: tenant-scoped theme tokens and logo pulled from Authority
/console/brandingafter login. The Theme & Branding editor exposes status and action color tokens separately from brand yellow so primary buttons, filters, quick links, and warning/status labels stay readable across warm light surfaces and Stella-night dark surfaces.
10) Performance budgets
SBOM Graph overlays: maintain >= 45 FPS pan/zoom/hover up to ~2,500 nodes / 10,000 edges (baseline laptop); degrade via LOD + sampling above this.
Reachability halo limits: cap visible halos to <= 2,000 at once; beyond this, aggregate (counts/heat) and require zoom-in or filtering to expand.
TTI ≤ 1.5 s on 4G/slow CPU (first visit), ≤ 0.6 s repeat (HTTP/2, cached).
JS initial < 300 KB gz (lazy routes).
SBOM list: render 10k rows in < 70 ms with virtualization; filter in < 150 ms.
Diff view: compute client-side grouping for 5k changes in < 120 ms.
Techniques: route-level code splitting; ChangeDetectionStrategy.OnPush; Signals; server compression (zstd/gzip), immutable assets with long max-age (cache busting via hashes).
11) Security headers & CSP
- CSP:
default-src 'self'; connect-src 'self' https://*.internal; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'self'; - HSTS enabled at gateway.
- Referrer Policy:
no-referrer. - X-Frame-Options:
SAMEORIGIN. - COOP/COEP for cross-origin isolation (design target; note the in-browser OPA-WASM evaluator referenced in earlier drafts is not implemented — see §3.4/§8).
- Subresource Integrity (SRI) for third-party fonts (minimize third-party).
12) Error handling & UX hygiene
- Global error boundary surfaces Problem+JSON
title/detail/instancewith correlation ID. - Retry toast for 429 (quota throttles) with backoff timer.
- Auth expiry: pre-emptive refresh; unobtrusive banner when < 60 s TTL; re-login modal if refresh fails.
- Network down: offline banner with queued actions (idempotent resubmits).
- File verify: show SHA-256 mismatch warnings if artifact altered in transit.
13) Observability
- Front-end telemetry (OpenTelemetry Web): route timings, API latency by service, error counts; sampled to 1–5% and shipped to backend OTLP endpoint.
- User actions logged anonymously (no PII): “policy promote”, “scan export”, “attest verify”.
- Metrics dash in admin shows SLOs and recent front-end errors.
14) Testing strategy
- Unit: component and service logic via Vitest (config in
vitest.config.ts/vitest.codex.config.ts); Storybook stories double as component fixtures. (The project uses Vitest, not Jest.) - Component harness via
@angular/cdktesting harnesses for table, code viewer, diff heatmap. - Contract tests: OpenAPI schemas pulled at build time; DTOs validated; breaking changes fail CI.
- e2e: Playwright scenarios (login, scan detail, diff, policy edit, admit deny).
- A11y: axe-core CI checks; color-contrast lints.
- i18n: key coverage tests (no missing translations in supported locales).
15) Deployment & ops
- Serving: production assets are served by the platform gateway (the built SPA is mounted into the gateway container); content-hashed, immutable bundles. There is no UI-local NGINX/Dockerfile in
src/Web/StellaOps.Web. - Config (
core/config/app-config.service.ts): runtime config is resolved by a fallback chain — (1)?envSettings=<url>query param (persisted in thestellaops_env_settings_urlcookie), (2) that cookie, (3) the canonical/platform/envsettings.json, (4) legacy/config.json(deprecated, emits a console warning), elsemissing. The config carriesauthority(issuer + endpoints +dpopAlgorithms+refreshLeewaySeconds),apiBaseUrls,telemetry.sampleRate, anddoctor.fixEnabled. - Version banner: footer shows UI & backend versions; warns on major mismatches.
- CDN (optional): cache static bundle; APIs stay behind internal gateway.
- Feature flags: environment gates (staged policies, eBPF runtime) readable from config.
16) Plugin system (route plug-ins)
Status: roadmap / not implemented. The Console does not currently load front-end route plug-ins via signed remote JS bundles — there is no module-federation/remote-module loader, manifest verifier, or
features/plugins/folder insrc/Web/StellaOps.Web. What exists today is read-only viewing of backend plugins (core/api/execution-plugin-registry.client.ts,features/release-orchestrator/plugins/installed-plugins.component.ts). The design below describes the intended future front-end plug-in model.
- Manifest: Backend provides a signed plug-in manifest with remote module URLs and cosign signature per JS bundle.
- Loader: dynamic import with SRI and signature verification (WebCrypto).
- Sandbox: plug-ins are routed modules receiving a limited UI SDK (navigation, theme, API gateway). No direct token access; API calls proxied through the UI SDK which enforces RBAC.
- Examples: custom reports, vendor dashboards, regulated TLS config UIs.
17) Wire sequences (representative)
A) View scan progress
sequenceDiagram
autonumber
participant UI
participant Auth as Authority
participant SW as Scanner.WebService
UI->>Auth: /authorize (PKCE)
Auth-->>UI: code → token (DPoP-bound)
UI->>SW: GET /scans/{id} (Authorization+DPoP)
SW-->>UI: { status: running }
UI->>SW: (SSE) GET /scans/{id}/events
SW-->>UI: progress events …
SW-->>UI: terminal event { status: completed, artifacts[] }
B) Verify attestation
sequenceDiagram
autonumber
participant UI
participant AT as Attestor
UI->>AT: POST /rekor/verify { uuid }
AT-->>UI: { ok:true, index, logURL }
C) Promote policy & preview
sequenceDiagram
autonumber
participant UI
participant BE as Scanner.WebService (Policy endpoint)
UI->>BE: POST /policy/stage { yaml, rego }
BE-->>UI: { policyRevision, diagnostics }
UI->>BE: POST /policy/preview { imageDigest, policyRevision }
BE-->>UI: { verdict: pass|fail, reasons[] }
UI->>BE: POST /policy/promote { policyRevision }
BE-->>UI: { ok:true }
18) Security hard lines
- Never store JWTs in
localStorage. - Enforce DPoP for API calls; if DPoP unsupported for a service, require SameSite=Lax cookies with CSRF token header.
- Block mixed-content; only HTTPS origins allowed.
- Validate and render only escaped user content; code viewer uses safe highlighter.
- Downloaded artifacts are treated as opaque binaries; no HTML rendering.
19) Roadmap
- PWA offline shell (read-only) for dashboards and cached scan details.
- SBOM graph visualization (force-directed) for small components sets.
- Runtime session replay (privacy-safe) to debug operator workflows (opt-in).
- Assistive wizards for policy creation with guided templates.
20) Advisory Commitments (2026-02-26 Batch)
SPRINT_20260226_227_FE_triage_risk_score_widget_wiring_and_paritydelivers:- triage evidence pill parity and quick-verify status clarity,
- risk dashboard widget parity with passing E2E coverage,
- findings score breakdown/history wiring to live API responses,
- removal of skipped risk/score E2E suites in favor of deterministic route mocks.
Runtime API Provider Contract
The Angular runtime must bind API injection tokens to HTTP clients from app.config.ts and route-level providers. Mock, stub, fake, and in-memory API clients are allowed only for specs, Storybook, and explicit local fixtures. Provider helpers in production source must default to HTTP clients; local/test callers must opt in to mocks. provideScoreApi() and provideReplayApi() follow this contract: calling them without arguments binds the HTTP client, while the named 'local-harness' mode is reserved for test-only/local fixture setup. Legacy truthy flags must not select fixture clients.
src/Web/StellaOps.Web/scripts/guard-runtime-mocks.mjs scans runtime config, all route files, and the score/replay provider helpers for mock provider bindings, local-harness provider bindings, or mock-default helper definitions. Run npm run guard:runtime-mocks whenever API provider wiring changes.
The same guard also checks routed release approval decision surfaces for hardcoded gate, security, evidence, replay, or placeholder decision-cockpit data. Production /releases/approvals/:id pages must render API-backed approval detail store truth and fail closed when required decision subresources are unavailable; deterministic fixtures belong in test-only files.
Frontend Truthfulness Contract
The Console must not create evidence-grade outcomes in the browser. UI components may format and explain backend results, but must not locally simulate DSSE verification, Rekor inclusion, policy verdict changes, export signing, or air-gap sealing state.
Current fail-closed requirements:
- Quick Verify surfaces are unavailable until a backend verification result is wired; they must not fabricate receipts or mark signatures verified locally.
- Triage cards may show a Rekor verification control only as an unavailable state until a backend verification result is wired; they must not fabricate issuer, subject, timestamp, index, entry, or verified status in the browser.
- Console export orchestration errors are propagated to callers after recording store error state; failures must not become
nullor empty-success results. - Air-gap mode controls are read-only unless an audited backend mutation command exists; the toggle must not emit a local state change.
- Setup topology environment detail routes fail closed when topology inventory, readiness, release activity, security finding, or evidence-pack reads fail; failed backend evidence must render an unavailable state, not empty healthy tables. Readiness validation POST failures must surface an error and leave existing readiness rows unchanged rather than applying a local result.
- Proof Studio what-if controls may collect evidence-factor removal selections in the browser, but must not locally recompute confidence, confidence deltas, or verdict changes. The UI may render what-if confidence and verdict changes only from a backend result matching the current proof hash and selected factor set; otherwise it must show an unavailable state.
- Lineage compare audit-pack export must call the backend export path and may report completion/download only when that backend result includes a download URL. The compare dialog must not offer DSSE/Rekor signing controls or show signing progress until the backend returns explicit signing and transparency proof fields for that export.
- Policy governance conflict analysis and acknowledgement actions must call
/api/v1/governance/conflicts/analyzeand/api/v1/governance/conflicts/{conflictId}/acknowledge. If those backend commands return unavailable or fail, the UI must keep existing conflict state unchanged and show an explicit error; it must not use timers or local status mutation to imply analysis or acknowledgement succeeded. - OpenAPI aggregate output carries draft-stub markers while service specs remain scaffolded, so UI/API consumers do not treat incomplete contracts as shipped truth.
