Commit Graph

155 Commits

Author SHA1 Message Date
Ilia Denisov c16f27475f R7: contour docker_stats observability + container limits/GOMAXPROCS
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 37s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m21s
Observability: replace cAdvisor (which resolves only the root cgroup on the
contour host — separate-XFS /var/lib/docker) with the otelcol docker_stats
receiver, which reads per-container CPU/memory/network straight from the Docker
API and works the same in prod. The collector joins the host docker group
(DOCKER_GID, default 989) and mounts the socket read-only; its metrics flow out
through the existing prometheus exporter, so the cAdvisor scrape job and the
privileged cAdvisor service are removed. The Resources dashboard panels are
retargeted to the docker_stats metric names (container_name label;
container.cpu.utilization/100 == cores).

Container limits: apply deploy.resources.limits (honoured by Compose v2) across
the contour and pin GOMAXPROCS to the CPU limit on the Go services so the runtime
matches the cgroup quota. Starting values are generous over the R2 peak (~1 core /
<=100 MiB per app service) to avoid skewing or OOM-killing the measurement run;
they are tightened to the agreed prod sizing after the final stress run (R7
Round 2). The privileged VPN sidecar is left unconstrained.
2026-06-10 18:53:19 +02:00
Ilia Denisov 04263a17ca R7: per-player transports + drop finished games in the load harness
Each virtual player now builds its own edge.Client (its own h2c connection
carrying both the Subscribe stream and the Execute calls), instead of every
player multiplexing over a single shared http2.Transport. The R2 trip report
traced the ~14% transport_error on game.state at 500 players to that single
shared transport; per-player connections mirror real clients and isolate the
artifact. The assembly burst and the gateway-hammer each get their own client.

