Stage 7 (wip): docs bake + stage renumber (insert UI Stage 8, shift +1)
Tests · Go / test (push) Successful in 7s
Tests · Integration / integration (push) Successful in 12s
Tests · UI / test (push) Failing after 51m19s

- PLAN.md: new Stage 8 (UI social/account/history); Telegram->9, Admin->10, Linking->11, Polish->12; tracker + Stage 7 refinements; split the Stage 6 'wired in Stage 7' note between 7 and 8
- ARCHITECTURE: promote ui to current (slice scope, board-replay, codegen, theming, mock)
- FUNCTIONAL(+ru): client-app section with the Stage 7/8 split
- README + ui/README + CLAUDE.md: UI build/run/test, codegen, pnpm notes
- bumped Stage 8-11 refs (+1) across docs and code comments
This commit is contained in:
Ilia Denisov
2026-06-03 01:01:58 +02:00
parent 0284c9b83a
commit 7a48327ab6
11 changed files with 274 additions and 43 deletions
+8
View File
@@ -121,4 +121,12 @@ go vet ./backend/...
gofmt -l . # must print nothing gofmt -l . # must print nothing
go test -count=1 ./backend/... go test -count=1 ./backend/...
go run ./backend/cmd/backend # /healthz, /readyz on :8080 go run ./backend/cmd/backend # /healthz, /readyz on :8080
cd ui && pnpm install && pnpm check && pnpm test:unit && pnpm build # the UI (Stage 7+)
pnpm start # UI mock mode: lobby -> game, no backend
``` ```
The `ui` module is a Node project (pnpm), **not** in `go.work`; its CI is
`.gitea/workflows/ui-test.yaml`. Committed edge codegen under `ui/src/gen/`
(regenerate with `pnpm codegen`); pnpm build-script approval lives in
`ui/pnpm-workspace.yaml` (`allowBuilds: esbuild: true`).
+129 -19
View File
@@ -40,11 +40,12 @@ independent (see ARCHITECTURE §9.1).
| 4 | Lobby & social (matchmaking, friends, block, chat, profile, nudge) | **done** | | 4 | Lobby & social (matchmaking, friends, block, chat, profile, nudge) | **done** |
| 5 | Robot opponent | **done** | | 5 | Robot opponent | **done** |
| 6 | Gateway edge (Connect/FB, platform auth, sessions, push bridge, admin) | **done** | | 6 | Gateway edge (Connect/FB, platform auth, sessions, push bridge, admin) | **done** |
| 7 | UI (plain Svelte + Vite, board, lobby, chat, i18n) | todo | | 7 | UI playable slice (Svelte+Vite, board, lobby, chat, hint/word-check, i18n) | todo |
| 8 | Telegram integration (bot side-service, deep-link, push) | todo | | 8 | UI — social/account/history (friends, blocks, invitations, profile edit, stats, history/GCG) | todo |
| 9 | Admin & dictionary ops (complaint review, version reload) | todo | | 9 | Telegram integration (bot side-service, deep-link, push) | todo |
| 10 | Account linking & merge | todo | | 10 | Admin & dictionary ops (complaint review, version reload) | todo |
| 11 | Polish (observability, perf with evidence, deploy) | todo | | 11 | Account linking & merge | todo |
| 12 | Polish (observability, perf with evidence, deploy) | todo |
Scaffolding is incremental: `go.work` lists only existing modules; each stage Scaffolding is incremental: `go.work` lists only existing modules; each stage
adds the modules it needs. adds the modules it needs.
@@ -70,7 +71,7 @@ platform identities.
Open details: Postgres version + DSN/`search_path` convention; jet vs Open details: Postgres version + DSN/`search_path` convention; jet vs
sqlc/sqlx (default jet); migration naming; exact session-token shape (opaque sqlc/sqlx (default jet); migration naming; exact session-token shape (opaque
random length, TTL, revocation); account/identity table shape; whether the random length, TTL, revocation); account/identity table shape; whether the
admin bootstrap lands here or in Stage 9. admin bootstrap lands here or in Stage 10.
### Stage 2 — Engine package ### Stage 2 — Engine package
Scope: `backend/internal/engine` over scrabble-solver — versioned DAWG Scope: `backend/internal/engine` over scrabble-solver — versioned DAWG
@@ -120,25 +121,90 @@ available); Capacitor-ready structure.
Open details: detailed game-board UX (deferred by the owner to this stage); Open details: detailed game-board UX (deferred by the owner to this stage);
client routing; offline/refresh behaviour; design system / theming. client routing; offline/refresh behaviour; design system / theming.
### Stage 8 — Telegram integration #### Suggested layouts (lobby + game screen)
User note:
> Detailed interview about UI/UX is **strongly** required.
> Too much to discuss.
```text
┌────────────────────┐
│ Display_Name =│- Profile
├────────────────────┤- Settings
│ Invitations │- About
│ - list │
├────────────────────┤
│ Active games │
│ - list │
├────────────────────┤
│ Finished games │
│ - list │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
├────────────────────┤
│ ┌───┐ ┌───┐ ┌───┐│
│ New │ Stats Tourn│
│ └───┘ └───┘ └───┘│
└────────────────────┘
┌────────────────────┐
Lobby│◄ ==│- History
├────────────────────┤- Chat
│You Ann Kaya Rick│- Check word
│136 700 179 39│- Drop game
├────────────────────┤
│ │
│ │
│ │
│ c │
│ words │
│ o │
│ s │
│ s │
│ │
│ │
├──┬──┬──┬──┬──┬──┬──┤ ┌──┐
│A │Q │Z │* │N │I │W │◄│ │MakeMove/Reset
├──┴──┴──┴──┴──┴──┴──┤ └──┘
│ ┌───┐ ┌───┐ ┌───┐ │
│ Draw│ Skip│ Shfl│ │
│ └───┘ └───┘ └───┘ │
└────────────────────┘
```
### Stage 8 — UI: social, account & history surfaces
Scope: the UI surfaces deferred from Stage 7's playable slice, wiring the matching
backend/gateway operations as each screen needs them (the Stage 6 vertical-slice
pattern): friends (request/accept/decline/list), per-user blocks, friend-game
invitations (create 24 player, accept/decline, invitations list), profile **editing**
(`account.UpdateProfile` + the email confirm-code binding UI), the statistics screen,
and the history viewer with GCG export/download.
Open details: friends/invitations UX; stats presentation; history/GCG viewer + download
mechanics; any new validation the profile-editing forms need.
### Stage 9 — Telegram integration
Scope: bot side-service, deep-link invites, platform push (your-turn / nudge), Scope: bot side-service, deep-link invites, platform push (your-turn / nudge),
Mini App launch/auth; backend↔platform internal API. Mini App launch/auth; backend↔platform internal API.
Open details: bot framework/library; deep-link scheme; push message templates; Open details: bot framework/library; deep-link scheme; push message templates;
internal API contract; Mini App hosting/origin. internal API contract; Mini App hosting/origin.
### Stage 9 — Admin & dictionary ops ### Stage 10 — Admin & dictionary ops
Scope: admin endpoints (users, games, complaint review queue, dictionary Scope: admin endpoints (users, games, complaint review queue, dictionary
versions + reload), complaint→dictionary update pipeline. versions + reload), complaint→dictionary update pipeline.
Open details: whether a server-rendered console is wanted or JSON-only; the Open details: whether a server-rendered console is wanted or JSON-only; the
dictionary rebuild/deploy pipeline; complaint resolution workflow. dictionary rebuild/deploy pipeline; complaint resolution workflow.
### Stage 10 — Account linking & merge ### Stage 11 — Account linking & merge
Scope: link-via-confirm; merge-into-A (stats sum, transfer games/friends, Scope: link-via-confirm; merge-into-A (stats sum, transfer games/friends,
dedupe). High blast-radius — focused regression tests. dedupe). High blast-radius — focused regression tests.
Open details: conflict resolution (active games on both, duplicate friends, Open details: conflict resolution (active games on both, duplicate friends,
display-name collisions); irreversibility/audit; confirm-flow per platform. display-name collisions); irreversibility/audit; confirm-flow per platform.
### Stage 11 — Polish ### Stage 12 — Polish
Scope: observability dashboards, evidence-based performance work, prod Scope: observability dashboards, evidence-based performance work, prod
build/deploy. build/deploy.
Open details: deployment target/host; dashboards; load expectations. Open details: deployment target/host; dashboards; load expectations.
@@ -164,9 +230,9 @@ Open details: deployment target/host; dashboards; load expectations.
- HTTP surface: **service/store/cache layer only**. `/api/v1/{public,user, - HTTP surface: **service/store/cache layer only**. `/api/v1/{public,user,
internal,admin}` groups + `X-User-ID` middleware are scaffolding (exposed via internal,admin}` groups + `X-User-ID` middleware are scaffolding (exposed via
`Server` group accessors); the session/account REST handlers land with the `Server` group accessors); the session/account REST handlers land with the
gateway in **Stage 6**. Admin bootstrap deferred to **Stage 9**. gateway in **Stage 6**. Admin bootstrap deferred to **Stage 10**.
- Telemetry: providers + request-timing middleware + otelsql; exporters - Telemetry: providers + request-timing middleware + otelsql; exporters
`none` (default) / `stdout`; OTLP + dashboards deferred to **Stage 11**. `none` (default) / `stdout`; OTLP + dashboards deferred to **Stage 12**.
- Tests/CI: integration tests behind the `integration` build tag in - Tests/CI: integration tests behind the `integration` build tag in
`backend/internal/inttest` + new `integration.yaml` (testcontainers, Ryuk `backend/internal/inttest` + new `integration.yaml` (testcontainers, Ryuk
off, serial), firing on push and PR. Backend now **hard-depends on Postgres off, serial), firing on push and PR. Backend now **hard-depends on Postgres
@@ -247,7 +313,7 @@ Open details: deployment target/host; dashboards; load expectations.
wins/losses; `max_word_points` = best single **move** score; ties draw, wins/losses; `max_word_points` = best single **move** score; ties draw,
resign/timeout is a loss, guests get no stats. resign/timeout is a loss, guests get no stats.
- **Complaint** (interview): full payload with `game_id`; word-check is scoped - **Complaint** (interview): full payload with `game_id`; word-check is scoped
to the game's pinned `(variant, dict_version)`. Stage 9 owns the resolution to the game's pinned `(variant, dict_version)`. Stage 10 owns the resolution
lifecycle, so the `status` column carries no value CHECK yet. lifecycle, so the `status` column carries no value CHECK yet.
- **GCG** (interview): standard Poslfit dialect (UTF-8, `#player`/`#lexicon` - **GCG** (interview): standard Poslfit dialect (UTF-8, `#player`/`#lexicon`
pragmas, `8G`/`H8` coordinates, lower-case blanks, `.` pass-throughs, `-TILES` pragmas, `8G`/`H8` coordinates, lower-case blanks, `.` pass-throughs, `-TILES`
@@ -295,7 +361,7 @@ Open details: deployment target/host; dashboards; load expectations.
stored as a **SHA-256 hash**; a `Mailer` seam with an SMTP relay stored as a **SHA-256 hash**; a `Mailer` seam with an SMTP relay
(`BACKEND_SMTP_*`) and a default **log mailer**. It binds an email to the (`BACKEND_SMTP_*`) and a default **log mailer**. It binds an email to the
current account; an email already confirmed by another account → `ErrEmailTaken` current account; an email already confirmed by another account → `ErrEmailTaken`
(**merge is Stage 10**); email-as-login is Stage 6 and reuses this mechanism. (**merge is Stage 11**); email-as-login is Stage 6 and reuses this mechanism.
- **Multi-player drop-out** (interview; discharges the Stage 3 deferral): the - **Multi-player drop-out** (interview; discharges the Stage 3 deferral): the
engine's `Resign` now drops a seat and the rest **play on** while ≥ 2 are engine's `Resign` now drops a seat and the rest **play on** while ≥ 2 are
active, finishing (last-survivor wins) when one remains; `winner` excludes all active, finishing (last-survivor wins) when one remains; `winner` excludes all
@@ -362,9 +428,11 @@ Open details: deployment target/host; dashboards; load expectations.
end-to-end — auth (`auth.telegram`/`auth.guest`/`auth.email.request`/ end-to-end — auth (`auth.telegram`/`auth.guest`/`auth.email.request`/
`auth.email.login`), `profile.get`, `game.submit_play`/`game.state`, `auth.email.login`), `profile.get`, `game.submit_play`/`game.state`,
`lobby.enqueue`/`lobby.poll`, `chat.post`, all five push events, and the admin `lobby.enqueue`/`lobby.poll`, `chat.post`, all five push events, and the admin
passthrough. The remaining domain operations (friends, blocks, invitations, passthrough. The remaining domain operations reuse the identical transcode
hint, word-check, pass/exchange/resign, history/GCG, profile editing) reuse the pattern and are wired as the UI needs them: the play-loop ops (pass/exchange/
identical transcode pattern and are wired in **Stage 7** as the UI needs them. resign, hint, evaluate, word-check/complaint, history, my-games list, chat
list/nudge) in **Stage 7**; the social/account ops (friends, blocks,
invitations, profile editing, stats, GCG export) in **Stage 8**.
- **Wire contracts in a new shared `scrabble/pkg` module** (interview): the - **Wire contracts in a new shared `scrabble/pkg` module** (interview): the
backend push proto (`pkg/proto/push/v1`) and the FlatBuffers edge payloads backend push proto (`pkg/proto/push/v1`) and the FlatBuffers edge payloads
(`pkg/fbs`, one `scrabblefb` namespace) live here with **committed** generated (`pkg/fbs`, one `scrabblefb` namespace) live here with **committed** generated
@@ -402,7 +470,7 @@ Open details: deployment target/host; dashboards; load expectations.
(the galaxy donor's crypto stack was dropped, per §3). (the galaxy donor's crypto stack was dropped, per §3).
- **Admin = gateway validates Basic-Auth** (interview): the gateway checks - **Admin = gateway validates Basic-Auth** (interview): the gateway checks
`GATEWAY_ADMIN_USER`/`_PASSWORD` and reverse-proxies to backend `GATEWAY_ADMIN_USER`/`_PASSWORD` and reverse-proxies to backend
`/api/v1/admin/*`; the backend admin surface is a single `ping` until Stage 9. `/api/v1/admin/*`; the backend admin surface is a single `ping` until Stage 10.
- **Rate-limit = 2 dimensions, 3 classes** (interview): public per-IP (30/min, - **Rate-limit = 2 dimensions, 3 classes** (interview): public per-IP (30/min,
burst 10), authenticated per-user (120/min, burst 40), admin per-IP (60/min, burst 10), authenticated per-user (120/min, burst 40), admin per-IP (60/min,
burst 20), plus an email-code per-IP sub-limit (5/10 min); token bucket burst 20), plus an email-code per-IP sub-limit (5/10 min); token bucket
@@ -416,6 +484,48 @@ Open details: deployment target/host; dashboards; load expectations.
(unit) — integration stays `./backend/...` (the only module with tagged tests). (unit) — integration stays `./backend/...` (the only module with tagged tests).
The solver clone + `BACKEND_DICT_DIR` steps are unchanged. The solver clone + `BACKEND_DICT_DIR` steps are unchanged.
- **Stage 7** (interview + implementation):
- **Scope = playable slice** (interview): the *whole* UI shell plus the core play
loop end-to-end; the social/account/history surfaces were split out into a new
**Stage 8** and the later stages shifted +1 (Telegram→9, Admin→10, Linking→11,
Polish→12). Stage 7 wires only the operations the slice needs (the Stage 6
"as the UI needs them" pattern): the new gateway/transcode + backend-REST ops
`games.list`, `game.{pass,exchange,resign,hint,evaluate,check_word,complaint,
history}`, `chat.{list,nudge}`. The only new domain code is `game.ListForAccount`
(the "my games" query) and seat **`display_name`** resolution (server DTO layer);
`SeatView` gained a trailing `display_name`. Friends/blocks/invitations,
profile-editing, stats and the history/GCG viewer are Stage 8.
- **Stack** (interview): plain **Svelte 5 (runes) + TypeScript + Vite**, no
SvelteKit; `@connectrpc/connect-web` + the `flatbuffers` runtime, with the edge
TS bindings generated from the **same** `edge.proto` (`protoc-gen-es`) and
`scrabble.fbs` (`flatc --ts`) and **committed** under `ui/src/gen/` (dev-time
codegen, like `cmd/jetgen` / `pkg/Makefile`; CI builds the committed output).
- **No board on the wire** (discovered): `StateView` carries no grid, so the client
**replays the decoded move journal** (`game.history`, newly wired) onto an empty
board; premium squares + tile values are a client-side map **ported from
`scrabble-solver/rules/rules.go`** with a Vitest parity test.
- **Board UX** (interview): full-width, borderless; tiles placed by **Pointer-Events
drag or tap** (no HTML5 DnD — it has no touch support); a contextual **MakeMove**
control (short tap → make/reset popup, ~1 s press-and-hold → commit); per-tile
recall by tapping a pending tile; a **two-state zoom** (15↔9 cells) on touch only
(auto-zoom-in on placement, double-tap / pinch manual); a blank-letter chooser.
All board/tiles/effects are **pure HTML5/CSS + Unicode** — no image/font/SVG asset.
- **Theming** (interview): own **CSS custom-property tokens**, light/dark via
`prefers-color-scheme`, **Telegram-themeParams-ready** (a runtime hook can override
the tokens; the SDK is wired in the Telegram stage). **Navigation** (interview):
dependency-free **hash router**; session token in memory + **IndexedDB**, re-resolved
on reload (reopen Subscribe, refetch the open game); stream reconnect on focus.
**i18n** en/ru is a hand-rolled typed catalog (compile-time key parity + a test).
- **Mock transport** (owner request): a build-flagged in-memory fake (`VITE_MOCK`,
`pnpm start`) drives lobby → active game → board with no backend, tree-shaken out
of production; it is the same fixture the Playwright smoke uses.
- **Tests/CI** (interview): **Vitest** units (board replay, placement machine,
premium parity, i18n parity, FlatBuffers codec) + a **Playwright** smoke against
the mock; a new **`ui-test.yaml`** workflow (type-check, unit, build with a
**bundle-size budget** — prod is ~67 KB gzip JS — and a chromium e2e). The Go
workflows already cover the new backend/gateway/pkg code; a `game.ListForAccount`
integration test and gateway transcode tests for the new ops were added.
## Deferred TODOs (cross-stage) ## Deferred TODOs (cross-stage)
- **TODO-1 — publish & version the solver.** Once `scrabble-solver` is stable, - **TODO-1 — publish & version the solver.** Once `scrabble-solver` is stable,
@@ -433,7 +543,7 @@ Open details: deployment target/host; dashboards; load expectations.
git submodule (the ~0.50.7 MB DAWGs are regenerated wholesale and bloat git git submodule (the ~0.50.7 MB DAWGs are regenerated wholesale and bloat git
history); pin by tag/hash for a reproducible startup set. A submodule/LFS pull history); pin by tag/hash for a reproducible startup set. A submodule/LFS pull
is a **deploy-time** way to populate the directory, **not** the runtime is a **deploy-time** way to populate the directory, **not** the runtime
dynamic-reload mechanism (Stage 9) — keep the `BACKEND_DICT_DIR` directory as dynamic-reload mechanism (Stage 10) — keep the `BACKEND_DICT_DIR` directory as
the runtime contract: a new `.dawg` appears in it and is loaded with the runtime contract: a new `.dawg` appears in it and is loaded with
`dawg.Load`. `dawg.Load`.
- **TODO-3 — garbage-collect abandoned guest accounts.** Stage 6 makes a guest a - **TODO-3 — garbage-collect abandoned guest accounts.** Stage 6 makes a guest a
+15 -2
View File
@@ -11,8 +11,9 @@ supports English Scrabble, Russian Scrabble and Эрудит.
admin surface behind Basic Auth. *(added in a later stage)* admin surface behind Basic Auth. *(added in a later stage)*
- **`backend`** — internal-only service that owns every domain concern and - **`backend`** — internal-only service that owns every domain concern and
embeds the [`scrabble-solver`](../scrabble-solver) engine library in-process. embeds the [`scrabble-solver`](../scrabble-solver) engine library in-process.
- **`ui`** — pure-HTML5 client (plain Svelte + Vite), embeddable in platform - **`ui`** — pure-HTML5 client (plain Svelte 5 + TypeScript + Vite) over Connect-RPC
webviews and packageable to native via Capacitor. *(added in a later stage)* + FlatBuffers, embeddable in platform webviews and packageable to native via
Capacitor. See [`ui/README.md`](ui/README.md).
- **`platform/*`** — per-platform side-services (e.g. the Telegram bot). - **`platform/*`** — per-platform side-services (e.g. the Telegram bot).
*(added in a later stage)* *(added in a later stage)*
@@ -67,3 +68,15 @@ Key environment: `BACKEND_HTTP_ADDR` (default `:8080`), `BACKEND_LOG_LEVEL`
(`debug|info|warn|error`, default `info`), `BACKEND_POSTGRES_DSN` (**required**). (`debug|info|warn|error`, default `info`), `BACKEND_POSTGRES_DSN` (**required**).
The full configuration surface and the go-jet regeneration step live in The full configuration surface and the go-jet regeneration step live in
[`backend/README.md`](backend/README.md). [`backend/README.md`](backend/README.md).
## Run the UI locally
```sh
cd ui && pnpm install
pnpm start # mock mode: lobby -> game with no backend, on http://localhost:5173
pnpm dev # against a running gateway (Vite proxies the RPC path to :8081)
```
`pnpm check` (type-check), `pnpm test:unit` (Vitest), `pnpm test:e2e` (Playwright
smoke vs the mock), `pnpm build` (static bundle). Details — including the committed
edge codegen (`pnpm codegen`) — are in [`ui/README.md`](ui/README.md).
+2 -2
View File
@@ -33,7 +33,7 @@ var (
// ErrInvalidEmail is returned for an unparseable email address. // ErrInvalidEmail is returned for an unparseable email address.
ErrInvalidEmail = errors.New("account: invalid email address") ErrInvalidEmail = errors.New("account: invalid email address")
// ErrEmailTaken is returned when the email is already confirmed by another // ErrEmailTaken is returned when the email is already confirmed by another
// account; binding it would be a merge, which Stage 10 owns. // account; binding it would be a merge, which Stage 11 owns.
ErrEmailTaken = errors.New("account: email already confirmed by another account") ErrEmailTaken = errors.New("account: email already confirmed by another account")
// ErrAlreadyConfirmed is returned when the email is already confirmed by the // ErrAlreadyConfirmed is returned when the email is already confirmed by the
// requesting account. // requesting account.
@@ -52,7 +52,7 @@ var (
// Mailer and verifies it, binding a confirmed email identity to the requesting // Mailer and verifies it, binding a confirmed email identity to the requesting
// account. Only the SHA-256 hash of a code is stored (never the plaintext), // account. Only the SHA-256 hash of a code is stored (never the plaintext),
// matching the session model. Binding an email already confirmed by a different // matching the session model. Binding an email already confirmed by a different
// account is refused (ErrEmailTaken) — merging two accounts is Stage 10 — and // account is refused (ErrEmailTaken) — merging two accounts is Stage 11 — and
// using an email as a login is Stage 6, which reuses this mechanism. // using an email as a login is Stage 6, which reuses this mechanism.
type EmailService struct { type EmailService struct {
store *Store store *Store
+2 -2
View File
@@ -15,7 +15,7 @@ const (
StatusFinished = "finished" StatusFinished = "finished"
) )
// ComplaintStatus values; Stage 9 owns the resolution lifecycle, Stage 3 only // ComplaintStatus values; Stage 10 owns the resolution lifecycle, Stage 3 only
// ever writes StatusComplaintOpen. // ever writes StatusComplaintOpen.
const StatusComplaintOpen = "open" const StatusComplaintOpen = "open"
@@ -176,7 +176,7 @@ type RobotTurn struct {
Seed int64 Seed int64
} }
// Complaint is a word-check complaint awaiting admin review (Stage 9). // Complaint is a word-check complaint awaiting admin review (Stage 10).
type Complaint struct { type Complaint struct {
ID uuid.UUID ID uuid.UUID
ComplainantID uuid.UUID ComplainantID uuid.UUID
+1 -1
View File
@@ -9,7 +9,7 @@ import (
// The /api/v1/admin/* endpoints are reached through the gateway's Basic-Auth // The /api/v1/admin/* endpoints are reached through the gateway's Basic-Auth
// reverse proxy (docs/ARCHITECTURE.md §12). The backend trusts the gateway to // reverse proxy (docs/ARCHITECTURE.md §12). The backend trusts the gateway to
// have authenticated the operator; the admin surface itself (complaint review, // have authenticated the operator; the admin surface itself (complaint review,
// dictionary versions) lands in Stage 9. handleAdminPing is the proxy target that // dictionary versions) lands in Stage 10. handleAdminPing is the proxy target that
// proves the path end to end until then. // proves the path end to end until then.
func (s *Server) handleAdminPing(c *gin.Context) { func (s *Server) handleAdminPing(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"}) c.JSON(http.StatusOK, gin.H{"status": "ok"})
+21 -10
View File
@@ -24,9 +24,20 @@ Three executables plus per-platform side-services:
administration. Embeds the **`scrabble-solver`** engine **as a library, administration. Embeds the **`scrabble-solver`** engine **as a library,
in-process** — there is no per-game container. The only network consumer of in-process** — there is no per-game container. The only network consumer of
`backend` is `gateway` (plus platform side-services over an internal API). `backend` is `gateway` (plus platform side-services over an internal API).
- **`ui`** *(planned)* — pure-HTML5 client (plain Svelte + Vite, static build). - **`ui`** — pure-HTML5 client (plain Svelte 5 + TypeScript + Vite, static build;
Talks to `backend` only through `gateway`. Embeddable in platform webviews; no SvelteKit). Talks to `backend` only through `gateway` over Connect-RPC +
packageable to native (iOS/Android) via Capacitor. FlatBuffers, with the edge TS bindings generated from the **same** `edge.proto`
and `scrabble.fbs` and committed under `ui/src/gen/`. The **playable slice**
(Stage 7) covers auth, "my games", auto-match, the board (play/pass/exchange/
resign), hint, word-check, chat/nudge, the live stream, i18n (en/ru) and a profile
view; the social/account/history surfaces follow in Stage 8. There is no board on
the wire — the client **reconstructs the 15×15 board by replaying the move
journal** (§9.1) and renders board, tiles, premium squares and effects as pure
CSS + Unicode (no image/font/SVG assets). Tiles are placed by Pointer-Events drag
or tap; a CSS-token theme is light/dark and Telegram-themeParams-ready; navigation
is a hash router and the session token is held in memory + IndexedDB. A build-flagged
in-memory mock transport (`pnpm start`) runs the whole slice with no backend.
Embeddable in platform webviews; packageable to native (iOS/Android) via Capacitor.
- **`platform/<name>`** *(planned)* — per-platform side-services (Telegram bot - **`platform/<name>`** *(planned)* — per-platform side-services (Telegram bot
first): deep-link invites and platform-native push notifications. They talk first): deep-link invites and platform-native push notifications. They talk
to `backend` over an internal API. to `backend` over an internal API.
@@ -108,7 +119,7 @@ arrive from a platform rather than completing a mandatory registration).
TTL, ≤ 5 attempts) is sent through a `Mailer` seam (an SMTP relay, or a TTL, ≤ 5 attempts) is sent through a `Mailer` seam (an SMTP relay, or a
development log mailer when none is configured) and, once verified, attaches a development log mailer when none is configured) and, once verified, attaches a
confirmed email identity. An email already confirmed by **another** account is confirmed email identity. An email already confirmed by **another** account is
refused — adopting it would be a merge, which Stage 10 owns. Accounts and refused — adopting it would be a merge, which Stage 11 owns. Accounts and
identities use application-generated **UUIDv7** primary keys. identities use application-generated **UUIDv7** primary keys.
- **Linking** is initiated from an authenticated profile: choose a platform → - **Linking** is initiated from an authenticated profile: choose a platform →
complete that platform's web-auth confirm → attach the identity to the complete that platform's web-auth confirm → attach the identity to the
@@ -140,7 +151,7 @@ Key points:
word-check tool through `Registry.Lookup`. word-check tool through `Registry.Lookup`.
- **Dictionary versioning — pin per game.** A game records the `dict_version` it - **Dictionary versioning — pin per game.** A game records the `dict_version` it
started on and finishes on that version; new games use the latest. Multiple started on and finishes on that version; new games use the latest. Multiple
versions may be resident at once. An admin reload *(planned, Stage 9)* versions may be resident at once. An admin reload *(planned, Stage 10)*
registers a new version through `Registry.Load`; delivery is the DAWG file in registers a new version through `Registry.Load`; delivery is the DAWG file in
the image / a volume mounted at the dictionary directory. (A future split of the image / a volume mounted at the dictionary directory. (A future split of
the solver into engine + dictionary generator with versioned artifacts is the solver into engine + dictionary generator with versioned artifacts is
@@ -202,7 +213,7 @@ Key points:
- **Word-check tool**: unlimited dictionary lookups against the game's pinned - **Word-check tool**: unlimited dictionary lookups against the game's pinned
dictionary; each result offers a **complaint** (complainant, game, variant, dictionary; each result offers a **complaint** (complainant, game, variant,
dict_version, word, the disputed result, an optional note) that lands in an dict_version, word, the disputed result, an optional note) that lands in an
admin review queue *(admin side planned, Stage 9)*. admin review queue *(admin side planned, Stage 10)*.
## 7. Robot opponent ## 7. Robot opponent
@@ -250,7 +261,7 @@ requires (there is no DM surface; chat is per-game).
emits a **match-found** notification (§10), delivered over the live stream; emits a **match-found** notification (§10), delivered over the live stream;
`Poll` remains as a fallback for a client that is not currently streaming. `Poll` remains as a fallback for a client that is not currently streaming.
- **Friends**: a **request → accept** graph (one `friendships` table) — add by - **Friends**: a **request → accept** graph (one `friendships` table) — add by
friend list or internal ID now, by platform deep-link with Stage 8. Declining or friend list or internal ID now, by platform deep-link with Stage 9. Declining or
cancelling removes the pending request; blocking someone severs an existing cancelling removes the pending request; blocking someone severs an existing
friendship. friendship.
- **Block**: two independent **global** account toggles (`block_chat`, - **Block**: two independent **global** account toggles (`block_chat`,
@@ -275,7 +286,7 @@ requires (there is no DM surface; chat is per-game).
(confirm-code binding, see §4), **timezone** (drives the away window and the (confirm-code binding, see §4), **timezone** (drives the away window and the
robot's sleep; user-editable), the daily **away window** and the block toggles — robot's sleep; user-editable), the daily **away window** and the block toggles —
all editable through `account.UpdateProfile`. Linked platform accounts and merge all editable through `account.UpdateProfile`. Linked platform accounts and merge
are Stage 10. are Stage 11.
## 9. Persistence ## 9. Persistence
@@ -337,7 +348,7 @@ does not cover.
## 10. Notifications ## 10. Notifications
Two channels: the **in-app live stream** (delivered from Stage 6) and Two channels: the **in-app live stream** (delivered from Stage 6) and
**platform-native push** (out-of-app, via the platform side-service — Stage 8). **platform-native push** (out-of-app, via the platform side-service — Stage 9).
The backend emits notification intents through an in-process hub The backend emits notification intents through an in-process hub
(`internal/notify`, a `Publisher` seam installed on the game, social and lobby (`internal/notify`, a `Publisher` seam installed on the game, social and lobby
services); a single backend→gateway **gRPC server-stream** (`Push.Subscribe`, services); a single backend→gateway **gRPC server-stream** (`Push.Subscribe`,
@@ -348,7 +359,7 @@ robot-driver and timeout-sweeper moves emit too), **chat-message** and **nudge**
(from the social service), and **match-found** (from the matchmaker, §8). Event (from the social service), and **match-found** (from the matchmaker, §8). Event
payloads are FlatBuffers-encoded by the backend and forwarded verbatim. A client payloads are FlatBuffers-encoded by the backend and forwarded verbatim. A client
that is not currently streaming falls back to the matchmaker's `Poll` for that is not currently streaming falls back to the matchmaker's `Poll` for
match-found. Out-of-app platform push (your-turn, nudge) is wired in Stage 8; match-found. Out-of-app platform push (your-turn, nudge) is wired in Stage 9;
session-revocation events and cursor-based stream resume are deferred session-revocation events and cursor-based stream resume are deferred
(single-instance MVP). (single-instance MVP).
+13 -3
View File
@@ -9,6 +9,16 @@ the detail is authored.
## Domains ## Domains
### Client app *(Stage 7 / 8)*
The web/app client (Svelte + Vite) realizes these stories. The **playable slice**
(Stage 7) covers signing in (guest or email), the "my games" lobby, starting an
auto-match, playing the board (place tiles by drag or tap, pass, exchange, resign),
the top-1 hint, the unlimited word-check with complaint, per-game chat and nudge,
real-time in-app updates, switching interface language (en/ru) and theme, and a
read-only profile. Managing friends and blocks, creating friend games (invitations),
editing the profile, the statistics screen and the history/GCG viewer arrive in
Stage 8.
### Identity & sessions *(Stage 1 / 6)* ### Identity & sessions *(Stage 1 / 6)*
A player arrives from a platform (Telegram first), via email login, or as an A player arrives from a platform (Telegram first), via email login, or as an
ephemeral guest. The gateway validates the credential once and mints a thin ephemeral guest. The gateway validates the credential once and mints a thin
@@ -17,7 +27,7 @@ session-only with restricted features (auto-match only; no friends, stats or
history). While the app is open the client keeps a live stream and receives history). While the app is open the client keeps a live stream and receives
in-app updates in real time — the opponent's move, your turn, chat, nudges and a in-app updates in real time — the opponent's move, your turn, chat, nudges and a
found match; out-of-app push (your turn, nudge) is delivered by the platform found match; out-of-app push (your turn, nudge) is delivered by the platform
later (Stage 8). later (Stage 9).
### Accounts, linking & merge *(Stage 1 / 10)* ### Accounts, linking & merge *(Stage 1 / 10)*
First platform contact auto-provisions a durable account. From the profile a First platform contact auto-provisions a durable account. From the profile a
@@ -76,7 +86,7 @@ Edit language (en/ru), display name, timezone, the daily away window and the blo
toggles, and bind an email by confirm-code: the backend emails a short code that, toggles, and bind an email by confirm-code: the backend emails a short code that,
once entered, attaches the email to the account (an email already confirmed by once entered, attaches the email to the account (an email already confirmed by
another account cannot be taken — that is a merge, a later stage). Linked platform another account cannot be taken — that is a merge, a later stage). Linked platform
accounts and merge arrive in Stage 10. accounts and merge arrive in Stage 11.
### History & statistics *(Stage 3)* ### History & statistics *(Stage 3)*
Finished games are archived in a dictionary-independent form and exportable to Finished games are archived in a dictionary-independent form and exportable to
@@ -84,6 +94,6 @@ GCG. Statistics (durable accounts only): wins, losses, draws, max points in a
game, and max points for a single move (the best play, which already includes game, and max points for a single move (the best play, which already includes
every word it formed plus the all-tiles bonus). every word it formed plus the all-tiles bonus).
### Administration *(Stage 9)* ### Administration *(Stage 10)*
Admin (Basic Auth at the gateway) reviews word complaints, manages dictionary Admin (Basic Auth at the gateway) reviews word complaints, manages dictionary
versions, and inspects users/games. versions, and inspects users/games.
+13 -3
View File
@@ -8,6 +8,16 @@
## Домены ## Домены
### Клиентское приложение *(Stage 7 / 8)*
Веб/приложение-клиент (Svelte + Vite) воплощает эти истории. **Играбельный срез**
(Stage 7) покрывает вход (гость или email), лобби «мои игры», старт авто-подбора,
игру на доске (постановка фишек перетаскиванием или тапом, пас, обмен, сдача),
top-1 подсказку, безлимитную проверку слова с жалобой, чат и nudge в партии,
обновления в реальном времени, переключение языка интерфейса (en/ru) и темы и
профиль только для чтения. Управление друзьями и блоками, создание дружеских игр
(приглашения), редактирование профиля, экран статистики и просмотр истории/GCG
появятся в Stage 8.
### Личность и сессии *(Stage 1 / 6)* ### Личность и сессии *(Stage 1 / 6)*
Игрок приходит с платформы (сначала Telegram), через email-вход или как Игрок приходит с платформы (сначала Telegram), через email-вход или как
эфемерный гость. Gateway один раз валидирует доступ и выдаёт тонкий эфемерный гость. Gateway один раз валидирует доступ и выдаёт тонкий
@@ -16,7 +26,7 @@ session-токен; backend сопоставляет его с внутренн
статистики и истории). Пока приложение открыто, клиент держит живой стрим и статистики и истории). Пока приложение открыто, клиент держит живой стрим и
получает обновления в реальном времени — ход соперника, ваш ход, чат, nudge и получает обновления в реальном времени — ход соперника, ваш ход, чат, nudge и
найденный матч; внеприложенческий push (ваш ход, nudge) платформа доставит найденный матч; внеприложенческий push (ваш ход, nudge) платформа доставит
позже (Stage 8). позже (Stage 9).
### Аккаунты, привязка и слияние *(Stage 1 / 10)* ### Аккаунты, привязка и слияние *(Stage 1 / 10)*
Первый контакт с платформы заводит постоянный аккаунт. Из профиля игрок Первый контакт с платформы заводит постоянный аккаунт. Из профиля игрок
@@ -76,7 +86,7 @@ push доставляется через платформу.
confirm-коду: backend шлёт на почту короткий код, и после ввода email confirm-коду: backend шлёт на почту короткий код, и после ввода email
привязывается к аккаунту (email, уже подтверждённый другим аккаунтом, занять привязывается к аккаунту (email, уже подтверждённый другим аккаунтом, занять
нельзя — это слияние, отдельный этап). Привязанные платформенные аккаунты и нельзя — это слияние, отдельный этап). Привязанные платформенные аккаунты и
слияние появятся в Stage 10. слияние появятся в Stage 11.
### История и статистика *(Stage 3)* ### История и статистика *(Stage 3)*
Завершённые партии архивируются в независимом от словаря виде и экспортируются Завершённые партии архивируются в независимом от словаря виде и экспортируются
@@ -84,6 +94,6 @@ confirm-коду: backend шлёт на почту короткий код, и
макс. очков за партию и макс. очков за один ход (лучший ход, уже включающий все макс. очков за партию и макс. очков за один ход (лучший ход, уже включающий все
образованные им слова и бонус за все фишки). образованные им слова и бонус за все фишки).
### Администрирование *(Stage 9)* ### Администрирование *(Stage 10)*
Админ (Basic Auth на gateway) разбирает жалобы на слова, управляет версиями Админ (Basic Auth на gateway) разбирает жалобы на слова, управляет версиями
словаря, смотрит пользователей/игры. словаря, смотрит пользователей/игры.
+1 -1
View File
@@ -2,7 +2,7 @@
// reverse proxy to the backend admin API (docs/ARCHITECTURE.md §12). The gateway // reverse proxy to the backend admin API (docs/ARCHITECTURE.md §12). The gateway
// validates the operator credential and forwards authenticated requests to // validates the operator credential and forwards authenticated requests to
// backend /api/v1/admin/*; the backend trusts the gateway on this segment. The // backend /api/v1/admin/*; the backend trusts the gateway on this segment. The
// admin API itself is filled in Stage 9. // admin API itself is filled in Stage 10.
package admin package admin
import ( import (
+69
View File
@@ -0,0 +1,69 @@
# scrabble-ui
Pure-HTML5 game client — **plain Svelte 5 (runes) + TypeScript + Vite**, no
SvelteKit. Talks to the `gateway` over **Connect-RPC + FlatBuffers**; embeddable in
platform webviews and packageable to native via Capacitor.
Stage 7 ships the **playable slice**: sign in (guest / email), the "my games" lobby,
auto-match, the board (place tiles by drag or tap, pass, exchange, resign), hint,
word-check + complaint, per-game chat and nudge, the live in-app stream, i18n (en/ru),
theme, and a read-only profile. Friends/blocks, friend-game invitations, profile
editing, the stats screen and the history/GCG viewer are Stage 8.
## Scripts
```sh
pnpm install
pnpm start # mock mode (VITE_MOCK): lobby -> game with no backend, :5173
pnpm dev # against a running gateway (Vite proxies /scrabble.edge.v1.Gateway -> :8081)
pnpm check # svelte-check / tsc
pnpm test:unit # Vitest (pure logic + FlatBuffers codec)
pnpm test:e2e # Playwright smoke against the mock
pnpm build # static bundle into dist/ (prod ~67 KB gzip JS)
pnpm codegen # regenerate src/gen from edge.proto + scrabble.fbs (dev-time)
```
`GATEWAY_URL` overrides the dev proxy target; `VITE_GATEWAY_URL` sets the runtime
gateway origin for a packaged (non-proxied) build.
## How it talks to the gateway
A single Connect `Execute(message_type, payload)` carries every unary op; the request
and response bodies are **FlatBuffers** tables (`pkg/fbs/scrabble.fbs`) in `payload`.
The session token rides in `Authorization: Bearer`; a domain failure comes back in
`result_code`. `Subscribe` is the live event stream. `lib/transport.ts` is the real
client; `lib/mock/` is an in-memory fake selected by `MODE === 'mock'` (and tree-shaken
out of production). Both speak the plain `lib/model.ts` types via `lib/codec.ts`.
**No board on the wire:** `StateView` is a summary + rack only, so the client
reconstructs the 15×15 board by replaying the decoded move journal (`game.history`).
Premium squares and tile values (`lib/premiums.ts`) are a client-side map **ported from
`scrabble-solver/rules/rules.go`** (pinned by a Vitest parity test). Board, tiles and
effects are pure CSS + Unicode — no image/font/SVG assets.
## Codegen
`src/gen/` is **committed**; CI builds it, it is not regenerated there (the same model
as the Go committed jet/fbs output). `pnpm codegen` runs `flatc --ts` on
`../pkg/fbs/scrabble.fbs` and `buf generate` (`protoc-gen-es`) on the edge proto. Needs
`flatc` 23.5.26 and `buf` on PATH.
## Theming
Design tokens are CSS custom properties (`src/app.css`); light/dark follows
`prefers-color-scheme` or an explicit choice in Settings. The token system is
**Telegram-themeParams-ready** (`lib/theme.ts`) — a Mini App can override the tokens at
runtime; the Telegram SDK itself is wired in the Telegram stage.
## Layout
```
src/
lib/ model, client facade, transport (+ mock), codec, board replay,
placement state machine, premiums, i18n, theme, session, router, app store
components/ Header, Modal, Toast
screens/ Login, Lobby, NewGame, Profile, Settings, About
game/ Game, Board, Rack, Controls, MakeMove, Chat
gen/ committed edge codegen (FlatBuffers + Connect)
e2e/ Playwright smoke (mock)
```