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.
This commit is contained in:
Ilia Denisov
2026-06-08 13:33:05 +02:00
parent b8787a4123
commit e16076c89e
27 changed files with 519 additions and 82 deletions
+7 -4
View File
@@ -573,13 +573,16 @@ a dedicated redeem sub-limit or a longer code is the hardening step if abuse app
## 13. Deployment (informational)
Single public origin, path-routed. The gateway **embeds** the static UI build
(`go:embed`, baked in by a node stage in `gateway/Dockerfile`) and serves the one
SPA at both `/` (web) and `/telegram/` (the Telegram Mini App; outside Telegram that
path redirects to the root — the client-side guard). An in-compose **caddy** is the
(`go:embed`, baked in by a node stage in `gateway/Dockerfile`). The Vite build has two
entries: a lightweight **landing page** served at `/`, and the game **SPA** served at
`/app/` (web) and `/telegram/` (the Telegram Mini App; outside Telegram that path
redirects to the root — the client-side guard). Hash-named `/assets/*` are served
`immutable` (a relaunch is a cache hit, not a re-download); the HTML shells are
`no-cache` so a new deploy is picked up. An in-compose **caddy** is the
contour's edge: it owns a single `/_gm` Basic-Auth and routes `/_gm/grafana/*` to
**Grafana** (anonymous-admin, so the one shared login gates it with no per-user
Grafana accounts) and the rest of `/_gm/*` to the backend-rendered **admin console**;
everything else (`/`, `/telegram/`, the Connect edge) goes to the gateway. The
everything else (`/`, `/app/`, `/telegram/`, the Connect edge) goes to the gateway. The
**Telegram connector** runs as a separate container with **no public ingress** — it
long-polls Telegram and egresses through a VPN sidecar, answering only internal gRPC.
+8 -1
View File
@@ -21,6 +21,9 @@ email, the statistics screen, and the in-game history viewer with GCG export.
Settings also pick the board's bonus-label style (beginner / classic / none). A hint **lays the suggested tiles on the board** for the player to confirm and
costs nothing when the rack has no legal move. The word-check accepts only the
variant's alphabet, remembers answers within the session and rate-limits repeats.
A public **landing page** at the site root introduces the game, switches language and
theme, and links into the web app or the matching Telegram bot; the game itself runs at
`/app/` (web) and `/telegram/` (the Telegram Mini App).
### Identity & sessions *(Stage 1 / 6 / 9 / 15)*
A player arrives from a platform (Telegram first), via email login, or as an
@@ -83,7 +86,11 @@ the other player and the leaver keeps their score. In a game with three or four
players the leaver's seat is dropped and the others play on, the game ending when a
single active player remains; the disposition of the leaver's tiles (returned to
the bag or removed from play) is chosen when the game is created, and the leaver's
rack is never shown to the others.
rack is never shown to the others. A player's **board composition is kept per game**:
the rack arrangement and the tiles laid but not yet submitted are saved as they compose
and restored on return (including on another device); a player may **arrange tiles during
the opponent's turn**, but that draft is position-only — the score preview and submission
stay available only on the player's own turn.
### Robot opponent *(Stage 5)*
When auto-match finds no human within ten seconds, a robot opponent takes the empty
+8 -1
View File
@@ -22,6 +22,9 @@ top-1 подсказку, безлимитную проверку слова с
доску** — игрок сам решает сделать ход, и подсказка не тратится, если ходов нет.
Проверка слова принимает только алфавит варианта, запоминает ответы в рамках сессии
и ограничивает частоту повторов.
Публичная **посадочная страница** в корне сайта представляет игру, переключает язык и
тему и ведёт в веб-приложение или в соответствующего Telegram-бота; сама игра живёт по
адресам `/app/` (веб) и `/telegram/` (Telegram Mini App).
### Личность и сессии *(Stage 1 / 6 / 9 / 15)*
Игрок приходит с платформы (сначала Telegram), через email-вход или как
@@ -85,7 +88,11 @@ Mini App** авторизует по подписанным `initData` плат
место вышедшего убирается, остальные играют дальше, и партия завершается, когда
остаётся один активный игрок; что делать с фишками вышедшего (вернуть в мешок или
убрать из игры) выбирается при создании партии, а его стойка никогда не
показывается остальным.
показывается остальным. **Композиция на доске сохраняется по партии**: расположение
фишек на стойке и выложенные, но не отправленные фишки сохраняются по мере составления
хода и восстанавливаются при возврате (в том числе на другом устройстве); игрок может
**раскладывать фишки и в ход соперника**, но такой черновик только позиционный —
предпросмотр счёта и отправка доступны лишь в собственный ход.
### Робот-соперник *(Stage 5)*
Если авто-подбор не находит человека за десять секунд, свободное место занимает