Sidebar 5-group restructure + demo data badges + audit emission infrastructure

Sprint 4 — Sidebar restructure (S4-T01+T02):
  5 groups: Release Control, Security, Operations, Audit & Evidence, Setup & Admin
  Groups 4+5 collapsed by default for new users
  Operations extracted from Release Control into own group
  Audit extracted from Security into own group
  groupOrder and resolveMenuGroupLabel updated
  Approvals badge moved to section-level

Sprint 2 — Demo data badges (S2-T04+T05):
  Backend: isDemo=true on all compatibility/seed responses in
    PackAdapterEndpoints, QuotaCompatibilityEndpoints, VulnerabilitiesController
  Frontend: "(Demo)" badges on Usage & Limits page quotas
  Frontend: "(Demo)" badges on triage artifact list when seed data
  New PlatformItemResponse/PlatformListResponse with IsDemo field

Sprint 6 — Audit emission infrastructure (S6-T01+T02):
  New shared library: src/__Libraries/StellaOps.Audit.Emission/
    - AuditActionAttribute: [AuditAction("module", "action")] endpoint tag
    - AuditActionFilter: IEndpointFilter that auto-emits UnifiedAuditEvent
    - HttpAuditEventEmitter: POSTs to Timeline /api/v1/audit/ingest
    - Single-line DI: services.AddAuditEmission(configuration)
  Timeline service: POST /api/v1/audit/ingest ingestion endpoint
    - IngestAuditEventStore: 10k-event ring buffer
    - CompositeUnifiedAuditEventProvider: merges HTTP-polled + ingested
  Documentation: docs/modules/audit/AUDIT_EMISSION_GUIDE.md

Angular build: 0 errors. .NET builds: 0 errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-16 14:48:18 +02:00
parent 66d53f1505
commit 189171c594
25 changed files with 1295 additions and 121 deletions

View File

@@ -352,6 +352,7 @@ export class MockVulnerabilityApiService implements VulnerabilityApi {
hasMore: offset + items.length < total,
etag: '"vuln-list-v1"',
traceId,
isDemo: true,
}).pipe(delay(200));
}

View File

