feat(awooop): surface recurrence repair work items
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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")}>
|
||||
|
||||
Reference in New Issue
Block a user