From 530270c339ec59beddbbc9b376d31448fa8f7cb6 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 1 Jul 2026 23:12:09 +0800 Subject: [PATCH] fix(ui): make autonomous control cockpit visual --- apps/web/messages/en.json | 39 + apps/web/messages/zh-TW.json | 39 + .../tabs/automation-inventory-tab.tsx | 692 +++++++++++++++--- apps/web/src/lib/api-client.ts | 5 + 4 files changed, 663 insertions(+), 112 deletions(-) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 3b70cdddd..9a3d7bca5 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -3852,6 +3852,45 @@ "overrideTitle": "舊規範覆寫", "hardBlockerTitle": "仍維持硬阻擋", "hardBlockerDetail": "需 break-glass 或專案級合約,不由一般自動化靜默執行。", + "cockpit": { + "liveLabel": "LIVE PRODUCTION", + "title": "AI Agent Cockpit", + "subtitle": "Production readback for the active control layer, closed loop, receipts, and hard boundaries.", + "production": "Production", + "dbOk": "DB readback OK", + "dbReview": "DB readback review", + "workItems": "Work items", + "consumer": "LOG consumer", + "apply": "Apply", + "verifier": "Verifier", + "km": "KM", + "telegram": "Telegram", + "receiptDetail": "controlled runtime receipt", + "verifierDetail": "post-apply verifier", + "kmDetail": "KM / PlayBook writeback", + "telegramDetail": "Gateway receipt", + "loopTitle": "Runtime loop", + "closed": "closed", + "review": "review", + "missing": "missing", + "runtimeWrite": "runtime write", + "noRuntimeWrite": "no runtime write", + "riskTitle": "Controlled risk lanes", + "on": "ON", + "off": "OFF", + "ownerRequired": "owner review required", + "ownerNotRequired": "owner review not required", + "criticalBreakGlass": "critical break-glass", + "criticalReview": "critical review" + }, + "flow": { + "candidate": "Candidate", + "checkMode": "Check-mode", + "apply": "Controlled apply", + "verifier": "Verifier", + "learning": "KM learning", + "telegram": "Telegram" + }, "readback": { "marker": "Production v2 marker", "markerDetail": "{task} · {status}", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 2de74a28a..c5c29f88f 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -3852,6 +3852,45 @@ "overrideTitle": "舊規範覆寫", "hardBlockerTitle": "仍維持硬阻擋", "hardBlockerDetail": "需 break-glass 或專案級合約,不由一般自動化靜默執行。", + "cockpit": { + "liveLabel": "LIVE PRODUCTION", + "title": "AI Agent Cockpit", + "subtitle": "用 production readback 呈現控制層、閉環、收據與硬邊界。", + "production": "Production", + "dbOk": "DB readback OK", + "dbReview": "DB readback review", + "workItems": "Work items", + "consumer": "LOG consumer", + "apply": "Apply", + "verifier": "Verifier", + "km": "KM", + "telegram": "Telegram", + "receiptDetail": "controlled runtime receipt", + "verifierDetail": "post-apply verifier", + "kmDetail": "KM / PlayBook writeback", + "telegramDetail": "Gateway receipt", + "loopTitle": "Runtime loop", + "closed": "closed", + "review": "review", + "missing": "missing", + "runtimeWrite": "runtime write", + "noRuntimeWrite": "no runtime write", + "riskTitle": "Controlled risk lanes", + "on": "ON", + "off": "OFF", + "ownerRequired": "owner review required", + "ownerNotRequired": "owner review not required", + "criticalBreakGlass": "critical break-glass", + "criticalReview": "critical review" + }, + "flow": { + "candidate": "Candidate", + "checkMode": "Check-mode", + "apply": "Controlled apply", + "verifier": "Verifier", + "learning": "KM learning", + "telegram": "Telegram" + }, "readback": { "marker": "Production v2 marker", "markerDetail": "{task} · {status}", diff --git a/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx b/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx index ec5648f31..ac99bd6c2 100644 --- a/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx +++ b/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx @@ -853,6 +853,237 @@ function GateMatrixRow({ ) } +function AutonomyCockpitMetric({ + label, + value, + detail, + tone = 'neutral', + icon, +}: { + label: string + value: number | string + detail: string + tone?: 'ok' | 'warn' | 'danger' | 'neutral' + icon: ReactNode +}) { + const color = toneColor(tone) + return ( +
+
+ {icon} +
+
+ {label} + + {value} + + + {redactPublicText(detail)} + +
+
+ ) +} + +function AutonomyFlowStep({ + label, + status, + detail, + tone = 'neutral', + icon, +}: { + label: string + status: string + detail: string + tone?: 'ok' | 'warn' | 'danger' | 'neutral' + icon: ReactNode +}) { + const color = toneColor(tone) + return ( +
+
+
+ {icon} +
+ + {redactPublicText(status)} + +
+ + {redactPublicText(label)} + +
+
+
+ + {redactPublicText(detail)} + +
+ ) +} + +function autonomyStageTone(stage: { present: boolean; status: string } | undefined, loopClosed: boolean): 'ok' | 'warn' | 'danger' | 'neutral' { + if (!stage?.present) return 'danger' + if (['success', 'sent', 'REVIEW', 'inferred_from_check_mode'].includes(stage.status)) return 'ok' + if (stage.status === 'failed' && loopClosed) return 'warn' + if (stage.status === 'failed') return 'danger' + return 'neutral' +} + +function AutonomyWorkspaceScene({ + stations, + completion, + status, +}: { + stations: Array<{ + key: string + label: string + value: number | string + tone: 'ok' | 'warn' | 'danger' | 'neutral' + icon: ReactNode + }> + completion: number + status: string +}) { + const safeCompletion = Math.max(0, Math.min(100, completion)) + return ( +
+ + ) +} + function AutonomousRuntimeControlReadbackGrid({ control, t, @@ -5782,6 +6013,99 @@ export function AutomationInventoryTab() { const currentAutonomyHardBlockers = autonomousRuntimeControl?.hard_blockers ?? [] const currentAutonomyPolicy = autonomousRuntimeControl?.current_policy const currentAutonomySwitches = autonomousRuntimeControl?.runtime_switches + const currentAutonomyRuntimeReadback = autonomousRuntimeControl?.runtime_receipt_readback + const currentAutonomyLoopLedger = currentAutonomyRuntimeReadback?.autonomous_execution_loop_ledger + const currentAutonomyLoopClosed = currentAutonomyLoopLedger?.closed === true + const currentAutonomyStageById = new Map((currentAutonomyLoopLedger?.stages ?? []).map(stage => [stage.stage_id, stage])) + const currentAutonomyFlowStages = [ + { + key: 'candidate', + label: t('globalControl.currentAutonomy.flow.candidate'), + stage: currentAutonomyStageById.get('candidate'), + icon: , + }, + { + key: 'checkMode', + label: t('globalControl.currentAutonomy.flow.checkMode'), + stage: currentAutonomyStageById.get('check_mode'), + icon: , + }, + { + key: 'apply', + label: t('globalControl.currentAutonomy.flow.apply'), + stage: currentAutonomyStageById.get('controlled_apply'), + icon: , + }, + { + key: 'verifier', + label: t('globalControl.currentAutonomy.flow.verifier'), + stage: currentAutonomyStageById.get('post_apply_verifier'), + icon: , + }, + { + key: 'learning', + label: t('globalControl.currentAutonomy.flow.learning'), + stage: currentAutonomyStageById.get('km_playbook_writeback'), + icon: , + }, + { + key: 'telegram', + label: t('globalControl.currentAutonomy.flow.telegram'), + stage: currentAutonomyStageById.get('telegram_receipt'), + icon: , + }, + ] + const currentAutonomyReceiptStats = [ + { + key: 'apply', + label: t('globalControl.currentAutonomy.cockpit.apply'), + value: autonomousRuntimeControl?.rollups.live_ansible_apply_executed_count ?? 0, + detail: t('globalControl.currentAutonomy.cockpit.receiptDetail'), + tone: (autonomousRuntimeControl?.rollups.live_ansible_apply_executed_count ?? 0) > 0 ? 'ok' as const : 'warn' as const, + icon: , + }, + { + key: 'verifier', + label: t('globalControl.currentAutonomy.cockpit.verifier'), + value: autonomousRuntimeControl?.rollups.live_post_apply_verifier_count ?? 0, + detail: t('globalControl.currentAutonomy.cockpit.verifierDetail'), + tone: (autonomousRuntimeControl?.rollups.live_post_apply_verifier_count ?? 0) > 0 ? 'ok' as const : 'warn' as const, + icon: , + }, + { + key: 'km', + label: t('globalControl.currentAutonomy.cockpit.km'), + value: autonomousRuntimeControl?.rollups.live_km_writeback_count ?? 0, + detail: t('globalControl.currentAutonomy.cockpit.kmDetail'), + tone: (autonomousRuntimeControl?.rollups.live_km_writeback_count ?? 0) > 0 ? 'ok' as const : 'warn' as const, + icon: , + }, + { + key: 'telegram', + label: t('globalControl.currentAutonomy.cockpit.telegram'), + value: autonomousRuntimeControl?.rollups.live_telegram_receipt_count ?? 0, + detail: t('globalControl.currentAutonomy.cockpit.telegramDetail'), + tone: (autonomousRuntimeControl?.rollups.live_telegram_receipt_count ?? 0) > 0 ? 'ok' as const : 'warn' as const, + icon: , + }, + ] + const currentAutonomyRiskRows = [ + { + key: 'low', + label: t('globalControl.currentAutonomy.policy.low'), + enabled: currentAutonomyPolicy?.low_risk_controlled_apply_allowed === true, + }, + { + key: 'medium', + label: t('globalControl.currentAutonomy.policy.medium'), + enabled: currentAutonomyPolicy?.medium_risk_controlled_apply_allowed === true, + }, + { + key: 'high', + label: t('globalControl.currentAutonomy.policy.high'), + enabled: currentAutonomyPolicy?.high_risk_controlled_apply_allowed === true, + }, + ] const globalControlRunwayRows: Array<{ key: string label: string @@ -6592,9 +6916,19 @@ export function AutomationInventoryTab() { {autonomousRuntimeControl ? ( -
-
-
+
+
+
- - {t('globalControl.currentAutonomy.title')} + {t('globalControl.currentAutonomy.cockpit.liveLabel')} + + {t('globalControl.currentAutonomy.cockpit.title')} - - {autonomousRuntimeControl.program_status.status_note} + + {t('globalControl.currentAutonomy.cockpit.subtitle')}
-
+
- + - +
- + -
- } /> - } /> - } /> - } /> - } /> - } /> +
+
+
+ {t('globalControl.currentAutonomy.metrics.completion')} + +
+
+ + {autonomousRuntimeControl.program_status.implementation_completion_percent}% + +
+
+
+
+ + {autonomousRuntimeControl.program_status.deploy_readback_marker} + +
+
+
+ + + +
+
+ +
+ {currentAutonomyReceiptStats.map(stat => ( + + ))} +
-
-
+
+
+ {t('globalControl.currentAutonomy.cockpit.loopTitle')} + +
+
+ {currentAutonomyFlowStages.map(row => ( + + ))} +
+
+ +
+
- {t('globalControl.currentAutonomy.policyTitle')} - + {t('globalControl.currentAutonomy.cockpit.riskTitle')} +
-
- - - -
-
- {currentAutonomyCadences.map(cadence => ( -
-
- - {cadence.display_name} - - -
- - {cadence.schedule} - - - {cadence.worker} +
+ {currentAutonomyRiskRows.map(row => ( +
+ {row.label} + + {row.enabled ? t('globalControl.currentAutonomy.cockpit.on') : t('globalControl.currentAutonomy.cockpit.off')}
))}
+
+ + + +
-
- {t('globalControl.currentAutonomy.runtimeTitle')} - - - -
-
- -
-
- {t('globalControl.currentAutonomy.executorTitle')} - {currentAutonomyExecutorReceipts.slice(0, 5).map(receipt => ( - +
+ {t('globalControl.currentAutonomy.runtimeTitle')} + +
+
+ } /> - ))} -
-
- {t('globalControl.currentAutonomy.overrideTitle')} - {currentAutonomyOverrides.slice(0, 4).map(row => ( - } /> - ))} -
-
- {t('globalControl.currentAutonomy.hardBlockerTitle')} - {currentAutonomyHardBlockers.slice(0, 5).map(blocker => ( - cadence.cadence).join(' / ') || '--'} + tone={currentAutonomyCadences.length > 0 ? 'ok' : 'warn'} + icon={} /> - ))} +
+
+ + +
@@ -20187,7 +20526,117 @@ export function AutomationInventoryTab() { white-space: normal !important; } + .automation-inventory-autonomy-workspace-rail { + position: absolute; + inset: 28px; + border: 0.5px solid rgba(21, 128, 61, 0.18); + border-radius: 8px; + background: + linear-gradient(90deg, rgba(21, 128, 61, 0.08) 1px, transparent 1px), + linear-gradient(0deg, rgba(21, 128, 61, 0.06) 1px, transparent 1px); + background-size: 44px 44px; + opacity: 0.88; + } + + .automation-inventory-autonomy-workspace-packet { + position: absolute; + z-index: 1; + width: 10px; + height: 10px; + border-radius: 3px; + background: #15803d; + box-shadow: 0 0 0 4px rgba(21, 128, 61, 0.12), 0 8px 18px rgba(21, 128, 61, 0.22); + } + + .automation-inventory-autonomy-workspace-packet-a { + top: 48%; + left: 10%; + animation: autonomy-packet-x 4.8s linear infinite; + } + + .automation-inventory-autonomy-workspace-packet-b { + top: 22%; + left: 48%; + animation: autonomy-packet-y 5.4s linear infinite; + animation-delay: 0.8s; + } + + .automation-inventory-autonomy-workspace-packet-c { + top: 72%; + left: 16%; + animation: autonomy-packet-x 6.2s linear infinite; + animation-delay: 1.6s; + } + + .automation-inventory-autonomy-workspace-station { + animation: autonomy-station-breathe 3.8s ease-in-out infinite; + } + + .automation-inventory-autonomy-workspace-station-1 { + animation-delay: 0.25s; + } + + .automation-inventory-autonomy-workspace-station-2 { + animation-delay: 0.5s; + } + + .automation-inventory-autonomy-workspace-station-3 { + animation-delay: 0.75s; + } + + @keyframes autonomy-packet-x { + 0% { + left: 10%; + opacity: 0; + } + 12% { + opacity: 1; + } + 88% { + opacity: 1; + } + 100% { + left: 88%; + opacity: 0; + } + } + + @keyframes autonomy-packet-y { + 0% { + top: 18%; + opacity: 0; + } + 12% { + opacity: 1; + } + 88% { + opacity: 1; + } + 100% { + top: 78%; + opacity: 0; + } + } + + @keyframes autonomy-station-breathe { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-2px); + } + } + @media (max-width: 900px) { + .automation-inventory-autonomy-cockpit-header, + .automation-inventory-autonomy-cockpit-hero-grid, + .automation-inventory-autonomy-cockpit-score-grid, + .automation-inventory-autonomy-cockpit-metric-grid, + .automation-inventory-autonomy-cockpit-flow-grid, + .automation-inventory-autonomy-cockpit-control-grid, + .automation-inventory-autonomy-cockpit-risk-grid, + .automation-inventory-autonomy-cockpit-runtime-grid, + .automation-inventory-autonomy-workspace-grid, .automation-inventory-visual-grid, .automation-inventory-stage-grid, .automation-inventory-visual-factor-grid, @@ -20269,6 +20718,25 @@ export function AutomationInventoryTab() { overflow-x: hidden; } + .automation-inventory-autonomy-workspace-scene { + min-height: 430px !important; + } + + .automation-inventory-autonomy-workspace-console { + position: relative !important; + inset: auto !important; + transform: none !important; + width: 100% !important; + height: auto !important; + min-height: 104px; + margin-bottom: 10px; + } + + .automation-inventory-autonomy-workspace-rail, + .automation-inventory-autonomy-workspace-packet { + display: none; + } + .automation-inventory-tab-root button, .automation-inventory-tab-root [role='button'], .automation-inventory-tab-root span, diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index 53e396603..76896665f 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -2213,6 +2213,11 @@ export interface AiAgentAutonomousRuntimeControlSnapshot { live_km_writeback_count?: number live_post_apply_verifier_count?: number live_telegram_receipt_count?: number + live_log_controlled_writeback_consumer_apply_receipt_count?: number + live_log_controlled_writeback_consumer_dispatch_ledger_count?: number + live_log_controlled_writeback_runtime_target_write_count?: number + live_work_item_completed_count?: number + live_work_item_blocked_count?: number mcp_sensor_count?: number rag_context_query_count?: number playbook_decision_class_count?: number