Enrich the in-app live stream into a delta channel so the UI renders a move from the event without a follow-up game.state, and make the matchmaking poll a stream-down fallback.
- pkg/fbs: trailing fields on opponent_moved (move+game+bag_len), your_turn (move_count), match_found (state), game_over (game), notify (account/invitation/state), MoveResult (rack+bag_len); regenerate Go + TS.
- backend: notify owns the FB encoding (encode.go + payload.go input structs); game/lobby/social map their domain types in. emitMove builds the move delta; game.Service.InitialState feeds match_found/game_started the recipient's initial StateView; friends/invitations notify carry their account/invitation. The move-commit response (submit_play/pass/exchange/resign) returns the actor's refilled rack + bag size.
- gateway: MoveResult transcode carries rack+bag_len.
- ui: pure lib/gamedelta.ts reducer advances the per-game cache keyed on move_count (idempotent + gap-safe); app.svelte seeds the cache on match_found/game_started; Game.svelte applies the delta (commit/pass/exchange/resign drop their load()); NewGame polls only while app.streamAlive is false.
- docs: ARCHITECTURE §10, FUNCTIONAL(+ru), backend/gateway/ui READMEs; PRERELEASE R4 marked done + Refinements.
Following the in-game bar, the Connecting indicator now also visually disables the
other proactive (server-sending) controls while offline: chat send + nudge, profile
save / link email|telegram / merge-confirm, friends (redeem, get-code, accept/decline,
unfriend, block, unblock), New Game (auto-match variant + send-invitation) and the
lobby hide ❌. Purely local controls (board/rack/reset, menus, navigation, settings,
copy-code) stay live. Each reads the global connection.online signal; full e2e + check
green.
Each auto-match variant is now a lobby-style plaque: the display name with a flag on the
right (🇺🇸 / 🇷🇺; Erudit uses a bundled minimalist USSR flag SVG) and a one-line rules
summary below — bag size, the ё rule, and bonus differences, sourced from the engine
rulesets (Scrabble 100 · Скрэббл 104, ё a letter · Эрудит 131, ё=е, no centre ×2, +15).
The move-time limit (24h auto-match clock) is shown under the buttons. e2e locks it.
(Multiple-words-per-move is the same for every variant, so it is described in About/landing
rather than repeated on each button.)
- Resign on the opponent's turn: engine ResignSeat(seat) resigns a specific seat
(not just toMove); game.Resign bypasses the turn check and forfeits the actor's seat.
- Quick-match cancel was a UI no-op (only stopped polling): add the full path
(REST /lobby/cancel -> gateway lobby.cancel -> client) and clear the matchmaker's
pending result on Cancel, so a cancelled search is dequeued (no 'already queued', no
later robot-substituted game). NewGame dequeues on cancel and on abandon.
- Lobby win/loss: result.ts ranked by score, so a 0-0 resignation read as a win.
The winner now takes rank 1 and the viewer is placed from rank 2 — matching the
game-detail screen.
- Friend request to a robot: robots no longer block requests; the request stays
pending and expires (friendRequestTTL), mirroring a human who ignores it.
- Nudge cooldown: ErrNudgeTooSoon now maps to a distinct nudge_too_soon code with a
correct message; the chat nudge button disables during the hourly cooldown; the
nudge note reads 'Waiting for your move!' (button keeps the Nudge action label).
Tests: engine/service off-turn resign, matchmaker cancel-clears-result, friend-to-robot
inttest, result.ts 0-0 resignation, nudge_too_soon mapping.
Service-agnostic refinement of the owner's idea: the sign-in service returns a
set of supported game languages with the user identity, and the lobby gates the
New Game variant choice by it (en -> English; ru -> Russian + Эрудит).
- Connector hosts two bots in one container (one per service language, each its
own token + game channel; the same telegram_id spans both). ValidateInitData
tries each token and returns the validating bot's service_language +
supported_languages. Per-language config (TELEGRAM_BOT_TOKEN_EN/_RU, channels).
- supported_languages rides the Session (fbs, session-scoped, not persisted); the
UI offers only the matching variants on New Game — gating only the START of a
new game (auto-match + friend invite), not accept/open/play; backend does not
enforce.
- service_language persisted (accounts.service_language, migration 00010, written
every login, last-login-wins) and routes the user-facing Notify push back
through the right bot (push-target coalesces with preferred_language).
- Admin SendToUser/SendToGameChannel gain an operator-chosen language selector in
the console (unrelated to ValidateInitData).
- Non-Telegram logins carry the gateway default set
(GATEWAY_DEFAULT_SUPPORTED_LANGUAGES, all variants).
Wire (committed regen): ValidateInitDataResponse +service_language
+supported_languages; Session +supported_languages; SendToUser/SendToGameChannel
+language. Docs (ARCHITECTURE/FUNCTIONAL/_ru/READMEs) + PLAN updated; stage marked done.
Third owner-review pass (iPhone):
- Modals (and the chat) size their backdrop to window.visualViewport, so they stay
fully above the software keyboard (dvh alone left the sheet partly behind it).
- On the owner's call, every profile / new-game picker is a native <select> for
consistent cross-platform behaviour: the away window returns to hour + 10-minute
selects (which also avoids the iOS time-wheel "clear" button), alongside the offset
timezone and the game-type / move-time / hints selects. Native time/wheel inputs
render differently per OS and cannot be forced to match.
- New-game "play with friends" has no preselected game type — an explicit, required
pick (empty placeholder); Send invitation stays disabled until both a type and a
friend are chosen. A smart default (from play history / language) is TODO-6.
Second owner-review pass (iPhone simulator):
- Chat (and the modal) are sized in dvh so they shrink above the software keyboard,
keeping the start of the conversation on screen instead of pushed off the top.
- The profile away window returns to a native <input type="time" step="600"> (the iOS
wheel with 10-minute steps) instead of separate dropdowns; the timezone stays a
native offset <select>.
- A finished game reserves the rack's height (min-height) so the footer no longer
collapses when the final rack is empty — no layout jump versus an active game.
- New-game "play with friends" is made compact: a searchable, bounded-scroll friend
list, the game-type / move-time / hints controls as native selects in one row
(labels above), and Send invitation pinned at the bottom — it scales to many friends.
Wire the deferred Stage 7 surfaces end-to-end (UI -> gateway transcode ->
backend REST -> existing domain services): friends (incl. one-time friend
codes), per-user blocks, friend-game invitations, profile editing + email
binding, the statistics screen, and the in-game history + GCG export.
Friends gain two add paths (interview decision, a deliberate plan change):
one-time 6-digit codes (friend_codes table, 12h TTL, single-use, rate-limited
redeem); and play-gated requests (shared game required) where an explicit
decline is permanent, an ignored request lapses after 30 days, and a code
bypasses a decline. Migration 00006 widens friendships_status_chk and adds
friend_codes.
Lobby notification badge is poll + push: a new generic `notify` event drives
it live; the client polls on open/focus. Language stays a single Settings
control that writes through to the durable account's preferred_language. GCG
export is finished-only (game.ErrGameActive) and shares/downloads the .gcg file.
Tests: backend unit + inttest (friend gate/decline/code, ListInvitations,
GetStats, GCG gate), gateway transcode round-trips + notify constructor, UI
vitest (codecs, win-rate, share choice) + Playwright social specs. Docs: PLAN
(Stage 8 done + refinements + TODO-5), ARCHITECTURE, FUNCTIONAL(+ru), UI_DESIGN,
TESTING, module READMEs.