feat(banner): per-campaign colour overrides and urgent alerts
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m8s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m50s

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).
This commit is contained in:
Ilia Denisov
2026-07-05 15:36:35 +02:00
parent ac383880b7
commit 6db9178449
32 changed files with 1297 additions and 152 deletions
@@ -13,13 +13,20 @@ import (
)
type AdCampaigns struct {
CampaignID uuid.UUID `sql:"primary_key"`
Name string
Weight int32
IsDefault bool
Enabled bool
StartsAt *time.Time
EndsAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
CampaignID uuid.UUID `sql:"primary_key"`
Name string
Weight int32
IsDefault bool
Enabled bool
StartsAt *time.Time
EndsAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
OverrideBg *string
OverrideFg *string
OverrideLink *string
OverrideBgDark *string
OverrideFgDark *string
OverrideLinkDark *string
Urgent bool
}
@@ -17,15 +17,22 @@ type adCampaignsTable struct {
postgres.Table
// Columns
CampaignID postgres.ColumnString
Name postgres.ColumnString
Weight postgres.ColumnInteger
IsDefault postgres.ColumnBool
Enabled postgres.ColumnBool
StartsAt postgres.ColumnTimestampz
EndsAt postgres.ColumnTimestampz
CreatedAt postgres.ColumnTimestampz
UpdatedAt postgres.ColumnTimestampz
CampaignID postgres.ColumnString
Name postgres.ColumnString
Weight postgres.ColumnInteger
IsDefault postgres.ColumnBool
Enabled postgres.ColumnBool
StartsAt postgres.ColumnTimestampz
EndsAt postgres.ColumnTimestampz
CreatedAt postgres.ColumnTimestampz
UpdatedAt postgres.ColumnTimestampz
OverrideBg postgres.ColumnString
OverrideFg postgres.ColumnString
OverrideLink postgres.ColumnString
OverrideBgDark postgres.ColumnString
OverrideFgDark postgres.ColumnString
OverrideLinkDark postgres.ColumnString
Urgent postgres.ColumnBool
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
@@ -67,33 +74,47 @@ func newAdCampaignsTable(schemaName, tableName, alias string) *AdCampaignsTable
func newAdCampaignsTableImpl(schemaName, tableName, alias string) adCampaignsTable {
var (
CampaignIDColumn = postgres.StringColumn("campaign_id")
NameColumn = postgres.StringColumn("name")
WeightColumn = postgres.IntegerColumn("weight")
IsDefaultColumn = postgres.BoolColumn("is_default")
EnabledColumn = postgres.BoolColumn("enabled")
StartsAtColumn = postgres.TimestampzColumn("starts_at")
EndsAtColumn = postgres.TimestampzColumn("ends_at")
CreatedAtColumn = postgres.TimestampzColumn("created_at")
UpdatedAtColumn = postgres.TimestampzColumn("updated_at")
allColumns = postgres.ColumnList{CampaignIDColumn, NameColumn, WeightColumn, IsDefaultColumn, EnabledColumn, StartsAtColumn, EndsAtColumn, CreatedAtColumn, UpdatedAtColumn}
mutableColumns = postgres.ColumnList{NameColumn, WeightColumn, IsDefaultColumn, EnabledColumn, StartsAtColumn, EndsAtColumn, CreatedAtColumn, UpdatedAtColumn}
defaultColumns = postgres.ColumnList{IsDefaultColumn, EnabledColumn, CreatedAtColumn, UpdatedAtColumn}
CampaignIDColumn = postgres.StringColumn("campaign_id")
NameColumn = postgres.StringColumn("name")
WeightColumn = postgres.IntegerColumn("weight")
IsDefaultColumn = postgres.BoolColumn("is_default")
EnabledColumn = postgres.BoolColumn("enabled")
StartsAtColumn = postgres.TimestampzColumn("starts_at")
EndsAtColumn = postgres.TimestampzColumn("ends_at")
CreatedAtColumn = postgres.TimestampzColumn("created_at")
UpdatedAtColumn = postgres.TimestampzColumn("updated_at")
OverrideBgColumn = postgres.StringColumn("override_bg")
OverrideFgColumn = postgres.StringColumn("override_fg")
OverrideLinkColumn = postgres.StringColumn("override_link")
OverrideBgDarkColumn = postgres.StringColumn("override_bg_dark")
OverrideFgDarkColumn = postgres.StringColumn("override_fg_dark")
OverrideLinkDarkColumn = postgres.StringColumn("override_link_dark")
UrgentColumn = postgres.BoolColumn("urgent")
allColumns = postgres.ColumnList{CampaignIDColumn, NameColumn, WeightColumn, IsDefaultColumn, EnabledColumn, StartsAtColumn, EndsAtColumn, CreatedAtColumn, UpdatedAtColumn, OverrideBgColumn, OverrideFgColumn, OverrideLinkColumn, OverrideBgDarkColumn, OverrideFgDarkColumn, OverrideLinkDarkColumn, UrgentColumn}
mutableColumns = postgres.ColumnList{NameColumn, WeightColumn, IsDefaultColumn, EnabledColumn, StartsAtColumn, EndsAtColumn, CreatedAtColumn, UpdatedAtColumn, OverrideBgColumn, OverrideFgColumn, OverrideLinkColumn, OverrideBgDarkColumn, OverrideFgDarkColumn, OverrideLinkDarkColumn, UrgentColumn}
defaultColumns = postgres.ColumnList{IsDefaultColumn, EnabledColumn, CreatedAtColumn, UpdatedAtColumn, UrgentColumn}
)
return adCampaignsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
CampaignID: CampaignIDColumn,
Name: NameColumn,
Weight: WeightColumn,
IsDefault: IsDefaultColumn,
Enabled: EnabledColumn,
StartsAt: StartsAtColumn,
EndsAt: EndsAtColumn,
CreatedAt: CreatedAtColumn,
UpdatedAt: UpdatedAtColumn,
CampaignID: CampaignIDColumn,
Name: NameColumn,
Weight: WeightColumn,
IsDefault: IsDefaultColumn,
Enabled: EnabledColumn,
StartsAt: StartsAtColumn,
EndsAt: EndsAtColumn,
CreatedAt: CreatedAtColumn,
UpdatedAt: UpdatedAtColumn,
OverrideBg: OverrideBgColumn,
OverrideFg: OverrideFgColumn,
OverrideLink: OverrideLinkColumn,
OverrideBgDark: OverrideBgDarkColumn,
OverrideFgDark: OverrideFgDarkColumn,
OverrideLinkDark: OverrideLinkDarkColumn,
Urgent: UrgentColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,