Adds a Directions data layer: click A and B on the globe for a
street-following drive, walk or cycle route, draped on terrain and 3D tiles,
with one dot per maneuver and a FLY chip that rides the camera along it.
Needs no key, no geocoder and no microphone.
Routing already existed for voice ("route from A to B" → /api/route → OSRM
on the FOSSGIS servers) but was unreachable without an OpenAI key and a
Google geocoder, and returned geometry only. This layer is the keyless front
end to it.
Layer (src/data/directions.js): the row chips are the interface — DRIVE /
WALK / BIKE, SET A / SET B (arm the next globe click; Escape cancels), SWAP,
FLY, CLEAR. Chip writes go through the manager's setLayerParams → setParams
(mode and arm are state; swap/fly/clear are one-shot commands). The route is
a ClassificationType.BOTH ground polyline with the annotation renderer's
flowing-dash material (now exported), so it reads on the keyless terrain
globe and on 3D tiles alike. Clicking a maneuver dot opens a shared-host card
with the instruction, the leg after it and the next instruction. FLY hands
the geometry to flyRoute — the same cinematic as voice fly_route — after an
idempotent initCameraVerbs, which now accepts a missing target getter and
never clears an installed one. A route the router cannot find says so; no
straight line is ever drawn as a route.
Proxy (vite.config.js): /api/route accepts steps=1 and returns turn-by-turn
steps; the upstream is always asked for steps and the full response cached,
so the annotation engine (no steps) and Directions (steps) share one upstream
call per route. src/data/routeSteps.js phrases OSRM maneuver type / modifier
/ exit / road name into one plain-English sentence per decision and folds
"exit roundabout" into the roundabout step.
Manager: adds a public refreshLayerStats() so a layer whose state changes
outside a tick (click placement, routing) can repaint its row.
Credits: OSRM / FOSSGIS routing is now credited in the Data attribution
popover (voice routes used it uncredited).
Verified: 2,719 unit tests, 2,718 pass, 1 skipped (Node 24 allocation gate;
12 new); vite build green; headless Chrome against the dev server over
Helsinki, keyless: A and B placed by click, a 12-step drive, a 28-step walk
after switching mode, the step card on a dot (overlay diagnostics:
directions-step painted), FLY moved the camera, CLEAR reset the row; no
console errors. Docs: README, CHANGELOG, DATA_SOURCES (FOSSGIS routing
terms), docs/CURRENT-STATE.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Recover across existing Overpass mirrors, reject cached refusals, and serve consistent last-good data to coalesced callers. Preserve the original contribution from PR #165.
The proxy lists four mirrors and rotated on 5xx alone, so a 4xx ended the
fan-out. Measured against the live services: overpass-api.de and its lz4 alias
answer 406 to this proxy's User-Agent, while overpass.kumi.systems and
overpass.private.coffee answer 200 to the byte-identical request. Every
Overpass-backed feature — road geometry, annotation outlines, place lookup —
was failing on an Apache error page with two healthy mirrors untried.
The refusal was then cached. The cache guard read `status < 500`, so the error
page was written to memory and to disk, and the serve-stale guard used the same
threshold and declined to replace it. Boundary-class queries hold a month-long
TTL: four of twenty-three cached entries on this machine held that 406, dated
days after the mirror had stopped refusing.
Both decisions now go through one predicate. A payload is data only when it is
a 2xx that is neither rate-limited nor a body-level runtime error, so what may
be cached and what may be replaced by a stale entry cannot drift apart again.
A refusal every mirror agrees on is still reported with the first mirror's
status and body, so a malformed query says what upstream said — after every
mirror has had its chance, not instead of it. fetchOverpassPayload takes
injectable endpoints and fetch so the rotation is covered without a live
mirror; restoring either half of the old behaviour fails the new tests.
Expanding the empty KEY_SETUP_EXTERNAL_KEYS array under set -u is a fatal
'unbound variable' on bash 3.2, which macOS ships as /bin/bash — so a keyless
./scripts/dev-fresh.sh died at the provenance-marker line before doing
anything. The ${arr[*]:-} guard keeps 3.2 alive and leaves populated launches
byte-identical (same idiom the script already uses for DEV_UNSET).
Verified with /bin/bash 3.2.57: the extracted block runs clean under set -u
with no keys (empty CSV) and with keys (ordered comma-joined names); the full
launcher boots keyless to a live server, and exported keys still classify as
externally managed in Provider Settings. A behavioral guard now runs the real
block from the script both ways in the dev-fresh test surface, plus a textual
assertion pinning the 3.2-safe idiom.
* fix: measure GBFS response cap in bytes, not string length
The 5 MB cap compared GBFS_MAX_BODY_BYTES against body.length, which is the
string's UTF-16 code-unit count rather than its byte size. Those match for
ASCII but diverge for multi-byte payloads, so a large non-ASCII response
could slip past the cap. Switched to Buffer.byteLength.
Closes#32
* docs(changelog): move the GBFS byte-cap entry into the current Unreleased section
---------
Co-authored-by: Bilawal Sidhu <106619546+bilawalsidhu@users.noreply.github.com>
Co-authored-by: Bilawal Sidhu <bilawal@metaversity.us>
* fix(firms): drop spread in fires.push — RangeError silently killed 2 of 3 VIIRS sources
fires.push(...records) passes every record as a function argument. A
world/2 VIIRS pull returns ~131k records for NOAA20 and SNPP, over V8's
~125k argument limit, so both threw RangeError: Maximum call stack size
exceeded. The throw landed after sources.push({ok:true}), so each failed
source was listed twice — once ok:true with its real count, once ok:false
— while its records were dropped entirely. Only NOAA21 (114k, under the
limit) survived, making the layer look healthy at a third of the data.
Global fire count goes 113,996 -> 377,169.
* docs(changelog): record the FIRMS multi-source merge fix
* docs(changelog): move the FIRMS fix into a new Unreleased section above 0.1.0
---------
Co-authored-by: Bilawal Sidhu <106619546+bilawalsidhu@users.noreply.github.com>
Co-authored-by: Bilawal Sidhu <bilawal@metaversity.us>
The first formal release.
- One-click install via Pinokio; keyless boot lands on a live Esri World
Imagery satellite globe with keyless terrain, with automatic OSM fallback
and graceful degradation when terrain is unavailable.
- Provider Settings (the POWER UP panel): add, replace, or remove API keys
inside the app; credential files made owner-only before any secret is
written; external keys shown read-only; the panel refuses shared or proxied
servers.
- Keyless capability responses for the optional HUD summary and place search.
- Aircraft-identity voice answers cover operator, type, and route, and say so
plainly when enrichment is unavailable.
- README rewritten keyless-first; concise 0.1.0 changelog section; CI
workflow included.
Gates: 2,672 unit tests pass / 0 fail, build clean, tracking regression
108/108, map-source tray QA pass, setup doctor ready.
naturalEarthRegions.js and neighborhoodPolygons.js each carried an
isNode branch that dynamically imported node:fs to read their bundled JSON
packs, because a plain dynamic JSON import needs an import attribute in
Node. Vite only warns about externalizing node:fs for the browser, so both
warnings survived every production build and the runtime boundary rested on
an environment guard.
Give both loaders the import attribute instead. Vite bundles the JSON as a
module exactly as before (the emitted regions/marine/san-francisco chunks
are byte-identical), Node loads the same files under node:test, and the
isNode split disappears.
Add a source-boundary test so a node: import cannot silently return to a
browser-built module.
Closes#34
- Public release set for August 18, 2026, with a running updates log
- New hero GIF and four interface stills replacing the old previews
- Rewrite copy: lead with lineage, then the news, then the product
- Remove superseded hero images
- Add .gitignore for .DS_Store