Sprint 7+8: Journey UX fixes + identity envelope shared middleware
Sprint 7 — Deep journey fixes:
S7-T01: Trust & Signing empty state with "Go to Signing Keys" CTA
S7-T02: Notifications 3-step setup guide (channel→rule→test)
S7-T03: Topology validate step skip — "Skip Validation" when API fails,
with validateSkipped signal matching agentSkipped pattern
S7-T04: VEX export note on Risk Report tab linking to VEX Ledger
Sprint 8 — Identity envelope shared middleware (ARCHITECTURE):
S8-T01: New UseIdentityEnvelopeAuthentication() extension in
StellaOps.Router.AspNet. Reads X-StellaOps-Identity-Envelope headers,
verifies HMAC-SHA256 via GatewayIdentityEnvelopeCodec, creates
ClaimsPrincipal with sub/tenant/scopes/roles. 5min clock skew.
S8-T02: Concelier refactored — removed 78 lines of inline impl,
now uses shared one-liner
S8-T03: Scanner — UseIdentityEnvelopeAuthentication() added
S8-T04: JobEngine — UseIdentityEnvelopeAuthentication() added
S8-T05: Timeline — UseIdentityEnvelopeAuthentication() added
S8-T06: Integrations — UseIdentityEnvelopeAuthentication() added
S8-T07: docs/modules/router/IDENTITY_ENVELOPE_MIDDLEWARE.md
All services now authenticate ReverseProxy requests via gateway envelope.
Scanner scan submit should now work with authenticated identity.
Angular: 0 errors. .NET (6 services): 0 errors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -84,6 +84,28 @@ type NotifyAdminTab = 'channels' | 'rules' | 'templates' | 'deliveries' | 'incid
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Setup Guidance (shown when no channels and no rules exist) -->
|
||||
@if (!loading() && channels().length === 0 && rules().length === 0) {
|
||||
<div class="setup-guidance">
|
||||
<h2 class="setup-guidance__title">Get started with notifications</h2>
|
||||
<p class="setup-guidance__desc">Follow these three steps to start receiving alerts for findings, gate decisions, and freshness events.</p>
|
||||
<ol class="setup-guidance__steps">
|
||||
<li>
|
||||
<strong>Create a channel</strong> (Slack, webhook, or email)
|
||||
<br><button class="btn-link" (click)="activeTab.set('channels')">Go to Channels tab</button>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Create a notification rule</strong> (trigger on findings, gates, or freshness)
|
||||
<br><button class="btn-link" (click)="activeTab.set('rules')">Go to Rules tab</button>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Test your channel</strong> to verify delivery
|
||||
<br><a routerLink="simulator" class="btn-link">Go to Simulator</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tabs">
|
||||
<button class="tab" [class.active]="activeTab() === 'channels'" (click)="activeTab.set('channels')">
|
||||
@@ -518,6 +540,21 @@ type NotifyAdminTab = 'channels' | 'rules' | 'templates' | 'deliveries' | 'incid
|
||||
.status-acknowledged { background: var(--color-status-warning-bg); color: var(--color-status-warning); }
|
||||
.status-resolved { background: var(--color-status-success-border); color: var(--color-status-success-text); }
|
||||
|
||||
.setup-guidance {
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1.25rem 1.5rem;
|
||||
border: 1px dashed var(--color-border-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-surface-secondary);
|
||||
}
|
||||
.setup-guidance__title { margin: 0 0 0.35rem; font-size: 1.1rem; }
|
||||
.setup-guidance__desc { margin: 0 0 1rem; color: var(--color-text-secondary); font-size: 0.875rem; }
|
||||
.setup-guidance__steps { margin: 0; padding-left: 1.25rem; display: grid; gap: 0.85rem; }
|
||||
.setup-guidance__steps li { line-height: 1.6; }
|
||||
.setup-guidance__steps strong { color: var(--color-text-primary); }
|
||||
.btn-link { background: none; border: none; color: var(--color-status-info-text); cursor: pointer; padding: 0; font-size: 0.85rem; font-weight: var(--font-weight-semibold); text-decoration: underline; }
|
||||
a.btn-link { text-decoration: underline; }
|
||||
|
||||
.empty-state { text-align: center; padding: 3rem; color: var(--color-text-secondary); }
|
||||
.loading { text-align: center; padding: 2rem; color: var(--color-text-secondary); }
|
||||
.error-banner { background: var(--color-status-error-bg); color: var(--color-status-error-text); padding: 1rem; border-radius: var(--radius-sm); margin-top: 1rem; }
|
||||
|
||||
@@ -546,10 +546,28 @@ import {
|
||||
</button>
|
||||
} @else {
|
||||
<div class="empty-state">
|
||||
<p>Validation has not been run yet.</p>
|
||||
<button type="button" class="btn btn--primary btn--sm" (click)="runValidation()">
|
||||
Run Validation
|
||||
</button>
|
||||
@if (validationFailed()) {
|
||||
<p>Validation requires deployed infrastructure. You can skip validation and complete setup.</p>
|
||||
<div class="empty-state__actions">
|
||||
<button type="button" class="btn btn--primary btn--sm" (click)="runValidation()">
|
||||
Retry Validation
|
||||
</button>
|
||||
<button type="button" class="btn btn--secondary btn--sm" (click)="skipValidation()">
|
||||
Skip Validation
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
<p>Validation has not been run yet.</p>
|
||||
<button type="button" class="btn btn--primary btn--sm" (click)="runValidation()">
|
||||
Run Validation
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (wizard.validateSkipped()) {
|
||||
<div class="skip-notice">
|
||||
Validation was skipped. You can run it later from the topology dashboard.
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
@@ -848,6 +866,23 @@ import {
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state__actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.skip-notice {
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid var(--color-status-warning-border, #e6c200);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-status-warning-bg, #fff8e1);
|
||||
color: var(--color-status-warning-text, #7a6100);
|
||||
font-size: 0.82rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.skip-agent-link {
|
||||
margin-top: 1rem;
|
||||
text-align: center;
|
||||
@@ -1379,6 +1414,7 @@ export class TopologyWizardComponent implements OnInit, OnDestroy {
|
||||
readonly environmentsLoading = signal(false);
|
||||
readonly bindingsLoading = signal(false);
|
||||
readonly validationLoading = signal(false);
|
||||
readonly validationFailed = signal(false);
|
||||
|
||||
// Region creation form
|
||||
readonly newRegionName = signal('');
|
||||
@@ -1591,6 +1627,7 @@ export class TopologyWizardComponent implements OnInit, OnDestroy {
|
||||
if (!targetId) return;
|
||||
|
||||
this.validationLoading.set(true);
|
||||
this.validationFailed.set(false);
|
||||
const sub = this.wizard.validateTarget(targetId).subscribe({
|
||||
next: (report) => {
|
||||
this.wizard.readinessReport.set(report);
|
||||
@@ -1598,9 +1635,14 @@ export class TopologyWizardComponent implements OnInit, OnDestroy {
|
||||
},
|
||||
error: (err: unknown) => {
|
||||
this.wizard.error.set(err instanceof Error ? err.message : 'Validation failed.');
|
||||
this.validationFailed.set(true);
|
||||
this.validationLoading.set(false);
|
||||
},
|
||||
});
|
||||
this.subscriptions.push(sub);
|
||||
}
|
||||
|
||||
skipValidation(): void {
|
||||
this.wizard.validateSkipped.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ export class TopologyWizardService {
|
||||
readonly createdTarget = signal<Target | null>(null);
|
||||
readonly selectedAgent = signal<Agent | null>(null);
|
||||
readonly agentSkipped = signal(false);
|
||||
readonly validateSkipped = signal(false);
|
||||
readonly resolvedBindings = signal<ResolvedBindings | null>(null);
|
||||
readonly readinessReport = signal<ReadinessReport | null>(null);
|
||||
readonly loading = signal(false);
|
||||
@@ -107,7 +108,7 @@ export class TopologyWizardService {
|
||||
case 'target': return this.createdTarget() !== null;
|
||||
case 'agent': return this.selectedAgent() !== null || this.agentSkipped();
|
||||
case 'infrastructure': return true;
|
||||
case 'validate': return this.readinessReport()?.isReady === true;
|
||||
case 'validate': return this.readinessReport()?.isReady === true || this.validateSkipped();
|
||||
default: return false;
|
||||
}
|
||||
});
|
||||
@@ -144,6 +145,7 @@ export class TopologyWizardService {
|
||||
this.createdTarget.set(null);
|
||||
this.selectedAgent.set(null);
|
||||
this.agentSkipped.set(false);
|
||||
this.validateSkipped.set(false);
|
||||
this.resolvedBindings.set(null);
|
||||
this.readinessReport.set(null);
|
||||
this.error.set(null);
|
||||
|
||||
@@ -96,6 +96,10 @@ interface PlatformListResponse<T> {
|
||||
Generate PDF
|
||||
</button>
|
||||
</div>
|
||||
<p class="vex-export-note">
|
||||
VEX decision exports are available on the
|
||||
<a class="vex-export-note__link" (click)="activeTab.set('vex')">VEX Ledger tab</a>.
|
||||
</p>
|
||||
<app-security-risk-overview></app-security-risk-overview>
|
||||
</section>
|
||||
}
|
||||
@@ -241,8 +245,27 @@ interface PlatformListResponse<T> {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.vex-export-note {
|
||||
margin: 0;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-text-secondary);
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
}
|
||||
|
||||
.vex-export-note__link {
|
||||
color: var(--color-brand-primary);
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.vex-export-note__link:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.tabs, .export-toolbar, .evidence-explainer, .export-center-link {
|
||||
.tabs, .export-toolbar, .evidence-explainer, .export-center-link, .vex-export-note {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,15 @@ import { RouterLink } from '@angular/router';
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="trust-overview__empty-state">
|
||||
<div class="trust-overview__empty-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>
|
||||
</div>
|
||||
<p class="trust-overview__empty-title">No signing keys configured</p>
|
||||
<p class="trust-overview__empty-desc">Generate a signing key to enable attestation signing on releases.</p>
|
||||
<a routerLink="keys" class="trust-overview__empty-action">Go to Signing Keys</a>
|
||||
</div>
|
||||
|
||||
<div class="trust-overview__grid">
|
||||
<article class="trust-overview__card">
|
||||
<h3>Signing Keys</h3>
|
||||
@@ -98,6 +107,53 @@ import { RouterLink } from '@angular/router';
|
||||
text-decoration: none;
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.trust-overview__empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 2rem 1.5rem;
|
||||
border: 1px dashed var(--color-border-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-surface-secondary);
|
||||
}
|
||||
|
||||
.trust-overview__empty-icon {
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.trust-overview__empty-title {
|
||||
margin: 0 0 0.35rem;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.trust-overview__empty-desc {
|
||||
margin: 0 0 0.85rem;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.trust-overview__empty-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.45rem 1rem;
|
||||
background: var(--color-brand-primary);
|
||||
color: #fff;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.85rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
text-decoration: none;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.trust-overview__empty-action:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
`],
|
||||
})
|
||||
export class TrustOverviewComponent {}
|
||||
|
||||
Reference in New Issue
Block a user