diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-29 18:42:43 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-29 18:52:53 +0200 |
| commit | 1f8a52484412b48205e5ff6aac506428e2fb77ed (patch) | |
| tree | 5598419ee4caef21f607876de6af2d30d6452191 /packages/meshbay-hub | |
| parent | 8de80b7e19738b716d9973c0b4ba3b42085f7359 (diff) | |
| download | meshbay-1f8a52484412b48205e5ff6aac506428e2fb77ed.tar.gz | |
feat(node): editable node settings in the Node page (D5)
Expose invite_ttl_hours, pair_ttl_hours, device_request_ttl_minutes,
max_concurrent_streams and transcode_incompatible_video in the Node
management panel. Changes are applied immediately via roster.db and
written back to node.toml so they survive a DB wipe. On startup,
roster overrides take precedence over node.toml defaults.
Draft v6 §2.11 documents the design; MNP gains node_settings_set /
node_settings_set_ack for the browser path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub')
13 files changed, 275 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 4aab150..c31653a 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -2373,6 +2373,9 @@ function NodePage({ token, username, userId, groups }) { const [rosterGroup, setRosterGroup] = useState(''); const [denylist, setDenylist] = useState(null); const [showDenylist, setShowDenylist] = useState(false); + const [nodeSettings, setNodeSettings] = useState(null); + const [editSettings, setEditSettings] = useState(null); + const [savingSettings, setSavingSettings] = useState(false); const [attachGroup_, setAttachGroup_] = useState(''); const [attachDir, setAttachDir] = useState(''); const [attachUpload, setAttachUpload] = useState(''); @@ -2434,6 +2437,7 @@ function NodePage({ token, username, userId, groups }) { transportRef.current = transport; setNodeGroups(result.groups || []); setOperatorPaired(!!result.operator_paired); + setNodeSettings(result.settings || null); setStatus('connected'); return; } catch (err) { @@ -2460,6 +2464,10 @@ function NodePage({ token, username, userId, groups }) { }; }, [connectAndFetch]); + useEffect(() => { + if (nodeSettings && !editSettings) setEditSettings({ ...nodeSettings }); + }, [nodeSettings]); + const refresh = useCallback(async () => { const transport = transportRef.current; if (!transport || !transport.connected) return; @@ -2467,6 +2475,7 @@ function NodePage({ token, username, userId, groups }) { const result = await transport.fetchNodeStatus(); setNodeGroups(result.groups || []); setOperatorPaired(!!result.operator_paired); + setNodeSettings(result.settings || null); } catch {} }, []); @@ -2673,6 +2682,22 @@ function NodePage({ token, username, userId, groups }) { } }, [refresh]); + const saveSettings = useCallback(async () => { + const transport = transportRef.current; + if (!transport || !transport.connected || !editSettings) return; + setSavingSettings(true); + setActionMsg(''); + try { + await transport.updateNodeSettings(editSettings); + setNodeSettings({ ...editSettings }); + setActionMsg(t('node.settings_saved')); + } catch (err) { + setActionMsg(err.message); + } finally { + setSavingSettings(false); + } + }, [editSettings]); + if (status === 'idle' || status === 'connecting') { return html`<div class="page-content"> <h2>${t('node.title')}</h2> @@ -2888,6 +2913,58 @@ function NodePage({ token, username, userId, groups }) { `} </div> + ${editSettings && html` + <div class="node-group"> + <span class="settings-heading">${t('node.settings')}</span> + <p class="node-hint">${t('node.settings_edit_hint')}</p> + <div class="node-settings-grid"> + <label class="node-setting"> + <span class="node-setting-label">${t('node.setting_invite_ttl')}</span> + <div class="node-setting-input"> + <input type="number" min="1" value=${editSettings.invite_ttl_hours} + onInput=${e => setEditSettings(s => ({...s, invite_ttl_hours: parseInt(e.target.value) || 1}))} /> + <span class="node-setting-unit">${t('node.setting_unit_hours')}</span> + </div> + </label> + <label class="node-setting"> + <span class="node-setting-label">${t('node.setting_pair_ttl')}</span> + <div class="node-setting-input"> + <input type="number" min="1" value=${editSettings.pair_ttl_hours} + onInput=${e => setEditSettings(s => ({...s, pair_ttl_hours: parseInt(e.target.value) || 1}))} /> + <span class="node-setting-unit">${t('node.setting_unit_hours')}</span> + </div> + </label> + <label class="node-setting"> + <span class="node-setting-label">${t('node.setting_device_ttl')}</span> + <div class="node-setting-input"> + <input type="number" min="1" value=${editSettings.device_request_ttl_minutes} + onInput=${e => setEditSettings(s => ({...s, device_request_ttl_minutes: parseInt(e.target.value) || 1}))} /> + <span class="node-setting-unit">${t('node.setting_unit_minutes')}</span> + </div> + </label> + <label class="node-setting"> + <span class="node-setting-label">${t('node.setting_max_streams')}</span> + <div class="node-setting-input"> + <input type="number" min="1" value=${editSettings.max_concurrent_streams} + onInput=${e => setEditSettings(s => ({...s, max_concurrent_streams: parseInt(e.target.value) || 1}))} /> + </div> + </label> + <label class="node-setting"> + <span class="node-setting-label">${t('node.setting_transcode')}</span> + <div class="node-setting-input"> + <input type="checkbox" checked=${editSettings.transcode_incompatible_video} + onChange=${e => setEditSettings(s => ({...s, transcode_incompatible_video: e.target.checked}))} /> + </div> + </label> + </div> + <div class="node-settings-actions"> + <button class="btn btn-primary btn-small" disabled=${savingSettings || busy} + onClick=${saveSettings}> + ${savingSettings ? t('node.settings_saving') : t('node.settings_save')}</button> + </div> + </div> + `} + ${/* In Electron, the Create Group wizard handles attaching. Keep for browser users who manage nodes via MNP. */ !platform.node.available && (() => { diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js index c4ee388..07f6e19 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -608,6 +608,20 @@ export default { 'node.restart_needed': 'Nach dem Hinzufügen: Gruppenschlüssel initialisieren (meshbay-node gek-init --group <name>).', 'node.no_groups': 'Keine Gruppen auf diesem Node konfiguriert.', 'node.stale': 'nicht auf dem Hub', + 'node.settings': 'Node settings', + 'node.settings_edit_hint': 'Changes are applied immediately and saved to node.toml.', + 'node.settings_save': 'Save', + 'node.settings_saving': 'Saving…', + 'node.settings_saved': 'Settings saved.', + 'node.setting_invite_ttl': 'Invitation TTL', + 'node.setting_pair_ttl': 'Pairing code TTL', + 'node.setting_device_ttl': 'Device request TTL', + 'node.setting_max_streams': 'Max concurrent streams', + 'node.setting_transcode': 'Transcode incompatible video', + 'node.setting_unit_hours': 'hours', + 'node.setting_unit_minutes': 'min', + 'node.setting_on': 'On', + 'node.setting_off': 'Off', 'node.unavailable': 'nicht verfügbar', 'node.files': { one: '1 Datei', other: '{n} Dateien' }, 'node.peers': { one: '1 Peer', other: '{n} Peers' }, diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js index 16ae3ab..2e5bee3 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -709,4 +709,18 @@ export default { 'node.detach_confirm': 'Remove "{name}" from this node?', 'node.detached': 'Group removed.', 'node.stale': 'not on hub', + 'node.settings': 'Node settings', + 'node.settings_edit_hint': 'Changes are applied immediately and saved to node.toml.', + 'node.settings_save': 'Save', + 'node.settings_saving': 'Saving…', + 'node.settings_saved': 'Settings saved.', + 'node.setting_invite_ttl': 'Invitation TTL', + 'node.setting_pair_ttl': 'Pairing code TTL', + 'node.setting_device_ttl': 'Device request TTL', + 'node.setting_max_streams': 'Max concurrent streams', + 'node.setting_transcode': 'Transcode incompatible video', + 'node.setting_unit_hours': 'hours', + 'node.setting_unit_minutes': 'min', + 'node.setting_on': 'On', + 'node.setting_off': 'Off', }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js index 88b222d..321ac95 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -604,6 +604,20 @@ export default { 'node.restart_needed': 'Después de añadir: inicialice la clave de grupo (meshbay-node gek-init --group <nombre>).', 'node.no_groups': 'No hay grupos configurados en este node.', 'node.stale': 'no está en el hub', + 'node.settings': 'Node settings', + 'node.settings_edit_hint': 'Changes are applied immediately and saved to node.toml.', + 'node.settings_save': 'Save', + 'node.settings_saving': 'Saving…', + 'node.settings_saved': 'Settings saved.', + 'node.setting_invite_ttl': 'Invitation TTL', + 'node.setting_pair_ttl': 'Pairing code TTL', + 'node.setting_device_ttl': 'Device request TTL', + 'node.setting_max_streams': 'Max concurrent streams', + 'node.setting_transcode': 'Transcode incompatible video', + 'node.setting_unit_hours': 'hours', + 'node.setting_unit_minutes': 'min', + 'node.setting_on': 'On', + 'node.setting_off': 'Off', 'node.unavailable': 'no disponible', 'node.files': { one: '1 archivo', other: '{n} archivos' }, 'node.peers': { one: '1 par', other: '{n} pares' }, diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js index b08cf28..51360a5 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -608,6 +608,20 @@ export default { 'node.restart_needed': 'Après l\'ajout : initialisez la clé de groupe (meshbay-node gek-init --group <name>).', 'node.no_groups': 'Aucun groupe configuré sur ce node.', 'node.stale': 'absent du hub', + 'node.settings': 'Paramètres du node', + 'node.settings_edit_hint': 'Les modifications sont appliquées immédiatement et sauvegardées dans node.toml.', + 'node.settings_save': 'Enregistrer', + 'node.settings_saving': 'Enregistrement…', + 'node.settings_saved': 'Paramètres enregistrés.', + 'node.setting_invite_ttl': 'Durée des invitations', + 'node.setting_pair_ttl': 'Durée du code d\'appairage', + 'node.setting_device_ttl': 'Durée des demandes d\'appareil', + 'node.setting_max_streams': 'Flux vidéo simultanés max', + 'node.setting_transcode': 'Transcoder les vidéos incompatibles', + 'node.setting_unit_hours': 'heures', + 'node.setting_unit_minutes': 'min', + 'node.setting_on': 'Activé', + 'node.setting_off': 'Désactivé', 'node.unavailable': 'indisponible', 'node.files': { one: '1 fichier', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js index f1e8467..8e53b5a 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -606,6 +606,20 @@ export default { 'node.restart_needed': "Dopo l'aggiunta: inizializzi la chiave di gruppo (meshbay-node gek-init --group <nome>).", 'node.no_groups': 'Nessun gruppo configurato su questo node.', 'node.stale': 'non presente sul hub', + 'node.settings': 'Node settings', + 'node.settings_edit_hint': 'Changes are applied immediately and saved to node.toml.', + 'node.settings_save': 'Save', + 'node.settings_saving': 'Saving…', + 'node.settings_saved': 'Settings saved.', + 'node.setting_invite_ttl': 'Invitation TTL', + 'node.setting_pair_ttl': 'Pairing code TTL', + 'node.setting_device_ttl': 'Device request TTL', + 'node.setting_max_streams': 'Max concurrent streams', + 'node.setting_transcode': 'Transcode incompatible video', + 'node.setting_unit_hours': 'hours', + 'node.setting_unit_minutes': 'min', + 'node.setting_on': 'On', + 'node.setting_off': 'Off', 'node.unavailable': 'non disponibile', 'node.files': { one: '1 file', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js index 95cc040..b6dc85d 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -594,6 +594,20 @@ export default { 'node.restart_needed': '追加後、グループ鍵を初期化してください(meshbay-node gek-init --group <name>)。', 'node.no_groups': 'この node にはグループが設定されていません。', 'node.stale': 'hub に未登録', + 'node.settings': 'Node settings', + 'node.settings_edit_hint': 'Changes are applied immediately and saved to node.toml.', + 'node.settings_save': 'Save', + 'node.settings_saving': 'Saving…', + 'node.settings_saved': 'Settings saved.', + 'node.setting_invite_ttl': 'Invitation TTL', + 'node.setting_pair_ttl': 'Pairing code TTL', + 'node.setting_device_ttl': 'Device request TTL', + 'node.setting_max_streams': 'Max concurrent streams', + 'node.setting_transcode': 'Transcode incompatible video', + 'node.setting_unit_hours': 'hours', + 'node.setting_unit_minutes': 'min', + 'node.setting_on': 'On', + 'node.setting_off': 'Off', 'node.unavailable': '利用不可', 'node.files': { other: '{n} 件のファイル', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js index 076aa15..ad086a8 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -608,6 +608,20 @@ export default { 'node.restart_needed': 'Na het toevoegen: initialiseer de groepssleutel (meshbay-node gek-init --group <naam>).', 'node.no_groups': 'Geen groepen geconfigureerd op deze node.', 'node.stale': 'niet op hub', + 'node.settings': 'Node settings', + 'node.settings_edit_hint': 'Changes are applied immediately and saved to node.toml.', + 'node.settings_save': 'Save', + 'node.settings_saving': 'Saving…', + 'node.settings_saved': 'Settings saved.', + 'node.setting_invite_ttl': 'Invitation TTL', + 'node.setting_pair_ttl': 'Pairing code TTL', + 'node.setting_device_ttl': 'Device request TTL', + 'node.setting_max_streams': 'Max concurrent streams', + 'node.setting_transcode': 'Transcode incompatible video', + 'node.setting_unit_hours': 'hours', + 'node.setting_unit_minutes': 'min', + 'node.setting_on': 'On', + 'node.setting_off': 'Off', 'node.unavailable': 'niet beschikbaar', 'node.files': { one: '1 bestand', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js index 00ef59e..29d95aa 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -627,6 +627,20 @@ export default { 'node.restart_needed': 'Po dodaniu: zainicjalizuj klucz grupy (meshbay-node gek-init --group <nazwa>).', 'node.no_groups': 'Na tym node nie skonfigurowano żadnych grup.', 'node.stale': 'brak na hub', + 'node.settings': 'Node settings', + 'node.settings_edit_hint': 'Changes are applied immediately and saved to node.toml.', + 'node.settings_save': 'Save', + 'node.settings_saving': 'Saving…', + 'node.settings_saved': 'Settings saved.', + 'node.setting_invite_ttl': 'Invitation TTL', + 'node.setting_pair_ttl': 'Pairing code TTL', + 'node.setting_device_ttl': 'Device request TTL', + 'node.setting_max_streams': 'Max concurrent streams', + 'node.setting_transcode': 'Transcode incompatible video', + 'node.setting_unit_hours': 'hours', + 'node.setting_unit_minutes': 'min', + 'node.setting_on': 'On', + 'node.setting_off': 'Off', 'node.unavailable': 'niedostępny', 'node.files': { one: '{n} plik', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js index 7d9173e..93f7198 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js @@ -605,6 +605,20 @@ export default { 'node.restart_needed': 'Após adicionar: inicialize a chave do grupo (meshbay-node gek-init --group <nome>).', 'node.no_groups': 'Nenhum grupo configurado neste node.', 'node.stale': 'fora do hub', + 'node.settings': 'Node settings', + 'node.settings_edit_hint': 'Changes are applied immediately and saved to node.toml.', + 'node.settings_save': 'Save', + 'node.settings_saving': 'Saving…', + 'node.settings_saved': 'Settings saved.', + 'node.setting_invite_ttl': 'Invitation TTL', + 'node.setting_pair_ttl': 'Pairing code TTL', + 'node.setting_device_ttl': 'Device request TTL', + 'node.setting_max_streams': 'Max concurrent streams', + 'node.setting_transcode': 'Transcode incompatible video', + 'node.setting_unit_hours': 'hours', + 'node.setting_unit_minutes': 'min', + 'node.setting_on': 'On', + 'node.setting_off': 'Off', 'node.unavailable': 'indisponível', 'node.files': { one: '1 arquivo', other: '{n} arquivos' }, 'node.peers': { one: '1 par', other: '{n} pares' }, diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js index 74d729e..b8ae04f 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js @@ -580,6 +580,20 @@ export default { 'node.restart_needed': '添加后请初始化群组密钥(meshbay-node gek-init --group <名称>)。', 'node.no_groups': '此 node 上未配置任何群组。', 'node.stale': '不在 hub 上', + 'node.settings': 'Node settings', + 'node.settings_edit_hint': 'Changes are applied immediately and saved to node.toml.', + 'node.settings_save': 'Save', + 'node.settings_saving': 'Saving…', + 'node.settings_saved': 'Settings saved.', + 'node.setting_invite_ttl': 'Invitation TTL', + 'node.setting_pair_ttl': 'Pairing code TTL', + 'node.setting_device_ttl': 'Device request TTL', + 'node.setting_max_streams': 'Max concurrent streams', + 'node.setting_transcode': 'Transcode incompatible video', + 'node.setting_unit_hours': 'hours', + 'node.setting_unit_minutes': 'min', + 'node.setting_on': 'On', + 'node.setting_off': 'Off', 'node.unavailable': '不可用', 'node.files': { other: '{n} 个文件', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css index 8e714f3..1ead2d0 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/style.css +++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css @@ -2651,6 +2651,56 @@ h2 .gn-owner, h3 .gn-owner { font-size: 0.55em; } } .node-attach-dir-row input { flex: 1; } +.node-settings-grid { + display: flex; + flex-direction: column; + gap: 0; + margin-top: 8px; +} +.node-setting { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0; + border-bottom: 1px solid var(--border); + cursor: default; +} +.node-setting-label { + font-size: 0.85em; + color: var(--text-secondary); +} +.node-setting-value { + font-size: 0.85em; + font-weight: 500; +} +.node-setting-input { + display: flex; + align-items: center; + gap: 6px; +} +.node-setting-input input[type="number"] { + width: 80px; + padding: 4px 8px; + border: 1px solid var(--border); + border-radius: 5px; + background: var(--bg-base); + color: var(--text); + font-size: 0.85em; + text-align: right; +} +.node-setting-input input[type="checkbox"] { + width: 16px; + height: 16px; + accent-color: var(--accent); +} +.node-setting-unit { + font-size: 0.8em; + color: var(--text-secondary); +} +.node-settings-actions { + margin-top: 12px; +} + /* ── Create Group Wizard ──────────────────────────────────────────────────── */ .wizard-root { diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js index 5a6e36b..b60bd93 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js @@ -1310,6 +1310,14 @@ class MeshBayTransport { return msg; } + async updateNodeSettings(settings) { + const msg = await this._sendAndWait({ + type: 'node_settings_set', v: '0.1', settings, + }); + if (msg.type === 'error') throw new Error(msg.detail); + return msg; + } + async addRoot(groupId, path, { name, kind, upload } = {}, signFn) { const msg = await this._sendAndWait({ type: 'root_add', v: '0.1', |