diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-30 22:17:30 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-30 22:17:30 +0200 |
| commit | 07e6e4271ea3a45e9cd364eb6ec05801a653d9e8 (patch) | |
| tree | 5d2dea4251ee2ecea32aeef80d09167d08085dd0 /packages/meshbay-hub/src/meshbay_hub/static/node-page.js | |
| parent | bf56b60b95c5413d592f0199320d637978792756 (diff) | |
| download | meshbay-07e6e4271ea3a45e9cd364eb6ec05801a653d9e8.tar.gz | |
feat: configurable STUN server fallbacks for WebRTC ICE
The WebRTC transport relied on a single Google STUN server — if it was
unreachable, ICE gathering waited the full 4s timeout. Now four public
servers are used by default (Google ×2, Cloudflare, Mozilla), configurable
via node.toml, the Node page UI, and the CLI (meshbay-node stun list|add|
remove|reset). Changes are hot-swapped on the live transport.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/node-page.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/node-page.js | 104 |
1 files changed, 104 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/node-page.js b/packages/meshbay-hub/src/meshbay_hub/static/node-page.js index 9a6db7d..87a9815 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/node-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/node-page.js @@ -102,6 +102,9 @@ export function NodePage({ groups }) { const [nodeSettings, setNodeSettings] = useState(null); const [editSettings, setEditSettings] = useState(null); const [savingSettings, setSavingSettings] = useState(false); + const [editStun, setEditStun] = useState(null); + const [savingStun, setSavingStun] = useState(false); + const [stunInput, setStunInput] = useState(''); const [operatorPaired, setOperatorPaired] = useState(false); const [pairBusy, setPairBusy] = useState(false); const [pairStatus, setPairStatus] = useState(''); @@ -131,6 +134,7 @@ export function NodePage({ groups }) { useEffect(() => { if (nodeSettings && !editSettings) setEditSettings({ ...nodeSettings }); + if (nodeSettings && !editStun) setEditStun([...(nodeSettings.stun_servers || [])]); }, [nodeSettings]); const refresh = useCallback(async () => { @@ -291,6 +295,61 @@ export function NodePage({ groups }) { } }, [editSettings]); + const saveStun = useCallback(async () => { + if (!editStun) return; + setSavingStun(true); + setActionMsg(''); + try { + await nodeCall('PUT', '/api/node-settings', { stun_servers: editStun }); + setNodeSettings(s => s ? { ...s, stun_servers: [...editStun] } : s); + setActionMsg(t('node.stun_saved')); + } catch (err) { + setActionMsg(platform.bridgeMessage(err)); + } finally { + setSavingStun(false); + } + }, [editStun]); + + const addStunServer = useCallback(() => { + const url = stunInput.trim(); + if (!url) return; + if (!url.startsWith('stun:')) { + setActionMsg(t('node.stun_invalid')); + return; + } + if (editStun && editStun.includes(url)) { + setActionMsg(t('node.stun_duplicate')); + return; + } + setEditStun(s => [...(s || []), url]); + setStunInput(''); + setActionMsg(''); + }, [stunInput, editStun]); + + const removeStunServer = useCallback((idx) => { + setEditStun(s => s.filter((_, i) => i !== idx)); + }, []); + + const moveStunServer = useCallback((idx, dir) => { + setEditStun(s => { + const a = [...s]; + const target = idx + dir; + if (target < 0 || target >= a.length) return a; + [a[idx], a[target]] = [a[target], a[idx]]; + return a; + }); + }, []); + + const resetStunDefaults = useCallback(() => { + setEditStun([ + 'stun:stun.l.google.com:19302', + 'stun:stun1.l.google.com:19302', + 'stun:stun.cloudflare.com:3478', + 'stun:stun.services.mozilla.com:3478', + ]); + setActionMsg(''); + }, []); + const doPairOperator = useCallback(async () => { setPairBusy(true); setPairStatus(''); @@ -559,6 +618,51 @@ export function NodePage({ groups }) { </div> </div> `} + + ${editStun && html` + <div class="node-group"> + <span class="settings-heading">${t('node.stun_servers')}</span> + <p class="node-hint">${t('node.stun_hint')}</p> + ${editStun.length === 0 ? html` + <p class="node-hint">${t('node.stun_empty')}</p> + ` : html` + <div class="stun-server-list"> + ${editStun.map((url, idx) => html` + <div class="stun-server-entry" key=${url + idx}> + <span class="stun-server-url">${url}</span> + <div class="stun-server-actions"> + <button class="btn btn-small btn-icon" disabled=${idx === 0} + onClick=${() => moveStunServer(idx, -1)} + title=${t('node.stun_move_up')}>▲</button> + <button class="btn btn-small btn-icon" disabled=${idx === editStun.length - 1} + onClick=${() => moveStunServer(idx, 1)} + title=${t('node.stun_move_down')}>▼</button> + <button class="btn btn-small btn-danger btn-icon" + onClick=${() => removeStunServer(idx)} + title=${t('node.stun_remove')}>✕</button> + </div> + </div> + `)} + </div> + `} + <div class="stun-add-row"> + <input type="text" placeholder=${t('node.stun_add_placeholder')} + value=${stunInput} + onInput=${e => setStunInput(e.target.value)} + onKeyDown=${e => { if (e.key === 'Enter') addStunServer(); }} /> + <button class="btn btn-small btn-secondary" onClick=${addStunServer}> + ${t('node.stun_add')}</button> + </div> + <div class="node-settings-actions"> + <button class="btn btn-primary btn-small" disabled=${savingStun || busy} + onClick=${saveStun}> + ${savingStun ? t('node.stun_saving') : t('node.stun_save')}</button> + <button class="btn btn-small btn-secondary" disabled=${savingStun || busy} + onClick=${resetStunDefaults}> + ${t('node.stun_reset')}</button> + </div> + </div> + `} </div> `; } |