perf(studio): stream nitro repair progress over SSE and allow cancelling
This commit is contained in:
1 parent
ba81d16f00
commit
9d571e0c29
4 files changed
+249
-26
No files matched your search
@@ -258,6 +258,12 @@ export function NitroCleanupPanel() {
|
||||
phase: string;
|
||||
scanned: number;
|
||||
} | null>(null);
|
||||
const [repairProgress, setRepairProgress] = useState<{
|
||||
completed: number;
|
||||
total: number;
|
||||
ok: number;
|
||||
failed: number;
|
||||
} | null>(null);
|
||||
const [scan, setScan] = useState<NitroCleanupScan | null>(null);
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
const [selectedSwf, setSelectedSwf] = useState<Set<string>>(new Set());
|
||||
@@ -486,32 +492,85 @@ export function NitroCleanupPanel() {
|
||||
variant: "default",
|
||||
});
|
||||
if (!confirmed) return;
|
||||
abortRef.current?.abort();
|
||||
setRepairing(true);
|
||||
setRepairProgress(null);
|
||||
const abort = new AbortController();
|
||||
abortRef.current = abort;
|
||||
|
||||
const holder: { result: RepairResponse | null } = { result: null };
|
||||
let lastError: string | null = null;
|
||||
try {
|
||||
const res = await adminFetch("/api/admin/studio/nitro-cleanup/rebuild", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "text/event-stream",
|
||||
},
|
||||
body: JSON.stringify({ fileNames }),
|
||||
signal: abort.signal,
|
||||
});
|
||||
const data = (await res.json()) as RepairResponse & {
|
||||
error?: string;
|
||||
};
|
||||
if (!res.ok) throw new Error(data.error || "Repair failed");
|
||||
if (data.repaired > 0) {
|
||||
if (!res.ok) {
|
||||
const body = (await res.json().catch(() => null)) as {
|
||||
error?: string;
|
||||
} | null;
|
||||
throw new Error(
|
||||
typeof body?.error === "string"
|
||||
? body.error
|
||||
: `Repair failed (HTTP ${res.status})`,
|
||||
);
|
||||
}
|
||||
if (!res.body) throw new Error("Repair returned no response body");
|
||||
|
||||
await readSseStream(
|
||||
res.body,
|
||||
(evt) => {
|
||||
if (evt.type === "progress") {
|
||||
setRepairProgress({
|
||||
completed: Number(evt.completed ?? 0),
|
||||
total: Number(evt.total ?? fileNames.length),
|
||||
ok: Number(evt.ok ?? 0),
|
||||
failed: Number(evt.failed ?? 0),
|
||||
});
|
||||
} else if (evt.type === "result") {
|
||||
holder.result = {
|
||||
repaired: Number(evt.repaired ?? 0),
|
||||
failed: Number(evt.failed ?? 0),
|
||||
errors: Array.isArray(evt.errors)
|
||||
? evt.errors.map(String)
|
||||
: undefined,
|
||||
};
|
||||
} else if (evt.type === "error") {
|
||||
lastError = String(evt.message ?? "Repair failed");
|
||||
}
|
||||
},
|
||||
abort.signal,
|
||||
SCAN_IDLE_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
if (lastError) throw new Error(lastError);
|
||||
const data = holder.result;
|
||||
if (data && data.repaired > 0) {
|
||||
toast.success(`Repaired ${data.repaired} .nitro bundle(s)`);
|
||||
} else {
|
||||
} else if (data) {
|
||||
toast.warning(`Could not re-obtain any bundle (${data.failed} failed)`);
|
||||
}
|
||||
// Only drop the attempted bundles when every one succeeded; partial
|
||||
// failures stay visible so they can be retried.
|
||||
if (data.repaired >= fileNames.length) {
|
||||
if (data && data.repaired >= fileNames.length) {
|
||||
dropFromScan("nitro", fileNames);
|
||||
}
|
||||
setSelected(new Set());
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "Repair failed");
|
||||
if ((err as Error).name === "AbortError") {
|
||||
toast.info("Repair cancelled");
|
||||
} else {
|
||||
toast.error(err instanceof Error ? err.message : "Repair failed");
|
||||
}
|
||||
} finally {
|
||||
setRepairing(false);
|
||||
setRepairProgress(null);
|
||||
if (abortRef.current === abort) abortRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -635,7 +694,7 @@ export function NitroCleanupPanel() {
|
||||
)}
|
||||
{scanning ? "Scanning assets…" : "Scan furniture assets"}
|
||||
</Button>
|
||||
{scanning && (
|
||||
{scanning || repairing ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
@@ -644,7 +703,7 @@ export function NitroCleanupPanel() {
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
) : null}
|
||||
{scan && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
@@ -734,6 +793,17 @@ export function NitroCleanupPanel() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{repairing && repairProgress && (
|
||||
<div className="flex items-center gap-2 rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-3 text-sm text-[var(--admin-text-muted)]">
|
||||
<Loader2 className="w-4 h-4 animate-spin shrink-0" />
|
||||
Re-downloading bundles…
|
||||
<span className="font-mono text-xs">
|
||||
{repairProgress.completed}/{repairProgress.total} — ok{" "}
|
||||
{repairProgress.ok}, failed {repairProgress.failed}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{scan && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap items-center gap-3 rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-3">
|
||||
|
||||
Reference in new issue
Block a user