From 7f4f5b24ba458a53dcf2860f13429aeec43baf5d Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 6 May 2026 22:24:28 +0800 Subject: [PATCH] fix(awooop): clarify operator disposition lanes --- apps/web/messages/en.json | 37 ++++- apps/web/messages/zh-TW.json | 37 ++++- .../app/[locale]/awooop/approvals/page.tsx | 85 +++++++++++- apps/web/src/app/[locale]/awooop/page.tsx | 96 +++++++++++++ .../web/src/app/[locale]/awooop/runs/page.tsx | 127 +++++++++++++++++- .../app/[locale]/awooop/work-items/page.tsx | 18 +++ docs/LOGBOOK.md | 32 +++++ 7 files changed, 417 insertions(+), 15 deletions(-) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 5f27ab30..6c3e6119 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -1501,11 +1501,38 @@ "runsDetail": "Run state is the single view into async work", "approvals": "Pending Approvals", "approvalsDetail": "Every high-risk action must stop at the human gate", - "contracts": "Contracts", - "contractsDetail": "Project / Agent / Policy contract publish state" - }, - "lanes": { - "title": "Flywheel Lanes", + "contracts": "Contracts", + "contractsDetail": "Project / Agent / Policy contract publish state" + }, + "disposition": { + "title": "Disposition Semantics", + "diagnosis": { + "title": "Read-only Diagnosis", + "signal": "AI collected evidence", + "owner": "Owner: AI summarizes, SRE judges", + "route": "Route: Run monitor / incident detail" + }, + "approval": { + "title": "Human Gate", + "signal": "High-risk approval pending", + "owner": "Owner: SRE approve / reject", + "route": "Route: Approval queue" + }, + "execute": { + "title": "Auto Execution", + "signal": "Low-risk closure path", + "owner": "Owner: MCP Gateway executes and audits", + "route": "Route: Run State / Audit" + }, + "manual": { + "title": "Manual Escalation", + "signal": "AI cannot safely repair", + "owner": "Owner: war room takes over", + "route": "Route: AwoooI SRE war room" + } + }, + "lanes": { + "title": "Flywheel Lanes", "live": "Live", "mirror": "Mirror", "providerName": "Provider Order", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index aad2a159..34a59f88 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -1502,11 +1502,38 @@ "runsDetail": "Run state 是非同步任務的唯一觀測入口", "approvals": "待審批", "approvalsDetail": "所有高風險動作都必須停在人工閘門", - "contracts": "合約", - "contractsDetail": "Project / Agent / Policy contract 發布狀態" - }, - "lanes": { - "title": "飛輪鏈路", + "contracts": "合約", + "contractsDetail": "Project / Agent / Policy contract 發布狀態" + }, + "disposition": { + "title": "處置語義", + "diagnosis": { + "title": "只讀診斷", + "signal": "AI 已收集證據", + "owner": "負責:AI 先整理,SRE 判讀", + "route": "流向:Run 監控 / 事件詳情" + }, + "approval": { + "title": "人工閘門", + "signal": "高風險待批准", + "owner": "負責:SRE approve / reject", + "route": "流向:審批佇列" + }, + "execute": { + "title": "自動執行", + "signal": "低風險可閉環", + "owner": "負責:MCP Gateway 執行並稽核", + "route": "流向:Run State / Audit" + }, + "manual": { + "title": "人工升級", + "signal": "AI 無法安全修復", + "owner": "負責:戰情室接手", + "route": "流向:AwoooI SRE 戰情室" + } + }, + "lanes": { + "title": "飛輪鏈路", "live": "已接線", "mirror": "Mirror", "providerName": "Provider 順序", diff --git a/apps/web/src/app/[locale]/awooop/approvals/page.tsx b/apps/web/src/app/[locale]/awooop/approvals/page.tsx index 2508f7b8..5561f5e2 100644 --- a/apps/web/src/app/[locale]/awooop/approvals/page.tsx +++ b/apps/web/src/app/[locale]/awooop/approvals/page.tsx @@ -5,13 +5,15 @@ "use client"; -import { useState, useEffect, useCallback, useRef } from "react"; +import { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { ShieldCheck, RefreshCw, AlertCircle, Clock, ArrowRight, + ListChecks, + TriangleAlert, } from "lucide-react"; import { cn } from "@/lib/utils"; import { Link } from "@/i18n/routing"; @@ -97,6 +99,15 @@ function TimeoutCell({ timeoutAt }: { timeoutAt: string | null }) { ); } +function DecisionPostureBadge() { + return ( + + + ); +} + function ApprovalRow({ approval }: { approval: Approval }) { const formattedDate = approval.created_at ? new Date(approval.created_at).toLocaleDateString("zh-TW", { @@ -137,6 +148,9 @@ function ApprovalRow({ approval }: { approval: Approval }) { {approval.agent_id || "--"} + + + {formattedDate} @@ -193,6 +207,43 @@ export default function ApprovalsPage() { const ms = getRemainingMs(a.timeout_at); return ms !== null && ms <= 5 * 60 * 1000; }).length; + const expiredCount = approvals.filter((a) => { + const ms = getRemainingMs(a.timeout_at); + return ms !== null && ms <= 0; + }).length; + const queueSummary = useMemo( + () => [ + { + label: "待人工決策", + value: approvals.length, + detail: "等待 approve / reject 的唯一佇列", + icon: ShieldCheck, + className: "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]", + }, + { + label: "即將逾時", + value: criticalCount, + detail: "5 分鐘內必須處置", + icon: Clock, + className: "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]", + }, + { + label: "已逾時", + value: expiredCount, + detail: "不得再自動 resume", + icon: TriangleAlert, + className: "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]", + }, + { + label: "操作來源", + value: "Run", + detail: "審批必須回到 Operator Run", + icon: ListChecks, + className: "border-[#d8d3c7] bg-white text-[#5f5b52]", + }, + ], + [approvals.length, criticalCount, expiredCount] + ); return (
@@ -230,6 +281,33 @@ export default function ApprovalsPage() {
+
+ {queueSummary.map((item) => { + const Icon = item.icon; + return ( +
+
+
+

{item.label}

+
+ {item.value} +
+
+ + +
+

{item.detail}

+
+ ); + })} +
+ {/* Error State */} {error && (
@@ -266,6 +344,9 @@ export default function ApprovalsPage() { Agent + + 處置 Lane + 建立時間 @@ -278,7 +359,7 @@ export default function ApprovalsPage() { {loading ? ( Array.from({ length: 5 }).map((_, i) => ( - {Array.from({ length: 5 }).map((_, j) => ( + {Array.from({ length: 6 }).map((_, j) => (
diff --git a/apps/web/src/app/[locale]/awooop/page.tsx b/apps/web/src/app/[locale]/awooop/page.tsx index 51994740..d7f784f1 100644 --- a/apps/web/src/app/[locale]/awooop/page.tsx +++ b/apps/web/src/app/[locale]/awooop/page.tsx @@ -14,8 +14,11 @@ import { CheckCircle2, FileText, GitBranch, + ListChecks, RefreshCw, + SearchCheck, ShieldCheck, + TriangleAlert, Waypoints, } from "lucide-react"; import { Link } from "@/i18n/routing"; @@ -140,6 +143,54 @@ function LaneRow({ ); } +function DispositionCell({ + title, + signal, + owner, + route, + tone, + icon: Icon, +}: { + title: string; + signal: string; + owner: string; + route: string; + tone: "diagnosis" | "approval" | "execute" | "manual"; + icon: typeof SearchCheck; +}) { + return ( +
+
+
+

{signal}

+

+ {title} +

+
+ + +
+
+
+
{route}
+
+
+
{owner}
+
+
+
+ ); +} + export default function AwoooPPage() { const t = useTranslations("awooop.home"); const locale = useLocale(); @@ -295,6 +346,51 @@ export default function AwoooPPage() { /> +
+
+
+
+
+
+ + + + +
+
+
diff --git a/apps/web/src/app/[locale]/awooop/runs/page.tsx b/apps/web/src/app/[locale]/awooop/runs/page.tsx index 5ee8b5b1..3683ac2a 100644 --- a/apps/web/src/app/[locale]/awooop/runs/page.tsx +++ b/apps/web/src/app/[locale]/awooop/runs/page.tsx @@ -5,7 +5,7 @@ "use client"; -import { useState, useEffect, useCallback, useRef } from "react"; +import { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { Activity, RefreshCw, @@ -15,6 +15,10 @@ import { ChevronLeft, ChevronRight, Cpu, + ListChecks, + SearchCheck, + ShieldCheck, + TriangleAlert, } from "lucide-react"; import { cn } from "@/lib/utils"; @@ -32,6 +36,8 @@ type RunState = | "cancelled" | "timeout"; +type RunLane = "intake" | "diagnosis" | "approval" | "execution" | "done" | "manual"; + interface Run { run_id: string; project_id: string; @@ -119,6 +125,62 @@ const STATE_CONFIG: Record< }, }; +const LANE_CONFIG: Record< + RunLane, + { + label: string; + detail: string; + icon: typeof Activity; + className: string; + } +> = { + intake: { + label: "排程入口", + detail: "等待 worker 接手", + icon: ListChecks, + className: "border-[#d8d3c7] bg-white text-[#5f5b52]", + }, + diagnosis: { + label: "只讀診斷", + detail: "MCP / SSH / RAG 收集證據", + icon: SearchCheck, + className: "border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]", + }, + approval: { + label: "人工閘門", + detail: "等待 SRE 批准或拒絕", + icon: ShieldCheck, + className: "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]", + }, + execution: { + label: "AI 執行中", + detail: "模型推理或安全動作執行", + icon: Activity, + className: "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]", + }, + done: { + label: "已收斂", + detail: "完成並等待稽核回寫", + icon: Activity, + className: "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]", + }, + manual: { + label: "人工升級", + detail: "AI 無法閉環,需人工處置", + icon: TriangleAlert, + className: "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]", + }, +}; + +function getRunLane(state: RunState): RunLane { + if (state === "pending") return "intake"; + if (state === "waiting_tool") return "diagnosis"; + if (state === "waiting_approval") return "approval"; + if (state === "running") return "execution"; + if (state === "completed") return "done"; + return "manual"; +} + // ============================================================================= // Sub Components // ============================================================================= @@ -151,6 +213,24 @@ function ShadowBadge({ isShadow }: { isShadow: boolean }) { ); } +function RunLaneBadge({ state }: { state: RunState }) { + const lane = LANE_CONFIG[getRunLane(state)]; + const Icon = lane.icon; + + return ( + + + ); +} + function RunRow({ run }: { run: Run }) { const formattedDate = run.created_at ? new Date(run.created_at).toLocaleDateString("zh-TW", { @@ -183,6 +263,9 @@ function RunRow({ run }: { run: Run }) { + + + @@ -279,6 +362,20 @@ export default function RunsPage() { }, [fetchRuns]); const totalPages = Math.ceil(total / PER_PAGE); + const laneSummary = useMemo(() => { + const counts: Record = { + intake: 0, + diagnosis: 0, + approval: 0, + execution: 0, + done: 0, + manual: 0, + }; + runs.forEach((run) => { + counts[getRunLane(run.state)] += 1; + }); + return counts; + }, [runs]); return (
@@ -309,6 +406,27 @@ export default function RunsPage() {
+
+ {(Object.keys(LANE_CONFIG) as RunLane[]).map((lane) => { + const config = LANE_CONFIG[lane]; + const Icon = config.icon; + return ( +
+
+
+

{config.label}

+

{config.detail}

+
+
+
+ {laneSummary[lane]} +
+
+ ); + })} +
+ {/* Filters */}