feat(governance): queue stale km owner review
All checks were successful
CD Pipeline / tests (push) Successful in 5m28s
Code Review / ai-code-review (push) Successful in 14s
Type Sync Check / check-type-sync (push) Successful in 27s
CD Pipeline / build-and-deploy (push) Successful in 4m19s
CD Pipeline / post-deploy-checks (push) Successful in 1m39s
All checks were successful
CD Pipeline / tests (push) Successful in 5m28s
Code Review / ai-code-review (push) Successful in 14s
Type Sync Check / check-type-sync (push) Successful in 27s
CD Pipeline / build-and-deploy (push) Successful in 4m19s
CD Pipeline / post-deploy-checks (push) Successful in 1m39s
This commit is contained in:
@@ -411,6 +411,29 @@ type KnowledgeStaleCandidatesResponse = {
|
||||
generated_at?: string | null;
|
||||
};
|
||||
|
||||
type KnowledgeStaleOwnerReviewResponse = {
|
||||
schema_version?: string;
|
||||
entry_id: string;
|
||||
project_id: string;
|
||||
status: "dry_run" | "queued" | "already_queued";
|
||||
governance_event_id?: string | null;
|
||||
dispatch_id?: string | null;
|
||||
workflow_stage: string;
|
||||
recommended_action: "refresh_with_evidence" | "owner_review" | "archive_or_supersede";
|
||||
owner: string;
|
||||
owner_note?: string | null;
|
||||
writes_km: boolean;
|
||||
writes_governance_audit: boolean;
|
||||
next_action: string;
|
||||
generated_at?: string | null;
|
||||
};
|
||||
|
||||
type KnowledgeStaleOwnerReviewAction = {
|
||||
loading: boolean;
|
||||
result: KnowledgeStaleOwnerReviewResponse | null;
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
type DriftFingerprintState = {
|
||||
schema_version?: string;
|
||||
namespace?: string;
|
||||
@@ -1075,6 +1098,17 @@ function kmStaleActionKey(value: string | null | undefined) {
|
||||
}
|
||||
}
|
||||
|
||||
function kmStaleReviewStatusKey(value: string | null | undefined) {
|
||||
switch (value) {
|
||||
case "dry_run":
|
||||
case "queued":
|
||||
case "already_queued":
|
||||
return value;
|
||||
default:
|
||||
return "queued";
|
||||
}
|
||||
}
|
||||
|
||||
function kmCorrelationSourceKey(value: string | null | undefined) {
|
||||
switch (value) {
|
||||
case "incident":
|
||||
@@ -2074,6 +2108,7 @@ function KnowledgeGovernancePanel({
|
||||
}) {
|
||||
const t = useTranslations("awooop.workItems.knowledgeGovernance");
|
||||
const [archiveActions, setArchiveActions] = useState<Record<string, KnowledgeReviewDraftArchiveAction>>({});
|
||||
const [staleReviewActions, setStaleReviewActions] = useState<Record<string, KnowledgeStaleOwnerReviewAction>>({});
|
||||
const items = queue?.items ?? [];
|
||||
const draftGroups = groupKnowledgeReviewDrafts(reviewDrafts, items);
|
||||
const dedupeGroups = dedupe?.groups ?? [];
|
||||
@@ -2177,6 +2212,37 @@ function KnowledgeGovernancePanel({
|
||||
}
|
||||
}, [archiveActions, onArchived, t]);
|
||||
|
||||
const queueStaleOwnerReview = useCallback(async (candidate: KnowledgeStaleCandidate) => {
|
||||
setStaleReviewActions((current) => ({
|
||||
...current,
|
||||
[candidate.entry_id]: {
|
||||
loading: true,
|
||||
result: current[candidate.entry_id]?.result ?? null,
|
||||
error: null,
|
||||
},
|
||||
}));
|
||||
const result = await postJson<KnowledgeStaleOwnerReviewResponse>(
|
||||
`${API_BASE}/api/v1/ai/governance/km-stale-candidates/${encodeURIComponent(candidate.entry_id)}/queue-review`,
|
||||
{
|
||||
owner: "operator_console",
|
||||
owner_note: candidate.priority_tier,
|
||||
dry_run: false,
|
||||
},
|
||||
15000
|
||||
);
|
||||
setStaleReviewActions((current) => ({
|
||||
...current,
|
||||
[candidate.entry_id]: {
|
||||
loading: false,
|
||||
result,
|
||||
error: result ? null : t("staleCandidates.queueFailed"),
|
||||
},
|
||||
}));
|
||||
if (result?.status === "queued" || result?.status === "already_queued") {
|
||||
onArchived();
|
||||
}
|
||||
}, [onArchived, 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">
|
||||
@@ -2325,77 +2391,109 @@ function KnowledgeGovernancePanel({
|
||||
</p>
|
||||
) : (
|
||||
<div className="grid gap-2 md:grid-cols-2">
|
||||
{staleCandidateItems.slice(0, 6).map((candidate) => (
|
||||
<article
|
||||
key={candidate.entry_id}
|
||||
className="border border-[#e0ddd4] bg-white px-3 py-2 text-xs text-[#5f5b52]"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-semibold text-[#141413]">
|
||||
{candidate.title}
|
||||
{staleCandidateItems.slice(0, 6).map((candidate) => {
|
||||
const reviewAction = staleReviewActions[candidate.entry_id];
|
||||
const reviewResult = reviewAction?.result ?? null;
|
||||
const reviewStatusKey = kmStaleReviewStatusKey(reviewResult?.status);
|
||||
return (
|
||||
<article
|
||||
key={candidate.entry_id}
|
||||
className="border border-[#e0ddd4] bg-white px-3 py-2 text-xs text-[#5f5b52]"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-semibold text-[#141413]">
|
||||
{candidate.title}
|
||||
</p>
|
||||
<p className="mt-1 truncate font-mono text-[11px] text-[#77736a]">
|
||||
{candidate.entry_id}
|
||||
</p>
|
||||
</div>
|
||||
<span className="shrink-0 border border-[#d9b36f] bg-[#fff7e8] px-2 py-0.5 font-mono font-semibold text-[#8a5a08]">
|
||||
{candidate.priority_tier}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 grid gap-1 leading-5">
|
||||
<p>
|
||||
{t("staleCandidates.meta", {
|
||||
days: candidate.stale_days,
|
||||
score: candidate.priority_score,
|
||||
views: candidate.view_count,
|
||||
})}
|
||||
</p>
|
||||
<p className="mt-1 truncate font-mono text-[11px] text-[#77736a]">
|
||||
{candidate.entry_id}
|
||||
<p>
|
||||
{t("staleCandidates.action", {
|
||||
action: t(
|
||||
`staleCandidates.actions.${kmStaleActionKey(candidate.recommended_action)}` as never
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t("staleCandidates.sources", {
|
||||
sources: candidate.correlation_sources.length
|
||||
? candidate.correlation_sources
|
||||
.map((source) => t(
|
||||
`staleCandidates.correlationSources.${kmCorrelationSourceKey(source)}` as never
|
||||
))
|
||||
.join(" / ")
|
||||
: t("staleCandidates.noSources"),
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t("staleCandidates.refs", {
|
||||
incident: candidate.related_incident_id ?? "--",
|
||||
playbook: candidate.related_playbook_id ?? "--",
|
||||
approval: candidate.related_approval_id ?? "--",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<span className="shrink-0 border border-[#d9b36f] bg-[#fff7e8] px-2 py-0.5 font-mono font-semibold text-[#8a5a08]">
|
||||
{candidate.priority_tier}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 grid gap-1 leading-5">
|
||||
<p>
|
||||
{t("staleCandidates.meta", {
|
||||
days: candidate.stale_days,
|
||||
score: candidate.priority_score,
|
||||
views: candidate.view_count,
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t("staleCandidates.action", {
|
||||
action: t(
|
||||
`staleCandidates.actions.${kmStaleActionKey(candidate.recommended_action)}` as never
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t("staleCandidates.sources", {
|
||||
sources: candidate.correlation_sources.length
|
||||
? candidate.correlation_sources
|
||||
.map((source) => t(
|
||||
`staleCandidates.correlationSources.${kmCorrelationSourceKey(source)}` as never
|
||||
))
|
||||
.join(" / ")
|
||||
: t("staleCandidates.noSources"),
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t("staleCandidates.refs", {
|
||||
incident: candidate.related_incident_id ?? "--",
|
||||
playbook: candidate.related_playbook_id ?? "--",
|
||||
approval: candidate.related_approval_id ?? "--",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{candidate.reasons.slice(0, 5).map((reason) => (
|
||||
<span
|
||||
key={`${candidate.entry_id}-${reason}`}
|
||||
className="border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5 leading-5"
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{candidate.reasons.slice(0, 5).map((reason) => (
|
||||
<span
|
||||
key={`${candidate.entry_id}-${reason}`}
|
||||
className="border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5 leading-5"
|
||||
>
|
||||
{t(`staleCandidates.reasons.${kmStaleReasonKey(reason)}` as never)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => queueStaleOwnerReview(candidate)}
|
||||
disabled={Boolean(reviewAction?.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"
|
||||
>
|
||||
{t(`staleCandidates.reasons.${kmStaleReasonKey(reason)}` as never)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<Link
|
||||
href={`/knowledge-base?q=${encodeURIComponent(candidate.title)}`}
|
||||
className="mt-2 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("staleCandidates.openKnowledge")}
|
||||
</Link>
|
||||
</article>
|
||||
))}
|
||||
<ClipboardList className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{reviewAction?.loading
|
||||
? t("staleCandidates.queueingReview")
|
||||
: t("staleCandidates.queueReview")}
|
||||
</button>
|
||||
<Link
|
||||
href={`/knowledge-base?q=${encodeURIComponent(candidate.title)}`}
|
||||
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("staleCandidates.openKnowledge")}
|
||||
</Link>
|
||||
</div>
|
||||
{reviewAction?.error ? (
|
||||
<p className="mt-2 border border-[#e2a29b] bg-[#fff0ef] px-2 py-1 text-[11px] leading-5 text-[#9f2f25]">
|
||||
{reviewAction.error}
|
||||
</p>
|
||||
) : null}
|
||||
{reviewResult ? (
|
||||
<p className="mt-2 border border-[#9bc7a4] bg-[#f0faf2] px-2 py-1 text-[11px] leading-5 text-[#17602a]">
|
||||
{t("staleCandidates.queueResult", {
|
||||
status: t(`staleCandidates.queueStatuses.${reviewStatusKey}` as never),
|
||||
dispatch: reviewResult.dispatch_id ?? "--",
|
||||
event: reviewResult.governance_event_id ?? "--",
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<p className="mt-3 text-[11px] leading-5 text-[#77736a]">
|
||||
|
||||
Reference in New Issue
Block a user