diff --git a/apps/admin-ui/src/app/nodes/page.tsx b/apps/admin-ui/src/app/nodes/page.tsx index dd86a72..9bc2000 100644 --- a/apps/admin-ui/src/app/nodes/page.tsx +++ b/apps/admin-ui/src/app/nodes/page.tsx @@ -3,6 +3,14 @@ import { useEffect, useState } from "react"; import { Nav, useAuth } from "@/components/Nav"; +interface ScanRootRow { + id?: string; + path: string; + shelfId: string; + shelfName?: string; + kind?: "movie" | "series"; +} + interface Node { id: string; name: string; @@ -19,14 +27,25 @@ interface Node { libraryFileCount: number; fullScanInterval: string; fullScanAt: string; + moviesPaths: string[]; + seriesPaths: string[]; + scanRoots: ScanRootRow[]; revoked: boolean; } +interface Shelf { + id: string; + name: string; + slug: string; + kind: "MOVIE" | "SERIES"; +} + const MASTER_URL = "https://master.vonas.nl"; export default function NodesPage() { useAuth(); const [nodes, setNodes] = useState([]); + const [shelves, setShelves] = useState([]); const [showEnroll, setShowEnroll] = useState(false); const [enrollName, setEnrollName] = useState(""); const [enrollLocation, setEnrollLocation] = useState(""); @@ -41,6 +60,8 @@ export default function NodesPage() { const [editPublicUrl, setEditPublicUrl] = useState(""); const [editScanInterval, setEditScanInterval] = useState("off"); const [editScanAt, setEditScanAt] = useState("03:30"); + const [editRoots, setEditRoots] = useState([]); + const [editBusy, setEditBusy] = useState(false); function loadNodes() { fetch("/api/v1/admin/nodes", { credentials: "include" }) @@ -48,8 +69,15 @@ export default function NodesPage() { .then((d) => setNodes(d.nodes ?? [])); } + function loadShelves() { + fetch("/api/v1/admin/library/shelves", { credentials: "include" }) + .then((r) => r.json()) + .then((d) => setShelves(d.shelves ?? [])); + } + useEffect(() => { loadNodes(); + loadShelves(); const t = setInterval(loadNodes, 15000); return () => clearInterval(t); }, []); @@ -95,6 +123,20 @@ export default function NodesPage() { alert("Rescan aangevraagd"); } + async function restartNode(id: string) { + if (!confirm("Node herstarten? De container komt terug via restart-policy.")) return; + const res = await fetch(`/api/v1/admin/nodes/${id}/restart`, { + method: "POST", + credentials: "include", + }); + if (!res.ok) { + const data = await res.json(); + alert(data.error?.message ?? "Herstart mislukt"); + return; + } + alert("Herstart aangevraagd"); + } + async function revokeNode(id: string) { if (!confirm("Weet je zeker dat je deze node wilt intrekken?")) return; await fetch(`/api/v1/admin/nodes/${id}/revoke`, { @@ -133,29 +175,59 @@ export default function NodesPage() { loadNodes(); } + function openEdit(node: Node) { + setEditId(node.id); + setEditName(node.name); + setEditLocation(node.location ?? ""); + setEditPublicUrl(node.publicStreamUrl); + setEditScanInterval(node.fullScanInterval ?? "off"); + setEditScanAt(node.fullScanAt ?? "03:30"); + if (node.scanRoots?.length) { + setEditRoots(node.scanRoots.map((r) => ({ path: r.path, shelfId: r.shelfId }))); + } else { + const defaults: ScanRootRow[] = []; + const filmShelf = shelves.find((s) => s.slug === "films")?.id ?? ""; + const seriesShelf = shelves.find((s) => s.slug === "series")?.id ?? ""; + for (const p of node.moviesPaths ?? []) defaults.push({ path: p, shelfId: filmShelf }); + for (const p of node.seriesPaths ?? []) defaults.push({ path: p, shelfId: seriesShelf }); + setEditRoots(defaults.length ? defaults : [{ path: "", shelfId: filmShelf }]); + } + } + async function saveEdit() { if (!editId) return; - const res = await fetch(`/api/v1/admin/nodes/${editId}`, { - method: "PATCH", - credentials: "include", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - name: editName, - location: editLocation, - publicStreamUrl: editPublicUrl.trim().replace(/\/$/, ""), - fullScanInterval: editScanInterval.trim() || "off", - fullScanAt: editScanAt.trim() || "03:30", - }), - }); - if (!res.ok) { - const data = await res.json().catch(() => ({})); - alert(data.error?.message ?? "Opslaan mislukt"); - return; + setEditBusy(true); + try { + const scanRoots = editRoots + .map((r) => ({ path: r.path.trim(), shelfId: r.shelfId })) + .filter((r) => r.path && r.shelfId); + const res = await fetch(`/api/v1/admin/nodes/${editId}`, { + method: "PATCH", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: editName, + location: editLocation, + publicStreamUrl: editPublicUrl.trim().replace(/\/$/, ""), + fullScanInterval: editScanInterval.trim() || "off", + fullScanAt: editScanAt.trim() || "03:30", + scanRoots, + }), + }); + if (!res.ok) { + const data = await res.json().catch(() => ({})); + alert(data.error?.message ?? "Opslaan mislukt"); + return; + } + setEditId(null); + loadNodes(); + } finally { + setEditBusy(false); } - setEditId(null); - loadNodes(); } + const defaultMovieShelf = shelves.find((s) => s.slug === "films")?.id ?? shelves.find((s) => s.kind === "MOVIE")?.id ?? ""; + return ( <>