a11oy / web /src /components /operations /governed-decision /policy-gate-panel.tsx
betterwithage's picture
sync(space): complete build context — fix BUILD_ERROR (CTO)
518343a verified
Raw
History Blame
12.8 kB
import {
AlertTriangle,
ArrowRight,
CheckCircle,
ChevronDown,
ChevronRight,
Clock,
Scale,
User,
XCircle,
} from 'lucide-react';
import { useState } from 'react';
const BG = { surface: 'var(--gi-bg-surface)', elevated: 'var(--gi-bg-raised)' };
const BORDER = { subtle: 'rgba(255,255,255,0.04)', muted: 'rgba(255,255,255,0.07)' };
const TEXT = {
primary: 'rgba(255,255,255,0.88)',
secondary: 'rgba(255,255,255,0.55)',
tertiary: 'rgba(255,255,255,0.28)',
muted: 'rgba(255,255,255,0.14)',
};
export type PolicyOutcome = 'approved' | 'denied' | 'escalated' | 'pending';
export interface PolicyEvaluation {
policyId: string;
policyName: string;
outcome: PolicyOutcome;
reason: string;
evaluatedAt: string;
durationMs: number;
}
export interface ApprovalStep {
role: string;
approver: string;
status: 'approved' | 'pending' | 'rejected';
timestamp?: string;
comment?: string;
}
export interface EscalationPath {
from: string;
to: string;
reason: string;
triggeredAt: string;
}
export interface AuditEntry {
timestamp: string;
action: string;
actor: string;
detail?: string;
}
export interface PolicyGatePanelProps {
finalOutcome: PolicyOutcome;
evaluations: PolicyEvaluation[];
approvalChain: ApprovalStep[];
escalation?: EscalationPath;
auditTrail: AuditEntry[];
compact?: boolean;
}
const OUTCOME_CFG: Record<
PolicyOutcome,
{ color: string; bg: string; icon: typeof CheckCircle; label: string }
> = {
approved: { color: '#6b8f71', bg: 'rgba(107,143,113,0.1)', icon: CheckCircle, label: 'Approved' },
denied: { color: '#c45a4a', bg: 'rgba(196,90,74,0.1)', icon: XCircle, label: 'Denied' },
escalated: {
color: '#ec4899',
bg: 'rgba(236,72,153,0.1)',
icon: AlertTriangle,
label: 'Escalated',
},
pending: { color: '#c8953c', bg: 'rgba(200,149,60,0.1)', icon: Clock, label: 'Pending' },
};
export function PolicyGatePanel({
finalOutcome,
evaluations,
approvalChain,
escalation,
auditTrail,
compact,
}: PolicyGatePanelProps) {
const [showAudit, setShowAudit] = useState(false);
const outcomeCfg = OUTCOME_CFG[finalOutcome];
const OutcomeIcon = outcomeCfg.icon;
if (compact) {
return (
<div
className="rounded-xl p-4"
style={{ background: BG.surface, border: `1px solid ${BORDER.muted}` }}
>
<div className="flex items-center gap-2 mb-3">
<Scale className="w-3.5 h-3.5" style={{ color: '#d4a054' }} />
<span
className="text-[10px] font-bold uppercase tracking-widest"
style={{ color: '#d4a054' }}
>
Covenant Policy Gate
</span>
</div>
<div className="flex items-center gap-3">
<div
className="w-7 h-7 rounded-lg flex items-center justify-center"
style={{ background: outcomeCfg.bg, border: `1px solid ${outcomeCfg.color}30` }}
>
<OutcomeIcon className="w-3.5 h-3.5" style={{ color: outcomeCfg.color }} />
</div>
<div>
<div className="text-sm font-bold" style={{ color: outcomeCfg.color }}>
{outcomeCfg.label}
</div>
<div className="text-[9px]" style={{ color: TEXT.tertiary }}>
{evaluations.length} policies evaluated
</div>
</div>
<div className="ml-auto flex -space-x-1">
{approvalChain.map((step, i) => {
const stepCfg =
step.status === 'approved'
? OUTCOME_CFG.approved
: step.status === 'rejected'
? OUTCOME_CFG.denied
: OUTCOME_CFG.pending;
const StepIcon = stepCfg.icon;
return (
<div
key={i}
className="w-5 h-5 rounded-full flex items-center justify-center"
style={{ background: stepCfg.bg, border: `2px solid ${stepCfg.color}` }}
>
<StepIcon className="w-2.5 h-2.5" style={{ color: stepCfg.color }} />
</div>
);
})}
</div>
</div>
</div>
);
}
return (
<div
className="rounded-xl overflow-hidden"
style={{ background: BG.surface, border: `1px solid ${BORDER.muted}` }}
>
<div
className="flex items-center gap-2 px-5 py-3"
style={{ borderBottom: `1px solid ${BORDER.subtle}` }}
>
<Scale className="w-4 h-4" style={{ color: '#d4a054' }} />
<span className="text-xs font-bold uppercase tracking-widest" style={{ color: '#d4a054' }}>
Covenant Policy Gate
</span>
<div className="ml-auto flex items-center gap-2">
<span
className="text-[9px] font-bold px-2 py-0.5 rounded"
style={{ color: outcomeCfg.color, background: outcomeCfg.bg }}
>
{outcomeCfg.label.toUpperCase()}
</span>
</div>
</div>
<div className="p-5">
<div
className="text-[9px] font-bold uppercase tracking-widest mb-2"
style={{ color: TEXT.muted }}
>
Policy Evaluations
</div>
<div className="space-y-2 mb-4">
{evaluations.map((ev) => {
const evCfg = OUTCOME_CFG[ev.outcome];
const EvIcon = evCfg.icon;
return (
<div
key={ev.policyId}
className="flex items-center gap-3 p-2.5 rounded-lg"
style={{
background: 'rgba(255,255,255,0.02)',
border: `1px solid ${BORDER.subtle}`,
}}
>
<EvIcon className="w-3.5 h-3.5 shrink-0" style={{ color: evCfg.color }} />
<div className="flex-1 min-w-0">
<div className="text-[10px] font-semibold" style={{ color: TEXT.primary }}>
{ev.policyName}
</div>
<div className="text-[9px]" style={{ color: TEXT.tertiary }}>
{ev.reason}
</div>
</div>
<span className="text-[9px] font-mono shrink-0" style={{ color: TEXT.muted }}>
{ev.durationMs}ms
</span>
</div>
);
})}
</div>
<div
className="text-[9px] font-bold uppercase tracking-widest mb-2"
style={{ color: TEXT.muted }}
>
Approval Chain
</div>
<div className="flex flex-col gap-2 mb-4">
{approvalChain.map((step, i) => {
const stepCfg =
step.status === 'approved'
? OUTCOME_CFG.approved
: step.status === 'rejected'
? OUTCOME_CFG.denied
: OUTCOME_CFG.pending;
const StepIcon = stepCfg.icon;
return (
<div key={i} className="flex gap-3">
<div className="flex flex-col items-center">
<div
className="w-7 h-7 rounded-full flex items-center justify-center"
style={{ background: stepCfg.bg, border: `2px solid ${stepCfg.color}` }}
>
<StepIcon className="w-3 h-3" style={{ color: stepCfg.color }} />
</div>
{i < approvalChain.length - 1 && (
<div
className="w-px flex-1 my-1"
style={{ background: BORDER.muted, minHeight: 16 }}
/>
)}
</div>
<div className="flex-1 pb-2">
<div className="flex items-center gap-2 mb-0.5">
<span className="text-[10px] font-semibold" style={{ color: TEXT.primary }}>
{step.role}
</span>
<span className="text-[9px]" style={{ color: TEXT.tertiary }}>
— {step.approver}
</span>
{step.timestamp && (
<span className="text-[9px] ml-auto font-mono" style={{ color: TEXT.muted }}>
{step.timestamp}
</span>
)}
</div>
{step.comment && (
<div className="text-[9px] italic" style={{ color: TEXT.tertiary }}>
"{step.comment}"
</div>
)}
{step.status === 'pending' &&
i > 0 &&
approvalChain[i - 1]?.status === 'approved' && (
<div className="flex gap-2 mt-1.5">
<button
className="px-2.5 py-1 rounded text-[9px] font-bold"
style={{ background: '#6b8f71', color: '#fff' }}
>
Approve
</button>
<button
className="px-2.5 py-1 rounded text-[9px]"
style={{
background: 'rgba(255,255,255,0.03)',
border: `1px solid ${BORDER.muted}`,
color: '#c45a4a',
}}
>
Reject
</button>
</div>
)}
</div>
</div>
);
})}
</div>
{escalation && (
<div
className="p-3 rounded-lg mb-4"
style={{
background: 'rgba(236,72,153,0.05)',
border: '1px solid rgba(236,72,153,0.15)',
}}
>
<div className="flex items-center gap-2 mb-1">
<AlertTriangle className="w-3 h-3" style={{ color: '#ec4899' }} />
<span className="text-[10px] font-bold" style={{ color: '#ec4899' }}>
Escalation
</span>
</div>
<div className="text-[10px]" style={{ color: TEXT.secondary }}>
{escalation.from}{' '}
<ArrowRight className="w-3 h-3 inline mx-1" style={{ color: TEXT.muted }} />{' '}
{escalation.to}
</div>
<div className="text-[9px] mt-0.5" style={{ color: TEXT.tertiary }}>
{escalation.reason} · {escalation.triggeredAt}
</div>
</div>
)}
<button
onClick={() => setShowAudit(!showAudit)}
className="flex items-center gap-1.5 text-[10px] font-medium"
style={{ color: '#d4a054' }}
aria-expanded={showAudit}
aria-label={`${showAudit ? 'Hide' : 'Show'} audit trail`}
>
{showAudit ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
Audit Trail ({auditTrail.length} events)
</button>
{showAudit && (
<div className="mt-2 space-y-1">
{auditTrail.map((entry, i) => (
<div
key={i}
className="flex items-center gap-3 py-1.5 px-2 rounded"
style={{ background: i % 2 === 0 ? 'rgba(255,255,255,0.01)' : 'transparent' }}
>
<span className="text-[9px] font-mono w-16 shrink-0" style={{ color: TEXT.muted }}>
{entry.timestamp}
</span>
<User className="w-2.5 h-2.5 shrink-0" style={{ color: TEXT.tertiary }} />
<span className="text-[10px] flex-1" style={{ color: TEXT.secondary }}>
{entry.action}
</span>
<span className="text-[9px] shrink-0" style={{ color: '#d4a054' }}>
{entry.actor}
</span>
</div>
))}
<div
className="flex items-center gap-1.5 pt-2 mt-1"
style={{ borderTop: `1px solid ${BORDER.subtle}` }}
>
<span
className="text-[7px] font-mono px-1.5 py-0.5 rounded"
style={{
background: 'rgba(212,160,84,0.05)',
border: '1px solid rgba(212,160,84,0.12)',
color: 'rgba(212,160,84,0.45)',
}}
>
synthetic · demo scenario
</span>
<span className="text-[7px] font-mono" style={{ color: TEXT.muted }}>
No live systems connected — audit log is illustrative
</span>
</div>
</div>
)}
</div>
</div>
);
}