From 804daeffcaa0725f51042680dfeb97bbcf6e8528 Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 21 Jul 2026 14:53:26 +0200 Subject: [PATCH] feat: add .nitro upload + sync-all + repair-icons features - Upload .nitro bundles directly with full DB, catalog, and furnidata integration - Generate SQL migration files on upload (optional) - Auto-sync missing furniture from all configured clone sources (SSE batch) - Repair missing icons by extracting from local .nitro or downloading from sources - All behind ASSETS_IMPORT permission --- src/app/admin/import/furni/upload/page.tsx | 26 ++ .../furni/upload/upload-furni-client.tsx | 331 ++++++++++++++++++ src/app/admin/import/page.tsx | 21 ++ src/app/admin/import/repair-icons/page.tsx | 26 ++ .../repair-icons/repair-icons-client.tsx | 159 +++++++++ src/app/admin/import/sync/page.tsx | 27 ++ src/app/admin/import/sync/sync-all-client.tsx | 176 ++++++++++ .../api/admin/import/clone/sync-all/route.ts | 153 ++++++++ .../admin/import/furni/repair-icons/route.ts | 33 ++ .../api/admin/import/furni/upload/route.ts | 92 +++++ src/lib/services/repair-icons.ts | 126 +++++++ src/lib/services/upload-import.ts | 307 ++++++++++++++++ src/messages/en.json | 8 +- 13 files changed, 1484 insertions(+), 1 deletion(-) create mode 100644 src/app/admin/import/furni/upload/page.tsx create mode 100644 src/app/admin/import/furni/upload/upload-furni-client.tsx create mode 100644 src/app/admin/import/repair-icons/page.tsx create mode 100644 src/app/admin/import/repair-icons/repair-icons-client.tsx create mode 100644 src/app/admin/import/sync/page.tsx create mode 100644 src/app/admin/import/sync/sync-all-client.tsx create mode 100644 src/app/api/admin/import/clone/sync-all/route.ts create mode 100644 src/app/api/admin/import/furni/repair-icons/route.ts create mode 100644 src/app/api/admin/import/furni/upload/route.ts create mode 100644 src/lib/services/repair-icons.ts create mode 100644 src/lib/services/upload-import.ts diff --git a/src/app/admin/import/furni/upload/page.tsx b/src/app/admin/import/furni/upload/page.tsx new file mode 100644 index 00000000..915d13e8 --- /dev/null +++ b/src/app/admin/import/furni/upload/page.tsx @@ -0,0 +1,26 @@ +import { redirect } from "next/navigation"; +import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; +import { UploadFurniClient } from "./upload-furni-client"; + +export default async function UploadFurniPage() { + const { session, permissions } = await getAdminContext(); + if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) { + redirect("/admin"); + } + + return ( +
+
+

+ Upload .nitro Furniture +

+

+ Upload a .nitro bundle and optional icon to import furniture into the + hotel. The database entry, catalog item, and FurnitureData.json will + be updated automatically. +

+
+ +
+ ); +} diff --git a/src/app/admin/import/furni/upload/upload-furni-client.tsx b/src/app/admin/import/furni/upload/upload-furni-client.tsx new file mode 100644 index 00000000..483960be --- /dev/null +++ b/src/app/admin/import/furni/upload/upload-furni-client.tsx @@ -0,0 +1,331 @@ +"use client"; + +import { AlertCircle, Check, Loader2, Upload } from "lucide-react"; +import { useRef, useState } from "react"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { adminFetch } from "@/lib/admin-fetch"; + +interface UploadResult { + itemId: number; + catalogItemId: number | null; + classname: string; + warnings: string[]; +} + +export function UploadFurniClient() { + const nitroRef = useRef(null); + const iconRef = useRef(null); + + const [classname, setClassname] = useState(""); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [itemType, setItemType] = useState<"s" | "i">("s"); + const [xdim, setXdim] = useState(1); + const [ydim, setYdim] = useState(1); + const [canstandon, setCanstandon] = useState(false); + const [cansiton, setCansiton] = useState(false); + const [canlayon, setCanlayon] = useState(false); + const [interactionType, setInteractionType] = useState("default"); + const [customparams, setCustomparams] = useState(""); + + const [nitroFile, setNitroFile] = useState(null); + const [iconFile, setIconFile] = useState(null); + + const [uploading, setUploading] = useState(false); + const [result, setResult] = useState(null); + const [error, setError] = useState(""); + + function resetForm() { + setClassname(""); + setName(""); + setDescription(""); + setItemType("s"); + setXdim(1); + setYdim(1); + setCanstandon(false); + setCansiton(false); + setCanlayon(false); + setInteractionType("default"); + setCustomparams(""); + setNitroFile(null); + setIconFile(null); + setResult(null); + setError(""); + if (nitroRef.current) nitroRef.current.value = ""; + if (iconRef.current) iconRef.current.value = ""; + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setUploading(true); + setError(""); + setResult(null); + + try { + const fd = new FormData(); + fd.set("classname", classname.trim()); + fd.set("name", name.trim()); + fd.set("description", description.trim()); + fd.set("itemType", itemType); + fd.set("xdim", String(xdim)); + fd.set("ydim", String(ydim)); + fd.set("canstandon", String(canstandon)); + fd.set("cansiton", String(cansiton)); + fd.set("canlayon", String(canlayon)); + fd.set("interactionType", interactionType.trim()); + fd.set("customparams", customparams.trim()); + if (nitroFile) fd.set("nitro", nitroFile); + if (iconFile) fd.set("icon", iconFile); + + const res = await adminFetch("/api/admin/import/furni/upload", { + method: "POST", + body: fd, + }); + const data = await res.json(); + + if (!res.ok) { + setError(data.error || "Upload failed"); + toast.error(data.error || "Upload failed"); + return; + } + + setResult(data); + toast.success(`${data.classname} imported successfully`); + } catch { + const msg = "Network error"; + setError(msg); + toast.error(msg); + } finally { + setUploading(false); + } + } + + return ( +
+ {!result ? ( +
+
+
Furniture Details
+ +
+
+ + setClassname(e.target.value)} + required + /> +
+
+ + setName(e.target.value)} + required + /> +
+
+ +
+ +