feat(security): expose scanner failures in focused cockpit
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / tests (push) Has been cancelled

This commit is contained in:
ogt
2026-07-14 17:59:04 +08:00
parent 52b870824f
commit ec4210e6bf
6 changed files with 668 additions and 254 deletions

View File

@@ -27,6 +27,9 @@ import { cn } from "@/lib/utils";
type Copy = {
title: string;
commandView: string;
assetView: string;
automationView: string;
live: string;
degraded: string;
overall: string;
@@ -64,6 +67,8 @@ type Copy = {
stale: string;
success: string;
failed: string;
failedCollectors: string;
failedCollectorsUnavailable: string;
sameRunMissing: string;
unavailable: string;
refresh: string;
@@ -72,6 +77,9 @@ type Copy = {
const COPY: Record<"zh" | "en", Copy> = {
zh: {
title: "AI 資安控制平面",
commandView: "指揮總覽",
assetView: "資產與供應鏈",
automationView: "SIEM 與 AI",
live: "LIVE",
degraded: "DEGRADED",
overall: "整體完成度",
@@ -109,12 +117,17 @@ const COPY: Record<"zh" | "en", Copy> = {
stale: "逾期資產",
success: "成功",
failed: "失敗",
failedCollectors: "失敗 Collector",
failedCollectorsUnavailable: "失敗原因待安全分類",
sameRunMissing: "尚無同 run 閉環證據",
unavailable: "Live readback 無法取得",
refresh: "重新整理資安控制平面",
},
en: {
title: "AI Security Control Plane",
commandView: "Command",
assetView: "Assets & supply chain",
automationView: "SIEM & AI",
live: "LIVE",
degraded: "DEGRADED",
overall: "Overall completion",
@@ -152,12 +165,26 @@ const COPY: Record<"zh" | "en", Copy> = {
stale: "Stale assets",
success: "Success",
failed: "Failed",
failedCollectors: "Failed collectors",
failedCollectorsUnavailable: "Failure detail awaiting safe classification",
sameRunMissing: "No same-run closure evidence",
unavailable: "Live readback unavailable",
refresh: "Refresh security control plane",
},
};
type CockpitView = "command" | "assets" | "automation";
const COCKPIT_VIEWS: Array<{
key: CockpitView;
icon: typeof ShieldCheck;
label: keyof Pick<Copy, "commandView" | "assetView" | "automationView">;
}> = [
{ key: "command", icon: Radar, label: "commandView" },
{ key: "assets", icon: Boxes, label: "assetView" },
{ key: "automation", icon: Bot, label: "automationView" },
];
const TONE_CLASS = {
healthy: "border-[#8fc39c] bg-[#f2faf4] text-[#17602a]",
warning: "border-[#d9b36f] bg-[#fff8e8] text-[#8a5a08]",
@@ -177,7 +204,9 @@ function freshnessLabel(ageSeconds: number | null) {
async function fetchControlPlane(): Promise<SecurityAssetControlPlanePayload | null> {
const base = getRuntimeApiBaseUrl();
const path = "/api/v1/iwooos/security-asset-control-plane";
const urls = Array.from(new Set([path, base ? `${base}${path}` : ""].filter(Boolean)));
const urls = Array.from(
new Set([path, base ? `${base}${path}` : ""].filter(Boolean)),
);
for (const url of urls) {
try {
const response = await fetch(url, { cache: "no-store" });
@@ -230,7 +259,7 @@ function TruthCell({
<div
className={cn(
"flex min-w-0 items-center gap-2 bg-white px-3 py-2.5",
wideOnMobile && "last:col-span-2 sm:last:col-span-1"
wideOnMobile && "last:col-span-2 lg:last:col-span-1",
)}
>
<Icon
@@ -240,12 +269,14 @@ function TruthCell({
? "text-[#2f8a50]"
: tone === "critical"
? "text-[#c9584d]"
: "text-[#77736a]"
: "text-[#77736a]",
)}
aria-hidden="true"
/>
<div className="min-w-0">
<div className="truncate text-[9px] font-medium text-[#77736a]">{label}</div>
<div className="truncate text-[9px] font-medium text-[#77736a]">
{label}
</div>
<div
className={cn(
"mt-0.5 truncate font-mono text-xs font-semibold",
@@ -253,7 +284,7 @@ function TruthCell({
? "text-[#17602a]"
: tone === "critical"
? "text-[#9f2f25]"
: "text-[#141413]"
: "text-[#141413]",
)}
>
{value}
@@ -273,7 +304,8 @@ function CertificateTruthStrip({
const certificate = payload?.certificate_inventory;
const assetCount = certificate?.certificate_asset_count ?? 0;
const evidencedCount =
certificate?.live_tls_evidenced_count ?? certificate?.live_tls_verified_count;
certificate?.live_tls_evidenced_count ??
certificate?.live_tls_verified_count;
const healthyCount = certificate?.live_tls_healthy_count;
const healthProven = certificate?.live_tls_health_proven === true;
@@ -335,7 +367,9 @@ function StageStrip({
}) {
return (
<div className="min-w-0">
<div className="mb-2 text-[11px] font-semibold text-[#4f4d47]">{title}</div>
<div className="mb-2 text-[11px] font-semibold text-[#4f4d47]">
{title}
</div>
<div className="grid grid-cols-4 gap-1 sm:grid-cols-7 lg:grid-cols-8">
{stages.map((stage) => {
const observed = stage.status === "observed";
@@ -346,11 +380,13 @@ function StageStrip({
"min-w-0 border px-1.5 py-2 text-center",
observed
? "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]"
: "border-[#e2a29b] bg-[#fff2f0] text-[#9f2f25]"
: "border-[#e2a29b] bg-[#fff2f0] text-[#9f2f25]",
)}
title={`${stage.label}: ${stageCount(stage)}`}
>
<div className="truncate text-[10px] font-semibold">{stage.label}</div>
<div className="truncate text-[10px] font-semibold">
{stage.label}
</div>
<div className="mt-1 font-mono text-xs">{stageCount(stage)}</div>
</div>
);
@@ -368,8 +404,10 @@ export function SecurityAssetControlPlaneCockpit({
className?: string;
}) {
const copy = COPY[locale.toLowerCase().startsWith("zh") ? "zh" : "en"];
const [payload, setPayload] = useState<SecurityAssetControlPlanePayload | null>(null);
const [payload, setPayload] =
useState<SecurityAssetControlPlanePayload | null>(null);
const [loading, setLoading] = useState(true);
const [view, setView] = useState<CockpitView>("command");
const load = useCallback(async () => {
setLoading(true);
@@ -387,13 +425,16 @@ export function SecurityAssetControlPlaneCockpit({
const tone = payload ? securityControlPlaneTone(payload) : "critical";
const priorityItems = useMemo(
() => (payload ? topSecurityControlPlaneWorkItems(payload, 3) : []),
[payload]
[payload],
);
if (!payload && !loading) {
return (
<section
className={cn("border border-[#e2a29b] bg-[#fff2f0] px-4 py-4", className)}
className={cn(
"border border-[#e2a29b] bg-[#fff2f0] px-4 py-4",
className,
)}
data-testid="security-asset-control-plane-cockpit"
>
<div className="flex items-center gap-2 text-sm font-semibold text-[#9f2f25]">
@@ -408,19 +449,24 @@ export function SecurityAssetControlPlaneCockpit({
<section
className={cn(
"overflow-hidden border border-[#d8d3c7] bg-[#f6f4ee]",
className
className,
)}
data-testid="security-asset-control-plane-cockpit"
aria-busy={loading}
>
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[#d8d3c7] bg-[#141413] px-4 py-3 text-white">
<div className="flex min-w-0 items-center gap-2.5">
<ShieldCheck className="h-5 w-5 shrink-0 text-[#7fd29b]" aria-hidden="true" />
<h2 className="truncate text-sm font-semibold tracking-normal">{copy.title}</h2>
<ShieldCheck
className="h-5 w-5 shrink-0 text-[#7fd29b]"
aria-hidden="true"
/>
<h2 className="truncate text-sm font-semibold tracking-normal">
{copy.title}
</h2>
<span
className={cn(
"border px-2 py-0.5 font-mono text-[10px] font-semibold",
TONE_CLASS[tone]
TONE_CLASS[tone],
)}
>
{payload?.status === "ready" ? copy.live : copy.degraded}
@@ -433,167 +479,334 @@ export function SecurityAssetControlPlaneCockpit({
aria-label={copy.refresh}
title={copy.refresh}
>
<RefreshCw className={cn("h-4 w-4", loading && "animate-spin")} aria-hidden="true" />
<RefreshCw
className={cn("h-4 w-4", loading && "animate-spin")}
aria-hidden="true"
/>
</button>
</div>
<CertificateTruthStrip copy={copy} payload={payload} />
<div className="grid grid-cols-2 gap-px bg-[#d8d3c7] sm:grid-cols-3 lg:grid-cols-6">
<MetricCell
label={copy.overall}
value={payload ? `${payload.summary.overall_security_completion_percent}%` : "--"}
icon={ShieldCheck}
/>
<MetricCell
label={copy.assetTypes}
value={payload ? `${payload.summary.asset_type_coverage_percent}%` : "--"}
icon={Activity}
/>
<MetricCell
label={copy.nist}
value={payload ? `${payload.summary.nist_controlled_percent}%` : "--"}
icon={ShieldCheck}
/>
<MetricCell
label={copy.siem}
value={payload ? `${payload.summary.siem_observed_stage_count}/${payload.summary.siem_stage_count}` : "--"}
icon={Radar}
/>
<MetricCell
label={copy.aiLoop}
value={payload ? `${payload.summary.ai_loop_observed_stage_count}/${payload.summary.ai_loop_stage_count}` : "--"}
icon={Bot}
/>
<MetricCell
label={copy.verified}
value={payload?.summary.verified_remediation_receipt_count_24h ?? "--"}
icon={CheckCircle2}
/>
</div>
<div
className="grid grid-cols-2 gap-px border-t border-[#d8d3c7] bg-[#d8d3c7] sm:grid-cols-5"
data-testid="security-asset-discovery-truth"
className="grid grid-cols-3 gap-px border-b border-[#d8d3c7] bg-[#d8d3c7]"
role="tablist"
aria-label={copy.title}
>
<TruthCell
label={copy.latestScan}
value={
payload?.discovery.latest_status === "success"
? copy.success
: payload
? copy.failed
: "--"
}
icon={Radar}
tone={payload?.discovery.latest_status === "success" ? "healthy" : "critical"}
/>
<TruthCell
label={copy.freshness}
value={payload ? freshnessLabel(payload.discovery.age_seconds) : "--"}
icon={RefreshCw}
tone={payload?.discovery.fresh ? "healthy" : "critical"}
/>
<TruthCell
label={copy.runAssets}
value={payload?.discovery.latest_total_assets ?? "--"}
icon={Database}
/>
<TruthCell
label={copy.retired}
value={payload?.discovery.disappeared_assets ?? "--"}
icon={Boxes}
tone={(payload?.discovery.disappeared_assets ?? 0) > 0 ? "healthy" : "neutral"}
/>
<TruthCell
label={copy.stale}
value={payload?.summary.stale_asset_count ?? "--"}
icon={TriangleAlert}
tone={(payload?.summary.stale_asset_count ?? 0) > 0 ? "critical" : "healthy"}
wideOnMobile
/>
{COCKPIT_VIEWS.map((item) => {
const Icon = item.icon;
const selected = view === item.key;
return (
<button
key={item.key}
type="button"
role="tab"
aria-selected={selected}
onClick={() => setView(item.key)}
className={cn(
"flex min-h-11 min-w-0 items-center justify-center gap-2 px-2 py-2 text-[11px] font-semibold",
selected
? "bg-white text-[#17602a]"
: "bg-[#eeece5] text-[#68655d] hover:bg-[#f8f7f3]",
)}
>
<Icon className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
<span className="truncate">{copy[item.label]}</span>
</button>
);
})}
</div>
<div className="border-t border-[#d8d3c7] bg-[#f6f4ee] px-3 py-2">
<div className="mb-1.5 flex items-center gap-1.5 text-[10px] font-semibold text-[#4f4d47]">
<Boxes className="h-3.5 w-3.5" aria-hidden="true" />
{copy.supplyChain}
</div>
<div
className="grid grid-cols-2 gap-px bg-[#d8d3c7] sm:grid-cols-4"
data-testid="security-supply-chain-truth"
>
<TruthCell
label={copy.runtimeImages}
value={payload?.supply_chain?.package_asset_count ?? "--"}
icon={Boxes}
tone={
payload?.supply_chain?.package_inventory_evidenced
? "healthy"
: "critical"
}
/>
<TruthCell
label={copy.digestPinned}
{view === "assets" ? (
<CertificateTruthStrip copy={copy} payload={payload} />
) : null}
{view === "command" ? (
<div className="grid grid-cols-3 gap-px bg-[#d8d3c7] lg:grid-cols-6">
<MetricCell
label={copy.overall}
value={
payload?.supply_chain
? `${payload.supply_chain.package_digest_pinned_count}/${payload.supply_chain.package_asset_count}`
payload
? `${payload.summary.overall_security_completion_percent}%`
: "--"
}
icon={ShieldCheck}
/>
<MetricCell
label={copy.assetTypes}
value={
payload ? `${payload.summary.asset_type_coverage_percent}%` : "--"
}
icon={Activity}
/>
<MetricCell
label={copy.nist}
value={
payload ? `${payload.summary.nist_controlled_percent}%` : "--"
}
icon={ShieldCheck}
/>
<MetricCell
label={copy.siem}
value={
payload
? `${payload.summary.siem_observed_stage_count}/${payload.summary.siem_stage_count}`
: "--"
}
icon={Radar}
/>
<MetricCell
label={copy.aiLoop}
value={
payload
? `${payload.summary.ai_loop_observed_stage_count}/${payload.summary.ai_loop_stage_count}`
: "--"
}
icon={Bot}
/>
<MetricCell
label={copy.verified}
value={
payload?.summary.verified_remediation_receipt_count_24h ?? "--"
}
icon={CheckCircle2}
tone={
payload?.supply_chain?.package_inventory_evidenced &&
payload.supply_chain.package_digest_unpinned_count === 0
? "healthy"
: "critical"
}
/>
<TruthCell
label={copy.forbiddenSources}
value={
payload?.supply_chain?.forbidden_github_supply_chain_asset_count ??
"--"
}
icon={ShieldAlert}
tone={
payload?.supply_chain?.package_inventory_evidenced &&
payload.supply_chain.forbidden_github_supply_chain_asset_count === 0
? "healthy"
: "critical"
}
/>
<TruthCell
label={copy.imagePolicy}
value={
!payload?.supply_chain?.package_inventory_evidenced
? copy.notEvidenced
: payload.supply_chain.runtime_image_policy_compliant
? copy.compliant
: copy.repair
}
icon={
payload?.supply_chain?.runtime_image_policy_compliant
? ShieldCheck
: TriangleAlert
}
tone={
payload?.supply_chain?.runtime_image_policy_compliant
? "healthy"
: "critical"
}
/>
</div>
</div>
) : null}
{view === "command" ? (
<div
className="grid grid-cols-3 gap-px border-t border-[#d8d3c7] bg-[#d8d3c7] lg:grid-cols-5"
data-testid="security-asset-discovery-truth"
>
<TruthCell
label={copy.latestScan}
value={
payload?.discovery.latest_status === "success"
? copy.success
: payload
? copy.failed
: "--"
}
icon={Radar}
tone={
payload?.discovery.latest_status === "success"
? "healthy"
: "critical"
}
/>
<TruthCell
label={copy.freshness}
value={
payload ? freshnessLabel(payload.discovery.age_seconds) : "--"
}
icon={RefreshCw}
tone={payload?.discovery.fresh ? "healthy" : "critical"}
/>
<TruthCell
label={copy.runAssets}
value={payload?.discovery.latest_total_assets ?? "--"}
icon={Database}
/>
<TruthCell
label={copy.retired}
value={payload?.discovery.disappeared_assets ?? "--"}
icon={Boxes}
tone={
(payload?.discovery.disappeared_assets ?? 0) > 0
? "healthy"
: "neutral"
}
/>
<TruthCell
label={copy.stale}
value={payload?.summary.stale_asset_count ?? "--"}
icon={TriangleAlert}
tone={
(payload?.summary.stale_asset_count ?? 0) > 0
? "critical"
: "healthy"
}
wideOnMobile
/>
</div>
) : null}
{view === "assets" ? (
<div className="border-t border-[#d8d3c7] bg-[#f6f4ee] px-3 py-2">
<div className="mb-1.5 flex items-center gap-1.5 text-[10px] font-semibold text-[#4f4d47]">
<Boxes className="h-3.5 w-3.5" aria-hidden="true" />
{copy.supplyChain}
</div>
<div
className="grid grid-cols-2 gap-px bg-[#d8d3c7] sm:grid-cols-4"
data-testid="security-supply-chain-truth"
>
<TruthCell
label={copy.runtimeImages}
value={payload?.supply_chain?.package_asset_count ?? "--"}
icon={Boxes}
tone={
payload?.supply_chain?.package_inventory_evidenced
? "healthy"
: "critical"
}
/>
<TruthCell
label={copy.digestPinned}
value={
payload?.supply_chain
? `${payload.supply_chain.package_digest_pinned_count}/${payload.supply_chain.package_asset_count}`
: "--"
}
icon={CheckCircle2}
tone={
payload?.supply_chain?.package_inventory_evidenced &&
payload.supply_chain.package_digest_unpinned_count === 0
? "healthy"
: "critical"
}
/>
<TruthCell
label={copy.forbiddenSources}
value={
payload?.supply_chain
?.forbidden_github_supply_chain_asset_count ?? "--"
}
icon={ShieldAlert}
tone={
payload?.supply_chain?.package_inventory_evidenced &&
payload.supply_chain
.forbidden_github_supply_chain_asset_count === 0
? "healthy"
: "critical"
}
/>
<TruthCell
label={copy.imagePolicy}
value={
!payload?.supply_chain?.package_inventory_evidenced
? copy.notEvidenced
: payload.supply_chain.runtime_image_policy_compliant
? copy.compliant
: copy.repair
}
icon={
payload?.supply_chain?.runtime_image_policy_compliant
? ShieldCheck
: TriangleAlert
}
tone={
payload?.supply_chain?.runtime_image_policy_compliant
? "healthy"
: "critical"
}
/>
</div>
</div>
) : null}
{payload ? (
<div className="grid gap-4 p-4 xl:grid-cols-[1.1fr_1.4fr]">
<div className="min-w-0 space-y-4">
<div className="p-4">
{view === "command" ? (
<div
className="grid gap-3 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1.6fr)]"
data-testid="security-command-attention"
>
<div
className={cn(
"min-w-0 border p-3",
payload.discovery.latest_status === "success"
? "border-[#9bc7a4] bg-[#f0faf2]"
: "border-[#e2a29b] bg-[#fff2f0]",
)}
>
<div className="flex items-center gap-2 text-[11px] font-semibold text-[#4f4d47]">
{payload.discovery.latest_status === "success" ? (
<CheckCircle2
className="h-4 w-4 text-[#2f8a50]"
aria-hidden="true"
/>
) : (
<TriangleAlert
className="h-4 w-4 text-[#c9584d]"
aria-hidden="true"
/>
)}
{copy.failedCollectors}
<span className="ml-auto font-mono text-xs text-[#141413]">
{payload.discovery.failed_collector_count}
</span>
</div>
<div className="mt-2 flex min-h-6 flex-wrap gap-1.5">
{payload.discovery.failed_collector_ids.length > 0 ? (
payload.discovery.failed_collector_ids.map(
(collectorId) => (
<span
key={collectorId}
className="max-w-full truncate border border-[#e2a29b] bg-white px-2 py-1 font-mono text-[10px] text-[#9f2f25]"
title={collectorId}
>
{collectorId}
</span>
),
)
) : (
<span className="text-[10px] text-[#6f6d66]">
{payload.discovery.latest_status === "success"
? copy.success
: copy.failedCollectorsUnavailable}
</span>
)}
</div>
</div>
<div className="min-w-0 border border-[#d8d3c7] bg-white p-3">
<div className="mb-2 flex items-center justify-between gap-2 text-[11px] font-semibold text-[#4f4d47]">
<span>{copy.priority}</span>
<span className="font-mono text-[#77736a]">
{priorityItems.length}
</span>
</div>
<div className="grid gap-1.5 sm:grid-cols-3">
{priorityItems.map((item) => (
<div
key={item.work_item_id}
className="flex min-w-0 items-center gap-2 border border-[#ebe8df] px-2 py-2"
>
<span className="border border-[#e2a29b] bg-[#fff2f0] px-1.5 py-0.5 font-mono text-[9px] font-semibold text-[#9f2f25]">
{item.priority}
</span>
<span
className="min-w-0 flex-1 truncate text-[10px] font-medium text-[#34332f]"
title={item.title}
>
{item.title}
</span>
<span className="font-mono text-[10px] text-[#77736a]">
{item.gap_count}
</span>
</div>
))}
</div>
</div>
</div>
) : null}
<div
className={cn("min-w-0 space-y-4", view !== "assets" && "hidden")}
>
<div>
<div className="mb-2 text-[11px] font-semibold text-[#4f4d47]">{copy.functions}</div>
<div className="mb-2 text-[11px] font-semibold text-[#4f4d47]">
{copy.functions}
</div>
<div className="grid grid-cols-3 gap-1 sm:grid-cols-6">
{payload.nist_csf_functions.map((item) => (
<div key={item.function_id} className="border border-[#d8d3c7] bg-white px-2 py-2">
<div className="truncate text-[10px] text-[#6f6d66]">{item.label}</div>
<div
key={item.function_id}
className="border border-[#d8d3c7] bg-white px-2 py-2"
>
<div className="truncate text-[10px] text-[#6f6d66]">
{item.label}
</div>
<div className="mt-1 font-mono text-sm font-semibold text-[#141413]">
{item.controlled_percent}%
</div>
@@ -605,9 +818,11 @@ export function SecurityAssetControlPlaneCockpit({
? "bg-[#2f8a50]"
: item.controlled_percent > 0
? "bg-[#c58a2e]"
: "bg-[#c9584d]"
: "bg-[#c9584d]",
)}
style={{ width: `${Math.min(100, Math.max(0, item.controlled_percent))}%` }}
style={{
width: `${Math.min(100, Math.max(0, item.controlled_percent))}%`,
}}
/>
</div>
</div>
@@ -638,7 +853,7 @@ export function SecurityAssetControlPlaneCockpit({
? "bg-[#2f8a50]"
: domain.status === "partial"
? "bg-[#c58a2e]"
: "bg-[#c9584d]"
: "bg-[#c9584d]",
)}
/>
<span className="min-w-0 flex-1 truncate text-[9px] text-[#4f4d47]">
@@ -657,13 +872,19 @@ export function SecurityAssetControlPlaneCockpit({
<div className="mb-2 flex items-center justify-between gap-3 text-[11px] font-semibold text-[#4f4d47]">
<span>{copy.scope}</span>
<span className="font-mono text-[#77736a]">
{payload.summary.controlled_scope_count}/{payload.summary.scope_count}
{payload.summary.controlled_scope_count}/
{payload.summary.scope_count}
</span>
</div>
<div className="grid gap-1 sm:grid-cols-2">
{payload.asset_scopes.map((scope) => (
<div key={scope.scope_id} className="flex items-center gap-2 border border-[#d8d3c7] bg-white px-2 py-2">
<span className="min-w-0 flex-1 truncate text-[10px] text-[#4f4d47]">{scope.label}</span>
<div
key={scope.scope_id}
className="flex items-center gap-2 border border-[#d8d3c7] bg-white px-2 py-2"
>
<span className="min-w-0 flex-1 truncate text-[10px] text-[#4f4d47]">
{scope.label}
</span>
<span className="font-mono text-[11px] font-semibold text-[#141413]">
{scope.managed_asset_count}
</span>
@@ -674,7 +895,7 @@ export function SecurityAssetControlPlaneCockpit({
? "bg-[#2f8a50]"
: scope.status === "partial"
? "bg-[#c58a2e]"
: "bg-[#c9584d]"
: "bg-[#c9584d]",
)}
title={`${scope.type_coverage_percent}%`}
/>
@@ -684,13 +905,23 @@ export function SecurityAssetControlPlaneCockpit({
</div>
</div>
<div className="min-w-0 space-y-4">
<div
className={cn(
"min-w-0 space-y-4",
view !== "automation" && "hidden",
)}
>
<StageStrip title={copy.siemFlow} stages={payload.siem.pipeline} />
<StageStrip title={copy.aiFlow} stages={payload.ai_automation.stages} />
<StageStrip
title={copy.aiFlow}
stages={payload.ai_automation.stages}
/>
<div className="grid gap-3 lg:grid-cols-[1fr_1.35fr]">
<div className="border border-[#d8d3c7] bg-white p-3">
<div className="mb-2 text-[11px] font-semibold text-[#4f4d47]">{copy.audit}</div>
<div className="mb-2 text-[11px] font-semibold text-[#4f4d47]">
{copy.audit}
</div>
<div className="grid grid-cols-4 gap-2 text-center">
<div>
<div className="font-mono text-sm font-semibold text-[#141413]">
@@ -708,13 +939,18 @@ export function SecurityAssetControlPlaneCockpit({
<div className="font-mono text-sm font-semibold text-[#141413]">
{payload.security_audit.ai_trace_count_24h}
</div>
<div className="mt-1 text-[9px] text-[#77736a]">{copy.aiTrace}</div>
<div className="mt-1 text-[9px] text-[#77736a]">
{copy.aiTrace}
</div>
</div>
<div>
<div className="font-mono text-sm font-semibold text-[#141413]">
{payload.summary.ready_source_count}/{payload.summary.source_count}
{payload.summary.ready_source_count}/
{payload.summary.source_count}
</div>
<div className="mt-1 text-[9px] text-[#77736a]">
{copy.sources}
</div>
<div className="mt-1 text-[9px] text-[#77736a]">{copy.sources}</div>
</div>
</div>
{payload.summary.unavailable_source_count > 0 ? (
@@ -728,32 +964,50 @@ export function SecurityAssetControlPlaneCockpit({
className="inline-flex min-w-0 items-center gap-1 border border-[#e2a29b] bg-[#fff2f0] px-1.5 py-0.5 font-mono text-[9px] text-[#9f2f25]"
title={source.reason_code ?? source.source_id}
>
<Database className="h-3 w-3 shrink-0" aria-hidden="true" />
<span className="max-w-32 truncate">{source.source_id}</span>
<Database
className="h-3 w-3 shrink-0"
aria-hidden="true"
/>
<span className="max-w-32 truncate">
{source.source_id}
</span>
</span>
))}
</div>
) : null}
{!payload.ai_automation.same_run_closed_loop_proven ? (
<div className="mt-3 flex items-center gap-1.5 border-t border-[#ebe8df] pt-2 text-[10px] font-medium text-[#9f2f25]">
<TriangleAlert className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
<TriangleAlert
className="h-3.5 w-3.5 shrink-0"
aria-hidden="true"
/>
{copy.sameRunMissing}
</div>
) : null}
</div>
<div className="border border-[#d8d3c7] bg-white p-3">
<div className="mb-2 text-[11px] font-semibold text-[#4f4d47]">{copy.priority}</div>
<div className="mb-2 text-[11px] font-semibold text-[#4f4d47]">
{copy.priority}
</div>
<div className="space-y-1.5">
{priorityItems.map((item) => (
<div key={item.work_item_id} className="flex min-w-0 items-center gap-2">
<div
key={item.work_item_id}
className="flex min-w-0 items-center gap-2"
>
<span className="border border-[#e2a29b] bg-[#fff2f0] px-1.5 py-0.5 font-mono text-[9px] font-semibold text-[#9f2f25]">
{item.priority}
</span>
<span className="min-w-0 flex-1 truncate text-[10px] font-medium text-[#34332f]" title={item.title}>
<span
className="min-w-0 flex-1 truncate text-[10px] font-medium text-[#34332f]"
title={item.title}
>
{item.title}
</span>
<span className="font-mono text-[10px] text-[#77736a]">{item.gap_count}</span>
<span className="font-mono text-[10px] text-[#77736a]">
{item.gap_count}
</span>
</div>
))}
</div>