mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
Merge branch 'main' into contrib/gods-012-surface-tab-order-20261011-reviewed
This commit is contained in:
15 files changed
+299
-32
No files matched your search
+19
-3
@@ -101,6 +101,10 @@ lists at 25 rows by default. Voice offers the same queries next to its app
|
||||
actions: the session lists them, and the browser runs them through the same
|
||||
catalog, loaded on first use. See [tools and the MCP server](TOOLS.md).
|
||||
|
||||
Catalog calls check their cancellation signal immediately before executing a
|
||||
tool. Already-cancelled queries do not start; cancellation after execution starts
|
||||
continues to follow the existing tool and caller behavior.
|
||||
|
||||
## Cyber HUD — September 30, 2026
|
||||
|
||||
Display > HUD > Layout includes Cyber, also available through the HUD voice
|
||||
@@ -1176,7 +1180,9 @@ stalled downloads are cancelled. Development and preview use the same handler.
|
||||
Overpass query validation, geometry simplification, disk caching and upstream
|
||||
transport now have separate modules; military-installation search reuses that
|
||||
transport. Regional briefing combines separate place, news and weather sources,
|
||||
while weather effects uses only the weather source. Existing process-scoped
|
||||
while weather effects uses only the weather source.
|
||||
GDELT compact dates that do not name a real calendar instant keep the article
|
||||
with an unknown publication time. Existing process-scoped
|
||||
caches, rate limits, stale fallbacks and route ordering are preserved.
|
||||
|
||||
Local voice has separate HUD-summary, debug-log and Realtime-token handlers,
|
||||
@@ -1205,6 +1211,11 @@ a browser key. Preview is for local build verification, not a production server.
|
||||
|
||||
## CCTV and radio provider modules
|
||||
|
||||
CCTV direction text resolves travel tokens first, then intercardinals, then
|
||||
bare cardinals when enabled. Only conflicting headings within the highest
|
||||
matching tier remain unknown; street quadrants and lower-tier words do not
|
||||
cancel an explicit travel heading.
|
||||
|
||||
CCTV catalog acquisition, source normalization and frame/media delivery now live
|
||||
in separate modules. Each CCTV factory owns its catalog and health state; its
|
||||
`sourceRoot` option resolves relative source files against the application root.
|
||||
@@ -1225,6 +1236,10 @@ The browser layers and their rendering remain in their existing modules.
|
||||
|
||||
## Terrain height cache bound
|
||||
|
||||
The terrain proxy parser rejects finite longitudes outside [-180, 180] and
|
||||
latitudes outside [-90, 90] for every client before upstream resolution. One
|
||||
out-of-range point rejects the whole batch; inclusive WGS84 bounds remain valid.
|
||||
|
||||
The client terrain-height resolver's in-memory cache is bounded at 20 000
|
||||
entries (`DEFAULT_MAX_CACHE_ENTRIES`, overridable per instance through
|
||||
`createTerrainHeights({ maxCacheEntries })`). Coordinate rounding makes repeated
|
||||
@@ -1462,7 +1477,7 @@ accessible name remains "Don't show this again". Control behavior is unchanged.
|
||||
|
||||
## FIRMS source status
|
||||
|
||||
FIRMS ingestion and adaptation require explicit finite coordinates within WGS84 ranges; genuine zero coordinates remain valid and admitted detection indices stay sequential.
|
||||
The FIRMS CSV parser rejects impossible acquisition calendar dates before the proxy filters records to the trailing 24 hours, while retaining valid UTC acquisition times.
|
||||
|
||||
The FIRMS proxy records source success after appending its rows. If aggregation
|
||||
throws, that source reports failure without a contradictory success entry.
|
||||
@@ -4131,7 +4146,8 @@ when its own signal aborts.
|
||||
Configured endpoints have per-instance cooldowns for 406/429, honoring
|
||||
Retry-After; absent delays back off from 30 seconds to five minutes. Empty
|
||||
`elements` is a valid answer. Existing byte, query, concurrency and cache caps
|
||||
remain. Consumer capability misses never schedule automatic retries.
|
||||
remain. Malformed capability-probe responses remain unknown and are retried after the existing 30-second backoff; valid configured decisions remain memoized.
|
||||
Consumer capability misses never schedule automatic retries.
|
||||
|
||||
Operator-configured Overpass area and focus-footprint queries use body-level
|
||||
`out geom` (with `center` for focus), preserving relation member outlines.
|
||||
|
||||
@@ -313,7 +313,9 @@ test('retry: a plain HTTP 500 still exhausts the existing 8s/25s transient ladde
|
||||
t.mock.timers.enable({ apis: ['setTimeout', 'Date'], now: 100_000 });
|
||||
globalThis.window = { setTimeout: globalThis.setTimeout, clearTimeout: globalThis.clearTimeout };
|
||||
const requestTimes = [];
|
||||
globalThis.fetch = async () => {
|
||||
globalThis.fetch = async (url) => {
|
||||
if (String(url).endsWith('/status'))
|
||||
return Response.json({ configured: true });
|
||||
requestTimes.push(Date.now());
|
||||
return httpFailure(500);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { directionToHeading } from './directionText.js';
|
||||
test('conflicting facing directions cannot manufacture one heading', () => {
|
||||
for (const bare of [false, true]) {
|
||||
for (const value of ['NB / SB', 'EASTBOUND WESTBOUND', 'NE SW'])
|
||||
assert.ok(Number.isNaN(directionToHeading(value, bare)), value);
|
||||
assert.equal(directionToHeading('NB northbound', bare), 0);
|
||||
assert.equal(directionToHeading('IH-35 SOUTHBOUND AT 15TH', bare), 180);
|
||||
}
|
||||
assert.ok(Number.isNaN(directionToHeading('North / East', true)));
|
||||
assert.equal(directionToHeading('West / SR-24', true), 270);
|
||||
});
|
||||
|
||||
test('travel tokens retain precedence over quadrant streets and bare words', () => {
|
||||
for (const [text, heading] of [
|
||||
['I-5 NB at NE 45th St', 0],
|
||||
['SR 520 EB at NE 148th', 90],
|
||||
['US 13 SB @ SW Front St', 180],
|
||||
]) {
|
||||
for (const bare of [false, true])
|
||||
assert.equal(directionToHeading(text, bare), heading);
|
||||
}
|
||||
assert.equal(
|
||||
directionToHeading('Hwy 401 east of Keele St - Westbound', true),
|
||||
270,
|
||||
);
|
||||
assert.equal(directionToHeading('NE near West St', true), 45);
|
||||
assert.ok(Number.isNaN(directionToHeading('NB SB near NE St', true)));
|
||||
});
|
||||
+28
-16
@@ -28,22 +28,34 @@ export function directionToHeading(value, allowBare = false) {
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
if (!text) return NaN;
|
||||
// Explicit travel/intercardinal forms — safe on free-form text (a street
|
||||
// name almost never contains "NORTHBOUND" or a lone "NB" token).
|
||||
if (/\bNORTHBOUND\b|\bNB\b/.test(text)) return 0;
|
||||
if (/\bSOUTHBOUND\b|\bSB\b/.test(text)) return 180;
|
||||
if (/\bEASTBOUND\b|\bEB\b/.test(text)) return 90;
|
||||
if (/\bWESTBOUND\b|\bWB\b/.test(text)) return 270;
|
||||
if (/\bNORTHEAST\b|\bNE\b/.test(text)) return 45;
|
||||
if (/\bNORTHWEST\b|\bNW\b/.test(text)) return 315;
|
||||
if (/\bSOUTHEAST\b|\bSE\b/.test(text)) return 135;
|
||||
if (/\bSOUTHWEST\b|\bSW\b/.test(text)) return 225;
|
||||
// Bare cardinals — dedicated direction fields only.
|
||||
if (allowBare) {
|
||||
if (/\bNORTH\b/.test(text)) return 0;
|
||||
if (/\bSOUTH\b/.test(text)) return 180;
|
||||
if (/\bEAST\b/.test(text)) return 90;
|
||||
if (/\bWEST\b/.test(text)) return 270;
|
||||
const tiers = [
|
||||
[
|
||||
[/\bNORTHBOUND\b|\bNB\b/, 0],
|
||||
[/\bSOUTHBOUND\b|\bSB\b/, 180],
|
||||
[/\bEASTBOUND\b|\bEB\b/, 90],
|
||||
[/\bWESTBOUND\b|\bWB\b/, 270],
|
||||
],
|
||||
[
|
||||
[/\bNORTHEAST\b|\bNE\b/, 45],
|
||||
[/\bNORTHWEST\b|\bNW\b/, 315],
|
||||
[/\bSOUTHEAST\b|\bSE\b/, 135],
|
||||
[/\bSOUTHWEST\b|\bSW\b/, 225],
|
||||
],
|
||||
];
|
||||
if (allowBare)
|
||||
tiers.push([
|
||||
[/\bNORTH\b/, 0],
|
||||
[/\bSOUTH\b/, 180],
|
||||
[/\bEAST\b/, 90],
|
||||
[/\bWEST\b/, 270],
|
||||
]);
|
||||
for (const forms of tiers) {
|
||||
const headings = new Set();
|
||||
for (const [pattern, heading] of forms) {
|
||||
if (pattern.test(text)) headings.add(heading);
|
||||
}
|
||||
if (headings.size)
|
||||
return headings.size === 1 ? headings.values().next().value : NaN;
|
||||
}
|
||||
return NaN;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { acquisitionMsUtc } from './firmsCsv.js';
|
||||
test('FIRMS acquisition dates cannot roll into a different calendar day', () => {
|
||||
for (const [date, time] of [
|
||||
['2026-02-30', '1200'],
|
||||
['2026-04-31', '1200'],
|
||||
['2026-13-01', '1200'],
|
||||
['2026-07-16junk', '1200'],
|
||||
['2026-07-16', '2400'],
|
||||
['2026-07-16', '1260'],
|
||||
['2026-07-16', null],
|
||||
['2026-07-16', ''],
|
||||
]) {
|
||||
assert.ok(Number.isNaN(acquisitionMsUtc(date, time)), `${date}:${time}`);
|
||||
}
|
||||
for (const [date, time, expected] of [
|
||||
['2024-02-29', '45', Date.UTC(2024, 1, 29, 0, 45)],
|
||||
['2026-07-16', '0', Date.UTC(2026, 6, 16)],
|
||||
]) {
|
||||
assert.equal(acquisitionMsUtc(date, time), expected);
|
||||
}
|
||||
});
|
||||
@@ -163,7 +163,13 @@ export function acquisitionMsUtc(acqDate, acqTime) {
|
||||
minutes > 59
|
||||
)
|
||||
return NaN;
|
||||
return Date.UTC(year, month - 1, day, hours, minutes);
|
||||
const ms = Date.UTC(year, month - 1, day, hours, minutes);
|
||||
const parsed = new Date(ms);
|
||||
return parsed.getUTCFullYear() === year &&
|
||||
parsed.getUTCMonth() === month - 1 &&
|
||||
parsed.getUTCDate() === day
|
||||
? ms
|
||||
: NaN;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { normalizeRegionalArticles } from './regionalModel.js';
|
||||
test('compact source dates must denote an actual calendar instant', () => {
|
||||
for (const date of [
|
||||
'20260230T120000Z',
|
||||
'20260431T120000Z',
|
||||
'20261301T120000Z',
|
||||
'20260716T240000Z',
|
||||
'20260716T126000Z',
|
||||
'20260716T120060Z',
|
||||
]) {
|
||||
const [article] = normalizeRegionalArticles({
|
||||
articles: [
|
||||
{
|
||||
title: 'Fixture',
|
||||
url: 'https://fixture.example/news',
|
||||
seendate: date,
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.equal(article.publishedAt, null, date);
|
||||
assert.equal(article.title, 'Fixture');
|
||||
}
|
||||
for (const [input, expected] of [
|
||||
['20240229T004500Z', '2024-02-29T00:45:00Z'],
|
||||
['20260716T000000Z', '2026-07-16T00:00:00Z'],
|
||||
['2026-07-16T12:00:00Z', '2026-07-16T12:00:00.000Z'],
|
||||
]) {
|
||||
assert.equal(
|
||||
normalizeRegionalArticles({
|
||||
articles: [
|
||||
{
|
||||
title: 'Fixture',
|
||||
url: 'https://fixture.example/news',
|
||||
seendate: input,
|
||||
},
|
||||
],
|
||||
})[0].publishedAt,
|
||||
expected,
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -67,11 +67,20 @@ export function normalizeRegionalArticles(payload, limit = MAX_ARTICLES) {
|
||||
seen.add(signature);
|
||||
const rawDate = cleanText(row?.seendate, 32);
|
||||
const compactDate = /^(\d{8})T(\d{6})Z$/.exec(rawDate);
|
||||
const publishedAt = compactDate
|
||||
const candidateDate = compactDate
|
||||
? `${compactDate[1].slice(0, 4)}-${compactDate[1].slice(4, 6)}-${compactDate[1].slice(6, 8)}T${compactDate[2].slice(0, 2)}:${compactDate[2].slice(2, 4)}:${compactDate[2].slice(4, 6)}Z`
|
||||
: Number.isNaN(Date.parse(rawDate))
|
||||
? null
|
||||
: new Date(rawDate).toISOString();
|
||||
: rawDate;
|
||||
const parsedMs = Date.parse(candidateDate);
|
||||
const validDate =
|
||||
Number.isFinite(parsedMs) &&
|
||||
(!compactDate ||
|
||||
new Date(parsedMs).toISOString().slice(0, 19) ===
|
||||
candidateDate.slice(0, 19));
|
||||
const publishedAt = validDate
|
||||
? compactDate
|
||||
? candidateDate
|
||||
: new Date(parsedMs).toISOString()
|
||||
: null;
|
||||
articles.push({
|
||||
title,
|
||||
url,
|
||||
|
||||
@@ -35,7 +35,13 @@ export function parseTerrainPoints(raw) {
|
||||
if (parts.length !== 2) return null;
|
||||
const lon = Number(parts[0]);
|
||||
const lat = Number(parts[1]);
|
||||
if (!Number.isFinite(lon) || !Number.isFinite(lat)) return null;
|
||||
if (
|
||||
!Number.isFinite(lon) ||
|
||||
!Number.isFinite(lat) ||
|
||||
Math.abs(lon) > 180 ||
|
||||
Math.abs(lat) > 90
|
||||
)
|
||||
return null;
|
||||
points.push([lon, lat]);
|
||||
}
|
||||
return points;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
parseTerrainPoints,
|
||||
fetchTerrainChunkWithRetry,
|
||||
resolveTerrainHeightRequest,
|
||||
terrainPointKey,
|
||||
@@ -227,3 +228,13 @@ test('malformed height objects remain refresh failures, not absent readings', as
|
||||
assert.equal(response.cacheChanged, false);
|
||||
}
|
||||
});
|
||||
|
||||
test('terrain parser rejects out-of-range WGS84 points and accepts inclusive bounds', () => {
|
||||
for (const value of ['181,0', '-181,0', '0,91', '0,-91', '1,2;0,91'])
|
||||
assert.equal(parseTerrainPoints(value), null, value);
|
||||
assert.deepEqual(parseTerrainPoints('-180,-90;180,90;0,0'), [
|
||||
[-180, -90],
|
||||
[180, 90],
|
||||
[0, 0],
|
||||
]);
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { parseTerrainPoints } from './data/terrainHeightsProxy.js';
|
||||
import { readResponseTextCapped, coalesceProxyRequest } from './sources/httpBody.js';
|
||||
|
||||
const source = ['local.js', 'common/http.js', 'aircraft/enrichment.js', 'terrain.js', 'space/celestrak.js', 'space/launch-library.js', '../../src/data/spaceProviderRequests.js']
|
||||
@@ -172,3 +173,18 @@ test('ADSBDB retains validation and missing-aircraft semantics', async () => {
|
||||
assert.equal(absent.status, 200);
|
||||
assert.deepEqual(JSON.parse(absent.body), { found: false });
|
||||
});
|
||||
|
||||
for (const preview of [false, true]) {
|
||||
test(`terrain server rejects out-of-range points before upstream resolution in ${preview ? 'preview' : 'development'}`, async () => {
|
||||
let calls = 0;
|
||||
const app = fixture('terrainHeightsProxy', { parseTerrainPoints,
|
||||
resolveTerrainHeightRequest: async () => { calls++; return { status: 200, body: { results: [] } }; },
|
||||
}, preview);
|
||||
for (const points of ['181,0', '-181,0', '0,91', '0,-91', '1,2;0,91']) {
|
||||
assert.equal((await app.request('/?points=' + encodeURIComponent(points))).status, 400);
|
||||
}
|
||||
assert.equal(calls, 0);
|
||||
assert.equal((await app.request('/?points=-180,-90;180,90')).status, 200);
|
||||
assert.equal(calls, 1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createApplicationRequestServices } from './requests.js';
|
||||
test('unknown capability replies recover after backoff', async (t) => {
|
||||
t.mock.timers.enable({ apis: ['Date'], now: 100_000 });
|
||||
for (const first of [
|
||||
() => Response.json({ wrong: true }),
|
||||
() => new Response('bad', { status: 503 }),
|
||||
]) {
|
||||
let probes = 0;
|
||||
let queries = 0;
|
||||
const services = createApplicationRequestServices({
|
||||
boundaryProbe: { timeoutMs: 1000, retryMs: 1000 },
|
||||
fetchImpl: async (url) => {
|
||||
if (url.endsWith('/status'))
|
||||
return ++probes === 1
|
||||
? first()
|
||||
: Response.json({ configured: false });
|
||||
queries++;
|
||||
return Response.json({ elements: [] });
|
||||
},
|
||||
});
|
||||
assert.deepEqual(await services.boundaries.query('fixture'), []);
|
||||
assert.deepEqual(await services.boundaries.query('fixture'), []);
|
||||
assert.equal(probes, 1, 'unknown reply is not re-probed inside backoff');
|
||||
t.mock.timers.tick(1000);
|
||||
assert.equal(
|
||||
(await services.boundaries.query('fixture')).code,
|
||||
'OVERPASS_NOT_CONFIGURED',
|
||||
);
|
||||
assert.equal(
|
||||
(await services.boundaries.query('fixture')).code,
|
||||
'OVERPASS_NOT_CONFIGURED',
|
||||
);
|
||||
assert.equal(probes, 2);
|
||||
assert.equal(queries, 2);
|
||||
}
|
||||
});
|
||||
@@ -66,9 +66,9 @@ export function createApplicationRequestServices({
|
||||
throw new Error(`${label} unavailable (${response.status})`);
|
||||
return response.data;
|
||||
}
|
||||
// Learn once per page whether the server has an Overpass instance, so an
|
||||
// Remember valid capability decisions for the page, so an
|
||||
// unconfigured server is never asked (and never answers with an error).
|
||||
// Discovery is bounded; a failed or timed-out probe is forgotten and not
|
||||
// Discovery is bounded; failed, malformed or timed-out probes are forgotten and not
|
||||
// retried for a short backoff, during which queries go to the server as
|
||||
// they would against an older server without the probe.
|
||||
let boundaryCapability = null;
|
||||
@@ -79,10 +79,13 @@ export function createApplicationRequestServices({
|
||||
boundaryCapability = request(`${urls.boundaries}/status`, {
|
||||
signal: AbortSignal.timeout(boundaryProbe.timeoutMs),
|
||||
}).then(
|
||||
(response) =>
|
||||
response.ok && typeof response.data?.configured === 'boolean'
|
||||
? response.data.configured
|
||||
: null, // an older server: fall back to asking per query
|
||||
(response) => {
|
||||
if (response.ok && typeof response.data?.configured === 'boolean')
|
||||
return response.data.configured;
|
||||
boundaryCapability = null;
|
||||
boundaryProbeRetryAt = Date.now() + boundaryProbe.retryMs;
|
||||
return null; // capability unknown: let this query ask the server
|
||||
},
|
||||
(error) => {
|
||||
boundaryCapability = null;
|
||||
if (lifetime?.aborted) throw error;
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { composeCatalog, defineTool } from './catalog.js';
|
||||
|
||||
for (const intercept of [false, true]) {
|
||||
test(`cancelled query refuses execution${intercept ? ' after an awaited interceptor' : ''}`, async () => {
|
||||
let calls = 0;
|
||||
const controller = new AbortController();
|
||||
const query = defineTool({
|
||||
name: 'fixture_query',
|
||||
kind: 'query',
|
||||
title: 'Fixture',
|
||||
description: 'Offline fixture',
|
||||
inputSchema: { type: 'object' },
|
||||
run: async () => {
|
||||
calls++;
|
||||
return { summary: 'fixture', data: {} };
|
||||
},
|
||||
});
|
||||
const catalog = composeCatalog({
|
||||
tools: [query],
|
||||
interceptors: intercept
|
||||
? [
|
||||
async (call, next) => {
|
||||
await Promise.resolve();
|
||||
controller.abort();
|
||||
return next(call);
|
||||
},
|
||||
]
|
||||
: [],
|
||||
});
|
||||
if (!intercept) controller.abort();
|
||||
await assert.rejects(
|
||||
catalog.call('fixture_query', {}, { signal: controller.signal }),
|
||||
{ name: 'AbortError' },
|
||||
);
|
||||
assert.equal(calls, 0);
|
||||
const fresh = composeCatalog({ tools: [query] });
|
||||
assert.equal(
|
||||
(
|
||||
await fresh.call(
|
||||
'fixture_query',
|
||||
{},
|
||||
{ signal: new AbortController().signal },
|
||||
)
|
||||
).summary,
|
||||
'fixture',
|
||||
);
|
||||
assert.equal(calls, 1);
|
||||
});
|
||||
}
|
||||
@@ -178,6 +178,7 @@ export function composeCatalog({
|
||||
return chain({ tool, args, signal, ...(parent ? { parent } : {}) });
|
||||
};
|
||||
const execute = async ({ tool, args, signal }) => {
|
||||
signal?.throwIfAborted();
|
||||
const problems = validateValue(tool.inputSchema, args);
|
||||
if (problems.length)
|
||||
throw new ToolError('invalid_arguments', problems.join('; '));
|
||||
|
||||
Reference in new issue
Block a user