feat(drift): surface fingerprint state handoff
This commit is contained in:
@@ -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")}>
|
||||
|
||||
Reference in New Issue
Block a user