Merge branch 'main' into fix/node-24.22.1-security-release

This commit is contained in:
Sameh Khamis committed 2026-11-02 08:05:41 -08:00
commit d411802d8c
11 files changed
+136 -14

No files matched your search

+19
View File
@@ -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 <app-url>` (or
`node scripts/track-regression.mjs --url <app-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
+9
View File
@@ -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
@@ -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).
+5
View File
@@ -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).
@@ -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).
+16
View File
@@ -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`,
+3 -3
View File
@@ -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`).
---
+3 -1
View File
@@ -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": []
},
@@ -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) {
+41 -3
View File
@@ -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', () =>
+30 -7
View File
@@ -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);
}