Closed the v3.0.x arc — ntfy push notifications for newly-discovered Aerodrome updates. With v3.0.0 (check + discover) and v3.0.1 (in-app apply) live, the GitHub channel had two of three notification surfaces wired: the gear-menu amber dot across admin pages, and the in-card status on /updates. v3.0.2 adds the third: an opt-in ntfy push notification that fires once when the background scheduler transitions from "no update known" (or "I know about an older release") to "v3.1.0 is available." The notification points the tap-to-open URL at /updates so a single tap from your phone lands you on the Apply button. Two-key opt-in: requires both updates.github.notify.ntfy: true AND notifications.events.update_available: true — the same double-gate every ntfy event uses, with both defaulting to off so existing installs don't suddenly start pushing without explicit consent.
How it works for users: enable ntfy as you would for any other event type (set notifications.enabled: true, point at your ntfy server, set both opt-in flags). The notification body reads "vX.Y.Z is available. You're on vA.B.C. Open the Updates page to apply." with an up-arrow emoji and a tap-to-open that goes straight to /updates. The notification fires exactly once per release discovery — not once per poll, not once per page load. If you ignore it for 24 hours, the cooldown allows one more push as a daily nudge until you apply the update or until an even newer version supersedes the available one. If you have multiple notification preferences configured (quiet hours, rate limits, etc.) they all apply to update_available the same way they apply to every other event — it's a first-class citizen in the notifier registry, not a special-cased side channel.
Behind the scenes — the transition-detection logic. The naive implementation would fire on every successful check that finds a newer-than-running tag, which would mean a daily-poll user gets a daily push until they apply (annoying — that's what the gear-badge is for). The right implementation fires only on the transition: the moment last_known_latest in SQLite changes from one tag to a strictly-newer tag. That requires reading the prior state BEFORE the save, comparing to the new state AFTER the save, and gating the notification on the change. The _perform_github_check() function now snapshots prior_state["last_known_latest"] inside the _update_check_lock, saves the new state, then fires the notification only if (a) the new tag is strictly newer than the prior tag (or prior was NULL — first-discovery counts as a transition), AND (b) the new tag is strictly newer than the running version. Both conditions matter: (a) prevents re-firing every poll until apply, (b) prevents firing for "you discovered v3.1.0 but you're already on v3.1.0" scenarios where a manual install ran ahead of the scheduler. The notifier call itself is wrapped in try/except so a notifier failure can never crash the scheduler — state is saved first, notification is best-effort.
Behind the scenes — the notifier integration. update_available is now in notifier.py's KNOWN_EVENTS set and in NEVER_COOLDOWN (the per-aircraft cooldown doesn't apply since there's no aircraft context). A new 'updates' route mapping in _build_click_url() translates click_route="updates" to {public_url}/updates for the tap-to-open — same pattern as 'live', 'watchlist', 'stats', etc. The default cooldown is 1440 minutes (24 hours) in notifications.cooldown_minutes.update_available, which is the value most users will leave alone — daily nudge if you've been sleeping on the update, silent otherwise. The notification body uses the arrow_up ntfy tag (an upward-pointing emoji) so the notification is visually distinct from the other Aerodrome alerts in your ntfy feed.
Behind the scenes — the failure modes. If public_url isn't set, the tap-to-open falls back to opening the ntfy app instead of /updates — the same graceful degradation other events use. If the notifier itself fails (network glitch contacting your ntfy server, malformed config, server down), the state is already saved so the user will still see the update on /updates the next time they visit; only the push is lost. If both opt-in flags are true but notifications.enabled is false, the notifier's master-switch gate suppresses cleanly. If update_available is the only event you've enabled and you've also disabled the master switch, nothing fires — exactly as expected.
The v3.0.x arc is now complete. v3.0.0 made Aerodrome aware of its own next version. v3.0.1 let users apply that version with one click. v3.0.2 means users don't have to look — Aerodrome tells them. Three notification surfaces (gear-menu badge, /updates banner, ntfy push), each independently toggleable; one apply path; one SQLite table holding the cached state; one scheduler thread doing all the periodic work. The channel is dogfooded end-to-end against the real public GitHub Releases API, and the maintainer's own deploys (this one included) flow through it now.