There was no way to see the money as a whole: the ledger was only ever rendered inside one account's card, so "what came in last month" meant exporting the entire CSV and reading it elsewhere. /_gm/ledger lists every operation, newest first, filtered by date range (defaulting to the last 30 days), by wallet, by rail, by kind and by account. Wallet and rail are deliberately separate axes because they answer different questions — "what happened on VK" matches the funded segment or the benefit origin, while "what came through YooKassa" matches the settling provider, and a chip spend has no rail at all, so it drops out of that filter by construction. Above the table sit the totals for everything the filter matches, not merely the page on screen, which is the point of showing them. Money is listed per currency because the rails settle in roubles, Votes and Stars and one sum across them would mean nothing. Row amounts come from the operation snapshot, since the ledger's own columns count chips rather than money. Paging, the CSV export and a refund's way back are all built from the same encoded filter, so none of them can quietly show a different slice than the screen. The export moves here from the user card and gains the filter along with money columns; it is capped, because an append-only ledger grows forever and an unbounded export would eventually time out. The refund action moves onto the funded rows here — an operator can now find a payment by filter without knowing whose it is first — and returns to the same filtered view. The destination travels with the form but is only honoured when it is a console path, so a crafted form cannot turn it into an open redirect. The user card keeps the account's standing rather than its history: balances, benefits, the risk flag and a lifetime summary (money paid and refunded per currency, chips credited and spent), plus a link into the ledger scoped to that account. Operations are rendered in one place, with filters and paging, instead of two. Tests: the filter axes, paging that neither repeats nor drops a row, totals that describe the range rather than the page, money recovered from a snapshot and absent on a spend, and the console page, the export and the card hand-off. The existing finance-panel test now asserts the summary and that the card no longer re-renders the rows.
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 injectsX-User-ID), routing tobackend, and an admin surface behind Basic Auth.backend— internal-only service that owns every domain concern and embeds thescrabble-solverengine 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.
- FlatBuffers, embeddable in platform webviews and packageable to native via
Capacitor. See
platform/*— per-platform side-services (e.g. the Telegram bot).
Documentation (sources of truth)
docs/ARCHITECTURE.md— global architecture, transport, security, cross-service contracts.docs/FUNCTIONAL.md(+_ru) — per-domain user stories.docs/TESTING.md— test layers and the CI gate.CLAUDE.md— project guide and development workflow.
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.