e089a0a997
CI / changes (pull_request) Successful in 4s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 28s
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 1m46s
The /_gm user card gains a Finance panel: an account's chip balances per funding segment, benefits per origin (hints, no-ads until/forever), the recorded refund risk (abuse flag + floor-0 loss), and the append-only ledger history newest-first. Backed by a new payments.AccountStatement read straight from the materialized tables + the ledger (uncached — an admin, rare view). Folds the agreed admin / reports / catalog plan into PLAN.md (the PR stack: this panel, then the catalog editor, admin grant, refund + ledger export) and moves the tournament-entry storage design to the tournament stage.
656 lines
20 KiB
Go
656 lines
20 KiB
Go
package adminconsole
|
|
|
|
import "html/template"
|
|
|
|
// The *View types are the display models the gin handlers fill and the templates
|
|
// render. Time values are pre-formatted to strings by the handlers so the
|
|
// templates stay logic-free.
|
|
|
|
// Pager is the shared list pagination state.
|
|
type Pager struct {
|
|
Page int
|
|
PageSize int
|
|
Total int
|
|
HasPrev bool
|
|
HasNext bool
|
|
PrevPage int
|
|
NextPage int
|
|
}
|
|
|
|
// NewPager builds the pagination state for a 1-based page of pageSize over total
|
|
// items.
|
|
func NewPager(page, pageSize, total int) Pager {
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
p := Pager{Page: page, PageSize: pageSize, Total: total, PrevPage: page - 1, NextPage: page + 1}
|
|
p.HasPrev = page > 1
|
|
p.HasNext = page*pageSize < total
|
|
return p
|
|
}
|
|
|
|
// VariantVersions lists the dictionary versions resident for one variant.
|
|
type VariantVersions struct {
|
|
Variant string
|
|
Latest string
|
|
Versions []string
|
|
}
|
|
|
|
// DashboardView is the landing-page summary.
|
|
type DashboardView struct {
|
|
Accounts int
|
|
Games int
|
|
ActiveGames int
|
|
OpenComplaints int
|
|
OpenFeedback int
|
|
PendingChanges int
|
|
// ActiveVersion is the dictionary version new games pin (the persisted active
|
|
// version), distinct from the per-variant resident versions.
|
|
ActiveVersion string
|
|
Variants []VariantVersions
|
|
}
|
|
|
|
// UsersView is the paginated account list.
|
|
type UsersView struct {
|
|
Items []UserRow
|
|
Pager Pager
|
|
// Robots is the active people/robots toggle; NameMask/ExternalIDMask are the current
|
|
// glob filters; FilterQuery is those URL-encoded for the pager links. It is an
|
|
// already-escaped query fragment (url.Values.Encode), so it is typed template.URL to
|
|
// be emitted verbatim — interpolated as a plain string it would have its "=" and "&"
|
|
// percent-encoded again by the contextual escaper.
|
|
Robots bool
|
|
Deleted bool
|
|
NameMask string
|
|
ExternalIDMask string
|
|
EmailExact string
|
|
FilterQuery template.URL
|
|
}
|
|
|
|
// UserRow is one account row in the list. MoveMin/Avg/Max are the account's
|
|
// pre-formatted move-duration summary (empty when it has no timed move);
|
|
// FlaggedHighRate marks the soft high-rate badge.
|
|
type UserRow struct {
|
|
ID string
|
|
DisplayName string
|
|
Kind string
|
|
Language string
|
|
Guest bool
|
|
Deleted bool
|
|
FlaggedHighRate bool
|
|
CreatedAt string
|
|
HasMoveStats bool
|
|
MoveMin string
|
|
MoveAvg string
|
|
MoveMax string
|
|
}
|
|
|
|
// MessagesView is the paginated chat-message moderation list. NameMask/ExtMask are the
|
|
// current sender glob filters; GameID/UserID pin the list to one game / sender (set from a
|
|
// game or user card); FilterQuery is the active filters URL-encoded for the pager and CSV
|
|
// links — an already-escaped query fragment, hence template.URL so it is not re-encoded
|
|
// inside the link (see UsersView.FilterQuery).
|
|
type MessagesView struct {
|
|
Items []MessageRow
|
|
Pager Pager
|
|
NameMask string
|
|
ExtMask string
|
|
GameID string
|
|
UserID string
|
|
UnreadOnly bool
|
|
FilterQuery template.URL
|
|
}
|
|
|
|
// MessageRow is one chat message in the moderation list: its sender (linked to the user
|
|
// card), source, IP, body, game (linked to the game card), time, and whether it is still
|
|
// unread by at least one recipient.
|
|
type MessageRow struct {
|
|
ID string
|
|
SenderID string
|
|
SenderName string
|
|
Source string
|
|
IP string
|
|
Body string
|
|
GameID string
|
|
CreatedAt string
|
|
Unread bool
|
|
}
|
|
|
|
// ChatMessageDetailView is one chat message with its per-seat read breakdown, for the
|
|
// console message card.
|
|
type ChatMessageDetailView struct {
|
|
ID string
|
|
GameID string
|
|
SenderID string
|
|
SenderName string
|
|
Source string
|
|
Kind string
|
|
Body string
|
|
IP string
|
|
CreatedAt string
|
|
Unread bool
|
|
Seats []ChatSeatStatusRow
|
|
}
|
|
|
|
// ChatSeatStatusRow is one seat's read status on the message card: the seat index, the
|
|
// occupant (linked to the user card) and its role ("sender", "read" or "unread").
|
|
type ChatSeatStatusRow struct {
|
|
Seat int
|
|
AccountID string
|
|
DisplayName string
|
|
Role string
|
|
}
|
|
|
|
// UserDetailView is one account with its stats, identities and recent games.
|
|
type UserDetailView struct {
|
|
ID string
|
|
DisplayName string
|
|
Language string
|
|
TimeZone string
|
|
Guest bool
|
|
NotificationsInAppOnly bool
|
|
// MergedInto is the primary account id when this account has been retired by a
|
|
// merge, or empty for a live account.
|
|
MergedInto string
|
|
// The account-deletion dossier. Deleted marks a tombstoned account; DeletedAt and
|
|
// DeletedName are its deletion time and retained real name; LastLoginAt/IP are the
|
|
// last cold-load stamp (shown for any account); Retained is the credential journal.
|
|
Deleted bool
|
|
DeletedAt string
|
|
DeletedName string
|
|
LastLoginAt string
|
|
LastLoginIP string
|
|
Retained []RetainedRow
|
|
// FlaggedHighRateAt is the pre-formatted soft high-rate marker timestamp,
|
|
// empty for an unflagged account; the card shows it with the Clear action.
|
|
FlaggedHighRateAt string
|
|
CreatedAt string
|
|
HasStats bool
|
|
Stats StatsRow
|
|
Identities []IdentityRow
|
|
// HasEmail gates the "Erase email" action; set when the account carries an email identity.
|
|
HasEmail bool
|
|
Games []GameRow
|
|
// TelegramID and VKID are the account's platform external ids (empty when absent).
|
|
// TelegramID gates the "Send Telegram message" operator action; VKID surfaces the VK
|
|
// user id with a link to the VK profile (there is no VK messaging to drive).
|
|
TelegramID string
|
|
VKID string
|
|
ConnectorEnabled bool
|
|
// MoveChart is the pre-rendered inline SVG of the account's per-move-number think
|
|
// time (min/mean/max), empty when the account has no timed move.
|
|
MoveChart template.HTML
|
|
// Suspension is the account's current manual-block state, shown with the block/unblock
|
|
// form; Reasons is the operator-editable reason picklist offered in the block form.
|
|
Suspension SuspensionView
|
|
Reasons []ReasonOption
|
|
// Roles is the account's current roles (each revocable); KnownRoles is the set the
|
|
// grant form offers. The first role is the feedback ban (see internal/account).
|
|
Roles []string
|
|
KnownRoles []string
|
|
// Blocks, BlockedBy and Friends are the social graph on the card: who this account has
|
|
// blocked, who currently blocks it, and its mutual friendships — each cross-linked to the
|
|
// other account with the date it happened. They are the full truth; the asymmetric block
|
|
// suppression that hides relationships from players never applies to the console.
|
|
Blocks []RelationRow
|
|
BlockedBy []RelationRow
|
|
Friends []RelationRow
|
|
// Finance is the account's payments picture (balances, benefits, refund risk, ledger). Present
|
|
// is false when the payments domain is unwired.
|
|
Finance FinanceView
|
|
}
|
|
|
|
// FinanceView is the account's payments picture on the user card: chip balances per funding
|
|
// segment, benefits per origin, the recorded refund risk, and the append-only ledger history
|
|
// (newest first). Present is false when the payments domain is unwired.
|
|
type FinanceView struct {
|
|
Present bool
|
|
Segments []SegmentRow
|
|
Benefits []BenefitRow
|
|
// Abuse is the refund abuse flag; Loss is the unrecoverable chip loss from floor-0 refunds.
|
|
Abuse bool
|
|
Loss int
|
|
Ledger []LedgerRow
|
|
}
|
|
|
|
// SegmentRow is one funding segment's chip balance.
|
|
type SegmentRow struct {
|
|
Source string
|
|
Chips int
|
|
}
|
|
|
|
// BenefitRow is one origin's benefit: the hint wallet, the ad-free expiry (pre-formatted, empty
|
|
// when none) and the lifetime ad-free flag.
|
|
type BenefitRow struct {
|
|
Origin string
|
|
Hints int
|
|
AdsUntil string
|
|
Forever bool
|
|
}
|
|
|
|
// LedgerRow is one append-only ledger entry: its kind, funding source / benefit origin, signed chip
|
|
// delta, the product / order / provider it references (empty when none), the raw snapshot JSON and
|
|
// the pre-formatted time.
|
|
type LedgerRow struct {
|
|
Kind string
|
|
Source string
|
|
Origin string
|
|
ChipsDelta int
|
|
Product string
|
|
Order string
|
|
Provider string
|
|
Snapshot string
|
|
At string
|
|
}
|
|
|
|
// RelationRow is one cross-linked account in the user card's blocks / blocked-by / friends
|
|
// lists: the other account's id (the link target), its display name, and the pre-formatted date.
|
|
type RelationRow struct {
|
|
AccountID string
|
|
DisplayName string
|
|
Date string
|
|
}
|
|
|
|
// SuspensionView is an account's current manual-block state shown on the user card: whether it
|
|
// is blocked, whether the block is permanent, the pre-formatted expiry (empty when permanent),
|
|
// when it was applied, and the reason snapshot in both languages (empty when none was cited).
|
|
type SuspensionView struct {
|
|
Blocked bool
|
|
Permanent bool
|
|
Until string
|
|
BlockedAt string
|
|
ReasonEn string
|
|
ReasonRu string
|
|
}
|
|
|
|
// ReasonOption is one suspension-reason picklist entry offered in the block form's dropdown.
|
|
type ReasonOption struct {
|
|
ID string
|
|
TextEn string
|
|
TextRu string
|
|
}
|
|
|
|
// StatsRow is an account's lifetime statistics.
|
|
type StatsRow struct {
|
|
Wins int
|
|
Losses int
|
|
Draws int
|
|
MaxGamePoints int
|
|
MaxWordPoints int
|
|
Moves int
|
|
HintsUsed int
|
|
}
|
|
|
|
// IdentityRow is one platform/email identity of an account.
|
|
type IdentityRow struct {
|
|
Kind string
|
|
ExternalID string
|
|
Confirmed bool
|
|
CreatedAt string
|
|
}
|
|
|
|
// RetainedRow is one credential in the account-deletion retention journal (the legal
|
|
// dossier of detached credentials): what was detached, when, and why.
|
|
type RetainedRow struct {
|
|
Kind string
|
|
ExternalID string
|
|
Reason string
|
|
Confirmed bool
|
|
LinkedAt string
|
|
DetachedAt string
|
|
}
|
|
|
|
// GameRow is one game row in a list.
|
|
type GameRow struct {
|
|
ID string
|
|
Variant string
|
|
Status string
|
|
Players int
|
|
UpdatedAt string
|
|
// VsAI marks an honest-AI game (rendered as 🤖 in the list's AI column).
|
|
VsAI bool
|
|
}
|
|
|
|
// GamesView is the paginated games list, optionally filtered by status.
|
|
type GamesView struct {
|
|
Items []GameRow
|
|
Status string
|
|
Pager Pager
|
|
}
|
|
|
|
// GameDetailView is one game with its seats.
|
|
type GameDetailView struct {
|
|
ID string
|
|
Variant string
|
|
DictVersion string
|
|
Status string
|
|
Players int
|
|
ToMove int
|
|
EndReason string
|
|
MoveCount int
|
|
CreatedAt string
|
|
UpdatedAt string
|
|
FinishedAt string
|
|
// VsAI marks an honest-AI game (shown as a 🤖 flag in the summary).
|
|
VsAI bool
|
|
Seats []SeatRow
|
|
// HasRobot is true when any seat is a robot, gating the robot-target caption;
|
|
// RobotTargetPct is the configured global play-to-win rate, in percent.
|
|
HasRobot bool
|
|
RobotTargetPct int
|
|
// ReplayJSON is the game-replay payload (board, seats, per-step racks/scores/bag) the
|
|
// game_detail page feeds to its vanilla-JS stepper; HasReplay gates the replay section.
|
|
ReplayJSON template.JS
|
|
HasReplay bool
|
|
// SetupDraws is the first-move draw — one row per tile drawn (docs/ARCHITECTURE.md §6) —
|
|
// and FirstMover is the resolved name of the seat-0 player the draw elected.
|
|
SetupDraws []SetupDrawRow
|
|
FirstMover string
|
|
}
|
|
|
|
// SetupDrawRow is one tile drawn in the first-move seeding (docs/ARCHITECTURE.md §6): the
|
|
// round, the player (Name/AccountID, or "(opponent)" with an empty AccountID for an
|
|
// auto-match synthetic draw not yet back-filled), the drawn letter (upper-cased; "?" for a
|
|
// blank) and its draw rank.
|
|
type SetupDrawRow struct {
|
|
Round int
|
|
Name string
|
|
AccountID string
|
|
Letter string
|
|
Blank bool
|
|
Rank int
|
|
}
|
|
|
|
// SeatRow is one seat of a game. For a robot seat (IsRobot) RobotIntent is the game's
|
|
// deterministic play-to-win decision ("play to win"/"play to lose"), and NextMove is the
|
|
// scheduled next-move ETA shown only while it is that robot's turn in an active game.
|
|
type SeatRow struct {
|
|
Seat int
|
|
DisplayName string
|
|
AccountID string
|
|
Score int
|
|
HintsUsed int
|
|
Winner bool
|
|
IsRobot bool
|
|
RobotIntent string
|
|
NextMove string
|
|
}
|
|
|
|
// ComplaintsView is the paginated complaint review queue.
|
|
type ComplaintsView struct {
|
|
Items []ComplaintRow
|
|
Status string
|
|
Pager Pager
|
|
}
|
|
|
|
// ComplaintRow is one complaint row in the queue.
|
|
type ComplaintRow struct {
|
|
ID string
|
|
Word string
|
|
Variant string
|
|
WasValid bool
|
|
Status string
|
|
Disposition string
|
|
CreatedAt string
|
|
}
|
|
|
|
// ComplaintDetailView is one complaint with its resolution state and form.
|
|
type ComplaintDetailView struct {
|
|
ID string
|
|
Word string
|
|
Variant string
|
|
DictVersion string
|
|
WasValid bool
|
|
Note string
|
|
Status string
|
|
Disposition string
|
|
ResolutionNote string
|
|
CreatedAt string
|
|
ResolvedAt string
|
|
GameID string
|
|
Resolved bool
|
|
}
|
|
|
|
// DictionaryView lists the resident versions per variant, the active version new
|
|
// games pin, and the pending wordlist changes from accepted complaints.
|
|
type DictionaryView struct {
|
|
// ActiveVersion is the dictionary version new games pin; the update form sets it.
|
|
ActiveVersion string
|
|
Variants []VariantVersions
|
|
Changes []DictChangeRow
|
|
}
|
|
|
|
// DictChangeRow is one pending wordlist edit.
|
|
type DictChangeRow struct {
|
|
Variant string
|
|
Word string
|
|
Action string
|
|
ResolvedAt string
|
|
}
|
|
|
|
// DictionaryPreviewView is the second step of a dictionary update: the version
|
|
// parsed from the uploaded archive (editable before confirming), the staging token
|
|
// that names the uploaded files on disk, the active version the diff is against, and
|
|
// the per-variant word diff.
|
|
type DictionaryPreviewView struct {
|
|
Version string
|
|
Token string
|
|
ActiveVersion string
|
|
Variants []VariantDiffRow
|
|
}
|
|
|
|
// VariantDiffRow summarises one variant's word diff in the update preview.
|
|
// AddedCount/RemovedCount are the full totals; AddedSample/RemovedSample are the
|
|
// first words shown (capped); AddedTruncated/RemovedTruncated mark a capped list;
|
|
// LargeRemoval flags a removal large enough to warrant caution before confirming.
|
|
type VariantDiffRow struct {
|
|
Variant string
|
|
AddedCount int
|
|
RemovedCount int
|
|
AddedSample []string
|
|
RemovedSample []string
|
|
AddedTruncated bool
|
|
RemovedTruncated bool
|
|
LargeRemoval bool
|
|
}
|
|
|
|
// BroadcastView is the operator-broadcast form page.
|
|
type BroadcastView struct {
|
|
ConnectorEnabled bool
|
|
}
|
|
|
|
// ThrottledView is the rate-limit observability page: the temporary IP bans the
|
|
// gateway is currently enforcing, the recent gateway-reported throttle episodes
|
|
// (in-memory, reset on restart) and the accounts currently carrying the high-rate
|
|
// flag. FlagThreshold and FlagWindow caption the active auto-flag tuning.
|
|
type ThrottledView struct {
|
|
Bans []BanRow
|
|
Episodes []ThrottleEpisodeRow
|
|
Flagged []FlaggedAccountRow
|
|
FlagThreshold int
|
|
FlagWindow string
|
|
}
|
|
|
|
// BanRow is one temporary IP ban the gateway is enforcing, with its reason and its
|
|
// since/expiry timestamps; the row carries an unban action.
|
|
type BanRow struct {
|
|
IP string
|
|
Reason string
|
|
Since string
|
|
Expires string
|
|
}
|
|
|
|
// ThrottleEpisodeRow is one recently throttled limiter key. UserID links to the
|
|
// user card and is set only for the user class (the other classes key by IP).
|
|
type ThrottleEpisodeRow struct {
|
|
Class string
|
|
Key string
|
|
UserID string
|
|
Rejected int
|
|
FirstSeen string
|
|
LastSeen string
|
|
}
|
|
|
|
// FlaggedAccountRow is one account carrying the high-rate flag.
|
|
type FlaggedAccountRow struct {
|
|
ID string
|
|
DisplayName string
|
|
FlaggedAt string
|
|
}
|
|
|
|
// ReasonsView is the suspension-reason picklist management page: every editable reason entry.
|
|
type ReasonsView struct {
|
|
Items []ReasonRow
|
|
}
|
|
|
|
// ReasonRow is one editable suspension-reason entry, with its English and Russian text.
|
|
type ReasonRow struct {
|
|
ID string
|
|
TextEn string
|
|
TextRu string
|
|
CreatedAt string
|
|
}
|
|
|
|
// MessageView is the result page shown after a POST action.
|
|
type MessageView struct {
|
|
Heading string
|
|
Body string
|
|
Back string
|
|
}
|
|
|
|
// BannersView is the advertising-campaign list page.
|
|
type BannersView struct {
|
|
Items []BannerCampaignRow
|
|
}
|
|
|
|
// BannerCampaignRow is one campaign in the list. Window is a human-readable
|
|
// validity window ("perpetual" for the default); ActiveNow reports whether it
|
|
// would rotate right now (enabled and within its window).
|
|
type BannerCampaignRow struct {
|
|
ID string
|
|
Name string
|
|
Weight int
|
|
IsDefault bool
|
|
Enabled bool
|
|
Window string
|
|
Messages int
|
|
ActiveNow bool
|
|
}
|
|
|
|
// BannerDetailView is the campaign detail/edit page. StartsAt/EndsAt are the
|
|
// "YYYY-MM-DDTHH:MM" (UTC) values for the datetime-local inputs, empty when open.
|
|
//
|
|
// The colour-override and urgent fields drive the non-default campaign's editor:
|
|
// OverrideAllOn/OverrideDarkOn report whether each colour set is active, and the
|
|
// six *Bg/*Fg/*Link values seed the native colour inputs — the stored override
|
|
// when a set is on, otherwise the neutral theme token so the picker starts from a
|
|
// sensible colour and the live preview shows the real fallback.
|
|
type BannerDetailView struct {
|
|
ID string
|
|
Name string
|
|
Weight int
|
|
IsDefault bool
|
|
Enabled bool
|
|
StartsAt string
|
|
EndsAt string
|
|
Urgent bool
|
|
OverrideAllOn bool
|
|
AllBg string
|
|
AllFg string
|
|
AllLink string
|
|
OverrideDarkOn bool
|
|
DarkBg string
|
|
DarkFg string
|
|
DarkLink string
|
|
Messages []BannerMessageRow
|
|
}
|
|
|
|
// BannerMessageRow is one bilingual message of a campaign. First/Last drive the
|
|
// reorder buttons (disabled at the ends).
|
|
type BannerMessageRow struct {
|
|
ID string
|
|
BodyEn string
|
|
BodyRu string
|
|
First bool
|
|
Last bool
|
|
}
|
|
|
|
// BannerSettingsView is the global display-timings form.
|
|
type BannerSettingsView struct {
|
|
HoldMs int
|
|
EdgePauseMs int
|
|
ScrollPxPerSec int
|
|
FadeOutMs int
|
|
GapMs int
|
|
FadeInMs int
|
|
}
|
|
|
|
// FeedbackView is the paginated user-feedback queue. Status is the active
|
|
// unread/read/archived filter; NameMask/ExtMask are the sender glob filters;
|
|
// UserID pins the list to one account (the per-user link from /users);
|
|
// FilterQuery is the active filters URL-encoded for the pager links (already
|
|
// escaped, hence template.URL — see UsersView.FilterQuery).
|
|
type FeedbackView struct {
|
|
Items []FeedbackRow
|
|
Status string
|
|
NameMask string
|
|
ExtMask string
|
|
UserID string
|
|
Pager Pager
|
|
FilterQuery template.URL
|
|
}
|
|
|
|
// FeedbackRow is one feedback message in the queue: its sender (linked to the user
|
|
// card), source, channel, whether it has an attachment / a reply, its state and
|
|
// time.
|
|
type FeedbackRow struct {
|
|
ID string
|
|
AccountID string
|
|
SenderName string
|
|
Source string
|
|
Channel string
|
|
HasAttachment bool
|
|
Read bool
|
|
Replied bool
|
|
Archived bool
|
|
CreatedAt string
|
|
}
|
|
|
|
// FeedbackDetailView is one feedback message with its body, attachment, state and
|
|
// the reply / archive / delete forms. Body, AttachmentName, SenderName and IP are
|
|
// user-controlled and rendered as plain auto-escaped text. IsImage gates the inline
|
|
// <img> preview; Banned shows whether the sender already holds the feedback ban.
|
|
type FeedbackDetailView struct {
|
|
ID string
|
|
AccountID string
|
|
SenderName string
|
|
Source string
|
|
Channel string
|
|
// InterfaceLanguage is the sender's interface language (account preference).
|
|
InterfaceLanguage string
|
|
IP string
|
|
Body string
|
|
HasAttachment bool
|
|
AttachmentName string
|
|
IsImage bool
|
|
Read bool
|
|
Archived bool
|
|
Replied bool
|
|
ReplyBody string
|
|
RepliedAt string
|
|
CreatedAt string
|
|
// Version is the client app build the report was sent from (empty for rows that predate it).
|
|
Version string
|
|
// The Filed time is shown in three zones so the operator can tell what is certainly known from
|
|
// what is merely defaulted. CreatedAt is the authoritative UTC time. CreatedAtBrowser is that
|
|
// instant in the client's UTC offset detected at submit (BrowserTZ its "±HH:MM" label), empty
|
|
// when the client reported none (an older build). CreatedAtUser is that instant in the sender's
|
|
// saved profile zone (UserTZ its label), empty when the account has no zone beyond the UTC
|
|
// default — the template then shows "N/A" so the missing datum is explicit.
|
|
CreatedAtBrowser string
|
|
BrowserTZ string
|
|
CreatedAtUser string
|
|
UserTZ string
|
|
Banned bool
|
|
}
|