Persist per-message read state as a chat_messages.unread_seats bitmask
(migration 00008): a text message seeds every recipient seat's bit, a nudge
only the awaited seat's. A seat's bit clears when the player opens the move
history or chat (POST /games/:id/chat/read, sent only when something is
unread), and a nudge additionally clears when its recipient answers by moving
(a wired game NudgeClearer, dependency-inverted so game keeps off social).
UI shows a per-viewer unread dot in the lobby (next to the opponent) and the
game score bar — the unread_chat game-view flag seeds it from authoritative
REST views, live chat/nudge events raise it. Opening the move history counts
as reading (even without entering chat): the 💬 fade-blinks twice and the
client acks. Admin Messages gains an unread-only filter, a read/unread column,
and a per-message card with the per-seat read breakdown. Observability:
chat_read_duration histogram + chat_unread_messages gauge + social tracing.
New Game's quick game gains an explicit opponent selector — 🤖 AI (default)
or 👤 Random player. AI starts a game seated with a pooled robot that joins
and moves at once: no per-move timeout (a 7-day inactivity loss reusing the
turn-timeout sweeper), chat/nudge disabled, no statistics, the opponent shown
as 🤖 everywhere. The random path (disguised robot) is unchanged.
Driven by one game flag (games.vs_ai), set only on AI-started games so the
disguised path is never revealed; Matchmaker.StartVsAI seats the robot
directly (no open pool); the robot replies event-driven via the game service's
after-commit/after-create hook. Wire: vs_ai on EnqueueRequest + GameView.
Thread the Telegram bot's service language (en/ru) from the session mint response
through the gateway into the FlatBuffers Session, so the UI knows which bot the
player signed in through. handleTelegramAuth refreshes the account's service
language onto the response before minting (it was set after the fetched copy).
Empty for a non-Telegram login.
Surface the per-game "single word" rule to the client and refine the
random-opponent New Game screen.
- Wire: thread multiple_words_per_turn into the GameView and Invitation
FlatBuffers tables (Go + TS regenerated), through pkg/wire builders and both
the backend push-event and gateway REST paths.
- In-game indicators (single-word games only): a small 1 in the status bar's
score-preview slot (yields to the live preview) and a centred "One word per
turn" label in the history-drawer header. Standard games show neither.
- Invitation card gains a "One word per turn" line for single-word invitations.
- Auto-match redesign: variant plaques are mutually-exclusive selects (highlight
on tap, no longer enqueue); a lone offered variant is pre-selected; a bottom
"Start game" button (disabled until a variant is chosen) confirms. The rule
toggle appears once a Russian variant is selected.
- Tests: e2e for the new auto flow and the in-game indicator (mock g3 is a
single-word game); mock/data + fixtures carry the new field. Docs: UI_DESIGN.
Squash the 12 goose migrations into one 00001_baseline.sql (there is no prod
data; verified schema-identical to the chain via a pg_dump diff + the green
integration suite) and rename the game-variant labels
english/russian_scrabble/erudit -> scrabble_en/scrabble_ru/erudit_ru across the
backend, the FlatBuffers wire values and the UI.
dawg filenames and the Go enum identifiers are unchanged; the i18n display keys
are kept. Adds PRERELEASE.md (the R1-R7 pre-release tracker), linked from
CLAUDE.md. Contour DB wipe and the scrabble-dictionary tidy are follow-ups.
Lobby: group the my-games list into your-turn / opponent-turn / finished
(empty sections hidden), ordered by last activity (your-turn oldest-first,
the other two newest-first), as a compact line-separated list. gameDTO and
FB GameView gain last_activity_unix (turn start while active, finish time
once finished); a pure lib/lobbysort.ts holds the grouping/ordering.
Friends: the in-game 'add to friends' item is now server-derived via a new
GET /user/friends/outgoing (+ friends.outgoing op), returning addressees with
a pending OR declined request (both read as 'request sent'), so it is correct
across reloads; it shows a disabled '✓ in friends' once accepted. It
live-updates when the opponent answers: RespondFriendRequest now publishes
friend_added (accept) / friend_declined (new notify sub-kind, decline) to the
original requester, whose open game re-derives its friend state.
Tests: lobbysort unit test; gateway outgoing + last_activity transcode tests;
backend integration ListOutgoingRequests + respond-publishes-to-requester;
e2e updated for the new lobby section labels + a non-friend active opponent.
Docs: ARCHITECTURE notify catalog, FUNCTIONAL(+ru) lobby/friends, PLAN.
- #6 align the UI variant id to the backend canonical 'russian_scrabble' (type, variants, Lobby, mock, tests) — fixes the New->Russian 400
- #11/#12 inside Telegram force the colour scheme from WebApp.colorScheme (over OS prefers-color-scheme, fixing the Telegram Desktop breakage) and hide the theme switcher
- #14/#15 nav bar takes Telegram's bg; announcement banner gets a dedicated subtle --ad-bg accent token
- #16 suppress the reconnect banner while backgrounded and silently reconnect the live stream on return to the foreground
- #17 hint zoom scrolls to the placement's bounding box, not the top-left
- #19/#20 players plaque: active seat raised with side shadows, others sunk; tap toggles history
- #21/#23 history: scrollbar-gutter:stable (no word jitter); fixed-height drawer pins the bottom shadow to the board
- #3 (UI) disable nudge on the player's own turn
- #18a directional screen slide transitions (forward in from the right, back reveals the lobby)
- #13 per-game in-memory cache: instant render on re-entry + background refresh
- e2e: openGame waits for the slide transition to settle
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.
New platform/telegram connector (own container, bot token only there):
- go-telegram/bot long-poll loop: /start deep-links + Mini App launch button.
- gRPC API pkg/proto/telegram/v1 (Telegram service): ValidateInitData, Notify
(renders a localized message + deep-link button), SendToUser/SendToGameChannel
(admin, wired in Stage 10). Generic methods are platform-agnostic (external_id).
- Bot API base override for Telegram's test environment; Dockerfile + compose
(VPN sidecar, no public ingress); README.
Gateway:
- initData validation relocated from the gateway into the connector; the gateway
calls ValidateInitData over gRPC (GATEWAY_CONNECTOR_ADDR), drops the bot token,
and deletes internal/auth.
- Out-of-app push: runPushPump routes events whose recipient has no live in-app
stream to connector.Notify, gated by /internal/push-target + the in-app-only
flag (race-free de-dup); HasSubscribers added to the push hub.
Backend:
- Migration 00007 accounts.notifications_in_app_only (default true) + jetgen.
- ProvisionTelegram seeds a new account's language/display name from the launch
fields; IdentityExternalID reverse lookup; /internal/push-target handler.
UI:
- Telegram Mini App launch: detect initData, apply themeParams, authTelegram,
route the deep-link start_param (g/i/f); /telegram/ guard redirects outside
Telegram. Vite relative base + telegram-web-app.js. In-app-only profile toggle;
share-to-Telegram link for a friend code. Vitest + Playwright coverage.
Wire/docs/CI: fbs Profile/UpdateProfileRequest gain notifications_in_app_only
(Go + TS); go.work uses ./platform/telegram; go-unit.yaml covers it; PLAN,
ARCHITECTURE, FUNCTIONAL (+ru), UI_DESIGN, READMEs updated.
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.