From 48c8a64c973b68135a3c58bf772a6f89c59f85a0 Mon Sep 17 00:00:00 2001 From: quanticsoul4772 <71096528+quanticsoul4772@users.noreply.github.com> Date: Sun, 18 Oct 2026 07:36:10 -0700 Subject: [PATCH 1/6] fix(cctv): Ontario 511 cameras take the developer key the API now requires The Ontario 511 camera list answered 400 "Invalid Key" on every catalog load ("[CCTV] Ontario 511 camera download failed: 400"), so the pack loaded nothing. 511on.ca's developer docs now say the API "Requires a developer key" (free, from 511on.ca/developers; 10 calls a minute). The loader reads ONTARIO_511_API_KEY and sends it as `key`; without it the pack is skipped with one log line instead of a failed request on every load. The key is never logged. .env.example and DATA_SOURCES no longer call the endpoint keyless. Co-Authored-By: Claude Opus 5.5 --- .env.example | 1 + DATA_SOURCES.md | 2 +- server/providers/cctv/ontario511.test.mjs | 65 +++++++++++++++++++++++ server/providers/cctv/sources.js | 34 +++++++++--- 4 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 server/providers/cctv/ontario511.test.mjs diff --git a/.env.example b/.env.example index a2f8afa24..6731e449e 100644 --- a/.env.example +++ b/.env.example @@ -201,6 +201,7 @@ AISSTREAM_API_KEY= # CCTV_TFL_ENABLED=1 # set 0 to disable Transport for London JamCams # CCTV_TFL_MAX_SOURCES=250 # cap cameras loaded from TfL # CCTV_ONTARIO_ENABLED=1 # set 0 to disable Ontario 511 cameras +# ONTARIO_511_API_KEY= # free developer key from 511on.ca/developers; the camera list needs it # CCTV_ONTARIO_MAX_SOURCES=1000 # cap cameras loaded from Ontario 511 # CCTV_FINTRAFFIC_ENABLED=1 # set 0 to drop the Fintraffic (Finland) pack # CCTV_FINTRAFFIC_MAX_SOURCES=300 # cap cameras loaded from the Fintraffic catalog diff --git a/DATA_SOURCES.md b/DATA_SOURCES.md index d930bcb8f..558d805fe 100644 --- a/DATA_SOURCES.md +++ b/DATA_SOURCES.md @@ -152,7 +152,7 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei - **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. +- **Ontario 511 CCTV.** The camera list comes from the `511on.ca/api/v2/get/cameras` endpoint, which now requires a free developer key (`ONTARIO_511_API_KEY`, from 511on.ca/developers; throttled to 10 calls a minute). Without the key the pack is skipped. 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. - **DelDOT live video cameras.** The keyless `tmc.deldot.gov/json/videocamera.json` catalog supplies active cameras and official `https://video.deldot.gov/live/…/playlist.m3u8` links. The local server transiently buffers bounded MPEG-TS segments in memory and deletes them on session release or idle expiry. No recordings are retained. `CCTV_DELDOT_ENABLED=0` disables the pack. Public availability is not a blanket redistribution license; deployment beyond personal local viewing needs confirmation of applicable DelDOT terms. - **Fintraffic weathercams.** The camera list is one keyless GeoJSON request to `tie.digitraffic.fi/api/weathercam/v1/stations` (809 stations / 2,275 presets), sent with the `Digitraffic-User: gods-eye-view` header the service asks clients to identify themselves with and with gzip; frames are stills on `weathercam.digitraffic.fi`, refreshed on each station's 600 s collection interval, which is also the client's ambient-still refresh cadence for this pack (the active camera keeps the usual 10-second refresh). One preset (a fixed camera view) is one camera; stations not `GATHERING` and presets not `inCollection` are dropped. Attribution is required by the CC BY 4.0 licence and is registered in the Data attribution popover. `CCTV_FINTRAFFIC_ENABLED=0` is the kill switch. - **DriveBC highway cameras.** The camera list comes from the keyless `https://www.drivebc.ca/api/webcams/` endpoint served by the DriveBC.ca site ([bcgov/DriveBC.ca](https://github.com/bcgov/DriveBC.ca)); frames come from `https://www.drivebc.ca/images/.jpg`, built from the numeric camera id. DataBC's [DriveBC HighwayCams](https://catalogue.data.gov.bc.ca/dataset/bc-highwaycams) catalogue entry publishes the same cameras under the Open Government Licence – British Columbia, but its CSV still lists the retired `images.drivebc.ca` frame URLs, which now return a placeholder. The licence's attribution statement is required and is registered in the Data attribution popover. By default the 250 cameras nearest Vancouver and Victoria load; `CCTV_DRIVEBC_MAX_SOURCES` changes the cap and `CCTV_DRIVEBC_ENABLED=0` turns the pack off. Some cameras in the feed are supplied by partners (TransLink, the City of Vancouver, the City of Surrey, Parks Canada / Alberta Motor Association and others); the feed's per-camera credit is carried onto the camera record and shown beside the provider in the CCTV panel. diff --git a/server/providers/cctv/ontario511.test.mjs b/server/providers/cctv/ontario511.test.mjs new file mode 100644 index 000000000..d654cf8be --- /dev/null +++ b/server/providers/cctv/ontario511.test.mjs @@ -0,0 +1,65 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { loadOntarioSourcesFromOpenData } from './sources.js'; + +const ROW = { + Id: '42', + Latitude: 43.6532, + Longitude: -79.3832, + Location: 'Hwy 401 near Yonge St', + Direction: 'Eastbound', + Views: [ + { + Status: 'Enabled', + Url: 'https://511on.ca/map/Cctv/4201', + Description: 'Eastbound', + }, + ], +}; + +test('without ONTARIO_511_API_KEY the Ontario pack is skipped, logged once', async (t) => { + const warnings = []; + t.mock.method(console, 'warn', (...args) => warnings.push(args.join(' '))); + t.mock.method(globalThis, 'fetch', async () => + assert.fail('no request without a key'), + ); + assert.deepEqual(await loadOntarioSourcesFromOpenData({}, {}), []); + assert.deepEqual(await loadOntarioSourcesFromOpenData({}, {}), []); + assert.equal(warnings.length, 1); + assert.match(warnings[0], /ONTARIO_511_API_KEY/); +}); + +test('with a key the camera list is requested with it and parsed', async (t) => { + const requested = []; + t.mock.method(globalThis, 'fetch', async (url) => { + requested.push(String(url)); + return Response.json([ROW]); + }); + const cameras = await loadOntarioSourcesFromOpenData( + { sourceRoot: '/ignored' }, + { ONTARIO_511_API_KEY: ' test-key ' }, + ); + const url = new URL(requested[0]); + assert.equal(url.host, '511on.ca'); + assert.equal(url.searchParams.get('key'), 'test-key'); + assert.equal(url.searchParams.get('format'), 'json'); + assert.equal(cameras.length, 1); + assert.equal(cameras[0].id, 'on-42'); + assert.equal(cameras[0].url, 'https://511on.ca/map/Cctv/4201'); +}); + +test('a rejected key is reported by status, never by the key itself', async (t) => { + const warnings = []; + t.mock.method(console, 'warn', (...args) => warnings.push(args.join(' '))); + t.mock.method(globalThis, 'fetch', async () => + Response.json({ Message: 'Invalid Key' }, { status: 400 }), + ); + const cameras = await loadOntarioSourcesFromOpenData( + {}, + { ONTARIO_511_API_KEY: 'secret-key-value' }, + ); + assert.deepEqual(cameras, []); + assert.match(warnings.join('\n'), /400/); + assert.doesNotMatch(warnings.join('\n'), /secret-key-value/); +}); diff --git a/server/providers/cctv/sources.js b/server/providers/cctv/sources.js index c3d11e752..19307aaea 100644 --- a/server/providers/cctv/sources.js +++ b/server/providers/cctv/sources.js @@ -502,17 +502,39 @@ function pickOntarioCctvView(views) { ); } +let ontarioKeyMissingLogged = false; + /** - * Fetch Ontario 511 CCTV cameras. Keyless: the catalog is exposed by the - * public 511 API, while frame URLs are stable still-image endpoints under - * 511on.ca/map/Cctv/. Only rows with finite Ontario coords and at least one - * enabled official still view are kept. + * Fetch Ontario 511 CCTV cameras. The camera list comes from the 511 API, + * which now requires a free developer key (511on.ca/developers, "Requires a + * developer key"; unkeyed calls answer 400 "Invalid Key"), read from + * ONTARIO_511_API_KEY. Without one the pack is skipped with a single log line. + * Frame URLs are stable still-image endpoints under 511on.ca/map/Cctv/. Only + * rows with finite Ontario coords and at least one enabled official still + * view are kept. * + * @param {object} [_options] The catalog's loader options (unused). + * @param {object} [env] Environment to read the key from. * @returns {Promise>} Normalized camera source objects. */ -export async function loadOntarioSourcesFromOpenData() { +export async function loadOntarioSourcesFromOpenData( + _options = {}, + env = process.env, +) { + const key = String(env.ONTARIO_511_API_KEY ?? '').trim(); + if (!key) { + if (!ontarioKeyMissingLogged) { + ontarioKeyMissingLogged = true; + console.warn( + '[CCTV] Ontario 511 cameras skipped: the 511on.ca API needs a free developer key (set ONTARIO_511_API_KEY)', + ); + } + return []; + } try { - const resp = await fetch(ONTARIO_511_CAMERAS_URL, { + const url = new URL(ONTARIO_511_CAMERAS_URL); + url.searchParams.set('key', key); + const resp = await fetch(url, { headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(CCTV_SOURCE_FETCH_TIMEOUT_MS), }); From fa085cbd648460b14f9210717985112d7ab924f0 Mon Sep 17 00:00:00 2001 From: quanticsoul4772 <71096528+quanticsoul4772@users.noreply.github.com> Date: Sun, 18 Oct 2026 07:37:34 -0700 Subject: [PATCH 2/6] docs(cctv): point to the 511on.ca pages that exist for the Ontario key 511on.ca/developers is a 404. Sign up at 511on.ca/my511/register, then request the key while logged in at 511on.ca/developers/doc (both checked). Co-Authored-By: Claude Opus 5.5 --- .env.example | 2 +- DATA_SOURCES.md | 2 +- server/providers/cctv/sources.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 6731e449e..520b95e96 100644 --- a/.env.example +++ b/.env.example @@ -201,7 +201,7 @@ AISSTREAM_API_KEY= # CCTV_TFL_ENABLED=1 # set 0 to disable Transport for London JamCams # CCTV_TFL_MAX_SOURCES=250 # cap cameras loaded from TfL # CCTV_ONTARIO_ENABLED=1 # set 0 to disable Ontario 511 cameras -# ONTARIO_511_API_KEY= # free developer key from 511on.ca/developers; the camera list needs it +# ONTARIO_511_API_KEY= # free developer key: sign up at 511on.ca/my511/register, then request it at 511on.ca/developers/doc # CCTV_ONTARIO_MAX_SOURCES=1000 # cap cameras loaded from Ontario 511 # CCTV_FINTRAFFIC_ENABLED=1 # set 0 to drop the Fintraffic (Finland) pack # CCTV_FINTRAFFIC_MAX_SOURCES=300 # cap cameras loaded from the Fintraffic catalog diff --git a/DATA_SOURCES.md b/DATA_SOURCES.md index 558d805fe..8034a7f6d 100644 --- a/DATA_SOURCES.md +++ b/DATA_SOURCES.md @@ -152,7 +152,7 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei - **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 `511on.ca/api/v2/get/cameras` endpoint, which now requires a free developer key (`ONTARIO_511_API_KEY`, from 511on.ca/developers; throttled to 10 calls a minute). Without the key the pack is skipped. 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. +- **Ontario 511 CCTV.** The camera list comes from the `511on.ca/api/v2/get/cameras` endpoint, which now requires a free developer key (`ONTARIO_511_API_KEY`: sign up at 511on.ca/my511/register, then request it while logged in at 511on.ca/developers/doc; throttled to 10 calls a minute). Without the key the pack is skipped. 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. - **DelDOT live video cameras.** The keyless `tmc.deldot.gov/json/videocamera.json` catalog supplies active cameras and official `https://video.deldot.gov/live/…/playlist.m3u8` links. The local server transiently buffers bounded MPEG-TS segments in memory and deletes them on session release or idle expiry. No recordings are retained. `CCTV_DELDOT_ENABLED=0` disables the pack. Public availability is not a blanket redistribution license; deployment beyond personal local viewing needs confirmation of applicable DelDOT terms. - **Fintraffic weathercams.** The camera list is one keyless GeoJSON request to `tie.digitraffic.fi/api/weathercam/v1/stations` (809 stations / 2,275 presets), sent with the `Digitraffic-User: gods-eye-view` header the service asks clients to identify themselves with and with gzip; frames are stills on `weathercam.digitraffic.fi`, refreshed on each station's 600 s collection interval, which is also the client's ambient-still refresh cadence for this pack (the active camera keeps the usual 10-second refresh). One preset (a fixed camera view) is one camera; stations not `GATHERING` and presets not `inCollection` are dropped. Attribution is required by the CC BY 4.0 licence and is registered in the Data attribution popover. `CCTV_FINTRAFFIC_ENABLED=0` is the kill switch. - **DriveBC highway cameras.** The camera list comes from the keyless `https://www.drivebc.ca/api/webcams/` endpoint served by the DriveBC.ca site ([bcgov/DriveBC.ca](https://github.com/bcgov/DriveBC.ca)); frames come from `https://www.drivebc.ca/images/.jpg`, built from the numeric camera id. DataBC's [DriveBC HighwayCams](https://catalogue.data.gov.bc.ca/dataset/bc-highwaycams) catalogue entry publishes the same cameras under the Open Government Licence – British Columbia, but its CSV still lists the retired `images.drivebc.ca` frame URLs, which now return a placeholder. The licence's attribution statement is required and is registered in the Data attribution popover. By default the 250 cameras nearest Vancouver and Victoria load; `CCTV_DRIVEBC_MAX_SOURCES` changes the cap and `CCTV_DRIVEBC_ENABLED=0` turns the pack off. Some cameras in the feed are supplied by partners (TransLink, the City of Vancouver, the City of Surrey, Parks Canada / Alberta Motor Association and others); the feed's per-camera credit is carried onto the camera record and shown beside the provider in the CCTV panel. diff --git a/server/providers/cctv/sources.js b/server/providers/cctv/sources.js index 19307aaea..7c048c68f 100644 --- a/server/providers/cctv/sources.js +++ b/server/providers/cctv/sources.js @@ -506,7 +506,7 @@ let ontarioKeyMissingLogged = false; /** * Fetch Ontario 511 CCTV cameras. The camera list comes from the 511 API, - * which now requires a free developer key (511on.ca/developers, "Requires a + * which now requires a free developer key (511on.ca/developers/doc, "Requires a * developer key"; unkeyed calls answer 400 "Invalid Key"), read from * ONTARIO_511_API_KEY. Without one the pack is skipped with a single log line. * Frame URLs are stable still-image endpoints under 511on.ca/map/Cctv/. Only From cd0956eef7b2f064d09cd7d0b14b89f55712fce1 Mon Sep 17 00:00:00 2001 From: quanticsoul4772 <71096528+quanticsoul4772@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:46:13 -0700 Subject: [PATCH 3/6] docs(changelog): fragment for #1228 Co-Authored-By: Claude Opus 5.5 --- changelog.d/1228-cctv-ontario-511-key.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelog.d/1228-cctv-ontario-511-key.md diff --git a/changelog.d/1228-cctv-ontario-511-key.md b/changelog.d/1228-cctv-ontario-511-key.md new file mode 100644 index 000000000..d9dc7986f --- /dev/null +++ b/changelog.d/1228-cctv-ontario-511-key.md @@ -0,0 +1,4 @@ +- Load Ontario 511 cameras again. 511on.ca's camera endpoint now requires a + free developer key; set `ONTARIO_511_API_KEY` to enable the pack, which is + otherwise skipped with one log line saying where to get a key + (quanticsoul4772, #1228). From 91461381ee8708c366ff5f4e144083774c5488f5 Mon Sep 17 00:00:00 2001 From: quanticsoul4772 <71096528+quanticsoul4772@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:43:15 -0700 Subject: [PATCH 4/6] fix(cctv): point a rejected Ontario key at ONTARIO_511_API_KEY; README notes the key A wrong or revoked key logged only "download failed: 400"; a 400, 401 or 403 now adds "(check ONTARIO_511_API_KEY)". The README's CCTV row listed Ontario among the default cameras with no mention of the key it now needs. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- server/providers/cctv/ontario511.test.mjs | 2 +- server/providers/cctv/sources.js | 9 ++++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c49c53ece..1be14d337 100644 --- a/README.md +++ b/README.md @@ -377,7 +377,7 @@ Twenty layers and map sources. **Eighteen have a keyless path.** Some offer addi | 🌍 **Earthquakes** | Global seismic activity, last 24h | USGS | 🟢 | | ◍ **Air quality** | Live AQHI on the published 1–10+ health-risk scale, with a Weather-panel card showing the band legend and the highest readings in view. **Canada only** — ECCC is the first registered provider | ECCC MSC GeoMet | 🟢 | | 🚗 **Traffic** | Simulated vehicles on OSM roads. With TomTom, live flow speeds drive the simulation and congestion colors below ~8 km; individual vehicle positions are not live observations | TomTom + OSM | 🟢 simulation · 🟡 live flow speeds | -| 📹 **CCTV Mesh** | ~4,000 public cameras (shared cap) projected _into_ the 3D space — Austin · Texas (TxDOT) · California (Caltrans) · London (TfL) · Ontario (511) · Finland (Fintraffic) · British Columbia (DriveBC) · Estonia (Tallinn, Tarktee) · Delaware (DelDOT live video) · New South Wales (Live Traffic NSW) · Calgary · Norway (Statens vegvesen) · Toronto · King County (WA) · Washington (WSDOT) · Catalonia (Servei Català de Trànsit), plus opt-in New York City (NYC DOT TMC, needs your own NYC DOT agreement) and opt-in Lithuania (Via Lietuva / Eismoinfo, photo licence unconfirmed). Positions are published; poses are estimated priors **you calibrate by dragging a gizmo on the camera itself** | City APIs | 🟢 | +| 📹 **CCTV Mesh** | ~4,000 public cameras (shared cap) projected _into_ the 3D space — Austin · Texas (TxDOT) · California (Caltrans) · London (TfL) · Ontario (511, free developer key) · Finland (Fintraffic) · British Columbia (DriveBC) · Estonia (Tallinn, Tarktee) · Delaware (DelDOT live video) · New South Wales (Live Traffic NSW) · Calgary · Norway (Statens vegvesen) · Toronto · King County (WA) · Washington (WSDOT) · Catalonia (Servei Català de Trànsit), plus opt-in New York City (NYC DOT TMC, needs your own NYC DOT agreement) and opt-in Lithuania (Via Lietuva / Eismoinfo, photo licence unconfirmed). Positions are published; poses are estimated priors **you calibrate by dragging a gizmo on the camera itself** | City APIs | 🟢 | | 📷 **Mapped ALPR Cameras** | License-plate-reader camera locations tagged by OpenStreetMap contributors, loaded one city-sized view at a time, with **SHOW NEAREST**. Locations and tags only: no plate data, no video | OpenStreetMap (incl. DeFlock mapping) | 🟢 | | 📷 **Street Level** | Street-level photos behind one panel, one chip per provider: coverage on the streets, image cones, an embedded viewer and **FOLLOW**. Mapillary, Google Street View, KartaView and Panoramax | Mapillary · Google · KartaView · Panoramax | 🟢 KartaView, Panoramax · 🟡 Mapillary token · 🔴 Google Maps key | | 📻 **Radio** | Geolocated world radio with an **analog tuner** — drag the needle across up to 750 stations and the globe flies to each broadcaster | Radio Browser / broadcasters | 🟢 | diff --git a/server/providers/cctv/ontario511.test.mjs b/server/providers/cctv/ontario511.test.mjs index d654cf8be..d0cb1f73e 100644 --- a/server/providers/cctv/ontario511.test.mjs +++ b/server/providers/cctv/ontario511.test.mjs @@ -60,6 +60,6 @@ test('a rejected key is reported by status, never by the key itself', async (t) { ONTARIO_511_API_KEY: 'secret-key-value' }, ); assert.deepEqual(cameras, []); - assert.match(warnings.join('\n'), /400/); + assert.match(warnings.join('\n'), /400 \(check ONTARIO_511_API_KEY\)/); assert.doesNotMatch(warnings.join('\n'), /secret-key-value/); }); diff --git a/server/providers/cctv/sources.js b/server/providers/cctv/sources.js index 7c048c68f..fe8b150b7 100644 --- a/server/providers/cctv/sources.js +++ b/server/providers/cctv/sources.js @@ -539,7 +539,14 @@ export async function loadOntarioSourcesFromOpenData( signal: AbortSignal.timeout(CCTV_SOURCE_FETCH_TIMEOUT_MS), }); if (!resp.ok) { - console.warn('[CCTV] Ontario 511 camera download failed:', resp.status); + // The API answers 400 "Invalid Key" to a wrong or revoked key. + console.warn( + '[CCTV] Ontario 511 camera download failed:', + resp.status, + resp.status === 400 || resp.status === 401 || resp.status === 403 + ? '(check ONTARIO_511_API_KEY)' + : '', + ); return []; } const rows = await resp.json(); From 83b99407330bcf89025e25564f36c2f420e4bbff Mon Sep 17 00:00:00 2001 From: quanticsoul4772 <71096528+quanticsoul4772@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:48:02 -0700 Subject: [PATCH 5/6] docs(cctv): Ontario 511 docs match the key requirement and the developer agreement Review fixes for #1228: CURRENT-STATE.md and the constants comment no longer call Ontario 511 keyless. DATA_SOURCES.md notes Ontario 511's Developer's Access Agreement (511on.ca/developers/resources): the data stays under the Open Government Licence - Ontario, key requests are reviewed case by case, attribution by link is encouraged and the logo rules don't apply because the app shows none, and the "as is" disclaimer should be passed on, which the credit now does. Co-Authored-By: Claude Opus 5.5 --- DATA_SOURCES.md | 2 +- docs/CURRENT-STATE.md | 6 ++++-- server/providers/cctv/constants.js | 2 +- src/data/dataCredits.js | 3 ++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/DATA_SOURCES.md b/DATA_SOURCES.md index 8034a7f6d..f4da7f433 100644 --- a/DATA_SOURCES.md +++ b/DATA_SOURCES.md @@ -152,7 +152,7 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei - **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 `511on.ca/api/v2/get/cameras` endpoint, which now requires a free developer key (`ONTARIO_511_API_KEY`: sign up at 511on.ca/my511/register, then request it while logged in at 511on.ca/developers/doc; throttled to 10 calls a minute). Without the key the pack is skipped. 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. +- **Ontario 511 CCTV.** The camera list comes from the `511on.ca/api/v2/get/cameras` endpoint, which now requires a free developer key (`ONTARIO_511_API_KEY`: sign up at 511on.ca/my511/register, then request it while logged in at 511on.ca/developers/doc; throttled to 10 calls a minute). Without the key the pack is skipped. 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. Access is governed by Ontario 511's Developer's Access Agreement (511on.ca/developers/resources): the data stays under the Open Government Licence - Ontario unless otherwise specified; key requests are reviewed case by case and ask for the intended use and any third parties that will receive the data; attribution by link to 511on.ca is encouraged, and the Ontario 511 logo may not be altered or used to imply endorsement (this app links and shows no logo); and the data is provided "as is", a disclaimer distributors should pass on, which the credit does. - **DelDOT live video cameras.** The keyless `tmc.deldot.gov/json/videocamera.json` catalog supplies active cameras and official `https://video.deldot.gov/live/…/playlist.m3u8` links. The local server transiently buffers bounded MPEG-TS segments in memory and deletes them on session release or idle expiry. No recordings are retained. `CCTV_DELDOT_ENABLED=0` disables the pack. Public availability is not a blanket redistribution license; deployment beyond personal local viewing needs confirmation of applicable DelDOT terms. - **Fintraffic weathercams.** The camera list is one keyless GeoJSON request to `tie.digitraffic.fi/api/weathercam/v1/stations` (809 stations / 2,275 presets), sent with the `Digitraffic-User: gods-eye-view` header the service asks clients to identify themselves with and with gzip; frames are stills on `weathercam.digitraffic.fi`, refreshed on each station's 600 s collection interval, which is also the client's ambient-still refresh cadence for this pack (the active camera keeps the usual 10-second refresh). One preset (a fixed camera view) is one camera; stations not `GATHERING` and presets not `inCollection` are dropped. Attribution is required by the CC BY 4.0 licence and is registered in the Data attribution popover. `CCTV_FINTRAFFIC_ENABLED=0` is the kill switch. - **DriveBC highway cameras.** The camera list comes from the keyless `https://www.drivebc.ca/api/webcams/` endpoint served by the DriveBC.ca site ([bcgov/DriveBC.ca](https://github.com/bcgov/DriveBC.ca)); frames come from `https://www.drivebc.ca/images/.jpg`, built from the numeric camera id. DataBC's [DriveBC HighwayCams](https://catalogue.data.gov.bc.ca/dataset/bc-highwaycams) catalogue entry publishes the same cameras under the Open Government Licence – British Columbia, but its CSV still lists the retired `images.drivebc.ca` frame URLs, which now return a placeholder. The licence's attribution statement is required and is registered in the Data attribution popover. By default the 250 cameras nearest Vancouver and Victoria load; `CCTV_DRIVEBC_MAX_SOURCES` changes the cap and `CCTV_DRIVEBC_ENABLED=0` turns the pack off. Some cameras in the feed are supplied by partners (TransLink, the City of Vancouver, the City of Surrey, Parks Canada / Alberta Motor Association and others); the feed's per-camera credit is carried onto the camera record and shown beside the provider in the CCTV panel. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 9562dc9f5..552e3d7ce 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -4388,8 +4388,10 @@ silently demoting every later lookup for the session. default 36 → 250, hard bound 300), filtered to `camera_status === TURNED_ON` (~815 live of 1,003 rows). City packs (2026-07-04): Caltrans (districts 4/7/11/3 — SF, LA, San Diego, Sacramento; cap 300) and TfL London JamCams (cap 250) join Austin (cap 250) as keyless default - sources. Ontario 511 (2026-09-12) adds keyless highway cameras including Kitchener-area routes - (cap 1,000, all enabled rows from the current ~944-camera catalog). All sources are RAW PRIOR + sources. Ontario 511 (2026-09-12) adds highway cameras including Kitchener-area routes + (cap 1,000, all enabled rows from the current ~944-camera catalog). The 511on.ca API now + requires a developer key, so the pack needs `ONTARIO_511_API_KEY` and is skipped without one + (#1228). All sources are RAW PRIOR poses, and the layer is stills-first. sources. Fintraffic Finland road weather cameras (2026-09-13; cap 300, `CCTV_FINTRAFFIC_MAX_SOURCES`, kill switch `CCTV_FINTRAFFIC_ENABLED=0`) are the fourth pack: one keyless GeoJSON station list diff --git a/server/providers/cctv/constants.js b/server/providers/cctv/constants.js index 20b963770..b0880533f 100644 --- a/server/providers/cctv/constants.js +++ b/server/providers/cctv/constants.js @@ -119,7 +119,7 @@ export const TFL_IMAGE_ORIGIN = 'https://s3-eu-west-1.amazonaws.com/jamcams.tfl.gov.uk/'; export const DEFAULT_TFL_MAX_SOURCES = 250; export const LONDON_CENTER = { lat: 51.5074, lon: -0.1278 }; -/** Ontario 511: keyless CARS/511 camera catalog; frame URLs are still images. */ +/** Ontario 511: CARS/511 camera catalog, which needs a developer key (ONTARIO_511_API_KEY); frame URLs are still images. */ export const ONTARIO_511_CAMERAS_URL = 'https://511on.ca/api/v2/get/cameras?format=json&lang=en'; export const ONTARIO_511_IMAGE_ORIGIN = 'https://511on.ca/map/Cctv/'; diff --git a/src/data/dataCredits.js b/src/data/dataCredits.js index 8d3949351..931452068 100644 --- a/src/data/dataCredits.js +++ b/src/data/dataCredits.js @@ -239,7 +239,8 @@ export const DATA_CREDITS = [ html: 'CCTV cameras & frames (Ontario): ' + 'Ontario 511 ' + - '(Open Government Licence - Ontario)', + '(Open Government Licence - Ontario) ' + + '· provided as is, without warranty; actual road conditions may differ', }, { key: 'fintraffic-cctv', From 0561277dcbb2774b04ec6a27c2c8f0a06d3f8c4d Mon Sep 17 00:00:00 2001 From: Said Turksever Date: Mon, 19 Oct 2026 16:59:52 +0100 Subject: [PATCH 6/6] test(cctv): pin Ontario 511 key handling against regressions - Assert the key never reaches the camera records sent to the browser or the "Loaded" log line. - Count fetches in the keyless test instead of relying on assert.fail inside the mock, which the loader's catch would swallow; a whitespace-only key is treated as missing. - Cover the key hint for 400/401/403 and its absence for 429/500/503, and log the failure as one string without a trailing empty argument. - Add _resetOntarioKeyWarningForTest so the logged-once check no longer depends on test order. - Read CCTV_ONTARIO_MAX_SOURCES from the same env as the key, and keep console output out of the suite. Co-Authored-By: Claude Opus 5.5 --- server/providers/cctv/ontario511.test.mjs | 102 ++++++++++++++++++---- server/providers/cctv/sources.js | 19 ++-- 2 files changed, 95 insertions(+), 26 deletions(-) diff --git a/server/providers/cctv/ontario511.test.mjs b/server/providers/cctv/ontario511.test.mjs index d0cb1f73e..51334b3cb 100644 --- a/server/providers/cctv/ontario511.test.mjs +++ b/server/providers/cctv/ontario511.test.mjs @@ -1,7 +1,10 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { loadOntarioSourcesFromOpenData } from './sources.js'; +import { + _resetOntarioKeyWarningForTest, + loadOntarioSourcesFromOpenData, +} from './sources.js'; const ROW = { Id: '42', @@ -18,19 +21,35 @@ const ROW = { ], }; +function captureConsole(t) { + const lines = []; + const capture = (...args) => lines.push(args.join(' ')); + t.mock.method(console, 'warn', capture); + t.mock.method(console, 'log', capture); + return lines; +} + +test.beforeEach(() => _resetOntarioKeyWarningForTest()); + test('without ONTARIO_511_API_KEY the Ontario pack is skipped, logged once', async (t) => { - const warnings = []; - t.mock.method(console, 'warn', (...args) => warnings.push(args.join(' '))); - t.mock.method(globalThis, 'fetch', async () => - assert.fail('no request without a key'), + const lines = captureConsole(t); + let fetches = 0; + t.mock.method(globalThis, 'fetch', async () => { + fetches += 1; + return Response.json([ROW]); + }); + assert.deepEqual(await loadOntarioSourcesFromOpenData({}, {}), []); + assert.deepEqual( + await loadOntarioSourcesFromOpenData({}, { ONTARIO_511_API_KEY: ' ' }), + [], ); - assert.deepEqual(await loadOntarioSourcesFromOpenData({}, {}), []); - assert.deepEqual(await loadOntarioSourcesFromOpenData({}, {}), []); - assert.equal(warnings.length, 1); - assert.match(warnings[0], /ONTARIO_511_API_KEY/); + assert.equal(fetches, 0); + assert.equal(lines.length, 1); + assert.match(lines[0], /ONTARIO_511_API_KEY/); }); test('with a key the camera list is requested with it and parsed', async (t) => { + const lines = captureConsole(t); const requested = []; t.mock.method(globalThis, 'fetch', async (url) => { requested.push(String(url)); @@ -40,6 +59,7 @@ test('with a key the camera list is requested with it and parsed', async (t) => { sourceRoot: '/ignored' }, { ONTARIO_511_API_KEY: ' test-key ' }, ); + assert.equal(requested.length, 1); const url = new URL(requested[0]); assert.equal(url.host, '511on.ca'); assert.equal(url.searchParams.get('key'), 'test-key'); @@ -47,19 +67,63 @@ test('with a key the camera list is requested with it and parsed', async (t) => assert.equal(cameras.length, 1); assert.equal(cameras[0].id, 'on-42'); assert.equal(cameras[0].url, 'https://511on.ca/map/Cctv/4201'); + // The records go to the browser through /api/cctv; the key must stay on + // the server-side catalog request. + assert.doesNotMatch(JSON.stringify(cameras), /test-key/); + assert.doesNotMatch(lines.join('\n'), /test-key/); }); -test('a rejected key is reported by status, never by the key itself', async (t) => { - const warnings = []; - t.mock.method(console, 'warn', (...args) => warnings.push(args.join(' '))); - t.mock.method(globalThis, 'fetch', async () => - Response.json({ Message: 'Invalid Key' }, { status: 400 }), - ); +test('the source cap is read from the same environment as the key', async (t) => { + captureConsole(t); + const rows = Array.from({ length: 12 }, (_, i) => ({ + ...ROW, + Id: String(100 + i), + Latitude: ROW.Latitude + i * 0.01, + Views: [{ ...ROW.Views[0], Url: `https://511on.ca/map/Cctv/${100 + i}` }], + })); + t.mock.method(globalThis, 'fetch', async () => Response.json(rows)); const cameras = await loadOntarioSourcesFromOpenData( {}, - { ONTARIO_511_API_KEY: 'secret-key-value' }, + { ONTARIO_511_API_KEY: 'test-key', CCTV_ONTARIO_MAX_SOURCES: '8' }, ); - assert.deepEqual(cameras, []); - assert.match(warnings.join('\n'), /400 \(check ONTARIO_511_API_KEY\)/); - assert.doesNotMatch(warnings.join('\n'), /secret-key-value/); + assert.equal(cameras.length, 8); }); + +for (const status of [400, 401, 403]) { + test(`a ${status} points at the key by name, never by value`, async (t) => { + const lines = captureConsole(t); + t.mock.method(globalThis, 'fetch', async () => + Response.json({ Message: 'Invalid Key' }, { status }), + ); + const cameras = await loadOntarioSourcesFromOpenData( + {}, + { ONTARIO_511_API_KEY: 'secret-key-value' }, + ); + assert.deepEqual(cameras, []); + assert.match( + lines.join('\n'), + new RegExp(`failed: ${status} \\(check ONTARIO_511_API_KEY\\)`), + ); + assert.doesNotMatch(lines.join('\n'), /secret-key-value/); + }); +} + +for (const status of [429, 500, 503]) { + test(`a ${status} is reported without blaming the key`, async (t) => { + const lines = captureConsole(t); + t.mock.method(globalThis, 'fetch', async () => + Response.json({}, { status }), + ); + const cameras = await loadOntarioSourcesFromOpenData( + {}, + { ONTARIO_511_API_KEY: 'secret-key-value' }, + ); + assert.deepEqual(cameras, []); + assert.equal(lines.length, 1); + assert.equal( + lines[0], + `[CCTV] Ontario 511 camera download failed: ${status}`, + ); + assert.doesNotMatch(lines[0], /secret-key-value/); + }); +} diff --git a/server/providers/cctv/sources.js b/server/providers/cctv/sources.js index 3804ed956..e4b7cf48b 100644 --- a/server/providers/cctv/sources.js +++ b/server/providers/cctv/sources.js @@ -511,6 +511,11 @@ function pickOntarioCctvView(views) { let ontarioKeyMissingLogged = false; +/** Test seam: let the next keyless call log the missing-key line again. */ +export function _resetOntarioKeyWarningForTest() { + ontarioKeyMissingLogged = false; +} + /** * Fetch Ontario 511 CCTV cameras. The camera list comes from the 511 API, * which now requires a free developer key (511on.ca/developers/doc, "Requires a @@ -521,7 +526,7 @@ let ontarioKeyMissingLogged = false; * view are kept. * * @param {object} [_options] The catalog's loader options (unused). - * @param {object} [env] Environment to read the key from. + * @param {object} [env] Environment to read the key and the source cap from. * @returns {Promise>} Normalized camera source objects. */ export async function loadOntarioSourcesFromOpenData( @@ -547,12 +552,12 @@ export async function loadOntarioSourcesFromOpenData( }); if (!resp.ok) { // The API answers 400 "Invalid Key" to a wrong or revoked key. - console.warn( - '[CCTV] Ontario 511 camera download failed:', - resp.status, + const keyHint = resp.status === 400 || resp.status === 401 || resp.status === 403 - ? '(check ONTARIO_511_API_KEY)' - : '', + ? ' (check ONTARIO_511_API_KEY)' + : ''; + console.warn( + `[CCTV] Ontario 511 camera download failed: ${resp.status}${keyHint}`, ); return []; } @@ -616,7 +621,7 @@ export async function loadOntarioSourcesFromOpenData( new Map(cameras.map((camera) => [camera.id, camera])).values(), ); const maxRaw = Number( - process.env.CCTV_ONTARIO_MAX_SOURCES || DEFAULT_ONTARIO_MAX_SOURCES, + env.CCTV_ONTARIO_MAX_SOURCES || DEFAULT_ONTARIO_MAX_SOURCES, ); const maxCount = Number.isFinite(maxRaw) ? Math.max(8, Math.min(1000, Math.floor(maxRaw)))