diff --git a/docs/implplan/SPRINT_20260309_002_FE_live_frontdoor_canonical_route_sweep.md b/docs/implplan/SPRINT_20260309_002_FE_live_frontdoor_canonical_route_sweep.md index 5752db088..cbaf7dfeb 100644 --- a/docs/implplan/SPRINT_20260309_002_FE_live_frontdoor_canonical_route_sweep.md +++ b/docs/implplan/SPRINT_20260309_002_FE_live_frontdoor_canonical_route_sweep.md @@ -54,6 +54,7 @@ Completion criteria: | 2026-03-09 | Fixed a harness defect in the shared auth/session model: the original live sweep restored `sessionStorage` only in the login tab, so every freshly opened route page was unauthenticated and falsely redirected to `/welcome`. Moved session seeding into `createAuthenticatedContext(...)` and reused the helper from the other live scripts. | Developer | | 2026-03-09 | Ran the authenticated 106-route sweep against the rebuilt stack. After removing redirect/copy false positives, the real live backlog is 19 failing routes: reachability; feeds-airgap; jobengine; quotas; dead-letter; aoc; signals; packs; ai-runs; notifications; status; sbom-sources; policy simulation; policy trust-weights; policy staleness; policy audit; setup/platform trust-signing; and setup notifications. | Developer | | 2026-03-09 | Expanded the canonical live sweep inventory to include the revived release-investigation, evidence-thread, and registry-admin routes so future frontdoor passes cover those pages as first-class surfaces instead of leaving them to ad hoc follow-up scripts. | Developer | +| 2026-03-09 | After the full image rebuild and the next web-only repair pass, reran the authenticated 111-route sweep. The live backlog moved to 24 failing routes, with the earlier title regressions and feeds-airgap issue cleared while new backend/runtime failures remained concentrated in analytics, JobEngine, integrations, policy governance, notifications, and trust authorization. | Developer | ## Decisions & Risks - Decision: keep this sprint focused on broad route-level live verification and action inventory, not on fixing specific route defects before the rebuilt stack is actually exercised. diff --git a/docs/implplan/SPRINT_20260309_009_FE_live_contract_alignment_titles_trust_feeds.md b/docs/implplan/SPRINT_20260309_009_FE_live_contract_alignment_titles_trust_feeds.md new file mode 100644 index 000000000..095d71ac1 --- /dev/null +++ b/docs/implplan/SPRINT_20260309_009_FE_live_contract_alignment_titles_trust_feeds.md @@ -0,0 +1,79 @@ +# Sprint 20260309-009 - FE Live Contract Alignment for Titles, Trust, and Feeds + +## Topic & Scope +- Repair the live frontdoor defects that are caused by frontend contract drift rather than backend outages: route titles being overwritten after branding loads, the feeds-airgap page advertising a blocking incident by default, and trust-signing pages still calling retired `/api/v1/trust/*` endpoints. +- Keep this iteration focused on canonical route correctness for `/security/*`, `/ops/operations/feeds-airgap`, `/ops/platform-setup/trust-signing`, and `/setup/trust-signing` on the rebuilt `https://stella-ops.local` stack. +- Working directory: `src/Web/StellaOps.Web`. +- Allowed coordination edits: `docs/implplan/SPRINT_20260309_002_FE_live_frontdoor_canonical_route_sweep.md`, `docs/modules/platform/architecture-overview.md`, `docs/technical/architecture/console-branding.md`, `docs/api/console/samples/console-status-sample.json`. +- Expected evidence: focused frontend unit tests, rebuilt web bundle synced into compose, and authenticated live Playwright rechecks for the repaired routes. + +## Dependencies & Concurrency +- Depends on `SPRINT_20260309_002_FE_live_frontdoor_canonical_route_sweep.md` for the current authenticated failure inventory and on the completed full-stack rebuild baseline. +- Safe parallelism: stay inside `src/Web/StellaOps.Web/**`; do not edit backend services or router configuration in this sprint. + +## Documentation Prerequisites +- `AGENTS.md` +- `src/Web/StellaOps.Web/AGENTS.md` +- `docs/code-of-conduct/CODE_OF_CONDUCT.md` +- `docs/qa/feature-checks/FLOW.md` +- `docs/technical/architecture/console-branding.md` + +## Delivery Tracker + +### FE-CONTRACT-009-001 - Stop branding from clobbering route titles +Status: DONE +Dependency: none +Owners: Developer, Test Automation +Task description: +- Remove the direct `document.title` overwrite from branding application and re-apply Angular route titles after branding changes so canonical route titles remain stable on live navigations. +- Add focused tests proving branding refreshes preserve route-derived titles instead of collapsing to the bare brand string. + +Completion criteria: +- [ ] Branding updates no longer overwrite route titles after navigation. +- [ ] Focused frontend tests cover the route-title preservation path. +- [ ] Live `/security/advisories-vex`, `/security/sbom-lake`, and `/security/reachability` pass the title expectation checks in the authenticated sweep. + +### FE-CONTRACT-009-002 - Align trust-signing UI with live administration endpoints +Status: DOING +Dependency: FE-CONTRACT-009-001 +Owners: Developer, Test Automation +Task description: +- Replace the retired `/api/v1/trust/*` assumptions used by the trust-signing shell and default key dashboard with adapter logic over the live `/api/v1/administration/trust-signing*` endpoints. +- Preserve operator-visible capabilities on the base shell and key inventory route without relying on dead frontdoor paths. + +Completion criteria: +- [ ] `TrustHttpService` no longer requests `/api/v1/trust/*` during canonical trust-signing page loads. +- [ ] Focused frontend tests prove the trust adapter maps live administration responses into the shell and key-dashboard view models. +- [ ] Live `/ops/platform-setup/trust-signing` and `/setup/trust-signing` render without 404 response errors. + +### FE-CONTRACT-009-003 - Replace the static feeds-airgap blocking incident baseline +Status: DONE +Dependency: FE-CONTRACT-009-002 +Owners: Developer, QA +Task description: +- Remove the hardcoded blocking incident state from the static feeds-airgap page baseline so the canonical route reflects a healthy control-plane default unless live health data says otherwise. +- Keep the airgap actions and cross-links intact while making the summary/status copy consistent with a clean demo bootstrap. + +Completion criteria: +- [ ] The page no longer renders a blocking incident banner by default. +- [ ] Operator actions and tab flows still work after the content refresh. +- [ ] Live `/ops/operations/feeds-airgap` passes the canonical route sweep. + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-03-09 | Sprint created after the full rebuild and authenticated 111-route sweep isolated three frontend-owned defect families: branding/title races, a hardcoded feeds-airgap blocking incident, and stale trust-signing API wiring against retired `/api/v1/trust/*` routes. | Developer | +| 2026-03-09 | Removed the branding title overwrite, added route-title reapplication in the root shell, rebuilt/synced the web bundle, and confirmed the live sweep now passes `/security/advisories-vex`, `/security/reachability`, and `/ops/operations/feeds-airgap`. | Developer | +| 2026-03-09 | Rebased trust-signing base routes onto an overview-first shell backed by the live administration projection and removed the old `/api/v1/trust/dashboard` 404 path. Live trust routes still fail, but now on a real `403` from `/api/v1/administration/trust-signing`, which narrows the remaining defect to authorization/policy alignment. | Developer | + +## Decisions & Risks +- Decision: treat these defects as frontend contract-alignment work first because the live stack rebuild already proved the failures reproduce after a clean redeploy. +- Risk: the trust-signing shell expects richer models than the live administration endpoints currently expose, so the adapter layer must preserve deterministic behavior without inventing backend-only actions that do not exist. +- Decision: keep the feeds-airgap page static for this sprint but move it to a healthy baseline rather than fabricating a live incident in the default control-plane state. +- Decision: switch the base trust-signing route to an overview-first shell because the rebuilt platform exposes an administration projection, while the prior default key dashboard depended on richer retired endpoints that no longer exist. +- Risk: trust-signing remains blocked by a live `403` even after frontend contract alignment; the next iteration needs to inspect demo scopes and platform authorization, not just web routing. + +## Next Checkpoints +- 2026-03-09: land the branding/title preservation fix with focused tests. +- 2026-03-09: land the trust-signing contract adapter and recheck the live setup routes. +- 2026-03-09: refresh the feeds-airgap baseline content and rerun the authenticated Playwright slice. diff --git a/docs/technical/architecture/console-branding.md b/docs/technical/architecture/console-branding.md index 92047a13b..41647b8c5 100644 --- a/docs/technical/architecture/console-branding.md +++ b/docs/technical/architecture/console-branding.md @@ -56,7 +56,8 @@ If Authority is unreachable, the UI uses the static defaults. ## 6. UI Application - Branding service fetches `/console/branding` after login. - Applies CSS variables on `document.documentElement`. -- Updates header/logo assets and document title. +- Updates header/logo assets and provides the brand suffix used by the router title strategy. +- Route titles remain owned by Angular route metadata so a late branding refresh cannot collapse the browser title to the bare tenant brand. - Supports theme-specific overrides using `data-theme` selectors. ## 7. Audit and Offline @@ -68,4 +69,3 @@ If Authority is unreachable, the UI uses the static defaults. - `docs/UI_GUIDE.md` - `docs/modules/ui/architecture.md` - `docs/modules/authority/architecture.md` - diff --git a/src/Web/StellaOps.Web/src/app/app.component.spec.ts b/src/Web/StellaOps.Web/src/app/app.component.spec.ts index 93ab0c897..f7d91a255 100644 --- a/src/Web/StellaOps.Web/src/app/app.component.spec.ts +++ b/src/Web/StellaOps.Web/src/app/app.component.spec.ts @@ -1,6 +1,6 @@ import { TestBed } from '@angular/core/testing'; import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { provideRouter } from '@angular/router'; +import { provideRouter, TitleStrategy } from '@angular/router'; import { of } from 'rxjs'; import { AppComponent } from './app.component'; @@ -19,6 +19,7 @@ import { DoctorTrendService } from './core/doctor/doctor-trend.service'; import { DoctorNotificationService } from './core/doctor/doctor-notification.service'; import { PlatformContextUrlSyncService } from './core/context/platform-context-url-sync.service'; import { AUTHORITY_CONSOLE_API } from './core/api/authority-console.client'; +import { BrandingService } from './core/branding/branding.service'; class AuthorityAuthServiceStub { beginLogin = jasmine.createSpy('beginLogin'); @@ -51,10 +52,15 @@ class PlatformContextUrlSyncServiceStub { readonly initialize = jasmine.createSpy('initialize'); } +class TitleStrategyStub { + readonly updateTitle = jasmine.createSpy('updateTitle'); +} + describe('AppComponent', () => { let doctorTrend: DoctorTrendServiceStub; let doctorNotification: DoctorNotificationServiceStub; let contextUrlSync: PlatformContextUrlSyncServiceStub; + let titleStrategy: TitleStrategyStub; beforeEach(async () => { await TestBed.configureTestingModule({ @@ -124,6 +130,7 @@ describe('AppComponent', () => { { provide: DoctorTrendService, useClass: DoctorTrendServiceStub }, { provide: DoctorNotificationService, useClass: DoctorNotificationServiceStub }, { provide: PlatformContextUrlSyncService, useClass: PlatformContextUrlSyncServiceStub }, + { provide: TitleStrategy, useClass: TitleStrategyStub }, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting(), ] @@ -132,6 +139,7 @@ describe('AppComponent', () => { doctorTrend = TestBed.inject(DoctorTrendService) as unknown as DoctorTrendServiceStub; doctorNotification = TestBed.inject(DoctorNotificationService) as unknown as DoctorNotificationServiceStub; contextUrlSync = TestBed.inject(PlatformContextUrlSyncService) as unknown as PlatformContextUrlSyncServiceStub; + titleStrategy = TestBed.inject(TitleStrategy) as unknown as TitleStrategyStub; }); it('creates the root component', () => { @@ -210,6 +218,23 @@ describe('AppComponent', () => { expect(doctorTrend.stop).toHaveBeenCalledTimes(1); expect(doctorNotification.stop).toHaveBeenCalledTimes(1); }); + + it('reapplies the route title when branding changes', () => { + const fixture = TestBed.createComponent(AppComponent); + const brandingService = TestBed.inject(BrandingService); + + fixture.detectChanges(); + titleStrategy.updateTitle.calls.reset(); + + brandingService.applyBranding({ + tenantId: 'tenant-1', + title: 'Tenant One Console', + themeTokens: {}, + }); + fixture.detectChanges(); + + expect(titleStrategy.updateTitle).toHaveBeenCalled(); + }); }); function buildSession(): AuthSession { diff --git a/src/Web/StellaOps.Web/src/app/app.component.ts b/src/Web/StellaOps.Web/src/app/app.component.ts index bd3dffc99..396787ec2 100644 --- a/src/Web/StellaOps.Web/src/app/app.component.ts +++ b/src/Web/StellaOps.Web/src/app/app.component.ts @@ -8,7 +8,7 @@ import { inject, ViewChild, } from '@angular/core'; -import { Router, RouterLink, RouterOutlet, NavigationEnd } from '@angular/router'; +import { Router, RouterLink, RouterOutlet, NavigationEnd, TitleStrategy } from '@angular/router'; import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { filter, map, startWith, take } from 'rxjs/operators'; @@ -61,6 +61,7 @@ export class AppComponent { ]; private readonly router = inject(Router); + private readonly titleStrategy = inject(TitleStrategy); private readonly auth = inject(AuthorityAuthService); private readonly sessionStore = inject(AuthSessionStore); private readonly consoleStore = inject(ConsoleSessionStore); @@ -104,6 +105,13 @@ export class AppComponent { // Initialize branding on app start this.brandingService.fetchBranding().subscribe(); + effect(() => { + this.brandingService.currentBranding(); + this.brandingService.isLoaded(); + this.currentUrl(); + this.titleStrategy.updateTitle(this.router.routerState.snapshot); + }); + // Attempt to silently restore the auth session if the user was // previously logged in (session cookie may still be active at the Authority). void this.auth.trySilentRefresh(); diff --git a/src/Web/StellaOps.Web/src/app/core/api/trust.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/trust.client.spec.ts new file mode 100644 index 000000000..278a9f66e --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/core/api/trust.client.spec.ts @@ -0,0 +1,99 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { TrustHttpService } from './trust.client'; + +describe('TrustHttpService', () => { + let service: TrustHttpService; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [TrustHttpService], + }); + + service = TestBed.inject(TrustHttpService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('maps the live administration overview projection', () => { + service.getAdministrationOverview().subscribe((overview) => { + expect(overview.inventory).toEqual({ + keys: 14, + issuers: 7, + certificates: 23, + }); + expect(overview.signals).toEqual([ + { + signalId: 'audit-log', + status: 'healthy', + message: 'Audit log ingestion is current.', + }, + { + signalId: 'certificate-expiry', + status: 'warning', + message: '1 certificate expires within 10 days.', + }, + ]); + expect(overview.evidenceConsumerPath).toBe('/evidence-audit/proofs'); + }); + + const req = httpMock.expectOne('/api/v1/administration/trust-signing'); + expect(req.request.method).toBe('GET'); + req.flush({ + inventory: { + keys: 14, + issuers: 7, + certificates: 23, + }, + signals: [ + { + signalId: 'audit-log', + status: 'healthy', + message: 'Audit log ingestion is current.', + }, + { + signalId: 'certificate-expiry', + status: 'warning', + message: '1 certificate expires within 10 days.', + }, + ], + legacyAliases: [], + evidenceConsumerPath: '/evidence-audit/proofs', + }); + }); + + it('builds the trust shell summary from the administration overview', () => { + service.getDashboardSummary().subscribe((summary) => { + expect(summary.keys.total).toBe(4); + expect(summary.issuers.total).toBe(3); + expect(summary.certificates.total).toBe(2); + expect(summary.certificates.expiringSoon).toBe(1); + expect(summary.expiryAlerts).toEqual([]); + }); + + const req = httpMock.expectOne('/api/v1/administration/trust-signing'); + expect(req.request.method).toBe('GET'); + req.flush({ + inventory: { + keys: 4, + issuers: 3, + certificates: 2, + }, + signals: [ + { + signalId: 'certificate-expiry', + status: 'warning', + message: '1 certificate expires within 10 days.', + }, + ], + legacyAliases: [], + evidenceConsumerPath: '/evidence/overview', + }); + }); +}); diff --git a/src/Web/StellaOps.Web/src/app/core/api/trust.client.ts b/src/Web/StellaOps.Web/src/app/core/api/trust.client.ts index 83862083a..073fe435e 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/trust.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/trust.client.ts @@ -6,7 +6,7 @@ import { Injectable, InjectionToken, inject } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; -import { Observable, of, delay } from 'rxjs'; +import { Observable, of, delay, map } from 'rxjs'; import { SigningKey, @@ -23,6 +23,7 @@ import { CertificateChain, CertificateExpiryAlert, TrustAuditEvent, + TrustAdministrationOverview, TrustDashboardSummary, ListKeysParams, ListIssuersParams, @@ -53,6 +54,7 @@ import { export interface TrustApi { // Dashboard getDashboardSummary(): Observable; + getAdministrationOverview(): Observable; // Keys listKeys(params?: ListKeysParams): Observable>; @@ -97,6 +99,25 @@ export interface TrustApi { export const TRUST_API = new InjectionToken('TRUST_API'); +interface AdministrationTrustOverviewDto { + inventory?: { + keys?: number; + issuers?: number; + certificates?: number; + }; + signals?: Array<{ + signalId?: string; + status?: string; + message?: string; + }>; + legacyAliases?: Array<{ + legacyPath?: string; + canonicalPath?: string; + mode?: string; + }>; + evidenceConsumerPath?: string | null; +} + // ============================================================================ // HTTP Implementation // ============================================================================ @@ -105,9 +126,63 @@ export const TRUST_API = new InjectionToken('TRUST_API'); export class TrustHttpService implements TrustApi { private readonly http = inject(HttpClient); private readonly baseUrl = '/api/v1/trust'; + private readonly administrationBaseUrl = '/api/v1/administration/trust-signing'; getDashboardSummary(): Observable { - return this.http.get(`${this.baseUrl}/dashboard`); + return this.getAdministrationOverview().pipe( + map((overview) => ({ + keys: { + total: overview.inventory.keys, + active: 0, + expiringSoon: 0, + expired: 0, + revoked: 0, + pendingRotation: 0, + }, + issuers: { + total: overview.inventory.issuers, + fullTrust: 0, + partialTrust: 0, + minimalTrust: 0, + untrusted: 0, + blocked: 0, + averageTrustScore: 0, + }, + certificates: { + total: overview.inventory.certificates, + valid: 0, + expiringSoon: overview.signals.filter((signal) => signal.signalId === 'certificate-expiry').length, + expired: 0, + revoked: 0, + invalidChains: 0, + }, + recentEvents: [], + expiryAlerts: [], + })) + ); + } + + getAdministrationOverview(): Observable { + return this.http.get(this.administrationBaseUrl).pipe( + map((dto) => ({ + inventory: { + keys: dto.inventory?.keys ?? 0, + issuers: dto.inventory?.issuers ?? 0, + certificates: dto.inventory?.certificates ?? 0, + }, + signals: (dto.signals ?? []).map((signal) => ({ + signalId: signal.signalId?.trim() || 'unknown', + status: this.normalizeAdministrationSignalStatus(signal.status), + message: signal.message?.trim() || 'No details provided.', + })), + legacyAliases: (dto.legacyAliases ?? []).map((alias) => ({ + legacyPath: alias.legacyPath?.trim() || '', + canonicalPath: alias.canonicalPath?.trim() || '', + mode: alias.mode?.trim() || 'redirect', + })), + evidenceConsumerPath: dto.evidenceConsumerPath?.trim() || '/evidence/overview', + })) + ); } // Keys @@ -285,6 +360,15 @@ export class TrustHttpService implements TrustApi { return this.http.post(`${this.baseUrl}/analytics/alerts/${alertId}/acknowledge`, {}); } + private normalizeAdministrationSignalStatus(status: string | undefined): 'healthy' | 'warning' | 'critical' | 'unknown' { + const normalized = status?.trim().toLowerCase(); + if (normalized === 'healthy' || normalized === 'warning' || normalized === 'critical') { + return normalized; + } + + return 'unknown'; + } + private buildParams(params: Record): HttpParams { let httpParams = new HttpParams(); for (const [key, value] of Object.entries(params)) { @@ -1338,4 +1422,41 @@ export class MockTrustApiService implements TrustApi { acknowledgeAnalyticsAlert(alertId: string): Observable { return of(undefined).pipe(delay(100)); } + + getAdministrationOverview(): Observable { + const overview: TrustAdministrationOverview = { + inventory: { + keys: this.mockKeys.length, + issuers: this.mockIssuers.length, + certificates: this.mockCertificates.length, + }, + signals: [ + { + signalId: 'audit-log', + status: 'healthy', + message: 'Audit ingestion is current.', + }, + { + signalId: 'certificate-expiry', + status: 'warning', + message: 'One certificate expires within 10 days.', + }, + { + signalId: 'transparency-log', + status: 'healthy', + message: 'Transparency log witness is reachable.', + }, + ], + legacyAliases: [ + { + legacyPath: '/admin/issuers', + canonicalPath: '/setup/trust-signing/issuers', + mode: 'redirect', + }, + ], + evidenceConsumerPath: '/evidence/overview', + }; + + return of(overview).pipe(delay(120)); + } } diff --git a/src/Web/StellaOps.Web/src/app/core/api/trust.models.ts b/src/Web/StellaOps.Web/src/app/core/api/trust.models.ts index a88195155..6da0de6d1 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/trust.models.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/trust.models.ts @@ -262,6 +262,29 @@ export interface TrustDashboardSummary { readonly expiryAlerts: readonly (KeyExpiryAlert | CertificateExpiryAlert)[]; } +export interface TrustAdministrationOverview { + readonly inventory: { + readonly keys: number; + readonly issuers: number; + readonly certificates: number; + }; + readonly signals: readonly TrustAdministrationSignal[]; + readonly legacyAliases: readonly TrustAdministrationRouteAlias[]; + readonly evidenceConsumerPath: string; +} + +export interface TrustAdministrationSignal { + readonly signalId: string; + readonly status: 'healthy' | 'warning' | 'critical' | 'unknown'; + readonly message: string; +} + +export interface TrustAdministrationRouteAlias { + readonly legacyPath: string; + readonly canonicalPath: string; + readonly mode: string; +} + export interface KeysSummary { readonly total: number; readonly active: number; diff --git a/src/Web/StellaOps.Web/src/app/core/branding/branding.service.spec.ts b/src/Web/StellaOps.Web/src/app/core/branding/branding.service.spec.ts index 4c714d2b6..faa1f2d6c 100644 --- a/src/Web/StellaOps.Web/src/app/core/branding/branding.service.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/branding/branding.service.spec.ts @@ -109,4 +109,16 @@ describe('BrandingService', () => { expect(service.currentBranding()!.title).toBe('Test Title'); expect(service.isLoaded()).toBe(true); }); + + it('does not overwrite the current route title when branding is applied', () => { + document.title = 'Reachability - Stella Ops Dashboard'; + + service.applyBranding({ + tenantId: 'default', + title: 'Tenant Console', + themeTokens: {}, + }); + + expect(document.title).toBe('Reachability - Stella Ops Dashboard'); + }); }); diff --git a/src/Web/StellaOps.Web/src/app/core/branding/branding.service.ts b/src/Web/StellaOps.Web/src/app/core/branding/branding.service.ts index f78a84540..5f9366623 100644 --- a/src/Web/StellaOps.Web/src/app/core/branding/branding.service.ts +++ b/src/Web/StellaOps.Web/src/app/core/branding/branding.service.ts @@ -74,11 +74,6 @@ export class BrandingService { applyBranding(branding: BrandingConfiguration): void { this.currentBranding.set(branding); - // Apply document title - if (branding.title) { - document.title = branding.title; - } - // Apply favicon if (branding.faviconUrl) { this.updateFavicon(branding.faviconUrl); diff --git a/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-feeds-airgap-page.component.ts b/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-feeds-airgap-page.component.ts index 7c6b0bf62..b49492299 100644 --- a/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-feeds-airgap-page.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-feeds-airgap-page.component.ts @@ -61,18 +61,18 @@ type FeedsAirgapAction = 'import' | 'export' | null;
Mirrors 2 - Synced 1 - Stale 1 - Errors 1 + Synced 2 + Stale 0 + Errors 0 Storage 12.4 GB
-
- Feeds degraded - Impact: BLOCKING - Mode: last-known-good snapshot (read-only) - corr-feed-8841 - Open incident +
+ Feeds healthy + Impact: none + Mode: live mirrors (read-write) + corr-feed-ok + Open freshness details
@@ -92,10 +92,10 @@ type FeedsAirgapAction = 'import' | 'export' | null; NVD Mirror https://nvd.nist.gov - 08:10 UTC - Stale 3h12m - WARN - BLOCKING + 11:58 UTC + Fresh + OK + INFO OSV Mirror @@ -216,10 +216,7 @@ type FeedsAirgapAction = 'import' | 'export' | null; } .status-banner { - border: 1px solid var(--color-status-warning-text); border-radius: var(--radius-md); - background: var(--color-status-warning-bg); - color: var(--color-status-warning-text); padding: 0.45rem 0.55rem; display: flex; gap: 0.35rem; @@ -228,6 +225,12 @@ type FeedsAirgapAction = 'import' | 'export' | null; font-size: 0.73rem; } + .status-banner--healthy { + border: 1px solid rgba(16, 185, 129, 0.35); + background: rgba(16, 185, 129, 0.12); + color: var(--color-status-success-border); + } + .status-banner code { font-size: 0.68rem; border: 1px solid var(--color-border-primary); diff --git a/src/Web/StellaOps.Web/src/app/features/trust-admin/trust-admin.component.spec.ts b/src/Web/StellaOps.Web/src/app/features/trust-admin/trust-admin.component.spec.ts index 963d1ca2b..bc9f6bde2 100644 --- a/src/Web/StellaOps.Web/src/app/features/trust-admin/trust-admin.component.spec.ts +++ b/src/Web/StellaOps.Web/src/app/features/trust-admin/trust-admin.component.spec.ts @@ -3,7 +3,7 @@ import { provideRouter, Router } from '@angular/router'; import { of } from 'rxjs'; import { TRUST_API, type TrustApi } from '../../core/api/trust.client'; -import type { TrustDashboardSummary } from '../../core/api/trust.models'; +import type { TrustAdministrationOverview } from '../../core/api/trust.models'; import { TrustAdminComponent } from './trust-admin.component'; describe('TrustAdminComponent', () => { @@ -12,51 +12,33 @@ describe('TrustAdminComponent', () => { let router: Router; let trustApi: jasmine.SpyObj; - const dashboardSummaryFixture: TrustDashboardSummary = { - keys: { - total: 12, - active: 9, - expiringSoon: 2, - expired: 1, - revoked: 0, - pendingRotation: 1, + const overviewFixture: TrustAdministrationOverview = { + inventory: { + keys: 12, + issuers: 8, + certificates: 5, }, - issuers: { - total: 8, - fullTrust: 3, - partialTrust: 3, - minimalTrust: 1, - untrusted: 1, - blocked: 0, - averageTrustScore: 86.4, - }, - certificates: { - total: 5, - valid: 4, - expiringSoon: 1, - expired: 0, - revoked: 0, - invalidChains: 0, - }, - recentEvents: [], - expiryAlerts: [ + signals: [ { - keyId: 'key-001', - keyName: 'Attestation Key', - expiresAt: '2026-03-01T00:00:00Z', - daysUntilExpiry: 19, - severity: 'warning', - purpose: 'attestation', - suggestedAction: 'Rotate key', + signalId: 'certificate-expiry', + status: 'warning', + message: 'One certificate expires within 10 days.', + }, + { + signalId: 'transparency-log', + status: 'healthy', + message: 'Transparency log witness is reachable.', }, ], + legacyAliases: [], + evidenceConsumerPath: '/evidence/overview', }; beforeEach(async () => { trustApi = jasmine.createSpyObj('TrustApi', [ - 'getDashboardSummary', + 'getAdministrationOverview', ]); - trustApi.getDashboardSummary.and.returnValue(of(dashboardSummaryFixture)); + trustApi.getAdministrationOverview.and.returnValue(of(overviewFixture)); await TestBed.configureTestingModule({ imports: [TrustAdminComponent], @@ -71,19 +53,19 @@ describe('TrustAdminComponent', () => { component = fixture.componentInstance; }); - it('loads the trust dashboard summary on init', () => { + it('loads the trust administration overview on init', () => { fixture.detectChanges(); - expect(trustApi.getDashboardSummary).toHaveBeenCalledTimes(1); - expect(component.summary()).toEqual(dashboardSummaryFixture); + expect(trustApi.getAdministrationOverview).toHaveBeenCalledTimes(1); + expect(component.overview()).toEqual(overviewFixture); expect(component.alertCount()).toBe(1); }); - it('renders the watchlist tab in the trust shell', () => { + it('renders the overview tab in the trust shell', () => { fixture.detectChanges(); const text = fixture.nativeElement.textContent as string; - expect(text).toContain('Watchlist'); + expect(text).toContain('Overview'); expect(text).toContain('Trust Management'); }); @@ -97,4 +79,15 @@ describe('TrustAdminComponent', () => { expect(component.activeTab()).toBe('watchlist'); }); + + it('uses the overview tab on the base trust-signing route', () => { + Object.defineProperty(router, 'url', { + configurable: true, + get: () => '/setup/trust-signing', + }); + + fixture.detectChanges(); + + expect(component.activeTab()).toBe('overview'); + }); }); diff --git a/src/Web/StellaOps.Web/src/app/features/trust-admin/trust-admin.component.ts b/src/Web/StellaOps.Web/src/app/features/trust-admin/trust-admin.component.ts index f80e9db46..72222be05 100644 --- a/src/Web/StellaOps.Web/src/app/features/trust-admin/trust-admin.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/trust-admin/trust-admin.component.ts @@ -11,10 +11,11 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { filter } from 'rxjs'; import { TRUST_API, TrustApi } from '../../core/api/trust.client'; -import { TrustDashboardSummary } from '../../core/api/trust.models'; +import { TrustAdministrationOverview } from '../../core/api/trust.models'; import { GlossaryTooltipDirective } from '../../shared/directives/glossary-tooltip.directive'; export type TrustAdminTab = + | 'overview' | 'keys' | 'issuers' | 'certificates' @@ -24,6 +25,7 @@ export type TrustAdminTab = | 'incidents' | 'analytics'; const TRUST_ADMIN_TABS: readonly TrustAdminTab[] = [ + 'overview', 'keys', 'issuers', 'certificates', @@ -65,16 +67,15 @@ const TRUST_ADMIN_TABS: readonly TrustAdminTab[] = [
Loading dashboard summary...
} @else if (error()) {
{{ error() }}
- } @else if (summary()) { + } @else if (overview()) {
K
- {{ summary()!.keys.total }} + {{ overview()!.inventory.keys }} Signing Keys - {{ summary()!.keys.active }} active, - {{ summary()!.keys.expiringSoon }} expiring soon + Administration inventory projection
@@ -82,10 +83,10 @@ const TRUST_ADMIN_TABS: readonly TrustAdminTab[] = [
I
- {{ summary()!.issuers.total }} + {{ overview()!.inventory.issuers }} Trusted Issuers - Avg score: {{ summary()!.issuers.averageTrustScore | number:'1.1-1' }} + Routed from live administration projection
@@ -93,11 +94,10 @@ const TRUST_ADMIN_TABS: readonly TrustAdminTab[] = [
C
- {{ summary()!.certificates.total }} + {{ overview()!.inventory.certificates }} Certificates - {{ summary()!.certificates.valid }} valid, - {{ summary()!.certificates.expiringSoon }} expiring + Evidence and issuer trust consumers stay linked
@@ -106,7 +106,7 @@ const TRUST_ADMIN_TABS: readonly TrustAdminTab[] = [
!
{{ alertCount() }} - Expiry Alerts + Attention Signals {{ criticalAlertCount() }} critical @@ -117,6 +117,16 @@ const TRUST_ADMIN_TABS: readonly TrustAdminTab[] = [