feat(awooop): preview recurrence repair work items
This commit is contained in:
@@ -104,6 +104,60 @@ type RecurrenceResponse = {
|
||||
items: RecurrenceItem[];
|
||||
};
|
||||
|
||||
type RecurrenceWorkItemActionResult = {
|
||||
schema_version?: string;
|
||||
work_item_id?: string | null;
|
||||
incident_id?: string | null;
|
||||
mode?: string | null;
|
||||
requested_mode?: string | null;
|
||||
allowed?: boolean | null;
|
||||
executed?: boolean | null;
|
||||
safety_level?: string | null;
|
||||
writes_incident_state?: boolean | null;
|
||||
writes_auto_repair_result?: boolean | null;
|
||||
writes_ticket?: boolean | null;
|
||||
verification_result_preview?: string | null;
|
||||
next_step?: string | null;
|
||||
checks?: Array<{ name?: string | null; passed?: boolean | null; detail?: string | null }>;
|
||||
current_state_summary?: {
|
||||
repair_status?: string | null;
|
||||
occurrence_total?: number | null;
|
||||
duplicate_total?: number | null;
|
||||
linked_run_total?: number | null;
|
||||
} | null;
|
||||
ticket_preview?: {
|
||||
would_create?: boolean | null;
|
||||
title?: string | null;
|
||||
labels?: string[] | null;
|
||||
body_preview?: string | null;
|
||||
} | null;
|
||||
plan?: {
|
||||
step?: string | null;
|
||||
flywheel_node?: string | null;
|
||||
agent_id?: string | null;
|
||||
required_scope?: string | null;
|
||||
target_action?: string | null;
|
||||
} | null;
|
||||
read_model_route?: {
|
||||
agent_id?: string | null;
|
||||
tool_name?: string | null;
|
||||
required_scope?: string | null;
|
||||
flywheel_node?: string | null;
|
||||
} | null;
|
||||
history?: {
|
||||
recorded?: boolean | null;
|
||||
reason?: string | null;
|
||||
alert_operation_id?: string | null;
|
||||
timeline_event_id?: string | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
type RecurrenceWorkItemActionState = {
|
||||
loading?: "preview" | "dryRun" | null;
|
||||
result?: RecurrenceWorkItemActionResult | null;
|
||||
error?: string | null;
|
||||
};
|
||||
|
||||
type SloResponse = {
|
||||
adr100?: {
|
||||
verification_coverage?: {
|
||||
@@ -194,6 +248,30 @@ async function fetchJson<T>(url: string, timeoutMs = 8000): Promise<T | null> {
|
||||
}
|
||||
}
|
||||
|
||||
async function postJson<T>(
|
||||
url: string,
|
||||
body: Record<string, unknown>,
|
||||
timeoutMs = 8000
|
||||
): Promise<T | null> {
|
||||
const controller = new AbortController();
|
||||
const timeout = window.setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
return (await response.json()) as T;
|
||||
} catch {
|
||||
return null;
|
||||
} finally {
|
||||
window.clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
function hasGateFailure(summary: AutomationQualitySummary | null, gate: string) {
|
||||
return Boolean(summary?.gate_failures?.some((row) => row.gate === gate && row.total > 0));
|
||||
}
|
||||
@@ -227,6 +305,32 @@ function recurrenceRepairStatusKey(status?: string | null) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function recurrenceActionModeKey(mode?: string | null) {
|
||||
if (
|
||||
mode === "auto" ||
|
||||
mode === "ticket" ||
|
||||
mode === "reverify" ||
|
||||
mode === "approval_review" ||
|
||||
mode === "observe"
|
||||
) {
|
||||
return mode;
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function recurrencePreviewKey(preview?: string | null) {
|
||||
if (
|
||||
preview === "ticket_preview_ready" ||
|
||||
preview === "reverify_preview_ready" ||
|
||||
preview === "approval_review_required" ||
|
||||
preview === "observe_only" ||
|
||||
preview === "blocked"
|
||||
) {
|
||||
return preview;
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function buildWorkItems(
|
||||
telemetry: Telemetry,
|
||||
t: ReturnType<typeof useTranslations>
|
||||
@@ -538,6 +642,7 @@ function RecurrenceWorkQueuePanel({
|
||||
projectId: string;
|
||||
}) {
|
||||
const t = useTranslations("awooop.workItems.recurrence");
|
||||
const [actionState, setActionState] = useState<Record<string, RecurrenceWorkItemActionState>>({});
|
||||
const openItems = recurrenceOpenItems(recurrence);
|
||||
const focusedItem = focusedWorkItemId
|
||||
? openItems.find((item) => item.work_item?.work_item_id === focusedWorkItemId)
|
||||
@@ -546,6 +651,42 @@ function RecurrenceWorkQueuePanel({
|
||||
? [focusedItem, ...openItems.filter((item) => item !== focusedItem).slice(0, 5)]
|
||||
: openItems.slice(0, 6);
|
||||
const summary = recurrence?.summary;
|
||||
const runWorkItemAction = useCallback(async (
|
||||
workItemId: string,
|
||||
action: "preview" | "dryRun"
|
||||
) => {
|
||||
setActionState((current) => ({
|
||||
...current,
|
||||
[workItemId]: { ...current[workItemId], loading: action, error: null },
|
||||
}));
|
||||
|
||||
const encodedProjectId = encodeURIComponent(projectId);
|
||||
const encodedWorkItemId = encodeURIComponent(workItemId);
|
||||
const result = action === "preview"
|
||||
? await fetchJson<RecurrenceWorkItemActionResult>(
|
||||
`${API_BASE}/api/v1/platform/events/dossier/recurrence/work-item/preview?project_id=${encodedProjectId}&work_item_id=${encodedWorkItemId}`,
|
||||
12000
|
||||
)
|
||||
: await postJson<RecurrenceWorkItemActionResult>(
|
||||
`${API_BASE}/api/v1/platform/events/dossier/recurrence/work-item/dry-run`,
|
||||
{
|
||||
project_id: projectId,
|
||||
work_item_id: workItemId,
|
||||
mode: "auto",
|
||||
limit: 300,
|
||||
},
|
||||
15000
|
||||
);
|
||||
|
||||
setActionState((current) => ({
|
||||
...current,
|
||||
[workItemId]: {
|
||||
loading: null,
|
||||
result,
|
||||
error: result ? null : t("actions.failed"),
|
||||
},
|
||||
}));
|
||||
}, [projectId, t]);
|
||||
|
||||
return (
|
||||
<section className="border border-[#e0ddd4] bg-white">
|
||||
@@ -582,17 +723,23 @@ function RecurrenceWorkQueuePanel({
|
||||
<div className="grid gap-px bg-[#eee9dd] md:grid-cols-2 xl:grid-cols-3">
|
||||
{visibleItems.map((item) => {
|
||||
const workItem = item.work_item;
|
||||
const workItemId = workItem?.work_item_id ?? "";
|
||||
const isFocused = Boolean(
|
||||
focusedWorkItemId && workItem?.work_item_id === focusedWorkItemId
|
||||
focusedWorkItemId && workItemId === focusedWorkItemId
|
||||
);
|
||||
const repairStatusKey = recurrenceRepairStatusKey(item.repair_summary?.status);
|
||||
const runHref = item.latest_run_id
|
||||
? `/awooop/runs/${item.latest_run_id}?project_id=${encodeURIComponent(projectId)}`
|
||||
: null;
|
||||
const currentAction = workItemId ? actionState[workItemId] : null;
|
||||
const actionResult = currentAction?.result;
|
||||
const actionAllowed = actionResult?.allowed === true;
|
||||
const actionModeKey = recurrenceActionModeKey(actionResult?.mode);
|
||||
const previewKey = recurrencePreviewKey(actionResult?.verification_result_preview);
|
||||
|
||||
return (
|
||||
<article
|
||||
key={workItem?.work_item_id ?? item.recurrence_key}
|
||||
key={workItemId || item.recurrence_key}
|
||||
className={cn(
|
||||
"bg-white px-4 py-3",
|
||||
isFocused && "outline outline-2 outline-[#d97757]"
|
||||
@@ -631,6 +778,32 @@ function RecurrenceWorkQueuePanel({
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||||
{workItemId ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => runWorkItemAction(workItemId, "preview")}
|
||||
disabled={currentAction?.loading === "preview"}
|
||||
className="inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white px-2 py-1 text-xs font-semibold text-[#2e2b26] hover:border-[#1f6feb] hover:bg-[#edf4ff] hover:text-[#0f4fa8] disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
<SearchCheck className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{currentAction?.loading === "preview"
|
||||
? t("actions.previewing")
|
||||
: t("actions.preview")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => runWorkItemAction(workItemId, "dryRun")}
|
||||
disabled={currentAction?.loading === "dryRun"}
|
||||
className="inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white px-2 py-1 text-xs font-semibold text-[#2e2b26] hover:border-[#d97757] hover:bg-[#fff7e8] hover:text-[#8a5a08] disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
<Gauge className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{currentAction?.loading === "dryRun"
|
||||
? t("actions.dryRunning")
|
||||
: t("actions.dryRun")}
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
{runHref ? (
|
||||
<Link
|
||||
href={runHref as never}
|
||||
@@ -648,6 +821,58 @@ function RecurrenceWorkQueuePanel({
|
||||
{t("openRuns")}
|
||||
</Link>
|
||||
</div>
|
||||
{currentAction?.error ? (
|
||||
<div className="mt-3 border border-[#e2a29b] bg-[#fff0ef] px-3 py-2 text-xs leading-5 text-[#9f2f25]">
|
||||
{currentAction.error}
|
||||
</div>
|
||||
) : null}
|
||||
{actionResult ? (
|
||||
<div
|
||||
className={cn(
|
||||
"mt-3 border px-3 py-2 text-xs leading-5",
|
||||
actionAllowed
|
||||
? "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]"
|
||||
: "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]"
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<span className="font-semibold">
|
||||
{actionAllowed ? t("actions.allowed") : t("actions.blocked")}
|
||||
</span>
|
||||
<span className="font-mono">
|
||||
{t("actions.mode", {
|
||||
mode: t(`actions.modes.${actionModeKey}` as never),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 grid gap-1 text-[#5f5b52]">
|
||||
<p>
|
||||
{t("actions.previewResult", {
|
||||
result: t(`actions.previews.${previewKey}` as never),
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t("actions.writes", {
|
||||
incident: String(actionResult.writes_incident_state ?? false),
|
||||
autoRepair: String(actionResult.writes_auto_repair_result ?? false),
|
||||
ticket: String(actionResult.writes_ticket ?? false),
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t("actions.history", {
|
||||
recorded: String(actionResult.history?.recorded ?? false),
|
||||
})}
|
||||
</p>
|
||||
{actionResult.ticket_preview?.title ? (
|
||||
<p className="truncate">
|
||||
{t("actions.ticket", {
|
||||
title: actionResult.ticket_preview.title,
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user