00d1bd33e37035fe156b6830b6113bb2d0ba85b3
40 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
13be7c3d9a |
feat(ads): VK post-move interstitial + retire deprecated hint_balance/paid_account
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 23s
CI / ui (pull_request) Successful in 1m12s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m53s
Interstitial video after a confirmed play or a hint, VK-only, offline banner-only. The gate is client-mirrored: the backend puts the config cooldowns (global 5m / vs_ai 30m / hint 1m) and a suppressed flag (the no-ads / no_banner gate, same as the banner) on Profile.ads via adsFor; the client self-gates on a per-kind last-shown time in localStorage, with the VITE_ADS_STUB contour "ad fired" toast. Never fires after a pass, exchange or resign. maybeShowInterstitial + vkShowInterstitial; the codec and gateway transcode carry the ads block. Also retires the deprecated accounts.hint_balance / paid_account domain usage (expand-contract, code only — the columns stay for a later DROP so image rollback stays DB-safe): drop the Account fields and their scan, the dead account.SpendHint, account.GrantHints and the admin grant-hints action; the in-game hint display now comes wholly from the payments hint benefit. Banner eligibility and account merge no longer read the legacy flags. Tests: ads.test.ts (the client-mirrored gate), the codec ads round-trip, the gateway ads transcode test, and a profile ads-config integration test (cooldowns + suppressed under no-ads / no_banner). Docs: PAYMENTS §10 (+ru) interstitial, the decision amends, backend README, the plan. |
||
|
|
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). |
||
|
|
4cc37e5760 |
fix(admin): unified user search across live + deleted, incl. the retention journal
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 1m6s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m2s
The email/name/external-id search was scoped to one tab and only looked in the live identities table, so a deleted account (whose credentials moved to retained_identities on deletion) could not be found by the email/id it held. Now a people search spans live and deleted accounts in one query (robots only on the Robots tab) and also matches the retention journal and the retained real name; results carry a 'deleted' badge. Integration test: a deleted account is found by its held email and external id. |
||
|
|
3faca690dd |
fix(delete): review follow-ups — admin Deleted filter, guest gate, dialog spacing
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 1m5s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m46s
- Admin /users gains a Deleted scope (between People and Robots); every other scope now hides tombstoned accounts (deleted_at filter in UserFilter). - Admin delete-user is offered for any non-deleted account (drop the not-guest gate, so a stale is_guest account can still be tombstoned). - Harden EmailService.ConfirmCode to ClearGuest — defence-in-depth so no confirmed-email path leaves is_guest set (the currently-live paths already do). - Space the delete/change dialog's action row from its input field. Integration tests: the Deleted filter scoping + ConfirmCode guest promotion. |
||
|
|
251c7af3f6 |
feat(admin): deleted-account dossier + operator delete-user action
The user-detail console gains a Deletion & retention panel: last login (time + IP), the tombstone (deleted-at + retained real name), and the retention journal (the legal dossier of detached credentials). A Delete-user action runs the same deletion orchestration as the in-app flow (mirrors the email-erase pattern). Store readers RetainedIdentities + DeletionInfo back the view; integration test covers them. |
||
|
|
356bf1a5ba |
feat(admin): search users by email + erase a bound email
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 1m3s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m40s
Add an exact (strict) email filter to the /users list (UserFilter.EmailExact → a kind='email' identity match) with a search input, and an 'Erase email' action on the user card that deletes the bound email identity and its pending confirmations, freeing the address. It refuses to remove the account's only identity (ErrLastIdentity), which would leave it unreachable. Integration tests for both. |
||
|
|
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. |
||
|
|
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. |
||
|
|
041106d623 |
feat(gateway): temporary IP ban (fail2ban) fed by rejections + honeypot/honeytoken
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 17s
CI / ui (pull_request) Successful in 53s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m11s
Add a prod-only, in-memory IP ban enforced at the edge, fed by three signals:
sustained rate-limiter rejections (the IP-keyed public/email/admin classes — the
user class stays the backend soft-flag's concern), a honeypot decoy-path hit (the
contour caddy tags decoys with X-Scrabble-Honeypot and routes them to the gateway),
and a honeytoken (a planted bearer, GATEWAY_HONEYTOKEN). A banned IP is refused with
429 by the abuseGuard middleware before any work — covering the Connect edge, the
live stream and the static SPA/landing the per-op limiter never gated.
The ban is off by default: it keys by the real client IP the shared-NAT test contour
does not expose, so a ban there would be self-inflicted; detection still logs in the
contour, only the ban action is gated (GATEWAY_ABUSE_BAN_ENABLED). Rejection bans last
GATEWAY_ABUSE_BAN_DURATION; tripwire/honeytoken hits are near-zero-false-positive and
earn longer fixed bans. Each ban increments gateway_abuse_banned_total{reason}.
Operators see and lift active bans on the admin console's Throttled page; the gateway
syncs its active set to the backend every 30s (POST /api/v1/internal/bans/sync,
backend/internal/banview) and applies the operator unbans the response returns.
PRERELEASE phase AG. Docs baked into ARCHITECTURE / FUNCTIONAL (+ru) / both READMEs.
|
||
|
|
57c778f9b2 |
feat(telegram,game): single bot + per-user variant preferences
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 54s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 57s
Collapse the two per-language Telegram bots into one unified bot and
replace language-based variant gating with explicit per-user variant
preferences.
- Telegram: one bot; drop service_language and the supported_languages
set everywhere (DB, account, auth, FlatBuffers Session wire, gateway,
connector proto). The single bot renders chat and out-of-app push in
the recipient's preferred_language; remove the game-language push
routing override (notify Intent.Language / push Event.language).
- Preferences: new accounts.variant_preferences (text[], DB default
{erudit_ru}, CHECK non-empty + subset of the three variants). Gates
the New Game picker, vs-AI and the friend invitation the player
creates, enforced server-side (HTTP 400 otherwise); an invited friend
may still accept any variant. Edited on the Settings screen; variants
are Erudit-first everywhere.
- Admin: drop the per-bot language selectors (broadcast / send-to-user)
and the feedback channel_lang column/field.
- Env/CI: collapse TELEGRAM_BOT_TOKEN_{EN,RU}, TELEGRAM_GAME_CHANNEL_ID_{EN,RU},
VITE_TELEGRAM_LINK{,_EN,_RU} and VITE_TELEGRAM_GAME_CHANNEL_NAME_{EN,RU}
to single unsuffixed names; drop GATEWAY_DEFAULT_SUPPORTED_LANGUAGES.
- Docs updated (ARCHITECTURE, FUNCTIONAL + _ru, platform/telegram, gateway,
backend, ui, UI_DESIGN, PRERELEASE).
The migration squash is deferred to a follow-up PR.
|
||
|
|
caefc8f579 |
feat(game): official first-move tile draw + admin step-by-step replay
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
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
Decide who moves first by the official rule: each seated player draws one tile, the one closest to "A" leads (a blank beats every letter), ties re-drawing until a single leader remains. Each draw uses honest per-draw crypto/rand entropy (not the deterministic bag seed), so the recorded draw — not a seed — is the only account of the outcome. The leader takes seat 0, so the engine and journal replay are unchanged. The draw is recorded with the game (game_setup_draws, migration 00013) for future tournaments, designed as a discrete "player N draws a tile" step. Friend/AI games draw at creation. Auto-match draws when the game opens, against a synthetic uuid.Nil opponent whose draw rows (NULL account_id) are back-filled to the real opponent on join — so the opener's seat is fixed up front and the existing open-game pre-move is preserved with no reseating. Admin /_gm/games/:id gains the recorded draw list and a simple step-by-step board replay (game.ReplayTimeline + a vanilla-JS stepper): a board with A-O/1-15 headers and highlighted premium squares, placed letters with their tile value as a subscript, rack panels around the board (seat 0 top, 1 bottom, 2 left, 3 right) with the current player highlighted, and a per-move log with the tiles drawn and the bag remainder. Docs: ARCHITECTURE §6/§9, FUNCTIONAL (+_ru), PRERELEASE (FM row), design spec. |
||
|
|
81b9e1529e |
feat(social): asymmetric per-user block, in-game block control, admin lists
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 51s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
Make a per-user block one-directional and non-destructive: the blocker stops
receiving everything from the blocked user (chat, nudge, friend requests,
invitations) and the matchmaker never pairs them, while the blocked user
notices nothing — their sends still persist by the normal rules but are never
delivered or surfaced (born-read). A block no longer deletes the friendship
(an unblock cleanly restores it) and instant-reads any unread the blocked user
had left for the blocker.
- backend: a directional blockExists guard across chat/nudge/friends/invitations
(store-but-hide for the blocked->blocker direction, refuse blocker->blocked);
the matchmaker excludes a block-related pair (both directions) from auto-match;
user_blocked/user_unblocked notifications to the blocker only (in-app only).
- ui: the opponent score card gains a block ✖️ control mirroring add-friend
(red "Block?" confirm, mutual-hide while confirming, struck name, hidden chat
composer when blocked); optimistic apply + event confirm + rollback for both.
- admin: the user card gains cross-linked blocks / blocked-by / friends lists.
- docs: FUNCTIONAL(+ru), ARCHITECTURE §10 + decision record, UI_DESIGN, PRERELEASE.
|
||
|
|
8793bd34f2 |
feat(stats): best-move word, moves & hint-share, and a hint-count fix (#81)
The statistics screen gains real depth, plus a hint-count bug fix found along the way. - Best move per variant: the screen shows the actual best-move word (drawn as game tiles; a wildcard shows its letter but no value), broken down by game variant, empty variants omitted. New account_best_move table, written at game finish. - Moves & hint share: two new lifetime tiles — the player's play count and the share of plays that used a hint — from summed account_stats counters (moves, hints_used). Honest-AI games are excluded, like the rest of the stats. - Hint-count fix: the in-game hint badge no longer goes stale across games. The global wallet now rides the wire apart from the per-game allowance (wallet_balance on StateView/HintResult/StatsView), so the client reads the live wallet rather than a per-game snapshot; game_players.hints_used now counts every hint (allowance + wallet), its true per-game total. - Account merge: sums the new moves/hints_used counters and merges the per-variant best moves (higher score kept), which it previously dropped. - Admin: the user card shows Moves and Hints used. - UI polish: tab/label wording, game-over text, and e2e selectors hardened against label changes. All wire additions are trailing (backward-compatible). Docs (ARCHITECTURE, FUNCTIONAL +ru, UISN_DESIGN) updated in step. |
||
|
|
aaac816dc2 |
feat(chat): unread read-receipts with lobby/game dot and history-open ack
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 49s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m16s
Persist per-message read state as a chat_messages.unread_seats bitmask
(migration 00008): a text message seeds every recipient seat's bit, a nudge
only the awaited seat's. A seat's bit clears when the player opens the move
history or chat (POST /games/:id/chat/read, sent only when something is
unread), and a nudge additionally clears when its recipient answers by moving
(a wired game NudgeClearer, dependency-inverted so game keeps off social).
UI shows a per-viewer unread dot in the lobby (next to the opponent) and the
game score bar — the unread_chat game-view flag seeds it from authoritative
REST views, live chat/nudge events raise it. Opening the move history counts
as reading (even without entering chat): the 💬 fade-blinks twice and the
client acks. Admin Messages gains an unread-only filter, a read/unread column,
and a per-message card with the per-seat read breakdown. Observability:
chat_read_duration histogram + chat_unread_messages gauge + social tracing.
|
||
|
|
9e72e2c799 |
feat(ads): banner message editor — top help aside + multi-line fields
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) Successful in 49s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m10s
Admin campaign editor polish: - move the link-formatting help aside to the top of the Messages section, beside the intro note (~40% width), so it no longer drops below and stretches the form fields. - make the English/Russian message fields 3-row, vertically resizable textareas (was single-line inputs) so long text wraps instead of scrolling off to the right. The strip is white-space:nowrap, so a stray newline collapses to a space on display. |
||
|
|
53c6e34c13 |
feat(ads): add a link-formatting help aside to the banner message editor
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 48s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m7s
On the campaign detail page, beside the "Add message" form, a static aside explains the message markdown: plain text is escaped, `[text](url)` becomes a link, and only http(s)/root-relative targets are linkified (others show as plain text). New .form-help (flex row) + .help (muted aside) console styles; wraps below the form on a narrow viewport. |
||
|
|
0946a3f66c |
feat(ads): server-driven ad-banner backend, wire & admin console (PR1)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 49s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m12s
Turn the gated-off mock banner into a real advertising subsystem (backend + admin half; the UI rotation lands in PR2). - internal/ads: campaigns (percent weight + validity window; a perpetual, undeletable default that fills the remainder up to 100%), 1..N bilingual messages (en+ru), global display timings; ActiveSet computes the window-filtered, default-remainder, GCD-reduced, language-resolved rotation feed. Smooth-weighted-round-robin math is unit-tested. - migration 00006 (+ jetgen): ad_campaigns / ad_messages / ad_settings, seeded default campaign + house message + default timings. - eligibility = !paid_account && hint_balance==0 && !no_banner role (new role; guests qualify). The resolved feed rides the profile.get response (no new RPC, works for guests, nothing distinct to filter); language by service_language. - live update: a notify `banner` sub-kind (re-poll signal) published when an operator grants hints or grants/revokes no_banner, so the client shows/hides in place. - admin console /_gm/banners (+ /_gm/banner-settings): campaign + message CRUD with reorder, default protection, clamped timings. - wire: fbs BannerInfo/BannerCampaign on Profile; gateway transcode forwards it. - docs: ARCHITECTURE §10, FUNCTIONAL (+ _ru), backend README, PRERELEASE tracker (incl. the deferred app.load aggregator note). |
||
|
|
d2a9441287 |
feat: AI-game refinements (GCG, your_turn, admin, metrics)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 49s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m7s
Follow-ups on the honest-AI game, same PR: - GCG export labels the robot seat "AI" instead of its pool name (ExportGCG overrides via accounts.IsRobot); the in-app 🤖 is unchanged. - vs_ai games emit no your_turn (the robot replies instantly, so it would be redundant); opponent_moved still advances the UI. - Admin console shows the AI flag: a 🤖 column in /games and an "AI game" line on the game card (GameRow/GameDetailView gain VsAI). - games_started_total / games_abandoned_total gain a vs_ai attribute; the Grafana Game-domain dashboard splits started/abandoned into human and AI panels. Tests: metrics unit (vs_ai split); integration (no your_turn, GCG "AI"). |
||
|
|
2a4ce319d9 |
feat(feedback): show the sender's interface language and connector bot in admin
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 47s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m7s
In the console feedback detail, show the sender's interface language (account preferred_language) always, and — for a message that arrived through an external connector (currently Telegram) — the bot they last used (en/ru, from the account's service_language). |
||
|
|
5287794a72 |
fix(feedback): theme buttons, badge the Info button, simplify admin actions
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Successful in 47s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
- style the About feedback button and the form's Send/attach/remove buttons with
the accent/border tokens used by the New Game CTA, so they follow light/dark
theme (the previous .btn/.ghost classes were not defined globally); the attach
button is a neutral 📎 icon button with an aria-label
- show a round badge on the About 'Feedback' button when a reply is waiting
- admin: one 'ban from feedback' checkbox shared by Delete and Delete-all (via
button formaction); hide Mark read when already read and Archive when archived
- e2e: match the About button by substring (its name gains the badge)
|
||
|
|
419ea11b14 |
feat(feedback): in-app user feedback with admin review and account roles
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Successful in 47s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m12s
User-facing Feedback screen (Settings -> Info, registered accounts only): a message (<=1024 runes) plus one optional attachment, an anti-spam gate (one unreviewed message at a time), and the operator's inline reply with a Settings/Info badge. Server-rendered admin console section (/_gm/feedback): unread/read/archived queue with per-user search, detail with read/reply/ archive/delete/delete-all, safe attachment serving (nosniff, images inline via <img>, others download-only). Introduces account_roles, the first per-account role table; feedback_banned blocks only feedback submission, granted/revoked from /users and the delete-with-block action. - migration 00004_feedback (feedback_messages + account_roles) + jetgen - backend internal/feedback (store+service), internal/account/roles.go - wire: FlatBuffers feedback.submit/get/unread; gateway guest gate (Op.NonGuest, is_guest via session resolve) -> guest_forbidden before any backend call - reply push reuses NotificationEvent with a new admin_reply sub-kind - UI: /feedback route + screen, attachment picker, badge, channel detection, i18n - tests: feedback unit (Go+UI), gateway guest-gate, inttest lifecycle, e2e - docs: PLAN stage 19, ARCHITECTURE s15, FUNCTIONAL(+ru), TESTING, READMEs |
||
|
|
192e4a2433 |
feat(admin): grant hints to a user's wallet from the console
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m5s
Add an "Add hints" form on the admin user card that additively tops up a player's hint wallet (1-100 per grant). The grant is raise-only by construction (an additive UPDATE never lowers the balance) and stays correct under a concurrent in-game spend; a per-grant cap bounds a fat-finger, since the console can never reduce a wallet. The in-game hint policy is unchanged and already correct: a game offers the per-seat allowance plus the wallet, spending the allowance first and the wallet only after (covered by TestHintPolicy). |
||
|
|
d1ba666495 |
feat(admin): manual account blocking (suspensions)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Successful in 45s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m10s
Operator-driven hard block, the counterpart to the soft high-rate flag: permanent or until a date, with an optional reason chosen from an editable en+ru picklist (snapshotted onto the block). A block forfeits the player's active games (opponent wins, as a resignation) and cancels their open matchmaking games. A backend gate refuses a blocked account on every /api/v1/user/* route except the block-status probe with 403 account_blocked, which threads through the gateway as the Execute result_code; the UI surfaces it as a terminal blocked screen and stops all push/poll. Temporary blocks self-expire; the operator can unblock at any time (lost games stay lost). Sessions are not revoked, so the blocked client can still reach the exempt block-status endpoint. Backend: migration 00003 (account_suspensions + suspension_reasons) + jet regen; account suspension store; game.ForfeitAllForAccount; requireNotSuspended gate + block-status endpoint; admin console block/unblock + Reasons CRUD. Wire: fbs BlockStatus + account.block_status gateway op. UI: blocked screen, app state, transport/codec, i18n. Docs: ARCHITECTURE, FUNCTIONAL(+ru), PRERELEASE (AB). |
||
|
|
0f3671f42d |
feat(admin): online dictionary update — upload archive, preview word diff, install & activate
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 23s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Successful in 45s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m5s
Replace the dictionary hot-reload with an online update flow in the GM console (/_gm/dictionary): the operator uploads scrabble-dawg-vX.Y.Z.tar.gz, previews the per-variant words added/removed against the active dictionary, and confirms to install + activate it. Versions are immutable; in-progress games keep their pinned version while new games use the new one. - engine: DiffWords (enumerate both DAWGs, decode only the differences), OpenFinder, Registry.Finder, DictFiles; OpenWithVersions skips the .staging area. - dictadmin: hardened release-archive validation + extraction (path-traversal, symlink, oversize, entry-count rejection) and staging -> install (atomic rename). - game: active dictionary version persisted in the dictionary_state singleton (single source of truth, restored on boot), concurrency-safe accessor. - storage: BACKEND_DICT_DIR is a named volume seeded from the image (nonroot-owned), so uploaded versions persist across redeploys; the build's DICT_VERSION labels the seed and equals the resident tag (BACKEND_DICT_VERSION). - docs: ARCHITECTURE §5, FUNCTIONAL (+ru), backend README, TESTING, PRERELEASE. Tests: engine + dictadmin unit; integration upload->preview->install->activate-> restart->pin->immutability->CSRF. |
||
|
|
94534ad0f2 |
feat(admin): add an 'open' filter to the console games list
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 45s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
The games-list status filter offered only active/finished; add 'open' (auto-match games awaiting an opponent) to the subnav and accept it in normalizeGameStatus. Render test covers the new filter link. |
||
|
|
eeb078d528 |
fix(admin): keep the filter query intact in console pager and export links
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
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 1m6s
The paginated users and messages lists interpolate the pre-encoded filter
query (url.Values.Encode) after the "?" in their pager and CSV-export links.
There html/template treats it as a single query value and percent-encodes the
structural "=" and "&" again, so "kind=robots" rendered as "kind%3drobots" and
the multi-pair message filter collapsed -- every page step dropped the active
filter.
Type FilterQuery as template.URL so the already-escaped fragment is emitted
verbatim (the attribute-level "&" -> "&" stays correct, the browser decodes
it back). It is safe because url.Values.Encode output is strictly
percent-encoded. games/complaints use status={{.Status}} -- a single value in
proper query-value context -- and were never affected.
|
||
|
|
8881214213 |
R6(a): de-stage code, docs, READMEs; split stage6_test
Mechanical, behaviour-preserving removal of Stage N / TODO-N / phase (RN) references from comments, doc-comments, service READMEs, the current-state docs (ARCHITECTURE, FUNCTIONAL+_ru, TESTING, UI_DESIGN), config-file comments, and the .fbs/.proto schema comments. PLAN.md / PRERELEASE.md / CLAUDE.md keep the stage history. - Rename the only stage-named identifiers: registerStage8 -> registerSocialOps, registerStage11 -> registerLinkOps (gateway transcode). - Split stage6_test.go: TestEmailLoginFlow -> email_test.go, TestGuestAutoMatchLeavesNoStats (+ provisionGuest) -> account_test.go. - Regenerated proto bindings (push.pb.go, telegram_grpc.pb.go) from the de-staged .proto comments; FB Go/TS bindings unchanged (flatc strips schema comments). go build/vet/gofmt clean across modules; integration typecheck and pnpm check green. |
||
|
|
ab58062565 |
R3: backend rate-limit observability — ratewatch, auto-flag, admin throttled view
- accounts.flagged_high_rate_at baked into the R1 baseline (no prod data; the contour schema is wiped after merge); jet regenerated — the regen also picks up the previously missing game_drafts/game_hidden models. - account.Store: FlagHighRate (set-once), ClearHighRateFlag, the flag in GetByID/ListUsers and a ListFlaggedHighRate review queue. - New internal/ratewatch: ingests the gateway rejection reports, keeps a bounded in-memory episode window for the console and applies the conservative auto-flag (1000 rejected / 10 min, BACKEND_HIGHRATE_FLAG_*). - POST /api/v1/internal/ratelimit/report (network-trusted, like sessions/resolve). - Admin console: Throttled page (episodes + flagged accounts), a high-rate badge in the user list, the marker + operator clear action on the user card. - Tests: ratewatch unit suite, report-route handler test, renderer cases, integration coverage for the store round-trip and the console flow. |
||
|
|
26aa154547 |
R1: schema & naming reset — squash migrations, rename variants
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 37s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
Squash the 12 goose migrations into one 00001_baseline.sql (there is no prod data; verified schema-identical to the chain via a pg_dump diff + the green integration suite) and rename the game-variant labels english/russian_scrabble/erudit -> scrabble_en/scrabble_ru/erudit_ru across the backend, the FlatBuffers wire values and the UI. dawg filenames and the Go enum identifiers are unchanged; the i18n display keys are kept. Adds PRERELEASE.md (the R1-R7 pre-release tracker), linked from CLAUDE.md. Contour DB wipe and the scrabble-dictionary tidy are follow-ups. |
||
|
|
c96d714fec |
Admin Messages: CSV export of the whole filtered list
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 33s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m14s
A right-aligned 'Export CSV ↓' link in the filter row downloads /_gm/messages.csv with the active filters (game / sender / name / ext masks), exporting every matching message (capped at 100k) regardless of the page window — columns time, source, sender_id, sender, ip, message, game_id. |
||
|
|
356f490546 |
Stage 17 round 6 (#18, PR D): admin Messages moderation section
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 32s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m14s
A new /_gm/messages console page lists posted chat messages (nudges excluded) newest-first — time, source (guest/robot/oldest identity kind), sender (linked to the user card), IP, body, game (linked to the game card) — searchable by sender name / external-id glob masks and pinnable to one game (?game=) or sender (?user=), linked from the game and user cards. The list query lives in social (raw SQL, kind='message', source via a SQL CASE), reusing the now-exported account.LikePattern. Server-rendered adminconsole MessagesView + messages.gohtml, 50/page via the shared pager. Tests: adminconsole render case; backend integration AdminListMessages (real Postgres) — nudge exclusion, game/sender pins, glob masks, source. Docs: ARCHITECTURE section 8 chat moderation, PLAN round-6. |
||
|
|
f916d5e0ca |
Stage 17 round 5 (L2): robot play-to-win intent + next-move ETA in the admin game card
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 29s
CI / gate (pull_request) Successful in 1s
CI / deploy (pull_request) Successful in 1m14s
The admin game detail now shows, per robot seat, the game's deterministic play-to-win decision (from the bag seed) and — while it is that robot's turn — its scheduled next-move ETA (sampled think-time delay, deferred past the sleep window), plus a caption with the ~40% global target. Wiring: robot.PlayToWin/NextMoveAt/PlayToWinTargetPercent exports, account.IsRobot, game RobotSchedule (seed + turn-start). Tests: NextMoveAt invariants (never early, never in the sleep window), PlayToWin export, and an admin render integration test asserting the intent + ETA + target appear. |
||
|
|
54497374e4 |
Stage 17 (#15): admin users people/robots toggle + display-name & external-id glob filters
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 27s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m11s
- account.ListUsers/CountUsers with a UserFilter: people vs robots (by a robot identity), case-insensitive '*'/'?' glob masks on display_name and any identity's external_id - admin users list shows the real kind (robot/guest/registered), defaults to people, with a People/Robots toggle + a filter form; pager preserves the filter - integration test for the filter; SQL verified against the live contour DB |
||
|
|
b15fd30c4f |
Stage 17 (contour round 4a): quick fixes
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Successful in 27s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m6s
- #4 bag label: '{n} in the bag' / 'Bag is empty' (was 'Bag {n}') - #6 allow a single trailing dot in display names (backend + UI regex + tests) - #1 double-tap zooms toward the tapped cell, not the top-left - #8 shuffle fires a short multi-pulse haptic - #11 highlighted/flashing tiles darken their bottom edge too (shadow joins the flash) - #13 toast slides up from the bottom and fades out - #7 hide the logout button (kept wired behind `hidden`) - #16 admin game seats: left-align numeric columns, clarify the 'Hints used' header |
||
|
|
c94cd3c3bf |
Stage 17 (contour round 2): Grafana Live/reload, rate-limit, iOS reconnect, hint/plaque/make-move UX
- Grafana: disable Live (GF_LIVE_MAX_CONNECTIONS=0) so its WebSocket no longer trips caddy Basic-Auth and re-prompts; admin console gains a Grafana nav link - deploy: force-recreate config-only services so reseeded Grafana dashboards / Caddyfile are actually picked up (the move-duration panel was invisible because the bind-mount went stale) - rate-limit: raise per-user budget 120/40 -> 300/80; UI skips reloading on the echo of the player's own move (fewer requests, no double-load) - iOS/Telegram reconnect: suppress the connection banner while backgrounded and for a short grace after resume; reconnect silently; wire visibilitychange + pageshow/pagehide + Telegram activated/deactivated (Bot API 8.0) - hint button disabled when 0 hints remain; nudge button shows a disabled state on your own turn - players plaque: invert so the active seat pops (accent chip, raised) and others recede - make-move UX: a direct ✅ commit button (no hold/popover); the Shuffle tab becomes ↩️ Reset while tiles are pending |
||
|
|
635f2fd9fc |
Stage 17: backend defect fixes (nudge code, TG name, robot names/timing, multi-device push, move-duration metric + admin analytics)
- #3 nudge-on-own-turn: distinct result code nudge_own_turn + i18n (was reused 'not_your_turn') - #2 sanitize connector registration name to the editable format; Player/Игрок-XXXXX fallback - #5 variant-aware robot name pools (composed full/colloquial first + surname forms; ru gets <=20% latin) - #4 move-number-aware robot move timing (early 1-5min -> late 10-90min, skew k=4) - #7 emit move event to the actor too (multi-device sync); opponent_moved stays in-app only - #1 live game_move_duration{variant,phase} histogram + admin console per-user min/avg/max columns and an inline-SVG move-time-by-move-number chart (offline from the journal) - ProvisionRobot bypasses editor name validation (system names like 'Peter J.') |
||
|
|
e9f836db87 |
Stage 15: dual Telegram bots & language-gated variants
Tests · Go / test (push) Successful in 9s
Tests · Integration / integration (push) Successful in 10s
Tests · UI / test (push) Successful in 20s
Tests · Go / test (pull_request) Successful in 8s
Tests · Integration / integration (pull_request) Successful in 11s
Tests · UI / test (pull_request) Successful in 19s
Service-agnostic refinement of the owner's idea: the sign-in service returns a set of supported game languages with the user identity, and the lobby gates the New Game variant choice by it (en -> English; ru -> Russian + Эрудит). - Connector hosts two bots in one container (one per service language, each its own token + game channel; the same telegram_id spans both). ValidateInitData tries each token and returns the validating bot's service_language + supported_languages. Per-language config (TELEGRAM_BOT_TOKEN_EN/_RU, channels). - supported_languages rides the Session (fbs, session-scoped, not persisted); the UI offers only the matching variants on New Game — gating only the START of a new game (auto-match + friend invite), not accept/open/play; backend does not enforce. - service_language persisted (accounts.service_language, migration 00010, written every login, last-login-wins) and routes the user-facing Notify push back through the right bot (push-target coalesces with preferred_language). - Admin SendToUser/SendToGameChannel gain an operator-chosen language selector in the console (unrelated to ValidateInitData). - Non-Telegram logins carry the gateway default set (GATEWAY_DEFAULT_SUPPORTED_LANGUAGES, all variants). Wire (committed regen): ValidateInitDataResponse +service_language +supported_languages; Session +supported_languages; SendToUser/SendToGameChannel +language. Docs (ARCHITECTURE/FUNCTIONAL/_ru/READMEs) + PLAN updated; stage marked done. |
||
|
|
01485d8fc6 | Stage 11: account linking & merge (email + Telegram Login Widget) (#12) | ||
|
|
3a640a17a4 | Stage 10: admin console & dictionary ops (complaint review, hot-reload, broadcasts) (#11) |