feat(offline): transport kill switch + gate the network-requiring UI
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m10s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m43s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m10s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m43s
Offline mode was 'fiction': the UI only disabled the Stats tab, but Profile was reachable and a profile save actually hit the server and reported 'saved'. Two layers now: - Transport kill switch (transport.ts): in offline mode every network op — each unary RPC (exec), the live stream (subscribe), the dict/metrics fetches and the reachability probe — is refused before it leaves the device. Offline truly means offline regardless of any UI that slips through. The local (device-only) game path never uses this transport, so it is unaffected. - UI gating: the Profile and Friends tabs (SettingsHub) and the Feedback entry (About) are disabled offline, and the hub falls back to Settings (which holds the online/offline toggle); the lobby Stats tab was already disabled. In-game social/export are already hidden for a vs_ai game. Online unaffected (offlineMode is off): e2e 196. Offline gating is contour-verified (the mock e2e cannot enter offline).
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import { t } from '../lib/i18n/index.svelte';
|
||||
import { app } from '../lib/app.svelte';
|
||||
import { navigate } from '../lib/router.svelte';
|
||||
import { offlineMode } from '../lib/offline.svelte';
|
||||
import { aboutContent } from '../lib/aboutContent';
|
||||
import { onExternalLinkClick } from '../lib/links';
|
||||
|
||||
@@ -35,7 +36,7 @@
|
||||
<p class="muted">{t('about.version', { v: version })}</p>
|
||||
|
||||
{#if !(app.profile?.isGuest ?? true)}
|
||||
<button class="feedback" onclick={() => navigate('/feedback')}>
|
||||
<button class="feedback" disabled={offlineMode.active} onclick={() => navigate('/feedback')}>
|
||||
{t('feedback.title')}{#if app.feedbackReplyUnread}<span class="fbadge">1</span>{/if}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import Friends from './Friends.svelte';
|
||||
import About from './About.svelte';
|
||||
import { app } from '../lib/app.svelte';
|
||||
import { offlineMode } from '../lib/offline.svelte';
|
||||
import { t, type MessageKey } from '../lib/i18n/index.svelte';
|
||||
|
||||
// The Settings hub: a single nav bar + bottom tab bar hosting the Settings / Profile /
|
||||
@@ -23,6 +24,12 @@
|
||||
$effect(() => {
|
||||
if (guest && tab === 'friends') tab = 'settings';
|
||||
});
|
||||
// Offline mode has no network, so the Profile and Friends surfaces (which read/save over the
|
||||
// network) are disabled — fall back to Settings if the hub is on one (a deep-link or a live flip).
|
||||
// Settings stays reachable: it holds the online/offline toggle.
|
||||
$effect(() => {
|
||||
if (offlineMode.active && (tab === 'profile' || tab === 'friends')) tab = 'settings';
|
||||
});
|
||||
|
||||
const titleKey: Record<SettingsTab, MessageKey> = {
|
||||
settings: 'settings.title',
|
||||
@@ -48,11 +55,11 @@
|
||||
<button class="tab" class:active={tab === 'settings'} onclick={() => (tab = 'settings')}>
|
||||
<span class="face"><span class="sq" aria-hidden="true">⚙️</span><span class="lbl">{t('settings.title')}</span></span>
|
||||
</button>
|
||||
<button class="tab" class:active={tab === 'profile'} onclick={() => (tab = 'profile')}>
|
||||
<button class="tab" class:active={tab === 'profile'} disabled={offlineMode.active} onclick={() => (tab = 'profile')}>
|
||||
<span class="face"><span class="sq" aria-hidden="true">👤</span><span class="lbl">{t('profile.title')}</span></span>
|
||||
</button>
|
||||
{#if !guest}
|
||||
<button class="tab" class:active={tab === 'friends'} onclick={() => (tab = 'friends')}>
|
||||
<button class="tab" class:active={tab === 'friends'} disabled={offlineMode.active} onclick={() => (tab = 'friends')}>
|
||||
<span class="face"><span class="sq" aria-hidden="true">🤝{#if app.notifications > 0}<span class="badge">{app.notifications}</span>{/if}</span><span class="lbl">{t('friends.title')}</span></span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user