aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/app.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-22 18:26:22 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-22 18:26:22 +0200
commit8dc11dc05a35a5d64ba4d2c892ccc01c7bfae3da (patch)
treee172bb4d596bde53c48ce2ca3ef2d59c98968147 /packages/meshbay-hub/src/meshbay_hub/static/app.js
parentb4baa4770d517ea7d1d25bb0ac50fc7c989531d6 (diff)
downloadmeshbay-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.js90
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));