From ca04b49d58975c44e1fcd478be786efa73e997ed Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 18 Jun 2026 21:03:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E5=9C=A8=20Work=20Items=20?= =?UTF-8?q?=E9=A1=AF=E7=A4=BA=E5=A0=B1=E8=A1=A8=E7=BC=BA=E5=8F=A3=20owner?= =?UTF-8?q?=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/messages/en.json | 50 +++ apps/web/messages/zh-TW.json | 50 +++ .../app/[locale]/awooop/work-items/page.tsx | 297 ++++++++++++++++++ 3 files changed, 397 insertions(+) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index de974b8b..edf2147b 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -7815,6 +7815,47 @@ "remediationHistory": "補救歷史" } }, + "reportSourceGapOwnerReview": { + "eyebrow": "報表資料源 owner review", + "title": "報表資料源 PlayBook / Verifier 處置板", + "subtitle": "把 report-source-gap 從報表頁接到 Work Items:每個缺口都要有 PlayBook 草案、Verifier 計畫、腳本 readback、排程 no-send 與 owner review。", + "loading": "正在讀取 report-source-gap owner review read model。", + "unavailable": "報表資料源 read model 尚未回應;不能把報表全 0 或缺資料判定為健康。", + "empty": "目前沒有 report-source-gap owner review 卡。", + "boundaryTitle": "不可誤讀合約", + "boundary": "live Telegram send={live};runtime gate={gate}。這裡只做草案與 owner review readback,不發送、不排程、不執行。", + "openReports": "回報表總控", + "ownerRequired": "需 owner review", + "ownerOptional": "owner review 可後補", + "scheduleBoundary": "排程仍維持 no-send preview", + "fieldsTitle": "PlayBook 必填欄位", + "checksTitle": "Verifier 檢查", + "nextAction": "下一步:{action}", + "metrics": { + "sources": "來源可讀", + "gaps": "資料缺口", + "playbooks": "PlayBook 草案", + "verifiers": "Verifier 計畫", + "confidence": "可信度", + "ownerReview": "需 owner" + }, + "assets": { + "playbook": "PlayBook", + "verifier": "Verifier", + "script": "腳本", + "schedule": "排程" + }, + "states": { + "draft_required": "需草案", + "plan_required": "需計畫", + "readback_only": "只讀回讀", + "no_send_preview": "no-send preview", + "ok": "正常", + "gap": "缺口", + "blocked": "阻塞", + "unknown": "待確認" + } + }, "status": { "live": "已完成", "in_progress": "推進中", @@ -7850,6 +7891,9 @@ "aiRouteRepairWorkItem": { "title": "AI Provider primary lane 修復工作項" }, + "reportSourceGapOwnerReview": { + "title": "報表資料源 PlayBook / Verifier owner review" + }, "configDriftFsm": { "title": "Config Drift fingerprint 狀態機" }, @@ -7898,6 +7942,7 @@ "autoRepair": "必須同時有 auto_repair、verification_result=success與KM 回寫", "recurrenceWorkItems": "Run 完成無修復、修復失敗與人工閘門必須進入可追蹤工作項", "aiRouteRepairWorkItem": "Provider lane 降級時必須顯示 evidence、owner、PlayBook候選與是否可自動修復", + "reportSourceGapOwnerReview": "每個 report-source-gap 必須有 PlayBook 草案、Verifier 計畫、腳本 readback、排程 no-send 與 owner review;不得把全 0 當健康或自動執行授權", "configDriftFsm": "同一 drift fingerprint 必須顯示重複、PR、零 diff、交接與下一步", "remediationQueue": "每筆 degraded / failed / timeout都必須映射到重跑、重驗、Ticket或人工檢查", "telegramCallbacks": "按下詳情與歷史不能再只依賴 Redis TTL或舊快照", @@ -7937,6 +7982,11 @@ "aiRouteRepairSafety": "可安全自動修復:{safe}", "aiRouteRepairSummary": "AI route 目前由 {selected} 承接;下一步:{action};需人工介入:{human}", "aiRouteRepairUnavailable": "AI route repair evidence 尚未回傳", + "reportSourceGapOwnerReview": "報表資料源缺口:{gaps};PlayBook 草案 {playbooks};Verifier 計畫 {verifiers};需 owner {owners}", + "reportSourceGapLatest": "最新工作項:{workItem};route={route}", + "reportSourceGapAssets": "資產狀態:PlayBook {playbook};Verifier {verifier};排程 {schedule}", + "reportSourceGapBoundary": "live send={live};runtime gate={gate}", + "reportSourceGapEmpty": "目前沒有 report-source-gap 處置卡", "humanRequired": { "yes": "是", "no": "否" diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index de974b8b..edf2147b 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -7815,6 +7815,47 @@ "remediationHistory": "補救歷史" } }, + "reportSourceGapOwnerReview": { + "eyebrow": "報表資料源 owner review", + "title": "報表資料源 PlayBook / Verifier 處置板", + "subtitle": "把 report-source-gap 從報表頁接到 Work Items:每個缺口都要有 PlayBook 草案、Verifier 計畫、腳本 readback、排程 no-send 與 owner review。", + "loading": "正在讀取 report-source-gap owner review read model。", + "unavailable": "報表資料源 read model 尚未回應;不能把報表全 0 或缺資料判定為健康。", + "empty": "目前沒有 report-source-gap owner review 卡。", + "boundaryTitle": "不可誤讀合約", + "boundary": "live Telegram send={live};runtime gate={gate}。這裡只做草案與 owner review readback,不發送、不排程、不執行。", + "openReports": "回報表總控", + "ownerRequired": "需 owner review", + "ownerOptional": "owner review 可後補", + "scheduleBoundary": "排程仍維持 no-send preview", + "fieldsTitle": "PlayBook 必填欄位", + "checksTitle": "Verifier 檢查", + "nextAction": "下一步:{action}", + "metrics": { + "sources": "來源可讀", + "gaps": "資料缺口", + "playbooks": "PlayBook 草案", + "verifiers": "Verifier 計畫", + "confidence": "可信度", + "ownerReview": "需 owner" + }, + "assets": { + "playbook": "PlayBook", + "verifier": "Verifier", + "script": "腳本", + "schedule": "排程" + }, + "states": { + "draft_required": "需草案", + "plan_required": "需計畫", + "readback_only": "只讀回讀", + "no_send_preview": "no-send preview", + "ok": "正常", + "gap": "缺口", + "blocked": "阻塞", + "unknown": "待確認" + } + }, "status": { "live": "已完成", "in_progress": "推進中", @@ -7850,6 +7891,9 @@ "aiRouteRepairWorkItem": { "title": "AI Provider primary lane 修復工作項" }, + "reportSourceGapOwnerReview": { + "title": "報表資料源 PlayBook / Verifier owner review" + }, "configDriftFsm": { "title": "Config Drift fingerprint 狀態機" }, @@ -7898,6 +7942,7 @@ "autoRepair": "必須同時有 auto_repair、verification_result=success與KM 回寫", "recurrenceWorkItems": "Run 完成無修復、修復失敗與人工閘門必須進入可追蹤工作項", "aiRouteRepairWorkItem": "Provider lane 降級時必須顯示 evidence、owner、PlayBook候選與是否可自動修復", + "reportSourceGapOwnerReview": "每個 report-source-gap 必須有 PlayBook 草案、Verifier 計畫、腳本 readback、排程 no-send 與 owner review;不得把全 0 當健康或自動執行授權", "configDriftFsm": "同一 drift fingerprint 必須顯示重複、PR、零 diff、交接與下一步", "remediationQueue": "每筆 degraded / failed / timeout都必須映射到重跑、重驗、Ticket或人工檢查", "telegramCallbacks": "按下詳情與歷史不能再只依賴 Redis TTL或舊快照", @@ -7937,6 +7982,11 @@ "aiRouteRepairSafety": "可安全自動修復:{safe}", "aiRouteRepairSummary": "AI route 目前由 {selected} 承接;下一步:{action};需人工介入:{human}", "aiRouteRepairUnavailable": "AI route repair evidence 尚未回傳", + "reportSourceGapOwnerReview": "報表資料源缺口:{gaps};PlayBook 草案 {playbooks};Verifier 計畫 {verifiers};需 owner {owners}", + "reportSourceGapLatest": "最新工作項:{workItem};route={route}", + "reportSourceGapAssets": "資產狀態:PlayBook {playbook};Verifier {verifier};排程 {schedule}", + "reportSourceGapBoundary": "live send={live};runtime gate={gate}", + "reportSourceGapEmpty": "目前沒有 report-source-gap 處置卡", "humanRequired": { "yes": "是", "no": "否" 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 684709c1..ddfdf50e 100644 --- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx +++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx @@ -956,6 +956,61 @@ type AiRouteStatusResponse = { repair_evidence?: AiRouteRepairEvidence | null; }; +type ReportSourceGapPlaybookVerifier = { + work_item_id: string; + source_id: string; + display_name: string; + route: string; + playbook_draft_id: string; + verifier_plan_id: string; + playbook_state: string; + verifier_state: string; + script_state: string; + schedule_state: string; + owner_review_required: boolean; + runtime_gate_open: boolean; + playbook_template_fields: string[]; + verifier_checks: string[]; + next_action: string; +}; + +type ReportSourceHealthResponse = { + source_health?: Array<{ + source_id?: string | null; + display_name?: string | null; + state?: string | null; + source_ok?: boolean | null; + work_item_id?: string | null; + next_action?: string | null; + }>; + automation_assets?: Array<{ + key?: string | null; + label?: string | null; + state?: string | null; + done_count?: number | null; + blocked_count?: number | null; + next_action?: string | null; + }>; + source_gap_playbook_verifier?: ReportSourceGapPlaybookVerifier[]; + all_zero_assessment?: { + is_all_zero_signal?: boolean | null; + verdict?: string | null; + next_action?: string | null; + }; + rollups?: { + source_count?: number | null; + source_ok_count?: number | null; + source_gap_count?: number | null; + confidence_percent?: number | null; + report_work_item_count?: number | null; + source_gap_playbook_draft_count?: number | null; + source_gap_verifier_plan_count?: number | null; + source_gap_owner_review_required_count?: number | null; + live_send_allowed_count?: number | null; + runtime_gate_count?: number | null; + }; +}; + type Telemetry = { quality: AutomationQualitySummary | null; governanceEvents: GovernanceEventsResponse | null; @@ -976,6 +1031,7 @@ type Telemetry = { statusChain: AwoooPStatusChain | null; incidentTimeline: IncidentTimelineResponse | null; aiRouteStatus: AiRouteStatusResponse | null; + reportSourceHealth: ReportSourceHealthResponse | null; }; type AutomationAssetLedgerKey = "km" | "playbook" | "script" | "schedule" | "verifier"; @@ -1291,6 +1347,21 @@ function routeLabel(item?: RemediationHistoryItem | null) { return route || "--"; } +function reportSourceStateKey(value?: string | null) { + if ( + value === "draft_required" || + value === "plan_required" || + value === "readback_only" || + value === "no_send_preview" || + value === "ok" || + value === "gap" || + value === "blocked" + ) { + return value; + } + return "unknown"; +} + function recurrenceOpenItems(recurrence: RecurrenceResponse | null) { return (recurrence?.items ?? []).filter((item) => item.work_item?.status === "open"); } @@ -2382,6 +2453,14 @@ function buildWorkItems( (governanceUnresolved > 0 && governanceQueuePending === 0); const mcpGateMissing = hasGateFailure(telemetry.quality, "mcp_gateway_observed"); const timelineGateMissing = hasGateFailure(telemetry.quality, "timeline_recorded"); + const reportSourceHealth = telemetry.reportSourceHealth; + const reportSourceCards = reportSourceHealth?.source_gap_playbook_verifier ?? []; + const reportSourceRollups = reportSourceHealth?.rollups; + const reportSourceRuntimeGate = toCount(reportSourceRollups?.runtime_gate_count); + const reportSourceOwnerReview = toCount( + reportSourceRollups?.source_gap_owner_review_required_count + ); + const firstReportSourceCard = reportSourceCards[0] ?? null; return [ { @@ -2578,6 +2657,48 @@ function buildWorkItems( : [t("evidence.remediationHistoryEmpty")], href: "/governance", }, + { + id: "reportSourceGapOwnerReview", + phase: "P2-110D", + status: reportSourceCards.length > 0 + ? "in_progress" + : reportSourceHealth + ? "watching" + : "blocked", + surfaceKey: "workItems", + source: "/api/v1/agents/agent-report-source-health", + gateKey: "reportSourceGapOwnerReview", + evidence: t("evidence.reportSourceGapOwnerReview", { + gaps: toCount(reportSourceRollups?.source_gap_count), + playbooks: toCount(reportSourceRollups?.source_gap_playbook_draft_count), + verifiers: toCount(reportSourceRollups?.source_gap_verifier_plan_count), + owners: reportSourceOwnerReview, + }), + evidenceDetails: firstReportSourceCard + ? [ + t("evidence.reportSourceGapLatest", { + workItem: firstReportSourceCard.work_item_id, + route: firstReportSourceCard.route, + }), + t("evidence.reportSourceGapAssets", { + playbook: t( + `reportSourceGapOwnerReview.states.${reportSourceStateKey(firstReportSourceCard.playbook_state)}` as never + ), + verifier: t( + `reportSourceGapOwnerReview.states.${reportSourceStateKey(firstReportSourceCard.verifier_state)}` as never + ), + schedule: t( + `reportSourceGapOwnerReview.states.${reportSourceStateKey(firstReportSourceCard.schedule_state)}` as never + ), + }), + t("evidence.reportSourceGapBoundary", { + live: toCount(reportSourceRollups?.live_send_allowed_count), + gate: reportSourceRuntimeGate, + }), + ] + : [t("evidence.reportSourceGapEmpty")], + href: `/awooop/work-items?project_id=awoooi#report-source-gap-owner-review`, + }, { id: "telegramCallbacks", phase: "T45", @@ -3193,6 +3314,172 @@ function AutomationAssetLedgerPanel({ ); } +function ReportSourceGapOwnerReviewPanel({ + sourceHealth, + loading, +}: { + sourceHealth: ReportSourceHealthResponse | null; + loading: boolean; +}) { + const t = useTranslations("awooop.workItems.reportSourceGapOwnerReview"); + const cards = sourceHealth?.source_gap_playbook_verifier ?? []; + const rollups = sourceHealth?.rollups; + const sourceCount = toCount(rollups?.source_count); + const sourceOk = toCount(rollups?.source_ok_count); + const sourceGap = toCount(rollups?.source_gap_count); + const playbookDrafts = toCount(rollups?.source_gap_playbook_draft_count); + const verifierPlans = toCount(rollups?.source_gap_verifier_plan_count); + const ownerReviews = toCount(rollups?.source_gap_owner_review_required_count); + const liveSend = toCount(rollups?.live_send_allowed_count); + const runtimeGate = toCount(rollups?.runtime_gate_count); + const confidence = toCount(rollups?.confidence_percent); + const isUnavailable = !sourceHealth && !loading; + + return ( +
+
+
+
+
+
+
+

+ {t("eyebrow")} +

+

+ {t("title")} +

+

+ {t("subtitle")} +

+
+
+ +
+ {[ + { key: "sources", value: loading && !sourceHealth ? "--" : `${sourceOk}/${sourceCount}` }, + { key: "gaps", value: loading && !sourceHealth ? "--" : sourceGap }, + { key: "playbooks", value: loading && !sourceHealth ? "--" : playbookDrafts }, + { key: "verifiers", value: loading && !sourceHealth ? "--" : verifierPlans }, + { key: "confidence", value: loading && !sourceHealth ? "--" : `${confidence}%` }, + { key: "ownerReview", value: loading && !sourceHealth ? "--" : ownerReviews }, + ].map((metric) => ( +
+

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

+

+ {metric.value} +

+
+ ))} +
+ +
+
+

{t("boundaryTitle")}

+

{t("boundary", { live: liveSend, gate: runtimeGate })}

+
+ + {t("openReports")} +
+
+ +
+ {isUnavailable ? ( +
+ {t("unavailable")} +
+ ) : cards.length === 0 ? ( +
+ {loading ? t("loading") : t("empty")} +
+ ) : ( +
+ {cards.map((card) => ( +
+
+
+

+ {card.display_name} +

+

+ {card.work_item_id} +

+
+ + {card.owner_review_required ? t("ownerRequired") : t("ownerOptional")} + +
+ +
+ {[ + ["playbook", card.playbook_state, card.playbook_draft_id], + ["verifier", card.verifier_state, card.verifier_plan_id], + ["script", card.script_state, card.route], + ["schedule", card.schedule_state, t("scheduleBoundary")], + ].map(([key, state, detail]) => ( +
+

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

+

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

+

+ {detail} +

+
+ ))} +
+ +
+
+

{t("fieldsTitle")}

+
+ {card.playbook_template_fields.map((field) => ( + + {field} + + ))} +
+
+
+

{t("checksTitle")}

+
+ {card.verifier_checks.map((check) => ( +
+
+ ))} +
+
+
+ +

+ {t("nextAction", { action: card.next_action })} +

+
+ ))} +
+ )} +
+
+
+ ); +} + function RepairCandidateDraftPanel({ draft, chain, @@ -6439,6 +6726,7 @@ export default function AwoooPWorkItemsPage() { statusChain: null, incidentTimeline: null, aiRouteStatus: null, + reportSourceHealth: null, }); const [loading, setLoading] = useState(true); const [lastUpdated, setLastUpdated] = useState(null); @@ -6487,6 +6775,7 @@ export default function AwoooPWorkItemsPage() { `${API_BASE}/api/v1/platform/ai-route-status?workload_type=deep_rca`, projectId ); + const reportSourceHealthUrl = `${API_BASE}/api/v1/agents/agent-report-source-health`; const [ quality, @@ -6506,6 +6795,7 @@ export default function AwoooPWorkItemsPage() { driftFingerprintState, callbackReplies, aiRouteStatus, + reportSourceHealth, ] = await Promise.all([ fetchJson(qualityUrl, 15000), fetchJson(governanceEventsUrl), @@ -6524,6 +6814,7 @@ export default function AwoooPWorkItemsPage() { fetchJson(driftFingerprintUrl, 12000), fetchJson(callbackRepliesUrl, 12000), fetchJson(aiRouteStatusUrl, 12000), + fetchJson(reportSourceHealthUrl, 12000), ]); const statusChainIncidentId = selectStatusChainIncidentId( @@ -6571,6 +6862,7 @@ export default function AwoooPWorkItemsPage() { statusChain, incidentTimeline, aiRouteStatus, + reportSourceHealth, }); setLastUpdated(new Date()); setLoading(false); @@ -6674,6 +6966,11 @@ export default function AwoooPWorkItemsPage() { loading={loading} /> + +