From 2b9a015afb12ec1bf3929ea9964718070d0296ea Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 19 Aug 2026 21:11:38 +0200 Subject: [PATCH] feat: add manual 'verify & fix all interactions' admin tool New POST /api/admin/import/furni/verify re-runs the full interaction verification over items_base at any time, plus a Tools dropdown entry in the furni import admin. It corrects interaction_type, interaction_modes_count and allow_sit/lay/walk against real furniture data (furnidata flags + .nitro animation states); items without real data are skipped and existing emulator handler types are preserved. --- .../import/furni/import-furni-client.tsx | 93 +++++++++++++++++++ .../api/admin/import/furni/verify/route.ts | 18 ++++ src/lib/admin/verify-interactions.ts | 40 ++++++++ 3 files changed, 151 insertions(+) create mode 100644 src/app/api/admin/import/furni/verify/route.ts create mode 100644 src/lib/admin/verify-interactions.ts diff --git a/src/app/admin/import/furni/import-furni-client.tsx b/src/app/admin/import/furni/import-furni-client.tsx index e88d1c4073..c18618ec53 100644 --- a/src/app/admin/import/furni/import-furni-client.tsx +++ b/src/app/admin/import/furni/import-furni-client.tsx @@ -58,6 +58,10 @@ import { type RecentFurniResyncResult, requestRecentFurniResync, } from "@/lib/admin/recent-furni-resync"; +import { + requestVerifyInteractions, + type VerifyInteractionsResult, +} from "@/lib/admin/verify-interactions"; import { adminFetch } from "@/lib/admin-fetch"; import type { FurniImportSource } from "@/lib/habbo-gamedata-hotel"; import { @@ -142,6 +146,9 @@ export function ImportFurniClient({ source }: { source: FurniImportSource }) { const [recentResyncing, setRecentResyncing] = useState(false); const [recentResyncResult, setRecentResyncResult] = useState(null); + const [verifyingInteractions, setVerifyingInteractions] = useState(false); + const [verifyResult, setVerifyResult] = + useState(null); const searchRef = useRef(null); const batchAbortRef = useRef(null); @@ -739,6 +746,35 @@ export function ImportFurniClient({ source }: { source: FurniImportSource }) { } } + async function verifyAllInteractions() { + setVerifyingInteractions(true); + setVerifyResult(null); + try { + const result = await requestVerifyInteractions(adminFetch); + setVerifyResult(result); + if (result.fixed === 0 && result.errors.length === 0) { + toast.success( + `Checked ${result.checked.toLocaleString()} items — all consistent`, + ); + } else if (result.errors.length > 0) { + toast.warning( + `Verified ${result.checked.toLocaleString()} items, ${result.errors.length} errors`, + ); + } else { + toast.success(`Fixed ${result.fixed} items from real furniture data`); + } + fetchStats(); + } catch (error) { + toast.error( + error instanceof Error + ? error.message + : "Interaction verification failed", + ); + } finally { + setVerifyingInteractions(false); + } + } + async function applyReorganize() { setReorgPreview(null); setReorganizing(true); @@ -957,6 +993,50 @@ export function ImportFurniClient({ source }: { source: FurniImportSource }) { )} + {verifyResult && ( +
+
+

Interaction Verification

+ +
+
+ {verifyResult.checked.toLocaleString()} checked + 0 + ? "text-[var(--admin-warning)]" + : "text-[var(--admin-success)]" + } + > + {verifyResult.fixed} fixed + + + {verifyResult.skipped.toLocaleString()} skipped (no real data) + +
+ {verifyResult.errors.length > 0 && ( +
+ + View errors + +
+ {verifyResult.errors.map((error) => ( +

+ {error} +

+ ))} +
+
+ )} +
+ )} + {/* Batch Summary */} {batchSummary && (
@@ -1471,6 +1551,19 @@ export function ImportFurniClient({ source }: { source: FurniImportSource }) { {reorganizing ? "Reorganizing..." : "Reorganize Catalog"} + + {verifyingInteractions ? ( + + ) : ( + + )} + {verifyingInteractions + ? "Verifying interactions..." + : "Verify & fix all interactions"} + diff --git a/src/app/api/admin/import/furni/verify/route.ts b/src/app/api/admin/import/furni/verify/route.ts new file mode 100644 index 0000000000..fc6e508ae2 --- /dev/null +++ b/src/app/api/admin/import/furni/verify/route.ts @@ -0,0 +1,18 @@ +import { withAdmin } from "@/lib/api-handler"; +import { apiOk } from "@/lib/api-response"; +import { PERMS } from "@/lib/permissions"; +import { verifyAndFixInteractionModesCount } from "@/lib/services/furni-import"; + +/** + * POST /api/admin/import/furni/verify + * + * Re-runs the interaction verification over the ENTIRE items_base table, + * correcting interaction_type, interaction_modes_count and allow_sit/ + * allow_lay/allow_walk against the real furniture data (furnidata flags + + * .nitro animation states). Only items with authoritative data are touched — + * no keyword guesses, so no false positives. + */ +export const POST = withAdmin({ permission: PERMS.ASSETS_IMPORT }, async () => { + const result = await verifyAndFixInteractionModesCount(); + return apiOk(result); +}); diff --git a/src/lib/admin/verify-interactions.ts b/src/lib/admin/verify-interactions.ts new file mode 100644 index 0000000000..98311d9b88 --- /dev/null +++ b/src/lib/admin/verify-interactions.ts @@ -0,0 +1,40 @@ +export const VERIFY_INTERACTIONS_URL = "/api/admin/import/furni/verify"; + +export interface VerifyInteractionsResult { + fixed: number; + checked: number; + skipped: number; + errors: string[]; +} + +type AdminFetcher = ( + input: RequestInfo | URL, + init?: RequestInit, +) => Promise; + +export async function requestVerifyInteractions( + fetcher: AdminFetcher, +): Promise { + const response = await fetcher(VERIFY_INTERACTIONS_URL, { method: "POST" }); + const payload = (await response.json()) as Record; + if (!response.ok) { + throw new Error( + typeof payload.error === "string" + ? payload.error + : "Interaction verification failed", + ); + } + + const errors = Array.isArray(payload.errors) + ? payload.errors.filter( + (value): value is string => typeof value === "string", + ) + : []; + + return { + fixed: Number(payload.fixed) || 0, + checked: Number(payload.checked) || 0, + skipped: Number(payload.skipped) || 0, + errors, + }; +}