feat(adr100): surface playbook ticket remediation
Some checks failed
CD Pipeline / tests (push) Successful in 1m21s
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / build-and-deploy (push) Failing after 9m11s
CD Pipeline / post-deploy-checks (push) Has been skipped

This commit is contained in:
Your Name
2026-06-01 20:01:10 +08:00
parent 40e65730c1
commit a7b807dbfa
5 changed files with 437 additions and 4 deletions

View File

@@ -263,11 +263,35 @@ type SloResponse = {
total: number;
ready_for_ai: number;
needs_human: number;
items?: RemediationQueueItem[];
by_status?: Array<{ name?: string | null; count?: number | null }>;
by_action?: Array<{ name?: string | null; count?: number | null }>;
};
};
};
};
type RemediationQueueItem = {
work_item_id?: string | null;
incident_id?: string | null;
auto_repair_id?: string | null;
alertname?: string | null;
playbook_id?: string | null;
failure_class?: string | null;
verification_result?: string | null;
remediation_status?: string | null;
remediation_action?: string | null;
remediation_owner?: string | null;
remediation_reason?: string | null;
source?: string | null;
auto_created_at?: string | null;
verification_collected_at?: string | null;
};
type RemediationQueue = NonNullable<
NonNullable<NonNullable<SloResponse["adr100"]>["verification_coverage"]>["remediation_queue"]
>;
type RemediationHistoryItem = {
work_item_id?: string | null;
incident_id?: string | null;
@@ -2616,6 +2640,202 @@ function WorkItemIncidentAuditPanel({
);
}
function Adr100RemediationQueuePanel({
queue,
focusedWorkItemId,
onRecorded,
}: {
queue: RemediationQueue | null | undefined;
focusedWorkItemId: string | null;
onRecorded: () => void;
}) {
const t = useTranslations("awooop.workItems.adr100Remediation");
const [actionState, setActionState] = useState<Record<string, RecurrenceWorkItemActionState>>({});
const items = queue?.items ?? [];
const focusedItem = focusedWorkItemId
? items.find((item) => item.work_item_id === focusedWorkItemId)
: null;
const visibleItems = focusedItem
? [focusedItem, ...items.filter((item) => item !== focusedItem).slice(0, 5)]
: items.slice(0, 6);
const runAction = useCallback(async (
item: RemediationQueueItem,
action: "preview" | "dryRun"
) => {
const workItemId = item.work_item_id ?? "";
if (!workItemId) return;
setActionState((current) => ({
...current,
[workItemId]: { ...current[workItemId], loading: action, error: null },
}));
const mode = item.remediation_status === "needs_playbook_ticket" ? "ticket" : "auto";
try {
const result = action === "preview"
? await fetchJson<RecurrenceWorkItemActionResult>(
`${API_BASE}/api/v1/ai/slo/remediation/preview?work_item_id=${encodeURIComponent(workItemId)}&mode=${encodeURIComponent(mode)}`,
12000
)
: await postJson<RecurrenceWorkItemActionResult>(
`${API_BASE}/api/v1/ai/slo/remediation/dry-run`,
{ work_item_id: workItemId, mode },
15000
);
setActionState((current) => ({
...current,
[workItemId]: {
loading: null,
result,
error: result ? null : t("actions.failed"),
},
}));
if (result?.history?.recorded) {
onRecorded();
}
} catch (error) {
setActionState((current) => ({
...current,
[workItemId]: {
loading: null,
result: null,
error: error instanceof Error ? error.message : t("actions.failed"),
},
}));
}
}, [onRecorded, t]);
return (
<section className="border border-[#e0ddd4] bg-white">
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
<div className="flex min-w-0 items-center gap-2">
<ListChecks className="h-4 w-4 text-brand-accent" aria-hidden="true" />
<div className="min-w-0">
<h3 className="text-sm font-semibold text-[#141413]">{t("title")}</h3>
<p className="mt-1 text-xs leading-5 text-[#77736a]">
{t("subtitle", {
total: queue?.total ?? 0,
ready: queue?.ready_for_ai ?? 0,
human: queue?.needs_human ?? 0,
})}
</p>
</div>
</div>
<Link
href="/governance"
className="inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white px-2.5 py-1 text-xs font-semibold text-[#141413] hover:border-[#d97757]"
>
{t("openGovernance")}
<ArrowRight className="h-3.5 w-3.5" aria-hidden="true" />
</Link>
</div>
{visibleItems.length === 0 ? (
<div className="px-4 py-5 text-sm leading-6 text-[#77736a]">
{t("empty")}
</div>
) : (
<div className="grid gap-px bg-[#e0ddd4] lg:grid-cols-2">
{visibleItems.map((item) => {
const workItemId = item.work_item_id ?? "";
const state = workItemId ? actionState[workItemId] : undefined;
const result = state?.result ?? null;
const ticketPreview = result?.ticket_preview ?? null;
return (
<article key={workItemId || item.incident_id || item.auto_repair_id} className="min-w-0 bg-white p-4">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0">
<p className="font-mono text-[11px] font-semibold text-[#77736a]">
{item.incident_id ?? "--"}
</p>
<h4 className="mt-1 truncate text-sm font-semibold text-[#141413]" title={item.alertname ?? undefined}>
{item.alertname ?? t("unknownAlert")}
</h4>
</div>
<span className={cn(
"inline-flex shrink-0 border px-2 py-0.5 text-[11px] font-semibold",
item.remediation_status === "needs_playbook_ticket"
? "border-[#f4c7a1] bg-[#fff7ed] text-[#9a4c18]"
: item.remediation_status?.startsWith("ready")
? "border-[#bbdfc5] bg-[#f1fbf3] text-[#24733d]"
: "border-[#ead5d5] bg-[#fff7f7] text-[#8b2f2f]"
)}>
{item.remediation_status ?? "--"}
</span>
</div>
<div className="mt-3 grid gap-2 text-xs leading-5 text-[#5f5b52] md:grid-cols-2">
<span className="min-w-0 truncate">
{t("fields.failure", { value: item.failure_class ?? "--" })}
</span>
<span className="min-w-0 truncate">
{t("fields.action", { value: item.remediation_action ?? "--" })}
</span>
<span className="min-w-0 truncate">
{t("fields.owner", { value: item.remediation_owner ?? "--" })}
</span>
<span className="min-w-0 truncate">
{t("fields.playbook", { value: item.playbook_id ?? "--" })}
</span>
</div>
<div className="mt-3 flex flex-wrap gap-2">
<button
type="button"
onClick={() => runAction(item, "preview")}
disabled={!workItemId || state?.loading === "preview"}
className="inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-[#faf9f3] px-2.5 py-1 text-xs font-semibold text-[#141413] hover:border-[#d97757] disabled:opacity-50"
>
<SearchCheck className="h-3.5 w-3.5" aria-hidden="true" />
{state?.loading === "preview" ? t("actions.loading") : t("actions.preview")}
</button>
<button
type="button"
onClick={() => runAction(item, "dryRun")}
disabled={!workItemId || state?.loading === "dryRun"}
className="inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white px-2.5 py-1 text-xs font-semibold text-[#141413] hover:border-[#d97757] disabled:opacity-50"
>
<FileText className="h-3.5 w-3.5" aria-hidden="true" />
{state?.loading === "dryRun" ? t("actions.loading") : t("actions.dryRun")}
</button>
</div>
{state?.error ? (
<p className="mt-3 border border-[#ead5d5] bg-[#fff7f7] px-3 py-2 text-xs leading-5 text-[#8b2f2f]">
{state.error}
</p>
) : null}
{result ? (
<div className="mt-3 border border-[#eee9dd] bg-[#faf9f3] px-3 py-2 text-xs leading-5 text-[#5f5b52]">
<div className="flex flex-wrap gap-2">
<span className="font-mono">{t("result.mode", { value: result.mode ?? "--" })}</span>
<span className="font-mono">{t("result.allowed", { value: String(result.allowed ?? false) })}</span>
<span className="font-mono">{t("result.writes", {
incident: String(result.writes_incident_state ?? false),
autoRepair: String(result.writes_auto_repair_result ?? false),
})}</span>
</div>
{ticketPreview ? (
<div className="mt-2 border-t border-[#e0ddd4] pt-2">
<p className="font-semibold text-[#141413]">{ticketPreview.title ?? t("ticketFallback")}</p>
<p className="mt-1 line-clamp-3 whitespace-pre-line text-[#5f5b52]">
{ticketPreview.body_preview ?? "--"}
</p>
</div>
) : null}
</div>
) : null}
</article>
);
})}
</div>
)}
</section>
);
}
function RecurrenceWorkQueuePanel({
recurrence,
focusedWorkItemId,
@@ -5351,6 +5571,12 @@ export default function AwoooPWorkItemsPage() {
writesAutoRepairResult={latestRemediationHistory?.writes_auto_repair_result}
/>
<Adr100RemediationQueuePanel
queue={telemetry.slo?.adr100?.verification_coverage?.remediation_queue}
focusedWorkItemId={focusedWorkItemId}
onRecorded={fetchTelemetry}
/>
<AwoooPStatusChainPanel chain={telemetry.statusChain} />
<WorkItemIncidentAuditPanel