feat(web-ui): align evidence home router and trust links

This commit is contained in:
master
2026-02-20 07:22:07 +02:00
parent 04cacdca8a
commit 30be41865f
3 changed files with 283 additions and 161 deletions

View File

@@ -296,7 +296,7 @@ export class ControlPlaneStore {
description: 'Signing key needs rotation', description: 'Signing key needs rotation',
severity: 'info', severity: 'info',
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
actionLink: '/settings/trust/keys', actionLink: '/evidence-audit/trust-signing/keys',
}, },
], ],
totalCount: 4, totalCount: 4,

View File

@@ -5,7 +5,7 @@
* Domain overview page for Evidence & Audit (V0). Routes users to the evidence surface * Domain overview page for Evidence & Audit (V0). Routes users to the evidence surface
* matching their need: promotion decision, bundle evidence, environment snapshot, * matching their need: promotion decision, bundle evidence, environment snapshot,
* proof verification, or audit trail. * 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 { import {
@@ -16,13 +16,12 @@ import {
} from '@angular/core'; } from '@angular/core';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
interface EvidenceEntryCard { interface EvidenceQuickViewTile {
id: string;
title: string; title: string;
description: string; window: string;
link: string; count: number;
linkLabel: string; detail: string;
icon: string;
status?: 'ok' | 'warning' | 'info';
} }
type EvidenceHomeMode = 'normal' | 'degraded' | 'empty'; type EvidenceHomeMode = 'normal' | 'degraded' | 'empty';
@@ -57,28 +56,72 @@ type EvidenceHomeMode = 'normal' | 'degraded' | 'empty';
</section> </section>
} }
<!-- Primary entry points --> <section class="search-section" aria-label="Evidence search router">
<section class="entry-section" aria-label="Evidence entry points"> <h2 class="section-title">Find Evidence</h2>
<h2 class="section-title">Evidence Surfaces</h2> <div class="search-grid">
@if (entryCards().length === 0) { <label class="search-field">
<div class="empty-state"> <span class="field-label">Release</span>
<p>No evidence records are available yet.</p> <select aria-label="Release selector">
<a routerLink="/release-control/promotions">Open Release Control Promotions</a> <option selected>Any release</option>
</div> </select>
} @else { </label>
<div class="entry-grid"> <label class="search-field">
@for (card of entryCards(); track card.link) { <span class="field-label">Bundle Version</span>
<a [routerLink]="card.link" class="entry-card" [class]="card.status ?? 'info'"> <select aria-label="Bundle version selector">
<div class="entry-icon" aria-hidden="true">{{ card.icon }}</div> <option selected>Any bundle version</option>
<div class="entry-body"> </select>
<div class="entry-title">{{ card.title }}</div> </label>
<div class="entry-description">{{ card.description }}</div> <label class="search-field">
</div> <span class="field-label">Environment</span>
<div class="entry-link-label">{{ card.linkLabel }} &#8594;</div> <select aria-label="Environment selector">
</a> <option selected>Any environment</option>
} </select>
</div> </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 &amp; Verify</a>
<a routerLink="/evidence-audit/proofs" class="shortcut-link">Proof Chains</a>
<a routerLink="/evidence-audit/trust-signing" class="shortcut-link">Trust &amp; Signing</a>
</div>
</section> </section>
<!-- Quick Stats --> <!-- Quick Stats -->
@@ -115,11 +158,11 @@ type EvidenceHomeMode = 'normal' | 'degraded' | 'empty';
</div> </div>
</a> </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">&#9632;</span> <span class="cross-link-icon" aria-hidden="true">&#9632;</span>
<div class="cross-link-body"> <div class="cross-link-body">
<div class="cross-link-title">Administration &gt; Trust &amp; Signing</div> <div class="cross-link-title">Evidence &amp; Audit &gt; Trust &amp; Signing</div>
<div class="cross-link-desc">Key management and signing policy (owned by Administration)</div> <div class="cross-link-desc">Key management and signing policy</div>
</div> </div>
</a> </a>
@@ -150,9 +193,9 @@ type EvidenceHomeMode = 'normal' | 'degraded' | 'empty';
<line x1="12" y1="8" x2="12" y2="12"/> <line x1="12" y1="8" x2="12" y2="12"/>
<line x1="12" y1="16" x2="12.01" y2="16"/> <line x1="12" y1="16" x2="12.01" y2="16"/>
</svg> </svg>
Trust and signing operations are owned by Trust and signing operations are available at
<a routerLink="/administration/trust-signing">Administration &gt; Trust &amp; Signing</a>. <a routerLink="/evidence-audit/trust-signing">Evidence &gt; Trust &amp; Signing</a>
Evidence &amp; Audit consumes trust state as a read-only consumer. with permanent aliases from legacy settings/admin paths.
</aside> </aside>
</div> </div>
`, `,
@@ -227,83 +270,127 @@ type EvidenceHomeMode = 'normal' | 'degraded' | 'empty';
font-size: 0.84rem; font-size: 0.84rem;
} }
/* Entry Cards */ .search-section {
.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;
border: 1px solid var(--color-border-primary); border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
background: var(--color-surface-primary); background: var(--color-surface-primary);
text-decoration: none; padding: 1rem;
color: var(--color-text-primary); display: flex;
transition: box-shadow 0.15s, border-color 0.15s; flex-direction: column;
position: relative; gap: 0.85rem;
} }
.entry-card:hover { .search-grid {
box-shadow: var(--shadow-md); display: grid;
border-color: var(--color-brand-primary); grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 0.75rem;
} }
.entry-card.ok { border-left: 4px solid var(--color-status-success); } .search-field,
.entry-card.warning { border-left: 4px solid var(--color-status-warning); } .lookup-field {
.entry-card.info { border-left: 4px solid var(--color-brand-primary); } display: flex;
flex-direction: column;
.entry-icon { gap: 0.35rem;
font-size: 1.5rem;
flex-shrink: 0;
width: 2rem;
text-align: center;
} }
.entry-body { .field-label {
flex: 1; font-size: 0.76rem;
}
.entry-title {
font-weight: var(--font-weight-semibold);
font-size: 0.95rem;
margin-bottom: 0.2rem;
}
.entry-description {
font-size: 0.8rem;
color: var(--color-text-secondary); 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; font-size: 0.8rem;
text-decoration: none;
color: var(--color-brand-primary); color: var(--color-brand-primary);
margin-top: 0.75rem; background: var(--color-surface-primary);
font-weight: var(--font-weight-medium); }
.shortcut-link:hover {
border-color: var(--color-brand-primary);
background: var(--color-surface-elevated);
} }
/* Stats Section */ /* Stats Section */
@@ -425,68 +512,103 @@ type EvidenceHomeMode = 'normal' | 'degraded' | 'empty';
export class EvidenceAuditOverviewComponent { export class EvidenceAuditOverviewComponent {
readonly mode = signal<EvidenceHomeMode>('normal'); readonly mode = signal<EvidenceHomeMode>('normal');
private readonly normalEntryCards: EvidenceEntryCard[] = [ readonly quickViews = computed((): EvidenceQuickViewTile[] => {
{ if (this.mode() === 'empty') {
title: 'Evidence Packs', return [
description: 'Structured evidence collections for releases, bundles, and promotion decisions.', {
link: '/evidence-audit/packs', id: 'latest-packs',
linkLabel: 'Browse packs', title: 'Latest promotion evidence packs',
icon: '&#128230;', window: '24h',
status: 'info', count: 0,
}, detail: 'No packs in the selected window.',
{ },
title: 'Proof Chains', {
description: 'Cryptographic proof chain traversal from subject digest to attestation.', id: 'latest-bundles',
link: '/evidence-audit/proofs', title: 'Latest sealed bundles',
linkLabel: 'View proofs', window: '7d',
icon: '&#128274;', count: 0,
status: 'info', detail: 'No bundles sealed in the selected window.',
}, },
{ {
title: 'Replay and Verify', id: 'failed-replay',
description: 'Replay historical verdict decisions and verify deterministic evidence outcomes.', title: 'Failed verification / replay',
link: '/evidence-audit/replay', window: '7d',
linkLabel: 'Open replay', count: 0,
icon: '&#8635;', detail: 'No failed replay jobs in the selected window.',
status: 'warning', },
}, {
{ id: 'expiring-trust',
title: 'Timeline', title: 'Expiring trust/certs',
description: 'Timeline and checkpoint history for release evidence progression.', window: '30d',
link: '/evidence-audit/timeline', count: 0,
linkLabel: 'Open timeline', detail: 'No trust certificates nearing expiration.',
icon: '&#9200;', },
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: '&#128196;',
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: '&#128202;',
status: 'info',
},
{
title: 'Evidence Export',
description: 'Export center: bundle exports, replay/verify, and scoped export jobs.',
link: '/evidence-audit/evidence',
linkLabel: 'Export center',
icon: '&#128226;',
status: 'info',
},
];
readonly entryCards = computed(() => { if (this.mode() === 'degraded') {
if (this.mode() === 'empty') return [] as EvidenceEntryCard[]; return [
return this.normalEntryCards; {
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(() => { readonly stats = computed(() => {

View File

@@ -20,7 +20,7 @@ import { AUTH_SERVICE, AuthService, StellaOpsScopes } from '../../core/auth';
class="chip" class="chip"
[class.chip--on]="isEnabled()" [class.chip--on]="isEnabled()"
[class.chip--off]="!isEnabled()" [class.chip--off]="!isEnabled()"
routerLink="/settings/trust" routerLink="/evidence-audit/trust-signing"
[attr.title]="tooltip()" [attr.title]="tooltip()"
> >
<svg class="chip__icon" viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"> <svg class="chip__icon" viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">