diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index c89d1ab7..7de309d0 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -292,6 +292,121 @@ "unknown": "Unknown state" }, "topGap": "Largest current gap: {gate}, {count} items." + }, + "automationDelivery": { + "eyebrow": "AI Automation Product Surface", + "title": "Delivered Work And Remaining Work", + "subtitle": "The homepage now summarizes production truth-chain, Telegram callbacks, AI providers, KM, Ansible, and auto-repair quality instead of vague KPIs.", + "claimLabel": "Full Auto-Repair Claim", + "claimReady": "Full loop can be claimed", + "claimBlocked": "Full loop cannot be claimed yet", + "claimLoading": "Reading production truth", + "claimUnavailable": "Production truth is not responding", + "claimDetail": "Verified {verified}/{evaluated}, average score {score}", + "unavailableValue": "no response", + "deliveredTitle": "Delivered Capabilities", + "remainingTitle": "Remaining Gaps", + "openWorkItems": "Open Work Items", + "openRuns": "Open Runs", + "status": { + "live": "Live", + "progress": "In Progress", + "blocked": "Blocked", + "watching": "Watching", + "loading": "Loading", + "unavailable": "No response" + }, + "delivered": { + "cicdTimeline": { + "title": "CI/CD notifications enter AwoooP Timeline", + "detail": "Gitea main deploys, deploy markers, and post-deploy notifications flow through the AWOOI API and AwoooP Run Timeline." + }, + "callbackEvidence": { + "title": "Telegram detail / history DB truth chain", + "detail": "{total} callback evidence rows are available for Run detail, history, and snapshot lookup." + }, + "callbackTrace": { + "title": "Callback trace recovery and backlog action lens", + "detail": "Recovery {status}, traced after gap {recovered}, 24h backlog {recent24h}." + }, + "aiRoute": { + "title": "AI Provider lane visibility", + "detail": "Current lane={lane}, selected provider={provider}; governance order is GCP-A / GCP-B / 111 / Gemini." + } + }, + "remaining": { + "fullAutoRepairClaim": { + "title": "Full auto-repair loop", + "detail": "Production quality is verified {verified}/{evaluated}; the system cannot claim full automation before this reaches the gate." + }, + "qualityGateBacklog": { + "title": "Auto-repair quality gate backlog", + "detail": "Top gap {gate}, {count} rows; execution, auto-repair, approval, or learning evidence must be completed." + }, + "ansibleRuntime": { + "title": "Ansible check-mode / apply wiring", + "detail": "check-mode {checkMode}, pending check-mode {pending}; blocker={blocker}." + }, + "kmGovernance": { + "title": "Stale KM governance", + "detail": "{stale} KM rows are older than {days} days; Hermes drafts, owner review, and writeback remain." + }, + "callbackBacklogDecay": { + "title": "Callback legacy backlog 24h decay", + "detail": "Missing trace total {missing}, 1h {recent1h}, 24h {recent24h}; closes only when 24h reaches zero." + } + } + }, + "automationDiagrams": { + "eyebrow": "Professional Visual Views", + "title": "Technical Diagrams For The Product", + "openTopology": "Open Topology", + "cards": { + "c4Runtime": { + "standard": "C4 / Deployment", + "title": "Product Architecture And Runtime Topology", + "detail": "Use C4 layers to explain users, Web, API, K8s, databases, external tools, and model providers.", + "nodes": { + "user": "Operator / Tenant", + "web": "AwoooP Web", + "api": "AWOOI API", + "k8s": "K8s / Providers" + } + }, + "incidentFlow": { + "standard": "BPMN / Swimlane", + "title": "Alert-To-Repair Flow", + "detail": "Use swimlanes to separate Telegram, OpenClaw, Hermes, MCP, Ansible, human approval, and verification ownership.", + "nodes": { + "alert": "Alert / Sentry / SigNoz", + "ai": "AI Analysis", + "playbook": "PlayBook / MCP", + "verify": "Verify / KM" + } + }, + "decisionRules": { + "standard": "DMN / Decision Table", + "title": "AI Decision And Approval Rules", + "detail": "Represent risk, confidence, policy, model routing, and auto-repair eligibility as auditable decision tables.", + "nodes": { + "risk": "Risk", + "confidence": "Confidence", + "policy": "Policy", + "approval": "Approval" + } + }, + "evidenceLineage": { + "standard": "Trace / Lineage", + "title": "Evidence Chain And Callback Trace", + "detail": "Show whether Telegram messages, DB events, Run Timeline, and KM / PlayBook writeback agree.", + "nodes": { + "telegram": "Telegram", + "db": "DB Truth", + "trace": "Run Trace", + "km": "KM / PlayBook" + } + } + } } }, "openclaw": { diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 3b3c4fed..7cf5276e 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -293,6 +293,121 @@ "unknown": "狀態未知" }, "topGap": "目前最大缺口:{gate},共 {count} 筆。" + }, + "automationDelivery": { + "eyebrow": "AI 自動化管理產品面", + "title": "目前完成項與待推進項", + "subtitle": "首頁直接呈現 production 真相鏈、Telegram callback、AI provider、KM、Ansible 與自動修復品質,不再只放空泛 KPI。", + "claimLabel": "完整自動修復宣稱", + "claimReady": "可宣稱完整閉環", + "claimBlocked": "尚不可宣稱完整閉環", + "claimLoading": "正在讀取 production 真相", + "claimUnavailable": "production 真相暫時未回應", + "claimDetail": "已驗證 {verified}/{evaluated},平均分數 {score}", + "unavailableValue": "未回應", + "deliveredTitle": "已上線能力", + "remainingTitle": "仍待推進缺口", + "openWorkItems": "打開 Work Items", + "openRuns": "打開 Runs", + "status": { + "live": "已上線", + "progress": "推進中", + "blocked": "阻塞", + "watching": "觀察", + "loading": "讀取中", + "unavailable": "未回應" + }, + "delivered": { + "cicdTimeline": { + "title": "CI/CD 通知進 AwoooP Timeline", + "detail": "Gitea main 推版、deploy marker、post-deploy 通知已走 AWOOI API 與 AwoooP Run Timeline。" + }, + "callbackEvidence": { + "title": "Telegram 詳情 / 歷史 DB 真相鏈", + "detail": "callback evidence 目前 {total} 筆,可從 Runs 反查詳情、歷史與快照。" + }, + "callbackTrace": { + "title": "Callback trace 復原與 backlog action lens", + "detail": "復原狀態 {status},gap 後 traced {recovered},24h backlog {recent24h}。" + }, + "aiRoute": { + "title": "AI Provider lane 可視化", + "detail": "目前 lane={lane},selected provider={provider};順序以 GCP-A / GCP-B / 111 / Gemini 為治理方向。" + } + }, + "remaining": { + "fullAutoRepairClaim": { + "title": "完整自動修復閉環", + "detail": "production quality 目前 verified {verified}/{evaluated};未達標前不能宣稱全自動完成。" + }, + "qualityGateBacklog": { + "title": "自動修復品質閘門缺口", + "detail": "最大缺口 {gate},目前 {count} 筆;需補 execution、auto-repair、approval 或 learning evidence。" + }, + "ansibleRuntime": { + "title": "Ansible check-mode / apply 接線", + "detail": "check-mode {checkMode},待 check-mode {pending};目前 blocker={blocker}。" + }, + "kmGovernance": { + "title": "KM 陳舊資料治理", + "detail": "超過 {days} 天未更新 KM:{stale} 筆;需 Hermes 產草稿、owner 審核後回寫。" + }, + "callbackBacklogDecay": { + "title": "Callback legacy backlog 24h decay", + "detail": "缺 trace 總數 {missing},1h {recent1h},24h {recent24h};24h 歸零才算關閉。" + } + } + }, + "automationDiagrams": { + "eyebrow": "專業圖像化視圖", + "title": "產品要用哪些圖來呈現", + "openTopology": "查看拓樸圖", + "cards": { + "c4Runtime": { + "standard": "C4 / Deployment", + "title": "產品架構與 Runtime 拓樸", + "detail": "用 C4 分層說明使用者、Web、API、K8s、資料庫、外部工具與模型供應商的關係。", + "nodes": { + "user": "Operator / Tenant", + "web": "AwoooP Web", + "api": "AWOOI API", + "k8s": "K8s / Providers" + } + }, + "incidentFlow": { + "standard": "BPMN / Swimlane", + "title": "告警到修復流程", + "detail": "用泳道圖拆開 Telegram、OpenClaw、Hermes、MCP、Ansible、人工審批與驗證責任。", + "nodes": { + "alert": "Alert / Sentry / SigNoz", + "ai": "AI 分析", + "playbook": "PlayBook / MCP", + "verify": "驗證 / KM" + } + }, + "decisionRules": { + "standard": "DMN / Decision Table", + "title": "AI 判斷與審批規則", + "detail": "把風險、信心分數、政策、模型路由與是否自動修復整理成可稽核決策表。", + "nodes": { + "risk": "Risk", + "confidence": "Confidence", + "policy": "Policy", + "approval": "Approval" + } + }, + "evidenceLineage": { + "standard": "Trace / Lineage", + "title": "證據鏈與 Callback Trace", + "detail": "用 trace lineage 呈現 Telegram 訊息、DB 事件、Run Timeline、KM / PlayBook 回寫是否一致。", + "nodes": { + "telegram": "Telegram", + "db": "DB Truth", + "trace": "Run Trace", + "km": "KM / PlayBook" + } + } + } } }, "openclaw": { diff --git a/apps/web/src/app/[locale]/page.tsx b/apps/web/src/app/[locale]/page.tsx index 8fe77a1d..1ee9761b 100644 --- a/apps/web/src/app/[locale]/page.tsx +++ b/apps/web/src/app/[locale]/page.tsx @@ -37,10 +37,95 @@ const STATUS_CHAIN_PREFETCH_LIMIT = 25 const HOMEPAGE_INCIDENT_LIMIT = STATUS_CHAIN_PREFETCH_LIMIT interface HomepageAutomationQualitySummary { + average_score?: number evaluated_total?: number verified_auto_repair_total?: number + gate_failures?: HomepageAutomationGateFailure[] + execution_backend_summary?: HomepageExecutionBackendSummary | null + ansible_runtime?: HomepageAnsibleRuntime | null production_claim?: { can_claim_full_auto_repair?: boolean + reason?: string | null + } +} + +interface HomepageAutomationGateFailure { + gate?: string | null + total?: number | null + statuses?: Record +} + +interface HomepageExecutionBackendSummary { + operation_records_total?: number + effective_execution_records_total?: number + audit_only_operation_records_total?: number + auto_repair_execution_records_total?: number + ansible_candidate_total?: number + ansible_check_mode_total?: number + ansible_apply_total?: number + ansible_pending_check_mode_total?: number +} + +interface HomepageAnsibleRuntime { + can_run_check_mode?: boolean + blockers?: string[] +} + +interface HomepageCallbackReplyAuditSummary { + outbound_reply_markup_missing_trace_ref_total?: number + outbound_reply_markup_missing_trace_ref_recent_1h_total?: number + outbound_reply_markup_missing_trace_ref_recent_24h_total?: number + outbound_reply_markup_trace_ref_after_gap_total?: number + outbound_reply_markup_trace_ref_gap_status?: string | null + outbound_reply_markup_trace_ref_gap_next_action?: string | null + outbound_reply_markup_trace_ref_gap_recovery_status?: string | null +} + +interface HomepageCallbackRepliesResponse { + total?: number + summary?: HomepageCallbackReplyAuditSummary | null +} + +interface HomepageAiRouteStatusResponse { + lane_mode?: string | null + selected_provider?: string | null + skipped_lanes?: Array<{ provider_name?: string | null }> + operator_action?: { + human_required?: boolean | null + action?: string | null + reason?: string | null + } | null +} + +interface HomepageKmStaleCandidatesResponse { + total_stale?: number + threshold_days?: number +} + +interface HomepageAutomationBriefSnapshot { + callbackReplies?: HomepageCallbackRepliesResponse | null + aiRouteStatus?: HomepageAiRouteStatusResponse | null + kmStaleCandidates?: HomepageKmStaleCandidatesResponse | null +} + +type HomepageWorkTone = 'live' | 'progress' | 'blocked' | 'watching' + +interface HomepageWorkItemSummary { + key: string + title: string + status: string + detail: string + href: string + tone: HomepageWorkTone +} + +async function fetchHomepageJson(url: string, signal?: AbortSignal): Promise { + try { + const response = await fetch(url, { signal, cache: 'no-store' }) + if (!response.ok) return null + return await response.json() as T + } catch { + return null } } @@ -547,6 +632,9 @@ export default function Home({ params }: { params: { locale: string } }) { // 2026-04-07 Claude Code: Sprint 4 E2 — 從 disposition API 取得真實自動化率 const [dispositionRate, setDispositionRate] = useState<{ auto_rate: number; total: number } | null>(null) const [automationQuality, setAutomationQuality] = useState(null) + const [automationQualityLoaded, setAutomationQualityLoaded] = useState(false) + const [automationBrief, setAutomationBrief] = useState({}) + const [automationBriefLoaded, setAutomationBriefLoaded] = useState(false) useEffect(() => { fetch(`${API_BASE}/api/v1/stats/disposition`) .then(r => r.json()) @@ -564,20 +652,55 @@ export default function Home({ params }: { params: { locale: string } }) { .then(d => { if (!d) return setAutomationQuality({ + average_score: typeof d.average_score === 'number' ? d.average_score : undefined, evaluated_total: typeof d.evaluated_total === 'number' ? d.evaluated_total : undefined, verified_auto_repair_total: typeof d.verified_auto_repair_total === 'number' ? d.verified_auto_repair_total : undefined, + gate_failures: Array.isArray(d.gate_failures) ? d.gate_failures : [], + execution_backend_summary: d.execution_backend_summary ?? null, + ansible_runtime: d.ansible_runtime ?? null, production_claim: d.production_claim, }) }) .catch(() => {}) + .finally(() => setAutomationQualityLoaded(true)) + return () => controller.abort() + }, []) + useEffect(() => { + const controller = new AbortController() + const encodedProjectId = encodeURIComponent('awoooi') + Promise.all([ + fetchHomepageJson( + `${API_BASE}/api/v1/platform/runs/callback-replies?project_id=${encodedProjectId}&per_page=20`, + controller.signal + ), + fetchHomepageJson( + `${API_BASE}/api/v1/platform/ai-route-status?workload_type=deep_rca`, + controller.signal + ), + fetchHomepageJson( + `${API_BASE}/api/v1/ai/governance/km-stale-candidates?project_id=${encodedProjectId}&limit=5`, + controller.signal + ), + ]).then(([callbackReplies, aiRouteStatus, kmStaleCandidates]) => { + setAutomationBrief({ callbackReplies, aiRouteStatus, kmStaleCandidates }) + }).catch(() => {}) + .finally(() => setAutomationBriefLoaded(true)) return () => controller.abort() }, []) // 自動處置率 — 首頁 KPI 使用 24h truth-chain 驗證率,避免把歷史 disposition 總表誤讀成今日閉環。 - const evaluatedAutomationTotal = automationQuality?.evaluated_total ?? 0 - const verifiedAutomationTotal = automationQuality?.verified_auto_repair_total ?? 0 + const automationQualityAvailable = automationQuality !== null + const unavailableValue = tDashboard('automationDelivery.unavailableValue') + const loadingStatus = tDashboard('automationDelivery.status.loading') + const unavailableStatus = tDashboard('automationDelivery.status.unavailable') + const evaluatedAutomationTotal = automationQuality?.evaluated_total + const verifiedAutomationTotal = automationQuality?.verified_auto_repair_total + const formatAutomationNumber = (value: number | null | undefined, loaded: boolean) => { + if (typeof value === 'number') return value.toLocaleString() + return loaded ? unavailableValue : loadingStatus + } const autoRemediationRate = (() => { - if (evaluatedAutomationTotal > 0) { + if (typeof evaluatedAutomationTotal === 'number' && evaluatedAutomationTotal > 0 && typeof verifiedAutomationTotal === 'number') { return `${Math.round((verifiedAutomationTotal / evaluatedAutomationTotal) * 100)}%` } return '--' @@ -585,17 +708,17 @@ export default function Home({ params }: { params: { locale: string } }) { // 自動處置率數值 (for progress bar) const autoRemediationPct = (() => { - if (evaluatedAutomationTotal > 0) { + if (typeof evaluatedAutomationTotal === 'number' && evaluatedAutomationTotal > 0 && typeof verifiedAutomationTotal === 'number') { return Math.round((verifiedAutomationTotal / evaluatedAutomationTotal) * 100) } return 0 })() const autoRemediationTone = automationQuality?.production_claim?.can_claim_full_auto_repair ? '#22C55E' - : evaluatedAutomationTotal > 0 + : typeof evaluatedAutomationTotal === 'number' && evaluatedAutomationTotal > 0 ? '#F59E0B' : '#141413' - const autoRemediationDetail = evaluatedAutomationTotal > 0 + const autoRemediationDetail = typeof evaluatedAutomationTotal === 'number' && evaluatedAutomationTotal > 0 && typeof verifiedAutomationTotal === 'number' ? tDashboard('autoRepairVerifiedCount', { verified: verifiedAutomationTotal, evaluated: evaluatedAutomationTotal, @@ -606,6 +729,240 @@ export default function Home({ params }: { params: { locale: string } }) { total: dispositionRate.total.toLocaleString(), }) : null + const topAutomationGate = automationQuality?.gate_failures?.[0] ?? null + const executionBackend = automationQuality?.execution_backend_summary ?? null + const ansibleRuntime = automationQuality?.ansible_runtime ?? null + const callbackTraceSummary = automationBrief.callbackReplies?.summary ?? null + const missingTraceRecent24h = callbackTraceSummary?.outbound_reply_markup_missing_trace_ref_recent_24h_total ?? 0 + const traceRecoveryStatus = callbackTraceSummary?.outbound_reply_markup_trace_ref_gap_recovery_status + ?? (automationBriefLoaded ? unavailableValue : loadingStatus) + const traceRecoveryCount = callbackTraceSummary?.outbound_reply_markup_trace_ref_after_gap_total + const aiRouteSelectedProvider = automationBrief.aiRouteStatus?.selected_provider ?? '--' + const aiRouteLaneMode = automationBrief.aiRouteStatus?.lane_mode ?? '--' + const kmStaleTotal = automationBrief.kmStaleCandidates?.total_stale + const kmStaleThreshold = automationBrief.kmStaleCandidates?.threshold_days + const hasCallbackEvidence = automationBrief.callbackReplies !== undefined && automationBrief.callbackReplies !== null + const hasAiRouteStatus = automationBrief.aiRouteStatus !== undefined && automationBrief.aiRouteStatus !== null + const hasKmStaleCandidates = automationBrief.kmStaleCandidates !== undefined && automationBrief.kmStaleCandidates !== null + const canClaimFullAutoRepair = automationQualityAvailable && Boolean(automationQuality?.production_claim?.can_claim_full_auto_repair) + const automationWorkToneStyle: Record = { + live: { bg: '#f0faf2', border: '#9bc7a4', color: '#17602a' }, + progress: { bg: '#fff7e8', border: '#d9b36f', color: '#8a5a08' }, + blocked: { bg: '#fff0ef', border: '#e2a29b', color: '#9f2f25' }, + watching: { bg: '#eef5ff', border: '#9bb6d9', color: '#1f5b9b' }, + } + const automationDeliveryHeadline = !automationQualityLoaded + ? tDashboard('automationDelivery.claimLoading') + : canClaimFullAutoRepair + ? tDashboard('automationDelivery.claimReady') + : automationQualityAvailable + ? tDashboard('automationDelivery.claimBlocked') + : tDashboard('automationDelivery.claimUnavailable') + const automationDeliveryClaimTone = canClaimFullAutoRepair + ? automationWorkToneStyle.live + : automationQualityLoaded && automationQualityAvailable + ? automationWorkToneStyle.progress + : automationWorkToneStyle.watching + const automationDeliveryItems: HomepageWorkItemSummary[] = [ + { + key: 'cicdTimeline', + title: tDashboard('automationDelivery.delivered.cicdTimeline.title'), + status: tDashboard('automationDelivery.status.live'), + detail: tDashboard('automationDelivery.delivered.cicdTimeline.detail'), + href: `/${locale}/awooop/runs`, + tone: 'live', + }, + { + key: 'callbackEvidence', + title: tDashboard('automationDelivery.delivered.callbackEvidence.title'), + status: hasCallbackEvidence + ? tDashboard('automationDelivery.status.live') + : automationBriefLoaded + ? unavailableStatus + : loadingStatus, + detail: tDashboard('automationDelivery.delivered.callbackEvidence.detail', { + total: formatAutomationNumber(automationBrief.callbackReplies?.total, automationBriefLoaded), + }), + href: `/${locale}/awooop/runs?project_id=awoooi#tg-callback-evidence`, + tone: hasCallbackEvidence ? 'live' : 'watching', + }, + { + key: 'callbackTrace', + title: tDashboard('automationDelivery.delivered.callbackTrace.title'), + status: callbackTraceSummary && traceRecoveryStatus === 'recovered_after_gap' + ? tDashboard('automationDelivery.status.progress') + : !automationBriefLoaded + ? loadingStatus + : callbackTraceSummary + ? tDashboard('automationDelivery.status.watching') + : unavailableStatus, + detail: tDashboard('automationDelivery.delivered.callbackTrace.detail', { + status: traceRecoveryStatus, + recovered: formatAutomationNumber(traceRecoveryCount, automationBriefLoaded), + recent24h: formatAutomationNumber(callbackTraceSummary?.outbound_reply_markup_missing_trace_ref_recent_24h_total, automationBriefLoaded), + }), + href: `/${locale}/awooop/work-items?project_id=awoooi`, + tone: traceRecoveryStatus === 'recovered_after_gap' ? 'progress' : 'watching', + }, + { + key: 'aiRoute', + title: tDashboard('automationDelivery.delivered.aiRoute.title'), + status: hasAiRouteStatus + ? tDashboard('automationDelivery.status.live') + : automationBriefLoaded + ? unavailableStatus + : loadingStatus, + detail: tDashboard('automationDelivery.delivered.aiRoute.detail', { + provider: aiRouteSelectedProvider, + lane: aiRouteLaneMode, + }), + href: `/${locale}/awooop/work-items?project_id=awoooi`, + tone: hasAiRouteStatus ? 'live' : 'watching', + }, + ] + const automationRemainingItems: HomepageWorkItemSummary[] = [ + { + key: 'fullAutoRepairClaim', + title: tDashboard('automationDelivery.remaining.fullAutoRepairClaim.title'), + status: !automationQualityLoaded + ? loadingStatus + : canClaimFullAutoRepair + ? tDashboard('automationDelivery.status.live') + : automationQualityAvailable + ? tDashboard('automationDelivery.status.blocked') + : unavailableStatus, + detail: tDashboard('automationDelivery.remaining.fullAutoRepairClaim.detail', { + verified: formatAutomationNumber(verifiedAutomationTotal, automationQualityLoaded), + evaluated: formatAutomationNumber(evaluatedAutomationTotal, automationQualityLoaded), + }), + href: `/${locale}/awooop/work-items?project_id=awoooi`, + tone: canClaimFullAutoRepair ? 'live' : automationQualityAvailable ? 'blocked' : 'watching', + }, + { + key: 'qualityGateBacklog', + title: tDashboard('automationDelivery.remaining.qualityGateBacklog.title'), + status: !automationQualityLoaded + ? loadingStatus + : topAutomationGate + ? tDashboard('automationDelivery.status.blocked') + : automationQualityAvailable + ? tDashboard('automationDelivery.status.live') + : unavailableStatus, + detail: tDashboard('automationDelivery.remaining.qualityGateBacklog.detail', { + gate: topAutomationGate?.gate ?? (automationQualityLoaded ? unavailableValue : loadingStatus), + count: formatAutomationNumber(topAutomationGate?.total, automationQualityLoaded), + }), + href: `/${locale}/awooop/runs?project_id=awoooi`, + tone: topAutomationGate ? 'blocked' : automationQualityAvailable ? 'live' : 'watching', + }, + { + key: 'ansibleRuntime', + title: tDashboard('automationDelivery.remaining.ansibleRuntime.title'), + status: !automationQualityLoaded + ? loadingStatus + : ansibleRuntime?.can_run_check_mode + ? tDashboard('automationDelivery.status.live') + : automationQualityAvailable + ? tDashboard('automationDelivery.status.blocked') + : unavailableStatus, + detail: tDashboard('automationDelivery.remaining.ansibleRuntime.detail', { + checkMode: formatAutomationNumber(executionBackend?.ansible_check_mode_total, automationQualityLoaded), + pending: formatAutomationNumber(executionBackend?.ansible_pending_check_mode_total, automationQualityLoaded), + blocker: ansibleRuntime?.blockers?.join(' / ') || (automationQualityLoaded ? unavailableValue : loadingStatus), + }), + href: `/${locale}/awooop/work-items?project_id=awoooi`, + tone: ansibleRuntime?.can_run_check_mode ? 'live' : automationQualityAvailable ? 'blocked' : 'watching', + }, + { + key: 'kmGovernance', + title: tDashboard('automationDelivery.remaining.kmGovernance.title'), + status: !automationBriefLoaded + ? loadingStatus + : typeof kmStaleTotal === 'number' && kmStaleTotal > 0 + ? tDashboard('automationDelivery.status.progress') + : hasKmStaleCandidates + ? tDashboard('automationDelivery.status.live') + : unavailableStatus, + detail: tDashboard('automationDelivery.remaining.kmGovernance.detail', { + stale: formatAutomationNumber(kmStaleTotal, automationBriefLoaded), + days: formatAutomationNumber(kmStaleThreshold, automationBriefLoaded), + }), + href: `/${locale}/awooop/work-items?project_id=awoooi`, + tone: typeof kmStaleTotal === 'number' && kmStaleTotal > 0 ? 'progress' : hasKmStaleCandidates ? 'live' : 'watching', + }, + { + key: 'callbackBacklogDecay', + title: tDashboard('automationDelivery.remaining.callbackBacklogDecay.title'), + status: !automationBriefLoaded + ? loadingStatus + : callbackTraceSummary && missingTraceRecent24h > 0 + ? tDashboard('automationDelivery.status.progress') + : callbackTraceSummary + ? tDashboard('automationDelivery.status.live') + : unavailableStatus, + detail: tDashboard('automationDelivery.remaining.callbackBacklogDecay.detail', { + recent1h: formatAutomationNumber(callbackTraceSummary?.outbound_reply_markup_missing_trace_ref_recent_1h_total, automationBriefLoaded), + recent24h: formatAutomationNumber(callbackTraceSummary?.outbound_reply_markup_missing_trace_ref_recent_24h_total, automationBriefLoaded), + missing: formatAutomationNumber(callbackTraceSummary?.outbound_reply_markup_missing_trace_ref_total, automationBriefLoaded), + }), + href: `/${locale}/awooop/runs?project_id=awoooi#tg-callback-evidence`, + tone: callbackTraceSummary && missingTraceRecent24h > 0 ? 'progress' : callbackTraceSummary ? 'live' : 'watching', + }, + ] + const productDiagramCards = [ + { + key: 'c4Runtime', + title: tDashboard('automationDiagrams.cards.c4Runtime.title'), + standard: tDashboard('automationDiagrams.cards.c4Runtime.standard'), + detail: tDashboard('automationDiagrams.cards.c4Runtime.detail'), + href: `/${locale}/topology`, + nodes: [ + tDashboard('automationDiagrams.cards.c4Runtime.nodes.user'), + tDashboard('automationDiagrams.cards.c4Runtime.nodes.web'), + tDashboard('automationDiagrams.cards.c4Runtime.nodes.api'), + tDashboard('automationDiagrams.cards.c4Runtime.nodes.k8s'), + ], + }, + { + key: 'incidentFlow', + title: tDashboard('automationDiagrams.cards.incidentFlow.title'), + standard: tDashboard('automationDiagrams.cards.incidentFlow.standard'), + detail: tDashboard('automationDiagrams.cards.incidentFlow.detail'), + href: `/${locale}/awooop/runs?project_id=awoooi`, + nodes: [ + tDashboard('automationDiagrams.cards.incidentFlow.nodes.alert'), + tDashboard('automationDiagrams.cards.incidentFlow.nodes.ai'), + tDashboard('automationDiagrams.cards.incidentFlow.nodes.playbook'), + tDashboard('automationDiagrams.cards.incidentFlow.nodes.verify'), + ], + }, + { + key: 'decisionRules', + title: tDashboard('automationDiagrams.cards.decisionRules.title'), + standard: tDashboard('automationDiagrams.cards.decisionRules.standard'), + detail: tDashboard('automationDiagrams.cards.decisionRules.detail'), + href: `/${locale}/awooop/approvals`, + nodes: [ + tDashboard('automationDiagrams.cards.decisionRules.nodes.risk'), + tDashboard('automationDiagrams.cards.decisionRules.nodes.confidence'), + tDashboard('automationDiagrams.cards.decisionRules.nodes.policy'), + tDashboard('automationDiagrams.cards.decisionRules.nodes.approval'), + ], + }, + { + key: 'evidenceLineage', + title: tDashboard('automationDiagrams.cards.evidenceLineage.title'), + standard: tDashboard('automationDiagrams.cards.evidenceLineage.standard'), + detail: tDashboard('automationDiagrams.cards.evidenceLineage.detail'), + href: `/${locale}/awooop/runs?project_id=awoooi#tg-callback-evidence`, + nodes: [ + tDashboard('automationDiagrams.cards.evidenceLineage.nodes.telegram'), + tDashboard('automationDiagrams.cards.evidenceLineage.nodes.db'), + tDashboard('automationDiagrams.cards.evidenceLineage.nodes.trace'), + tDashboard('automationDiagrams.cards.evidenceLineage.nodes.km'), + ], + }, + ] // ── 5 KPI Cards (Sprint 5R 設計稿批准版) ──────────────────────────────────── @@ -718,6 +1075,262 @@ export default function Home({ params }: { params: { locale: string } }) { overflow: 'hidden', }}> +
+
+
+
+ {tDashboard('automationDelivery.eyebrow')} +
+

+ {tDashboard('automationDelivery.title')} +

+

+ {tDashboard('automationDelivery.subtitle')} +

+
+
+
+ {tDashboard('automationDelivery.claimLabel')} +
+
+ {automationDeliveryHeadline} +
+
+ {tDashboard('automationDelivery.claimDetail', { + verified: formatAutomationNumber(verifiedAutomationTotal, automationQualityLoaded), + evaluated: formatAutomationNumber(evaluatedAutomationTotal, automationQualityLoaded), + score: automationQuality?.average_score != null + ? automationQuality.average_score.toFixed(1) + : automationQualityLoaded ? unavailableValue : loadingStatus, + })} +
+
+
+ +
+
+
+

+ {tDashboard('automationDelivery.deliveredTitle')} +

+ + {tDashboard('automationDelivery.openWorkItems')} + +
+
+ {automationDeliveryItems.map(item => { + const tone = automationWorkToneStyle[item.tone] + return ( + +
+
{item.title}
+
{item.detail}
+
+ + {item.status} + +
+ ) + })} +
+
+ +
+
+

+ {tDashboard('automationDelivery.remainingTitle')} +

+ + {tDashboard('automationDelivery.openRuns')} + +
+
+ {automationRemainingItems.map(item => { + const tone = automationWorkToneStyle[item.tone] + return ( + +
+
{item.title}
+
{item.detail}
+
+ + {item.status} + +
+ ) + })} +
+
+
+
+ +
+
+
+
+ {tDashboard('automationDiagrams.eyebrow')} +
+

+ {tDashboard('automationDiagrams.title')} +

+
+ + {tDashboard('automationDiagrams.openTopology')} + +
+ +
+ {/* ── KPI Strip (5 卡片 — Sprint 5R 設計稿) ──────────────────────── */}
{/* 系統健康 */}