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 19s
CI / ui (pull_request) Successful in 1m13s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Failing after 20s

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.

Wire the version gate through the deploy: GATEWAY_MIN_CLIENT_VERSION + GATEWAY_RECOMMENDED_CLIENT_VERSION are plain (unprefixed) Gitea vars, passed via compose + ci.yaml (test) + prod-deploy.yaml + write-prod-env.sh (prod) + .env.example, so the gate is live when set (a 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.

Tests: gateway go green (incl. two-tier gate over a real HTTP handler), compose --no-interpolate confirms the gateway env, svelte-check 0/0, vitest 617, e2e 122/122 (chromium + webkit).
This commit is contained in:
Ilia Denisov
2026-07-13 01:44:28 +02:00
parent 09e05eef18
commit 872804e02e
53 changed files with 1979 additions and 973 deletions
+56
View File
@@ -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>