Reflect the weather stack, analyst records and feed provenance, satellite pass predictions and live camera video that landed since the last full pass.
Co-authored-by: Matt Van Horn <[email protected]>
Wind (NOAA GFS and ECMWF IFS: animated particles, colour field, a reading at map centre), observed weather (NOAA MRMS radar, GOES and NESDIS infrared clouds, lightning density) on one shared history timeline, NHC and CPHC cyclone advisories with tracks and cones, and a WEATHER panel in the right rail. On 3D Tiles the observed products render as raised shells with a sharper window around the view.
Co-authored-by: Gustavo Beneduzi <[email protected]>
Co-authored-by: Daniel Slay <[email protected]>
Co-authored-by: Rehaan Delmotra <[email protected]>
Co-authored-by: Cursor Agent <[email protected]>
At 720px and below both panel stacks scroll vertically, so each panel's absolutely positioned glow (negative inset) became scrollable overflow on both axes and drew a horizontal scrollbar band plus a spurious vertical scrollbar whenever CCTV, Context, Data Layers or Scenes was expanded. Pin the glow to its panel box in the narrow-screen block and add a stylesheet test that checks every 720px media block.
The macOS credential hardener strips ACLs with `chmod -N`, spawning
`chmod` by bare name. On machines where Nix home-manager or Homebrew
coreutils sit ahead of /bin on PATH, that resolves to GNU chmod, which
has no `-N` flag and exits non-zero. The hardener treated that as a
failure and refused every save from the Provider Settings panel with
"could not restrict the credential file to your account; nothing was
saved".
Spawn /bin/chmod by absolute path instead, matching how the Windows
branch already resolves its native tools without consulting PATH, and
add a regression test that pins the absolute path.
Street Traffic draws on OpenStreetMap for geometry and TomTom for flow,
and the row said "Road data temporarily unavailable" whichever one
failed. The reporter of #661 went and verified a TomTom key while every
public Overpass mirror was refusing their network with HTTP 406 — a
status fetchRoads already had and dropped on the floor.
Classify the refusal where the layer talks to Overpass: 429 is
"Overpass rate-limited", 504 is "Overpass timed out", anything else is
"Overpass refused the road query (HTTP <status>)". The first two are the
words alpr's Overpass source already uses; the status is what says which
upstream declined, which a single-source layer never has to.
The error carries its status as a RoadRequestError, following
LiveSourceError in src/sources/live/contract.js, so a caller can branch
on the code without parsing English. The catch promotes a classified
refusal and keeps the general line for everything else — the branch is
on the error type, not on its text, because a dropped socket throws
whatever the platform felt like saying and "Failed to fetch" on a panel
row helps nobody.
Retries, backoff, caching and the mirror rotation are untouched.
The media relay only deadlined the wait for upstream response headers. Once
headers arrived the timer stopped, so an upstream that answered and then sent
nothing held both the client response and the upstream socket open for as long
as the camera host allowed. The declared Content-Length ceiling was the only
body bound, and a chunked or length-less body had none.
A 30-second idle deadline now measures the gap between upstream chunks and
tears the stream down through the existing release path. Because a paused pipe
also stops upstream bytes arriving, the deadline reschedules itself whenever
the response is still waiting to drain, so a viewer on a slow link is not read
as a dead camera. Live feeds that keep delivering frames are unaffected.
.gitignore listed only the bare filename `.env`, but the app reads four
credential files, not one. scripts/setup-doctor.mjs:92 walks
`['.env', '.env.local', `.env.${mode}`, `.env.${mode}.local`]`, and
server/standalone/vite.config.js:11 calls Vite's `loadEnv(mode, root, '')`,
which reads the same ladder.
gitignore matches literally, so three of those four files were
committable. A key placed in .env.local is fully live for the app --
the doctor reports it as configured and the provider proxies use it --
while `git add -A` stages it. Verified on a clean checkout of main:
.env ignored
.env.local COMMITTABLE
.env.production COMMITTABLE
.env.production.local COMMITTABLE
This matters more than usual here because the in-app Provider Settings
panel trains people to keep real keys in the checkout rather than in a
shell profile, and because the repository has ~7.7k forks that inherit
the same gap.
Replaces the single `.env` entry with `.env` plus `.env.*`, and re-includes
`.env.example` with a negation so the tracked template is unaffected.
Deliberately scoped to the dotenv ladder alone. Other credential-bearing
artifacts (a repo-local OPENSKY_CREDENTIALS_FILE, *.pem/*.key) are a
separate question and are left for a follow-up rather than widened here.
Co-Authored-By: Claude Opus 5 <[email protected]>
TOMTOM_DAILY_TILE_BUDGET defaulted to 40,000 per day against a free
tier that is granted per MONTH, not per day. TomTom currently allows
200,000 Traffic Flow & Incidents tile requests per month
(https://docs.tomtom.com/pricing/), so the default exhausted a user's
entire monthly allowance in five days and left the traffic layer dead
for the rest of the billing period.
The docs contradicted themselves on this. traffic.js described the
free tier as "~50k/day", while .env.example cited the correct 200K
per month two lines below quoting the same 40,000 default. 50k/day
would be 1.5M/month, 7.5x the actual allowance.
Sets the default to 6,000/day. A 31-day month lands at 186,000,
inside the allowance with headroom. Corrects the stale figures in
traffic.js, .env.example and DATA_SOURCES.md.
This remains an application-side safety ceiling, not a billing cap,
and the surrounding text still says so. TomTom's free tier requires
no card, so overrunning it cuts the layer off rather than generating
a charge — but a layer that dies three weeks into every month is
still worth avoiding by default.
Co-Authored-By: Claude Opus 5 <[email protected]>
adsbdb supplies the aircraft type, model name and registration behind every
enriched flight, and the airline and origin/destination pair behind the
tracked contact's FROM -> TO strip. It appeared in neither attribution
surface: DATA_SOURCES.md and src/data/dataCredits.js each held zero
occurrences of it, against the rule both files state for themselves and
CONTRIBUTING's fourth pull-request item. adsb.lol is a different operator and
its ODbL credit does not reach this data.
The reporter of #256 left the patch off deliberately, saying the terms were
what should not be guessed. They are published, so this takes them from the
source rather than inventing them: adsbdb credits PlaneBase for the aircraft
data and Guillaume Michel for the ICAO-to-N-number conversion, and states that
the flight 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". No
licence or rate limit is published for the service itself.
That restriction is the reason the note records what the proxy does with what
it fetches: responses are cached per key for 24 hours in .gev-cache/adsbdb.json,
which is gitignored, so the data stays on the operator's machine and is never
bundled with this repo or redistributed.
A test pins the credit. The attribution is a requirement rather than a
preference, so it should fail loudly if a later edit drops the names or the
restriction, instead of surviving on luck.
scripts/format.mjs skips *.test.mjs during runtime discovery, so a test
added under a discovered root is never seen by format:check until it is
listed in scripts/format-scope.json — which is what CODE-BOUNDARIES.md
means by adding tests to the explicit list as they are adopted. The file
went in unformatted and the check stayed green, hiding it.
Listed and formatted; format:check now covers 917 files instead of 916.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
CONTRIBUTING asks for docs/CURRENT-STATE.md and CHANGELOG.md in the same PR
as a runtime behaviour change.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
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 reaches us as 102.3 knots and COG 3600 as 360 degrees.
Both are finite, so numberValue passed them through as ordinary readings and
every consumer — vessel cards, the HUD, voice analyst queries — treated a
missing measurement as a real one.
Observed on a live AISStream feed of 3,062 vessels: exactly 3 records at
102.3 knots with no reading anywhere between 40 and 102.3, and 404 records
at exactly 360 degrees. One Dutch rescue boat moored at its station carried
both sentinels at once, so an analyst summary reported a maximum speed of
102.3 knots — faster than any vessel afloat.
Filter both at the store, where AISStream messages are decoded, so the
sentinels never enter the cache. Heading already had this treatment through
normalizedHeading with its own separate sentinel of 511.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Every pack sets headingConfidence on each camera, but nothing consumed it:
a bearing synthesized by fallbackHeadingFromId (a hash of the id string,
no geographic input) rendered exactly like a surveyed one — 2,827 of 3,997
cameras in a default catalog. Fixes#639.
Two consumers, plus a passthrough:
- HUD: the heading token is tagged when the bearing is synthetic —
`HDG 194° (ESTIMATED)` — so a hash never reads as a measured facing.
- Wireframe: a low-confidence camera draws its coverage frustum with dashed
polylines instead of solid ones. Colors, widths, and the active/idle
emphasis are unchanged; the dash is the only signal.
- Public camera state gains `headingConfidence` (the raw pack flag) and
`headingEstimated` (the derived bit) so UI consumers never re-derive it.
A human-vouched pose always wins over the pack flag: a manually saved
calibration (calSource:'manual') or a curated catalog entry
(poseSource:'curated') is never presented as estimated, mirroring the CAL
badge states in calibration.js.
- src/layers/cctv/headingConfidence.js: pure helpers (isHeadingEstimated,
headingHudToken)
- src/layers/cctv/headingConfidence.test.mjs: confidence normalization,
calibration/curation overrides, HUD token formatting
- src/layers/cctv/presentation.js: HUD token + public-state passthrough
- src/layers/cctv/rendering.js: dashed materials for estimated bearings
CONTRIBUTING.md named only npm run build, npm test and npm run test:track,
so a contributor following it literally never learns that most features
have a dedicated gate in scripts/qa-*.mjs. Today 25 of the 40 gates are
not named in CURRENT-STATE.md either, so for those there was nothing in
the written guide to find them by.
Point at the directory rather than transcribing a list that would go stale
the same way: every gate already opens with a comment saying what it
asserts and how to run it, so `ls scripts/qa-*.mjs` plus the script header
is the canonical, self-maintaining reference.
Also state plainly that CI runs neither test:track nor any qa gate, since
that is what makes running them locally matter.