diff --git a/PLAN.md b/PLAN.md index c4214b1..fee5f9b 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1428,9 +1428,9 @@ cannot submit; three-way admin filter. docker-network caddy hop `172.18.0.x` for chat moderation, and bucketing the gateway's per-IP rate limiter on it). Correct + spoof-safe in **both** contours (prod has no host caddy → public clients untrusted → real peer used). `peerIP` unit-tested. - - **Ad banner** gated **off** behind a compile-time `SHOW_AD_BANNER=false` in `Screen.svelte` - — the `{#if}` branch, the `AdBanner` import and `banner.ts` are tree-shaken out of the prod - bundle (code kept for post-release polish). + - **Ad banner** was gated off here behind `SHOW_AD_BANNER=false`; the **AD** phase + (PRERELEASE.md) later turned it into the real server-driven advertising network and removed + the gate (`Screen` renders it from `app.profile.banner`). - **Landing** Telegram entry is now just the **64px logo** (clickable, no button/caption). - **TG-fullscreen header** reworked again: title + menu are one **centred pair** (hamburger right of the title) pinned to the **bottom** of the TG nav band, lining up with Telegram's diff --git a/PRERELEASE.md b/PRERELEASE.md index 9bf751a..accdc65 100644 --- a/PRERELEASE.md +++ b/PRERELEASE.md @@ -32,7 +32,7 @@ the edge before prod. Each phase maps back to the owner's raw pre-release TODO l | DA | Dictionary admin: online release-archive upload → word-diff preview → install/activate; versioned dict volume; active version persisted in DB; resident label = release tag | owner ad-hoc | **done** | | AB | Manual account block (admin suspension): permanent/temporary with an editable en+ru reason picklist; a block forfeits the player's active games + cancels their open ones; a backend gate refuses a blocked account with **403 `account_blocked`**; the UI shows a terminal blocked screen and stops all push/poll; manual unblock; temporary blocks self-expire (migration `00003`) | owner ad-hoc | **done** | | AI | Honest AI opponent in quick game: an explicit 🤖 AI / 👤 random selector (AI default); the robot is seated and moves at once; 7-day inactivity loss (the per-turn timeout reused); chat/nudge disabled, no statistics; the opponent is shown as 🤖 everywhere | owner ad-hoc | **done** | -| AD | Advertising banner ("ad network"): server-driven weighted campaigns (percent weight + validity window; the perpetual default fills the remainder up to 100%), bilingual messages shown by bot (`service_language`); eligibility = free account + empty hint wallet + no `no_banner` role (guests included); the resolved feed rides `profile.get` with a `notify` `banner` re-poll on eligibility change; `/_gm/banners` admin + global display timings; client smooth-weighted-round-robin rotation + fade-out/gap/fade-in UX. A single `app.load` bootstrap aggregator was considered and **deferred** (see ARCHITECTURE §10). | owner ad-hoc | PR1 backend+admin **done**; PR2 UI rotation **next** | +| AD | Advertising banner ("ad network"): server-driven weighted campaigns (percent weight + validity window; the perpetual default fills the remainder up to 100%), bilingual messages shown by bot (`service_language`); eligibility = free account + empty hint wallet + no `no_banner` role (guests included); the resolved feed rides `profile.get` with a `notify` `banner` re-poll on eligibility change; `/_gm/banners` admin + global display timings; client smooth-weighted-round-robin rotation + fade-out/gap/fade-in UX. A single `app.load` bootstrap aggregator was considered and **deferred** (see ARCHITECTURE §10). | owner ad-hoc | **done** (PR1 backend+admin, PR2 UI rotation) | | → | Stage 18 — prod contour deploy | — | see [`PLAN.md`](PLAN.md) | ## Key findings (these reshaped the raw list — read before starting a phase) diff --git a/docs/UI_DESIGN.md b/docs/UI_DESIGN.md index 83b1a85..28c8151 100644 --- a/docs/UI_DESIGN.md +++ b/docs/UI_DESIGN.md @@ -195,16 +195,24 @@ Login uses `Screen`. under-board slot shows the **Scores: N** preview. The screen **title** is the variant's display name (Scrabble / Скрэббл / Erudite / Эрудит), not a constant "Scrabble". -## Announcement banner (`components/AdBanner.svelte`, `lib/banner.ts`) +## Advertising banner (`components/AdBanner.svelte`, `lib/banner.ts`) A one-line inset strip under the nav bar, drawn on a dedicated `--ad-bg` token — a subtle accent, a touch darker than the surroundings in the light theme and a touch lighter in the dark theme, mapped to Telegram's `secondary_bg_color` inside the Mini App. Content is -minimal markdown (text + links, escaped + linkified). A parameterised **rotator** drives messages: a fitting message -holds `holdMs` (default 60 s) then cross-fades to the next; a message wider than the strip -pauses (`edgePauseMs`), scrolls to its right edge at `scrollPxPerSec`, pauses, and repeats -until the cycle exceeds `holdMs`. Today a **mock** provider rotates a long and a short -message; the source becomes a server-driven channel later (see ARCHITECTURE). +minimal markdown (text + links, escaped + linkified). It is **server-driven**: the campaigns and +display timings ride the `profile.get` response (`app.profile.banner`, present only for an eligible +viewer — see ARCHITECTURE §10), so `Screen` renders the strip only when that block is present, and +a `notify` `banner` event re-fetches the profile to show or hide it in place. + +A **smooth weighted round-robin** (`createScheduler`) picks the next message: campaigns compete by +their weight (each appears its weight share per cycle, evenly interleaved, not at random), and a +campaign's own messages advance round-robin. The **rotator** then drives one message: it fades in +(`fadeInMs`), holds `holdMs` (a message wider than the strip pauses `edgePauseMs`, scrolls to its +right edge at `scrollPxPerSec`, pauses, and repeats while under `holdMs`), then — when more than one +message exists — fades out (`fadeOutMs`), waits `gapMs`, and fades the next in. A lone message stays +put (a long one keeps scrolling). All timings are operator-set (`/_gm/banner-settings`). Under +**reduce-motion** the fades collapse to an instant swap and a long message does not scroll. ## Result / status iconography (`lib/result.ts`) diff --git a/ui/src/components/AdBanner.svelte b/ui/src/components/AdBanner.svelte index b9e37fc..a49baa4 100644 --- a/ui/src/components/AdBanner.svelte +++ b/ui/src/components/AdBanner.svelte @@ -1,43 +1,57 @@
- {#key current} -
- {@html linkify(items[current]?.md ?? '')} -
- {/key} +
+ {@html linkify(current)} +