Commit Graph
3627 Commits
Author SHA1 Message Date
Sameh Khamis aa5c0fb5e3 docs(changelog): weather frame source (#1665) 2026-11-10 21:54:27 -08:00
Sameh Khamis e18353f769 feat(weather): a host can supply decoded frames to the weather layers
A weather feed may also be a frame source. With an optional
getFrameImage({ product, time, size, bbox, signal }) the layers take
whole-extent images, detail windows and the global mosaic from it; with an
optional getFrameTile({ product, time, z, x, y, size, signal }) regional
tiles and their prefetch come from it, null being a transparent tile. Pixels
may be an ImageBitmap, ImageData, canvas or VideoFrame of exactly the
requested size; the layer draws them once and closes them. Cancellation
reaches the source, and a feed without these methods reads the image and
tile routes as before.
2026-11-10 21:54:08 -08:00
Sameh Khamis aff5ce7f17 Merge pull request #1663 from bilawalsidhu/sameh/news-test-shared-time
test(news): briefing stories share one published time
2026-11-10 21:29:18 -08:00
Sameh Khamis b226fa820a test(news): briefing stories share one published time, so their order never depends on a clock tick 2026-11-10 21:19:33 -08:00
Sameh Khamis 1fb0fdad71 Merge pull request #1662 from bilawalsidhu/sameh/contacts-follow-world-clock
feat(live): flights, military and vessels follow the world clock; vessels keyed by source id; share a selected vessel
2026-11-10 18:35:44 -08:00
Sameh Khamis c45d9cfb03 Merge branch 'main' into sameh/contacts-follow-world-clock 2026-11-10 18:26:22 -08:00
Sameh Khamis cb414a1671 Merge pull request #1661 from bilawalsidhu/sameh/gemini-error-locale
fix(voice): Gemini voice messages follow a language change
2026-11-10 18:20:57 -08:00
Sameh Khamis 5826420a27 chore(changelog): name the contacts fragment for #1662 2026-11-10 18:17:52 -08:00
Sameh Khamis cbbf55cacd feat(vessels): key vessels by their source's stable id, and share a selected vessel
AIS Vessels are keyed by the id their live source gives, with the MMSI as an
attribute: an MMSI can be shared, spoofed or reused, so two vessels with one
MMSI are now two vessels. Looking a vessel up or selecting it by MMSI still
finds it.

A share link made while a vessel is selected opens that vessel. A UUID vessel
id travels as 16 bytes in a new thing field (13). A shared thing that is not
reporting now says so in a sentence per kind, with its id (ICAO, NORAD, MMSI
or vessel UUID) on a second line.
2026-11-10 18:12:53 -08:00
Sameh Khamis 57ff81f723 feat(live): flights, military and vessels follow the world clock when their source declares a timeline
A live source may now declare a timeline: it can answer a snapshot at a
given time (source.timeline = { coverage(), stepMs, subscribe? }, read by
readSnapshotTimeline). While such a layer is on it follows the world clock
and registers the coverage. At the live edge it polls exactly as before;
away from it the layer asks for getSnapshot({ ...query, atMs }) at the step
at or before the clock time and shows that snapshot instead of polling.

src/time/snapshotTimeline.js owns the following: a seek, time going
backwards or leaving live replaces every contact with the snapshot at the
new time (play waits on it, for at most 8 s); playing on keeps one
snapshot ahead and gates play on it, applying those snapshots as polls so
contacts move smoothly; outside coverage the layer is empty; back at live
it is cleared and polls at once. One load at a time, and a load the
source drops for a newer one is not a failure.

While following, aircraft are drawn at the clock time with no render
delay, records and absences are judged at the clock, track requests carry
atMs, and vessels move between fixes along the great circle. The world
clock counts jumps (seeks and returns to live) in getState().

A source with no timeline never follows the clock, and every layer stays
live, unchanged.
2026-11-10 18:12:53 -08:00
Sameh Khamis 874f83ba48 fix(voice): messages worded before reaching the status also follow a language change (#1661)
The radio and push-to-talk helpers shared by both voice providers now pass
English catalog text, and the OpenAI controller words its detail when shown,
like the Gemini session. A token failure without a server message carries
its status as sourceParams, so it is worded again with the number. The docs
say sourceDetail is the message as given, catalog text when it is the app's
own.
2026-11-10 18:11:07 -08:00
Sameh Khamis 44dd6d48a8 Changelog fragment for #1661 2026-11-10 18:05:04 -08:00
Sameh Khamis b1c5de0ed1 fix(voice): Gemini voice messages follow a language change
The Gemini session translated each status and error message once, when it
happened, so after a language change the voice control kept showing it in
the old language. The state event now also carries the English catalog text
as sourceDetail, and the control words it when shown. The local server's
known token errors stay catalog text until then instead of being translated
on arrival.
2026-11-10 18:04:52 -08:00
Sameh Khamis 962f7293c6 Merge pull request #1660 from bilawalsidhu/sameh/gemini-keyless-mic-hint
fix(voice): keyless Gemini voice drops the microphone hint
2026-11-10 17:20:20 -08:00
Sameh Khamis fdbff75e1e Format the keyless Gemini voice tests (#1660) 2026-11-10 17:10:45 -08:00
Sameh Khamis 0118e17ea6 Changelog fragment for #1660 2026-11-10 17:07:01 -08:00
Sameh Khamis b89f81b729 fix(voice): keyless Gemini voice drops the microphone hint
The Gemini token route now answers a missing key with code API_KEY_MISSING,
the session carries it to the error state as missingKey, and the voice
control hides "Check microphone permission" for that error, as it already
does for OpenAI. The OpenAI controller sends the same flag, so the shared
control and the controller agree whichever updates last.
2026-11-10 17:06:50 -08:00
Sameh Khamis 9560a6dcd1 Merge pull request #1599 from bilawalsidhu/fix/keyless-voice-key-hint
fix(voice): keyless mic says where to add the OpenAI key
2026-11-10 16:22:58 -08:00
Sameh Khamis f86b8954f6 fix(voice): a missing-key error keeps its wording and hides the microphone hint after a language change (#1599)
The voice error was matched against the translated key message to decide
whether to show the microphone hint, so switching language brought the hint
back and left the message in the old language. The error now carries a
missing-key flag into the status, which words the message at render time.
A controller test covers the language switch, and CURRENT-STATE.md documents
the keyless voice response.
2026-11-10 16:11:30 -08:00
Said Turksever 0687e0e3da Changelog: 1599 fragment 2026-11-10 16:11:17 -08:00
Said Turksever c03a54de40 Tests: pin the API_KEY_MISSING code on the keyless voice token response 2026-11-10 16:11:17 -08:00
Said Turksever 09d02b6e7a Keyless voice: say where to add the OpenAI key instead of blaming the microphone 2026-11-10 16:11:17 -08:00
Sameh Khamis 775e9df120 Merge pull request #835 from kvnloo/feat/viewport-context-decision-upstream
feat(voice): reason-code viewport context escalation
2026-11-10 16:04:54 -08:00
Sameh Khamis 49edc31879 Merge branch 'main' into feat/viewport-context-decision-upstream 2026-11-10 15:54:49 -08:00
Sameh Khamis 4442243749 Merge pull request #1659 from bilawalsidhu/sameh/detection-occlusion
Hide street-vehicle brackets and labels behind buildings and terrain
2026-11-10 15:45:56 -08:00
Sameh Khamis 100369f9af Merge remote-tracking branch 'origin/main' into feat/viewport-context-decision-upstream 2026-11-10 15:44:17 -08:00
Sameh Khamis 9234c1b3d2 fix(voice): drop viewport decision codes the caller can never log, name the crop decision eligible (#835)
The decision no longer reports not-entity-context or channel-closed, which
its only caller rules out first, nor pointer-scale-not-useful, which the
crop path already checks and logs. A closed channel returns before any
record, as on main. The crop decision is pointer-crop-eligible, since the
crop checks after it decide whether the pointer is on ground, and a sender
called on a closed channel logs channel-lost.
2026-11-10 15:44:17 -08:00
Sameh Khamis 85f42f6cd8 Changelog fragment for #1659 2026-11-10 15:28:44 -08:00
Sameh Khamis 076d264982 Hide street vehicles behind low walls and trees seen from above
In a dense downtown view from about 400 m up, the occlusion margin of
max(3 m, 2% of the range) came to 14 to 20 m, so a wall, awning, tree or
lower facade standing 10 to 20 m in front of a street was read as the
road itself: the vehicle's dot was hidden by the scene, but its bracket and
callout were still drawn. Checked against scene.pickPosition in one such
view, 18 of 470 vehicles in frame fell in that band.

The margin now depends on how steeply the camera looks down on the
contact. A point on the line of sight s metres in front of a vehicle
stands s * sin(elevation) above it, so a surface there hides the vehicle
once that rise passes 2 m. The margin stays at least 3 m and at most 2%
of the range, so grazing and street-level views are unchanged. In the
same views no vehicle with the drawn surface within 3 m in front of it
(its own road) is newly hidden.

From the review of the first change:

- A contact back in view after longer than a retest interval is held back
  and retested like a new one, instead of being drawn from an answer
  given before it left the view. It gets a fresh record, so a test still
  in flight from before cannot end the hold-back either.
- The hold-back for a contact's first answer stretches with the retest
  pace when a crowded view slows it.
- A batch whose control point failed no longer counts as a first answer.
- An unknown log-depth flag reads as "no depth" rather than as standard
  depth, which would have put every surface in front of every contact.
- A shader that fails to build leaves no shader behind and is not built
  again on the same context.
2026-11-10 15:08:26 -08:00
Sameh Khamis 630e1b1998 Hide street-vehicle brackets and labels behind buildings and terrain
The detection overlay only hid a contact behind the Earth's curve, so a
street vehicle behind or under a building, a 3D tile or a hill still had
its bracket and "VEH-nnnn" callout painted over that geometry, while its
own dot was depth-tested and correctly hidden.

A contact a layer marks `depthTested: true` (only Street Traffic does) is
now tested against the depth the scene drew and left out of the overlay
while a drawn surface stands in front of it by more than max(3 m, 2% of
its range). Other layers are untouched: their markers are drawn over the
scene on purpose.

The test runs on the GPU and never waits for it. Each tested contact is
one point drawn inside an occlusion query; the vertex shader looks up the
frame's packed pick depth at the contact's pixel, decodes it and drops the
point when the surface is nearer than the contact's threshold. Query
results are collected on later frames. Reading depth back was measured
and rejected: `scene.pickPosition` per contact costs 0.2 to 1.4 ms each,
and Chromium answers `getBufferSubData` with a round trip to its GPU
process even behind a signalled sync object (1.5 ms on a quiet machine,
15 to 25 ms on a busy one).

- Contacts are retested every 100 to 200 ms, spread across frames, at
  most 256 a frame; with more in view the pace stretches so none starves.
- Hiding is immediate. A hidden contact returns 600 ms after its last
  "hidden" answer, so one on a silhouette edge does not blink, and a
  contact new to the view is held back until its first answer (400 ms at
  most), so orbiting does not flash brackets behind buildings.
- A callout whose contact goes behind something is dropped at once, not
  carried to the next solve and not faded out on the facade.
- These times run on painted frames, so a hidden tab does not expire them.
- A control point in every batch must draw; if it does not, the batch is
  ignored and after three the probe stands down for ten seconds. With no
  pick depth (2D, Columbus view, depth picking off) nothing is hidden.
  A WebGL 1 context reads depth back synchronously about 8 times a second.

The renderer internals this depends on are read in one function,
`readSceneDepthSource`, with the engine version they were checked against.
2026-11-10 15:08:26 -08:00
Sameh Khamis 126616ac6c Merge pull request #1657 from bilawalsidhu/sameh/photo-without-credit
feat(flights): show a Planespotters photo even when it names no photographer
2026-11-10 14:32:17 -08:00
Sameh Khamis c068dc2290 Merge branch 'main' into sameh/photo-without-credit 2026-11-10 14:23:06 -08:00
Sameh Khamis e5bf072f6c Merge pull request #1583 from IMBrandao/feat/shipwrecks
feat(layers): add shipwrecks layer (EMODnet + Wikidata)
2026-11-10 14:19:54 -08:00
Sameh Khamis f1b843aaa1 docs(changelog): uncredited Planespotters photos are shown 2026-11-10 14:12:58 -08:00
Sameh Khamis 259bc1a024 feat(flights): show a Planespotters photo even when it names no photographer
A photo whose answer gave no photographer was dropped as no photo. It is
now shown, still linked to its Planespotters page, with no credit line.
2026-11-10 14:12:42 -08:00
Sameh Khamis 2992232fcc fix(shipwrecks): tidy after merging main: the retired-token test reuses the retired token, natural Hebrew, voice comment beside its entry
- layerStateTokenLedger.test: the retired-token reuse case now offers the
  retired token (02) to the newcomer, not News's 00.
- he.json: the catalog's singular imperative, {count} first in the plural,
  Wikidata read as one source, and smoother legend and info text.
- layerManifest: News after Shipwrecks, so the prompt-budget comment stays
  next to the entries it explains.
- CURRENT-STATE: Prettier-formatted (table column, blank line before a heading).
2026-11-10 14:07:07 -08:00
Sameh Khamis 8ff22f1a12 Merge main into feat/shipwrecks
Main added the News layer at the same places: both layers are kept in the
layer names, panel order (News stays last of Events), standalone sources,
voice manifest, credits, DATA_SOURCES and the CURRENT-STATE table. News took
share-link token 00 first, so Shipwrecks moves to 01 (ledger, registry,
CURRENT-STATE and the token tests). The catalog now has 40 layers and the
share-link registry 38. Shipwreck strings are added to the new Hebrew
catalog; the other catalogs keep both sides' strings.
2026-11-10 14:03:39 -08:00
Mazeyar Moeini Feizabadi 558a2f8854 Merge pull request #1593 from ZloyS/denis/gev-news-layer
feat(news): News layer with keyless GDELT conflicts and local news
2026-11-10 22:49:35 +01:00
Sameh Khamis f9b7a77140 test(shipwrecks): the page deadline subtracts the newest available source's reported age from the limit 2026-11-10 13:42:08 -08:00
Sameh Khamis 41641cd91a fix(shipwrecks): the page clears shown wrecks as a whole, once even the newest source is too old
Removing one source's wrecks on the page split merged records: a wreck both
sources list is one Wikidata record carrying EMODnet facts, so expiring
Wikidata dropped wrecks EMODnet still had, and expiring EMODnet kept its
facts. The page now keeps every shown wreck, marked stale, until the newest
source passes the limit, then clears them all. Adds a source test for the
reported ages and limit.
2026-11-10 13:40:40 -08:00
Mazeyar Moeini Feizabadi 8f489d203f Merge branch 'main' into denis/gev-news-layer 2026-11-10 22:38:56 +01:00
Sameh Khamis 7704de5075 fix(shipwrecks): the page expires each source's wrecks by its own clock, from the age the proxy reports
The page compared the proxy's fetchedAt with its own clock, so a skewed
browser clock cleared or kept wrecks wrongly; a snapshot without a timestamp
never expired; and one old source cleared the other's fresh wrecks. The proxy
now reports each source's age on its own clock and its seven-day limit; the
page turns them into per-source deadlines when the snapshot arrives (seven
days from arrival when no age is given) and, on a failed refresh, removes only
the sources past theirs. A refused short refresh is logged.
2026-11-10 13:38:31 -08:00
Sameh Khamis 6e4dd6299e docs(shipwrecks): refresh timing and the personal-text filter as they now work, rewrapped
CURRENT-STATE said each source is asked at most once a day; a failed source
is retried after 15 minutes. It now also covers refused short refreshes and
the page clearing expired wrecks, and its two shipwreck paragraphs are
rewrapped. The card's comment no longer says the records carry no names.
2026-11-10 13:36:18 -08:00
Sameh Khamis c885825f2b fix(shipwrecks): rebuild unless the snapshot has a version string; a failed page refresh marks the wrecks stale and clears them after seven days
- A source without a version (undefined, not null) compared as unchanged, so
  an injected source's new snapshots were never drawn.
- When the page could not refresh (the proxy answers 502 once both sources
  are past seven days), it kept showing the old wrecks as fresh. They are now
  marked stale, and removed once older than the proxy's own seven-day limit.
2026-11-10 13:36:18 -08:00
Sameh Khamis 66fba3da47 fix(shipwrecks): a source refresh with less than half of its last good wrecks is refused until the old set expires
A 200 answer that was valid but cut short (30 Wikidata wrecks instead of
about 1,450) replaced the last good set for a day. It now counts as a failed
refresh: the old records are served as stale and the source is retried after
15 minutes; once the old set is past seven days, the new one is taken.
2026-11-10 13:36:18 -08:00
Sameh Khamis 0c159186f9 fix(shipwrecks): small row and query fixes: era counts in one pass, pickers list only years with a wreck, no Portuguese default
- The legend counted each era twice, scanning every record eight times per
  row refresh; it now counts once.
- Each year picker listed every year from the oldest dated wreck (well over
  a thousand entries); it lists the years that have a wreck, plus the chosen
  ends.
- Labels fell back to Portuguese and links to Portuguese Wikipedia while the
  app ships 11 locales: labels now fall back through the app's languages
  after English, and a wreck with no English article links its Wikidata
  item, which lists the article in every language.
2026-11-10 13:29:25 -08:00
Sameh Khamis 38cf1e2be5 test(shipwrecks): an upstream body over its size cap counts as a failed source and is cancelled at the cap 2026-11-10 13:28:10 -08:00
Mazeyar Moeini FeizabadiandClaude Fable 5.2 16b1bc7b74 feat(news): larger markers, and a country's own news as one badge with a count
A report placed no closer than a country was a marker at the country's
centre, so a country's news piled there. They are now one disc with the
number of reports, named, and a click opens the Local list over the
country. Markers are 24 px at least: the category icon was unreadable at 16.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-10 22:28:08 +01:00
Sameh Khamis 6c4c5dd041 docs(shipwrecks): say the personal-text filter is a keyword filter, not a guarantee that no name is shown
DATA_SOURCES and CURRENT-STATE said text naming people is dropped. The filter
drops text with a title or role word (Mr, captain, owner, crew, reported by);
a bare name with none of them still passes. The docs and the code comment
now say so.
2026-11-10 13:27:59 -08:00
Sameh Khamis dd731a5da9 docs(shipwrecks): describe the layer on its own, not by a layer that is not on main
Air accidents (#1584) is not merged, so comments, the changelog fragment and
CURRENT-STATE no longer explain the shipwrecks markers, card and picker by
it; the row-range test fixture is the shipwrecks row. CURRENT-STATE now says
the markers are one billboard collection and the period is stored swapped.
2026-11-10 13:27:26 -08:00