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

@@ -1,6 +1,6 @@
"use client";
import { ImageIcon, Loader2, Wrench } from "lucide-react";
import { Check, ImageIcon, Loader2, Wrench } from "lucide-react";
import { useCallback, useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
@@ -10,8 +10,13 @@ interface RepairEvent {
type: string;
classname?: string;
status?: string;
index?: number;
total?: number;
succeeded?: number;
failed?: number;
skipped?: number;
message?: string;
warnings?: string[];
}
export function RepairIconsClient() {
@@ -24,6 +29,7 @@ export function RepairIconsClient() {
const [result, setResult] = useState<{
succeeded: number;
failed: number;
skipped: number;
} | null>(null);
const startRepair = useCallback(async () => {
@@ -46,9 +52,10 @@ export function RepairIconsClient() {
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "";
let total = 0;
let succeeded = 0;
let failed = 0;
let totalItems = 0;
let skipped = 0;
while (true) {
const { value, done: streamDone } = await reader.read();
@@ -60,22 +67,47 @@ export function RepairIconsClient() {
if (!part.startsWith("data: ")) continue;
try {
const evt = JSON.parse(part.slice(6)) as RepairEvent;
if (
evt.type === "item_progress" &&
(evt.status === "done" || evt.status === "failed")
) {
totalItems++;
if (evt.status === "done") succeeded++;
else failed++;
setProgress({ done: succeeded + failed, total: totalItems });
if (evt.type === "started") {
total = evt.total ?? 0;
setProgress({ done: 0, total });
setLog((prev) => [...prev, `Scanning ${total} furniture items…`]);
}
if (evt.type === "progress") {
const processed = (evt.index ?? 0) + 1;
setProgress({ done: processed, total });
if (evt.status === "skipped") {
skipped++;
} else if (evt.status === "done") {
succeeded++;
} else if (evt.status === "failed") {
failed++;
}
const icon =
evt.status === "done"
? "✓"
: evt.status === "failed"
? "✗"
: "–";
setLog((prev) => [
...prev,
`${evt.status === "done" ? "✓" : "✗"} ${evt.classname ?? ""}`,
`${icon} ${evt.classname ?? ""} (${evt.status})`,
]);
}
if (evt.type === "batch_complete") {
succeeded = evt.succeeded ?? succeeded;
failed = evt.failed ?? failed;
skipped = evt.skipped ?? skipped;
}
if (evt.type === "error") {
setLog((prev) => [
...prev,
`ERROR: ${evt.message ?? "Unknown error"}`,
]);
toast.error(evt.message ?? "Repair failed");
}
} catch {
// skip parse errors
@@ -83,8 +115,8 @@ export function RepairIconsClient() {
}
}
setResult({ succeeded, failed });
if (succeeded > 0) toast.success(`${succeeded} icons repaired`);
setResult({ succeeded, failed, skipped });
if (succeeded > 0) toast.success(`${succeeded} icons downloaded`);
if (failed > 0) toast.error(`${failed} icons could not be obtained`);
} catch {
toast.error("Repair request failed");
@@ -110,8 +142,7 @@ export function RepairIconsClient() {
{progress && (
<div className="text-sm text-muted-foreground">
Processed {progress.done} items
{progress.total > 0 ? ` out of ${progress.total}` : ""}
Processed {progress.done} / {progress.total} items
</div>
)}
@@ -128,19 +159,34 @@ export function RepairIconsClient() {
{result && (
<div className="rounded-xl border bg-card p-5 space-y-3">
<div className="flex items-center gap-2">
<ImageIcon className="w-5 h-5 text-[var(--admin-success)]" />
<Check className="w-5 h-5 text-[var(--admin-success)]" />
<span className="text-lg font-semibold">Repair Complete</span>
</div>
<div className="text-sm space-y-1">
<p>
<span className="text-muted-foreground">Icons downloaded:</span>{" "}
<span className="text-muted-foreground">Downloads:</span>{" "}
{result.succeeded}
</p>
<p>
<span className="text-muted-foreground">
Skipped (already had icon):
</span>{" "}
{result.skipped}
</p>
<p>
<span className="text-muted-foreground">Failed:</span>{" "}
{result.failed}
</p>
</div>
<Button
variant="outline"
onClick={() => {
setResult(null);
setLog([]);
}}
>
Run Again
</Button>
</div>
)}