diff --git a/content/quiet-corners-maps.md b/content/quiet-corners-maps.md index 19c73d7..4196a8e 100644 --- a/content/quiet-corners-maps.md +++ b/content/quiet-corners-maps.md @@ -209,9 +209,9 @@ Proof stage. The engine builds real PDFs; what is outstanding is the physical ve - **State road cameras (links only)** — five operators whose public camera lists are read for pins and links only: [Missouri DOT's MoDOT Traveler Information](https://traveler.modot.org/timconfig/feed/desktop/StreamingCams2.json), [Maryland CHART's](https://chart.maryland.gov/DataFeeds/GetCamerasJson), [Delaware DOT's](https://tmc.deldot.gov/json/videocamera.json), [New Mexico DOT's NMRoads](https://servicev5.nmroads.com/RealMapWAR/GetCameraInfo) and the [New Jersey Turnpike Authority's camera list](https://www.njta.gov/travel-resources/camera-list/). Each is read with a plain request carrying this site's own user agent and nothing else. On 2026-10-04 each operator's robots.txt was fetched, and the terms page was read where one is linked: the Turnpike Authority's disclaimer, the State of Maryland's and State of Delaware's privacy notices (linked from CHART and DelDOT) and MoDOT's privacy policy (linked from modot.org; no terms page is linked from traveler.modot.org) contain no ban on this use, and no terms page is linked from NMRoads. West Virginia DOT's WV511 is left out because its disclaimer forbids republishing its material. A pin shows the camera's name, the operator's credit, a "View on" link to the operator's public camera page (or its camera map), and no picture; the site keeps no picture or stream address for them, the usage terms are unverified, and the page's security policy lists none of these hosts. A pin outside its own state, or within 30 m of another pin from the same operator, is dropped (an in-service pin is kept over an out-of-service one). Like the National Park Service list, these lists are snapshots this site builds ahead of time (one file per state, committed here and fetched again only by a `--refresh-seeds` run, so a normal site build never asks these operators). The map reads them through the Camera links switch, area by area; opening one shows the neutral card (name, credit and the owner's link) without loading a picture. - **PhenoCam Network research cameras** — [camera list](https://phenocam.nau.edu/api/cameras/), a screen-only surface credited "PhenoCam Network (CC BY)". Only a `--refresh-seeds` run reads the list (a normal site build serves the committed file and asks the network nothing); it keeps the United States cameras the network marks active and that sent a picture in the previous 30 days, as one snapshot file, which the map reads through the Research cameras (PhenoCam) switch. A camera's card links to the network's own page for the camera and its picture is the network's latest image, hotlinked from `phenocam.nau.edu`, never stored here. The list's contact names and addresses are never copied. A failed refresh keeps the last good file. - **AviationWX airport cameras** — [airport list](https://api.aviationwx.org/v1/airports), a screen-only surface credited with the service's own line, "Weather data from AviationWX.org". Only a `--refresh-seeds` run reads the airport list (never a picture; a normal site build asks the service nothing), and it keeps one pin per webcam at each listed United States airport, as one snapshot file, which the map reads through the Airport cameras (AviationWX) switch. A pin's picture is the service's 720-pixel image, hotlinked from `api.aviationwx.org`, never stored, and its card links to the airport's own AviationWX page. A failed refresh keeps the last good file. -- **USGS VolcView and AVO volcano cameras** — [VolcView ashcam list](https://volcview.wr.usgs.gov/ashcam-api/webcamApi/webcams) with the [Alaska Volcano Observatory list](https://avo.alaska.edu/ashcam-api/webcamApi/webcams), a screen-only surface credited per camera to its owner ("Alaska Volcano Observatory", "USGS Hawaiian Volcano Observatory", "FAA weather cameras", "National Park Service", "USGS Volcano Hazards Program", each "via USGS VolcView" except "USGS Volcano Hazards Program"). Only a `--refresh-seeds` run reads the two lists (a normal build asks USGS and AVO nothing), with a plain GET that sends the build's own User-Agent, never a browser's, an Origin or a Referer, with a pause between requests. The AVO list names each camera's `organization`: a known one (AVO, HVO, YVO, FAA) is credited, any other named owner (UAF, DGGS, WSDOT, private) drops the camera; a camera it does not name is credited by the page it links to, and one that links to a private or state owner (a ski resort, WSDOT) is dropped. A camera is kept once per `webcamCode` (the AVO list is a subset of VolcView's: 396 of 460 on 2026-10-05, none only on AVO), and one with the same name within 50 m of another counts as the same camera. A camera with no picture, no position (the feed uses 0,0), a position outside US territory (the 50 states, DC, Alaska west of the dateline and the territories), or a newest picture over 24 hours old (or stamped more than 10 minutes in the future) is dropped, and every kept still is also checked with a HEAD request, at most 4 at a time (404, 403, a non-image or a Last-Modified over 24 hours drops it). Rights are `unverified`, credit plus a link to the camera's VolcView page. [USGS copyrights and credits](https://www.usgs.gov/information-policies-and-instructions/copyrights-and-credits) (read 2026-10-05): "USGS-authored or produced data and information are considered to be in the U.S. Public Domain. While the content of most USGS websites is in the U.S. Public Domain, not all information, illustrations, or photographs on our site are. Some non-USGS photographs, images, and/or graphics that appear on USGS websites are used by the USGS with permission from the copyright holder." The VolcView host publishes no robots.txt (404) and answers its API with 200. avo.alaska.edu has no terms or disclaimer page (its about page carries no copyright text and /disclaimer is 404) and its robots.txt is an HTML 200 page with no rules, so decision D1 applies to its credit: rights `unverified`, credit and link to the owner; its list is read for `organization` only. Pictures are hotlinked from `volcview.wr.usgs.gov`, never stored; the cameras show today under the **Research cameras (PhenoCam) switch**, which asks no host for anything when turned on; opening a pin loads its picture from `volcview.wr.usgs.gov` (`avo.alaska.edu` is asked only by the build, for its list). Cameras west of the dateline are written to their own files, the western Aleutians to `volcview/ak-west.json` and any in Guam or the Northern Mariana Islands to `volcview/gu-mp.json`, so no index file box crosses the antimeridian. In the Alaska box, east of -141, a point is kept only southwest of the panhandle border (Atlin, Stewart, Mount Edziza and Dease Lake, in British Columbia, are dropped). A failed refresh keeps the last good file. +- **USGS VolcView and AVO volcano cameras** — [VolcView ashcam list](https://volcview.wr.usgs.gov/ashcam-api/webcamApi/webcams) with the [Alaska Volcano Observatory list](https://avo.alaska.edu/ashcam-api/webcamApi/webcams), a screen-only surface credited per camera to its owner ("Alaska Volcano Observatory", "USGS Hawaiian Volcano Observatory", "FAA weather cameras", "National Park Service", "USGS Volcano Hazards Program", each "via USGS VolcView" except "USGS Volcano Hazards Program"). Only a `--refresh-seeds` run reads the two lists (a normal build asks USGS and AVO nothing), with a plain GET that sends the build's own User-Agent, never a browser's, an Origin or a Referer, with a pause between requests. The AVO list names each camera's `organization`: a known one (AVO, HVO, YVO, FAA) is credited, any other named owner (UAF, DGGS, WSDOT, private) drops the camera; a camera it does not name is credited by the page it links to, and one that links to a private or state owner (a ski resort, WSDOT) is dropped. A camera is kept once per `webcamCode` (the AVO list is a subset of VolcView's: 396 of 460 on 2026-10-05, none only on AVO), and one with the same name within 50 m of another counts as the same camera. A camera with no picture, no position (the feed uses 0,0), a position outside US territory (the 50 states, DC, Alaska west of the dateline and the territories), or a newest picture over 24 hours old (or stamped more than 10 minutes in the future) is dropped, and every kept still is also checked with a HEAD request, at most 4 at a time (404, 403, a non-image or a Last-Modified over 24 hours drops it). Rights are `unverified`, credit plus a link to the camera's VolcView page. [USGS copyrights and credits](https://www.usgs.gov/information-policies-and-instructions/copyrights-and-credits) (read 2026-10-05): "USGS-authored or produced data and information are considered to be in the U.S. Public Domain. While the content of most USGS websites is in the U.S. Public Domain, not all information, illustrations, or photographs on our site are. Some non-USGS photographs, images, and/or graphics that appear on USGS websites are used by the USGS with permission from the copyright holder." The VolcView host publishes no robots.txt (404) and answers its API with 200. avo.alaska.edu has no terms or disclaimer page (its about page carries no copyright text and /disclaimer is 404) and its robots.txt is an HTML 200 page with no rules, so decision D1 applies to its credit: rights `unverified`, credit and link to the owner; its list is read for `organization` only. Pictures are hotlinked from `volcview.wr.usgs.gov`, never stored; the cameras show today under the **Research cameras (PhenoCam) switch**, which asks no host for anything when turned on; opening a pin loads its picture from `volcview.wr.usgs.gov` (`avo.alaska.edu` is asked only by the build, for its list). Cameras west of the dateline are written to their own files, the western Aleutians to `volcview/ak-west.json` and any in Guam or the Northern Mariana Islands to `volcview/gu-mp.json`, so no index file box crosses the antimeridian. In the Alaska box, east of -141, a point is kept only inside the panhandle border, which closes to the south along the Dixon Entrance line of the 1903 Alaska Boundary Tribunal award (Cape Muzon, USGS GNIS, to the south end of the Portland Channel boundary, International Boundary Commission digital boundary v1.3, read 2026-10-09), so everything south of that line is treated as Canada: Atlin, Stewart, Mount Edziza, Dease Lake, Prince Rupert, Masset and Haida Gwaii (British Columbia) are dropped, and Ketchikan, Metlakatla, Hyder, Craig, Sitka and Yakutat stay. Canada reads the line as the maritime boundary and the US as only dividing the land; this build uses it as the cut-off either way. The rough Washington box is likewise cut along the Strait of Juan de Fuca, Haro Strait and Boundary Pass boundary, so Victoria, Saanich, Sidney, Duncan and the Gulf Islands (British Columbia) are dropped while Port Angeles, Neah Bay, Friday Harbor and Point Roberts stay. A failed refresh keeps the last good file. - **NOAA GLERL Great Lakes cameras** — [camera page](https://www.glerl.noaa.gov/metdata/), a screen-only surface credited "NOAA Great Lakes Environmental Research Laboratory (GLERL)". Only a `--refresh-seeds` run reads the page and HEADs each still (4 at a time, with a pause; a plain GET and HEAD with the build's own User-Agent), keeping a camera whose picture is under 24 hours old (the Muskegon cameras last updated in 2013 drop out). The station list and positions were copied from GLERL's /metdata/ map on 2026-10-05 and are not re-read on a refresh, so a station GLERL adds later is skipped until the table is updated. Rights are `unverified`, credit plus a link to the station's page. [GLERL's site notice](https://www.glerl.noaa.gov/home/notice.html) (read 2026-10-05): "Information presented on these pages is considered public information and may be distributed or copied. Use of appropriate byline/photo/image credits is requested." robots.txt disallows only `/incoming/` and a few file types, not `/metdata/`. Pictures are hotlinked from `www.glerl.noaa.gov`, never stored; the cameras show today under the **Research cameras (PhenoCam) switch**, which asks no host for anything when turned on, and opening a pin loads its picture from `www.glerl.noaa.gov`. -- **NOAA NDBC buoy cameras — not included.** The station list (`activestations.xml`) answers, but the picture host `www.ndbc.noaa.gov/buoycam` answers 403 from its CDN to this build's honest User-Agent. The build does not imitate a browser, so NDBC buoy cameras are dropped until NOAA allows the build agent or a public API (a follow-up). +- **NOAA NDBC buoy cameras — not included.** The station list (`activestations.xml`) answers, but the picture host `www.ndbc.noaa.gov/buoycam` answers 403 from its CDN to this build's honest User-Agent. The build does not imitate a browser, so NDBC buoy cameras are dropped until NOAA allows the build agent or a public API (a follow-up). A build test fails if any NDBC provider, source, origin or picture host is ever published. Every source a set is built from is credited by name on its printed map key, derived from what the build actually consumed rather than written by hand. diff --git a/scripts/qcm/build-camera-snapshot.mjs b/scripts/qcm/build-camera-snapshot.mjs index 9ceae65..1f91576 100644 --- a/scripts/qcm/build-camera-snapshot.mjs +++ b/scripts/qcm/build-camera-snapshot.mjs @@ -944,19 +944,40 @@ export const PROVIDERS = Object.freeze([ // (the Aleutians, lon 172 to 180) and the territories (PR, VI, GU, AS, MP), as the unit spec counts them. const VOLCVIEW_EXTRA_BOXES = Object.freeze([[51.0, 53.6, 172.0, 180.0], ...['pr', 'vi', 'gu', 'as', 'mp'].map((c) => STATE_BBOX[c])]); // The rough Alaska box spills into British Columbia and Yukon (east of -141) and the rough Washington -// box into British Columbia (north of 49.0); those corners are not US territory. East of -141 a point -// in the Alaska box stays only southwest of the panhandle border (a polyline, closed to the south). -const PANHANDLE_BORDER = [[-141, 60.3], [-135.5, 59.8], [-133.4, 58.9], [-130.0, 56.1], [-130.0, 55.0], [-130.6, 54.7], [-130.6, 40], [-141, 40]]; -function insidePanhandle(lat, lon) { +// box into British Columbia (north of 49.0) and onto southern Vancouver Island; those corners are not +// US territory. East of -141 a point in the Alaska box stays only inside the panhandle polygon below, +// and everything south of the polygon (Prince Rupert, Haida Gwaii) is Canada. +// +// Sources, read 2026-10-09 (public geography, nothing invented): +// - Point A, Cape Muzon: USGS GNIS feature 1420865, 54.6678 N 132.6889 W. Point B, the south end of +// the Portland Channel boundary: International Boundary Commission digital boundary v1.3 (NAD83), +// 54.7074 N 130.6156 W. The straight A-B line is the Dixon Entrance line of the 1903 Alaska +// Boundary Tribunal award (about 54 40 N); Canada reads it as the maritime boundary, the US as +// only dividing the land. This filter treats it as the cut-off: a camera south of it is not US. +// - The Portland Channel vertices between B and the 56th parallel, and the Juan de Fuca / Haro Strait +// / Boundary Pass chain below, are vertices of the same IBC shapefile, thinned to about 500 m. +// - The north and west edges are the older rough hull, unchanged. +const PANHANDLE_BORDER = [[-141, 60.3], [-135.5, 59.8], [-133.4, 58.9], [-130.0, 56.1], [-130.0, 55.3], + [-130.1752, 55.0979], [-130.3369, 54.9248], [-130.5803, 54.7903], [-130.6156, 54.7074], [-132.6889, 54.6678], [-141, 54.6678]]; +// Canada west of the Juan de Fuca / Haro Strait / Boundary Pass line (IBC v1.3) and north of it, up to 49.05 +// (the 49.0 to 49.4 band is the other clause of inCanadianCorner). Victoria, Saanich, Sidney, Duncan and +// the Gulf Islands sit inside the rough Washington box (lat up to 49.1, lon from -124.9). +const SW_BC_BORDER = [[-124.9, 48.4934], [-124.7272, 48.4934], [-124.0121, 48.2967], [-123.6791, 48.24], [-123.5412, 48.2246], + [-123.2484, 48.284], [-123.1151, 48.4228], [-123.1599, 48.4535], [-123.2189, 48.5487], [-123.2679, 48.694], + [-123.0085, 48.7671], [-123.0085, 48.8312], [-123.3222, 49.0021], [-123.3222, 49.05], [-124.9, 49.05]]; +function insidePolygon(poly, lat, lon) { let inside = false; - for (let i = 0, j = PANHANDLE_BORDER.length - 1; i < PANHANDLE_BORDER.length; j = i++) { - const [xi, yi] = PANHANDLE_BORDER[i]; - const [xj, yj] = PANHANDLE_BORDER[j]; + for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) { + const [xi, yi] = poly[i]; + const [xj, yj] = poly[j]; if ((yi > lat) !== (yj > lat) && lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi) inside = !inside; } return inside; } -const inCanadianCorner = (lat, lon) => (lon > -141 && lon <= -129.9 && lat >= 54.5 && lat <= 71.5 && !insidePanhandle(lat, lon)) || (lat > 49.0 && lat <= 49.4 && lon >= -124.9 && lon <= -117.0); +const insidePanhandle = (lat, lon) => insidePolygon(PANHANDLE_BORDER, lat, lon); +const inCanadianCorner = (lat, lon) => (lon > -141 && lon <= -129.9 && lat <= 71.5 && !insidePanhandle(lat, lon)) + || (lat > 49.0 && lat <= 49.4 && lon >= -124.9 && lon <= -117.0) + || insidePolygon(SW_BC_BORDER, lat, lon); function inVolcviewCoverage(lat, lon) { if (inCanadianCorner(lat, lon)) return false; return inUnitedStates(lat, lon) || VOLCVIEW_EXTRA_BOXES.some((b) => lat >= b[0] && lat <= b[1] && lon >= b[2] && lon <= b[3]); diff --git a/scripts/qcm/build-camera-snapshot.test.mjs b/scripts/qcm/build-camera-snapshot.test.mjs index 4603025..25c617d 100644 --- a/scripts/qcm/build-camera-snapshot.test.mjs +++ b/scripts/qcm/build-camera-snapshot.test.mjs @@ -2597,6 +2597,97 @@ test('volcview regions: a camera east of the dateline (lon > 0) goes to its own assert.deepEqual(regions.map((r) => [r.region, r.records.map((x) => x.providerId)]), [['us', ['kanaga']], ['ak-west', ['amchitka', 'semi']]]); }); +// QQA-10c (2026-10-09): the Alaska / British Columbia cut-off follows the real boundary, not a +// latitude floor. Town positions below are public gazetteer values, read 2026-10-09: +// Alaska: USGS GNIS (Geographic Names Information System), the National Map "geonames" service, +// feature ids Ketchikan 1423039, Metlakatla 1423661, Hyder 1422711, Sitka 1414736, Yakutat +// 1415858, Craig 1421260, Hydaburg 1422709, Adak 1418109, Cape Muzon 1420865. +// Canada: Natural Resources Canada, Canadian Geographical Names Data Base (CGNDB), ids Prince +// Rupert JCNWW, Masset JCLGE, Queen Charlotte JCQPU, Langara Island JCKSV, Port Clements JCSNP, +// Victoria JBOBQ, Sidney JBWFW, Duncan JBBWA, Saanich JAXME. +// Washington: USGS GNIS, Port Angeles 1524581, Neah Bay 1512497, Friday Harbor 1505051, Point +// Roberts 1512573. +const SCI_TOWN = Object.freeze({ + ketchikan: [55.3422, -131.6461], metlakatla: [55.1292, -131.5722], hyder: [55.9169, -130.0247], sitka: [57.0531, -135.33], + yakutat: [59.5469, -139.7272], craig: [55.4764, -133.1484], hydaburg: [55.2081, -132.8267], adak: [51.88, -176.6581], + princeRupert: [54.312778, -130.325278], masset: [54.011111, -132.146667], queenCharlotte: [53.254722, -132.1025], + langara: [54.229722, -133.017778], portClements: [53.685556, -132.183611], + victoria: [48.428333, -123.364722], sidney: [48.650556, -123.398333], duncan: [48.778611, -123.708056], saanich: [48.456667, -123.373333], + portAngeles: [48.1182, -123.4308], neahBay: [48.3681, -124.625], fridayHarbor: [48.5343, -123.0171], pointRoberts: [48.9854, -123.078], +}); +function sciKept(points) { + const cam = ([code, [lat, lon]]) => ({ webcamCode: code, webcamName: code, latitude: lat, longitude: lon, externalUrl: null, + currentImageUrl: `https://volcview.wr.usgs.gov/ashcam-api/images/webcams/${code}/current.jpg`, + lastImageTimestamp: Date.parse(SCI_AT) / 1000 - 60 }); + const regions = volc().regions({ volcview: { webcams: Object.entries(points).map(cam) }, avo: { webcams: [] } }, { fetchedAt: SCI_AT }); + return regions.flatMap((r) => r.records.map((x) => x.providerId)).sort(); +} +const pick = (...names) => Object.fromEntries(names.map((n) => [n, SCI_TOWN[n]])); + +test('volcview regions (QQA-10c): Prince Rupert, Masset, Queen Charlotte, Langara and Port Clements (British Columbia, south of Dixon Entrance) are dropped', () => { + assert.deepEqual(sciKept(pick('princeRupert', 'masset', 'queenCharlotte', 'langara', 'portClements')), []); +}); + +test('volcview regions (QQA-10c): Ketchikan, Metlakatla, Hyder, Sitka, Yakutat, Craig, Hydaburg and an Aleutian point (Adak) stay', () => { + assert.deepEqual(sciKept(pick('ketchikan', 'metlakatla', 'hyder', 'sitka', 'yakutat', 'craig', 'hydaburg', 'adak')), + ['adak', 'craig', 'hydaburg', 'hyder', 'ketchikan', 'metlakatla', 'sitka', 'yakutat']); +}); + +// The A-B line: Cape Muzon (USGS GNIS 1420865, 54.6678 N 132.6889 W) to the south end of the Portland +// Channel boundary (International Boundary Commission digital boundary v1.3, NAD83: 54.7074 N 130.6156 W). +const AB_A = [54.6678, -132.6889]; +const AB_B = [54.7074, -130.6156]; +const abLat = (lon) => AB_A[0] + ((lon - AB_A[1]) / (AB_B[1] - AB_A[1])) * (AB_B[0] - AB_A[0]); + +test('volcview regions (QQA-10c): a grid of points east of -141 and south of the Dixon Entrance A-B line keeps nothing', () => { + const points = {}; + for (let lon = -140.75; lon <= -130; lon += 0.25) { + // Every point 0.05 degrees (about 5 km) or more south of the line, down to the Alaska box floor (51.2). + const edge = lon < AB_A[1] ? AB_A[0] : lon <= AB_B[1] ? abLat(lon) : AB_B[0]; + for (let lat = edge - 0.05; lat >= 51.2; lat -= 0.25) points[`p${lat.toFixed(2)}_${lon.toFixed(2)}`] = [lat, lon]; + } + assert.ok(Object.keys(points).length > 200, 'the grid is not trivially small'); + assert.deepEqual(sciKept(points), []); +}); + +test('volcview regions (QQA-10c): just north of the A-B line, Dixon Entrance and the Prince of Wales shore stay US', () => { + const north = {}; + for (const lon of [-132.5, -132.0, -131.5, -131.0]) north[`n${lon}`] = [abLat(lon) + 0.05, lon]; + assert.deepEqual(sciKept(north), Object.keys(north).sort()); +}); + +test('volcview regions (QQA-10c): Victoria, Saanich, Sidney and Duncan (Vancouver Island, inside the rough Washington box) drop; Port Angeles, Neah Bay, Friday Harbor and Point Roberts stay', () => { + assert.deepEqual(sciKept(pick('victoria', 'saanich', 'sidney', 'duncan')), []); + assert.deepEqual(sciKept(pick('portAngeles', 'neahBay', 'fridayHarbor', 'pointRoberts')), ['fridayHarbor', 'neahBay', 'pointRoberts', 'portAngeles']); +}); + +test('volcview regions (QQA-10c): a grid of points south of 49.0 and west of -123.3 keeps nothing north of the Juan de Fuca boundary', () => { + // Boundary latitude in the Strait of Juan de Fuca at a few longitudes (IBC digital boundary v1.3): + // about 48.22 at -123.54, 48.30 at -124.01 and 48.49 at -124.73. 0.1 degrees north of those is Canadian water or land. + const strait = [[-123.5, 48.33], [-124.0, 48.40], [-124.5, 48.58]]; + const points = {}; + for (const [lon, lat] of strait) for (let la = lat; la < 49.0; la += 0.1) points[`s${lon}_${la.toFixed(2)}`] = [la, lon]; + assert.ok(Object.keys(points).length > 15); + assert.deepEqual(sciKept(points), []); +}); + +test('NDBC contract (QQA-10c): no NDBC provider, source, origin or picture host is published', () => { + // NDBC buoy pictures (www.ndbc.noaa.gov/buoycam) answer 403 to this build's honest User-Agent, and the + // build never imitates a browser; see content/quiet-corners-maps.md ("NOAA NDBC buoy cameras - not + // included"). Until NOAA allows the build agent or offers a public API, nothing for NDBC may be added. + const text = JSON.stringify({ providers: PROVIDERS.map((p) => [p.id, p.layer, p.source, p.officialUrl]), sources: SOURCES }); + assert.doesNotMatch(text, /ndbc/i); + assert.ok(!APPROVED_ORIGINS.some((o) => /ndbc\.noaa\.gov/i.test(o))); + assert.ok(!PROVIDERS.some((p) => /ndbc|buoy/i.test(p.id))); + // Every committed camera file: no NDBC id and no ndbc.noaa.gov picture or page. + const dir = new URL('../../static/data/quiet-corners-maps/cameras/', import.meta.url).pathname; + const files = []; + const walk = (d) => { for (const n of readdirSync(d)) { const p = join(d, n); if (statSync(p).isDirectory()) walk(p); else files.push(p); } }; + walk(dir); + assert.ok(files.length > 3); + for (const f of files) assert.doesNotMatch(readFileSync(f, 'utf8'), /ndbc/i, f); +}); + test('glerl refresh: the table page and a HEAD per still, plain GET and the build\'s own agent; at most 4 at a time', async () => { mock.method(Date, 'now', () => Date.parse(SCI_AT)); try {