From 6437e42c30ffe50eb1c166a8e19684c15f33ef28 Mon Sep 17 00:00:00 2001 From: Jos Vooges | STH Date: Wed, 2 Sep 2026 03:08:36 +0200 Subject: [PATCH] Add one-click remote node upgrade via WebSocket (v1.3.3). Co-authored-by: Cursor --- apps/admin-ui/src/app/nodes/page.tsx | 57 +++-- apps/master-api/src/nodes/routes.ts | 72 +++++- apps/master-api/src/nodes/version.ts | 56 +++++ apps/master-api/src/websocket/manager.ts | 49 ++++ deploy/docker/Dockerfile.master-api | 2 +- deploy/node/upgrade-node.sh | 2 +- docs/unraid-node.md | 2 +- node/media-node/cmd/media-node/main.go | 2 +- node/media-node/internal/control/client.go | 2 + node/media-node/internal/control/upgrade.go | 261 ++++++++++++++++++++ packages/protocol/src/index.ts | 14 ++ 11 files changed, 488 insertions(+), 31 deletions(-) create mode 100644 apps/master-api/src/nodes/version.ts create mode 100644 node/media-node/internal/control/upgrade.go diff --git a/apps/admin-ui/src/app/nodes/page.tsx b/apps/admin-ui/src/app/nodes/page.tsx index ff0fc33..69963f3 100644 --- a/apps/admin-ui/src/app/nodes/page.tsx +++ b/apps/admin-ui/src/app/nodes/page.tsx @@ -64,11 +64,16 @@ export default function NodesPage() { const [editScanAt, setEditScanAt] = useState("03:30"); const [editRoots, setEditRoots] = useState([]); const [editBusy, setEditBusy] = useState(false); + const [upgradingId, setUpgradingId] = useState(null); + const [targetNodeVersion, setTargetNodeVersion] = useState("1.3.3"); function loadNodes() { fetch("/api/v1/admin/nodes", { credentials: "include" }) .then((r) => r.json()) - .then((d) => setNodes(d.nodes ?? [])); + .then((d) => { + setNodes(d.nodes ?? []); + if (d.targetNodeVersion) setTargetNodeVersion(d.targetNodeVersion); + }); } function loadShelves() { @@ -164,24 +169,30 @@ export default function NodesPage() { } async function upgradeNode(id: string, name: string) { - const res = await fetch(`/api/v1/admin/nodes/${id}/upgrade-command`, { - credentials: "include", - }); - if (!res.ok) { + if (!confirm(`Node "${name}" upgraden naar ${targetNodeVersion}?`)) return; + setUpgradingId(id); + try { + const res = await fetch(`/api/v1/admin/nodes/${id}/upgrade`, { + method: "POST", + credentials: "include", + }); const data = await res.json(); - alert(data.error?.message ?? "Upgrade-commando ophalen mislukt"); - return; + if (data.manualRequired) { + await navigator.clipboard.writeText(data.command); + alert( + `${data.message ?? "Handmatige upgrade nodig."}\n\nCommando gekopieerd — plak in SSH op de ${data.platform === "synology" ? "Synology" : "Unraid"}-host:\n\n${data.command}` + ); + return; + } + if (!res.ok) { + alert(data.error?.message ?? "Upgrade mislukt"); + return; + } + alert(data.message ?? `Upgrade naar ${targetNodeVersion} gestart.`); + loadNodes(); + } finally { + setUpgradingId(null); } - const data = (await res.json()) as { - command: string; - currentVersion: string | null; - targetVersion: string; - platform: string; - }; - await navigator.clipboard.writeText(data.command); - alert( - `Upgrade-commando gekopieerd voor "${name}".\n\nPlak in SSH/terminal op de ${data.platform === "synology" ? "Synology" : "Unraid"}-host:\n\n${data.command}\n\nHuidig: ${data.currentVersion ?? "?"} → ${data.targetVersion}` - ); } async function revokeNode(id: string) { @@ -536,16 +547,20 @@ export default function NodesPage() { {node.activeStreams} {node.version ?? "-"} - {node.version && node.version !== "1.3.2" && ( + {node.version && node.version !== targetNodeVersion && ( update )} - {node.version !== "1.3.2" && ( - )}