aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/app.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-24 14:33:20 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-24 14:33:20 +0200
commit0b0da86f1f9d6f0b1a27b5e1e1658c42de9f356a (patch)
treeba8daf5dcf050d44b7b0e766babbfda8fadac59f /packages/meshbay-hub/src/meshbay_hub/static/app.js
parent6af05abf410bbd038ce7fa6915a659defc509071 (diff)
downloadmeshbay-0b0da86f1f9d6f0b1a27b5e1e1658c42de9f356a.tar.gz
feat(node,hub): season-specific overviews, manual TMDB match correction, and wizard polish
Two operator-facing fixes for a real 3-season show whose automatic TMDB match was wrong at the show level: per-season overview/air_date tabs in the detail modal (falling back to the show-level text when a season's own is empty), and a "Fix match…" search-and-correct affordance that re-resolves every file sharing the corrected show's display_title. New signed op OP_TMDB_OVERRIDE and two read-only pairs (season_meta_req/resp, tmdb_search_req/resp), MNP_VERSION 0.5 -> 0.6. Also: the create-group wizard gets a spinning indexing indicator and an app-selection step, group settings default the TMDB language to the operator's own locale (never as a global default), and a file renamed mid-session now re-triggers title parsing instead of being silently skipped by the enrichment dedup guard. Fixes two bugs found during this work: the search overlay's z-index lost to the base video-overlay class and rendered invisibly, and season_meta's own empty overview didn't fall back to the show-level one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LAmyXtc6dAADsH23ydXQpY
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/app.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js66
1 files changed, 56 insertions, 10 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js
index 58ffe8e..f9bcd43 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js
@@ -839,6 +839,15 @@ function CreateGroupWizard({ token, username, onCreated }) {
const [joinPolicy, setJoinPolicy] = useState('invite');
const [roots, setRoots] = useState([]);
const [uploadIdx, setUploadIdx] = useState(0);
+ // Every registered app, on by default — narrowing this down here means
+ // members never briefly see one the operator meant to leave off, the way
+ // toggling it afterward from Settings would.
+ const [enabledApps, setEnabledApps] = useState(() => APPS.map(a => a.key));
+ const toggleWizardApp = useCallback((key) => {
+ setEnabledApps(prev => prev.includes(key)
+ ? prev.filter(k => k !== key)
+ : [...prev, key]);
+ }, []);
// Step 2 progress
const [setupSteps, setSetupSteps] = useState([]);
@@ -915,8 +924,13 @@ function CreateGroupWizard({ token, username, onCreated }) {
const steps = [
{ label: t('wizard.step_create_hub'), status: 'pending' },
{ label: t('wizard.step_attach'), status: 'pending' },
- { label: t('wizard.step_index'), status: 'pending' },
];
+ // Only a step at all when it does something — the common case (every
+ // app left on, the default) has nothing to set and no reason to show a
+ // step for it.
+ if (enabledApps.length < APPS.length)
+ steps.push({ label: t('wizard.step_apps'), status: 'pending' });
+ steps.push({ label: t('wizard.step_index'), status: 'pending' });
if (roots.length > 1)
steps.push({ label: t('wizard.step_add_roots'), status: 'pending' });
steps.push({ label: t('wizard.step_gek'), status: 'pending' });
@@ -980,11 +994,24 @@ function CreateGroupWizard({ token, username, onCreated }) {
update('done');
advance();
- // 3. Wait for the node's own initial scan of this group to finish —
+ // 3. Narrow the enabled apps down, if the operator unchecked any —
+ // before the scan below, so a member who joins while it is still
+ // running never briefly sees an app meant to be off. Same
+ // not-hosted-yet race as steps 4+ below: the group is attached, but
+ // may not have reached groups_ctx yet.
+ if (enabledApps.length < APPS.length) {
+ update('running');
+ await withRetry(() => platform.node.call(
+ 'PUT', `/api/groups/${gid}/apps`, { apps: enabledApps }));
+ update('done');
+ advance();
+ }
+
+ // 4. Wait for the node's own initial scan of this group to finish —
// the group is not usable for anything below (extra roots, GEK) until
// this finishes, so nobody lands on a page that looks broken, or hits
// a "not configured" error from racing ahead of it. Can take tens of
- // minutes on a slow disk (see the StarWars benchmark) — the node
+ // minutes on a slow disk with a large library — the node
// keeps scanning on its own either way (test_hot_reload_survives_
// client_close.py); this step is only about not lying about it.
update('running');
@@ -992,7 +1019,7 @@ function CreateGroupWizard({ token, username, onCreated }) {
update('done');
advance();
- // 4. Add extra roots (if >1)
+ // 5. Add extra roots (if >1)
if (roots.length > 1) {
update('running');
for (let i = 0; i < roots.length; i++) {
@@ -1007,13 +1034,13 @@ function CreateGroupWizard({ token, username, onCreated }) {
advance();
}
- // 5. GEK init
+ // 6. GEK init
update('running');
await withRetry(() => platform.node.call('POST', `/api/groups/${gid}/gek`));
update('done');
advance();
- // 6. Generate pairing code
+ // 7. Generate pairing code
update('running');
const pairResult = await platform.node.call('POST', '/api/operator/pair');
if (pairResult && pairResult.code) {
@@ -1031,7 +1058,7 @@ function CreateGroupWizard({ token, username, onCreated }) {
update('error');
setSetupError(platform.bridgeMessage(err));
}
- }, [name, description, joinPolicy, roots, uploadIdx, token, onCreated]);
+ }, [name, description, joinPolicy, roots, uploadIdx, enabledApps, token, onCreated]);
// Step 0: Node detection
if (step === 0) {
@@ -1065,7 +1092,7 @@ function CreateGroupWizard({ token, username, onCreated }) {
// Step 1: Group details + directories
if (step === 1) {
- const canProceed = name.trim() && roots.length > 0;
+ const canProceed = name.trim() && roots.length > 0 && enabledApps.length > 0;
return html`<div class="page-content">
<h2>${t('wizard.title')}</h2>
${error && html`<div class="error-msg" style="margin-bottom:16px">${error}</div>`}
@@ -1112,6 +1139,25 @@ function CreateGroupWizard({ token, username, onCreated }) {
</div>
<div class="settings-section">
+ <h3 class="settings-heading">${t('members.apps_title')}</h3>
+ <p style="font-size:0.85em;color:var(--text-dim);margin-bottom:8px">
+ ${t('members.apps_hint')}</p>
+ <ul class="apps-toggle-list">
+ ${APPS.map(a => html`
+ <li key=${a.key} class="settings-row">
+ <label class="settings-label">
+ <input type="checkbox" checked=${enabledApps.includes(a.key)}
+ onChange=${() => toggleWizardApp(a.key)} />
+ ${' '}${t(a.labelKey)}
+ </label>
+ </li>
+ `)}
+ </ul>
+ ${enabledApps.length === 0 && html`
+ <p class="error-msg">${t('members.apps_need_one')}</p>`}
+ </div>
+
+ <div class="settings-section">
<h3 class="settings-heading">${t('wizard.directories')}</h3>
<p style="font-size:0.85em;color:var(--text-dim);margin-bottom:8px">
${t('wizard.directories_hint')}</p>
@@ -1161,8 +1207,8 @@ function CreateGroupWizard({ token, username, onCreated }) {
${setupSteps.map((s, i) => html`
<div class="wizard-step wizard-step-${s.status}" key=${i}>
<span class="wizard-step-icon">
- ${s.status === 'done' ? '✓' :
- s.status === 'running' ? '●' :
+ ${s.status === 'running' ? html`<span class="spinner"></span>` :
+ s.status === 'done' ? '✓' :
s.status === 'error' ? '✗' : '○'}
</span>
<span>${s.label}</span>