diff --git a/src/Web/StellaOps.Web/src/app/app.routes.ts b/src/Web/StellaOps.Web/src/app/app.routes.ts index 1c6bcd8b9..065a10145 100644 --- a/src/Web/StellaOps.Web/src/app/app.routes.ts +++ b/src/Web/StellaOps.Web/src/app/app.routes.ts @@ -198,10 +198,49 @@ export const routes: Routes = [ }, { path: 'administration', - title: 'Administration', - canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard, requireSetupGuard], - data: { breadcrumb: 'Administration' }, - loadChildren: () => import('./routes/administration.routes').then((m) => m.ADMINISTRATION_ROUTES), + children: [ + { path: '', redirectTo: '/setup', pathMatch: 'full' }, + { path: 'identity-access', redirectTo: preserveAppRedirect('/setup/identity-access'), pathMatch: 'full' }, + { path: 'identity-access/:page', redirectTo: preserveAppRedirect('/setup/identity-access'), pathMatch: 'full' }, + { path: 'profile', redirectTo: '/console/profile', pathMatch: 'full' }, + { path: 'admin', redirectTo: preserveAppRedirect('/setup/identity-access'), pathMatch: 'full' }, + { path: 'admin/:page', redirectTo: preserveAppRedirect('/setup/identity-access'), pathMatch: 'full' }, + { path: 'tenant-branding', redirectTo: preserveAppRedirect('/setup/tenant-branding'), pathMatch: 'full' }, + { path: 'notifications', redirectTo: preserveAppRedirect('/setup/notifications'), pathMatch: 'full' }, + { path: 'usage', redirectTo: preserveAppRedirect('/setup/usage'), pathMatch: 'full' }, + { path: 'policy-governance', redirectTo: preserveAppRedirect('/ops/policy/governance'), pathMatch: 'full' }, + { path: 'policy-governance/exceptions', redirectTo: preserveAppRedirect('/ops/policy/vex/exceptions'), pathMatch: 'full' }, + { path: 'policy-governance/exceptions/:id', redirectTo: preserveAppRedirect('/ops/policy/vex/exceptions/:id'), pathMatch: 'full' }, + { path: 'policy-governance/:page', redirectTo: preserveAppRedirect('/ops/policy/governance/:page'), pathMatch: 'full' }, + { path: 'policy-governance/:page/:child', redirectTo: preserveAppRedirect('/ops/policy/governance/:page/:child'), pathMatch: 'full' }, + { path: 'policy', redirectTo: preserveAppRedirect('/ops/policy/governance'), pathMatch: 'full' }, + { path: 'policy/packs', redirectTo: preserveAppRedirect('/ops/policy/packs'), pathMatch: 'full' }, + { path: 'policy/exceptions', redirectTo: preserveAppRedirect('/ops/policy/vex/exceptions'), pathMatch: 'full' }, + { path: 'policy/exceptions/:id', redirectTo: preserveAppRedirect('/ops/policy/vex/exceptions/:id'), pathMatch: 'full' }, + { path: 'policy/packs/:packId', redirectTo: preserveAppRedirect('/ops/policy/packs/:packId'), pathMatch: 'full' }, + { path: 'policy/packs/:packId/:page', redirectTo: preserveAppRedirect('/ops/policy/packs/:packId/:page'), pathMatch: 'full' }, + { path: 'policy/packs/:packId/explain/:runId', redirectTo: preserveAppRedirect('/ops/policy/packs/:packId/explain/:runId'), pathMatch: 'full' }, + { path: 'policy/governance', redirectTo: preserveAppRedirect('/ops/policy/governance'), pathMatch: 'full' }, + { path: 'policy/governance/:page', redirectTo: preserveAppRedirect('/ops/policy/governance/:page'), pathMatch: 'full' }, + { path: 'policy/governance/:page/:child', redirectTo: preserveAppRedirect('/ops/policy/governance/:page/:child'), pathMatch: 'full' }, + { path: 'audit', redirectTo: preserveAppRedirect('/evidence/audit-log'), pathMatch: 'full' }, + { path: 'audit/:page', redirectTo: preserveAppRedirect('/evidence/audit-log/:page'), pathMatch: 'full' }, + { path: 'audit/:page/:child', redirectTo: preserveAppRedirect('/evidence/audit-log/:page/:child'), pathMatch: 'full' }, + { path: 'trust-signing', redirectTo: preserveAppRedirect('/setup/trust-signing'), pathMatch: 'full' }, + { path: 'trust', redirectTo: preserveAppRedirect('/setup/trust-signing'), pathMatch: 'full' }, + { path: 'trust/issuers', redirectTo: preserveAppRedirect('/setup/trust-signing/issuers'), pathMatch: 'full' }, + { path: 'trust/:page/:child', redirectTo: preserveAppRedirect('/setup/trust-signing/:page/:child'), pathMatch: 'full' }, + { path: 'trust/:page', redirectTo: preserveAppRedirect('/setup/trust-signing/:page'), pathMatch: 'full' }, + { path: 'identity-providers', redirectTo: preserveAppRedirect('/setup/identity-providers'), pathMatch: 'full' }, + { path: 'system', redirectTo: preserveAppRedirect('/setup/system'), pathMatch: 'full' }, + { path: 'offline', redirectTo: preserveAppRedirect('/setup/offline'), pathMatch: 'full' }, + { path: 'configuration-pane', redirectTo: preserveAppRedirect('/setup/configuration-pane'), pathMatch: 'full' }, + { path: 'security-data', redirectTo: preserveAppRedirect('/setup/security-data'), pathMatch: 'full' }, + { path: 'security-data/trivy', redirectTo: preserveAppRedirect('/setup/security-data/trivy'), pathMatch: 'full' }, + { path: 'workflows', redirectTo: preserveAppRedirect('/setup/workflows'), pathMatch: 'full' }, + { path: 'ai-preferences', redirectTo: preserveAppRedirect('/setup/ai-preferences'), pathMatch: 'full' }, + { path: '**', redirectTo: '/setup' }, + ], }, { path: 'console-admin', @@ -213,7 +252,7 @@ export const routes: Routes = [ { path: 'admin', children: [ - { path: '', redirectTo: '/administration', pathMatch: 'full' }, + { path: '', redirectTo: '/setup', pathMatch: 'full' }, { path: 'notifications', redirectTo: preserveAppRedirect('/setup/notifications'), pathMatch: 'full' }, { path: 'notifications/:page', redirectTo: preserveAppRedirect('/setup/notifications/:page'), pathMatch: 'full' }, { path: 'trust', redirectTo: preserveAppRedirect('/setup/trust-signing'), pathMatch: 'full' }, @@ -230,7 +269,7 @@ export const routes: Routes = [ pathMatch: 'full', }, { path: 'registries', redirectTo: preserveAppRedirect('/setup/integrations'), pathMatch: 'full' }, - { path: '**', redirectTo: '/administration' }, + { path: '**', redirectTo: '/setup' }, ], }, { diff --git a/src/Web/StellaOps.Web/src/app/core/navigation/navigation.config.ts b/src/Web/StellaOps.Web/src/app/core/navigation/navigation.config.ts index 48018a2d1..7b82f8f4c 100644 --- a/src/Web/StellaOps.Web/src/app/core/navigation/navigation.config.ts +++ b/src/Web/StellaOps.Web/src/app/core/navigation/navigation.config.ts @@ -637,7 +637,7 @@ export const NAVIGATION_GROUPS: NavGroup[] = [ { id: 'identity-providers', label: 'Identity Providers', - route: '/administration/identity-providers', + route: '/setup/identity-providers', icon: 'id-card', requiredScopes: ['ui.admin'], tooltip: 'Configure external identity providers (LDAP, SAML, OIDC)', diff --git a/src/Web/StellaOps.Web/src/app/features/evidence-pack/evidence-pack-list.component.ts b/src/Web/StellaOps.Web/src/app/features/evidence-pack/evidence-pack-list.component.ts index cb0f68dbf..6a0748695 100644 --- a/src/Web/StellaOps.Web/src/app/features/evidence-pack/evidence-pack-list.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/evidence-pack/evidence-pack-list.component.ts @@ -17,7 +17,7 @@ import { } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; -import { Router } from '@angular/router'; +import { Router, RouterModule } from '@angular/router'; import { EvidencePackSummary, EvidencePackQuery, @@ -28,7 +28,7 @@ import { FilterBarComponent, FilterOption, ActiveFilter } from '../../shared/ui/ @Component({ selector: 'stellaops-evidence-pack-list', - imports: [CommonModule, FormsModule, ErrorStateComponent, FilterBarComponent], + imports: [CommonModule, FormsModule, RouterModule, ErrorStateComponent, FilterBarComponent], template: `
@@ -71,7 +71,9 @@ import { FilterBarComponent, FilterOption, ActiveFilter } from '../../shared/ui/ -

No decision capsules found

+

No decision capsules found

+

Decision capsules are created when releases are sealed. Create your first release to generate evidence.

+ Create a Release →
} @else {
@@ -212,6 +214,37 @@ import { FilterBarComponent, FilterOption, ActiveFilter } from '../../shared/ui/ margin-bottom: 1rem; } + .empty-title { + margin: 0 0 0.5rem; + font-size: 1rem; + font-weight: var(--font-weight-semibold); + color: var(--color-text-primary); + } + + .empty-guidance { + margin: 0 0 1rem; + font-size: 0.8rem; + color: var(--color-text-secondary); + max-width: 36ch; + } + + .empty-action-link { + display: inline-block; + padding: 0.5rem 1rem; + font-size: 0.8rem; + font-weight: var(--font-weight-medium); + color: var(--color-brand-primary); + border: 1px solid var(--color-brand-primary); + border-radius: var(--radius-sm); + text-decoration: none; + transition: background 0.15s ease, color 0.15s ease; + } + + .empty-action-link:hover { + background: var(--color-brand-primary); + color: #fff; + } + .retry-btn { padding: 0.5rem 1rem; border: 1px solid var(--color-border-primary); diff --git a/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/data-quality-slos-page.component.ts b/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/data-quality-slos-page.component.ts index d9dc095ee..6f179fdd6 100644 --- a/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/data-quality-slos-page.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/platform-ops/data-integrity/data-quality-slos-page.component.ts @@ -48,7 +48,7 @@ interface SloRow {
diff --git a/src/Web/StellaOps.Web/src/app/features/platform/setup/topology-wizard/topology-wizard.component.ts b/src/Web/StellaOps.Web/src/app/features/platform/setup/topology-wizard/topology-wizard.component.ts index 8b75b901d..3632ce94a 100644 --- a/src/Web/StellaOps.Web/src/app/features/platform/setup/topology-wizard/topology-wizard.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/platform/setup/topology-wizard/topology-wizard.component.ts @@ -367,9 +367,24 @@ import { } + + @if (!wizard.selectedAgent() && !wizard.agentSkipped()) { + + } } @else { -
-

No agents found. You can onboard an agent first.

+
+

No agents deployed yet. You can:

+ + Onboard Agent @@ -382,6 +397,13 @@ import { Agent "{{ wizard.selectedAgent()?.displayName }}" selected.
} + + @if (wizard.agentSkipped()) { +
+ + Agent assignment skipped. You can assign an agent later from the Topology page. +
+ } } @@ -557,7 +579,7 @@ import {
- Agent: {{ wizard.selectedAgent()?.displayName ?? 'N/A' }} + Agent: {{ wizard.selectedAgent()?.displayName ?? (wizard.agentSkipped() ? 'None (skipped)' : 'N/A') }}
@@ -595,7 +617,7 @@ import { [disabled]="!wizard.canGoNext()" (click)="onNext()" > - {{ wizard.currentStep() === 'validate' ? 'Finish' : 'Next' }} + {{ wizard.currentStep() === 'validate' ? 'Finish' : (wizard.currentStep() === 'agent' && wizard.agentSkipped() && !wizard.selectedAgent() ? 'Next (without agent)' : 'Next') }} } @@ -826,6 +848,74 @@ import { } } + .skip-agent-link { + margin-top: 1rem; + text-align: center; + } + + .btn-link { + background: none; + border: none; + color: var(--color-brand-primary); + cursor: pointer; + font-size: 0.82rem; + text-decoration: underline; + padding: 0; + + &:hover { + color: var(--color-brand-primary-hover); + } + } + + .no-agents-guidance { + padding: 1.5rem; + background: var(--color-surface-secondary); + border: 1px solid var(--color-border-primary); + border-radius: var(--radius-md); + font-size: 0.82rem; + color: var(--color-text-secondary); + + p { + margin: 0 0 0.75rem; + font-weight: var(--font-weight-medium); + color: var(--color-text-primary); + } + + ul { + margin: 0 0 1rem; + padding-left: 1.25rem; + } + + li { + margin-bottom: 0.4rem; + } + + code { + font-family: ui-monospace, monospace; + font-size: 0.75rem; + background: var(--color-surface-tertiary); + padding: 0.1rem 0.35rem; + border-radius: var(--radius-sm); + } + + .btn { + margin-right: 0.5rem; + } + } + + .skip-notice { + display: flex; + align-items: center; + gap: 0.4rem; + margin-top: 1rem; + padding: 0.5rem 0.75rem; + border-radius: var(--radius-md); + background: var(--color-status-warning-bg, rgba(245, 158, 11, 0.1)); + color: var(--color-status-warning, #f59e0b); + font-size: 0.78rem; + border: 1px solid rgba(245, 158, 11, 0.3); + } + /* Option List / Cards */ .option-list { display: grid; @@ -1424,7 +1514,13 @@ export class TopologyWizardComponent implements OnInit, OnDestroy { } // --- Agent --- + skipAgent(): void { + this.wizard.agentSkipped.set(true); + this.wizard.selectedAgent.set(null); + } + selectAgent(agent: Agent): void { + this.wizard.agentSkipped.set(false); this.wizard.selectedAgent.set(agent); const targetId = this.wizard.createdTarget()?.id; if (targetId) { diff --git a/src/Web/StellaOps.Web/src/app/features/platform/setup/topology-wizard/topology-wizard.service.ts b/src/Web/StellaOps.Web/src/app/features/platform/setup/topology-wizard/topology-wizard.service.ts index 95c757e85..8073744d5 100644 --- a/src/Web/StellaOps.Web/src/app/features/platform/setup/topology-wizard/topology-wizard.service.ts +++ b/src/Web/StellaOps.Web/src/app/features/platform/setup/topology-wizard/topology-wizard.service.ts @@ -88,6 +88,7 @@ export class TopologyWizardService { readonly createdEnvironment = signal(null); readonly createdTarget = signal(null); readonly selectedAgent = signal(null); + readonly agentSkipped = signal(false); readonly resolvedBindings = signal(null); readonly readinessReport = signal(null); readonly loading = signal(false); @@ -104,7 +105,7 @@ export class TopologyWizardService { case 'environment': return this.createdEnvironment() !== null; case 'stage-order': return true; case 'target': return this.createdTarget() !== null; - case 'agent': return this.selectedAgent() !== null; + case 'agent': return this.selectedAgent() !== null || this.agentSkipped(); case 'infrastructure': return true; case 'validate': return this.readinessReport()?.isReady === true; default: return false; @@ -142,6 +143,7 @@ export class TopologyWizardService { this.createdEnvironment.set(null); this.createdTarget.set(null); this.selectedAgent.set(null); + this.agentSkipped.set(false); this.resolvedBindings.set(null); this.readinessReport.set(null); this.error.set(null); diff --git a/src/Web/StellaOps.Web/src/app/features/settings/settings.routes.ts b/src/Web/StellaOps.Web/src/app/features/settings/settings.routes.ts index e55efb768..639bcdda3 100644 --- a/src/Web/StellaOps.Web/src/app/features/settings/settings.routes.ts +++ b/src/Web/StellaOps.Web/src/app/features/settings/settings.routes.ts @@ -83,13 +83,13 @@ export const SETTINGS_ROUTES: Routes = [ { path: 'admin', title: 'Identity & Access', - redirectTo: redirectToCanonical('/administration/admin'), + redirectTo: redirectToCanonical('/setup/identity-access'), pathMatch: 'full' as const, }, { path: 'admin/:page', title: 'Identity & Access', - redirectTo: redirectToCanonical('/administration/admin/:page'), + redirectTo: redirectToCanonical('/setup/identity-access'), pathMatch: 'full' as const, }, { @@ -113,19 +113,19 @@ export const SETTINGS_ROUTES: Routes = [ { path: 'identity-providers', title: 'Identity Providers', - redirectTo: redirectToCanonical('/administration/identity-providers'), + redirectTo: redirectToCanonical('/setup/identity-providers'), pathMatch: 'full' as const, }, { path: 'system', title: 'System', - redirectTo: redirectToCanonical('/administration/system'), + redirectTo: redirectToCanonical('/setup/system'), pathMatch: 'full' as const, }, { path: 'security-data', title: 'Security Data', - redirectTo: redirectToCanonical('/administration/security-data'), + redirectTo: redirectToCanonical('/setup/security-data'), pathMatch: 'full' as const, }, @@ -153,7 +153,7 @@ export const SETTINGS_ROUTES: Routes = [ { path: 'offline', title: 'Offline Settings', - redirectTo: redirectToCanonical('/administration/offline'), + redirectTo: redirectToCanonical('/setup/offline'), pathMatch: 'full' as const, }, { diff --git a/src/Web/StellaOps.Web/src/app/features/triage/components/decision-drawer/decision-drawer-enhanced.component.ts b/src/Web/StellaOps.Web/src/app/features/triage/components/decision-drawer/decision-drawer-enhanced.component.ts index 444c53662..9160169f0 100644 --- a/src/Web/StellaOps.Web/src/app/features/triage/components/decision-drawer/decision-drawer-enhanced.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/triage/components/decision-drawer/decision-drawer-enhanced.component.ts @@ -71,178 +71,180 @@ export interface ApprovalResponse { standalone: true, imports: [CommonModule, FormsModule, PlaybookSuggestionComponent], template: ` - - - + @if (isOpen) { - + } @@ -255,30 +257,35 @@ export interface ApprovalResponse { } `, styles: [` - .decision-drawer { + .decision-overlay { position: fixed; - right: 0; top: 0; - bottom: 0; - width: 380px; - background: var(--color-surface-primary); - border-left: 1px solid var(--color-border-primary); - box-shadow: -4px 0 16px rgba(0,0,0,0.1); + left: 0; + width: 100%; + height: 100%; + z-index: 1000; + background: rgba(0,0,0,0.5); display: flex; - flex-direction: column; - transform: translateX(100%); - transition: transform 0.3s ease; - z-index: 101; - overflow-y: auto; + align-items: center; + justify-content: center; } - .decision-drawer.open { transform: translateX(0); } + .decision-modal { + width: 520px; + max-width: 95vw; + max-height: 90vh; + background: var(--color-surface-primary); + border: 1px solid var(--color-border-primary); + border-radius: var(--radius-lg); + box-shadow: 0 8px 32px rgba(0,0,0,0.2); + display: flex; + flex-direction: column; + } - .backdrop { - position: fixed; - inset: 0; - background: rgba(0,0,0,0.3); - z-index: 100; + .modal-body { + overflow-y: auto; + max-height: calc(90vh - 120px); + flex: 1; } header { @@ -287,9 +294,9 @@ export interface ApprovalResponse { align-items: center; padding: 16px; border-bottom: 1px solid var(--color-border-primary); - position: sticky; - top: 0; background: var(--color-surface-primary); + border-radius: var(--radius-lg) var(--radius-lg) 0 0; + flex-shrink: 0; } h3 { margin: 0; font-size: var(--font-size-lg); } @@ -429,15 +436,14 @@ export interface ApprovalResponse { .hash { font-family: ui-monospace, monospace; font-size: var(--font-size-xs); word-break: break-all; } footer { - margin-top: auto; padding: 16px; display: flex; gap: 8px; justify-content: flex-end; border-top: 1px solid var(--color-border-primary); - position: sticky; - bottom: 0; background: var(--color-surface-primary); + border-radius: 0 0 var(--radius-lg) var(--radius-lg); + flex-shrink: 0; } .btn { diff --git a/src/Web/StellaOps.Web/src/app/features/triage/components/decision-drawer/decision-drawer.component.ts b/src/Web/StellaOps.Web/src/app/features/triage/components/decision-drawer/decision-drawer.component.ts index 50aab349d..fb4509df4 100644 --- a/src/Web/StellaOps.Web/src/app/features/triage/components/decision-drawer/decision-drawer.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/triage/components/decision-drawer/decision-drawer.component.ts @@ -29,140 +29,146 @@ export interface AlertSummary { selector: 'app-decision-drawer', imports: [FormsModule], template: ` - - @if (isOpen) { - + } `, styles: [` - .decision-drawer { + .decision-overlay { position: fixed; - right: 0; top: 0; - bottom: 0; - width: 360px; + left: 0; + width: 100%; + height: 100%; + z-index: 1000; + background: rgba(0,0,0,0.5); + display: flex; + align-items: center; + justify-content: center; + } + + .decision-modal { + width: 480px; + max-width: 95vw; + max-height: 90vh; background: var(--color-surface-primary); - border-left: 1px solid var(--color-border-primary); - box-shadow: -4px 0 16px rgba(0,0,0,0.1); + border: 1px solid var(--color-border-primary); + border-radius: var(--radius-lg); + box-shadow: 0 8px 32px rgba(0,0,0,0.2); display: flex; flex-direction: column; - transform: translateX(100%); - transition: transform 0.3s ease; - z-index: 101; } - .decision-drawer.open { - transform: translateX(0); - } - - .backdrop { - position: fixed; - inset: 0; - background: rgba(0,0,0,0.3); - z-index: 100; + .modal-body { + overflow-y: auto; + max-height: calc(90vh - 120px); + flex: 1; } header { diff --git a/src/Web/StellaOps.Web/src/app/features/unknowns-tracking/unknowns-dashboard.component.ts b/src/Web/StellaOps.Web/src/app/features/unknowns-tracking/unknowns-dashboard.component.ts index 30524959c..6cc565c18 100644 --- a/src/Web/StellaOps.Web/src/app/features/unknowns-tracking/unknowns-dashboard.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/unknowns-tracking/unknowns-dashboard.component.ts @@ -32,12 +32,15 @@ import { @if (error()) { -