feat(ads): server-driven ad-banner backend, wire & admin console (PR1) #70

Merged
developer merged 2 commits from feature/ad-network-backend into development 2026-06-15 21:09:29 +00:00
Owner

PR1 of 2 for the server-driven advertising banner ("ad network"). This is the backend + admin half; PR2 wires the UI rotation + fade UX and removes the SHOW_AD_BANNER gate.

What's here

  • internal/ads — campaigns (percent weight 1..100 + optional validity window; a perpetual, undeletable default that fills the remainder up to 100% and drops out at 0), 1..N bilingual messages (en+ru), global display timings. ActiveSet builds the window-filtered, default-remainder, GCD-reduced, language-resolved rotation feed. The 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 profile.get — no new RPC, works for guests, nothing distinct to filter; message 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 the banner in place.
  • Admin/_gm/banners (campaign + message CRUD with reorder, default protection) and /_gm/banner-settings (clamped global timings).
  • WireBannerInfo/BannerCampaign on the Profile FlatBuffers table; the gateway transcode forwards it verbatim.
  • Docs — ARCHITECTURE §10, FUNCTIONAL (+ _ru), backend README, PRERELEASE tracker row (incl. the deferred app.load aggregator note: client↔gateway is h2c/HTTP/2, so bootstrap RPCs already multiplex — revisit only with evidence).

Verification (local, all green)

gofmt/go vet; backend + gateway unit tests; full integration suite (-tags=integration, incl. new banner console + profile-eligibility tests); UI codegen idempotent, svelte-check, test:unit (243), build.

**PR1 of 2** for the server-driven advertising banner ("ad network"). This is the backend + admin half; PR2 wires the UI rotation + fade UX and removes the `SHOW_AD_BANNER` gate. ### What's here - **`internal/ads`** — campaigns (percent weight 1..100 + optional validity window; a perpetual, undeletable **default** that fills the remainder up to 100% and drops out at 0), 1..N **bilingual** messages (en+ru), global display timings. `ActiveSet` builds the window-filtered, default-remainder, GCD-reduced, language-resolved rotation feed. The 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 `profile.get`** — no new RPC, works for guests, nothing distinct to filter; message 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 the banner in place. - **Admin** — `/_gm/banners` (campaign + message CRUD with reorder, default protection) and `/_gm/banner-settings` (clamped global timings). - **Wire** — `BannerInfo`/`BannerCampaign` on the `Profile` FlatBuffers table; the gateway transcode forwards it verbatim. - **Docs** — ARCHITECTURE §10, FUNCTIONAL (+ `_ru`), backend README, PRERELEASE tracker row (incl. the **deferred `app.load` aggregator** note: client↔gateway is h2c/HTTP/2, so bootstrap RPCs already multiplex — revisit only with evidence). ### Verification (local, all green) `gofmt`/`go vet`; backend + gateway unit tests; **full integration suite** (`-tags=integration`, incl. new banner console + profile-eligibility tests); UI codegen idempotent, `svelte-check`, `test:unit` (243), `build`.
developer added 1 commit 2026-06-15 21:00:42 +00:00
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
0946a3f66c
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).
developer added 1 commit 2026-06-15 21:05:43 +00:00
fix(ads): verify a message belongs to its campaign before edit/delete
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 1m5s
00a33c227b
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.
owner approved these changes 2026-06-15 21:09:13 +00:00
developer merged commit a5f066224c into development 2026-06-15 21:09:29 +00:00
developer deleted branch feature/ad-network-backend 2026-06-15 21:09:29 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: developer/scrabble-game#70