feat: group unrepairable legacy items separately in catalog audit
CI / check (push) Successful in 25s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m2s

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.
This commit is contained in:
openhands committed 2026-08-04 11:18:37 +02:00
1 parent d587749b50
commit 2ee5ba5c4c
2 files changed
+124 -24

No files matched your search

+52 -11
View File
@@ -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<void> {
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,
});
}