Ilia Denisov fe5a3d6d3b
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 22s
CI / ui (pull_request) Successful in 1m13s
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m7s
fix(login): stop a rate-limit from latching a phantom offline on the login screen
A fresh email login that hit the per-IP email rate limit stranded the user in
an unrecoverable "offline" state that survived a full PWA restart, even though
the network was fine.

Root cause: the gateway email class (auth.email.request + auth.email.login,
keyed per IP, 5/10min burst 2) trips on the third event, which in the natural
"request code -> wrong code -> correct code" sequence is the correct-code login.
The gateway returns ResourceExhausted; the client mapped it to 'rate_limited',
which retry.ts classified as retryable + a connection code, so exec() called
reportOffline(). That pushes the net-state machine into connecting, whose
recovery probe is an authenticated profile.get -- but the login screen has no
session, so the probe can never succeed and the machine latches offline. The
transport kill switch (assertOnline) then refuses the very login that would fix
it, and because the IP's email bucket refills only 1 token / 120s, each fresh
attempt after a restart is rate-limited again -> offline again.

Fix (client, narrow -- the trigger):
- A rate-limit is no longer treated as connectivity. retry.ts no longer marks
  'rate_limited' retryable or a connection code, so exec() never reports it
  offline; it surfaces as the existing error.rate_limited "slow down" message.
  Not auto-retrying it also stops the ~20s button freeze and avoids feeding the
  gateway's ban tripwire with 6 extra rejections per attempt.

Fix (server):
- Raise the email-code burst 2 -> 4 so the honest request + a mistyped code +
  the correct one is not throttled mid-login. Defence-in-depth over the
  backend's own per-code guards (5-attempt cap + 15-min TTL + send throttle).

Tests: retry classification units updated to the new semantics; a gateway
regression guard asserts the honest three-event email flow passes under the
default policy. gateway/README.md rate-limit note updated.

The deeper gap (the net-state recovery probe is session-gated, so any real
transport failure on the session-less login/confirm screens still cannot
self-heal) is left as a known residual, deferred by the owner.
2026-07-13 22:48:59 +02:00

scrabble-game

Multiplatform Scrabble game. Players arrive from a platform (Telegram first; later VK/MAX/iOS/Android) or from standalone web (email / guest). The game supports English Scrabble, Russian Scrabble and Эрудит.

Components

  • gateway — the only public ingress: anti-abuse, platform authentication (resolves the player and injects X-User-ID), routing to backend, and an admin surface behind Basic Auth.
  • backend — internal-only service that owns every domain concern and embeds the scrabble-solver engine library in-process.
  • 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.
  • platform/* — per-platform side-services (e.g. the Telegram bot).

Documentation (sources of truth)

Build & test

go build ./backend/... ./pkg/... ./gateway/...  # per module (the workspace spans several)
go vet ./backend/... ./pkg/... ./gateway/...
gofmt -l .                                       # must print nothing
go test -count=1 ./backend/... ./pkg/... ./gateway/...   # unit tests
go test -tags=integration -count=1 -p=1 ./backend/...    # + Postgres (needs Docker)

The integration-tagged tests start a throwaway postgres:17-alpine container via testcontainers-go and require a reachable Docker daemon; they live in the backend module. The wire contracts in pkg and the Connect edge in gateway have committed generated code (regenerate dev-time with make -C pkg gen / make -C gateway gen).

Run the backend locally

The backend now owns persistence, so it needs Postgres and applies its embedded migrations at startup:

docker run -d --name scrabble-pg -e POSTGRES_PASSWORD=dev -p 5432:5432 postgres:17-alpine
BACKEND_POSTGRES_DSN='postgres://postgres:dev@localhost:5432/postgres?search_path=backend&sslmode=disable' \
  go run ./backend/cmd/backend     # HTTP API + probes on :8080, push gRPC on :9090

Run the gateway locally

The gateway is the public edge; point it at a running backend:

GATEWAY_BACKEND_HTTP_URL=http://localhost:8080 \
GATEWAY_BACKEND_GRPC_ADDR=localhost:9090 \
  go run ./gateway/cmd/gateway     # Connect/h2c edge on :8081

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.

Run the UI locally

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.

Deploy (deploy/)

The full contour is deploy/docker-compose.yml: backend + gateway (with the UI embedded via go:embed, baked in by its node build stage) + Postgres + the Telegram connector (with a VPN sidecar) + an observability stack (OTel Collector → Prometheus + Tempo → Grafana) + a front caddy that owns a single /_gm Basic-Auth (admin console + Grafana). The Go services build from multi-stage distroless */Dockerfile.

docker build --build-arg DICT_VERSION=v1.3.0 -f backend/Dockerfile -t scrabble-backend .  # DICT_VERSION required; pulls that DAWG release artifact
docker build -f gateway/Dockerfile -t scrabble-gateway .   # node stage builds + embeds the UI
docker compose -f deploy/docker-compose.yml config         # validate (needs the TEST_/PROD_ env)

CI auto-deploys the test contour on a PR into — or push to — development (.gitea/workflows/ci.yaml); the prod contour is a manual deploy after development → master. Env reference: deploy/.env.example; the topology and the two-contour model are in docs/ARCHITECTURE.md §13.

S
Description
No description provided
Readme 26 MiB
v1.1.0 Latest
2026-06-22 07:27:41 +00:00
Languages
Go 57.8%
TypeScript 29.2%
Svelte 8.7%
JavaScript 1.3%
Go Template 1.2%
Other 1.7%