mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Merge pull request #1273 from bilawalsidhu/codex/fix-military-history-failures
fix(military): retire unavailable history and keep local trails
This commit is contained in:
9 files changed
+112
-52
No files matched your search
@@ -0,0 +1 @@
|
||||
- Retire unavailable history and keep local trails (asturksever, #1273).
|
||||
+3
-11
@@ -9,15 +9,7 @@ broader suffix variants. Follow-ups retain boundary provenance, and cancelled
|
||||
lookups preserve the previous analyst cohort and boundary source. Navigation
|
||||
search keeps a successful geocode, with no search or view-based region recovery.
|
||||
|
||||
CelesTrak accepts only the eight satellite groups used by the app (the seven
|
||||
satellite-layer groups plus `active` for Space Missions) and gates cross-site
|
||||
requests before provider work with the keyless same-site policy, so the
|
||||
documented reverse proxy still works. CelesTrak and Launch Library failures
|
||||
start a provider-wide cooldown that a later failure never shortens: Retry-After
|
||||
seconds or HTTP dates are bounded to 5 seconds through 30 minutes, with a
|
||||
60-second fallback. During cooldown dated last-good data is served with stale
|
||||
provenance, otherwise the proxy returns a bounded error without another upstream
|
||||
attempt. Error bodies are cancelled before reading.
|
||||
The standalone military source no longer requests the unavailable, undocumented ADSB.lol trace endpoint. Tracking retains its locally accumulated trail, and custom sources may still supply optional getTrack history. The old HTTP route validates the identifier then reports 503 history_unavailable without any upstream request, rather than forwarding a misleading 403. The `get_aircraft_track` tool reports `unsupported` for a military feed without `getTrack` instead of a retry-later failure. This does not restore historical coverage or download bulk daily archives.
|
||||
|
||||
Terrain height requests accept at most 64 points, matching the browser service's
|
||||
existing batch size and one bounded upstream chunk. Larger requests return HTTP
|
||||
@@ -4723,7 +4715,7 @@ silently demoting every later lookup for the session.
|
||||
A selected mission renders its orbit as four repeating tactical sectors, each containing one prominent cyan dot followed by one hundred thin translucent dashes. The bright dots act as orbit anchors while the subdued dash field remains depth-tested against the globe and is shown only for the selected mission.
|
||||
Close selected-pad views add one static 500 m-radius cyan launch-zone ring with a low-opacity translucent fill over the sampled photoreal launch-site surface. The single scene primitive is created only for the visible selected site and is otherwise dormant. It appears during Focus, sufficiently close manual zoom, and the replay countdown, but is suppressed above 120 km camera altitude, beyond 180 km direct camera-to-pad range, for unselected missions, and whenever Space Missions is inactive. Focus establishes a launch-site-centered camera transform once; subsequent manual heading and pitch changes remain centered on that site without an automated per-frame correction. Surface mission markers and labels use an additional conservative globe-limb margin before the exact ellipsoid occluder boundary, preventing near-horizon visibility from alternating between frames.
|
||||
- **AIS vessels**: chevron symbology (naval cyan base, type tints), world-space headings, MMSI-keyed reconciliation (selection survives refreshes; pinned 3 refreshes with STALE marker when absent), detection-overlay integration (`type: 'SEA'`), contextStore registration for voice Q&A. Empty-space clicks, id-less photorealistic-tile picks, and Escape dismiss the vessel card/HUD/context and clear its trail; picks owned by another layer (including `gev-trail:*`) and raw vessel-record picks without a live MMSI key are no-ops for vessel selection. Click and key handlers detach while the layer is disabled and reinstall on enable. Selecting another vessel replaces the selection and trail, and reconciliation clears a trail if its owning vessel is evicted.
|
||||
- **Track trails**: server accumulates per-MMSI ring buffers (`/api/vessels/track?mmsi=`, Float32+Uint32, 64 samples, 30s/25m thinning); aircraft backfill proxies `/api/flights/track` (OAuth, own credit bucket) and `/api/military/track` (tar1090 readsb, ~24h history, ODbL — credit adsb.lol).
|
||||
- **Track trails**: server accumulates per-MMSI ring buffers (`/api/vessels/track?mmsi=`, Float32+Uint32, 64 samples, 30s/25m thinning); aircraft backfill proxies `/api/flights/track` (OAuth, own credit bucket). Military aircraft keep only their locally accumulated trails: `/api/military/track` validates the hex, then answers `503 history_unavailable` without an upstream request.
|
||||
- Shared `src/data/pickRegistry.js` stops the two flight layers' click handlers from fighting over the camera.
|
||||
|
||||
### Optional Overpass configuration
|
||||
@@ -5287,7 +5279,7 @@ pins and share links already supply complete boxes.
|
||||
- `/api/overpass` is bounded by body/response caps, per-client/global rate limits, concurrency limits, operator-configured upstreams, in-flight dedupe, cache bounds, and static validation that every selector is spatially bounded.
|
||||
- `/api/military-installations` uses an independent limiter with the same 90-per-client/300-global one-minute bounds, so viewport installation refreshes never consume `/api/overpass` annotation capacity.
|
||||
- `/api/route` proxies bounded OSRM route requests for annotation routes, with profile allowlisting, distance caps, response caps, caching, and sanitized "no route found" errors.
|
||||
- Track endpoints: `/api/vessels/track?mmsi=` (server-accumulated ring buffers; sub-route handled before the rows snapshot), `/api/flights/track?icao24=` (OAuth, 60s cache, sanitized errors, independent OpenSky credit bucket), `/api/military/track?hex=` (60s cache, 5MB cap, ODbL attribution required in UI).
|
||||
- Track endpoints: `/api/vessels/track?mmsi=` (server-accumulated ring buffers; sub-route handled before the rows snapshot), `/api/flights/track?icao24=` (OAuth, 60s cache, sanitized errors, independent OpenSky credit bucket), `/api/military/track?hex=` (400 for an invalid hex, otherwise `503 history_unavailable` with `Retry-After: 1800` and `Cache-Control: no-store`, no upstream request; military tracking uses its locally accumulated trail).
|
||||
- Realtime debug logs redact API keys, bearer tokens, client secrets, and image data URLs before writing to disk; request bodies are size-capped.
|
||||
- `/api/realtime/debug-log` enforces an always-on 120-per-client/400-global one-minute limiter (not the opt-in `GEV_RATELIMIT_OPENAI_PER_MIN` bucket the cost-bearing OpenAI routes share), appends asynchronously through a serialized queue, and rotates `realtime-conversations.jsonl` at 32 MB keeping one prior generation, so the sink is bounded at twice that regardless of session length. A malformed record answers 400 and a failed write 500, both with a fixed message.
|
||||
|
||||
|
||||
@@ -7,10 +7,9 @@ import { readCappedResponseText } from '../common/http.js';
|
||||
* own credit bucket, 4 credits per call on the free tier). OAuth via the
|
||||
* shared coalesced token. 60s per-icao cache; 404/429 forwarded so the
|
||||
* client can fall back to its accumulated trail silently.
|
||||
* /api/military/track?hex=<hex> — adsb.lol tar1090 readsb trace
|
||||
* (undocumented but live; no browser CORS, hence this proxy). Up to ~24h
|
||||
* of real history per aircraft. Treat as best-effort; data is ODbL —
|
||||
* credit "adsb.lol (ODbL)" in the UI.
|
||||
* /api/military/track?hex=<hex> — compatibility refusal for the retired,
|
||||
* undocumented ADSB.lol trace route. The standalone military source uses
|
||||
* locally accumulated trails; provider-supplied history remains optional.
|
||||
*/
|
||||
export function trackBackfillProxies() {
|
||||
const TRACK_CACHE_MS = 60000;
|
||||
@@ -107,10 +106,16 @@ export function trackBackfillProxies() {
|
||||
);
|
||||
return;
|
||||
}
|
||||
await proxyJson(
|
||||
res,
|
||||
`lol:${hex}`,
|
||||
`https://adsb.lol/data/traces/${hex.slice(-2)}/trace_full_${hex}.json`,
|
||||
res.statusCode = 503;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.setHeader('Cache-Control', 'no-store');
|
||||
res.setHeader('Retry-After', '1800');
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
error:
|
||||
'Military history unavailable; locally accumulated trails remain available',
|
||||
code: 'history_unavailable',
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
res.statusCode = 502;
|
||||
|
||||
@@ -593,7 +593,7 @@ export function createTracking({
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire-and-forget adsb.lol readsb trace backfill (PRD F2). Trace points are
|
||||
* Fire-and-forget optional source history backfill (PRD F2). Trace points are
|
||||
* stride-thinned to the remaining vertex budget, then spliced strictly older
|
||||
* than the oldest seeded fix AHEAD of the locally accumulated fine segment,
|
||||
* capped at TRAIL_MAX_POINTS (newest kept). Any failure (404/timeout/
|
||||
|
||||
@@ -214,11 +214,10 @@ test('construction is inert; adapters preserve routes, viewport query, cache epo
|
||||
assert.equal(mil.observedAtMs, now - 60000);
|
||||
assert.equal(mil.records[0].positionTimeMs, now - 75000);
|
||||
assert.equal(mil.stale, true);
|
||||
const track = await military.getTrack('abc123');
|
||||
assert.equal(track.complete, false);
|
||||
assert.equal(track.records[0].observedAtMs, now - 50000);
|
||||
assert.equal(track.records[0].baroAltitudeM, 304.8);
|
||||
assert.equal(track.records[0].ellipsoidAltitudeM, null);
|
||||
assert.equal(military.getTrack, undefined);
|
||||
assert.ok(
|
||||
requests.every(({ url }) => !url.startsWith('/api/military/track')),
|
||||
);
|
||||
});
|
||||
|
||||
test('cancellation after slow body parsing rejects even with a transport that ignores abort', async () => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
epoch,
|
||||
finite,
|
||||
httpError,
|
||||
LiveSourceError,
|
||||
@@ -159,26 +158,8 @@ export function createMilitarySource({
|
||||
status: response.status,
|
||||
};
|
||||
},
|
||||
async getTrack(reference, { signal } = {}) {
|
||||
const { response, payload } = await readResponse(
|
||||
fetchImpl,
|
||||
'/api/military/track?hex=' + encodeURIComponent(reference),
|
||||
{ signal },
|
||||
'Military aircraft',
|
||||
);
|
||||
if (!response.ok) throw httpError(response, 'Military aircraft');
|
||||
const baseTimeMs = epoch(payload?.timestamp, 1000);
|
||||
return {
|
||||
records:
|
||||
baseTimeMs == null
|
||||
? []
|
||||
: normalizeAircraftTrack(payload?.trace, {
|
||||
baseTimeMs,
|
||||
readsb: true,
|
||||
}),
|
||||
complete: false,
|
||||
};
|
||||
},
|
||||
// ADSB.lol does not expose a supported live-history endpoint. Omitting
|
||||
// optional getTrack keeps the tracking layer on its accumulated trail.
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -584,6 +584,24 @@ test('track backfill proxy returns 502 on an oversized upstream body and caches
|
||||
assert.equal(callCount, 1);
|
||||
});
|
||||
|
||||
test('retired military history refuses without upstream I/O while validating identifiers', async (t) => {
|
||||
t.mock.method(globalThis, 'fetch', async () => {
|
||||
assert.fail('retired history must not contact the upstream');
|
||||
});
|
||||
const route = install(providers.trackBackfillProxies());
|
||||
for (const hex of ['abc123', 'abc456']) {
|
||||
const response = await route('/api/military/track', '?hex=' + hex);
|
||||
assert.equal(response.statusCode, 503);
|
||||
assert.equal(JSON.parse(response.body).code, 'history_unavailable');
|
||||
assert.equal(response.headers['retry-after'], '1800');
|
||||
assert.equal(response.headers['cache-control'], 'no-store');
|
||||
}
|
||||
assert.equal(
|
||||
(await route('/api/military/track', '?hex=invalid')).statusCode,
|
||||
400,
|
||||
);
|
||||
});
|
||||
|
||||
test('global aircraft snapshots coalesce cold and expired callers and clear rejected flights', async (t) => {
|
||||
environment(t, { OPENSKY_AUTH_MODE: 'anon' });
|
||||
t.mock.method(console, 'warn', () => {});
|
||||
|
||||
@@ -333,13 +333,20 @@ export const getAircraftTrack = defineTool({
|
||||
requires: ['aircraft'],
|
||||
async run(args, { services, signal }) {
|
||||
const icao24 = args.icao24.toLowerCase();
|
||||
const source = feedFor(services, args.military);
|
||||
// History is optional: the standalone military source has none.
|
||||
if (typeof source.getTrack !== 'function')
|
||||
throw new ToolError(
|
||||
'unsupported',
|
||||
args.military
|
||||
? 'Track history is not available for the military aircraft feed'
|
||||
: 'Track history is not available for the aircraft feed',
|
||||
);
|
||||
// A track server answers 404 for an aircraft it has no positions for.
|
||||
const track = await feedFor(services, args.military)
|
||||
.getTrack(icao24, { signal })
|
||||
.catch((error) => {
|
||||
if (error?.status === 404) return { records: [] };
|
||||
throw error;
|
||||
});
|
||||
const track = await source.getTrack(icao24, { signal }).catch((error) => {
|
||||
if (error?.status === 404) return { records: [] };
|
||||
throw error;
|
||||
});
|
||||
const points = [...track.records].sort(
|
||||
(a, b) => a.observedAtMs - b.observedAtMs,
|
||||
);
|
||||
|
||||
@@ -3,6 +3,8 @@ import test from 'node:test';
|
||||
import { composeCatalog, coreTools } from '../index.js';
|
||||
import { airlineDesignator } from './aviation.js';
|
||||
import { LiveSourceError } from '../../sources/live/contract.js';
|
||||
import { createMcpServer } from '../mcp/index.js';
|
||||
import { createToolServices } from '../services.js';
|
||||
|
||||
const record = (id, latitude, longitude, extra = {}) => ({
|
||||
id,
|
||||
@@ -242,6 +244,61 @@ test('tracks are ordered and thinned to 200 points', async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('military track history is unsupported by the default composition and kept for custom sources', async () => {
|
||||
const requests = [];
|
||||
const services = createToolServices({
|
||||
fetchImpl: async (url) => {
|
||||
requests.push(String(url));
|
||||
throw new Error('no request expected');
|
||||
},
|
||||
appUrl: 'http://localhost:5173',
|
||||
});
|
||||
const catalog = composeCatalog({ tools: coreTools, services });
|
||||
await assert.rejects(
|
||||
catalog.call('get_aircraft_track', { icao24: 'AE0001', military: true }),
|
||||
(error) =>
|
||||
error.name === 'ToolError' &&
|
||||
error.code === 'unsupported' &&
|
||||
/military/.test(error.message),
|
||||
);
|
||||
const server = createMcpServer({ catalog, name: 'test', version: '1.0.0' });
|
||||
const { result } = await server.handle({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'get_aircraft_track',
|
||||
arguments: { icao24: 'ae0001', military: true },
|
||||
},
|
||||
});
|
||||
assert.equal(result.isError, true);
|
||||
assert.equal(result.structuredContent.error, 'unsupported');
|
||||
assert.doesNotMatch(result.content[0].text, /try again|later/i);
|
||||
assert.deepEqual(requests, []);
|
||||
|
||||
const track = [
|
||||
{
|
||||
latitude: 37.6,
|
||||
longitude: -122.3,
|
||||
observedAtMs: Date.UTC(2026, 0, 1),
|
||||
baroAltitudeM: 3000,
|
||||
onGround: false,
|
||||
},
|
||||
];
|
||||
const custom = feed(military, { track });
|
||||
const withHistory = composeCatalog({
|
||||
tools: coreTools,
|
||||
services: { aircraft: feed(civil), military: custom },
|
||||
});
|
||||
const kept = await withHistory.call('get_aircraft_track', {
|
||||
icao24: 'AE0001',
|
||||
military: true,
|
||||
});
|
||||
assert.equal(custom.calls[0], 'ae0001');
|
||||
assert.equal(kept.data.total, 1);
|
||||
assert.equal(kept.data.view.follow.kind, 'military_aircraft');
|
||||
});
|
||||
|
||||
test('aircraft info combines type and route lookups', async () => {
|
||||
const aircraft = feed(civil, {
|
||||
enrichment: {
|
||||
|
||||
Reference in new issue
Block a user