0c5e40c509
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Has been skipped
CI / conformance (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Has been skipped
12 KiB
12 KiB
Agent field notes — scrabble-game
Non-obvious, hard-won project knowledge that is not in the main docs (CLAUDE.md, docs/*,
deploy/README.md). Kept in the repo so it travels with a clone to any host. These are working
memory, not a spec — verify any named file / flag / function against the current code before acting
on it, and prefer the authoritative docs where they overlap. Add to this file as new gotchas turn up.
Codegen & build
- jetgen churns everything.
backend/cmd/jetgenregenerates go-jet code for all tables and may reorder output. After running it, revert the churn on tables you did not touch and commit only your table's change. - flatc is version-pinned (
pkg/Makefile,REQUIRED_FLATC = 23.5.26, hard-checked). A different flatc silently churns the generated wire code and can flip wire defaults. Never regenerate FBS with another flatc version. - gopls lags codegen. Right after an FBS/jet regen, gopls shows phantom "undefined" errors. Trust
go build/go test, not the editor squiggles. - go-jet type mapping: SQL
numeric→float64,interval→string. Never store money asnumeric(float precision loss) — use bigint minor units + aMoneytype; store durations as int seconds, notinterval. go mod tidychokes on dot-free local module paths (scrabble/...). Hand-editgo.modwhen a bump is needed. The solver (../scrabble-solver) is consumed viago.workreplace locally, but a prod bump goes through a solver PR → master + a published tag, not a local replace.- Run the whole CI suite locally before pushing — unit + integration (
//go:build integration, Postgres) + the UI job + codegen check. Do not lean on CI to catch what a local run would. - CI runner shares this host's
/tmpas a different user. In workflow steps, write artifacts to${GITHUB_WORKSPACE}, never a fixed/tmp/...path (cross-user permission failures otherwise). - pnpm corepack pre-flight flakes.
pnpm exec/pnpm checkoccasionally abort on a corepack pre-flight. Dodge it by invoking the tool directly:node_modules/.bin/<tool>.
Wire / schema evolution (client ↔ gateway ↔ backend)
- FBS is additive-only. Add trailing fields ("added trailing — backward-compatible"). Never
delete or reorder a mid-table field — deprecate it (
(deprecated)); deleting shifts field IDs and breaks older readers. - Retiring a domain field must also retire the WIRE field it fed — by deprecation, not deletion,
and do not just zero it: a dead
0the client dutifully syncs will clobber the real value. - The gateway transcodes FBS ↔ backend JSON DTOs in lockstep. A wire change usually means editing both the FBS schema and the backend DTO.
- Seat display name is built in two places —
game.Servicelive events and the server REST DTOs. Change both or they drift. - A per-viewer flag is computed only in the per-viewer REST DTO; the client seeds it from REST, then bumps it from the live event. Don't expect it on the shared broadcast.
UI / Svelte 5
- Never name a
$statevariablestate.svelte-checkthen misreads$stateas a store subscription. Rename (e.g.view). - Svelte trims literal edge whitespace in markup. To keep a separator space, emit an expression:
{' : '}. - No global
.btn/.ghostbutton classes. Style buttons per-component with scoped CSS + design tokens (mirrorNewGame's.invite). - A
$stateproxy fails structured-clone when persisted to IndexedDB. Snapshot at the call site with$state.snapshot(...)before storing.
Platform / WebView quirks (Telegram, VK, iOS, native, old Android)
- Telegram
showPopupeats the user-activation. Share / clipboard called from inside ashowPopupcallback fail (no gesture). Use your ownModalfor gesture-gated Web APIs. - iOS Telegram
<a download blob:>navigates away and strands the SPA. Deliver files by Web Share on mobile, and only use a<a download>Blob path on desktop. - Android TG/VK WebViews lack
navigator.shareand ignore<a download>. Deliver a client-generated file by copying it to the clipboard. - VK Android WebView ignores
target=_blank. Open external links throughlib/links.ts(routes tovk.com/away.php). Verify on-device. - Per-platform file-delivery last hop differs (TG / VK / plain browser) — there is a delivery matrix; do not re-litigate it without new on-device facts.
- Old Android System WebView floor ≈ Chrome 67. The bundle targets es2019 (esbuild lowers
syntax), a conditional
core-jspolyfill loads only on old engines, andindex.htmlhas a boot gate (BigInt / Proxy are the hard block → the unsupported-engine screen). There's also avminglyph fallback for old rendering. - iOS WKWebView overscroll and Telegram swipe-to-close are not reproducible in Playwright. Verify those live on the deployed contour, not in the e2e.
- Telegram Desktop Mini App shows a persistent bottom-right loader — that's the long-lived Subscribe stream, cosmetic, deliberately left as-is.
Testing
- UI test layers: vitest (node env, pure logic, no jsdom) + Playwright mock e2e. The mock e2e bypasses the codec, so wire/codec bugs need codec unit tests, not e2e coverage.
- Mock overlay blocks e2e. The cold-load overlay must be instant under the mock build or it intercepts Playwright taps.
- Mock tile pools lack a blank
'?'. The seeded gameG1hard-codes one; flipG1's variant to eyeball per-variant tiles. - Durable Playwright MCP servers (chromium + webkit) exist for UI inspection (there's a plugin-config gotcha in wiring them).
docker run -p ...boot tests fail from the shell (published ports unreachable in this env). Use testcontainers for container-backed tests.- Distroless images run as UID 65532 (nonroot). Bind-mounted TLS keys must be 0644 (not 0600) or the service crash-loops on start.
Deploy / test contour (operational)
- The TEST contour runs on THIS dev host. Inspect it via
docker/ Prometheus. A host-sidecurlto caddy hangs (NAT hairpin) — don't debug the edge that way. - The contour's client IP is the home-router SNAT address, not the real external IP. Correct in prod, not a bug — which is why the IP ban / blocklist are prod-only.
- The contour is one shared env, last-deploy-wins. Keep a multi-PR batch a linear stack (one PR, one deploy) so deploys don't clobber each other.
- A schema/wire PR breaks the contour until a
DROP SCHEMA+ backend restart. Note such a prerelease step inPRERELEASE.md. - A contour DB wipe resets the account but not the client's stored locale; the reconciler then
syncs the stale locale, masking a fresh TG
language_codeseed. Clear client prefs too when testing locale. DNS=inTEST_AWG_CONFpins the VPN netns to 1.1.1.1 → internal names go NXDOMAIN → the bot-link silently dies. Diagnose from inside the netns.- Swap one contour service to a local image without deploy secrets via a busybox-in-the-netns socket-inspect trick (single-service recreate).
- A rolling deploy did NOT recreate caddy on a config-only change. Force
--force-recreatefor caddy; don't trust "deploy green" for an edge-config change. - A new gateway edge route MUST be added to the Caddyfile
@gatewaymatcher or it falls through to the landing catch-all. Add a CI probe for the route. - Prod caddy logs warnings only, no access log. Trace a request via the backend "http request" telemetry log or Tempo, not caddy.
- Confirm a release is live without SSH by grepping the served SPA:
__APP_VERSION__inside/assets/main-*.js. - "App hangs on load" was a dead HTTP/3 advert: caddy sent
Alt-Svc: h3with no UDP/443 open; clients cache it ~30 days. Fix withAlt-Svc: clear. - Config-poison deploy loop: a crash-looping config-mounted service + a missing bind source produces a root-owned directory that then fails deploys. Break it by removing the root-owned dir.
- Maintenance-window contract (planned-deploy 503): a marker header,
/_gmexempt, the flag spans the whole roll, the SPA overlay reloads on recovery. Don't break these invariants. - A new dictionary goes live via a
/_gm/dictionaryupload, NOT a redeploy. In-flight games keep their pinned version. The owner does the upload. - Renderer deploy job flakes on the skia-canvas GitHub binary download when its lockfile changes — re-run, it's not your code.
Repo workflow
- PR-based, zero issues. Work is tracked via PRs +
PRERELEASE.md. "заведи задачу" means do it / add a plan line — not file a tracker issue. teaCLI for all Gitea ops (PRs, secrets, variables, dispatch).ghdoes not work here. The agent cannot self-approve a PR but can merge it after the owner approves. Watch the stale-mergeable trap (re-check mergeability right before merging).- Watch every push/merge/deploy to green with
python3 ~/.claude/bin/gitea-ci-watch.py, launched bare under a background task. It polls run-level conclusions; itsALL GREENalready covers the gated deploy job. Pass--no-runs 600when the runner is busy. A merge is the most-forgotten case — watch the post-merge runs too. - After a merge, switch to the merged-into branch (
development/master), pull, and prune the local feature branch. - The contour deploy probe checks the backend
/readyz; a PR deploy builds the PR's own code; a wedged contour can be recovered by recreating the host container set.
Domain semantics (not obvious from the code)
- Account deletion must NOT delete any user messages — including feedback / support. Interview the owner on every deletion point before wiring it.
account.time_zoneisNOT NULL DEFAULT 'UTC', seeded from a detected ±HH:MM offset. An email account row is created at the code-request step, not at confirmation.- The robot has two distinct time windows: a sleep window (~00:00–07:00, gates its moves and nudges) vs the player away window (turn-timeout only). "окно отсутствия" in dialogue = the sleep window.
Production topology
- Two prod hosts.
main— full stack + ACME/edge (Selectel);tg— Telegram bot only (vdsina). SSH aliasesscrabble-main-ops/scrabble-tg-ops. Deploy is manual dispatch only, rolling + health-gated + auto-rollback. Hosts are provisioned bydeploy/ansible/(inventory + vars there — the source of truth for IPs/roles). - The agent has targeted root SSH to the hosts (and may run the prod Ansible). Surface every owner-side step before a deploy, not after.
Feature-area pointers (state lives in the linked docs/plans, not here)
- Monetization — «Фишка» currency, per-platform wallets, ads. Agreements in
docs/PAYMENTS_DECISIONS_ru.md; a phased plan (E0–E9). go-jet money rule above applies. - PITR — pgBackRest → Selectel S3 (encrypted, 30-day), currently gated off; runbook in
deploy/README.md. Gotcha: run pgBackRest via docker-exec as thepostgresuser with--pg1-user=scrabble. - Offline mode — the robot brain, move generator/validator/scorer and DAWG reader are JS ports
of the Go engine, bundled client-side and parity-pinned by golden tests. Multi-phase; native
offline-first bundles the dictionaries (see
ANDROID_PLAN.md). - VK ID web login — raw OAuth 2.1 against
id.vk.com, a separate VK "Web" app from the Mini App, server-side confidential code exchange. - Email relay — Selectel SMTP + confirm / link / unlink / deletion codes + alerts.
- Native Android — see
ANDROID_PLAN.md(Capacitor bundle model, client-version gate, offline-first, RuStore).