"use client"; import { useCallback, useEffect, useState } from "react"; import { Nav, useAuth } from "@/components/Nav"; interface Dashboard { nodesOnline: number; nodesOffline: number; totalStorage: string; freeStorage: string; movies: number; series: number; episodes: number; availableMovies?: number; availableSeries?: number; mediaFiles?: number; availableFiles?: number; moviesMissingImdb?: number; moviesWithImdbStreams?: number; seriesMissingImdb?: number; seriesWithImdb?: number; scannedFiles?: number; activeStreams: number; currentBandwidth: string; } function formatBytes(bytes: string | number | null | undefined): string { if (bytes === null || bytes === undefined || bytes === "") return "0 B"; let n: bigint; try { n = typeof bytes === "bigint" ? bytes : BigInt(String(bytes)); } catch { return "0 B"; } const units = ["B", "KB", "MB", "GB", "TB"]; let val = Number(n); if (!Number.isFinite(val)) return "0 B"; let i = 0; while (val >= 1024 && i < units.length - 1) { val /= 1024; i++; } return `${val.toFixed(1)} ${units[i]}`; } export default function DashboardPage() { useAuth(); const [data, setData] = useState(null); const [error, setError] = useState(""); const [dedupeMsg, setDedupeMsg] = useState(""); const [imdbMsg, setImdbMsg] = useState(""); const [repairMsg, setRepairMsg] = useState(""); const [repairBusy, setRepairBusy] = useState(false); const [dedupeBusy, setDedupeBusy] = useState(false); const [imdbBusy, setImdbBusy] = useState(false); const load = useCallback(() => { fetch("/api/v1/admin/dashboard", { credentials: "include" }) .then(async (r) => { const json = await r.json(); if (!r.ok) { throw new Error(json.error?.message ?? `HTTP ${r.status}`); } setData(json); }) .catch((err) => { console.error(err); setError(err instanceof Error ? err.message : "Laden mislukt"); }); }, []); useEffect(() => { load(); }, [load]); async function repairLibrary() { if ( !confirm( "Bibliotheek herstellen na pad-migratie?\n\n• Duplicaten samenvoegen naar IMDb/TMDB-records\n• Dode (unavailable) bestanden verwijderen\n• Dubbele file-rijen opruimen\n• Ontbrekende IMDb-IDs backfillen (max ~400 per run)\n\nDuurt meestal 1–3 minuten. Herhaal indien nodig." ) ) { return; } setRepairBusy(true); setRepairMsg("Bezig — duplicaten en dode files opruimen…"); try { const res = await fetch("/api/v1/admin/library/repair", { method: "POST", credentials: "include", }); const json = await res.json().catch(() => ({})); if (!res.ok) { setRepairMsg(json.error?.message ?? `Mislukt (HTTP ${res.status})`); return; } const imdb = json.imdb ?? {}; const imdbRemaining = (imdb.movies?.remaining ?? 0) + (imdb.series?.remaining ?? 0); setRepairMsg( `Klaar: ${json.moviesMerged ?? 0} film-merges, ${json.seriesMerged ?? 0} serie-merges, ${json.staleFilesRemoved ?? 0} dode files, ${json.duplicateFilesRemoved ?? 0} dubbele files · IMDb +${imdb.movies?.updated ?? 0} films, +${imdb.series?.updated ?? 0} series${imdbRemaining > 0 ? ` · nog ${imdbRemaining} IMDb open (herhaal of gebruik backfill-knop)` : ""}. Herlaad daarna de Stremio-addon.` ); load(); } catch (err) { setRepairMsg( err instanceof Error ? `Verbinding mislukt: ${err.message}` : "Verbinding mislukt — timeout of server offline?" ); } finally { setRepairBusy(false); } } async function backfillImdb() { if ( !confirm( "IMDb-IDs ophalen via TMDB voor films en series die die nog missen? Dit gebeurt in batches (duurt even bij grote bibliotheken)." ) ) { return; } setImdbBusy(true); setImdbMsg("Batch 1 bezig…"); let batch = 0; let totalMoviesUpdated = 0; let totalSeriesUpdated = 0; let prevRemaining = Number.POSITIVE_INFINITY; try { while (batch < 300) { batch += 1; setImdbMsg(`Batch ${batch} bezig…`); const res = await fetch("/api/v1/admin/library/backfill-imdb", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ limit: 100 }), }); const json = await res.json().catch(() => ({})); if (!res.ok) { setImdbMsg(json.error?.message ?? "Mislukt"); return; } const movies = json.movies ?? {}; const series = json.series ?? {}; const checked = (movies.checked ?? 0) + (series.checked ?? 0); const updated = (movies.updated ?? 0) + (series.updated ?? 0); totalMoviesUpdated += movies.updated ?? 0; totalSeriesUpdated += series.updated ?? 0; const remaining = (movies.remaining ?? 0) + (series.remaining ?? 0); if (remaining === 0) { setImdbMsg( `Klaar: films +${totalMoviesUpdated}, series +${totalSeriesUpdated} · alles ingevuld` ); load(); return; } if (checked === 0) { setImdbMsg( `Klaar: films +${totalMoviesUpdated}, series +${totalSeriesUpdated} · nog ${remaining} open (geen IMDb op TMDB)` ); load(); return; } if (remaining >= prevRemaining && updated === 0) { setImdbMsg( `Gestopt na batch ${batch}: films +${totalMoviesUpdated}, series +${totalSeriesUpdated} · nog ${remaining} zonder IMDb (niet beschikbaar op TMDB)` ); load(); return; } prevRemaining = remaining; setImdbMsg( `Batch ${batch}: +${movies.updated ?? 0} films, +${series.updated ?? 0} series · nog ${remaining} open…` ); } setImdbMsg( `Batchlimiet bereikt: films +${totalMoviesUpdated}, series +${totalSeriesUpdated} · druk opnieuw op de knop om verder te gaan` ); load(); } catch (err) { setImdbMsg(err instanceof Error ? `Mislukt: ${err.message}` : "Mislukt"); } finally { setImdbBusy(false); } } async function dedupe() { if ( !confirm( "Duplicaten samenvoegen? Titels met dezelfde naam worden samengevoegd (TMDB-versie wint)." ) ) { return; } setDedupeBusy(true); setDedupeMsg("Bezig…"); try { const res = await fetch("/api/v1/admin/library/dedupe", { method: "POST", credentials: "include", }); const json = await res.json().catch(() => ({})); if (!res.ok) { setDedupeMsg(json.error?.message ?? "Mislukt"); return; } setDedupeMsg( `Klaar: ${json.moviesMerged} film-duplicaten, ${json.seriesMerged} serie-duplicaten, ${json.emptied} lege records opgeruimd.` ); load(); } catch (err) { setDedupeMsg(err instanceof Error ? `Mislukt: ${err.message}` : "Mislukt"); } finally { setDedupeBusy(false); } } return ( <>