Revert "Merge pull request 'Complete Housekeeping migration and /ase cutover' (#52) from codex/housekeeping-complete into main"
CI / check (push) Successful in 27s
CI / release (push) Skipped
CI / deploy (push) Successful in 43s

This reverts commit 488b6e57c4, reversing
changes made to b506b4499a.
This commit is contained in:
Simo committed 2026-08-30 21:31:34 +02:00
1 parent 488b6e57c4
commit b1ddda66ff
802 files changed
+61370 -76659

No files matched your search

@@ -0,0 +1,190 @@
"use client";
import { Ban, Check, Loader2, ShieldCheck, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import type { BatchItemStatus, VerificationSummary } from "./studio-types";
function SummaryRow({
label,
value,
tone,
}: {
label: string;
value: string | number;
tone?: "ok" | "warn";
}) {
return (
<div className="flex items-center justify-between gap-2 text-xs">
<span className="text-[var(--admin-text-muted)]">{label}</span>
<span
className={cn(
"font-mono font-semibold",
tone === "warn"
? "text-[var(--admin-warning)]"
: "text-[var(--admin-success)]",
)}
>
{value}
</span>
</div>
);
}
export function BatchProgress({
progress,
done,
succeeded,
failed,
cancelling,
verification,
onCancel,
onDismiss,
}: {
progress: Map<string, BatchItemStatus>;
done: boolean;
succeeded: number;
failed: number;
cancelling?: boolean;
verification?: VerificationSummary | null;
onCancel?: () => void;
onDismiss?: () => void;
}) {
const entries = [...progress.entries()];
const total = entries.length;
const completed = entries.filter(
([, s]) => s.status === "done" || s.status === "failed",
).length;
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
return (
<div className="border-t border-[var(--admin-border)] p-3">
<div className="mb-2 flex items-center justify-between gap-3">
<span className="text-xs font-bold uppercase tracking-wider text-[var(--admin-text-muted)]">
{done ? "Import finished" : "Importing…"}
</span>
{done ? (
<span
className={cn(
"text-xs font-bold",
failed > 0
? "text-[var(--admin-warning)]"
: "text-[var(--admin-success)]",
)}
>
{completed}/{total} · {succeeded} ok
{failed > 0 ? ` · ${failed} failed` : ""}
</span>
) : (
<span className="flex items-center gap-2">
<span className="text-xs font-mono text-[var(--admin-text-muted)]">
{completed}/{total}
</span>
<Loader2
size={14}
className="animate-spin text-[var(--admin-accent)]"
/>
</span>
)}
</div>
{!done && (
<div className="mb-2 h-1.5 overflow-hidden rounded-full bg-[var(--admin-accent)]/10">
<div
className="h-full rounded-full bg-[var(--admin-accent)] transition-all duration-300"
style={{ width: `${percent}%` }}
/>
</div>
)}
<div className="max-h-44 space-y-1 overflow-y-auto pr-1">
{entries.map(([classname, s]) => (
<div key={classname} className="flex items-center gap-2 text-xs">
{s.status === "done" ? (
<Check
size={12}
className="shrink-0 text-[var(--admin-success)]"
/>
) : s.status === "failed" ? (
<X size={12} className="shrink-0 text-[var(--admin-warning)]" />
) : (
<Loader2
size={12}
className="shrink-0 animate-spin text-[var(--admin-accent)]"
/>
)}
<span className="min-w-0 flex-1 truncate font-mono">
{classname}
</span>
<span className="shrink-0 capitalize text-[var(--admin-text-muted)]">
{s.status.replace(/_/g, " ")}
</span>
</div>
))}
</div>
{done && verification && (
<div className="mt-2 space-y-1.5 rounded-lg border border-[var(--admin-border)] bg-[var(--admin-canvas)] p-2.5">
<div className="flex items-center gap-1.5 text-xs font-bold uppercase tracking-wider text-[var(--admin-text-muted)]">
<ShieldCheck size={13} className="text-[var(--admin-success)]" />
Post-import verification
</div>
<SummaryRow
label="Catalog offer_id fixed"
value={verification.offerIdsFixed}
/>
<SummaryRow
label="Furnidata ids fixed"
value={verification.furniDataFixedIds}
/>
<SummaryRow
label="Furnidata entries missing"
value={verification.furniDataMissing}
tone={verification.furniDataMissing > 0 ? "warn" : "ok"}
/>
<SummaryRow
label="Furnidata id conflicts"
value={verification.furniDataConflicts}
tone={verification.furniDataConflicts > 0 ? "warn" : "ok"}
/>
<SummaryRow
label="Nitros synced"
value={verification.nitrosSynced.length}
/>
<SummaryRow
label="Icons synced"
value={verification.iconsSynced.length}
/>
<SummaryRow
label="Folders chowned"
value={verification.ownershipFixed.length}
/>
</div>
)}
<div className="mt-2 flex justify-end gap-2">
{done ? (
<Button size="sm" variant="secondary" onClick={onDismiss}>
<Check size={14} />
Dismiss
</Button>
) : (
<Button
size="sm"
variant="outline"
onClick={onCancel}
disabled={cancelling}
>
{cancelling ? (
<Loader2 size={14} className="animate-spin" />
) : (
<Ban size={14} />
)}
Cancel
</Button>
)}
</div>
</div>
);
}
@@ -0,0 +1,103 @@
"use client";
import { ChevronDown, ChevronRight, FolderTree } from "lucide-react";
import { useMemo, useState } from "react";
import { cn } from "@/lib/utils";
import type { TreeNode } from "@/types/catalog";
export function CatalogRail({
tree,
selectedId,
onSelect,
}: {
tree: TreeNode[];
selectedId: number | null;
onSelect: (id: number | null) => void;
}) {
const [collapsed, setCollapsed] = useState<Set<number>>(new Set());
const visible = useMemo(() => {
const hidden = new Set<number>();
for (const node of tree) {
if (hidden.has(node.parentId)) {
hidden.add(node.id);
continue;
}
if (collapsed.has(node.parentId)) {
hidden.add(node.id);
}
}
return tree.filter((n) => !hidden.has(n.id));
}, [tree, collapsed]);
const toggle = (id: number) => {
setCollapsed((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};
return (
<aside className="flex h-full min-h-0 w-64 shrink-0 flex-col border-r border-[var(--admin-border)] bg-[var(--admin-surface)]">
<div className="flex items-center gap-2 border-b border-[var(--admin-border)] px-3 py-2.5">
<FolderTree size={14} className="text-[var(--admin-accent)]" />
<span className="text-xs font-bold uppercase tracking-wider text-[var(--admin-text-muted)]">
Catalog
</span>
</div>
<div className="min-h-0 flex-1 overflow-y-auto p-1.5">
{visible.length === 0 ? (
<p className="px-2 py-4 text-xs text-[var(--admin-text-muted)]">
No catalog pages found.
</p>
) : (
visible.map((node) => {
const isRoot = node.parentId <= 0;
const hasChildren = node.childCount > 0;
const isCollapsed = collapsed.has(node.id);
return (
<button
key={node.id}
type="button"
onClick={() => {
if (hasChildren) toggle(node.id);
onSelect(selectedId === node.id ? null : node.id);
}}
className={cn(
"group flex w-full cursor-pointer items-center gap-1.5 rounded-md px-1.5 py-1.5 text-left text-xs transition-colors outline-none focus-visible:ring-2 focus-visible:ring-[var(--admin-accent)]",
selectedId === node.id
? "bg-[var(--admin-accent)]/15 text-[var(--admin-text)]"
: "text-[var(--admin-text-muted)] hover:bg-[var(--admin-accent)]/10 hover:text-[var(--admin-text)]",
)}
style={{ paddingLeft: `${6 + node.depth * 12}px` }}
>
{hasChildren ? (
isCollapsed ? (
<ChevronRight size={12} className="shrink-0" />
) : (
<ChevronDown size={12} className="shrink-0" />
)
) : (
<span className="w-3 shrink-0" />
)}
<span className="min-w-0 flex-1 truncate">
{isRoot ? (
<span className="font-semibold">{node.caption}</span>
) : (
node.caption
)}
</span>
<span className="shrink-0 rounded-full bg-[var(--admin-accent)]/10 px-1.5 text-[0.6rem] font-semibold text-[var(--admin-accent)]">
{node.itemCount}
</span>
</button>
);
})
)}
</div>
</aside>
);
}
@@ -0,0 +1,22 @@
"use client";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils";
export function CheckboxDot({ checked }: { checked: boolean }) {
return (
<span
className={cn(
"grid h-3.5 w-3.5 place-items-center rounded border transition-colors",
checked
? "border-[var(--admin-accent)] bg-[var(--admin-accent)]"
: "border-[var(--admin-border)] bg-transparent",
)}
>
{checked ? (
<Check size={10} className="text-[var(--admin-accent-foreground)]" />
) : null}
</span>
);
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,40 @@
import type { SyntheticEvent } from "react";
import type { FurniItem } from "./studio-types";
export function getFurniImageUrl(item: FurniItem) {
if (item.iconUrl) return item.iconUrl;
const base = item.classname.includes("*")
? item.classname.substring(0, item.classname.indexOf("*"))
: item.classname;
return `https://images.habbo.com/dcr/hof_furni/${item.revision}/${encodeURIComponent(base)}_icon.png`;
}
export function getFurniIconUrl(classname: string, revision: number) {
const iconName = classname.replace(/\*/g, "_");
return `https://images.habbo.com/dcr/hof_furni/${revision}/${encodeURIComponent(iconName)}_icon.png`;
}
export function handleImgError(
e: SyntheticEvent<HTMLImageElement>,
item: FurniItem,
) {
const img = e.target as HTMLImageElement;
const iconName = item.classname.replace(/\*/g, "_");
if (!img.dataset.fallback) {
img.dataset.fallback = "1";
img.src = getFurniIconUrl(item.classname, item.revision);
} else if (img.dataset.fallback === "1") {
img.dataset.fallback = "2";
img.src = `/swf/dcr/hof_furni/icons/${encodeURIComponent(iconName)}_icon.png`;
} else {
img.style.opacity = "0.3";
img.src =
'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="%23666" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg>';
}
}
export const TYPE_LABELS: Record<string, string> = {
flooritem: "Floor",
wallitem: "Wall",
};
@@ -0,0 +1,53 @@
export interface FurniItem {
id: number;
classname: string;
type: string;
name: string;
description: string;
revision: number;
category: string;
alreadyImported: boolean;
nitroExists: boolean;
/** Custom-source preview icon (retro hotel CDN). Absent for official Habbo items. */
iconUrl?: string;
}
export interface Meta {
currentPage: number;
lastPage: number;
total: number;
perPage: number;
}
export interface FurniStats {
totalInDb: number;
inCatalog: number;
notInCatalog: number;
missingNitro: number;
}
export interface BatchItemStatus {
classname: string;
status:
| "pending"
| "started"
| "downloading"
| "converting"
| "writing_db"
| "done"
| "failed";
message?: string;
warnings?: string[];
itemId?: number;
}
/** Post-import verification results reported by the import API. */
export interface VerificationSummary {
offerIdsFixed: number;
furniDataFixedIds: number;
furniDataMissing: number;
furniDataConflicts: number;
nitrosSynced: string[];
iconsSynced: string[];
ownershipFixed: string[];
}