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).
DeleteMessage/EditMessage acted on the message id without checking it belonged
to the campaign id in the URL. A mismatched pair could edit an unrelated
campaign's message or — worse — delete the default campaign's last message via
another campaign's URL, bypassing the "default keeps >=1 message" guard. Both
now load the campaign and return ErrNotFound unless it owns the message
(MoveMessage already did). Operator-only, but a real business-logic bypass.
Regression test: TestBannerMessageOwnership.
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).