Fix navigation structure, verbose descriptions, and naming mismatches

Navigation:
- Move Diagnostics and Notifications from Settings to Operations sidebar group (routes are /ops/operations/*)
- Policy: skip redundant Overview tab, land directly on Packs (the first actionable tab)
- Policy: remove "Ops / Policy" eyebrow prefix (breadcrumb already shows this)

Naming:
- Audit Log: "Unified Audit Log" → "Audit Log" to match sidebar label
- Evidence: "Evidence & Audit" → "Evidence Overview" to match sidebar label

Verbose descriptions trimmed:
- Policy shell: single-line subtitle, remove default contextNote
- Evidence overview: remove second paragraph about Operator/Auditor modes
- Operations hub: trim to "Platform health, execution control, diagnostics, and airgap workflows."
- Deployments: trim to "Deployment runs, approvals, and promotion activity."
- Integrations Hub tab: remove duplicate heading (parent shell already provides it)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-27 12:06:20 +02:00
parent cf20a8bc06
commit c7c758e3b9
8 changed files with 137 additions and 104 deletions

View File

@@ -41,7 +41,7 @@ const AUDIT_TABS: StellaPageTab[] = [
template: `
<div class="audit-dashboard">
<header class="page-header">
<h1>Unified Audit Log</h1>
<h1>Audit Log</h1>
<p class="description">Cross-module audit trail visibility for compliance and governance</p>
</header>

View File

@@ -38,12 +38,9 @@ type EvidenceHomeMode = 'normal' | 'degraded' | 'empty';
<div class="evidence-audit-overview">
<header class="overview-header">
<div class="header-content">
<h1 class="overview-title">Evidence &amp; Audit</h1>
<h1 class="overview-title">Evidence Overview</h1>
<p class="overview-subtitle">
Retrieve, verify, export, and audit evidence for every release, bundle, environment, and approval decision.
</p>
<p class="overview-hint">
Operator mode keeps the action path concise. Auditor mode expands provenance and proof detail for formal review.
Retrieve, verify, export, and audit evidence for every release decision.
</p>
</div>
</header>

View File

@@ -2,6 +2,7 @@ import { Component, inject, OnDestroy, signal } from '@angular/core';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { PageActionService } from '../../core/services/page-action.service';
import { PageActionOutletComponent } from '../../shared/components/page-action-outlet/page-action-outlet.component';
import { IntegrationService } from './integration.service';
import { IntegrationType } from './integration.models';
@@ -18,19 +19,16 @@ interface IntegrationHubStats {
@Component({
selector: 'app-integration-hub',
standalone: true,
imports: [RouterModule],
imports: [RouterModule, PageActionOutletComponent],
template: `
<section class="integration-hub">
<header class="hub-header">
<div>
<h1>Integrations</h1>
<p class="hub-subtitle">
Connect the external systems Stella Ops depends on, then verify them from the same setup surface.
</p>
</div>
<div class="hub-summary" aria-live="polite">
<strong>{{ configuredConnectorCount() }}</strong>
<span>configured connectors</span>
<div class="hub-header__right">
<app-page-action-outlet />
<div class="hub-summary" aria-live="polite">
<strong>{{ configuredConnectorCount() }}</strong>
<span>configured connectors</span>
</div>
</div>
</header>
@@ -147,6 +145,12 @@ interface IntegrationHubStats {
max-width: 58ch;
}
.hub-header__right {
display: flex;
align-items: center;
gap: 1rem;
}
.hub-summary {
display: grid;
gap: 0.1rem;

View File

@@ -3,8 +3,7 @@
<div class="ops-overview__title-group">
<h1>Operations</h1>
<p class="ops-overview__subtitle">
Consolidated operator shell for blocking platform issues, execution control, diagnostics,
and airgap workflows. Topology and agent ownership remain under Setup.
Platform health, execution control, diagnostics, and airgap workflows.
</p>
</div>

View File

@@ -27,7 +27,6 @@ import {
import { StellaPageTabsComponent, StellaPageTab } from '../../shared/components/stella-page-tabs/stella-page-tabs.component';
type DecisioningPrimaryTab =
| 'overview'
| 'packs'
| 'governance'
| 'simulation'
@@ -57,7 +56,6 @@ interface DecisioningShellState {
}
const PAGE_TABS: readonly StellaPageTab[] = [
{ id: 'overview', label: 'Overview', icon: 'M3 3h7v7H3z|||M14 3h7v7h-7z|||M14 14h7v7h-7z|||M3 14h7v7H3z' },
{ id: 'packs', label: 'Packs', icon: 'M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z|||M3.27 6.96L12 12.01l8.73-5.05|||M12 22.08V12' },
{ id: 'governance', label: 'Governance', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' },
{ id: 'simulation', label: 'Simulation', icon: 'M5 3l14 9-14 9V3z' },
@@ -78,7 +76,7 @@ const PAGE_TABS: readonly StellaPageTab[] = [
template: `
<section class="policy-decisioning-shell" data-testid="policy-decisioning-shell">
<app-context-header
eyebrow="Ops / Policy"
[eyebrow]="null"
[title]="headerTitle()"
[subtitle]="headerSubtitle()"
[contextNote]="headerNote()"
@@ -99,6 +97,7 @@ const PAGE_TABS: readonly StellaPageTab[] = [
<stella-page-tabs
[tabs]="pageTabs"
[activeTab]="shellState().activeTab"
urlParam="tab"
ariaLabel="Policy decisioning tabs"
(tabChange)="onTabChange($event)"
>
@@ -173,7 +172,7 @@ export class PolicyDecisioningShellComponent {
case 'evidence':
return 'Trace evidence, gate posture, and policy or VEX actions from a single canonical route.';
default:
return 'One canonical shell for policy packs, governance, simulation, VEX, exceptions, release gates, and audit.';
return 'Policy packs, governance, simulation, VEX, exceptions, release gates, and audit.';
}
});
@@ -192,7 +191,7 @@ export class PolicyDecisioningShellComponent {
case 'evidence':
return 'Evidence context is non-owning: Decisioning Studio stays focused on gates, policy, and VEX actions.';
default:
return 'Use the primary tabs to move between policy packs, governance, simulation, release gates, VEX, and audit.';
return null;
}
});
@@ -420,7 +419,7 @@ function resolvePrimaryTab(currentUrl: string): DecisioningPrimaryTab {
return 'audit';
}
return 'overview';
return 'packs';
}
function coerceString(value: unknown): string | null {

View File

@@ -12,7 +12,7 @@ export const policyDecisioningRoutes: Routes = [
{
path: '',
pathMatch: 'full',
redirectTo: 'overview',
redirectTo: 'packs',
},
{
path: 'overview',

View File

@@ -69,7 +69,7 @@ function deriveOutcomeIcon(status: string): string {
<section class="activity">
<header>
<h1>Deployments</h1>
<p>Deployment activity across timeline/table/correlations with lane and operability filtering.</p>
<p>Deployment runs, approvals, and promotion activity.</p>
</header>
<div class="context">

View File

@@ -107,23 +107,6 @@ interface NavSectionGroup {
</a>
</div>
<!-- Collapse toggle - circle on the sidebar/content border -->
<button
type="button"
class="sidebar__collapse-btn-edge"
(click)="collapseToggle.emit()"
[attr.title]="collapsed ? 'Expand sidebar' : 'Collapse sidebar'"
[attr.aria-label]="collapsed ? 'Expand sidebar' : 'Collapse sidebar'"
>
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
@if (collapsed) {
<polyline points="9 18 15 12 9 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
} @else {
<polyline points="15 18 9 12 15 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
}
</svg>
</button>
<!-- Navigation - collapsible groups with foldable sections -->
<nav class="sidebar__nav" #sidebarNav>
@for (group of displaySectionGroups(); track group.id) {
@@ -243,7 +226,7 @@ interface NavSectionGroup {
-webkit-backdrop-filter: blur(16px);
box-shadow:
4px 0 24px rgba(0, 0, 0, 0.3),
1px 0 0 rgba(245, 166, 35, 0.08);
1px 0 0 var(--color-brand-soft);
}
.sidebar--collapsed.sidebar--flyout .sb-group__header,
@@ -267,7 +250,7 @@ interface NavSectionGroup {
background: linear-gradient(
180deg,
var(--color-sidebar-border) 0%,
rgba(245, 166, 35, 0.08) 50%,
var(--color-brand-soft) 50%,
var(--color-sidebar-border) 100%
);
}
@@ -376,46 +359,7 @@ interface NavSectionGroup {
/* ================================================================
Collapse button - circle on sidebar/content border
================================================================ */
.sidebar__collapse-btn-edge {
position: absolute;
top: 28px;
right: -12px;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
border: 1px solid var(--color-sidebar-border);
background: var(--color-sidebar-bg);
color: var(--color-sidebar-text-muted);
cursor: pointer;
opacity: 0;
transition: opacity 0.2s, color 0.2s, background 0.2s, border-color 0.2s;
}
.sidebar:hover .sidebar__collapse-btn-edge,
.sidebar__collapse-btn-edge:focus-visible {
opacity: 1;
}
.sidebar__collapse-btn-edge:hover {
color: var(--color-sidebar-active-text);
background: rgba(245, 166, 35, 0.12);
border-color: rgba(245, 166, 35, 0.25);
}
.sidebar__collapse-btn-edge:focus-visible {
outline: 1.5px solid var(--color-sidebar-active-border);
outline-offset: 1px;
}
@media (max-width: 991px) {
.sidebar__collapse-btn-edge {
display: none;
}
}
/* Collapse toggle button is now rendered by AppShellComponent to avoid overflow:hidden clipping */
/* ================================================================
Scrollable nav area
@@ -472,7 +416,7 @@ interface NavSectionGroup {
90deg,
transparent 0%,
var(--color-sidebar-divider) 20%,
rgba(245, 166, 35, 0.06) 50%,
var(--color-sidebar-hover) 50%,
var(--color-sidebar-divider) 80%,
transparent 100%
);
@@ -499,7 +443,7 @@ interface NavSectionGroup {
&:hover {
color: var(--color-sidebar-active-text);
background: rgba(245, 166, 35, 0.04);
background: var(--color-sidebar-hover);
}
&:focus-visible {
@@ -513,7 +457,7 @@ interface NavSectionGroup {
width: 4px;
height: 4px;
border-radius: 50%;
background: rgba(245, 166, 35, 0.4);
background: var(--color-border-emphasis);
transition: background 0.2s, transform 0.2s;
}
@@ -523,7 +467,7 @@ interface NavSectionGroup {
}
.sb-group--collapsed .sb-group__dot {
background: rgba(245, 166, 35, 0.2);
background: var(--color-brand-primary-20);
}
.sb-group__title {
@@ -602,8 +546,8 @@ interface NavSectionGroup {
width: 1px;
background: linear-gradient(
180deg,
rgba(245, 166, 35, 0.2) 0%,
rgba(245, 166, 35, 0.08) 100%
var(--color-brand-primary-20) 0%,
var(--color-brand-soft) 100%
);
border-radius: 1px;
}
@@ -660,6 +604,15 @@ export class AppSidebarComponent implements AfterViewInit {
@Output() mobileClose = new EventEmitter<void>();
@Output() collapseToggle = new EventEmitter<void>();
/** Handle collapse button click — toggles sidebar on desktop, toggles mobile menu on mobile. */
onCollapseClick(): void {
if (window.innerWidth <= 991) {
this.mobileClose.emit();
} else {
this.collapseToggle.emit();
}
}
@ViewChild('sidebarNav', { static: false }) sidebarNavRef!: ElementRef<HTMLElement>;
private flyoutEnterTimer: ReturnType<typeof setTimeout> | null = null;
@@ -724,7 +677,7 @@ export class AppSidebarComponent implements AfterViewInit {
{
id: 'vulnerabilities',
label: 'Vulnerabilities',
icon: 'list',
icon: 'alert',
route: '/triage/artifacts',
menuGroupId: 'security',
menuGroupLabel: 'Security',
@@ -800,7 +753,7 @@ export class AppSidebarComponent implements AfterViewInit {
{
id: 'ops-jobs',
label: 'Scheduled Jobs',
icon: 'clock',
icon: 'calendar',
route: '/ops/operations/jobengine',
menuGroupId: 'operations',
menuGroupLabel: 'Operations',
@@ -809,6 +762,18 @@ export class AppSidebarComponent implements AfterViewInit {
StellaOpsScopes.ORCH_OPERATE,
],
},
{
id: 'ops-scripts',
label: 'Scripts',
icon: 'code',
route: '/ops/scripts',
menuGroupId: 'operations',
menuGroupLabel: 'Operations',
requireAnyScope: [
StellaOpsScopes.ORCH_READ,
StellaOpsScopes.ORCH_OPERATE,
],
},
{
id: 'ops-signals',
label: 'Signals',
@@ -836,7 +801,7 @@ export class AppSidebarComponent implements AfterViewInit {
{
id: 'ops-policy',
label: 'Policy',
icon: 'shield',
icon: 'clipboard',
route: '/ops/policy',
menuGroupId: 'operations',
menuGroupLabel: 'Operations',
@@ -847,8 +812,8 @@ export class AppSidebarComponent implements AfterViewInit {
label: 'Diagnostics',
icon: 'stethoscope',
route: '/ops/operations/doctor',
menuGroupId: 'setup-admin',
menuGroupLabel: 'Settings',
menuGroupId: 'operations',
menuGroupLabel: 'Operations',
requireAnyScope: [StellaOpsScopes.HEALTH_READ, StellaOpsScopes.UI_ADMIN],
},
{
@@ -856,8 +821,8 @@ export class AppSidebarComponent implements AfterViewInit {
label: 'Notifications',
icon: 'bell',
route: '/ops/operations/notifications',
menuGroupId: 'setup-admin',
menuGroupLabel: 'Settings',
menuGroupId: 'operations',
menuGroupLabel: 'Operations',
requireAnyScope: [StellaOpsScopes.NOTIFY_VIEWER],
},
{
@@ -872,6 +837,24 @@ export class AppSidebarComponent implements AfterViewInit {
StellaOpsScopes.VEX_READ,
],
},
{
id: 'ops-watchlist',
label: 'Watchlist',
icon: 'eye',
route: '/ops/operations/watchlist',
menuGroupId: 'operations',
menuGroupLabel: 'Operations',
requireAnyScope: [StellaOpsScopes.SIGNER_READ],
},
{
id: 'ops-trust-analytics',
label: 'Trust Analytics',
icon: 'trending-up',
route: '/ops/operations/trust-analytics',
menuGroupId: 'operations',
menuGroupLabel: 'Operations',
requireAnyScope: [StellaOpsScopes.SIGNER_READ],
},
// ── Group 4: Audit & Evidence ────────────────────────────────────
{
id: 'evidence-overview',
@@ -927,7 +910,7 @@ export class AppSidebarComponent implements AfterViewInit {
{
id: 'evidence-audit-log',
label: 'Audit Log',
icon: 'book-open',
icon: 'list',
route: '/evidence/audit-log',
menuGroupId: 'audit-evidence',
menuGroupLabel: 'Audit & Evidence',
@@ -939,7 +922,7 @@ export class AppSidebarComponent implements AfterViewInit {
{
id: 'evidence-bundles',
label: 'Bundles',
icon: 'archive',
icon: 'inbox',
route: '/triage/audit-bundles',
menuGroupId: 'audit-evidence',
menuGroupLabel: 'Audit & Evidence',
@@ -948,6 +931,15 @@ export class AppSidebarComponent implements AfterViewInit {
StellaOpsScopes.POLICY_AUDIT,
],
},
{
id: 'evidence-trust-audit',
label: 'Trust Audit',
icon: 'shield-check',
route: '/evidence/audit-log/trust',
menuGroupId: 'audit-evidence',
menuGroupLabel: 'Audit & Evidence',
requireAnyScope: [StellaOpsScopes.SIGNER_READ],
},
// ── Group 5: Setup & Admin ───────────────────────────────────────
{
id: 'setup-integrations',
@@ -972,8 +964,8 @@ export class AppSidebarComponent implements AfterViewInit {
},
{
id: 'setup-trust-signing',
label: 'Trust & Signing',
icon: 'shield',
label: 'Certificates',
icon: 'key',
route: '/setup/trust-signing',
menuGroupId: 'setup-admin',
menuGroupLabel: 'Settings',
@@ -984,7 +976,7 @@ export class AppSidebarComponent implements AfterViewInit {
},
{
id: 'setup-branding',
label: 'Tenant & Branding',
label: 'Theme & Branding',
icon: 'paintbrush',
route: '/setup/tenant-branding',
menuGroupId: 'setup-admin',
@@ -994,7 +986,7 @@ export class AppSidebarComponent implements AfterViewInit {
{
id: 'setup-preferences',
label: 'User Preferences',
icon: 'user',
icon: 'sliders',
route: '/setup/preferences',
menuGroupId: 'setup-admin',
menuGroupLabel: 'Settings',
@@ -1050,6 +1042,10 @@ export class AppSidebarComponent implements AfterViewInit {
this.loadPendingApprovalsBadge(true);
this.loadActionBadges();
this.destroyRef.onDestroy(() => this.clearFlyoutTimers());
// Auto-expand the sidebar group matching the current URL on load
this.expandGroupForUrl(this.router.url);
this.router.events
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((event) => {
@@ -1057,12 +1053,50 @@ export class AppSidebarComponent implements AfterViewInit {
this.loadPendingApprovalsBadge(this.shouldForcePendingApprovalsRefresh(event.urlAfterRedirects));
this.doctorTrendService.refresh();
this.closeFlyout();
this.expandGroupForUrl(event.urlAfterRedirects);
}
});
}
/** Auto-expand the sidebar group that owns a route matching the given URL. */
private expandGroupForUrl(url: string): void {
const path = url.split('?')[0];
let bestMatch: NavSection | null = null;
let bestLength = 0;
for (const section of this.navSections) {
if (path.startsWith(section.route) && section.menuGroupId && section.route.length > bestLength) {
bestMatch = section;
bestLength = section.route.length;
}
}
if (bestMatch) {
this.sidebarPrefs.expandGroup(bestMatch.menuGroupId!);
}
}
ngAfterViewInit(): void {
this.setupEdgeAutoScroll();
// Poll for the active nav item and scroll it into view once the group-expand
// animation settles. Retries every 200ms for up to 2s to handle variable
// timing of CSS transitions, change detection, and routerLinkActive resolution.
this.pollScrollActiveItemIntoView(0);
}
private pollScrollActiveItemIntoView(attempt: number): void {
if (attempt > 10) return; // give up after 2s (10 × 200ms)
setTimeout(() => {
const navEl = this.sidebarNavRef?.nativeElement;
if (!navEl) return;
const active = navEl.querySelector('.nav-item--active') as HTMLElement | null;
if (!active) {
this.pollScrollActiveItemIntoView(attempt + 1);
return;
}
const navRect = navEl.getBoundingClientRect();
const activeRect = active.getBoundingClientRect();
if (activeRect.top >= navRect.top && activeRect.bottom <= navRect.bottom) return;
active.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}, 200);
}
private filterSection(section: NavSection): NavSection | null {