From df06c025ffafce5cae6a5ac97bb73f39fbe478cb Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 24 May 2026 12:15:02 +0800 Subject: [PATCH] fix(web): show ai route fallback evidence --- apps/web/messages/en.json | 14 ++- apps/web/messages/zh-TW.json | 14 ++- .../dashboard/automation-evidence-card.tsx | 88 ++++++++++++++++--- 3 files changed, 103 insertions(+), 13 deletions(-) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 62d9b6c6..6b793ed0 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -253,7 +253,19 @@ "humanGap": "Human gap", "humanGapDetail": "{gate} missing {count}", "modelRoute": "Model route", - "routeDetail": "{model}; fallback {fallback}", + "routeDetail": "{model}; current {selected}; {primary}={primaryStatus}; fallback {fallback}", + "routeReasonSeparator": "; ", + "routeReason": "Reason: {reason}", + "routeErrorDetail": "Route check failed: {error}", + "routeNoFallback": "none", + "routeHealth": { + "healthy": "healthy", + "slow": "slow", + "degraded": "degraded", + "offline": "offline", + "not_checked": "standby", + "unknown": "unknown" + }, "topGap": "Largest current gap: {gate}, {count} items." } }, diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index b24465d1..205b80d4 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -254,7 +254,19 @@ "humanGap": "人工缺口", "humanGapDetail": "{gate} 缺 {count} 筆", "modelRoute": "模型路由", - "routeDetail": "{model};備援 {fallback}", + "routeDetail": "{model};目前 {selected};{primary}={primaryStatus};備援 {fallback}", + "routeReasonSeparator": ";", + "routeReason": "原因:{reason}", + "routeErrorDetail": "路由檢查失敗:{error}", + "routeNoFallback": "無", + "routeHealth": { + "healthy": "健康", + "slow": "偏慢", + "degraded": "降級", + "offline": "離線", + "not_checked": "待命", + "unknown": "未知" + }, "topGap": "目前最大缺口:{gate},共 {count} 筆。" } }, diff --git a/apps/web/src/components/dashboard/automation-evidence-card.tsx b/apps/web/src/components/dashboard/automation-evidence-card.tsx index 65436159..d2a92a1d 100644 --- a/apps/web/src/components/dashboard/automation-evidence-card.tsx +++ b/apps/web/src/components/dashboard/automation-evidence-card.tsx @@ -79,12 +79,27 @@ interface RunsResponse { items?: RunSummary[] } +interface AiRoutePolicyItem { + provider_name: string + role?: string | null + runtime?: string | null +} + +interface AiRouteHealthItem { + status?: string | null + latency_ms?: number | null + reason?: string | null + checked?: boolean +} + interface AiRouteStatusResponse { + policy_order?: AiRoutePolicyItem[] selected_provider?: string | null selected_model?: string | null - fallback_chain?: Array<{ - provider_name: string - }> + fallback_chain?: AiRoutePolicyItem[] + route_reason?: string | null + route_error?: string | null + health?: Record } interface EvidenceSnapshot { @@ -137,6 +152,42 @@ function gateLabelKey(gate?: string) { } } +function routeHealthLabelKey(status?: string | null) { + if ( + status === 'healthy' || + status === 'slow' || + status === 'degraded' || + status === 'offline' || + status === 'not_checked' + ) { + return `routeHealth.${status}` + } + return 'routeHealth.unknown' +} + +function providerDisplayName(provider?: string | null) { + switch (provider) { + case 'ollama_gcp_a': + return 'GCP-A' + case 'ollama_gcp_b': + return 'GCP-B' + case 'ollama_local': + return '111' + case 'gemini': + return 'Gemini' + default: + return provider || '--' + } +} + +function routeTone(route: AiRouteStatusResponse | null): Tone { + if (!route?.selected_provider || route.route_error) return 'warn' + const primaryProvider = route.policy_order?.[0]?.provider_name + if (primaryProvider && route.selected_provider !== primaryProvider) return 'warn' + const primaryStatus = primaryProvider ? route.health?.[primaryProvider]?.status : null + return primaryStatus === 'healthy' ? 'good' : 'warn' +} + async function fetchJson(path: string, signal: AbortSignal): Promise { const response = await fetch(`${API_BASE}${path}`, { signal }) if (!response.ok) return null @@ -254,10 +305,26 @@ export function AutomationEvidenceCard() { const topGate = quality?.gate_failures?.[0] const claimReady = Boolean(quality?.production_claim?.can_claim_full_auto_repair) - const selectedProvider = snapshot?.route?.selected_provider ?? '--' - const fallback = snapshot?.route?.fallback_chain + const route = snapshot?.route ?? null + const primaryProvider = route?.policy_order?.[0]?.provider_name ?? null + const primaryStatus = primaryProvider ? route?.health?.[primaryProvider]?.status : null + const selectedProvider = providerDisplayName(route?.selected_provider) + const fallback = route?.fallback_chain ?.map((item) => item.provider_name) + .map(providerDisplayName) .join(' -> ') + const routeSummary = route?.route_error + ? t('routeErrorDetail', { error: route.route_error }) + : t('routeDetail', { + model: route?.selected_model ?? '--', + selected: selectedProvider, + primary: providerDisplayName(primaryProvider), + primaryStatus: t(routeHealthLabelKey(primaryStatus) as never), + fallback: fallback || t('routeNoFallback'), + }) + const routeDetail = route?.route_reason && !route.route_error + ? `${routeSummary}${t('routeReasonSeparator')}${t('routeReason', { reason: route.route_reason })}` + : routeSummary return { quality, @@ -268,8 +335,10 @@ export function AutomationEvidenceCard() { claimReady, selectedProvider, fallback, + routeDetail, + routeTone: routeTone(route), } - }, [snapshot]) + }, [snapshot, t]) const hasData = Boolean(snapshot?.quality || snapshot?.coverage || snapshot?.recurrence) @@ -365,11 +434,8 @@ export function AutomationEvidenceCard() {