diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-22 18:26:22 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-22 18:26:22 +0200 |
| commit | 8dc11dc05a35a5d64ba4d2c892ccc01c7bfae3da (patch) | |
| tree | e172bb4d596bde53c48ce2ca3ef2d59c98968147 /packages/meshbay-hub/src/meshbay_hub/static/app.js | |
| parent | b4baa4770d517ea7d1d25bb0ac50fc7c989531d6 (diff) | |
| download | meshbay-8dc11dc05a35a5d64ba4d2c892ccc01c7bfae3da.tar.gz | |
feat(node): systemd service panel on the Node page, and a clean CLI restart
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016SF6RKNBKg9qejmoMJ9ybA
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/app.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/app.js | 90 |
1 files changed, 90 insertions, 0 deletions
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`<div class="node-service"> + <span class="spinner"></span>${' '}${t('node.service_checking')} + </div>`; + } + + 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` + <div class="node-service"> + <div class="node-service-status"> + <span class="presence presence-${dot}" title="${label}" aria-label="${label}"></span> + <span>${label}</span> + </div> + ${err && html`<div class="error-msg">${err}</div>`} + <div class="node-service-actions"> + <button class="btn btn-small btn-secondary" disabled=${!!busy || running} + onClick=${() => act('start', () => platform.node.start())}> + ${busy === 'start' ? t('node.service_starting') : t('node.service_start')}</button> + ${info.installed && html` + <button class="btn btn-small btn-secondary" disabled=${!!busy || !running} + onClick=${() => act('stop', () => platform.node.service.stop())}> + ${busy === 'stop' ? t('node.service_stopping') : t('node.service_stop')}</button> + <button class="btn btn-small btn-secondary" disabled=${!!busy} + onClick=${() => act('restart', () => platform.node.service.restart())}> + ${busy === 'restart' ? t('node.service_restarting') : t('node.service_restart')}</button> + `} + </div> + </div>`; +} + 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`<div class="page-content"> + <h2>${t('node.title')}</h2> + <${NodeServicePanel} onChanged=${connectAndFetch} /> <p class="page-message"><span class="spinner"></span>${' '}${t('status.connecting')}</p> </div>`; } if (status === 'no_groups') { return html`<div class="page-content"> + <h2>${t('node.title')}</h2> + <${NodeServicePanel} onChanged=${connectAndFetch} /> <p class="page-message">${t('node.no_groups')}</p> </div>`; } if (status === 'error') { return html`<div class="page-content"> <h2>${t('node.title')}</h2> + <${NodeServicePanel} onChanged=${connectAndFetch} /> <p class="error-msg">${error}</p> <button class="btn btn-primary" onClick=${connectAndFetch}> ${t('node.retry')}</button> @@ -5943,6 +6032,7 @@ function NodePage({ token, username, userId, groups }) { onClick=${reloadConfig}> ${t('node.reload')}</button> </div> + <${NodeServicePanel} onChanged=${connectAndFetch} /> ${actionMsg && html`<div class="node-message">${actionMsg}</div>`} ${(() => { const hubIds = new Set((groups || []).map(g => g.id)); |