28afcff5516bc5ea41c19a31d7c1a7733cbf6fcb
783 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
68ecd881d9 |
feat(offline): offline-mode state, Settings toggle + blue chrome (Phase C)
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 1m9s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
The deliberate offline mode is now visible: an installed web-PWA user with a confirmed email can switch to offline in Settings, and the header turns blue with an "Offline" chip. (Network gating, the dict preload, the offline lobby + local-game creation, and the service-worker precache follow in later Phase C steps.) - offline.ts / offline.svelte.ts: a sticky, device-scoped reactive offline flag (offlineMode), distinct from connection.svelte's transient reachability signal, with the pure persistence + readiness helpers (offlineReady / missingDicts) unit-tested. - Settings.svelte: an Online / Offline toggle, shown only for an installed web PWA with a confirmed email (the SW-launch + durable-account preconditions). - Header.svelte: a blue-tinted nav (color-mix from the accent, so it tracks light/dark and a Telegram theme override) + an "Offline" chip while offline; the deliberate mode suppresses the transient "Connecting…" indicator. Behaviour-preserving online (the toggle is hidden and offlineMode is false there; e2e 196 green). App entry stays within its size budget (111.6 / 112 KB gzip). |
||
|
|
d80d28a402 |
Merge pull request 'feat(offline): wire the local game source into the game screen (Phase B3.2)' (#194) from feature/offline-game-seam-wire into development
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 1m8s
CI / conformance (push) Successful in 10s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m39s
|
||
|
|
1654131904 |
feat(offline): wire the local game source into the game screen (Phase B3.2)
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 1m9s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m40s
The game screen now drives a local vs_ai game through the offline engine, dispatched by game id — completing the playable local game (on top of the source, #193). Online play is unchanged. - gamesource.ts: gameSource(id) returns the local source for a `local:` id, else the gateway (the same game-loop interface). The offline engine stays OUT of the app entry bundle — it is dynamically imported on first use (a separate chunk), so online-only users never pay for it (the app entry stays within its size budget). - localgame/id.ts: the tiny id helper (no engine imports) the dispatcher branches on. - Game.svelte: the game-loop calls (state/history/submit/pass/exchange/resign/hint/ evaluate/draft) go through gameSource(id) instead of the gateway directly; a local game's robot-reply events route through the same app event hub the network stream feeds, so the screen reacts to opponent_moved / game_over identically. Behaviour-preserving for network games (gameSource returns the gateway for them). Local verify green: check + test:unit + build + bundle-size gate + e2e (196 passed). |
||
|
|
9ecbe480db |
Merge pull request 'feat(offline): local game source (Phase B3.1)' (#193) from feature/offline-game-seam into development
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 10s
CI / integration (push) Successful in 17s
CI / ui (push) Successful in 1m9s
CI / conformance (push) Successful in 10s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m40s
|
||
|
|
32298595f2 |
feat(offline): local game source (Phase B3.1)
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 1m9s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
A GatewayClient-shaped facade over the offline engine, so the same game screen can drive a local vs_ai game with no backend (the wiring into Game.svelte is B3.2). - source.ts: LocalSource implements the game-loop subset (GameLoopSource) for a local game id — gameState/gameHistory via replay, submitPlay/pass/exchange/resign apply the human move then run the robot (decide(generateMoves)) synchronously, persisting both and delivering the robot's move through a per-game event emitter (no live stream). hint is gated to >30 min since the robot's last move; evaluate/checkWord are local. It translates the UI's glyph space to the engine's index space with the static letters table. - ruleset.ts: add the static per-variant letters (glyphs), pinned to the Go alphabet — offline is now fully self-contained (no reliance on a warm server alphabet cache). - engine.ts: submitPlay (infers the direction like the server SubmitPlay), evaluatePlay + dictionaryHas for the move preview / word check, and record the main-word coordinate + the words on a play (for the history MoveRecord). - source.test.ts: create -> human pass -> synchronous robot reply via the event, the hint gate, decoded history, and a whole game driven to completion. Pure additive library code; no runtime behavior change (bundle unchanged). |
||
|
|
2847412b5b |
Merge pull request 'feat(offline): local game persistence + replay (Phase B2)' (#192) from feature/offline-localgame-store into development
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 1m8s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m40s
|
||
|
|
4fa77bf82c |
feat(offline): local game persistence + replay (Phase B2)
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m8s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m41s
Store an offline game durably and reconstruct it — the offline counterpart of the server's replay rehydration. Builds on the engine (#191); not wired into the UI yet (Phase B3). - serialize.ts: a LocalGameRecord (seed + rules + seat metadata + the alphabet-index move journal) and replayGame() — reconstruct a live LocalGame by seeding a fresh engine identically and replaying the journal. The board/bag/racks are not stored; they are deterministic from the seed and the replayed operations, so the record stays small. The journal is dictionary-independent (alphabet-index space, stable per variant). - store.ts: an IndexedDB store for local games (save/get/list/delete), mirroring lib/dict/store.ts — its own database, best-effort, guarded when IndexedDB is absent. - engine.ts: record the swapped tiles on an exchange (needed for exact replay) and expose the game's rule config. - serialize.test.ts: a round-trip — reconstruct a mid-game and a finished game by replay and assert the state (board/racks/bag/scores/turn/log) is identical. Pure additive library code; no runtime behavior change (bundle unchanged). |
||
|
|
afa44d41b4 |
Merge pull request 'feat(offline): local game engine (Phase B1)' (#191) from feature/offline-localgame-engine into development
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 9s
CI / integration (push) Successful in 14s
CI / ui (push) Successful in 1m8s
CI / conformance (push) Successful in 10s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m39s
|
||
|
|
e4cf143e9f |
feat(offline): local game engine (Phase B1)
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m9s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
The offline vs_ai game engine — a faithful TS port of backend/internal/engine that drives a whole local game with no backend. Composes with the move generator (#188) and robot strategy (#189) from Phase A; not yet wired into the UI (Phase B2/B3). - ui/src/lib/localgame/ruleset.ts: static per-variant tile values / bag counts / blank count, mirrored from rules.go (offline scoring is self-contained; online uses the server alphabet). Pinned by ruleset.parity.test.ts against a Go fixture. - bag.ts: the tile bag (fill from counts/blanks, draw-from-end, return+reshuffle) on a deterministic in-house PRNG — a game replays from its seed, not bit-identical to a server game (per plan). - board.ts: the mutable board, satisfying the validator/generator read view + set(). - engine.ts: LocalGame — deal / play (reusing validate.ts) / pass / exchange / resign, scoreless(6) & out-of-tiles end detection, end-of-game rack penalties, winner; mirrors game.go. The end-game math is exported as pure functions, pinned against the Go engine (engine.parity.test.ts, 9 constructed positions). - engine.test.ts: a full-loop smoke — two robots play a whole vs_ai game to completion via generateMoves + decide, and it is reproducible from the seed. - backend: movegen now dumps the per-variant rulesets; a new in-package engine emitter (endfixture_test.go, env-gated) produces the end-game golden. Pure additive library code; no runtime behavior change (unused at runtime, bundle unchanged). |
||
|
|
543cbe56b9 |
Merge pull request 'test(offline): real-dictionary move-generator conformance in CI' (#190) from feature/offline-realdict-conformance into development
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 10s
CI / integration (push) Successful in 21s
CI / ui (push) Successful in 1m9s
CI / conformance (push) Successful in 10s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m36s
|
||
|
|
a9c8f1ecfe |
test(offline): real-dictionary move-generator conformance in CI
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m8s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m38s
Phase A (A4): prove the ported move generator (#188) against the FULL shipped dictionaries, not just the tiny samples — the deep graphs and complete 26/33-letter alphabets the samples cannot reach. - backend/cmd/movegen: add a -dawg-dir mode that emits per-variant golden move-gen vectors from the real dawgs (a bounded first-move + a blank case + a deep 7-tile mid-game position). Regenerated in CI to /tmp, never committed (like the dictgen/validategen vectors), so no dictionary version is pinned into the repo. - ui/src/lib/dict/generate.realparity.test.ts: env-gated (DICT_DAWG_DIR + DICT_MOVEGEN_DIR) parity against that golden — 9 positions across scrabble_en / scrabble_ru / erudit_ru match the Go solver exactly. Skips cleanly when unset. - .gitea/workflows/ci.yaml: the conformance job now generates the movegen golden and points the gated vitest at it (DICT_MOVEGEN_DIR). |
||
|
|
d694ead7b6 |
Merge pull request 'feat(offline): robot move-choice strategy in TS (parity-pinned)' (#189) from feature/offline-robot-strategy into development
CI / changes (push) Successful in 1s
CI / unit (push) Successful in 10s
CI / integration (push) Successful in 15s
CI / ui (push) Successful in 1m8s
CI / conformance (push) Successful in 10s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m42s
|
||
|
|
8c5995c076 |
feat(offline): port robot move-choice strategy to TS (parity-pinned)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m9s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m37s
Phase A (2/2) of PWA offline mode: the offline robot picks its move exactly as the server does, so a local vs_ai game plays the same. Builds on the move generator from #188; not wired into a game loop yet (Phase B). - ui/src/lib/robot/strategy.ts: port of backend/internal/robot/strategy.go's move-choice slice — mix (FNV-1a, via BigInt for bit-exact uint64), playToWin (~40% play-to-win), deviates (the fading off-strategy wobble) and selectMove (pick the candidate whose resulting margin lands closest to the +/-[1,30] band, conservative tie-break), composed by decide(). The generator's ranked moves feed straight in. Think-time/sleep/nudge scheduling is server-only and not ported. - backend/internal/robot/strategyfixture_test.go: an in-package, env-gated emitter (EMIT_STRATEGY_FIXTURES=1) writing golden fixtures from the real Go strategy — it reaches the unexported mix/playToWin/deviates/selectMove. - strategy.parity.test.ts: 21 mix + 56 decision cases match Go exactly (play/ exchange/pass, the deviate flip, tie-break, band overshoot). Pure additive library code; no runtime behavior change (unused at runtime, so the bundle is unchanged). |
||
|
|
cedc9ffae1 |
Merge pull request 'feat(offline): DAWG cursor + move generator in TS (parity-pinned)' (#188) from feature/offline-movegen into development
CI / changes (push) Successful in 1s
CI / unit (push) Successful in 10s
CI / integration (push) Successful in 15s
CI / ui (push) Successful in 1m9s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 2m32s
|
||
|
|
c334a9d7b7 |
feat(offline): port DAWG cursor + move generator to TS (parity-pinned)
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m8s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m38s
First engine-first step of PWA offline mode (Phase A): the client-side move generator — the "robot brain" a local vs_ai game will run on-device — with no runtime wiring yet (Phase B). - dawg.ts: add the step-by-step cursor (root/final/next/arcs), a faithful port of dafsa traverse.go over the reader's existing bitstream. - generate.ts: the Appel-Jacobson generator (leftPart/extendRight + cross-sets + counts-rack + board transpose + moveKey ranking), reusing the cursor and validate.ts evaluate/connected. A cross-set LetterSet is a Uint8Array, so the 33-letter Russian alphabet (index 32) is exact under JS bit ops. - validate.ts: export connected for the generator's connectivity filter. - backend/cmd/movegen: dev tool building small sample dictionaries and emitting golden move-generation fixtures from the real Go solver (EN + RU). - tests: dawg.cursor.test.ts (enumeration bijection vs indexOf) and generate.parity.test.ts (7/7 vs the Go solver: empty board, mid-game, blank, single-word rule, Russian index-32 cross-set). The committed EN sample also unblocks the existing skipped dawg.parity.test.ts once wired with DICT_* in CI. Pure additive library code; no runtime behavior change. |
||
|
|
3a85f64726 |
Merge pull request 'fix(metrics): second-scale buckets for edge_request_duration' (#186) from fix/edge-latency-histogram-buckets into development
CI / gate (push) Successful in 0s
CI / changes (pull_request) Successful in 1s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m8s
CI / conformance (pull_request) Successful in 9s
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 11s
CI / integration (push) Successful in 14s
CI / ui (push) Has been skipped
CI / conformance (push) Successful in 10s
CI / deploy (push) Successful in 1m46s
CI / unit (pull_request) Successful in 10s
CI / deploy (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
|
||
|
|
00bb66ba0f |
fix(metrics): second-scale buckets for edge_request_duration
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) Has been skipped
CI / conformance (pull_request) Successful in 8s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m44s
The edge request-latency histogram records seconds but used the OTel SDK's default millisecond-calibrated bucket boundaries (first boundary 5), so every sub-5s request fell into one bucket and histogram_quantile(0.99) interpolated to ~4.95s for every message type — regardless of the real (millisecond) latency. That tripped the >1s "Gateway request latency p99 high" alert on essentially every request, flapping fire/resolve on each app open (seen on the test and prod contours). Set explicit second-scale bucket boundaries (0.005 … 10s) straddling the 1s SLO so the p99 reflects real latency and the alert fires only on genuine slowness. Regression test asserts the histogram carries a sub-second boundary. |
||
|
|
a2c79c149a |
Merge pull request 'feat(email): PWA login sends code only; drop admin link from alert emails' (#185) from feature/pwa-login-code-only into development
CI / changes (push) Successful in 1s
CI / unit (push) Successful in 10s
CI / integration (push) Successful in 18s
CI / ui (push) Successful in 1m9s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m50s
|
||
|
|
061366da5a |
feat(email): PWA login sends code only; drop admin link from alert emails
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m8s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m40s
Two email changes, per the owner: 1. Code-only login from an installed PWA. A login requested while running as a standalone PWA now omits the one-tap confirm link from the email — the link would open in a separate browser whose minted session cannot reach the PWA, stranding the login. The code is typed in the same window instead. The client sends a `pwa` flag (isStandalone) on the email-code request (a new FBS field, threaded through the gateway); the backend omits the deeplink when it is set, reusing the existing deletion-code link-omission path. Non-PWA browser logins keep the one-tap link. No polling, no migration. 2. Security: the operator alert digest no longer embeds the admin-console (/_gm) URL. An admin link must never travel in an email, where a mail provider could cache or index it; the operator opens the console directly. Tests: an inttest asserting the PWA login email omits the link (and a browser one keeps it); a codec round-trip of the new pwa field; the alert-digest test flipped to guard the admin link is absent. Docs: ARCHITECTURE + FUNCTIONAL (+ru). |
||
|
|
d19609f87d |
Merge pull request 'feat(pwa): installable web app + landing web entry' (#184) from feature/pwa-install-cta into development
CI / changes (push) Successful in 3s
CI / unit (push) Successful in 10s
CI / integration (push) Successful in 15s
CI / ui (push) Successful in 1m8s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 2m40s
|
||
|
|
51147a1429 |
feat(pwa): installable web app + landing web entry
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Successful in 1m8s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m43s
Make the web SPA an installable PWA and surface it to users: - manifest.webmanifest + an install-only service worker + 192/512/maskable icons (ui/public); PWA head tags in index.html; the .webmanifest MIME type registered in the gateway (the distroless image has no /etc/mime.types). - Platform-adaptive install CTA (components/InstallApp.svelte + lib/pwa): one-tap on Chromium, manual Add-to-Home-Screen instructions on iOS Safari, hidden elsewhere / once installed / inside a Mini App. Shown under the logged-out login card and at the bottom of Settings. - The landing gains a third entry linking /app/ (the brand tile), with a caption under all three (Telegram / VK / Веб-версия). The service worker is navigation-only (network-first, cached-shell fallback); hashed assets and the Connect stream are untouched. It exists to satisfy Chromium's installability requirement and is the single growth point for a future opt-in offline mode. Tests: pwa.ts unit tests; a webui probe (manifest/sw.js content-type + the SPA-fallback-to-HTML trap); e2e for the one-tap CTA, the iOS instructions modal and the landing web entry. Docs: ARCHITECTURE §13, FUNCTIONAL (+ru), gateway README. |
||
|
|
d61ba68e9b |
Merge pull request 'feat(banner): per-campaign colour overrides and urgent alerts' (#182) from feature/banner-colors-urgent into development
CI / integration (push) Successful in 15s
CI / deploy (push) Successful in 1m57s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / conformance (pull_request) Successful in 8s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Has been skipped
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 10s
CI / ui (push) Successful in 1m8s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / changes (pull_request) Successful in 2s
CI / ui (pull_request) Successful in 1m8s
|
||
|
|
6db9178449 |
feat(banner): per-campaign colour overrides and urgent alerts
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m8s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m50s
Non-default campaigns gain an optional colour override (background / text / link) in two sets — one for every theme, one for the dark theme only — and an "urgent" flag. - Colours ride profile.get as six trailing FlatBuffers strings on BannerCampaign (backward-compatible). The client resolves the cascade (dark <- dark ?? all, light <- all) per rendered theme and derives the strip border from the background in JS (no CSS color-mix, for the old Android WebView floor); AdBanner applies them as inline vars scoped to the strip. - Urgent is resolved entirely server-side: while any enabled, in-window urgent campaign exists, computeActiveSet returns only the urgent campaigns and bannerFor skips the eligibility gate — so a system notice reaches every viewer (paid / hint-holding / no_banner included) and preempts the ordinary feed. No wire field; it appears on each viewer's next profile.get. - Admin console (/_gm/banners): native colour pickers + a live light/dark preview of the strip, and an urgent toggle. The default campaign stays plain, enforced by the service and a DB CHECK. Migration 00009 is additive (nullable colour columns + a bool default + all-or-nothing / hex / default-plain CHECKs) — expand-contract, rollback-safe. Docs: ARCHITECTURE §10, UI_DESIGN, FUNCTIONAL (+ru). Tests: ads unit (urgent preempt + colour validation), codec + resolver unit, gateway transcode, and integration (colour round-trip + urgent bypass against real Postgres). |
||
|
|
ac383880b7 |
Merge pull request 'fix(ui): hold info toast ~1s before it rises and fades' (#181) from feature/toast-hold-before-fade into development
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 1m7s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m44s
|
||
|
|
16349f5ad9 |
Merge pull request 'fix(ui): poll for out-of-band email confirmation' (#180) from feature/email-confirm-poll-fallback into development
CI / changes (push) Successful in 1s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 1m8s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m45s
|
||
|
|
101a3f118c |
Merge pull request 'feat(ui): hide current-host sign-in row in profile' (#179) from feature/hide-current-host-signin-row into development
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 1m4s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 2m33s
|
||
|
|
400b6ac2a5 |
fix(ui): hold info toast ~1s before it rises and fades
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 1m8s
CI / conformance (pull_request) Successful in 8s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
The info toast began drifting up and fading the instant it finished appearing (the CSS keyframes went straight from the 12% appeared-stop to the 100% risen-and-faded stop), so a glanced message was already leaving. Insert a ~1s hold at full opacity/rest before the rise-and-fade: extend the animation 2s → 3s with keyframe stops at 8% (appeared, ~240ms) and 41% (end of the ~1s hold), keeping the original rise-and-fade pace for the tail. The reduced-motion variant gets the same appear/hold/fade timing (fade only, no travel). The showToast dismissal timer is bumped 2000 → 3000ms to stay in lockstep with the animation (the error toast's 4s dwell is unchanged). |
||
|
|
fb7490f1df |
fix(ui): poll for out-of-band email confirmation
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 1m8s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
An email link/code can be confirmed out of band: the recipient taps the one-tap link in the email, which confirms in another browser/session. The backend already publishes a `notify` `profile` re-fetch signal for this (handlers_auth.go handleEmailConfirmLink), and the client re-fetches on it. But the live stream is single-shot with no replay: a Mini App backgrounded while the user is in their mail app tapping the link drops the stream and misses the event (the gateway hub has no subscriber to deliver to), and the reconnect on foreground does not re-sync — so the open code form stayed until a manual reload. On Telegram Desktop the app is never backgrounded, so the push works and there was no bug. Add a client-side fallback: while an add-email confirmation is pending (a code was sent, no email yet), poll `profile.get` on a 4s interval and on foreground regain until the address lands; the effect stops as soon as the email appears. The live push still updates instantly when foregrounded — this only covers the backgrounded-miss gap. Tests: a mock e2e attaches the email WITHOUT emitting a live event (new window.__mock.clearEmail / confirmEmailOutOfBand seams), so it exercises the poll, not the push, and asserts the code form collapses into the email row. Docs: ARCHITECTURE.md §10 notes the single-shot gap + the poll fallback. |
||
|
|
c1805e5b7c |
feat(ui): hide current-host sign-in row in profile
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 1m6s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m40s
Inside a Telegram/VK Mini App the host provider is auto-linked. Once the player also linked an email, `canUnlink` turned true and the "Unlink" control appeared on the host platform's own row — letting them unlink the very platform they are signed in through, which is meaningless. Gate the Telegram row on `!insideTelegram()` and the VK row on `!insideVK()`, reusing the runtime host detectors that already gate the "link" buttons. Symmetric: inside TG only the TG row is hidden (the VK row still shows, since VK is not the current host), and vice versa. The web and native builds are unchanged (both detectors are false there); the backend is untouched — this is a UI display gate, and `linkUnlink` still refuses to remove the last identity. Docs: FUNCTIONAL.md (+_ru mirror). |
||
|
|
fcedadcb5b |
Merge pull request 'feat(gateway): unsupported-engine telemetry beacon + Grafana counter' (#177) from feature/unsupported-engine-telemetry into development
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 10s
CI / integration (push) Successful in 19s
CI / ui (push) Successful in 1m6s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m44s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m5s
CI / conformance (pull_request) Successful in 9s
CI / deploy (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
|
||
|
|
2feb638329 |
feat(gateway): unsupported-engine telemetry beacon + Grafana counter
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Successful in 1m5s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m40s
The index.html boot guard now fires a fire-and-forget beacon (POST /telemetry/unsupported)
when it turns a client away on the unsupported-engine screen, so the owner can see — on the
Users dashboard beside "app opens" — how many real clients hit it and on which engines.
- Client: navigator.sendBeacon (fetch fallback) with a localStorage dedup keyed by app version
+ reason + Chromium, so a user reopening the app is one report, not ten.
- Gateway: a new unauthenticated POST /telemetry/unsupported handler (the client never booted,
so it carries no session), per-IP public-limited and body-capped, mirroring the export-download
route. It folds the beacon into the OTel counter unsupported_engine_total {reason =
no_bigint/no_proxy/boot_error/other, chromium}, with reason allow-listed and the Chromium major
reduced to a bounded range (normalizeUnsupported) so a spoofed beacon cannot inflate the metric
cardinality; the full user agent is logged, not labelled.
- Caddy: /telemetry/* added to the @gateway matcher (else it falls to the landing catch-all).
- Grafana: two panels on the Scrabble — Users dashboard (by reason, by Chromium major).
- Docs: ARCHITECTURE.md §11.
Tests: recordUnsupportedEngine (metric split), normalizeUnsupported (cardinality bounding), and
the handler route end to end (204 / 405 / counter). go build+vet+test and gofmt clean; ui
check/build/e2e green; the client beacon + dedup verified against a forced hard-gate (BigInt
removed) — one POST, deduped on reopen, correct reason/chromium/version labels.
|
||
|
|
4dfedd02a3 |
feat(ui): support old Android in-app WebViews (es2019 + core-js + engine screen)
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 1m5s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m54s
Old Android System WebViews (the Telegram/VK in-app browser; a real device with Google Play auto-updates the WebView, but emulators / no-Play / restricted devices stay frozen) showed a white screen. Two causes, fixed in order: - build.target es2022 shipped ?./?? verbatim, which the old engine cannot parse. Lowered to es2019 so esbuild down-levels the syntax. - The es2020+ runtime globals the bundle and its deps call (globalThis, structuredClone, Array.at, ...) are missing on those engines. A conditional core-js loader (emitPolyfills writes dist/polyfills.js, document.write'd by an index.html gate only when needed) covers them; modern engines download nothing, so the bundle-size budget is untouched. BigInt (the 64-bit FlatBuffers timestamp decode) and Proxy (Svelte 5 runes) cannot be polyfilled, so the effective floor is Chrome 67. Below it, a permanent ES5 boot guard in index.html shows a friendly "this device's OS or browser can't run the app" screen (with the web-version link inside a Mini App) and a "Diagnostic information" view with a Copy button, instead of a white screen. A reactive net raises the same screen on an uncaught boot error that never signals window.__booted. Board tile glyphs used container-query units (cqw, Chrome 105+) under .cell font-size:0, so they collapsed to 0px on Chrome 74 (invisible letters); added vmin fallbacks. The unsupported-engine telemetry beacon is a separate follow-up PR. |
||
|
|
44117e906c |
Merge pull request 'fix(ci): prod build needs EXPORT_SIGN_KEY for compose interpolation' (#174) from fix/prod-build-export-sign-key into development
CI / changes (push) Successful in 2s
CI / changes (pull_request) Successful in 1s
CI / unit (push) Successful in 10s
CI / integration (push) Successful in 18s
CI / ui (push) Successful in 1m5s
CI / conformance (push) Successful in 9s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m5s
CI / conformance (pull_request) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m40s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Has been skipped
|
||
|
|
c90331b189 |
fix(ci): prod build needs EXPORT_SIGN_KEY for compose interpolation
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 1m5s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m44s
The first v1.8.0 prod-deploy build job failed: `docker compose build` interpolates the WHOLE compose file, and the backend's :?-guarded EXPORT_SIGN_KEY — added with the finished-game export after the last prod release (v1.7.0), so the prod build never exercised it — was absent from the build job's env, tripping the guard before any image built. Prod was untouched (build-only failure: no image pushed, no deploy, no migration). Add EXPORT_SIGN_KEY (audited every :?-guarded compose var against the build job env — it was the only genuinely-missing one; TELEGRAM_MINIAPP_URL is derived in the run step). Verified by reproducing the build-job env + `docker compose config`. |
||
|
|
0dfecc2af7 |
Merge pull request 'fix(ci): arm the maintenance flag on the test-contour deploy' (#172) from fix/maintenance-flag-test-contour into development
CI / deploy (push) Successful in 1m44s
CI / changes (pull_request) Successful in 1s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Has been skipped
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 10s
CI / integration (push) Successful in 16s
CI / ui (push) Successful in 1m6s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 16s
CI / ui (pull_request) Successful in 1m6s
CI / conformance (pull_request) Successful in 8s
|
||
|
|
446ea2ac45 |
fix(ci): arm the maintenance flag on the test-contour deploy
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m5s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m53s
The maintenance overlay never showed on the test contour: only prod-deploy.sh raised
the caddy maintenance flag, so a test redeploy was a bare gateway/backend recreate — the
SPA saw a transient 502 ("Reconnecting…"), never the 503 + X-Scrabble-Maintenance marker
the overlay keys on. So the feature (PR #171) was unverifiable off prod.
Raise the flag around the recreate in ci.yaml's deploy job too, mirroring prod. Ordering
matters because of the config reseed: ci does `rm -rf $conf` + recreate, so the running
caddy sits on the stale pre-reseed bind mount and cannot see a flag written to the new
dir until it is recreated. So: raise the flag, force-recreate caddy FIRST (onto the fresh
mount, where it carries the flag and 503s), then recreate gateway/backend (the window),
then the other config services, then lower it. A trap clears the flag if the step fails,
and the next deploy's reseed wipes a stale one — so the contour can't stick in maintenance.
The caddy-routed probes run in the next step, after the flag is lowered.
Prod was already correct (prod-deploy.sh); this only makes the test contour faithful so the
overlay + reload can be seen there. An open SPA must already be on the PR-#171 bundle (which
carries the overlay code) — reload once to bootstrap onto it.
|
||
|
|
01a9249002 |
Merge pull request 'feat(ui): in-session maintenance overlay on the edge 503 marker' (#171) from feature/maintenance-overlay into development
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 1m5s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 2m6s
|
||
|
|
7dcd62fdd7 |
feat(ui): reload the SPA on maintenance recovery
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 1m6s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m47s
The deploy that ends a maintenance window may ship a client-incompatible change (wire/schema bump), and the in-session bundle is the old one. On recovery, reload to pick up the fresh client instead of just hiding the overlay. Ordering is safe: the edge maintenance flag (deploy/prod-deploy.sh) spans the WHOLE roll and clears only at script exit — after the gateway (which serves the embedded SPA) has rolled — so the edge 503s everything until the entire deploy is live; recovery therefore always serves the new SPA. A window.__maint.recover() hook + e2e cover the reload. |
||
|
|
d67e582c03 |
feat(ui): in-session maintenance overlay on the edge 503 marker
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 1m4s
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
The edge caddy 503 carries X-Scrabble-Maintenance during a deploy window, but a user already in the running SPA only sees their calls start failing — the static caddy page catches only a fresh load. Detect that marker (strictly — not any transient 'unavailable', which the Connecting indicator already covers) on the raw ConnectError at the two transport catch sites (unary exec + the live subscribe stream), before toGatewayError discards the response headers, and raise a non-dismissable dimmed overlay that mirrors the static caddy page. It self-clears: a capped-backoff poll (a cheap read, mirroring connection.svelte.ts) lifts it on the first success, and a manual "retry" button forces an immediate re-check — so it can never get stuck. On detection the read retry loop fails fast, so a window doesn't burn the retry budget on every call. - pure detector maintenance.ts (maintenanceRetryMs / parseRetryAfterMs) + unit tests; the store + self-clearing poll in maintenance.svelte.ts (mirrors connection.svelte.ts) - MaintenanceOverlay.svelte (clones Splash's fixed/inset/dimmed shell, non-dismissable), mounted app-global in App.svelte after Coachmark - transport.ts detects + reports at both catch sites, clears on any successful read - i18n RU "Технические работы" / EN "Under maintenance"; a window.__maint mock hook (the mock can't emit a real 503) + a Playwright spec Prod is same-origin (VITE_GATEWAY_URL empty) so the marker header is readable without a CORS expose-header. Verified: pnpm check (0), unit (402), build, e2e (186, Chromium+WebKit). |
||
|
|
75fe07865a |
Merge pull request 'feat(deploy): prod OOM swap cushion + edge maintenance page' (#170) from feature/prod-hardening into development
CI / changes (push) Successful in 1s
CI / unit (push) Successful in 9s
CI / integration (push) Successful in 15s
CI / ui (push) Successful in 1m6s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m43s
|
||
|
|
597e200f37 |
Merge remote-tracking branch 'origin/development' into feature/prod-hardening
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m4s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 1s
CI / deploy (pull_request) Successful in 1m40s
|
||
|
|
3ef18d33b6 |
Merge pull request 'feat(gateway): enable GATEWAY_HONEYTOKEN + GF_SMTP_ENABLED on both contours' (#169) from feature/enable-honeytoken into development
CI / changes (push) Successful in 1s
CI / unit (push) Successful in 10s
CI / integration (push) Successful in 17s
CI / ui (push) Successful in 1m6s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 2m35s
|
||
|
|
c8601c0115 |
feat(deploy): prod OOM swap cushion + edge maintenance page
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 1m5s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
Two prod-deploy hardening measures (owner-requested): - Swap file (Ansible common role, swap_size=1G, vm.swappiness=10). The per-container memory caps enforce that one service can't eat all RAM, but they overcommit the 1.9 GiB main host (~2.8 GiB of caps), so a simultaneous spike could hit the kernel OOM-killer (and it might pick postgres). A small swap absorbs the overshoot. Idempotent, builtin-only (no ansible.posix). - Edge maintenance page. prod-deploy.sh raises a flag around the rolling swap / migration window that the caddy edge serves a static 503 "технические работы" page from (deploy/caddy/maintenance.html), for the user-facing routes only — /_gm (Grafana) stays reachable. Cleared on any exit (success, health failure + rollback, or error) by a shell trap so it can never stick on. The 503 carries Retry-After + an X-Scrabble-Maintenance marker so a follow-up SPA overlay can tell a planned window apart from a transient error (the static page only catches a fresh load; an in-session user needs the app-side overlay). Not zero-downtime — the single stateful backend still blips — but the window is graceful instead of raw 502s. Verified: caddy validate; the gate 503s every non-/_gm path incl. the Connect/gRPC edge and serves the page + markers; /_gm bypasses; toggling needs no reload (per-request stat). Ansible --syntax-check + compose config (base+prod) pass. |
||
|
|
a0021d1994 |
feat(gateway): wire GATEWAY_HONEYTOKEN through the deploy
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 20s
CI / ui (pull_request) Successful in 1m6s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m44s
The planted honeytoken bearer trap already existed in the gateway + compose, but no workflow fed GATEWAY_HONEYTOKEN, so it was always empty (inert). Wire the per-contour TEST_/PROD_GATEWAY_HONEYTOKEN secret into the ci deploy, prod-deploy and prod-rollback env (the prod path via the shared deploy/write-prod-env.sh), document it (deploy/README + .env.example), and fix the stale compose comment. Empty secret = trap off (no ":?" guard), so a deploy is safe before the operator sets the value + plants the bait. On prod (IP ban on) presenting it earns a 24h ban + alarm; on test (ban off) it logs + a ban metric. GF_SMTP_ENABLED is enabled separately via the TEST_/PROD_GF_SMTP_ENABLED Gitea variables (=true) — no code change. |
||
|
|
4b4dcab9b6 |
Merge pull request 'chore(deploy): dedupe & regroup Gitea CI variables/secrets' (#168) from feature/ci-vars-dedupe into development
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 10s
CI / integration (push) Successful in 15s
CI / ui (push) Successful in 1m4s
CI / conformance (push) Successful in 8s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m41s
|
||
|
|
7f85362288 |
chore(deploy): dedupe & regroup Gitea CI variables/secrets
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 1m4s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m44s
Collapse identical TEST_/PROD_ pairs to single unprefixed Gitea entries, derive the public URLs from PUBLIC_BASE_URL at deploy time, and share the prod env.sh renderer between deploy and rollback. - Collapse to one unprefixed variable: DICT_VERSION, SMTP_RELAY_HOST/PORT/TLS, GRAFANA_SMTP_PORT, VITE_VK_APP_LINK, VITE_VK_APP_ID (one Selectel relay + one pair of VK apps serve every contour). Secrets collapsed by the owner: SMTP_RELAY_USER/PASS, GATEWAY_VK_APP_SECRET, GATEWAY_VK_ID_CLIENT_SECRET. - Derive at deploy from PUBLIC_BASE_URL (no longer stored): TELEGRAM_MINIAPP_URL, GRAFANA_ROOT_URL, VITE_VK_ID_REDIRECT_URL. Removes the prod/test asymmetry and fills the missing test VITE_VK_APP_ID (VK web login was half-configured on test). - Extract deploy/write-prod-env.sh + write-prod-bot-env.sh, shared by prod-deploy and prod-rollback so the two cannot drift: a rollback now re-renders the FULL runtime env (email / VK login / Grafana alerts previously went dark after a rollback) and passes TELEGRAM_SUPPORT_CHAT_ID. - Single-source DICT_VERSION (CI env + both deploys), fix the v1.3.0/v1.3.1 drift in .env.example/README, correct the misleading honeytoken/abuse-ban compose comment, and rewrite the deploy/README variable list (+ the previously undocumented GATEWAY_VK_APP_SECRET and TELEGRAM_SUPPORT_CHAT_ID). The Gitea variables are reworked via the API; the stale TEST_/PROD_ entries are deleted after the test contour goes green. |
||
|
|
72e9b600b0 |
Merge pull request 'fix(account): dedupe colliding identities on merge, journaling to the dossier' (#167) from feature/merge-email-dedupe into development
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 10s
CI / integration (push) Successful in 15s
CI / ui (push) Has been skipped
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m53s
|
||
|
|
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. |
||
|
|
c680e695d3 |
Merge pull request 'feat(account): VK ID web login to link a VK identity from a browser' (#166) from feature/vk-web-link into development
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 10s
CI / integration (push) Successful in 15s
CI / ui (push) Successful in 1m4s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 2m5s
|
||
|
|
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.
|