Merge origin/main into fix/sanitize-openai-proxy-errors

This commit is contained in:
Sameh Khamis committed 2026-09-20 17:42:54 -07:00
commit a88904a02f
156 files changed
+19702 -6530

No files matched your search

+12 -4
View File
@@ -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;
+24 -2
View File
@@ -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();