diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub')
18 files changed, 114 insertions, 39 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/middleware.py b/packages/meshbay-hub/src/meshbay_hub/api/middleware.py index bed7b54..3a2a5c3 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/middleware.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/middleware.py @@ -7,7 +7,11 @@ to mitigate credential stuffing and registration floods. """ from slowapi import Limiter -from slowapi.util import get_remote_address -# Rate limiter instance — mounted on the FastAPI app in app.py -limiter = Limiter(key_func=get_remote_address) +from meshbay_hub.api.netutil import client_ip + +# Rate limiter instance — mounted on the FastAPI app in app.py. +# Keyed on client_ip, not slowapi's get_remote_address: behind Caddy every +# request's peer is loopback, so the peer address put the whole internet in one +# bucket — ten node sign-ins a minute, shared by every node there is. +limiter = Limiter(key_func=client_ip) diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index 6b20a63..3101be7 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -924,8 +924,31 @@ function App() { .catch(() => {}); }, [user, notifDisabled]); + // Whether this account has a node linked, which is what shows the sidebar's + // Node section. Asked again whenever a node may just have been linked (the + // Create Group wizard), not only when the session changes -- the section + // stayed hidden until a reload after the first group -- and retried rather + // than left false by one failed request after a session blip. + const nodeKeyAskedForRef = useRef(null); + const refreshNodeKey = useCallback(() => { + nodeKeyAskedForRef.current = user; + if (!user || !platform.capabilities.nodeAdmin) return; + // A late answer, or a retry, must not land on a session that has changed. + const current = () => nodeKeyAskedForRef.current === user; + const ask = (attempt) => hubFetch(`/v1/users/${user.username}/pubkeys`, { token: user.token }) + .then(data => { if (current()) setHasNodeKey(Boolean(data.pk_node_ed25519)); }) + .catch(() => { + if (attempt < 3 && current()) setTimeout(() => ask(attempt + 1), 2000 * attempt); + }); + ask(1); + }, [user]); + useEffect(() => { - if (!user) { setGroups([]); setNotifications([]); setUnreadCount(0); setHasNodeKey(false); return; } + if (!user) { + nodeKeyAskedForRef.current = null; + setGroups([]); setNotifications([]); setUnreadCount(0); setHasNodeKey(false); + return; + } hubFetch('/v1/groups/mine', { token: user.token }) .then(data => setGroups(data.groups || [])) .catch(() => setGroups([])); @@ -935,11 +958,7 @@ function App() { if (prefs.notifications_disabled === 'true') setNotifDisabled(true); }) .catch(() => {}); - if (platform.capabilities.nodeAdmin) { - hubFetch(`/v1/users/${user.username}/pubkeys`, { token: user.token }) - .then(data => setHasNodeKey(Boolean(data.pk_node_ed25519))) - .catch(() => {}); - } + refreshNodeKey(); fetchNotifications(); }, [user]); @@ -1176,10 +1195,12 @@ function App() { } else if (route === '/create-group') { page = html`<${LazyCreateGroupPage} token=${user.token} username=${user.username} allowPublicGroups=${allowPublicGroups} + onNodeLinked=${refreshNodeKey} onCreated=${() => { hubFetch('/v1/groups/mine', { token: user.token }) .then(data => setGroups(data.groups || [])) .catch(() => {}); + refreshNodeKey(); }} />`; } else if (route === '/node' && platform.capabilities.nodeAdmin && hasNodeKey) { page = html`<${LazyNodePage} groups=${groups} token=${user.token} username=${user.username} />`; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js index 2164eae..09c4acf 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js @@ -344,6 +344,23 @@ function WelcomePitch() { `; } +// The sign-in page's dark gradient backdrop and frosted card, without the +// pitch beside it — so Register (and its verify/recovery/done steps) sits on +// the same background and reads in the same dark theme as Login. A lone +// `.welcome-side` is centred by `.welcome`'s `justify-content`. +function AuthShell({ children }) { + return html` + <div class="page-center"> + <div class="welcome-backdrop" aria-hidden="true"></div> + <div class="welcome"> + <div class="welcome-side"> + ${children} + </div> + </div> + </div> + `; +} + export function RegisterPage() { const [username, setUsername] = useState(''); const [email, setEmail] = useState(''); @@ -358,7 +375,9 @@ export function RegisterPage() { const [recoveryMnemonic, setRecoveryMnemonic] = useState(''); const [recoverySaved, setRecoverySaved] = useState(false); const [recoveryCopied, setRecoveryCopied] = useState(false); - const [emailRecovery, setEmailRecovery] = useState(true); + // Off by default: mailing the recovery key is opt-in — the key is shown on + // screen to save, and sending a copy is the user's own choice to make. + const [emailRecovery, setEmailRecovery] = useState(false); const captcha = useCaptcha(); const onSubmit = async (e) => { @@ -457,7 +476,7 @@ export function RegisterPage() { if (phase === 'done') { return html` - <div class="page-center"> + <${AuthShell}> <div class="card login-card"> <h2>${t('register.verified_title')}</h2> <p style="text-align:center; margin-bottom:16px; color:var(--text-secondary)"> @@ -467,7 +486,7 @@ export function RegisterPage() { <p style="text-align:center; margin-bottom:16px">${t('invite.after_register')}</p>`} <a href="#/login" style="display:block; text-align:center">${t('register.go_login')}</a> </div> - </div> + </${AuthShell}> `; } @@ -480,7 +499,7 @@ export function RegisterPage() { } catch { /* clipboard blocked — the text is on screen to copy by hand */ } }; return html` - <div class="page-center"> + <${AuthShell}> <div class="card login-card"> <h2>${t('register.recovery_title')}</h2> <p style="margin-bottom:12px; color:var(--text-secondary)"> @@ -509,13 +528,13 @@ export function RegisterPage() { ${t('register.recovery_continue')} </button> </div> - </div> + </${AuthShell}> `; } if (phase === 'verify') { return html` - <div class="page-center"> + <${AuthShell}> <div class="card login-card"> <h2>${t('register.success_title')}</h2> <p style="text-align:center; margin-bottom:16px; color:var(--text-secondary)"> @@ -538,12 +557,12 @@ export function RegisterPage() { ${t('register.resend_sent')}</span>`} </div> </div> - </div> + </${AuthShell}> `; } return html` - <div class="page-center"> + <${AuthShell}> <div class="card login-card"> <h2>${t('register.title')}</h2> <form onSubmit=${onSubmit}> @@ -585,7 +604,7 @@ export function RegisterPage() { ${t('register.has_account')} <a href="#/login">${t('register.login_link')}</a> </div> </div> - </div> + </${AuthShell}> `; } 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 85fe53c..7556010 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 @@ -124,7 +124,7 @@ function CreateGroupFormSimple({ token, onCreated, allowPublicGroups = true }) { `; } -function CreateGroupWizard({ token, username, onCreated, allowPublicGroups = true }) { +function CreateGroupWizard({ token, username, onCreated, onNodeLinked, allowPublicGroups = true }) { const [step, setStep] = useState(0); const [nodeStatus, setNodeStatus] = useState(null); const [nodeStarting, setNodeStarting] = useState(false); @@ -151,7 +151,10 @@ function CreateGroupWizard({ token, username, onCreated, allowPublicGroups = tru await hubFetch('/v1/users/me/node_key', { method: 'PUT', token, body: { pk_node_ed25519: pk }, }); - }, [token]); + // The sidebar's Node section depends on this link; it only re-read it at + // sign-in, so the first node set up here stayed out of it until a reload. + if (onNodeLinked) onNodeLinked(); + }, [token, onNodeLinked]); const detectNode = useCallback(async () => { setNodeStatus(null); @@ -177,6 +180,8 @@ function CreateGroupWizard({ token, username, onCreated, allowPublicGroups = tru setError(''); try { const result = await platform.node.start({ hubUrl: HUB, username, token }); + // node.start links the key from the main process, out of this page's sight. + if (onNodeLinked) onNodeLinked(); setNodeStatus({ detected: true, ...result }); setNodeStarting(false); setStep(1); @@ -337,6 +342,7 @@ function CreateGroupWizard({ token, username, onCreated, allowPublicGroups = tru && !provisionAttempted.current && (nodeStatus.status === 'waiting_for_account' || nodeStatus.status === 'waiting_for_node_key' + || nodeStatus.status === 'waiting_for_hub' || nodeStatus.status === 'starting')) { provisionAttempted.current = true; startNode(); 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 a510e63..fba8a0e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js @@ -417,7 +417,8 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, // Whether the hub mails the invitation. Checked, the hub is handed the code // to write it into the mail — so it is the inviter's choice, remembered per // account (docs/MESHBAY_DESIGN.md §3.4). Unchecked, the hub never sees it. - const [inviteByEmail, setInviteByEmail] = useState(true); + // Off by default: mailing the code is opt-in, not something to do unasked. + const [inviteByEmail, setInviteByEmail] = useState(false); const [error, setError] = useState(''); // Node loopback state (Electron-only) @@ -811,7 +812,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, useEffect(() => { hubFetch('/v1/users/me/preferences', { token }) - .then(prefs => setInviteByEmail(prefs[INVITE_EMAIL_PREF] !== 'false')) + .then(prefs => setInviteByEmail(prefs[INVITE_EMAIL_PREF] === 'true')) .catch(() => {}); }, [token]); @@ -1059,7 +1060,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, ${inviting ? '...' : t('members.invite_btn')} </button> </div> - <label style="display:flex; gap:8px; align-items:flex-start; margin:6px 0 0; + <label style="display:flex; gap:8px; align-items:center; margin:6px 0 0; font-size:0.88em; color:var(--text-secondary)"> <input type="checkbox" checked=${inviteByEmail} onChange=${e => toggleInviteByEmail(e.target.checked)} /> @@ -1105,7 +1106,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, ${linking ? '...' : t('members.link_btn')} </button> </div> - <label style="display:flex; gap:8px; align-items:flex-start; margin:6px 0 0; + <label style="display:flex; gap:8px; align-items:center; margin:6px 0 0; font-size:0.88em; color:var(--text-secondary)"> <input type="checkbox" checked=${inviteByEmail} onChange=${e => toggleInviteByEmail(e.target.checked)} /> 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 7bd5169..41a56be 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -910,7 +910,7 @@ export default { 'node.service_restarting': 'Wird neu gestartet…', 'node.service_mode_hint': 'Läuft als Hintergrunddienst — startet beim Booten, vor der Anmeldung.', 'node.startup_mode_label': 'Automatisch starten:', - 'node.startup_mode_off': 'Aus (manuell starten)', + 'node.startup_mode_off': 'Nur solange MeshBay geöffnet ist', 'node.startup_mode_signin': 'Bei der Anmeldung', 'node.startup_mode_service': 'Als Hintergrunddienst (startet beim Booten)', 'node.startup_mode_updating': 'Modus wird gewechselt — achten Sie auf eine Administrator-Eingabeaufforderung…', 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 0c9cb19..7c06815 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -1006,7 +1006,7 @@ export default { 'node.service_restarting': 'Restarting…', 'node.service_mode_hint': 'Running as a background service — it starts at boot, before sign-in.', 'node.startup_mode_label': 'Start automatically:', - 'node.startup_mode_off': 'Off (start manually)', + 'node.startup_mode_off': 'Only while MeshBay is open', 'node.startup_mode_signin': 'At sign-in', 'node.startup_mode_service': 'As a background service (starts at boot)', 'node.startup_mode_updating': 'Switching mode — check for an administrator prompt…', 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 1399a83..939b96c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -904,7 +904,7 @@ export default { 'node.service_restarting': 'Reiniciando…', 'node.service_mode_hint': 'Se ejecuta como servicio en segundo plano — se inicia al arrancar, antes de iniciar sesión.', 'node.startup_mode_label': 'Iniciar automáticamente:', - 'node.startup_mode_off': 'Desactivado (iniciar manualmente)', + 'node.startup_mode_off': 'Solo mientras MeshBay está abierto', 'node.startup_mode_signin': 'Al iniciar sesión', 'node.startup_mode_service': 'Como servicio en segundo plano (se inicia al arrancar)', 'node.startup_mode_updating': 'Cambiando de modo — compruebe si aparece un aviso de administrador…', 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 ae278d9..c546d4e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -907,7 +907,7 @@ export default { 'node.service_restarting': 'Redémarrage…', 'node.service_mode_hint': 'Fonctionne comme service en arrière-plan — démarre au boot, avant l\'ouverture de session.', 'node.startup_mode_label': 'Démarrer automatiquement :', - 'node.startup_mode_off': 'Désactivé (démarrage manuel)', + 'node.startup_mode_off': 'Uniquement quand MeshBay est ouvert', 'node.startup_mode_signin': 'À l\'ouverture de session', 'node.startup_mode_service': 'Comme service en arrière-plan (démarre au boot)', 'node.startup_mode_updating': 'Changement de mode — vérifiez une invite d\'administrateur…', 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 f0fb0d1..893a563 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -906,7 +906,7 @@ export default { 'node.service_restarting': 'Riavvio…', 'node.service_mode_hint': 'In esecuzione come servizio in background — si avvia all\'avvio del sistema, prima dell\'accesso.', 'node.startup_mode_label': 'Avvia automaticamente:', - 'node.startup_mode_off': 'Disattivato (avvio manuale)', + 'node.startup_mode_off': 'Solo mentre MeshBay è aperto', 'node.startup_mode_signin': 'All\'accesso', 'node.startup_mode_service': 'Come servizio in background (si avvia all\'avvio del sistema)', 'node.startup_mode_updating': 'Cambio modalità — controlli se compare una richiesta di amministratore…', 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 560332d..1a03c52 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -894,7 +894,7 @@ export default { 'node.service_restarting': '再起動中…', 'node.service_mode_hint': 'バックグラウンドサービスとして実行中 — サインインより前、起動時に開始します。', 'node.startup_mode_label': '自動的に開始:', - 'node.startup_mode_off': 'オフ(手動で開始)', + 'node.startup_mode_off': 'MeshBay が開いている間のみ', 'node.startup_mode_signin': 'サインイン時', 'node.startup_mode_service': 'バックグラウンドサービスとして(起動時に開始)', 'node.startup_mode_updating': 'モードを切り替え中 — 管理者の確認ダイアログをご確認ください…', 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 adb94c6..8ae0ab3 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -908,7 +908,7 @@ export default { 'node.service_restarting': 'Herstarten…', 'node.service_mode_hint': 'Actief als achtergrondservice — start bij het opstarten, vóór het aanmelden.', 'node.startup_mode_label': 'Automatisch starten:', - 'node.startup_mode_off': 'Uit (handmatig starten)', + 'node.startup_mode_off': 'Alleen zolang MeshBay open is', 'node.startup_mode_signin': 'Bij aanmelden', 'node.startup_mode_service': 'Als achtergrondservice (start bij het opstarten)', 'node.startup_mode_updating': 'Modus wijzigen — let op een beheerdersprompt…', 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 fb5a4ed..a2cc5bb 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -926,7 +926,7 @@ export default { 'node.service_restarting': 'Ponowne uruchamianie…', 'node.service_mode_hint': 'Działa jako usługa w tle — uruchamia się przy starcie systemu, przed zalogowaniem.', 'node.startup_mode_label': 'Uruchamiaj automatycznie:', - 'node.startup_mode_off': 'Wyłączone (uruchamianie ręczne)', + 'node.startup_mode_off': 'Tylko gdy MeshBay jest otwarty', 'node.startup_mode_signin': 'Przy logowaniu', 'node.startup_mode_service': 'Jako usługa w tle (uruchamia się przy starcie systemu)', 'node.startup_mode_updating': 'Zmiana trybu — proszę sprawdzić, czy pojawiło się okno uprawnień administratora…', 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 7bd11f6..2b98298 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 @@ -905,7 +905,7 @@ export default { 'node.service_restarting': 'Reiniciando…', 'node.service_mode_hint': 'Em execução como serviço em segundo plano — inicia na inicialização, antes do login.', 'node.startup_mode_label': 'Iniciar automaticamente:', - 'node.startup_mode_off': 'Desativado (iniciar manualmente)', + 'node.startup_mode_off': 'Somente enquanto o MeshBay estiver aberto', 'node.startup_mode_signin': 'Ao entrar na sessão', 'node.startup_mode_service': 'Como serviço em segundo plano (inicia na inicialização)', 'node.startup_mode_updating': 'Alternando modo — verifique se aparece um aviso de administrador…', 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 5509332..4ac583a 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 @@ -882,7 +882,7 @@ export default { 'node.service_restarting': '正在重启…', 'node.service_mode_hint': '以后台服务方式运行 — 在开机时启动,早于登录。', 'node.startup_mode_label': '自动启动:', - 'node.startup_mode_off': '关闭(手动启动)', + 'node.startup_mode_off': '仅在 MeshBay 打开时', 'node.startup_mode_signin': '登录时', 'node.startup_mode_service': '作为后台服务(开机时启动)', 'node.startup_mode_updating': '正在切换模式 — 请留意管理员权限提示…', 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 a59c100..7fd7ab1 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/node-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/node-page.js @@ -13,6 +13,14 @@ import { HUB } from './hub-client.js'; // (platform.node.call), 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 +// not yet (it publishes the roster only once it has signed in to the hub) -- +// which is not the same as having no operator. +export function pairedFrom(result) { + const v = result && result.operator_paired; + return v === true || v === false ? v : null; +} + function NodeServicePanel({ onChanged, token, username }) { const [info, setInfo] = useState(null); const [busy, setBusy] = useState(''); @@ -202,7 +210,7 @@ export function NodePage({ groups, token, username }) { const [editIce, setEditIce] = useState(null); const [savingIce, setSavingIce] = useState(false); const [iceInput, setIceInput] = useState(''); - const [operatorPaired, setOperatorPaired] = useState(false); + const [operatorPaired, setOperatorPaired] = useState(null); const [pairBusy, setPairBusy] = useState(false); const [pairStatus, setPairStatus] = useState(''); const [nodeInfo, setNodeInfo] = useState(null); @@ -231,7 +239,7 @@ export function NodePage({ groups, token, username }) { } const result = await nodeCall('GET', '/api/groups'); setNodeGroups(result.groups || []); - setOperatorPaired(!!result.operator_paired); + setOperatorPaired(pairedFrom(result)); setNodeSettings(result.settings || null); try { setNodeInfo(await nodeCall('GET', '/api/status')); } catch {} setStatus('connected'); @@ -253,7 +261,7 @@ export function NodePage({ groups, token, username }) { try { const result = await nodeCall('GET', '/api/groups'); setNodeGroups(result.groups || []); - setOperatorPaired(!!result.operator_paired); + setOperatorPaired(pairedFrom(result)); setNodeSettings(result.settings || null); try { setNodeInfo(await nodeCall('GET', '/api/status')); } catch {} } catch {} @@ -711,7 +719,7 @@ export function NodePage({ groups, token, username }) { </div> <${NodeServicePanel} onChanged=${fetchStatus} token=${token} username=${username} /> ${actionMsg && html`<div class="node-message">${actionMsg}</div>`} - ${!operatorPaired && html` + ${operatorPaired === false && html` <div class="node-pair-banner"> <p>${t('node.pair_needed')}</p> <button class="btn btn-primary btn-small" diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css index 37775d8..8433b1a 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/style.css +++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css @@ -682,7 +682,7 @@ a:hover { text-decoration: underline; } overflow: hidden; pointer-events: none; background: linear-gradient(155deg, - #86a3c4 0%, #6a819b 18%, #3d4d61 42%, #232b36 66%, #0f1113 100%); + #809cbc 0%, #6a819b 18%, #3d4d61 42%, #232b36 66%, #0f1113 100%); } /* The night-blue pool. */ .welcome-backdrop::before { diff --git a/packages/meshbay-hub/src/meshbay_hub/static/video-app.js b/packages/meshbay-hub/src/meshbay_hub/static/video-app.js index 124033e..4c9f90c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/video-app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/video-app.js @@ -976,6 +976,22 @@ function VideoApp({ useEffect(() => { setMode(loadViewMode()); }, [groupId]); useEffect(() => { setFilter(''); setTypeFilter('all'); }, [groupId]); + // When the operator changes the node's TMDB language, the node drops its + // metadata cache and refetches in the new language, and — until a language + // is set — answers nothing at all rather than querying in English (§9.7). + // Tell every mounted tile to redo its media_meta_req and drop the + // show-level meta already merged here, so the grid switches language (or + // fills in for the first time, right after the operator picks one) without + // a page reload. Skip the initial mount: the language is already right then, + // and bumping would restorm TMDB on every open of the Videos tab. + const tmdbLanguage = tmdbConfig ? (tmdbConfig.language || '') : ''; + const firstLangRef = useRef(true); + useEffect(() => { + if (firstLangRef.current) { firstLangRef.current = false; return; } + setMetaByGroup({}); + bumpMediaMetaGeneration(); + }, [tmdbLanguage]); + const setModeAndSave = (m) => { setMode(m); saveViewMode(m); }; const videoEntries = availableEntries || entries; |