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,
|
type RecentFurniResyncResult,
|
||||||
requestRecentFurniResync,
|
requestRecentFurniResync,
|
||||||
} from "@/lib/admin/recent-furni-resync";
|
} from "@/lib/admin/recent-furni-resync";
|
||||||
|
import {
|
||||||
|
requestVerifyInteractions,
|
||||||
|
type VerifyInteractionsResult,
|
||||||
|
} from "@/lib/admin/verify-interactions";
|
||||||
import { adminFetch } from "@/lib/admin-fetch";
|
import { adminFetch } from "@/lib/admin-fetch";
|
||||||
import type { FurniImportSource } from "@/lib/habbo-gamedata-hotel";
|
import type { FurniImportSource } from "@/lib/habbo-gamedata-hotel";
|
||||||
import {
|
import {
|
||||||
@@ -142,6 +146,9 @@ export function ImportFurniClient({ source }: { source: FurniImportSource }) {
|
|||||||
const [recentResyncing, setRecentResyncing] = useState(false);
|
const [recentResyncing, setRecentResyncing] = useState(false);
|
||||||
const [recentResyncResult, setRecentResyncResult] =
|
const [recentResyncResult, setRecentResyncResult] =
|
||||||
useState<RecentFurniResyncResult | null>(null);
|
useState<RecentFurniResyncResult | null>(null);
|
||||||
|
const [verifyingInteractions, setVerifyingInteractions] = useState(false);
|
||||||
|
const [verifyResult, setVerifyResult] =
|
||||||
|
useState<VerifyInteractionsResult | null>(null);
|
||||||
const searchRef = useRef<HTMLInputElement>(null);
|
const searchRef = useRef<HTMLInputElement>(null);
|
||||||
const batchAbortRef = useRef<AbortController | null>(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() {
|
async function applyReorganize() {
|
||||||
setReorgPreview(null);
|
setReorgPreview(null);
|
||||||
setReorganizing(true);
|
setReorganizing(true);
|
||||||
@@ -957,6 +993,50 @@ export function ImportFurniClient({ source }: { source: FurniImportSource }) {
|
|||||||
</div>
|
</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 */}
|
{/* Batch Summary */}
|
||||||
{batchSummary && (
|
{batchSummary && (
|
||||||
<div className="rounded-lg border p-4 space-y-3 bg-card">
|
<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" />
|
<FolderSync className="w-4 h-4 mr-2" />
|
||||||
{reorganizing ? "Reorganizing..." : "Reorganize Catalog"}
|
{reorganizing ? "Reorganizing..." : "Reorganize Catalog"}
|
||||||
</DropdownMenuItem>
|
</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 />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={cleanSwf} disabled={cleaningSwf}>
|
<DropdownMenuItem onClick={cleanSwf} disabled={cleaningSwf}>
|
||||||
<Trash2 className="w-4 h-4 mr-2" />
|
<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