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
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:
@@ -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}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user