"use client"; import { AlertCircle, CheckCircle2, ClipboardList, ExternalLink, Loader2, SearchX, } from "lucide-react"; import { useCallback, useRef, useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { adminFetch } from "@/lib/admin-fetch"; interface AuditIssue { type: string; severity: "error" | "warning" | "info"; classname?: string; itemId?: number; catalogId?: number; message: string; sourceName?: string; } interface MissingFromSource { sourceName: string; sourceId: string; classname: string; itemName: string; } interface AuditSummary { totalItemsBase: number; totalCatalogItems: number; missingCatalog: number; orphanedCatalog: number; missingNitro: number; missingIcon: number; duplicates: number; missingFromSourcesTotal: number; } interface AuditEvent { type: string; message?: string; current?: number; total?: number; section?: string; issues?: AuditIssue[]; missingFromSources?: MissingFromSource[]; summary?: AuditSummary; } const TYPE_LABELS: Record = { missing_catalog: "No catalog entry", orphaned_catalog: "Orphaned catalog reference", missing_nitro: "Missing .nitro file", missing_icon: "Missing icon file", duplicate_classname: "Duplicate classname", source_fetch_failed: "Source fetch failed", }; export function AuditClient() { const [loading, setLoading] = useState(false); const [progress, setProgress] = useState(""); const [activeTab, setActiveTab] = useState<"issues" | "sources">("issues"); const [issues, setIssues] = useState([]); const [missingFromSources, setMissingFromSources] = useState< MissingFromSource[] >([]); const [summary, setSummary] = useState(null); const [checkProgress, setCheckProgress] = useState<{ section: string; current: number; total: number; } | null>(null); const abortRef = useRef(null); const runAudit = useCallback(async () => { setLoading(true); setProgress("Starting audit…"); setIssues([]); setMissingFromSources([]); setSummary(null); setCheckProgress(null); setActiveTab("issues"); const abort = new AbortController(); abortRef.current = abort; try { const res = await adminFetch("/api/admin/import/audit", { method: "POST", signal: abort.signal, }); if (!res.body) { toast.error("No response stream"); setLoading(false); return; } const reader = res.body.getReader(); const decoder = new TextDecoder(); let buf = ""; while (true) { const { value, done } = await reader.read(); if (done) break; buf += decoder.decode(value, { stream: true }); const parts = buf.split("\n\n"); buf = parts.pop() ?? ""; for (const part of parts) { if (!part.startsWith("data: ")) continue; try { const evt = JSON.parse(part.slice(6)) as AuditEvent; if (evt.type === "progress") { setProgress(evt.message ?? ""); } if (evt.type === "items_loaded") { setProgress(`Loaded ${evt.total} items_base entries`); } if (evt.type === "catalog_loaded") { setProgress(`Loaded ${evt.total} catalog_items entries`); } if (evt.type === "checking") { setProgress(`Checking ${evt.section}…`); if (evt.total) { setCheckProgress({ section: evt.section ?? "", current: evt.current ?? 0, total: evt.total, }); } } if (evt.type === "batch_complete") { setIssues(evt.issues ?? []); setMissingFromSources(evt.missingFromSources ?? []); if (evt.summary) setSummary(evt.summary); setCheckProgress(null); setProgress("Done"); if ( evt.issues && evt.issues.filter((i) => i.severity === "error").length > 0 ) { toast.error( `${evt.issues.filter((i) => i.severity === "error").length} error(s) found`, ); } else if ( evt.issues && evt.issues.filter((i) => i.severity === "warning").length > 0 ) { toast.warning("Warnings found"); } else { toast.success("No issues found"); } } if (evt.type === "error") { setProgress(`ERROR: ${evt.message ?? ""}`); toast.error(evt.message ?? "Audit failed"); } } catch { // skip parse errors } } } } catch (err) { if ((err as Error).name !== "AbortError") { toast.error("Audit request failed"); } } finally { setLoading(false); abortRef.current = null; } }, []); const errors = issues.filter((i) => i.severity === "error"); const warnings = issues.filter((i) => i.severity === "warning"); return (
{!summary && ( )} {loading && checkProgress && (
{checkProgress.section}: {checkProgress.current} /{" "} {checkProgress.total}
)} {summary && ( <> {/* Summary cards */}
{summary.totalItemsBase}
items_base entries
{summary.totalCatalogItems}
catalog_items entries
0 ? "border-[var(--admin-error-border)] bg-[var(--admin-error-subtle)]" : "bg-card"}`} >
0 ? "text-[var(--admin-error)]" : ""}`} > {errors.length}
Errors
0 ? "border-[var(--admin-warning-border)] bg-[var(--admin-warning-subtle)]" : "bg-card"}`} >
0 ? "text-[var(--admin-warning)]" : ""}`} > {warnings.length}
Warnings
{/* Quick stats */}
{summary.missingCatalog > 0 ? ( ) : ( )} {summary.missingCatalog} items without catalog entry
{summary.orphanedCatalog > 0 ? ( ) : ( )} {summary.orphanedCatalog} orphaned catalog references
{summary.missingNitro > 0 ? ( ) : ( )} {summary.missingNitro} items without .nitro
{summary.missingIcon > 0 ? ( ) : ( )} {summary.missingIcon} items without icon
{summary.duplicates > 0 ? ( ) : ( )} {summary.duplicates} duplicate classnames
{summary.missingFromSourcesTotal > 0 ? ( ) : ( )} {summary.missingFromSourcesTotal} items missing from clone sources
{/* Tabs */} {issues.length > 0 || missingFromSources.length > 0 ? (
) : null} {/* Issues list */} {activeTab === "issues" && issues.length > 0 && (
{issues.map((issue) => (
{TYPE_LABELS[issue.type] || issue.type} {issue.classname && ( {issue.classname} )} {issue.sourceName && ( source: {issue.sourceName} )}

{issue.message}

))}
)} {/* Missing from sources list */} {activeTab === "sources" && missingFromSources.length > 0 && (
{missingFromSources.map((item) => (
{item.classname} — {item.itemName} in {item.sourceName}
))}
)} {activeTab === "issues" && issues.length === 0 && missingFromSources.length === 0 && (

No issues found

Your catalog is clean!

)} )} {!loading && !summary && (

Run a full audit to check for missing catalog entries, broken references, missing files, and items available from clone sources.

)}
); }