@@ -89,6 +89,8 @@ export interface VulnerabilitiesResponse {
readonly etag?: string;
/** Trace ID for the request. */
readonly traceId?: string;
/** Whether the response contains demo/seed data. */
readonly isDemo?: boolean;
}
/**

View File

@@ -13,12 +13,12 @@ import { RouterLink } from '@angular/router';
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="usage-settings">
<h1 class="page-title">Usage & Limits</h1>
<h1 class="page-title">Usage & Limits <span class="demo-badge">(Demo)</span></h1>
<p class="page-subtitle">Monitor usage and configure quotas</p>
<div class="usage-grid">
<div class="usage-card">
<h3>Scans</h3>
<h3>Scans <span class="demo-chip">(Demo)</span></h3>
<div class="usage-bar">
<div class="usage-bar__fill" style="width: 65%"></div>
</div>
@@ -26,7 +26,7 @@ import { RouterLink } from '@angular/router';
</div>
<div class="usage-card">
<h3>Storage</h3>
<h3>Storage <span class="demo-chip">(Demo)</span></h3>
<div class="usage-bar">
<div class="usage-bar__fill" style="width: 42%"></div>
</div>
@@ -34,7 +34,7 @@ import { RouterLink } from '@angular/router';
</div>
<div class="usage-card">
<h3>Evidence Packets</h3>
<h3>Evidence Packets <span class="demo-chip">(Demo)</span></h3>
<div class="usage-bar">
<div class="usage-bar__fill" style="width: 28%"></div>
</div>
@@ -42,7 +42,7 @@ import { RouterLink } from '@angular/router';
</div>
<div class="usage-card">
<h3>API Requests</h3>
<h3>API Requests <span class="demo-chip">(Demo)</span></h3>
<div class="usage-bar">
<div class="usage-bar__fill" style="width: 15%"></div>
</div>
@@ -104,6 +104,23 @@ import { RouterLink } from '@angular/router';
text-decoration: none;
}
.btn--secondary { background: var(--color-surface-secondary); border: 1px solid var(--color-border-primary); }
.demo-badge {
display: inline-block;
padding: 0.125rem 0.5rem;
font-size: 0.75rem;
font-weight: var(--font-weight-semibold);
color: var(--color-warning, #b45309);
background: var(--color-warning-bg, #fef3c7);
border-radius: var(--radius-sm, 4px);
vertical-align: middle;
margin-left: 0.5rem;
}
.demo-chip {
font-size: 0.625rem;
font-weight: normal;
color: var(--color-warning, #b45309);
opacity: 0.85;
}
`]
})
export class UsageSettingsPageComponent {}

View File

@@ -1,7 +1,12 @@
<section class="triage-artifacts" data-testid="triage-artifacts-page">
<header class="triage-artifacts__header">
<div>
<h1>Artifact workspace</h1>
<h1>
Artifact workspace
@if (isDemo()) {
<span class="demo-badge">(Demo)</span>
}
</h1>
<p class="triage-artifacts__subtitle">
Triage live artifacts by lane, then open a single evidence-first decision workspace.
</p>
@@ -165,6 +170,9 @@
</td>
<td class="triage-table__td">
<code class="artifact-id">{{ row.artifactId }}</code>
@if (isDemo()) {
<span class="demo-chip">(Demo)</span>
}
@if (row.readyToDeploy) {
<span class="ready-pill" title="Signed evidence is available and no open findings remain.">
Ready to deploy

View File

@@ -370,6 +370,26 @@
font-size: var(--font-size-sm);
}
.demo-badge {
display: inline-block;
padding: 0.125rem 0.5rem;
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semibold);
color: var(--color-status-warning-text, #b45309);
background: var(--color-status-warning-bg, #fef3c7);
border-radius: var(--radius-sm);
vertical-align: middle;
margin-left: var(--space-2);
}
.demo-chip {
margin-left: var(--space-1);
font-size: var(--font-size-xs);
font-weight: normal;
color: var(--color-status-warning-text, #b45309);
opacity: 0.85;
}
.empty-state {
padding: var(--space-6);
color: var(--color-text-muted);

View File

@@ -73,6 +73,7 @@ export class TriageArtifactsComponent implements OnInit {
readonly loading = signal(false);
readonly error = signal<string | null>(null);
readonly vulnerabilities = signal<readonly Vulnerability[]>([]);
readonly isDemo = signal(false);
readonly search = signal('');
readonly environment = signal<EnvironmentHint | 'all'>('all');
@@ -203,6 +204,7 @@ export class TriageArtifactsComponent implements OnInit {
try {
const resp = await firstValueFrom(this.api.listVulnerabilities({ includeReachability: true }));
this.vulnerabilities.set(resp.items);
this.isDemo.set(resp.isDemo === true);
this.pruneSelection();
} catch (err) {
this.error.set(err instanceof Error ? err.message : 'Failed to load vulnerabilities');

View File

@@ -624,11 +624,11 @@ export class AppSidebarComponent implements AfterViewInit {
private readonly pendingApprovalsBadgeLoading = signal(false);
/**
* Navigation sections - pre-alpha canonical IA.
* Root modules: Mission Control, Releases, Security, Evidence, Ops, Setup.
* Navigation sections - canonical 5-group IA.
* Groups: Release Control, Security, Operations, Audit & Evidence, Setup & Admin.
*/
readonly navSections: NavSection[] = [
// ── Release Control ──────────────────────────────────────────────
// ── Group 1: Release Control ─────────────────────────────────────
{
id: 'dashboard',
label: 'Dashboard',
@@ -668,67 +668,70 @@ export class AppSidebarComponent implements AfterViewInit {
route: '/releases/health',
icon: 'activity',
},
{
id: 'rel-approvals',
label: 'Approvals',
route: '/releases/approvals',
icon: 'check-circle',
badge: 0,
requireAnyScope: [
StellaOpsScopes.RELEASE_PUBLISH,
StellaOpsScopes.POLICY_REVIEW,
StellaOpsScopes.POLICY_APPROVE,
StellaOpsScopes.EXCEPTION_APPROVE,
],
},
{
id: 'rel-promotions',
label: 'Promotions',
route: '/releases/promotions',
icon: 'git-merge',
requireAnyScope: [
StellaOpsScopes.RELEASE_READ,
StellaOpsScopes.RELEASE_WRITE,
StellaOpsScopes.RELEASE_PUBLISH,
],
},
{ id: 'rel-hotfix-list', label: 'Hotfixes', route: '/releases/hotfixes', icon: 'zap' },
{ id: 'rel-deployments', label: 'Deployments', route: '/releases/deployments', icon: 'upload-cloud' },
],
},
{
id: 'ops',
label: 'Operations',
icon: 'settings',
route: '/ops/operations',
id: 'promotions',
label: 'Promotions',
icon: 'git-merge',
route: '/releases/promotions',
menuGroupId: 'release-control',
menuGroupLabel: 'Release Control',
sparklineData$: () => this.doctorTrendService.platformTrend(),
requireAnyScope: [
StellaOpsScopes.UI_ADMIN,
StellaOpsScopes.ORCH_READ,
StellaOpsScopes.ORCH_OPERATE,
StellaOpsScopes.HEALTH_READ,
StellaOpsScopes.NOTIFY_VIEWER,
StellaOpsScopes.POLICY_READ,
],
children: [
{ id: 'ops-jobs', label: 'Scheduled Jobs', route: '/ops/operations/jobengine', icon: 'clock' },
{ id: 'ops-signals', label: 'Signals', route: '/ops/operations/signals', icon: 'radio' },
{ id: 'ops-offline-kit', label: 'Offline Kit', route: '/ops/operations/offline-kit', icon: 'download-cloud' },
{ id: 'ops-environments', label: 'Environments', route: '/ops/operations/environments', icon: 'globe' },
{ id: 'ops-policy', label: 'Policy', route: '/ops/policy', icon: 'shield' },
{ id: 'ops-platform-setup', label: 'Platform Setup', route: '/ops/platform-setup', icon: 'cog' },
{ id: 'ops-notifications', label: 'Notifications', route: '/ops/operations/notifications', icon: 'bell' },
StellaOpsScopes.RELEASE_READ,
StellaOpsScopes.RELEASE_WRITE,
StellaOpsScopes.RELEASE_PUBLISH,
],
},
// ── Security & Audit ─────────────────────────────────────────────
{
id: 'approvals',
label: 'Approvals',
icon: 'check-circle',
route: '/releases/approvals',
menuGroupId: 'release-control',
menuGroupLabel: 'Release Control',
badge$: () => this.pendingApprovalsCount(),
requireAnyScope: [
StellaOpsScopes.RELEASE_PUBLISH,
StellaOpsScopes.POLICY_REVIEW,
StellaOpsScopes.POLICY_APPROVE,
StellaOpsScopes.EXCEPTION_APPROVE,
],
},
{
id: 'hotfixes',
label: 'Hotfixes',
icon: 'zap',
route: '/releases/hotfixes',
menuGroupId: 'release-control',
menuGroupLabel: 'Release Control',
requireAnyScope: [
StellaOpsScopes.RELEASE_READ,
StellaOpsScopes.RELEASE_WRITE,
],
},
// ── Group 2: Security ────────────────────────────────────────────
{
id: 'vulnerabilities',
label: 'Vulnerabilities',
icon: 'list',
route: '/triage/artifacts',
menuGroupId: 'security',
menuGroupLabel: 'Security',
requireAnyScope: [
StellaOpsScopes.SCANNER_READ,
StellaOpsScopes.FINDINGS_READ,
StellaOpsScopes.VULN_VIEW,
],
},
{
id: 'security-posture',
label: 'Security Posture',
icon: 'shield',
route: '/security',
menuGroupId: 'security-audit',
menuGroupLabel: 'Security & Audit',
menuGroupId: 'security',
menuGroupLabel: 'Security',
sparklineData$: () => this.doctorTrendService.securityTrend(),
requireAnyScope: [
StellaOpsScopes.SCANNER_READ,
@@ -740,28 +743,145 @@ export class AppSidebarComponent implements AfterViewInit {
StellaOpsScopes.VULN_VIEW,
],
children: [
{ id: 'sec-posture', label: 'Posture', route: '/security', icon: 'shield' },
{ id: 'sec-triage', label: 'Vulnerabilities', route: '/triage/artifacts', icon: 'list' },
{ id: 'sec-supply-chain', label: 'Supply-Chain Data', route: '/security/supply-chain-data', icon: 'graph' },
{ id: 'sec-reachability', label: 'Reachability', route: '/security/reachability', icon: 'cpu' },
{ id: 'sec-unknowns', label: 'Unknowns', route: '/security/unknowns', icon: 'help-circle' },
{
id: 'sec-scan-image',
label: 'Scan Image',
route: '/security/scan',
icon: 'search',
requireAnyScope: [StellaOpsScopes.SCANNER_READ],
},
{ id: 'sec-reports', label: 'Reports', route: '/security/reports', icon: 'book-open' },
],
},
{
id: 'audit',
label: 'Audit',
id: 'scan-image',
label: 'Scan Image',
icon: 'search',
route: '/security/scan',
menuGroupId: 'security',
menuGroupLabel: 'Security',
requireAnyScope: [StellaOpsScopes.SCANNER_READ],
},
{
id: 'sec-reports',
label: 'Reports',
icon: 'book-open',
route: '/security/reports',
menuGroupId: 'security',
menuGroupLabel: 'Security',
requireAnyScope: [
StellaOpsScopes.SCANNER_READ,
StellaOpsScopes.FINDINGS_READ,
],
},
// ── Group 3: Operations ──────────────────────────────────────────
{
id: 'ops',
label: 'Operations Hub',
icon: 'settings',
route: '/ops/operations',
menuGroupId: 'operations',
menuGroupLabel: 'Operations',
sparklineData$: () => this.doctorTrendService.platformTrend(),
requireAnyScope: [
StellaOpsScopes.UI_ADMIN,
StellaOpsScopes.ORCH_READ,
StellaOpsScopes.ORCH_OPERATE,
StellaOpsScopes.HEALTH_READ,
StellaOpsScopes.NOTIFY_VIEWER,
StellaOpsScopes.POLICY_READ,
],
},
{
id: 'ops-jobs',
label: 'Scheduled Jobs',
icon: 'clock',
route: '/ops/operations/jobengine',
menuGroupId: 'operations',
menuGroupLabel: 'Operations',
requireAnyScope: [
StellaOpsScopes.ORCH_READ,
StellaOpsScopes.ORCH_OPERATE,
],
},
{
id: 'ops-signals',
label: 'Signals',
icon: 'radio',
route: '/ops/operations/signals',
menuGroupId: 'operations',
menuGroupLabel: 'Operations',
requireAnyScope: [
StellaOpsScopes.ORCH_READ,
StellaOpsScopes.HEALTH_READ,
],
},
{
id: 'ops-environments',
label: 'Environments',
icon: 'globe',
route: '/ops/operations/environments',
menuGroupId: 'operations',
menuGroupLabel: 'Operations',
requireAnyScope: [
StellaOpsScopes.ORCH_READ,
StellaOpsScopes.ORCH_OPERATE,
],
},
{
id: 'ops-policy',
label: 'Policy',
icon: 'shield',
route: '/ops/policy',
menuGroupId: 'operations',
menuGroupLabel: 'Operations',
requireAnyScope: [StellaOpsScopes.POLICY_READ],
},
{
id: 'ops-diagnostics',
label: 'Diagnostics',
icon: 'stethoscope',
route: '/ops/operations/doctor',
menuGroupId: 'operations',
menuGroupLabel: 'Operations',
requireAnyScope: [StellaOpsScopes.HEALTH_READ, StellaOpsScopes.UI_ADMIN],
},
{
id: 'ops-notifications',
label: 'Notifications',
icon: 'bell',
route: '/ops/operations/notifications',
menuGroupId: 'operations',
menuGroupLabel: 'Operations',
requireAnyScope: [StellaOpsScopes.NOTIFY_VIEWER],
},
{
id: 'ops-feeds-airgap',
label: 'Feeds & Airgap',
icon: 'download-cloud',
route: '/ops/operations/feeds-airgap',
menuGroupId: 'operations',
menuGroupLabel: 'Operations',
requireAnyScope: [
StellaOpsScopes.ADVISORY_READ,
StellaOpsScopes.VEX_READ,
],
},
{
id: 'ops-offline-kit',
label: 'Offline Kit',
icon: 'download-cloud',
route: '/ops/operations/offline-kit',
menuGroupId: 'operations',
menuGroupLabel: 'Operations',
requireAnyScope: [
StellaOpsScopes.UI_ADMIN,
StellaOpsScopes.ORCH_OPERATE,
],
},
// ── Group 4: Audit & Evidence ────────────────────────────────────
{
id: 'evidence-overview',
label: 'Evidence Overview',
icon: 'file-text',
route: '/evidence/overview',
menuGroupId: 'security-audit',
menuGroupLabel: 'Security & Audit',
menuGroupId: 'audit-evidence',
menuGroupLabel: 'Audit & Evidence',
requireAnyScope: [
StellaOpsScopes.RELEASE_READ,
StellaOpsScopes.POLICY_AUDIT,
@@ -769,44 +889,132 @@ export class AppSidebarComponent implements AfterViewInit {
StellaOpsScopes.SIGNER_READ,
StellaOpsScopes.VEX_EXPORT,
],
children: [
{ id: 'ev-overview', label: 'Overview', route: '/evidence/overview', icon: 'home' },
{ id: 'ev-capsules', label: 'Decision Capsules', route: '/evidence/capsules', icon: 'archive' },
{ id: 'ev-verify', label: 'Replay & Verify', route: '/evidence/verify-replay', icon: 'refresh' },
{ id: 'ev-exports', label: 'Export Center', route: '/evidence/exports', icon: 'download' },
{ id: 'ev-audit', label: 'Logs', route: '/evidence/audit-log', icon: 'book-open' },
{ id: 'ev-bundles', label: 'Bundles', route: '/triage/audit-bundles', icon: 'archive' },
},
{
id: 'evidence-capsules',
label: 'Decision Capsules',
icon: 'archive',
route: '/evidence/capsules',
menuGroupId: 'audit-evidence',
menuGroupLabel: 'Audit & Evidence',
requireAnyScope: [
StellaOpsScopes.RELEASE_READ,
StellaOpsScopes.POLICY_AUDIT,
],
},
// ── Platform & Setup ─────────────────────────────────────────────
{
id: 'setup',
label: 'Setup',
icon: 'server',
route: '/setup/system',
menuGroupId: 'platform-setup',
menuGroupLabel: 'Platform & Setup',
id: 'evidence-verify',
label: 'Replay & Verify',
icon: 'refresh',
route: '/evidence/verify-replay',
menuGroupId: 'audit-evidence',
menuGroupLabel: 'Audit & Evidence',
requireAnyScope: [
StellaOpsScopes.RELEASE_READ,
StellaOpsScopes.SIGNER_READ,
],
},
{
id: 'evidence-exports',
label: 'Export Center',
icon: 'download',
route: '/evidence/exports',
menuGroupId: 'audit-evidence',
menuGroupLabel: 'Audit & Evidence',
requireAnyScope: [
StellaOpsScopes.VEX_EXPORT,
StellaOpsScopes.RELEASE_READ,
],
},
{
id: 'evidence-audit-log',
label: 'Audit Log',
icon: 'book-open',
route: '/evidence/audit-log',
menuGroupId: 'audit-evidence',
menuGroupLabel: 'Audit & Evidence',
requireAnyScope: [
StellaOpsScopes.POLICY_AUDIT,
StellaOpsScopes.AUTHORITY_AUDIT_READ,
],
},
{
id: 'evidence-bundles',
label: 'Bundles',
icon: 'archive',
route: '/triage/audit-bundles',
menuGroupId: 'audit-evidence',
menuGroupLabel: 'Audit & Evidence',
requireAnyScope: [
StellaOpsScopes.RELEASE_READ,
StellaOpsScopes.POLICY_AUDIT,
],
},
// ── Group 5: Setup & Admin ───────────────────────────────────────
{
id: 'setup-topology',
label: 'Topology',
icon: 'globe',
route: '/setup/topology/overview',
menuGroupId: 'setup-admin',
menuGroupLabel: 'Setup & Admin',
requireAnyScope: [
StellaOpsScopes.UI_ADMIN,
StellaOpsScopes.RELEASE_READ,
StellaOpsScopes.ORCH_READ,
StellaOpsScopes.ORCH_OPERATE,
],
children: [
{ id: 'setup-topology', label: 'Topology', route: '/setup/topology/overview', icon: 'globe' },
{
id: 'setup-diagnostics',
label: 'Diagnostics',
route: '/ops/operations/doctor',
icon: 'stethoscope',
requireAnyScope: [StellaOpsScopes.HEALTH_READ, StellaOpsScopes.UI_ADMIN],
},
{ id: 'setup-integrations', label: 'Integrations', route: '/setup/integrations', icon: 'plug' },
{ id: 'setup-iam', label: 'Identity & Access', route: '/setup/identity-access', icon: 'user' },
{ id: 'setup-trust-signing', label: 'Trust & Signing', route: '/setup/trust-signing', icon: 'shield' },
{ id: 'setup-branding', label: 'Tenant & Branding', route: '/setup/tenant-branding', icon: 'paintbrush' },
},
{
id: 'setup-integrations',
label: 'Integrations',
icon: 'plug',
route: '/setup/integrations',
menuGroupId: 'setup-admin',
menuGroupLabel: 'Setup & Admin',
requireAnyScope: [
StellaOpsScopes.UI_ADMIN,
StellaOpsScopes.ORCH_OPERATE,
],
},
{
id: 'setup-iam',
label: 'Identity & Access',
icon: 'user',
route: '/setup/identity-access',
menuGroupId: 'setup-admin',
menuGroupLabel: 'Setup & Admin',
requireAnyScope: [StellaOpsScopes.UI_ADMIN],
},
{
id: 'setup-trust-signing',
label: 'Trust & Signing',
icon: 'shield',
route: '/setup/trust-signing',
menuGroupId: 'setup-admin',
menuGroupLabel: 'Setup & Admin',
requireAnyScope: [
StellaOpsScopes.UI_ADMIN,
StellaOpsScopes.SIGNER_READ,
],
},
{
id: 'setup-branding',
label: 'Tenant & Branding',
icon: 'paintbrush',
route: '/setup/tenant-branding',
menuGroupId: 'setup-admin',
menuGroupLabel: 'Setup & Admin',
requireAnyScope: [StellaOpsScopes.UI_ADMIN],
},
{
id: 'setup-system',
label: 'System Settings',
icon: 'server',
route: '/setup/system',
menuGroupId: 'setup-admin',
menuGroupLabel: 'Setup & Admin',
requireAnyScope: [StellaOpsScopes.UI_ADMIN],
},
];
/** Navigation sections filtered by user scopes */
@@ -828,7 +1036,7 @@ export class AppSidebarComponent implements AfterViewInit {
/** Menu groups rendered in deterministic order for scanability */
readonly displaySectionGroups = computed<NavSectionGroup[]>(() => {
const orderedGroups = new Map<string, NavSectionGroup>();
const groupOrder = ['release-control', 'security-audit', 'platform-setup', 'misc'];
const groupOrder = ['release-control', 'security', 'operations', 'audit-evidence', 'setup-admin', 'misc'];
for (const groupId of groupOrder) {
orderedGroups.set(groupId, {
@@ -900,10 +1108,14 @@ export class AppSidebarComponent implements AfterViewInit {
switch (groupId) {
case 'release-control':
return 'Release Control';
case 'security-audit':
return 'Security & Audit';
case 'platform-setup':
return 'Platform & Setup';
case 'security':
return 'Security';
case 'operations':
return 'Operations';
case 'audit-evidence':
return 'Audit & Evidence';
case 'setup-admin':
return 'Setup & Admin';
default:
return 'Global Menu';
}

View File

@@ -10,7 +10,7 @@ const STORAGE_KEY = 'stellaops.sidebar.preferences';
const DEFAULTS: SidebarPreferences = {
sidebarCollapsed: false,
collapsedGroups: [],
collapsedGroups: ['audit-evidence', 'setup-admin'],
collapsedSections: [],
};