style: format code with prettier
Local Build and Deploy / deploy (push) Successful in 49s

This commit is contained in:
openhands committed 2026-07-12 21:07:34 +02:00
1 parent e85e4d74ea
commit 2e4ed76121
378 files changed
+22404 -20686

No files matched your search

+247 -277
View File
@@ -1,4 +1,4 @@
'use client'
"use client";
import {
AlertCircle,
@@ -10,13 +10,13 @@ import {
Trash2,
Wrench,
XCircle,
} from 'lucide-react'
import { useCallback, useMemo, useState } from 'react'
import { toast } from 'sonner'
import { deleteImportedFurni } from '@/actions/import-furni'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
} from "lucide-react";
import { useCallback, useMemo, useState } from "react";
import { toast } from "sonner";
import { deleteImportedFurni } from "@/actions/import-furni";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
@@ -24,245 +24,238 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
type MissingKind = 'nitro' | 'icon' | 'both'
type MissingKind = "nitro" | "icon" | "both";
interface MissingItem {
id: number
classname: string
baseClassname: string
revision: number
type: 's' | 'i'
publicName: string
hasSwf: boolean
hasIcon: boolean
hasNitro: boolean
missing: MissingKind
unrepairable: boolean
id: number;
classname: string;
baseClassname: string;
revision: number;
type: "s" | "i";
publicName: string;
hasSwf: boolean;
hasIcon: boolean;
hasNitro: boolean;
missing: MissingKind;
unrepairable: boolean;
}
interface AuditSummary {
total: number
missingBoth: number
missingNitro: number
missingIcon: number
unrepairable: number
items: MissingItem[]
total: number;
missingBoth: number;
missingNitro: number;
missingIcon: number;
unrepairable: number;
items: MissingItem[];
}
type ItemStatus = 'idle' | 'working' | 'done' | 'partial' | 'failed'
type ItemStatus = "idle" | "working" | "done" | "partial" | "failed";
interface ItemProgress {
status: ItemStatus
message?: string
nitroRepaired?: boolean
iconRepaired?: boolean
unrepairable?: boolean
status: ItemStatus;
message?: string;
nitroRepaired?: boolean;
iconRepaired?: boolean;
unrepairable?: boolean;
}
interface RepairSummary {
succeeded: number
partial: number
failed: number
unrepairable: number
duration: number
succeeded: number;
partial: number;
failed: number;
unrepairable: number;
duration: number;
}
const TAB_FILTERS: Record<string, (m: MissingItem) => boolean> = {
all: () => true,
both: (m) => m.missing === 'both',
nitro: (m) => m.missing === 'nitro',
icon: (m) => m.missing === 'icon',
both: (m) => m.missing === "both",
nitro: (m) => m.missing === "nitro",
icon: (m) => m.missing === "icon",
unrepairable: (m) => m.unrepairable,
}
};
export function RepairClient() {
const [summary, setSummary] = useState<AuditSummary | null>(null)
const [auditing, setAuditing] = useState(false)
const [repairing, setRepairing] = useState(false)
const [search, setSearch] = useState('')
const [tab, setTab] = useState<keyof typeof TAB_FILTERS>('all')
const [selected, setSelected] = useState<Set<string>>(new Set())
const [progress, setProgress] = useState<Map<string, ItemProgress>>(new Map())
const [lastSummary, setLastSummary] = useState<RepairSummary | null>(null)
const [confirmDeleteAll, setConfirmDeleteAll] = useState(false)
const [summary, setSummary] = useState<AuditSummary | null>(null);
const [auditing, setAuditing] = useState(false);
const [repairing, setRepairing] = useState(false);
const [search, setSearch] = useState("");
const [tab, setTab] = useState<keyof typeof TAB_FILTERS>("all");
const [selected, setSelected] = useState<Set<string>>(new Set());
const [progress, setProgress] = useState<Map<string, ItemProgress>>(new Map());
const [lastSummary, setLastSummary] = useState<RepairSummary | null>(null);
const [confirmDeleteAll, setConfirmDeleteAll] = useState(false);
// ── Audit ──────────────────────────────────────────────────────────
const runAudit = useCallback(async () => {
setAuditing(true)
setLastSummary(null)
setAuditing(true);
setLastSummary(null);
try {
const res = await fetch('/api/admin/import/repair/audit', { method: 'POST' })
const res = await fetch("/api/admin/import/repair/audit", { method: "POST" });
if (!res.ok) {
toast.error('Audit failed')
return
toast.error("Audit failed");
return;
}
const data = await res.json()
setSummary(data)
setSelected(new Set())
setProgress(new Map())
const data = await res.json();
setSummary(data);
setSelected(new Set());
setProgress(new Map());
toast.success(
`Audit: ${data.items.length} item${data.items.length === 1 ? '' : 's'} need repair (${data.total} scanned)`,
)
`Audit: ${data.items.length} item${data.items.length === 1 ? "" : "s"} need repair (${data.total} scanned)`,
);
} catch {
toast.error('Audit network error')
toast.error("Audit network error");
} finally {
setAuditing(false)
setAuditing(false);
}
}, [])
}, []);
// ── Filters ────────────────────────────────────────────────────────
const filtered = useMemo(() => {
if (!summary) return []
const s = search.trim().toLowerCase()
if (!summary) return [];
const s = search.trim().toLowerCase();
return summary.items.filter((it) => {
if (!TAB_FILTERS[tab](it)) return false
if (!s) return true
if (!TAB_FILTERS[tab](it)) return false;
if (!s) return true;
return (
it.classname.toLowerCase().includes(s) ||
it.publicName.toLowerCase().includes(s) ||
String(it.id).includes(s)
)
})
}, [summary, search, tab])
);
});
}, [summary, search, tab]);
// ── Selection ──────────────────────────────────────────────────────
const allSelected = filtered.length > 0 && filtered.every((i) => selected.has(i.classname))
const allSelected = filtered.length > 0 && filtered.every((i) => selected.has(i.classname));
const toggleAll = () => {
setSelected((prev) => {
if (allSelected) {
const next = new Set(prev)
const next = new Set(prev);
for (const i of filtered) {
next.delete(i.classname)
next.delete(i.classname);
}
return next
return next;
}
const next = new Set(prev)
const next = new Set(prev);
for (const i of filtered) {
next.add(i.classname)
next.add(i.classname);
}
return next
})
}
return next;
});
};
const toggleOne = (cn: string) => {
setSelected((prev) => {
const next = new Set(prev)
if (next.has(cn)) next.delete(cn)
else next.add(cn)
return next
})
}
const next = new Set(prev);
if (next.has(cn)) next.delete(cn);
else next.add(cn);
return next;
});
};
// ── Repair ─────────────────────────────────────────────────────────
const runRepair = useCallback(async (classnames: string[]) => {
if (classnames.length === 0) return
setRepairing(true)
setLastSummary(null)
if (classnames.length === 0) return;
setRepairing(true);
setLastSummary(null);
// Init progress for these items
setProgress((prev) => {
const next = new Map(prev)
const next = new Map(prev);
for (const cn of classnames) {
next.set(cn, { status: 'working' })
next.set(cn, { status: "working" });
}
return next
})
return next;
});
try {
const res = await fetch('/api/admin/import/repair', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
const res = await fetch("/api/admin/import/repair", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ classnames, concurrency: 6 }),
})
});
if (!res.ok || !res.body) {
toast.error('Failed to start repair')
setRepairing(false)
return
toast.error("Failed to start repair");
setRepairing(false);
return;
}
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() || ''
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (!line.startsWith('data: ')) continue
if (!line.startsWith("data: ")) continue;
try {
const event = JSON.parse(line.slice(6))
if (event.type === 'repair_progress') {
const event = JSON.parse(line.slice(6));
if (event.type === "repair_progress") {
setProgress((prev) => {
const next = new Map(prev)
const next = new Map(prev);
next.set(event.classname, {
status: event.status as ItemStatus,
message: event.error || (event.warnings?.[0] ?? undefined),
nitroRepaired: event.nitroRepaired,
iconRepaired: event.iconRepaired,
unrepairable: event.unrepairable,
})
return next
})
});
return next;
});
// Promote newly-discovered unrepairable flags onto the audit list
if (event.unrepairable) {
setSummary((prev) => {
if (!prev) return prev
const item = prev.items.find((i) => i.classname === event.classname)
if (!item || item.unrepairable) return prev
if (!prev) return prev;
const item = prev.items.find((i) => i.classname === event.classname);
if (!item || item.unrepairable) return prev;
return {
...prev,
unrepairable: prev.unrepairable + 1,
items: prev.items.map((i) =>
i.classname === event.classname ? { ...i, unrepairable: true } : i,
),
}
})
};
});
}
// Remove repaired items from the audit list on "done"
if (event.status === 'done') {
if (event.status === "done") {
setSummary((prev) => {
if (!prev) return prev
const items = prev.items.filter((i) => i.classname !== event.classname)
const diff = prev.items.length - items.length
if (diff === 0) return prev
const removed = prev.items.find((i) => i.classname === event.classname)
if (!prev) return prev;
const items = prev.items.filter((i) => i.classname !== event.classname);
const diff = prev.items.length - items.length;
if (diff === 0) return prev;
const removed = prev.items.find((i) => i.classname === event.classname);
return {
...prev,
items,
missingBoth: prev.missingBoth - (removed?.missing === 'both' ? 1 : 0),
missingNitro: prev.missingNitro - (removed?.missing === 'nitro' ? 1 : 0),
missingIcon: prev.missingIcon - (removed?.missing === 'icon' ? 1 : 0),
}
})
missingBoth: prev.missingBoth - (removed?.missing === "both" ? 1 : 0),
missingNitro: prev.missingNitro - (removed?.missing === "nitro" ? 1 : 0),
missingIcon: prev.missingIcon - (removed?.missing === "icon" ? 1 : 0),
};
});
setSelected((prev) => {
const next = new Set(prev)
next.delete(event.classname)
return next
})
const next = new Set(prev);
next.delete(event.classname);
return next;
});
}
} else if (event.type === 'repair_complete') {
} else if (event.type === "repair_complete") {
setLastSummary({
succeeded: event.succeeded,
partial: event.partial,
failed: event.failed,
unrepairable: event.unrepairable ?? 0,
duration: event.duration,
})
});
}
} catch {
/* skip malformed */
@@ -270,72 +263,72 @@ export function RepairClient() {
}
}
} catch {
toast.error('Connection lost during repair')
toast.error("Connection lost during repair");
} finally {
setRepairing(false)
setRepairing(false);
}
}, [])
}, []);
// ── Delete unrepairable items ──────────────────────────────────────
const deleteSelected = useCallback(async () => {
const toDelete = Array.from(selected)
if (toDelete.length === 0) return
setConfirmDeleteAll(false)
const toDelete = Array.from(selected);
if (toDelete.length === 0) return;
setConfirmDeleteAll(false);
let ok = 0
let fail = 0
let ok = 0;
let fail = 0;
for (const cn of toDelete) {
setProgress((prev) => {
const next = new Map(prev)
next.set(cn, { status: 'working', message: 'Deleting…' })
return next
})
const next = new Map(prev);
next.set(cn, { status: "working", message: "Deleting…" });
return next;
});
try {
const result = await deleteImportedFurni({ classname: cn })
const result = await deleteImportedFurni({ classname: cn });
if (result.ok) {
ok++
ok++;
setSummary((prev) => {
if (!prev) return prev
const removed = prev.items.find((i) => i.classname === cn)
if (!removed) return prev
if (!prev) return prev;
const removed = prev.items.find((i) => i.classname === cn);
if (!removed) return prev;
return {
...prev,
total: prev.total - 1,
items: prev.items.filter((i) => i.classname !== cn),
missingBoth: prev.missingBoth - (removed.missing === 'both' ? 1 : 0),
missingNitro: prev.missingNitro - (removed.missing === 'nitro' ? 1 : 0),
missingIcon: prev.missingIcon - (removed.missing === 'icon' ? 1 : 0),
}
})
missingBoth: prev.missingBoth - (removed.missing === "both" ? 1 : 0),
missingNitro: prev.missingNitro - (removed.missing === "nitro" ? 1 : 0),
missingIcon: prev.missingIcon - (removed.missing === "icon" ? 1 : 0),
};
});
setSelected((prev) => {
const next = new Set(prev)
next.delete(cn)
return next
})
const next = new Set(prev);
next.delete(cn);
return next;
});
} else {
fail++
fail++;
setProgress((prev) => {
const next = new Map(prev)
next.set(cn, { status: 'failed', message: 'Delete failed' })
return next
})
const next = new Map(prev);
next.set(cn, { status: "failed", message: "Delete failed" });
return next;
});
}
} catch {
fail++
fail++;
setProgress((prev) => {
const next = new Map(prev)
next.set(cn, { status: 'failed', message: 'Delete error' })
return next
})
const next = new Map(prev);
next.set(cn, { status: "failed", message: "Delete error" });
return next;
});
}
}
toast.success(`Deleted ${ok}${fail > 0 ? `, ${fail} failed` : ''}`)
}, [selected])
toast.success(`Deleted ${ok}${fail > 0 ? `, ${fail} failed` : ""}`);
}, [selected]);
// ── Render ─────────────────────────────────────────────────────────
const selectedArray = Array.from(selected)
const selectedCount = selectedArray.length
const selectedArray = Array.from(selected);
const selectedCount = selectedArray.length;
return (
<div className="space-y-6">
@@ -344,9 +337,8 @@ export function RepairClient() {
<div>
<h1 className="text-3xl font-bold tracking-tight">Repair Imported Assets</h1>
<p className="text-muted-foreground">
Re-download missing <code className="rounded bg-muted px-1">.nitro</code> bundles and
icons for items already imported. Items with no public asset on habbo.com can be deleted
from here.
Re-download missing <code className="rounded bg-muted px-1">.nitro</code> bundles and icons for
items already imported. Items with no public asset on habbo.com can be deleted from here.
</p>
</div>
<div className="flex gap-2">
@@ -357,7 +349,7 @@ export function RepairClient() {
</>
) : (
<>
<RefreshCw className="mr-2 h-4 w-4" /> {summary ? 'Re-scan' : 'Scan Assets'}
<RefreshCw className="mr-2 h-4 w-4" /> {summary ? "Re-scan" : "Scan Assets"}
</>
)}
</Button>
@@ -368,12 +360,10 @@ export function RepairClient() {
{!summary && !auditing && (
<div className="rounded-md border bg-muted/30 p-10 text-center">
<Wrench className="mx-auto mb-3 h-10 w-10 text-muted-foreground" />
<p className="font-medium">
Click &ldquo;Scan Assets&rdquo; to audit imported furniture.
</p>
<p className="font-medium">Click &ldquo;Scan Assets&rdquo; to audit imported furniture.</p>
<p className="text-sm text-muted-foreground">
This checks every row in <code>items_base</code> against files on disk. Scanning ~33k
items takes a few seconds.
This checks every row in <code>items_base</code> against files on disk. Scanning ~33k items takes
a few seconds.
</p>
</div>
)}
@@ -388,12 +378,7 @@ export function RepairClient() {
tone="danger"
sub="no .nitro · no icon"
/>
<StatCard
label="Missing .nitro"
value={summary.missingNitro}
tone="warn"
sub="shows black cube"
/>
<StatCard label="Missing .nitro" value={summary.missingNitro} tone="warn" sub="shows black cube" />
<StatCard
label="Missing icon"
value={summary.missingIcon}
@@ -437,9 +422,7 @@ export function RepairClient() {
<div className="rounded-md border bg-muted/30 p-10 text-center">
<CheckCircle2 className="mx-auto mb-3 h-10 w-10 text-[var(--admin-success)]" />
<p className="font-medium">All imported items have their assets on disk.</p>
<p className="text-sm text-muted-foreground">
{summary.total} items scanned, nothing to repair.
</p>
<p className="text-sm text-muted-foreground">{summary.total} items scanned, nothing to repair.</p>
</div>
)}
@@ -461,16 +444,15 @@ export function RepairClient() {
variant="default"
onClick={() => {
// Skip unrepairable items — no point paying the network cost for them
const pool = selectedCount > 0 ? selectedArray : filtered.map((i) => i.classname)
const unrep =
summary?.items.filter((i) => i.unrepairable).map((i) => i.classname) ?? []
const unrepSet = new Set(unrep)
const targets = pool.filter((cn) => !unrepSet.has(cn))
const pool = selectedCount > 0 ? selectedArray : filtered.map((i) => i.classname);
const unrep = summary?.items.filter((i) => i.unrepairable).map((i) => i.classname) ?? [];
const unrepSet = new Set(unrep);
const targets = pool.filter((cn) => !unrepSet.has(cn));
if (targets.length === 0) {
toast.info('Nothing to repair — selected items are all flagged unrepairable.')
return
toast.info("Nothing to repair — selected items are all flagged unrepairable.");
return;
}
runRepair(targets)
runRepair(targets);
}}
disabled={repairing || filtered.length === 0}
>
@@ -502,9 +484,9 @@ export function RepairClient() {
variant="destructive"
onClick={() => {
// Auto-select every unrepairable item, then open the same confirm dialog
const unrep = summary.items.filter((i) => i.unrepairable).map((i) => i.classname)
setSelected(new Set(unrep))
setConfirmDeleteAll(true)
const unrep = summary.items.filter((i) => i.unrepairable).map((i) => i.classname);
setSelected(new Set(unrep));
setConfirmDeleteAll(true);
}}
disabled={repairing}
>
@@ -529,11 +511,7 @@ export function RepairClient() {
<TableHeader>
<TableRow>
<TableHead className="w-[40px]">
<Checkbox
checked={allSelected}
onCheckedChange={toggleAll}
aria-label="Select all"
/>
<Checkbox checked={allSelected} onCheckedChange={toggleAll} aria-label="Select all" />
</TableHead>
<TableHead className="w-[80px]">ID</TableHead>
<TableHead>Classname</TableHead>
@@ -576,13 +554,12 @@ export function RepairClient() {
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>
Delete {selectedCount} imported item{selectedCount === 1 ? '' : 's'}?
Delete {selectedCount} imported item{selectedCount === 1 ? "" : "s"}?
</DialogTitle>
<DialogDescription>
This removes the rows from <code>items_base</code> / <code>catalog_items</code>, the
entry in <code>FurnitureData.json</code>, and any remaining asset files on disk. Use
this for items that have no public asset on habbo.com and therefore cannot be
repaired. This cannot be undone.
This removes the rows from <code>items_base</code> / <code>catalog_items</code>, the entry in{" "}
<code>FurnitureData.json</code>, and any remaining asset files on disk. Use this for items that
have no public asset on habbo.com and therefore cannot be repaired. This cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
@@ -596,7 +573,7 @@ export function RepairClient() {
</DialogContent>
</Dialog>
</div>
)
);
}
// ── Small cell helpers ────────────────────────────────────────────────
@@ -607,24 +584,24 @@ function StatCard({
tone,
sub,
}: {
label: string
value: number
tone: 'default' | 'danger' | 'warn' | 'info'
sub?: string
label: string;
value: number;
tone: "default" | "danger" | "warn" | "info";
sub?: string;
}) {
const tones = {
default: 'border-border',
danger: 'border-destructive/40 bg-destructive/5',
warn: 'border-[var(--admin-warning-border)] bg-[var(--admin-warning-subtle)]',
info: 'border-[var(--admin-info)]/40 bg-[var(--admin-info)]/5',
}
default: "border-border",
danger: "border-destructive/40 bg-destructive/5",
warn: "border-[var(--admin-warning-border)] bg-[var(--admin-warning-subtle)]",
info: "border-[var(--admin-info)]/40 bg-[var(--admin-info)]/5",
};
return (
<div className={`rounded-md border p-4 ${tones[tone]}`}>
<div className="text-xs uppercase tracking-wide text-muted-foreground">{label}</div>
<div className="mt-1 text-2xl font-bold">{value.toLocaleString()}</div>
{sub && <div className="mt-0.5 text-xs text-muted-foreground">{sub}</div>}
</div>
)
);
}
function FileBadge({ present }: { present: boolean }) {
@@ -639,49 +616,49 @@ function FileBadge({ present }: { present: boolean }) {
<Badge variant="secondary" className="border-destructive/40 bg-destructive/10 text-destructive">
<XCircle className="mr-1 h-3 w-3" /> Missing
</Badge>
)
);
}
function StatusCell({ progress }: { progress?: ItemProgress }) {
if (!progress || progress.status === 'idle')
return <span className="text-muted-foreground text-xs">—</span>
if (progress.status === 'working') {
if (!progress || progress.status === "idle")
return <span className="text-muted-foreground text-xs">—</span>;
if (progress.status === "working") {
return (
<span className="flex items-center gap-1 text-xs text-muted-foreground">
<Loader2 className="h-3 w-3 animate-spin" />
{progress.message || 'Working…'}
{progress.message || "Working…"}
</span>
)
);
}
if (progress.status === 'done') {
if (progress.status === "done") {
const parts = [
progress.nitroRepaired ? '.nitro ✓' : null,
progress.iconRepaired ? 'icon ✓' : null,
].filter(Boolean)
progress.nitroRepaired ? ".nitro ✓" : null,
progress.iconRepaired ? "icon ✓" : null,
].filter(Boolean);
return (
<span className="flex items-center gap-1 text-xs text-[var(--admin-success)] dark:text-[var(--admin-success)]">
<CheckCircle2 className="h-3 w-3" />
{parts.length > 0 ? parts.join(' · ') : 'Repaired'}
{parts.length > 0 ? parts.join(" · ") : "Repaired"}
</span>
)
);
}
if (progress.status === 'partial') {
if (progress.status === "partial") {
return (
<span
className="flex items-center gap-1 text-xs text-[var(--admin-warning)] dark:text-[var(--admin-warning)]"
title={progress.message}
>
<AlertCircle className="h-3 w-3" />
Partial · {progress.message || 'check logs'}
Partial · {progress.message || "check logs"}
</span>
)
);
}
return (
<span className="flex items-center gap-1 text-xs text-destructive" title={progress.message}>
<XCircle className="h-3 w-3" />
{progress.message || 'Failed'}
{progress.message || "Failed"}
</span>
)
);
}
function ItemRow({
@@ -692,39 +669,32 @@ function ItemRow({
onRepair,
disabled,
}: {
item: MissingItem
selected: boolean
onToggle: () => void
progress?: ItemProgress
onRepair: () => void
disabled: boolean
item: MissingItem;
selected: boolean;
onToggle: () => void;
progress?: ItemProgress;
onRepair: () => void;
disabled: boolean;
}) {
const isUnrepairable = item.unrepairable || progress?.unrepairable
const isUnrepairable = item.unrepairable || progress?.unrepairable;
return (
<TableRow className={isUnrepairable ? 'bg-destructive/5' : undefined}>
<TableRow className={isUnrepairable ? "bg-destructive/5" : undefined}>
<TableCell>
<Checkbox
checked={selected}
onCheckedChange={onToggle}
aria-label={`Select ${item.classname}`}
/>
<Checkbox checked={selected} onCheckedChange={onToggle} aria-label={`Select ${item.classname}`} />
</TableCell>
<TableCell className="font-mono text-xs">{item.id}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<span className="font-mono text-sm">{item.classname}</span>
{isUnrepairable && (
<Badge
variant="secondary"
className="border-destructive/40 bg-destructive/10 text-destructive"
>
<Badge variant="secondary" className="border-destructive/40 bg-destructive/10 text-destructive">
<AlertCircle className="mr-1 h-3 w-3" /> Unrepairable
</Badge>
)}
</div>
<div className="text-xs text-muted-foreground">{item.publicName}</div>
</TableCell>
<TableCell className="text-xs">{item.revision || '—'}</TableCell>
<TableCell className="text-xs">{item.revision || "—"}</TableCell>
<TableCell>
<FileBadge present={item.hasNitro} />
</TableCell>
@@ -742,10 +712,10 @@ function ItemRow({
size="sm"
variant="outline"
onClick={onRepair}
disabled={disabled || progress?.status === 'working' || isUnrepairable}
title={isUnrepairable ? 'habbo.com does not have this asset' : 'Retry download + convert'}
disabled={disabled || progress?.status === "working" || isUnrepairable}
title={isUnrepairable ? "habbo.com does not have this asset" : "Retry download + convert"}
>
{progress?.status === 'working' ? (
{progress?.status === "working" ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<Wrench className="h-3 w-3" />
@@ -753,5 +723,5 @@ function ItemRow({
</Button>
</TableCell>
</TableRow>
)
);
}