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
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:
@@ -90,15 +90,23 @@ func (s *Store) Campaign(ctx context.Context, id uuid.UUID) (Campaign, error) {
|
||||
func (s *Store) CreateCampaign(ctx context.Context, c Campaign) (uuid.UUID, error) {
|
||||
id := uuid.New()
|
||||
now := time.Now().UTC()
|
||||
abg, afg, alink := colorCols(c.OverrideAll)
|
||||
dbg, dfg, dlink := colorCols(c.OverrideDark)
|
||||
stmt := table.AdCampaigns.INSERT(
|
||||
table.AdCampaigns.CampaignID, table.AdCampaigns.Name, table.AdCampaigns.Weight,
|
||||
table.AdCampaigns.IsDefault, table.AdCampaigns.Enabled,
|
||||
table.AdCampaigns.StartsAt, table.AdCampaigns.EndsAt,
|
||||
table.AdCampaigns.OverrideBg, table.AdCampaigns.OverrideFg, table.AdCampaigns.OverrideLink,
|
||||
table.AdCampaigns.OverrideBgDark, table.AdCampaigns.OverrideFgDark, table.AdCampaigns.OverrideLinkDark,
|
||||
table.AdCampaigns.Urgent,
|
||||
table.AdCampaigns.CreatedAt, table.AdCampaigns.UpdatedAt,
|
||||
).VALUES(
|
||||
postgres.UUID(id), postgres.String(c.Name), postgres.Int(int64(c.Weight)),
|
||||
postgres.Bool(false), postgres.Bool(c.Enabled),
|
||||
tsOrNull(c.StartsAt), tsOrNull(c.EndsAt),
|
||||
abg, afg, alink,
|
||||
dbg, dfg, dlink,
|
||||
postgres.Bool(c.Urgent),
|
||||
postgres.TimestampzT(now), postgres.TimestampzT(now),
|
||||
)
|
||||
if _, err := stmt.ExecContext(ctx, s.db); err != nil {
|
||||
@@ -111,12 +119,20 @@ func (s *Store) CreateCampaign(ctx context.Context, c Campaign) (uuid.UUID, erro
|
||||
// window. The default flag is never touched here. Returns ErrNotFound when no
|
||||
// campaign matches.
|
||||
func (s *Store) UpdateCampaign(ctx context.Context, c Campaign) error {
|
||||
abg, afg, alink := colorCols(c.OverrideAll)
|
||||
dbg, dfg, dlink := colorCols(c.OverrideDark)
|
||||
stmt := table.AdCampaigns.UPDATE(
|
||||
table.AdCampaigns.Name, table.AdCampaigns.Weight, table.AdCampaigns.Enabled,
|
||||
table.AdCampaigns.StartsAt, table.AdCampaigns.EndsAt, table.AdCampaigns.UpdatedAt,
|
||||
table.AdCampaigns.StartsAt, table.AdCampaigns.EndsAt,
|
||||
table.AdCampaigns.OverrideBg, table.AdCampaigns.OverrideFg, table.AdCampaigns.OverrideLink,
|
||||
table.AdCampaigns.OverrideBgDark, table.AdCampaigns.OverrideFgDark, table.AdCampaigns.OverrideLinkDark,
|
||||
table.AdCampaigns.Urgent, table.AdCampaigns.UpdatedAt,
|
||||
).SET(
|
||||
postgres.String(c.Name), postgres.Int(int64(c.Weight)), postgres.Bool(c.Enabled),
|
||||
tsOrNull(c.StartsAt), tsOrNull(c.EndsAt), postgres.TimestampzT(time.Now().UTC()),
|
||||
tsOrNull(c.StartsAt), tsOrNull(c.EndsAt),
|
||||
abg, afg, alink,
|
||||
dbg, dfg, dlink,
|
||||
postgres.Bool(c.Urgent), postgres.TimestampzT(time.Now().UTC()),
|
||||
).WHERE(table.AdCampaigns.CampaignID.EQ(postgres.UUID(c.ID)))
|
||||
return execOne(ctx, s.db, stmt, "update campaign")
|
||||
}
|
||||
@@ -257,18 +273,40 @@ func execOne(ctx context.Context, db qrm.Executable, stmt postgres.Statement, wh
|
||||
|
||||
func modelToCampaign(r model.AdCampaigns) Campaign {
|
||||
return Campaign{
|
||||
ID: r.CampaignID,
|
||||
Name: r.Name,
|
||||
Weight: int(r.Weight),
|
||||
IsDefault: r.IsDefault,
|
||||
Enabled: r.Enabled,
|
||||
StartsAt: r.StartsAt,
|
||||
EndsAt: r.EndsAt,
|
||||
CreatedAt: r.CreatedAt,
|
||||
UpdatedAt: r.UpdatedAt,
|
||||
ID: r.CampaignID,
|
||||
Name: r.Name,
|
||||
Weight: int(r.Weight),
|
||||
IsDefault: r.IsDefault,
|
||||
Enabled: r.Enabled,
|
||||
StartsAt: r.StartsAt,
|
||||
EndsAt: r.EndsAt,
|
||||
OverrideAll: colorSetFrom(r.OverrideBg, r.OverrideFg, r.OverrideLink),
|
||||
OverrideDark: colorSetFrom(r.OverrideBgDark, r.OverrideFgDark, r.OverrideLinkDark),
|
||||
Urgent: r.Urgent,
|
||||
CreatedAt: r.CreatedAt,
|
||||
UpdatedAt: r.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// colorSetFrom rebuilds an optional ColorSet from three nullable colour columns.
|
||||
// The all-or-nothing CHECK keeps the trio consistent, so a nil in any one means
|
||||
// the set is absent.
|
||||
func colorSetFrom(bg, fg, link *string) *ColorSet {
|
||||
if bg == nil || fg == nil || link == nil {
|
||||
return nil
|
||||
}
|
||||
return &ColorSet{Bg: *bg, Fg: *fg, Link: *link}
|
||||
}
|
||||
|
||||
// colorCols renders a *ColorSet as its three column value-expressions in
|
||||
// bg, fg, link order — NULL for each when the set is absent.
|
||||
func colorCols(cs *ColorSet) (bg, fg, link postgres.Expression) {
|
||||
if cs == nil {
|
||||
return postgres.NULL, postgres.NULL, postgres.NULL
|
||||
}
|
||||
return postgres.String(cs.Bg), postgres.String(cs.Fg), postgres.String(cs.Link)
|
||||
}
|
||||
|
||||
func modelToMessage(r model.AdMessages) Message {
|
||||
return Message{
|
||||
ID: r.MessageID,
|
||||
|
||||
Reference in New Issue
Block a user