From ec498e457d1dfe776a5ec75732a7e6ba81044003 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 1 Jul 2026 23:07:32 +0800 Subject: [PATCH] feat(web): surface AI automation production proof --- apps/web/messages/en.json | 18 ++- apps/web/messages/zh-TW.json | 18 ++- .../autonomous-runtime-receipt-panel.tsx | 127 +++++++++++++++++- 3 files changed, 158 insertions(+), 5 deletions(-) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 9a3d7bca..4ef528b9 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -3878,8 +3878,8 @@ "riskTitle": "Controlled risk lanes", "on": "ON", "off": "OFF", - "ownerRequired": "owner review required", - "ownerNotRequired": "owner review not required", + "ownerRequired": "Controlled evidence review required", + "ownerNotRequired": "Controlled evidence review cleared", "criticalBreakGlass": "critical break-glass", "criticalReview": "critical review" }, @@ -11585,6 +11585,20 @@ "playbook": "PlayBook", "telegram": "Telegram" }, + "proof": { + "deploy": "Production deploy", + "deployDetail": "CD / readback: {status}", + "runtime": "Runtime DB", + "runtimeDetail": "marker: {marker}", + "workItems": "Work complete", + "workItemsDetail": "{percent}% complete", + "sources": "Log sources", + "sourcesDetail": "Project, product, site, service, package, and tool", + "events": "Classified events", + "eventsDetail": "24h {recent}", + "ok": "ok", + "degraded": "degraded" + }, "recent": "24h {count}", "missing": "{count} missing", "closedDetail": "required stages ok", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index c5c29f88..e28dce17 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -3878,8 +3878,8 @@ "riskTitle": "Controlled risk lanes", "on": "ON", "off": "OFF", - "ownerRequired": "owner review required", - "ownerNotRequired": "owner review not required", + "ownerRequired": "受控證據複核需要補齊", + "ownerNotRequired": "受控證據複核已免除", "criticalBreakGlass": "critical break-glass", "criticalReview": "critical review" }, @@ -11585,6 +11585,20 @@ "playbook": "PlayBook", "telegram": "Telegram" }, + "proof": { + "deploy": "正式部署", + "deployDetail": "CD / readback:{status}", + "runtime": "Runtime DB", + "runtimeDetail": "marker:{marker}", + "workItems": "工作完成", + "workItemsDetail": "完成度 {percent}%", + "sources": "Log 來源", + "sourcesDetail": "專案 / 產品 / 網站 / 服務 / 套件 / 工具", + "events": "分類事件", + "eventsDetail": "近 24h {recent}", + "ok": "ok", + "degraded": "degraded" + }, "recent": "近 24h {count}", "missing": "缺 {count} 節點", "closedDetail": "required stages ok", diff --git a/apps/web/src/components/awooop/autonomous-runtime-receipt-panel.tsx b/apps/web/src/components/awooop/autonomous-runtime-receipt-panel.tsx index ca378484..26578f99 100644 --- a/apps/web/src/components/awooop/autonomous-runtime-receipt-panel.tsx +++ b/apps/web/src/components/awooop/autonomous-runtime-receipt-panel.tsx @@ -9,9 +9,11 @@ import { Bot, BookOpenCheck, CheckCircle2, + Database, Gauge, ListChecks, RefreshCw, + Rocket, Send, ShieldCheck, SlidersHorizontal, @@ -182,6 +184,22 @@ type RuntimeControlPayload = { rollups?: Record | null; }; +type PriorityWorkOrderPayload = { + status?: string | null; + summary?: { + latest_successful_deploy_marker?: string | null; + latest_successful_deployed_source_sha?: string | null; + latest_successful_deployed_source_short_sha?: string | null; + ai_loop_current_blocker_deploy_marker_readback_required?: boolean | null; + ai_loop_current_blocker_deploy_marker_resolved_by_production_readback?: boolean | null; + } | null; + mainline_execution_state?: { + current_main_cd_run_id?: string | null; + current_main_cd_run_status?: string | null; + current_main_latest_source_sha?: string | null; + } | null; +}; + type PanelMode = "full" | "compact"; type Tone = "ok" | "warn" | "neutral"; type WorkFilter = "all" | "completed" | "active" | "pending" | "blocked"; @@ -262,6 +280,23 @@ async function fetchRuntimeControl(): Promise { } } +async function fetchPriorityWorkOrder(): Promise { + const controller = new AbortController(); + const timeout = window.setTimeout(() => controller.abort(), 12_000); + try { + const response = await fetch(`${API_BASE}/api/v1/agents/awoooi-priority-work-order-readback`, { + cache: "no-store", + signal: controller.signal, + }); + if (!response.ok) return null; + return (await response.json()) as PriorityWorkOrderPayload; + } catch { + return null; + } finally { + window.clearTimeout(timeout); + } +} + export function AutonomousRuntimeReceiptPanel({ mode = "full", }: { @@ -270,6 +305,7 @@ export function AutonomousRuntimeReceiptPanel({ const t = useTranslations("awooop.autonomousRuntime"); const locale = useLocale(); const [payload, setPayload] = useState(null); + const [priorityPayload, setPriorityPayload] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); const [updatedAt, setUpdatedAt] = useState(null); @@ -277,8 +313,12 @@ export function AutonomousRuntimeReceiptPanel({ const refresh = useCallback(async () => { setLoading(true); - const next = await fetchRuntimeControl(); + const [next, priority] = await Promise.all([ + fetchRuntimeControl(), + fetchPriorityWorkOrder(), + ]); setPayload(next); + setPriorityPayload(priority); setError(next === null); setUpdatedAt(next ? new Date() : null); setLoading(false); @@ -510,6 +550,64 @@ export function AutonomousRuntimeReceiptPanel({ const workCompletionPercent = workTotal > 0 ? Math.min(100, Math.round((workCompleted / workTotal) * 100)) : 0; + const prioritySummary = priorityPayload?.summary; + const mainlineState = priorityPayload?.mainline_execution_state; + const deployedSourceSha = prioritySummary?.latest_successful_deployed_source_sha + ?? mainlineState?.current_main_latest_source_sha + ?? null; + const deployedSourceShortSha = prioritySummary?.latest_successful_deployed_source_short_sha + ?? (deployedSourceSha ? deployedSourceSha.slice(0, 10) : null); + const deployReadbackResolved = prioritySummary?.ai_loop_current_blocker_deploy_marker_resolved_by_production_readback === true; + const proofCards = [ + { + key: "deploy", + label: t("proof.deploy"), + value: deployedSourceShortSha ?? "--", + detail: t("proof.deployDetail", { + status: mainlineState?.current_main_cd_run_status ?? priorityPayload?.status ?? "--", + }), + icon: Rocket, + tone: deployReadbackResolved ? "ok" as Tone : "warn" as Tone, + }, + { + key: "runtime", + label: t("proof.runtime"), + value: dbOk ? t("proof.ok") : t("proof.degraded"), + detail: t("proof.runtimeDetail", { + marker: shortRef(payload?.program_status?.deploy_readback_marker), + }), + icon: Database, + tone: dbOk ? "ok" as Tone : "warn" as Tone, + }, + { + key: "work", + label: t("proof.workItems"), + value: `${numberValue(workCompleted)}/${numberValue(workTotal)}`, + detail: t("proof.workItemsDetail", { + percent: numberValue(workCompletionPercent), + }), + icon: ListChecks, + tone: workTotal > 0 && workCompleted === workTotal ? "ok" as Tone : "warn" as Tone, + }, + { + key: "sources", + label: t("proof.sources"), + value: `${numberValue(rollups.live_log_active_source_family_count ?? logRollups.active_source_family_count)}/${numberValue(rollups.live_log_source_family_count ?? logRollups.source_family_count)}`, + detail: t("proof.sourcesDetail"), + icon: Bot, + tone: toNumber(rollups.live_log_active_source_family_count ?? logRollups.active_source_family_count) > 0 ? "ok" as Tone : "warn" as Tone, + }, + { + key: "events", + label: t("proof.events"), + value: numberValue(rollups.live_log_classified_event_total ?? logRollups.classified_event_total), + detail: t("proof.eventsDetail", { + recent: numberValue(rollups.live_log_recent_classified_event_total ?? logRollups.recent_classified_event_total), + }), + icon: Activity, + tone: toNumber(rollups.live_log_classified_event_total ?? logRollups.classified_event_total) > 0 ? "ok" as Tone : "neutral" as Tone, + }, + ]; const visibleWorkItems = orderedWorkItems.filter((item) => matchesWorkFilter(item, workFilter)); const orderedCompleted = orderedWorkItems.filter((item) => item.status === "completed").length; const orderedActive = orderedWorkItems.filter((item) => ( @@ -574,6 +672,33 @@ export function AutonomousRuntimeReceiptPanel({ +
+ {proofCards.map((card) => { + const Icon = card.icon; + return ( +
+
+
+

{card.label}

+

+ {card.value} +

+
+ + +
+

+ {card.detail} +

+
+ ); + })} +
+

{t("metrics.loop")}