From 9c15f093a1705b52807d44ab02a57f40249ee977 Mon Sep 17 00:00:00 2001 From: Bilawal Sidhu Date: Wed, 23 Sep 2026 21:54:43 -0500 Subject: [PATCH 1/6] fix(cyclones): accept advisories NHC publishes before their nominal time NHC stamps an advisory with its nominal time and writes it about half an hour early, so the 5-minute future limit rejected the whole feed each advisory cycle and the layer showed no storms. Allow 90 minutes ahead. --- CHANGELOG.md | 5 +++++ server/providers/cyclones.js | 5 ++++- src/data/cyclonesProxy.test.mjs | 9 +++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58103d04d..431d282f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +- Cyclone advisories no longer go dark while NHC publishes an advisory ahead + of its nominal time: advisory and position times up to 90 minutes ahead of + the server clock are accepted (previously 5 minutes), so the whole feed is + not rejected for about half an hour each advisory cycle. + - Public Overpass instances are no longer used by default. Street Traffic roads come from TomTom flow tiles, OpenFreeMap vector tiles, or both, chosen on the layer row (TomTom / OSM / Hybrid) or with `?trafficRoads=`. With a diff --git a/server/providers/cyclones.js b/server/providers/cyclones.js index fede1aade..d580357ce 100644 --- a/server/providers/cyclones.js +++ b/server/providers/cyclones.js @@ -41,6 +41,9 @@ function number(value, min, max) { const n = Number(value); return Number.isFinite(n) && n >= min && n <= max ? n : null; } +// NHC stamps an advisory with its nominal time (e.g. 03:00Z) and publishes it +// up to about half an hour before; allow that, but not a clock far ahead. +const EARLY_ADVISORY_MS = 90 * 60_000; function iso(value, now) { if ( typeof value !== 'string' || @@ -50,7 +53,7 @@ function iso(value, now) { const n = Date.parse(value); if ( !Number.isFinite(n) || - n > now + 5 * 60_000 || + n > now + EARLY_ADVISORY_MS || now - n > 12 * HOUR || new Date(n).toISOString().replace('.000Z', 'Z') !== value.replace('.000Z', 'Z') diff --git a/src/data/cyclonesProxy.test.mjs b/src/data/cyclonesProxy.test.mjs index 04729d33a..31433b326 100644 --- a/src/data/cyclonesProxy.test.mjs +++ b/src/data/cyclonesProxy.test.mjs @@ -162,6 +162,15 @@ test('malformed, duplicate, oversized, future and expired status is rejected rat assert.deepEqual(parseCycloneStatus({ activeStorms: [] }, NOW), []); }); +test('advisories published before their nominal time are accepted, far-future ones are not', () => { + // NHC wrote the 03:00Z advisory at 02:34Z. + const early = Date.UTC(2026, 8, 16, 2, 34); + assert.equal(parseCycloneStatus(status(), early)[0].issuedAt, TIME); + assert.throws(() => + parseCycloneStatus(status(), Date.UTC(2026, 8, 16, 1, 25)), + ); +}); + test('coherent geometry is retained with holes/dateline while older advisory geometry is omitted', () => { const storms = parseCycloneStatus(status(), NOW); const parts = collections(); From b90bcf1829c4a44f16ceb9ee166a3785cb0345b4 Mon Sep 17 00:00:00 2001 From: Aditya <205600203+Rohilalala@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:00:04 +0530 Subject: [PATCH 2/6] fix(flights): don't treat OpenSky's position-less rows as a partial snapshot OpenSky's state-vector spec allows null longitude/latitude, and every live /states/all response carries such rows (about 50 of 7,800). The normaliser rejects them, so admitRecords reported complete: false on effectively every poll. While a snapshot is incomplete, FlightRecords.absence() retains any aircraft received in the last five minutes, which meant the stale fade, MISSING_POLL_LIMIT and the landed fast-cull never ran: vanished and landed aircraft stayed on the globe at full opacity for up to five minutes. admitRecords now takes an optional unpositionedId(row). A rejected row it names is a contact without a position: its id is returned in retainIds and the row does not count against completeness. OpenSky opts in for rows with an ICAO24 address and strictly null longitude and latitude; the id comes from the same helper the normaliser uses. Rows with no usable id, out-of-range or half-null coordinates, truncated rows and positioned duplicates still make the snapshot incomplete, and a nonempty feed with no positioned rows is still malformed. absence() retains exactly the retainIds aircraft under the existing five-minute bound, except likely-landed ones, which keep the landed limit. Everything else ages out normally. Military readsb admission does not opt in and is unchanged. --- CHANGELOG.md | 5 +++ docs/CURRENT-STATE.md | 8 ++++- src/layers/flights/records.js | 8 +++-- src/layers/flights/records.test.mjs | 24 ++++++++++++++ src/layers/flights/snapshotRenderer.js | 1 + src/sources/live/aircraft.js | 17 ++++++++-- src/sources/live/contract.js | 20 ++++++++++-- src/sources/live/contract.test.mjs | 44 ++++++++++++++++++++++++++ 8 files changed, 118 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b24ac95ed..c608afa5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +- Flights age out vanished aircraft again. OpenSky's routine position-less rows + no longer mark every snapshot partial, which had kept vanished and landed + aircraft at full opacity for up to five minutes; those rows retain only their + own aircraft, and a likely-landed one still leaves on the landed limit. + - Treat explicitly current, complete empty vessel coverage as a successful update, clearing obsolete contacts while retaining stale or incomplete snapshots. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 26d56e66b..15d5c50f7 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -825,7 +825,13 @@ addition to the locally accumulated trail, never a promise of complete coverage. Snapshot coverage, completeness and freshness are separate fields. A partially admitted aircraft snapshot retains absent contacts for up to five minutes before -the usual missed-poll eviction. Unknown snapshot times remain unknown in stats. An invalid nonempty +the usual missed-poll eviction. An OpenSky row with an ICAO24 address but null +longitude and latitude is a contact without a position, not a malformed row: it +leaves the snapshot complete and is listed in `retainIds`, so civil flights +retain that aircraft under the same five-minute bound, unless it is likely +landed, while other absent aircraft fade and leave on the missed-poll limits. A +nonempty snapshot with no positioned rows is still malformed. Military readsb +admission is unchanged. Unknown snapshot times remain unknown in stats. An invalid nonempty snapshot retains the previous display. Empty vessel refreshes retain the existing first-connect grace and warm-data behavior. Known source failures have bounded messages; arbitrary HTTP response bodies are not surfaced as diagnostics. diff --git a/src/layers/flights/records.js b/src/layers/flights/records.js index 43809f516..fdb65c62c 100644 --- a/src/layers/flights/records.js +++ b/src/layers/flights/records.js @@ -258,10 +258,12 @@ export class FlightRecords { return { icao24, prevMeta, meta, groundFlipped, fixEpochMs }; } - absence(id, { complete, likelyLanded }) { - // Partial admissions do not prove absence, but retention remains bounded. + absence(id, { complete, retainIds, likelyLanded }) { + // Partial admissions and position-less rows do not prove absence, but + // retention remains bounded; a likely-landed position-less contact + // still takes the landed limit. if ( - !complete && + (!complete || (retainIds?.has(id) && !likelyLanded)) && Date.now() - (this.data.get(id)?.observedReceiptMs ?? 0) < 300000 ) return 'retain'; diff --git a/src/layers/flights/records.test.mjs b/src/layers/flights/records.test.mjs index d001c7811..783b6d06a 100644 --- a/src/layers/flights/records.test.mjs +++ b/src/layers/flights/records.test.mjs @@ -106,3 +106,27 @@ test('ground transitions preserve last render height while a surface cell is col assert.equal(next.meta.renderAltitudeM, 1040); assert.equal(next.meta.wasAirborne, true); }); + +test('complete snapshots age out vanished aircraft and retain position-less ones unless landed', () => { + const store = records(); + for (const id of ['abc123', 'def456', 'ghi789']) + store.receive(observation({ id, reference: id }), view()); + // abc123 and ghi789 are still listed without a position; def456 vanished. + const absent = (id, likelyLanded = false) => + store.absence(id, { + complete: true, + retainIds: new Set(['abc123', 'ghi789']), + likelyLanded, + }); + assert.deepEqual( + [absent('def456'), absent('def456'), absent('def456')], + ['stale', 'stale', 'remove'], + ); + assert.equal(absent('abc123'), 'retain'); + assert.equal(store.missingPolls.has('abc123'), false); + // A landed contact that lost its position takes the landed fast-cull. + assert.equal(absent('ghi789', true), 'remove'); + // Position-less retention stays bounded like partial admission. + store.data.get('abc123').observedReceiptMs = Date.now() - 300001; + assert.equal(absent('abc123'), 'stale'); +}); diff --git a/src/layers/flights/snapshotRenderer.js b/src/layers/flights/snapshotRenderer.js index f6e74b125..ec6c2933b 100644 --- a/src/layers/flights/snapshotRenderer.js +++ b/src/layers/flights/snapshotRenderer.js @@ -243,6 +243,7 @@ export function createFlightSnapshotRenderer({ if (currentIcaos.has(icao24)) continue; const absence = records.absence(icao24, { complete: snapshot.complete, + retainIds: snapshot.retainIds, likelyLanded: queries._likelyLanded(icao24), }); if (absence === 'retain') continue; diff --git a/src/sources/live/aircraft.js b/src/sources/live/aircraft.js index 9534a5c3c..1ba3af603 100644 --- a/src/sources/live/aircraft.js +++ b/src/sources/live/aircraft.js @@ -6,10 +6,12 @@ import { finite, } from './contract.js'; +const openSkyId = (row) => + Array.isArray(row) ? cleanText(row[0]).toLowerCase() : ''; + /** Normalize the OpenSky state-vector route, including its regional fallback. */ export function normalizeOpenSkyAircraft(row) { - if (!Array.isArray(row)) return null; - const id = cleanText(row[0]).toLowerCase(); + const id = openSkyId(row); if (!id || !coordinates(row[6], row[5])) return null; return { id, @@ -71,6 +73,16 @@ export function normalizeReadsbAircraft(row, snapshotTimeMs) { }; } +/** + * OpenSky allows null longitude/latitude for a contact that is still + * broadcasting. Strict nulls also reject rows truncated before the position. + */ +function openSkyUnpositionedId(row) { + return Array.isArray(row) && row[5] === null && row[6] === null + ? openSkyId(row) + : ''; +} + export function openSkySnapshot( payload, { @@ -84,6 +96,7 @@ export function openSkySnapshot( payload?.states, normalizeOpenSkyAircraft, 'OpenSky', + openSkyUnpositionedId, ); const observedAtMs = epoch(payload?.time, 1000); const ageMs = observedAtMs == null ? null : Math.max(0, now - observedAtMs); diff --git a/src/sources/live/contract.js b/src/sources/live/contract.js index 2415ff43c..d99b55f0d 100644 --- a/src/sources/live/contract.js +++ b/src/sources/live/contract.js @@ -53,17 +53,30 @@ export function coordinates(latitude, longitude) { ); } -/** Admit a snapshot atomically; an all-invalid nonempty feed is unavailable. */ -export function admitRecords(rows, normalize, label) { +/** + * Admit a snapshot atomically; an all-invalid nonempty feed is unavailable. + * `unpositionedId(row)` names a rejected row that is a valid contact without + * a position: it is returned in `retainIds` and does not make the snapshot + * incomplete. + */ +export function admitRecords(rows, normalize, label, unpositionedId) { if (!Array.isArray(rows)) throw new LiveSourceError('malformed', `Malformed ${label} response`); const records = []; const ids = new Set(); + const retainIds = new Set(); + let unpositioned = 0; for (const row of rows) { const record = normalize(row); if (record && !ids.has(record.id)) { records.push(record); ids.add(record.id); + } else if (!record) { + const id = unpositionedId?.(row); + if (id) { + retainIds.add(id); + unpositioned += 1; + } } } if (rows.length && !records.length) { @@ -71,7 +84,8 @@ export function admitRecords(rows, normalize, label) { } return { records, - complete: records.length === rows.length, + retainIds, + complete: records.length + unpositioned === rows.length, rejectedCount: rows.length - records.length, }; } diff --git a/src/sources/live/contract.test.mjs b/src/sources/live/contract.test.mjs index 391fe1e72..143a7fade 100644 --- a/src/sources/live/contract.test.mjs +++ b/src/sources/live/contract.test.mjs @@ -67,6 +67,50 @@ test('mixed-invalid and duplicate admissions are explicitly incomplete; all-inva assert.equal(openSkySnapshot({ states: [] }).complete, true); }); +test('position-less OpenSky rows are seen, not missing; malformed rows stay incomplete', () => { + // OpenSky's spec allows null longitude/latitude; every live /states/all + // carries such rows. + const unpositioned = ['DEF456', ...aircraft.slice(1)]; + unpositioned[5] = null; + unpositioned[6] = null; + const snapshot = openSkySnapshot({ states: [aircraft, unpositioned] }); + assert.equal(snapshot.records.length, 1); + assert.equal(snapshot.complete, true); + assert.deepEqual([...snapshot.retainIds], ['def456']); + assert.equal(snapshot.rejectedCount, 1); + const malformed = [...aircraft]; + malformed[5] = 181; + const garbled = openSkySnapshot({ + states: [aircraft, unpositioned, malformed], + }); + assert.equal(garbled.complete, false); + assert.deepEqual([...garbled.retainIds], ['def456']); + // No usable id, only half a position, a truncated row, or no row at all. + for (const row of [ + [' ', ...unpositioned.slice(1)], + Object.assign([...unpositioned], { 5: -97 }), + ['DEF456'], + null, + ]) + assert.equal(openSkySnapshot({ states: [aircraft, row] }).complete, false); + // Repeated position-less rows stay complete. + const repeated = openSkySnapshot({ + states: [aircraft, unpositioned, unpositioned], + }); + assert.equal(repeated.complete, true); + // A feed with no positions at all still cannot prove absence. + assert.throws(() => openSkySnapshot({ states: [unpositioned] }), { + code: 'malformed', + }); + // Military readsb admission keeps its existing semantics. + const military = readsbSnapshot( + { ac: [{ hex: 'abc123', lat: 30, lon: -97 }, { hex: 'def456' }] }, + { observedAtMs: null }, + ); + assert.equal(military.complete, false); + assert.equal(military.retainIds.size, 0); +}); + test('readsb units and position/contact ages are normalized once against the source epoch', () => { const row = normalizeReadsbAircraft( { From bdd64660e7d9d0de31ce58dc8450a09e7a632214 Mon Sep 17 00:00:00 2001 From: Cliff Flood Date: Wed, 16 Sep 2026 18:17:09 -0400 Subject: [PATCH 3/6] feat: ship a web app manifest and home-screen icons Adding the app to a phone home screen produced a blank tile: index.html linked only an SVG favicon, and Safari ignores SVG for apple-touch-icon, substituting a screenshot of the page. There was no manifest at all, so Android had nothing to install against either. Generate the rasters from public/logo.svg with sharp, which the repo already carries. The output is committed so a plain `vite build` needs no image toolchain; re-run scripts/generate-icons.mjs after changing the logo. The logo's viewBox is wider than it is tall, so each icon centres the mark on a square #0a0a0f canvas rather than stretching it, inset to 78% normally and 54% for the maskable variant to survive Android's circular crop. The SVG favicon stays as the browser tab icon. --- CHANGELOG.md | 6 +++ docs/CURRENT-STATE.md | 16 +++++++ index.html | 10 +++++ public/icons/icon-180.png | Bin 0 -> 12055 bytes public/icons/icon-192.png | Bin 0 -> 12993 bytes public/icons/icon-32.png | Bin 0 -> 1160 bytes public/icons/icon-512-maskable.png | Bin 0 -> 28226 bytes public/icons/icon-512.png | Bin 0 -> 40995 bytes public/manifest.webmanifest | 21 +++++++++ scripts/format-scope.json | 4 +- scripts/generate-icons.mjs | 70 +++++++++++++++++++++++++++++ src/tooling/webAppIcons.test.mjs | 70 +++++++++++++++++++++++++++++ 12 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 public/icons/icon-180.png create mode 100644 public/icons/icon-192.png create mode 100644 public/icons/icon-32.png create mode 100644 public/icons/icon-512-maskable.png create mode 100644 public/icons/icon-512.png create mode 100644 public/manifest.webmanifest create mode 100644 scripts/generate-icons.mjs create mode 100644 src/tooling/webAppIcons.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 032c6fe2a..eb61ad1db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +- Ship a web app manifest and raster home-screen icons, so adding the app to a + phone home screen or installing it on desktop uses the God's Eye View mark + instead of a blank tile. The icons are generated from `public/logo.svg` by + `node scripts/generate-icons.mjs` and committed, so a plain `vite build` needs + no image toolchain. The existing SVG favicon still serves the browser tab. + - GBFS v3 station names use English translations when available, then the first usable translation or short name. Empty or malformed names no longer appear as `[object Object]` or interrupt station loading; legacy string names remain diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 0d3ef31e0..f8e6bff3a 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1131,6 +1131,22 @@ services, local voice and standalone key setup. Importing them starts no network acquisition. Browser layer lifecycle, rendering and voice execution stay in their existing modules. +## Web app install icons + +`public/manifest.webmanifest` declares the installable app: standalone display, +`#0a0a0f` background and theme, and 192/512 icons plus a 512 maskable variant. +`index.html` links the manifest, a 180px `apple-touch-icon` and a 32px PNG +favicon; the existing SVG favicon still serves the browser tab. Safari ignores an +SVG apple-touch-icon and substitutes a page screenshot, so the raster is what +makes an installed icon appear at all. + +`node scripts/generate-icons.mjs` renders every raster from `public/logo.svg` and +writes `public/icons/`. Output is committed, so building needs no image +toolchain; re-run the script after changing the logo. Ordinary icons inset the +mark to 78% of the canvas and the maskable variant to 54%, keeping it inside the +circle Android may crop to. `src/tooling/webAppIcons.test.mjs` checks the links, +the manifest fields and that each committed PNG is square at its declared size. + ## Local build preview After `npm run build`, `npm run preview` serves the built app with the same data diff --git a/index.html b/index.html index 2e630c4a8..31db11b06 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,16 @@ God's Eye View + + + + + + + + diff --git a/public/icons/icon-180.png b/public/icons/icon-180.png new file mode 100644 index 0000000000000000000000000000000000000000..5655a9e705643b9a36e8d27666014bc940e3d94f GIT binary patch literal 12055 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 literal 0 HcmV?d00001 diff --git a/public/icons/icon-32.png b/public/icons/icon-32.png new file mode 100644 index 0000000000000000000000000000000000000000..a19279f0690e87da20ab7989e3ef4dd401336f59 GIT binary patch literal 1160 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

