From 0dcb02c6dabb8c1ec5dcf696f12595dc7966adaa Mon Sep 17 00:00:00 2001 From: cyberspace-cs Date: Thu, 17 Sep 2026 00:06:33 +0800 Subject: [PATCH 1/5] docs: add Contributing link to top navigation bar Make the contribution guide discoverable from the README's main navigation alongside Quick Start, Under the Hood, and Keys & Costs. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4224bacc0..304181c32 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ _“pretty cool”_ — [Brendan Eich](https://x.com/BrendanEich/status/20945920
-**[Quick Start](#-quick-start) · [First Five Minutes](#-the-first-five-minutes) · [Talk to It](#-talk-to-it) · [What's Live](#-whats-on-the-globe) · [Under the Hood](#-under-the-hood) · [Keys & Costs](#-api-keys)** +**[Quick Start](#-quick-start) · [First Five Minutes](#-the-first-five-minutes) · [Talk to It](#-talk-to-it) · [What's Live](#-whats-on-the-globe) · [Under the Hood](#-under-the-hood) · [Keys & Costs](#-api-keys) · [Contributing](CONTRIBUTING.md)**
From ae35a2c93928313a075dbc21eb47d70abe0eb50d Mon Sep 17 00:00:00 2001 From: daikaginza Date: Sat, 19 Sep 2026 20:12:36 +0700 Subject: [PATCH 2/5] docs(attribution): credit adsbdb and carry its route-data terms adsbdb supplies the aircraft type, model name and registration behind every enriched flight, and the airline and origin/destination pair behind the tracked contact's FROM -> TO strip. It appeared in neither attribution surface: DATA_SOURCES.md and src/data/dataCredits.js each held zero occurrences of it, against the rule both files state for themselves and CONTRIBUTING's fourth pull-request item. adsb.lol is a different operator and its ODbL credit does not reach this data. The reporter of #256 left the patch off deliberately, saying the terms were what should not be guessed. They are published, so this takes them from the source rather than inventing them: adsbdb credits PlaneBase for the aircraft data and Guillaume Michel for the ICAO-to-N-number conversion, and states that the flight route data "is the work of David Taylor, Edinburgh and Jim Mason, Glasgow, and may not be copied, published, or incorporated into other databases without the explicit permission of David J Taylor, Edinburgh". No licence or rate limit is published for the service itself. That restriction is the reason the note records what the proxy does with what it fetches: responses are cached per key for 24 hours in .gev-cache/adsbdb.json, which is gitignored, so the data stays on the operator's machine and is never bundled with this repo or redistributed. A test pins the credit. The attribution is a requirement rather than a preference, so it should fail loudly if a later edit drops the names or the restriction, instead of surviving on luck. --- CHANGELOG.md | 10 +++++++++ DATA_SOURCES.md | 4 +++- scripts/format-scope.json | 1 + src/data/dataCredits.js | 10 +++++++++ src/data/dataCredits.test.mjs | 41 +++++++++++++++++++++++++++++++++++ 5 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 src/data/dataCredits.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f3fd7822..f2c044acd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +- Credit adsbdb, which supplies the aircraft type, model name and registration + on enriched flights and the airline and origin/destination pair behind the + tracked contact's route strip. `DATA_SOURCES.md` now records the terms adsbdb + publishes — aircraft data from PlaneBase, and route data that is the work of + David Taylor, Edinburgh, and Jim Mason, Glasgow, and may not be copied into + another database without his permission — along with the request bounds and + the gitignored 24-hour cache that keep the app inside them. A matching + `DATA_CREDITS` entry surfaces the credit in the in-app Data attribution + popover, and a test pins the wording the route data requires. + - Distinguish PARTIAL vessel snapshots from STALE data in the layer panel, with accepted-record counts and unchanged retention, freshness and outage safeguards. diff --git a/DATA_SOURCES.md b/DATA_SOURCES.md index 850d27e39..6e20435d8 100644 --- a/DATA_SOURCES.md +++ b/DATA_SOURCES.md @@ -20,6 +20,7 @@ How to read this: | **OpenSky Network** | Primary worldwide live-flight snapshot | Non-commercial research/education license | Schäfer et al., _"Bringing Up OpenSky"_, IPSN 2014 + opensky-network.org | | **adsb.lol point API** | Bounded live-flight fallback when OpenSky has no usable snapshot | ODbL 1.0 | adsb.lol contributors; `api.adsb.lol/v2/lat/{lat}/lon/{lon}/dist/{radius}` | | **adsb.lol** | Military flights + aircraft traces | ODbL 1.0 | "adsb.lol" (ODbL) | +| **adsbdb** (`api.adsbdb.com`) | Aircraft type, model name and registration by ICAO hex for enriched flights; airline and origin/destination airports for the tracked flight's callsign | No licence or rate limit is published for the service. Its own credits: aircraft data from PlaneBase, ICAO-to-N-number conversion by Guillaume Michel, and flight route data that "is the work of David Taylor, Edinburgh and Jim Mason, Glasgow, and may not be copied, published, or incorporated into other databases without the explicit permission of David J Taylor, Edinburgh" | "adsbdb — api.adsbdb.com" (courtesy); route data credited to David Taylor, Edinburgh, and Jim Mason, Glasgow | | **AISStream.io** | Live vessels (AIS) | Free, beta, no formal ToS; AIS is a public broadcast | "AISStream.io" (courtesy) | | **CelesTrak** | Satellite TLEs (SGP4) | US-government-origin data, no license; citation requested | "CelesTrak (celestrak.org), Dr. T.S. Kelso" | | **The Space Devs — Launch Library 2 v2.3** | Recent launch, payload, stage, and recovery metadata for Space Missions (30d) | [The Space Devs terms of use](https://github.com/TheSpaceDevs/Tutorials/blob/main/faqs/faq_TSD.md#terms-of-use): data may be used and shared in any form; avoid forwarding it without added value; attribution is encouraged (not mandatory). [Official API limits](https://ll.thespacedevs.com/docs/): 15 unauthenticated calls/hour; optional token | "Launch Library 2 — The Space Devs" (courtesy attribution) | @@ -62,6 +63,7 @@ How to read this: - **Google Maps Platform.** You supply your own API key and are bound by [Google's ToS](https://cloud.google.com/maps-platform/terms). Google Maps Content (tiles, geocodes, places) **may not be cached, stored, rehosted, or committed** — this app only ever uses it live, which is the compliant pattern. The "Google" attribution is displayed on the globe and must stay visible. Restrict your key (see [SECURITY.md](SECURITY.md)). - **OpenSky Network.** Its license is **non-commercial**, and operational use of the REST API in a live product can require a prior written agreement with OpenSky — even for non-profit/government use. If you deploy this commercially, contact OpenSky for your own terms. The flights layer is a toggle and runs anonymously by default. - **adsb.lol flight fallback.** When OpenSky is unavailable and no last-good OpenSky response exists, the server requests a cached, capped 250 nm adsb.lol point snapshot around the current camera subpoint. This is regional observed context, not worldwide completeness; provenance is exposed in the Flights stats/context row. Military ICAOs remain reconciled through the existing dedicated military registry rather than duplicated. +- **adsbdb enrichment.** `/api/adsbdb` resolves an ICAO hex to aircraft type, model name and registration, and the tracked flight's callsign to an airline and an origin/destination pair. Fan-out is deliberately small because adsbdb is a free community API that publishes no quota: at most 4 requests in flight, dispatches dripped to roughly 5/s, and each key asked for at most once per session, with route lookups reserved for the tracked aircraft. The dev-server proxy caches each key for 24 hours in `.gev-cache/adsbdb.json`, negative answers included, so a repeat session does not ask again. That cache is gitignored and stays on the operator's machine, which is what the route data's terms require: it may not be copied into another database without David J Taylor's permission, so it is never bundled with this repo or redistributed. Enrichment is best-effort and fail-silent — a flight that gets no answer keeps what the flight feed already gave it. - **Launch Library 2.** `/api/launches` makes a server-side rolling-30-day query against the supported v2.3 detailed launch endpoint, caches successful responses for 15 minutes in memory and on disk, and serves the last successful response during a throttle or transient outage. Anonymous access is limited to 15 calls/hour; deployments can provide `LL2_API_TOKEN` for authenticated access. The Space Devs' published terms permit using and sharing the API data in any form, ask users not to forward it without adding value, disclaim complete accuracy, and encourage—but do not require—attribution. This app keeps a courtesy credit. Payload and stage/recovery records are shown only when supplied. Failed launches expose their source status and never receive fallback orbit geometry or a live/estimated marker. LL2 supplies launch context and event timing, not continuous ascent telemetry or live orbital state. - **TfL JamCams.** The camera list comes from the keyless `api.tfl.gov.uk` endpoint (an optional `TFL_APP_KEY` raises its rate limit); frames come from TfL's public S3 bucket. The "Powered by TfL Open Data" attribution is required by TfL's terms and is registered in the Data attribution popover. - **Ontario 511 CCTV.** The camera list comes from the keyless `511on.ca/api/v2/get/cameras` endpoint; frames are server-registered still images under `511on.ca/map/Cctv/`. The source is live-fetched at runtime and attributed under the Open Government Licence - Ontario. @@ -191,4 +193,4 @@ Douglas-Peucker simplification, 6-decimal rounding). ## In-app attribution -The required Google Maps / Cesium credit renders on the on-globe credit line (`#cesium-credits`, bottom-left) and must stay visible — including in clean-view and recording modes (the whole line, logo + "Google Maps" + the "Data attribution" link, stays on screen; only the GEV panels/HUD fade). The layer-specific credits (adsb.lol, TeleGeography, OSM datacenters/dams/roads, NASA FIRMS, CelesTrak, USGS, City of Austin, Fintraffic, GBFS, Radio Browser, OpenSky, AISStream) are registered into the expandable **"Data attribution"** popover on that credit line via `viewer.creditDisplay.addStaticCredit(new Cesium.Credit(html, /* showOnScreen */ false))` — see `src/data/dataCredits.js`. When you add a new data source, add its license and attribution to this file **and** append an entry to `DATA_CREDITS` in `src/data/dataCredits.js` so it surfaces in the app. +The required Google Maps / Cesium credit renders on the on-globe credit line (`#cesium-credits`, bottom-left) and must stay visible — including in clean-view and recording modes (the whole line, logo + "Google Maps" + the "Data attribution" link, stays on screen; only the GEV panels/HUD fade). The layer-specific credits (adsb.lol, adsbdb, TeleGeography, OSM datacenters/dams/roads, NASA FIRMS, CelesTrak, USGS, City of Austin, Fintraffic, GBFS, Radio Browser, OpenSky, AISStream) are registered into the expandable **"Data attribution"** popover on that credit line via `viewer.creditDisplay.addStaticCredit(new Cesium.Credit(html, /* showOnScreen */ false))` — see `src/data/dataCredits.js`. When you add a new data source, add its license and attribution to this file **and** append an entry to `DATA_CREDITS` in `src/data/dataCredits.js` so it surfaces in the app. diff --git a/scripts/format-scope.json b/scripts/format-scope.json index caffd41d2..1104f30c7 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -495,6 +495,7 @@ "src/layers/cctv/source.test.mjs", "src/data/alprCameras.js", "src/data/alprCameras.test.mjs", + "src/data/dataCredits.test.mjs", "src/layers/alpr/index.js", "src/layers/alpr/model.js", "src/layers/alpr/policy.js", diff --git a/src/data/dataCredits.js b/src/data/dataCredits.js index 76029ace8..196256930 100644 --- a/src/data/dataCredits.js +++ b/src/data/dataCredits.js @@ -42,6 +42,16 @@ export const DATA_CREDITS = [ 'adsb.lol ' + '(ODbL 1.0)', }, + { + key: 'adsbdb', + html: + 'Aircraft type, registration & flight routes: ' + + 'adsbdb ' + + '· aircraft data from PlaneBase · route data is the work of ' + + 'David Taylor, Edinburgh, and Jim Mason, Glasgow, and may not be ' + + 'copied, published, or incorporated into other databases without the ' + + 'explicit permission of David J Taylor, Edinburgh', + }, { key: 'aisstream', html: diff --git a/src/data/dataCredits.test.mjs b/src/data/dataCredits.test.mjs new file mode 100644 index 000000000..89d7fed87 --- /dev/null +++ b/src/data/dataCredits.test.mjs @@ -0,0 +1,41 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { DATA_CREDITS } from './dataCredits.js'; + +test('every credit carries a unique key and some markup to render', () => { + const keys = DATA_CREDITS.map((entry) => entry.key); + assert.equal( + new Set(keys).size, + keys.length, + 'a duplicate key would silently shadow one provider’s credit', + ); + for (const entry of DATA_CREDITS) { + assert.ok(entry.key, 'a credit without a key cannot be registered'); + assert.ok( + entry.html && entry.html.trim().length > 0, + `credit ${entry.key} has nothing to show`, + ); + } +}); + +test('adsbdb is credited, and keeps the wording its route data requires', () => { + const credit = DATA_CREDITS.find((entry) => entry.key === 'adsbdb'); + assert.ok( + credit, + 'adsbdb supplies aircraft type and routes and must be credited', + ); + // Not a stylistic preference: adsbdb states that the route data "may not be + // copied, published, or incorporated into other databases without the + // explicit permission of David J Taylor, Edinburgh". The names and that + // restriction are the attribution, so they are pinned here rather than left + // to survive the next edit by luck. + assert.match(credit.html, /David Taylor, Edinburgh/); + assert.match(credit.html, /Jim Mason, Glasgow/); + assert.match( + credit.html, + /may not be\s+copied, published, or incorporated into other databases/, + ); + assert.match(credit.html, /explicit permission of David J Taylor, Edinburgh/); + assert.match(credit.html, /PlaneBase/); + assert.match(credit.html, /href="https:\/\/www\.adsbdb\.com"/); +}); From 5e99adc978f61eb8676c283a9575653113697b54 Mon Sep 17 00:00:00 2001 From: GGPOShadows <187427758+GGPOShadows@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:02:59 -0700 Subject: [PATCH 3/5] fix(tomtom): size the daily tile budget to the real free allowance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TOMTOM_DAILY_TILE_BUDGET defaulted to 40,000 per day against a free tier that is granted per MONTH, not per day. TomTom currently allows 200,000 Traffic Flow & Incidents tile requests per month (https://docs.tomtom.com/pricing/), so the default exhausted a user's entire monthly allowance in five days and left the traffic layer dead for the rest of the billing period. The docs contradicted themselves on this. traffic.js described the free tier as "~50k/day", while .env.example cited the correct 200K per month two lines below quoting the same 40,000 default. 50k/day would be 1.5M/month, 7.5x the actual allowance. Sets the default to 6,000/day. A 31-day month lands at 186,000, inside the allowance with headroom. Corrects the stale figures in traffic.js, .env.example and DATA_SOURCES.md. This remains an application-side safety ceiling, not a billing cap, and the surrounding text still says so. TomTom's free tier requires no card, so overrunning it cuts the layer off rather than generating a charge — but a layer that dies three weeks into every month is still worth avoiding by default. Co-Authored-By: Claude Opus 5 --- .env.example | 14 ++++++++------ CHANGELOG.md | 8 ++++++++ DATA_SOURCES.md | 2 +- docs/CURRENT-STATE.md | 3 ++- server/providers/traffic.js | 16 ++++++++++++---- 5 files changed, 31 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index 5bc02df9c..1949c48f4 100644 --- a/.env.example +++ b/.env.example @@ -129,18 +129,20 @@ AISSTREAM_API_KEY= # 0 disables the silence watch entirely. # AISSTREAM_SILENCE_TIMEOUT_MS=120000 -# Optional: TomTom live traffic flow (BYOK freemium — free tier is ~50,000 tile -# requests/day, get a key at https://developer.tomtom.com). SERVER-SIDE ONLY: +# Optional: TomTom live traffic flow (BYOK freemium — the free tier is 200,000 +# Traffic Flow & Incidents tile requests per MONTH, not per day; no credit card +# required. Get a key at https://developer.tomtom.com). SERVER-SIDE ONLY: # the browser fetches same-origin /api/tomtom/* and never sees the key. # Keyless fallback: the traffic layer runs its built-in simulation (white dots, # hardcoded per-road-class speeds) — no key required for the layer to work. # TOMTOM_API_KEY= -# Optional soft cap on upstream tile fetches per UTC day (default 40000) — a +# Optional soft cap on upstream tile fetches per UTC day (default 6000) — a # configurable application safety ceiling, not a guarantee of staying within # TomTom's free allowance (currently 200K tile requests/month: -# https://docs.tomtom.com/pricing/). Over the cap the proxy serves cached/stale -# tiles instead of hitting upstream. -# TOMTOM_DAILY_TILE_BUDGET=40000 +# https://docs.tomtom.com/pricing/). 6000/day keeps a 31-day month at 186K, +# inside that allowance. Over the cap the proxy serves cached/stale tiles +# instead of hitting upstream. +# TOMTOM_DAILY_TILE_BUDGET=6000 # Optional: CCTV layer tuning (advanced). Defaults are sensible — leave unset # unless you're customizing the camera source pack. diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f3fd7822..26f47cbbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +- Size the TomTom daily tile budget to the provider's real free allowance. + `TOMTOM_DAILY_TILE_BUDGET` defaulted to 40,000/day against an allowance + granted monthly (200,000 tile requests/month), exhausting a month in five + days and leaving the traffic layer dead for the rest of the period. The + default is now 6,000/day (186,000 over a 31-day month). Corrects the stale + "~50k/day" free-tier figure in the proxy, `.env.example` and + `DATA_SOURCES.md`. Still an application-side ceiling, not a billing cap. + - Distinguish PARTIAL vessel snapshots from STALE data in the layer panel, with accepted-record counts and unchanged retention, freshness and outage safeguards. diff --git a/DATA_SOURCES.md b/DATA_SOURCES.md index 850d27e39..068bf0431 100644 --- a/DATA_SOURCES.md +++ b/DATA_SOURCES.md @@ -76,7 +76,7 @@ How to read this: - **Camera frame content.** Public camera frames can contain people, vehicles and license plates, and the camera layer does nothing with them beyond putting them on the map. A successful upstream response is relayed as the provider served it: nothing in the camera pipeline blurs, redacts, enhances or restores it, and nothing there runs face, plate or object detection or otherwise recognises or classifies what is in the picture. (The ALPR layer described below maps where plate-reader cameras are, from locations contributors tagged in OpenStreetMap; it reads nothing from any camera frame.) When an upstream has no frame for a camera, what you see instead is a Google Street View still or the app's own placeholder card; the panel's source badge names which of them you are looking at, and a refresh that fails after a frame has arrived leaves the last good picture on screen rather than replacing it. The browser resamples each frame for display: onto a fixed 1920x1080 surface for the camera's projection plane in the scene, and onto a 192x108 canvas for the small ambient card. That scaling is the only change made to the picture, and the only pixels the camera layer reads back are a 64x36 downscale hashed to tell whether the picture has changed. Live streams are piped through; the still path reads the image into memory to pass it on — TxDOT wraps its JPEG in a JSON envelope, which is decoded there — and no frame is written to disk (the disk cache, `.gev-cache/`, holds feed JSON for other layers and never camera imagery). Outside that pipeline, the voice assistant is the one feature that sends imagery anywhere: when it answers a question about what is in view it may attach a downscaled screenshot of the rendered scene — including any camera plane on screen — to the request it sends to the voice model. Frames are fetched only from server-registered camera URLs — a client cannot hand the proxy a URL (see [SECURITY.md](SECURITY.md)). What the app does stays inside the line README draws: no named-person search, no face recognition, no tracking individuals. - **Transit (GTFS-Realtime).** The proxy requests only registered operator feeds, validates redirects, bounds responses, and uses conditional requests, short snapshot caching and failure backoff. All seven registered regions are live-enabled; each can be disabled independently. Vehicles use delayed playback between reports, with report age and waiting states shown separately. Session-local selected trails are available for every feed. MBTA additionally retains up to 15 minutes of recently observed positions in the running proxy, so available history can survive a browser reload; server restart clears it. History reads never fetch upstream, and other feeds have no proxy retention. Nothing is archived to disk or collected in the background. Operator text credits are included as a courtesy and do not imply endorsement. Heights are aligned to work with Google 3D tiles; subway markers are shown at street level because these feeds do not supply depth. - **Radio Browser.** `/api/radio/stations` discovers official API mirrors, makes bounded and coalesced healthy/geolocated HTTPS-station queries, caches the normalized public-domain directory for 45 minutes, and may serve the last good catalog for up to seven days during an outage. Refreshes must meet minimum accepted-query and station coverage before replacing a warm catalog; schema-valid responses whose rows all fail the product's health policy do not count as successful queries. A usable partial cold catalog is explicitly `DEGRADED`, and malformed or empty successful payloads are rejected atomically. Every directory and click-count request rejects redirects, validates all resolved addresses as globally routable (including reserved/documentation IPv4 and special/non-global IPv6 exclusions), and pins the TLS connection to a validated address. Only MP3/AAC non-HLS directory rows with public HTTPS stream targets are returned; favicons are intentionally omitted. Pressing play connects one browser audio element directly to the selected broadcaster and calls the directory's click counter through known-ID-only `POST /api/radio/click/:uuid`. GEV never proxies, caches, records, bundles, or redistributes audio. Radio Browser supplies station-level tags, not dependable current-song or upcoming-program metadata, so Radio filtering never claims either. Direct playback exposes the listener's IP address to the broadcaster, whose own stream terms apply. -- **TomTom Traffic.** Optional and BYOK: without `TOMTOM_API_KEY` the traffic layer runs its built-in simulation and no TomTom data (or attribution) appears. With a key, flow vector tiles are fetched through the server-side `/api/tomtom` proxy (120 s cache + a daily tile-budget governor — `TOMTOM_DAILY_TILE_BUDGET`, default 40,000, a configurable application safety ceiling, not a guarantee of staying within TomTom's monthly free allowance; TomTom's [current pricing](https://docs.tomtom.com/pricing/) lists 200K free tile requests per month) and the "Traffic flow data © TomTom" credit is registered in the Data attribution popover the moment live mode activates. TomTom data is served live and cached only transiently (≤120 s TTL under `.gev-cache/`, gitignored) — it is not bundled or redistributed. One 23 KB point-in-time tile snapshot is committed as a decode-test fixture (`src/data/fixtures/`, © TomTom, never served to the app). +- **TomTom Traffic.** Optional and BYOK: without `TOMTOM_API_KEY` the traffic layer runs its built-in simulation and no TomTom data (or attribution) appears. With a key, flow vector tiles are fetched through the server-side `/api/tomtom` proxy (120 s cache + a daily tile-budget governor — `TOMTOM_DAILY_TILE_BUDGET`, default 6,000 — sized so a 31-day month stays inside TomTom's 200K monthly free allowance; still a configurable application safety ceiling, not a guarantee; TomTom's [current pricing](https://docs.tomtom.com/pricing/) lists 200K free tile requests per month) and the "Traffic flow data © TomTom" credit is registered in the Data attribution popover the moment live mode activates. TomTom data is served live and cached only transiently (≤120 s TTL under `.gev-cache/`, gitignored) — it is not bundled or redistributed. One 23 KB point-in-time tile snapshot is committed as a decode-test fixture (`src/data/fixtures/`, © TomTom, never served to the app). - **Routing (OSRM / FOSSGIS).** `/api/route` proxies the public OSRM instances FOSSGIS e.V. runs for the OpenStreetMap community (car, bike, foot profiles). They are keyless and intended for fair, interactive use — the proxy rate-limits clients (60/min, 200/min total), caps route span and response size, caches each route for 10 minutes, and identifies itself in the User-Agent. Heavy or automated use must run its own OSRM. Routes are computed from OpenStreetMap data (ODbL); the Directions layer and voice routes never redistribute them. A route the router cannot find is reported as such; the Directions layer draws nothing in that case, and voice routes fall back to a clearly labeled straight line. - **Re:Earth Terrain.** Keyless (no API key). Used two ways: (1) `src/mapStackController.js` swaps in a `Cesium.CesiumTerrainProvider` pointed at Re:Earth's `cesium-mesh/ellipsoid` quantized-mesh endpoint for globe stacks without a Cesium ion token (e.g. OSM), replacing a flat `EllipsoidTerrainProvider`; falls back to the flat provider if the endpoint can't be reached. (2) The server-side `/api/terrain/heights` proxy (disk-cached, serve-stale) resolves per-point ellipsoidal ground height for entity placement. Both are best-effort with a keyless-safe fallback (bundled EGM96 geoid math) if Re:Earth is unreachable. - **OSRM on the FOSSGIS routing servers.** Keyless (no API key). Every route is requested by a person: clicking A and B in the Directions layer, or a voice "route from A to B". The server-side `/api/route` proxy sends an identifying `User-Agent`, pins the upstream host, accepts 2-12 coordinates, rejects any leg over 600 km or a total over 2,500 km, caps the buffered response at 8 MB, times the upstream out at 12 s, rate-limits per client, caches each route for 10 minutes, and coalesces identical requests that are in flight at the same time. Turn-by-turn maneuvers are asked for only when a caller wants them (`steps=1`), so the callers that do not read them send the smaller request. The "fix the map" link the policy asks for is in the Data attribution popover beside the OpenStreetMap credit. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index b5727f353..b8928752b 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -3800,7 +3800,8 @@ easier to meet (detection is now on more often), but does not create it. - Traffic runs in `sim` mode (white dots, hardcoded speeds) unless `TOMTOM_API_KEY` is configured (env or Keychain `tomtom-api`/`api-key`), which enables `live` mode: TomTom flow vector tiles via the budget-governed `/api/tomtom` proxy - (`.gev-cache/tomtom/`, 120 s TTL, `TOMTOM_DAILY_TILE_BUDGET` default 40k/day), + (`.gev-cache/tomtom/`, 120 s TTL, `TOMTOM_DAILY_TILE_BUDGET` default 6k/day, + sized so a 31-day month stays inside TomTom's 200K/month free allowance), decoded client-side (`flowTiles.js`), matched onto Overpass roads (`flowMatch.js`), and rendered as green/amber/red dot color + speed/density scaling (`trafficFlowStyle.js`); closures spawn no dots; unmatched roads stay diff --git a/server/providers/traffic.js b/server/providers/traffic.js index fda118753..2a3c2e60a 100644 --- a/server/providers/traffic.js +++ b/server/providers/traffic.js @@ -23,9 +23,16 @@ import { * Budget governor (mirrors the OpenSky credit-governor philosophy — last-good * data beats a dead layer): a persistent counter (.gev-cache/tomtom/budget.json, * keyed by UTC date, reset on day change) counts upstream fetch attempts - * against a soft cap (TOMTOM_DAILY_TILE_BUDGET, default 40,000 of the free - * tier's ~50k/day). Over the cap the proxy serves stale tiles when available, - * else 429 {error:'budget'}. + * against a soft cap (TOMTOM_DAILY_TILE_BUDGET). Over the cap the proxy + * serves stale tiles when available, else 429 {error:'budget'}. + * + * The default is derived from TomTom's published free allowance, which is + * granted MONTHLY, not daily: 200,000 Traffic Flow & Incidents tile requests + * per month (https://docs.tomtom.com/pricing/). The previous 40,000/day + * default — and the "~50k/day" figure this comment used to cite — exhausted + * a month's entire allowance in five days, leaving the traffic layer dead + * for the rest of the billing period. 6,000/day keeps a full 31-day month + * inside the allowance (186,000) with headroom to spare. * * GET /api/tomtom/status → {hasKey, dailyCount, budget, date}. Keyless mode: * status reports hasKey:false and the tile endpoint 503s {error:'no_key'} @@ -37,7 +44,8 @@ export function tomtomProxy() { const TILE_TTL_MS = 120_000; const CACHE_DIR = path.join(process.cwd(), '.gev-cache', 'tomtom'); const BUDGET_PATH = path.join(CACHE_DIR, 'budget.json'); - const DEFAULT_DAILY_BUDGET = 40000; + // 200,000/month free tier / 31 days = 6,451. Rounded down for headroom. + const DEFAULT_DAILY_BUDGET = 6000; const MEM_MAX_ENTRIES = 256; const UPSTREAM_TIMEOUT_MS = 15000; From 82f35161667c7fdc5df1d945543d5d0a44bda61e Mon Sep 17 00:00:00 2001 From: GGPOShadows <187427758+GGPOShadows@users.noreply.github.com> Date: Sat, 19 Sep 2026 22:55:56 -0700 Subject: [PATCH 4/5] fix(security): ignore the whole dotenv ladder, not just .env .gitignore listed only the bare filename `.env`, but the app reads four credential files, not one. scripts/setup-doctor.mjs:92 walks `['.env', '.env.local', `.env.${mode}`, `.env.${mode}.local`]`, and server/standalone/vite.config.js:11 calls Vite's `loadEnv(mode, root, '')`, which reads the same ladder. gitignore matches literally, so three of those four files were committable. A key placed in .env.local is fully live for the app -- the doctor reports it as configured and the provider proxies use it -- while `git add -A` stages it. Verified on a clean checkout of main: .env ignored .env.local COMMITTABLE .env.production COMMITTABLE .env.production.local COMMITTABLE This matters more than usual here because the in-app Provider Settings panel trains people to keep real keys in the checkout rather than in a shell profile, and because the repository has ~7.7k forks that inherit the same gap. Replaces the single `.env` entry with `.env` plus `.env.*`, and re-includes `.env.example` with a negation so the tracked template is unaffected. Deliberately scoped to the dotenv ladder alone. Other credential-bearing artifacts (a repo-local OPENSKY_CREDENTIALS_FILE, *.pem/*.key) are a separate question and are left for a follow-up rather than widened here. Co-Authored-By: Claude Opus 5 --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index 491a82c15..1a64fff39 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,13 @@ node_modules dist +# Credential files. `.env` alone is not enough: Vite's loadEnv (see +# server/standalone/vite.config.js) and scripts/setup-doctor.mjs both read the +# full dotenv ladder — .env, .env.local, .env. and .env..local — +# so a key placed in any of them is live for the app. Only .env was ignored, +# leaving the other three committable. .env.example is the tracked template. .env +.env.* +!.env.example *.log screenshots/ qa-shots/ From 9f8f6284b5bf8aab58bfda67c86ce79e8041cef2 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sun, 20 Sep 2026 15:49:22 -0700 Subject: [PATCH 5/5] docs(attribution): clarify adsbdb data-use wording --- CHANGELOG.md | 12 +++++------- DATA_SOURCES.md | 2 +- src/data/dataCredits.js | 3 ++- src/data/dataCredits.test.mjs | 10 ++++------ 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98ee67fdd..a5b1cd7cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,11 @@ - Credit adsbdb, which supplies the aircraft type, model name and registration on enriched flights and the airline and origin/destination pair behind the - tracked contact's route strip. `DATA_SOURCES.md` now records the terms adsbdb - publishes — aircraft data from PlaneBase, and route data that is the work of - David Taylor, Edinburgh, and Jim Mason, Glasgow, and may not be copied into - another database without his permission — along with the request bounds and - the gitignored 24-hour cache that keep the app inside them. A matching - `DATA_CREDITS` entry surfaces the credit in the in-app Data attribution - popover, and a test pins the wording the route data requires. + tracked contact's route strip. `DATA_SOURCES.md` now records adsbdb's + published credits and route-data restriction, along with the request bounds + and gitignored 24-hour local cache. A matching `DATA_CREDITS` entry surfaces + the credit in the in-app Data attribution popover, and a test protects it + against accidental removal. - Size the TomTom daily tile budget to the provider's real free allowance. `TOMTOM_DAILY_TILE_BUDGET` defaulted to 40,000/day against an allowance diff --git a/DATA_SOURCES.md b/DATA_SOURCES.md index d2472869d..7c822558a 100644 --- a/DATA_SOURCES.md +++ b/DATA_SOURCES.md @@ -63,7 +63,7 @@ How to read this: - **Google Maps Platform.** You supply your own API key and are bound by [Google's ToS](https://cloud.google.com/maps-platform/terms). Google Maps Content (tiles, geocodes, places) **may not be cached, stored, rehosted, or committed** — this app only ever uses it live, which is the compliant pattern. The "Google" attribution is displayed on the globe and must stay visible. Restrict your key (see [SECURITY.md](SECURITY.md)). - **OpenSky Network.** Its license is **non-commercial**, and operational use of the REST API in a live product can require a prior written agreement with OpenSky — even for non-profit/government use. If you deploy this commercially, contact OpenSky for your own terms. The flights layer is a toggle and runs anonymously by default. - **adsb.lol flight fallback.** When OpenSky is unavailable and no last-good OpenSky response exists, the server requests a cached, capped 250 nm adsb.lol point snapshot around the current camera subpoint. This is regional observed context, not worldwide completeness; provenance is exposed in the Flights stats/context row. Military ICAOs remain reconciled through the existing dedicated military registry rather than duplicated. -- **adsbdb enrichment.** `/api/adsbdb` resolves an ICAO hex to aircraft type, model name and registration, and the tracked flight's callsign to an airline and an origin/destination pair. Fan-out is deliberately small because adsbdb is a free community API that publishes no quota: at most 4 requests in flight, dispatches dripped to roughly 5/s, and each key asked for at most once per session, with route lookups reserved for the tracked aircraft. The dev-server proxy caches each key for 24 hours in `.gev-cache/adsbdb.json`, negative answers included, so a repeat session does not ask again. That cache is gitignored and stays on the operator's machine, which is what the route data's terms require: it may not be copied into another database without David J Taylor's permission, so it is never bundled with this repo or redistributed. Enrichment is best-effort and fail-silent — a flight that gets no answer keeps what the flight feed already gave it. +- **adsbdb enrichment.** `/api/adsbdb` resolves an ICAO hex to aircraft type, model name and registration, and the tracked flight's callsign to an airline and an origin/destination pair. Fan-out is deliberately small because adsbdb is a free community API that publishes no quota: at most 4 requests in flight, dispatches dripped to roughly 5/s, and each key asked for at most once per session, with route lookups reserved for the tracked aircraft. The dev-server proxy caches each key for 24 hours in `.gev-cache/adsbdb.json`, negative answers included, so fresh entries survive server restarts. That gitignored cache remains local to the deployment and is not bundled with this repo. adsbdb publishes no general service license; its README says the route data may not be copied, published, or incorporated into other databases without the explicit permission of David J Taylor, Edinburgh. Deployers are responsible for obtaining any permission their use requires. Enrichment is best-effort and fail-silent — a flight that gets no answer keeps what the flight feed already gave it. - **Launch Library 2.** `/api/launches` makes a server-side rolling-30-day query against the supported v2.3 detailed launch endpoint, caches successful responses for 15 minutes in memory and on disk, and serves the last successful response during a throttle or transient outage. Anonymous access is limited to 15 calls/hour; deployments can provide `LL2_API_TOKEN` for authenticated access. The Space Devs' published terms permit using and sharing the API data in any form, ask users not to forward it without adding value, disclaim complete accuracy, and encourage—but do not require—attribution. This app keeps a courtesy credit. Payload and stage/recovery records are shown only when supplied. Failed launches expose their source status and never receive fallback orbit geometry or a live/estimated marker. LL2 supplies launch context and event timing, not continuous ascent telemetry or live orbital state. - **TfL JamCams.** The camera list comes from the keyless `api.tfl.gov.uk` endpoint (an optional `TFL_APP_KEY` raises its rate limit); frames come from TfL's public S3 bucket. The "Powered by TfL Open Data" attribution is required by TfL's terms and is registered in the Data attribution popover. - **Ontario 511 CCTV.** The camera list comes from the keyless `511on.ca/api/v2/get/cameras` endpoint; frames are server-registered still images under `511on.ca/map/Cctv/`. The source is live-fetched at runtime and attributed under the Open Government Licence - Ontario. diff --git a/src/data/dataCredits.js b/src/data/dataCredits.js index 196256930..dde7d3b62 100644 --- a/src/data/dataCredits.js +++ b/src/data/dataCredits.js @@ -47,7 +47,8 @@ export const DATA_CREDITS = [ html: 'Aircraft type, registration & flight routes: ' + 'adsbdb ' + - '· aircraft data from PlaneBase · route data is the work of ' + + '· aircraft data from PlaneBase · ICAO-to-N-number conversion by ' + + 'Guillaume Michel · route data is the work of ' + 'David Taylor, Edinburgh, and Jim Mason, Glasgow, and may not be ' + 'copied, published, or incorporated into other databases without the ' + 'explicit permission of David J Taylor, Edinburgh', diff --git a/src/data/dataCredits.test.mjs b/src/data/dataCredits.test.mjs index 89d7fed87..5172867c4 100644 --- a/src/data/dataCredits.test.mjs +++ b/src/data/dataCredits.test.mjs @@ -18,17 +18,14 @@ test('every credit carries a unique key and some markup to render', () => { } }); -test('adsbdb is credited, and keeps the wording its route data requires', () => { +test('adsbdb is credited and carries its published route-data restriction', () => { const credit = DATA_CREDITS.find((entry) => entry.key === 'adsbdb'); assert.ok( credit, 'adsbdb supplies aircraft type and routes and must be credited', ); - // Not a stylistic preference: adsbdb states that the route data "may not be - // copied, published, or incorporated into other databases without the - // explicit permission of David J Taylor, Edinburgh". The names and that - // restriction are the attribution, so they are pinned here rather than left - // to survive the next edit by luck. + // adsbdb publishes this restriction for its route data. Pin the provider's + // credits and restriction here so a later edit cannot silently remove them. assert.match(credit.html, /David Taylor, Edinburgh/); assert.match(credit.html, /Jim Mason, Glasgow/); assert.match( @@ -37,5 +34,6 @@ test('adsbdb is credited, and keeps the wording its route data requires', () => ); assert.match(credit.html, /explicit permission of David J Taylor, Edinburgh/); assert.match(credit.html, /PlaneBase/); + assert.match(credit.html, /Guillaume Michel/); assert.match(credit.html, /href="https:\/\/www\.adsbdb\.com"/); });