aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-02 00:38:27 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-02 00:38:27 +0200
commitf7917bdde37fe089485bb2b65c5504315dcc9c56 (patch)
tree92c4f7d25779609a77e3ae0edd1cef2cbc7392ff /packages/meshbay-hub/src/meshbay_hub/static
parentd6713a4c7b3f94a3b63e0c0f78e7939fa7eae4e8 (diff)
downloadmeshbay-f7917bdde37fe089485bb2b65c5504315dcc9c56.tar.gz
fix(hub): fall back to the group's first app when the landing tab is absent
A group could open on a tab that rendered nothing: no panel, no tab shown active, and nothing on screen to explain it. The landing tab is chosen at mount from a preference -- default_tab for the group, else the account-wide one, else 'chat'. Which applications the group runs comes from the node, in the handshake ack, several awaits later. A preference is a preference, not a promise that the app exists here, so the two disagree in two ordinary cases: the group has Chat disabled while 'chat' is everyone's default, or the reader prefers an app this group does not run. `apps.map(a => tab === a.key && ...)` then matches nothing. The first app the group does offer answers both. Two more cases come free: a preference naming an app that no longer exists, and an operator disabling the app someone is currently looking at -- enabledApps changes live over apps_enabled, and being moved to a working tab beats staring at an empty panel. Settings is exempt: it is not an application, and the create-group wizard lands on it deliberately. `const apps` moves above the effect that reads it; a const further down would be in its temporal dead zone, which is the hook-ordering trap already recorded in CLAUDE.md. tests/harness/group_tab_probe.py renders the real GroupPage against a stub node answering a chosen enabled_apps and reads the tab bar back, over five cases. With the fix reverted the three fallback cases report no active tab at all and four of the six tests fail; the two that pass either way are the ones that must not change -- a group running everything, and a preference the group does honour (Videos stays selected, so the fallback has not become "always the first app"). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W8oRqEHhnKUr1NfmTVdcyL
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-page.js25
1 files changed, 24 insertions, 1 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
index 6b9c82a..7d13260 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
@@ -96,6 +96,30 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
// every registered app when a node predates the setting (or hasn't answered
// yet), so nothing disappears for an existing group.
const [enabledApps, setEnabledApps] = useState(null);
+ // Declared here rather than beside the render, because the effect below
+ // depends on it and a `const` further down would be in its temporal dead
+ // zone — the hook-ordering trap this codebase has already paid for.
+ const apps = visibleApps(enabledApps);
+
+ // The landing tab is chosen before the node has said which applications this
+ // group has, and a preference is a preference — not a promise that the app
+ // exists here. Two ways to land on a tab that renders nothing at all, with no
+ // tab shown active and no way to tell what went wrong: the group has Chat
+ // disabled while 'chat' is the default, or the reader's preferred app is one
+ // this group does not run. The first app the group *does* offer is the
+ // answer to both.
+ //
+ // Also covers an operator disabling the app someone is currently looking at:
+ // `enabledApps` changes live over `apps_enabled`, and being moved to a
+ // working tab beats being left staring at an empty panel.
+ //
+ // Settings is exempt: it is not an application, it is never in `apps`, and
+ // the create-group wizard lands on it deliberately.
+ useEffect(() => {
+ if (tab === 'settings') return;
+ if (!apps.length || apps.some(a => a.key === tab)) return;
+ setTab(apps[0].key);
+ }, [enabledApps, tab]);
// Reconcile interval / debounce currently in effect on the node — shown
// to the operator in Settings, not enforced from here (indexer.py owns
// that). Null until the handshake ack arrives.
@@ -532,7 +556,6 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
setPreviewEntry(entry);
}, [entries, onPlayQueue, onStopMusic]);
- const apps = visibleApps(enabledApps);
const commonProps = {
groupId, transportRef, gekRef, status, username,
entries, nodeDirs, nodeRoots, setEntries, setNodeDirs, setNodeRoots, applyIndex,