fix: rewrite repair-icons with proper error handling and progress
Deploy / release (push) Skipped
Deploy / deploy (push) Successful in 1m38s

- Wrap all DB/file operations in try-catch, send error events via SSE
- Report skipped/progress events for items that already have icons
- Add 'started' event with total count so the UI shows real-time progress
- Catch route-level errors and stream them instead of returning JSON
- Use log line content as React key instead of array index
This commit is contained in:
openhands committed 2026-07-21 15:04:43 +02:00
1 parent 13cb5f8670
commit 90107c83d5
3 files changed
+160 -65

No files matched your search

+79 -43
View File
@@ -8,52 +8,81 @@ import { downloadFile } from "@/lib/services/import/core/download";
import { ensureDirectories } from "./furni-import";
export interface RepairEvent {
type: "item_progress" | "batch_complete" | "error";
type: "started" | "progress" | "done" | "batch_complete" | "error";
classname?: string;
status?: string;
index?: number;
total?: number;
message?: string;
warnings?: string[];
succeeded?: number;
failed?: number;
skipped?: number;
duration?: number;
}
export async function repairMissingIcons(
onEvent?: (evt: RepairEvent) => void,
): Promise<{ succeeded: number; failed: number }> {
await ensureDirectories();
const { iconDir, nitroDir } = await getFurniAssetDirs();
const items = await prisma.$queryRaw<
Array<{ id: number; item_name: string }>
>`SELECT id, item_name FROM items_base ORDER BY id`;
if (items.length === 0) {
onEvent?.({
type: "batch_complete",
succeeded: 0,
failed: 0,
duration: 0,
});
return { succeeded: 0, failed: 0 };
}
): Promise<{ succeeded: number; failed: number; skipped: number }> {
let succeeded = 0;
let failed = 0;
let skipped = 0;
try {
await ensureDirectories();
} catch (err) {
onEvent?.({
type: "error",
message: `ensureDirectories failed: ${(err as Error).message}`,
});
return { succeeded: 0, failed: 0, skipped: 0 };
}
let iconDir: string;
let nitroDir: string;
try {
const dirs = await getFurniAssetDirs();
iconDir = dirs.iconDir;
nitroDir = dirs.nitroDir;
} catch (err) {
onEvent?.({
type: "error",
message: `getFurniAssetDirs failed: ${(err as Error).message}`,
});
return { succeeded: 0, failed: 0, skipped: 0 };
}
let items: Array<{ id: number; item_name: string }>;
try {
items = await prisma.$queryRaw<
Array<{ id: number; item_name: string }>
>`SELECT id, item_name FROM items_base ORDER BY id`;
} catch (err) {
onEvent?.({
type: "error",
message: `Database query failed: ${(err as Error).message}`,
});
return { succeeded: 0, failed: 0, skipped: 0 };
}
const total = items.length;
onEvent?.({ type: "started", total });
for (let i = 0; i < items.length; i++) {
const { item_name: classname } = items[i];
const iconPath = path.join(iconDir, `${classname}_icon.png`);
if (existsSync(iconPath)) continue;
onEvent?.({
type: "item_progress",
classname,
status: "started",
index: i,
});
if (existsSync(iconPath)) {
skipped++;
onEvent?.({
type: "progress",
classname,
status: "skipped",
index: i,
total,
});
continue;
}
let ok = false;
const warnings: string[] = [];
@@ -77,40 +106,46 @@ export async function repairMissingIcons(
}
if (!ok) {
const sources = await listSources();
for (const source of sources) {
try {
const dl = await downloadFile(
`${source.iconBaseUrl}/${classname}_icon.png`,
iconPath,
{ maxRetries: 1, validate: "png" },
);
if (dl.ok) {
ok = true;
break;
try {
const sources = await listSources();
for (const source of sources) {
try {
const dl = await downloadFile(
`${source.iconBaseUrl}/${classname}_icon.png`,
iconPath,
{ maxRetries: 1, validate: "png" },
);
if (dl.ok) {
ok = true;
break;
}
} catch {
warnings.push(`download from ${source.name} failed`);
}
} catch {
warnings.push(`download from ${source.name} failed`);
}
} catch (err) {
warnings.push(`listSources failed: ${(err as Error).message}`);
}
}
if (ok) {
succeeded++;
onEvent?.({
type: "item_progress",
type: "progress",
classname,
status: "done",
index: i,
total,
warnings: warnings.length ? warnings : undefined,
});
} else {
failed++;
onEvent?.({
type: "item_progress",
type: "progress",
classname,
status: "failed",
index: i,
total,
message: warnings[0] || "could not obtain icon",
});
}
@@ -120,7 +155,8 @@ export async function repairMissingIcons(
type: "batch_complete",
succeeded,
failed,
skipped,
});
return { succeeded, failed };
return { succeeded, failed, skipped };
}