aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-05 11:27:20 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-05 11:27:20 +0200
commit28752696f376eb11feb686580a435b166750a723 (patch)
treea879254f126d6d83096c7a7c3a181a151e3be7f2 /packages/meshbay-hub
parent6cdc6016d72dcfb7530ac38a8fa92232418ac305 (diff)
downloadmeshbay-28752696f376eb11feb686580a435b166750a723.tar.gz
fix: list as members only the accounts the node has admitted
The Members list showed the hub's membership, which an account gains when it accepts the invitation or redeems a link, before it has presented its code to the node. The node's roster is the authority (MESHBAY_DESIGN.md §3.4), so the list now crosses the hub's members with the sealed group roster the node already sends every connected member. An account the node has not admitted yet is shown to the owner alone, as waiting for its code, with the Remove button; other members do not see it. When the roster cannot be read, the hub's list is shown as before. groupRoster() takes { fresh: true } so the page sees who joined since the connection opened. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-settings.js42
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport-devices.js5
-rw-r--r--packages/meshbay-hub/tests/test_spa_ordering.py4
13 files changed, 55 insertions, 6 deletions
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 29aa7eb..3ffa524 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
@@ -783,6 +783,27 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
useEffect(() => { loadMembers(); }, [loadMembers]);
+ // Who the node has admitted to this group. The hub counts someone a member
+ // once they accept the invitation; the node, once they present their code.
+ // Until then they are not in the group, and are listed as waiting, to the
+ // owner only. Null when the roster cannot be read: the hub's list is shown.
+ const [admitted, setAdmitted] = useState(null);
+ useEffect(() => {
+ const transport = transportRef.current;
+ if (!connected || !transport || !transport.connected) { setAdmitted(null); return; }
+ let cancelled = false;
+ transport.groupRoster({ fresh: true })
+ .then((roster) => { if (!cancelled) setAdmitted(new Set(roster.byAccount.keys())); })
+ .catch(() => { if (!cancelled) setAdmitted(null); });
+ return () => { cancelled = true; };
+ }, [connected, transportRef, members]);
+ const joined = admitted
+ ? members.filter(m => m.user_id === adminId || admitted.has(m.user_id))
+ : members;
+ const awaitingCode = admitted
+ ? members.filter(m => m.user_id !== adminId && !admitted.has(m.user_id))
+ : [];
+
const ownsGroup = Boolean(group && group.is_admin);
const loadHosts = useCallback(() => {
if (!ownsGroup) return;
@@ -1340,7 +1361,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
</${CollapsibleSection}>
`}
- <${CollapsibleSection} title=${`${t('group.tab_members')} (${members.length})`}>
+ <${CollapsibleSection} title=${`${t('group.tab_members')} (${joined.length})`}>
<table class="admin-table">
<thead>
<tr>
@@ -1350,7 +1371,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
</tr>
</thead>
<tbody>
- ${members.map(m => html`
+ ${joined.map(m => html`
<tr key=${m.user_id}>
<td>${m.username}</td>
<td>
@@ -1372,6 +1393,21 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
</td>
</tr>
`)}
+ ${isAdmin && awaitingCode.map(m => html`
+ <tr key=${m.user_id}>
+ <td>${m.username}</td>
+ <td><span class="badge">${t('members.awaiting_code')}</span></td>
+ <td class="admin-actions">
+ <button class="admin-btn danger" disabled=${removing === m.user_id}
+ onClick=${async () => {
+ if (!await ask(t('members.remove_confirm', { user: m.username }))) return;
+ removeMember(m);
+ }}>
+ ${removing === m.user_id ? '...' : t('members.remove')}
+ </button>
+ </td>
+ </tr>
+ `)}
${invited.map(m => html`
<tr key=${m.user_id}>
<td>${m.username}</td>
@@ -1391,7 +1427,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
`)}
</tbody>
</table>
- ${isAdmin && members.length > 1 && html`
+ ${isAdmin && joined.length > 1 && html`
<p class="settings-hint">${t('members.remove_hint')}</p>
`}
</${CollapsibleSection}>
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 16061af..5cc9968 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -1237,6 +1237,7 @@ export default {
'home.accept': "Annehmen",
'home.decline': "Ablehnen",
'members.invited': "eingeladen",
+ 'members.awaiting_code': "waiting for their code",
'hosts.title': "Hosts",
'hosts.hint': "Ihre eigenen Nodes stellen diese Gruppe ohne Rückfrage bereit. Ein anderer Node tut es erst, wenn Sie ihn hier genehmigen; ein Node, der anfragt, steht unten.",
'hosts.none': "Kein anderer Node hat angefragt, diese Gruppe bereitzustellen.",
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 73c6217..5aa53b7 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -1218,6 +1218,7 @@ export default {
'home.accept': "Accept",
'home.decline': "Decline",
'members.invited': "invited",
+ 'members.awaiting_code': "waiting for their code",
'hosts.title': "Hosts",
'hosts.hint': "Your own nodes serve this group without asking. Another node serves it only once you approve it here; a node that asks is listed below.",
'hosts.none': "No other node has asked to host this group.",
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 7beead3..6558a46 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -1231,6 +1231,7 @@ export default {
'home.accept': "Aceptar",
'home.decline': "Rechazar",
'members.invited': "invitado",
+ 'members.awaiting_code': "waiting for their code",
'hosts.title': "Anfitriones",
'hosts.hint': "Sus propios nodes sirven este grupo sin preguntar. Otro node lo sirve solo cuando usted lo aprueba aquí; un node que lo pide aparece abajo.",
'hosts.none': "Ningún otro node ha pedido alojar este grupo.",
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 91f9253..5e4c0bf 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -1246,6 +1246,7 @@ export default {
'home.accept': "Accepter",
'home.decline': "Refuser",
'members.invited': "invité",
+ 'members.awaiting_code': "en attente du code",
'hosts.title': "Hôtes",
'hosts.hint': "Vos propres nodes servent ce groupe sans rien demander. Un autre node ne le sert qu'une fois approuvé ici ; un node qui le demande apparaît ci-dessous.",
'hosts.none': "Aucun autre node n'a demandé à héberger ce groupe.",
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 f558f5f..8772376 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -1245,6 +1245,7 @@ export default {
'home.accept': "Accetta",
'home.decline': "Rifiuta",
'members.invited': "invitato",
+ 'members.awaiting_code': "waiting for their code",
'hosts.title': "Host",
'hosts.hint': "I tuoi node servono questo gruppo senza chiedere. Un altro node lo serve solo dopo che lo approvi qui; un node che lo chiede compare qui sotto.",
'hosts.none': "Nessun altro node ha chiesto di ospitare questo gruppo.",
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 f03b3e6..fa34470 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -1229,6 +1229,7 @@ export default {
'home.accept': "承諾",
'home.decline': "辞退",
'members.invited': "招待中",
+ 'members.awaiting_code': "waiting for their code",
'hosts.title': "ホスト",
'hosts.hint': "あなた自身の node は確認なしでこのグループを提供します。他の node は、ここで承認した後にのみ提供します。申請した node は下に表示されます。",
'hosts.none': "このグループのホストを申請した node は他にありません。",
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 130213c..fa0841d 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -1247,6 +1247,7 @@ export default {
'home.accept': "Accepteren",
'home.decline': "Weigeren",
'members.invited': "uitgenodigd",
+ 'members.awaiting_code': "waiting for their code",
'hosts.title': "Hosts",
'hosts.hint': "Uw eigen nodes bedienen deze groep zonder te vragen. Een andere node doet dat pas nadat u hem hier goedkeurt; een node die erom vraagt, staat hieronder.",
'hosts.none': "Geen andere node heeft gevraagd deze groep te hosten.",
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 b71094f..9de29ea 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -1273,6 +1273,7 @@ export default {
'home.accept': "Akceptuj",
'home.decline': "Odrzuć",
'members.invited': "zaproszony",
+ 'members.awaiting_code': "waiting for their code",
'hosts.title': "Hosty",
'hosts.hint': "Twoje własne node'y obsługują tę grupę bez pytania. Inny node robi to dopiero po Twojej akceptacji tutaj; node, który o to prosi, jest widoczny poniżej.",
'hosts.none': "Żaden inny node nie prosił o hostowanie tej grupy.",
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 fa5be5f..908a8db 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
@@ -1232,6 +1232,7 @@ export default {
'home.accept': "Aceitar",
'home.decline': "Recusar",
'members.invited': "convidado",
+ 'members.awaiting_code': "waiting for their code",
'hosts.title': "Hosts",
'hosts.hint': "Seus próprios nodes servem este grupo sem perguntar. Outro node só o serve depois que você o aprova aqui; um node que pede aparece abaixo.",
'hosts.none': "Nenhum outro node pediu para hospedar este grupo.",
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 eee91a9..d6fe782 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
@@ -1218,6 +1218,7 @@ export default {
'home.accept': "接受",
'home.decline': "拒绝",
'members.invited': "已邀请",
+ 'members.awaiting_code': "waiting for their code",
'hosts.title': "主机",
'hosts.hint': "你自己的 node 无需询问即可提供此群组。其他 node 只有在你于此处批准后才会提供;提出申请的 node 列在下方。",
'hosts.none': "没有其他 node 申请托管此群组。",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport-devices.js b/packages/meshbay-hub/src/meshbay_hub/static/transport-devices.js
index 8dbb6c0..f079f80 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport-devices.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport-devices.js
@@ -23,7 +23,10 @@ extendTransport(class {
* at first sight, where there is nothing to compare against — that boundary
* is `docs/MESHBAY_DESIGN.md` §3.2's and does not move.
*/
- async groupRoster() {
+ async groupRoster({ fresh = false } = {}) {
+ // `fresh`: read it again rather than keep this connection's copy, for a
+ // page that lists the members and must show who joined since.
+ if (fresh && !this._rosterInFlight) this._roster = null;
if (this._roster) return this._roster;
if (this._rosterInFlight) return this._rosterInFlight;
diff --git a/packages/meshbay-hub/tests/test_spa_ordering.py b/packages/meshbay-hub/tests/test_spa_ordering.py
index 6f28aaa..44f98db 100644
--- a/packages/meshbay-hub/tests/test_spa_ordering.py
+++ b/packages/meshbay-hub/tests/test_spa_ordering.py
@@ -162,7 +162,7 @@ def _component(name: str) -> str:
def test_the_group_settings_panel_renders_what_it_owns():
panel = _component("GroupSettingsPanel")
- assert "members.map(" in panel, "the member list is not rendered"
+ assert "joined.map(" in panel, "the member list is not rendered"
assert "onSubmit=${doInvite}" in panel, "the invite form is not rendered"
assert "onSubmit=${doPair}" in panel, "the pairing form is not rendered"
assert "device.mine_title" in panel, "the devices section is not rendered"
@@ -175,7 +175,7 @@ def test_the_roster_comes_last():
order is for — asked for in those terms.
"""
panel = _component("GroupSettingsPanel")
- listing = panel.index("members.map(")
+ listing = panel.index("joined.map(")
for name, marker in (("the invite form", "onSubmit=${doInvite}"),
("the pairing form", "onSubmit=${doPair}"),
("the devices section", "device.mine_title"),