From 8dc11dc05a35a5d64ba4d2c892ccc01c7bfae3da Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sat, 22 Aug 2026 18:26:22 +0200 Subject: feat(node): systemd service panel on the Node page, and a clean CLI restart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a status panel at the top of the Node page — always visible, even before an MNP connection exists — showing the meshbay-node systemd unit's own state (via `systemctl --user show`, main process only) with Start/Stop/Restart controls. This is the piece the rest of the page cannot provide: it has to work while the daemon is stopped or crash- looping, which the MNP-based sections require the daemon to already answer. While touching node lifecycle: `reload` and `restart-daemon` in the CLI shelled out to pgrep + SIGTERM/SIGHUP and respawned the process by hand, logging to a hardcoded /tmp path. That pattern already SIGHUPed a developer's own running node by accident once (see the old test_cli_dispatch.py comment). Both now delegate to `systemctl --user reload|restart meshbay-node`, which the unit already supports correctly (ExecReload=, Restart=on-failure). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_016SF6RKNBKg9qejmoMJ9ybA --- packages/meshbay-hub/src/meshbay_hub/static/app.js | 90 ++++++++++++++++++++++ 1 file changed, 90 insertions(+) (limited to 'packages/meshbay-hub/src/meshbay_hub/static/app.js') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index b863460..36df0cd 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -5633,6 +5633,90 @@ function BlocklistForm({ onAdd }) { // ── Node management (D5) ──────────────────────────────────────────────────── +/** + * The systemd unit's own state, independent of the MNP connection below it. + * + * The rest of NodePage talks to the daemon over a signed MNP session, which + * requires the daemon to already be up and answering — no use at all for + * "the node is stopped, start it" or "it is crash-looping, tell me". This + * asks systemd directly (main process → `systemctl --user`), the same way + * `node:installed` and the wizard's `node:start` already do, so it works + * from every state the connection below can be in. + */ +function NodeServicePanel({ onChanged }) { + const [info, setInfo] = useState(null); + const [busy, setBusy] = useState(''); + const [err, setErr] = useState(''); + + const refresh = useCallback(async () => { + try { + const r = await platform.node.service.status(); + setInfo(r); + setErr(''); + } catch (e) { + setErr(platform.bridgeMessage(e)); + } + }, []); + + useEffect(() => { + if (!platform.node.service.available) return; + refresh(); + const timer = setInterval(refresh, 5000); + return () => clearInterval(timer); + }, [refresh]); + + const act = useCallback(async (name, fn) => { + setBusy(name); + setErr(''); + try { + await fn(); + await refresh(); + if (onChanged) onChanged(); + } catch (e) { + setErr(platform.bridgeMessage(e)); + } finally { + setBusy(''); + } + }, [refresh, onChanged]); + + if (!platform.node.service.available) return null; + if (!info || info.supported === false) { + return html`
+ ${' '}${t('node.service_checking')} +
`; + } + + const KNOWN_STATES = ['active', 'inactive', 'failed', 'activating', 'deactivating']; + const stateKey = KNOWN_STATES.includes(info.activeState) ? info.activeState : 'unknown'; + const running = info.activeState === 'active' || info.activeState === 'activating'; + const dot = info.activeState === 'active' ? 'online' + : info.activeState === 'failed' ? 'offline' : 'unknown'; + const label = info.installed ? t('node.service_state_' + stateKey) + : t('node.service_not_installed'); + + return html` +
+
+ + ${label} +
+ ${err && html`
${err}
`} +
+ + ${info.installed && html` + + + `} +
+
`; +} + function NodePage({ token, username, userId, groups }) { const [status, setStatus] = useState('idle'); const [error, setError] = useState(''); @@ -5918,17 +6002,22 @@ function NodePage({ token, username, userId, groups }) { if (status === 'idle' || status === 'connecting') { return html`
+

${t('node.title')}

+ <${NodeServicePanel} onChanged=${connectAndFetch} />

${' '}${t('status.connecting')}

`; } if (status === 'no_groups') { return html`
+

${t('node.title')}

+ <${NodeServicePanel} onChanged=${connectAndFetch} />

${t('node.no_groups')}

`; } if (status === 'error') { return html`

${t('node.title')}

+ <${NodeServicePanel} onChanged=${connectAndFetch} />

${error}

@@ -5943,6 +6032,7 @@ function NodePage({ token, username, userId, groups }) { onClick=${reloadConfig}> ${t('node.reload')}
+ <${NodeServicePanel} onChanged=${connectAndFetch} /> ${actionMsg && html`
${actionMsg}
`} ${(() => { const hubIds = new Set((groups || []).map(g => g.id)); -- cgit v1.2.3