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:
@@ -29,7 +29,7 @@ describe('createScheduler', () => {
|
||||
{ weight: 7, messages: ['b'] },
|
||||
]);
|
||||
expect(sched.total).toBe(2);
|
||||
const cycle = Array.from({ length: 10 }, () => sched.next());
|
||||
const cycle = Array.from({ length: 10 }, () => sched.next().md);
|
||||
const a = cycle.filter((m) => m === 'a').length;
|
||||
const b = cycle.filter((m) => m === 'b').length;
|
||||
expect(a).toBe(3);
|
||||
@@ -42,7 +42,7 @@ describe('createScheduler', () => {
|
||||
it('round-robins messages within a campaign', () => {
|
||||
const sched = createScheduler([{ weight: 1, messages: ['m0', 'm1', 'm2'] }]);
|
||||
expect(sched.total).toBe(3);
|
||||
expect([sched.next(), sched.next(), sched.next(), sched.next()]).toEqual(['m0', 'm1', 'm2', 'm0']);
|
||||
expect([sched.next().md, sched.next().md, sched.next().md, sched.next().md]).toEqual(['m0', 'm1', 'm2', 'm0']);
|
||||
});
|
||||
|
||||
it('drops empty and non-positive-weight campaigns', () => {
|
||||
@@ -52,13 +52,27 @@ describe('createScheduler', () => {
|
||||
{ weight: 2, messages: ['keep'] },
|
||||
]);
|
||||
expect(sched.total).toBe(1);
|
||||
expect(sched.next()).toBe('keep');
|
||||
expect(sched.next().md).toBe('keep');
|
||||
});
|
||||
|
||||
it('is empty for no campaigns', () => {
|
||||
const sched = createScheduler([]);
|
||||
expect(sched.total).toBe(0);
|
||||
expect(sched.next()).toBe('');
|
||||
expect(sched.next().md).toBe('');
|
||||
});
|
||||
|
||||
it('carries each campaign colour override with its message', () => {
|
||||
const red = { bg: '#aa0000', fg: '#ffffff', link: '#ffdd00' };
|
||||
const darkRed = { bg: '#330000', fg: '#eeeeee', link: '#ffcc00' };
|
||||
const sched = createScheduler([
|
||||
{ weight: 1, messages: ['plain'] },
|
||||
{ weight: 1, messages: ['branded'], overrideAll: red, overrideDark: darkRed },
|
||||
]);
|
||||
const two = [sched.next(), sched.next()];
|
||||
const branded = two.find((it) => it.md === 'branded');
|
||||
const plain = two.find((it) => it.md === 'plain');
|
||||
expect(branded?.colors).toEqual({ all: red, dark: darkRed });
|
||||
expect(plain?.colors).toEqual({ all: null, dark: null });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user