Demo mode for users without an ADS-B receiver. Until now, the answer to "I'd like to try Aerodrome but don't have a receiver yet" has been: don't, because the dashboard would just be empty. v3.1.0 changes that. Pass --demo to the bootstrap (or pick option 2 at the interactive "Do you have a receiver?" prompt) and Aerodrome installs in demo mode with a small synthetic feeder running alongside it. The dashboard fills with 50 simulated aircraft, a starter watchlist of 8 deterministic "regulars," ~5% in the US military hex range so the military classifier exercises, and the occasional emergency squawk (max one per hour) so the alert path is visible. When you have a real receiver ready, the in-app switch-to-real wizard at Configuration → Demo handles the destructive transition cleanly — test reachability of the new receiver first, stop and remove the feeder service, nuke the demo database, clear the demo watchlist, and restart aerodrome pointed at your real address. The whole arc from "I want to try this" to "I've connected my real receiver" lives entirely inside the install + dashboard, no terminal required beyond the one-line install.
What you see in demo mode: A persistent yellow banner across every page reading "Demo mode — showing simulated data. Configure real receiver" with a link to the switch-to-real wizard, so there's no risk of forgetting the dashboard is simulated. Every push notification (if you've configured ntfy) arrives prefixed with [DEMO] so messages announce themselves as synthetic rather than alarming you with what looks like a real alert — the prefix disappears automatically when the wizard flips you to real mode, no notifier reload required. Clicking any "Track ↗" link on an aircraft pops a confirmation dialog explaining that the ICAO is simulated and won't be found by external trackers; "Continue anyway" still opens the link (respects curiosity), "Cancel" closes the dialog. An 8-aircraft starter watchlist seeded at install time, labelled "Demo: regular #1" through "#8", surfaces watchlist hits as the deterministic regulars come into range.
Behind the scenes — the architecture choice: the synthetic feeder runs as a separate systemd service (aerodrome-synthetic-feeder.service) bound to 127.0.0.1:8080, not as an in-process module. The main aerodrome.service polls the same URL it would poll a real ADS-B receiver — it has no idea the data is synthetic. This was a deliberate split: the collector, classifier, notifier, and database pipeline stay completely ignorant of demo mode, no demo-specific branches in the data plane. Only the UX layer (banner, notification prefix, Track-link guard) reads the demo.enabled config flag. The fleet seed is locked to 1903 (Wright Brothers, first powered flight) so every demo install everywhere sees the exact same simulated aircraft, and the seed_watchlist tool that picks the starter watchlist's 8 ICAOs uses the same seed so the watchlisted aircraft are the actual "regulars" the feeder generates. Service-restart safety: if the feeder isn't up yet when aerodrome.service starts polling, the collector's existing receiver-unreachable retry behaviour handles it gracefully — no special demo-aware ordering needed.
The switch-to-real wizard: a 4-step flow at /setup/switch-to-real. Step 1 is the confirm-intent screen with an explicit warning that switching deletes the demo database, demo watchlist, and accumulated stats (per design, this is destructive — carrying simulated sightings into real-receiver mode would pollute all-time stats forever). Step 2 prompts for the real receiver's IP, port, optional latitude/longitude, and optional path override. Step 3 executes with live per-step progress: the server tests reachability of the new receiver first (catches typo'd IPs before destroying anything), then stops + disables + removes the synthetic-feeder unit file, deletes aircraft_history.db and its WAL/SHM siblings, rewrites config.yaml to flip demo.enabled=false and set the real receiver details, and restarts aerodrome.service. Step 4 is the confirmation page pointing back at the dashboard. If any step fails, the wizard displays which one and offers a back button to fix the inputs — the demo install is unchanged on early-step failures (reachability test is intentionally first for this reason).
Three install paths to demo mode, all functionally equivalent:
- Curl one-liner, interactive:
bash <(curl -fsSL https://install.aerodromeadsb.com)→ pick option 2 at the prompt. - Curl one-liner, scripted:
bash <(curl -fsSL https://install.aerodromeadsb.com) --demo --lat 44.84 --lon -88.23. - Manual zip install: download the release zip, unzip,
./install.sh --demo --home-lat 44.84 --home-lon -88.23. This third path is fully standalone —install.sh --demopatchesconfig.yamlto flipdemo.enabled=trueand pointreceiver.ip/portat the synthetic feeder, installs the feeder systemd unit, runs the watchlist seeder, and extends the scoped sudoers rule so the switch-to-real wizard'ssystemctl stop/disablecalls work without a password. Manual zip installs of the real-receiver flow (no--demoflag) are completely unchanged from v3.0.x.
Uninstall awareness: ./uninstall.sh is now aware of the synthetic-feeder service. On any install where the feeder unit exists, uninstall stops + disables + removes it alongside the main aerodrome.service — no special demo-mode uninstall flag needed, and non-demo installs see "no feeder service found" rather than error noise.
What v3.1.0 touches:
- New top-level config key:
demo.enabled(bool, default false). Validated inconfig_validator.py. Section commentary inconfig.yaml.exampleexplicitly warns users not to hand-edit — the switch-to-real wizard is the supported transition. - New systemd unit shape:
aerodrome-synthetic-feeder.service, installed byinstall.sh --demo, removed by the switch-to-real wizard or byuninstall.sh. - Notifier
[DEMO]prefix: ~10 lines innotifier.py— applied at the top ofnotify()(catches every event including daily_summary which routes through notify) and at thesend_test()call site (which bypasses notify for event-gate reasons). The flag plumbs through_build_notifier()/_refresh_notifier_config()so live config edits flip the prefix without a notifier rebuild. /api/status.demo_enabledfield: so admin pages can read demo state without their own status fetches. Every admin template already polls/api/statusfor the gear-menu health badge.- Centrally-injected demo banner: in
static/theme.js. Self-installs across all 11 admin templates (theme.js is shared); injects the banner element asdocument.body's first child so it sits above the header on every page regardless of layout. No per-template edits needed. Also exposeswindow.aerodromeDemoModefor the Track-link guard. - External-link guard: delegated click handler in
static/theme.jstargetinga.track-external-link. Bothtemplates/index.html'strackLink()helper andtemplates/aircraft.html's#trackLinkbutton now emit this class, with adata-icaoattribute so the dialog can show the user which aircraft they're trying to track. /configDemo tab: new section (iddemo, between Updates and Backup & Restore in the tab list). On demo installs, shows a status summary ("what's running") and a prominent "Switch to real receiver" launcher. On real installs, shows a friendly explainer of what demo mode is and notes that it can only be entered via a fresh install (no in-app "switch to demo" — that would mean discarding real receiver data, which has no legitimate use case)./setup/switch-to-realroute and template: new dedicated URL for the wizard. Lives at its own URL (rather than embedded in /config) so the browser back button does the right thing during the multi-step destructive flow. Renderstemplates/switch-to-real.htmlwhich is a 4-step single-page wizard with JS step-state-machine./api/setup/switch-to-realPOST endpoint: server-side handler for the transition. Returns{ok, error, steps}so the wizard can advance or display the failure point. Defensive guards: refuses to run on non-demo installs (defends against accidental nukes via stale browser state), validates required inputs, reachability-tests the new receiver before destruction.- Synthetic feeder additions (
tools/synthetic_feeder/generator.py): emergency-squawk event state machine onFleet, with a strict 1/hour cooldown enforced by_next_emergency_eligible_at. Theemergencyfield on each aircraft snapshot is now computed fromsquawkrather than hardcoded"none"— so when an emergency event marks an aircraft with squawk 7500/7600/7700, the emergency field correctly reads"hijack"/"radio"/"emergency"exactly as readsb would emit. - Watchlist seeder (
tools/synthetic_feeder/seed_watchlist.py): new one-shot script. Picks the first 8 ICAOs the deterministic fleet would generate at install time, then pattern-substitutes them intoconfig.yaml'swatchlist: []line preserving all surrounding comments (same approachbootstrap.sh's config patcher uses). Idempotent — bails if the watchlist is already non-empty. - SUDOERS_VERSION bumped 3 → 4: the new feeder-service lifecycle rules require a sudoers refresh on upgrade from v3.0.x to demo-capable v3.1.0. The updater's existing drift-detection surfaces this in the UI; users get the same "Sudoers update required" modal that v3.0.x deploys use, no new install pattern.
- Documentation pass: README's Install section gains a demo-mode paragraph;
docs/INSTALL.mdgains a full Demo mode section between Installation and Remote access (with full subsections on the three install paths, what's running, the user-visible surfaces, and the switch-to-real flow);docs/overview.mdgets a brief mention in Getting Started;ARCHITECTURE.mdgets a Demo mode architectural note explaining the separate-process choice and the single-flag/three-surfaces structure; CONTRIBUTING.md gets a forker-pointer bullet pointing attools/synthetic_feeder/and the seed-lock convention. The README Project Structure listing now includestools/synthetic_feeder/with all four bundled files. - PDF + screenshots regenerated. v3.1.0 runs
bump-version.shwithout--skip-docs-driftper the working convention; drift check passes clean.