Merge remote-tracking branch 'upstream/main' into feature/catalonia-cctv-cameras

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
ulihhhh committed 2026-09-21 19:00:01 +02:00
commit 49a322519a
29 files changed
+1025 -64

No files matched your search

+113
View File
@@ -2,6 +2,7 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { createApplication } from './application.js';
import { installTrackpadPinchZoom } from './viewer.js';
const phases = ['Scene', 'Controls', 'Data', 'Tools'];
function fixture(overrides = {}) {
@@ -217,3 +218,115 @@ test('the separate viewer export imports without constructing a browser viewer',
assert.equal(typeof createApplicationViewer, 'function');
assert.throws(() => createApplicationViewer({}), /containers are required/);
});
function pinchFixture({ zoomEventTypes } = {}) {
const handlers = new Set();
const relayed = [];
const container = {
addEventListener(type, handler, options) {
assert.equal(type, 'wheel');
assert.deepEqual(options, { capture: true, passive: false });
handlers.add(handler);
},
removeEventListener(type, handler, capture) {
assert.equal(type, 'wheel');
assert.equal(capture, true);
handlers.delete(handler);
},
emit(event) {
for (const handler of handlers) handler(event);
},
};
const canvas = {
dispatchEvent(event) {
// A real canvas dispatch crosses the container's capture listener again.
container.emit(event);
relayed.push(event);
return true;
},
};
const controller = { zoomEventTypes };
const viewer = {
container,
canvas,
scene: { screenSpaceCameraController: controller },
};
const createEvent = (init = {}) => ({
deltaX: 0,
deltaY: 0,
deltaZ: 0,
deltaMode: 0,
screenX: 0,
screenY: 0,
clientX: 0,
clientY: 0,
ctrlKey: false,
defaultPrevented: false,
propagationStopped: false,
preventDefault() {
this.defaultPrevented = true;
},
stopPropagation() {
this.propagationStopped = true;
},
...init,
});
return { viewer, controller, container, handlers, relayed, createEvent };
}
test('trackpad pinch adds the Ctrl+wheel binding without replacing Cesium inputs', () => {
const original = [1, 2, 3];
const f = pinchFixture({ zoomEventTypes: original });
const dispose = installTrackpadPinchZoom(f.viewer, {
createWheelEvent: (_type, init) => f.createEvent(init),
});
assert.deepEqual(f.controller.zoomEventTypes.slice(0, 3), original);
assert.equal(f.controller.zoomEventTypes.length, 4);
const plain = f.createEvent({ deltaY: 2 });
f.container.emit(plain);
assert.equal(plain.defaultPrevented, false);
const lineMode = f.createEvent({ ctrlKey: true, deltaY: 2, deltaMode: 1 });
f.container.emit(lineMode);
assert.equal(lineMode.defaultPrevented, false);
assert.equal(f.relayed.length, 0);
dispose();
dispose();
assert.equal(f.controller.zoomEventTypes, original);
assert.equal(f.handlers.size, 0);
});
test('trackpad pinch relays pixel deltas once with bounded amplification', () => {
const f = pinchFixture({ zoomEventTypes: [1, 2, 3] });
installTrackpadPinchZoom(f.viewer, {
createWheelEvent: (_type, init) => f.createEvent(init),
});
const small = f.createEvent({ ctrlKey: true, deltaY: 2 });
f.container.emit(small);
assert.equal(small.defaultPrevented, true);
assert.equal(small.propagationStopped, true);
assert.equal(f.relayed.length, 1);
assert.equal(f.relayed[0].deltaY, 16);
assert.equal(f.relayed[0].ctrlKey, true);
const large = f.createEvent({ ctrlKey: true, deltaY: -100 });
f.container.emit(large);
assert.equal(f.relayed.length, 2);
assert.equal(f.relayed[1].deltaY, -120);
});
test('trackpad pinch falls back to the original event when relay construction fails', () => {
const f = pinchFixture({ zoomEventTypes: [1, 2, 3] });
installTrackpadPinchZoom(f.viewer, {
createWheelEvent() {
throw new Error('unsupported');
},
});
const event = f.createEvent({ ctrlKey: true, deltaY: 2 });
f.container.emit(event);
assert.equal(event.defaultPrevented, false);
assert.equal(event.propagationStopped, false);
assert.equal(f.relayed.length, 0);
});
+5 -1
View File
@@ -1,6 +1,9 @@
import { createApplicationOperations } from './operations.js';
import * as Cesium from 'cesium';
import { createApplicationViewer } from '../app/viewer.js';
import {
createApplicationViewer,
installTrackpadPinchZoom,
} from '../app/viewer.js';
import { registerDataCredits } from '../data/dataCredits.js';
import { configureCreditKeyboardAccess } from '../creditKeyboard.js';
import { MapStackController } from '../mapStackController.js';
@@ -52,6 +55,7 @@ export async function createApplicationScene({
uninstallRenderGovernor(viewer);
if (!viewer.isDestroyed()) viewer.destroy();
});
defer(installTrackpadPinchZoom(viewer));
registerDataCredits(viewer, credits);
configureCreditKeyboardAccess(document);
loaderStatus.textContent =
+101
View File
@@ -1,5 +1,106 @@
import * as Cesium from 'cesium';
const PINCH_ZOOM_MULTIPLIER = 8;
const MAX_PINCH_PIXEL_DELTA = 120;
function boundedPinchDelta(delta) {
if (!Number.isFinite(delta) || delta === 0) return delta;
return (
Math.sign(delta) *
Math.min(Math.abs(delta) * PINCH_ZOOM_MULTIPLIER, MAX_PINCH_PIXEL_DELTA)
);
}
/**
* Add browser trackpad pinch to Cesium's zoom inputs and return its disposer.
* Browsers expose this gesture as a small pixel-mode Ctrl+wheel event.
*/
export function installTrackpadPinchZoom(
viewer,
{ createWheelEvent = (type, init) => new WheelEvent(type, init) } = {},
) {
const controller = viewer?.scene?.screenSpaceCameraController;
const container = viewer?.container;
const canvas = viewer?.canvas;
if (!controller || !container || !canvas)
throw new TypeError('A complete Cesium viewer is required');
const originalZoomEventTypes = controller.zoomEventTypes;
const zoomEventTypes = Array.isArray(originalZoomEventTypes)
? originalZoomEventTypes
: originalZoomEventTypes === undefined
? []
: [originalZoomEventTypes];
const alreadyHandlesControlWheel = zoomEventTypes.some(
(binding) =>
binding?.eventType === Cesium.CameraEventType.WHEEL &&
binding?.modifier === Cesium.KeyboardEventModifier.CTRL,
);
const configuredZoomEventTypes = alreadyHandlesControlWheel
? originalZoomEventTypes
: [
...zoomEventTypes,
{
eventType: Cesium.CameraEventType.WHEEL,
modifier: Cesium.KeyboardEventModifier.CTRL,
},
];
if (!alreadyHandlesControlWheel)
controller.zoomEventTypes = configuredZoomEventTypes;
const relayedEvents = new WeakSet();
const relayPinch = (event) => {
if (
!event.ctrlKey ||
relayedEvents.has(event) ||
event.deltaMode !== 0 ||
!Number.isFinite(event.deltaY) ||
event.deltaY === 0
)
return;
let relayed;
try {
relayed = createWheelEvent('wheel', {
deltaX: event.deltaX,
deltaY: boundedPinchDelta(event.deltaY),
deltaZ: event.deltaZ,
deltaMode: event.deltaMode,
screenX: event.screenX,
screenY: event.screenY,
clientX: event.clientX,
clientY: event.clientY,
ctrlKey: true,
bubbles: true,
cancelable: true,
view: globalThis.window,
});
} catch {
// The registered Ctrl+wheel binding can still consume the original.
return;
}
relayedEvents.add(relayed);
event.preventDefault();
event.stopPropagation();
canvas.dispatchEvent(relayed);
};
container.addEventListener('wheel', relayPinch, {
capture: true,
passive: false,
});
let active = true;
return () => {
if (!active) return;
active = false;
container.removeEventListener('wheel', relayPinch, true);
if (
!alreadyHandlesControlWheel &&
controller.zoomEventTypes === configuredZoomEventTypes
)
controller.zoomEventTypes = originalZoomEventTypes;
};
}
/** Create the standard globe viewer in caller-owned, visible containers. */
export function createApplicationViewer({ container, creditContainer }) {
if (!container || !creditContainer)
+65
View File
@@ -0,0 +1,65 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
aisStreamRows,
ingestAisStreamEnvelope,
} from '../../server/providers/vessels/ais-store.js';
// ITU-R M.1371 encodes speed over ground in 0.1-knot units and course over
// ground in 0.1-degree units, reserving the top code of each field for "not
// available": SOG 1023 arrives as 102.3 knots and COG 3600 as 360 degrees.
// Both are finite numbers, so they reach consumers as ordinary readings unless
// the store rejects them here.
const positionReport = (mmsi, body) => ({
MessageType: 'PositionReport',
Message: { PositionReport: { UserID: Number(mmsi), ...body } },
MetaData: {
MMSI: mmsi,
latitude: 53.40879,
longitude: 6.19741,
time_utc: '2026-09-17 20:00:00.000000000 +0000 UTC',
},
});
const rowFor = (mmsi) => aisStreamRows(50000).find((row) => row.mmsi === mmsi);
test('speed over ground reported as unavailable is unknown, not 102.3 knots', () => {
assert.equal(
ingestAisStreamEnvelope(positionReport('244060365', { Sog: 102.3 })),
true,
);
assert.equal(rowFor('244060365').speed, null);
});
test('course over ground reported as unavailable is unknown, not 360 degrees', () => {
assert.equal(
ingestAisStreamEnvelope(positionReport('245382000', { Cog: 360 })),
true,
);
assert.equal(rowFor('245382000').course, null);
});
test('genuine speed and course readings survive the unavailable check', () => {
ingestAisStreamEnvelope(
positionReport('341260000', { Sog: 12.9, Cog: 74.6 }),
);
const row = rowFor('341260000');
assert.equal(row.speed, 12.9);
assert.equal(row.course, 74.6);
});
test('a stopped vessel keeps its zero speed and zero course', () => {
ingestAisStreamEnvelope(positionReport('244374235', { Sog: 0, Cog: 0 }));
const row = rowFor('244374235');
assert.equal(row.speed, 0);
assert.equal(row.course, 0);
});
test('the fastest encodable reading below the unavailable code is kept', () => {
ingestAisStreamEnvelope(
positionReport('244060366', { Sog: 102.2, Cog: 359.9 }),
);
const row = rowFor('244060366');
assert.equal(row.speed, 102.2);
assert.equal(row.course, 359.9);
});
+11
View File
@@ -42,6 +42,17 @@ export const DATA_CREDITS = [
'<a href="https://adsb.lol" target="_blank" rel="noopener">adsb.lol</a> ' +
'(ODbL 1.0)',
},
{
key: 'adsbdb',
html:
'Aircraft type, registration &amp; flight routes: ' +
'<a href="https://www.adsbdb.com" target="_blank" rel="noopener">adsbdb</a> ' +
'· aircraft data from PlaneBase · ICAO-to-N-number conversion by ' +
'Guillaume Michel · route data is the work of ' +
'David Taylor, Edinburgh, and Jim Mason, Glasgow, and may not be ' +
'copied, published, or incorporated into other databases without the ' +
'explicit permission of David J Taylor, Edinburgh',
},
{
key: 'aisstream',
html:
+39
View File
@@ -0,0 +1,39 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { DATA_CREDITS } from './dataCredits.js';
test('every credit carries a unique key and some markup to render', () => {
const keys = DATA_CREDITS.map((entry) => entry.key);
assert.equal(
new Set(keys).size,
keys.length,
'a duplicate key would silently shadow one provider’s credit',
);
for (const entry of DATA_CREDITS) {
assert.ok(entry.key, 'a credit without a key cannot be registered');
assert.ok(
entry.html && entry.html.trim().length > 0,
`credit ${entry.key} has nothing to show`,
);
}
});
test('adsbdb is credited and carries its published route-data restriction', () => {
const credit = DATA_CREDITS.find((entry) => entry.key === 'adsbdb');
assert.ok(
credit,
'adsbdb supplies aircraft type and routes and must be credited',
);
// adsbdb publishes this restriction for its route data. Pin the provider's
// credits and restriction here so a later edit cannot silently remove them.
assert.match(credit.html, /David Taylor, Edinburgh/);
assert.match(credit.html, /Jim Mason, Glasgow/);
assert.match(
credit.html,
/may not be\s+copied, published, or incorporated into other databases/,
);
assert.match(credit.html, /explicit permission of David J Taylor, Edinburgh/);
assert.match(credit.html, /PlaneBase/);
assert.match(credit.html, /Guillaume Michel/);
assert.match(credit.html, /href="https:\/\/www\.adsbdb\.com"/);
});
+46 -4
View File
@@ -123,12 +123,51 @@ export function deriveFetchCenter({
return { lat: pulled.lat, lon: pulled.lon, source: 'pulled' };
}
/**
* Wrapped longitude span between two meridians, in degrees.
*
* Longitude is cyclic, so a box crossing the antimeridian (±180°) has
* `east < west` (e.g. `west = 179.98, east = -179.98`). A plain `east - west`
* goes negative in that case; wrap through 360° instead.
*
* @param {number} west - West edge longitude (degrees).
* @param {number} east - East edge longitude (degrees).
* @returns {number} Span in degrees, always >= 0.
*/
function longitudeSpanDeg(west, east) {
const raw = east - west;
return raw >= 0 ? raw : raw + 360;
}
/** Keep a monotonic longitude range inside the traffic source's legal domain. */
function boundedLongitudeRange(center, span) {
let west = center - span / 2;
let east = center + span / 2;
if (east > 180) {
west -= east - 180;
east = 180;
}
if (west < -180) {
east += -180 - west;
west = -180;
}
return { west, east };
}
/**
* Clamp a bounding box's spans to `maxSpanDeg` and recenter it on `center`.
*
* Preserves the pre-C4 span semantics (each axis capped at 0.05° ≈ 5.5 km)
* but centers the box on the derived look-at point instead of the view
* rectangle's midpoint. Idempotent when `center` is the box's own midpoint.
* rectangle's midpoint. At the antimeridian the whole box shifts just far
* enough to remain inside [-180, 180], because the road source accepts one
* monotonic Overpass bounding box. Idempotent when `center` is the box's own
* midpoint.
*
* Longitude is handled as a cyclic axis for the span calculation, so the
* input span wraps correctly across the antimeridian. Output remains
* monotonic because callers such as `getBoundsCenter` and `boundsOverlap`
* rely on `west <= east`.
*
* @param {{south:number, west:number, north:number, east:number}} bounds
* Source bounds (span donor).
@@ -138,11 +177,14 @@ export function deriveFetchCenter({
*/
export function clampBoundsAroundCenter(bounds, center, maxSpanDeg = 0.05) {
const latSpan = Math.min(bounds.north - bounds.south, maxSpanDeg);
const lonSpan = Math.min(bounds.east - bounds.west, maxSpanDeg);
const lonSpan = Math.min(
longitudeSpanDeg(bounds.west, bounds.east),
maxSpanDeg,
);
const longitude = boundedLongitudeRange(center.lon, lonSpan);
return {
south: center.lat - latSpan / 2,
north: center.lat + latSpan / 2,
west: center.lon - lonSpan / 2,
east: center.lon + lonSpan / 2,
...longitude,
};
}
+16
View File
@@ -108,3 +108,19 @@ test('span clamp is idempotent on already-clamped bounds (loadRoadsForBounds re-
const twice = clampBoundsAroundCenter(once, midpoint, 0.05);
assert.deepEqual(twice, once);
});
test('span clamp keeps antimeridian bounds monotonic and request-safe', () => {
for (const centerLon of [179.99, -179.99]) {
const result = clampBoundsAroundCenter({
south: -0.02,
north: 0.02,
west: 179.98,
east: -179.98,
}, { lat: 0, lon: centerLon }, 0.05);
assert.ok(result.west >= -180 && result.east <= 180);
assert.ok(result.west <= result.east, `west/east not monotonic: ${result.west}, ${result.east}`);
assert.ok(Math.abs(result.east - result.west - 0.04) < 1e-12);
assert.ok(result.west <= centerLon && centerLon <= result.east);
}
});
+23
View File
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
import test from 'node:test';
import { readFileSync } from 'node:fs';
import { createTrafficSource } from './source.js';
import { clampBoundsAroundCenter } from '../../data/trafficBounds.js';
const bounds = { south: 30.267, west: -97.744, north: 30.268, east: -97.743 };
const fixture = readFileSync(
new URL(
@@ -87,6 +88,28 @@ test('road requests have finite bounds and retain the two-pass query', async ()
/residential/,
);
});
test('antimeridian clamps produce road bounds accepted on either side', async () => {
const calls = [];
const source = createTrafficSource({
fetchImpl: async (...args) => {
calls.push(args);
return new Response('{"elements":[]}');
},
});
for (const centerLon of [179.99, -179.99]) {
const clamped = clampBoundsAroundCenter(
{
south: -0.02,
north: 0.02,
west: 179.98,
east: -179.98,
},
{ lat: 0, lon: centerLon },
);
await source.requestRoads(clamped);
}
assert.equal(calls.length, 2);
});
test('malformed availability is an unavailable source rather than a keyless response', async () => {
const source = createTrafficSource({
fetchImpl: async () => new Response('{}'),
+63 -1
View File
@@ -12,6 +12,7 @@ import {
isConfiguredValue,
npmProcessSpec,
readDoctorDotenvValue,
resolveOpenSkyAuthMode,
resolveCredential,
} from '../scripts/setup-doctor.mjs';
@@ -145,6 +146,67 @@ test('doctor reads the dotenv ladder without requiring Vite to be installed', ()
}
});
test('doctor resolves OpenSky auth mode with launcher-compatible precedence', () => {
const root = mkdtempSync(path.join(tmpdir(), 'gev-doctor-opensky-mode-'));
try {
writeFileSync(path.join(root, '.env'), 'OPENSKY_AUTH_MODE=anon\n');
assert.equal(resolveOpenSkyAuthMode({ environment: {}, rootDir: root }), 'anon');
assert.equal(resolveOpenSkyAuthMode({ environment: { OPENSKY_AUTH_MODE: 'oauth' }, rootDir: root }), 'oauth');
assert.equal(resolveOpenSkyAuthMode({ environment: { OPENSKY_AUTH_MODE: '' }, rootDir: root }), 'anon');
assert.equal(resolveOpenSkyAuthMode({ environment: { OPENSKY_AUTH_MODE: 'invalid' }, rootDir: root }), 'oauth');
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test('doctor reports keyless anonymous OpenSky access for explicit anon mode', () => {
const credentials = {
OPENSKY_CLIENT_ID: { configured: true },
OPENSKY_CLIENT_SECRET: { configured: true },
};
assert.equal(
buildCapabilitySummary(credentials, { openSkyAuthMode: 'anon' }).flights,
'OpenSky keyless anonymous access (rate-limited)',
);
});
test('doctor retains OAuth capability wording for a complete client pair', () => {
const credentials = {
OPENSKY_CLIENT_ID: { configured: true },
OPENSKY_CLIENT_SECRET: { configured: true },
};
assert.equal(
buildCapabilitySummary(credentials, { openSkyAuthMode: 'oauth' }).flights,
'OpenSky OAuth credentials present (runtime mode and validity not verified)',
);
});
test('doctor reports Basic mode without inferring runtime auth from OAuth credentials', () => {
const oauthCredentials = {
OPENSKY_CLIENT_ID: { configured: true },
OPENSKY_CLIENT_SECRET: { configured: true },
};
for (const credentials of [{}, oauthCredentials]) {
assert.equal(
buildCapabilitySummary(credentials, { openSkyAuthMode: 'basic' }).flights,
'OpenSky Basic mode selected (credential presence and validity not verified)',
);
}
});
test('doctor reports auto mode without assuming its eventual credential choice', () => {
const oauthCredentials = {
OPENSKY_CLIENT_ID: { configured: true },
OPENSKY_CLIENT_SECRET: { configured: true },
};
for (const credentials of [{}, oauthCredentials]) {
assert.equal(
buildCapabilitySummary(credentials, { openSkyAuthMode: 'auto' }).flights,
'OpenSky auto mode selected (runtime credential choice and validity not verified)',
);
}
});
test('doctor describes the credential ladder without exposing values', () => {
const credentials = {
GOOGLE_MAPS_API_KEY: { configured: false },
@@ -163,7 +225,7 @@ test('doctor describes the credential ladder without exposing values', () => {
assert.match(capabilities.map, /Bing and world-terrain stacks/);
assert.equal(capabilities.voice, 'available');
assert.match(capabilities.missions, /token allowance/);
assert.equal(capabilities.flights, 'OpenSky OAuth credentials not configured');
assert.equal(capabilities.flights, 'OpenSky keyless anonymous access (rate-limited)');
const report = formatSetupReport({
ready: true,
+151
View File
@@ -1,5 +1,6 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createServer } from 'node:http';
import { Readable } from 'node:stream';
import {
mkdtempSync,
@@ -279,7 +280,157 @@ test('Realtime service configuration selects compatible endpoint/model without f
url: '/?tier=arbitrary-model&model=other',
});
assert.equal(response.status, 200);
assert.deepEqual(response.json(), { value: 'short-lived-fixture' });
assert.equal(response.headers['x-gev-voice-model'], 'configured-model');
assert.equal(response.headers['cache-control'], 'no-store');
assert.doesNotMatch(response.body, /server-fixture|voice\.example/);
});
test('OpenAI routes answer generically when the upstream or the request fails', async (t) => {
env(t, 'OPENAI_API_KEY', 'fixture-upstream-secret');
env(t, 'GEV_RATELIMIT_OPENAI_PER_MIN', undefined);
const leak =
'fixture-upstream-secret req_fixture_1234 org-fixture quota exhausted';
// `data.error.message` is OpenAI's own wording — request ids, organization
// hints, quota phrasing — and was relayed verbatim whenever upstream was not ok.
t.mock.method(globalThis, 'fetch', async () =>
Response.json({ error: { message: leak } }, { status: 429 }),
);
const summary = await request(
install(openAiRealtimeProxy()).get('/api/openai/hud-summary'),
{ method: 'POST', body: JSON.stringify({ context: {} }) },
);
assert.equal(summary.json().error, 'OpenAI HUD summary request failed');
assert.equal(summary.body.includes('req_fixture_1234'), false);
assert.equal(summary.body.includes('fixture-upstream-secret'), false);
// An HTTP error from the client-secret endpoint carries the same upstream
// detail, while a successful response must still pass the ephemeral secret.
t.mock.restoreAll();
t.mock.method(globalThis, 'fetch', async () =>
Response.json({ error: { message: leak } }, { status: 429 }),
);
const rejectedToken = await request(
install(openAiRealtimeProxy()).get('/api/realtime/token'),
);
assert.equal(rejectedToken.status, 429);
assert.equal(
rejectedToken.headers['content-type'],
'application/json; charset=utf-8',
);
assert.deepEqual(rejectedToken.json(), {
error: 'Failed to create Realtime token',
});
assert.equal(rejectedToken.body.includes('req_fixture_1234'), false);
assert.equal(rejectedToken.body.includes('fixture-upstream-secret'), false);
// A network fault surfaced a resolver message naming the upstream host.
t.mock.restoreAll();
t.mock.method(globalThis, 'fetch', async () => {
throw Error(`getaddrinfo ENOTFOUND api.openai.com ${leak}`);
});
const token = await request(
install(openAiRealtimeProxy()).get('/api/realtime/token'),
);
assert.equal(token.status, 502);
assert.deepEqual(token.json(), { error: 'Failed to create Realtime token' });
assert.equal(token.body.includes('api.openai.com'), false);
assert.equal(token.body.includes('fixture-upstream-secret'), false);
});
test('the debug-log sink stays bounded, rate limited, and quiet about failures', async (t) => {
const sourceRoot = root(t);
const handler = install(openAiRealtimeProxy({ sourceRoot })).get(
'/api/realtime/debug-log',
);
const file = path.join(sourceRoot, '.gev-logs/realtime-conversations.jsonl');
const write = (record) =>
request(handler, { method: 'POST', body: JSON.stringify(record) });
// A malformed record is the caller's fault and a 400; neither answer carries
// the error text, which for a write failure is an errno and an absolute path.
const malformed = await request(handler, { method: 'POST', body: '{nope' });
assert.equal(malformed.status, 400);
assert.deepEqual(malformed.json(), {
error: 'Failed to write Realtime debug log',
});
// The limiter is always on, unlike the opt-in one the cost-bearing routes
// share: 120/min per IP, far above what a voice session writes.
let limited = null;
let accepted = 0;
for (let n = 0; n < 130 && !limited; n += 1) {
const response = await write({ n });
if (response.status === 429) limited = response;
else if (response.status === 204) accepted += 1;
}
assert.ok(limited, 'the sink refuses a caller past its per-minute ceiling');
assert.equal(limited.headers['retry-after'], '60');
assert.deepEqual(limited.json(), { error: 'Rate limit exceeded' });
// The limiter counts requests rather than successful writes, so the malformed
// record above already spent one of the 120 slots.
assert.equal(accepted, 119);
// Every accepted record is on disk and parses: the queue serializes appends,
// so none was lost or truncated by the ones beside it.
const lines = readFileSync(file, 'utf8').split('\n').filter(Boolean);
assert.equal(lines.length, accepted);
for (const line of lines) assert.doesNotThrow(() => JSON.parse(line));
assert.ok(lines.every((line) => JSON.parse(line).loggedAt));
});
test('an oversized debug-log request receives the fixed error response', async (t) => {
const handler = install(openAiRealtimeProxy({ sourceRoot: root(t) })).get(
'/api/realtime/debug-log',
);
const server = createServer(handler);
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
t.after(() => new Promise((resolve) => server.close(resolve)));
const response = await fetch(
`http://127.0.0.1:${server.address().port}/api/realtime/debug-log`,
{ method: 'POST', body: 'x'.repeat(8 * 1024 * 1024 + 1) },
);
assert.equal(response.status, 400);
assert.deepEqual(await response.json(), {
error: 'Failed to write Realtime debug log',
});
});
test('the debug log rotates instead of growing without bound', async (t) => {
const sourceRoot = root(t);
const handler = install(openAiRealtimeProxy({ sourceRoot })).get(
'/api/realtime/debug-log',
);
const file = path.join(sourceRoot, '.gev-logs/realtime-conversations.jsonl');
// 8 MB bounds one request body; nothing bounded the file until now, so a
// single page could grow it for as long as the dev server ran. Each record
// here is ~1 MB, well inside the body cap.
const pad = 'x'.repeat(1024 * 1024);
// Seventy-two records force two rotations. The second one replaces an
// existing `.1`, which requires an explicit removal on Windows.
for (let n = 0; n < 72; n += 1) {
assert.equal(
(
await request(handler, {
method: 'POST',
body: JSON.stringify({ n, pad }),
})
).status,
204,
);
}
const live = statSync(file).size;
const previous = statSync(`${file}.1`).size;
assert.ok(live <= 32 * 1024 * 1024, `live log under the ceiling (${live})`);
assert.ok(
live + previous <= 64 * 1024 * 1024,
'both generations together stay within twice the ceiling',
);
// Unrotated, these records would be ~75 MB in one file.
assert.ok(live + previous < 64 * 1024 * 1024);
assert.ok(!existsSync(`${file}.2`), 'exactly one generation is retained');
});
+42
View File
@@ -149,3 +149,45 @@ for (const hook of ['configureServer', 'configurePreviewServer']) {
assert.equal(cancelled, true);
});
}
test('a failed CCTV media fetch reports a fixed health message, not the error text', async (t) => {
isolate(t);
const root = mkdtempSync(path.join(tmpdir(), 'gev-cctv-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
mkdirSync(path.join(root, 'config'));
writeFileSync(
path.join(root, 'config/cctv_sources.austin.json'),
JSON.stringify([
{
id: 'leaky',
name: 'Leaky camera',
lat: 30.27,
lon: -97.74,
feedType: 'video',
url: 'https://cams.fixture.invalid/leaky.m3u8',
},
]),
);
const leak =
'connect ETIMEDOUT 203.0.113.9:443 C:\fixture\secret-path\cams.json';
t.mock.restoreAll();
t.mock.method(globalThis, 'fetch', () => {
throw Error(leak);
});
const call = install(cctvProxy({ sourceRoot: root }));
const media = await call('/media/leaky');
assert.equal(media.status, 502);
assert.equal(media.body.includes('203.0.113.9'), false);
// GET /health is the second door: src/layers/cctv/frames.js renders each
// entry's `message` as the camera's status label, so a raw errno stored here
// reaches the screen even though the response above is sanitized.
const camera = JSON.parse((await call('/health')).body).cameras.find(
(entry) => entry.id === 'leaky',
);
assert.equal(camera.status, 'degraded');
assert.equal(camera.message, 'Media fetch failed');
assert.equal(camera.message.includes('ETIMEDOUT'), false);
assert.equal(camera.message.includes('secret-path'), false);
});
+22
View File
@@ -14,6 +14,8 @@
--text-primary: #e8eaed;
--text-secondary: rgba(232, 234, 237, 0.5);
--text-dim: rgba(232, 234, 237, 0.3);
/* Opaque surface for browser-painted menus, which cannot use --glass-bg. */
--menu-bg: #12121c;
--font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
--panel-radius: 16px;
@@ -69,6 +71,26 @@
outline-offset: 2px;
}
/* Native `<select>` popups are painted by the browser, not by this sheet: the
closed control inherits its panel skin, but the option list falls back to the
platform's light palette unless the control declares a scheme. Declare it on
the controls rather than on :root so the app keeps its own hand-tuned
scrollbar and input skins, and set the option colors explicitly because the
popup surface is not themed on every platform. */
select {
color-scheme: dark;
}
select option,
select optgroup {
background-color: var(--menu-bg);
color: var(--text-primary);
}
select option:disabled {
color: var(--text-dim);
}
html,
body {
width: 100%;