aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-29 18:55:42 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-29 18:55:42 +0200
commita2215bb071c4e1ce11c616d26df8fd4e7a8fdf57 (patch)
treede2969394d8673194d219b22c4af0dd7faef2d10 /packages
parentc37fe3047367f401765351e5aca73f8f0ed6810c (diff)
parent1f8a52484412b48205e5ff6aac506428e2fb77ed (diff)
downloadmeshbay-a2215bb071c4e1ce11c616d26df8fd4e7a8fdf57.tar.gz
Merge remote-tracking branch 'origin/main'
Diffstat (limited to 'packages')
-rw-r--r--packages/meshbay-common/src/meshbay_common/protocol.py2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js77
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js14
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js14
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js14
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js14
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js14
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js14
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js14
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js14
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js14
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js14
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/style.css50
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport.js8
-rw-r--r--packages/meshbay-node/src/meshbay_node/daemon.py14
-rw-r--r--packages/meshbay-node/src/meshbay_node/ops.py115
-rw-r--r--packages/meshbay-node/src/meshbay_node/roster.py35
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py20
-rw-r--r--packages/meshbay-node/src/meshbay_node/ui/app.py32
19 files changed, 492 insertions, 1 deletions
diff --git a/packages/meshbay-common/src/meshbay_common/protocol.py b/packages/meshbay-common/src/meshbay_common/protocol.py
index a3fb4de..ff951e7 100644
--- a/packages/meshbay-common/src/meshbay_common/protocol.py
+++ b/packages/meshbay-common/src/meshbay_common/protocol.py
@@ -166,6 +166,8 @@ class MNP:
GROUP_ATTACH_ACK = "group_attach_ack"
GROUP_DETACH = "group_detach" # operator → node: stop hosting a group
GROUP_DETACH_ACK = "group_detach_ack"
+ NODE_SETTINGS_SET = "node_settings_set" # operator → node: change daemon settings
+ NODE_SETTINGS_SET_ACK = "node_settings_set_ack"
NODE_RELOAD = "node_reload" # operator → node: re-read node.toml
NODE_RELOAD_ACK = "node_reload_ack"
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 7017736..d1cbf2b 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 50f9ac5..6ddfa91 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 2aa4599..c942869 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 4750349..8eefb29 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 12e0c77..4ee261c 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 7f4a0c2..e30f5bf 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 5ea6371..2f66e00 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 6aa6cd1..f809182 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 2cff7a6..2e1a9f5 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 704204d..1ec23eb 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 2a2bd8a..739f9c6 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/style.css
+++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css
@@ -2660,6 +2660,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',
diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py
index 266693a..e1e4dee 100644
--- a/packages/meshbay-node/src/meshbay_node/daemon.py
+++ b/packages/meshbay-node/src/meshbay_node/daemon.py
@@ -267,6 +267,20 @@ class NodeDaemon:
await self._roster.open()
await self._roster.purge_expired()
+ # Apply any roster overrides to node config (panel-edited values
+ # take precedence over node.toml defaults).
+ nd = self._config.node
+ defaults = {
+ "invite_ttl_hours": nd.invite_ttl_hours,
+ "pair_ttl_hours": nd.pair_ttl_hours,
+ "device_request_ttl_minutes": nd.device_request_ttl_minutes,
+ "max_concurrent_streams": nd.max_concurrent_streams,
+ "transcode_incompatible_video": nd.transcode_incompatible_video,
+ }
+ effective = await self._roster.node_settings(defaults)
+ for k, v in effective.items():
+ setattr(nd, k, v)
+
# X25519 key material for GEK unwrapping
from cryptography.hazmat.primitives import serialization
sk_x_raw = keys.sk_x25519.private_bytes(
diff --git a/packages/meshbay-node/src/meshbay_node/ops.py b/packages/meshbay-node/src/meshbay_node/ops.py
index c921b05..d1314f0 100644
--- a/packages/meshbay-node/src/meshbay_node/ops.py
+++ b/packages/meshbay-node/src/meshbay_node/ops.py
@@ -332,7 +332,19 @@ async def list_groups(state: dict) -> dict:
members = await roster.list_members()
has_operator = any(m["role"] == "operator" and m["status"] == "active"
for m in members)
- return {"groups": out, "operator_paired": has_operator}
+ nd = config.node if config else None
+ defaults = {
+ "invite_ttl_hours": nd.invite_ttl_hours if nd else 168,
+ "pair_ttl_hours": nd.pair_ttl_hours if nd else 24,
+ "device_request_ttl_minutes": nd.device_request_ttl_minutes if nd else 60,
+ "max_concurrent_streams": nd.max_concurrent_streams if nd else 8,
+ "transcode_incompatible_video": nd.transcode_incompatible_video if nd else True,
+ }
+ if roster:
+ settings = await roster.node_settings(defaults)
+ else:
+ settings = defaults
+ return {"groups": out, "operator_paired": has_operator, "settings": settings}
async def attach_group(state: dict, name: str, shared_dir: str,
@@ -472,6 +484,57 @@ def _find_group_range(lines: list[str], group_id: str) -> tuple[int, int] | None
return None
+def _update_node_toml(conf_path: Path, updates: dict) -> None:
+ """Write changed [node] settings back to node.toml without disturbing comments.
+
+ For each key, if the line exists (commented or not) it is replaced in place;
+ otherwise the key is appended to the end of the [node] section.
+ """
+ if not conf_path.exists():
+ return
+ text = conf_path.read_text()
+ lines = text.split("\n")
+
+ node_start = None
+ node_end = len(lines)
+ for i, line in enumerate(lines):
+ stripped = line.strip()
+ if stripped == "[node]":
+ node_start = i
+ elif node_start is not None and re.match(r'^\[', stripped):
+ node_end = i
+ break
+
+ if node_start is None:
+ lines.append("")
+ lines.append("[node]")
+ node_start = len(lines) - 1
+ node_end = len(lines)
+
+ remaining = dict(updates)
+ for i in range(node_start + 1, node_end):
+ for key in list(remaining):
+ pattern = re.compile(
+ r'^(\s*#?\s*)' + re.escape(key) + r'\s*=\s*.*$')
+ if pattern.match(lines[i]):
+ value = remaining.pop(key)
+ if isinstance(value, bool):
+ lines[i] = f"{key} = {'true' if value else 'false'}"
+ else:
+ lines[i] = f"{key} = {value}"
+ break
+
+ for key, value in remaining.items():
+ if isinstance(value, bool):
+ insert_line = f"{key} = {'true' if value else 'false'}"
+ else:
+ insert_line = f"{key} = {value}"
+ lines.insert(node_end, insert_line)
+ node_end += 1
+
+ conf_path.write_text("\n".join(lines))
+
+
def _insert_roots_block(conf_path: Path, group_id: str,
root_block: str) -> None:
"""Append a [[groups.roots]] block inside the matching [[groups]] section."""
@@ -718,6 +781,56 @@ async def set_member_upload(state: dict, group_id: str, allowed: bool) -> dict:
return {"allowed": allowed, "group_id": group_id}
+# ── Node settings ────────────────────────────────────────────────────────────
+
+async def set_node_settings(state: dict, settings: dict) -> dict:
+ """Update node-level daemon settings. Writes to both roster.db and node.toml."""
+ roster = _roster(state)
+ config = _config(state)
+ nd = config.node
+ conf_path = Path(state.get("config_path") or DEFAULT_CONFIG_PATH)
+
+ allowed_keys = {
+ "invite_ttl_hours": ("int", roster.SETTING_INVITE_TTL),
+ "pair_ttl_hours": ("int", roster.SETTING_PAIR_TTL),
+ "device_request_ttl_minutes": ("int", roster.SETTING_DEVICE_TTL),
+ "max_concurrent_streams": ("int", roster.SETTING_MAX_STREAMS),
+ "transcode_incompatible_video": ("bool", roster.SETTING_TRANSCODE),
+ }
+
+ set_by = state.get("node_user_id", "")
+ updated = {}
+ for key, value in settings.items():
+ if key not in allowed_keys:
+ continue
+ kind, setting_key = allowed_keys[key]
+ if kind == "int":
+ try:
+ v = int(value)
+ except (TypeError, ValueError):
+ raise OpError(f"{key} must be an integer")
+ if v < 1:
+ raise OpError(f"{key} must be positive")
+ setattr(nd, key, v)
+ await roster.set_node_setting(setting_key, str(v), set_by)
+ updated[key] = v
+ elif kind == "bool":
+ v = bool(value)
+ setattr(nd, key, v)
+ await roster.set_node_setting(setting_key, "1" if v else "0", set_by)
+ updated[key] = v
+
+ if updated:
+ _update_node_toml(conf_path, updated)
+ if "max_concurrent_streams" in updated:
+ webrtc = state.get("webrtc")
+ if webrtc and hasattr(webrtc, '_stream_sem'):
+ webrtc._stream_sem = asyncio.Semaphore(updated["max_concurrent_streams"])
+
+ log.info("Node settings updated: %s", updated)
+ return {"updated": updated}
+
+
# ── Applications ─────────────────────────────────────────────────────────────
async def set_enabled_apps(state: dict, group_id: str, apps: list[str]) -> dict:
diff --git a/packages/meshbay-node/src/meshbay_node/roster.py b/packages/meshbay-node/src/meshbay_node/roster.py
index ff5282d..cf0d421 100644
--- a/packages/meshbay-node/src/meshbay_node/roster.py
+++ b/packages/meshbay-node/src/meshbay_node/roster.py
@@ -754,6 +754,41 @@ class Roster:
str(float(debounce_secs)), set_by)
return await self.scan_settings(group_id)
+ # ── Node-wide daemon settings ───────────────────────────────────────────
+ # Same pattern as TMDB config: stored under NODE_WIDE_GROUP_ID.
+ # On startup, node.toml values are the defaults; the roster override
+ # takes precedence at runtime. Changing a setting writes to both
+ # roster.db (immediate) and node.toml (survives a DB wipe).
+ SETTING_INVITE_TTL = "invite_ttl_hours"
+ SETTING_PAIR_TTL = "pair_ttl_hours"
+ SETTING_DEVICE_TTL = "device_request_ttl_minutes"
+ SETTING_MAX_STREAMS = "max_concurrent_streams"
+ SETTING_TRANSCODE = "transcode_incompatible_video"
+
+ async def node_settings(self, defaults: dict) -> dict:
+ """Current effective settings: roster override if present, else config default."""
+ result = {}
+ for key, setting in [
+ ("invite_ttl_hours", self.SETTING_INVITE_TTL),
+ ("pair_ttl_hours", self.SETTING_PAIR_TTL),
+ ("device_request_ttl_minutes", self.SETTING_DEVICE_TTL),
+ ("max_concurrent_streams", self.SETTING_MAX_STREAMS),
+ ("transcode_incompatible_video", self.SETTING_TRANSCODE),
+ ]:
+ stored = await self.get_setting(self.NODE_WIDE_GROUP_ID, setting)
+ if stored is not None:
+ if key == "transcode_incompatible_video":
+ result[key] = stored != "0"
+ else:
+ result[key] = int(stored)
+ else:
+ result[key] = defaults.get(key)
+ return result
+
+ async def set_node_setting(self, key: str, value: str,
+ set_by: str = "") -> None:
+ await self.set_setting(self.NODE_WIDE_GROUP_ID, key, value, set_by)
+
async def create_invite(
self,
group_id: str,
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
index 1bd7203..a934418 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
@@ -504,6 +504,8 @@ class WebRTCPeerSession:
self._do_group_attach(msg)
elif mtype == MNP.GROUP_DETACH:
self._do_group_detach(msg)
+ elif mtype == MNP.NODE_SETTINGS_SET:
+ self._spawn(self._do_node_settings_set(msg))
elif mtype == MNP.NODE_RELOAD:
self._spawn(self._do_node_reload(msg))
elif mtype == MNP.KEYPAIR_BUNDLE_STORE:
@@ -2187,6 +2189,24 @@ class WebRTCPeerSession:
log.error("node_status failed: %s", e, exc_info=True)
self._send({"type": "error", "detail": "Internal error"})
+ async def _do_node_settings_set(self, msg: dict) -> None:
+ if not self._is_node_admin():
+ self._send({"type": "error", "detail": "Not the node operator"})
+ return
+ settings = msg.get("settings", {})
+ if not settings:
+ self._send({"type": "error", "detail": "No settings provided"})
+ return
+ try:
+ result = await self._run_op(ops.set_node_settings, settings)
+ self._send({"type": MNP.NODE_SETTINGS_SET_ACK, "v": MNP_VERSION,
+ **result})
+ except ops.OpError as e:
+ self._send({"type": "error", "detail": e.message})
+ except Exception as e:
+ log.error("node_settings_set failed: %s", e, exc_info=True)
+ self._send({"type": "error", "detail": "Internal error"})
+
async def _do_roster_read(self, msg: dict) -> None:
if not self._is_node_admin():
self._send({"type": "error", "detail": "Not the node operator"})
diff --git a/packages/meshbay-node/src/meshbay_node/ui/app.py b/packages/meshbay-node/src/meshbay_node/ui/app.py
index 514a143..b25b36a 100644
--- a/packages/meshbay-node/src/meshbay_node/ui/app.py
+++ b/packages/meshbay-node/src/meshbay_node/ui/app.py
@@ -291,6 +291,13 @@ def create_ui_app(state: dict) -> FastAPI:
"quic_port": config.node.quic_port,
"ui_port": config.node.ui_port,
"data_dir": str(config.data_dir),
+ "settings": {
+ "invite_ttl_hours": config.node.invite_ttl_hours,
+ "pair_ttl_hours": config.node.pair_ttl_hours,
+ "device_request_ttl_minutes": config.node.device_request_ttl_minutes,
+ "max_concurrent_streams": config.node.max_concurrent_streams,
+ "transcode_incompatible_video": config.node.transcode_incompatible_video,
+ },
"groups": [
{
"id": g.id,
@@ -438,6 +445,12 @@ def create_ui_app(state: dict) -> FastAPI:
# seconds, on a real library) — see ops.start_reload for why.
return await _op(lambda: ops.start_reload(state))
+ # ── Node settings (operator only, localhost) ───────────────────────────
+
+ @app.put("/api/node-settings")
+ async def update_node_settings(payload: dict):
+ return await _op(lambda: ops.set_node_settings(state, payload))
+
# ── Chat endpoints ───────────────────────────────────────────────────────
_chat_subscribers: list[WebSocket] = []
@@ -521,6 +534,24 @@ def _fmt_size(n: int) -> str:
return f"{n / (1024 * 1024 * 1024):.2f} GB"
+def _render_node_settings(config) -> str:
+ if not config:
+ return ""
+ nd = config.node
+ transcode = "on" if nd.transcode_incompatible_video else "off"
+ return f"""
+ <table style="margin-top:10px">
+ <thead><tr><th>Setting</th><th>Value</th></tr></thead>
+ <tbody>
+ <tr><td>Invitation TTL</td><td>{nd.invite_ttl_hours} hours</td></tr>
+ <tr><td>Pairing code TTL</td><td>{nd.pair_ttl_hours} hours</td></tr>
+ <tr><td>Device request TTL</td><td>{nd.device_request_ttl_minutes} minutes</td></tr>
+ <tr><td>Max concurrent streams</td><td>{nd.max_concurrent_streams}</td></tr>
+ <tr><td>Transcode incompatible video</td><td>{transcode}</td></tr>
+ </tbody>
+ </table>"""
+
+
def _render_roster(roster_view: dict | None) -> str:
"""
Who this node recognises, and which keys are theirs.
@@ -766,6 +797,7 @@ def _render_page(state: dict, roster_view: dict | None = None,
<p><b>Hub:</b> {state.get("hub_url", "—")}</p>
<p><b>QUIC port:</b> {state.get("quic_port", "—")}</p>
<p><b>Node ID:</b> <code>{state.get("endpoint_hint") or "—"}</code></p>
+ {_render_node_settings(config)}
</div>
<h2>Maintenance</h2>