Commit Graph

125 Commits

Author SHA1 Message Date
Ilia Denisov d87c0fb10b Stage 17: cap display-name special characters at 5 (ui + backend)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 35s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m3s
display_name validation gains a rule: at most 5 special characters — the '.' / '_'
punctuation (spaces, which separate words, don't count) — so a still-well-formed name
can't be mostly punctuation. Mirrored in the Go ValidateDisplayName and the UI
validDisplayName; both unit-tested (5 ok, 6 rejected, 'J. R. R. Tolkien' ok). Docs:
FUNCTIONAL (+ _ru).
2026-06-09 07:42:47 +02:00
Ilia Denisov 84ecc85f51 Stage 17 #2 fix: connection failures show only the spinner, never a toast
A dropped/reset/timed-out connection can surface as a Connect code other than
Unavailable (Canceled/DeadlineExceeded/Unknown/…) which fell through to the generic
'internal' -> a red 'something went wrong' toast appeared alongside the Connecting
spinner. Now toGatewayError (moved to the pure retry.ts, unit-tested) collapses every
transport-level code to 'unavailable' so it is retried + flips offline; and handleError
suppresses the toast for any connection code AND whenever the app is mid-reconnect
(!connection.online), covering the race where a unary error lands before the stream
reports the drop. Genuine server-internal / domain errors still toast while online.
2026-06-09 07:42:47 +02:00
Ilia Denisov efa1d0bd22 Stage 17 #2: extend the offline soft-disable to all server-action buttons
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 35s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m6s
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.
2026-06-09 07:23:32 +02:00
Ilia Denisov ef61b778fc Stage 17 #2: Connecting indicator + auto-retry, instead of red toasts
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 36s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 55s
Connectivity failures become state, not a toast on every attempt. A global online
signal (lib/connection.svelte.ts) flips on a transport unavailable / rate_limited and
on the live stream's drop, driving a pure-CSS header spinner + 'Connecting…' in place
of the title and softly disabling the in-game server actions (commit / exchange / pass
/ hint; local board/rack/reset stay live).

- transport: exec auto-retries with capped exponential backoff — every op on a
  rate-limit (rejected before processing, safe), reads only on unavailable (a mutation
  is never blindly re-sent, to avoid double-applying one whose response was lost; its
  button is disabled while offline so the player re-issues on reconnect). A reachability
  watcher (profile.get probe) and any successful traffic clear the signal.
- the old red error.unavailable toast is gone (handleError suppresses connection codes;
  the indicator replaces it). A server-data screen still opens with the spinner and
  fills on reconnect (global indicator + read auto-retry), so navigation is never dead.
- pure retry policy unit-tested (retry.ts); a mock-only window.__conn hook drives a
  Chromium+WebKit e2e (indicator shows offline, the action disables, both clear on
  reconnect). Full suite + build green.
- docs: ARCHITECTURE transport note, FUNCTIONAL (+ _ru), PLAN tracker (incl. #1 — the
  bot already drains all updates, no change).

Also records #1 as investigated/no-change in PLAN. Other server-action buttons (chat
send, profile save, …) still degrade to a safe no-op offline; visual disable is easy to
extend.
2026-06-09 01:48:20 +02:00
developer 844f26bbae Merge pull request 'Stage 17 #4: enrich the out-of-app your-turn push + add game-over' (#28) from feature/push-enrichment into development
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 8s
CI / integration (push) Successful in 11s
CI / ui (push) Successful in 35s
CI / gate (push) Successful in 1s
CI / deploy (push) Successful in 1m10s
2026-06-08 23:29:16 +00:00
Ilia Denisov f166ff30fe Stage 17 #4: enrich the out-of-app your-turn push + add game-over
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Successful in 34s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m20s
The Telegram 'your turn' notification now names the opponent and recaps their last
move (voiced as the opponent: «{name}: my move — «WORD». Score 120:95» for a scoring
play; a short 'swapped / passed, your turn' otherwise), and a new game-over
notification reports the result + final score when a game ends by any path (closing
play, all-pass, resign, timeout). Scores are recipient-first (the reader's score
leads), 2-4 players (120:95:80).

- schema: YourTurnEvent gains opponent_name/last_action/last_word/score_line
  (appended, backward-compatible); new GameOverEvent{result, score_line}. Go + UI
  bindings regenerated (flatc 23.5.26 + pnpm codegen).
- backend: notify.YourTurn enriched + notify.GameOver; emitMove resolves the mover's
  name and emits per-recipient (your_turn to the next mover, game_over to every seat),
  with recipient-first score lines built in one place.
- gateway: game_over joins the out-of-app whitelist (routing.go).
- connector: render builds the enriched your_turn + game_over text per language (en/ru).
- tests: notify round-trip (enriched + game_over), emit (enriched fields + game_over to
  all seats / per-seat result), connector render (en/ru), routing; integration replay
  (play → your_turn with real name; resign → game_over) green.
- docs: ARCHITECTURE push catalog + out-of-app set, FUNCTIONAL (+ _ru), PLAN tracker.
2026-06-09 01:15:18 +02:00
developer 6956dad354 Merge pull request 'Stage 17 #5: hide finished games from your own lobby list' (#27) from feature/hide-finished-games into development
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 9s
CI / integration (push) Successful in 11s
CI / ui (push) Successful in 35s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m5s
2026-06-08 22:45:07 +00:00
Ilia Denisov 13361c098c Stage 17 #5: make the active-row chevron open the game (not a no-op)
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 36s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m5s
Owner review: the '>' on an active game row should be a real tap target that opens
the game, like the rest of the row — not inert. The chevron now navigates (kept out
of the tab order / a11y tree since the row's main button already does the same), and
active-row swipes no longer suppress the tap. Adds an e2e for the chevron navigation.
2026-06-09 00:39:54 +02:00
Ilia Denisov 4999478ded Stage 17 #5: hide finished games from your own lobby list
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 35s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m16s
A player can remove a finished game from their own 'my games' list. The action is
per-account, finished-only and irreversible (the game stays for the other players;
there is no un-hide).

- backend: migration 00012 game_hidden(account_id, game_id); store HideGame +
  hiddenGameIDs + ListGamesForAccount filtering; service HideGame (seat + finished
  checks, reusing ErrNotAPlayer / ErrGameActive); POST /api/v1/user/games/:id/hide.
- gateway: game.hide edge op (reuses GameActionRequest -> Ack) + backendclient.HideGame.
- ui: finished rows reveal a delete via swipe-left (touch) or a kebab tap (desktop),
  active rows get an inert chevron for icon alignment; optimistic removal + lobby-cache
  sync; mock + transport + client wiring; lobby.hideGame label (en/ru).
- tests: integration (active->ErrGameActive, outsider->ErrNotAPlayer, per-account,
  idempotent), gateway transcode round-trip, mock e2e (kebab -> delete); hardened a
  pre-existing chat-screen .back transition flake surfaced by the new test's timing.
- docs: ARCHITECTURE persistence list, FUNCTIONAL (+ _ru) lobby story, PLAN tracker.
2026-06-09 00:26:35 +02:00
developer a7c566d2d1 Merge pull request 'Round-6 follow-up: UX polish + client-IP fix' (#26) from feature/ux-polish-ipfix into development
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 8s
CI / integration (push) Successful in 11s
CI / ui (push) Successful in 34s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m6s
2026-06-08 21:40:13 +00:00
Ilia Denisov a84e9d8cb7 Fix screen-slide direction: by route depth, so back from chat/check slides back
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Successful in 35s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m7s
The 'lobby is back' rule slid the chat/check back-to-the-game forward. Direction is now
computed from route depth (lobby < game < chat/check): shallower = back, deeper = forward.
2026-06-08 23:37:02 +02:00
Ilia Denisov 70110effd9 Chat + word-check as their own screens; in-game unread badge (review item 7)
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 34s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m6s
- Chat and word-check are now routed screens (/game/:id/chat, /game/:id/check) with a
  header back to the game and no tab-bar, replacing their modals. The soft keyboard just
  resizes the visible viewport (tracked into --vvh, which the Screen height uses since iOS
  does not shrink dvh for the keyboard) with the input pinned to the bottom: no modal
  relayout, no page jump. Supersedes the earlier bottom-sheet Modal attempt.
- A new chat message raises an unread badge on the in-game hamburger + the Chat menu row
  (per game, cleared on opening the chat), mirroring the lobby badge.
- TG native back + the header back chevron return chat/check to their game.
- Exposes --tg-safe-top (device notch) for the finalised TG-fullscreen header.

Tests: e2e for chat/check opening as their own screens + back. Docs: PLAN, FUNCTIONAL(+ru).
2026-06-08 23:23:05 +02:00
Ilia Denisov 295e45486d TG-fullscreen header: add height via padding (min-height wasn't binding), +12px breathing room
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Successful in 33s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m10s
2026-06-08 22:22:43 +02:00
Ilia Denisov a132edd40a TG-fullscreen header: +6px band height so native controls aren't flush (owner tweak)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Successful in 33s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m14s
2026-06-08 22:10:44 +02:00
Ilia Denisov 461e330bfc Admin Messages CSV: defuse spreadsheet formula injection
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 33s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m15s
The sender name and message body are user-controlled; a leading =, +, -, @, tab or
CR in the CSV export would execute as a formula when a moderator opens it in a
spreadsheet. csvSafe() prefixes such values with a single quote. Unit-tested.
2026-06-08 22:09:26 +02:00
Ilia Denisov c96d714fec Admin Messages: CSV export of the whole filtered list
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 33s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m14s
A right-aligned 'Export CSV ↓' link in the filter row downloads /_gm/messages.csv
with the active filters (game / sender / name / ext masks), exporting every matching
message (capped at 100k) regardless of the page window — columns time, source,
sender_id, sender, ip, message, game_id.
2026-06-08 22:07:33 +02:00
Ilia Denisov 7e34897d6d Review fixes: swipe (capture-phase, enabled in TG), TG header aligns to the nav band, DnD zoom delay 1s->0.7s
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 32s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
- Edge-swipe back now listens at the window in the CAPTURE phase (the board's
  pointer handlers can't swallow it) and is no longer skipped inside Telegram
  (where the owner tests it).
- TG-fullscreen header: expose the device safe-area top (--tg-safe-top) and
  centre the title + menu pair within Telegram's nav band ([safe-top,
  content-top]) below the notch, keeping the band's height — lining up with
  Telegram's own controls.
- DnD auto-zoom-on-hover delay reduced 1000ms -> 700ms.

(Client-IP: diagnosed as the owner's home-router SNAT — the host caddy already
receives 192.168.0.1 with no XFF, so the real IP is lost upstream of our stack;
correct in prod. No code change.)
2026-06-08 22:01:43 +02:00
Ilia Denisov 645df52c0b Round-6 follow-up: UX polish + client-IP fix
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 32s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
- Client IP: the compose caddy trusts X-Forwarded-For from private-range
  upstreams (trusted_proxies private_ranges), so the real client IP survives
  the host-caddy hop (it was logging the docker caddy hop 172.18.0.x for chat
  moderation and bucketing the gateway per-IP rate limiter on it). Correct and
  spoof-safe in both contours (prod has no host caddy); peerIP unit-tested.
- Ad banner gated off behind a compile-time SHOW_AD_BANNER=false (the if-branch,
  the AdBanner import and banner.ts are tree-shaken out of the prod bundle).
- Landing: the Telegram entry is just the 64px logo (clickable, no button/text).
- TG-fullscreen header: title + menu centred as a pair (hamburger right of the
  title), pinned to the bottom of the TG nav band.
- Edge-swipe back (Screen): a left-edge rightward drag navigates to back
  (touch/pen only, armed from <=24px; skipped inside Telegram).
- Chat soft-keyboard: a bottom-sheet Modal lifted above the keyboard by a
  visualViewport-driven transform (compositor-only, no page/sheet relayout).
  iOS-specific, needs on-device tuning; native resize=none awaits Capacitor.
- Tests: e2e for the in-game '✓ in friends' item and a board→board tile
  relocation; codec units for last_activity_unix + OutgoingRequestList.

Deferred to the next PR (agreed): #4 enrich the your-turn/game-end push; #5 hide
finished games from the lobby.
2026-06-08 21:31:44 +02:00
developer f95a6cb9c8 Merge pull request 'Stage 17 round 6 (#18, PR D): admin Messages moderation section' (#25) from feature/admin-messages into development
CI / changes (push) Successful in 1s
CI / unit (push) Successful in 8s
CI / integration (push) Successful in 11s
CI / ui (push) Has been skipped
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m5s
2026-06-08 18:28:30 +00:00
developer 5d677cb282 Merge pull request 'Stage 17 round 6 (#16/#17, PR C): lobby sort + server-derived in-game friend state' (#24) from feature/lobby-friends into development
CI / changes (push) Successful in 1s
CI / unit (push) Successful in 8s
CI / integration (push) Successful in 12s
CI / ui (push) Successful in 31s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m6s
2026-06-08 18:28:26 +00:00
developer c9a1eee510 Merge pull request 'Game/Telegram review polish: USSR flag, touch drag ghost, TG fullscreen header' (#23) from feature/game-ux into development
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 31s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m8s
2026-06-08 18:28:21 +00:00
developer 83e9a90d40 Merge pull request 'Landing v2: icon switchers, ephemeral theme, channel link, drop browser CTA' (#22) from feature/landing-v2 into development
CI / changes (push) Successful in 1s
CI / unit (push) Successful in 9s
CI / integration (push) Successful in 12s
CI / ui (push) Successful in 31s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m7s
2026-06-08 18:28:17 +00:00
Ilia Denisov 356f490546 Stage 17 round 6 (#18, PR D): admin Messages moderation section
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 32s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m14s
A new /_gm/messages console page lists posted chat messages (nudges
excluded) newest-first — time, source (guest/robot/oldest identity kind),
sender (linked to the user card), IP, body, game (linked to the game card)
— searchable by sender name / external-id glob masks and pinnable to one
game (?game=) or sender (?user=), linked from the game and user cards.

The list query lives in social (raw SQL, kind='message', source via a SQL
CASE), reusing the now-exported account.LikePattern. Server-rendered
adminconsole MessagesView + messages.gohtml, 50/page via the shared pager.

Tests: adminconsole render case; backend integration AdminListMessages
(real Postgres) — nudge exclusion, game/sender pins, glob masks, source.
Docs: ARCHITECTURE section 8 chat moderation, PLAN round-6.
2026-06-08 20:10:27 +02:00
Ilia Denisov 6b6baf5710 Stage 17 round 6 (#16/#17, PR C): lobby sort + server-derived in-game friend state
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 31s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
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.
2026-06-08 19:23:48 +02:00
Ilia Denisov b720907db2 Review fixes #2: bigger flag star, TG header below nav, board-tile relocation
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 31s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m7s
Addressing the review on #23:
- Flag star scaled up ~25% (the hammer&sickle emblem unchanged, kept clear of it).
- TG fullscreen header: drop the WHOLE header below the content-safe-area top
  inset (the hamburger stays to the right of the title), instead of pinning the
  hamburger to the physical top edge.
- DnD: a placed (pending) tile can now be relocated by dragging it to another
  board cell (board->board); it lifts off its source cell while dragged; and it
  can be grabbed even on the zoomed board (touch-action:none on the pending
  cell, so the drag wins over the board pan). The manual-selection blue frame
  now clears on recall.
2026-06-08 18:23:10 +02:00
Ilia Denisov 34385240b9 Game/Telegram review polish: USSR flag, touch drag ghost, TG fullscreen header
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 31s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 55s
Backlog item 2 of ~4 (owner review pass):
- USSR flag emblem redrawn (canonical hammer & sickle, scaled down 1.5x
  below the star).
- Touch drag-and-drop: enlarge the drag ghost 1.5x on touch only (the finger
  hides the tile); suppress the iOS tap-highlight that lingered on a rack tile
  sliding into a dragged tile's slot.
- Telegram fullscreen: its native nav no longer hides our header -- the header
  drops below the content-safe-area top inset and the menu (hamburger) lifts
  into the nav band, centred (--tg-content-top from the SDK inset + a
  tg-fullscreen class; new telegram.ts helper + app wiring).

Tests: UI check/test:unit/build + full e2e (60) green. The iOS tap-highlight
fix and the TG-fullscreen layout want on-device verification on the deploy.
2026-06-08 17:11:10 +02:00
Ilia Denisov 3fd279cf8c Landing v2: icon switchers, ephemeral theme, channel link, drop browser CTA
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 7s
CI / integration (pull_request) Successful in 10s
CI / ui (pull_request) Successful in 32s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m6s
Owner review-pass rework of the landing page:
- Rename the per-language Telegram link build var
  VITE_TELEGRAM_LINK_EN/_RU -> VITE_TELEGRAM_GAME_CHANNEL_NAME_EN/_RU
  (it carries a channel username; the landing builds https://t.me/<name> --
  the same channels the connector posts to via TELEGRAM_GAME_CHANNEL_ID_*).
- Language switcher -> a globe icon dropdown (flags + names), saved + synced
  to the app prefs.
- Theme switcher -> a sun/moon icon toggle, ephemeral (follows the system
  scheme, no auto, never persisted) -- galaxy-game style.
- Drop the "Play in browser" CTA (no standalone-web onboarding yet).

Docs: FUNCTIONAL(+ru), PLAN, deploy + ui READMEs.
2026-06-08 16:40:07 +02:00
developer 5928be40b0 Merge pull request 'Stage 17 round 6 (#16-20): landing page, /app/ move, cache + stream fixes' (#21) from feature/stage-17-round-6-landing into development
CI / changes (push) Successful in 1s
CI / unit (push) Successful in 8s
CI / integration (push) Successful in 11s
CI / ui (push) Successful in 31s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m6s
2026-06-08 14:07:49 +00:00
Ilia Denisov e16076c89e Stage 17 round 6 (#16-20): landing page, /app/ move, cache + stream fixes
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 31s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 55s
Close out Stage 17 round 6:

- Landing page at / — one Vite build with two entries (index.html = game
  SPA, landing.html = a lightweight landing reusing the theme/i18n/
  aboutContent leaf modules, not the app store).
- Move the web game SPA to /app/; the Telegram Mini App stays at /telegram/
  (gateway webui.Handler(stripPrefix, indexName): landing at /, SPA at /app/
  + /telegram/). Per-language "Play in Telegram" link via new
  VITE_TELEGRAM_LINK_EN/_RU build vars (button hides when unset).
- Cache headers: hash-named /assets/* immutable, HTML shells no-cache (the
  go:embed zero modtime emitted no validators, so the client re-downloaded
  the whole bundle every launch).
- Live-stream 15s abort fix: an immediate heartbeat on open + a 10s default
  interval (the first tick at 15s raced the edge idle timeout -> reconnect
  storm).

PLAN/ARCHITECTURE(§13)/FUNCTIONAL(+ru)/gateway+ui+deploy READMEs updated;
round 6 closed. Tests: gateway webui/connectsrv units, ui landing unit + e2e,
full e2e (60) green.
2026-06-08 13:33:05 +02:00
developer b8787a4123 Merge pull request 'Stage 17 round 6 (#4/#5/#6): draft persistence wire + gateway + UI' (#20) from feature/stage-17-round-6-drafts into development
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 9s
CI / integration (push) Successful in 11s
CI / ui (push) Successful in 32s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m1s
2026-06-08 11:08:42 +00:00
Ilia Denisov f5c2404123 Stage 17 round 6 (#4/#5/#6): draft persistence wire + gateway + UI
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 32s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m7s
Complete the client-side draft feature on top of the shipped backend
foundation (the game_drafts store/service):

- FB: DraftRequest{game_id,json} + DraftView{json} (a draft get reuses
  GameActionRequest); regenerated committed Go + TS bindings.
- Backend REST: GET/PUT /games/:id/draft, a draftDTO
  (rack_order/board_tiles) mapped to game.Draft.
- Gateway: draft.get/draft.save transcode forwarding the composition
  JSON verbatim (json.RawMessage both ways -- no double-encode).
- UI: debounced save of the rack order + board tiles and restore on
  load (lib/draft.ts), plus #5 -- tiles may be arranged on the
  opponent's turn (placement relaxed; the preview and Make-move stay
  your-turn-only, so an off-turn draft is position-only).

Tests: backend handler validation, gateway pass-through round-trip, UI
draft/codec units, and a draft-restore e2e.
2026-06-07 22:25:29 +02:00
developer 353dff20c4 Merge pull request 'Stage 17: test-contour verification & defect fixes' (#19) from feature/stage-17-contour-verification-fixes into development
CI / changes (push) Successful in 1s
CI / unit (push) Successful in 8s
CI / integration (push) Successful in 11s
CI / ui (push) Successful in 29s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m9s
2026-06-07 19:20:40 +00:00
Ilia Denisov 3632c2239f Stage 17 round 6: log progress + the next-pass plan in the tracker
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 30s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m6s
Record round 6 in PLAN.md so the repository (not conversation memory) carries the trace:
the shipped follow-ups (profile, tap-flash, alphabet-keyed variant names + title, chat/nudge
by turn + cooldown reset, About + git-describe version, quick-game rules plaques, the two
fixes, #3 rack drag-reorder, and the #4/#5/#6 persistence backend foundation), plus the
REMAINING next-pass work with ready designs — the persistence gateway op-slice + UI wiring
(lean JSON-string FB; #5 off-turn placement) and the landing + /app/ move (#16-20).
2026-06-07 21:16:45 +02:00
Ilia Denisov 06c8039281 Stage 17 round 6 (#4/#5/#6 backend): per-game draft store + conflict reset
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 30s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m3s
Foundation for persisting a player's client-side composition: a game_drafts table
(game_id, account_id, rack_order, board_tiles jsonb) with raw-SQL store/service methods —
GetDraft/SaveDraft (seated-player check) and, on every committed move, clearing the actor's
own draft and resetting any opponent's board draft whose cell the play overlapped (the
draft can no longer be placed; the rack order is kept). Integration tests cover the
round-trip, the actor clear, the overlap reset, a non-conflicting survival, and the
outsider rejection. The gateway op slice + UI wiring follow.
2026-06-07 12:29:32 +02:00
Ilia Denisov 2b0b1c0035 Stage 17 round 6 (#3): drag-reorder rack tiles with a visual gap
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 30s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m7s
Dragging a rack tile and dropping it back on the rack reorders it: the dragged tile is
lifted out (the drag ghost stands in) and the tiles at/after the pointer's drop slot slide
right to open a gap there, so the drop position is visible. On drop the rack and its stable
ids are permuted (reorderIndices, unit-tested). Reorder applies only with no pending tiles,
so it stays a clean permutation; dropping on a board cell still places as before. Server
persistence of the order follows (#4).
2026-06-07 12:21:09 +02:00
Ilia Denisov 35666e1705 Stage 17 round 6 fixes: pin the nudge button right; schematic USSR flag emblem
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 30s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m7s
- Chat: always render the (possibly empty) flex:1 caption before the nudge button, so the
  nudge stays pinned right whether or not the cooldown text shows (it drifted left when
  available).
- USSR flag: redraw the hammer & sickle as a thin schematic sketch — an elongated
  semicircle sickle with a handle, crossed by a T-shaped hammer (per the original's
  structure), instead of the bold over-filled emblem; the star is a touch smaller.
2026-06-07 12:10:52 +02:00
Ilia Denisov d3657fdf5c Stage 17 round 6 (#11/#12): quick-game variant plaques with rules, flag, and move-limit
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 30s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 55s
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.)
2026-06-07 11:48:19 +02:00
Ilia Denisov 74683f294f Stage 17 round 6 (#13/About): About screen content + app version from git describe
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 29s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 56s
- About screen: prominent localized title (Scrabble / Эрудит (Скрэббл)), a rules link
  (en/ru Wikipedia), and the Random-game / Game-with-friends sections; copy lives in a
  shared aboutContent module (the landing will reuse it). The random-game move limit
  inlines the 24h auto-match clock.
- App version: Vite define __APP_VERSION__ from VITE_APP_VERSION (default 'dev'), wired as
  a Docker build-arg sourced from `git describe --tags --always` in the deploy step — no
  manual version bumps. The fallback keeps a plain/local build working.
2026-06-07 11:39:31 +02:00
Ilia Denisov cdf616d6c4 Stage 17 round 6 (#7): reset the nudge cooldown once the player acts
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 30s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m4s
The hourly nudge cooldown now clears as soon as the sender has moved or posted a chat
since their last nudge — engagement lifts the 'don't spam' limit. Backend: Nudge checks
game.LastMoveAt + the sender's last non-nudge chat against the last nudge time
(GameReader gains LastMoveAt). UI: nudgeOnCooldown mirrors it — a chat reset is read from
the message list, a move is tracked client-side (lastActedAt on commit/pass/exchange; the
backend stays authoritative across a reload). Integration test covers the reset.
2026-06-07 11:32:08 +02:00
Ilia Denisov 2cb2b57cdb Stage 17 round 6 (#10 backend): enforce chat only on your turn
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 30s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m3s
PostMessage now rejects a chat sent on a finished game or when it is not the sender's
turn (ErrChatNotYourTurn -> 409 chat_not_your_turn), matching the UI where the message
field is hidden off-turn and only the nudge shows. Existing chat tests post on the
to-move seat and are unaffected; adds an off-turn-rejection integration test + the dto
mapping case + the UI error message.
2026-06-07 11:23:43 +02:00
Ilia Denisov 512ad4dfb9 Stage 17 round 6 (cluster 1): profile, tap flash, variant naming, chat/nudge by turn
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 30s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m3s
- Profile: drop the hint-balance line.
- Board: no mobile tap flash on a cell tap (-webkit-tap-highlight-color: transparent),
  matching the web click; the only intentional cell animation stays the last-word flash.
- Variant names keyed by the game's alphabet, not the UI language: english -> Scrabble
  always, russian_scrabble -> Скрэббл always (unlocalized, never collide), erudit localized.
- Chat/nudge are mutually exclusive by turn: the message field + Send show on your turn,
  the nudge replaces them on the opponent's turn; while the nudge cooldown is active the
  button is disabled with a grey 'awaiting reply' caption to its left.
2026-06-07 11:18:25 +02:00
Ilia Denisov a420d6a2cd Stage 17 round 5 docs: bake the bug fixes + UI polish + L2 into live docs
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 29s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 52s
- ARCHITECTURE: resign on the opponent's turn (ResignSeat + turn-check bypass); robots
  block chat but accept-and-ignore friend requests; quick-match /lobby/cancel; the admin
  robot play-to-win intent + next-move ETA panel.
- UI_DESIGN: even A->B zoom (recentre only on zoom-in), pinch, drop-target highlight,
  shuffle ≤0.3s + reduce-motion, borderless make-move disabled on illegal, variant title.
- FUNCTIONAL (+ru): variant display names (Scrabble/Erudite); robot ignores friend requests.
- PLAN: round-5 refinements bullet (+ the bilingual two-Scrabble open edge).
2026-06-07 09:48:08 +02:00
Ilia Denisov f916d5e0ca Stage 17 round 5 (L2): robot play-to-win intent + next-move ETA in the admin game card
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 29s
CI / gate (pull_request) Successful in 1s
CI / deploy (pull_request) Successful in 1m14s
The admin game detail now shows, per robot seat, the game's deterministic play-to-win
decision (from the bag seed) and — while it is that robot's turn — its scheduled next-move
ETA (sampled think-time delay, deferred past the sleep window), plus a caption with the
~40% global target. Wiring: robot.PlayToWin/NextMoveAt/PlayToWinTargetPercent exports,
account.IsRobot, game RobotSchedule (seed + turn-start). Tests: NextMoveAt invariants
(never early, never in the sleep window), PlayToWin export, and an admin render integration
test asserting the intent + ETA + target appear.
2026-06-07 09:42:23 +02:00
Ilia Denisov 29d1193a0a Stage 17 round 5 — board interaction & UI polish
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 29s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
- Even zoom: interpolate the board scroll toward a pre-clamped target as the real width
  grows/shrinks, so it magnifies A->B in one motion instead of lurching and snapping back
  near the edges/centre. Recentre only on a zoom toggle, never on a focus change — so a
  2nd+ placed tile and a hovered dragged tile no longer jump the board.
- Drag: highlight the aimed-at empty cell as a drop target; hover-hold auto-zoom now
  fires only for the first (zoom-in) hold.
- Pinch zoom: two-finger spread/close toggles zoom toward the pinch midpoint (preventDefault
  only for two touches, so one-finger scroll stays native); a second finger aborts a drag.
- Shuffle hop capped at 0.3s and disabled under reduce-motion.
- Make-move is a borderless icon button, disabled while the pending word is known illegal.
- Variant display names: english & russian_scrabble -> Scrabble/Скрэббл, erudit ->
  Erudite/Эрудит; the in-game title shows the variant name (was always 'Scrabble').
2026-06-07 09:34:07 +02:00
Ilia Denisov 3899ffda0f Stage 17 round 5: fix robot-pool test for the new friend-request policy
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 30s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Failing after 11s
TestRobotPoolProvisionsRobotAccounts asserted robots block friend requests; they no
longer do (a request stays pending and expires like a human ignore). Assert chat is
blocked and friend requests are open. (Unblocks the integration job / contour deploy.)
2026-06-07 09:21:22 +02:00
Ilia Denisov 10412fee8e Stage 17 round 5 — backend/correctness bug fixes
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Failing after 12s
CI / ui (pull_request) Successful in 30s
CI / gate (pull_request) Failing after 1s
CI / deploy (pull_request) Has been skipped
- 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.
2026-06-07 09:17:35 +02:00
Ilia Denisov 3856b34f8a Stage 17 docs: round-4 UI (inline profile, double-tap/drag recall, hover-zoom, animated shuffle, lines-off board)
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 29s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 41s
- UI_DESIGN: double-tap recall vs zoom, hover-hold drag auto-zoom, placing & recall
  rules, grid-lines toggle (gapless checkerboard default), animated shuffle; fix the
  stale MakeMove/Reset description (direct  button + ↩️ Reset tab, no popover).
- FUNCTIONAL (+ru): optional trailing '.' in display names; profile edited inline.
- PLAN: robot early band [1,5]→[3,10] (#14); round-4 refinements + deferred #2/#16.
2026-06-06 14:55:17 +02:00
Ilia Denisov 71b054227a Stage 17 (#12): lines-off board variant (gapless checkerboard), Settings toggle
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 30s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 55s
Add a 'Grid lines' preference (default off): when off the board drops the 1px grid
gaps for a gapless checkerboard (plain cells alternate shades; tiles get rounded
corners and a soft right-side shadow so adjacent gapless tiles still read apart),
saving ~14px of width. When on, the classic lined grid returns. Persisted with the
other board-style prefs; wired through Board's new lines prop. e2e locks the default
and the toggle.
2026-06-06 14:51:48 +02:00
Ilia Denisov d0c1306d9b Stage 17 (#9): animated shuffle — tiles hop along a low parabola to new slots
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 28s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 53s
Give each rack slot a stable id permuted with the letters on shuffle, so the keyed
rack reorders (rather than relabelling in place) and Svelte's animate directive fires.
hop flies each tile along a parabola (apogee ~half a tile height) with a duration that
scales with the horizontal distance (arc length): the longest 1<->7 swap takes ~0.5s,
shorter swaps land sooner. Ordinary reflow (place/recall) stays instant via a guard.
e2e locks that a shuffle preserves the rack's tile multiset.
2026-06-06 14:46:59 +02:00
Ilia Denisov 1bbf0bc654 Stage 17 (#3,#5,#10): hover-hold drag zoom, always-editable profile, drag-back + double-tap recall
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 27s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 56s
- Board drag now auto-zooms toward a cell after holding the tile over it ~1s (#3).
- Profile is inline-editable: drop the Edit/Cancel toggle, form is always shown
  for durable accounts; hint balance stays read-only; re-populate after link/merge (#5).
- A pending tile recalls by double-tap (same cell) or by dragging it back onto the
  rack (unzoomed board); a single tap no longer recalls (#10).
- e2e: lock double-tap recall + single-tap no-op; drop the removed Edit-profile click.
2026-06-06 14:42:09 +02:00