feat(drift): surface fingerprint state handoff
All checks were successful
Code Review / ai-code-review (push) Successful in 11s
CD Pipeline / tests (push) Successful in 1m14s
CD Pipeline / build-and-deploy (push) Successful in 3m46s
CD Pipeline / post-deploy-checks (push) Successful in 1m35s

This commit is contained in:
Your Name
2026-05-19 00:39:49 +08:00
parent 55ab8732c5
commit 0b5268a666
7 changed files with 1316 additions and 7 deletions

View File

@@ -1057,7 +1057,20 @@
"adoptConfirm": "Adopt this change and update Git?",
"pending": "Pending",
"resolved": "Resolved",
"ignored": "Ignored"
"acknowledged": "Acknowledged",
"rolled_back": "Rolled back",
"adopted": "Adopted",
"ignored": "Ignored",
"fingerprintState": {
"title": "Same-fingerprint state chain",
"occurrences": "12h {count}x",
"report": "Report: {report}",
"state": "State: {state}",
"next": "Next: {step}",
"writes": "Writes: drift={drift}; incident={incident}; repair={repair}; ticket={ticket}",
"pr": "PR: {pr}; zeroDiff={zeroDiff}",
"p0Dedup": "P0 dedupe {hours}h"
}
},
"neuralCommand": {
"title": "Neural Command Center",
@@ -1751,6 +1764,9 @@
"recurrenceWorkItems": {
"title": "Recurring alert work item / ticket entry"
},
"configDriftFsm": {
"title": "Config Drift fingerprint state machine"
},
"remediationQueue": {
"title": "Non-success verification remediation queue"
},
@@ -1777,6 +1793,7 @@
"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",
"configDriftFsm": "The same drift fingerprint must expose recurrence, PR, zero diff, handoff, and next step",
"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",
@@ -1792,6 +1809,12 @@
"recurrenceLatest": "Latest: {alert} / {incident}",
"recurrenceReason": "Reason: {reason}",
"recurrenceEmpty": "No open recurring-alert work item in the recent window",
"driftFingerprint": "Config Drift: {state}; {count}x in 12h",
"driftFingerprintUnavailable": "Config Drift fingerprint state API has not responded",
"driftFingerprintId": "Fingerprint: {fingerprint}; Report: {report}",
"driftFingerprintPr": "PR: {pr}; zeroDiff={zeroDiff}",
"driftFingerprintNext": "Next: {step}",
"driftFingerprintEmpty": "No Config Drift fingerprint state yet",
"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",
@@ -1825,6 +1848,60 @@
"evaluatedUnknown": "Evaluated --",
"gateFailuresUnknown": "Gaps --"
},
"driftFingerprint": {
"title": "Config Drift Fingerprint State",
"subtitle": "Collapses hourly drift reports into one state chain with PR, zero diff, P0 dedupe, and human handoff evidence",
"unavailable": "The drift fingerprint state API has not responded, so recurrence, PR, and handoff state cannot be claimed.",
"occurrences": "12h {count}x",
"risk": "HIGH {high} / MEDIUM {medium} / INFO {info}",
"report": "Report: {report}; Namespace: {namespace}",
"summary": "Summary: {summary}",
"next": "Next: {step}",
"p0Dedup": "P0 dedupe: {enabled}; window {hours}h",
"writes": "Writes: drift={drift}; incident={incident}; repair={repair}; ticket={ticket}",
"fsmStates": {
"pending_human": "Waiting for human",
"pending_human_repeated": "Repeated human wait",
"pr_open_zero_diff": "PR open but zero diff",
"pr_open_waiting_review": "PR waiting review",
"pr_merged_unverified": "PR merged, unverified",
"handoff_recorded": "Handoff recorded",
"adopted_unverified": "Adopted, unverified",
"rolled_back": "Rolled back",
"acknowledged": "Acknowledged",
"ignored": "Ignored",
"unknown": "Unknown"
},
"nextSteps": {
"close_zero_diff_pr_and_prepare_real_yaml_patch": "Close zero-diff PR and prepare a real YAML patch",
"review_pr_then_merge_or_reject": "Review PR, then merge or reject",
"verify_git_baseline_then_mark_adopted": "Verify Git baseline, then mark adopted",
"operator_review_handoff_and_execute_manual_plan": "Operator reviews handoff and executes manual plan",
"verify_k8s_matches_git_baseline": "Verify K8s matches Git baseline",
"confirm_no_repeat_after_rollback": "Confirm no repeat after rollback",
"monitor_for_recurrence": "Monitor for recurrence",
"retry_pr_lookup_then_review_drift": "Retry PR lookup, then review drift",
"manual_investigation_or_ansible_check_mode": "Manual investigation or Ansible check-mode",
"unknown": "Unknown"
},
"pr": {
"title": "PR / Baseline",
"number": "PR: {number}",
"zeroDiff": "zeroDiff={zeroDiff}; files={files}; commits={commits}",
"status": "Status: {status}"
},
"handoff": {
"latest": "Latest handoff: {status}"
},
"actions": {
"record": "Record handoff",
"recording": "Recording",
"openDrift": "Open Drift",
"failed": "The handoff API did not respond, so human handoff cannot be claimed.",
"recorded": "Handoff stored: {recorded}",
"handoffStatus": "Handoff status: {status}"
}
},
"recurrence": {
"title": "Recurring Alert Work Items",
"subtitle": "Turns run_completed_no_repair, failed repair, and manual gates into trackable work items",

View File

@@ -1058,7 +1058,20 @@
"adoptConfirm": "確定要將此變更承認並更新至 Git 嗎?",
"pending": "待處理",
"resolved": "已解決",
"ignored": "已忽略"
"acknowledged": "已知悉",
"rolled_back": "已回滾",
"adopted": "已採納",
"ignored": "已忽略",
"fingerprintState": {
"title": "同指紋狀態鏈",
"occurrences": "12h {count} 次",
"report": "Report{report}",
"state": "狀態:{state}",
"next": "下一步:{step}",
"writes": "寫入drift={drift}incident={incident}repair={repair}ticket={ticket}",
"pr": "PR{pr}zeroDiff={zeroDiff}",
"p0Dedup": "P0 去重 {hours}h"
}
},
"neuralCommand": {
"title": "神經指揮中心",
@@ -1752,6 +1765,9 @@
"recurrenceWorkItems": {
"title": "重複告警工作項 / Ticket 入口"
},
"configDriftFsm": {
"title": "Config Drift fingerprint 狀態機"
},
"remediationQueue": {
"title": "非成功驗證補救工作佇列"
},
@@ -1778,6 +1794,7 @@
"sourceDossier": "入站告警必須能查到 received / incident_linked / source refs",
"autoRepair": "必須同時有 auto_repair、verification_result=success 與 KM 回寫",
"recurrenceWorkItems": "Run 完成無修復、修復失敗與人工閘門必須進入可追蹤工作項",
"configDriftFsm": "同一 drift fingerprint 必須顯示重複、PR、零 diff、交接與下一步",
"remediationQueue": "每筆 degraded / failed / timeout 都必須映射到重跑、重驗、Ticket 或人工檢查",
"telegramCallbacks": "按下詳情與歷史不能再只依賴 Redis TTL 或舊快照",
"ciSecretHygiene": "workflow 不可再把 secrets 掛在 step env / action input歷史 log 需另做輪換與保留期治理",
@@ -1793,6 +1810,12 @@
"recurrenceLatest": "最新:{alert} / {incident}",
"recurrenceReason": "原因:{reason}",
"recurrenceEmpty": "近期重複告警尚無待處理工作項",
"driftFingerprint": "Config Drift{state}12h 內 {count} 次",
"driftFingerprintUnavailable": "Config Drift fingerprint state API 尚未回應",
"driftFingerprintId": "Fingerprint{fingerprint}Report{report}",
"driftFingerprintPr": "PR{pr}zeroDiff={zeroDiff}",
"driftFingerprintNext": "下一步:{step}",
"driftFingerprintEmpty": "尚無 Config Drift fingerprint 狀態",
"remediationQueue": "補救工作:{total}AI 可接手:{ready};人工:{human}",
"telegramCallbacks": "目前修補 Telegram callback 查詢鏈與歷史摘要",
"telegramCallbacksLive": "read-only callback toast 400 已非致命;詳情 / 歷史改由 DB truth-chain 回覆",
@@ -1826,6 +1849,60 @@
"evaluatedUnknown": "已評估 --",
"gateFailuresUnknown": "缺口 --"
},
"driftFingerprint": {
"title": "Config Drift fingerprint 狀態",
"subtitle": "把每小時 drift report 收斂成同一狀態鏈,顯示 PR、零 diff、P0 去重與人工交接",
"unavailable": "drift fingerprint state API 尚未回應,不能判定是否重複、是否已有 PR 或是否已交接。",
"occurrences": "12h {count} 次",
"risk": "HIGH {high} / MEDIUM {medium} / INFO {info}",
"report": "Report{report}Namespace{namespace}",
"summary": "摘要:{summary}",
"next": "下一步:{step}",
"p0Dedup": "P0 去重:{enabled};視窗 {hours}h",
"writes": "寫入drift={drift}incident={incident}repair={repair}ticket={ticket}",
"fsmStates": {
"pending_human": "等待人工",
"pending_human_repeated": "重複等待人工",
"pr_open_zero_diff": "PR 開啟但零 diff",
"pr_open_waiting_review": "PR 等待 review",
"pr_merged_unverified": "PR 已 merge 待驗證",
"handoff_recorded": "交接已記錄",
"adopted_unverified": "已採納待驗證",
"rolled_back": "已回滾",
"acknowledged": "已知悉",
"ignored": "已忽略",
"unknown": "未知"
},
"nextSteps": {
"close_zero_diff_pr_and_prepare_real_yaml_patch": "關閉零 diff PR準備真實 YAML patch",
"review_pr_then_merge_or_reject": "review PR 後 merge 或 reject",
"verify_git_baseline_then_mark_adopted": "驗證 Git baseline 後標記採納",
"operator_review_handoff_and_execute_manual_plan": "Operator review 交接並執行人工方案",
"verify_k8s_matches_git_baseline": "驗證 K8s 與 Git baseline 一致",
"confirm_no_repeat_after_rollback": "確認回滾後不再重複",
"monitor_for_recurrence": "監控是否復發",
"retry_pr_lookup_then_review_drift": "重試 PR 查詢後 review drift",
"manual_investigation_or_ansible_check_mode": "人工調查或 Ansible check-mode",
"unknown": "未知"
},
"pr": {
"title": "PR / Baseline",
"number": "PR{number}",
"zeroDiff": "zeroDiff={zeroDiff}files={files}commits={commits}",
"status": "狀態:{status}"
},
"handoff": {
"latest": "最近交接:{status}"
},
"actions": {
"record": "記錄交接",
"recording": "記錄中",
"openDrift": "開啟 Drift",
"failed": "交接 API 未回應,不能宣稱已轉人工。",
"recorded": "交接入庫:{recorded}",
"handoffStatus": "交接狀態:{status}"
}
},
"recurrence": {
"title": "重複告警工作項",
"subtitle": "把 run_completed_no_repair、修復失敗與人工閘門接成可追蹤 work item",

View File

@@ -13,8 +13,10 @@ import {
ArrowRight,
ClipboardList,
Database,
Fingerprint,
Gauge,
GitBranch,
GitPullRequest,
ListChecks,
Network,
RefreshCw,
@@ -192,6 +194,63 @@ type RemediationHistoryResponse = {
items?: RemediationHistoryItem[];
};
type DriftFingerprintState = {
schema_version?: string;
namespace?: string;
fingerprint?: string;
latest_report_id?: string;
latest_status?: string;
summary?: string;
high_count?: number;
medium_count?: number;
info_count?: number;
occurrences_12h?: number;
fsm_state?: string;
operator_stage?: string;
next_step?: string;
open_pr?: {
number?: number | string | null;
title?: string | null;
state?: string | null;
merged?: boolean | null;
mergeable?: boolean | null;
html_url?: string | null;
url?: string | null;
file_count?: number | null;
commit_count?: number | null;
is_zero_diff?: boolean | null;
lookup_status?: string | null;
lookup_error?: string | null;
} | null;
latest_handoff?: {
handoff_kind?: string | null;
handoff_status?: string | null;
pr_url?: string | null;
created_at?: string | null;
lookup_error?: string | null;
} | null;
p0_escalation?: {
suppresses_repeated_p0?: boolean | null;
dedup_window_hours?: number | null;
} | null;
writes_incident_state?: boolean | null;
writes_auto_repair_result?: boolean | null;
writes_drift_status?: boolean | null;
writes_ticket?: boolean | null;
creates_external_ticket?: boolean | null;
};
type DriftFingerprintHandoffResult = DriftFingerprintState & {
handoff_kind?: string | null;
handoff_status?: string | null;
history?: {
recorded?: boolean | null;
alert_operation_id?: string | null;
timeline_event_id?: string | null;
reason?: string | null;
} | null;
};
type Telemetry = {
quality: AutomationQualitySummary | null;
governanceEvents: GovernanceEventsResponse | null;
@@ -200,6 +259,7 @@ type Telemetry = {
eventRecurrence: RecurrenceResponse | null;
slo: SloResponse | null;
remediationHistory: RemediationHistoryResponse | null;
driftFingerprintState: DriftFingerprintState | null;
};
type WorkItem = {
@@ -354,6 +414,50 @@ function recurrenceHandoffKindKey(kind?: string | null) {
return "unknown";
}
function driftFsmStateKey(state?: string | null) {
if (
state === "pending_human" ||
state === "pending_human_repeated" ||
state === "pr_open_zero_diff" ||
state === "pr_open_waiting_review" ||
state === "pr_merged_unverified" ||
state === "handoff_recorded" ||
state === "adopted_unverified" ||
state === "rolled_back" ||
state === "acknowledged" ||
state === "ignored"
) {
return state;
}
return "unknown";
}
function driftNextStepKey(step?: string | null) {
if (
step === "close_zero_diff_pr_and_prepare_real_yaml_patch" ||
step === "review_pr_then_merge_or_reject" ||
step === "verify_git_baseline_then_mark_adopted" ||
step === "operator_review_handoff_and_execute_manual_plan" ||
step === "verify_k8s_matches_git_baseline" ||
step === "confirm_no_repeat_after_rollback" ||
step === "monitor_for_recurrence" ||
step === "retry_pr_lookup_then_review_drift" ||
step === "manual_investigation_or_ansible_check_mode"
) {
return step;
}
return "unknown";
}
function driftStatusForWorkItem(state: DriftFingerprintState | null): WorkStatus {
const fsm = state?.fsm_state;
if (!state) return "blocked";
if (fsm === "adopted_unverified" || fsm === "pr_merged_unverified") return "in_progress";
if (fsm === "rolled_back" || fsm === "acknowledged" || fsm === "ignored") return "watching";
if (fsm === "pr_open_waiting_review" || fsm === "handoff_recorded") return "in_progress";
return "blocked";
}
function buildWorkItems(
telemetry: Telemetry,
t: ReturnType<typeof useTranslations>
@@ -375,6 +479,9 @@ function buildWorkItems(
const recurrenceFailedRepair = recurrenceSummary?.failed_repair_group_total ?? 0;
const recurrenceManualGate = recurrenceSummary?.manual_gate_group_total ?? 0;
const latestRecurrenceOpenItem = recurrenceOpenItems(telemetry.eventRecurrence)[0] ?? null;
const driftState = telemetry.driftFingerprintState;
const driftFsmKey = driftFsmStateKey(driftState?.fsm_state);
const driftNextKey = driftNextStepKey(driftState?.next_step);
const governanceEventsUnavailable = telemetry.governanceEvents === null;
const governanceQueueMissing = telemetry.governanceQueue?.table_pending === true;
const governanceDispatchBlocked =
@@ -442,6 +549,36 @@ function buildWorkItems(
? `/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: "configDriftFsm",
phase: "T64",
status: driftStatusForWorkItem(driftState),
surfaceKey: "workItems",
source: "/api/v1/drift/fingerprints/state",
gateKey: "configDriftFsm",
evidence: driftState
? t("evidence.driftFingerprint", {
state: t(`driftFingerprint.fsmStates.${driftFsmKey}` as never),
count: driftState.occurrences_12h ?? 0,
})
: t("evidence.driftFingerprintUnavailable"),
evidenceDetails: driftState
? [
t("evidence.driftFingerprintId", {
fingerprint: driftState.fingerprint ?? "--",
report: driftState.latest_report_id ?? "--",
}),
t("evidence.driftFingerprintPr", {
pr: driftState.open_pr?.number ?? "--",
zeroDiff: String(driftState.open_pr?.is_zero_diff ?? false),
}),
t("evidence.driftFingerprintNext", {
step: t(`driftFingerprint.nextSteps.${driftNextKey}` as never),
}),
]
: [t("evidence.driftFingerprintEmpty")],
href: "/drift",
},
{
id: "remediationQueue",
phase: "T24",
@@ -948,6 +1085,187 @@ function RecurrenceWorkQueuePanel({
);
}
function DriftFingerprintPanel({
state,
onRecorded,
}: {
state: DriftFingerprintState | null;
onRecorded: () => void;
}) {
const t = useTranslations("awooop.workItems.driftFingerprint");
const [action, setAction] = useState<{
loading: boolean;
result: DriftFingerprintHandoffResult | null;
error: string | null;
}>({
loading: false,
result: null,
error: null,
});
const fsmKey = driftFsmStateKey(state?.fsm_state);
const nextKey = driftNextStepKey(state?.next_step);
const handoffKind = state?.open_pr?.is_zero_diff
? "zero_diff_pr_cleanup"
: "open_pr_review";
const prUrl = state?.open_pr?.html_url ?? state?.open_pr?.url ?? null;
const recordHandoff = useCallback(async () => {
if (!state?.latest_report_id) return;
setAction({ loading: true, result: null, error: null });
const result = await postJson<DriftFingerprintHandoffResult>(
`${API_BASE}/api/v1/drift/fingerprints/handoff`,
{
report_id: state.latest_report_id,
namespace: state.namespace ?? "awoooi-prod",
handoff_kind: handoffKind,
pr_url: prUrl,
},
12000
);
setAction({
loading: false,
result,
error: result ? null : t("actions.failed"),
});
if (result?.history?.recorded) onRecorded();
}, [handoffKind, onRecorded, prUrl, state?.latest_report_id, state?.namespace, 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 items-center gap-2">
<Fingerprint 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("occurrences", { count: state?.occurrences_12h ?? 0 })}
</span>
<span className="border border-[#d8d3c7] bg-white px-2 py-0.5">
{t("risk", {
high: state?.high_count ?? 0,
medium: state?.medium_count ?? 0,
info: state?.info_count ?? 0,
})}
</span>
</div>
</div>
{state === null ? (
<div className="px-4 py-4 text-sm text-[#8a5a08]">
{t("unavailable")}
</div>
) : (
<div className="grid gap-px bg-[#eee9dd] lg:grid-cols-[1.3fr_1fr]">
<div className="bg-white px-4 py-4">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0">
<p className="font-mono text-xs font-semibold text-[#141413]">
{state.fingerprint ?? "--"}
</p>
<p className="mt-1 text-xs leading-5 text-[#77736a]">
{t("report", {
report: state.latest_report_id ?? "--",
namespace: state.namespace ?? "--",
})}
</p>
</div>
<span className="border border-[#d9b36f] bg-[#fff7e8] px-2 py-0.5 text-xs font-semibold text-[#8a5a08]">
{t(`fsmStates.${fsmKey}` as never)}
</span>
</div>
<div className="mt-3 grid gap-1 text-xs leading-5 text-[#5f5b52]">
<p>{t("summary", { summary: state.summary ?? "--" })}</p>
<p>
{t("next", {
step: t(`nextSteps.${nextKey}` as never),
})}
</p>
<p>
{t("p0Dedup", {
hours: state.p0_escalation?.dedup_window_hours ?? 24,
enabled: String(state.p0_escalation?.suppresses_repeated_p0 ?? false),
})}
</p>
<p>
{t("writes", {
drift: String(state.writes_drift_status ?? false),
incident: String(state.writes_incident_state ?? false),
repair: String(state.writes_auto_repair_result ?? false),
ticket: String(state.writes_ticket ?? false),
})}
</p>
</div>
</div>
<div className="bg-white px-4 py-4">
<div className="flex items-center gap-2">
<GitPullRequest className="h-4 w-4 text-[#87867f]" aria-hidden="true" />
<h4 className="text-sm font-semibold text-[#141413]">{t("pr.title")}</h4>
</div>
<div className="mt-3 grid gap-1 text-xs leading-5 text-[#5f5b52]">
<p>{t("pr.number", { number: state.open_pr?.number ?? "--" })}</p>
<p>
{t("pr.zeroDiff", {
zeroDiff: String(state.open_pr?.is_zero_diff ?? false),
files: state.open_pr?.file_count ?? "--",
commits: state.open_pr?.commit_count ?? "--",
})}
</p>
<p>{t("pr.status", { status: state.open_pr?.state ?? "--" })}</p>
<p>
{t("handoff.latest", {
status: state.latest_handoff?.handoff_status ?? "--",
})}
</p>
</div>
<div className="mt-3 flex flex-wrap gap-2">
<button
type="button"
onClick={recordHandoff}
disabled={!state.latest_report_id || action.loading}
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-[#9bc7a4] hover:bg-[#f0faf2] hover:text-[#17602a] disabled:cursor-not-allowed disabled:opacity-60"
>
<GitBranch className="h-3.5 w-3.5" aria-hidden="true" />
{action.loading ? t("actions.recording") : t("actions.record")}
</button>
<Link
href={"/drift" 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("actions.openDrift")}
</Link>
</div>
{action.error ? (
<div className="mt-3 border border-[#e2a29b] bg-[#fff0ef] px-3 py-2 text-xs leading-5 text-[#9f2f25]">
{action.error}
</div>
) : null}
{action.result ? (
<div className="mt-3 border border-[#9bc7a4] bg-[#f0faf2] px-3 py-2 text-xs leading-5 text-[#17602a]">
<p className="font-semibold">
{t("actions.recorded", {
recorded: String(action.result.history?.recorded ?? false),
})}
</p>
<p className="mt-1 text-[#5f5b52]">
{t("actions.handoffStatus", {
status: action.result.handoff_status ?? "--",
})}
</p>
</div>
) : null}
</div>
</div>
)}
</section>
);
}
export default function AwoooPWorkItemsPage() {
const t = useTranslations("awooop.workItems");
const locale = useLocale();
@@ -963,6 +1281,7 @@ export default function AwoooPWorkItemsPage() {
eventRecurrence: null,
slo: null,
remediationHistory: null,
driftFingerprintState: null,
});
const [loading, setLoading] = useState(true);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
@@ -977,6 +1296,7 @@ export default function AwoooPWorkItemsPage() {
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 driftFingerprintUrl = `${API_BASE}/api/v1/drift/fingerprints/state?namespace=awoooi-prod`;
const [
quality,
@@ -986,6 +1306,7 @@ export default function AwoooPWorkItemsPage() {
eventRecurrence,
slo,
remediationHistory,
driftFingerprintState,
] = await Promise.all([
fetchJson<AutomationQualitySummary>(qualityUrl, 15000),
fetchJson<GovernanceEventsResponse>(governanceEventsUrl),
@@ -994,6 +1315,7 @@ export default function AwoooPWorkItemsPage() {
fetchJson<RecurrenceResponse>(recurrenceUrl),
fetchJson<SloResponse>(sloUrl),
fetchJson<RemediationHistoryResponse>(remediationHistoryUrl),
fetchJson<DriftFingerprintState>(driftFingerprintUrl, 12000),
]);
setTelemetry({
@@ -1004,6 +1326,7 @@ export default function AwoooPWorkItemsPage() {
eventRecurrence,
slo,
remediationHistory,
driftFingerprintState,
});
setLastUpdated(new Date());
setLoading(false);
@@ -1116,6 +1439,11 @@ export default function AwoooPWorkItemsPage() {
projectId={projectId}
/>
<DriftFingerprintPanel
state={telemetry.driftFingerprintState}
onRecorded={fetchTelemetry}
/>
<div className="overflow-hidden border border-[#e0ddd4] bg-white">
<div className="overflow-x-auto">
<table className="w-full" role="table" aria-label={t("tableLabel")}>

View File

@@ -14,7 +14,7 @@ import { useTranslations } from 'next-intl'
import { cn } from '@/lib/utils'
import {
Diff, RefreshCw, AlertTriangle, CheckCircle2,
Clock, Terminal, GitMerge, Info,
Clock, Terminal, GitMerge, Info, Fingerprint, GitPullRequest,
} from 'lucide-react'
// =============================================================================
@@ -30,7 +30,7 @@ interface DriftReport {
medium_count: number
info_count: number
interpretation: string | null
status: 'pending' | 'resolved' | 'ignored'
status: 'pending' | 'acknowledged' | 'rolled_back' | 'adopted' | 'ignored'
created_at: string
resolved_at: string | null
}
@@ -45,6 +45,39 @@ interface ScanResult {
interpretation: string | null
}
interface DriftFingerprintState {
namespace?: string
fingerprint?: string
latest_report_id?: string
latest_status?: string
summary?: string
occurrences_12h?: number
fsm_state?: string
next_step?: string
high_count?: number
medium_count?: number
info_count?: number
open_pr?: {
number?: number | string | null
state?: string | null
file_count?: number | null
commit_count?: number | null
is_zero_diff?: boolean | null
lookup_error?: string | null
} | null
latest_handoff?: {
handoff_status?: string | null
} | null
p0_escalation?: {
suppresses_repeated_p0?: boolean | null
dedup_window_hours?: number | null
} | null
writes_drift_status?: boolean | null
writes_incident_state?: boolean | null
writes_auto_repair_result?: boolean | null
writes_ticket?: boolean | null
}
// =============================================================================
// Helpers
// =============================================================================
@@ -107,7 +140,9 @@ function DriftLevelBadge({ high, medium, info, t }: {
function StatusBadge({ status, t }: { status: DriftReport['status']; t: (k: string) => string }) {
const styles: Record<string, string> = {
pending: 'bg-status-warning/10 text-status-warning border-status-warning/20',
resolved: 'bg-status-healthy/10 text-status-healthy border-status-healthy/20',
acknowledged: 'bg-neutral-100 text-neutral-500 border-neutral-200',
rolled_back: 'bg-status-healthy/10 text-status-healthy border-status-healthy/20',
adopted: 'bg-status-healthy/10 text-status-healthy border-status-healthy/20',
ignored: 'bg-neutral-100 text-neutral-400 border-neutral-200',
}
return (
@@ -120,6 +155,63 @@ function StatusBadge({ status, t }: { status: DriftReport['status']; t: (k: stri
)
}
function DriftFingerprintStateCard({
state,
t,
}: {
state: DriftFingerprintState | null
t: (k: string, values?: Record<string, string | number>) => string
}) {
if (!state) return null
return (
<div className="mx-6 mt-4 border border-neutral-200 bg-neutral-50 px-4 py-3">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="flex min-w-0 items-start gap-2">
<Fingerprint size={15} className="mt-0.5 shrink-0 text-neutral-500" />
<div className="min-w-0">
<p className="text-[12px] font-semibold text-neutral-800">
{t('fingerprintState.title')}
</p>
<p className="mt-1 truncate font-mono text-[11px] text-neutral-500">
{state.fingerprint ?? '--'}
</p>
</div>
</div>
<span className="border border-status-warning/20 bg-status-warning/10 px-2 py-0.5 text-[11px] font-medium text-status-warning">
{t('fingerprintState.occurrences', { count: state.occurrences_12h ?? 0 })}
</span>
</div>
<div className="mt-3 grid gap-2 text-[11px] text-neutral-500 md:grid-cols-2">
<p>{t('fingerprintState.report', { report: state.latest_report_id ?? '--' })}</p>
<p>{t('fingerprintState.state', { state: state.fsm_state ?? '--' })}</p>
<p>{t('fingerprintState.next', { step: state.next_step ?? '--' })}</p>
<p>
{t('fingerprintState.writes', {
drift: String(state.writes_drift_status ?? false),
incident: String(state.writes_incident_state ?? false),
repair: String(state.writes_auto_repair_result ?? false),
ticket: String(state.writes_ticket ?? false),
})}
</p>
</div>
<div className="mt-3 flex flex-wrap items-center gap-2 text-[11px] text-neutral-500">
<span className="inline-flex items-center gap-1 border border-neutral-200 bg-white px-2 py-0.5">
<GitPullRequest size={11} />
{t('fingerprintState.pr', {
pr: state.open_pr?.number ?? '--',
zeroDiff: String(state.open_pr?.is_zero_diff ?? false),
})}
</span>
<span className="border border-neutral-200 bg-white px-2 py-0.5">
{t('fingerprintState.p0Dedup', {
hours: state.p0_escalation?.dedup_window_hours ?? 24,
})}
</span>
</div>
</div>
)
}
// =============================================================================
// DriftPanel
// =============================================================================
@@ -130,6 +222,7 @@ export function DriftPanel() {
const [loading, setLoading] = useState(true)
const [scanning, setScanning] = useState(false)
const [scanResult, setScanResult] = useState<ScanResult | null>(null)
const [fingerprintState, setFingerprintState] = useState<DriftFingerprintState | null>(null)
const [error, setError] = useState<string | null>(null)
const fetchReports = useCallback(async () => {
@@ -139,7 +232,17 @@ export function DriftPanel() {
const res = await fetch(`${getApiBase()}/api/v1/drift/reports?limit=20`)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
setReports(data.items ?? [])
const nextReports = data.items ?? []
setReports(nextReports)
const latestReportId = nextReports[0]?.report_id
if (latestReportId) {
const stateRes = await fetch(
`${getApiBase()}/api/v1/drift/fingerprints/state?report_id=${encodeURIComponent(latestReportId)}`
)
setFingerprintState(stateRes.ok ? await stateRes.json() : null)
} else {
setFingerprintState(null)
}
} catch (e) {
setError(e instanceof Error ? e.message : 'Failed to fetch')
} finally {
@@ -252,6 +355,8 @@ export function DriftPanel() {
</div>
)}
<DriftFingerprintStateCard state={fingerprintState} t={t} />
{/* Content */}
<div className="flex-1 px-6 py-4">
{loading && reports.length === 0 ? (