feat(awooop): surface recurrence repair work items
All checks were successful
Code Review / ai-code-review (push) Successful in 10s
CD Pipeline / tests (push) Successful in 1m20s
CD Pipeline / build-and-deploy (push) Successful in 3m31s
CD Pipeline / post-deploy-checks (push) Successful in 1m32s

This commit is contained in:
Your Name
2026-05-18 20:30:43 +08:00
parent bbf5105fb4
commit b50614528e
7 changed files with 530 additions and 9 deletions

View File

@@ -127,6 +127,8 @@ class ChannelEventRecurrenceSummary(BaseModel):
verified_repair_group_total: int = 0
open_work_item_group_total: int = 0
manual_gate_group_total: int = 0
automation_gap_group_total: int = 0
failed_repair_group_total: int = 0
latest_received_at: datetime | None

View File

@@ -239,6 +239,18 @@ def build_dossier_recurrence(
for item in items
if _as_dict(item.get("repair_summary")).get("status") == "manual_gate"
),
"automation_gap_group_total": sum(
1
for item in items
if _as_dict(item.get("repair_summary")).get("status")
== "run_completed_no_repair"
),
"failed_repair_group_total": sum(
1
for item in items
if _as_dict(item.get("repair_summary")).get("status")
== "auto_repair_failed"
),
"latest_received_at": latest_received_at,
},
"items": items,
@@ -275,13 +287,49 @@ def _repair_status(
def _work_item_status(repair_status: str) -> str:
if repair_status in {"no_incident_link", "run_completed_no_repair"}:
if repair_status == "no_incident_link":
return "none"
if repair_status == "auto_repair_verified":
return "closed"
return "open"
def _work_item_kind(repair_status: str, auto_repair_id: Any) -> str:
if auto_repair_id:
return "verification"
if repair_status == "run_completed_no_repair":
return "automation_gap"
if repair_status == "manual_gate":
return "approval_followup"
if repair_status == "investigating":
return "investigation"
return "incident_followup"
def _work_item_next_step(repair_status: str) -> str:
return {
"auto_repair_succeeded_unverified": "run_post_verification",
"auto_repair_failed": "triage_failed_repair",
"auto_repair_recorded": "review_repair_record",
"manual_gate": "review_approval",
"investigating": "wait_for_run_completion",
"run_completed_no_repair": "create_repair_ticket",
"no_repair_record": "triage_missing_repair_record",
}.get(repair_status, "none")
def _work_item_reason(repair_status: str) -> str:
return {
"auto_repair_succeeded_unverified": "auto_repair_missing_verification",
"auto_repair_failed": "auto_repair_failed",
"auto_repair_recorded": "auto_repair_record_needs_review",
"manual_gate": "approval_required",
"investigating": "run_still_investigating",
"run_completed_no_repair": "completed_run_without_auto_repair",
"no_repair_record": "incident_without_repair_record",
}.get(repair_status, "none")
def _attach_work_item_summary(
group: dict[str, Any],
repair_summaries_by_incident: dict[str, dict[str, Any]],
@@ -335,7 +383,9 @@ def _attach_work_item_summary(
"incident_id": latest_incident_id,
"auto_repair_id": auto_repair_id,
"status": work_status,
"kind": "verification" if auto_repair_id else "incident_followup",
"kind": _work_item_kind(status_value, auto_repair_id),
"next_step": _work_item_next_step(status_value),
"reason": _work_item_reason(status_value),
"needs_human": work_status == "open",
}

View File

@@ -261,6 +261,8 @@ def test_build_dossier_recurrence_groups_events_and_run_state() -> None:
assert recurrence["summary"]["auto_repair_linked_total"] == 1
assert recurrence["summary"]["open_work_item_group_total"] == 1
assert recurrence["summary"]["verified_repair_group_total"] == 0
assert recurrence["summary"]["automation_gap_group_total"] == 0
assert recurrence["summary"]["failed_repair_group_total"] == 1
host_group = recurrence["items"][0]
assert host_group["recurrence_key"] == "fingerprint:fp-host-disk"
@@ -281,6 +283,64 @@ def test_build_dossier_recurrence_groups_events_and_run_state() -> None:
"auto_repair_id": "repair-1",
"status": "open",
"kind": "verification",
"next_step": "triage_failed_repair",
"reason": "auto_repair_failed",
"needs_human": True,
}
def test_build_dossier_recurrence_opens_work_item_for_completed_run_without_repair() -> None:
recurrence = build_dossier_recurrence(
[
{
"event_id": "event-1",
"project_id": "awoooi",
"channel_type": "internal",
"provider_event_id": "alertmanager:received:1",
"content_hash": "a" * 64,
"content_preview": "Docker container unhealthy",
"content_redacted": "Docker container unhealthy",
"redaction_version": "audit_sink_v1",
"source_envelope": {
"provider": "alertmanager",
"source_refs": {
"alert_ids": ["alert-1"],
"incident_ids": ["INC-20260517-F25B4A"],
"fingerprints": ["fp-container-unhealthy"],
},
"log_correlation": {
"alertname": "DockerContainerUnhealthy",
"severity": "warning",
"namespace": "momo",
"target_resource": "bitan-pharmacy-bitan-1",
"fingerprint": "fp-container-unhealthy",
},
},
"is_duplicate": True,
"provider_ts": None,
"received_at": "2026-05-17T23:47:00",
"run_id": UUID("33333333-3333-4333-8333-333333333333"),
"run_state": "completed",
"run_agent_id": "openclaw",
}
],
project_id="awoooi",
limit=20,
)
item = recurrence["items"][0]
assert recurrence["summary"]["open_work_item_group_total"] == 1
assert recurrence["summary"]["automation_gap_group_total"] == 1
assert item["repair_summary"]["status"] == "run_completed_no_repair"
assert item["work_item"] == {
"schema_version": "awooop_recurrence_work_item_link_v1",
"work_item_id": "incident:INC-20260517-F25B4A",
"incident_id": "INC-20260517-F25B4A",
"auto_repair_id": None,
"status": "open",
"kind": "automation_gap",
"next_step": "create_repair_ticket",
"reason": "completed_run_without_auto_repair",
"needs_human": True,
}
@@ -301,6 +361,8 @@ def test_recurrence_response_model_preserves_repair_work_item_fields() -> None:
"verified_repair_group_total": 1,
"open_work_item_group_total": 0,
"manual_gate_group_total": 0,
"automation_gap_group_total": 0,
"failed_repair_group_total": 0,
"latest_received_at": "2026-05-13T13:47:00",
},
"items": [

View File

@@ -1748,6 +1748,9 @@
"autoRepair": {
"title": "Low-risk Alertmanager auto-repair loop"
},
"recurrenceWorkItems": {
"title": "Recurring alert work item / ticket entry"
},
"remediationQueue": {
"title": "Non-success verification remediation queue"
},
@@ -1773,6 +1776,7 @@
"gates": {
"sourceDossier": "Inbound alerts must show received / incident_linked / source refs",
"autoRepair": "Requires auto_repair, verification_result=success, and KM writeback",
"recurrenceWorkItems": "Completed-without-repair, failed repair, and manual gate groups must become trackable work items",
"remediationQueue": "Every degraded / failed / timeout row must map to replay, reverify, ticket, or manual review",
"telegramCallbacks": "Detail and history buttons cannot depend only on Redis TTL or stale snapshots",
"ciSecretHygiene": "Workflows must not mount secrets in step env / action inputs; historical logs still need rotation and retention governance",
@@ -1784,6 +1788,10 @@
"evidence": {
"channelEvents": "Recent Alertmanager channel events: {count}",
"autoRepair": "Verified auto-repairs: {verified}/{evaluated}",
"recurrenceWorkItems": "Recurring alert work: {open}; no repair: {gap}; failed repair: {failed}; manual gates: {manual}",
"recurrenceLatest": "Latest: {alert} / {incident}",
"recurrenceReason": "Reason: {reason}",
"recurrenceEmpty": "No open recurring-alert work item in the recent window",
"remediationQueue": "Remediation work: {total}; AI-ready: {ready}; human: {human}",
"telegramCallbacks": "Telegram callback lookup and history summary are being repaired",
"telegramCallbacksLive": "Read-only callback toast 400 is nonfatal; detail / history replies now use DB truth-chain",
@@ -1816,6 +1824,55 @@
"verifiedUnknown": "Verified --",
"evaluatedUnknown": "Evaluated --",
"gateFailuresUnknown": "Gaps --"
},
"recurrence": {
"title": "Recurring Alert Work Items",
"subtitle": "Turns run_completed_no_repair, failed repair, and manual gates into trackable work items",
"open": "Open {count}",
"automationGap": "No repair {count}",
"failed": "Failed {count}",
"unavailable": "The recurrence API has not responded, so work item state cannot be claimed.",
"empty": "No open recurring-alert work items in the recent window.",
"occurrences": "{count}x",
"incident": "Incident: {incident}",
"workItem": "Work item: {id}",
"repair": "Repair status: {status}",
"reason": "Reason: {reason}",
"nextStep": "Next: {step}",
"openRun": "Open Run",
"openRuns": "Back to Runs",
"statuses": {
"auto_repair_verified": "Verified repair",
"auto_repair_succeeded_unverified": "Repair needs verification",
"auto_repair_failed": "Repair failed",
"auto_repair_recorded": "Repair recorded",
"manual_gate": "Manual gate needed",
"investigating": "Investigating",
"run_completed_no_repair": "Run completed without repair",
"no_repair_record": "No repair record",
"unknown": "Unknown"
},
"reasons": {
"auto_repair_missing_verification": "Auto-repair lacks verification",
"auto_repair_failed": "Auto-repair failed",
"auto_repair_record_needs_review": "Repair record needs review",
"approval_required": "Approval required",
"run_still_investigating": "Run is still investigating",
"completed_run_without_auto_repair": "Run completed without an auto-repair record",
"incident_without_repair_record": "Incident has no repair record",
"none": "None",
"unknown": "Unknown"
},
"nextSteps": {
"run_post_verification": "Run post-execution verification",
"triage_failed_repair": "Triage failed repair",
"review_repair_record": "Review repair record",
"review_approval": "Review approval",
"wait_for_run_completion": "Wait for Run completion",
"create_repair_ticket": "Create repair ticket",
"triage_missing_repair_record": "Fill missing repair record",
"none": "None"
}
}
},
"listEvidence": {

View File

@@ -1749,6 +1749,9 @@
"autoRepair": {
"title": "低風險 Alertmanager 自動修復閉環"
},
"recurrenceWorkItems": {
"title": "重複告警工作項 / Ticket 入口"
},
"remediationQueue": {
"title": "非成功驗證補救工作佇列"
},
@@ -1774,6 +1777,7 @@
"gates": {
"sourceDossier": "入站告警必須能查到 received / incident_linked / source refs",
"autoRepair": "必須同時有 auto_repair、verification_result=success 與 KM 回寫",
"recurrenceWorkItems": "Run 完成無修復、修復失敗與人工閘門必須進入可追蹤工作項",
"remediationQueue": "每筆 degraded / failed / timeout 都必須映射到重跑、重驗、Ticket 或人工檢查",
"telegramCallbacks": "按下詳情與歷史不能再只依賴 Redis TTL 或舊快照",
"ciSecretHygiene": "workflow 不可再把 secrets 掛在 step env / action input歷史 log 需另做輪換與保留期治理",
@@ -1785,6 +1789,10 @@
"evidence": {
"channelEvents": "最近 Alertmanager channel events{count}",
"autoRepair": "已驗證自動修復:{verified}/{evaluated}",
"recurrenceWorkItems": "重複告警待處理:{open};無修復:{gap};修復失敗:{failed};人工閘門:{manual}",
"recurrenceLatest": "最新:{alert} / {incident}",
"recurrenceReason": "原因:{reason}",
"recurrenceEmpty": "近期重複告警尚無待處理工作項",
"remediationQueue": "補救工作:{total}AI 可接手:{ready};人工:{human}",
"telegramCallbacks": "目前修補 Telegram callback 查詢鏈與歷史摘要",
"telegramCallbacksLive": "read-only callback toast 400 已非致命;詳情 / 歷史改由 DB truth-chain 回覆",
@@ -1817,6 +1825,55 @@
"verifiedUnknown": "已驗證 --",
"evaluatedUnknown": "已評估 --",
"gateFailuresUnknown": "缺口 --"
},
"recurrence": {
"title": "重複告警工作項",
"subtitle": "把 run_completed_no_repair、修復失敗與人工閘門接成可追蹤 work item",
"open": "待處理 {count}",
"automationGap": "無修復 {count}",
"failed": "修復失敗 {count}",
"unavailable": "recurrence API 尚未回應,不能判定工作項狀態。",
"empty": "近期重複告警沒有待處理工作項。",
"occurrences": "{count} 次",
"incident": "Incident{incident}",
"workItem": "Work item{id}",
"repair": "修復狀態:{status}",
"reason": "原因:{reason}",
"nextStep": "下一步:{step}",
"openRun": "開啟 Run",
"openRuns": "回 Run 監控",
"statuses": {
"auto_repair_verified": "已驗證修復",
"auto_repair_succeeded_unverified": "修復待驗證",
"auto_repair_failed": "修復失敗",
"auto_repair_recorded": "修復已記錄",
"manual_gate": "需人工閘門",
"investigating": "調查中",
"run_completed_no_repair": "Run 完成無修復",
"no_repair_record": "無修復記錄",
"unknown": "未知"
},
"reasons": {
"auto_repair_missing_verification": "自動修復缺驗證",
"auto_repair_failed": "自動修復失敗",
"auto_repair_record_needs_review": "修復紀錄待確認",
"approval_required": "需要審批",
"run_still_investigating": "Run 尚在調查",
"completed_run_without_auto_repair": "Run 已完成但沒有自動修復紀錄",
"incident_without_repair_record": "Incident 沒有修復紀錄",
"none": "無",
"unknown": "未知"
},
"nextSteps": {
"run_post_verification": "執行修復後驗證",
"triage_failed_repair": "盤點失敗修復",
"review_repair_record": "檢查修復紀錄",
"review_approval": "處理審批",
"wait_for_run_completion": "等待 Run 完成",
"create_repair_ticket": "建立修復 Ticket",
"triage_missing_repair_record": "補齊修復紀錄",
"none": "無"
}
}
},
"listEvidence": {

View File

@@ -197,6 +197,8 @@ interface EventRecurrenceSummary {
verified_repair_group_total?: number;
open_work_item_group_total?: number;
manual_gate_group_total?: number;
automation_gap_group_total?: number;
failed_repair_group_total?: number;
latest_received_at?: string | null;
}
@@ -227,6 +229,8 @@ interface EventRecurrenceWorkItem {
auto_repair_id?: string | null;
status?: "open" | "closed" | "none" | string;
kind?: string | null;
next_step?: string | null;
reason?: string | null;
needs_human?: boolean;
}

View File

@@ -6,6 +6,7 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useSearchParams } from "next/navigation";
import { useLocale, useTranslations } from "next-intl";
import {
Activity,
@@ -14,8 +15,10 @@ import {
Database,
Gauge,
GitBranch,
ListChecks,
Network,
RefreshCw,
SearchCheck,
ShieldCheck,
TriangleAlert,
} from "lucide-react";
@@ -51,6 +54,56 @@ type RecentEventsResponse = {
events?: Array<{ provider_event_id: string; is_duplicate: boolean }>;
};
type RecurrenceWorkItem = {
work_item_id?: string | null;
incident_id?: string | null;
auto_repair_id?: string | null;
status?: string | null;
kind?: string | null;
next_step?: string | null;
reason?: string | null;
needs_human?: boolean | null;
};
type RecurrenceItem = {
recurrence_key: string;
provider?: string | null;
alertname?: string | null;
severity?: string | null;
namespace?: string | null;
target_resource?: string | null;
latest_run_id?: string | null;
latest_run_state?: string | null;
latest_incident_id?: string | null;
occurrence_total: number;
duplicate_total: number;
repair_summary?: {
status?: string | null;
latest_auto_repair_id?: string | null;
latest_verification_result?: string | null;
auto_repair_total?: number | null;
} | null;
work_item?: RecurrenceWorkItem | null;
};
type RecurrenceResponse = {
project_id: string;
summary: {
source_event_total: number;
recurrence_group_total: number;
recurrent_group_total: number;
duplicate_event_total: number;
linked_run_total: number;
auto_repair_linked_total?: number;
verified_repair_group_total?: number;
open_work_item_group_total?: number;
manual_gate_group_total?: number;
automation_gap_group_total?: number;
failed_repair_group_total?: number;
};
items: RecurrenceItem[];
};
type SloResponse = {
adr100?: {
verification_coverage?: {
@@ -86,6 +139,7 @@ type Telemetry = {
governanceEvents: GovernanceEventsResponse | null;
governanceQueue: GovernanceQueueResponse | null;
channelEvents: RecentEventsResponse | null;
eventRecurrence: RecurrenceResponse | null;
slo: SloResponse | null;
remediationHistory: RemediationHistoryResponse | null;
};
@@ -153,6 +207,26 @@ function routeLabel(item?: RemediationHistoryItem | null) {
return route || "--";
}
function recurrenceOpenItems(recurrence: RecurrenceResponse | null) {
return (recurrence?.items ?? []).filter((item) => item.work_item?.status === "open");
}
function recurrenceRepairStatusKey(status?: string | null) {
if (
status === "auto_repair_verified" ||
status === "auto_repair_succeeded_unverified" ||
status === "auto_repair_failed" ||
status === "auto_repair_recorded" ||
status === "manual_gate" ||
status === "investigating" ||
status === "run_completed_no_repair" ||
status === "no_repair_record"
) {
return status;
}
return "unknown";
}
function buildWorkItems(
telemetry: Telemetry,
t: ReturnType<typeof useTranslations>
@@ -168,6 +242,12 @@ function buildWorkItems(
const remediationNeedsHuman = remediationQueue?.needs_human ?? 0;
const remediationHistoryTotal = telemetry.remediationHistory?.total ?? 0;
const latestRemediationHistory = telemetry.remediationHistory?.items?.[0] ?? null;
const recurrenceSummary = telemetry.eventRecurrence?.summary;
const recurrenceOpen = recurrenceSummary?.open_work_item_group_total ?? 0;
const recurrenceAutomationGap = recurrenceSummary?.automation_gap_group_total ?? 0;
const recurrenceFailedRepair = recurrenceSummary?.failed_repair_group_total ?? 0;
const recurrenceManualGate = recurrenceSummary?.manual_gate_group_total ?? 0;
const latestRecurrenceOpenItem = recurrenceOpenItems(telemetry.eventRecurrence)[0] ?? null;
const governanceEventsUnavailable = telemetry.governanceEvents === null;
const governanceQueueMissing = telemetry.governanceQueue?.table_pending === true;
const governanceDispatchBlocked =
@@ -201,6 +281,40 @@ function buildWorkItems(
}),
href: "/awooop/runs",
},
{
id: "recurrenceWorkItems",
phase: "T61",
status: recurrenceOpen > 0
? "in_progress"
: telemetry.eventRecurrence
? "watching"
: "blocked",
surfaceKey: "workItems",
source: "/api/v1/platform/events/dossier/recurrence",
gateKey: "recurrenceWorkItems",
evidence: t("evidence.recurrenceWorkItems", {
open: recurrenceOpen,
gap: recurrenceAutomationGap,
failed: recurrenceFailedRepair,
manual: recurrenceManualGate,
}),
evidenceDetails: latestRecurrenceOpenItem
? [
t("evidence.recurrenceLatest", {
alert: latestRecurrenceOpenItem.alertname ?? "--",
incident: latestRecurrenceOpenItem.latest_incident_id ?? "--",
}),
t("evidence.recurrenceReason", {
reason: t(
`recurrence.reasons.${latestRecurrenceOpenItem.work_item?.reason ?? "unknown"}` as never
),
}),
]
: [t("evidence.recurrenceEmpty")],
href: latestRecurrenceOpenItem?.work_item?.work_item_id
? `/awooop/work-items?project_id=${encodeURIComponent(telemetry.eventRecurrence?.project_id ?? "awoooi")}&work_item_id=${encodeURIComponent(latestRecurrenceOpenItem.work_item.work_item_id)}${latestRecurrenceOpenItem.work_item.incident_id ? `&incident_id=${encodeURIComponent(latestRecurrenceOpenItem.work_item.incident_id)}` : ""}`
: "/awooop/runs",
},
{
id: "remediationQueue",
phase: "T24",
@@ -414,14 +528,148 @@ function ProductionClaimBanner({
);
}
function RecurrenceWorkQueuePanel({
recurrence,
focusedWorkItemId,
projectId,
}: {
recurrence: RecurrenceResponse | null;
focusedWorkItemId: string | null;
projectId: string;
}) {
const t = useTranslations("awooop.workItems.recurrence");
const openItems = recurrenceOpenItems(recurrence);
const focusedItem = focusedWorkItemId
? openItems.find((item) => item.work_item?.work_item_id === focusedWorkItemId)
: null;
const visibleItems = focusedItem
? [focusedItem, ...openItems.filter((item) => item !== focusedItem).slice(0, 5)]
: openItems.slice(0, 6);
const summary = recurrence?.summary;
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 items-center gap-2">
<ListChecks className="h-4 w-4 text-[#8a5a08]" aria-hidden="true" />
<div>
<h3 className="text-sm font-semibold text-[#141413]">{t("title")}</h3>
<p className="text-xs text-[#77736a]">{t("subtitle")}</p>
</div>
</div>
<div className="flex flex-wrap items-center gap-2 font-mono text-xs text-[#5f5b52]">
<span className="border border-[#d9b36f] bg-[#fff7e8] px-2 py-0.5">
{t("open", { count: summary?.open_work_item_group_total ?? 0 })}
</span>
<span className="border border-[#d8d3c7] bg-white px-2 py-0.5">
{t("automationGap", { count: summary?.automation_gap_group_total ?? 0 })}
</span>
<span className="border border-[#d8d3c7] bg-white px-2 py-0.5">
{t("failed", { count: summary?.failed_repair_group_total ?? 0 })}
</span>
</div>
</div>
{recurrence === null ? (
<div className="px-4 py-4 text-sm text-[#8a5a08]">
{t("unavailable")}
</div>
) : openItems.length === 0 ? (
<div className="px-4 py-4 text-sm text-[#5f5b52]">
{t("empty")}
</div>
) : (
<div className="grid gap-px bg-[#eee9dd] md:grid-cols-2 xl:grid-cols-3">
{visibleItems.map((item) => {
const workItem = item.work_item;
const isFocused = Boolean(
focusedWorkItemId && workItem?.work_item_id === 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;
return (
<article
key={workItem?.work_item_id ?? item.recurrence_key}
className={cn(
"bg-white px-4 py-3",
isFocused && "outline outline-2 outline-[#d97757]"
)}
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="truncate font-mono text-xs font-semibold text-[#141413]">
{item.alertname || item.provider || item.recurrence_key}
</p>
<p className="mt-1 truncate text-xs text-[#77736a]">
{item.namespace || "--"} / {item.target_resource || "--"}
</p>
</div>
<span className="shrink-0 border border-[#d9b36f] bg-[#fff7e8] px-2 py-0.5 font-mono text-xs font-semibold text-[#8a5a08]">
{t("occurrences", { count: item.occurrence_total })}
</span>
</div>
<div className="mt-3 grid gap-1 text-xs leading-5 text-[#5f5b52]">
<p>{t("incident", { incident: item.latest_incident_id ?? "--" })}</p>
<p>{t("workItem", { id: workItem?.work_item_id ?? "--" })}</p>
<p>
{t("repair", {
status: t(`statuses.${repairStatusKey}` as never),
})}
</p>
<p>
{t("reason", {
reason: t(`reasons.${workItem?.reason ?? "unknown"}` as never),
})}
</p>
<p>
{t("nextStep", {
step: t(`nextSteps.${workItem?.next_step ?? "none"}` as never),
})}
</p>
</div>
<div className="mt-3 flex flex-wrap items-center gap-2">
{runHref ? (
<Link
href={runHref as never}
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]"
>
<SearchCheck className="h-3.5 w-3.5" aria-hidden="true" />
{t("openRun")}
</Link>
) : null}
<Link
href={`/awooop/runs?project_id=${encodeURIComponent(projectId)}` as never}
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]"
>
<ArrowRight className="h-3.5 w-3.5" aria-hidden="true" />
{t("openRuns")}
</Link>
</div>
</article>
);
})}
</div>
)}
</section>
);
}
export default function AwoooPWorkItemsPage() {
const t = useTranslations("awooop.workItems");
const locale = useLocale();
const searchParams = useSearchParams();
const projectId = searchParams.get("project_id") || "awoooi";
const focusedWorkItemId = searchParams.get("work_item_id");
const focusedIncidentId = searchParams.get("incident_id");
const [telemetry, setTelemetry] = useState<Telemetry>({
quality: null,
governanceEvents: null,
governanceQueue: null,
channelEvents: null,
eventRecurrence: null,
slo: null,
remediationHistory: null,
});
@@ -430,26 +678,45 @@ export default function AwoooPWorkItemsPage() {
const fetchTelemetry = useCallback(async () => {
setLoading(true);
const qualityUrl = `${API_BASE}/api/v1/platform/truth-chain/quality/summary?project_id=awoooi&hours=24&limit=30`;
const encodedProjectId = encodeURIComponent(projectId);
const qualityUrl = `${API_BASE}/api/v1/platform/truth-chain/quality/summary?project_id=${encodedProjectId}&hours=24&limit=30`;
const governanceEventsUrl = `${API_BASE}/api/v1/ai/governance/events?event_type=knowledge_degradation&event_type=governance_slo_data_gap&status=unresolved&size=10`;
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 channelEventsUrl = `${API_BASE}/api/v1/platform/events/recent?project_id=${encodedProjectId}&provider_prefix=alertmanager&limit=20`;
const recurrenceUrl = `${API_BASE}/api/v1/platform/events/dossier/recurrence?project_id=${encodedProjectId}&limit=100`;
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, remediationHistory] = await Promise.all([
const [
quality,
governanceEvents,
governanceQueue,
channelEvents,
eventRecurrence,
slo,
remediationHistory,
] = await Promise.all([
fetchJson<AutomationQualitySummary>(qualityUrl, 15000),
fetchJson<GovernanceEventsResponse>(governanceEventsUrl),
fetchJson<GovernanceQueueResponse>(governanceQueueUrl),
fetchJson<RecentEventsResponse>(channelEventsUrl),
fetchJson<RecurrenceResponse>(recurrenceUrl),
fetchJson<SloResponse>(sloUrl),
fetchJson<RemediationHistoryResponse>(remediationHistoryUrl),
]);
setTelemetry({ quality, governanceEvents, governanceQueue, channelEvents, slo, remediationHistory });
setTelemetry({
quality,
governanceEvents,
governanceQueue,
channelEvents,
eventRecurrence,
slo,
remediationHistory,
});
setLastUpdated(new Date());
setLoading(false);
}, []);
}, [projectId]);
useEffect(() => {
fetchTelemetry();
@@ -467,6 +734,22 @@ export default function AwoooPWorkItemsPage() {
),
[telemetry.remediationHistory?.items]
);
const recurrenceIncidentIds = useMemo(
() => recurrenceOpenItems(telemetry.eventRecurrence)
.map((item) => item.latest_incident_id ?? item.work_item?.incident_id)
.filter(Boolean),
[telemetry.eventRecurrence]
);
const visibleIncidentIds = useMemo(
() => Array.from(
new Set([
...remediationIncidentIds,
...recurrenceIncidentIds,
focusedIncidentId,
].filter(Boolean))
),
[focusedIncidentId, recurrenceIncidentIds, remediationIncidentIds]
);
const summary = useMemo(
() => [
{ label: t("summary.live"), value: workItems.filter((item) => item.status === "live").length, icon: Activity },
@@ -528,14 +811,20 @@ export default function AwoooPWorkItemsPage() {
</div>
<IncidentEvidenceHeader
projectId="awoooi"
incidentIds={remediationIncidentIds}
projectId={projectId}
incidentIds={visibleIncidentIds}
dryRunCount={telemetry.remediationHistory?.total}
latestRoute={routeLabel(latestRemediationHistory)}
writesIncidentState={latestRemediationHistory?.writes_incident_state}
writesAutoRepairResult={latestRemediationHistory?.writes_auto_repair_result}
/>
<RecurrenceWorkQueuePanel
recurrence={telemetry.eventRecurrence}
focusedWorkItemId={focusedWorkItemId}
projectId={projectId}
/>
<div className="overflow-hidden border border-[#e0ddd4] bg-white">
<div className="overflow-x-auto">
<table className="w-full" role="table" aria-label={t("tableLabel")}>