44117e906cf6900a9a3baac1ad166a20a2c89f88
162 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0eefbfd6a4 |
fix(account): dedupe colliding identities on merge, journaling to the dossier
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 16s
CI / ui (pull_request) Has been skipped
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m2s
An account merge blanket-reassigned all of the secondary's identities to the primary, so merging two accounts that each had a confirmed email (or telegram/vk) left the survivor with two identities of one kind — which the profile and the retention dossier both treat as singular (the profile showed an arbitrary one; a later change-email journaled only one). The merge now keeps the primary's identity and journals the secondary's colliding one to retained_identities (reason=merge) before dropping it, so the survivor has one identity per kind and the absorbed credential still lands in the legal dossier. - migration 00008: widen retained_identities.reason CHECK to admit 'merge' (expand-contract — Up only widens the set, so an image rollback stays DB-safe). - accountmerge: dedupeIdentities + retainMergedIdentity before the identity reassign. - inttest TestAccountMergeDedupesEmail; docs ARCHITECTURE §4 + retention.go reason note. |
||
|
|
2c465c01d2 |
feat(account): VK ID web login to link a VK identity from a browser
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Successful in 1m4s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
A browser has no signed VK Mini App launch params, so linking VK on the web uses
VK ID's raw OAuth 2.1 flow (PKCE, no @vkid/sdk): the SPA redirects to VK's hosted
login and returns with an authorization code, which the gateway exchanges
server-side (confidential, under the VK "Web" app's protected key) for the trusted
vk user id — then the existing link/merge machinery attaches or merges it.
- fbs LinkVKRequest{code, device_id, code_verifier}; codec + TS bindings.
- backend link.Service ConfirmVK/MergeVK/attachVK (KindVK, mirror Telegram),
handleLinkVK[Merge], routes /user/link/vk[/merge], backendclient LinkVK[Merge].
- gateway internal/vkid confidential code exchange (id.vk.com/oauth2/auth);
transcode link.vk.confirm/merge (registered only when configured) + config
GATEWAY_VK_ID_{APP_ID,CLIENT_SECRET,REDIRECT_URL} + main wiring.
- UI lib/vkid (PKCE authorize redirect + callback), Profile "Link VK" control,
boot callback handling; a merge re-authorizes for a fresh code (VK codes are
single-use). Web-only (a redirect strands a Mini App webview).
- Deploy: VITE_VK_APP_ID + VITE_VK_ID_REDIRECT_URL build args + gateway env,
ci.yaml/prod-deploy TEST_/PROD_ vars, compose/Dockerfile/.env.example/README.
- Tests: vkid exchange unit (string/number user_id, id_token fallback, errors),
transcode link.vk, backend ConfirmVK/MergeVK inttest, codec encodeLinkVK.
- Docs: ARCHITECTURE §4, FUNCTIONAL(+ru), gateway README.
|
||
|
|
70f0f9e36a |
docs(architecture): observability alerting + admin-alert worker
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Successful in 1m6s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Failing after 1m8s
§11 gains the alerting layer: Grafana infra rules (scrape-down, edge error-rate/p99, host mem/disk/cpu, postgres connections, TLS cert < 20d via blackbox), noDataState=OK, and the backend admin-alert worker (coalesced email on new feedback / complaints). |
||
|
|
c2a8426b74 |
docs: account deletion = legal retention, not erasure
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Successful in 1m5s
CI / conformance (pull_request) Successful in 8s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m43s
FUNCTIONAL (+ru): the deletion user story — anonymised live surfaces ([Deleted]), freed sign-in methods, a two-year-retained dossier, the code/phrase step-up, game forfeit + all-robot drop, fresh account on reopen. ARCHITECTURE: the retention model (retained_identities journal on every detach, tombstone + [Deleted] sentinel, drop all-robot games, purpose=delete step-up, cold-load last-login, two-year TTL reaper). |
||
|
|
150660819a |
docs: sign-in methods matrix — unlink + change-email
FUNCTIONAL (+ru): drop the stale 'linking UI hidden' note; document the profile sign-in-methods matrix, the unlink last-identity guard, and the non-disclosing atomic email change. ARCHITECTURE: unlink + change-email behaviour and the purpose=change deeplink branch. |
||
|
|
1dca6741f1 |
feat(ui): auto-confirm the email deeplink on load
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 1m3s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m44s
Drop the confirm button: the /confirm screen confirms the token as soon as it loads. The token rides the URL fragment (never sent to the server), so a plain link prefetch cannot reach it, and the manual six-digit code is the fallback if an aggressive scanner runs the page. Update the ARCHITECTURE/FUNCTIONAL wording accordingly and swap the confirm.prompt/action strings for confirm.busy (en+ru). |
||
|
|
65f2c87a74 |
docs+test(email): document the confirm deeplink + wire-contract tests
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 16s
CI / ui (pull_request) Successful in 1m3s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m52s
Document the one-tap confirm deeplink in ARCHITECTURE (§4 the login magic-link / link confirm+profile-refresh, prefetch-safe token) and the new notify 'profile' sub-kind (§10), and add the one-tap link to the FUNCTIONAL email story (+ru). Add codec round-trip assertions for the EmailRequest language field and encodeEmailConfirmLink (the mock e2e bypasses the codec). |
||
|
|
c702f1bdac |
fix(email): explicit TLS mode for non-standard relay ports
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 17s
CI / ui (pull_request) Successful in 1m4s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m54s
The 465-only implicit-TLS heuristic mis-classified Selectel's SSL port (1127), which the mailer would have dialled with STARTTLS and failed. Add BACKEND_SMTP_TLS (ssl|starttls) — empty still derives the mode from the port (implicit on 465, else STARTTLS) — and dial implicit TLS with WithSSL()+WithPort so any port works, not just 465. Wire SMTP_RELAY_TLS through compose/ci/prod/.env.example and document it (Selectel: 1127 = SSL, 1126 = STARTTLS). Unit-tested. |
||
|
|
9e0f929e40 |
test+docs(email): squat-fix coverage and the email-pipeline docs
Add an integration test asserting the guest-until-confirmed squat fix (an email-login account is a reapable guest until the code is confirmed, then durable). Document the branded relay pipeline in ARCHITECTURE (go-mail, TLS by port, no client cert, PUBLIC_BASE_URL anti-injection, per-recipient send throttle, guest-until-confirmed), FUNCTIONAL (+ru), TESTING and the backend README config table (BACKEND_SMTP_* + BACKEND_PUBLIC_BASE_URL). |
||
|
|
d5fbaa3034 |
feat(export): server-rendered artifacts behind one signed download URL (#160)
CI / changes (push) Successful in 1s
CI / unit (push) Successful in 9s
CI / integration (push) Successful in 15s
CI / ui (push) Successful in 1m2s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m42s
The finished-game export (GCG + a new PNG of the final position) is one signed, short-lived relative URL (game.export_url; HMAC-SHA256, 10-min TTL, BACKEND_EXPORT_SIGN_KEY) resolved against the client's own origin and delivered by the best affordance each platform has (five on-device review rounds): - TG Android/desktop: native showPopup chooser -> native downloadFile dialog (bridge-only chain, activation-safe). - TG iOS: app-modal chooser -> OS share sheet with the fetched file (a popup callback cannot supply the activation the sheet needs). - VK iOS: VKWebAppDownloadFile for both formats. - VK Android: the PNG opens in VK's native image viewer, the GCG copies to the clipboard (the VK Android downloader hangs on any download, Content-Length/Range notwithstanding). - VK desktop iframe / desktop browsers: plain anchor downloads. - Mobile browsers: the OS share sheet (fetch-then-share). - Legacy TG (< Bot API 8.0): app modal + GCG clipboard, no image option. The PNG is rasterized on demand by the new internal `renderer` sidecar (node:22-slim + skia-canvas + baked Liberation/Noto Color Emoji fonts) executing the SAME ui/src/lib/gameimage.ts the ui project unit-tests; the backend rebuilds the render payload from the journal + engine.AlphabetTable, and the device date locale, IANA time zone and localized non-play labels ride the signed URL. Nothing is stored — the artifact re-derives from the immutable journal on each GET. The gateway forwards /dl/* (caddy @gateway matcher extended) behind the per-IP public rate limiter and serves bytes via http.ServeContent. Deploy: renderer service in compose + prod overlay + rolling order + prod push list; TEST_/PROD_EXPORT_SIGN_KEY secrets; the sidecar smoke runs in the ui CI job. Docs: ARCHITECTURE, FUNCTIONAL(+_ru), UI_DESIGN, TESTING, deploy/README, renderer/README. |
||
|
|
946420db93 |
fix(game): own export chooser modal; PNG option outside in-app webviews only
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 59s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m26s
Telegram's native showPopup delivers its callback with no user activation, so navigator.share (TG iOS) and clipboard writes (TG Android GCG copy) silently fail from it — the chooser is now always the app's own modal, keeping the button click's gesture alive for the delivery APIs. The data:URL preview modal is dropped: the Android TG/VK long-press menu mangles data: URLs (dead download, black-screen open, base64 clipboard garbage), so a binary PNG has no working client-side route in those webviews at all. The image option is withheld there until the server-rendered signed-URL delivery (Telegram downloadFile / VKWebAppDownloadFile) lands; the plain web and mobile browsers keep it. On-device findings by the owner on the test contour (TG iOS, TG Android, VK Android). |
||
|
|
a0eacf3011 |
feat(game): finished-game export as a PNG image behind a format chooser
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 59s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m16s
The history header's export button now opens a chooser — Telegram's native popup inside Telegram, the app's own modal elsewhere — offering the GCG file and a new client-rendered PNG of the final position (lib/gameimage, Canvas 2D, lazy dynamic import, zero dependencies): light theme, classic A..O/1..15 axes, label-free premium fills, and a fixed-typography per-seat scoresheet with GCG-style move coordinates, multi-word sub-lines, endgame rack-settlement row, winner trophy and a hostname + device-locale finish date footer; a long game stretches the board, never the typography. Delivery mirrors the GCG rules (Web Share with no blob fallback, else download) except on Android Telegram/VK WebViews and the desktop VK iframe, where a binary PNG has no clipboard-text fallback: those get a preview modal with a long-press/right-click save hint and a copy-image button where ClipboardItem exists. |
||
|
|
1ed624eaf1 |
feat(landing): Russian default + SEO head, icons and OG card
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 57s
CI / conformance (pull_request) Successful in 8s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m20s
The landing now always opens in Russian (saved 🌐 choice still wins) —
browser-language detection made the indexed content nondeterministic
(Googlebot renders with en-US). landing.html gains the static Russian
SEO head: title/description, canonical pinned to the production origin,
Open Graph card (Telegram/VK link previews), twitter:card, JSON-LD,
theme-color and the favicon set; the SPA shell turns noindex and its
tab title becomes «Эрудит (Скрэббл)». New assets/icons generator
(same tile design as the VK loader) produces favicon.svg/ico,
apple-touch-icon.png and og-image.png into ui/public/, plus robots.txt.
|
||
|
|
aec915d5c1 |
feat(ui): drop the one-word status-bar badge
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 17s
CI / ui (pull_request) Successful in 57s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
The small 1️⃣ in the status bar's score-preview slot read as noise; the
single-word rule keeps its spelled-out label in the history header and
the lobby invitation card.
|
||
|
|
5f574a765d |
feat(landing): VK entry logo + restore the Telegram channel build-arg
Add the VK Mini App logo next to the Telegram one on the landing hero,
linked via the new VITE_VK_APP_LINK build-arg (full URL, wired through
compose, CI and prod-deploy from TEST_/PROD_VITE_VK_APP_LINK).
Also restore the landing's Telegram link itself: commit
|
||
|
|
2e8fa83814 |
feat(telemetry): local move-preview adoption metrics (Phase 4)
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 57s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
Measure uptake of the client-side local move-preview accelerator (§5) so adoption can be watched before defaulting it on: app cold starts, dictionary loads by result (fetched / cache_hit / miss) and move previews by path (local / network — the backend load shed). A small best-effort client beacon (POST /metrics/local-eval, session-gated) batches counter deltas and posts them on a 60s timer and when the app is backgrounded — never on the gameplay path: the in-app counters are plain in-memory increments, only the periodic flush touches the network and it is fire-and-forget. The gateway folds each batch into three OTel counters (local_eval_cold_start_total, local_eval_dict_load_total, local_eval_preview_total), clamped against a spoofed inflation. - gateway: counters + recordLocalEval + session-gated /metrics/local-eval handler - ui: localeval-metrics accumulator/beacon; hooks in the dict loader, in Game.recompute and in bootstrap (skipped under the mock harness) - caddy: route /metrics/* to the gateway - docs: ARCHITECTURE §11; Grafana "Scrabble — Users" dashboard panels |
||
|
|
5689f7f6a3 |
feat: on-device move preview (local eval) with network fallback
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 58s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
Score and validate a tentative move on-device instead of a per-arrangement network
round trip. The dawg reader and the validate/score/direction slice of the
scrabble-solver engine are ported to TypeScript (ui/src/lib/dict), pinned
byte-for-byte to the Go engine by a `conformance` CI job (full-dictionary reader
parity plus a battery of plays across every variant and both cross-word rules,
including the inferred orientation). The server stays authoritative — submit_play
re-validates — so the local result is an advisory accelerator only.
- backend: Registry.DictBytes + an authed GET /api/v1/user/dict/{variant}/{version}
(immutable) streaming the pinned per-game dawg; caddy routes /dict to the gateway.
- gateway: a session-gated /dict edge route proxying it; fetchDict on the transport.
- client: the dictionary loads on game open (low priority so it never starves the
game on a slow link; aborted at a 5s cap or when leaving the game), is cached in
IndexedDB (best-effort, self-healing on a rejected blob) and reused across
sessions; a warm-up overlay covers a cold load, then the network preview is the
fallback; a bad-connection breaker stops warming after repeated misses; the move
preview cancels its in-flight request when the tiles change.
- parity generators backend/cmd/{dictgen,validategen} + gated Vitest suites, run in
CI against the release dictionaries. A hidden debug readout lists the cached
dictionaries + breaker state, and its reset clears the cache.
- docs: ARCHITECTURE §5, TESTING, UI_DESIGN, FUNCTIONAL (+ru).
|
||
|
|
e6c5198caa |
build(ui): drop sourcemaps from prod bundle
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
The production UI build shipped `.map` files with full `sourcesContent`, and the gateway/landing images serve `dist/` verbatim, so anyone could fetch `/app/assets/main-*.js.map` (same assets under `/vk/`, `/telegram/`) and reconstruct the entire TypeScript/Svelte source at the edge. Gate `build.sourcemap` off for `mode === 'production'` (the Docker image build). Dev and the `mock` e2e build (`vite build --mode mock`) keep maps for debugging. Document the posture in ARCHITECTURE.md §12. |
||
|
|
5f9b4a7a38 |
feat(ui): paint VK status bar to the app theme (VKWebAppSetViewSettings)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 58s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m13s
Parity with the Telegram chrome painting: on a VK Mini App launch and on every theme change, set VK's status-bar appearance (and, on Android, the action/navigation bar colours) from the app's live theme tokens, so the VK chrome matches the UI instead of clashing. syncVKChrome mirrors syncTelegramChrome; the status-bar appearance is derived from the --bg token's luminance (appearanceForBg, unit-tested). Wired into the VK onScheme handler (fires on launch + on theme change) and setTheme. Docs: UI_DESIGN VK integration. |
||
|
|
4458f0e545 | Merge pull request 'fix(ui): copy GCG to clipboard on Android in-app WebViews' (#151) from feature/gcg-export-android-fix into development | ||
|
|
41f26da9a7 | Merge pull request 'fix(game): clear nudges on game completion' (#148) from feature/nudge-clear-on-completion into development | ||
|
|
88b6761e28 |
fix(ui): copy GCG to clipboard on Android in-app WebViews
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m16s
On-device diagnostics from Android Telegram and VK confirmed both expose no navigator.share AND no navigator.canShare, so the export fell to a Blob <a download> that those WebViews silently ignore — nothing happened. pickGcgDelivery is now a 3-way decision: Web Share where available (iOS), a clipboard copy in an Android in-app WebView (Telegram/VK: no share, dead download), else a desktop Blob download. shareOrDownloadGcg reports the outcome so the game shows a "GCG copied" toast; the copy is VKWebAppCopyText inside VK (which also covers the desktop VK iframe, where navigator.clipboard is blocked) and navigator.clipboard otherwise. Unit tests cover the 3-way choice and the copy/failed outcomes; new i18n key game.gcgCopied (en+ru); docs ARCHITECTURE/FUNCTIONAL(+ru)/UI_DESIGN/TESTING. |
||
|
|
71c3411276 |
fix(ui): mobile in-app WebView polish (tap-flash, VK haptics, swipe-back)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m13s
- Tap-highlight: add -webkit-tap-highlight-color: transparent on #app. Android in-app WebViews (Telegram, VK) flashed a momentary selection-like box on tappable nodes on tap — seen on the header title and the back chevron; user-select (already none) does not govern it. Inherited, so this clears it app-wide. - VK haptics: mirror the Telegram haptic set on VK via VK Bridge taptic (impact / notification / selection), routed through a new shared lib/haptics.ts dispatcher. VK users previously got no haptics; the game and error call sites now fire haptic() instead of telegramHaptic(). - VK swipe-back: disable VK's horizontal swipe-back at launch (VKWebAppSetSwipeSettings history:false) so it does not fight the app's own edge-swipe-back and on-board tile drag — parity with Telegram's disabled vertical swipes; the app owns navigation via its back chevron. Docs: UI_DESIGN (no-select / tap-highlight, VK integration). |
||
|
|
f9acea1d9a |
fix(game): clear nudges on game completion
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
Nudge badges lingered in the lobby on games that ended by turn-timeout, resignation or forfeit: those paths commit the finish directly, bypassing the move path's per-mover NudgeClearer, so the awaited seat's nudge was never marked read. A finished game's nudges are stale, so clear them all. Add a wired NudgeExpirer (social.ExpireNudges) called from the shared commit finish block — covering every completion path — and from the voidGame recovery path. It clears every seat's nudge bits for the game and leaves chat messages unread; unlike ClearNudges it records no publish-to-read latency, since a completion is an expiry, not a read. An integration test reproduces the timeout case (nudge cleared, chat kept). Docs: ARCHITECTURE §9.1, FUNCTIONAL (+_ru), backend/README. |
||
|
|
6636d7c309 |
feat(ui): first-run onboarding coachmarks
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) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m20s
A one-time coachmark overlay walks a new player through the lobby and their first game board: a light dimmed layer draws one tail-pointed hint bubble at a time, advancing on a tap anywhere and removing itself for good after the last hint. Two independent series (lobby: settings/stats/new game; game: header/pass-exchange/hints/shuffle/rack), gated by a per-device persisted flag and marked done only after the last hint, so an interrupted run replays from the start. A deep-link into Settings -> Friends still triggers the lobby series on the first trip back to the lobby. Targets carry a data-coach attribute, so one positioning engine anchors the bubble in both portrait and landscape, re-measuring each frame until the geometry settles (route slide, hidden-banner reflow, fonts). The promo banner hides while the overlay is up (app.coachActive); a hidden DebugPanel "Reset visited" control replays the walk-through. Off by default in the mock build so the Playwright smoke is unaffected; ?coach forces it on for the dedicated e2e. Pure geometry (step lists, nextVisibleStep, placeBubble) in lib/coachmark.ts (unit-tested); Coachmark.svelte renders. Docs: FUNCTIONAL(+ru) onboarding story, UI_DESIGN coachmark section. |
||
|
|
373aa2aa6d |
docs: sync VK theme-follow + home-bar safe-area into FUNCTIONAL/UI_DESIGN/.claude
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
The #142 polish shipped three VK behaviours the docs never picked up (they were written for the #140 auth MVP): - the "auto" theme follows the VK client light/dark (VKWebAppUpdateConfig), not the VK webview's prefers-color-scheme; - the layout clears the VK mobile home bar via CSS env() max'd with the VK bridge insets (VKWebAppUpdateInsets) — the bridge value is needed on Android, where the VK webview exposes no env() inset (.claude said env() handled it alone); - share/copy route through the bridge (VKWebAppShare/VKWebAppCopyText). FUNCTIONAL (+_ru) VK paragraph gains theme+home-bar parity with the Telegram one; UI_DESIGN gets a VK-integration note beside the Telegram one. |
||
|
|
4a6fe318be |
docs: correct VK invite-link reality + note desktop board drag-to-pan
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
Two doc edits that did not land alongside their code:
- FUNCTIONAL (+_ru) claimed a friend invitation is shareable as a Telegram
*or VK* deep link "that opens it directly". On VK that is false: vkShareLink
emits a plain vk.com/app<id> link (VK forwards no payload to the Mini App),
so the recipient enters the copied code by hand (#142, commit
|
||
|
|
da17c18895 |
feat(vk): native share/copy, auto theme, friend-code deep link, home-bar safe area
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 55s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m25s
Group B of the VK integration — the contour-verified follow-up to the launch+auth MVP: - Share/copy inside the VK iframe go through VK Bridge: the friend-code invite shares via VKWebAppShare and copies via VKWebAppCopyText, since navigator.share is absent in the desktop iframe and navigator.clipboard is blocked there. - The invite link is a VK Mini App direct link (vk.com/app<id>#f<code>) on VK instead of the Telegram link; the app id comes from vk_app_id in the launch params (no build arg needed). The recipient's launch routes the deep link from VK's `hash` launch query parameter. - The app's "auto" theme follows the VK client's light/dark appearance (VKWebAppUpdateConfig), which the VK mobile webview's prefers-color-scheme does not track. - The safe-area CSS vars default to env(safe-area-inset-*), so the VK mobile layout clears the home bar (and Capacitor/PWA too); Telegram still overrides them from its SDK. vk.ts adds vkAppId/vkStartParam/vkShare/vkCopyText/vkOnScheme. Verified: svelte-check, 347 unit (+ vkAppId/vkStartParam/vkShareLink), build, bundle-gate. The VK-Bridge behaviours need the live contour (not reproducible headless). |
||
|
|
65c194264c |
feat(vk): embed the game as a VK Mini App
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 1m0s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
Mirror the Telegram Mini App wrapper for VK: the SPA loads at a new /vk/ entry, authenticates from VK's signed launch parameters, and provisions a 'vk' platform identity — the minimum to run the game in VK test mode. - Gateway verifies the launch signature in-process (internal/vkauth: HMAC-SHA256 over the sorted vk_* params under GATEWAY_VK_APP_SECRET, base64url) — a pure offline check, no side-service. New auth.vk op (gated on the secret), backendclient.VKAuth, /vk/ SPA mount. - Backend: KindVK + ProvisionVK/vkSeed, /sessions/vk handler, identity kind widened to include 'vk' (migration 00005, expand-contract). - UI: src/lib/vk.ts (VK Bridge, lazy-imported), bootVK + the /vk/ boot dispatch, encodeVKLogin + authVK across transport/client/mock. VK omits the name from the signed params, so the client reads it via VKWebAppGetUserInfo as an unsigned display seed. - Deploy: /vk in the edge Caddyfile, GATEWAY_VK_APP_SECRET wired through compose + .env.example + CI (TEST_) + prod-deploy (PROD_). - Admin console: surface the VK user id (link to the VK profile) next to the Telegram id on the user card. - Docs: ARCHITECTURE §12/§13, FUNCTIONAL (+ _ru), gateway README; VK integration reference under .claude/. Signature algorithm verified against dev.vk.com plus independent Node/Python references and a %2C edge-case vector. |
||
|
|
29b6c7e4d8 |
docs(telegram): document the Mini App embedding enhancements
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 17s
CI / ui (pull_request) Successful in 56s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m22s
Record the current state in ARCHITECTURE and FUNCTIONAL (+ _ru mirror): inside the Mini App the client tracks Telegram's live theme switch, fits the full device safe-area, exposes a native Settings button into the in-app settings, and syncs the device-independent display prefs (theme, reduce-motion, board labels — not the interface language) across the user's Telegram devices via CloudStorage; the validator denies a bot user (is_bot) before provisioning an account. |
||
|
|
03dfc29a54 |
feat(telegram): promo deep-link seeds English Scrabble for new users
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Successful in 56s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 50s
The promo bot button carries a configurable variant-seed start-param (default verudit_ru-scrabble_en). The gateway parses start_param from the validated initData and forwards it; the backend, on first contact only, seeds the new account variant_preferences from it (English Scrabble alongside the default Erudit). No schema change (the scrabble_en CHECK is already in the baseline) and the gateway<->backend REST field is additive, so the rolling deploy is safe in either order. TELEGRAM_PROMO_START_PARAM configures the payload (empty forwards the user own /start payload). Covered by account unit tests, a gateway transcode test, and an integration test asserting new-only seeding. |
||
|
|
bb18dc362b |
fix(telegram): support card — text_mention name, no command/dead link
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 17s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
The topic info card built the profile link as `tg://user?id=`, which clients render as dead text for a user they don't already know (the test-vs-prod difference an operator saw), and it put the raw display name through HTML — so a name starting with "/" was auto-detected as a tappable bot command that fired when tapped. Render the card with message entities instead: the display name is covered by a `text_mention` entity (the reliable way to mention a username-less user the bot has already seen — it messaged the bot), which links the profile AND, because the name sits inside an entity, suppresses the "/command" and "@mention" auto-detection on it. Drop the HTML parse mode and the tg:// link. Entity offsets are UTF-16. |
||
|
|
6a602aefae |
feat(telegram): bot support relay — per-user forum topics
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 12s
CI / integration (pull_request) Successful in 16s
CI / ui (pull_request) Successful in 56s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m20s
Users who DM the bot anything but /start are relayed into a private forum supergroup, one topic per user. Operators (the chat's admins) reply in the topic and the bot copies it back to the user; an info card opening each topic carries a Block/Unblock toggle and a Clear button. State is a small JSON file on a new /data volume — the bot host has no database. Off by default (TELEGRAM_SUPPORT_CHAT_ID=0): the prod bot is unchanged until the operator sets the chat id and adds the bot as a forum admin. - internal/support: concurrency-safe JSON store (topic map, block list, relayed message ids) with field-targeted mutators and atomic save - bot/support.go: relay both ways via copyMessage, short-TTL admin cache, callback buttons, per-user topic-create lock, loop guard (skip the bot's own posts), reopen a deleted topic on the next message - config + compose + CI/prod-deploy: TELEGRAM_SUPPORT_CHAT_ID per contour + TELEGRAM_SUPPORT_STATE_DIR; bot-state named volume; /data pre-owned by UID 65532 so a fresh volume is writable under distroless - docs: ARCHITECTURE §15 + decision record, FUNCTIONAL (+ru), README |
||
|
|
37070c3cb7 |
feat(ui): drop Telegram fullscreen; own back chevron everywhere; hidden debug panel
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 56s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m13s
Finalises the Telegram Mini App navigation work after on-device testing (Pixel 10 / Android 17 + iOS, fresh beta clients): - Remove requestFullscreen entirely. Immersive fullscreen hid Telegram's native header (and its BackButton) and the Android system swipe-back minimised the app; the owner prefers the windowed full-size (expand) presentation, so the app never requests fullscreen on any platform now. - The app's own back chevron (Header, showBack = !!back) drives back-navigation on every platform; the native Telegram BackButton is dropped — it does not render in the windowed Mini App (backVisible=false on iOS and Android), so relying on it lost back navigation (notably none on iOS). - Replace the temporary always-on diagnostic overlay with a hidden debug panel (components/DebugPanel): ten quick taps on the header title open it; it shows a privacy-safe client diagnostic snapshot (app version, locale, online, userId, Telegram chrome / viewport / SDK state — no secrets, no IP) and shares it via the OS share sheet / clipboard; a tap anywhere except Share dismisses it. - Drop the now-dead telegramRequestFullscreen / telegramBackButton / isTelegramAndroid helpers and the iOS-fullscreen unit test. Telegram has no native non-modal notification API (only modal showPopup / showAlert), so in-app toasts stay ours. Docs: UI_DESIGN.md. |
||
|
|
6aa5023b24 |
fix(ui): Android Telegram nav — windowed mode, no close guard
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 56s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m18s
Three Android Mini App issues, all in the Telegram chrome: - Entering a game showed no native back button, and the Android system swipe-back minimised the app instead of navigating. Root cause: immersive fullscreen (requestFullscreen). On Android, fullscreen replaces the native header — and its BackButton, which also captures the system back — with a bare close/menu pill, so back navigation has no control to land on. Request fullscreen on iOS only; Android stays windowed, keeping the native header + BackButton (and the system swipe-back that routes to it). iOS is unchanged. - Closing the game board always prompted "changes that you made may not be saved", even on a board just opened and untouched. The close-confirmation was armed unconditionally on game mount. Remove it entirely: move drafts auto-save (debounced during play + flushed on destroy), so nothing is lost on close. Drops the now-unused telegramClosingConfirmation + isMobilePlatform helpers and their SDK interface fields. Docs: UI_DESIGN.md. |
||
|
|
ae5090b851 |
fix(ui): load telegram-web-app.js dynamically with a timeout
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 55s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m18s
The Telegram SDK was a render-blocking <script src="telegram.org/..."> in the shared index.html shell, so it ran on every entry (/telegram/, /app/, native). On a network that blocks telegram.org — common where Telegram itself reaches users only over a proxy — the script hangs forever, stranding the whole page, including the launch-diagnostic screen meant to surface exactly this failure. This is the likely root cause of the Android "won't open" reports (all launch methods fail identically; iOS on a different network works). Remove the head <script> and load the SDK dynamically (lib/telegram.ts loadTelegramSDK) with a 10s timeout, only on a Telegram entry (the /telegram/ path or a tgWebApp launch fragment). The SPA — served from our own reachable origin — boots first and controls the load: on a block/error it falls through to the diagnostic screen (reporting sdk: no) instead of hanging, and Retry re-attempts. /app/ and the native build no longer touch telegram.org. Pin the SDK to the version the official page recommends (?62) for the newer client features the app already uses (fullscreen, safe areas, swipe guard). Tests: loadTelegramSDK unit tests (present / error / timeout); an e2e that aborts the script fetch and asserts the diagnostic still renders. |
||
|
|
0c5d3808d7 |
feat(ui): diagnostic screen on /telegram/ instead of the landing bounce
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 55s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m22s
A Mini App launch on /telegram/ without sign-in data (empty initData) used to
location.replace('/') — bouncing the visitor to the marketing landing. That
destroyed all diagnosability and, on some Android clients that reach the entry
with no initData, simply looked like the app refusing to open.
Replace the bounce with a compact, privacy-safe launch-error screen
(screens/TelegramLaunchError.svelte) that renders a one-screenshot diagnostic
snapshot captured at the moment of failure: SDK/WebApp presence, Telegram
platform/version, whether initData is empty, whether the URL fragment carried
tgWebAppData, the initData field NAMES present/missing (never the signed
values, never an IP), and OS/mobile/browser via User-Agent Client Hints plus
the full User-Agent. A Share button delivers it through the OS share sheet
(clipboard copy on desktop, reusing the GCG no-webview-strand guard); Retry
re-checks in place to recover a late initData without a reload (which would
discard the launch fragment).
This is the instrument to root-cause the Android empty-initData failure, which
is not reproducible in Playwright.
Tests: collectTelegramDiag + shareText/pickTextShare unit tests; the /telegram/
e2e now asserts the diagnostic screen, not a redirect. Docs: ARCHITECTURE.md +
UI_DESIGN.md updated.
|
||
|
|
5a80696fe8 |
feat(ui): friends list as lobby-style rows with kebab + confirm modals
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 53s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m31s
Settings -> Friends previously rendered each friend as a bordered card with two always-visible text buttons (Remove / Block) that fired immediately. Rework the whole screen to the lobby's visual language: one-line rows split by hairline separators across all three sections (friends, incoming requests, blocked). Each friend row gains a right-hand kebab that slides the row open to reveal two icon actions split by a vertical divider -- block (no-entry) and remove (cross) -- mirroring the lobby's slide-to-reveal. Both actions now require a confirmation modal; since the slide moves a short name off-screen, the modal keeps a generic title and shows the friend's name in the body, above the buttons, so a long name cannot stretch the sheet. Incoming keeps its accept/decline buttons and blocked keeps unblock, inline on their rows. Add the friends.actions / friends.blockConfirm / friends.unfriendConfirm keys to both i18n catalogs and document the flow in FUNCTIONAL (+_ru). |
||
|
|
9253b1bdca |
fix(edge): suppress dead HTTP/3 advert with Alt-Svc: clear
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 17s
CI / ui (pull_request) Successful in 54s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m21s
Caddy enables HTTP/3 by default on any TLS listener and emits Alt-Svc: h3=":443"; ma=2592000, but UDP/443 is never reachable: the prod compose maps only "443:443" (TCP) and ufw opens 443/tcp (test contour: the host caddy publishes only :443/tcp). A client that cached the 30-day advert tries QUIC first on later opens, gets no response, and waits for the QUIC attempt to time out before falling back to h2 -- which surfaced as the Telegram Mini App intermittently hanging on load (a barely-noticeable pause up to a blank window). The h2/TCP serving path itself is healthy (~10ms TTFB). Emit Alt-Svc: clear site-wide at the contour caddy so clients actively drop any cached alternative and stay on h2/h1. This caddy terminates TLS in prod (the fix target); in the test contour it serves plain :80 and the host caddy re-stamps its own Alt-Svc, so the live test fix lives in the host caddy. Add docs/EDGE_HTTP3.md (symptom, diagnosis method, verify, and option B -- serving h3 for real -- if it recurs) and link it from ARCHITECTURE.md. |
||
|
|
81b716569f |
fix(i18n): reconcile preferred_language to the interface locale on every adopt
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 16s
CI / ui (pull_request) Successful in 54s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m22s
A user who never changed the language in Settings kept their account at the creation-time preferred_language seed (e.g. en from the Telegram launch language_code) even after switching the device to another language: the UI followed the device (ru) but the ad banner and out-of-app push — both resolved server-side from preferred_language — stayed en. The on-adopt reconcile was gated on an explicit local choice (localeLocked), so a system-guess locale was never pushed through. Reconcile preferred_language to the active interface locale (app.locale) on every session adopt and link, regardless of how the locale was chosen; persistLanguageToServer already self-gates (a no-op for guests and when already equal), so there is no steady-state write. The banner and push are the only server-rendered language surfaces and both read preferred_language, so this keeps the whole interface consistent — not just the banner. Drop the now-dead localeLocked flag (the reconcile guards were its only readers; the saved prefs.locale still restores the UI choice per device). Trade-off: preferred_language now follows the most-recently-opened device, so an explicit choice on one device can be overwritten by a system guess on another (the "explicit" mark is local, per-device); making it globally sticky would need a DB flag. Docs: ARCHITECTURE §4 + the profile field. |
||
|
|
ef2c2d1eb9 |
feat(account): seed the time zone from the client's detected offset at creation
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Successful in 54s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m18s
A new account's time_zone defaulted to 'UTC' until the player saved a profile, so the robot's sleep window and the turn-timeout away-window sweeper — both anchored to the account zone via account.ResolveZone — ran on UTC for every fresh player, skewing robot-game timing until a manual Settings save. Seed the zone at creation instead, from the client's detected "±HH:MM" offset. - Carry browser_tz on the three account-creating auth requests (TelegramLoginRequest, GuestLoginRequest, EmailRequestRequest — the email account is provisioned at the code-request step, not at login) through the fbs envelope (+ Go/TS codegen), the gateway transcode + backend client, and the backend auth handlers into ProvisionTelegram / ProvisionGuest / ProvisionEmail. - create() now writes time_zone explicitly: the validated detected offset, or 'UTC' (equal to the column default) when absent or malformed — deterministic, never guessed. The column is already NOT NULL DEFAULT 'UTC', so no migration is needed and existing accounts keep 'UTC'. An existing account is never overwritten on re-login. - A detected zero offset is stored as "+00:00" (the zone is known and equals UTC), distinct from the "UTC" default that means "unknown" — which the feedback console's three-zone Filed display already reflects. - Guard the guest handler against an empty payload (the bootstrap historically carried none) so it degrades to no-seed rather than panicking in GetRootAs*. - Tests: zone seeding across Telegram/guest/email plus the "+00:00"/malformed/empty cases and the not-overwrite rule; codec round-trip for the three auth encoders. ARCHITECTURE + FUNCTIONAL(+ru) updated. |
||
|
|
004aca4e97 |
feat(feedback): capture the browser UTC offset; Filed time in three zones
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 55s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m20s
The account time zone defaults to UTC until a player saves a profile, so a report's Filed time could only render in UTC even for a player clearly in another zone. Capture the client's detected "±HH:MM" offset (browser_tz) with each submission and show the Filed time in three zones in the operator console — UTC, the browser offset detected at submit, and the sender's saved profile zone — each shown "N/A" when not known, so the operator can tell what is certainly known from what is merely defaulted. - Thread browser_tz through the fbs envelope (+ Go/TS codegen), the gateway transcode + backend client, and the backend feedback service/store; add the column via migration 00004 (additive, image-rollback-safe). - Fix fmtTimeIn to resolve "±HH:MM" offsets via account.ResolveZone; time.LoadLocation alone silently fell back to UTC for offset zones, which is the second reason a "+02:00" sender showed only UTC. - Update ARCHITECTURE/FUNCTIONAL(+ru) docs and the feedback integration test. |
||
|
|
b78ce42922 |
feat(feedback): capture the app version; show version + local Filed time
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 16s
CI / ui (pull_request) Successful in 54s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
Each feedback submission now carries the client app version (__APP_VERSION__), snapshotted like the interface language: FlatBuffers FeedbackSubmitRequest gains a version field → gateway transcode → backend, persisted in a new nullable feedback_messages.app_version column (migration 00003, additive so an image rollback stays DB-safe). The operator console detail shows the app version and renders the Filed time in UTC plus the sender's time zone (fmtTimeIn). Touches: fbs schema + regenerated Go/TS codegen, codec + transport (the client attaches its build), gateway transcode + backendclient, feedback store/service, admin view + template, docs (ARCHITECTURE §15, FUNCTIONAL + _ru). Verified: feedback integration tests (migration + version round-trip), codec round-trip, check/unit/build green. |
||
|
|
e3e4cedc77 |
feat(ui): Erudit blank tiles carry the star (✻) mark
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 54s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
The Erudit variant's blank is the "звёздочка", so render it with a star. An empty rack blank (and its drag ghost) shows ✻ centred; a placed blank keeps its designated letter and carries ✻ where the (absent) point value sits — on the board and in the Stats best-move tiles. The Scrabble variants are unchanged. Gated by usesStarBlank() in lib/variants.ts. |
||
|
|
48b06f4594 |
docs: finalize documentation to the production state
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 16s
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m21s
The project is live in production, so the staged-development scaffolding is removed. - Delete the staged trackers PLAN.md and PRERELEASE.md. - Rewrite CLAUDE.md: drop the per-stage workflow; codify the ongoing development principles (How we work) and the production model (Branching, CI & production): manual prod-deploy / prod-rollback, semver release tags, Ansible provisioning, expand-contract migrations. - De-stage the living docs (README, ARCHITECTURE, TESTING, deploy/ansible, loadtest, platform/telegram READMEs) and the docker-compose tuning comments: drop the Stage N / R1-R7 / pre-release labels, keep every number and rationale, and fix the now-dangling PLAN.md / PRERELEASE.md references to describe the current state. - Reword stale 'later stage' Go doc comments for subsystems that have shipped. |
||
|
|
c59e522732 |
feat(deploy): visible prod-deploy jobs + manual prod-rollback
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 16s
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m23s
- prod-deploy.yaml is now four visible sequential jobs (build -> deploy-main -> deploy-bot -> verify) so the rollout stages show in the Actions UI; the per-service rolling stays in the deploy-main log. - prod-rollback.yaml: a separate manual workflow_dispatch. Leave target_version blank to roll back to the previous deployed version (the host now tracks DEPLOYED_TAG + PREVIOUS_TAG), or pick a release tag. Re-deploys an already published image rolling + health-gated, image-only (no rebuild, no DB migration). - prod-deploy.sh tracks the previous tag (commit_tag) for the blank-input rollback. - Docs: ARCHITECTURE §13 + deploy/README runbook cover versioning + rollback. |
||
|
|
3d9ba3ac3d |
docs(deploy): bake Stage 18 prod-deploy decisions into the live docs
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 16s
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
- ARCHITECTURE §13 prod bullet -> the realized mechanism: registry transport, two-host, rolling + auto-rollback, migration maintenance window, node_exporter, the undersized launch; the contour paragraph notes node_exporter + the telegram-local profile. - deploy/README gains a prod rollout runbook (how to run, migrations/restore, cert rotation, sizing/monitoring, the full PROD_ set) + node_exporter row, the telegram-local profile note, and the soft AWG_CONF note. - PLAN Stage 18 records the resolved open details and the remaining live cutover (pending erudit-game.ru DNS); the tracker reads 'machinery built; cutover pending DNS'. - PRERELEASE TX/AG note the prod wiring is built. |
||
|
|
e336638ca8 |
fix(ui): retry Mini App launch on backend failure; hide account linking
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
Inside Telegram, a failed initData authentication (e.g. the backend down during a deploy) dropped the user onto the web login screen — the /app/ experience, which has no place inside the Mini App. bootstrap now retries the launch a few times in silence and then renders a dedicated boot-error screen with a Retry button (new BootError.svelte, app.bootError), never falling back to the web sign-in. A blocked account is still terminal and goes straight to the blocked screen. The profile "Link an account" section (email + Telegram link) is hidden while sign-in is provider-only; the anonymous /app/ guest whose upgrade path this is comes later. The flow is kept wired (`hidden` on .emailbox) and its two e2e specs are skipped, both to be re-enabled together. Adds i18n boot.* copy (en/ru), a mock authTelegram failure hook plus an e2e covering the retry screen, and bakes both behaviours into FUNCTIONAL(.md/_ru). |
||
|
|
e2771826fd |
perf(gateway): pool backend conns; loadtest evaluate hot path
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m5s
The loadtest harness never modelled game.evaluate — the debounced per-tile play preview a real client fires several times per turn, the hottest gameplay call. Model it (one evaluate per placed tile + reconsideration re-previews + draft.save, human-paced; --eval / --eval-recon toggle it). That realistic load surfaced the real bottleneck: the gateway's backend HTTP client used the default transport (MaxIdleConnsPerHost=2), so every sync call to the single backend host churned a fresh TCP connection — ~26500 TIME_WAIT sockets at 500 players (near the ephemeral-port ceiling), burning ~1.75 gateway cores while the backend sat near-idle. It was the unfixed root of the residual transport_error the earlier passes chased on the client side. Widen the keep-alive pool (backendMaxIdleConns=512, ~2x the observed 225-conn peak). At 500 players the churn collapses to ~0 and peak gateway CPU drops ~7x (~1.75 -> ~0.26 cores); postgres (~1.65 cores) becomes the busiest service. This overturns the earlier "gateway is the binding constraint, scale it horizontally" sizing — that was sizing around this bug, not a real floor. Consolidate the loadtest trip reports into one loadtest/REPORT.md (drop the R2/R7 split) and bake the finding into README / PRERELEASE / ARCHITECTURE / TESTING. |
||
|
|
1ba789a1f1 |
docs(telegram): invert chat-gate strategy in docs; tune logs; i18n text
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m15s
- Bake the final default-allow + mute-the-ineligible strategy into docs/ARCHITECTURE.md, docs/FUNCTIONAL.md (+_ru), platform/telegram/README.md, the deploy compose comment and the PRERELEASE tracker. The live test proved a per-user grant cannot exceed a deny-by-default group (Telegram intersects the chat default with the per-user permission), so the chat allows sending by default and the bot restricts the ineligible instead of granting the eligible. - Lower the per-event chat_member trace and eligibility evaluation to Debug; keep the actual mute/unmute actions, the startup self-check and warnings at Info, so prod logs only what the bot did. - Update game.searchingForOpponent (Searching -> Waiting for opponent / Поиск -> Ждём соперника) and the quickmatch e2e assertions to match. |