Skip to content

Add in-app announcement banner with i18n feed - #2

Merged
512banque merged 2 commits into
mainfrom
feature/in-app-announcements
Apr 23, 2026
Merged

Add in-app announcement banner with i18n feed#2
512banque merged 2 commits into
mainfrom
feature/in-app-announcements

Conversation

@512banque

Copy link
Copy Markdown
Collaborator

Summary

  • Adds an opt-in in-app banner polling a remote JSON feed every 10 min; users dismiss per-message (localStorage) or opt out globally (config.yaml).
  • Messages are localized via a translations map (locale → {title, body, cta_label, cta_url}); falls back to default_locale, then any translation.
  • Backend picks the first active message (respects published_at and show_until); legacy flat schema still accepted for forward/backward compat.

Notes

  • Feature is off-path for the open-source spirit: user-controlled opt-out is one click away, no telemetry, no per-user tracking.
  • GET /api/announcements is whitelisted during setup mode so the banner surfaces right after onboarding without waiting a full poll.
  • Concurrency on the fetcher pointer is guarded by an RWMutex; start/stop go through dedicated helpers.
  • Feed is served from crawlobserver.com/announcements/feed.json (Cloudflare Pages, 5min cache).

Test plan

  • CI (golangci-lint, go test, frontend build, prettier, eslint) passes
  • go test ./internal/announcements/... — 10 tests covering active-window filtering, legacy schema, 404 cache preservation, empty-feed retraction
  • Manual: fresh install → no stale message shown (installTs cutoff)
  • Manual: existing user upgrade → sees current message once, can dismiss
  • Manual: locale switch → banner content switches language live
  • Manual: click "Ne plus afficher" → announcements.enabled = false in config.yaml, poll stops

Polls a configurable URL every 10 minutes and displays the latest
message as a dismissible banner in the main alerts area. Users can
dismiss individual messages (persisted in localStorage) or opt out
globally (persisted in config.yaml).

Includes install cutoff to hide pre-install messages for fresh
installs, timed release via published_at, optional expiration via
show_until, and XSS protection (HTML-escaped body, https-only
links and CTA).
Replace flat title/body fields with a translations map keyed by locale.
The frontend picks the user's locale via getLocale(), falling back to
the message's default_locale and then to any available translation.
CTA URL can be overridden per translation.

Review fixes on top of the initial feature:
- Schema backward compat: accept legacy flat title/body/cta_label and
  normalize them into a synthesized single-locale translation.
- Timed release in the fetcher: pick the first active message instead
  of the first well-formed one, so a pre-scheduled message never
  shadows an active message lower in the feed.
- Setup-mode readiness: allow GET /api/announcements through the
  requireReady middleware, and retry the first fetch faster on the
  frontend (30s, capped) so the banner appears shortly after setup
  completes instead of waiting a full 10-minute poll.
- Concurrency: guard announcer/announcerCancel with an RWMutex; add
  announcerSnapshot()/stopAnnouncer() helpers.

Adds i18n keys in en.json and fr.json for the dismiss/opt-out labels;
other locales fall back to English via the existing i18n mechanism.
Adds Go tests covering active-window filtering, legacy schema
normalization, cache preservation on 404, and end-to-end fetch.
@SEObserver
SEObserver force-pushed the feature/in-app-announcements branch from 91f8e2c to fc26635 Compare April 23, 2026 20:20
@512banque
512banque merged commit 31a0ab8 into main Apr 23, 2026
4 checks passed
@512banque
512banque deleted the feature/in-app-announcements branch April 23, 2026 20:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant