Fix Policy Governance theming, replace non-Material patterns, remove redundant tabs

Theme fixes:
- Replace swapped CSS variables across 15 governance sub-components
  (text colors as backgrounds, surface colors as text, text colors as borders)
- Replace 50+ hardcoded rgba() colors with CSS variable references

Navigation fixes:
- Remove redundant parent tab bar from PolicyDecisioningShellComponent
  (sidebar already provides section navigation)
- Fix governance tab switching: remove conflicting urlParam, add route sync
- Remove redundant eyebrow header from governance component

Material compliance (AGENTS.md):
- Replace 11 window.confirm/prompt calls with app-confirm-dialog and app-modal
- Replace custom KPI cards with stella-metric-card/stella-metric-grid (3 components)
- Replace custom modal implementations with app-modal (5 modals in 2 components)
- Replace custom filter UIs with stella-filter-chip (3 components)
- Replace Loading text with app-loading-state component (6 components)
- Add [title] attributes to truncated text elements

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-28 10:50:59 +02:00
parent 971869affd
commit aaae7d771d
18 changed files with 1174 additions and 1324 deletions

View File

@@ -24,15 +24,6 @@ import {
import {
buildContextRouteParams,
} from '../../shared/ui/context-route-state/context-route-state';
import { StellaPageTabsComponent, StellaPageTab } from '../../shared/components/stella-page-tabs/stella-page-tabs.component';
type DecisioningPrimaryTab =
| 'packs'
| 'governance'
| 'simulation'
| 'vex'
| 'gates'
| 'audit';
type DecisioningContextKind =
| 'global'
@@ -43,7 +34,6 @@ type DecisioningContextKind =
| 'evidence';
interface DecisioningShellState {
readonly activeTab: DecisioningPrimaryTab;
readonly kind: DecisioningContextKind;
readonly packId: string | null;
readonly releaseId: string | null;
@@ -55,15 +45,6 @@ interface DecisioningShellState {
readonly evidenceId: string | null;
}
const PAGE_TABS: readonly StellaPageTab[] = [
{ 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' },
{ id: 'vex', label: 'VEX & Exceptions', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|||M14 2v6h6|||M16 13H8|||M16 17H8|||M10 9H8' },
{ id: 'gates', label: 'Release Gates', icon: 'M9 11l3 3L22 4|||M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11' },
{ id: 'audit', label: 'Audit', icon: 'M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2|||M8 2h8v4H8z' },
];
@Component({
selector: 'app-policy-decisioning-shell',
imports: [
@@ -71,7 +52,6 @@ const PAGE_TABS: readonly StellaPageTab[] = [
RouterLink,
RouterOutlet,
ContextHeaderComponent,
StellaPageTabsComponent,
],
template: `
<section class="policy-decisioning-shell" data-testid="policy-decisioning-shell">
@@ -94,15 +74,7 @@ const PAGE_TABS: readonly StellaPageTab[] = [
</a>
</app-context-header>
<stella-page-tabs
[tabs]="pageTabs"
[activeTab]="shellState().activeTab"
urlParam="tab"
ariaLabel="Policy decisioning tabs"
(tabChange)="onTabChange($event)"
>
<router-outlet />
</stella-page-tabs>
<router-outlet />
</section>
`,
styles: [`
@@ -136,7 +108,6 @@ export class PolicyDecisioningShellComponent {
private readonly router = inject(Router);
private readonly destroyRef = inject(DestroyRef);
readonly pageTabs = PAGE_TABS;
readonly shellState = signal<DecisioningShellState>(this.readShellState());
readonly headerTitle = computed(() => {
@@ -172,7 +143,7 @@ export class PolicyDecisioningShellComponent {
case 'evidence':
return 'Trace evidence, gate posture, and policy or VEX actions from a single canonical route.';
default:
return 'Policy packs, governance, simulation, VEX, exceptions, release gates, and audit.';
return 'Policy packs, governance, simulation, VEX, exceptions, and audit.';
}
});
@@ -236,42 +207,6 @@ export class PolicyDecisioningShellComponent {
});
}
onTabChange(tabId: string): void {
const state = this.shellState();
const queryParams = this.contextQueryParams();
let route: readonly unknown[];
switch (tabId) {
case 'overview':
route = this.overviewRoute();
break;
case 'packs':
route = state.packId
? ['/ops/policy/packs', state.packId]
: ['/ops/policy/packs'];
break;
case 'governance':
route = ['/ops/policy/governance'];
break;
case 'simulation':
route = ['/ops/policy/simulation'];
break;
case 'vex':
route = ['/ops/policy/vex'];
break;
case 'gates':
route = this.gatesRoute();
break;
case 'audit':
route = ['/ops/policy/audit'];
break;
default:
route = this.overviewRoute();
}
void this.router.navigate([...route], { queryParams });
}
overviewRoute(): readonly unknown[] {
return ['/ops/policy/overview'];
}
@@ -318,7 +253,6 @@ export class PolicyDecisioningShellComponent {
private readShellState(): DecisioningShellState {
const params = collectRouteParams(this.route.snapshot.root);
const queryParams = this.route.snapshot.root.queryParams ?? {};
const currentUrl = this.router.url.split('?')[0] ?? '';
const releaseId = coerceString(params['releaseId']) ?? coerceString(queryParams['releaseId']);
const approvalId = coerceString(params['approvalId']) ?? coerceString(queryParams['approvalId']);
const packId = coerceString(params['packId']) ?? coerceString(queryParams['packId']);
@@ -347,7 +281,6 @@ export class PolicyDecisioningShellComponent {
}
return {
activeTab: resolvePrimaryTab(currentUrl),
kind,
packId,
releaseId,
@@ -382,46 +315,6 @@ function collectRouteParams(snapshot: ActivatedRouteSnapshot | null): Record<str
return params;
}
function resolvePrimaryTab(currentUrl: string): DecisioningPrimaryTab {
if (currentUrl.includes('/ops/policy/packs') || currentUrl.includes('/ops/policy/baselines')) {
return 'packs';
}
if (
currentUrl.includes('/ops/policy/governance') ||
currentUrl.includes('/ops/policy/risk-budget') ||
currentUrl.includes('/ops/policy/budget') ||
currentUrl.includes('/ops/policy/trust-weights') ||
currentUrl.includes('/ops/policy/staleness') ||
currentUrl.includes('/ops/policy/sealed-mode') ||
currentUrl.includes('/ops/policy/profiles') ||
currentUrl.includes('/ops/policy/validator') ||
currentUrl.includes('/ops/policy/conflicts') ||
currentUrl.includes('/ops/policy/impact-preview') ||
currentUrl.includes('/ops/policy/schema-playground') ||
currentUrl.includes('/ops/policy/schema-docs')
) {
return 'governance';
}
if (currentUrl.includes('/ops/policy/simulation')) {
return 'simulation';
}
if (
currentUrl.includes('/ops/policy/vex') ||
currentUrl.includes('/ops/policy/waivers') ||
currentUrl.includes('/ops/policy/exceptions')
) {
return 'vex';
}
if (currentUrl.includes('/ops/policy/gates')) {
return 'gates';
}
if (currentUrl.includes('/ops/policy/audit')) {
return 'audit';
}
return 'packs';
}
function coerceString(value: unknown): string | null {
if (typeof value !== 'string') {
return null;

View File

@@ -366,13 +366,11 @@ export const policyDecisioningRoutes: Routes = [
},
],
},
// Release Gates main view moved to Deployments > Approvals
{
path: 'gates',
title: 'Release Gates',
loadComponent: () =>
import('./policy-decisioning-gates-page.component').then(
(m) => m.PolicyDecisioningGatesPageComponent,
),
redirectTo: '/releases/deployments?view=approvals',
pathMatch: 'full' as const,
},
{
path: 'gates/catalog',

View File

@@ -415,7 +415,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
margin: 0;
font-size: 1.25rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.wizard__subtitle {
@@ -430,7 +430,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
gap: 0.5rem;
margin-bottom: 1.5rem;
padding: 1rem;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-lg);
}
@@ -446,7 +446,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.wizard-step:hover {
background: var(--color-text-primary);
background: var(--color-surface-tertiary);
}
.wizard-step__number {
@@ -456,7 +456,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
align-items: center;
justify-content: center;
border-radius: var(--radius-full);
background: var(--color-text-primary);
background: var(--color-surface-tertiary);
color: var(--color-text-muted);
font-size: 0.85rem;
font-weight: var(--font-weight-semibold);
@@ -468,7 +468,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.wizard-step--active {
background: var(--color-text-primary);
background: var(--color-surface-tertiary);
}
.wizard-step--active .wizard-step__number {
@@ -477,7 +477,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.wizard-step--active .wizard-step__label {
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.wizard-step--completed .wizard-step__number {
@@ -487,8 +487,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
/* Wizard Content */
.wizard-content {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
padding: 1.5rem;
min-height: 400px;
@@ -506,7 +506,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.step-title {
margin: 0 0 0.25rem;
font-size: 1.1rem;
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.step-desc {
@@ -517,8 +517,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
/* Conflict Summary */
.conflict-summary {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
padding: 1.25rem;
}
@@ -537,7 +537,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
font-weight: var(--font-weight-semibold);
}
.badge--type { background: var(--color-text-primary); color: var(--color-text-muted); }
.badge--type { background: var(--color-surface-tertiary); color: var(--color-text-muted); }
.badge--info { background: var(--color-status-info-text); color: #fff; }
.badge--warning { background: var(--color-status-warning-text); color: #fff; }
.badge--error { background: var(--color-severity-high); color: var(--color-severity-high-bg); }
@@ -546,7 +546,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.conflict-title {
margin: 0 0 0.5rem;
font-size: 1rem;
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.conflict-desc {
@@ -558,8 +558,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.impact-box {
padding: 0.75rem;
background: rgba(234, 179, 8, 0.1);
border: 1px solid rgba(234, 179, 8, 0.3);
background: var(--color-status-warning-bg);
border: 1px solid var(--color-status-warning-border);
border-radius: var(--radius-md);
font-size: 0.9rem;
color: var(--color-status-warning-border);
@@ -583,8 +583,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.comparison-panel {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
overflow: hidden;
display: flex;
@@ -596,19 +596,19 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: var(--color-text-primary);
background: var(--color-surface-tertiary);
}
.panel-header h4 {
margin: 0;
font-size: 0.9rem;
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.source-badge {
font-size: 0.7rem;
padding: 0.15rem 0.5rem;
background: var(--color-text-primary);
background: var(--color-surface-tertiary);
color: var(--color-text-muted);
border-radius: var(--radius-sm);
}
@@ -635,20 +635,20 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.info-value {
color: var(--color-border-primary);
color: var(--color-text-primary);
}
.info-code {
font-family: monospace;
font-size: 0.8rem;
color: var(--color-status-info);
background: var(--color-text-primary);
background: var(--color-surface-tertiary);
padding: 0.1rem 0.35rem;
border-radius: var(--radius-sm);
}
.source-preview {
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-md);
padding: 0.75rem;
overflow-x: auto;
@@ -663,7 +663,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.panel-actions {
padding: 0.75rem 1rem;
border-top: 1px solid var(--color-text-heading);
border-top: 1px solid var(--color-border-primary);
}
.comparison-indicator {
@@ -689,8 +689,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.3);
background: var(--color-status-success-bg);
border: 1px solid var(--color-status-success-border);
border-radius: var(--radius-md);
margin-top: 1rem;
color: var(--color-status-success-border);
@@ -710,8 +710,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.strategy-card {
background: var(--color-text-heading);
border: 2px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 2px solid var(--color-border-primary);
border-radius: var(--radius-lg);
padding: 1.25rem;
cursor: pointer;
@@ -720,12 +720,12 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.strategy-card:hover {
border-color: var(--color-text-primary);
border-color: var(--color-border-primary);
}
.strategy-card--selected {
border-color: var(--color-status-info);
background: rgba(34, 211, 238, 0.05);
background: var(--color-status-info-bg);
}
.strategy-icon {
@@ -735,20 +735,20 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
display: flex;
align-items: center;
justify-content: center;
background: var(--color-text-primary);
background: var(--color-surface-tertiary);
border-radius: var(--radius-xl);
color: var(--color-text-muted);
}
.strategy-card--selected .strategy-icon {
background: rgba(34, 211, 238, 0.2);
background: var(--color-status-info-bg);
color: var(--color-status-info);
}
.strategy-name {
margin: 0 0 0.35rem;
font-size: 0.95rem;
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.strategy-desc {
@@ -762,8 +762,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
align-items: flex-start;
gap: 0.75rem;
padding: 1rem;
background: rgba(34, 211, 238, 0.1);
border: 1px solid rgba(34, 211, 238, 0.3);
background: var(--color-status-info-bg);
border: 1px solid var(--color-status-info-border);
border-radius: var(--radius-lg);
}
@@ -787,8 +787,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
/* Resolution Summary */
.resolution-summary {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
padding: 1.25rem;
margin-bottom: 1.5rem;
@@ -797,7 +797,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.resolution-summary h4 {
margin: 0 0 1rem;
font-size: 1rem;
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.summary-grid {
@@ -818,7 +818,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.summary-value {
color: var(--color-border-primary);
color: var(--color-text-primary);
}
.form-field {
@@ -827,7 +827,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.form-label {
display: block;
color: var(--color-border-primary);
color: var(--color-text-primary);
font-size: 0.85rem;
font-weight: var(--font-weight-medium);
margin-bottom: 0.35rem;
@@ -836,10 +836,10 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.form-textarea {
width: 100%;
padding: 0.75rem;
background: var(--color-text-heading);
border: 1px solid var(--color-text-primary);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-md);
color: var(--color-border-primary);
color: var(--color-text-primary);
font-size: 0.9rem;
resize: vertical;
}
@@ -857,8 +857,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.preview-changes {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
padding: 1.25rem;
}
@@ -866,7 +866,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.preview-changes h4 {
margin: 0 0 0.75rem;
font-size: 1rem;
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.changes-list {
@@ -887,7 +887,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
gap: 1rem;
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid var(--color-text-heading);
border-top: 1px solid var(--color-border-primary);
}
.wizard-nav__spacer {
@@ -912,11 +912,11 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.btn--primary:hover:not(:disabled) { background: var(--color-status-info); }
.btn--primary:disabled { opacity: 0.5; cursor: not-allowed; }
.btn--secondary { background: var(--color-text-primary); color: var(--color-border-primary); border: 1px solid var(--color-text-primary); }
.btn--secondary:hover { background: var(--color-text-primary); }
.btn--secondary { background: var(--color-surface-tertiary); color: var(--color-text-primary); border: 1px solid var(--color-border-primary); }
.btn--secondary:hover { background: var(--color-surface-tertiary); }
.btn--ghost { background: transparent; color: var(--color-text-muted); }
.btn--ghost:hover { background: var(--color-text-primary); color: var(--color-border-primary); }
.btn--ghost:hover { background: var(--color-surface-tertiary); color: var(--color-text-primary); }
.btn--ghost:disabled { opacity: 0.5; cursor: not-allowed; }
.btn--small { padding: 0.35rem 0.75rem; font-size: 0.8rem; }
@@ -929,8 +929,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
justify-content: center;
padding: 4rem 2rem;
text-align: center;
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
}
@@ -941,7 +941,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.empty-state h3 {
margin: 0 0 0.5rem;
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.empty-state p {

View File

@@ -13,6 +13,8 @@ import {
AuditEventType,
GovernanceAuditDiff,
} from '../../core/api/policy-governance.models';
import { LoadingStateComponent } from '../../shared/components/loading-state/loading-state.component';
import { StellaFilterChipComponent } from '../../shared/components/stella-filter-chip/stella-filter-chip.component';
import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
/**
@@ -23,7 +25,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
*/
@Component({
selector: 'app-governance-audit',
imports: [CommonModule, FormsModule],
imports: [CommonModule, FormsModule, LoadingStateComponent, StellaFilterChipComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="audit" [attr.aria-busy]="loading()">
@@ -36,15 +38,12 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
<!-- Filters -->
<div class="filters">
<div class="filter-group">
<label class="filter-label">Event Type</label>
<select [(ngModel)]="filters.eventType" (change)="applyFilters()" class="form-select">
<option value="">All Types</option>
@for (type of eventTypes; track type) {
<option [value]="type">{{ formatEventType(type) }}</option>
}
</select>
</div>
<stella-filter-chip
label="Event Type"
[value]="filters.eventType"
[options]="eventTypeOptions"
(valueChange)="filters.eventType = $event; applyFilters()"
/>
<div class="filter-group">
<label class="filter-label">Actor</label>
@@ -91,7 +90,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
<div class="event-card__content">
<div class="event-card__type">{{ formatEventType(event.type) }}</div>
<div class="event-card__summary">{{ event.summary }}</div>
<div class="event-card__summary" [title]="event.summary">{{ event.summary }}</div>
</div>
<div class="event-card__meta">
@@ -219,7 +218,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
</div>
}
} @else if (loading()) {
<div class="loading-state">Loading audit events...</div>
<app-loading-state message="Loading audit events..." />
} @else {
<div class="empty-state">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="48" height="48">
@@ -245,7 +244,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
margin: 0;
font-size: 1.25rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.audit__subtitle {
@@ -262,9 +261,10 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
align-items: flex-end;
margin-bottom: 1.5rem;
padding: 1rem;
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
overflow: visible;
}
.filter-group {
@@ -278,17 +278,17 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
color: var(--color-text-muted);
}
.form-input, .form-select {
.form-input {
padding: 0.5rem 0.75rem;
background: var(--color-text-heading);
border: 1px solid var(--color-text-primary);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-md);
color: var(--color-border-primary);
color: var(--color-text-primary);
font-size: 0.85rem;
min-width: 150px;
}
.form-input:focus, .form-select:focus {
.form-input:focus {
outline: none;
border-color: var(--color-status-info);
}
@@ -303,8 +303,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
border: none;
}
.btn--ghost { background: transparent; color: var(--color-text-muted); border: 1px solid var(--color-text-primary); }
.btn--ghost:hover { background: var(--color-text-primary); color: var(--color-border-primary); }
.btn--ghost { background: transparent; color: var(--color-text-muted); border: 1px solid var(--color-border-primary); }
.btn--ghost:hover { background: var(--color-surface-tertiary); color: var(--color-text-primary); }
.btn--ghost:disabled { opacity: 0.5; cursor: not-allowed; }
.btn--small { padding: 0.35rem 0.75rem; font-size: 0.8rem; }
@@ -317,8 +317,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.event-card {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
overflow: hidden;
}
@@ -333,7 +333,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.event-card__header:hover {
background: var(--color-text-heading);
background: var(--color-surface-elevated);
}
.event-card__icon {
@@ -346,10 +346,10 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
flex-shrink: 0;
}
.event-card__icon--config { background: rgba(34, 211, 238, 0.2); color: var(--color-status-info); }
.event-card__icon--security { background: rgba(234, 179, 8, 0.2); color: var(--color-status-warning); }
.event-card__icon--profile { background: rgba(168, 85, 247, 0.2); color: var(--color-status-excepted); }
.event-card__icon--other { background: rgba(148, 163, 184, 0.2); color: var(--color-text-muted); }
.event-card__icon--config { background: var(--color-status-info-bg); color: var(--color-status-info); }
.event-card__icon--security { background: var(--color-status-warning-bg); color: var(--color-status-warning); }
.event-card__icon--profile { background: var(--color-brand-primary-20); color: var(--color-status-excepted); }
.event-card__icon--other { background: var(--color-surface-tertiary); color: var(--color-text-muted); }
.event-card__content {
flex: 1;
@@ -364,7 +364,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.event-card__summary {
color: var(--color-surface-secondary);
color: var(--color-text-heading);
font-weight: var(--font-weight-medium);
white-space: nowrap;
overflow: hidden;
@@ -382,7 +382,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
justify-content: flex-end;
gap: 0.5rem;
font-size: 0.85rem;
color: var(--color-border-primary);
color: var(--color-text-primary);
}
.actor-badge {
@@ -393,7 +393,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.actor-badge--user { background: var(--color-status-info-text); color: #fff; }
.actor-badge--system { background: var(--color-text-primary); color: var(--color-text-muted); }
.actor-badge--system { background: var(--color-surface-tertiary); color: var(--color-text-muted); }
.actor-badge--automation { background: var(--color-status-success-text); color: #fff; }
.event-card__time {
@@ -416,8 +416,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
/* Event Details */
.event-card__details {
padding: 1rem;
border-top: 1px solid var(--color-text-heading);
background: var(--color-text-heading);
border-top: 1px solid var(--color-border-primary);
background: var(--color-surface-elevated);
}
.detail-row {
@@ -433,7 +433,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.detail-value {
color: var(--color-border-primary);
color: var(--color-text-primary);
}
.detail-value--mono {
@@ -445,13 +445,13 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.diff-section, .state-section {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--color-text-heading);
border-top: 1px solid var(--color-border-primary);
}
.diff-section h4, .state-section h4 {
margin: 0 0 0.75rem;
font-size: 0.9rem;
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.diff-viewer {
@@ -461,7 +461,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.diff-group {
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-md);
padding: 0.75rem;
border-left: 3px solid;
@@ -501,7 +501,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.state-block {
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-md);
overflow: hidden;
}
@@ -510,7 +510,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
padding: 0.5rem 0.75rem;
font-size: 0.75rem;
text-transform: uppercase;
background: var(--color-text-primary);
background: var(--color-surface-tertiary);
}
.state-block--before .state-block__title { color: var(--color-status-error-border); }
@@ -520,7 +520,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
padding: 0.75rem;
margin: 0;
font-size: 0.8rem;
color: var(--color-border-primary);
color: var(--color-text-primary);
white-space: pre-wrap;
overflow-x: auto;
}
@@ -532,8 +532,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
align-items: center;
margin-top: 1rem;
padding: 1rem;
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
}
@@ -550,11 +550,11 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.pagination__current {
font-size: 0.85rem;
color: var(--color-border-primary);
color: var(--color-text-primary);
}
/* Empty & Loading States */
.empty-state, .loading-state {
/* Empty State */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
@@ -593,6 +593,11 @@ export class GovernanceAuditComponent implements OnInit {
'conflict_resolved',
];
readonly eventTypeOptions = [
{ id: '', label: 'All Types' },
...this.eventTypes.map(t => ({ id: t, label: this.formatEventType(t) })),
];
protected filters = {
eventType: '',
actor: '',

View File

@@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
import { Component, ChangeDetectionStrategy, inject, signal, OnInit } from '@angular/core';
import { Component, ChangeDetectionStrategy, inject, signal, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, RouterModule } from '@angular/router';
import { finalize } from 'rxjs/operators';
@@ -11,6 +11,9 @@ import {
TrustWeightAffectedFinding,
Severity,
} from '../../core/api/policy-governance.models';
import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component';
import { StellaMetricCardComponent } from '../../shared/components/stella-metric-card/stella-metric-card.component';
import { StellaMetricGridComponent } from '../../shared/components/stella-metric-card/stella-metric-grid.component';
import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
/**
@@ -21,7 +24,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
*/
@Component({
selector: 'app-impact-preview',
imports: [CommonModule, RouterModule],
imports: [CommonModule, RouterModule, ConfirmDialogComponent, StellaMetricCardComponent, StellaMetricGridComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="preview" [attr.aria-busy]="loading()">
@@ -40,20 +43,23 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
@if (impact(); as i) {
<!-- Impact Summary -->
<div class="summary-grid">
<div class="summary-card">
<div class="summary-card__value">{{ i.affectedVulnerabilities }}</div>
<div class="summary-card__label">Affected Vulnerabilities</div>
</div>
<div class="summary-card summary-card--warning">
<div class="summary-card__value">{{ i.severityChanges }}</div>
<div class="summary-card__label">Severity Changes</div>
</div>
<div class="summary-card summary-card--danger">
<div class="summary-card__value">{{ i.decisionChanges }}</div>
<div class="summary-card__label">Decision Changes</div>
</div>
</div>
<stella-metric-grid [columns]="3">
<stella-metric-card
label="Affected Vulnerabilities"
[value]="i.affectedVulnerabilities"
subtitle="total impacted"
icon="M12 9v3.75m0 3.008v.007|||M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Z" />
<stella-metric-card
label="Severity Changes"
[value]="i.severityChanges"
subtitle="severity transitions"
icon="M3 7.5 7.5 3m0 0L12 7.5M7.5 3v13.5m13.5-6L16.5 16.5m0 0L12 10.5m4.5 6V3" />
<stella-metric-card
label="Decision Changes"
[value]="i.decisionChanges"
subtitle="policy decisions affected"
icon="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
</stella-metric-grid>
<!-- Severity Transitions -->
@if (getTransitionEntries(i.severityTransitions).length > 0) {
@@ -187,6 +193,12 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
<a routerLink="../trust-weights" class="btn btn--primary">Configure Trust Weights</a>
</div>
}
<app-confirm-dialog #applyConfirmDialog
title="Apply Changes"
message="Are you sure you want to apply these changes? This action cannot be undone."
confirmLabel="Apply" cancelLabel="Cancel" variant="warning"
(confirmed)="executeApply()" />
</div>
`,
styles: [`
@@ -208,7 +220,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
margin: 0;
font-size: 1.25rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.preview__subtitle {
@@ -240,54 +252,13 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.btn--primary:hover:not(:disabled) { background: var(--color-status-info); }
.btn--primary:disabled { opacity: 0.5; cursor: not-allowed; }
.btn--ghost { background: transparent; color: var(--color-text-muted); border: 1px solid var(--color-text-primary); }
.btn--ghost:hover { background: var(--color-text-primary); color: var(--color-border-primary); }
/* Summary Grid */
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.summary-card {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
border-radius: var(--radius-lg);
padding: 1.25rem;
text-align: center;
}
.summary-card--warning {
border-color: rgba(234, 179, 8, 0.3);
background: rgba(234, 179, 8, 0.05);
}
.summary-card--danger {
border-color: rgba(239, 68, 68, 0.3);
background: rgba(239, 68, 68, 0.05);
}
.summary-card__value {
font-size: 2.5rem;
font-weight: var(--font-weight-bold);
color: var(--color-status-info);
}
.summary-card--warning .summary-card__value { color: var(--color-status-warning); }
.summary-card--danger .summary-card__value { color: var(--color-status-error); }
.summary-card__label {
margin-top: 0.25rem;
font-size: 0.9rem;
color: var(--color-text-muted);
}
.btn--ghost { background: transparent; color: var(--color-text-muted); border: 1px solid var(--color-border-primary); }
.btn--ghost:hover { background: var(--color-surface-tertiary); color: var(--color-text-primary); }
/* Sections */
.section {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
padding: 1.25rem;
margin-bottom: 1rem;
@@ -304,13 +275,13 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
margin: 0;
font-size: 1rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.section__badge {
font-size: 0.8rem;
padding: 0.2rem 0.6rem;
background: var(--color-text-primary);
background: var(--color-surface-tertiary);
color: var(--color-text-muted);
border-radius: var(--radius-full);
}
@@ -327,13 +298,13 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-md);
}
.transition-card__label {
font-size: 0.85rem;
color: var(--color-border-primary);
color: var(--color-text-primary);
}
.transition-card__count {
@@ -356,7 +327,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.findings-table td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid var(--color-text-heading);
border-bottom: 1px solid var(--color-border-primary);
}
.findings-table th {
@@ -365,23 +336,23 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.03em;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
}
.findings-table td {
font-size: 0.9rem;
color: var(--color-border-primary);
color: var(--color-text-primary);
}
.findings-table tr:hover td {
background: var(--color-text-heading);
background: var(--color-surface-elevated);
}
.purl {
font-family: monospace;
font-size: 0.8rem;
color: var(--color-status-info);
background: var(--color-text-heading);
background: var(--color-surface-elevated);
padding: 0.15rem 0.35rem;
border-radius: var(--radius-sm);
max-width: 200px;
@@ -393,7 +364,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.advisory-id {
font-family: monospace;
font-size: 0.85rem;
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
/* Severity Badges */
@@ -410,7 +381,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.severity--high { background: var(--color-severity-high); color: var(--color-severity-high-bg); }
.severity--medium { background: var(--color-status-warning-text); color: #fff; }
.severity--low { background: var(--color-status-info-text); color: #fff; }
.severity--info { background: var(--color-text-primary); color: var(--color-text-muted); }
.severity--info { background: var(--color-surface-tertiary); color: var(--color-text-muted); }
.change-indicator {
display: inline-flex;
@@ -446,8 +417,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
align-items: flex-start;
gap: 1rem;
padding: 1rem 1.25rem;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
background: var(--color-status-error-bg);
border: 1px solid var(--color-status-error-border);
border-radius: var(--radius-lg);
margin-top: 1rem;
}
@@ -478,8 +449,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
justify-content: center;
padding: 4rem 2rem;
text-align: center;
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
}
@@ -490,7 +461,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.empty-state h3 {
margin: 0 0 0.5rem;
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.empty-state p {
@@ -512,7 +483,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.spinner {
width: 40px;
height: 40px;
border: 3px solid var(--color-text-heading);
border: 3px solid var(--color-border-primary);
border-top-color: var(--color-status-info);
border-radius: var(--radius-full);
animation: spin 0.8s linear infinite;
@@ -580,11 +551,13 @@ export class ImpactPreviewComponent implements OnInit {
return projectedIndex > currentIndex ? 'up' : 'down';
}
protected applyChanges(): void {
if (!confirm('Are you sure you want to apply these changes? This action cannot be undone.')) {
return;
}
@ViewChild('applyConfirmDialog') private applyConfirmDialog!: ConfirmDialogComponent;
protected applyChanges(): void {
this.applyConfirmDialog.open();
}
protected executeApply(): void {
this.applying.set(true);
// In real implementation, apply the trust weight changes
setTimeout(() => {

View File

@@ -1,5 +1,6 @@
import { CommonModule } from '@angular/common';
import { Component, ChangeDetectionStrategy, inject, signal, OnInit } from '@angular/core';
import { ModalComponent } from '../../shared/components/modal/modal.component';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { finalize } from 'rxjs/operators';
@@ -13,6 +14,9 @@ import {
PolicyConflictType,
PolicyConflictSeverity,
} from '../../core/api/policy-governance.models';
import { StellaMetricCardComponent } from '../../shared/components/stella-metric-card/stella-metric-card.component';
import { StellaMetricGridComponent } from '../../shared/components/stella-metric-card/stella-metric-grid.component';
import { StellaFilterChipComponent } from '../../shared/components/stella-filter-chip/stella-filter-chip.component';
import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
/**
@@ -23,7 +27,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
*/
@Component({
selector: 'app-policy-conflict-dashboard',
imports: [CommonModule, FormsModule, RouterModule],
imports: [CommonModule, FormsModule, RouterModule, ModalComponent, StellaMetricCardComponent, StellaMetricGridComponent, StellaFilterChipComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="conflicts" [attr.aria-busy]="loading()">
@@ -42,24 +46,28 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
@if (dashboard(); as d) {
<!-- Summary Cards -->
<div class="summary-grid">
<div class="summary-card">
<div class="summary-card__value">{{ d.totalConflicts }}</div>
<div class="summary-card__label">Total Conflicts</div>
</div>
<div class="summary-card summary-card--open">
<div class="summary-card__value">{{ d.openConflicts }}</div>
<div class="summary-card__label">Open</div>
</div>
<div class="summary-card">
<div class="summary-card__value">{{ d.bySeverity['warning'] || 0 }}</div>
<div class="summary-card__label">Warnings</div>
</div>
<div class="summary-card">
<div class="summary-card__value">{{ d.bySeverity['error'] || 0 }}</div>
<div class="summary-card__label">Errors</div>
</div>
</div>
<stella-metric-grid [columns]="4">
<stella-metric-card
label="Total Conflicts"
[value]="d.totalConflicts"
subtitle="all detected"
icon="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
<stella-metric-card
label="Open"
[value]="d.openConflicts"
subtitle="need attention"
icon="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
<stella-metric-card
label="Warnings"
[value]="d.bySeverity['warning'] || 0"
subtitle="warning severity"
icon="M12 9v3.75m0 3.008v.007|||M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Z" />
<stella-metric-card
label="Errors"
[value]="d.bySeverity['error'] || 0"
subtitle="error severity"
icon="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</stella-metric-grid>
<!-- By Type Breakdown -->
<div class="breakdown-section">
@@ -93,27 +101,18 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
<!-- Filters -->
<div class="filters">
<div class="filter-group">
<label class="filter-label">Type</label>
<select [(ngModel)]="typeFilter" (change)="loadConflicts()" class="form-select">
<option value="">All Types</option>
<option value="rule_overlap">Rule Overlap</option>
<option value="precedence_ambiguity">Precedence Ambiguity</option>
<option value="circular_dependency">Circular Dependency</option>
<option value="incompatible_actions">Incompatible Actions</option>
<option value="scope_collision">Scope Collision</option>
</select>
</div>
<div class="filter-group">
<label class="filter-label">Severity</label>
<select [(ngModel)]="severityFilter" (change)="loadConflicts()" class="form-select">
<option value="">All Severities</option>
<option value="critical">Critical</option>
<option value="error">Error</option>
<option value="warning">Warning</option>
<option value="info">Info</option>
</select>
</div>
<stella-filter-chip
label="Type"
[value]="typeFilter"
[options]="typeOptions"
(valueChange)="typeFilter = $event; loadConflicts()"
/>
<stella-filter-chip
label="Severity"
[value]="severityFilter"
[options]="severityOptions"
(valueChange)="severityFilter = $event; loadConflicts()"
/>
</div>
<!-- Conflict List -->
@@ -204,6 +203,48 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
<p>Your policy configuration has no detected conflicts.</p>
</div>
}
<!-- Resolve Conflict Modal -->
@if (showResolveModal()) {
<app-modal [open]="true" title="Resolve Conflict" size="md" (closed)="showResolveModal.set(false)">
<div class="form-field">
<label class="form-label" for="resolutionNotes">Resolution Notes *</label>
<textarea
id="resolutionNotes"
class="form-textarea"
rows="4"
placeholder="Enter resolution notes..."
[ngModel]="resolveText()"
(ngModelChange)="resolveText.set($event)"
></textarea>
</div>
<ng-container modal-footer>
<button class="btn btn--ghost" (click)="showResolveModal.set(false)">Cancel</button>
<button class="btn btn--primary" (click)="submitResolve()" [disabled]="!resolveText()">Resolve</button>
</ng-container>
</app-modal>
}
<!-- Ignore Conflict Modal -->
@if (showIgnoreModal()) {
<app-modal [open]="true" title="Ignore Conflict" size="md" (closed)="showIgnoreModal.set(false)">
<div class="form-field">
<label class="form-label" for="ignoreReason">Reason for Ignoring *</label>
<textarea
id="ignoreReason"
class="form-textarea"
rows="4"
placeholder="Enter reason for ignoring..."
[ngModel]="ignoreText()"
(ngModelChange)="ignoreText.set($event)"
></textarea>
</div>
<ng-container modal-footer>
<button class="btn btn--ghost" (click)="showIgnoreModal.set(false)">Cancel</button>
<button class="btn btn--primary" (click)="submitIgnore()" [disabled]="!ignoreText()">Ignore</button>
</ng-container>
</app-modal>
}
</div>
`,
styles: [`
@@ -224,7 +265,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
margin: 0;
font-size: 1.25rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.conflicts__subtitle {
@@ -251,55 +292,19 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.btn--primary:hover:not(:disabled) { background: var(--color-status-info); }
.btn--primary:disabled { opacity: 0.5; cursor: not-allowed; }
.btn--secondary { background: var(--color-text-primary); color: var(--color-border-primary); border: 1px solid var(--color-text-primary); }
.btn--secondary:hover:not(:disabled) { background: var(--color-text-primary); }
.btn--secondary { background: var(--color-surface-tertiary); color: var(--color-text-primary); border: 1px solid var(--color-border-primary); }
.btn--secondary:hover:not(:disabled) { background: var(--color-surface-tertiary); }
.btn--secondary:disabled { opacity: 0.5; cursor: not-allowed; }
.btn--ghost { background: transparent; color: var(--color-text-muted); }
.btn--ghost:hover { background: var(--color-text-primary); color: var(--color-border-primary); }
.btn--ghost:hover { background: var(--color-surface-tertiary); color: var(--color-text-primary); }
.btn--small { padding: 0.35rem 0.75rem; font-size: 0.8rem; }
/* Summary Grid */
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.summary-card {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
border-radius: var(--radius-lg);
padding: 1rem;
text-align: center;
}
.summary-card--open {
border-color: var(--color-status-warning);
background: rgba(234, 179, 8, 0.05);
}
.summary-card__value {
font-size: 2rem;
font-weight: var(--font-weight-bold);
color: var(--color-status-info);
}
.summary-card--open .summary-card__value {
color: var(--color-status-warning);
}
.summary-card__label {
font-size: 0.85rem;
color: var(--color-text-muted);
}
/* Breakdown Section */
.breakdown-section, .trend-section {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
padding: 1rem;
margin-bottom: 1rem;
@@ -309,7 +314,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
margin: 0 0 1rem;
font-size: 0.9rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.type-breakdown {
@@ -329,12 +334,12 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.type-bar__label {
font-size: 0.85rem;
color: var(--color-border-primary);
color: var(--color-text-primary);
}
.type-bar__track {
height: 8px;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-sm);
overflow: hidden;
}
@@ -388,32 +393,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.filter-group {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.filter-label {
font-size: 0.8rem;
color: var(--color-text-muted);
}
.form-select {
padding: 0.5rem 0.75rem;
background: var(--color-text-heading);
border: 1px solid var(--color-text-primary);
border-radius: var(--radius-md);
color: var(--color-border-primary);
font-size: 0.85rem;
min-width: 150px;
}
.form-select:focus {
outline: none;
border-color: var(--color-status-info);
overflow: visible;
}
/* Conflict List */
@@ -424,8 +404,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.conflict-card {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
overflow: hidden;
border-left: 3px solid;
@@ -441,7 +421,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
}
.conflict-card__badges {
@@ -457,17 +437,17 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
font-weight: var(--font-weight-semibold);
}
.badge--type { background: var(--color-text-primary); color: var(--color-text-muted); }
.badge--type { background: var(--color-surface-tertiary); color: var(--color-text-muted); }
.badge--info { background: var(--color-status-info-text); color: #fff; }
.badge--warning { background: var(--color-status-warning-text); color: #fff; }
.badge--error { background: var(--color-severity-high); color: var(--color-severity-high-bg); }
.badge--critical { background: var(--color-status-error-text); color: #fff; }
.badge--status { background: var(--color-text-primary); color: var(--color-text-muted); }
.badge--status { background: var(--color-surface-tertiary); color: var(--color-text-muted); }
.badge--status-open { background: var(--color-status-warning-text); color: #fff; }
.badge--status-acknowledged { background: var(--color-status-info-text); color: #fff; }
.badge--status-resolved { background: var(--color-status-success-text); color: #fff; }
.badge--status-ignored { background: var(--color-text-primary); color: var(--color-text-muted); }
.badge--status-ignored { background: var(--color-surface-tertiary); color: var(--color-text-muted); }
.conflict-card__time {
font-size: 0.75rem;
@@ -481,7 +461,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.conflict-card__summary {
margin: 0 0 0.5rem;
font-size: 1rem;
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.conflict-card__desc {
@@ -497,7 +477,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
gap: 0.5rem;
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-md);
}
@@ -524,7 +504,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.conflict-impact__text {
color: var(--color-border-primary);
color: var(--color-text-primary);
}
.conflict-suggestion {
@@ -532,7 +512,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
align-items: flex-start;
gap: 0.5rem;
padding: 0.75rem;
background: rgba(34, 211, 238, 0.1);
background: var(--color-status-info-bg);
border-radius: var(--radius-md);
font-size: 0.85rem;
color: var(--color-status-info-border);
@@ -546,7 +526,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.conflict-resolution {
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid var(--color-text-heading);
border-top: 1px solid var(--color-border-primary);
}
.resolution-by {
@@ -564,8 +544,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
display: flex;
gap: 0.5rem;
padding: 0.75rem 1rem;
border-top: 1px solid var(--color-text-heading);
background: var(--color-text-heading);
border-top: 1px solid var(--color-border-primary);
background: var(--color-surface-elevated);
}
/* Empty State */
@@ -576,8 +556,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
justify-content: center;
padding: 3rem;
text-align: center;
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
}
@@ -588,13 +568,42 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.empty-state h3 {
margin: 0 0 0.5rem;
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.empty-state p {
margin: 0;
color: var(--color-text-muted);
}
/* Form Fields (for modals) */
.form-field {
margin-bottom: 1rem;
}
.form-label {
display: block;
color: var(--color-text-primary);
font-size: 0.85rem;
font-weight: var(--font-weight-medium);
margin-bottom: 0.35rem;
}
.form-textarea {
width: 100%;
padding: 0.5rem 0.75rem;
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-md);
color: var(--color-text-primary);
font-size: 0.9rem;
resize: vertical;
}
.form-textarea:focus {
outline: none;
border-color: var(--color-status-info);
}
`]
})
export class PolicyConflictDashboardComponent implements OnInit {
@@ -606,9 +615,32 @@ export class PolicyConflictDashboardComponent implements OnInit {
protected readonly dashboard = signal<PolicyConflictDashboard | null>(null);
protected readonly conflicts = signal<PolicyConflict[]>([]);
protected readonly showResolveModal = signal(false);
protected readonly showIgnoreModal = signal(false);
protected readonly resolveText = signal('');
protected readonly ignoreText = signal('');
private readonly pendingConflict = signal<PolicyConflict | null>(null);
protected typeFilter: PolicyConflictType | '' = '';
protected severityFilter: PolicyConflictSeverity | '' = '';
readonly typeOptions = [
{ id: '', label: 'All Types' },
{ id: 'rule_overlap', label: 'Rule Overlap' },
{ id: 'precedence_ambiguity', label: 'Precedence Ambiguity' },
{ id: 'circular_dependency', label: 'Circular Dependency' },
{ id: 'incompatible_actions', label: 'Incompatible Actions' },
{ id: 'scope_collision', label: 'Scope Collision' },
];
readonly severityOptions = [
{ id: '', label: 'All Severities' },
{ id: 'critical', label: 'Critical' },
{ id: 'error', label: 'Error' },
{ id: 'warning', label: 'Warning' },
{ id: 'info', label: 'Info' },
];
ngOnInit(): void {
this.loadDashboard();
this.loadConflicts();
@@ -683,9 +715,17 @@ export class PolicyConflictDashboardComponent implements OnInit {
}
protected resolveConflict(conflict: PolicyConflict): void {
const resolution = prompt('Enter resolution notes:');
if (!resolution) return;
this.pendingConflict.set(conflict);
this.resolveText.set('');
this.showResolveModal.set(true);
}
protected submitResolve(): void {
const conflict = this.pendingConflict();
const resolution = this.resolveText();
if (!conflict || !resolution) return;
this.showResolveModal.set(false);
this.api.resolveConflict(conflict.id, resolution, this.governanceScope()).subscribe({
next: () => {
this.loadConflicts();
@@ -696,9 +736,17 @@ export class PolicyConflictDashboardComponent implements OnInit {
}
protected ignoreConflict(conflict: PolicyConflict): void {
const reason = prompt('Enter reason for ignoring:');
if (!reason) return;
this.pendingConflict.set(conflict);
this.ignoreText.set('');
this.showIgnoreModal.set(true);
}
protected submitIgnore(): void {
const conflict = this.pendingConflict();
const reason = this.ignoreText();
if (!conflict || !reason) return;
this.showIgnoreModal.set(false);
this.api.ignoreConflict(conflict.id, reason, this.governanceScope()).subscribe({
next: () => {
this.loadConflicts();

View File

@@ -1,6 +1,8 @@
import { Component, ChangeDetectionStrategy, signal, inject } from '@angular/core';
import { Router, RouterOutlet } from '@angular/router';
import { Component, ChangeDetectionStrategy, signal, inject, OnInit, DestroyRef } from '@angular/core';
import { Router, RouterOutlet, NavigationEnd, ActivatedRoute } from '@angular/router';
import { filter } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { StellaPageTabsComponent, StellaPageTab } from '../../shared/components/stella-page-tabs/stella-page-tabs.component';
/**
@@ -30,7 +32,6 @@ const GOVERNANCE_TABS: readonly StellaPageTab[] = [
template: `
<section class="governance">
<div class="governance__header">
<p class="governance__eyebrow">Admin / Policy</p>
<h1 class="governance__title">Policy Governance</h1>
<p class="governance__subtitle">Configure risk budgets, trust weights, staleness rules, sealed mode, and risk profiles.</p>
</div>
@@ -38,7 +39,6 @@ const GOVERNANCE_TABS: readonly StellaPageTab[] = [
<stella-page-tabs
[tabs]="GOVERNANCE_TABS"
[activeTab]="activeTab()"
urlParam="tab"
(tabChange)="onTabChange($any($event))"
ariaLabel="Policy governance sections"
>
@@ -64,15 +64,6 @@ const GOVERNANCE_TABS: readonly StellaPageTab[] = [
margin-bottom: 1.5rem;
}
.governance__eyebrow {
margin: 0;
color: var(--color-status-info);
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 0.75rem;
font-weight: var(--font-weight-semibold);
}
.governance__title {
margin: 0.25rem 0 0;
font-size: 1.75rem;
@@ -88,7 +79,7 @@ const GOVERNANCE_TABS: readonly StellaPageTab[] = [
`]
})
export class PolicyGovernanceComponent {
export class PolicyGovernanceComponent implements OnInit {
private static readonly TAB_ROUTES: Record<string, string> = {
budget: '/ops/policy/governance',
trust: '/ops/policy/governance/trust-weights',
@@ -102,11 +93,45 @@ export class PolicyGovernanceComponent {
'schema-docs': '/ops/policy/governance/schema-docs',
};
private static readonly ROUTE_TO_TAB: Record<string, string> = {
'': 'budget',
'overview': 'budget',
'risk-budget': 'budget',
'budget': 'budget',
'trust-weights': 'trust',
'staleness': 'staleness',
'sealed-mode': 'sealed',
'profiles': 'profiles',
'validator': 'validator',
'audit': 'audit',
'conflicts': 'conflicts',
'schema-playground': 'schema-playground',
'schema-docs': 'schema-docs',
};
private readonly router = inject(Router);
private readonly route = inject(ActivatedRoute);
private readonly destroyRef = inject(DestroyRef);
protected readonly activeTab = signal<string>('budget');
protected readonly GOVERNANCE_TABS = GOVERNANCE_TABS;
ngOnInit(): void {
this.syncTabFromRoute();
this.router.events
.pipe(
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
takeUntilDestroyed(this.destroyRef),
)
.subscribe(() => this.syncTabFromRoute());
}
private syncTabFromRoute(): void {
const childPath = this.route.firstChild?.snapshot.url[0]?.path ?? '';
const tabId = PolicyGovernanceComponent.ROUTE_TO_TAB[childPath] ?? 'budget';
this.activeTab.set(tabId);
}
protected onTabChange(tabId: string): void {
this.activeTab.set(tabId);
const route = PolicyGovernanceComponent.TAB_ROUTES[tabId];

View File

@@ -180,7 +180,7 @@ import {
margin: 0;
font-size: 1.25rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.validator__subtitle {
@@ -207,8 +207,8 @@ import {
.editor-section {
display: flex;
flex-direction: column;
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
overflow: hidden;
}
@@ -218,14 +218,14 @@ import {
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--color-text-heading);
background: var(--color-text-heading);
border-bottom: 1px solid var(--color-border-primary);
background: var(--color-surface-elevated);
}
.editor-section__header h3 {
margin: 0;
font-size: 0.9rem;
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.editor-controls {
@@ -236,9 +236,9 @@ import {
.editor {
flex: 1;
padding: 1rem;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border: none;
color: var(--color-border-primary);
color: var(--color-text-primary);
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 0.85rem;
line-height: 1.5;
@@ -252,8 +252,8 @@ import {
.editor-footer {
padding: 0.5rem 1rem;
border-top: 1px solid var(--color-text-heading);
background: var(--color-text-heading);
border-top: 1px solid var(--color-border-primary);
background: var(--color-surface-elevated);
color: var(--color-text-secondary);
font-size: 0.75rem;
}
@@ -262,8 +262,8 @@ import {
.results-section {
display: flex;
flex-direction: column;
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
overflow: hidden;
}
@@ -272,9 +272,9 @@ import {
margin: 0;
padding: 0.75rem 1rem;
font-size: 0.9rem;
color: var(--color-surface-secondary);
border-bottom: 1px solid var(--color-text-heading);
background: var(--color-text-heading);
color: var(--color-text-heading);
border-bottom: 1px solid var(--color-border-primary);
background: var(--color-surface-elevated);
}
/* Buttons */
@@ -292,15 +292,15 @@ import {
.btn--primary:hover:not(:disabled) { background: var(--color-status-info); }
.btn--primary:disabled { opacity: 0.5; cursor: not-allowed; }
.btn--secondary { background: var(--color-text-primary); color: var(--color-border-primary); border: 1px solid var(--color-text-primary); }
.btn--secondary:hover { background: var(--color-text-primary); }
.btn--secondary { background: var(--color-surface-tertiary); color: var(--color-text-primary); border: 1px solid var(--color-border-primary); }
.btn--secondary:hover { background: var(--color-surface-tertiary); }
.form-select--small {
padding: 0.35rem 0.5rem;
background: var(--color-text-heading);
border: 1px solid var(--color-text-primary);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-md);
color: var(--color-border-primary);
color: var(--color-text-primary);
font-size: 0.85rem;
}
@@ -315,13 +315,13 @@ import {
}
.result-summary--valid {
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.3);
background: var(--color-status-success-bg);
border: 1px solid var(--color-status-success-border);
}
.result-summary--invalid {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
background: var(--color-status-error-bg);
border: 1px solid var(--color-status-error-border);
}
.result-summary__icon {
@@ -338,7 +338,7 @@ import {
.result-summary__status {
font-size: 1.1rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.result-summary__meta {
@@ -388,7 +388,7 @@ import {
.issue-item {
padding: 0.75rem;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-md);
border-left: 3px solid;
}
@@ -418,7 +418,7 @@ import {
}
.issue-item__message {
color: var(--color-border-primary);
color: var(--color-text-primary);
font-size: 0.9rem;
}

View File

@@ -244,7 +244,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
margin: 0;
font-size: 1.25rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.config__subtitle {
@@ -261,9 +261,9 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.error-state {
margin-bottom: 1rem;
padding: 0.625rem 0.75rem;
border: 1px solid rgba(239, 68, 68, 0.45);
border: 1px solid var(--color-status-error-border);
border-radius: var(--radius-lg);
background: rgba(239, 68, 68, 0.12);
background: var(--color-status-error-bg);
color: var(--color-status-error-border);
font-size: 0.875rem;
font-weight: var(--font-weight-medium);
@@ -300,12 +300,12 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.btn--ghost {
background: transparent;
color: var(--color-text-muted);
border: 1px solid var(--color-text-primary);
border: 1px solid var(--color-border-primary);
}
.btn--ghost:hover {
background: var(--color-text-primary);
color: var(--color-border-primary);
background: var(--color-surface-tertiary);
color: var(--color-text-primary);
}
.btn--icon {
@@ -321,8 +321,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
/* Form Sections */
.config__section {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
padding: 1.25rem;
margin-bottom: 1rem;
@@ -332,7 +332,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
margin: 0 0 1rem;
font-size: 1rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.config__section-desc {
@@ -364,24 +364,24 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.form-label {
color: var(--color-border-primary);
color: var(--color-text-primary);
font-size: 0.85rem;
font-weight: var(--font-weight-medium);
}
.form-input, .form-select {
padding: 0.5rem 0.75rem;
background: var(--color-text-heading);
border: 1px solid var(--color-text-primary);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-md);
color: var(--color-border-primary);
color: var(--color-text-primary);
font-size: 0.9rem;
}
.form-input:focus, .form-select:focus {
outline: none;
border-color: var(--color-status-info);
box-shadow: 0 0 0 2px rgba(34, 211, 238, 0.2);
box-shadow: 0 0 0 2px var(--color-status-info-bg);
}
.form-select--small {
@@ -408,7 +408,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.form-checkbox__label {
color: var(--color-border-primary);
color: var(--color-text-primary);
font-size: 0.9rem;
}
@@ -458,8 +458,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.threshold-item {
background: var(--color-text-heading);
border: 1px solid var(--color-text-primary);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
padding: 1rem;
}
@@ -494,8 +494,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
align-items: center;
gap: 0.35rem;
padding: 0.35rem 0.6rem;
background: var(--color-text-primary);
border: 1px solid var(--color-text-primary);
background: var(--color-surface-tertiary);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-full);
cursor: pointer;
font-size: 0.8rem;
@@ -504,7 +504,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.action-chip:has(input:checked) {
background: rgba(34, 211, 238, 0.2);
background: var(--color-status-info-bg);
border-color: var(--color-status-info);
color: var(--color-status-info);
}

View File

@@ -11,6 +11,9 @@ import {
RiskBudgetContributor,
RiskBudgetAlert,
} from '../../core/api/policy-governance.models';
import { StellaMetricCardComponent } from '../../shared/components/stella-metric-card/stella-metric-card.component';
import { StellaMetricGridComponent } from '../../shared/components/stella-metric-card/stella-metric-grid.component';
import { LoadingStateComponent } from '../../shared/components/loading-state/loading-state.component';
import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
/**
@@ -21,7 +24,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
*/
@Component({
selector: 'app-risk-budget-dashboard',
imports: [CommonModule, RouterModule],
imports: [CommonModule, RouterModule, StellaMetricCardComponent, StellaMetricGridComponent, LoadingStateComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="dashboard" [attr.aria-busy]="loading()">
@@ -41,37 +44,28 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
@if (data(); as d) {
<!-- KPI Cards -->
<div class="kpi-grid">
<div class="kpi-card">
<div class="kpi-card__label">Current Utilization</div>
<div class="kpi-card__value" [class]="'kpi-card__value--' + d.status">
{{ d.utilizationPercent | number:'1.0-0' }}%
</div>
<div class="kpi-card__meta">{{ d.currentRiskPoints }} / {{ d.config.totalBudget }} points</div>
</div>
<div class="kpi-card">
<div class="kpi-card__label">Headroom</div>
<div class="kpi-card__value">{{ d.headroom }}</div>
<div class="kpi-card__meta" [class.kpi-card__meta--negative]="d.kpis.headroomDelta24h < 0">
{{ d.kpis.headroomDelta24h >= 0 ? '+' : '' }}{{ d.kpis.headroomDelta24h }} (24h)
</div>
</div>
<div class="kpi-card">
<div class="kpi-card__label">Burn Rate</div>
<div class="kpi-card__value">{{ d.kpis.burnRate | number:'1.1-1' }}</div>
<div class="kpi-card__meta">points/day</div>
</div>
<div class="kpi-card">
<div class="kpi-card__label">Days to Exceeded</div>
<div class="kpi-card__value" [class.kpi-card__value--warning]="(d.kpis.projectedDaysToExceeded ?? 999) < 30">
{{ d.kpis.projectedDaysToExceeded ?? '--' }}
</div>
<div class="kpi-card__meta">projected</div>
</div>
</div>
<stella-metric-grid [columns]="4">
<stella-metric-card
label="Current Utilization"
[value]="formatUtilization(d.utilizationPercent)"
[subtitle]="d.currentRiskPoints + ' / ' + d.config.totalBudget + ' points'"
icon="M18 20V10|||M12 20V4|||M6 20v-6" />
<stella-metric-card
label="Headroom"
[value]="d.headroom"
[subtitle]="(d.kpis.headroomDelta24h >= 0 ? '+' : '') + d.kpis.headroomDelta24h + ' (24h)'"
icon="M2.25 18 9 11.25l4.306 4.306a11.95 11.95 0 0 1 5.814-5.518l2.74-1.22M16.5 6.75V10.5h3.75" />
<stella-metric-card
label="Burn Rate"
[value]="formatBurnRate(d.kpis.burnRate)"
subtitle="points/day"
icon="M15.362 5.214A8.252 8.252 0 0 1 12 21a8.252 8.252 0 0 1-6.038-7.048 8.287 8.287 0 0 0 9.4-7.738Z|||M12 3v1.5" />
<stella-metric-card
label="Days to Exceeded"
[value]="'' + (d.kpis.projectedDaysToExceeded ?? '--')"
subtitle="projected"
icon="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</stella-metric-grid>
<!-- Budget Progress Bar -->
<div class="progress-section">
@@ -130,7 +124,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
<li class="contributor-item">
<div class="contributor-item__header">
<span class="contributor-item__type">{{ contrib.type }}</span>
<span class="contributor-item__name">{{ contrib.displayName }}</span>
<span class="contributor-item__name" [title]="contrib.displayName">{{ contrib.displayName }}</span>
<span class="contributor-item__trend" [class]="'contributor-item__trend--' + contrib.trend">
{{ getTrendIcon(contrib.trend) }}
</span>
@@ -191,7 +185,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
<span>Last Updated: {{ d.updatedAt | date:'medium' }}</span>
</div>
} @else if (loading()) {
<div class="loading-state">Loading budget data...</div>
<app-loading-state message="Loading budget data..." />
}
</div>
`,
@@ -213,7 +207,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
margin: 0;
font-size: 1.25rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.dashboard__subtitle {
@@ -225,9 +219,9 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.error-state {
margin-bottom: 1rem;
padding: 0.625rem 0.75rem;
border: 1px solid rgba(239, 68, 68, 0.45);
border: 1px solid var(--color-status-error-border);
border-radius: var(--radius-lg);
background: rgba(239, 68, 68, 0.12);
background: var(--color-status-error-bg);
color: var(--color-status-error-border);
font-size: 0.875rem;
font-weight: var(--font-weight-medium);
@@ -247,13 +241,13 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.btn--secondary {
background: var(--color-text-primary);
color: var(--color-border-primary);
border: 1px solid var(--color-text-primary);
background: var(--color-surface-tertiary);
color: var(--color-text-primary);
border: 1px solid var(--color-border-primary);
}
.btn--secondary:hover {
background: var(--color-text-primary);
background: var(--color-surface-tertiary);
}
.btn--small {
@@ -268,49 +262,6 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
background: var(--color-status-info);
}
/* KPI Grid */
.kpi-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.kpi-card {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
border-radius: var(--radius-lg);
padding: 1rem;
}
.kpi-card__label {
color: var(--color-text-muted);
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.kpi-card__value {
font-size: 2rem;
font-weight: var(--font-weight-bold);
color: var(--color-status-info);
margin: 0.25rem 0;
}
.kpi-card__value--healthy { color: var(--color-status-success); }
.kpi-card__value--warning { color: var(--color-status-warning); }
.kpi-card__value--critical { color: var(--color-status-error); }
.kpi-card__value--exceeded { color: var(--color-status-error); }
.kpi-card__meta {
color: var(--color-text-secondary);
font-size: 0.85rem;
}
.kpi-card__meta--negative {
color: var(--color-status-error);
}
/* Progress Bar */
.progress-section {
margin-bottom: 1.5rem;
@@ -323,7 +274,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.progress-bar__track {
position: relative;
height: 24px;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-xl);
overflow: hidden;
}
@@ -403,8 +354,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.chart-card {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
padding: 1rem;
}
@@ -413,7 +364,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
margin: 0 0 1rem;
font-size: 1rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.simple-chart {
@@ -467,7 +418,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.contributor-item {
padding: 0.5rem 0;
border-bottom: 1px solid var(--color-text-heading);
border-bottom: 1px solid var(--color-border-primary);
}
.contributor-item:last-child {
@@ -484,7 +435,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.contributor-item__type {
font-size: 0.7rem;
padding: 0.15rem 0.4rem;
background: var(--color-text-primary);
background: var(--color-surface-tertiary);
border-radius: var(--radius-sm);
color: var(--color-text-muted);
text-transform: uppercase;
@@ -492,7 +443,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.contributor-item__name {
flex: 1;
color: var(--color-border-primary);
color: var(--color-text-primary);
font-weight: var(--font-weight-medium);
font-size: 0.9rem;
overflow: hidden;
@@ -510,7 +461,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.contributor-item__bar {
height: 6px;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-sm);
margin-bottom: 0.25rem;
}
@@ -541,7 +492,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
margin: 0 0 0.75rem;
font-size: 1rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.alert-list {
@@ -558,19 +509,19 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-lg);
border-left: 3px solid var(--color-status-warning);
}
.alert-item--high, .alert-item--critical {
border-left-color: var(--color-status-error);
background: rgba(239, 68, 68, 0.1);
background: var(--color-status-error-bg);
}
.alert-item--medium {
border-left-color: var(--color-status-warning);
background: rgba(234, 179, 8, 0.1);
background: var(--color-status-warning-bg);
}
.alert-item__icon {
@@ -591,7 +542,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.alert-item__title {
color: var(--color-border-primary);
color: var(--color-text-primary);
font-weight: var(--font-weight-medium);
}
@@ -613,16 +564,9 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
color: var(--color-text-secondary);
font-size: 0.8rem;
padding-top: 1rem;
border-top: 1px solid var(--color-text-heading);
border-top: 1px solid var(--color-border-primary);
}
.loading-state {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: var(--color-text-muted);
}
`]
})
export class RiskBudgetDashboardComponent implements OnInit {
@@ -738,6 +682,14 @@ export class RiskBudgetDashboardComponent implements OnInit {
return numericValue > 0 ? numericValue : fallback;
}
protected formatUtilization(percent: number): string {
return `${Math.round(percent)}%`;
}
protected formatBurnRate(rate: number): string {
return rate.toFixed(1);
}
protected formatDate(timestamp: string): string {
const date = new Date(timestamp);
return `${date.getMonth() + 1}/${date.getDate()}`;

View File

@@ -265,7 +265,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
margin: 0;
font-size: 1.25rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.editor__subtitle {
@@ -297,11 +297,11 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.btn--primary:hover:not(:disabled) { background: var(--color-status-info); }
.btn--primary:disabled { opacity: 0.5; cursor: not-allowed; }
.btn--secondary { background: var(--color-text-primary); color: var(--color-border-primary); border: 1px solid var(--color-text-primary); }
.btn--secondary:hover:not(:disabled) { background: var(--color-text-primary); }
.btn--secondary { background: var(--color-surface-tertiary); color: var(--color-text-primary); border: 1px solid var(--color-border-primary); }
.btn--secondary:hover:not(:disabled) { background: var(--color-surface-tertiary); }
.btn--ghost { background: transparent; color: var(--color-text-muted); }
.btn--ghost:hover { background: var(--color-text-primary); color: var(--color-border-primary); }
.btn--ghost:hover { background: var(--color-surface-tertiary); color: var(--color-text-primary); }
.btn--icon { padding: 0.35rem; }
.btn--danger:hover { color: var(--color-status-error); }
@@ -310,7 +310,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
width: 100%;
justify-content: center;
padding: 0.75rem;
border: 1px dashed var(--color-text-primary);
border: 1px dashed var(--color-border-primary);
margin-top: 0.75rem;
}
@@ -326,14 +326,14 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.validation-banner--success {
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.3);
background: var(--color-status-success-bg);
border: 1px solid var(--color-status-success-border);
color: var(--color-status-success-border);
}
.validation-banner--error {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
background: var(--color-status-error-bg);
border: 1px solid var(--color-status-error-border);
color: var(--color-status-error-border);
}
@@ -354,8 +354,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
/* Form Sections */
.form-section {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
padding: 1.25rem;
margin-bottom: 1rem;
@@ -372,12 +372,12 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
margin: 0;
font-size: 1rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.form-section__badge {
padding: 0.15rem 0.5rem;
background: var(--color-text-primary);
background: var(--color-surface-tertiary);
border-radius: var(--radius-full);
font-size: 0.8rem;
color: var(--color-text-muted);
@@ -400,17 +400,17 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.form-label {
color: var(--color-border-primary);
color: var(--color-text-primary);
font-size: 0.85rem;
font-weight: var(--font-weight-medium);
}
.form-input, .form-select, .form-textarea {
padding: 0.5rem 0.75rem;
background: var(--color-text-heading);
border: 1px solid var(--color-text-primary);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-md);
color: var(--color-border-primary);
color: var(--color-text-primary);
font-size: 0.9rem;
}
@@ -436,8 +436,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
border-radius: var(--radius-md);
}
.weight-sum--valid { background: rgba(34, 197, 94, 0.2); color: var(--color-status-success-border); }
.weight-sum--invalid { background: rgba(234, 179, 8, 0.2); color: var(--color-status-warning-border); }
.weight-sum--valid { background: var(--color-status-success-bg); color: var(--color-status-success-border); }
.weight-sum--invalid { background: var(--color-status-warning-bg); color: var(--color-status-warning-border); }
.signal-editor {
display: flex;
@@ -451,7 +451,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
gap: 0.75rem;
align-items: center;
padding: 0.5rem;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-md);
}
@@ -490,7 +490,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
position: absolute;
cursor: pointer;
inset: 0;
background: var(--color-text-primary);
background: var(--color-surface-tertiary);
border-radius: var(--radius-2xl);
transition: 0.2s;
}
@@ -502,7 +502,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
width: 14px;
left: 3px;
bottom: 3px;
background: var(--color-border-primary);
background: var(--color-surface-elevated);
border-radius: var(--radius-full);
transition: 0.2s;
}
@@ -522,8 +522,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.override-card {
background: var(--color-text-heading);
border: 1px solid var(--color-text-primary);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
overflow: hidden;
}
@@ -533,7 +533,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
justify-content: space-between;
align-items: center;
padding: 0.5rem 0.75rem;
background: var(--color-text-primary);
background: var(--color-surface-tertiary);
}
.override-card__label {

View File

@@ -1,5 +1,6 @@
import { CommonModule } from '@angular/common';
import { Component, ChangeDetectionStrategy, inject, signal, OnInit } from '@angular/core';
import { Component, ChangeDetectionStrategy, inject, signal, OnInit, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { finalize } from 'rxjs/operators';
@@ -11,6 +12,10 @@ import {
RiskProfileGovernanceStatus,
} from '../../core/api/policy-governance.models';
import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component';
import { ModalComponent } from '../../shared/components/modal/modal.component';
import { LoadingStateComponent } from '../../shared/components/loading-state/loading-state.component';
import { StellaFilterChipComponent } from '../../shared/components/stella-filter-chip/stella-filter-chip.component';
/**
* Risk Profile List component.
@@ -20,7 +25,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
*/
@Component({
selector: 'app-risk-profile-list',
imports: [CommonModule, RouterModule],
imports: [CommonModule, RouterModule, FormsModule, ConfirmDialogComponent, ModalComponent, LoadingStateComponent, StellaFilterChipComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="profiles" [attr.aria-busy]="loading()">
@@ -30,28 +35,12 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
<p class="profiles__subtitle">Manage risk evaluation profiles and signal weights.</p>
</div>
<div class="profiles__actions">
<div class="filter-group">
<button
class="filter-btn"
[class.filter-btn--active]="statusFilter() === null"
(click)="setStatusFilter(null)"
>All</button>
<button
class="filter-btn"
[class.filter-btn--active]="statusFilter() === 'active'"
(click)="setStatusFilter('active')"
>Active</button>
<button
class="filter-btn"
[class.filter-btn--active]="statusFilter() === 'draft'"
(click)="setStatusFilter('draft')"
>Draft</button>
<button
class="filter-btn"
[class.filter-btn--active]="statusFilter() === 'deprecated'"
(click)="setStatusFilter('deprecated')"
>Deprecated</button>
</div>
<stella-filter-chip
label="Status"
[value]="statusFilter() ?? ''"
[options]="statusOptions"
(valueChange)="setStatusFilter($event || null)"
/>
<a routerLink="new" class="btn btn--primary">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="16" height="16">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
@@ -117,7 +106,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
Edit
</a>
@if (profile.status === 'draft') {
<button class="btn btn--ghost btn--small" (click)="activateProfile(profile)">
<button class="btn btn--ghost btn--small" (click)="requestActivateProfile(profile)">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="14" height="14">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
@@ -125,7 +114,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
</button>
}
@if (profile.status === 'active') {
<button class="btn btn--ghost btn--small" (click)="deprecateProfile(profile)">
<button class="btn btn--ghost btn--small" (click)="requestDeprecateProfile(profile)">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="14" height="14">
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 0 0 5.636 5.636m12.728 12.728A9 9 0 0 1 5.636 5.636m12.728 12.728L5.636 5.636" />
</svg>
@@ -133,7 +122,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
</button>
}
@if (profile.status !== 'active') {
<button class="btn btn--ghost btn--small btn--danger" (click)="deleteProfile(profile)">
<button class="btn btn--ghost btn--small btn--danger" (click)="requestDeleteProfile(profile)">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="14" height="14">
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
@@ -145,7 +134,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
</div>
} @else if (loading()) {
<div class="loading-state">Loading profiles...</div>
<app-loading-state message="Loading profiles..." />
} @else {
<div class="empty-state">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="48" height="48">
@@ -157,6 +146,30 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
<a routerLink="new" class="btn btn--primary">Create Profile</a>
</div>
}
<app-confirm-dialog #activateConfirm
title="Activate Profile"
[message]="'Activate profile \\'' + pendingActivateProfile()?.name + '\\'?'"
confirmLabel="Activate" cancelLabel="Cancel" variant="warning"
(confirmed)="executeActivateProfile()" />
<app-confirm-dialog #deleteProfileConfirm
title="Delete Profile"
[message]="'Delete profile \\'' + pendingDeleteProfile()?.name + '\\'? This cannot be undone.'"
confirmLabel="Delete" cancelLabel="Cancel" variant="danger"
(confirmed)="executeDeleteProfile()" />
@if (showDeprecateModal()) {
<app-modal [open]="true" title="Deprecate Profile" size="sm" (closed)="showDeprecateModal.set(false)">
<div class="form-field">
<label class="form-label">Reason for deprecating "{{ pendingDeprecateProfile()?.name }}"</label>
<textarea class="form-textarea" rows="2" [ngModel]="deprecateReasonText()" (ngModelChange)="deprecateReasonText.set($event)" placeholder="Enter reason for deprecation"></textarea>
</div>
<div modal-footer>
<button class="btn btn--ghost" (click)="showDeprecateModal.set(false)">Cancel</button>
<button class="btn btn--primary" [disabled]="!deprecateReasonText()" (click)="executeDeprecateProfile()">Deprecate</button>
</div>
</app-modal>
}
</div>
`,
styles: [`
@@ -179,7 +192,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
margin: 0;
font-size: 1.25rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.profiles__subtitle {
@@ -194,30 +207,6 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
gap: 1rem;
}
.filter-group {
display: flex;
background: var(--color-text-heading);
border-radius: var(--radius-md);
overflow: hidden;
}
.filter-btn {
padding: 0.5rem 0.75rem;
background: none;
border: none;
color: var(--color-text-muted);
font-size: 0.85rem;
cursor: pointer;
transition: all 0.15s ease;
}
.filter-btn:hover { background: var(--color-text-primary); color: var(--color-border-primary); }
.filter-btn--active {
background: var(--color-status-info);
color: var(--color-text-heading);
}
.btn {
padding: 0.5rem 1rem;
border-radius: var(--radius-md);
@@ -236,7 +225,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.btn--primary:hover { background: var(--color-status-info); }
.btn--ghost { background: transparent; color: var(--color-text-muted); }
.btn--ghost:hover { background: var(--color-text-primary); color: var(--color-border-primary); }
.btn--ghost:hover { background: var(--color-surface-tertiary); color: var(--color-text-primary); }
.btn--small { padding: 0.35rem 0.6rem; font-size: 0.8rem; }
@@ -250,8 +239,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.profile-card {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-xl);
padding: 1.25rem;
display: flex;
@@ -262,7 +251,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.profile-card--active { border-left: 3px solid var(--color-status-success); }
.profile-card--draft { border-left: 3px solid var(--color-status-warning); }
.profile-card--deprecated { border-left: 3px solid var(--color-text-secondary); opacity: 0.8; }
.profile-card--archived { border-left: 3px solid var(--color-text-primary); opacity: 0.6; }
.profile-card--archived { border-left: 3px solid var(--color-border-primary); opacity: 0.6; }
.profile-card__header {
display: flex;
@@ -281,8 +270,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.status--active { background: var(--color-status-success-text); color: #fff; }
.status--draft { background: var(--color-status-warning-text); color: #fff; }
.status--deprecated { background: var(--color-text-primary); color: var(--color-text-muted); }
.status--archived { background: var(--color-text-heading); color: var(--color-text-secondary); }
.status--deprecated { background: var(--color-surface-tertiary); color: var(--color-text-muted); }
.status--archived { background: var(--color-surface-elevated); color: var(--color-text-secondary); }
.profile-card__version {
font-size: 0.8rem;
@@ -298,7 +287,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
margin: 0;
font-size: 1.1rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.profile-card__desc {
@@ -318,7 +307,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.profile-card__signals {
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-md);
padding: 0.75rem;
}
@@ -343,7 +332,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
font-size: 0.85rem;
}
.signal-item__name { color: var(--color-border-primary); }
.signal-item__name { color: var(--color-text-primary); }
.signal-item__weight { color: var(--color-status-info); font-weight: var(--font-weight-medium); }
.signal-item--more {
@@ -363,28 +352,48 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
flex-wrap: wrap;
gap: 0.5rem;
padding-top: 0.75rem;
border-top: 1px solid var(--color-text-heading);
border-top: 1px solid var(--color-border-primary);
}
/* Empty State */
.empty-state {
text-align: center;
padding: 3rem;
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-xl);
}
.empty-state svg { color: var(--color-text-secondary); margin-bottom: 1rem; }
.empty-state h3 { margin: 0 0 0.5rem; color: var(--color-surface-secondary); }
.empty-state h3 { margin: 0 0 0.5rem; color: var(--color-text-heading); }
.empty-state p { margin: 0 0 1.5rem; color: var(--color-text-muted); }
.loading-state {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: var(--color-text-muted);
/* Form fields for deprecate modal */
.form-field {
margin-bottom: 1rem;
}
.form-label {
display: block;
color: var(--color-text-primary);
font-size: 0.85rem;
font-weight: var(--font-weight-medium);
margin-bottom: 0.35rem;
}
.form-textarea {
width: 100%;
padding: 0.5rem 0.75rem;
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-md);
color: var(--color-text-primary);
font-size: 0.9rem;
}
.form-textarea:focus {
outline: none;
border-color: var(--color-status-info);
}
`]
})
@@ -396,6 +405,22 @@ export class RiskProfileListComponent implements OnInit {
protected readonly profiles = signal<RiskProfileGov[]>([]);
protected readonly statusFilter = signal<RiskProfileGovernanceStatus | null>(null);
readonly statusOptions = [
{ id: '', label: 'All' },
{ id: 'active', label: 'Active' },
{ id: 'draft', label: 'Draft' },
{ id: 'deprecated', label: 'Deprecated' },
];
protected readonly pendingActivateProfile = signal<RiskProfileGov | null>(null);
protected readonly pendingDeleteProfile = signal<RiskProfileGov | null>(null);
protected readonly pendingDeprecateProfile = signal<RiskProfileGov | null>(null);
protected readonly showDeprecateModal = signal(false);
protected readonly deprecateReasonText = signal('');
@ViewChild('activateConfirm') private activateConfirmRef!: ConfirmDialogComponent;
@ViewChild('deleteProfileConfirm') private deleteProfileConfirmRef!: ConfirmDialogComponent;
ngOnInit(): void {
this.loadProfiles();
}
@@ -417,13 +442,19 @@ export class RiskProfileListComponent implements OnInit {
});
}
protected setStatusFilter(status: RiskProfileGovernanceStatus | null): void {
this.statusFilter.set(status);
protected setStatusFilter(status: RiskProfileGovernanceStatus | string | null): void {
this.statusFilter.set((status || null) as RiskProfileGovernanceStatus | null);
this.loadProfiles();
}
protected activateProfile(profile: RiskProfileGov): void {
if (!confirm(`Activate profile "${profile.name}"?`)) return;
protected requestActivateProfile(profile: RiskProfileGov): void {
this.pendingActivateProfile.set(profile);
this.activateConfirmRef.open();
}
protected executeActivateProfile(): void {
const profile = this.pendingActivateProfile();
if (!profile) return;
this.api.activateRiskProfile(profile.id, this.governanceScope()).subscribe({
next: () => this.loadProfiles(),
@@ -431,18 +462,32 @@ export class RiskProfileListComponent implements OnInit {
});
}
protected deprecateProfile(profile: RiskProfileGov): void {
const reason = prompt(`Reason for deprecating "${profile.name}":`);
if (!reason) return;
protected requestDeprecateProfile(profile: RiskProfileGov): void {
this.pendingDeprecateProfile.set(profile);
this.deprecateReasonText.set('');
this.showDeprecateModal.set(true);
}
protected executeDeprecateProfile(): void {
const profile = this.pendingDeprecateProfile();
const reason = this.deprecateReasonText();
if (!profile || !reason) return;
this.showDeprecateModal.set(false);
this.api.deprecateRiskProfile(profile.id, reason, this.governanceScope()).subscribe({
next: () => this.loadProfiles(),
error: (err) => console.error('Failed to deprecate profile:', err),
});
}
protected deleteProfile(profile: RiskProfileGov): void {
if (!confirm(`Delete profile "${profile.name}"? This cannot be undone.`)) return;
protected requestDeleteProfile(profile: RiskProfileGov): void {
this.pendingDeleteProfile.set(profile);
this.deleteProfileConfirmRef.open();
}
protected executeDeleteProfile(): void {
const profile = this.pendingDeleteProfile();
if (!profile) return;
this.api.deleteRiskProfile(profile.id, this.governanceScope()).subscribe({
next: () => this.loadProfiles(),

View File

@@ -313,7 +313,7 @@ interface SchemaSection {
margin: 0;
font-size: 1.25rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.docs__subtitle {
@@ -331,11 +331,11 @@ interface SchemaSection {
.search-input {
padding: 0.5rem 0.75rem;
padding-left: 2.25rem;
background: var(--color-text-heading) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='%236B5A2E'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z'/%3E%3C/svg%3E") no-repeat 0.75rem center;
background: var(--color-surface-elevated) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='%236B5A2E'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z'/%3E%3C/svg%3E") no-repeat 0.75rem center;
background-size: 16px;
border: 1px solid var(--color-text-primary);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-md);
color: var(--color-border-primary);
color: var(--color-text-primary);
font-size: 0.85rem;
width: 240px;
}
@@ -362,8 +362,8 @@ interface SchemaSection {
.btn--primary { background: var(--color-status-info); color: var(--color-btn-primary-text); }
.btn--primary:hover { background: var(--color-status-info); }
.btn--secondary { background: var(--color-text-primary); color: var(--color-border-primary); border: 1px solid var(--color-text-primary); }
.btn--secondary:hover { background: var(--color-text-primary); }
.btn--secondary { background: var(--color-surface-tertiary); color: var(--color-text-primary); border: 1px solid var(--color-border-primary); }
.btn--secondary:hover { background: var(--color-surface-tertiary); }
/* Content Layout */
.docs__content {
@@ -416,7 +416,7 @@ interface SchemaSection {
}
.nav-link:hover {
color: var(--color-border-primary);
color: var(--color-text-primary);
}
.nav-link--active {
@@ -426,10 +426,10 @@ interface SchemaSection {
.version-select {
width: 100%;
padding: 0.35rem 0.5rem;
background: var(--color-text-heading);
border: 1px solid var(--color-text-primary);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-sm);
color: var(--color-border-primary);
color: var(--color-text-primary);
font-size: 0.8rem;
}
@@ -439,8 +439,8 @@ interface SchemaSection {
}
.doc-section {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
padding: 1.5rem;
margin-bottom: 1.5rem;
@@ -449,7 +449,7 @@ interface SchemaSection {
.section-title {
margin: 0 0 0.35rem;
font-size: 1.1rem;
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.section-desc {
@@ -473,7 +473,7 @@ interface SchemaSection {
.fields-table td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid var(--color-text-heading);
border-bottom: 1px solid var(--color-border-primary);
}
.fields-table th {
@@ -482,7 +482,7 @@ interface SchemaSection {
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-secondary);
background: var(--color-text-heading);
background: var(--color-surface-elevated);
}
.field-row {
@@ -491,7 +491,7 @@ interface SchemaSection {
}
.field-row:hover {
background: var(--color-text-heading);
background: var(--color-surface-elevated);
}
.field-name {
@@ -507,7 +507,7 @@ interface SchemaSection {
.field-desc {
font-size: 0.85rem;
color: var(--color-border-primary);
color: var(--color-text-primary);
}
.badge {
@@ -519,7 +519,7 @@ interface SchemaSection {
}
.badge--required { background: var(--color-status-error-text); color: #fff; }
.badge--optional { background: var(--color-text-primary); color: var(--color-text-muted); }
.badge--optional { background: var(--color-surface-tertiary); color: var(--color-text-muted); }
/* Field Details (expanded) */
.field-details td {
@@ -549,8 +549,8 @@ interface SchemaSection {
.detail-value {
font-family: monospace;
font-size: 0.85rem;
color: var(--color-border-primary);
background: var(--color-text-primary);
color: var(--color-text-primary);
background: var(--color-surface-tertiary);
padding: 0.15rem 0.4rem;
border-radius: var(--radius-sm);
}
@@ -571,7 +571,7 @@ interface SchemaSection {
}
.detail-example {
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-sm);
padding: 0.75rem;
overflow-x: auto;
@@ -583,7 +583,7 @@ interface SchemaSection {
}
.children-list {
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-sm);
padding: 0.75rem;
}
@@ -591,7 +591,7 @@ interface SchemaSection {
.child-item {
padding: 0.35rem 0;
font-size: 0.85rem;
border-bottom: 1px solid var(--color-text-heading);
border-bottom: 1px solid var(--color-border-primary);
}
.child-item:last-child {
@@ -618,7 +618,7 @@ interface SchemaSection {
.examples-panel h4 {
margin: 0 0 0.75rem;
font-size: 0.9rem;
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.examples-tabs {
@@ -629,7 +629,7 @@ interface SchemaSection {
.example-tab {
padding: 0.35rem 0.75rem;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border: none;
border-radius: var(--radius-sm);
color: var(--color-text-muted);
@@ -639,8 +639,8 @@ interface SchemaSection {
}
.example-tab:hover {
background: var(--color-text-primary);
color: var(--color-border-primary);
background: var(--color-surface-tertiary);
color: var(--color-text-primary);
}
.example-tab--active {
@@ -673,7 +673,7 @@ interface SchemaSection {
top: 0.5rem;
right: 0.5rem;
padding: 0.35rem 0.5rem;
background: var(--color-text-primary);
background: var(--color-surface-tertiary);
border: none;
border-radius: var(--radius-sm);
color: var(--color-text-muted);
@@ -686,8 +686,8 @@ interface SchemaSection {
}
.copy-btn:hover {
background: var(--color-text-primary);
color: var(--color-border-primary);
background: var(--color-surface-tertiary);
color: var(--color-text-primary);
}
/* Validation Rules */
@@ -698,10 +698,10 @@ interface SchemaSection {
}
.rule-card {
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-md);
padding: 1rem;
border-left: 3px solid var(--color-text-primary);
border-left: 3px solid var(--color-border-primary);
}
.rule-header {
@@ -732,14 +732,14 @@ interface SchemaSection {
.rule-desc {
margin: 0 0 0.5rem;
font-size: 0.9rem;
color: var(--color-border-primary);
color: var(--color-text-primary);
}
.rule-fix {
font-size: 0.85rem;
color: var(--color-text-muted);
padding-top: 0.5rem;
border-top: 1px solid var(--color-text-heading);
border-top: 1px solid var(--color-border-primary);
}
/* Best Practices */
@@ -752,7 +752,7 @@ interface SchemaSection {
.practice-card {
display: flex;
gap: 1rem;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-lg);
padding: 1rem;
}
@@ -763,15 +763,15 @@ interface SchemaSection {
display: flex;
align-items: center;
justify-content: center;
background: var(--color-text-primary);
background: var(--color-surface-tertiary);
border-radius: var(--radius-lg);
flex-shrink: 0;
color: var(--color-text-muted);
}
.icon--security { color: var(--color-status-success); background: rgba(34, 197, 94, 0.1); }
.icon--performance { color: var(--color-status-warning); background: rgba(234, 179, 8, 0.1); }
.icon--maintainability { color: var(--color-status-info); background: rgba(34, 211, 238, 0.1); }
.icon--security { color: var(--color-status-success); background: var(--color-status-success-bg); }
.icon--performance { color: var(--color-status-warning); background: var(--color-status-warning-bg); }
.icon--maintainability { color: var(--color-status-info); background: var(--color-status-info-bg); }
.practice-content {
flex: 1;
@@ -781,7 +781,7 @@ interface SchemaSection {
.practice-title {
margin: 0 0 0.35rem;
font-size: 0.95rem;
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.practice-desc {
@@ -803,7 +803,7 @@ interface SchemaSection {
.practice-example pre {
margin: 0.5rem 0 0;
padding: 0.75rem;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-sm);
overflow-x: auto;
}
@@ -821,8 +821,8 @@ interface SchemaSection {
justify-content: center;
padding: 4rem 2rem;
text-align: center;
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
}
@@ -833,7 +833,7 @@ interface SchemaSection {
.no-results h3 {
margin: 0 0 0.5rem;
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.no-results p {

View File

@@ -278,7 +278,7 @@ import {
margin: 0;
font-size: 1.25rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.playground__subtitle {
@@ -295,10 +295,10 @@ import {
.form-select {
padding: 0.5rem 0.75rem;
background: var(--color-text-heading);
border: 1px solid var(--color-text-primary);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-md);
color: var(--color-border-primary);
color: var(--color-text-primary);
font-size: 0.85rem;
min-width: 180px;
}
@@ -325,8 +325,8 @@ import {
.btn--primary:hover:not(:disabled) { background: var(--color-status-info); }
.btn--primary:disabled { opacity: 0.5; cursor: not-allowed; }
.btn--secondary { background: var(--color-text-primary); color: var(--color-border-primary); border: 1px solid var(--color-text-primary); }
.btn--secondary:hover { background: var(--color-text-primary); }
.btn--secondary { background: var(--color-surface-tertiary); color: var(--color-text-primary); border: 1px solid var(--color-border-primary); }
.btn--secondary:hover { background: var(--color-surface-tertiary); }
/* Content Layout */
.playground__content {
@@ -344,8 +344,8 @@ import {
/* Editor Panel */
.editor-panel, .results-panel {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
overflow: hidden;
display: flex;
@@ -358,14 +358,14 @@ import {
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: var(--color-text-heading);
border-bottom: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border-bottom: 1px solid var(--color-border-primary);
}
.panel-header h3 {
margin: 0;
font-size: 0.9rem;
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.panel-meta {
@@ -416,20 +416,20 @@ import {
.line-number--error {
color: var(--color-status-error);
background: rgba(239, 68, 68, 0.1);
background: var(--color-status-error-bg);
}
.line-number--warning {
color: var(--color-status-warning);
background: rgba(234, 179, 8, 0.1);
background: var(--color-status-warning-bg);
}
.editor {
flex: 1;
padding: 1rem;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border: none;
color: var(--color-border-primary);
color: var(--color-text-primary);
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 0.85rem;
line-height: 1.5rem;
@@ -444,7 +444,7 @@ import {
align-items: center;
padding: 0.5rem 1rem;
background: var(--color-surface-inverse);
border-top: 1px solid var(--color-text-heading);
border-top: 1px solid var(--color-border-primary);
overflow-x: auto;
}
@@ -456,8 +456,8 @@ import {
.snippet-btn {
padding: 0.25rem 0.5rem;
background: var(--color-text-primary);
border: 1px solid var(--color-text-primary);
background: var(--color-surface-tertiary);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-sm);
color: var(--color-text-muted);
font-size: 0.7rem;
@@ -467,8 +467,8 @@ import {
}
.snippet-btn:hover {
background: var(--color-text-primary);
color: var(--color-border-primary);
background: var(--color-surface-tertiary);
color: var(--color-text-primary);
}
/* Results Panel */
@@ -487,17 +487,17 @@ import {
.summary-stat {
flex: 1;
padding: 0.75rem;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-md);
text-align: center;
}
.summary-stat--error {
border: 1px solid rgba(239, 68, 68, 0.3);
border: 1px solid var(--color-status-error-border);
}
.summary-stat--warning {
border: 1px solid rgba(234, 179, 8, 0.3);
border: 1px solid var(--color-status-warning-border);
}
.stat-value {
@@ -543,7 +543,7 @@ import {
.issue-item {
padding: 0.75rem;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-md);
margin-bottom: 0.5rem;
border-left: 3px solid;
@@ -552,7 +552,7 @@ import {
}
.issue-item:hover {
background: var(--color-text-primary);
background: var(--color-surface-tertiary);
}
.issue-item--error {
@@ -572,7 +572,7 @@ import {
.issue-message {
font-size: 0.85rem;
color: var(--color-border-primary);
color: var(--color-text-primary);
}
.issue-path {
@@ -604,17 +604,17 @@ import {
.parsed-preview {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--color-text-heading);
border-top: 1px solid var(--color-border-primary);
}
.parsed-preview h4 {
margin: 0 0 0.75rem;
font-size: 0.9rem;
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.preview-tree {
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-md);
padding: 0.75rem;
}
@@ -651,8 +651,8 @@ import {
/* Schema Reference */
.schema-reference {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
padding: 1.25rem;
}
@@ -660,7 +660,7 @@ import {
.schema-reference h3 {
margin: 0 0 1rem;
font-size: 1rem;
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.reference-grid {
@@ -670,7 +670,7 @@ import {
}
.reference-card {
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-md);
padding: 1rem;
}
@@ -695,15 +695,15 @@ import {
.reference-card code {
font-family: monospace;
font-size: 0.8rem;
color: var(--color-border-primary);
background: var(--color-text-primary);
color: var(--color-text-primary);
background: var(--color-surface-tertiary);
padding: 0.1rem 0.35rem;
border-radius: var(--radius-sm);
}
.reference-card pre {
margin: 0;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
padding: 0.75rem;
border-radius: var(--radius-sm);
overflow-x: auto;

View File

@@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
import { Component, ChangeDetectionStrategy, inject, signal, OnInit } from '@angular/core';
import { Component, ChangeDetectionStrategy, inject, signal, OnInit, ViewChild } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, FormsModule, Validators, FormArray } from '@angular/forms';
import { finalize } from 'rxjs/operators';
@@ -12,7 +12,10 @@ import {
SealedModeToggleRequest,
SealedModeOverrideRequest,
} from '../../core/api/policy-governance.models';
import { LoadingStateComponent } from '../../shared/components/loading-state/loading-state.component';
import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component';
import { ModalComponent } from '../../shared/components/modal/modal.component';
/**
* Sealed Mode Control component.
@@ -22,7 +25,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
*/
@Component({
selector: 'app-sealed-mode-control',
imports: [CommonModule, ReactiveFormsModule, FormsModule],
imports: [CommonModule, ReactiveFormsModule, FormsModule, ConfirmDialogComponent, ModalComponent, LoadingStateComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="sealed" [attr.aria-busy]="loading()">
@@ -156,7 +159,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
</div>
@if (override.active && !isExpired(override)) {
<div class="override-card__actions">
<button class="btn btn--ghost btn--small btn--danger" (click)="revokeOverride(override)">
<button class="btn btn--ghost btn--small btn--danger" (click)="requestRevokeOverride(override)">
Revoke
</button>
</div>
@@ -174,180 +177,145 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
</section>
} @else if (loading()) {
<div class="loading-state">Loading sealed mode status...</div>
<app-loading-state message="Loading sealed mode status..." />
}
<!-- Seal Confirmation Modal -->
@if (showSealConfirm()) {
<div class="modal-backdrop" (click)="closeSealConfirm()">
<div class="modal" (click)="$event.stopPropagation()">
<header class="modal__header">
<h3>Enable Sealed Mode</h3>
<button class="btn btn--icon btn--ghost" (click)="closeSealConfirm()">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="20" height="20">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</header>
<form [formGroup]="sealForm" class="modal__body" (ngSubmit)="confirmSeal()">
<div class="warning-banner">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="24" height="24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
</svg>
<div>
<strong>Warning: Air-Gap Mode</strong>
<p>Enabling sealed mode will restrict all external network access and require trust roots for verification.</p>
</div>
</div>
<div class="form-field">
<label class="form-label">Reason for Sealing *</label>
<textarea formControlName="reason" class="form-textarea" rows="2" placeholder="e.g., Air-gap deployment for production"></textarea>
</div>
<div class="form-field">
<label class="form-label">Trust Roots (one per line)</label>
<textarea formControlName="trustRoots" class="form-textarea" rows="3" placeholder="Enter trust root identifiers, one per line"></textarea>
<span class="form-hint">Certificate fingerprints or key identifiers to trust</span>
</div>
<div class="form-field">
<label class="form-label">Allowed Sources (one per line)</label>
<textarea formControlName="allowedSources" class="form-textarea" rows="3" placeholder="Enter allowed source URLs, one per line"></textarea>
<span class="form-hint">URLs that are permitted for data fetching in sealed mode</span>
</div>
<div class="form-field form-field--checkbox">
<label class="form-checkbox">
<input type="checkbox" formControlName="confirm" />
<span>I understand that this will enable air-gapped operation mode</span>
</label>
</div>
</form>
<footer class="modal__footer">
<button type="button" class="btn btn--ghost" (click)="closeSealConfirm()">Cancel</button>
<button
type="button"
class="btn btn--primary"
(click)="confirmSeal()"
[disabled]="!sealForm.valid || toggling()"
>
{{ toggling() ? 'Sealing...' : 'Enable Sealed Mode' }}
</button>
</footer>
<app-modal [open]="showSealConfirm()" title="Enable Sealed Mode" size="md" (closed)="closeSealConfirm()">
<form [formGroup]="sealForm" (ngSubmit)="confirmSeal()">
<div class="warning-banner">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="24" height="24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
</svg>
<div>
<strong>Warning: Air-Gap Mode</strong>
<p>Enabling sealed mode will restrict all external network access and require trust roots for verification.</p>
</div>
</div>
<div class="form-field">
<label class="form-label">Reason for Sealing *</label>
<textarea formControlName="reason" class="form-textarea" rows="2" placeholder="e.g., Air-gap deployment for production"></textarea>
</div>
<div class="form-field">
<label class="form-label">Trust Roots (one per line)</label>
<textarea formControlName="trustRoots" class="form-textarea" rows="3" placeholder="Enter trust root identifiers, one per line"></textarea>
<span class="form-hint">Certificate fingerprints or key identifiers to trust</span>
</div>
<div class="form-field">
<label class="form-label">Allowed Sources (one per line)</label>
<textarea formControlName="allowedSources" class="form-textarea" rows="3" placeholder="Enter allowed source URLs, one per line"></textarea>
<span class="form-hint">URLs that are permitted for data fetching in sealed mode</span>
</div>
<div class="form-field form-field--checkbox">
<label class="form-checkbox">
<input type="checkbox" formControlName="confirm" />
<span>I understand that this will enable air-gapped operation mode</span>
</label>
</div>
</form>
<div modal-footer>
<button type="button" class="btn btn--ghost" (click)="closeSealConfirm()">Cancel</button>
<button
type="button"
class="btn btn--primary"
(click)="confirmSeal()"
[disabled]="!sealForm.valid || toggling()"
>
{{ toggling() ? 'Sealing...' : 'Enable Sealed Mode' }}
</button>
</div>
}
</app-modal>
<!-- Unseal Confirmation Modal -->
@if (showUnsealConfirm()) {
<div class="modal-backdrop" (click)="closeUnsealConfirm()">
<div class="modal" (click)="$event.stopPropagation()">
<header class="modal__header">
<h3>Disable Sealed Mode</h3>
<button class="btn btn--icon btn--ghost" (click)="closeUnsealConfirm()">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="20" height="20">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</header>
<div class="modal__body">
<div class="warning-banner warning-banner--info">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="24" height="24">
<path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
</svg>
<div>
<strong>Confirm Unseal</strong>
<p>This will disable air-gap mode and allow external network connections. All active overrides will be cleared.</p>
</div>
</div>
<div class="form-field">
<label class="form-label">Reason for Unsealing *</label>
<textarea [(ngModel)]="unsealReason" class="form-textarea" rows="2" placeholder="e.g., Returning to normal operation"></textarea>
</div>
</div>
<footer class="modal__footer">
<button type="button" class="btn btn--ghost" (click)="closeUnsealConfirm()">Cancel</button>
<button
type="button"
class="btn btn--danger"
(click)="confirmUnseal()"
[disabled]="!unsealReason || toggling()"
>
{{ toggling() ? 'Unsealing...' : 'Disable Sealed Mode' }}
</button>
</footer>
<app-modal [open]="showUnsealConfirm()" title="Disable Sealed Mode" size="md" (closed)="closeUnsealConfirm()">
<div class="warning-banner warning-banner--info">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="24" height="24">
<path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
</svg>
<div>
<strong>Confirm Unseal</strong>
<p>This will disable air-gap mode and allow external network connections. All active overrides will be cleared.</p>
</div>
</div>
}
<div class="form-field">
<label class="form-label">Reason for Unsealing *</label>
<textarea [(ngModel)]="unsealReason" class="form-textarea" rows="2" placeholder="e.g., Returning to normal operation"></textarea>
</div>
<div modal-footer>
<button type="button" class="btn btn--ghost" (click)="closeUnsealConfirm()">Cancel</button>
<button
type="button"
class="btn btn--danger"
(click)="confirmUnseal()"
[disabled]="!unsealReason || toggling()"
>
{{ toggling() ? 'Unsealing...' : 'Disable Sealed Mode' }}
</button>
</div>
</app-modal>
<!-- Override Modal -->
@if (showOverrideModal()) {
<div class="modal-backdrop" (click)="closeOverrideModal()">
<div class="modal" (click)="$event.stopPropagation()">
<header class="modal__header">
<h3>Create Override</h3>
<button class="btn btn--icon btn--ghost" (click)="closeOverrideModal()">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="20" height="20">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</header>
<form [formGroup]="overrideForm" class="modal__body" (ngSubmit)="createOverride()">
<div class="form-field">
<label class="form-label">Override Type *</label>
<select formControlName="type" class="form-select">
<option value="source">Source URL</option>
<option value="operation">Operation</option>
<option value="component">Component</option>
</select>
</div>
<div class="form-field">
<label class="form-label">Target *</label>
<input type="text" formControlName="target" class="form-input" placeholder="e.g., https://nvd.nist.gov or pkg:npm/lodash" />
<span class="form-hint">The source URL, operation name, or component PURL to allow</span>
</div>
<div class="form-field">
<label class="form-label">Reason *</label>
<textarea formControlName="reason" class="form-textarea" rows="2" placeholder="Justification for this override"></textarea>
</div>
<div class="form-field">
<label class="form-label">Duration (hours) *</label>
<input type="number" formControlName="durationHours" class="form-input" min="1" max="168" />
<span class="form-hint">Maximum 168 hours (7 days)</span>
</div>
<div class="info-banner">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="20" height="20">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z" />
</svg>
<p>This override requires two-person approval and will be logged in the audit trail.</p>
</div>
</form>
<footer class="modal__footer">
<button type="button" class="btn btn--ghost" (click)="closeOverrideModal()">Cancel</button>
<button
type="button"
class="btn btn--primary"
(click)="createOverride()"
[disabled]="!overrideForm.valid || creatingOverride()"
>
{{ creatingOverride() ? 'Creating...' : 'Create Override' }}
</button>
</footer>
<app-modal [open]="showOverrideModal()" title="Create Override" size="md" (closed)="closeOverrideModal()">
<form [formGroup]="overrideForm" (ngSubmit)="createOverride()">
<div class="form-field">
<label class="form-label">Override Type *</label>
<select formControlName="type" class="form-select">
<option value="source">Source URL</option>
<option value="operation">Operation</option>
<option value="component">Component</option>
</select>
</div>
<div class="form-field">
<label class="form-label">Target *</label>
<input type="text" formControlName="target" class="form-input" placeholder="e.g., https://nvd.nist.gov or pkg:npm/lodash" />
<span class="form-hint">The source URL, operation name, or component PURL to allow</span>
</div>
<div class="form-field">
<label class="form-label">Reason *</label>
<textarea formControlName="reason" class="form-textarea" rows="2" placeholder="Justification for this override"></textarea>
</div>
<div class="form-field">
<label class="form-label">Duration (hours) *</label>
<input type="number" formControlName="durationHours" class="form-input" min="1" max="168" />
<span class="form-hint">Maximum 168 hours (7 days)</span>
</div>
<div class="info-banner">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="20" height="20">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z" />
</svg>
<p>This override requires two-person approval and will be logged in the audit trail.</p>
</div>
</form>
<div modal-footer>
<button type="button" class="btn btn--ghost" (click)="closeOverrideModal()">Cancel</button>
<button
type="button"
class="btn btn--primary"
(click)="createOverride()"
[disabled]="!overrideForm.valid || creatingOverride()"
>
{{ creatingOverride() ? 'Creating...' : 'Create Override' }}
</button>
</div>
}
</app-modal>
<app-confirm-dialog #revokeOverrideConfirm
title="Revoke Override"
[message]="'Revoke override for \\'' + pendingRevokeOverride()?.target + '\\'? This cannot be undone.'"
confirmLabel="Revoke" cancelLabel="Cancel" variant="danger"
(confirmed)="executeRevokeOverride()" />
</div>
`,
styles: [`
@@ -365,7 +333,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
margin: 0;
font-size: 1.25rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.sealed__subtitle {
@@ -380,7 +348,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
align-items: center;
gap: 1.5rem;
padding: 1.5rem;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border: 2px solid var(--color-status-success);
border-radius: var(--radius-xl);
margin-bottom: 1.5rem;
@@ -388,7 +356,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.status-card--sealed {
border-color: var(--color-status-warning);
background: linear-gradient(135deg, var(--color-text-heading) 0%, rgba(234, 179, 8, 0.1) 100%);
background: linear-gradient(135deg, var(--color-surface-elevated) 0%, var(--color-status-warning-bg) 100%);
}
.status-card__icon {
@@ -463,11 +431,11 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.btn--primary:hover:not(:disabled) { background: var(--color-status-info); }
.btn--primary:disabled { opacity: 0.5; cursor: not-allowed; }
.btn--secondary { background: var(--color-text-primary); color: var(--color-border-primary); border: 1px solid var(--color-text-primary); }
.btn--secondary:hover { background: var(--color-text-primary); }
.btn--secondary { background: var(--color-surface-tertiary); color: var(--color-text-primary); border: 1px solid var(--color-border-primary); }
.btn--secondary:hover { background: var(--color-surface-tertiary); }
.btn--ghost { background: transparent; color: var(--color-text-muted); }
.btn--ghost:hover { background: var(--color-text-primary); color: var(--color-border-primary); }
.btn--ghost:hover { background: var(--color-surface-tertiary); color: var(--color-text-primary); }
.btn--danger { background: var(--color-status-error); color: var(--color-surface-primary); }
.btn--danger:hover:not(:disabled) { background: var(--color-status-error-text); }
@@ -477,8 +445,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
/* Sections */
.section {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
padding: 1.25rem;
margin-bottom: 1rem;
@@ -495,7 +463,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
margin: 0;
font-size: 1rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
/* Trust Roots & Sources */
@@ -510,10 +478,10 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: var(--color-text-heading);
border: 1px solid var(--color-text-primary);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-md);
color: var(--color-border-primary);
color: var(--color-text-primary);
font-size: 0.85rem;
font-family: monospace;
}
@@ -529,8 +497,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.override-card {
padding: 1rem;
background: var(--color-text-heading);
border: 1px solid var(--color-text-primary);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
}
@@ -548,7 +516,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.override-card__type {
font-size: 0.7rem;
padding: 0.15rem 0.5rem;
background: var(--color-text-primary);
background: var(--color-surface-tertiary);
border-radius: var(--radius-sm);
color: var(--color-text-muted);
text-transform: uppercase;
@@ -572,7 +540,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.override-card__reason {
font-size: 0.85rem;
color: var(--color-border-primary);
color: var(--color-text-primary);
margin-bottom: 0.5rem;
}
@@ -586,7 +554,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.override-card__actions {
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid var(--color-text-primary);
border-top: 1px solid var(--color-border-primary);
}
.empty-state {
@@ -600,53 +568,6 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
color: var(--color-text-secondary);
}
/* Modal */
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
border-radius: var(--radius-xl);
width: 90%;
max-width: 520px;
max-height: 90vh;
overflow-y: auto;
}
.modal__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--color-text-heading);
}
.modal__header h3 {
margin: 0;
font-size: 1.1rem;
color: var(--color-surface-secondary);
}
.modal__body {
padding: 1.25rem;
}
.modal__footer {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
padding: 1rem 1.25rem;
border-top: 1px solid var(--color-text-heading);
}
/* Form */
.form-field {
margin-bottom: 1rem;
@@ -659,7 +580,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.form-label {
display: block;
color: var(--color-border-primary);
color: var(--color-text-primary);
font-size: 0.85rem;
font-weight: var(--font-weight-medium);
margin-bottom: 0.35rem;
@@ -668,10 +589,10 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.form-input, .form-select, .form-textarea {
width: 100%;
padding: 0.5rem 0.75rem;
background: var(--color-text-heading);
border: 1px solid var(--color-text-primary);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-md);
color: var(--color-border-primary);
color: var(--color-text-primary);
font-size: 0.9rem;
}
@@ -691,7 +612,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
align-items: center;
gap: 0.5rem;
cursor: pointer;
color: var(--color-border-primary);
color: var(--color-text-primary);
font-size: 0.9rem;
}
@@ -706,8 +627,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
display: flex;
gap: 0.75rem;
padding: 1rem;
background: rgba(234, 179, 8, 0.1);
border: 1px solid rgba(234, 179, 8, 0.3);
background: var(--color-status-warning-bg);
border: 1px solid var(--color-status-warning-border);
border-radius: var(--radius-lg);
margin-bottom: 1rem;
}
@@ -728,8 +649,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.warning-banner--info {
background: rgba(34, 211, 238, 0.1);
border-color: rgba(34, 211, 238, 0.3);
background: var(--color-status-info-bg);
border-color: var(--color-status-info-border);
}
.warning-banner--info svg { color: var(--color-status-info); }
@@ -737,20 +658,13 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.warning-banner--info p { color: var(--color-status-info-border); }
.info-banner {
background: rgba(34, 211, 238, 0.05);
border-color: rgba(34, 211, 238, 0.2);
background: var(--color-status-info-bg);
border-color: var(--color-status-info-border);
}
.info-banner svg { color: var(--color-status-info); flex-shrink: 0; }
.info-banner p { margin: 0; color: var(--color-text-muted); font-size: 0.85rem; }
.loading-state {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: var(--color-text-muted);
}
`]
})
export class SealedModeControlComponent implements OnInit {
@@ -767,6 +681,9 @@ export class SealedModeControlComponent implements OnInit {
protected readonly showUnsealConfirm = signal(false);
protected readonly showOverrideModal = signal(false);
protected readonly pendingRevokeOverride = signal<SealedModeOverride | null>(null);
@ViewChild('revokeOverrideConfirm') private revokeOverrideConfirmRef!: ConfirmDialogComponent;
protected unsealReason = '';
protected readonly sealForm: FormGroup = this.fb.group({
@@ -901,8 +818,14 @@ export class SealedModeControlComponent implements OnInit {
});
}
protected revokeOverride(override: SealedModeOverride): void {
if (!confirm('Revoke this override?')) return;
protected requestRevokeOverride(override: SealedModeOverride): void {
this.pendingRevokeOverride.set(override);
this.revokeOverrideConfirmRef.open();
}
protected executeRevokeOverride(): void {
const override = this.pendingRevokeOverride();
if (!override) return;
this.api.revokeSealedModeOverride(override.id, 'user_revoked', this.governanceScope()).subscribe({
next: () => this.loadStatus(),

View File

@@ -1,5 +1,6 @@
import { CommonModule } from '@angular/common';
import { Component, ChangeDetectionStrategy, inject, signal, OnInit } from '@angular/core';
import { ModalComponent } from '../../shared/components/modal/modal.component';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { finalize } from 'rxjs/operators';
@@ -21,7 +22,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
*/
@Component({
selector: 'app-sealed-mode-overrides',
imports: [CommonModule, FormsModule, RouterModule],
imports: [CommonModule, FormsModule, RouterModule, ModalComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="overrides" [attr.aria-busy]="loading()">
@@ -232,6 +233,50 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
</div>
</div>
}
<!-- Extend Override Modal -->
@if (showExtendModal()) {
<app-modal [open]="true" title="Extend Override" size="sm" (closed)="showExtendModal.set(false)">
<div class="form-field">
<label class="form-label" for="extendHours">Extend by how many hours? *</label>
<input
type="number"
id="extendHours"
class="form-input"
min="1"
max="168"
[ngModel]="extendHours()"
(ngModelChange)="extendHours.set($event)"
/>
<span class="form-hint">Between 1 and 168 hours (7 days)</span>
</div>
<ng-container modal-footer>
<button class="btn btn--ghost" (click)="showExtendModal.set(false)">Cancel</button>
<button class="btn btn--primary" (click)="submitExtend()" [disabled]="extendHours() < 1 || extendHours() > 168">Extend</button>
</ng-container>
</app-modal>
}
<!-- Revoke Override Modal -->
@if (showRevokeModal()) {
<app-modal [open]="true" title="Revoke Override" size="md" (closed)="showRevokeModal.set(false)">
<div class="form-field">
<label class="form-label" for="revokeReason">Reason for revoking this override *</label>
<textarea
id="revokeReason"
class="form-textarea"
rows="4"
placeholder="Enter reason for revoking..."
[ngModel]="revokeReasonText()"
(ngModelChange)="revokeReasonText.set($event)"
></textarea>
</div>
<ng-container modal-footer>
<button class="btn btn--ghost" (click)="showRevokeModal.set(false)">Cancel</button>
<button class="btn btn--primary btn--danger" (click)="submitRevoke()" [disabled]="!revokeReasonText()">Revoke</button>
</ng-container>
</app-modal>
}
</div>
`,
styles: [`
@@ -278,7 +323,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
margin: 0;
font-size: 1.25rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.overrides__subtitle {
@@ -306,7 +351,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.btn--primary:disabled { opacity: 0.5; cursor: not-allowed; }
.btn--ghost { background: transparent; color: var(--color-text-muted); }
.btn--ghost:hover { background: var(--color-text-primary); color: var(--color-border-primary); }
.btn--ghost:hover { background: var(--color-surface-tertiary); color: var(--color-text-primary); }
.btn--small { padding: 0.35rem 0.75rem; font-size: 0.8rem; }
.btn--danger:hover { color: var(--color-status-error); }
@@ -316,7 +361,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
display: flex;
gap: 0.25rem;
margin-bottom: 1.5rem;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-lg);
padding: 0.25rem;
}
@@ -333,8 +378,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.filter-tab:hover {
background: var(--color-text-primary);
color: var(--color-border-primary);
background: var(--color-surface-tertiary);
color: var(--color-text-primary);
}
.filter-tab--active {
@@ -350,8 +395,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.override-card {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
overflow: hidden;
border-left: 3px solid var(--color-status-info);
@@ -371,7 +416,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
}
.override-card__badges {
@@ -387,9 +432,9 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
font-weight: var(--font-weight-semibold);
}
.badge--type { background: var(--color-text-primary); color: var(--color-text-muted); }
.badge--type { background: var(--color-surface-tertiary); color: var(--color-text-muted); }
.badge--active { background: var(--color-status-success-text); color: #fff; }
.badge--expired { background: var(--color-text-primary); color: var(--color-text-muted); }
.badge--expired { background: var(--color-surface-tertiary); color: var(--color-text-muted); }
.override-card__expires {
font-size: 0.8rem;
@@ -414,7 +459,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
font-family: monospace;
font-size: 0.9rem;
color: var(--color-status-info);
background: var(--color-text-heading);
background: var(--color-surface-elevated);
padding: 0.2rem 0.5rem;
border-radius: var(--radius-sm);
}
@@ -433,7 +478,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.reason-text {
margin: 0;
font-size: 0.9rem;
color: var(--color-border-primary);
color: var(--color-text-primary);
line-height: 1.4;
}
@@ -471,8 +516,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
display: flex;
gap: 0.5rem;
padding: 0.75rem 1rem;
border-top: 1px solid var(--color-text-heading);
background: var(--color-text-heading);
border-top: 1px solid var(--color-border-primary);
background: var(--color-surface-elevated);
}
/* Modal */
@@ -487,8 +532,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.modal {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-xl);
width: 90%;
max-width: 500px;
@@ -501,13 +546,13 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
justify-content: space-between;
align-items: center;
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--color-text-heading);
border-bottom: 1px solid var(--color-border-primary);
}
.modal__header h3 {
margin: 0;
font-size: 1.1rem;
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.modal__close {
@@ -519,7 +564,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.modal__close:hover {
color: var(--color-border-primary);
color: var(--color-text-primary);
}
.modal__body {
@@ -531,7 +576,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
justify-content: flex-end;
gap: 0.5rem;
padding: 1rem 1.25rem;
border-top: 1px solid var(--color-text-heading);
border-top: 1px solid var(--color-border-primary);
}
/* Form Fields */
@@ -541,7 +586,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.form-label {
display: block;
color: var(--color-border-primary);
color: var(--color-text-primary);
font-size: 0.85rem;
font-weight: var(--font-weight-medium);
margin-bottom: 0.35rem;
@@ -550,10 +595,10 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.form-input, .form-select, .form-textarea {
width: 100%;
padding: 0.5rem 0.75rem;
background: var(--color-text-heading);
border: 1px solid var(--color-text-primary);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-md);
color: var(--color-border-primary);
color: var(--color-text-primary);
font-size: 0.9rem;
}
@@ -574,8 +619,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
align-items: center;
gap: 0.5rem;
padding: 0.75rem;
background: rgba(234, 179, 8, 0.1);
border: 1px solid rgba(234, 179, 8, 0.3);
background: var(--color-status-warning-bg);
border: 1px solid var(--color-status-warning-border);
border-radius: var(--radius-md);
font-size: 0.85rem;
color: var(--color-status-warning-border);
@@ -589,8 +634,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
justify-content: center;
padding: 3rem;
text-align: center;
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
}
@@ -601,7 +646,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.empty-state h3 {
margin: 0 0 0.5rem;
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.empty-state p {
@@ -623,6 +668,11 @@ export class SealedModeOverridesComponent implements OnInit {
protected readonly allOverrides = signal<SealedModeOverride[]>([]);
protected readonly statusFilter = signal<'all' | 'active' | 'expired' | 'pending'>('all');
protected readonly showCreateModal = signal(false);
protected readonly showExtendModal = signal(false);
protected readonly showRevokeModal = signal(false);
protected readonly extendHours = signal(24);
protected readonly revokeReasonText = signal('');
private readonly pendingOverride = signal<SealedModeOverride | null>(null);
protected readonly newOverride: SealedModeOverrideRequest = {
type: 'source',
@@ -713,12 +763,17 @@ export class SealedModeOverridesComponent implements OnInit {
}
protected extendOverride(override: SealedModeOverride): void {
const hours = prompt('Extend override by how many hours?', '24');
if (!hours) return;
this.pendingOverride.set(override);
this.extendHours.set(24);
this.showExtendModal.set(true);
}
const durationHours = parseInt(hours, 10);
if (isNaN(durationHours) || durationHours < 1 || durationHours > 168) return;
protected submitExtend(): void {
const override = this.pendingOverride();
const durationHours = this.extendHours();
if (!override || isNaN(durationHours) || durationHours < 1 || durationHours > 168) return;
this.showExtendModal.set(false);
this.creating.set(true);
this.api
.createSealedModeOverride(
@@ -738,9 +793,17 @@ export class SealedModeOverridesComponent implements OnInit {
}
protected revokeOverride(override: SealedModeOverride): void {
const reason = prompt('Reason for revoking this override:');
if (!reason) return;
this.pendingOverride.set(override);
this.revokeReasonText.set('');
this.showRevokeModal.set(true);
}
protected submitRevoke(): void {
const override = this.pendingOverride();
const reason = this.revokeReasonText();
if (!override || !reason) return;
this.showRevokeModal.set(false);
this.api.revokeSealedModeOverride(override.id, reason, this.governanceScope()).subscribe({
next: () => this.loadOverrides(),
error: (err) => console.error('Failed to revoke override:', err),

View File

@@ -13,6 +13,7 @@ import {
StalenessDataType,
StalenessLevel,
} from '../../core/api/policy-governance.models';
import { LoadingStateComponent } from '../../shared/components/loading-state/loading-state.component';
import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
/**
@@ -23,7 +24,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
*/
@Component({
selector: 'app-staleness-config',
imports: [CommonModule, ReactiveFormsModule],
imports: [CommonModule, ReactiveFormsModule, LoadingStateComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="staleness" [attr.aria-busy]="loading()">
@@ -205,7 +206,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
</section>
} @else if (loading()) {
<div class="loading-state">Loading staleness configuration...</div>
<app-loading-state message="Loading staleness configuration..." />
}
</div>
`,
@@ -224,7 +225,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
margin: 0;
font-size: 1.25rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.staleness__subtitle {
@@ -237,7 +238,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
margin: 0 0 1rem;
font-size: 1rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
/* Status Section */
@@ -252,8 +253,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.status-card {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
padding: 1rem;
border-left: 3px solid var(--color-status-success);
@@ -273,7 +274,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.status-card__type {
font-size: 0.7rem;
padding: 0.15rem 0.5rem;
background: var(--color-text-primary);
background: var(--color-surface-tertiary);
border-radius: var(--radius-sm);
color: var(--color-text-muted);
text-transform: uppercase;
@@ -293,7 +294,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.status-card__name {
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
margin-bottom: 0.35rem;
}
@@ -307,7 +308,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.status-card__blocked {
margin-top: 0.5rem;
padding: 0.35rem 0.5rem;
background: rgba(239, 68, 68, 0.2);
background: var(--color-status-error-bg);
border-radius: var(--radius-sm);
color: var(--color-status-error-border);
font-size: 0.8rem;
@@ -315,8 +316,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
/* Config Section */
.config-section {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
overflow: hidden;
}
@@ -324,12 +325,12 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.config-section .section-title {
padding: 1rem 1.25rem;
margin: 0;
border-bottom: 1px solid var(--color-text-heading);
border-bottom: 1px solid var(--color-border-primary);
}
.config-tabs {
display: flex;
border-bottom: 1px solid var(--color-text-heading);
border-bottom: 1px solid var(--color-border-primary);
overflow-x: auto;
}
@@ -344,7 +345,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
white-space: nowrap;
}
.config-tab:hover { color: var(--color-border-primary); }
.config-tab:hover { color: var(--color-text-primary); }
.config-tab--active {
color: var(--color-status-info);
@@ -361,7 +362,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--color-text-heading);
border-bottom: 1px solid var(--color-border-primary);
}
.config-panel__toggle {
@@ -386,7 +387,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
position: absolute;
cursor: pointer;
inset: 0;
background: var(--color-text-primary);
background: var(--color-surface-tertiary);
border-radius: var(--radius-3xl);
transition: 0.2s;
}
@@ -398,7 +399,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
width: 18px;
left: 3px;
bottom: 3px;
background: var(--color-border-primary);
background: var(--color-surface-elevated);
border-radius: var(--radius-full);
transition: 0.2s;
}
@@ -412,7 +413,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.toggle__label {
color: var(--color-border-primary);
color: var(--color-text-primary);
font-size: 0.9rem;
}
@@ -438,7 +439,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
grid-template-columns: 120px 100px 120px 1fr;
gap: 1rem;
padding: 0.5rem 0.75rem;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-md);
font-size: 0.75rem;
color: var(--color-text-muted);
@@ -451,7 +452,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
grid-template-columns: 120px 100px 120px 1fr;
gap: 1rem;
padding: 0.75rem;
border-bottom: 1px solid var(--color-text-heading);
border-bottom: 1px solid var(--color-border-primary);
align-items: center;
}
@@ -464,7 +465,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
align-items: center;
gap: 0.5rem;
font-weight: var(--font-weight-medium);
color: var(--color-border-primary);
color: var(--color-text-primary);
}
.level-indicator {
@@ -480,10 +481,10 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.form-input, .form-select {
padding: 0.5rem 0.75rem;
background: var(--color-text-heading);
border: 1px solid var(--color-text-primary);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-md);
color: var(--color-border-primary);
color: var(--color-text-primary);
font-size: 0.9rem;
}
@@ -509,7 +510,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
background: var(--color-text-primary);
background: var(--color-surface-tertiary);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.8rem;
@@ -517,7 +518,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.action-toggle:has(input:checked) {
background: rgba(34, 211, 238, 0.2);
background: var(--color-status-info-bg);
color: var(--color-status-info);
}
@@ -533,7 +534,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.timeline-preview h4 {
margin: 0 0 0.75rem;
font-size: 0.9rem;
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.timeline {
@@ -570,7 +571,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
display: flex;
justify-content: flex-end;
padding-top: 1rem;
border-top: 1px solid var(--color-text-heading);
border-top: 1px solid var(--color-border-primary);
}
.btn {
@@ -591,13 +592,6 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.btn--primary:hover:not(:disabled) { background: var(--color-status-info); }
.btn--primary:disabled { opacity: 0.5; cursor: not-allowed; }
.loading-state {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: var(--color-text-muted);
}
`]
})
export class StalenessConfigComponent implements OnInit {

View File

@@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
import { Component, ChangeDetectionStrategy, inject, signal, OnInit, computed } from '@angular/core';
import { Component, ChangeDetectionStrategy, inject, signal, OnInit, computed, ViewChild } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { finalize } from 'rxjs/operators';
@@ -12,7 +12,10 @@ import {
TrustWeightImpact,
TrustWeightSource,
} from '../../core/api/policy-governance.models';
import { LoadingStateComponent } from '../../shared/components/loading-state/loading-state.component';
import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component';
import { ModalComponent } from '../../shared/components/modal/modal.component';
/**
* Trust Weighting component.
@@ -22,7 +25,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
*/
@Component({
selector: 'app-trust-weighting',
imports: [CommonModule, ReactiveFormsModule],
imports: [CommonModule, ReactiveFormsModule, ConfirmDialogComponent, ModalComponent, LoadingStateComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="trust" [attr.aria-busy]="loading()">
@@ -66,7 +69,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" />
</svg>
</button>
<button class="btn btn--icon btn--ghost btn--danger" (click)="deleteWeight(weight)" title="Delete">
<button class="btn btn--icon btn--ghost btn--danger" (click)="requestDeleteWeight(weight)" title="Delete">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="16" height="16">
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
@@ -110,155 +113,132 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
</div>
} @else if (loading()) {
<div class="loading-state">Loading trust weights...</div>
<app-loading-state message="Loading trust weights..." />
}
<!-- Edit/Add Modal -->
@if (showModal()) {
<div class="modal-backdrop" (click)="closeModal()">
<div class="modal" (click)="$event.stopPropagation()">
<header class="modal__header">
<h3>{{ editingWeight() ? 'Edit Trust Weight' : 'Add Trust Weight' }}</h3>
<button class="btn btn--icon btn--ghost" (click)="closeModal()">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="20" height="20">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</header>
<form [formGroup]="weightForm" class="modal__body" (ngSubmit)="saveWeight()">
<div class="form-field">
<label class="form-label">Issuer ID</label>
<input type="text" formControlName="issuerId" class="form-input" placeholder="e.g., cisa, nist, vendor-redhat" />
</div>
<div class="form-field">
<label class="form-label">Issuer Name</label>
<input type="text" formControlName="issuerName" class="form-input" placeholder="e.g., CISA, NIST NVD" />
</div>
<div class="form-field">
<label class="form-label">Source Type</label>
<select formControlName="source" class="form-select">
<option value="vendor">Vendor</option>
<option value="cisa">CISA</option>
<option value="nist">NIST</option>
<option value="mitre">MITRE</option>
<option value="community">Community</option>
<option value="internal">Internal</option>
<option value="cve_org">CVE.org</option>
<option value="custom">Custom</option>
</select>
</div>
<div class="form-field">
<label class="form-label">Weight (0.0 - 2.0)</label>
<div class="weight-slider">
<input type="range" formControlName="weight" min="0" max="2" step="0.1" class="form-range" />
<span class="weight-slider__value">{{ weightForm.value.weight | number:'1.1-1' }}</span>
</div>
<div class="weight-scale">
<span>0.0 (Ignore)</span>
<span>1.0 (Neutral)</span>
<span>2.0 (High Trust)</span>
</div>
</div>
<div class="form-field">
<label class="form-label">Priority (lower = higher priority)</label>
<input type="number" formControlName="priority" class="form-input" min="1" />
</div>
<div class="form-field">
<label class="form-label">Reason</label>
<textarea formControlName="reason" class="form-textarea" rows="2" placeholder="Reason for this weight assignment"></textarea>
</div>
<div class="form-field form-field--checkbox">
<label class="form-checkbox">
<input type="checkbox" formControlName="active" />
<span>Active</span>
</label>
</div>
</form>
<footer class="modal__footer">
<button type="button" class="btn btn--ghost" (click)="closeModal()">Cancel</button>
<button type="button" class="btn btn--primary" (click)="saveWeight()" [disabled]="!weightForm.valid || saving()">
{{ saving() ? 'Saving...' : 'Save' }}
</button>
</footer>
<app-modal [open]="showModal()" [title]="editingWeight() ? 'Edit Trust Weight' : 'Add Trust Weight'" size="md" (closed)="closeModal()">
<form [formGroup]="weightForm" (ngSubmit)="saveWeight()">
<div class="form-field">
<label class="form-label">Issuer ID</label>
<input type="text" formControlName="issuerId" class="form-input" placeholder="e.g., cisa, nist, vendor-redhat" />
</div>
<div class="form-field">
<label class="form-label">Issuer Name</label>
<input type="text" formControlName="issuerName" class="form-input" placeholder="e.g., CISA, NIST NVD" />
</div>
<div class="form-field">
<label class="form-label">Source Type</label>
<select formControlName="source" class="form-select">
<option value="vendor">Vendor</option>
<option value="cisa">CISA</option>
<option value="nist">NIST</option>
<option value="mitre">MITRE</option>
<option value="community">Community</option>
<option value="internal">Internal</option>
<option value="cve_org">CVE.org</option>
<option value="custom">Custom</option>
</select>
</div>
<div class="form-field">
<label class="form-label">Weight (0.0 - 2.0)</label>
<div class="weight-slider">
<input type="range" formControlName="weight" min="0" max="2" step="0.1" class="form-range" />
<span class="weight-slider__value">{{ weightForm.value.weight | number:'1.1-1' }}</span>
</div>
<div class="weight-scale">
<span>0.0 (Ignore)</span>
<span>1.0 (Neutral)</span>
<span>2.0 (High Trust)</span>
</div>
</div>
<div class="form-field">
<label class="form-label">Priority (lower = higher priority)</label>
<input type="number" formControlName="priority" class="form-input" min="1" />
</div>
<div class="form-field">
<label class="form-label">Reason</label>
<textarea formControlName="reason" class="form-textarea" rows="2" placeholder="Reason for this weight assignment"></textarea>
</div>
<div class="form-field form-field--checkbox">
<label class="form-checkbox">
<input type="checkbox" formControlName="active" />
<span>Active</span>
</label>
</div>
</form>
<div modal-footer>
<button type="button" class="btn btn--ghost" (click)="closeModal()">Cancel</button>
<button type="button" class="btn btn--primary" (click)="saveWeight()" [disabled]="!weightForm.valid || saving()">
{{ saving() ? 'Saving...' : 'Save' }}
</button>
</div>
}
</app-modal>
<!-- Impact Preview Modal -->
@if (showImpactModal()) {
<div class="modal-backdrop" (click)="closeImpactModal()">
<div class="modal modal--wide" (click)="$event.stopPropagation()">
<header class="modal__header">
<h3>Impact Preview</h3>
<button class="btn btn--icon btn--ghost" (click)="closeImpactModal()">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="20" height="20">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</header>
<app-modal [open]="showImpactModal()" title="Impact Preview" size="lg" (closed)="closeImpactModal()">
@if (impactLoading()) {
<app-loading-state message="Calculating impact..." size="sm" />
} @else {
@if (impact(); as i) {
<div class="impact-summary">
<div class="impact-stat">
<div class="impact-stat__value">{{ i.affectedVulnerabilities }}</div>
<div class="impact-stat__label">Affected Vulnerabilities</div>
</div>
<div class="impact-stat">
<div class="impact-stat__value">{{ i.severityChanges }}</div>
<div class="impact-stat__label">Severity Changes</div>
</div>
<div class="impact-stat">
<div class="impact-stat__value">{{ i.decisionChanges }}</div>
<div class="impact-stat__label">Decision Changes</div>
</div>
</div>
<div class="modal__body">
@if (impactLoading()) {
<div class="loading-state">Calculating impact...</div>
} @else {
@if (impact(); as i) {
<div class="impact-summary">
<div class="impact-stat">
<div class="impact-stat__value">{{ i.affectedVulnerabilities }}</div>
<div class="impact-stat__label">Affected Vulnerabilities</div>
</div>
<div class="impact-stat">
<div class="impact-stat__value">{{ i.severityChanges }}</div>
<div class="impact-stat__label">Severity Changes</div>
</div>
<div class="impact-stat">
<div class="impact-stat__value">{{ i.decisionChanges }}</div>
<div class="impact-stat__label">Decision Changes</div>
</div>
</div>
<h4>Severity Transitions</h4>
<div class="transition-list">
@for (entry of getTransitionEntries(i.severityTransitions); track entry[0]) {
<div class="transition-item">
<span class="transition-item__label">{{ entry[0] }}</span>
<span class="transition-item__count">{{ entry[1] }}</span>
</div>
}
</div>
<h4>Sample Affected Findings</h4>
<div class="sample-list">
@for (finding of i.sampleAffected; track finding.findingId) {
<div class="sample-item">
<div class="sample-item__component">{{ finding.componentPurl }}</div>
<div class="sample-item__advisory">{{ finding.advisoryId }}</div>
<div class="sample-item__change">
<span [class]="'severity-badge severity-badge--' + finding.currentSeverity">{{ finding.currentSeverity }}</span>
<span class="arrow">-&gt;</span>
<span [class]="'severity-badge severity-badge--' + finding.projectedSeverity">{{ finding.projectedSeverity }}</span>
</div>
</div>
}
</div>
}
<h4>Severity Transitions</h4>
<div class="transition-list">
@for (entry of getTransitionEntries(i.severityTransitions); track entry[0]) {
<div class="transition-item">
<span class="transition-item__label">{{ entry[0] }}</span>
<span class="transition-item__count">{{ entry[1] }}</span>
</div>
}
</div>
<footer class="modal__footer">
<button type="button" class="btn btn--ghost" (click)="closeImpactModal()">Close</button>
</footer>
</div>
<h4>Sample Affected Findings</h4>
<div class="sample-list">
@for (finding of i.sampleAffected; track finding.findingId) {
<div class="sample-item">
<div class="sample-item__component">{{ finding.componentPurl }}</div>
<div class="sample-item__advisory">{{ finding.advisoryId }}</div>
<div class="sample-item__change">
<span [class]="'severity-badge severity-badge--' + finding.currentSeverity">{{ finding.currentSeverity }}</span>
<span class="arrow">-&gt;</span>
<span [class]="'severity-badge severity-badge--' + finding.projectedSeverity">{{ finding.projectedSeverity }}</span>
</div>
</div>
}
</div>
}
}
<div modal-footer>
<button type="button" class="btn btn--ghost" (click)="closeImpactModal()">Close</button>
</div>
}
</app-modal>
<app-confirm-dialog #deleteWeightConfirm
title="Delete Trust Weight"
[message]="'Delete trust weight for \\'' + pendingDeleteWeight()?.issuerName + '\\'? This cannot be undone.'"
confirmLabel="Delete" cancelLabel="Cancel" variant="danger"
(confirmed)="executeDeleteWeight()" />
</div>
`,
styles: [`
@@ -279,7 +259,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
margin: 0;
font-size: 1.25rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.trust__subtitle {
@@ -303,12 +283,12 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.btn--secondary {
background: var(--color-text-primary);
color: var(--color-border-primary);
border: 1px solid var(--color-text-primary);
background: var(--color-surface-tertiary);
color: var(--color-text-primary);
border: 1px solid var(--color-border-primary);
}
.btn--secondary:hover { background: var(--color-text-primary); }
.btn--secondary:hover { background: var(--color-surface-tertiary); }
.btn--primary {
background: var(--color-status-info);
@@ -323,7 +303,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
color: var(--color-text-muted);
}
.btn--ghost:hover { background: var(--color-text-primary); color: var(--color-border-primary); }
.btn--ghost:hover { background: var(--color-surface-tertiary); color: var(--color-text-primary); }
.btn--icon { padding: 0.35rem; }
@@ -335,8 +315,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
margin-bottom: 1rem;
}
@@ -359,8 +339,8 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
}
.weight-card {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
padding: 1rem;
}
@@ -379,7 +359,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.weight-card__source {
font-size: 0.7rem;
padding: 0.15rem 0.5rem;
background: var(--color-text-primary);
background: var(--color-surface-tertiary);
border-radius: var(--radius-sm);
color: var(--color-text-muted);
text-transform: uppercase;
@@ -398,7 +378,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.weight-card__name {
font-size: 1.1rem;
font-weight: var(--font-weight-semibold);
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.weight-card__id {
@@ -420,7 +400,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.weight-gauge__track {
flex: 1;
height: 8px;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-sm);
overflow: hidden;
}
@@ -452,7 +432,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
font-size: 0.85rem;
color: var(--color-text-muted);
padding: 0.5rem;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-sm);
margin-bottom: 0.5rem;
}
@@ -462,61 +442,10 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
gap: 0.5rem;
font-size: 0.75rem;
color: var(--color-text-secondary);
border-top: 1px solid var(--color-text-heading);
border-top: 1px solid var(--color-border-primary);
padding-top: 0.5rem;
}
/* Modal */
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal {
background: var(--color-text-heading);
border: 1px solid var(--color-text-heading);
border-radius: var(--radius-xl);
width: 90%;
max-width: 480px;
max-height: 90vh;
overflow-y: auto;
}
.modal--wide {
max-width: 640px;
}
.modal__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--color-text-heading);
}
.modal__header h3 {
margin: 0;
font-size: 1.1rem;
color: var(--color-surface-secondary);
}
.modal__body {
padding: 1.25rem;
}
.modal__footer {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
padding: 1rem 1.25rem;
border-top: 1px solid var(--color-text-heading);
}
/* Form */
.form-field {
margin-bottom: 1rem;
@@ -529,7 +458,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.form-label {
display: block;
color: var(--color-border-primary);
color: var(--color-text-primary);
font-size: 0.85rem;
font-weight: var(--font-weight-medium);
margin-bottom: 0.35rem;
@@ -538,10 +467,10 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.form-input, .form-select, .form-textarea {
width: 100%;
padding: 0.5rem 0.75rem;
background: var(--color-text-heading);
border: 1px solid var(--color-text-primary);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-md);
color: var(--color-border-primary);
color: var(--color-text-primary);
font-size: 0.9rem;
}
@@ -555,7 +484,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
align-items: center;
gap: 0.5rem;
cursor: pointer;
color: var(--color-border-primary);
color: var(--color-text-primary);
}
.form-checkbox input {
@@ -600,7 +529,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.impact-stat {
text-align: center;
padding: 1rem;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-lg);
}
@@ -618,7 +547,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
h4 {
margin: 1rem 0 0.5rem;
font-size: 0.9rem;
color: var(--color-surface-secondary);
color: var(--color-text-heading);
}
.transition-list {
@@ -633,12 +562,12 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0.75rem;
background: var(--color-text-primary);
background: var(--color-surface-tertiary);
border-radius: var(--radius-md);
}
.transition-item__label {
color: var(--color-border-primary);
color: var(--color-text-primary);
font-size: 0.85rem;
}
@@ -658,7 +587,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
grid-template-columns: 1fr auto auto;
gap: 1rem;
padding: 0.5rem;
background: var(--color-text-heading);
background: var(--color-surface-elevated);
border-radius: var(--radius-md);
align-items: center;
}
@@ -666,7 +595,7 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.sample-item__component {
font-family: monospace;
font-size: 0.8rem;
color: var(--color-border-primary);
color: var(--color-text-primary);
overflow: hidden;
text-overflow: ellipsis;
}
@@ -698,13 +627,6 @@ import { injectPolicyGovernanceScopeResolver } from './policy-governance-scope';
.severity-badge--low { background: var(--color-status-success-text); color: #fff; }
.severity-badge--info { background: var(--color-status-info-text); color: #fff; }
.loading-state {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: var(--color-text-muted);
}
`]
})
export class TrustWeightingComponent implements OnInit {
@@ -718,6 +640,9 @@ export class TrustWeightingComponent implements OnInit {
protected readonly showModal = signal(false);
protected readonly editingWeight = signal<TrustWeight | null>(null);
protected readonly pendingDeleteWeight = signal<TrustWeight | null>(null);
@ViewChild('deleteWeightConfirm') private deleteWeightConfirmRef!: ConfirmDialogComponent;
protected readonly showImpactModal = signal(false);
protected readonly impactLoading = signal(false);
protected readonly impact = signal<TrustWeightImpact | null>(null);
@@ -827,8 +752,14 @@ export class TrustWeightingComponent implements OnInit {
});
}
protected deleteWeight(weight: TrustWeight): void {
if (!confirm(`Delete trust weight for ${weight.issuerName}?`)) return;
protected requestDeleteWeight(weight: TrustWeight): void {
this.pendingDeleteWeight.set(weight);
this.deleteWeightConfirmRef.open();
}
protected executeDeleteWeight(): void {
const weight = this.pendingDeleteWeight();
if (!weight) return;
this.api.deleteTrustWeight(weight.id, this.governanceScope()).subscribe({
next: () => this.loadConfig(),