From 6fc14b9b847b26c94329beb1945e9fc246ad0586 Mon Sep 17 00:00:00 2001 From: openhands Date: Mon, 3 Aug 2026 18:08:45 +0200 Subject: [PATCH] feat: catalog audit can repair orphaned refs and duplicate classnames - add repairOrphanedCatalog: remove catalog_items rows whose item_ids only reference missing items_base entries, strip orphaned ids from mixed rows - add repairDuplicateClassnames: merge items_base duplicates into one canonical row per classname, remap references, delete duplicate rows - add 'repair structural issues' checkbox + result display in audit UI --- src/app/admin/import/audit/audit-client.tsx | 86 ++++++++- src/app/api/admin/import/audit/route.ts | 4 + src/lib/services/catalog-audit.ts | 96 ++++++++++ src/lib/services/catalog-repair.ts | 199 ++++++++++++++++++++ 4 files changed, 384 insertions(+), 1 deletion(-) diff --git a/src/app/admin/import/audit/audit-client.tsx b/src/app/admin/import/audit/audit-client.tsx index 9d896fb1..d2bf0b17 100644 --- a/src/app/admin/import/audit/audit-client.tsx +++ b/src/app/admin/import/audit/audit-client.tsx @@ -48,6 +48,11 @@ interface AuditSummary { catalogSqlApplied: number; furniDataAdded: number; furniDataDuplicatesRemoved: number; + orphanedRemoved: number; + orphanedCleaned: number; + duplicatesMerged: number; + duplicateRowsRemoved: number; + remappedReferences: number; } interface RepairStats { @@ -73,6 +78,13 @@ interface AuditEvent { applied?: number; sqlFailed?: number; furniData?: { added: number; removedDuplicates: number }; + structure?: { + orphanedRemoved: number; + orphanedCleaned: number; + duplicatesMerged: number; + duplicateRowsRemoved: number; + remappedReferences: number; + }; } const TYPE_LABELS: Record = { @@ -131,6 +143,7 @@ export function AuditClient() { const [generateSql, setGenerateSql] = useState(false); const [applySql, setApplySql] = useState(false); const [repairFurniData, setRepairFurniData] = useState(false); + const [repairStructure, setRepairStructure] = useState(false); const [issues, setIssues] = useState([]); const [missingFromSources, setMissingFromSources] = useState< @@ -150,6 +163,13 @@ export function AuditClient() { added: number; removedDuplicates: number; } | null>(null); + const [structureResult, setStructureResult] = useState<{ + orphanedRemoved: number; + orphanedCleaned: number; + duplicatesMerged: number; + duplicateRowsRemoved: number; + remappedReferences: number; + } | null>(null); const [checkProgress, setCheckProgress] = useState<{ section: string; @@ -170,6 +190,7 @@ export function AuditClient() { setSqlText(null); setSqlApplied(null); setFurniDataResult(null); + setStructureResult(null); setCheckProgress(null); setActiveTab("issues"); @@ -187,6 +208,7 @@ export function AuditClient() { sql: generateSql || applySql, applySql, repairFurniData, + repairStructure, }), }); @@ -267,6 +289,10 @@ export function AuditClient() { if (evt.furniData) setFurniDataResult(evt.furniData); } + if (evt.type === "structure_repair_complete") { + if (evt.structure) setStructureResult(evt.structure); + } + if (evt.type === "batch_complete") { setIssues(evt.issues ?? []); setMissingFromSources(evt.missingFromSources ?? []); @@ -309,7 +335,14 @@ export function AuditClient() { setLoading(false); abortRef.current = null; } - }, [repairEnabled, repairNitros, generateSql, applySql, repairFurniData]); + }, [ + repairEnabled, + repairNitros, + generateSql, + applySql, + repairFurniData, + repairStructure, + ]); const errors = issues.filter((i) => i.severity === "error"); const warnings = issues.filter((i) => i.severity === "warning"); @@ -372,6 +405,13 @@ export function AuditClient() { > Repair FurnitureData.json (add missing + dedupe) + + Repair structural issues (orphaned refs + duplicates) + {loading && (