diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 6f3a52bf..9965d5de 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -1706,6 +1706,33 @@ "ready": "In Sync", "loading": "Loading", "degraded": "Degraded", + "sourceFlow": { + "title": "Source Flow and Work Progress", + "subtitle": "Reads recent Channel Event recurrence data so the overview shows source persistence, run linkage, work items, and source correlation state.", + "sourceEvents": "{count} source events", + "unavailable": "Unavailable", + "loadFailed": "Unable to load the source flow overview. Check the Work Chain or Run Monitor recurrence API.", + "empty": "No source event data is available yet.", + "metrics": { + "linkedRuns": "Run Linkage", + "linkedRunsDetail": "Unlinked events: {unlinked}", + "openWork": "Open Work", + "openWorkDetail": "No repair {gap} / manual gates {manual} / failed repairs {failed}", + "sourceDecision": "Source Decision", + "sourceDecisionNone": "No Review", + "sourceDecisionDetail": "Recorded reviews: {recorded}", + "latest": "Latest Event", + "latestDetail": "{groups} recurrence groups" + }, + "progress": { + "linked": "Source to Run Coverage", + "linkedDetail": "Whether source events can be traced back to Run / Incident", + "work": "Work Item Cleanup", + "workDetail": "Whether recurrence groups still have open work", + "decision": "Source Match Decision", + "decisionDetail": "Whether source review / apply has a decision record" + } + }, "quality": { "title": "Automation Quality", "subtitle": "Whether recent alerts actually reached AI auto-repair, verification, and learning writeback in the last 24 hours.", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 5f344641..a8672b56 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -1707,6 +1707,33 @@ "ready": "同步中", "loading": "讀取中", "degraded": "降級", + "sourceFlow": { + "title": "來源流程與工作進度", + "subtitle": "從 Channel Event recurrence 讀取最近來源事件,讓首頁直接呈現來源落庫、Run 連結、工作項與 source correlation 狀態。", + "sourceEvents": "來源事件 {count}", + "unavailable": "無法讀取", + "loadFailed": "無法讀取來源流程總覽。請回工作鏈路或 Run 監控檢查 recurrence API。", + "empty": "尚無來源事件資料。", + "metrics": { + "linkedRuns": "Run 連結", + "linkedRunsDetail": "未連結事件:{unlinked}", + "openWork": "待處理工作", + "openWorkDetail": "無修復 {gap} / 人工閘門 {manual} / 修復失敗 {failed}", + "sourceDecision": "來源決策", + "sourceDecisionNone": "無待審", + "sourceDecisionDetail": "已記錄審核:{recorded}", + "latest": "最新事件", + "latestDetail": "共 {groups} 個 recurrence group" + }, + "progress": { + "linked": "來源到 Run 覆蓋", + "linkedDetail": "來源事件是否已能回到 Run / Incident", + "work": "工作項清理", + "workDetail": "recurrence group 是否仍有待處理項", + "decision": "來源配對決策", + "decisionDetail": "source review / apply 是否已有決策紀錄" + } + }, "quality": { "title": "自動化品質", "subtitle": "最近 24 小時告警是否真正走到 AI 自動修復、驗證與學習回寫。", diff --git a/apps/web/src/app/[locale]/awooop/page.tsx b/apps/web/src/app/[locale]/awooop/page.tsx index 7b0c15d7..dccfdb29 100644 --- a/apps/web/src/app/[locale]/awooop/page.tsx +++ b/apps/web/src/app/[locale]/awooop/page.tsx @@ -84,6 +84,27 @@ type AutomationQualitySummary = { }; }; +type SourceFlowSummary = { + source_event_total: number; + recurrence_group_total: number; + linked_run_total: number; + unlinked_event_total: number; + open_work_item_group_total?: number; + manual_gate_group_total?: number; + automation_gap_group_total?: number; + failed_repair_group_total?: number; + source_correlation_review_group_total?: number; + source_correlation_decision_recorded_group_total?: number; + source_correlation_applied_group_total?: number; + latest_received_at?: string | null; +}; + +type SourceFlowResponse = { + project_id: string; + limit: number; + summary: SourceFlowSummary; +}; + const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ""; const emptySnapshot: Snapshot = { @@ -304,6 +325,171 @@ function QualityMetric({ ); } +function percentValue(numerator: number, denominator: number): number { + if (denominator <= 0) return 0; + return Math.max(0, Math.min(100, (numerator / denominator) * 100)); +} + +function ProgressRow({ + label, + value, + detail, + percent, + tone, +}: { + label: string; + value: string; + detail: string; + percent: number; + tone: "good" | "warn" | "neutral"; +}) { + return ( +
+
+
+

{label}

+

{detail}

+
+ {value} +
+
+
+
+
+ ); +} + +function SourceFlowOverviewPanel({ + summary, + error, + locale, +}: { + summary: SourceFlowSummary | null; + error: boolean; + locale: string; +}) { + const t = useTranslations("awooop.home.sourceFlow"); + const sourceTotal = summary?.source_event_total ?? 0; + const groupTotal = summary?.recurrence_group_total ?? 0; + const linkedRuns = summary?.linked_run_total ?? 0; + const unlinkedEvents = summary?.unlinked_event_total ?? 0; + const openWork = summary?.open_work_item_group_total ?? 0; + const automationGaps = summary?.automation_gap_group_total ?? 0; + const manualGates = summary?.manual_gate_group_total ?? 0; + const failedRepairs = summary?.failed_repair_group_total ?? 0; + const sourceReview = summary?.source_correlation_review_group_total ?? 0; + const sourceReviewRecorded = summary?.source_correlation_decision_recorded_group_total ?? 0; + const sourceApplied = summary?.source_correlation_applied_group_total ?? 0; + const sourceDecisionKnown = sourceReviewRecorded + sourceApplied; + const sourceDecisionTotal = sourceReview + sourceDecisionKnown; + const linkedPercent = percentValue(linkedRuns, sourceTotal); + const workClearPercent = percentValue(Math.max(0, groupTotal - openWork), groupTotal); + const sourceDecisionPercent = sourceDecisionTotal === 0 + ? 100 + : percentValue(sourceDecisionKnown, sourceDecisionTotal); + const latestTime = summary?.latest_received_at + ? new Date(summary.latest_received_at).toLocaleTimeString(locale === "zh-TW" ? "zh-TW" : "en-US", { + hour: "2-digit", + minute: "2-digit", + }) + : "--"; + + return ( +
+
+
+
+ 0 && "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]", + !error && sourceTotal === 0 && "border-[#d8d3c7] bg-white text-[#5f5b52]" + )} + > + {error ? t("unavailable") : t("sourceEvents", { count: sourceTotal })} + +
+ + {error || !summary ? ( +
+ {error ? t("loadFailed") : t("empty")} +
+ ) : ( +
+
+ + + + +
+ +
+ + + +
+
+ )} +
+ ); +} + function AutomationQualityPanel({ summary, error, @@ -463,6 +649,8 @@ export default function AwoooPPage() { const [snapshot, setSnapshot] = useState(emptySnapshot); const [qualitySummary, setQualitySummary] = useState(null); const [qualityError, setQualityError] = useState(false); + const [sourceFlowSummary, setSourceFlowSummary] = useState(null); + const [sourceFlowError, setSourceFlowError] = useState(false); const [status, setStatus] = useState("loading"); const [lastUpdated, setLastUpdated] = useState(null); @@ -474,13 +662,19 @@ export default function AwoooPPage() { hours: "24", limit: "50", }); - const [tenantRes, runRes, approvalRes, contractRes, qualityRes] = await Promise.all([ + const sourceFlowParams = new URLSearchParams({ + project_id: "awoooi", + limit: "100", + }); + const [tenantRes, runRes, approvalRes, contractRes, qualityRes, sourceFlowRes] = await Promise.all([ fetch(`${API_BASE}/api/v1/platform/tenants`), fetch(`${API_BASE}/api/v1/platform/runs/list?per_page=1`), fetch(`${API_BASE}/api/v1/platform/approvals`), fetch(`${API_BASE}/api/v1/platform/contracts?per_page=1`), fetch(`${API_BASE}/api/v1/platform/truth-chain/quality/summary?${qualityParams.toString()}`) .catch(() => null), + fetch(`${API_BASE}/api/v1/platform/events/dossier/recurrence?${sourceFlowParams.toString()}`) + .catch(() => null), ]); if (![tenantRes, runRes, approvalRes, contractRes].every((res) => res.ok)) { @@ -511,12 +705,22 @@ export default function AwoooPPage() { setQualitySummary(null); setQualityError(true); } + if (sourceFlowRes?.ok) { + const sourceFlowData = await sourceFlowRes.json() as SourceFlowResponse; + setSourceFlowSummary(sourceFlowData.summary); + setSourceFlowError(false); + } else { + setSourceFlowSummary(null); + setSourceFlowError(true); + } setLastUpdated(new Date()); setStatus("ready"); } catch { setStatus("degraded"); setQualitySummary(null); setQualityError(true); + setSourceFlowSummary(null); + setSourceFlowError(true); setLastUpdated(new Date()); } }, []); @@ -631,6 +835,12 @@ export default function AwoooPPage() { /> + +