Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 25 additions & 18 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,15 +416,22 @@ Reference-data loaders live under `FlightJar.Core.ReferenceData`:
- **`AirlinesDb`** — OpenFlights `airlines.dat`; callsign prefix → IATA
+ airline + alliance. Hand-curated Star / oneworld / SkyTeam dict.

### P2P federation
### Community Feed (P2P federation internally)

User-facing name is **Community Feed** (entered via the sidebar-footer
"Share planes" / "Community Feed" button → `#community-feed-dialog`).
Backend identifiers stay as `P2P*` / `/api/p2p/config` / `/data/p2p.json`
/ `/p2p/ws` / `P2P_*` env vars — internal-only, never renamed.

Four moving parts split across the .NET service and a separate
Cloudflare Worker:

- **`P2PConfigStore`** (`FlightJar.Persistence.P2P`) — UI-managed
on/off + share-site-name toggles. Persisted to `/data/p2p.json`
(atomic write-to-temp + rename). Defaults `enabled=true`,
`share_site_name=false` — federation is on out of the box.
on/off + consent + share-site-name. Persisted to `/data/p2p.json`
(atomic write-to-temp + rename). Schema v2 adds `consent_given`;
fresh installs default `Enabled=false, ConsentGiven=false`,
v1 files are grandfathered on load (`ConsentGiven = Enabled`).
The relay client refuses to dial out unless both flags are true.
Exposes a `Changed` event the background service hooks into.
- **`P2PRelayCredentialsStore`** + **`P2PRelayRegistrar`** — token
acquisition. Precedence: `P2P_RELAY_TOKEN` env override → token
Expand All @@ -436,9 +443,10 @@ Cloudflare Worker:
next connect re-registers — called from the WS client when the
relay rejects with HTTP 401 (TTL eviction or rotation).
- **`P2PRelayClientService : BackgroundService`** — always
registered. Outer loop consults `P2PConfigStore.Current.Enabled`;
when disabled it parks on a `TaskCompletionSource` until the
store fires `Changed`, when re-enabled it calls
registered. Outer loop consults
`P2PConfigStore.Current.{Enabled,ConsentGiven}`; when either is
false it parks on a `TaskCompletionSource` until the store fires
`Changed`, when both flip on it calls
`P2PRelayRegistrar.EnsureTokenAsync` and dials the relay with the
resulting bearer token. Mid-connection toggles cancel the live
`ClientWebSocket` via the registered `_connectionCts`. Push side:
Expand Down Expand Up @@ -477,15 +485,14 @@ The relay URL, bearer token, and push interval are env-only
are the advanced "self-hosted relay" knobs. `P2P_RELAY_TOKEN` is
normally unset; the registrar auto-registers on first connect and
persists the issued token. Set it explicitly only when pointing at
a relay that issues tokens out-of-band. `P2P_ENABLED` is a hard
env-only kill switch (default `1`): when `0` the
`P2PRelayClientService` is never registered, so the process makes
no outbound WebSocket connections at all. Used by the e2e harness
(`playwright.config.js`) and the `WebApplicationFactory`-based API
tests so the relay's outbound chatter doesn't destabilise
timing-sensitive assertions or pollute snapshot counts. Everything
user-facing still goes through `GET/POST /api/p2p/config`, both
auth-gated by `RequireAuthSession()`.
a relay that issues tokens out-of-band. There is no env-only kill
switch any more: the relay BackgroundService is always registered
but parks on a signal until `P2PConfigStore.Current.Enabled &&
ConsentGiven` is true. Fresh installs (and the e2e harness, which
uses a clean data dir each run) default to off, so the relay stays
parked unless the operator opts in via the Community Feed dialog.
Everything user-facing still goes through `GET/POST /api/p2p/config`,
both auth-gated by `RequireAuthSession()`.

### Watchlist + alerts

Expand Down Expand Up @@ -533,7 +540,7 @@ Handled in `FlightJar.Api.Configuration.AppOptionsBinder` against the
`BLACKSPOTS_ANTENNA_MSL_M` (optional override; preferred when known),
`BLACKSPOTS_RADIUS_KM`, `BLACKSPOTS_GRID_DEG`,
`BLACKSPOTS_MAX_AGL_M`, `BLACKSPOTS_IDLE_TIMEOUT_MIN`, `TERRAIN_CACHE_DIR`,
`P2P_ENABLED`, `P2P_RELAY_URL`, `P2P_RELAY_TOKEN`, `P2P_PUSH_INTERVAL_S`.
`P2P_RELAY_URL`, `P2P_RELAY_TOKEN`, `P2P_PUSH_INTERVAL_S`.
Target altitude is UI-only (slider on the map), not env-driven. The
README has the full reference table.

