- 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.
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.
- 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.
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.
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.
- 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.
- Grafana: disable Live (GF_LIVE_MAX_CONNECTIONS=0) so its WebSocket no longer trips caddy Basic-Auth and re-prompts; admin console gains a Grafana nav link
- deploy: force-recreate config-only services so reseeded Grafana dashboards / Caddyfile are actually picked up (the move-duration panel was invisible because the bind-mount went stale)
- rate-limit: raise per-user budget 120/40 -> 300/80; UI skips reloading on the echo of the player's own move (fewer requests, no double-load)
- iOS/Telegram reconnect: suppress the connection banner while backgrounded and for a short grace after resume; reconnect silently; wire visibilitychange + pageshow/pagehide + Telegram activated/deactivated (Bot API 8.0)
- hint button disabled when 0 hints remain; nudge button shows a disabled state on your own turn
- players plaque: invert so the active seat pops (accent chip, raised) and others recede
- make-move UX: a direct ✅ commit button (no hold/popover); the Shuffle tab becomes ↩️ Reset while tiles are pending
- #10 a `changes` job path-filters unit/integration/ui; an always-running `gate` job aggregates them (success-or-skipped) and becomes the only required check
- #9 deploy adds a Telegram-connector liveness probe (docker inspect: running, not restarting, stable restart count) with a VPN-handshake grace period
- #1a Game-domain dashboard gains a 'Move think-time by phase (p50/p95)' panel
- deploy README: branch protection now requires only CI / gate
Root cause of the Grafana "readdirent /etc/grafana/dashboards: no such file or
directory": the CI runner checks out into an ephemeral act workspace that is
removed after the job, so binding the compose config files straight from it
dangles the mounts in the long-lived containers (verified the act source dir is
emptied after the job). caddy/otelcol/prometheus/tempo read their config once at
startup so they survive, but would break on a restart — same latent bug.
Fix (mirrors ../galaxy-game's $HOME/.galaxy-dev/monitoring): the deploy job seeds
the config dirs to a stable $HOME/.scrabble-deploy and the compose binds them via
${SCRABBLE_CONFIG_DIR:-.} (local runs keep "."). Documented in the compose header,
deploy/README.md and the ci.yaml step.
- deploy/docker-compose.yml: mount the provisioned dashboards at
/etc/grafana/dashboards, not /var/lib/grafana/dashboards — the grafana-data
volume mounts over the latter and shadows the nested bind, so the provider
logged "readdirent /var/lib/grafana/dashboards: no such file or directory".
dashboards.yaml provider path updated to match.
- Connector telemetry stays OTLP. The VPN sidecar's netns reaches the collector's
internal IP fine (connected route, off-tunnel), but the sidecar's DNS hijacks
name resolution: AWG_CONF must NOT carry a DNS= directive, else otelcol won't
resolve ("produced zero addresses"). Without DNS= the netns uses Docker's
resolver (resolves both otelcol and api.telegram.org). Documented in
deploy/README.md (AWG_CONF row + wiring note), ARCHITECTURE §13, compose comment.
- PLAN.md: new Stage 17 "Test-contour verification & defect fixes" (exercise the
deployed contour end-to-end and fix what it surfaces — connector liveness check,
path-conditional CI); the former prod-deploy stage becomes Stage 18.
- Renumber every "Stage 17" prod-deploy reference to "Stage 18" across docs,
compose, Caddyfile, ci.yaml and CLAUDE.md; the post-Stage-14 split range is now
"Stages 15–18".
- bot.New now selects Telegram's test environment with the library's native
tgbot.UseTestEnvironment() instead of a token += "/test" hack (functionally
identical URL /bot<token>/test/METHOD, but idiomatic) + a bot test asserting
the getMe path for both test and prod.
- ci.yaml pins TELEGRAM_TEST_ENV=true for the test contour (it IS the test
environment) instead of a TEST_TELEGRAM_TEST_ENV variable: removes the
confusing double-TEST, telegram-specific, prefixed operator knob and the
secret-vs-variable footgun. Prod (Stage 17) leaves it false.
- deploy/README.md + PLAN.md updated.
- deploy/README.md documents the services, how to run it locally and in CI, and
every variable: required (the four :? ones + ≥1 bot token) and optional with
defaults, marked secret-vs-variable and with the TEST_/PROD_ Gitea mapping;
plus the fixed internal wiring and the host-side setup.
- ci.yaml maps the remaining POSTGRES_DB/USER, DICT_VERSION and LOG_LEVEL (unset
renders empty -> the compose ":-" defaults apply), so every documented var is
per-contour overridable.
- .env.example points at the README for the full reference.
- backend + gateway multi-stage distroless Dockerfiles; the gateway embeds and
serves the SPA at / and /telegram/ via go:embed (committed dist placeholder,
real build baked in by the image's node stage)
- deploy/docker-compose.yml: backend + gateway + Postgres + Telegram connector
(VPN sidecar) + OTel Collector + Prometheus (15d) + Tempo (72h) + Grafana,
fronted by a caddy owning a single /_gm Basic-Auth (admin console + Grafana
subpath); inter-service on a private network, only caddy on the edge network
- new metrics: backend accounts_created_total{kind} (robots excluded) and an
in-memory gateway active_users{window=24h,7d} gauge
- CI: single .gitea/workflows/ci.yaml (unit/integration/ui + a gated test-contour
deploy) on the new feature/* -> development -> master branch model; the old
go-unit/integration/ui-test workflows are folded in; the connector-scoped
compose is retired (superseded by deploy/)
- docs: ARCHITECTURE §11/§12/§13, root + gateway READMEs, CLAUDE.md branching,
PLAN.md (stage 16 done + refinements + Stage 17 forward-notes)