feat: parallelize audit repair and add nitro/SQL repair options
CI / check (push) Successful in 48s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m55s

- Parallelize icon and nitro downloads in the audit repair with a
  sliding-window worker pool (6 concurrent) to speed up large catalogs
- Add repairMissingNitros: fetch missing .nitro bundles from configured
  nitro sources (Wibbo default), validating each bundle before writing
- Add catalog-repair service: generate/apply catalog_items SQL for furni
  missing a catalog entry and repair FurnitureData.json (add missing +
  dedupe classnames)
- Wire all options through the audit API and client UI with live progress
  and result stats (icons, nitros, SQL, furnidata)
- Add Wibbo as default nitro source alongside existing icon sources
- Ignore runtime furni assets downloaded into public/ during repair
This commit is contained in:
openhands committed 2026-08-02 19:12:28 +02:00
1 parent 0c8e62357f
commit 5308ce6a12
8 files changed
+1166 -58

No files matched your search

+64 -33
View File
@@ -6,41 +6,72 @@ function sseEvent(data: unknown): string {
return `data: ${JSON.stringify(data)}\n\n`;
}
export const POST = withAdmin({ permission: PERMS.ASSETS_IMPORT }, async () => {
const encoder = new TextEncoder();
export const POST = withAdmin(
{ permission: PERMS.ASSETS_IMPORT },
async (request) => {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
const send = (data: unknown) => {
try {
controller.enqueue(encoder.encode(sseEvent(data)));
} catch {
/* stream closed */
}
let repair = false;
let repairNitros = false;
let sql = false;
let applySql = false;
let repairFurniData = false;
try {
const body = (await request.json().catch(() => ({}))) as {
repair?: unknown;
repairNitros?: unknown;
sql?: unknown;
applySql?: unknown;
repairFurniData?: unknown;
};
repair = body.repair === true;
repairNitros = body.repairNitros === true;
sql = body.sql === true;
applySql = body.applySql === true;
repairFurniData = body.repairFurniData === true;
} catch {
/* no body */
}
try {
await runCatalogAudit(send);
} catch (err) {
send({
type: "error",
message: `Audit threw: ${(err as Error).message}`,
});
}
const stream = new ReadableStream({
async start(controller) {
const send = (data: unknown) => {
try {
controller.enqueue(encoder.encode(sseEvent(data)));
} catch {
/* stream closed */
}
};
try {
controller.close();
} catch {
/* ignore */
}
},
});
try {
await runCatalogAudit(send, {
repair,
repairNitros,
sql,
applySql,
repairFurniData,
});
} catch (err) {
send({
type: "error",
message: `Audit threw: ${(err as Error).message}`,
});
}
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
});
try {
controller.close();
} catch {
/* ignore */
}
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
},
);