Expand Down Expand Up @@ -569,7 +576,7 @@ throughput ceiling on CI and build boxes):
| `heatmap.json` | `TrafficHeatmap` | 7×24 weekday/hour grid. |
| `watchlist.json` | `WatchlistStore` | Watchlist of ICAO24 hex codes + last-seen map. |
| `notifications.json` | `NotificationsConfigStore` | UI-managed alert channels. |
| `p2p.json` | `P2PConfigStore` | UI-managed P2P federation on/off + share-site-name (schema v1). Default on first run: `enabled=true`, `share_site_name=false`. |
| `p2p.json` | `P2PConfigStore` | UI-managed Community Feed on/off + consent + share-site-name (schema v2). Default on fresh install: `enabled=false`, `consent_given=false`, `share_site_name=false`. v1 files (which only had `enabled`/`share_site_name`) are grandfathered on load: `consent_given = enabled`. |
| `p2p_credentials.json` | `P2PRelayCredentialsStore` | Bearer token issued by the relay's `/register` endpoint on first connect. Reused across restarts so we don't re-register on every container start. Cleared on HTTP 401 (relay-side eviction or rotation). |
| `vfrmap_cycle.json` | `VfrmapCycle` | Auto-discovered FAA chart cycle date. |
| `openaip.json.gz` | `OpenAipClient` | Cached OpenAIP airspaces / obstacles / reporting points per 2° bbox tile (schema v1, 7-day TTL). |
Expand Down
41 changes: 23 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,39 +390,45 @@ or your own tooling.
Browser notifications (while a tab is open) still fire alongside; the
Alerts channels are what keep working once every tab is closed.

## P2P federation
## Community Feed

Flightjar instances can pool what they see — each one shares its
locally decoded aircraft with a small community network and receives
the aggregated feed from every other connected instance. Aircraft
seen by peers appear on your map with a dashed indigo outline and a
"Network" badge in the detail panel; the "Peers" chip in the sidebar
filter bar toggles them on or off.

**Enabled by default.** Fresh installs participate out of the box —
no setup, no signup, no key to copy. To opt out, open the **About**
dialog from the sidebar footer and uncheck "Enable P2P federation".
seen via the feed appear with a "Network" badge in the detail panel;
on the map they're indistinguishable from aircraft your own radio
picked up.

**Opt-in.** Fresh installs do **not** connect to the network until
you opt in. Click **Share planes** in the sidebar footer to open the
Community Feed dialog — it explains what's shared and what isn't,
then a single "Turn on Community Feed" button starts the relay
connection. Toggling it back off later is one click in the same
dialog. (Existing installs that had the previous "enabled by
default" behaviour are grandfathered in across the upgrade and
continue to share unless you turn it off.)

**Aircraft seen by multiple receivers are combined, not duplicated.**
When your receiver and a peer both see the same aircraft, the records
merge: local data wins where both sides have a value, peer data fills
gaps. Receiver-specific values like distance, signal strength, and
trail always stay local.
When your receiver and another both see the same aircraft, the
records merge: local data wins where both sides have a value, the
peer's data fills gaps. Receiver-specific values like distance,
signal strength, and trail always stay local.

**What's shared, and what isn't.** Each instance sanitises its
outbound payload before it leaves the container:

- **Stripped:** your receiver coordinates, per-aircraft distance
(which would let observers back-solve your location), and your
site name (unless you opt in via the second checkbox in About).
site name (unless you opt in via the second checkbox in the
Community Feed dialog).
- **Kept:** aircraft ICAO24, position, callsign, altitude, speed,
track, squawk, and trail — all derived from public ADS-B
broadcasts that anyone in radio range can already pick up.

**Self-hosting.** If you'd rather run your own relay (e.g. a private
federation between a small group of receivers), the worker source
lives in `relay-worker/`. Deploy it under your own Cloudflare account
and point your Flightjar instance at it with `P2P_RELAY_URL`.
network between a small group of receivers), the worker source lives
in `relay-worker/`. Deploy it under your own Cloudflare account and
point your Flightjar instance at it with `P2P_RELAY_URL`.

## Running multiple receivers

