"use client"; import { FormEvent, useCallback, useEffect, useRef, useState } from "react"; import Link from "next/link"; import { Nav, useAuth } from "@/components/Nav"; type TweakMeta = { key: string; label: string; hint: string; kind: "number" | "boolean"; unit?: string; }; type OdidoTweaks = Record; type OdidoAccount = { id: string; label: string; username: string; hasPin: boolean; enabled: boolean; channelCount: number; healthStatus: "ok" | "dead" | "unknown"; healthCheckedAt: string | null; healthError: string | null; }; type OdidoSettings = { configured: boolean; enabled: boolean; hasWvd: boolean; wvdPath: string; wvdUploadedAt: string | null; lastSyncAt: string | null; lastSyncSummary: Record | null; lastMpdRefreshAt: string | null; lastMpdRefreshSummary: Record | null; accounts: OdidoAccount[]; accountCount: number; channelOwnerCount: number; tweaks: OdidoTweaks; tweakDefaults: OdidoTweaks; tweakMeta: TweakMeta[]; }; function readFileAsBase64(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { const result = String(reader.result ?? ""); const comma = result.indexOf(","); resolve(comma >= 0 ? result.slice(comma + 1) : result); }; reader.onerror = () => reject(new Error("Bestand lezen mislukt")); reader.readAsDataURL(file); }); } export default function ScriptsPage() { useAuth(); const [odido, setOdido] = useState(null); const [wvdPath, setWvdPath] = useState(""); const [enabled, setEnabled] = useState(true); const [tweaks, setTweaks] = useState({}); const [showTweaks, setShowTweaks] = useState(false); const [busy, setBusy] = useState(false); const [msg, setMsg] = useState(null); const [err, setErr] = useState(null); const wvdInputRef = useRef(null); const [newLabel, setNewLabel] = useState(""); const [newUsername, setNewUsername] = useState(""); const [newPin, setNewPin] = useState(""); const applySettings = useCallback((d: OdidoSettings) => { setOdido(d); setWvdPath(d.wvdPath ?? ""); setEnabled(d.enabled !== false); setTweaks(d.tweaks ?? d.tweakDefaults ?? {}); }, []); const load = useCallback(async () => { const r = await fetch("/api/v1/admin/scripts/odido", { credentials: "include" }); const d = await r.json(); if (!r.ok) throw new Error(d.error?.message ?? "Odido-instellingen laden mislukt"); applySettings(d); }, [applySettings]); useEffect(() => { void load().catch((e) => setErr(String(e.message ?? e))); }, [load]); async function saveOdido(e: FormEvent) { e.preventDefault(); setBusy(true); setErr(null); setMsg(null); try { const r = await fetch("/api/v1/admin/scripts/odido", { method: "PUT", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ wvdPath: wvdPath.trim(), enabled, tweaks }), }); const d = await r.json(); if (!r.ok) throw new Error(d.error?.message ?? "Opslaan mislukt"); applySettings(d); setMsg("Odido-instellingen opgeslagen"); } catch (e) { setErr(String((e as Error).message ?? e)); } finally { setBusy(false); } } async function addAccount(e: FormEvent) { e.preventDefault(); setBusy(true); setErr(null); setMsg(null); try { const r = await fetch("/api/v1/admin/scripts/odido/accounts", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ label: newLabel.trim() || undefined, username: newUsername.trim(), pin: newPin.trim(), enabled: true, }), }); const d = await r.json(); if (!r.ok) throw new Error(d.error?.message ?? "Account toevoegen mislukt"); applySettings(d); setNewLabel(""); setNewUsername(""); setNewPin(""); setMsg("Odido-account toegevoegd"); } catch (e) { setErr(String((e as Error).message ?? e)); } finally { setBusy(false); } } async function toggleAccount(account: OdidoAccount) { setBusy(true); setErr(null); try { const r = await fetch(`/api/v1/admin/scripts/odido/accounts/${account.id}`, { method: "PATCH", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ enabled: !account.enabled }), }); const d = await r.json(); if (!r.ok) throw new Error(d.error?.message ?? "Account bijwerken mislukt"); applySettings(d); } catch (e) { setErr(String((e as Error).message ?? e)); } finally { setBusy(false); } } async function removeAccount(account: OdidoAccount) { if (!confirm(`Account “${account.label || account.username}” verwijderen?`)) return; setBusy(true); setErr(null); setMsg(null); try { const r = await fetch(`/api/v1/admin/scripts/odido/accounts/${account.id}`, { method: "DELETE", credentials: "include", }); const d = await r.json(); if (!r.ok) throw new Error(d.error?.message ?? "Verwijderen mislukt"); applySettings(d); setMsg("Account verwijderd"); } catch (e) { setErr(String((e as Error).message ?? e)); } finally { setBusy(false); } } async function probeAccounts(accountId?: string) { setBusy(true); setErr(null); setMsg(null); try { const url = accountId ? `/api/v1/admin/scripts/odido/accounts/${accountId}/probe` : "/api/v1/admin/scripts/odido/accounts/probe"; const r = await fetch(url, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: accountId ? undefined : JSON.stringify({}), }); const d = await r.json(); if (!r.ok) throw new Error(d.error?.message ?? "Statuscheck mislukt"); applySettings(d); const list: OdidoAccount[] = d.accounts ?? []; const ok = list.filter((a) => a.healthStatus === "ok").length; const dead = list.filter((a) => a.healthStatus === "dead").length; setMsg(`Accountstatus: ${ok} actief · ${dead} dood`); } catch (e) { setErr(String((e as Error).message ?? e)); } finally { setBusy(false); } } function healthBadge(a: OdidoAccount) { if (!a.enabled) { return ( uit ); } if (a.healthStatus === "ok") { return ( actief ); } if (a.healthStatus === "dead") { return ( dood ); } return ( onbekend ); } async function uploadWvd(file: File | null) { if (!file) return; setBusy(true); setErr(null); setMsg(null); try { if (file.size > 2 * 1024 * 1024) { throw new Error("WVD-bestand mag max. 2 MB zijn"); } const contentBase64 = await readFileAsBase64(file); const r = await fetch("/api/v1/admin/scripts/odido/wvd", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ contentBase64 }), }); const d = await r.json(); if (!r.ok) throw new Error(d.error?.message ?? "WVD-upload mislukt"); applySettings(d); setMsg(`WVD.wvd geüpload (${d.bytes ?? file.size} bytes)`); } catch (e) { setErr(String((e as Error).message ?? e)); } finally { setBusy(false); if (wvdInputRef.current) wvdInputRef.current.value = ""; } } async function refreshMpds() { setBusy(true); setErr(null); setMsg(null); try { const r = await fetch("/api/v1/admin/scripts/odido/refresh-mpd", { method: "POST", credentials: "include", }); const d = await r.json(); if (!r.ok) throw new Error(d.error?.message ?? "MPD-refresh mislukt"); setMsg( `MPD-cache vernieuwd: ${d.refreshed ?? 0} ok · ${d.failed ?? 0} fout · ${d.skipped ?? 0} skip (${d.durationMs ?? "?"} ms)` ); await load(); } catch (e) { setErr(String((e as Error).message ?? e)); } finally { setBusy(false); } } async function flushMpds() { if (!confirm("Alle gecachte Odido-MPD’s wissen? Volgende play haalt dan verse URLs.")) return; setBusy(true); setErr(null); setMsg(null); try { const r = await fetch("/api/v1/admin/scripts/odido/flush-mpd", { method: "POST", credentials: "include", }); const d = await r.json(); if (!r.ok) throw new Error(d.error?.message ?? "Flush mislukt"); setMsg(`MPD-cache geleegd (${d.cleared ?? 0} zenders)`); await load(); } catch (e) { setErr(String((e as Error).message ?? e)); } finally { setBusy(false); } } function resetTweaks() { if (odido?.tweakDefaults) setTweaks({ ...odido.tweakDefaults }); } const summary = odido?.lastSyncSummary; const refreshSummary = odido?.lastMpdRefreshSummary; const meta = odido?.tweakMeta ?? []; const accounts = odido?.accounts ?? []; return ( <>