mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
Merge origin/main into fix/realtime-debug-log-hardening
# Conflicts: # CHANGELOG.md # src/tooling/localServices.test.mjs
This commit is contained in:
commit
a948274326
161 files changed
+19838
-6545
No files matched your search
@@ -308,11 +308,21 @@ export function cctvProxy({ sourceRoot = process.cwd() } = {}) {
|
||||
}
|
||||
const timedOut =
|
||||
error?.name === 'AbortError' || error?.name === 'TimeoutError';
|
||||
// GET /api/cctv/health serializes `message`, and the CCTV panel
|
||||
// renders it as a status label, so the raw error would leave the
|
||||
// server by a different door than the sanitized body below and
|
||||
// land on screen.
|
||||
//
|
||||
// The log line drops the text too, unlike the catch at the bottom
|
||||
// of this file: a media-fetch failure names the camera's upstream
|
||||
// host, and these lines get pasted into issues. The status codes
|
||||
// below carry the diagnosis — 504 for a timeout, 502 otherwise.
|
||||
console.warn('[CCTV Proxy] media fetch failed');
|
||||
setHealth(cameraId, {
|
||||
status: 'degraded',
|
||||
sourceKind: 'upstream',
|
||||
label: source?.provider || 'Configured source',
|
||||
message: error?.message || 'Media fetch failed',
|
||||
message: 'Media fetch failed',
|
||||
});
|
||||
res.writeHead(timedOut ? 504 : 502, {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -78,20 +78,23 @@ async function handleHudSummary(req, res) {
|
||||
res.statusCode = response.ok && summary ? 200 : response.status || 502;
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.setHeader('Cache-Control', 'no-store');
|
||||
if (!response.ok)
|
||||
console.warn(`[hud-summary] upstream HTTP ${response.status}`);
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
summary: summary || null,
|
||||
error: response.ok
|
||||
? null
|
||||
: data.error?.message || 'OpenAI HUD summary request failed',
|
||||
// Never relay `data.error.message`: that is OpenAI's own wording, and
|
||||
// it carries request ids, organization hints and quota phrasing.
|
||||
error: response.ok ? null : 'OpenAI HUD summary request failed',
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
console.warn('[hud-summary] request failed');
|
||||
res.statusCode = 502;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
error: error?.message || 'OpenAI HUD summary request failed',
|
||||
error: 'OpenAI HUD summary request failed',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -132,26 +132,35 @@ function createRealtimeTokenHandler({
|
||||
});
|
||||
const body = await response.text();
|
||||
res.statusCode = response.status;
|
||||
res.setHeader(
|
||||
'Content-Type',
|
||||
response.headers.get('content-type') || 'application/json',
|
||||
);
|
||||
// Which tier/model this secret was actually minted for. The upstream
|
||||
// body is passed through untouched (the client parses it verbatim), so
|
||||
// these headers are the authoritative echo — including the case where a
|
||||
// bogus ?tier= was silently downgraded to standard.
|
||||
// success body is passed through untouched (the client parses it
|
||||
// verbatim), so these headers are the authoritative echo — including the
|
||||
// case where a bogus ?tier= was silently downgraded to standard.
|
||||
res.setHeader('X-GEV-Voice-Tier', tier);
|
||||
res.setHeader('X-GEV-Voice-Model', model);
|
||||
if (requestedTier && !isKnownVoiceTier(requestedTier)) {
|
||||
res.setHeader('X-GEV-Voice-Tier-Fallback', '1');
|
||||
}
|
||||
if (!response.ok) {
|
||||
console.warn(`[realtime-token] upstream HTTP ${response.status}`);
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.end(JSON.stringify({ error: 'Failed to create Realtime token' }));
|
||||
return;
|
||||
}
|
||||
res.setHeader(
|
||||
'Content-Type',
|
||||
response.headers.get('content-type') || 'application/json',
|
||||
);
|
||||
res.end(body);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// For a network fault this was a resolver message naming the upstream
|
||||
// host; the client only needs to know the mint failed.
|
||||
console.warn('[realtime-token] mint failed');
|
||||
res.statusCode = 502;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
error: error?.message || 'Failed to create Realtime token',
|
||||
error: 'Failed to create Realtime token',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,9 +23,16 @@ import {
|
||||
* Budget governor (mirrors the OpenSky credit-governor philosophy — last-good
|
||||
* data beats a dead layer): a persistent counter (.gev-cache/tomtom/budget.json,
|
||||
* keyed by UTC date, reset on day change) counts upstream fetch attempts
|
||||
* against a soft cap (TOMTOM_DAILY_TILE_BUDGET, default 40,000 of the free
|
||||
* tier's ~50k/day). Over the cap the proxy serves stale tiles when available,
|
||||
* else 429 {error:'budget'}.
|
||||
* against a soft cap (TOMTOM_DAILY_TILE_BUDGET). Over the cap the proxy
|
||||
* serves stale tiles when available, else 429 {error:'budget'}.
|
||||
*
|
||||
* The default is derived from TomTom's published free allowance, which is
|
||||
* granted MONTHLY, not daily: 200,000 Traffic Flow & Incidents tile requests
|
||||
* per month (https://docs.tomtom.com/pricing/). The previous 40,000/day
|
||||
* default — and the "~50k/day" figure this comment used to cite — exhausted
|
||||
* a month's entire allowance in five days, leaving the traffic layer dead
|
||||
* for the rest of the billing period. 6,000/day keeps a full 31-day month
|
||||
* inside the allowance (186,000) with headroom to spare.
|
||||
*
|
||||
* GET /api/tomtom/status → {hasKey, dailyCount, budget, date}. Keyless mode:
|
||||
* status reports hasKey:false and the tile endpoint 503s {error:'no_key'}
|
||||
@@ -37,7 +44,8 @@ export function tomtomProxy() {
|
||||
const TILE_TTL_MS = 120_000;
|
||||
const CACHE_DIR = path.join(process.cwd(), '.gev-cache', 'tomtom');
|
||||
const BUDGET_PATH = path.join(CACHE_DIR, 'budget.json');
|
||||
const DEFAULT_DAILY_BUDGET = 40000;
|
||||
// 200,000/month free tier / 31 days = 6,451. Rounded down for headroom.
|
||||
const DEFAULT_DAILY_BUDGET = 6000;
|
||||
const MEM_MAX_ENTRIES = 256;
|
||||
const UPSTREAM_TIMEOUT_MS = 15000;
|
||||
|
||||
|
||||
@@ -72,8 +72,8 @@ export function ingestAisStreamEnvelope(envelope) {
|
||||
imo: stringValue(message.ImoNumber ?? message.IMO ?? staticData.imo),
|
||||
type: vesselTypeFromAis(message, staticData),
|
||||
destination: stringValue(message.Destination ?? staticData.destination),
|
||||
speed: numberValue(message.Sog ?? message.SOG),
|
||||
course: numberValue(message.Cog ?? message.COG),
|
||||
speed: normalizedSpeedOverGround(message.Sog ?? message.SOG),
|
||||
course: normalizedCourseOverGround(message.Cog ?? message.COG),
|
||||
heading: normalizedHeading(message.TrueHeading ?? message.Heading),
|
||||
last_position_UTC: normalizeAisTimestamp(
|
||||
metadata.time_utc ?? metadata.TimeUtc,
|
||||
@@ -277,6 +277,28 @@ function normalizedHeading(value) {
|
||||
return heading !== null && heading >= 0 && heading <= 360 ? heading : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Speed over ground in knots, or null when the report carries AIS's "not
|
||||
* available" code. ITU-R M.1371 encodes SOG in 0.1-knot units where 1022 means
|
||||
* "102.2 knots or higher" and 1023 means unavailable, so 102.3 is a sentinel
|
||||
* and not a reading. It is finite, so every consumer would otherwise treat it
|
||||
* as a genuine speed.
|
||||
*/
|
||||
function normalizedSpeedOverGround(value) {
|
||||
const speed = numberValue(value);
|
||||
return speed !== null && speed >= 0 && speed < 102.3 ? speed : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Course over ground in degrees, or null when the report carries AIS's "not
|
||||
* available" code (3600 in 0.1-degree units, i.e. 360). Valid courses stop at
|
||||
* 359.9; heading keeps its own separate sentinel of 511.
|
||||
*/
|
||||
function normalizedCourseOverGround(value) {
|
||||
const course = numberValue(value);
|
||||
return course !== null && course >= 0 && course < 360 ? course : null;
|
||||
}
|
||||
|
||||
function normalizeAisTimestamp(value) {
|
||||
const text = stringValue(value);
|
||||
if (!text) return new Date().toISOString();
|
||||
|
||||
Reference in new issue
Block a user