Expand Down Expand Up @@ -465,8 +471,7 @@ It shows up next to "Flightjar" in the sidebar and in the browser tab title
| `BLACKSPOTS_IDLE_TIMEOUT_MIN` | `15` | Minutes the blackspots feature can sit idle before reclaiming its in-memory caches. Disk caches survive eviction, so re-engaging the layer is cheap. `0` disables eviction. |
| `TERRAIN_CACHE_DIR` | `/data/terrain` | Directory for the downloaded elevation tiles. |
| `TELEMETRY_ENABLED` | `1` | Anonymous usage telemetry — see below. |
| `P2P_ENABLED` | `1` | Set to `0` to disable P2P federation entirely (no outbound relay connections). The runtime on/off and share-`SITE_NAME` toggle live in the **About** dialog. |
| `P2P_RELAY_URL` | `wss://relay.flightjar.xyz/ws` | Override the relay this instance connects to. Leave at the default to use the community relay; change it to point at one you've self-hosted. |
| `P2P_RELAY_URL` | `wss://relay.flightjar.xyz/ws` | Override the relay this instance connects to. Leave at the default to use the community relay; change it to point at one you've self-hosted. The Community Feed runtime on/off, consent, and share-`SITE_NAME` toggle live in the **Community Feed** dialog reached from the sidebar footer — fresh installs default to off until you opt in. |
| `P2P_RELAY_TOKEN` | (unset) | Override the bearer token sent to the relay. Normally unset — instances acquire a token automatically on first connect. Set only when pointing at a relay that issues tokens out-of-band. |
| `P2P_PUSH_INTERVAL_S` | `5` | Seconds between snapshot pushes to the relay. |
| `FLIGHTJAR_PASSWORD` | (unset) | Optional shared secret. When non-empty, the watchlist + notification-channel endpoints (`/api/watchlist`, `/api/notifications/*`) require an authenticated session cookie minted by `POST /api/auth/login`. Empty disables auth entirely (default — fine on a private LAN). Set this when exposing the instance to the internet so unauthenticated callers can't read your bot tokens or scrape your watchlist. See **Optional password protection** below. |
Expand Down
39 changes: 33 additions & 6 deletions app/static/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,39 @@
background: #888; margin-right: 6px; vertical-align: middle; }
#status.live .dot { background: #4ade80; }
#status.dead .dot { background: #ef4444; }
#p2p-status { font-size: 11px; color: var(--muted); display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
#p2p-status[hidden] { display: none; }
#p2p-status .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%;
background: #888; margin-right: 6px; vertical-align: middle; }
#p2p-status.live .dot { background: #818cf8; }
#p2p-status.dead .dot { background: #ef4444; }
/* Community Feed status pill. Lives in the header next to the
connection status; doubles as the dialog entry point so the
feature isn't buried in the footer. Two states:
- .is-off: accent-coloured dot, drives the eye on a fresh
install. Click to enable.
- .is-on: muted, dot live (green) when the relay's connected,
dead (amber) while reconnecting. Click to manage. */
.community-feed-pill {
font-family: inherit; font-size: 11px;
display: inline-flex; align-items: center; gap: 6px;
padding: 3px 9px; border-radius: 11px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border);
color: var(--muted); cursor: pointer;
transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
.community-feed-pill:hover {
color: var(--text); background: rgba(255, 255, 255, 0.08);
}
.community-feed-pill-dot {
display: inline-block; width: 8px; height: 8px; border-radius: 50%;
background: #888;
}
.community-feed-pill.is-off {
color: var(--accent); border-color: rgba(37, 99, 235, 0.55);
background: rgba(37, 99, 235, 0.08);
}
.community-feed-pill.is-off:hover {
color: #60a5fa; background: rgba(37, 99, 235, 0.15);
}
.community-feed-pill.is-off .community-feed-pill-dot { background: var(--accent); }
.community-feed-pill.is-on .community-feed-pill-dot { background: #4ade80; }
.community-feed-pill.is-disconnected .community-feed-pill-dot { background: #fbbf24; }
.home-control {
/* Mirror Leaflet's .leaflet-bar a sizing — 26×26 by default, bumped
to 30×30 on touch devices — so the home button lines up flush with
Expand Down
2 changes: 2 additions & 0 deletions app/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { initAboutDialog, initMapKeyDialog, initStatsDialog, initWatchlistDialog
import { initAirspaceFiltersDialog } from './airspace_filters_dialog.js';
import { initAirspaceSlice, refreshSlice, syncSliceEnabled } from './airspace_slice.js';
import { initAlertsDialog } from './alerts_dialog.js';
import { initCommunityFeedDialog } from './community_feed_dialog.js';
import { initAirportTooltip } from './tooltip.js';
import {
initDetailPanel,
Expand Down Expand Up @@ -99,6 +100,7 @@ async function boot() {
setAirspaceCacheListener(refreshSlice);
syncSliceEnabled();
initAboutDialog();
initCommunityFeedDialog();
initMapKeyDialog();
initStatsDialog();
initWatchlistDialog();
Expand Down
Loading
Loading