feat(governance): surface stale km owner review inbox
All checks were successful
CD Pipeline / tests (push) Successful in 5m29s
Code Review / ai-code-review (push) Successful in 16s
Type Sync Check / check-type-sync (push) Successful in 28s
CD Pipeline / build-and-deploy (push) Successful in 4m12s
CD Pipeline / post-deploy-checks (push) Successful in 1m30s

This commit is contained in:
Your Name
2026-05-24 21:32:29 +08:00
parent d04377dd20
commit 0c447acb19
7 changed files with 676 additions and 0 deletions

View File

@@ -489,6 +489,47 @@ type KnowledgeStaleOwnerReviewBatchAction = {
error: string | null;
};
type KnowledgeStaleOwnerReviewInboxItem = {
dispatch_id: string;
governance_event_id: string;
entry_id: string;
project_id: string;
title: string;
dispatch_status: string;
workflow_stage: string;
next_action?: string | null;
owner?: string | null;
owner_note?: string | null;
batch_governance_event_id?: string | null;
batch_dispatch_id?: string | null;
priority_tier: "P0" | "P1" | "P2";
priority_score: number;
recommended_action: "refresh_with_evidence" | "owner_review" | "archive_or_supersede";
stale_days: number;
view_count: number;
correlation_sources: string[];
reasons: string[];
related_incident_id?: string | null;
related_playbook_id?: string | null;
related_approval_id?: string | null;
dry_run_plan_fingerprint?: string | null;
queued_at?: string | null;
started_at?: string | null;
completed_at?: string | null;
};
type KnowledgeStaleOwnerReviewInboxResponse = {
schema_version?: string;
project_id: string;
dispatch_status: string;
total: number;
returned: number;
writes_on_read: boolean;
manual_review_required: boolean;
items: KnowledgeStaleOwnerReviewInboxItem[];
generated_at?: string | null;
};
type KnowledgeStaleOwnerReviewCompleteResponse = {
schema_version?: string;
entry_id: string;
@@ -608,6 +649,7 @@ type Telemetry = {
knowledgeReviewDrafts: KnowledgeListResponse | null;
knowledgeReviewDedupe: KnowledgeReviewDraftDedupeResponse | null;
knowledgeStaleCandidates: KnowledgeStaleCandidatesResponse | null;
knowledgeStaleOwnerReviews: KnowledgeStaleOwnerReviewInboxResponse | null;
channelEvents: RecentEventsResponse | null;
eventRecurrence: RecurrenceResponse | null;
slo: SloResponse | null;
@@ -1247,6 +1289,33 @@ function kmStaleReviewOutcomeForCandidate(
return "refresh_with_evidence";
}
function staleCandidateFromOwnerReviewItem(
item: KnowledgeStaleOwnerReviewInboxItem
): KnowledgeStaleCandidate {
return {
entry_id: item.entry_id,
project_id: item.project_id,
title: item.title,
entry_type: "auto_runbook",
status: "review",
stale_days: item.stale_days,
view_count: item.view_count,
priority_score: item.priority_score,
priority_tier: item.priority_tier,
recommended_action: item.recommended_action,
reasons: item.reasons,
correlation_sources: item.correlation_sources,
related_incident_id: item.related_incident_id,
related_playbook_id: item.related_playbook_id,
related_approval_id: item.related_approval_id,
tags: [],
owner_review_dispatch_id: item.dispatch_id,
owner_review_status: item.dispatch_status,
owner_review_stage: item.workflow_stage,
owner_review_next_action: item.next_action,
};
}
function kmCorrelationSourceKey(value: string | null | undefined) {
switch (value) {
case "incident":
@@ -2236,12 +2305,14 @@ function KnowledgeGovernancePanel({
reviewDrafts,
dedupe,
staleCandidates,
ownerReviewInbox,
onArchived,
}: {
queue: GovernanceQueueResponse | null;
reviewDrafts: KnowledgeListResponse | null;
dedupe: KnowledgeReviewDraftDedupeResponse | null;
staleCandidates: KnowledgeStaleCandidatesResponse | null;
ownerReviewInbox: KnowledgeStaleOwnerReviewInboxResponse | null;
onArchived: () => void;
}) {
const t = useTranslations("awooop.workItems.knowledgeGovernance");
@@ -2264,6 +2335,7 @@ function KnowledgeGovernancePanel({
?? draftGroups.reduce((sum, group) => sum + group.duplicateCount, 0)
);
const staleCandidateItems = staleCandidates?.items ?? [];
const ownerReviewItems = ownerReviewInbox?.items ?? [];
const draftTotal = dedupe?.total_review_drafts ?? reviewDrafts?.total ?? 0;
const activeCount = items.filter((item) =>
["pending", "dispatched", "executing"].includes(item.dispatch_status)
@@ -2829,6 +2901,155 @@ function KnowledgeGovernancePanel({
</div>
) : null}
</div>
<div className="mb-3 border border-[#e0ddd4] bg-white px-3 py-2 text-xs text-[#5f5b52]">
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-2">
<ClipboardList className="h-4 w-4 text-[#5f5b52]" aria-hidden="true" />
<div>
<p className="font-semibold text-[#141413]">
{t("staleCandidates.ownerReviewInbox.title")}
</p>
<p className="mt-1 text-[11px] leading-5 text-[#77736a]">
{t("staleCandidates.ownerReviewInbox.subtitle")}
</p>
</div>
</div>
<div className="flex flex-wrap gap-2 font-mono text-[11px] text-[#5f5b52]">
<span className="border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5">
{t("staleCandidates.ownerReviewInbox.total", {
count: ownerReviewInbox?.total ?? 0,
})}
</span>
<span className="border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5">
{t("staleCandidates.ownerReviewInbox.returned", {
count: ownerReviewInbox?.returned ?? 0,
})}
</span>
</div>
</div>
{ownerReviewInbox === null ? (
<p className="text-[11px] leading-5 text-[#8a5a08]">
{t("staleCandidates.ownerReviewInbox.unavailable")}
</p>
) : ownerReviewItems.length === 0 ? (
<p className="text-[11px] leading-5 text-[#5f5b52]">
{t("staleCandidates.ownerReviewInbox.empty")}
</p>
) : (
<div className="grid gap-2 md:grid-cols-2">
{ownerReviewItems.slice(0, 6).map((item) => {
const inboxCandidate = staleCandidateFromOwnerReviewItem(item);
const completionAction = staleReviewCompletionActions[item.entry_id];
const completionPreview = completionAction?.previewResult ?? null;
const completionResult = completionAction?.result ?? null;
const completionPreviewReady = Boolean(completionPreview?.dry_run_plan_fingerprint);
const completionPreviewStatusKey = kmStaleReviewCompleteStatusKey(completionPreview?.status);
const completionResultStatusKey = kmStaleReviewCompleteStatusKey(completionResult?.status);
return (
<article
key={item.dispatch_id}
className="border border-[#e0ddd4] bg-[#faf9f3] px-3 py-2 text-[11px] leading-5 text-[#5f5b52]"
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="truncate font-semibold text-[#141413]">
{item.title}
</p>
<p className="mt-1 truncate font-mono text-[#77736a]">
{item.dispatch_id}
</p>
</div>
<span className="shrink-0 border border-[#d9b36f] bg-[#fff7e8] px-2 py-0.5 font-mono font-semibold text-[#8a5a08]">
{item.priority_tier}
</span>
</div>
<div className="mt-2 grid gap-1">
<p>
{t("staleCandidates.ownerReviewInbox.meta", {
days: item.stale_days,
score: item.priority_score,
views: item.view_count,
})}
</p>
<p>
{t("staleCandidates.ownerReviewInbox.state", {
status: t(`statuses.${governanceKmDispatchStatusKey(item.dispatch_status)}` as never),
stage: t(`stages.${governanceKmStageKey(item.workflow_stage)}` as never),
})}
</p>
<p className="truncate">
{t("staleCandidates.ownerReviewInbox.batch", {
batch: item.batch_dispatch_id ?? "--",
})}
</p>
<p>
{t("staleCandidates.refs", {
incident: item.related_incident_id ?? "--",
playbook: item.related_playbook_id ?? "--",
approval: item.related_approval_id ?? "--",
})}
</p>
</div>
<div className="mt-2 flex flex-wrap items-center gap-2">
<button
type="button"
onClick={() => previewStaleReviewCompletion(inboxCandidate)}
disabled={completionAction?.previewLoading || completionAction?.confirmLoading}
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"
>
<SearchCheck className="h-3.5 w-3.5" aria-hidden="true" />
{completionAction?.previewLoading
? t("staleCandidates.completeActions.previewing")
: t("staleCandidates.completeActions.preview")}
</button>
<button
type="button"
onClick={() => confirmStaleReviewCompletion(inboxCandidate)}
disabled={
!completionPreviewReady ||
completionAction?.previewLoading ||
completionAction?.confirmLoading
}
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-[#d9b36f] hover:bg-[#fff7e8] hover:text-[#8a5a08] disabled:cursor-not-allowed disabled:opacity-60"
>
<ShieldCheck className="h-3.5 w-3.5" aria-hidden="true" />
{completionAction?.confirmLoading
? t("staleCandidates.completeActions.confirming")
: t("staleCandidates.completeActions.confirm")}
</button>
</div>
{completionAction?.error ? (
<p className="mt-2 border border-[#e2a29b] bg-[#fff0ef] px-2 py-1 text-[#9f2f25]">
{completionAction.error}
</p>
) : null}
{completionPreview ? (
<p className="mt-2 border border-[#d9b36f] bg-[#fff7e8] px-2 py-1 text-[#8a5a08]">
{t(
`staleCandidates.completeActions.statuses.${completionPreviewStatusKey}` as never
)}{" "}
{t("staleCandidates.completeActions.planFingerprint", {
fingerprint: completionPreview.dry_run_plan_fingerprint ?? "--",
})}
</p>
) : null}
{completionResult ? (
<p className="mt-2 border border-[#9bc7a4] bg-[#f0faf2] px-2 py-1 text-[#17602a]">
{t(
`staleCandidates.completeActions.statuses.${completionResultStatusKey}` as never
)}{" "}
{t("staleCandidates.completeActions.result", {
audit: completionResult.audit_dispatch_id ?? "--",
recheck: completionResult.stale_ratio_recheck_dispatch_id ?? "--",
})}
</p>
) : null}
</article>
);
})}
</div>
)}
</div>
{staleCandidates === null ? (
<p className="text-xs text-[#8a5a08]">
{t("staleCandidates.unavailable")}
@@ -3577,6 +3798,7 @@ export default function AwoooPWorkItemsPage() {
knowledgeReviewDrafts: null,
knowledgeReviewDedupe: null,
knowledgeStaleCandidates: null,
knowledgeStaleOwnerReviews: null,
channelEvents: null,
eventRecurrence: null,
slo: null,
@@ -3597,6 +3819,7 @@ export default function AwoooPWorkItemsPage() {
const knowledgeReviewDraftsUrl = `${API_BASE}/api/v1/knowledge?entry_type=auto_runbook&status=review&q=${encodeURIComponent("KM healthcheck")}&limit=100`;
const knowledgeReviewDedupeUrl = `${API_BASE}/api/v1/ai/governance/km-review-drafts/dedupe?limit=100`;
const knowledgeStaleCandidatesUrl = `${API_BASE}/api/v1/ai/governance/km-stale-candidates?project_id=${encodedProjectId}&limit=20`;
const knowledgeStaleOwnerReviewsUrl = `${API_BASE}/api/v1/ai/governance/km-stale-owner-reviews?project_id=${encodedProjectId}&dispatch_status=pending&limit=30`;
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`;
@@ -3611,6 +3834,7 @@ export default function AwoooPWorkItemsPage() {
knowledgeReviewDrafts,
knowledgeReviewDedupe,
knowledgeStaleCandidates,
knowledgeStaleOwnerReviews,
channelEvents,
eventRecurrence,
slo,
@@ -3624,6 +3848,7 @@ export default function AwoooPWorkItemsPage() {
fetchJson<KnowledgeListResponse>(knowledgeReviewDraftsUrl),
fetchJson<KnowledgeReviewDraftDedupeResponse>(knowledgeReviewDedupeUrl),
fetchJson<KnowledgeStaleCandidatesResponse>(knowledgeStaleCandidatesUrl),
fetchJson<KnowledgeStaleOwnerReviewInboxResponse>(knowledgeStaleOwnerReviewsUrl),
fetchJson<RecentEventsResponse>(channelEventsUrl),
fetchJson<RecurrenceResponse>(recurrenceUrl),
fetchJson<SloResponse>(sloUrl),
@@ -3654,6 +3879,7 @@ export default function AwoooPWorkItemsPage() {
knowledgeReviewDrafts,
knowledgeReviewDedupe,
knowledgeStaleCandidates,
knowledgeStaleOwnerReviews,
channelEvents,
eventRecurrence,
slo,
@@ -3781,6 +4007,7 @@ export default function AwoooPWorkItemsPage() {
reviewDrafts={telemetry.knowledgeReviewDrafts}
dedupe={telemetry.knowledgeReviewDedupe}
staleCandidates={telemetry.knowledgeStaleCandidates}
ownerReviewInbox={telemetry.knowledgeStaleOwnerReviews}
onArchived={fetchTelemetry}
/>