diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index ac3a06ddd..09e8b6539 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -7353,6 +7353,91 @@ "watching": "觀察期", "blocked": "阻塞" }, + "assetLedger": { + "eyebrow": "自動化資產總帳", + "title": "KM / PlayBook / 腳本 / 排程 / Verifier 沉澱矩陣", + "subtitle": "把告警到修復的沉澱結果集中顯示;這裡只讀取真相鏈、KM 治理、補救佇列、recurrence、AI route 與 verifier,不把待補項目包裝成已自動修復。", + "rate": "資產沉澱率", + "rateHint": "沉澱率只代表已在讀模型中看見可追蹤資產,不等於 runtime 已授權或修復成功。", + "primaryRef": "主要參照:{ref}", + "metrics": { + "assets": "資產類型", + "ready": "已沉澱", + "pending": "待補", + "blocked": "阻塞" + }, + "states": { + "ready": "沉澱", + "pending": "待補", + "blocked": "阻塞" + }, + "flow": { + "incident": { + "title": "Incident", + "detail": "時間線事件與狀態鏈" + }, + "km": { + "title": "KM", + "detail": "草稿、審核、回測" + }, + "playbook": { + "title": "PlayBook", + "detail": "服務專屬策略與 trust" + }, + "automation": { + "title": "Script", + "detail": "Ansible / 安全路由" + }, + "verifier": { + "title": "Verifier", + "detail": "修復後驗證與回寫" + } + }, + "items": { + "km": { + "title": "Knowledge Base", + "detail": "Hermes 草稿、陳舊 KM、owner review 與 stale ratio 回測。", + "next": "下一步:把待補 KM 與 Incident / PlayBook / verifier 結果綁定。" + }, + "playbook": { + "title": "PlayBook", + "detail": "OpenClaw 修復候選、服務專屬策略、trust 與人工閘門。", + "next": "下一步:把通用兜底改成服務專屬 PlayBook,補 rollback 與適用條件。" + }, + "script": { + "title": "Script / Ansible", + "detail": "Executor lane、Ansible candidate、check-mode 與安全路由狀態。", + "next": "下一步:只把通過安全路由的命令列為候選,不把診斷命令當修復。" + }, + "schedule": { + "title": "排程 / 監控規則", + "detail": "recurrence、Alertmanager、Telegram callback trace 與日週月報來源。", + "next": "下一步:把無效心跳、重複告警與真正 action-required 告警分層。" + }, + "verifier": { + "title": "Verifier", + "detail": "status-chain、remediation history、quality summary 與最終驗證。", + "next": "下一步:每次修復或人工接手都必須留下 success / degraded / failed 判定。" + } + }, + "sources": { + "knowledgeBase": "Knowledge Base", + "ownerReview": "Owner review", + "staleRatio": "Stale ratio", + "statusChain": "Status-chain", + "remediationQueue": "補救佇列", + "aiRoute": "AI route", + "runs": "Runs", + "ansible": "Ansible", + "safeRoute": "安全路由", + "recurrence": "Recurrence", + "alertmanager": "Alertmanager", + "callbackTrace": "Callback trace", + "timeline": "時間線", + "quality": "Quality SLO", + "remediationHistory": "補救歷史" + } + }, "status": { "live": "已完成", "in_progress": "推進中", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index ac3a06ddd..09e8b6539 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -7353,6 +7353,91 @@ "watching": "觀察期", "blocked": "阻塞" }, + "assetLedger": { + "eyebrow": "自動化資產總帳", + "title": "KM / PlayBook / 腳本 / 排程 / Verifier 沉澱矩陣", + "subtitle": "把告警到修復的沉澱結果集中顯示;這裡只讀取真相鏈、KM 治理、補救佇列、recurrence、AI route 與 verifier,不把待補項目包裝成已自動修復。", + "rate": "資產沉澱率", + "rateHint": "沉澱率只代表已在讀模型中看見可追蹤資產,不等於 runtime 已授權或修復成功。", + "primaryRef": "主要參照:{ref}", + "metrics": { + "assets": "資產類型", + "ready": "已沉澱", + "pending": "待補", + "blocked": "阻塞" + }, + "states": { + "ready": "沉澱", + "pending": "待補", + "blocked": "阻塞" + }, + "flow": { + "incident": { + "title": "Incident", + "detail": "時間線事件與狀態鏈" + }, + "km": { + "title": "KM", + "detail": "草稿、審核、回測" + }, + "playbook": { + "title": "PlayBook", + "detail": "服務專屬策略與 trust" + }, + "automation": { + "title": "Script", + "detail": "Ansible / 安全路由" + }, + "verifier": { + "title": "Verifier", + "detail": "修復後驗證與回寫" + } + }, + "items": { + "km": { + "title": "Knowledge Base", + "detail": "Hermes 草稿、陳舊 KM、owner review 與 stale ratio 回測。", + "next": "下一步:把待補 KM 與 Incident / PlayBook / verifier 結果綁定。" + }, + "playbook": { + "title": "PlayBook", + "detail": "OpenClaw 修復候選、服務專屬策略、trust 與人工閘門。", + "next": "下一步:把通用兜底改成服務專屬 PlayBook,補 rollback 與適用條件。" + }, + "script": { + "title": "Script / Ansible", + "detail": "Executor lane、Ansible candidate、check-mode 與安全路由狀態。", + "next": "下一步:只把通過安全路由的命令列為候選,不把診斷命令當修復。" + }, + "schedule": { + "title": "排程 / 監控規則", + "detail": "recurrence、Alertmanager、Telegram callback trace 與日週月報來源。", + "next": "下一步:把無效心跳、重複告警與真正 action-required 告警分層。" + }, + "verifier": { + "title": "Verifier", + "detail": "status-chain、remediation history、quality summary 與最終驗證。", + "next": "下一步:每次修復或人工接手都必須留下 success / degraded / failed 判定。" + } + }, + "sources": { + "knowledgeBase": "Knowledge Base", + "ownerReview": "Owner review", + "staleRatio": "Stale ratio", + "statusChain": "Status-chain", + "remediationQueue": "補救佇列", + "aiRoute": "AI route", + "runs": "Runs", + "ansible": "Ansible", + "safeRoute": "安全路由", + "recurrence": "Recurrence", + "alertmanager": "Alertmanager", + "callbackTrace": "Callback trace", + "timeline": "時間線", + "quality": "Quality SLO", + "remediationHistory": "補救歷史" + } + }, "status": { "live": "已完成", "in_progress": "推進中", diff --git a/apps/web/src/app/[locale]/awooop/work-items/page.tsx b/apps/web/src/app/[locale]/awooop/work-items/page.tsx index 0f9f589ae..684709c1a 100644 --- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx +++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx @@ -978,6 +978,150 @@ type Telemetry = { aiRouteStatus: AiRouteStatusResponse | null; }; +type AutomationAssetLedgerKey = "km" | "playbook" | "script" | "schedule" | "verifier"; + +type AutomationAssetLedgerItem = { + key: AutomationAssetLedgerKey; + icon: typeof Activity; + tone: WorkStatus; + ready: number; + pending: number; + blocked: number; + sourceKeys: string[]; + primaryRef: string; +}; + +const toCount = (value: number | null | undefined) => + Number.isFinite(value) && value ? Math.max(0, Math.trunc(value)) : 0; + +const uniqueText = (values: Array) => + Array.from( + new Set( + values + .map((value) => value?.trim()) + .filter((value): value is string => Boolean(value)) + ) + ); + +function ledgerTone(item: Pick): WorkStatus { + if (item.blocked > 0) return "blocked"; + if (item.pending > 0) return "in_progress"; + if (item.ready > 0) return "live"; + return "watching"; +} + +function buildAutomationAssetLedger( + telemetry: Telemetry, + focusedDraft: RepairCandidateDraftFocus | null +): AutomationAssetLedgerItem[] { + const remediationQueue = telemetry.slo?.adr100?.verification_coverage?.remediation_queue ?? null; + const remediationQueueItems = remediationQueue?.items ?? []; + const statusChain = telemetry.statusChain; + const ansible = statusChain?.execution?.ansible; + const aiRepairEvidence = telemetry.aiRouteStatus?.repair_evidence; + const reviewDraftTotal = toCount(telemetry.knowledgeReviewDrafts?.total); + const staleTotal = toCount(telemetry.knowledgeStaleCandidates?.total_stale); + const ownerPending = toCount( + telemetry.knowledgeStaleOwnerReviews?.total + ?? telemetry.knowledgeStaleOwnerReviewCompletionQueue?.pending_count + ); + const ownerReady = toCount(telemetry.knowledgeStaleOwnerReviewCompletionQueue?.ready_count); + const ownerBlocked = toCount(telemetry.knowledgeStaleOwnerReviewCompletionQueue?.blocked_count); + const ownerCompleted = toCount( + telemetry.knowledgeStaleOwnerReviewCompletionQueue?.completed_count + ?? telemetry.knowledgeStaleOwnerReviewBurnDown?.completed_owner_reviews + ); + const dedupeDuplicates = toCount(telemetry.knowledgeReviewDedupe?.duplicate_draft_total); + + const playbookRefs = uniqueText([ + ...(statusChain?.execution?.playbook_ids ?? []), + ...(statusChain?.execution?.playbook_paths ?? []), + ...remediationQueueItems.map((item) => item.playbook_id), + ...(telemetry.governanceQueue?.items ?? []).map((item) => item.playbook_id), + aiRepairEvidence?.playbook_recommendation?.playbook_id, + aiRepairEvidence?.owner_action?.playbook_id, + ]); + const playbookPending = toCount(remediationQueue?.needs_human) + + (focusedDraft ? 1 : 0) + + toCount(aiRepairEvidence?.access_blockers?.length); + const playbookBlocked = aiRepairEvidence?.playbook_recommendation?.safe_to_auto_execute === false ? 1 : 0; + + const ansibleCandidates = toCount(ansible?.candidate_count ?? ansible?.candidate_playbooks?.length); + const ansibleChecks = toCount(ansible?.check_mode_total) + toCount(ansible?.apply_total); + const scriptPending = ansibleCandidates + (focusedDraft ? 1 : 0); + const scriptBlocked = ansible?.not_used_reason ? 1 : 0; + + const recurrenceOpen = recurrenceOpenItems(telemetry.eventRecurrence).length; + const alertEvents = toCount(telemetry.channelEvents?.total); + const callbackEvents = toCount(telemetry.callbackReplies?.total); + const callbackTraceGap = toCount( + telemetry.callbackReplies?.summary?.outbound_reply_markup_missing_trace_ref_recent_24h_total + ); + + const evaluated = toCount(telemetry.quality?.evaluated_total); + const verified = toCount(telemetry.quality?.verified_auto_repair_total); + const verificationText = String(statusChain?.verification ?? "").toLowerCase(); + const verifierBlocked = /failed|degraded|error|timeout/.test(verificationText) ? 1 : 0; + const verifierPending = Math.max(0, evaluated - verified) + + toCount(remediationQueue?.total) + + (statusChain?.current_stage && statusChain?.verification !== "success" ? 1 : 0); + + const items: AutomationAssetLedgerItem[] = [ + { + key: "km", + icon: Database, + ready: ownerCompleted, + pending: reviewDraftTotal + staleTotal + ownerPending + ownerReady, + blocked: ownerBlocked + dedupeDuplicates, + sourceKeys: ["knowledgeBase", "ownerReview", "staleRatio"], + primaryRef: telemetry.knowledgeStaleOwnerReviewBurnDown?.burn_down_status ?? "knowledge_degradation", + tone: "watching", + }, + { + key: "playbook", + icon: ClipboardList, + ready: playbookRefs.length, + pending: playbookPending, + blocked: playbookBlocked, + sourceKeys: ["statusChain", "remediationQueue", "aiRoute"], + primaryRef: playbookRefs[0] ?? aiRepairEvidence?.owner_action?.next_step ?? "repair_candidate_draft", + tone: "watching", + }, + { + key: "script", + icon: GitBranch, + ready: ansibleChecks, + pending: scriptPending, + blocked: scriptBlocked, + sourceKeys: ["runs", "ansible", "safeRoute"], + primaryRef: ansible?.latest_playbook_path ?? ansible?.not_used_reason ?? "script_or_ansible_ref", + tone: "watching", + }, + { + key: "schedule", + icon: Activity, + ready: callbackEvents, + pending: recurrenceOpen + alertEvents, + blocked: callbackTraceGap, + sourceKeys: ["recurrence", "alertmanager", "callbackTrace"], + primaryRef: telemetry.eventRecurrence?.project_id ?? "alert_recurrence", + tone: "watching", + }, + { + key: "verifier", + icon: CheckCircle2, + ready: verified, + pending: verifierPending, + blocked: verifierBlocked, + sourceKeys: ["timeline", "quality", "remediationHistory"], + primaryRef: statusChain?.verification ?? statusChain?.next_step ?? "verifier_result", + tone: "watching", + }, + ]; + + return items.map((item) => ({ ...item, tone: ledgerTone(item) })); +} + type IncidentTimelineEvent = { stage: string; status: string; @@ -2862,6 +3006,193 @@ function ProductionClaimBanner({ ); } +function AutomationAssetLedgerPanel({ + telemetry, + focusedDraft, + loading, +}: { + telemetry: Telemetry; + focusedDraft: RepairCandidateDraftFocus | null; + loading: boolean; +}) { + const t = useTranslations("awooop.workItems.assetLedger"); + const locale = useLocale(); + const items = useMemo( + () => buildAutomationAssetLedger(telemetry, focusedDraft), + [focusedDraft, telemetry] + ); + const formatCount = useCallback( + (value: number) => value.toLocaleString(locale === "zh-TW" ? "zh-TW" : "en-US"), + [locale] + ); + const totals = useMemo(() => { + const ready = items.reduce((sum, item) => sum + item.ready, 0); + const pending = items.reduce((sum, item) => sum + item.pending, 0); + const blocked = items.reduce((sum, item) => sum + item.blocked, 0); + const total = ready + pending + blocked; + return { + ready, + pending, + blocked, + total, + rate: total > 0 ? Math.round((ready / total) * 100) : 0, + }; + }, [items]); + const flow = [ + { key: "incident", value: telemetry.incidentTimeline?.events?.length ?? 0 }, + { key: "km", value: items.find((item) => item.key === "km")?.ready ?? 0 }, + { key: "playbook", value: items.find((item) => item.key === "playbook")?.ready ?? 0 }, + { key: "automation", value: items.find((item) => item.key === "script")?.ready ?? 0 }, + { key: "verifier", value: items.find((item) => item.key === "verifier")?.ready ?? 0 }, + ]; + + return ( +
+
+
+
+
+
+
+

+ {t("eyebrow")} +

+

+ {t("title")} +

+

+ {t("subtitle")} +

+
+
+ +
+ {[ + { key: "assets", value: items.length }, + { key: "ready", value: totals.ready }, + { key: "pending", value: totals.pending }, + { key: "blocked", value: totals.blocked }, + ].map((metric) => ( +
+

+ {t(`metrics.${metric.key}` as never)} +

+

+ {loading ? "--" : formatCount(metric.value)} +

+
+ ))} +
+ +
+
+

{t("rate")}

+ + {loading ? "--" : `${totals.rate}%`} + +
+
+
+
+

{t("rateHint")}

+
+
+ +
+
+ {flow.map((node, index) => ( +
+ {index > 0 && ( +
+ )} +

+ {String(index + 1).padStart(2, "0")} +

+

+ {t(`flow.${node.key}.title` as never)} +

+

+ {loading ? "--" : formatCount(node.value)} +

+

+ {t(`flow.${node.key}.detail` as never)} +

+
+ ))} +
+
+
+ +
+ {items.map((item) => { + const Icon = item.icon; + const denominator = item.ready + item.pending + item.blocked; + const itemRate = denominator > 0 ? Math.round((item.ready / denominator) * 100) : 0; + return ( +
+
+
+

+ {t(`items.${item.key}.title` as never)} +

+

+ {t(`items.${item.key}.detail` as never)} +

+
+
+
+ {(["ready", "pending", "blocked"] as const).map((state) => ( +
+

+ {t(`states.${state}` as never)} +

+

+ {loading ? "--" : formatCount(item[state])} +

+
+ ))} +
+
+
+
+
+ {item.sourceKeys.map((source) => ( + + {t(`sources.${source}` as never)} + + ))} +
+

+ {t("primaryRef", { ref: item.primaryRef })} +

+

+ {t(`items.${item.key}.next` as never)} +

+
+ ); + })} +
+
+ ); +} + function RepairCandidateDraftPanel({ draft, chain, @@ -6337,6 +6668,12 @@ export default function AwoooPWorkItemsPage() { ))} + +