feat(web-ui): align evidence home router and trust links
This commit is contained in:
@@ -296,7 +296,7 @@ export class ControlPlaneStore {
|
||||
description: 'Signing key needs rotation',
|
||||
severity: 'info',
|
||||
createdAt: new Date().toISOString(),
|
||||
actionLink: '/settings/trust/keys',
|
||||
actionLink: '/evidence-audit/trust-signing/keys',
|
||||
},
|
||||
],
|
||||
totalCount: 4,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Domain overview page for Evidence & Audit (V0). Routes users to the evidence surface
|
||||
* matching their need: promotion decision, bundle evidence, environment snapshot,
|
||||
* proof verification, or audit trail.
|
||||
* Trust & Signing ownership remains in Administration; Evidence consumes trust state.
|
||||
* Trust & Signing is owned within Evidence & Audit after v2 ownership transition.
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -16,13 +16,12 @@ import {
|
||||
} from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
|
||||
interface EvidenceEntryCard {
|
||||
interface EvidenceQuickViewTile {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
link: string;
|
||||
linkLabel: string;
|
||||
icon: string;
|
||||
status?: 'ok' | 'warning' | 'info';
|
||||
window: string;
|
||||
count: number;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
type EvidenceHomeMode = 'normal' | 'degraded' | 'empty';
|
||||
@@ -57,28 +56,72 @@ type EvidenceHomeMode = 'normal' | 'degraded' | 'empty';
|
||||
</section>
|
||||
}
|
||||
|
||||
<!-- Primary entry points -->
|
||||
<section class="entry-section" aria-label="Evidence entry points">
|
||||
<h2 class="section-title">Evidence Surfaces</h2>
|
||||
@if (entryCards().length === 0) {
|
||||
<div class="empty-state">
|
||||
<p>No evidence records are available yet.</p>
|
||||
<a routerLink="/release-control/promotions">Open Release Control Promotions</a>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="entry-grid">
|
||||
@for (card of entryCards(); track card.link) {
|
||||
<a [routerLink]="card.link" class="entry-card" [class]="card.status ?? 'info'">
|
||||
<div class="entry-icon" aria-hidden="true">{{ card.icon }}</div>
|
||||
<div class="entry-body">
|
||||
<div class="entry-title">{{ card.title }}</div>
|
||||
<div class="entry-description">{{ card.description }}</div>
|
||||
</div>
|
||||
<div class="entry-link-label">{{ card.linkLabel }} →</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<section class="search-section" aria-label="Evidence search router">
|
||||
<h2 class="section-title">Find Evidence</h2>
|
||||
<div class="search-grid">
|
||||
<label class="search-field">
|
||||
<span class="field-label">Release</span>
|
||||
<select aria-label="Release selector">
|
||||
<option selected>Any release</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="search-field">
|
||||
<span class="field-label">Bundle Version</span>
|
||||
<select aria-label="Bundle version selector">
|
||||
<option selected>Any bundle version</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="search-field">
|
||||
<span class="field-label">Environment</span>
|
||||
<select aria-label="Environment selector">
|
||||
<option selected>Any environment</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="search-field">
|
||||
<span class="field-label">Approval</span>
|
||||
<select aria-label="Approval selector">
|
||||
<option selected>Any approval</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<label class="lookup-field">
|
||||
<span class="field-label">Digest / Verdict / Bundle ID</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Paste digest, verdict-id, or bundle-id"
|
||||
aria-label="Digest verdict or bundle identifier"
|
||||
/>
|
||||
</label>
|
||||
<div class="search-actions">
|
||||
<button type="button">Search</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="quick-views-section" aria-label="Evidence quick views">
|
||||
<h2 class="section-title">Quick Views</h2>
|
||||
<div class="quick-views-grid">
|
||||
@for (tile of quickViews(); track tile.id) {
|
||||
<article class="quick-view-tile">
|
||||
<div class="quick-view-header">
|
||||
<h3 class="quick-view-title">{{ tile.title }}</h3>
|
||||
<span class="quick-view-window">{{ tile.window }}</span>
|
||||
</div>
|
||||
<div class="quick-view-value">{{ tile.count.toLocaleString() }}</div>
|
||||
<p class="quick-view-detail">{{ tile.detail }}</p>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="shortcuts-section" aria-label="Evidence home shortcuts">
|
||||
<h2 class="section-title">Shortcuts</h2>
|
||||
<div class="shortcut-links">
|
||||
<a routerLink="/evidence-audit/evidence" class="shortcut-link">Export Center</a>
|
||||
<a routerLink="/evidence-audit/bundles" class="shortcut-link">Evidence Bundles</a>
|
||||
<a routerLink="/evidence-audit/replay" class="shortcut-link">Replay & Verify</a>
|
||||
<a routerLink="/evidence-audit/proofs" class="shortcut-link">Proof Chains</a>
|
||||
<a routerLink="/evidence-audit/trust-signing" class="shortcut-link">Trust & Signing</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Quick Stats -->
|
||||
@@ -115,11 +158,11 @@ type EvidenceHomeMode = 'normal' | 'degraded' | 'empty';
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a routerLink="/administration/trust-signing" class="cross-link">
|
||||
<a routerLink="/evidence-audit/trust-signing" class="cross-link">
|
||||
<span class="cross-link-icon" aria-hidden="true">■</span>
|
||||
<div class="cross-link-body">
|
||||
<div class="cross-link-title">Administration > Trust & Signing</div>
|
||||
<div class="cross-link-desc">Key management and signing policy (owned by Administration)</div>
|
||||
<div class="cross-link-title">Evidence & Audit > Trust & Signing</div>
|
||||
<div class="cross-link-desc">Key management and signing policy</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -150,9 +193,9 @@ type EvidenceHomeMode = 'normal' | 'degraded' | 'empty';
|
||||
<line x1="12" y1="8" x2="12" y2="12"/>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"/>
|
||||
</svg>
|
||||
Trust and signing operations are owned by
|
||||
<a routerLink="/administration/trust-signing">Administration > Trust & Signing</a>.
|
||||
Evidence & Audit consumes trust state as a read-only consumer.
|
||||
Trust and signing operations are available at
|
||||
<a routerLink="/evidence-audit/trust-signing">Evidence > Trust & Signing</a>
|
||||
with permanent aliases from legacy settings/admin paths.
|
||||
</aside>
|
||||
</div>
|
||||
`,
|
||||
@@ -227,83 +270,127 @@ type EvidenceHomeMode = 'normal' | 'degraded' | 'empty';
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
/* Entry Cards */
|
||||
.entry-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
border: 1px dashed var(--color-border-primary);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface-primary);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0 0 0.4rem;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.empty-state a {
|
||||
color: var(--color-brand-primary);
|
||||
text-decoration: none;
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.entry-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
padding: 1.1rem 1.25rem;
|
||||
.search-section {
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-surface-primary);
|
||||
text-decoration: none;
|
||||
color: var(--color-text-primary);
|
||||
transition: box-shadow 0.15s, border-color 0.15s;
|
||||
position: relative;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.entry-card:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
border-color: var(--color-brand-primary);
|
||||
.search-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.entry-card.ok { border-left: 4px solid var(--color-status-success); }
|
||||
.entry-card.warning { border-left: 4px solid var(--color-status-warning); }
|
||||
.entry-card.info { border-left: 4px solid var(--color-brand-primary); }
|
||||
|
||||
.entry-icon {
|
||||
font-size: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
width: 2rem;
|
||||
text-align: center;
|
||||
.search-field,
|
||||
.lookup-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.entry-body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.entry-title {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.entry-description {
|
||||
font-size: 0.8rem;
|
||||
.field-label {
|
||||
font-size: 0.76rem;
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.4;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.entry-link-label {
|
||||
.search-field select,
|
||||
.lookup-field input {
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.45rem 0.5rem;
|
||||
font-size: 0.84rem;
|
||||
background: var(--color-surface-primary);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.search-actions {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.search-actions button {
|
||||
border: 1px solid var(--color-brand-primary);
|
||||
background: var(--color-brand-primary);
|
||||
color: var(--color-text-inverse, #fff);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.45rem 0.8rem;
|
||||
font-size: 0.82rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.quick-views-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.quick-view-tile {
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface-primary);
|
||||
padding: 0.85rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.quick-view-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.quick-view-title {
|
||||
margin: 0;
|
||||
font-size: 0.84rem;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.quick-view-window {
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.quick-view-value {
|
||||
font-size: 1.3rem;
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.quick-view-detail {
|
||||
margin: 0;
|
||||
font-size: 0.76rem;
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.shortcut-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.shortcut-link {
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: 999px;
|
||||
padding: 0.35rem 0.65rem;
|
||||
font-size: 0.8rem;
|
||||
text-decoration: none;
|
||||
color: var(--color-brand-primary);
|
||||
margin-top: 0.75rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
background: var(--color-surface-primary);
|
||||
}
|
||||
|
||||
.shortcut-link:hover {
|
||||
border-color: var(--color-brand-primary);
|
||||
background: var(--color-surface-elevated);
|
||||
}
|
||||
|
||||
/* Stats Section */
|
||||
@@ -425,68 +512,103 @@ type EvidenceHomeMode = 'normal' | 'degraded' | 'empty';
|
||||
export class EvidenceAuditOverviewComponent {
|
||||
readonly mode = signal<EvidenceHomeMode>('normal');
|
||||
|
||||
private readonly normalEntryCards: EvidenceEntryCard[] = [
|
||||
{
|
||||
title: 'Evidence Packs',
|
||||
description: 'Structured evidence collections for releases, bundles, and promotion decisions.',
|
||||
link: '/evidence-audit/packs',
|
||||
linkLabel: 'Browse packs',
|
||||
icon: '📦',
|
||||
status: 'info',
|
||||
},
|
||||
{
|
||||
title: 'Proof Chains',
|
||||
description: 'Cryptographic proof chain traversal from subject digest to attestation.',
|
||||
link: '/evidence-audit/proofs',
|
||||
linkLabel: 'View proofs',
|
||||
icon: '🔒',
|
||||
status: 'info',
|
||||
},
|
||||
{
|
||||
title: 'Replay and Verify',
|
||||
description: 'Replay historical verdict decisions and verify deterministic evidence outcomes.',
|
||||
link: '/evidence-audit/replay',
|
||||
linkLabel: 'Open replay',
|
||||
icon: '↻',
|
||||
status: 'warning',
|
||||
},
|
||||
{
|
||||
title: 'Timeline',
|
||||
description: 'Timeline and checkpoint history for release evidence progression.',
|
||||
link: '/evidence-audit/timeline',
|
||||
linkLabel: 'Open timeline',
|
||||
icon: '⏰',
|
||||
status: 'info',
|
||||
},
|
||||
{
|
||||
title: 'Audit Log',
|
||||
description: 'Comprehensive audit log filtered by actor, action, resource, and domain context.',
|
||||
link: '/evidence-audit/audit',
|
||||
linkLabel: 'Open audit log',
|
||||
icon: '📄',
|
||||
status: 'ok',
|
||||
},
|
||||
{
|
||||
title: 'Change Trace',
|
||||
description: 'Byte-level change tracing between artifact versions with proof annotations.',
|
||||
link: '/evidence-audit/change-trace',
|
||||
linkLabel: 'Explore changes',
|
||||
icon: '📊',
|
||||
status: 'info',
|
||||
},
|
||||
{
|
||||
title: 'Evidence Export',
|
||||
description: 'Export center: bundle exports, replay/verify, and scoped export jobs.',
|
||||
link: '/evidence-audit/evidence',
|
||||
linkLabel: 'Export center',
|
||||
icon: '📢',
|
||||
status: 'info',
|
||||
},
|
||||
];
|
||||
readonly quickViews = computed((): EvidenceQuickViewTile[] => {
|
||||
if (this.mode() === 'empty') {
|
||||
return [
|
||||
{
|
||||
id: 'latest-packs',
|
||||
title: 'Latest promotion evidence packs',
|
||||
window: '24h',
|
||||
count: 0,
|
||||
detail: 'No packs in the selected window.',
|
||||
},
|
||||
{
|
||||
id: 'latest-bundles',
|
||||
title: 'Latest sealed bundles',
|
||||
window: '7d',
|
||||
count: 0,
|
||||
detail: 'No bundles sealed in the selected window.',
|
||||
},
|
||||
{
|
||||
id: 'failed-replay',
|
||||
title: 'Failed verification / replay',
|
||||
window: '7d',
|
||||
count: 0,
|
||||
detail: 'No failed replay jobs in the selected window.',
|
||||
},
|
||||
{
|
||||
id: 'expiring-trust',
|
||||
title: 'Expiring trust/certs',
|
||||
window: '30d',
|
||||
count: 0,
|
||||
detail: 'No trust certificates nearing expiration.',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
readonly entryCards = computed(() => {
|
||||
if (this.mode() === 'empty') return [] as EvidenceEntryCard[];
|
||||
return this.normalEntryCards;
|
||||
if (this.mode() === 'degraded') {
|
||||
return [
|
||||
{
|
||||
id: 'latest-packs',
|
||||
title: 'Latest promotion evidence packs',
|
||||
window: '24h',
|
||||
count: 28,
|
||||
detail: 'One data source is stale; count may lag.',
|
||||
},
|
||||
{
|
||||
id: 'latest-bundles',
|
||||
title: 'Latest sealed bundles',
|
||||
window: '7d',
|
||||
count: 91,
|
||||
detail: 'Bundle freshness from last successful index sync.',
|
||||
},
|
||||
{
|
||||
id: 'failed-replay',
|
||||
title: 'Failed verification / replay',
|
||||
window: '7d',
|
||||
count: 4,
|
||||
detail: 'Two jobs need replay triage.',
|
||||
},
|
||||
{
|
||||
id: 'expiring-trust',
|
||||
title: 'Expiring trust/certs',
|
||||
window: '30d',
|
||||
count: 3,
|
||||
detail: 'Rotate issuer certs before export attestations.',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'latest-packs',
|
||||
title: 'Latest promotion evidence packs',
|
||||
window: '24h',
|
||||
count: 33,
|
||||
detail: 'All packs sealed and attached to promotion runs.',
|
||||
},
|
||||
{
|
||||
id: 'latest-bundles',
|
||||
title: 'Latest sealed bundles',
|
||||
window: '7d',
|
||||
count: 106,
|
||||
detail: 'Bundles are available for auditor download.',
|
||||
},
|
||||
{
|
||||
id: 'failed-replay',
|
||||
title: 'Failed verification / replay',
|
||||
window: '7d',
|
||||
count: 1,
|
||||
detail: 'One replay mismatch pending operator review.',
|
||||
},
|
||||
{
|
||||
id: 'expiring-trust',
|
||||
title: 'Expiring trust/certs',
|
||||
window: '30d',
|
||||
count: 2,
|
||||
detail: 'Two certificates need planned rotation.',
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
readonly stats = computed(() => {
|
||||
|
||||
@@ -20,7 +20,7 @@ import { AUTH_SERVICE, AuthService, StellaOpsScopes } from '../../core/auth';
|
||||
class="chip"
|
||||
[class.chip--on]="isEnabled()"
|
||||
[class.chip--off]="!isEnabled()"
|
||||
routerLink="/settings/trust"
|
||||
routerLink="/evidence-audit/trust-signing"
|
||||
[attr.title]="tooltip()"
|
||||
>
|
||||
<svg class="chip__icon" viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
|
||||
|
||||
Reference in New Issue
Block a user