369ed800b9
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>