aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/node-page.js
diff options
context:
space:
mode:
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.js104
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>
`;
}