c13f5cdb1e6fb34c7a38fc443fdcd4f87fa360ed
10 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3b744b7d2f |
feat(admin): an all-accounts ledger section with filters and totals
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 12s
CI / integration (pull_request) Successful in 22s
CI / ui (pull_request) Successful in 1m16s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m47s
There was no way to see the money as a whole: the ledger was only ever rendered inside one account's card, so "what came in last month" meant exporting the entire CSV and reading it elsewhere. /_gm/ledger lists every operation, newest first, filtered by date range (defaulting to the last 30 days), by wallet, by rail, by kind and by account. Wallet and rail are deliberately separate axes because they answer different questions — "what happened on VK" matches the funded segment or the benefit origin, while "what came through YooKassa" matches the settling provider, and a chip spend has no rail at all, so it drops out of that filter by construction. Above the table sit the totals for everything the filter matches, not merely the page on screen, which is the point of showing them. Money is listed per currency because the rails settle in roubles, Votes and Stars and one sum across them would mean nothing. Row amounts come from the operation snapshot, since the ledger's own columns count chips rather than money. Paging, the CSV export and a refund's way back are all built from the same encoded filter, so none of them can quietly show a different slice than the screen. The export moves here from the user card and gains the filter along with money columns; it is capped, because an append-only ledger grows forever and an unbounded export would eventually time out. The refund action moves onto the funded rows here — an operator can now find a payment by filter without knowing whose it is first — and returns to the same filtered view. The destination travels with the form but is only honoured when it is a console path, so a crafted form cannot turn it into an open redirect. The user card keeps the account's standing rather than its history: balances, benefits, the risk flag and a lifetime summary (money paid and refunded per currency, chips credited and spent), plus a link into the ledger scoped to that account. Operations are rendered in one place, with filters and paging, instead of two. Tests: the filter axes, paging that neither repeats nor drops a row, totals that describe the range rather than the page, money recovered from a snapshot and absent on a spend, and the console page, the export and the card hand-off. The existing finance-panel test now asserts the summary and that the card no longer re-renders the rows. |
||
|
|
ed53e25e57 |
feat(backend): per-tier, per-kind active-game limits with a guest funnel
CI / changes (pull_request) Successful in 5s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 20s
CI / ui (pull_request) Has been skipped
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m41s
Cap a player's simultaneous unfinished games per kind (vs_ai, random, friends) with independent guest and durable-account tiers, held in a new single-row backend.config table (-1 = unlimited) behind an in-memory cache and editable live in the admin console (/_gm/limits). Each game is tagged with games.game_kind on creation. This replaces the earlier flat MaxActiveQuickGames=10 combined cap: the per-tier/kind config is the single mechanism, enforced at the same handler gate (ensureUnderGameLimit by kind on lobby/enqueue) plus the durable friends cap in CreateInvitation. game.Service.AtGameLimit only resolves the tier and counts; the limit policy stays at the request edge. Guests are now refused friend requests, friend-code redemption, befriend-in-game and invitation creation outright (403 guest_forbidden) -- previously only the UI hid these. Admin: a kind column in both game lists and the config editor. Defaults: guest 1 vs_ai / 1 random / 0 friends; durable 10 / 10 / 10. |
||
|
|
0036b55618 |
feat(admin): product catalog editor
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 24s
CI / ui (pull_request) Has been skipped
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m53s
The /_gm console gains a Catalog editor — the source of truth for products. Create / edit / archive-unarchive products, their atom composition and per-rail prices (RUB via direct / VOTE via vk / XTR via telegram / CHIP value), and hard-delete only a never-transacted product (an order or ledger reference forces archive-only, backed by the FK RESTRICT). The archived flag reuses the existing product.active. Activation revalidates the sellable shape — a pack (the chips atom ⇒ a money price per rail, chips-only) or a value (no chips ⇒ a CHIP price); a tournament-bearing product is composable but never sellable yet. Backed by payments AdminCatalog / CreateProduct / UpdateProduct / SetProductActive / DeleteProduct + a pure validateProduct. Tests: validateProduct (pack / value / tournament / duplicate / shape); the console editor end to end (create, edit, archive, delete-if-clean, refuse a transacted delete). |
||
|
|
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). |
||
|
|
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 |
||
|
|
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). |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
3a640a17a4 | Stage 10: admin console & dictionary ops (complaint review, hot-reload, broadcasts) (#11) |