feat(adr100): surface playbook ticket remediation
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user