diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9361e1f3c..f2c78f4b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,6 +29,25 @@ through Pinokio; the terminal path above remains the contributor path. Open `http://localhost:4173`. Before sending a PR run `npm run build`, `npm test`, and `npm run test:track` (dev server must be up) — **all three must stay green.** +### Test prerequisites + +- **`npm test`** — unit tests. Runs standalone; no dev server or credentials + required. Exit code is the gate. +- **`npm run test:track`** — the 3D flight-tracking regression harness. It + drives the **real app** in headless Chromium, so it **requires a running + dev server** (default `http://localhost:4173`). To target another instance, + run `npm run test:track -- --url ` (or + `node scripts/track-regression.mjs --url `). It does not depend on + live OpenSky / adsb.lol / AISStream (those are optional or rate-limited + locally); it installs a persistent `fetch` shim that returns synthetic + aircraft in the exact upstream payload shapes the layers parse. +- **`npm run build`** — production Vite build. No server or credentials + required; it validates that the client bundle compiles cleanly. + +Each feature also has its own headless gate; see +[Feature regression gates](#feature-regression-gates) below. Docs and changelog +rules are under [Pull requests](#pull-requests). + ## Checking a built app locally Run `npm run build` followed by `npm run preview`. Preview serves the built diff --git a/DATA_SOURCES.md b/DATA_SOURCES.md index a4035f175..403972d5e 100644 --- a/DATA_SOURCES.md +++ b/DATA_SOURCES.md @@ -304,6 +304,15 @@ the retrieval date (2026-07-30), exact download URL, license evidence, and the deterministic transform (`scripts/build-sf-neighborhoods.mjs`: `nhood` → `name`, ~2 m Douglas-Peucker simplification, 6-decimal rounding). +### Local CCTV source packs (`config/`) + +The server can read a local camera list from `CCTV_SOURCES_FILE` (default +`config/cctv_sources.austin.json`, which ships empty) and inline JSON from +`CCTV_SOURCES_JSON`; the two are merged, and inline entries win on a duplicate +id. A non-empty local pack replaces the live open-data camera packs unless +`CCTV_FORCE_AUSTIN=1` is set. The packs under `config/` show the schema, and a +pack's cameras keep their own source's license and attribution. + --- ## Weather sources diff --git a/changelog.d/1427-military-installations-cross-site-gate.md b/changelog.d/1427-military-installations-cross-site-gate.md new file mode 100644 index 000000000..35d9e73e7 --- /dev/null +++ b/changelog.d/1427-military-installations-cross-site-gate.md @@ -0,0 +1,4 @@ +- The military-installations proxy (`/api/military-installations`) now refuses + cross-site and reverse-proxy-forwarded browser requests with 403 before any + Overpass work, through the same shared gate as `/api/overpass`. Same-origin + app requests and local tools such as curl still work (@Sunil56224972, #1427). diff --git a/changelog.d/1457-dock-icon-only-wings.md b/changelog.d/1457-dock-icon-only-wings.md new file mode 100644 index 000000000..3d82cb934 --- /dev/null +++ b/changelog.d/1457-dock-icon-only-wings.md @@ -0,0 +1,5 @@ +- Between 721px and 900px wide (tablet portrait, phone landscape), the + Location and Visual Presets dock wings show only their icons, so their + labels no longer spill over the voice control or take taps meant for the + mic button. The full name stays in the tooltip and accessible name, and the + Cyber layout keeps its labels (Denis Groshev, @ZloyS, #1457). diff --git a/changelog.d/210-contributor-docs-refresh.md b/changelog.d/210-contributor-docs-refresh.md new file mode 100644 index 000000000..ed6dbe354 --- /dev/null +++ b/changelog.d/210-contributor-docs-refresh.md @@ -0,0 +1,4 @@ +- Contributor docs: CONTRIBUTING lists what each of the three baseline checks + needs to run, DATA_SOURCES documents loading a local CCTV source pack, and + KNOWN-ISSUES points the floor-placement note at its verification script + instead of a missing report (Gaurav Patidar, @gaurav7902, #210). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 1792c73de..62a85a1ac 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -2796,6 +2796,20 @@ isCurrent })` gates BOTH halves of the map-stack switch, which is its only > name the panel (`#location-bar` / `#control-panel`) to reach (2,5,0). Adding > a new tray rule means checking it against the pinned variants, not just > against the base rule. +> - **Dock wings are icon-only from 900px down to 721px (2026-10-28):** at +> ≤900px `--dock-side-compact` shrinks the Location and Visual Presets wings +> to 4.25rem, open or closed (an open tray pops out above the dock), which is +> too narrow for their text labels. The labels used to spill over the voice +> control, and the LOCATION label captured taps on the left half of the mic +> button. `src/ui/styles/command-dock-trays.css` now sets the +> labels to `font-size: 0` and drops their `›`/`‹` chevrons in that band, so +> each wing shows only its icon. `CommandDockView.vue` still gives the toggles +> their "Expand/Collapse …" `aria-label` and `title` (`trayWords`), so the +> browser tooltip is the only visible name on hover. +> The Cyber HUD layout is excluded: it sets `--dock-side-compact` to +> 9–9.75rem, which fits its labels. The former ≤620px ellipsis clip is gone: +> the phone shell takes over at 720px, so it never applied to a visible +> dock. > - **LOCATION mini-status is data-only today (2026-08-20):** the collapsed > readout now follows a free-text geocode search as well as preset pills > (`src/locationStatus.js` owns the copy for both), and every other camera @@ -5087,6 +5101,8 @@ repointing `OVERPASS_UPSTREAMS` refetches instead of serving another instance's answers; untagged entries stop serving once an instance is configured. The shared transport guards `/api/overpass` and `/api/military-installations`. +Both query handlers refuse cross-site and forwarded browser requests through +the shared local-request gate, before any rate-limit, cache or upstream work. With no configured upstream, each route serves existing cache data (including expired entries, marked stale with the original cache/retrieval date) without refresh. An annotation miss returns HTTP 503, `OVERPASS_NOT_CONFIGURED`, diff --git a/docs/KNOWN-ISSUES.md b/docs/KNOWN-ISSUES.md index 6afd37839..53c7949c6 100644 --- a/docs/KNOWN-ISSUES.md +++ b/docs/KNOWN-ISSUES.md @@ -71,9 +71,9 @@ Status: Open (owner-accepted 2026-07-08, documented) a few stragglers take one more poll. - **Born-grounded first poll:** a contact first seen on the ground with no altitude data renders at the geoid for ≤1 poll until its floor cell warms. -- Full context, improvement ideas, and the verification oracle - (`scripts/qa-floor-verify.mjs`): - `docs/superpowers/reports/2026-07-08-height-datum-handover.md`. +- Verify floor-placement behavior with `scripts/qa-floor-verify.mjs` against a + running local app; set `QA_BASE_URL` when it is not on the default port + (`http://localhost:4173`). --- diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 303107647..217628822 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -432,7 +432,9 @@ "server/providers/overpass/transport.js", "src/sources/httpBody.js", "src/i18n/marker.js", - "src/sources/rateLimit.js" + "src/sources/rateLimit.js", + "server/providers/common/same-site.js", + "src/localRequestGate.mjs" ], "external": [] }, diff --git a/server/providers/military-installations.js b/server/providers/military-installations.js index 7d252b8ed..1b6c82107 100644 --- a/server/providers/military-installations.js +++ b/server/providers/military-installations.js @@ -1,4 +1,5 @@ import { makeRateLimiter, clientKey } from './common/rate-limit.js'; +import { admitSameSite } from './common/same-site.js'; import { MILITARY_INSTALLATION_ELEMENT_CAP, MILITARY_INSTALLATION_MAX_RESPONSE_BYTES, @@ -97,6 +98,7 @@ function militaryInstallationsProxy() { res.end(JSON.stringify({ error: 'Method Not Allowed' })); return; } + if (admitSameSite(req, res)) return; const url = new URL(req.url, 'http://localhost'); const requested = validMilitaryInstallationBox(url.searchParams); if (!requested) { diff --git a/src/overpassOffload.test.mjs b/src/overpassOffload.test.mjs index 602bab8e4..7ef6b0c2b 100644 --- a/src/overpassOffload.test.mjs +++ b/src/overpassOffload.test.mjs @@ -54,14 +54,22 @@ async function call(handlers, url, init = {}) { Object.assign(req, { method: init.method || 'GET', url: parsed.pathname.slice(mount.length) + parsed.search || '/', - headers: {}, + headers: init.headers || {}, socket: { remoteAddress: '127.0.0.1' }, }); - let status, headers, body; + let status, body; + let headers = {}; await handlers.get(mount)(req, { + // The cross-site gate answers through statusCode/setHeader, not writeHead. + set statusCode(s) { + status = s; + }, + setHeader(name, value) { + headers[name] = value; + }, writeHead(s, h) { status = s; - headers = h; + headers = { ...headers, ...h }; }, end(b) { body = b; @@ -166,6 +174,36 @@ test('zero egress: every Overpass consumer reaches real default handlers, and re ); }); +test('the installations proxy refuses cross-site and forwarded requests before any Overpass work', async (t) => { + env(t); + t.mock.method(globalThis, 'fetch', () => + assert.fail('unexpected upstream fetch'), + ); + const handlers = routes(militaryInstallationsProxy()); + const url = + '/api/military-installations?south=48.8&west=2.2&north=48.9&east=2.4'; + for (const headers of [ + { 'sec-fetch-site': 'cross-site' }, + { 'sec-fetch-site': 'same-site' }, + { origin: 'https://foreign.example' }, + { origin: 'null' }, + { 'sec-fetch-site': 'same-origin', 'x-forwarded-for': '203.0.113.9' }, + ]) { + const response = await call(handlers, url, { headers }); + assert.equal(response.status, 403, JSON.stringify(headers)); + assert.equal(response.headers.get('cache-control'), 'no-store'); + } + for (const headers of [ + {}, + { origin: 'http://localhost:4173', 'sec-fetch-site': 'same-origin' }, + ]) { + const response = await call(handlers, url, { + headers: { host: 'localhost:4173', ...headers }, + }); + assert.notEqual(response.status, 403, JSON.stringify(headers)); + } +}); + test('an unconfigured server is probed once and never sent a feature query', async (t) => { env(t); t.mock.method(globalThis, 'fetch', () => diff --git a/src/ui/styles/command-dock-trays.css b/src/ui/styles/command-dock-trays.css index ecc6bc432..fb41acb8e 100644 --- a/src/ui/styles/command-dock-trays.css +++ b/src/ui/styles/command-dock-trays.css @@ -495,6 +495,36 @@ body:not(.ui-clean-view):not(.recording-mode):has( #command-dock .gev-voice-error-tray { width: min(24rem, calc(100vw - 1.5rem)); } + + /* The wings shrink to --dock-side-compact at this width, open or closed (an + open tray pops out above the dock), which is too narrow for the text + labels; they would spill over the voice control and take its taps. The + wings show only their icon; the toggles keep their aria-label. Cyber sets + a wider --dock-side-compact that fits its labels, so it keeps them. */ + :root:not([data-ui-theme='cyber']) + #command-dock + :is(#location-bar, #control-panel) + .panel-title, + :root:not([data-ui-theme='cyber']) + #command-dock + :is(#location-bar, #control-panel) + .location-toolbar-label { + font-size: 0; + letter-spacing: 0; + /* The empty text is still a flex item; drop the gap it would claim. */ + gap: 0; + } + + :root:not([data-ui-theme='cyber']) + #command-dock + :is(#location-bar, #control-panel) + .panel-title::after, + :root:not([data-ui-theme='cyber']) + #command-dock + :is(#location-bar, #control-panel) + .location-toolbar-label::after { + content: none; + } } @media (max-width: 620px) { @@ -503,13 +533,6 @@ body:not(.ui-clean-view):not(.recording-mode):has( --dock-voice-width: 8.75rem; } - #command-dock .panel-title, - #command-dock .location-toolbar-label { - max-width: 2.3rem; - overflow: hidden; - text-overflow: ellipsis; - } - #left-panel-stack { bottom: calc(50vh + 8px); }