fix(admin): harden clothing/pets/effects/clone imports

Align grids on data.items, only treat SSE done as success, hard-fail
clothing sets when libs fail, and add Cancel via AbortController.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-08-01 15:56:38 +02:00
1 parent 9c4949186c
commit 16191cef14
8 files changed
+256 -85

No files matched your search

File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -336,7 +336,7 @@ export function ImportFurniClient() {
setError(data.error || "Failed to load");
return;
}
const loadedItems = data.item || [];
const loadedItems = data.items || [];
setItems(loadedItems);
setMeta(data.meta || null);
setSelected(new Set());
File diff suppressed because it is too large. Load diff
@@ -87,4 +87,20 @@ describe("importClothingSet", () => {
const r = await importClothingSet({ setType: "ch", setId: 999999 });
expect(r.ok).toBe(false);
});
it("does not write set data when a required library import fails", async () => {
mergeSet.mockClear();
mergePalette.mockClear();
importSingleFigure.mockResolvedValueOnce({
ok: false,
lib: "shirt_M_x",
warnings: [],
error: "nitro missing",
});
const r = await importClothingSet({ setType: "ch", setId: 5000 });
expect(r.ok).toBe(false);
expect(r.error).toMatch(/required figure libraries failed/i);
expect(mergeSet).not.toHaveBeenCalled();
expect(mergePalette).not.toHaveBeenCalled();
});
});
+15
View File
@@ -133,6 +133,8 @@ export async function importClothingSet(params: {
await ensureFigureDirs();
let hardFail = false;
for (const libId of requiredLibIds) {
const officialLib = officialLibs.find((l) => l.id === libId);
if (!officialLib) continue;
@@ -141,18 +143,31 @@ export async function importClothingSet(params: {
onProgress?.(`importing library ${libId}`);
const result = await importSingleFigure({ lib: libId, onProgress });
if (!result.ok) {
hardFail = true;
warnings.push(
`Library import failed for ${libId}: ${result.error ?? "unknown error"}`,
);
continue;
}
if (result.warnings.length) {
warnings.push(...result.warnings);
}
localLibIds.add(libId);
}
await mergeLibrary(officialLib);
}
if (hardFail) {
return {
ok: false,
setId,
warnings,
error:
"One or more required figure libraries failed to import — set was not written",
};
}
const mappedSet = mapOfficialSet(set);
await mergeSet(setType, st.paletteId, mappedSet as never);
+19 -5
View File
@@ -50,6 +50,7 @@ export async function runSseImport(
onDone: (classname: string) => void,
onComplete: (succeeded: number, failed: number) => void,
signal?: AbortSignal,
onFailed?: (classname: string, error?: string) => void,
): Promise<void> {
const res = await adminFetch(url, {
method: "POST",
@@ -57,8 +58,19 @@ export async function runSseImport(
body: JSON.stringify(body),
signal,
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
toast.error(
typeof data.error === "string"
? data.error
: `Import failed (${res.status})`,
);
onComplete(0, 0);
return;
}
if (!res.body) {
toast.error("No response stream");
onComplete(0, 0);
return;
}
@@ -68,11 +80,13 @@ export async function runSseImport(
await readSseStream(
res.body,
(evt) => {
if (
evt.type === "item_progress" &&
(evt.status === "done" || evt.status === "failed")
) {
onDone(String(evt.classname ?? ""));
if (evt.type === "item_progress") {
const classname = String(evt.classname ?? "");
if (evt.status === "done") {
onDone(classname);
} else if (evt.status === "failed") {
onFailed?.(classname, String(evt.error ?? ""));
}
}
if (evt.type === "batch_complete") {
succeeded = Number(evt.succeeded ?? 0);