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.
This commit is contained in:
1 parent
d587749b50
commit
2ee5ba5c4c
2 files changed
+124
-24
No files matched your search
@@ -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<UnrepairableItem[]>([]);
|
||||
const [summary, setSummary] = useState<AuditSummary | null>(null);
|
||||
const [repairStats, setRepairStats] = useState<RepairStats | null>(null);
|
||||
const [nitroRepairStats, setNitroRepairStats] = useState<RepairStats | null>(
|
||||
@@ -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
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{summary.unrepairableCount > 0 ? (
|
||||
<AlertCircle className="w-4 h-4 text-[var(--admin-text-muted)]" />
|
||||
) : (
|
||||
<CheckCircle2 className="w-4 h-4 text-[var(--admin-success)]" />
|
||||
)}
|
||||
<span>
|
||||
{summary.unrepairableCount} item(s) can't be repaired
|
||||
(legacy/broken)
|
||||
</span>
|
||||
</div>
|
||||
{repairStats && (
|
||||
<div className="flex items-center gap-2 col-span-full">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
@@ -690,7 +714,9 @@ export function AuditClient() {
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
{issues.length > 0 || missingFromSources.length > 0 ? (
|
||||
{issues.length > 0 ||
|
||||
missingFromSources.length > 0 ||
|
||||
unrepairable.length > 0 ? (
|
||||
<div className="border-b border-border">
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
@@ -715,6 +741,17 @@ export function AuditClient() {
|
||||
>
|
||||
Missing from sources ({missingFromSources.length})
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("unrepairable")}
|
||||
className={`pb-2 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === "unrepairable"
|
||||
? "border-primary text-foreground"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
Unrepairable (legacy) ({unrepairable.length})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -796,6 +833,27 @@ export function AuditClient() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Unrepairable (legacy) list */}
|
||||
{activeTab === "unrepairable" && unrepairable.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{unrepairable.map((item) => (
|
||||
<div
|
||||
key={item.classname}
|
||||
className="rounded-xl border bg-card p-3 text-sm flex items-center gap-2"
|
||||
>
|
||||
<AlertCircle className="w-3.5 h-3.5 text-[var(--admin-text-muted)] shrink-0" />
|
||||
<code className="font-mono text-xs">{item.classname}</code>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
(ID {item.itemId})
|
||||
</span>
|
||||
<span className="text-xs text-[var(--admin-warning)] ml-auto">
|
||||
missing {item.missing.join(" + ")}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user