playTurn now reports when a game has finished so playerLoop drops it from the
rotation (slices.DeleteFunc); once no active game remains the player idles while
still holding its stream. This stops secondary ops from hammering game_finished
on already-ended games (the other R2 harness finding).
2026-06-10 18:53:07 +02:00
developer 7210bed560 Merge pull request 'R6: refactor + docs reconciliation + de-staging' (#37) from feature/r6-refactor-destage 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 36s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m1s
2026-06-10 16:03:18 +00:00
Ilia Denisov 40ccfb9514 R6: mark phase done in PRERELEASE.md + log refinements
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 37s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m12s
2026-06-10 17:32:30 +02:00
Ilia Denisov c6e0dac940 R6(c): centralize shared integration-test fixtures in helpers.go
Move the cross-file integration fixtures — the service constructors
(newGameService/newSocialService/newRobotService/newMatchmaker), the game-assembly
helpers (newMirror/newGameWithSeats/newDraftGame), account provisioning
(provisionAccount/provisionGuest) and the stats reader — out of the domain test
files (newGameService alone was used by 10 files) into a single
backend/internal/inttest/helpers.go. Helpers used by a single file stay local.

Pure relocation: the helper bodies are unchanged, no test logic changes; the
imports the moves left unused are pruned. go vet -tags=integration is clean.
2026-06-10 17:30:53 +02:00
Ilia Denisov b47c47e969 R6(c): share the nested FB builders between notify and gateway transcode
Extract the FlatBuffers builders for the wire tables shared by the backend push
encoder and the gateway edge transcoder — GameView, MoveRecord, StateView,
AccountRef, Invitation and their nested rows — into a new scrabble/pkg/wire
package. Both callers keep their local builder signatures (no call sites move)
but now map their own source types (the backend's notify.* payloads and the
decoded engine.MoveRecord; the gateway's backendclient.* REST DTOs) to neutral
wire.* structs and delegate the construction to package wire, the single
definition of the nested-table layout.

Behaviour-preserving: the verified-identical field sets mean the wire bytes
decode the same, and the notify + transcode round-trip tests pass unchanged. The
fiddly Start/Add/End + reverse-prepend vector boilerplate now lives once; the two
encode files shrink while pkg/wire carries the shared logic.
2026-06-10 17:21:18 +02:00
Ilia Denisov 1079878654 R6(c): drop dead opponent_moved scalars (seat/action/score/total)
These pre-R4 summary scalars on OpponentMovedEvent were redundant with the
move/game delta and read by nobody — the UI codec and mock take only
move/game/bag_len, and the gateway forwards the push payload verbatim. Removed
from scrabble.fbs, the notify emit (notify/events.go) and the round-trip test;
regenerated the FB Go + TS bindings. No prod data, so the wire-slot renumber is
free and there is no DB change.
2026-06-10 17:06:25 +02:00
Ilia Denisov c31ac7088c R6(b): reconcile docs with code — restore guest-reaper mention
Pass (a) removed a stale "(reaping abandoned guest rows is deferred — TODO-3)"
note from ARCHITECTURE §3, but guest reaping is implemented (the background
reaper, BACKEND_GUEST_REAP_INTERVAL / BACKEND_GUEST_RETENTION, covered by
inttest). State the current behaviour instead.

A full section-by-section review of ARCHITECTURE / FUNCTIONAL (+_ru) / TESTING /
UI_DESIGN against the code found no other drift — each R-phase baked its own docs,
and FUNCTIONAL/TESTING already describe the reaper correctly.
2026-06-10 17:02:48 +02:00
Ilia Denisov 8881214213 R6(a): de-stage code, docs, READMEs; split stage6_test
Mechanical, behaviour-preserving removal of Stage N / TODO-N / phase (RN)
references from comments, doc-comments, service READMEs, the current-state docs
(ARCHITECTURE, FUNCTIONAL+_ru, TESTING, UI_DESIGN), config-file comments, and the
.fbs/.proto schema comments. PLAN.md / PRERELEASE.md / CLAUDE.md keep the stage
history.

- Rename the only stage-named identifiers: registerStage8 -> registerSocialOps,
  registerStage11 -> registerLinkOps (gateway transcode).
- Split stage6_test.go: TestEmailLoginFlow -> email_test.go,
  TestGuestAutoMatchLeavesNoStats (+ provisionGuest) -> account_test.go.
- Regenerated proto bindings (push.pb.go, telegram_grpc.pb.go) from the de-staged
  .proto comments; FB Go/TS bindings unchanged (flatc strips schema comments).

go build/vet/gofmt clean across modules; integration typecheck and pnpm check green.
2026-06-10 16:56:03 +02:00
developer a372343797 Merge pull request 'R5: bundle slimming — retarget the budget to the app, no code slimming' (#36) from feature/r5-bundle-budget 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 36s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 57s
2026-06-10 13:18:27 +00:00
Ilia Denisov d4ef951db9 R5: bundle slimming — retarget the budget to the app, no code slimming
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 37s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m0s
Analysed the real dist (gzip + sourcemap attribution): the bundle is already minified + tree-shaken and dominated by the Connect/FlatBuffers transport runtime + generated bindings + the Svelte runtime (~2/3 of main), so no in-scope code slimming is warranted. Lazy-loading was rejected (bundle-size.mjs sums every chunk -> zero total-size win, plus +N gateway fetches of latency); i18n lazy-load and chunk-collapsing likewise (caching/HTTP2).

Instead bundle-size.mjs now measures per HTML entry with three independent gates (app entry <=100 KB, Svelte+i18n shared <=30 KB, landing-own <=5 KB): the app's real payload is its entry chunk + the shared chunk (~97 KB), never landing.js. Same CLI + exit-code contract, CI step unchanged. Fixed the stale ~82 KB figure in the script and ui/README.md. No app code change.
2026-06-10 15:11:45 +02:00
developer 7ec17cdd53 Merge pull request 'R4: push enrichment — events carry a state delta, kill the last poll' (#35) from feature/r4-push-enrichment into development
CI / changes (push) Successful in 1s
CI / unit (push) Successful in 10s
CI / integration (push) Successful in 11s
CI / ui (push) Successful in 37s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 59s
2026-06-10 10:30:49 +00:00
Ilia Denisov 41a642ef97 R4: push enrichment — events carry a state delta, kill the last poll
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 37s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
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.
2026-06-10 08:01:50 +02:00
developer e3b08461f0 Merge pull request 'R3: edge hardening — body cap, rate-limit observability, auto-flag, landing split' (#34) from feature/r3-edge-hardening 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 36s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 57s
2026-06-10 03:38:51 +00:00
Ilia Denisov 7e75c32d07 R3: dashboards, docs and tracker bake-back
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 36s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m7s
- Edge/UX dashboard: aggregate request-rate vs rejection-rate panel
  (gateway_rate_limited_total by class; no per-user labels).
- ARCHITECTURE §2/§11/§12/§13: body cap + explicit h2c sizing, the rate-limit
  observability pipeline and auto-flag policy, the admin-limiter note (and the
  caddy-path gap), the landing container topology; fixed the stale 120/min
  per-user figure.
- FUNCTIONAL (+_ru): the Throttled view and the reversible high-rate flag.
- gateway/backend/deploy READMEs, TESTING.md, root CLAUDE.md updated.
- PRERELEASE.md: R3 interview decisions + implementation refinements logged;
  tracker R3 -> done (this PR implements it; CI gates the merge).
2026-06-10 05:12:30 +02:00
Ilia Denisov f20a4b49ff R3: split the landing into its own static container
- gateway/Dockerfile gains a `landing` target: caddy:2-alpine + the shared
  Vite build (identical build args keep the ui stage a single cached build);
  the gateway target drops landing.html from the embed.
- The contour caddy routes /app/, /telegram/ and the Connect path to the
  gateway; the catch-all — the landing at / and any stray path — goes to the
  new landing service, so junk traffic is absorbed by static file serving.
- deploy/landing/Caddyfile mirrors the webui caching (immutable assets,
  no-cache shells) and falls back unknown paths to the landing shell.
- The gateway's / now 308-redirects to /app/ (keeps a local no-caddy run
  usable); webui placeholder landing.html removed.
- CI deploy probe checks both / (landing) and /app/ (gateway).

Verified: both images build; the landing container serves landing.html at /
(no-cache) with junk-path fallback; the gateway image redirects / to /app/
and carries no landing content.
2026-06-10 02:20:10 +02:00
Ilia Denisov ab58062565 R3: backend rate-limit observability — ratewatch, auto-flag, admin throttled view
- accounts.flagged_high_rate_at baked into the R1 baseline (no prod data; the
  contour schema is wiped after merge); jet regenerated — the regen also picks
  up the previously missing game_drafts/game_hidden models.
- account.Store: FlagHighRate (set-once), ClearHighRateFlag, the flag in
  GetByID/ListUsers and a ListFlaggedHighRate review queue.
- New internal/ratewatch: ingests the gateway rejection reports, keeps a
  bounded in-memory episode window for the console and applies the
  conservative auto-flag (1000 rejected / 10 min, BACKEND_HIGHRATE_FLAG_*).
- POST /api/v1/internal/ratelimit/report (network-trusted, like
  sessions/resolve).
- Admin console: Throttled page (episodes + flagged accounts), a high-rate
  badge in the user list, the marker + operator clear action on the user card.
- Tests: ratewatch unit suite, report-route handler test, renderer cases,
  integration coverage for the store round-trip and the console flow.
2026-06-10 02:14:10 +02:00
Ilia Denisov 8878711cf3 R3: gateway edge hardening — body cap, h2c sizing, rate-limit observability
- GATEWAY_MAX_BODY_BYTES (1 MiB): connect WithReadMaxBytes + http.MaxBytesReader
  on the public mux; explicit http2.Server MaxConcurrentStreams/IdleTimeout and
  an http.Server ReadHeaderTimeout (R2 report follow-up).
- gateway_rate_limited_total{class} counter, Debug per rejection, a rejection
  tracker drained every 30 s into a Warn summary per key and a report POST to
  /api/v1/internal/ratelimit/report (feeds the admin view + auto-flag).
- The dead AdminPerMinute/AdminBurst policy now guards the /_gm mount (429),
  ahead of its Basic-Auth.
- resolve() logs the cause of infra session-resolve failures at Warn (the
  transient unauthenticated dips from the R2 run); unknown tokens stay silent.
2026-06-10 01:58:48 +02:00
developer c23ac94c4e Merge pull request 'R2: stress harness + contour resource observability + early run' (#33) from feature/r2-loadtest-observability into development
CI / changes (push) Successful in 1s
CI / unit (push) Successful in 9s
CI / integration (push) Successful in 11s
CI / ui (push) Successful in 36s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 55s
2026-06-09 23:01:30 +00:00
Ilia Denisov a2265a122e R2: early-pass trip report + mark R2 done
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 37s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 57s
Ran the moderate early pass (50/200/500, 10 min/step) against the contour: ramped
clean to 500 players, 1.2 M edge calls, 48 870 plays, 2 798 games finished, no
crash/deadlock; cleanup removed all 11 000 seeded accounts. The per-user limiter held
under the gateway-hammer (99.97 % rejected, p99 2 ms).

Top finding: ~14 % transport_error on game.state at 500 players under CPU saturation
(backend/gateway/Postgres each ~1 core), amplified by the harness's single shared
http2.Transport (the harness itself peaked at 86 % of a core on the same host).
Observability finding: cAdvisor yields only the root cgroup on the contour host
(separate XFS /var/lib/docker); per-container metrics captured via docker stats; R7
should adopt the otelcol docker_stats receiver. Full report in loadtest/REPORT-R2.md;
PRERELEASE refinements logged; R2 marked done.
2026-06-10 00:47:16 +02:00
Ilia Denisov 422bd14b53 R2: harness payload fixes found by the smoke pass
- display-name marker: letters-only 'Zzloadtest' (the editable-name validator
  forbids digits/colons), so profile.update resends the seeded name successfully.
- draft.save: rack_order is a string in the backend draft DTO (was sent as []),
  fixing the bad_request.
Both confirmed ok against the contour. chat_not_your_turn / nudge_own_turn are
by-design turn gates (backend/internal/social/chat.go), correctly exercised.
2026-06-10 00:11:33 +02:00
Ilia Denisov 0c55574ddd R2: drop ./loadtest from the backend/gateway/telegram image builds
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 36s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
Adding the loadtest module to go.work (use ./loadtest + the scrabble/gateway
replace it needs) broke the other services' Docker builds: their reduced
workspace still referenced ./loadtest (not in their build context), failing with
'cannot load module loadtest: open loadtest/go.mod: no such file or directory'.
Each service Dockerfile now also -dropuse=./loadtest; backend and telegram (which
do not COPY ./gateway) additionally -dropreplace the loadtest-only scrabble/gateway
replace. Verified by building all three images plus loadtest locally.
2026-06-09 23:57:26 +02:00
Ilia Denisov aa137e3558 R2: load-test harness + contour resource observability
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 38s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Failing after 3s
New scrabble/loadtest module (the pre-release stress harness): seeds 1000 guest +
10000 durable accounts with pre-created sessions directly in Postgres (token hash
matches backend/internal/session), drives virtual players through the edge protocol
(real 2-4p games assembled via invitations, mid-ranked legal moves generated locally
by the embedded scrabble-solver — the edge carries no board, so the client replays
history), plus nudge/chat/check-word/draft/profile/stats and a gateway-hammer that
verifies the rate limiter. Prints a trip-report summary (per-op latency percentiles,
result codes, live-event tally). Go unit tests cover the pure pieces; the DAWG-backed
move test runs under BACKEND_DICT_DIR.

Contour: add cAdvisor + postgres_exporter + a 'Scrabble - Resources' Grafana
dashboard and the two Prometheus scrape jobs, for the R2/R7 stress-run resource
baseline.

CI: gate ./loadtest/... (path filter + vet/build/test). Docs: TESTING, ARCHITECTURE,
project CLAUDE repo layout.
2026-06-09 23:45:24 +02:00
developer bf3ee62711 Merge pull request 'R1: mark done in PRERELEASE.md (post-merge close-out)' (#32) from feature/r1-close into development
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Has been skipped
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m1s
2026-06-09 20:38:47 +00:00
Ilia Denisov 8bfc44aad0 R1: mark done in PRERELEASE.md (post-merge close-out)
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) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 55s
scrabble-game #31 + scrabble-dictionary #2 merged, v1.0.1 cut, contour DB wiped
and re-migrated to the baseline (verified).
2026-06-09 16:11:03 +02:00
developer bf07f77078 Merge pull request 'R1: schema & naming reset — squash migrations, rename variants' (#31) from feature/r1-schema-naming-reset into development
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 9s
CI / integration (push) Successful in 12s
CI / ui (push) Successful in 36s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 56s
2026-06-09 14:09:31 +00:00
Ilia Denisov 26aa154547 R1: schema & naming reset — squash migrations, rename variants
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 37s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
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.
2026-06-09 12:09:50 +02:00
developer 70e3fab512 Merge pull request 'Stage 17: robot-nudge frequency + per-game push language' (#30) from feature/nudge-fix 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 54s
2026-06-09 06:12:10 +00:00
Ilia Denisov bf7dca0a09 Stage 17: fix the robot-nudge frequency + per-game push language
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) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m6s
Two owner-reported defects from a live contour game.

A. Frequency: the robot's proactive nudge fired hourly for 12h+ (a 12h idle threshold
   then the 1h cooldown, uncapped). Replaced with a lengthening, randomized schedule
   (proactiveNudgeGap): the first nudge ~60-90 min into the human's turn, each later gap
   growing toward 1-6h (uniform sample in [60min, ceil], ceil ramping 90min->6h over 12h
   of idle, measured from the previous nudge), so a long wait gets a handful of
   increasingly-spaced reminders instead of a stream.

B. Language: out-of-app push routed by the recipient's GLOBAL service_language
   (last-login-wins), so after re-logging via the RU bot an English game's nudges came
   from the RU bot. Now a game push (your_turn, game_over, nudge, match_found) carries
   the game's own language (engine.Variant.Language) on push.Event, and the gateway
   routes by it (falling back to service_language for non-game pushes). The New-Game
   variant-gating guarantees the game's bot is one the player has started, so delivery is
   never blocked.

Tests: proactiveNudgeGap unit + retimed TestRobotProactiveNudge; TestVariantLanguage;
emit your_turn/game_over language; TestNudgeRoutedByGameLanguage integration. Docs:
ARCHITECTURE (§7 nudge, §10/§13 routing), FUNCTIONAL (+ _ru), PLAN tracker.
2026-06-09 08:06:58 +02:00
developer 265e442252 Merge pull request 'Stage 17 #2: Connecting indicator + auto-retry (no more red toasts)' (#29) from feature/connecting-indicator 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 36s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 58s
2026-06-09 05:46:43 +00:00
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