Skip to main content

Announcements

Backend-controlled announcements tell users about scheduled maintenance and deprecated app versions or features. Both apps show them in the same dialog. The native iOS app reads the same feed, so one backend row can reach all three clients.

Not to be confused with the portal's hand-written launch cards (apps/portal/src/components/announcements/) — those are static UI, not backend data.

Backend contract​

GET /api/system/announcements?client=&platform=&version= in enode_backend_v1 (SystemController). Unauthenticated, so it works before login. Missing or unknown parameters return { items: [] }, never an error.

ParameterTracking appPortal
clienttrackingportal
platformios, ipados, android (web in the browser build)web
versioninstalled native version (App.getInfo), native builds onlynot sent

The backend filters by clients, platforms, is_active, the published_at / expires_at window and min_app_version / max_app_version (a missing version matches every gate). It sorts blocking first, then by priority. Copy is localized by Accept-Language.

Each item carries title, message, ctas (label + url), dismissLabel, condition, trigger, blocking, displayInterval (seconds), priority and expiresAt (epoch ms). Validation: schema.ts.

There is no admin UI. Rows are created with SQL or a seed function in enode_backend_v1/Tests/AppTestsV2/Scripts/CreateAnnouncementScript.swift.

Presentation​

AnnouncementHost is mounted once in each root layout (tracking, portal). It shows one AnnouncementDialog at a time: title, message, one button per CTA, and the dismiss button.

  • CTAs open outside the app (window.open; on a Capacitor build the OS opens the URL, so a store link lands in the App Store / Play Store). Only https:, http: and mailto: links become buttons.
  • A blocking announcement has no dismiss button, ignores backdrop / Escape / Back, and stays open after a CTA. A blocking row without a working CTA is shown as a normal, dismissible one.
  • The dialog sits below BlockingErrorDialog.

Triggers and anchors​

trigger decides when a row is evaluated:

  • launch — after each fetch: app start, return to the foreground, reconnect, language switch.
  • screen:<anchor> — when the user lands on a view that declares the anchor with useAnnouncementAnchor(anchor, active?).

Anchor ids mean the same thing in every app, so one row can target several clients. The native app uses training, dashboard and profile.

AnchorTracking appPortal
training/workouts/today–
dashboard–/dashboard
settingsSettings drawer (shared)Settings drawer (shared)
insights–/dashboard/data/insights
export–/dashboard/data/export
workouts–/dashboard/workouts

A row with an anchor that a build doesn't declare never fires there. Use launch for anything that has to reach old builds, such as a version deprecation.

Display rules​

Implemented in rules.ts, a port of the native ENAnnouncementService.buildQueue:

  • Frequency — displayInterval: null shows once per device, 0 on every evaluation, n again after n seconds. Blocking ignores frequency.
  • Expiry — expiresAt in the past hides the row, also from the offline cache.
  • Condition — privilege=<key>, license=<name|uuid>, with != and | alternatives (condition.ts). Anything unparsable evaluates to false. Signed out means no privileges and no license. While a signed-in profile is still loading, conditioned rows wait.
  • Order — blocking first, then by descending priority.

Refresh, cache and hold​

store.ts fetches on start, foreground / reconnect (store-kit revalidation) and language switch. Triggers evaluate the cached list, never the network. Nothing is evaluated before the first fetch has settled, so a withdrawn row doesn't flash from the cache.

The last response and the per-announcement lastShownAt live in localStorage (enode.announcements.cache, enode.announcements.displayStates). A blocking deprecation therefore keeps blocking offline.

useHoldAnnouncements(active) holds new dialogs back. The tracking app holds them while a training runs. Releasing the last hold evaluates launch plus the anchors on screen, so a held-back announcement shows right after the training.

Authoring examples​

PurposeclientsplatformsVersionWindowblockingdisplay_interval_secondsCTA
Scheduled maintenanceenode,tracking,portal––published_at = announce from, expires_at = end of maintenanceno86400optional status page
Retire an old tracking versiontrackingios (second row for android)max_app_version = last retired version–yes–store link
Retire a featureper app––expires_at = switch-off dateno259200explanation page

Limitations​

  • Only builds that contain this feature see announcements. The first version deprecation can only reach versions released after it.
  • A row has one set of CTA links, so platform-specific store links need one row per platform.
  • The message is static text. Write maintenance times with their time zone; the client doesn't convert them.
  • A missing translation falls back to the row's original text.