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).
Add email / telegram_linked / vk_linked to the Profile (fbs table + regenerated
Go/TS bindings, gateway ProfileResp + encodeProfile, backend DTO, UI model +
decode). They are filled outside the pure projection — Server.profileResponse now
reads the account's identities (like the banner seam) — and will drive the profile's
Add / Unlink / change-email controls.
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.
Two regressions from the previous banner pass:
- Fade (#2): the manual-opacity fade could paint opacity 0 and 1 in one frame and
skip the transition — most visible for a single (default) campaign message,
whose only fade is the first show. Revert the fade to Svelte transition:fade
(which forces the from-state, so even the first/only message fades), keeping it
on its own {#if} layer independent of the scroll. A freshly-mounted view onto a
running cycle still renders the live message instantly (inFade duration 0 once),
so navigation does not replay the fade. Verified by opacity sampling: advances
fade, navigation stays at opacity 1.
- Profile update (#3): the banner block was attached only to GET /profile, so a
profile.update (e.g. a language switch) returned a profile without it and the
banner vanished until reload. A shared profileResponse() now attaches the banner
to GET, PUT and the link/merge profile responses. Regression test added
(TestBannerSurvivesProfileUpdate).
Still open: the scroll position is not preserved across navigation (the view
remounts); discussed separately.
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).