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.
This commit is contained in:
1 parent
0b1d3b24c2
commit
2b9a015afb
3 files changed
+151
No files matched your search
@@ -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<RecentFurniResyncResult | null>(null);
|
||||
const [verifyingInteractions, setVerifyingInteractions] = useState(false);
|
||||
const [verifyResult, setVerifyResult] =
|
||||
useState<VerifyInteractionsResult | null>(null);
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
const batchAbortRef = useRef<AbortController | null>(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 }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{verifyResult && (
|
||||
<div className="rounded-lg border bg-card p-4 space-y-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h3 className="text-sm font-semibold">Interaction Verification</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setVerifyResult(null)}
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-4 text-sm">
|
||||
<span>{verifyResult.checked.toLocaleString()} checked</span>
|
||||
<span
|
||||
className={
|
||||
verifyResult.fixed > 0
|
||||
? "text-[var(--admin-warning)]"
|
||||
: "text-[var(--admin-success)]"
|
||||
}
|
||||
>
|
||||
{verifyResult.fixed} fixed
|
||||
</span>
|
||||
<span className="text-muted-foreground">
|
||||
{verifyResult.skipped.toLocaleString()} skipped (no real data)
|
||||
</span>
|
||||
</div>
|
||||
{verifyResult.errors.length > 0 && (
|
||||
<details className="text-xs">
|
||||
<summary className="cursor-pointer text-muted-foreground">
|
||||
View errors
|
||||
</summary>
|
||||
<div className="mt-2 max-h-48 space-y-1 overflow-y-auto">
|
||||
{verifyResult.errors.map((error) => (
|
||||
<p key={error}>
|
||||
<span className="font-mono">{error}</span>
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Batch Summary */}
|
||||
{batchSummary && (
|
||||
<div className="rounded-lg border p-4 space-y-3 bg-card">
|
||||
@@ -1471,6 +1551,19 @@ export function ImportFurniClient({ source }: { source: FurniImportSource }) {
|
||||
<FolderSync className="w-4 h-4 mr-2" />
|
||||
{reorganizing ? "Reorganizing..." : "Reorganize Catalog"}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={verifyAllInteractions}
|
||||
disabled={verifyingInteractions}
|
||||
>
|
||||
{verifyingInteractions ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<CheckCheck className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{verifyingInteractions
|
||||
? "Verifying interactions..."
|
||||
: "Verify & fix all interactions"}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={cleanSwf} disabled={cleaningSwf}>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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<Response>;
|
||||
|
||||
export async function requestVerifyInteractions(
|
||||
fetcher: AdminFetcher,
|
||||
): Promise<VerifyInteractionsResult> {
|
||||
const response = await fetcher(VERIFY_INTERACTIONS_URL, { method: "POST" });
|
||||
const payload = (await response.json()) as Record<string, unknown>;
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user