feat(adr100): bridge playbook authoring approvals
All checks were successful
CD Pipeline / tests (push) Successful in 1m20s
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / build-and-deploy (push) Successful in 7m44s
CD Pipeline / post-deploy-checks (push) Successful in 2m49s

This commit is contained in:
Your Name
2026-06-01 20:48:24 +08:00
parent f0daaccbba
commit 16775bb4fa
9 changed files with 784 additions and 7 deletions

View File

@@ -196,7 +196,10 @@ type RecurrenceWorkItemActionResult = {
writes_source_event?: boolean | null;
writes_auto_repair_result?: boolean | null;
writes_ticket?: boolean | null;
writes_approval_record?: boolean | null;
creates_external_ticket?: boolean | null;
deduplicated?: boolean | null;
fingerprint?: string | null;
verification_result_preview?: string | null;
next_step?: string | null;
checks?: Array<{ name?: string | null; passed?: boolean | null; detail?: string | null }>;
@@ -214,12 +217,24 @@ type RecurrenceWorkItemActionResult = {
labels?: string[] | null;
body_preview?: string | null;
} | null;
approval_id?: string | null;
approval?: {
id?: string | null;
status?: string | null;
risk_level?: string | null;
required_signatures?: number | null;
current_signatures?: number | null;
requested_by?: string | null;
incident_id?: string | null;
matched_playbook_id?: string | null;
} | null;
plan?: {
step?: string | null;
flywheel_node?: string | null;
agent_id?: string | null;
required_scope?: string | null;
target_action?: string | null;
writes?: string[] | null;
} | null;
read_model_route?: {
agent_id?: string | null;
@@ -239,6 +254,7 @@ type RecurrenceWorkItemActionState = {
loading?:
| "preview"
| "dryRun"
| "approval"
| "handoff"
| "acceptSource"
| "rejectSource"
@@ -2661,7 +2677,7 @@ function Adr100RemediationQueuePanel({
const runAction = useCallback(async (
item: RemediationQueueItem,
action: "preview" | "dryRun"
action: "preview" | "dryRun" | "approval"
) => {
const workItemId = item.work_item_id ?? "";
if (!workItemId) return;
@@ -2677,7 +2693,13 @@ function Adr100RemediationQueuePanel({
`${API_BASE}/api/v1/ai/slo/remediation/preview?work_item_id=${encodeURIComponent(workItemId)}&mode=${encodeURIComponent(mode)}`,
12000
)
: await postJson<RecurrenceWorkItemActionResult>(
: action === "approval"
? await postJson<RecurrenceWorkItemActionResult>(
`${API_BASE}/api/v1/ai/slo/remediation/approval-request`,
{ work_item_id: workItemId, mode: "approval" },
15000
)
: await postJson<RecurrenceWorkItemActionResult>(
`${API_BASE}/api/v1/ai/slo/remediation/dry-run`,
{ work_item_id: workItemId, mode },
15000
@@ -2742,6 +2764,9 @@ function Adr100RemediationQueuePanel({
const state = workItemId ? actionState[workItemId] : undefined;
const result = state?.result ?? null;
const ticketPreview = result?.ticket_preview ?? null;
const approval = result?.approval ?? null;
const canCreateApproval = item.remediation_status === "needs_playbook_ticket"
|| item.remediation_action === "promote_diagnostic_to_repair_playbook";
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">
@@ -2799,6 +2824,17 @@ function Adr100RemediationQueuePanel({
<FileText className="h-3.5 w-3.5" aria-hidden="true" />
{state?.loading === "dryRun" ? t("actions.loading") : t("actions.dryRun")}
</button>
{canCreateApproval ? (
<button
type="button"
onClick={() => runAction(item, "approval")}
disabled={!workItemId || state?.loading === "approval"}
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"
>
<ShieldCheck className="h-3.5 w-3.5" aria-hidden="true" />
{state?.loading === "approval" ? t("actions.loading") : t("actions.approval")}
</button>
) : null}
</div>
{state?.error ? (
@@ -2816,7 +2852,22 @@ function Adr100RemediationQueuePanel({
incident: String(result.writes_incident_state ?? false),
autoRepair: String(result.writes_auto_repair_result ?? false),
})}</span>
{result.writes_approval_record ? (
<span className="font-mono">{t("result.approvalWrite")}</span>
) : null}
{result.deduplicated ? (
<span className="font-mono">{t("result.deduplicated")}</span>
) : null}
</div>
{approval ? (
<p className="mt-2 font-mono text-[11px] text-[#5f5b52]">
{t("result.approval", {
id: approval.id ?? result.approval_id ?? "--",
status: approval.status ?? "--",
risk: approval.risk_level ?? "--",
})}
</p>
) : null}
{ticketPreview ? (
<div className="mt-2 border-t border-[#e0ddd4] pt-2">
<p className="font-semibold text-[#141413]">{ticketPreview.title ?? t("ticketFallback")}</p>