feat(offline): implicit net-state model, two-tier version gate, unified lobby
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 20s
CI / ui (pull_request) Successful in 1m12s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m54s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 20s
CI / ui (pull_request) Successful in 1m12s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m54s
Land the offline-model redesign (ANDROID_PLAN.md O1-O7): replace the explicit offline toggle with a single detected net-state machine, unify the lobby, and add a two-tier client-version gate. Contour-safe: both version vars empty => dormant, the wire change is additive, so web/pwa/vk/tg behaviour is unchanged unless the gate is deliberately configured. O1 net-state reducer (test-first). O2 store + wiring (connection/offline shims; +@capacitor/network). O3 remove the offline toggle + migrate the pref. O4 two-tier gate: hard update_required degrades to an offline Update/Play-offline notice (not terminal); soft GATEWAY_RECOMMENDED_CLIENT_VERSION -> X-Update-Recommended nudge. O5 unified lobby (device-local + greyed-from-cache server games; self-set identity; closes G-step-0). O6 create flows (with-friends online/offline segment + offline dict guard). O7 docs. Telegram/VK stay online-only (contour review): the offline model is channel-gated via offlineCapable() (native + plain web; false in the mini-apps). offlineMode.active is hard-gated on it, so the whole model (blue chrome, unified/greyed lobby, transport kill switch, device-local vs_ai/hotseat create) stays inert in Telegram/VK regardless of the detected net state (closing a version-lock path that could have flipped them offline); and New Game's with-friends hides the online/offline segment there, leaving the remote invite alone. ARCHITECTURE already declared "Telegram/VK are exempt" - this makes the code match. Deploy/CI: wire the version gate through the deploy (GATEWAY_MIN_CLIENT_VERSION + GATEWAY_RECOMMENDED_CLIENT_VERSION as plain unprefixed vars via compose + ci.yaml + prod-deploy.yaml + write-prod-env.sh + .env.example) so the gate is live when set (test-contour commit-hash version fails open; empty => dormant). Disable the manual android-build CI workflow for now (rename .disabled). Bump the app bundle budget 127->130 KB for the added always-loaded wiring. Fix the UI Docker stage (gateway/Dockerfile): install --ignore-scripts so the Alpine/musl SPA build no longer tries to native-build sharp (a local android:assets tool, unused in the image); esbuild's binary is an optional dep so Vite still builds. Tests: gateway go green (two-tier gate over a real HTTP handler); docker build of the landing + gateway targets green locally; compose --no-interpolate confirms the gateway env; two new telegram.spec.ts e2e (offline signal keeps the chrome online + quick-match opponent choice visible => server enqueue; with-friends shows no pass-and-play), RED-verified; svelte-check 0/0, vitest 617, e2e 248 (chromium + webkit), build + bundle-size 127.8/130.
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
// The soft "update available" nudge (the soft version tier). A non-blocking banner: play continues.
|
||||
// Shown only while online — the hard "update required" notice is an offline state, so it naturally
|
||||
// supersedes this — and only until the user dismisses it for the session. Rendered at the bottom of
|
||||
// the lobby (above the tab bar), so it never covers a game in progress.
|
||||
import { netState } from '../lib/netstate.svelte';
|
||||
import { updateRecommended, dismissUpdateNudge, openUpdate } from '../lib/update.svelte';
|
||||
import { t } from '../lib/i18n/index.svelte';
|
||||
|
||||
const show = $derived(netState.online && updateRecommended.active && !updateRecommended.dismissed);
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<div class="nudge" role="status">
|
||||
<span class="msg">{t('update.available')}</span>
|
||||
<button class="upd" type="button" onclick={openUpdate}>{t('update.action')}</button>
|
||||
<button class="x" type="button" onclick={dismissUpdateNudge} aria-label={t('common.close')}>×</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.nudge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.5rem 0.9rem;
|
||||
border-top: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.msg {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
.upd {
|
||||
flex: 0 0 auto;
|
||||
font: inherit;
|
||||
padding: 0.3rem 0.8rem;
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--accent);
|
||||
color: var(--accent-text);
|
||||
cursor: pointer;
|
||||
}
|
||||
.x {
|
||||
flex: 0 0 auto;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -1,33 +1,29 @@
|
||||
<script lang="ts">
|
||||
// Non-dismissable "update required" cover. Shown when the gateway has refused a foreground call
|
||||
// as too old (update.svelte.ts — the client-version gate). Unlike the maintenance overlay this is
|
||||
// terminal: an installed build cannot become compatible without an actual update, so its one
|
||||
// action takes the user to the fix — the store listing on a native build (VITE_RUSTORE_URL, opened
|
||||
// in the system browser / store app) or a plain reload on the web (which fetches the current
|
||||
// client). Mirrors MaintenanceOverlay.svelte's look.
|
||||
import { updateRequired } from '../lib/update.svelte';
|
||||
import { clientChannel } from '../lib/channel';
|
||||
// The "update required" notice (the hard version tier). Shown when the gateway has refused a
|
||||
// foreground call as too old, driving the net-state machine into offlineVersionLocked. Unlike the
|
||||
// old terminal cover it is dismissable: "Update" takes the user to the fix (the store on native,
|
||||
// a reload on the web), while "Play offline" dismisses the notice and leaves the app in the offline
|
||||
// lobby (the version lock is an offline state, sticky until an actual update on the next launch).
|
||||
// Mirrors MaintenanceOverlay.svelte's look.
|
||||
import { netState } from '../lib/netstate.svelte';
|
||||
import { openUpdate } from '../lib/update.svelte';
|
||||
import { t } from '../lib/i18n/index.svelte';
|
||||
|
||||
function onAction(): void {
|
||||
const ch = clientChannel();
|
||||
if (ch === 'android' || ch === 'ios') {
|
||||
// '_system' hands the URL to the OS (the store app / external browser) rather than the WebView.
|
||||
const url = import.meta.env.VITE_RUSTORE_URL;
|
||||
if (url) window.open(url, '_system');
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
// Dismissed for the session once the user chooses "Play offline". The lock is sticky until a fresh
|
||||
// launch, so the notice does not reappear this session; the app stays in the offline lobby.
|
||||
let dismissed = $state(false);
|
||||
</script>
|
||||
|
||||
{#if updateRequired.active}
|
||||
{#if netState.versionLocked && !dismissed}
|
||||
<div class="scrim" role="alertdialog" aria-modal="true" aria-labelledby="update-title" aria-describedby="update-body">
|
||||
<div class="card">
|
||||
<div class="tile" aria-hidden="true">Э</div>
|
||||
<h1 id="update-title">{t('update.title')}</h1>
|
||||
<p id="update-body">{t('update.body')}</p>
|
||||
<button type="button" onclick={onAction}>{t('update.action')}</button>
|
||||
<div class="acts">
|
||||
<button type="button" class="primary" onclick={openUpdate}>{t('update.action')}</button>
|
||||
<button type="button" onclick={() => (dismissed = true)}>{t('update.playOffline')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -36,8 +32,7 @@
|
||||
.scrim {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
/* Above the game (z 60) and toasts (z 50) — a terminal update block covers everything the user
|
||||
could otherwise interact with; below the dev DebugPanel (z 10000). */
|
||||
/* Above the game (z 60) and toasts (z 50); below the dev DebugPanel (z 10000). */
|
||||
z-index: 100;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
display: grid;
|
||||
@@ -79,6 +74,11 @@
|
||||
color: var(--text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.acts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
button {
|
||||
font: inherit;
|
||||
padding: 0.55rem 1.4rem;
|
||||
@@ -92,4 +92,13 @@
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
.primary {
|
||||
background: var(--accent);
|
||||
color: var(--accent-text);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.primary:hover {
|
||||
color: var(--accent-text);
|
||||
opacity: 0.9;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user