diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static')
16 files changed, 149 insertions, 92 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index 3a85bf7..a12fea4 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -1397,6 +1397,9 @@ const mount = () => { // browser and on macOS. A language change reloads the page, which comes back // through here, so nothing else has to watch for it. platform.setTrayLabels(trayLabels()).catch(() => {}); + // Same moment, same reason: the app's own confirmation dialogs are worded + // from the catalogue of the language this page settled on. + platform.setUiLocale(getLocale()).catch(() => {}); }; initLocale().then(mount, (err) => { // Nothing in initLocale() is supposed to reject. If something does, an diff --git a/packages/meshbay-hub/src/meshbay_hub/static/create-group-page.js b/packages/meshbay-hub/src/meshbay_hub/static/create-group-page.js index 7556010..d4c2ab8 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/create-group-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/create-group-page.js @@ -253,11 +253,11 @@ function CreateGroupWizard({ token, username, onCreated, onNodeLinked, allowPubl const mainRoot = roots[0]; const attachBody = { name: name.trim(), - shared_dir: mainRoot.path, + path: mainRoot.path, writable: mainRoot.writable !== false, }; - await platform.node.call('POST', '/api/groups/attach', attachBody); - await platform.node.call('POST', '/api/reload'); + await platform.node.op('attachGroup', attachBody); + await platform.node.op('reload'); update('done'); advance(); @@ -272,8 +272,9 @@ function CreateGroupWizard({ token, username, onCreated, onNodeLinked, allowPubl update('running'); for (let i = 1; i < roots.length; i++) { const r = roots[i]; - await withRetry(() => platform.node.call('POST', `/api/groups/${gid}/roots`, { - path: r.path, name: r.name, writable: !!r.writable, removable: !!r.removable, + await withRetry(() => platform.node.op('addRoot', { + groupId: gid, path: r.path, name: r.name, + writable: !!r.writable, removable: !!r.removable, })); } await platform.waitForRootsIndexed(gid, setIndexProgress); @@ -283,20 +284,20 @@ function CreateGroupWizard({ token, username, onCreated, onNodeLinked, allowPubl // 5. GEK init update('running'); - await withRetry(() => platform.node.call('POST', `/api/groups/${gid}/gek`)); + await withRetry(() => platform.node.op('initGek', { groupId: gid })); update('done'); advance(); // 6. Generate pairing code update('running'); - const pairResult = await platform.node.call('POST', '/api/operator/pair'); + const pairResult = await platform.node.op('pairOperator'); if (pairResult && pairResult.code) { await platform.node.setPairingCode(pairResult.code); session.pendingJoinCode = pairResult.code; } update('done'); - try { await platform.node.call('POST', '/api/reload'); } catch { /* best effort */ } + try { await platform.node.op('reload'); } catch { /* best effort */ } setStep(3); if (onCreated) onCreated(); diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js index 048f831..f16bdf8 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js @@ -122,9 +122,6 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn, const overLoopback = !isLocal && !overMnp && nodeAvail; const canEdit = isLocal || overMnp || overLoopback; - const rootUrl = (name, suffix = '') => - '/api/groups/' + groupId + '/roots/' + encodeURIComponent(name) + suffix; - // Deliberately no index refresh after a root change. // // Adding a root makes the node reload, which rescans — minutes on a real @@ -162,7 +159,7 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn, })); const ok = await run(async () => { if (overMnp) await transport.updateRoot(groupId, rootName, updates, signFn); - else if (overLoopback) await platform.node.call('PATCH', rootUrl(rootName), updates); + else if (overLoopback) await platform.node.op('updateRoot', { groupId, rootName, updates }); else throw new Error(t('node.root_no_route')); }); // Only a failure clears the patch here; a success waits for the node's @@ -177,13 +174,13 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn, const doEjectRoot = useCallback((rootName) => run(async () => { if (overMnp) await transport.ejectRoot(groupId, rootName, signFn); - else if (overLoopback) await platform.node.call('PUT', rootUrl(rootName, '/eject')); + else if (overLoopback) await platform.node.op('ejectRoot', { groupId, rootName }); else throw new Error(t('node.root_no_route')); }), [overMnp, overLoopback, transport, groupId, signFn, run]); const doPlugRoot = useCallback((rootName) => run(async () => { if (overMnp) await transport.plugRoot(groupId, rootName, signFn); - else if (overLoopback) await platform.node.call('PUT', rootUrl(rootName, '/plug')); + else if (overLoopback) await platform.node.op('plugRoot', { groupId, rootName }); else throw new Error(t('node.root_no_route')); }), [overMnp, overLoopback, transport, groupId, signFn, run]); @@ -198,8 +195,8 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn, const ok = await run(async () => { if (overMnp) await transport.removeRoot(groupId, rootName, signFn); else if (overLoopback) { - await platform.node.call('DELETE', rootUrl(rootName)); - await platform.node.call('POST', '/api/reload'); + await platform.node.op('removeRoot', { groupId, rootName }); + await platform.node.op('reload'); } else throw new Error(t('node.root_no_route')); }); if (ok) say(t('node.root_removed')); @@ -228,9 +225,8 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn, if (overMnp) { await transport.addRoot(groupId, path, { name }, signFn); } else if (overLoopback) { - await platform.node.call('POST', '/api/groups/' + groupId + '/roots', - { path, name }); - await platform.node.call('POST', '/api/reload'); + await platform.node.op('addRoot', { groupId, path, name }); + await platform.node.op('reload'); await platform.watchIndexProgress(groupId, setIndexProgress); } else throw new Error(t('node.root_no_route')); }); @@ -468,7 +464,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, const detect = await platform.node.detect(); if (!detect.detected) { setNodeDetected(false); return; } setNodeDetected(true); - const data = await platform.node.call('GET', '/api/groups'); + const data = await platform.node.op('groups'); const groups = data.groups || []; const ng = groups.find(g => g.id === groupId); if (ng) { @@ -496,7 +492,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, const waitForRootCount = useCallback(async (expectedCount) => { for (let i = 0; i < 10; i++) { try { - const data = await platform.node.call('GET', '/api/groups'); + const data = await platform.node.op('groups'); const ng = (data.groups || []).find(g => g.id === groupId); const roots = (ng && ng.roots) || []; if (roots.length === expectedCount) { @@ -780,8 +776,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, try { if (platform.node.available) { try { - await platform.node.call('POST', - `/api/members/${member.user_id}/revoke?group_id=${groupId}`); + await platform.node.op('revokeMember', { userId: member.user_id, groupId }); } catch { /* best effort — node may not host this group */ } } else if (transport && transport.connected && operatorPaired) { const sk = transport.sessionKeys && transport.sessionKeys.skEdB64; @@ -1311,8 +1306,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, // Node detach first (reversible), then hub delete (irreversible) if (nodeDetected && nodeGroupName) { try { - await platform.node.call('POST', '/api/groups/detach', - { name: nodeGroupName }); + await platform.node.op('detachGroup', { name: nodeGroupName }); } catch (detachErr) { if (!await ask(t('settings_node.detach_failed_continue'))) return; } diff --git a/packages/meshbay-hub/src/meshbay_hub/static/index-dock.js b/packages/meshbay-hub/src/meshbay_hub/static/index-dock.js index b136578..6730e60 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/index-dock.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/index-dock.js @@ -54,7 +54,7 @@ function useLoopbackActivity() { const poll = async () => { let delay = IDLE_POLL_MS; try { - const data = await platform.node.call('GET', '/api/index-status'); + const data = await platform.node.op('indexStatus'); const next = {}; for (const g of (data && data.groups) || []) next[g.group_id] = fromLoopback(g); if (Object.values(next).some((j) => j.scanning || j.queued.length)) { 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 3c0f1ec..e2363eb 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -1240,4 +1240,10 @@ export default { 'hosts.approve': "Genehmigen", 'hosts.refuse': "Ablehnen", 'hosts.online': "online", + + // Worded by the desktop main process for its own dialogs (main.js). + 'native.attach_confirm': 'Die Gruppe „{name}" auf diesem Computer hosten und den Ordner {path} mit ihren Mitgliedern teilen?', + 'native.folder_confirm': 'Den Ordner {path} mit den Mitgliedern einer auf diesem Computer gehosteten Gruppe teilen? Er wurde nicht in der Ordnerauswahl gewählt.', + 'native.node_account_confirm': 'Der Node auf diesem Computer ist für {current} eingerichtet. Stattdessen für {next} einrichten? Er stellt dann die Gruppen, die er für {current} hostet, nicht mehr bereit.', + 'native.declined': 'Abgebrochen — nichts wurde geändert.', }; 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 947c61d..2fdda50 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -1221,4 +1221,10 @@ export default { 'hosts.approve': "Approve", 'hosts.refuse': "Refuse", 'hosts.online': "online", + + // Worded by the desktop main process for its own dialogs (main.js). + 'native.attach_confirm': 'Host the group "{name}" on this computer and share the folder {path} with its members?', + 'native.folder_confirm': 'Share the folder {path} with the members of a group hosted on this computer? It was not chosen in the folder picker.', + 'native.node_account_confirm': 'The node on this computer is set up for {current}. Set it up for {next} instead? It will stop serving the groups it hosts for {current}.', + 'native.declined': 'Cancelled — nothing was changed.', }; 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 a0220d8..498cba3 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -1234,4 +1234,10 @@ export default { 'hosts.approve': "Aprobar", 'hosts.refuse': "Rechazar", 'hosts.online': "en línea", + + // Worded by the desktop main process for its own dialogs (main.js). + 'native.attach_confirm': '¿Alojar el grupo «{name}» en este ordenador y compartir la carpeta {path} con sus miembros?', + 'native.folder_confirm': '¿Compartir la carpeta {path} con los miembros de un grupo alojado en este ordenador? No se eligió en el selector de carpetas.', + 'native.node_account_confirm': 'El node de este ordenador está configurado para {current}. ¿Configurarlo para {next} en su lugar? Dejará de servir los grupos que aloja para {current}.', + 'native.declined': 'Cancelado: no se ha cambiado nada.', }; 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 c4bc37e..c62ed98 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -1249,4 +1249,10 @@ export default { 'hosts.approve': "Approuver", 'hosts.refuse': "Refuser", 'hosts.online': "en ligne", + + // Worded by the desktop main process for its own dialogs (main.js). + 'native.attach_confirm': 'Héberger le groupe « {name} » sur cet ordinateur et partager le dossier {path} avec ses membres ?', + 'native.folder_confirm': 'Partager le dossier {path} avec les membres d\'un groupe hébergé sur cet ordinateur ? Il n\'a pas été choisi dans le sélecteur de dossier.', + 'native.node_account_confirm': 'Le node de cet ordinateur est configuré pour {current}. Le configurer pour {next} à la place ? Il cessera de servir les groupes qu\'il héberge pour {current}.', + 'native.declined': 'Annulé — rien n\'a été modifié.', }; 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 59505b8..9f1d9f6 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -1248,4 +1248,10 @@ export default { 'hosts.approve': "Approva", 'hosts.refuse': "Rifiuta", 'hosts.online': "online", + + // Worded by the desktop main process for its own dialogs (main.js). + 'native.attach_confirm': 'Ospitare il gruppo «{name}» su questo computer e condividere la cartella {path} con i suoi membri?', + 'native.folder_confirm': 'Condividere la cartella {path} con i membri di un gruppo ospitato su questo computer? Non è stata scelta nel selettore di cartelle.', + 'native.node_account_confirm': 'Il node di questo computer è configurato per {current}. Configurarlo invece per {next}? Smetterà di servire i gruppi che ospita per {current}.', + 'native.declined': 'Annullato: non è stato modificato nulla.', }; 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 bba9564..a4bb5ca 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -1232,4 +1232,10 @@ export default { 'hosts.approve': "承認", 'hosts.refuse': "拒否", 'hosts.online': "オンライン", + + // Worded by the desktop main process for its own dialogs (main.js). + 'native.attach_confirm': 'このコンピューターでグループ「{name}」をホストし、フォルダー {path} をメンバーと共有しますか?', + 'native.folder_confirm': 'このコンピューターでホストしているグループのメンバーとフォルダー {path} を共有しますか?このフォルダーはフォルダー選択画面で選ばれたものではありません。', + 'native.node_account_confirm': 'このコンピューターの node は {current} 用に設定されています。代わりに {next} 用に設定しますか?{current} のためにホストしているグループは提供されなくなります。', + 'native.declined': 'キャンセルしました。何も変更されていません。', }; 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 87e5b87..2fdf236 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -1250,4 +1250,10 @@ export default { 'hosts.approve': "Goedkeuren", 'hosts.refuse': "Weigeren", 'hosts.online': "online", + + // Worded by the desktop main process for its own dialogs (main.js). + 'native.attach_confirm': 'De groep "{name}" op deze computer hosten en de map {path} met de leden delen?', + 'native.folder_confirm': 'De map {path} delen met de leden van een groep die op deze computer wordt gehost? Hij is niet gekozen in de mapkiezer.', + 'native.node_account_confirm': 'De node op deze computer is ingesteld voor {current}. In plaats daarvan instellen voor {next}? Hij stopt dan met het aanbieden van de groepen die hij voor {current} host.', + 'native.declined': 'Geannuleerd — er is niets gewijzigd.', }; 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 6d81b25..0530b79 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -1276,4 +1276,10 @@ export default { 'hosts.approve': "Akceptuj", 'hosts.refuse': "Odrzuć", 'hosts.online': "online", + + // Worded by the desktop main process for its own dialogs (main.js). + 'native.attach_confirm': 'Hostować grupę „{name}" na tym komputerze i udostępnić jej członkom folder {path}?', + 'native.folder_confirm': 'Udostępnić folder {path} członkom grupy hostowanej na tym komputerze? Nie został wybrany w oknie wyboru folderu.', + 'native.node_account_confirm': 'Node na tym komputerze jest skonfigurowany dla {current}. Skonfigurować go zamiast tego dla {next}? Przestanie obsługiwać grupy, które hostuje dla {current}.', + 'native.declined': 'Anulowano — nic nie zostało zmienione.', }; 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 7b12ea5..d2be432 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 @@ -1235,4 +1235,10 @@ export default { 'hosts.approve': "Aprovar", 'hosts.refuse': "Recusar", 'hosts.online': "online", + + // Worded by the desktop main process for its own dialogs (main.js). + 'native.attach_confirm': 'Hospedar o grupo "{name}" neste computador e compartilhar a pasta {path} com os membros?', + 'native.folder_confirm': 'Compartilhar a pasta {path} com os membros de um grupo hospedado neste computador? Ela não foi escolhida no seletor de pastas.', + 'native.node_account_confirm': 'O node deste computador está configurado para {current}. Configurá-lo para {next} em vez disso? Ele deixará de servir os grupos que hospeda para {current}.', + 'native.declined': 'Cancelado — nada foi alterado.', }; 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 9f3c902..c994780 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 @@ -1221,4 +1221,10 @@ export default { 'hosts.approve': "批准", 'hosts.refuse': "拒绝", 'hosts.online': "在线", + + // Worded by the desktop main process for its own dialogs (main.js). + 'native.attach_confirm': '在这台电脑上托管群组“{name}”,并与其成员共享文件夹 {path}?', + 'native.folder_confirm': '与这台电脑上托管的群组成员共享文件夹 {path}?该文件夹不是在文件夹选择器中选择的。', + 'native.node_account_confirm': '这台电脑上的 node 已为 {current} 设置。改为为 {next} 设置吗?它将不再为 {current} 提供其托管的群组。', + 'native.declined': '已取消,未做任何更改。', }; 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 7fd7ab1..f940934 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/node-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/node-page.js @@ -10,7 +10,7 @@ import { HUB } from './hub-client.js'; // ── Node management (D5) ──────────────────────────────────────────────────── // // Electron-only: talks to the local node daemon via its loopback HTTP API -// (platform.node.call), not over MNP/WebRTC. The MNP protocol types remain +// (platform.node.op), not over MNP/WebRTC. The MNP protocol types remain // for potential future browser-side use. // true / false from a node that has read its roster, null from one that has @@ -163,8 +163,8 @@ function NodeServicePanel({ onChanged, token, username }) { </div>`; } -async function nodeCall(method, path, body) { - return platform.node.call(method, path, body); +async function nodeOp(name, args) { + return platform.node.op(name, args); } // Hand a generated file to the user: native Save As on the desktop, a blob @@ -237,11 +237,11 @@ export function NodePage({ groups, token, username }) { setStatus('error'); return; } - const result = await nodeCall('GET', '/api/groups'); + const result = await nodeOp('groups'); setNodeGroups(result.groups || []); setOperatorPaired(pairedFrom(result)); setNodeSettings(result.settings || null); - try { setNodeInfo(await nodeCall('GET', '/api/status')); } catch {} + try { setNodeInfo(await nodeOp('status')); } catch {} setStatus('connected'); } catch (err) { setError(platform.bridgeMessage(err)); @@ -259,11 +259,11 @@ export function NodePage({ groups, token, username }) { const refresh = useCallback(async () => { try { - const result = await nodeCall('GET', '/api/groups'); + const result = await nodeOp('groups'); setNodeGroups(result.groups || []); setOperatorPaired(pairedFrom(result)); setNodeSettings(result.settings || null); - try { setNodeInfo(await nodeCall('GET', '/api/status')); } catch {} + try { setNodeInfo(await nodeOp('status')); } catch {} } catch {} }, []); @@ -283,8 +283,8 @@ export function NodePage({ groups, token, username }) { setBusy(true); setActionMsg(''); try { - await nodeCall('POST', `/api/groups/${groupId}/roots`, { path: chosen.path }); - await nodeCall('POST', '/api/reload'); + await nodeOp('addRoot', { groupId, path: chosen.path }); + await nodeOp('reload'); await refresh(); setActionMsg(t('node.root_added')); } catch (err) { @@ -299,8 +299,8 @@ export function NodePage({ groups, token, username }) { setBusy(true); setActionMsg(''); try { - await nodeCall('DELETE', `/api/groups/${groupId}/roots/${encodeURIComponent(rootName)}`); - await nodeCall('POST', '/api/reload'); + await nodeOp('removeRoot', { groupId, rootName }); + await nodeOp('reload'); await refresh(); setActionMsg(t('node.root_removed')); } catch (err) { @@ -313,7 +313,7 @@ export function NodePage({ groups, token, username }) { const loadRoster = useCallback(async (groupId) => { setBusy(true); try { - const result = await nodeCall('GET', `/api/roster?group_id=${groupId}`); + const result = await nodeOp('roster', { groupId }); setRoster(result); setRosterGroup(groupId); } catch (err) { @@ -328,7 +328,7 @@ export function NodePage({ groups, token, username }) { setBusy(true); setActionMsg(''); try { - await nodeCall('POST', `/api/members/${userId}/unpin`); + await nodeOp('unpinMember', { userId }); setActionMsg(t('node.unpin_done')); if (rosterGroup) await loadRoster(rosterGroup); } catch (err) { @@ -338,12 +338,13 @@ export function NodePage({ groups, token, username }) { } }, [rosterGroup, loadRoster]); + // No confirmation drawn here: replacing the key is asked natively by the + // app itself (node:op), which a script in this page cannot answer. const rotateGek = useCallback(async (groupId) => { - if (!await ask(t('node.gek_rotate_confirm'))) return; setBusy(true); setActionMsg(''); try { - await nodeCall('POST', `/api/groups/${groupId}/gek?rotate=true`); + await nodeOp('initGek', { groupId, rotate: true }); setActionMsg(t('node.gek_rotated')); } catch (err) { setActionMsg(platform.bridgeMessage(err)); @@ -355,7 +356,7 @@ export function NodePage({ groups, token, username }) { const loadDenylist = useCallback(async () => { setBusy(true); try { - const result = await nodeCall('GET', '/api/denylist'); + const result = await nodeOp('denylist'); setDenylist(result); setShowDenylist(true); } catch (err) { @@ -366,12 +367,11 @@ export function NodePage({ groups, token, username }) { }, []); const clearDenylist = useCallback(async (subject) => { - const label = subject || t('node.denylist_clear_all'); - if (!await ask(t('node.denylist_clear_confirm', { subject: label }))) return; + // Asked natively by the app (node:op): it re-admits whoever it kept out. setBusy(true); setActionMsg(''); try { - await nodeCall('POST', `/api/denylist/clear?subject=${encodeURIComponent(subject)}`); + await nodeOp('clearDenylist', { subject }); setActionMsg(t('node.denylist_cleared')); await loadDenylist(); } catch (err) { @@ -386,8 +386,8 @@ export function NodePage({ groups, token, username }) { setBusy(true); setActionMsg(''); try { - await nodeCall('POST', '/api/groups/detach', { name }); - await nodeCall('POST', '/api/reload'); + await nodeOp('detachGroup', { name }); + await nodeOp('reload'); setActionMsg(t('node.detached')); await refresh(); } catch (err) { @@ -401,7 +401,7 @@ export function NodePage({ groups, token, username }) { setBusy(true); setActionMsg(''); try { - await nodeCall('POST', '/api/reload'); + await nodeOp('reload'); setActionMsg(t('node.reloaded')); await refresh(); } catch (err) { @@ -416,7 +416,7 @@ export function NodePage({ groups, token, username }) { setSavingSettings(true); setActionMsg(''); try { - await nodeCall('PUT', '/api/node-settings', editSettings); + await nodeOp('setNodeSettings', { settings: editSettings }); setNodeSettings({ ...editSettings }); setActionMsg(t('node.settings_saved')); } catch (err) { @@ -431,7 +431,7 @@ export function NodePage({ groups, token, username }) { setSavingStun(true); setActionMsg(''); try { - await nodeCall('PUT', '/api/node-settings', { stun_servers: editStun }); + await nodeOp('setNodeSettings', { settings: { stun_servers: editStun } }); setNodeSettings(s => s ? { ...s, stun_servers: [...editStun] } : s); setActionMsg(t('node.stun_saved')); } catch (err) { @@ -486,7 +486,7 @@ export function NodePage({ groups, token, username }) { setSavingIce(true); setActionMsg(''); try { - await nodeCall('PUT', '/api/node-settings', { ice_interfaces: editIce }); + await nodeOp('setNodeSettings', { settings: { ice_interfaces: editIce } }); setNodeSettings(s => s ? { ...s, ice_interfaces: [...editIce] } : s); setActionMsg(t('node.ice_saved')); } catch (err) { @@ -521,7 +521,7 @@ export function NodePage({ groups, token, username }) { setPairBusy(true); setPairStatus(''); try { - const result = await nodeCall('POST', '/api/operator/pair'); + const result = await nodeOp('pairOperator'); if (result && result.code) { await platform.node.setPairingCode(result.code); } @@ -538,7 +538,7 @@ export function NodePage({ groups, token, username }) { const loadPeers = useCallback(async () => { setBusy(true); try { - const r = await nodeCall('GET', '/api/peers'); + const r = await nodeOp('peers'); setPeers(r.peers || []); } catch (err) { setActionMsg(platform.bridgeMessage(err)); @@ -551,9 +551,8 @@ export function NodePage({ groups, token, username }) { setBusy(true); try { const offset = auditPage * auditPageSize; - let path = `/api/audit?limit=${auditPageSize}&offset=${offset}`; - if (auditEvent) path += `&event=${encodeURIComponent(auditEvent)}`; - const r = await nodeCall('GET', path); + const r = await nodeOp('audit', { limit: auditPageSize, offset, + event: auditEvent || undefined }); setAudit(r.entries || []); setAuditHasMore(!!r.has_more); } catch (err) { @@ -572,12 +571,11 @@ export function NodePage({ groups, token, username }) { // mid-export (which shifts rows to a higher offset) cannot duplicate one. const PAGE = 1000; const MAX_PAGES = 1000; // 1M-row stop, so a bug cannot spin forever - const evq = auditEvent ? `&event=${encodeURIComponent(auditEvent)}` : ''; const seen = new Set(); const rows = []; for (let page = 0; page < MAX_PAGES; page++) { - const r = await nodeCall( - 'GET', `/api/audit?limit=${PAGE}&offset=${page * PAGE}${evq}`); + const r = await nodeOp('audit', { limit: PAGE, offset: page * PAGE, + event: auditEvent || undefined }); const batch = r.entries || []; for (const e of batch) { if (!seen.has(e.id)) { seen.add(e.id); rows.push(e); } @@ -613,7 +611,7 @@ export function NodePage({ groups, token, username }) { const loadCache = useCallback(async () => { setBusy(true); try { - const r = await nodeCall('GET', '/api/index-cache'); + const r = await nodeOp('indexCache'); setCacheCount(r.count ?? 0); } catch (err) { setActionMsg(platform.bridgeMessage(err)); @@ -626,7 +624,7 @@ export function NodePage({ groups, token, username }) { setBusy(true); setActionMsg(''); try { - const r = await nodeCall('POST', '/api/index-cache/prune'); + const r = await nodeOp('pruneIndexCache'); setCacheCount(r.kept ?? null); setActionMsg(t('node.maintenance_pruned', { n: r.removed ?? 0 })); } catch (err) { @@ -639,7 +637,7 @@ export function NodePage({ groups, token, username }) { const loadNodeRoster = useCallback(async () => { setBusy(true); try { - const r = await nodeCall('GET', '/api/roster'); + const r = await nodeOp('roster'); setNodeRoster(r); } catch (err) { setActionMsg(platform.bridgeMessage(err)); @@ -653,7 +651,7 @@ export function NodePage({ groups, token, username }) { setBusy(true); setActionMsg(''); try { - await nodeCall('POST', `/api/members/${userId}/unpin`); + await nodeOp('unpinMember', { userId }); setActionMsg(t('node.unpin_done')); await loadNodeRoster(); } catch (err) { @@ -668,7 +666,7 @@ export function NodePage({ groups, token, username }) { setUnlinkBusy(true); setActionMsg(''); try { - await nodeCall('DELETE', '/api/unlink'); + await nodeOp('unlink'); setActionMsg(t('node.unlink_done')); await refresh(); } catch (err) { diff --git a/packages/meshbay-hub/src/meshbay_hub/static/platform.js b/packages/meshbay-hub/src/meshbay_hub/static/platform.js index a3fb80b..8bf09b4 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/platform.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/platform.js @@ -67,30 +67,15 @@ export const capabilities = { }; /** - * Where the identity keys live. + * The app's secret store, as far as the page may know it. * - * In a browser: exactly where they live today — IndexedDB and sessionStorage, - * with the keypair bundle on the node as the way a second browser recovers - * them, which is finding C4 and is the reason the app exists. - * - * In the app: the OS keychain, and no bundle is stored anywhere. That is what - * closes C4 for a native device — unconditionally for that device, and for the - * account only once it stops signing in from a browser too. + * The store is the main process's (the OS keychain through safeStorage) and it + * holds the device's hub key, which the page is never handed. The page used to + * be able to read and write it by name; nothing here did, and that was a way to + * both read the key and replace it. What is left is whether the OS protects it. */ export const secrets = { available: Boolean(bridge && bridge.secrets), - async get(name) { - if (!bridge || !bridge.secrets) return null; - return bridge.secrets.get(name); - }, - async set(name, value) { - if (!bridge || !bridge.secrets) return false; - return bridge.secrets.set(name, value); - }, - async clear(name) { - if (!bridge || !bridge.secrets) return false; - return bridge.secrets.clear(name); - }, /** * Whether the OS is really protecting them. * @@ -257,9 +242,15 @@ export const node = { if (!bridge || !bridge.node) throw new Error('Node bridge not available'); return bridge.node.start(opts); }, - async call(method, path, body) { + /** + * One of the local node's operations, by name (`NODE_OPS` in the desktop + * client's main.js). The page never names a route: the main process checks + * the arguments, builds the request, and asks the person itself before + * anything that widens what the node shares. + */ + async op(name, args) { if (!bridge || !bridge.node) throw new Error('Node bridge not available'); - return bridge.node.call(method, path, body); + return bridge.node.op(name, args); }, async pairingCode() { return bridge && bridge.node ? bridge.node.pairingCode() : null; @@ -344,7 +335,7 @@ export async function watchIndexProgress(groupId, onUpdate, { intervalMs = 500 } for (;;) { let status; try { - status = await node.call('GET', `/api/groups/${groupId}/index-status`); + status = await node.op('groupIndexStatus', { groupId }); } catch { // The node went away mid-poll — stop rather than spin forever; the // caller's own connection-status handling already covers that case. @@ -376,12 +367,12 @@ export async function waitForGroupHosted(groupId, onProgress, const deadline = Date.now() + timeoutMs; for (;;) { try { - const status = await node.call('GET', `/api/groups/${groupId}/index-status`); + const status = await node.op('groupIndexStatus', { groupId }); if (onProgress) onProgress(status); } catch { /* keep waiting — the loopback API can be momentarily busy */ } try { - const list = await node.call('GET', '/api/groups'); + const list = await node.op('groups'); if (Array.isArray(list.groups) && list.groups.some((g) => g.id === groupId)) return; } catch { /* keep waiting */ } @@ -420,7 +411,7 @@ export async function waitForRootsIndexed(groupId, onProgress, for (;;) { let status; try { - status = await node.call('GET', `/api/groups/${groupId}/index-status`); + status = await node.op('groupIndexStatus', { groupId }); } catch { return; // the node went away mid-poll — same stance as watchIndexProgress } @@ -507,9 +498,19 @@ export async function setTrayLabels(labels) { return bridge.setTrayLabels(labels); } +/** + * Tell the app which language the interface is in. The confirmations its main + * process draws for itself are worded from the same catalogues, which it reads + * from the packaged files -- only the code crosses the bridge. + */ +export async function setUiLocale(code) { + if (!bridge || !bridge.setLocale) return false; + return bridge.setLocale(code); +} + export default { isNative, hubBase, capabilities, secrets, nativeSave, apiFetch, device, bridgeMessage, folder, rootPicker, node, - cast, minimizeToTray, setTrayLabels }; + cast, minimizeToTray, setTrayLabels, setUiLocale }; // Also a global, because `transport.js` is loaded as a classic script — it // predates the module graph and exposes `MeshBayTransport` the same way. The @@ -519,5 +520,5 @@ if (typeof window !== 'undefined') { window.MeshBayPlatform = { isNative, hubBase, capabilities, secrets, nativeSave, apiFetch, device, bridgeMessage, folder, rootPicker, node, - cast, minimizeToTray, setTrayLabels }; + cast, minimizeToTray, setTrayLabels, setUiLocale }; } |