diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 6713fc1ea..288bf8a24 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -3250,6 +3250,68 @@ "queued": "已排入 owner review", "already_queued": "已在 owner review" }, + "operationRail": { + "title": "Owner Review 操作軌道", + "subtitle": "把陳舊 KM 從偵測、審核、乾跑、確認、寫回到比例回測收斂成一條可掃描流程。", + "nextAction": "建議下一步", + "guardrailTitle": "寫入防護", + "metric": { + "ratio": "陳舊比例", + "pending": "待審核", + "completed": "已完成", + "remaining": "距門檻", + "threshold": "門檻 {threshold}", + "readyBlocked": "可乾跑 {ready} / 卡住 {blocked}", + "failed": "失敗 {failed}", + "total": "陳舊 {stale} / 總數 {total}" + }, + "state": { + "warning": "需處理", + "ready": "可操作", + "waiting": "等待", + "done": "已有證據", + "blocked": "卡住" + }, + "flow": { + "node": { + "detected": "偵測", + "ownerReview": "Owner Review", + "dryRun": "乾跑預覽", + "ownerConfirm": "Owner 確認", + "writeback": "寫回 KM", + "recheck": "比例回測" + }, + "detail": { + "detected": "目前 {ratio};門檻 {threshold}", + "ownerReview": "{count} 筆等待 owner 審核", + "dryRun": "{ready} 筆可乾跑;{blocked} 筆卡住", + "ownerConfirm": "確認後才允許寫回,避免 AI 固化錯誤知識", + "writeback": "{count} 筆已有 completion audit", + "recheck": "{count} 筆已回測;距離門檻仍差 {remaining} 筆" + } + }, + "next": { + "waitData": "等待治理資料回填", + "clearBlocked": "先處理 blocked owner-review", + "previewReady": "先對 ready item 做單筆乾跑", + "waitOwner": "等待 owner 補齊審核內容", + "queueReview": "先把高優先陳舊 KM 排入審核", + "stable": "目前沒有待處理 owner-review" + }, + "nextDetail": { + "waitData": "completion queue 尚未回應;先刷新或檢查 API 狀態。", + "clearBlocked": "blocked item 需要先補 owner note、dispatch 或必要欄位。", + "previewReady": "先按單筆乾跑取得 plan fingerprint,再由 owner 確認寫回。", + "waitOwner": "已排入審核但還不能寫回;需要 owner 補內容或確認策略。", + "queueReview": "stale candidate 還沒進 owner-review;先用批次乾跑或單筆排入審核。", + "stable": "目前 completion queue 沒有待處理項;持續看 stale ratio 是否低於門檻。" + }, + "guardrail": { + "writesOnRead": "讀取即寫入:{value}", + "manualReview": "人工覆核必要:{value}", + "batchWrites": "批次寫入允許:{value}" + } + }, "ownerReviewInbox": { "title": "Owner review 工作台", "subtitle": "顯示已排入 waiting_owner_review 的 P0/P1 KM,逐筆乾跑與確認完成。", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 6713fc1ea..288bf8a24 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -3250,6 +3250,68 @@ "queued": "已排入 owner review", "already_queued": "已在 owner review" }, + "operationRail": { + "title": "Owner Review 操作軌道", + "subtitle": "把陳舊 KM 從偵測、審核、乾跑、確認、寫回到比例回測收斂成一條可掃描流程。", + "nextAction": "建議下一步", + "guardrailTitle": "寫入防護", + "metric": { + "ratio": "陳舊比例", + "pending": "待審核", + "completed": "已完成", + "remaining": "距門檻", + "threshold": "門檻 {threshold}", + "readyBlocked": "可乾跑 {ready} / 卡住 {blocked}", + "failed": "失敗 {failed}", + "total": "陳舊 {stale} / 總數 {total}" + }, + "state": { + "warning": "需處理", + "ready": "可操作", + "waiting": "等待", + "done": "已有證據", + "blocked": "卡住" + }, + "flow": { + "node": { + "detected": "偵測", + "ownerReview": "Owner Review", + "dryRun": "乾跑預覽", + "ownerConfirm": "Owner 確認", + "writeback": "寫回 KM", + "recheck": "比例回測" + }, + "detail": { + "detected": "目前 {ratio};門檻 {threshold}", + "ownerReview": "{count} 筆等待 owner 審核", + "dryRun": "{ready} 筆可乾跑;{blocked} 筆卡住", + "ownerConfirm": "確認後才允許寫回,避免 AI 固化錯誤知識", + "writeback": "{count} 筆已有 completion audit", + "recheck": "{count} 筆已回測;距離門檻仍差 {remaining} 筆" + } + }, + "next": { + "waitData": "等待治理資料回填", + "clearBlocked": "先處理 blocked owner-review", + "previewReady": "先對 ready item 做單筆乾跑", + "waitOwner": "等待 owner 補齊審核內容", + "queueReview": "先把高優先陳舊 KM 排入審核", + "stable": "目前沒有待處理 owner-review" + }, + "nextDetail": { + "waitData": "completion queue 尚未回應;先刷新或檢查 API 狀態。", + "clearBlocked": "blocked item 需要先補 owner note、dispatch 或必要欄位。", + "previewReady": "先按單筆乾跑取得 plan fingerprint,再由 owner 確認寫回。", + "waitOwner": "已排入審核但還不能寫回;需要 owner 補內容或確認策略。", + "queueReview": "stale candidate 還沒進 owner-review;先用批次乾跑或單筆排入審核。", + "stable": "目前 completion queue 沒有待處理項;持續看 stale ratio 是否低於門檻。" + }, + "guardrail": { + "writesOnRead": "讀取即寫入:{value}", + "manualReview": "人工覆核必要:{value}", + "batchWrites": "批次寫入允許:{value}" + } + }, "ownerReviewInbox": { "title": "Owner review 工作台", "subtitle": "顯示已排入 waiting_owner_review 的 P0/P1 KM,逐筆乾跑與確認完成。", 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 5a5be6a63..92eaab86e 100644 --- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx +++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx @@ -3532,6 +3532,151 @@ function KnowledgeGovernancePanel({ const reviewCount = items.filter((item) => item.dispatch_status === "skipped" || item.workflow_stage === "waiting_owner_review" ).length; + const ownerReviewOperation = useMemo(() => { + const staleCount = burnDownSnapshot?.stale_count ?? staleCandidates?.total_stale ?? 0; + const totalCount = burnDownSnapshot?.total_count ?? 0; + const staleRatio = burnDownSnapshot + ? formatStaleRatio(burnDownSnapshot.stale_ratio) + : "--"; + const threshold = burnDownSnapshot + ? formatStaleRatio(burnDownSnapshot.threshold) + : "--"; + const pending = completionQueue?.pending_count ?? ownerReviewInbox?.total ?? 0; + const ready = completionQueue?.ready_count ?? 0; + const blocked = completionQueue?.blocked_count ?? 0; + const completed = completionQueue?.completed_count ?? burnDown?.completed_owner_reviews ?? 0; + const failed = completionQueue?.failed_count ?? 0; + const auditTotal = burnDown?.completion_audit_total ?? completed; + const recheckTotal = burnDown?.stale_ratio_recheck_total ?? 0; + const remaining = burnDown?.entries_to_threshold ?? 0; + const writesOnRead = Boolean( + staleCandidates?.writes_on_read || + ownerReviewInbox?.writes_on_read || + completionQueue?.writes_on_read || + burnDown?.writes_on_read + ); + const manualReviewRequired = Boolean( + staleCandidates?.manual_review_required ?? + ownerReviewInbox?.manual_review_required ?? + completionQueue?.manual_review_required ?? + burnDown?.manual_review_required ?? + true + ); + const batchWritesAllowed = Boolean(completionQueue?.batch_writes_allowed ?? false); + const nextActionKey = + completionQueue === null + ? "waitData" + : blocked > 0 + ? "clearBlocked" + : ready > 0 + ? "previewReady" + : pending > 0 + ? "waitOwner" + : staleCount > 0 + ? "queueReview" + : "stable"; + + const nodeTone = (state: "warning" | "ready" | "waiting" | "done" | "blocked") => { + switch (state) { + case "warning": + return "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]"; + case "ready": + return "border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]"; + case "done": + return "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]"; + case "blocked": + return "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]"; + default: + return "border-[#d8d3c7] bg-white text-[#5f5b52]"; + } + }; + + const flowNodes = [ + { + key: "detected", + icon: TriangleAlert, + value: staleCount, + state: burnDown?.burn_down_status === "above_threshold" ? "warning" : "done", + detail: t("staleCandidates.operationRail.flow.detail.detected", { + ratio: staleRatio, + threshold, + }), + }, + { + key: "ownerReview", + icon: ClipboardList, + value: pending, + state: pending > 0 ? "warning" : "done", + detail: t("staleCandidates.operationRail.flow.detail.ownerReview", { + count: pending, + }), + }, + { + key: "dryRun", + icon: SearchCheck, + value: ready, + state: blocked > 0 ? "blocked" : ready > 0 ? "ready" : "waiting", + detail: t("staleCandidates.operationRail.flow.detail.dryRun", { + ready, + blocked, + }), + }, + { + key: "ownerConfirm", + icon: ShieldCheck, + value: ready, + state: ready > 0 ? "ready" : failed > 0 ? "blocked" : "waiting", + detail: t("staleCandidates.operationRail.flow.detail.ownerConfirm"), + }, + { + key: "writeback", + icon: Database, + value: auditTotal, + state: auditTotal > 0 ? "done" : "waiting", + detail: t("staleCandidates.operationRail.flow.detail.writeback", { + count: auditTotal, + }), + }, + { + key: "recheck", + icon: RefreshCw, + value: recheckTotal, + state: recheckTotal > 0 ? "done" : "waiting", + detail: t("staleCandidates.operationRail.flow.detail.recheck", { + count: recheckTotal, + remaining, + }), + }, + ].map((node) => ({ + ...node, + tone: nodeTone(node.state as "warning" | "ready" | "waiting" | "done" | "blocked"), + })); + + return { + staleCount, + totalCount, + staleRatio, + threshold, + pending, + ready, + blocked, + completed, + failed, + remaining, + writesOnRead, + manualReviewRequired, + batchWritesAllowed, + nextActionKey, + flowNodes, + }; + }, [ + burnDown, + burnDownSnapshot, + completionQueue, + ownerReviewInbox, + staleCandidates, + t, + ]); const previewArchiveDuplicates = useCallback(async (group: KnowledgeReviewDraftDedupeGroup) => { setArchiveActions((current) => ({ ...current, @@ -4016,6 +4161,119 @@ function KnowledgeGovernancePanel({ +
+ {t("staleCandidates.operationRail.title")} +
++ {t("staleCandidates.operationRail.subtitle")} +
++ {t(`staleCandidates.operationRail.metric.${metric.key}` as never)} +
++ {metric.value} +
++ {metric.detail} +
++ {t(`staleCandidates.operationRail.flow.node.${node.key}` as never)} +
++ {node.value} +
++ {node.detail} +
++ {t("staleCandidates.operationRail.nextAction")} +
++ {t(`staleCandidates.operationRail.next.${ownerReviewOperation.nextActionKey}` as never)} +
++ {t(`staleCandidates.operationRail.nextDetail.${ownerReviewOperation.nextActionKey}` as never)} +
++ {t("staleCandidates.operationRail.guardrailTitle")} +
+{t("staleCandidates.operationRail.guardrail.writesOnRead", { value: String(ownerReviewOperation.writesOnRead) })}
+{t("staleCandidates.operationRail.guardrail.manualReview", { value: String(ownerReviewOperation.manualReviewRequired) })}
+{t("staleCandidates.operationRail.guardrail.batchWrites", { value: String(ownerReviewOperation.batchWritesAllowed) })}
+