Drops the staleness gate from 1h to 1m and adds a Timer.periodic that
fires recheckIfStale every minute while the app is foregrounded. Net
effect: ~1 check per minute of active use, ~60 KB/hr data — trivially
affordable for the value of faster recovery when min_client_version
bumps server-side.
Timer is paired with the lifecycle observer: started in initState +
on resume, stopped in dispose + on pause/inactive/hidden/detached so
backgrounded apps don't burn battery on probes the user can't see.
Staleness gate still wraps the call so concurrent triggers (timer +
resume firing close together) dedupe to one network call. Manual
"Check now" still bypasses the gate via recheck().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cold-start spinner was up to ~38s on slow / remote connections
because VersionGate blocked the entire ShellRoute on /healthz, with
the default dio's 8s connect + 30s receive timeouts. The /healthz
server handler itself is fine (microsecond JSON encode); the blocker
was client-side. Three issues fixed in one pass:
1. Optimistic render. VersionGate becomes a ConsumerStatefulWidget
that always renders its child and just activates the version
check controller on mount. The "you're too old" experience moves
from a full-screen hard-block (_TooOldScreen, deleted) to a soft
banner above the AppBar that lets the user keep playing cached
content while they update.
2. 1h-throttled background check. New VersionCheckController
(AsyncNotifier) hydrates from a secure-storage cache on boot,
returning the cached result instantly. If the cache is missing
or >1h old, fires a background recheck. AppLifecycleState.resumed
triggers recheckIfStale so foregrounding after >1h re-checks
without per-frame hammering. "Check now" button on the banner
bypasses the staleness gate so dev iteration (push new APK, want
to see banner clear) doesn't wait an hour.
3. Bounded health-check dio. The /healthz request uses a dedicated
dio with connectTimeout: 3s + receiveTimeout: 2s rather than the
default 8s / 30s. Health probes should fail fast — if the server
can't ack in 5s, the user has bigger problems than a stale
min_client_version and the cached value remains in effect.
Cache keys live alongside the existing tz cadence cache in
flutter_secure_storage (kResult + kAtMs). On any network error or
parse failure, _runCheck soft-fails without bumping the timestamp,
so the next staleness check will retry.
VersionTooOldBanner renders in _ShellWithPlayerBar's Column above
the existing UpdateBanner — the two coexist when both apply
(server rejects you AND an APK is queued).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cold launch flow: no server-url -> /server-url. URL set, no token ->
/login. Token present -> /home with shell. VersionGate runs once when
the URL changes, hits /healthz, blocks if min_client_version > package
version. /home is a placeholder until Task 14.
Replaces Navigator.pushReplacementNamed in server_url_screen +
login_screen with context.go now that go_router owns the routes.