fix(web): surface ai route action summaries
Some checks failed
CD Pipeline / tests (push) Successful in 1m21s
Code Review / ai-code-review (push) Successful in 12s
CD Pipeline / build-and-deploy (push) Successful in 4m21s
CD Pipeline / post-deploy-checks (push) Has been cancelled

This commit is contained in:
Your Name
2026-06-04 10:26:59 +08:00
parent 623ff19b0f
commit 9116ff7bf6
5 changed files with 181 additions and 23 deletions

View File

@@ -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({
</div>
) : (
<>
<div className={cn(
"border-b px-4 py-3 text-sm",
aiRouteSummaryTone(laneMode)
)}>
<p className="font-semibold text-[#141413]">
{laneMode === "primary"
? t("summary.primaryTitle", { provider: selectedProvider ?? "--" })
: t("summary.fallbackTitle", { provider: selectedProvider ?? "--" })}
</p>
<p className="mt-1 leading-5">
{laneMode === "primary"
? t("summary.primaryDetail", {
standby: routeSummary.standby,
})
: t("summary.fallbackDetail", {
skipped: routeSummary.skipped,
action: t(operatorActionKey as never),
})}
</p>
</div>
{laneMode && laneMode !== "primary" && (
<div className="flex items-start gap-3 border-b border-[#e0ddd4] bg-[#fff7e8] px-4 py-3 text-sm text-[#6d4707]">
<TriangleAlert className="mt-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
@@ -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() {
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground"> 30 </span>
<button
onClick={() => { setLoading(true); fetchRuns({ refresh: true }); }}
onClick={() => {
setLoading(true);
fetchAiRouteStatus();
fetchRuns({ refresh: true });
}}
disabled={loading}
className="flex items-center gap-2 px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-accent rounded-lg transition disabled:opacity-50"
aria-label="立即重新整理"

View File

@@ -2060,6 +2060,21 @@ function callbackTraceRecoveryAction(
return { actionKey: "observeRecovery", humanRequired: false };
}
function aiRouteOperatorActionText(
action: string | null | undefined,
t: ReturnType<typeof useTranslations>
) {
if (
action === "monitor" ||
action === "repair_skipped_primary_lane" ||
action === "restore_ollama_lanes" ||
action === "inspect_ai_router"
) {
return t(`evidence.aiRouteActions.${action}` as never);
}
return t("evidence.aiRouteActions.unknown");
}
function buildWorkItems(
telemetry: Telemetry,
t: ReturnType<typeof useTranslations>
@@ -2105,6 +2120,14 @@ function buildWorkItems(
?.map((lane) => lane.provider_name)
.filter(Boolean)
.join(" -> ");
const aiRouteActionText = aiRouteOperatorActionText(
aiRoute?.operator_action?.action,
t
);
const aiRouteHumanRequired = aiRoute?.operator_action?.human_required === true;
const aiRouteHumanRequiredText = aiRouteHumanRequired
? t("evidence.humanRequired.yes")
: t("evidence.humanRequired.no");
const remediationQueue = telemetry.slo?.adr100?.verification_coverage?.remediation_queue;
const remediationTotal = remediationQueue?.total ?? 0;
const remediationReadyForAi = remediationQueue?.ready_for_ai ?? 0;
@@ -2233,6 +2256,11 @@ function buildWorkItems(
}),
evidenceDetails: aiRouteRepairEvidence
? [
t("evidence.aiRouteRepairSummary", {
selected: aiRoute?.selected_provider ?? "--",
action: aiRouteActionText,
human: aiRouteHumanRequiredText,
}),
t("evidence.aiRouteRepairWorkItemId", {
id: aiRouteWorkItem?.work_item_id ?? "--",
}),
@@ -2251,7 +2279,14 @@ function buildWorkItems(
safe: String(aiRouteOwnerAction?.safe_to_auto_repair ?? false),
}),
]
: [t("evidence.aiRouteRepairUnavailable")],
: [
t("evidence.aiRouteRepairSummary", {
selected: aiRoute?.selected_provider ?? "--",
action: aiRouteActionText,
human: aiRouteHumanRequiredText,
}),
t("evidence.aiRouteRepairUnavailable"),
],
href: aiRouteWorkItem?.target_href ?? "/awooop/runs",
},
{