diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json
index 24d24d04..5e8c8e86 100644
--- a/apps/web/messages/en.json
+++ b/apps/web/messages/en.json
@@ -3084,12 +3084,24 @@
"recurrenceSourceApplied": "來源配對已套用:{count}",
"recurrenceEmpty": "近期重複告警尚無待處理工作項",
"aiRouteRepairWorkItem": "AI route:{lane};目前 {selected};目標 {target};阻塞 {blockers} 項",
+ "aiRouteActions": {
+ "monitor": "持續監控即可",
+ "repair_skipped_primary_lane": "修復被跳過的 Primary lane",
+ "restore_ollama_lanes": "恢復 Ollama lanes,避免只剩雲端",
+ "inspect_ai_router": "檢查 AI Router / provider 狀態",
+ "unknown": "待確認下一步"
+ },
"aiRouteRepairWorkItemId": "Work item:{id}",
"aiRouteRepairSkipped": "已跳過:{skipped}",
"aiRouteRepairOwner": "Owner:{owner};主責 Agent:{lead}",
"aiRouteRepairPlaybook": "PlayBook:{playbook};步驟 {steps}",
"aiRouteRepairSafety": "可安全自動修復:{safe}",
+ "aiRouteRepairSummary": "AI route 目前由 {selected} 承接;下一步:{action};需人工介入:{human}",
"aiRouteRepairUnavailable": "AI route repair evidence 尚未回傳",
+ "humanRequired": {
+ "yes": "是",
+ "no": "否"
+ },
"driftFingerprint": "Config Drift:{state};12h 內 {count} 次",
"driftFingerprintUnavailable": "Config Drift fingerprint state API 尚未回應",
"driftFingerprintId": "Fingerprint:{fingerprint};Report:{report}",
@@ -4263,6 +4275,12 @@
"inspect_ai_router": "需檢查 AI Router / provider 狀態",
"unknown": "待確認下一步"
},
+ "summary": {
+ "primaryTitle": "目前由 {provider} 承接,AI lane 正常",
+ "primaryDetail": "後續備援順序:{standby}。Gemini 只在 Ollama lanes 都不可用後接手;目前下一步是持續監控與保留 fallback 證據。",
+ "fallbackTitle": "目前由 {provider} 接手,AI lane 已降級",
+ "fallbackDetail": "已跳過:{skipped}。下一步:{action};需確認是否已有 Work Item、PlayBook 與人工 gate。"
+ },
"degradedSummary": "目前由 {active} 接手;已跳過 {skipped};下一步:{action}",
"repairEvidence": {
"title": "最新修復診斷證據",
diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json
index 24d24d04..5e8c8e86 100644
--- a/apps/web/messages/zh-TW.json
+++ b/apps/web/messages/zh-TW.json
@@ -3084,12 +3084,24 @@
"recurrenceSourceApplied": "來源配對已套用:{count}",
"recurrenceEmpty": "近期重複告警尚無待處理工作項",
"aiRouteRepairWorkItem": "AI route:{lane};目前 {selected};目標 {target};阻塞 {blockers} 項",
+ "aiRouteActions": {
+ "monitor": "持續監控即可",
+ "repair_skipped_primary_lane": "修復被跳過的 Primary lane",
+ "restore_ollama_lanes": "恢復 Ollama lanes,避免只剩雲端",
+ "inspect_ai_router": "檢查 AI Router / provider 狀態",
+ "unknown": "待確認下一步"
+ },
"aiRouteRepairWorkItemId": "Work item:{id}",
"aiRouteRepairSkipped": "已跳過:{skipped}",
"aiRouteRepairOwner": "Owner:{owner};主責 Agent:{lead}",
"aiRouteRepairPlaybook": "PlayBook:{playbook};步驟 {steps}",
"aiRouteRepairSafety": "可安全自動修復:{safe}",
+ "aiRouteRepairSummary": "AI route 目前由 {selected} 承接;下一步:{action};需人工介入:{human}",
"aiRouteRepairUnavailable": "AI route repair evidence 尚未回傳",
+ "humanRequired": {
+ "yes": "是",
+ "no": "否"
+ },
"driftFingerprint": "Config Drift:{state};12h 內 {count} 次",
"driftFingerprintUnavailable": "Config Drift fingerprint state API 尚未回應",
"driftFingerprintId": "Fingerprint:{fingerprint};Report:{report}",
@@ -4263,6 +4275,12 @@
"inspect_ai_router": "需檢查 AI Router / provider 狀態",
"unknown": "待確認下一步"
},
+ "summary": {
+ "primaryTitle": "目前由 {provider} 承接,AI lane 正常",
+ "primaryDetail": "後續備援順序:{standby}。Gemini 只在 Ollama lanes 都不可用後接手;目前下一步是持續監控與保留 fallback 證據。",
+ "fallbackTitle": "目前由 {provider} 接手,AI lane 已降級",
+ "fallbackDetail": "已跳過:{skipped}。下一步:{action};需確認是否已有 Work Item、PlayBook 與人工 gate。"
+ },
"degradedSummary": "目前由 {active} 接手;已跳過 {skipped};下一步:{action}",
"repairEvidence": {
"title": "最新修復診斷證據",
diff --git a/apps/web/src/app/[locale]/awooop/runs/page.tsx b/apps/web/src/app/[locale]/awooop/runs/page.tsx
index 5f0e578c..d9f9f77d 100644
--- a/apps/web/src/app/[locale]/awooop/runs/page.tsx
+++ b/apps/web/src/app/[locale]/awooop/runs/page.tsx
@@ -3437,6 +3437,29 @@ function aiRouteOperatorActionLabelKey(action?: string | null) {
return "operatorActions.unknown";
}
+function aiRouteSummaryTone(mode?: string | null) {
+ if (mode === "primary") {
+ return "border-[#b8d8bd] bg-[#f0faf2] text-[#17602a]";
+ }
+ if (mode === "cloud_fallback" || mode === "unavailable") {
+ return "border-[#e1aaa2] bg-[#fff3f1] text-[#9f2f25]";
+ }
+ return "border-[#d9b36f] bg-[#fff7e8] text-[#6d4707]";
+}
+
+function aiRouteStandbySummary(status: AiRouteStatusResponse) {
+ const standby = status.policy_order
+ .filter((item) => item.provider_name !== status.selected_provider)
+ .map((item) => item.provider_name)
+ .slice(0, 3)
+ .join(" -> ");
+ const skipped = (status.skipped_lanes ?? [])
+ .map((lane) => lane.provider_name)
+ .filter(Boolean)
+ .join(" -> ");
+ return { standby: standby || "--", skipped: skipped || "--" };
+}
+
const AI_ROUTE_REPAIR_BLOCKER_KEYS = new Set([
"gcloud_compute_instances_get_missing",
"gcloud_compute_instances_list_missing",
@@ -3499,6 +3522,7 @@ function AiRouteStatusPanel({
const repairBlockers = repairEvidence?.access_blockers?.filter(Boolean).slice(0, 4) ?? [];
const repairProbes = Object.entries(repairEvidence?.live_probe ?? {}).slice(0, 6);
const skippedLanes = status?.skipped_lanes ?? [];
+ const routeSummary = status ? aiRouteStandbySummary(status) : { standby: "--", skipped: "--" };
const skippedProviderSet = new Set(
skippedLanes
.map((lane) => lane.provider_name)
@@ -3551,6 +3575,27 @@ function AiRouteStatusPanel({
) : (
<>
+
+
+ {laneMode === "primary"
+ ? t("summary.primaryTitle", { provider: selectedProvider ?? "--" })
+ : t("summary.fallbackTitle", { provider: selectedProvider ?? "--" })}
+
+
+ {laneMode === "primary"
+ ? t("summary.primaryDetail", {
+ standby: routeSummary.standby,
+ })
+ : t("summary.fallbackDetail", {
+ skipped: routeSummary.skipped,
+ action: t(operatorActionKey as never),
+ })}
+
+
+
{laneMode && laneMode !== "primary" && (
@@ -3852,6 +3897,27 @@ export default function RunsPage() {
.catch(() => {});
}, []);
+ const fetchAiRouteStatus = useCallback(async () => {
+ try {
+ const routeStatusRes = await fetch(
+ `${API_BASE}/api/v1/platform/ai-route-status?workload_type=deep_rca`
+ );
+ if (routeStatusRes.ok) {
+ const routeStatusData: AiRouteStatusResponse = await routeStatusRes.json();
+ setAiRouteStatus(routeStatusData);
+ setAiRouteStatusError(null);
+ } else {
+ setAiRouteStatus(null);
+ setAiRouteStatusError(`HTTP ${routeStatusRes.status}`);
+ }
+ } catch (routeStatusError) {
+ setAiRouteStatus(null);
+ setAiRouteStatusError(
+ routeStatusError instanceof Error ? routeStatusError.message : "route status failed"
+ );
+ }
+ }, []);
+
const fetchRuns = useCallback(async (options?: { refresh?: boolean }) => {
try {
setError(null);
@@ -3989,25 +4055,6 @@ export default function RunsPage() {
);
}
- try {
- const routeStatusRes = await fetch(
- `${API_BASE}/api/v1/platform/ai-route-status?workload_type=deep_rca`
- );
- if (routeStatusRes.ok) {
- const routeStatusData: AiRouteStatusResponse = await routeStatusRes.json();
- setAiRouteStatus(routeStatusData);
- setAiRouteStatusError(null);
- } else {
- setAiRouteStatus(null);
- setAiRouteStatusError(`HTTP ${routeStatusRes.status}`);
- }
- } catch (routeStatusError) {
- setAiRouteStatus(null);
- setAiRouteStatusError(
- routeStatusError instanceof Error ? routeStatusError.message : "route status failed"
- );
- }
-
setLastRefresh(new Date());
} catch (err) {
setError(err instanceof Error ? err.message : "載入失敗");
@@ -4020,18 +4067,20 @@ export default function RunsPage() {
// 初次載入
useEffect(() => {
setLoading(true);
+ fetchAiRouteStatus();
fetchRuns();
- }, [fetchRuns]);
+ }, [fetchAiRouteStatus, fetchRuns]);
// 30 秒自動刷新
useEffect(() => {
intervalRef.current = setInterval(() => {
+ fetchAiRouteStatus();
fetchRuns();
}, AUTO_REFRESH_INTERVAL);
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
- }, [fetchRuns]);
+ }, [fetchAiRouteStatus, fetchRuns]);
const totalPages = Math.ceil(total / PER_PAGE);
const laneSummary = useMemo(() => {
@@ -4106,7 +4155,11 @@ export default function RunsPage() {
每 30 秒自動刷新