diff --git a/CLAUDE.md b/CLAUDE.md index 89f2068..9cad34b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -121,4 +121,12 @@ go vet ./backend/... gofmt -l . # must print nothing go test -count=1 ./backend/... 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`). diff --git a/PLAN.md b/PLAN.md index ec1d610..dcd43a5 100644 --- a/PLAN.md +++ b/PLAN.md @@ -40,11 +40,12 @@ independent (see ARCHITECTURE §9.1). | 4 | Lobby & social (matchmaking, friends, block, chat, profile, nudge) | **done** | | 5 | Robot opponent | **done** | | 6 | Gateway edge (Connect/FB, platform auth, sessions, push bridge, admin) | **done** | -| 7 | UI (plain Svelte + Vite, board, lobby, chat, i18n) | todo | -| 8 | Telegram integration (bot side-service, deep-link, push) | todo | -| 9 | Admin & dictionary ops (complaint review, version reload) | todo | -| 10 | Account linking & merge | todo | -| 11 | Polish (observability, perf with evidence, deploy) | todo | +| 7 | UI — playable slice (Svelte+Vite, board, lobby, chat, hint/word-check, i18n) | todo | +| 8 | UI — social/account/history (friends, blocks, invitations, profile edit, stats, history/GCG) | todo | +| 9 | Telegram integration (bot side-service, deep-link, push) | todo | +| 10 | Admin & dictionary ops (complaint review, version reload) | 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 adds the modules it needs. @@ -70,7 +71,7 @@ platform identities. Open details: Postgres version + DSN/`search_path` convention; jet vs sqlc/sqlx (default jet); migration naming; exact session-token shape (opaque 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 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); 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 2–4 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), Mini App launch/auth; backend↔platform internal API. Open details: bot framework/library; deep-link scheme; push message templates; 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 versions + reload), complaint→dictionary update pipeline. Open details: whether a server-rendered console is wanted or JSON-only; the 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, dedupe). High blast-radius — focused regression tests. Open details: conflict resolution (active games on both, duplicate friends, display-name collisions); irreversibility/audit; confirm-flow per platform. -### Stage 11 — Polish +### Stage 12 — Polish Scope: observability dashboards, evidence-based performance work, prod build/deploy. 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, internal,admin}` groups + `X-User-ID` middleware are scaffolding (exposed via `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 - `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 `backend/internal/inttest` + new `integration.yaml` (testcontainers, Ryuk 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, resign/timeout is a loss, guests get no stats. - **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. - **GCG** (interview): standard Poslfit dialect (UTF-8, `#player`/`#lexicon` 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 (`BACKEND_SMTP_*`) and a default **log mailer**. It binds an email to the 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 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 @@ -362,9 +428,11 @@ Open details: deployment target/host; dashboards; load expectations. end-to-end — auth (`auth.telegram`/`auth.guest`/`auth.email.request`/ `auth.email.login`), `profile.get`, `game.submit_play`/`game.state`, `lobby.enqueue`/`lobby.poll`, `chat.post`, all five push events, and the admin - passthrough. The remaining domain operations (friends, blocks, invitations, - hint, word-check, pass/exchange/resign, history/GCG, profile editing) reuse the - identical transcode pattern and are wired in **Stage 7** as the UI needs them. + passthrough. The remaining domain operations reuse the identical transcode + pattern and are wired as the UI needs them: the play-loop ops (pass/exchange/ + 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 backend push proto (`pkg/proto/push/v1`) and the FlatBuffers edge payloads (`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). - **Admin = gateway validates Basic-Auth** (interview): the gateway checks `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, 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 @@ -416,6 +484,48 @@ Open details: deployment target/host; dashboards; load expectations. (unit) — integration stays `./backend/...` (the only module with tagged tests). 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) - **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.5–0.7 MB DAWGs are regenerated wholesale and bloat git 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 - 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 `dawg.Load`. - **TODO-3 — garbage-collect abandoned guest accounts.** Stage 6 makes a guest a diff --git a/README.md b/README.md index c8dc1ca..12f6ac4 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,9 @@ supports English Scrabble, Russian Scrabble and Эрудит. admin surface behind Basic Auth. *(added in a later stage)* - **`backend`** — internal-only service that owns every domain concern and embeds the [`scrabble-solver`](../scrabble-solver) engine library in-process. -- **`ui`** — pure-HTML5 client (plain Svelte + Vite), embeddable in platform - webviews and packageable to native via Capacitor. *(added in a later stage)* +- **`ui`** — pure-HTML5 client (plain Svelte 5 + TypeScript + Vite) over Connect-RPC + + 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). *(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**). The full configuration surface and the go-jet regeneration step live in [`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). diff --git a/backend/internal/account/email.go b/backend/internal/account/email.go index f113d2c..ebbb775 100644 --- a/backend/internal/account/email.go +++ b/backend/internal/account/email.go @@ -33,7 +33,7 @@ var ( // ErrInvalidEmail is returned for an unparseable email address. ErrInvalidEmail = errors.New("account: invalid email address") // 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") // ErrAlreadyConfirmed is returned when the email is already confirmed by the // requesting account. @@ -52,7 +52,7 @@ var ( // 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), // 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. type EmailService struct { store *Store diff --git a/backend/internal/game/types.go b/backend/internal/game/types.go index a466229..4642e1a 100644 --- a/backend/internal/game/types.go +++ b/backend/internal/game/types.go @@ -15,7 +15,7 @@ const ( 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. const StatusComplaintOpen = "open" @@ -176,7 +176,7 @@ type RobotTurn struct { 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 { ID uuid.UUID ComplainantID uuid.UUID diff --git a/backend/internal/server/handlers_admin.go b/backend/internal/server/handlers_admin.go index 7bf5b23..81b5adf 100644 --- a/backend/internal/server/handlers_admin.go +++ b/backend/internal/server/handlers_admin.go @@ -9,7 +9,7 @@ import ( // 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 // 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. func (s *Server) handleAdminPing(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7955482..81ae482 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -24,9 +24,20 @@ Three executables plus per-platform side-services: administration. Embeds the **`scrabble-solver`** engine **as a library, in-process** — there is no per-game container. The only network consumer of `backend` is `gateway` (plus platform side-services over an internal API). -- **`ui`** *(planned)* — pure-HTML5 client (plain Svelte + Vite, static build). - Talks to `backend` only through `gateway`. Embeddable in platform webviews; - packageable to native (iOS/Android) via Capacitor. +- **`ui`** — pure-HTML5 client (plain Svelte 5 + TypeScript + Vite, static build; + no SvelteKit). Talks to `backend` only through `gateway` over Connect-RPC + + 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/`** *(planned)* — per-platform side-services (Telegram bot first): deep-link invites and platform-native push notifications. They talk 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 development log mailer when none is configured) and, once verified, attaches a 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. - **Linking** is initiated from an authenticated profile: choose a platform → complete that platform's web-auth confirm → attach the identity to the @@ -140,7 +151,7 @@ Key points: word-check tool through `Registry.Lookup`. - **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 - 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 the image / a volume mounted at the dictionary directory. (A future split of 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 dictionary; each result offers a **complaint** (complainant, game, variant, 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 @@ -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; `Poll` remains as a fallback for a client that is not currently streaming. - **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 friendship. - **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 robot's sleep; user-editable), the daily **away window** and the block toggles — all editable through `account.UpdateProfile`. Linked platform accounts and merge - are Stage 10. + are Stage 11. ## 9. Persistence @@ -337,7 +348,7 @@ does not cover. ## 10. Notifications 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 (`internal/notify`, a `Publisher` seam installed on the game, social and lobby 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 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 -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 (single-instance MVP). diff --git a/docs/FUNCTIONAL.md b/docs/FUNCTIONAL.md index 0fc268f..261e473 100644 --- a/docs/FUNCTIONAL.md +++ b/docs/FUNCTIONAL.md @@ -9,6 +9,16 @@ the detail is authored. ## 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)* 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 @@ -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 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 -later (Stage 8). +later (Stage 9). ### Accounts, linking & merge *(Stage 1 / 10)* 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, 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 -accounts and merge arrive in Stage 10. +accounts and merge arrive in Stage 11. ### History & statistics *(Stage 3)* 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 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 versions, and inspects users/games. diff --git a/docs/FUNCTIONAL_ru.md b/docs/FUNCTIONAL_ru.md index b29ea58..ee1cd09 100644 --- a/docs/FUNCTIONAL_ru.md +++ b/docs/FUNCTIONAL_ru.md @@ -8,6 +8,16 @@ ## Домены +### Клиентское приложение *(Stage 7 / 8)* +Веб/приложение-клиент (Svelte + Vite) воплощает эти истории. **Играбельный срез** +(Stage 7) покрывает вход (гость или email), лобби «мои игры», старт авто-подбора, +игру на доске (постановка фишек перетаскиванием или тапом, пас, обмен, сдача), +top-1 подсказку, безлимитную проверку слова с жалобой, чат и nudge в партии, +обновления в реальном времени, переключение языка интерфейса (en/ru) и темы и +профиль только для чтения. Управление друзьями и блоками, создание дружеских игр +(приглашения), редактирование профиля, экран статистики и просмотр истории/GCG +появятся в Stage 8. + ### Личность и сессии *(Stage 1 / 6)* Игрок приходит с платформы (сначала Telegram), через email-вход или как эфемерный гость. Gateway один раз валидирует доступ и выдаёт тонкий @@ -16,7 +26,7 @@ session-токен; backend сопоставляет его с внутренн статистики и истории). Пока приложение открыто, клиент держит живой стрим и получает обновления в реальном времени — ход соперника, ваш ход, чат, nudge и найденный матч; внеприложенческий push (ваш ход, nudge) платформа доставит -позже (Stage 8). +позже (Stage 9). ### Аккаунты, привязка и слияние *(Stage 1 / 10)* Первый контакт с платформы заводит постоянный аккаунт. Из профиля игрок @@ -76,7 +86,7 @@ push доставляется через платформу. confirm-коду: backend шлёт на почту короткий код, и после ввода email привязывается к аккаунту (email, уже подтверждённый другим аккаунтом, занять нельзя — это слияние, отдельный этап). Привязанные платформенные аккаунты и -слияние появятся в Stage 10. +слияние появятся в Stage 11. ### История и статистика *(Stage 3)* Завершённые партии архивируются в независимом от словаря виде и экспортируются @@ -84,6 +94,6 @@ confirm-коду: backend шлёт на почту короткий код, и макс. очков за партию и макс. очков за один ход (лучший ход, уже включающий все образованные им слова и бонус за все фишки). -### Администрирование *(Stage 9)* +### Администрирование *(Stage 10)* Админ (Basic Auth на gateway) разбирает жалобы на слова, управляет версиями словаря, смотрит пользователей/игры. diff --git a/gateway/internal/admin/admin.go b/gateway/internal/admin/admin.go index e006c21..ced600c 100644 --- a/gateway/internal/admin/admin.go +++ b/gateway/internal/admin/admin.go @@ -2,7 +2,7 @@ // reverse proxy to the backend admin API (docs/ARCHITECTURE.md §12). The gateway // validates the operator credential and forwards authenticated requests to // 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 import ( diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 0000000..c8f93f3 --- /dev/null +++ b/ui/README.md @@ -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) +```