feat(governance): archive duplicate km review drafts
All checks were successful
Code Review / ai-code-review (push) Successful in 10s
Type Sync Check / check-type-sync (push) Successful in 33s
CD Pipeline / tests (push) Successful in 3m31s
CD Pipeline / build-and-deploy (push) Successful in 4m41s
CD Pipeline / post-deploy-checks (push) Successful in 1m53s
All checks were successful
Code Review / ai-code-review (push) Successful in 10s
Type Sync Check / check-type-sync (push) Successful in 33s
CD Pipeline / tests (push) Successful in 3m31s
CD Pipeline / build-and-deploy (push) Successful in 4m41s
CD Pipeline / post-deploy-checks (push) Successful in 1m53s
This commit is contained in:
@@ -10,6 +10,7 @@ import { useSearchParams } from "next/navigation";
|
||||
import { useLocale, useTranslations } from "next-intl";
|
||||
import {
|
||||
Activity,
|
||||
Archive,
|
||||
ArrowRight,
|
||||
ClipboardList,
|
||||
Database,
|
||||
@@ -274,6 +275,25 @@ type KnowledgeReviewDraftDedupeResponse = {
|
||||
generated_at?: string | null;
|
||||
};
|
||||
|
||||
type KnowledgeReviewDraftArchiveResponse = {
|
||||
schema_version?: string;
|
||||
governance_event_id: string;
|
||||
canonical_entry_id: string;
|
||||
requested_duplicate_entry_ids: string[];
|
||||
archived_entry_ids: string[];
|
||||
skipped_entry_ids: string[];
|
||||
would_archive_entry_ids: string[];
|
||||
status: "dry_run" | "archived" | "noop_already_archived";
|
||||
owner: string;
|
||||
owner_approved: boolean;
|
||||
dry_run: boolean;
|
||||
writes_km: boolean;
|
||||
writes_governance_audit: boolean;
|
||||
audit_dispatch_id?: string | null;
|
||||
next_action: string;
|
||||
generated_at?: string | null;
|
||||
};
|
||||
|
||||
type DriftFingerprintState = {
|
||||
schema_version?: string;
|
||||
namespace?: string;
|
||||
@@ -740,6 +760,17 @@ function kmDedupeActionKey(action?: string | null) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function groupArchiveStatusKey(status?: string | null) {
|
||||
if (
|
||||
status === "dry_run" ||
|
||||
status === "archived" ||
|
||||
status === "noop_already_archived"
|
||||
) {
|
||||
return status;
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function buildWorkItems(
|
||||
telemetry: Telemetry,
|
||||
t: ReturnType<typeof useTranslations>
|
||||
@@ -1428,12 +1459,19 @@ function KnowledgeGovernancePanel({
|
||||
queue,
|
||||
reviewDrafts,
|
||||
dedupe,
|
||||
onArchived,
|
||||
}: {
|
||||
queue: GovernanceQueueResponse | null;
|
||||
reviewDrafts: KnowledgeListResponse | null;
|
||||
dedupe: KnowledgeReviewDraftDedupeResponse | null;
|
||||
onArchived: () => void;
|
||||
}) {
|
||||
const t = useTranslations("awooop.workItems.knowledgeGovernance");
|
||||
const [archiveActions, setArchiveActions] = useState<Record<string, {
|
||||
loading: boolean;
|
||||
result: KnowledgeReviewDraftArchiveResponse | null;
|
||||
error: string | null;
|
||||
}>>({});
|
||||
const items = queue?.items ?? [];
|
||||
const draftGroups = groupKnowledgeReviewDrafts(reviewDrafts, items);
|
||||
const dedupeGroups = dedupe?.groups ?? [];
|
||||
@@ -1448,6 +1486,34 @@ function KnowledgeGovernancePanel({
|
||||
const reviewCount = items.filter((item) =>
|
||||
item.dispatch_status === "skipped" || item.workflow_stage === "waiting_owner_review"
|
||||
).length;
|
||||
const archiveDuplicates = useCallback(async (group: KnowledgeReviewDraftDedupeGroup) => {
|
||||
setArchiveActions((current) => ({
|
||||
...current,
|
||||
[group.governance_event_id]: { loading: true, result: null, error: null },
|
||||
}));
|
||||
const result = await postJson<KnowledgeReviewDraftArchiveResponse>(
|
||||
`${API_BASE}/api/v1/ai/governance/km-review-drafts/dedupe/${encodeURIComponent(group.governance_event_id)}/archive-duplicates`,
|
||||
{
|
||||
canonical_entry_id: group.canonical_entry_id,
|
||||
duplicate_entry_ids: group.duplicate_entry_ids,
|
||||
owner: "operator_console",
|
||||
owner_approved: true,
|
||||
dry_run: false,
|
||||
},
|
||||
15000
|
||||
);
|
||||
setArchiveActions((current) => ({
|
||||
...current,
|
||||
[group.governance_event_id]: {
|
||||
loading: false,
|
||||
result,
|
||||
error: result ? null : t("archiveActions.failed"),
|
||||
},
|
||||
}));
|
||||
if (result?.status === "archived" || result?.status === "noop_already_archived") {
|
||||
onArchived();
|
||||
}
|
||||
}, [onArchived, t]);
|
||||
|
||||
return (
|
||||
<section className="border border-[#e0ddd4] bg-white">
|
||||
@@ -1574,50 +1640,88 @@ function KnowledgeGovernancePanel({
|
||||
</h4>
|
||||
</div>
|
||||
<div className="grid gap-2 md:grid-cols-2">
|
||||
{dedupeGroups.slice(0, 4).map((group) => (
|
||||
<div
|
||||
key={group.governance_event_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-mono font-semibold text-[#141413]">
|
||||
{group.governance_event_id}
|
||||
</p>
|
||||
<p className="mt-1 truncate text-[#77736a]">{group.canonical_title}</p>
|
||||
{dedupeGroups.slice(0, 4).map((group) => {
|
||||
const archiveAction = archiveActions[group.governance_event_id];
|
||||
const resultKey = groupArchiveStatusKey(archiveAction?.result?.status);
|
||||
return (
|
||||
<div
|
||||
key={group.governance_event_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-mono font-semibold text-[#141413]">
|
||||
{group.governance_event_id}
|
||||
</p>
|
||||
<p className="mt-1 truncate text-[#77736a]">{group.canonical_title}</p>
|
||||
</div>
|
||||
<span className="shrink-0 border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5 font-mono">
|
||||
{group.canonical_entry_id}
|
||||
</span>
|
||||
</div>
|
||||
<span className="shrink-0 border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5 font-mono">
|
||||
{group.canonical_entry_id}
|
||||
</span>
|
||||
<div className="mt-2 grid gap-1 leading-5">
|
||||
<p>
|
||||
{t("draftGroup", {
|
||||
count: group.total_entries,
|
||||
duplicates: group.duplicate_count,
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t("archiveProposal", {
|
||||
count: group.duplicate_count,
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t("ownerAction", {
|
||||
action: t(
|
||||
`ownerActions.${kmDedupeActionKey(group.owner_action)}` as never
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t("readOnlyPlan", {
|
||||
writes: String(group.writes_on_read),
|
||||
blocked: String(!group.can_archive_without_owner_approval),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => archiveDuplicates(group)}
|
||||
disabled={group.duplicate_count === 0 || archiveAction?.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"
|
||||
>
|
||||
<Archive className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{archiveAction?.loading
|
||||
? t("archiveActions.archiving")
|
||||
: t("archiveActions.archive")}
|
||||
</button>
|
||||
<span className="text-[11px] leading-5 text-[#77736a]">
|
||||
{t("archiveActions.requiresOwner")}
|
||||
</span>
|
||||
</div>
|
||||
{archiveAction?.error ? (
|
||||
<div className="mt-2 border border-[#e2a29b] bg-[#fff0ef] px-2 py-1.5 text-[#9f2f25]">
|
||||
{archiveAction.error}
|
||||
</div>
|
||||
) : null}
|
||||
{archiveAction?.result ? (
|
||||
<div className="mt-2 border border-[#9bc7a4] bg-[#f0faf2] px-2 py-1.5 text-[#17602a]">
|
||||
<p className="font-semibold">
|
||||
{t(`archiveActions.statuses.${resultKey}` as never)}
|
||||
</p>
|
||||
<p className="mt-1 text-[#5f5b52]">
|
||||
{t("archiveActions.result", {
|
||||
archived: archiveAction.result.archived_entry_ids.length,
|
||||
audit: archiveAction.result.audit_dispatch_id ?? "--",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-2 grid gap-1 leading-5">
|
||||
<p>
|
||||
{t("draftGroup", {
|
||||
count: group.total_entries,
|
||||
duplicates: group.duplicate_count,
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t("archiveProposal", {
|
||||
count: group.duplicate_count,
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t("ownerAction", {
|
||||
action: t(
|
||||
`ownerActions.${kmDedupeActionKey(group.owner_action)}` as never
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t("readOnlyPlan", {
|
||||
writes: String(group.writes_on_read),
|
||||
blocked: String(!group.can_archive_without_owner_approval),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : draftGroups.length > 0 ? (
|
||||
@@ -2090,6 +2194,7 @@ export default function AwoooPWorkItemsPage() {
|
||||
queue={telemetry.governanceKnowledgeQueue}
|
||||
reviewDrafts={telemetry.knowledgeReviewDrafts}
|
||||
dedupe={telemetry.knowledgeReviewDedupe}
|
||||
onArchived={fetchTelemetry}
|
||||
/>
|
||||
|
||||
<DriftFingerprintPanel
|
||||
|
||||
Reference in New Issue
Block a user