jis$0avX_KC%q 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 kDTR-v5{3c)59}m9;{X5v literal 0 HcmV?d00001 diff --git a/public/icons/icon-512.png b/public/icons/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..644a5c8eae4afb4859ea929be2c517a19e834b39 GIT binary patch literal 40995 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 literal 0 HcmV?d00001 diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 000000000..574b90faf --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,21 @@ +{ + "name": "God's Eye View", + "short_name": "God's Eye", + "description": "Live spatial intelligence in the browser.", + "start_url": "/", + "scope": "/", + "display": "standalone", + "orientation": "any", + "background_color": "#0a0a0f", + "theme_color": "#0a0a0f", + "icons": [ + { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" }, + { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }, + { + "src": "/icons/icon-512-maskable.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 662d33a6b..d910536a2 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -981,5 +981,7 @@ "src/tools/globePanel.test.mjs", "src/costEndpointThrottle.test.mjs", "src/sources/gbfsStations.test.mjs", - "scripts/qa-gbfs-names.mjs" + "scripts/qa-gbfs-names.mjs", + "scripts/generate-icons.mjs", + "src/tooling/webAppIcons.test.mjs" ] diff --git a/scripts/generate-icons.mjs b/scripts/generate-icons.mjs new file mode 100644 index 000000000..3c16eb46e --- /dev/null +++ b/scripts/generate-icons.mjs @@ -0,0 +1,70 @@ +#!/usr/bin/env node +// Renders the home-screen and favicon rasters from `public/logo.svg`. +// +// Re-run after changing the logo: `node scripts/generate-icons.mjs`. The output +// is committed, because the install icons have to exist in a plain `vite build` +// and contributors should not need an image toolchain to ship the app. +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import sharp from 'sharp'; + +const REPO_ROOT = path.resolve( + fileURLToPath(new URL('.', import.meta.url)), + '..', +); +const LOGO = path.join(REPO_ROOT, 'public', 'logo.svg'); +const OUT_DIR = path.join(REPO_ROOT, 'public', 'icons'); + +/** `--bg-dark` from src/ui/styles/foundation.css, so the tile matches the app. */ +const BACKGROUND = '#0a0a0f'; + +/** + * Fraction of the canvas the mark is allowed to occupy. + * + * A maskable icon may be cropped to a circle inscribed in the middle 80% of the + * square, so its mark stays well inside that circle; the other icons are shown + * as drawn and only need optical breathing room. + */ +const INSET = { normal: 0.78, maskable: 0.54 }; + +const TARGETS = [ + { file: 'icon-32.png', size: 32, inset: INSET.normal }, + { file: 'icon-180.png', size: 180, inset: INSET.normal }, + { file: 'icon-192.png', size: 192, inset: INSET.normal }, + { file: 'icon-512.png', size: 512, inset: INSET.normal }, + { file: 'icon-512-maskable.png', size: 512, inset: INSET.maskable }, +]; + +/** Fit the logo inside `size * inset` and centre it on an opaque square. */ +async function render(svg, { size, inset }) { + const box = Math.round(size * inset); + // The logo's viewBox is wider than it is tall, so `contain` bounds it by + // width and leaves the vertical padding uneven — centring is done by the + // composite below rather than by the resize. + const mark = await sharp(svg, { density: 384 }) + .resize(box, box, { + fit: 'contain', + background: { r: 0, g: 0, b: 0, alpha: 0 }, + }) + .png() + .toBuffer(); + return sharp({ + create: { + width: size, + height: size, + channels: 4, + background: BACKGROUND, + }, + }) + .composite([{ input: mark, gravity: 'centre' }]) + .png({ compressionLevel: 9 }) + .toBuffer(); +} + +const svg = await readFile(LOGO); +await mkdir(OUT_DIR, { recursive: true }); +for (const target of TARGETS) { + await writeFile(path.join(OUT_DIR, target.file), await render(svg, target)); + console.log(`icons: wrote ${target.file} (${target.size}px)`); +} diff --git a/src/tooling/webAppIcons.test.mjs b/src/tooling/webAppIcons.test.mjs new file mode 100644 index 000000000..2267872fe --- /dev/null +++ b/src/tooling/webAppIcons.test.mjs @@ -0,0 +1,70 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import sharp from 'sharp'; + +const REPO_ROOT = path.resolve( + fileURLToPath(new URL('.', import.meta.url)), + '../..', +); +const PUBLIC_ROOT = path.join(REPO_ROOT, 'public'); +const INDEX_HTML = readFileSync(path.join(REPO_ROOT, 'index.html'), 'utf8'); + +/** `public/` is served at the site root, so a manifest href maps to a file by dropping the leading slash. */ +function publicFile(href) { + assert.ok(href.startsWith('/'), `${href} must be a root-relative URL`); + return path.join(PUBLIC_ROOT, href.slice(1)); +} + +const manifestHref = INDEX_HTML.match( + /]+rel="manifest"[^>]+href="([^"]+)"/, +)?.[1]; + +test('index.html links a web app manifest', () => { + assert.ok(manifestHref, 'no in index.html'); +}); + +test('iOS gets a raster home-screen icon', () => { + // Safari ignores SVG for apple-touch-icon and falls back to a screenshot of + // the page, which is the blank tile this whole file exists to prevent. The + // existing `rel="icon"` SVG covers the browser tab and nothing else. + const href = INDEX_HTML.match( + /]+rel="apple-touch-icon"[^>]+href="([^"]+)"/, + )?.[1]; + assert.ok(href, 'no in index.html'); + assert.match(href, /\.png$/, 'apple-touch-icon must be a PNG'); + assert.doesNotThrow(() => readFileSync(publicFile(href))); +}); + +test('the manifest declares a standalone installable app', () => { + const manifest = JSON.parse(readFileSync(publicFile(manifestHref), 'utf8')); + assert.equal(manifest.display, 'standalone'); + assert.equal(manifest.start_url, '/'); + for (const field of ['name', 'short_name', 'background_color', 'theme_color']) + assert.ok(manifest[field], `manifest is missing ${field}`); + // Android's install prompt needs both, and uses the 512 for the splash screen. + const sizes = new Set(manifest.icons.map((icon) => icon.sizes)); + for (const required of ['192x192', '512x512']) + assert.ok(sizes.has(required), `manifest declares no ${required} icon`); + // Without a maskable icon Android pads the square into a circle and clips + // the mark; with one it crops to the safe zone instead. + assert.ok( + manifest.icons.some((icon) => icon.purpose?.includes('maskable')), + 'manifest declares no maskable icon', + ); +}); + +test('every declared icon is a square PNG at its declared size', async () => { + const manifest = JSON.parse(readFileSync(publicFile(manifestHref), 'utf8')); + for (const icon of manifest.icons) { + const meta = await sharp(publicFile(icon.src)).metadata(); + const [width, height] = icon.sizes.split('x').map(Number); + assert.equal(meta.format, 'png', `${icon.src} is not a PNG`); + // A size the manifest overstates is worse than a missing one: the platform + // trusts the declaration and scales whatever it finds. + assert.equal(meta.width, width, `${icon.src} width`); + assert.equal(meta.height, height, `${icon.src} height`); + } +}); From fc2c372ffcc7cbb67ecda42bd8e27a08be719ca5 Mon Sep 17 00:00:00 2001 From: Cliff Flood Date: Thu, 1 Oct 2026 15:01:32 -0400 Subject: [PATCH 4/6] fix: keep install assets inside a non-root base Manifest start_url, scope and icon src resolve against the manifest URL, so root-relative values sent an app served under a path prefix to the domain root. Make them relative, and add a build test under base: '/example/' that asserts every emitted link and manifest URL stays inside the base. index.html is unchanged: Vite already rewrites its links under base. --- CHANGELOG.md | 2 + docs/CURRENT-STATE.md | 6 +++ public/manifest.webmanifest | 10 ++-- src/tooling/webAppIcons.test.mjs | 85 ++++++++++++++++++++++++++++++-- 4 files changed, 95 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb61ad1db..0c07f3c64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ instead of a blank tile. The icons are generated from `public/logo.svg` by `node scripts/generate-icons.mjs` and committed, so a plain `vite build` needs no image toolchain. The existing SVG favicon still serves the browser tab. + Manifest paths are relative to the manifest, so a build served under a path + prefix installs and launches inside that prefix. - GBFS v3 station names use English translations when available, then the first usable translation or short name. Empty or malformed names no longer appear as diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index f8e6bff3a..3d51bad2a 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1140,12 +1140,18 @@ favicon; the existing SVG favicon still serves the browser tab. Safari ignores a SVG apple-touch-icon and substitutes a page screenshot, so the raster is what makes an installed icon appear at all. +Manifest `start_url`, `scope` and icon paths are relative to the manifest, and +Vite rewrites the `index.html` links under `base`, so a build served from a path +prefix installs and launches inside that prefix. + `node scripts/generate-icons.mjs` renders every raster from `public/logo.svg` and writes `public/icons/`. Output is committed, so building needs no image toolchain; re-run the script after changing the logo. Ordinary icons inset the mark to 78% of the canvas and the maskable variant to 54%, keeping it inside the circle Android may crop to. `src/tooling/webAppIcons.test.mjs` checks the links, the manifest fields and that each committed PNG is square at its declared size. +It also builds the install links under `base: '/example/'` and asserts every +emitted link and manifest URL stays inside that base. ## Local build preview diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest index 574b90faf..cf4b2b791 100644 --- a/public/manifest.webmanifest +++ b/public/manifest.webmanifest @@ -2,17 +2,17 @@ "name": "God's Eye View", "short_name": "God's Eye", "description": "Live spatial intelligence in the browser.", - "start_url": "/", - "scope": "/", + "start_url": ".", + "scope": ".", "display": "standalone", "orientation": "any", "background_color": "#0a0a0f", "theme_color": "#0a0a0f", "icons": [ - { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" }, - { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }, + { "src": "icons/icon-192.png", "sizes": "192x192", "type": "image/png" }, + { "src": "icons/icon-512.png", "sizes": "512x512", "type": "image/png" }, { - "src": "/icons/icon-512-maskable.png", + "src": "icons/icon-512-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/src/tooling/webAppIcons.test.mjs b/src/tooling/webAppIcons.test.mjs index 2267872fe..6a6cdb350 100644 --- a/src/tooling/webAppIcons.test.mjs +++ b/src/tooling/webAppIcons.test.mjs @@ -1,9 +1,12 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; +import { rm, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import sharp from 'sharp'; +import { build } from 'vite'; +import { makeFixtureRoot } from './fixtureRoot.mjs'; const REPO_ROOT = path.resolve( fileURLToPath(new URL('.', import.meta.url)), @@ -12,7 +15,7 @@ const REPO_ROOT = path.resolve( const PUBLIC_ROOT = path.join(REPO_ROOT, 'public'); const INDEX_HTML = readFileSync(path.join(REPO_ROOT, 'index.html'), 'utf8'); -/** `public/` is served at the site root, so a manifest href maps to a file by dropping the leading slash. */ +/** `public/` is served at the site root, so an index.html href maps to a file by dropping the leading slash. */ function publicFile(href) { assert.ok(href.startsWith('/'), `${href} must be a root-relative URL`); return path.join(PUBLIC_ROOT, href.slice(1)); @@ -22,6 +25,18 @@ const manifestHref = INDEX_HTML.match( /]+rel="manifest"[^>]+href="([^"]+)"/, )?.[1]; +/** + * Manifest URLs resolve against the manifest, not the page. Keeping them + * relative is what lets a build served under a path prefix install inside it. + */ +function manifestFile(src) { + assert.ok( + !src.startsWith('/') && !/^[a-z][a-z\d+.-]*:/i.test(src), + `${src} must be relative to the manifest`, + ); + return path.join(path.dirname(publicFile(manifestHref)), src); +} + test('index.html links a web app manifest', () => { assert.ok(manifestHref, 'no in index.html'); }); @@ -41,7 +56,8 @@ test('iOS gets a raster home-screen icon', () => { test('the manifest declares a standalone installable app', () => { const manifest = JSON.parse(readFileSync(publicFile(manifestHref), 'utf8')); assert.equal(manifest.display, 'standalone'); - assert.equal(manifest.start_url, '/'); + assert.equal(manifest.start_url, '.'); + assert.equal(manifest.scope, '.'); for (const field of ['name', 'short_name', 'background_color', 'theme_color']) assert.ok(manifest[field], `manifest is missing ${field}`); // Android's install prompt needs both, and uses the 512 for the splash screen. @@ -59,7 +75,7 @@ test('the manifest declares a standalone installable app', () => { test('every declared icon is a square PNG at its declared size', async () => { const manifest = JSON.parse(readFileSync(publicFile(manifestHref), 'utf8')); for (const icon of manifest.icons) { - const meta = await sharp(publicFile(icon.src)).metadata(); + const meta = await sharp(manifestFile(icon.src)).metadata(); const [width, height] = icon.sizes.split('x').map(Number); assert.equal(meta.format, 'png', `${icon.src} is not a PNG`); // A size the manifest overstates is worse than a missing one: the platform @@ -68,3 +84,66 @@ test('every declared icon is a square PNG at its declared size', async () => { assert.equal(meta.height, height, `${icon.src} height`); } }); + +test('install assets stay inside a non-root Vite base', async (t) => { + // The real index.html pulls the whole Cesium app into a build, so only its + // install links are copied, verbatim, into a fixture page. + const tags = INDEX_HTML.match( + /]+rel="(?:icon|apple-touch-icon|manifest)"[^>]*>/g, + ); + assert.ok(tags?.length >= 3, 'index.html lost its install links'); + const root = await makeFixtureRoot('gev-webapp-icons-'); + t.after(() => rm(root, { recursive: true, force: true })); + await writeFile( + path.join(root, 'index.html'), + `${tags.join('')}`, + ); + const base = '/example/'; + const origin = 'https://host.test'; + const result = await build({ + root, + configFile: false, + envFile: false, + logLevel: 'silent', + base, + publicDir: PUBLIC_ROOT, + build: { write: false }, + }); + const html = result.output.find( + (item) => item.fileName === 'index.html', + ).source; + const hrefs = [...html.matchAll(/]+href="([^"]+)"/g)].map( + (match) => match[1], + ); + assert.equal(hrefs.length, tags.length); + for (const href of hrefs) { + assert.ok(href.startsWith(base), `${href} escapes ${base}`); + assert.doesNotThrow(() => + readFileSync(path.join(PUBLIC_ROOT, href.slice(base.length))), + ); + } + + // Vite copies public/ only when it writes, and copies it verbatim, so the + // deployed manifest is the committed one, served at the emitted href. + const manifestUrl = new URL( + html.match(/]+rel="manifest"[^>]+href="([^"]+)"/)[1], + origin, + ); + const manifest = JSON.parse(readFileSync(publicFile(manifestHref), 'utf8')); + const inBase = (value) => { + const url = new URL(value, manifestUrl); + assert.ok( + url.href.startsWith(origin + base), + `${value} resolves to ${url.href}, outside ${base}`, + ); + return url; + }; + assert.equal(inBase(manifest.start_url).href, origin + base); + assert.equal(inBase(manifest.scope).href, origin + base); + for (const icon of manifest.icons) { + const url = inBase(icon.src); + assert.doesNotThrow(() => + readFileSync(path.join(PUBLIC_ROOT, url.pathname.slice(base.length))), + ); + } +}); From 6d6ce364c684bffb01fb2af1ef18b4a29579d42a Mon Sep 17 00:00:00 2001 From: Mazeyar Moeini Feizabadi Date: Fri, 16 Oct 2026 02:03:16 +0200 Subject: [PATCH 5/6] docs(cyclones): document the early-advisory window and pin its bounds Maintainer follow-up for #741, no runtime change. - CURRENT-STATE: advisory issue and position times up to and including 90 minutes ahead of the server clock are accepted because NHC can release advisories before their nominal time; 90 minutes is the app's tolerance, not an NHC limit. Later, non-canonical and over-12-hour-old times still reject the snapshot. - Tests: pin exact 90-minute acceptance and +1 ms rejection, and exact 12-hour acceptance and +1 ms rejection, for issuance and lastUpdate independently; the early-publication test now asserts invalid_cyclone_data. Co-Authored-By: Claude Opus 5.5 --- docs/CURRENT-STATE.md | 7 ++++++- src/data/cyclonesProxy.test.mjs | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index e5b3d2ace..0af075a6d 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -339,7 +339,12 @@ Cyclones combine fixed NHC/CPHC status and summary GIS endpoints through a bound same-origin provider. The status endpoint lacks browser CORS; each installation fetches keyless public sources at runtime, with no centralized ingestion service. The five-minute snapshot covers Atlantic and eastern/central North Pacific only. -Current positions, advisory issue time and position time stay distinct. Tracks, +Current positions, advisory issue time and position time stay distinct. NHC may +release an advisory before its nominal time (routinely up to about 30 minutes, +rarely about an hour), so advisory issue and position times up to and including +90 minutes ahead of the server clock are accepted; 90 minutes is this app's +tolerance, not an NHC limit. Later, non-canonical, or more than 12-hour-old +times still reject the status snapshot. Tracks, forecast lead-hour points and cones render only when all source parts match the status advisory; a newer status shows its position with geometry pending rather than relabeling older geometry. The cone represents forecast center uncertainty, diff --git a/src/data/cyclonesProxy.test.mjs b/src/data/cyclonesProxy.test.mjs index 31433b326..23e84826c 100644 --- a/src/data/cyclonesProxy.test.mjs +++ b/src/data/cyclonesProxy.test.mjs @@ -166,11 +166,40 @@ test('advisories published before their nominal time are accepted, far-future on // NHC wrote the 03:00Z advisory at 02:34Z. const early = Date.UTC(2026, 8, 16, 2, 34); assert.equal(parseCycloneStatus(status(), early)[0].issuedAt, TIME); - assert.throws(() => - parseCycloneStatus(status(), Date.UTC(2026, 8, 16, 1, 25)), + assert.throws( + () => parseCycloneStatus(status(), Date.UTC(2026, 8, 16, 1, 25)), + /invalid_cyclone_data/, ); }); +test('advisory and position times are bounded at 90 minutes ahead and 12 hours old', () => { + const MINUTE = 60_000; + const at = (offsetMs) => new Date(NOW + offsetMs).toISOString(); + const fields = { + issuedAt: (value) => + status({ + forecastAdvisory: { + advNum: '010', + issuance: value, + url: 'https://www.nhc.noaa.gov/text/MIATCMEP5.shtml', + }, + }), + positionAt: (value) => status({ lastUpdate: value }), + }; + for (const [field, payload] of Object.entries(fields)) { + for (const offset of [90 * MINUTE, -12 * 60 * MINUTE]) { + const [storm] = parseCycloneStatus(payload(at(offset)), NOW); + assert.equal(storm[field], at(offset), `${field} ${offset}`); + } + for (const offset of [90 * MINUTE + 1, -12 * 60 * MINUTE - 1]) + assert.throws( + () => parseCycloneStatus(payload(at(offset)), NOW), + /invalid_cyclone_data/, + `${field} ${offset}`, + ); + } +}); + test('coherent geometry is retained with holes/dateline while older advisory geometry is omitted', () => { const storms = parseCycloneStatus(status(), NOW); const parts = collections(); From 3cdb440bc2166fb9bd42104b69f4ddc162c3dd31 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Thu, 15 Oct 2026 17:27:55 -0700 Subject: [PATCH 6/6] Use the opaque iOS status bar and keep the SVG tab icon black-translucent needs viewport-fit=cover and safe-area padding, which this change does not add. List the PNG favicon before the SVG so browsers that take the last usable icon keep the SVG, add mobile-web-app-capable, and give the manifest a stable id. --- index.html | 7 +++++-- public/manifest.webmanifest | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index bdc82bd25..8a01415b6 100644 --- a/index.html +++ b/index.html @@ -4,16 +4,19 @@ God's Eye View - + + + - + diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest index cf4b2b791..478cd9019 100644 --- a/public/manifest.webmanifest +++ b/public/manifest.webmanifest @@ -1,4 +1,5 @@ { + "id": ".", "name": "God's Eye View", "short_name": "God's Eye", "description": "Live spatial intelligence in the browser.",