feat(awooop): surface ai route repair evidence
All checks were successful
CD Pipeline / tests (push) Successful in 1m24s
Code Review / ai-code-review (push) Successful in 12s
CD Pipeline / build-and-deploy (push) Successful in 3m53s
CD Pipeline / post-deploy-checks (push) Successful in 1m48s

This commit is contained in:
Your Name
2026-05-25 14:21:25 +08:00
parent e570d9f6a9
commit 67296746c0
8 changed files with 642 additions and 7 deletions

View File

@@ -445,6 +445,30 @@ interface AiRouteOperatorAction {
reason?: string | null;
}
interface AiRouteRepairEvidence {
provider?: string | null;
stage?: string | null;
provider_event_id?: string | null;
conversation_event_id?: string | null;
run_id?: string | null;
alertname?: string | null;
severity?: string | null;
fingerprint?: string | null;
target_resource?: string | null;
observed_state?: Record<string, unknown>;
live_probe?: Record<string, unknown>;
access_blockers?: string[];
side_effects?: {
incident_created?: boolean | null;
telegram_sent?: boolean | null;
approval_created?: boolean | null;
runtime_route_changed?: boolean | null;
};
source_ref_count?: number | null;
provider_ts?: string | null;
received_at?: string | null;
}
interface AiRouteStatusResponse {
schema_version: string;
workload_type: string;
@@ -461,6 +485,7 @@ interface AiRouteStatusResponse {
active_lane?: AiRouteLaneItem | null;
skipped_lanes?: AiRouteLaneItem[];
operator_action?: AiRouteOperatorAction | null;
repair_evidence?: AiRouteRepairEvidence | null;
checked_at: string;
}
@@ -2050,6 +2075,49 @@ function aiRouteOperatorActionLabelKey(action?: string | null) {
return "operatorActions.unknown";
}
const AI_ROUTE_REPAIR_BLOCKER_KEYS = new Set([
"gcloud_compute_instances_get_missing",
"gcloud_compute_instances_list_missing",
"gcloud_projects_get_iam_policy_missing",
"gcp_a_ssh_refused",
"gcp_a_ollama_11434_refused",
"proxy_110_11435_http_502",
]);
const AI_ROUTE_REPAIR_PROBE_KEYS = new Set([
"gcp_a_ping",
"gcp_a_ssh_22",
"gcp_a_direct_11434",
"gcp_b_direct_11434",
"proxy_110_11435",
"proxy_110_11436",
"proxy_110_11437",
]);
const AI_ROUTE_REPAIR_SIDE_EFFECT_KEYS = [
"incident_created",
"telegram_sent",
"approval_created",
"runtime_route_changed",
] as const;
function aiRouteRepairBlockerLabelKey(blocker?: string | null) {
return blocker && AI_ROUTE_REPAIR_BLOCKER_KEYS.has(blocker)
? `repairEvidence.blockers.${blocker}`
: "repairEvidence.blockers.unknown";
}
function aiRouteRepairProbeLabelKey(probe?: string | null) {
return probe && AI_ROUTE_REPAIR_PROBE_KEYS.has(probe)
? `repairEvidence.probes.${probe}`
: "repairEvidence.probes.unknown";
}
function compactEvidenceId(value?: string | null) {
if (!value) return "--";
return value.length > 18 ? `${value.slice(0, 12)}...` : value;
}
function AiRouteStatusPanel({
status,
error,
@@ -2064,6 +2132,9 @@ function AiRouteStatusPanel({
const laneMode = status?.lane_mode ?? null;
const laneModeKey = aiRouteLaneModeLabelKey(laneMode);
const operatorActionKey = aiRouteOperatorActionLabelKey(status?.operator_action?.action);
const repairEvidence = status?.repair_evidence ?? null;
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 skippedProviderSet = new Set(
skippedLanes
@@ -2076,6 +2147,12 @@ function AiRouteStatusPanel({
minute: "2-digit",
})
: "--";
const repairEvidenceAt = repairEvidence?.received_at
? new Date(repairEvidence.received_at).toLocaleTimeString("zh-TW", {
hour: "2-digit",
minute: "2-digit",
})
: "--";
return (
<section className="border border-[#e0ddd4] bg-white">
@@ -2132,6 +2209,117 @@ function AiRouteStatusPanel({
</div>
)}
{repairEvidence && (
<div className="border-b border-[#e0ddd4] bg-[#fbfcfb] px-4 py-4">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="flex min-w-0 items-center gap-2">
<SearchCheck className="h-4 w-4 shrink-0 text-[#17602a]" aria-hidden="true" />
<div className="min-w-0">
<p className="text-sm font-semibold text-[#141413]">
{t("repairEvidence.title")}
</p>
<p className="mt-1 truncate font-mono text-xs text-[#77736a]">
{repairEvidence.provider_event_id ?? "--"}
</p>
</div>
</div>
<span className="border border-[#cbdccf] bg-[#f0faf2] px-2 py-0.5 text-xs font-semibold text-[#17602a]">
{t("repairEvidence.sourceRefs", {
count: repairEvidence.source_ref_count ?? 0,
})}
</span>
</div>
<div className="mt-4 grid gap-3 lg:grid-cols-3">
<div className="border border-[#e0ddd4] bg-white px-3 py-3">
<p className="text-xs font-semibold text-[#77736a]">
{t("repairEvidence.meta")}
</p>
<div className="mt-2 space-y-1 text-xs leading-5 text-[#5f5b52]">
<p>
{t("repairEvidence.target", {
target: repairEvidence.target_resource ?? "--",
})}
</p>
<p>
{t("repairEvidence.run", {
run: compactEvidenceId(repairEvidence.run_id),
})}
</p>
<p>
{t("repairEvidence.receivedAt", {
time: repairEvidenceAt,
})}
</p>
</div>
</div>
<div className="border border-[#e0ddd4] bg-white px-3 py-3">
<p className="text-xs font-semibold text-[#77736a]">
{t("repairEvidence.blockerTitle")}
</p>
<div className="mt-2 flex flex-wrap gap-1.5">
{repairBlockers.length > 0 ? repairBlockers.map((blocker) => (
<span
key={blocker}
className="border border-[#d9b36f] bg-[#fff7e8] px-2 py-1 text-xs font-medium text-[#6d4707]"
title={blocker}
>
{AI_ROUTE_REPAIR_BLOCKER_KEYS.has(blocker)
? t(aiRouteRepairBlockerLabelKey(blocker) as never)
: t("repairEvidence.blockers.unknown", { blocker })}
</span>
)) : (
<span className="text-xs text-[#77736a]">
{t("repairEvidence.emptyValue")}
</span>
)}
</div>
</div>
<div className="border border-[#e0ddd4] bg-white px-3 py-3">
<p className="text-xs font-semibold text-[#77736a]">
{t("repairEvidence.sideEffectTitle")}
</p>
<div className="mt-2 space-y-1 text-xs leading-5 text-[#5f5b52]">
{AI_ROUTE_REPAIR_SIDE_EFFECT_KEYS.map((key) => {
const value = repairEvidence.side_effects?.[key];
const valueKey = value === true
? "repairEvidence.values.yes"
: value === false
? "repairEvidence.values.no"
: "repairEvidence.values.unknown";
return (
<p key={key}>
{t(`repairEvidence.sideEffects.${key}` as never)}
{t("repairEvidence.sideEffectSeparator")}
{t(valueKey as never)}
</p>
);
})}
</div>
</div>
</div>
{repairProbes.length > 0 && (
<div className="mt-3 grid gap-px bg-[#e0ddd4] md:grid-cols-2 xl:grid-cols-3">
{repairProbes.map(([probe, value]) => (
<div key={probe} className="bg-white px-3 py-2 text-xs leading-5">
<p className="font-semibold text-[#141413]">
{AI_ROUTE_REPAIR_PROBE_KEYS.has(probe)
? t(aiRouteRepairProbeLabelKey(probe) as never)
: t("repairEvidence.probes.unknown", { probe })}
</p>
<p className="mt-1 truncate font-mono text-[#77736a]" title={String(value ?? "--")}>
{String(value ?? "--")}
</p>
</div>
))}
</div>
)}
</div>
)}
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-4">
<div className="bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("fields.workload")}</p>

View File

@@ -126,6 +126,11 @@ interface AiRouteStatusResponse {
action?: string | null
human_required?: boolean
} | null
repair_evidence?: {
target_resource?: string | null
access_blockers?: string[]
source_ref_count?: number | null
} | null
}
interface EvidenceSnapshot {
@@ -394,8 +399,17 @@ export function AutomationEvidenceCard() {
skipped: skippedLanes || '--',
})
: null
const repairEvidence = route?.repair_evidence ?? null
const repairBlockers = repairEvidence?.access_blockers?.slice(0, 2).join(', ')
const repairDetail = repairEvidence
? t('routeRepairDetail', {
target: repairEvidence.target_resource ?? '--',
blockers: repairBlockers || '--',
sourceRefs: repairEvidence.source_ref_count ?? 0,
})
: null
const routeDetail = route?.route_reason && !route.route_error
? `${routeSummary}${laneDetail ? t('routeReasonSeparator') + laneDetail : ''}${t('routeReasonSeparator')}${t('routeReason', { reason: route.route_reason })}`
? `${routeSummary}${laneDetail ? t('routeReasonSeparator') + laneDetail : ''}${repairDetail ? t('routeReasonSeparator') + repairDetail : ''}${t('routeReasonSeparator')}${t('routeReason', { reason: route.route_reason })}`
: routeSummary
return {