Align live titles and trust setup overview
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
99
src/Web/StellaOps.Web/src/app/core/api/trust.client.spec.ts
Normal file
99
src/Web/StellaOps.Web/src/app/core/api/trust.client.spec.ts
Normal file
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<TrustDashboardSummary>;
|
||||
getAdministrationOverview(): Observable<TrustAdministrationOverview>;
|
||||
|
||||
// Keys
|
||||
listKeys(params?: ListKeysParams): Observable<PagedResult<SigningKey>>;
|
||||
@@ -97,6 +99,25 @@ export interface TrustApi {
|
||||
|
||||
export const TRUST_API = new InjectionToken<TrustApi>('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<TrustApi>('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<TrustDashboardSummary> {
|
||||
return this.http.get<TrustDashboardSummary>(`${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<TrustAdministrationOverview> {
|
||||
return this.http.get<AdministrationTrustOverviewDto>(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<void>(`${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<string, unknown>): 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<void> {
|
||||
return of(undefined).pipe(delay(100));
|
||||
}
|
||||
|
||||
getAdministrationOverview(): Observable<TrustAdministrationOverview> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -61,18 +61,18 @@ type FeedsAirgapAction = 'import' | 'export' | null;
|
||||
|
||||
<section class="summary">
|
||||
<span>Mirrors 2</span>
|
||||
<span>Synced 1</span>
|
||||
<span>Stale 1</span>
|
||||
<span>Errors 1</span>
|
||||
<span>Synced 2</span>
|
||||
<span>Stale 0</span>
|
||||
<span>Errors 0</span>
|
||||
<span>Storage 12.4 GB</span>
|
||||
</section>
|
||||
|
||||
<section class="status-banner">
|
||||
<strong>Feeds degraded</strong>
|
||||
<span>Impact: BLOCKING</span>
|
||||
<span>Mode: last-known-good snapshot (read-only)</span>
|
||||
<code>corr-feed-8841</code>
|
||||
<a [routerLink]="feedsFreshnessPath">Open incident</a>
|
||||
<section class="status-banner status-banner--healthy">
|
||||
<strong>Feeds healthy</strong>
|
||||
<span>Impact: none</span>
|
||||
<span>Mode: live mirrors (read-write)</span>
|
||||
<code>corr-feed-ok</code>
|
||||
<a [routerLink]="feedsFreshnessPath">Open freshness details</a>
|
||||
</section>
|
||||
|
||||
<article class="panel">
|
||||
@@ -92,10 +92,10 @@ type FeedsAirgapAction = 'import' | 'export' | null;
|
||||
<tr>
|
||||
<td>NVD Mirror</td>
|
||||
<td>https://nvd.nist.gov</td>
|
||||
<td>08:10 UTC</td>
|
||||
<td>Stale 3h12m</td>
|
||||
<td>WARN</td>
|
||||
<td>BLOCKING</td>
|
||||
<td>11:58 UTC</td>
|
||||
<td>Fresh</td>
|
||||
<td>OK</td>
|
||||
<td>INFO</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>OSV Mirror</td>
|
||||
@@ -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);
|
||||
|
||||
@@ -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<TrustApi>;
|
||||
|
||||
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>('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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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[] = [
|
||||
<div class="trust-admin__loading">Loading dashboard summary...</div>
|
||||
} @else if (error()) {
|
||||
<div class="trust-admin__error">{{ error() }}</div>
|
||||
} @else if (summary()) {
|
||||
} @else if (overview()) {
|
||||
<div class="trust-admin__summary">
|
||||
<div class="summary-card">
|
||||
<div class="summary-card__icon summary-card__icon--keys">K</div>
|
||||
<div class="summary-card__content">
|
||||
<span class="summary-card__value">{{ summary()!.keys.total }}</span>
|
||||
<span class="summary-card__value">{{ overview()!.inventory.keys }}</span>
|
||||
<span class="summary-card__label">Signing Keys</span>
|
||||
<span class="summary-card__detail">
|
||||
{{ summary()!.keys.active }} active,
|
||||
{{ summary()!.keys.expiringSoon }} expiring soon
|
||||
Administration inventory projection
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,10 +83,10 @@ const TRUST_ADMIN_TABS: readonly TrustAdminTab[] = [
|
||||
<div class="summary-card">
|
||||
<div class="summary-card__icon summary-card__icon--issuers">I</div>
|
||||
<div class="summary-card__content">
|
||||
<span class="summary-card__value">{{ summary()!.issuers.total }}</span>
|
||||
<span class="summary-card__value">{{ overview()!.inventory.issuers }}</span>
|
||||
<span class="summary-card__label">Trusted Issuers</span>
|
||||
<span class="summary-card__detail">
|
||||
Avg score: {{ summary()!.issuers.averageTrustScore | number:'1.1-1' }}
|
||||
Routed from live administration projection
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,11 +94,10 @@ const TRUST_ADMIN_TABS: readonly TrustAdminTab[] = [
|
||||
<div class="summary-card">
|
||||
<div class="summary-card__icon summary-card__icon--certs">C</div>
|
||||
<div class="summary-card__content">
|
||||
<span class="summary-card__value">{{ summary()!.certificates.total }}</span>
|
||||
<span class="summary-card__value">{{ overview()!.inventory.certificates }}</span>
|
||||
<span class="summary-card__label">Certificates</span>
|
||||
<span class="summary-card__detail">
|
||||
{{ summary()!.certificates.valid }} valid,
|
||||
{{ summary()!.certificates.expiringSoon }} expiring
|
||||
Evidence and issuer trust consumers stay linked
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -106,7 +106,7 @@ const TRUST_ADMIN_TABS: readonly TrustAdminTab[] = [
|
||||
<div class="summary-card__icon summary-card__icon--alerts">!</div>
|
||||
<div class="summary-card__content">
|
||||
<span class="summary-card__value">{{ alertCount() }}</span>
|
||||
<span class="summary-card__label">Expiry Alerts</span>
|
||||
<span class="summary-card__label">Attention Signals</span>
|
||||
<span class="summary-card__detail">
|
||||
{{ criticalAlertCount() }} critical
|
||||
</span>
|
||||
@@ -117,6 +117,16 @@ const TRUST_ADMIN_TABS: readonly TrustAdminTab[] = [
|
||||
</header>
|
||||
|
||||
<nav class="trust-admin__tabs" role="tablist">
|
||||
<a
|
||||
class="trust-admin__tab"
|
||||
[class.trust-admin__tab--active]="activeTab() === 'overview'"
|
||||
routerLink="overview"
|
||||
[queryParams]="{}"
|
||||
role="tab"
|
||||
[attr.aria-selected]="activeTab() === 'overview'"
|
||||
>
|
||||
Overview
|
||||
</a>
|
||||
<a
|
||||
class="trust-admin__tab"
|
||||
[class.trust-admin__tab--active]="activeTab() === 'keys'"
|
||||
@@ -125,8 +135,8 @@ const TRUST_ADMIN_TABS: readonly TrustAdminTab[] = [
|
||||
[attr.aria-selected]="activeTab() === 'keys'"
|
||||
>
|
||||
Signing Keys
|
||||
@if (summary()?.keys?.expiringSoon) {
|
||||
<span class="tab-badge tab-badge--warning">{{ summary()?.keys?.expiringSoon }}</span>
|
||||
@if (warningSignalCount()) {
|
||||
<span class="tab-badge tab-badge--warning">{{ warningSignalCount() }}</span>
|
||||
}
|
||||
</a>
|
||||
<a
|
||||
@@ -137,9 +147,6 @@ const TRUST_ADMIN_TABS: readonly TrustAdminTab[] = [
|
||||
[attr.aria-selected]="activeTab() === 'issuers'"
|
||||
>
|
||||
Trusted Issuers
|
||||
@if (summary()?.issuers?.blocked) {
|
||||
<span class="tab-badge tab-badge--danger">{{ summary()?.issuers?.blocked }}</span>
|
||||
}
|
||||
</a>
|
||||
<a
|
||||
class="trust-admin__tab"
|
||||
@@ -149,8 +156,8 @@ const TRUST_ADMIN_TABS: readonly TrustAdminTab[] = [
|
||||
[attr.aria-selected]="activeTab() === 'certificates'"
|
||||
>
|
||||
Certificates
|
||||
@if (summary()?.certificates?.expiringSoon) {
|
||||
<span class="tab-badge tab-badge--warning">{{ summary()?.certificates?.expiringSoon }}</span>
|
||||
@if (warningSignalCount()) {
|
||||
<span class="tab-badge tab-badge--warning">{{ warningSignalCount() }}</span>
|
||||
}
|
||||
</a>
|
||||
<a
|
||||
@@ -422,16 +429,19 @@ export class TrustAdminComponent implements OnInit {
|
||||
readonly loading = signal(false);
|
||||
readonly refreshing = signal(false);
|
||||
readonly error = signal<string | null>(null);
|
||||
readonly summary = signal<TrustDashboardSummary | null>(null);
|
||||
readonly activeTab = signal<TrustAdminTab>('keys');
|
||||
readonly overview = signal<TrustAdministrationOverview | null>(null);
|
||||
readonly activeTab = signal<TrustAdminTab>('overview');
|
||||
readonly workspaceLabel = signal<'Setup' | 'Administration'>('Setup');
|
||||
|
||||
// Computed
|
||||
readonly alertCount = computed(() => this.summary()?.expiryAlerts?.length ?? 0);
|
||||
readonly alertCount = computed(() =>
|
||||
(this.overview()?.signals ?? []).filter((signal) => signal.status !== 'healthy').length
|
||||
);
|
||||
readonly criticalAlertCount = computed(() =>
|
||||
(this.summary()?.expiryAlerts ?? []).filter(
|
||||
a => a.severity === 'critical'
|
||||
).length
|
||||
(this.overview()?.signals ?? []).filter((signal) => signal.status === 'critical').length
|
||||
);
|
||||
readonly warningSignalCount = computed(() =>
|
||||
(this.overview()?.signals ?? []).filter((signal) => signal.status === 'warning').length
|
||||
);
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -452,9 +462,9 @@ export class TrustAdminComponent implements OnInit {
|
||||
this.loading.set(true);
|
||||
this.error.set(null);
|
||||
|
||||
this.trustApi.getDashboardSummary().subscribe({
|
||||
next: (summary) => {
|
||||
this.summary.set(summary);
|
||||
this.trustApi.getAdministrationOverview().subscribe({
|
||||
next: (overview) => {
|
||||
this.overview.set(overview);
|
||||
this.loading.set(false);
|
||||
},
|
||||
error: (err) => {
|
||||
@@ -467,9 +477,9 @@ export class TrustAdminComponent implements OnInit {
|
||||
refreshDashboard(): void {
|
||||
this.refreshing.set(true);
|
||||
this.error.set(null);
|
||||
this.trustApi.getDashboardSummary().subscribe({
|
||||
next: (summary) => {
|
||||
this.summary.set(summary);
|
||||
this.trustApi.getAdministrationOverview().subscribe({
|
||||
next: (overview) => {
|
||||
this.overview.set(overview);
|
||||
this.error.set(null);
|
||||
this.refreshing.set(false);
|
||||
},
|
||||
@@ -485,14 +495,16 @@ export class TrustAdminComponent implements OnInit {
|
||||
const routeRoot = segments[0];
|
||||
const path = segments.includes('watchlist')
|
||||
? 'watchlist'
|
||||
: segments.at(-1) ?? 'keys';
|
||||
: segments.at(-1) === 'trust-signing'
|
||||
? 'overview'
|
||||
: segments.at(-1) ?? 'overview';
|
||||
|
||||
this.workspaceLabel.set(routeRoot === 'administration' ? 'Administration' : 'Setup');
|
||||
|
||||
if (TRUST_ADMIN_TABS.includes(path as TrustAdminTab)) {
|
||||
this.activeTab.set(path as TrustAdminTab);
|
||||
} else {
|
||||
this.activeTab.set('keys');
|
||||
this.activeTab.set('overview');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,16 @@ export const trustAdminRoutes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
loadComponent: () =>
|
||||
import('./signing-key-dashboard.component').then(
|
||||
(m) => m.SigningKeyDashboardComponent
|
||||
import('./trust-overview.component').then(
|
||||
(m) => m.TrustOverviewComponent
|
||||
),
|
||||
data: { requiredScopes: [StellaOpsScopes.SIGNER_READ] },
|
||||
},
|
||||
{
|
||||
path: 'overview',
|
||||
loadComponent: () =>
|
||||
import('./trust-overview.component').then(
|
||||
(m) => m.TrustOverviewComponent
|
||||
),
|
||||
data: { requiredScopes: [StellaOpsScopes.SIGNER_READ] },
|
||||
},
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-trust-overview',
|
||||
standalone: true,
|
||||
imports: [RouterLink],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<section class="trust-overview">
|
||||
<div class="trust-overview__panel">
|
||||
<h2>Administration Overview</h2>
|
||||
<p>
|
||||
This workspace is anchored to the live administration trust-signing projection exposed by the
|
||||
rebuilt platform. Use the tabs to move into specific inventory surfaces as they are aligned to the
|
||||
current backend contracts.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="trust-overview__grid">
|
||||
<article class="trust-overview__card">
|
||||
<h3>Signing Keys</h3>
|
||||
<p>Review the currently registered signing keys and rotation controls.</p>
|
||||
<a routerLink="keys">Open key inventory</a>
|
||||
</article>
|
||||
|
||||
<article class="trust-overview__card">
|
||||
<h3>Trusted Issuers</h3>
|
||||
<p>Inspect issuer onboarding and trust policy configuration from the canonical setup shell.</p>
|
||||
<a routerLink="issuers">Open issuer inventory</a>
|
||||
</article>
|
||||
|
||||
<article class="trust-overview__card">
|
||||
<h3>Certificates</h3>
|
||||
<p>Check certificate enrollment state and follow evidence consumers that depend on the trust chain.</p>
|
||||
<a routerLink="certificates">Open certificate inventory</a>
|
||||
</article>
|
||||
|
||||
<article class="trust-overview__card">
|
||||
<h3>Evidence</h3>
|
||||
<p>Cross-check trust-signing outputs against evidence and replay flows before promotions.</p>
|
||||
<a routerLink="/evidence/overview">Open evidence overview</a>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
`,
|
||||
styles: [`
|
||||
.trust-overview {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.trust-overview__panel,
|
||||
.trust-overview__card {
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-surface-primary);
|
||||
padding: 1rem 1.1rem;
|
||||
}
|
||||
|
||||
.trust-overview__panel h2,
|
||||
.trust-overview__card h3 {
|
||||
margin: 0 0 0.45rem;
|
||||
}
|
||||
|
||||
.trust-overview__panel p,
|
||||
.trust-overview__card p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.trust-overview__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.trust-overview__card {
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.trust-overview__card a {
|
||||
color: var(--color-brand-primary);
|
||||
text-decoration: none;
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
`],
|
||||
})
|
||||
export class TrustOverviewComponent {}
|
||||
@@ -3,57 +3,30 @@ import { provideRouter, Router } from '@angular/router';
|
||||
import { of, throwError } from 'rxjs';
|
||||
|
||||
import { TRUST_API, type TrustApi } from '../../app/core/api/trust.client';
|
||||
import type { TrustDashboardSummary } from '../../app/core/api/trust.models';
|
||||
import type { TrustAdministrationOverview } from '../../app/core/api/trust.models';
|
||||
import { TrustAdminComponent } from '../../app/features/trust-admin/trust-admin.component';
|
||||
import { trustAdminRoutes } from '../../app/features/trust-admin/trust-admin.routes';
|
||||
|
||||
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.',
|
||||
},
|
||||
{
|
||||
certificateId: 'cert-001',
|
||||
certificateName: 'Gateway mTLS',
|
||||
certificateType: 'mtls_server',
|
||||
expiresAt: '2026-02-20T00:00:00Z',
|
||||
daysUntilExpiry: 9,
|
||||
severity: 'critical',
|
||||
affectedServices: ['gateway'],
|
||||
signalId: 'transparency-log',
|
||||
status: 'critical',
|
||||
message: 'Witness connection is unavailable.',
|
||||
},
|
||||
],
|
||||
legacyAliases: [],
|
||||
evidenceConsumerPath: '/evidence/overview',
|
||||
};
|
||||
|
||||
describe('trust-scoring-dashboard-ui behavior', () => {
|
||||
@@ -63,8 +36,8 @@ describe('trust-scoring-dashboard-ui behavior', () => {
|
||||
let router: Router;
|
||||
|
||||
beforeEach(async () => {
|
||||
trustApi = jasmine.createSpyObj('TrustApi', ['getDashboardSummary']) as jasmine.SpyObj<TrustApi>;
|
||||
trustApi.getDashboardSummary.and.returnValue(of(dashboardSummaryFixture));
|
||||
trustApi = jasmine.createSpyObj('TrustApi', ['getAdministrationOverview']) as jasmine.SpyObj<TrustApi>;
|
||||
trustApi.getAdministrationOverview.and.returnValue(of(overviewFixture));
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TrustAdminComponent],
|
||||
@@ -79,13 +52,14 @@ describe('trust-scoring-dashboard-ui behavior', () => {
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('declares trust-admin routes for keys, issuers, certificates, watchlist, audit, airgap, incidents, and analytics', () => {
|
||||
it('declares trust-admin routes for overview, keys, issuers, certificates, watchlist, audit, airgap, incidents, and analytics', () => {
|
||||
const root = trustAdminRoutes.find((route) => route.path === '');
|
||||
expect(root).toBeDefined();
|
||||
|
||||
const childPaths = (root?.children ?? []).map((child) => child.path);
|
||||
expect(childPaths).toEqual([
|
||||
'',
|
||||
'overview',
|
||||
'keys',
|
||||
'issuers',
|
||||
'certificates',
|
||||
@@ -100,11 +74,11 @@ describe('trust-scoring-dashboard-ui behavior', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('loads dashboard summary and computes deterministic alert counters', () => {
|
||||
it('loads the administration overview and computes deterministic alert counters', () => {
|
||||
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(2);
|
||||
expect(component.criticalAlertCount()).toBe(1);
|
||||
|
||||
@@ -127,14 +101,17 @@ describe('trust-scoring-dashboard-ui behavior', () => {
|
||||
(component as any).setActiveTabFromUrl('/setup/trust-signing/watchlist/alerts?scope=tenant');
|
||||
expect(component.activeTab()).toBe('watchlist');
|
||||
|
||||
(component as any).setActiveTabFromUrl('/setup/trust-signing');
|
||||
expect(component.activeTab()).toBe('overview');
|
||||
|
||||
(component as any).setActiveTabFromUrl('/admin/trust/not-a-tab');
|
||||
expect(component.activeTab()).toBe('keys');
|
||||
expect(component.activeTab()).toBe('overview');
|
||||
});
|
||||
|
||||
it('clears stale error state when refresh succeeds after a failed load', () => {
|
||||
trustApi.getDashboardSummary.and.returnValues(
|
||||
trustApi.getAdministrationOverview.and.returnValues(
|
||||
throwError(() => new Error('Dashboard unavailable')),
|
||||
of(dashboardSummaryFixture)
|
||||
of(overviewFixture)
|
||||
);
|
||||
|
||||
fixture.detectChanges();
|
||||
@@ -143,8 +120,8 @@ describe('trust-scoring-dashboard-ui behavior', () => {
|
||||
component.refreshDashboard();
|
||||
|
||||
expect(component.error()).toBeNull();
|
||||
expect(component.summary()).toEqual(dashboardSummaryFixture);
|
||||
expect(component.overview()).toEqual(overviewFixture);
|
||||
expect(component.refreshing()).toBeFalse();
|
||||
expect(trustApi.getDashboardSummary).toHaveBeenCalledTimes(2);
|
||||
expect(trustApi.getAdministrationOverview).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user