From 2ee5ba5c4c1a55df9fc9313a00c51d69d100efd0 Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 4 Aug 2026 11:18:37 +0200 Subject: [PATCH] feat: group unrepairable legacy items separately in catalog audit After a repair attempt, items whose nitro/icon assets cannot be restored from any source are reclassified from hard errors into a dedicated 'Unrepairable (legacy)' group (info), so a fully repaired catalog can reach 0 errors while still listing exactly what is not restorable. Adds an unrepairable summary count and a dedicated tab in the audit UI. --- src/app/admin/import/audit/audit-client.tsx | 85 +++++++++++++++++---- src/lib/services/catalog-audit.ts | 63 ++++++++++++--- 2 files changed, 124 insertions(+), 24 deletions(-) diff --git a/src/app/admin/import/audit/audit-client.tsx b/src/app/admin/import/audit/audit-client.tsx index 7eb129e3..3d1885de 100644 --- a/src/app/admin/import/audit/audit-client.tsx +++ b/src/app/admin/import/audit/audit-client.tsx @@ -32,6 +32,12 @@ interface MissingFromSource { itemName: string; } +interface UnrepairableItem { + classname: string; + itemId: number; + missing: Array<"nitro" | "icon">; +} + interface AuditSummary { totalItemsBase: number; totalCatalogItems: number; @@ -41,6 +47,7 @@ interface AuditSummary { missingIcon: number; duplicates: number; missingFromSourcesTotal: number; + unrepairableCount: number; iconsRepaired: number; iconsStillMissing: number; nitrosRepaired: number; @@ -71,6 +78,7 @@ interface AuditEvent { section?: string; issues?: AuditIssue[]; missingFromSources?: MissingFromSource[]; + unrepairable?: UnrepairableItem[]; summary?: AuditSummary; repair?: RepairStats; repairNitro?: RepairStats; @@ -138,7 +146,9 @@ function AuditCheckbox({ export function AuditClient() { const [loading, setLoading] = useState(false); const [progress, setProgress] = useState(""); - const [activeTab, setActiveTab] = useState<"issues" | "sources">("issues"); + const [activeTab, setActiveTab] = useState< + "issues" | "sources" | "unrepairable" + >("issues"); const [repairEnabled, setRepairEnabled] = useState(true); const [repairNitros, setRepairNitros] = useState(true); const [generateSql, setGenerateSql] = useState(false); @@ -150,6 +160,7 @@ export function AuditClient() { const [missingFromSources, setMissingFromSources] = useState< MissingFromSource[] >([]); + const [unrepairable, setUnrepairable] = useState([]); const [summary, setSummary] = useState(null); const [repairStats, setRepairStats] = useState(null); const [nitroRepairStats, setNitroRepairStats] = useState( @@ -185,6 +196,7 @@ export function AuditClient() { setProgress("Starting audit…"); setIssues([]); setMissingFromSources([]); + setUnrepairable([]); setSummary(null); setRepairStats(null); setNitroRepairStats(null); @@ -298,21 +310,22 @@ export function AuditClient() { if (evt.type === "batch_complete") { setIssues(evt.issues ?? []); setMissingFromSources(evt.missingFromSources ?? []); + setUnrepairable(evt.unrepairable ?? []); if (evt.summary) setSummary(evt.summary); setCheckProgress(null); setProgress("Done"); - if ( - evt.issues && - evt.issues.filter((i) => i.severity === "error").length > 0 - ) { - toast.error( - `${evt.issues.filter((i) => i.severity === "error").length} error(s) found`, - ); - } else if ( - evt.issues && - evt.issues.filter((i) => i.severity === "warning").length > 0 - ) { + const errCount = + evt.issues?.filter((i) => i.severity === "error").length ?? 0; + const warnCount = + evt.issues?.filter((i) => i.severity === "warning").length ?? + 0; + const legacyCount = evt.unrepairable?.length ?? 0; + if (errCount > 0) { + toast.error(`${errCount} error(s) found`); + } else if (warnCount > 0) { toast.warning("Warnings found"); + } else if (legacyCount > 0) { + toast.info(`${legacyCount} legacy item(s) can't be repaired`); } else { toast.success("No issues found"); } @@ -529,6 +542,17 @@ export function AuditClient() { sources +
+ {summary.unrepairableCount > 0 ? ( + + ) : ( + + )} + + {summary.unrepairableCount} item(s) can't be repaired + (legacy/broken) + +
{repairStats && (
@@ -690,7 +714,9 @@ export function AuditClient() {
{/* Tabs */} - {issues.length > 0 || missingFromSources.length > 0 ? ( + {issues.length > 0 || + missingFromSources.length > 0 || + unrepairable.length > 0 ? (
+
) : null} @@ -796,6 +833,27 @@ export function AuditClient() { )} + {/* Unrepairable (legacy) list */} + {activeTab === "unrepairable" && unrepairable.length > 0 && ( +
+ {unrepairable.map((item) => ( +
+ + {item.classname} + + (ID {item.itemId}) + + + missing {item.missing.join(" + ")} + +
+ ))} +
+ )} + {activeTab === "issues" && issues.length === 0 && missingFromSources.length === 0 && ( @@ -814,6 +872,7 @@ export function AuditClient() { setSummary(null); setIssues([]); setMissingFromSources([]); + setUnrepairable([]); setRepairStats(null); setNitroRepairStats(null); setSqlText(null); diff --git a/src/lib/services/catalog-audit.ts b/src/lib/services/catalog-audit.ts index dab6536a..07adcdc4 100644 --- a/src/lib/services/catalog-audit.ts +++ b/src/lib/services/catalog-audit.ts @@ -47,6 +47,7 @@ export interface AuditEvent { section?: string; issues?: AuditIssue[]; missingFromSources?: MissingFromSource[]; + unrepairable?: UnrepairableItem[]; summary?: AuditSummary; repair?: RepairStats; repairNitro?: RepairStats; @@ -88,6 +89,12 @@ export interface MissingFromSource { itemName: string; } +export interface UnrepairableItem { + classname: string; + itemId: number; + missing: Array<"nitro" | "icon">; +} + export interface AuditSummary { totalItemsBase: number; totalCatalogItems: number; @@ -97,6 +104,7 @@ export interface AuditSummary { missingIcon: number; duplicates: number; missingFromSourcesTotal: number; + unrepairableCount: number; iconsRepaired: number; iconsStillMissing: number; nitrosRepaired: number; @@ -127,6 +135,13 @@ export async function runCatalogAudit( ): Promise { let issues: AuditIssue[] = []; const missingFromSources: MissingFromSource[] = []; + // Items whose missing assets could not be restored from any source even + // after a repair attempt — legacy / broken furniture. Keyed by classname so + // an item missing both assets is listed once. + const unrepairable = new Map< + string, + { itemId: number; missing: Set<"nitro" | "icon"> } + >(); try { await ensureDirectories(); @@ -450,7 +465,8 @@ export async function runCatalogAudit( }); // Re-check which of the previously-missing icons are still missing - // and drop the ones the repair fixed from the issue list. + // and drop the ones the repair fixed from the issue list. The ones + // left over cannot be restored from any source → legacy/broken. const stillMissing: string[] = []; for (const classname of missingIconClassnames) { const hasIcon = assetNameCandidates(classname).icon.some((n) => @@ -460,10 +476,15 @@ export async function runCatalogAudit( stillMissing.push(classname); } } - issues = issues.filter( - (i) => - i.type !== "missing_icon" || stillMissing.includes(i.classname ?? ""), - ); + issues = issues.filter((i) => i.type !== "missing_icon"); + for (const classname of stillMissing) { + const entry = unrepairable.get(classname) ?? { + itemId: itemByName.get(classname)?.id ?? 0, + missing: new Set(), + }; + entry.missing.add("icon"); + unrepairable.set(classname, entry); + } repairStats = { succeeded: result.succeeded, @@ -508,7 +529,8 @@ export async function runCatalogAudit( }); // Re-check which of the previously-missing nitros are still missing - // and drop the ones the repair fixed from the issue list. + // and drop the ones the repair fixed from the issue list. The ones + // left over cannot be restored from any source → legacy/broken. const stillMissing: string[] = []; for (const classname of missingNitroClassnames) { const hasNitro = assetNameCandidates(classname).nitro.some((n) => @@ -518,11 +540,15 @@ export async function runCatalogAudit( stillMissing.push(classname); } } - issues = issues.filter( - (i) => - i.type !== "missing_nitro" || - stillMissing.includes(i.classname ?? ""), - ); + issues = issues.filter((i) => i.type !== "missing_nitro"); + for (const classname of stillMissing) { + const entry = unrepairable.get(classname) ?? { + itemId: itemByName.get(classname)?.id ?? 0, + missing: new Set(), + }; + entry.missing.add("nitro"); + unrepairable.set(classname, entry); + } nitroRepairStats = { succeeded: result.succeeded, @@ -720,11 +746,24 @@ export async function runCatalogAudit( } } + // Items that a repair attempt could not fix are legacy/broken furniture — + // surfaced as a separate informational group (not counted as errors) so a + // fully repaired catalog can reach 0 errors while still listing exactly + // what is not restorable. + issues.sort((a, b) => { const sev = { error: 0, warning: 1, info: 2 }; return (sev[a.severity] ?? 0) - (sev[b.severity] ?? 0); }); + const unrepairableList: UnrepairableItem[] = [...unrepairable.entries()] + .sort(([a], [b]) => a.localeCompare(b)) + .map(([classname, entry]) => ({ + classname, + itemId: entry.itemId, + missing: [...entry.missing].sort(), + })); + const summary: AuditSummary = { totalItemsBase: items.length, totalCatalogItems: catalogItems.length, @@ -734,6 +773,7 @@ export async function runCatalogAudit( missingIcon: issues.filter((i) => i.type === "missing_icon").length, duplicates: issues.filter((i) => i.type === "duplicate_classname").length, missingFromSourcesTotal: missingFromSources.length, + unrepairableCount: unrepairableList.length, iconsRepaired: repairStats?.succeeded ?? 0, iconsStillMissing: repairStats?.stillMissing ?? missingIconClassnames.length, @@ -755,6 +795,7 @@ export async function runCatalogAudit( type: "batch_complete", issues, missingFromSources, + unrepairable: unrepairableList, summary, }); }