fix(telegram): preserve incident history html output
All checks were successful
Code Review / ai-code-review (push) Successful in 11s
CD Pipeline / tests (push) Successful in 1m1s
CD Pipeline / build-and-deploy (push) Successful in 3m27s
CD Pipeline / post-deploy-checks (push) Successful in 1m29s

This commit is contained in:
Your Name
2026-05-14 23:33:43 +08:00
parent f4a8390dc0
commit 65001da0d8
6 changed files with 218 additions and 5 deletions

View File

@@ -58,12 +58,31 @@ type SloResponse = {
};
};
type RemediationHistoryItem = {
work_item_id?: string | null;
incident_id?: string | null;
verification_result_preview?: string | null;
agent_id?: string | null;
tool_name?: string | null;
required_scope?: string | null;
writes_incident_state?: boolean | null;
writes_auto_repair_result?: boolean | null;
created_at?: string | null;
};
type RemediationHistoryResponse = {
schema_version?: string;
total?: number;
items?: RemediationHistoryItem[];
};
type Telemetry = {
quality: AutomationQualitySummary | null;
governanceEvents: GovernanceEventsResponse | null;
governanceQueue: GovernanceQueueResponse | null;
channelEvents: RecentEventsResponse | null;
slo: SloResponse | null;
remediationHistory: RemediationHistoryResponse | null;
};
type WorkItem = {
@@ -74,6 +93,7 @@ type WorkItem = {
source: string;
gateKey: string;
evidence: string;
evidenceDetails?: string[];
href: string;
};
@@ -119,6 +139,15 @@ function hasGateFailure(summary: AutomationQualitySummary | null, gate: string)
return Boolean(summary?.gate_failures?.some((row) => row.gate === gate && row.total > 0));
}
function routeLabel(item?: RemediationHistoryItem | null) {
if (!item) return "--";
const route = [item.agent_id, item.tool_name, item.required_scope]
.filter(Boolean)
.map(String)
.join("/");
return route || "--";
}
function buildWorkItems(
telemetry: Telemetry,
t: ReturnType<typeof useTranslations>
@@ -132,6 +161,8 @@ function buildWorkItems(
const remediationTotal = remediationQueue?.total ?? 0;
const remediationReadyForAi = remediationQueue?.ready_for_ai ?? 0;
const remediationNeedsHuman = remediationQueue?.needs_human ?? 0;
const remediationHistoryTotal = telemetry.remediationHistory?.total ?? 0;
const latestRemediationHistory = telemetry.remediationHistory?.items?.[0] ?? null;
const governanceEventsUnavailable = telemetry.governanceEvents === null;
const governanceQueueMissing = telemetry.governanceQueue?.table_pending === true;
const governanceDispatchBlocked =
@@ -174,13 +205,28 @@ function buildWorkItems(
? "in_progress"
: "blocked",
surfaceKey: "governance",
source: "/api/v1/ai/slo remediation_queue",
source: "/api/v1/ai/slo remediation_queue + remediation_history",
gateKey: "remediationQueue",
evidence: t("evidence.remediationQueue", {
total: remediationTotal,
ready: remediationReadyForAi,
human: remediationNeedsHuman,
}),
evidenceDetails: latestRemediationHistory
? [
t("evidence.remediationHistory", {
count: remediationHistoryTotal,
preview: latestRemediationHistory.verification_result_preview ?? "--",
}),
t("evidence.remediationRoute", {
route: routeLabel(latestRemediationHistory),
}),
t("evidence.remediationWrites", {
incident: String(latestRemediationHistory.writes_incident_state ?? "--"),
autoRepair: String(latestRemediationHistory.writes_auto_repair_result ?? "--"),
}),
]
: [t("evidence.remediationHistoryEmpty")],
href: "/governance",
},
{
@@ -295,6 +341,7 @@ export default function AwoooPWorkItemsPage() {
governanceQueue: null,
channelEvents: null,
slo: null,
remediationHistory: null,
});
const [loading, setLoading] = useState(true);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
@@ -306,16 +353,18 @@ export default function AwoooPWorkItemsPage() {
const governanceQueueUrl = `${API_BASE}/api/v1/ai/governance/queue?dispatch_status=pending&size=10`;
const channelEventsUrl = `${API_BASE}/api/v1/platform/events/recent?project_id=awoooi&provider_prefix=alertmanager&limit=20`;
const sloUrl = `${API_BASE}/api/v1/ai/slo`;
const remediationHistoryUrl = `${API_BASE}/api/v1/ai/slo/remediation/history?limit=80`;
const [quality, governanceEvents, governanceQueue, channelEvents, slo] = await Promise.all([
const [quality, governanceEvents, governanceQueue, channelEvents, slo, remediationHistory] = await Promise.all([
fetchJson<AutomationQualitySummary>(qualityUrl),
fetchJson<GovernanceEventsResponse>(governanceEventsUrl),
fetchJson<GovernanceQueueResponse>(governanceQueueUrl),
fetchJson<RecentEventsResponse>(channelEventsUrl),
fetchJson<SloResponse>(sloUrl),
fetchJson<RemediationHistoryResponse>(remediationHistoryUrl),
]);
setTelemetry({ quality, governanceEvents, governanceQueue, channelEvents, slo });
setTelemetry({ quality, governanceEvents, governanceQueue, channelEvents, slo, remediationHistory });
setLastUpdated(new Date());
setLoading(false);
}, []);
@@ -437,6 +486,18 @@ export default function AwoooPWorkItemsPage() {
</td>
<td className="min-w-[220px] px-4 py-3 text-sm leading-5 text-[#5f5b52]">
{item.evidence}
{item.evidenceDetails && item.evidenceDetails.length > 0 && (
<div className="mt-2 flex flex-col gap-1">
{item.evidenceDetails.map((detail) => (
<span
key={detail}
className="inline-flex w-fit max-w-full border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5 font-mono text-[11px] leading-5 text-[#5f5b52]"
>
{detail}
</span>
))}
</div>
)}
</td>
<td className="min-w-[240px] px-4 py-3">
<span className="inline-flex items-start gap-1.5 text-sm leading-5 text-[#5f5b52]">