From ef1c4dbf55b649f18093697db1a7ef4921741b9b Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 2 Jul 2026 01:05:50 +0800 Subject: [PATCH] feat(web): surface ai loop writeback receipts --- apps/web/messages/en.json | 35 +++ apps/web/messages/zh-TW.json | 35 +++ .../src/app/[locale]/awooop/alerts/page.tsx | 88 +++++- .../autonomous-runtime-receipt-panel.tsx | 252 +++++++++++++++++- 4 files changed, 405 insertions(+), 5 deletions(-) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index b9630a8d..8cddd017 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -11611,9 +11611,34 @@ "sourcesDetail": "Project, product, site, service, package, and tool", "events": "Classified events", "eventsDetail": "24h {recent}", + "consumer": "Consumer writeback", + "consumerDetail": "{targets} targets ready", "ok": "ok", "degraded": "degraded" }, + "writeback": { + "dispatch": "Dispatch ledger", + "dispatchDetail": "metadata-only LOG controlled receipts", + "apply": "Consumer apply", + "applyDetail": "runtime target context receipt readback", + "bindings": "Consumer binding", + "bindingsDetail": "ready / total", + "targets": "Ready targets", + "targetsDetail": "KM / RAG / PlayBook / MCP / Verifier / AI Agent", + "contextWrites": "Context receipts", + "contextWritesDetail": "target writeback receipts", + "blockers": "Active blockers", + "noBlockers": "No active blocker", + "targetDetail": "context writes / bindings", + "targetsMap": { + "km": "KM", + "rag": "RAG", + "playbook": "PlayBook", + "mcp": "MCP", + "verifier": "Verifier", + "aiAgent": "AI Agent" + } + }, "recent": "24h {count}", "missing": "{count} missing", "closedDetail": "required stages ok", @@ -11663,6 +11688,16 @@ } } }, + "alerts": { + "aiLoop": { + "title": "Alert AI Loop", + "subtitle": "Alert signals are aligned to LOG / KM / RAG / MCP / PlayBook / Verifier runtime receipts.", + "badge": "controlled automation", + "runs": "Runs", + "workItems": "Work Items", + "approvals": "Approvals" + } + }, "automationAssetLedger": { "column": "資產沉澱", "title": "資產沉澱", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 1dd07bd3..67bde74a 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -11611,9 +11611,34 @@ "sourcesDetail": "專案 / 產品 / 網站 / 服務 / 套件 / 工具", "events": "分類事件", "eventsDetail": "近 24h {recent}", + "consumer": "Consumer 回寫", + "consumerDetail": "{targets} 個 target ready", "ok": "ok", "degraded": "degraded" }, + "writeback": { + "dispatch": "Dispatch ledger", + "dispatchDetail": "metadata-only LOG controlled receipts", + "apply": "Consumer apply", + "applyDetail": "runtime target context receipt readback", + "bindings": "Consumer binding", + "bindingsDetail": "ready / total", + "targets": "Ready targets", + "targetsDetail": "KM / RAG / PlayBook / MCP / Verifier / AI Agent", + "contextWrites": "Context receipts", + "contextWritesDetail": "target writeback receipts", + "blockers": "Active blockers", + "noBlockers": "無 active blocker", + "targetDetail": "context writes / bindings", + "targetsMap": { + "km": "KM", + "rag": "RAG", + "playbook": "PlayBook", + "mcp": "MCP", + "verifier": "Verifier", + "aiAgent": "AI Agent" + } + }, "recent": "近 24h {count}", "missing": "缺 {count} 節點", "closedDetail": "required stages ok", @@ -11663,6 +11688,16 @@ } } }, + "alerts": { + "aiLoop": { + "title": "告警 AI Loop", + "subtitle": "告警訊號直接對齊 LOG / KM / RAG / MCP / PlayBook / Verifier 的 runtime receipt。", + "badge": "controlled automation", + "runs": "Runs", + "workItems": "Work Items", + "approvals": "Approvals" + } + }, "automationAssetLedger": { "column": "資產沉澱", "title": "資產沉澱", diff --git a/apps/web/src/app/[locale]/awooop/alerts/page.tsx b/apps/web/src/app/[locale]/awooop/alerts/page.tsx index 85498afe..ce1d1bcd 100644 --- a/apps/web/src/app/[locale]/awooop/alerts/page.tsx +++ b/apps/web/src/app/[locale]/awooop/alerts/page.tsx @@ -1,5 +1,87 @@ -import { redirect } from "next/navigation"; +"use client"; -export default function AwoooPAlertsPage({ params }: { params: { locale: string } }) { - redirect(`/${params.locale}/awooop/runs#ai-alert-card-delivery-readback`); +import { useTranslations } from "next-intl"; +import { + Activity, + ArrowRight, + BellRing, + CheckCircle2, + ListChecks, + ShieldCheck, +} from "lucide-react"; + +import { AutonomousRuntimeReceiptPanel } from "@/components/awooop/autonomous-runtime-receipt-panel"; +import { Link } from "@/i18n/routing"; + +const NAV_ITEMS = [ + { + href: "/awooop/runs#ai-alert-card-delivery-readback", + labelKey: "runs", + icon: Activity, + }, + { + href: "/awooop/work-items", + labelKey: "workItems", + icon: ListChecks, + }, + { + href: "/awooop/approvals", + labelKey: "approvals", + icon: ShieldCheck, + }, +] as const; + +export default function AwoooPAlertsPage() { + const t = useTranslations("awooop.alerts.aiLoop"); + + return ( +
+
+
+
+
+ + +
+
+

+ {t("title")} +

+ + +
+

+ {t("subtitle")} +

+
+
+ +
+
+ + +
+
+ ); } 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 26578f99..11b53e65 100644 --- a/apps/web/src/components/awooop/autonomous-runtime-receipt-panel.tsx +++ b/apps/web/src/components/awooop/autonomous-runtime-receipt-panel.tsx @@ -200,6 +200,53 @@ type PriorityWorkOrderPayload = { } | null; }; +type LogConsumerReadbackPayload = { + status?: string | null; + active_blockers?: string[] | null; + controlled_consume?: { + controlled_consume_allowed?: boolean | null; + runtime_target_write_performed?: boolean | null; + } | null; + target_rollups?: Array<{ + target?: string | null; + binding_count?: number | null; + ready_binding_count?: number | null; + target_write_performed?: boolean | null; + context_receipt_write_count?: number | null; + metadata_only?: boolean | null; + }> | null; + rollups?: { + dispatch_ledger_row_count?: number | null; + consumer_apply_receipt_row_count?: number | null; + consumer_binding_count?: number | null; + ready_consumer_binding_count?: number | null; + ready_target_count?: number | null; + metadata_only_receipt_count?: number | null; + post_apply_verifier_ref_count?: number | null; + target_context_receipt_write_count?: number | null; + controlled_consumer_readback_ready?: boolean | null; + runtime_target_write_performed?: boolean | null; + km_consumer_binding_count?: number | null; + rag_consumer_binding_count?: number | null; + playbook_consumer_binding_count?: number | null; + mcp_consumer_binding_count?: number | null; + verifier_consumer_binding_count?: number | null; + ai_agent_consumer_binding_count?: number | null; + km_context_receipt_write_count?: number | null; + rag_context_receipt_write_count?: number | null; + playbook_context_receipt_write_count?: number | null; + mcp_context_receipt_write_count?: number | null; + verifier_context_receipt_write_count?: number | null; + ai_agent_context_receipt_write_count?: number | null; + } | null; + operation_boundaries?: { + runtime_target_write_performed?: boolean | null; + raw_log_payload_persisted?: boolean | null; + secret_value_collection_allowed?: boolean | null; + github_api_used?: boolean | null; + } | null; +}; + type PanelMode = "full" | "compact"; type Tone = "ok" | "warn" | "neutral"; type WorkFilter = "all" | "completed" | "active" | "pending" | "blocked"; @@ -297,6 +344,23 @@ 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/agent-log-controlled-writeback-consumer-readback`, { + cache: "no-store", + signal: controller.signal, + }); + if (!response.ok) return null; + return (await response.json()) as LogConsumerReadbackPayload; + } catch { + return null; + } finally { + window.clearTimeout(timeout); + } +} + export function AutonomousRuntimeReceiptPanel({ mode = "full", }: { @@ -306,6 +370,7 @@ export function AutonomousRuntimeReceiptPanel({ const locale = useLocale(); const [payload, setPayload] = useState(null); const [priorityPayload, setPriorityPayload] = useState(null); + const [consumerPayload, setConsumerPayload] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); const [updatedAt, setUpdatedAt] = useState(null); @@ -313,12 +378,14 @@ export function AutonomousRuntimeReceiptPanel({ const refresh = useCallback(async () => { setLoading(true); - const [next, priority] = await Promise.all([ + const [next, priority, consumer] = await Promise.all([ fetchRuntimeControl(), fetchPriorityWorkOrder(), + fetchLogConsumerReadback(), ]); setPayload(next); setPriorityPayload(priority); + setConsumerPayload(consumer); setError(next === null); setUpdatedAt(next ? new Date() : null); setLoading(false); @@ -347,6 +414,14 @@ export function AutonomousRuntimeReceiptPanel({ const sourceFamilyItems = readback?.work_item_progress?.source_family_items ?? []; const latestFlow = readback?.latest_flow_closure; const rollups = payload?.rollups ?? {}; + const consumerRollups = consumerPayload?.rollups ?? {}; + const consumerBlockers = consumerPayload?.active_blockers ?? []; + const consumerReady = consumerRollups.controlled_consumer_readback_ready === true + || consumerPayload?.status === "controlled_writeback_consumer_readback_ready"; + const runtimeTargetWritePerformed = consumerRollups.runtime_target_write_performed === true + || consumerPayload?.controlled_consume?.runtime_target_write_performed === true + || consumerPayload?.operation_boundaries?.runtime_target_write_performed === true + || rollups.live_log_controlled_writeback_runtime_target_write_count === 1; const closed = ledger?.closed === true || latestFlow?.closed === true; const dbOk = readback?.db_read_status === "ok"; const missingStages = traceLedger?.missing_required_stage_ids @@ -607,6 +682,125 @@ export function AutonomousRuntimeReceiptPanel({ icon: Activity, tone: toNumber(rollups.live_log_classified_event_total ?? logRollups.classified_event_total) > 0 ? "ok" as Tone : "neutral" as Tone, }, + { + key: "consumer", + label: t("proof.consumer"), + value: `${numberValue( + rollups.live_log_controlled_writeback_consumer_apply_receipt_count + ?? consumerRollups.consumer_apply_receipt_row_count + )}/${numberValue( + rollups.live_log_controlled_writeback_consumer_dispatch_ledger_count + ?? consumerRollups.dispatch_ledger_row_count + )}`, + detail: t("proof.consumerDetail", { + targets: numberValue(consumerRollups.ready_target_count), + }), + icon: Database, + tone: consumerReady ? "ok" as Tone : "warn" as Tone, + }, + ]; + const writebackCards = [ + { + key: "dispatch", + label: t("writeback.dispatch"), + value: numberValue( + rollups.live_log_controlled_writeback_consumer_dispatch_ledger_count + ?? consumerRollups.dispatch_ledger_row_count + ), + detail: t("writeback.dispatchDetail"), + icon: Send, + tone: consumerReady ? "ok" as Tone : "warn" as Tone, + }, + { + key: "apply", + label: t("writeback.apply"), + value: numberValue( + rollups.live_log_controlled_writeback_consumer_apply_receipt_count + ?? consumerRollups.consumer_apply_receipt_row_count + ), + detail: t("writeback.applyDetail"), + icon: CheckCircle2, + tone: runtimeTargetWritePerformed ? "ok" as Tone : "neutral" as Tone, + }, + { + key: "bindings", + label: t("writeback.bindings"), + value: `${numberValue(consumerRollups.ready_consumer_binding_count)}/${numberValue(consumerRollups.consumer_binding_count)}`, + detail: t("writeback.bindingsDetail"), + icon: Bot, + tone: toNumber(consumerRollups.ready_consumer_binding_count) === toNumber(consumerRollups.consumer_binding_count) + && toNumber(consumerRollups.consumer_binding_count) > 0 + ? "ok" as Tone + : "warn" as Tone, + }, + { + key: "targets", + label: t("writeback.targets"), + value: numberValue(consumerRollups.ready_target_count), + detail: t("writeback.targetsDetail"), + icon: ListChecks, + tone: toNumber(consumerRollups.ready_target_count) >= 6 ? "ok" as Tone : "warn" as Tone, + }, + { + key: "context", + label: t("writeback.contextWrites"), + value: numberValue(consumerRollups.target_context_receipt_write_count), + detail: t("writeback.contextWritesDetail"), + icon: BookOpenCheck, + tone: runtimeTargetWritePerformed ? "ok" as Tone : "neutral" as Tone, + }, + { + key: "blockers", + label: t("writeback.blockers"), + value: numberValue(consumerBlockers.length), + detail: consumerBlockers.length > 0 ? consumerBlockers[0] : t("writeback.noBlockers"), + icon: TriangleAlert, + tone: consumerBlockers.length > 0 ? "warn" as Tone : "ok" as Tone, + }, + ]; + const targetWritebackCards = [ + { + key: "km", + label: t("writeback.targetsMap.km"), + binding: consumerRollups.km_consumer_binding_count, + writes: consumerRollups.km_context_receipt_write_count, + icon: BookOpenCheck, + }, + { + key: "rag", + label: t("writeback.targetsMap.rag"), + binding: consumerRollups.rag_consumer_binding_count, + writes: consumerRollups.rag_context_receipt_write_count, + icon: Database, + }, + { + key: "playbook", + label: t("writeback.targetsMap.playbook"), + binding: consumerRollups.playbook_consumer_binding_count, + writes: consumerRollups.playbook_context_receipt_write_count, + icon: ShieldCheck, + }, + { + key: "mcp", + label: t("writeback.targetsMap.mcp"), + binding: consumerRollups.mcp_consumer_binding_count, + writes: consumerRollups.mcp_context_receipt_write_count, + icon: Bot, + }, + { + key: "verifier", + label: t("writeback.targetsMap.verifier"), + binding: consumerRollups.verifier_consumer_binding_count, + writes: consumerRollups.verifier_context_receipt_write_count, + icon: Gauge, + }, + { + key: "aiAgent", + label: t("writeback.targetsMap.aiAgent"), + binding: consumerRollups.ai_agent_consumer_binding_count, + writes: consumerRollups.ai_agent_context_receipt_write_count, + icon: Rocket, + }, ]; const visibleWorkItems = orderedWorkItems.filter((item) => matchesWorkFilter(item, workFilter)); const orderedCompleted = orderedWorkItems.filter((item) => item.status === "completed").length; @@ -674,7 +868,7 @@ export function AutonomousRuntimeReceiptPanel({
{proofCards.map((card) => { const Icon = card.icon; @@ -699,6 +893,60 @@ export function AutonomousRuntimeReceiptPanel({ })}
+
+
+ {writebackCards.map((card) => { + const Icon = card.icon; + return ( +
+
+
+

{card.label}

+

+ {card.value} +

+
+ + +
+

+ {card.detail} +

+
+ ); + })} +
+
+ {targetWritebackCards.map((card) => { + const Icon = card.icon; + const bindingCount = toNumber(card.binding); + const writeCount = toNumber(card.writes); + return ( +
+
+
+

{card.label}

+

+ {numberValue(writeCount)} + {" / "} + {numberValue(bindingCount)} +

+
+
+

+ {t("writeback.targetDetail")} +

+
+ ); + })} +
+
+

{t("metrics.loop")}