Between real time and an hour a second there was nothing to watch a few
minutes of weather or a launch play out at. The speed menu now offers a
minute a second as its second choice.
The zoomed track now drags: the handle still drags the time, while the
track around it drags the view along (or the time, with all of it shown),
a tap goes to the time under it, two fingers pinch it, and a sideways swipe
or a shift-wheel drags it too, the same with a mouse, a trackpad or a
finger. A view dragged off the handle stays put until the time moves.
Play moved in steps: the world clock told of its time four times a second,
so the handle and the globe jumped, and play stopped at every weather frame
while it drew, loaded ahead or not. The clock now tells of its time each
animation frame, and play no longer waits on the frame it is at: that
frame loads as play goes on, and play holds only at the next frame while
the one before it still loads. Weather loads its next frame ahead while the
world plays too, tells its listeners of frames as they change rather than
every frame, and the sun and moon move on each minute of world time.
Play holds while a layer loads the frame for its time, and the bar's only
sign of it was a faint pulse, so a stopped handle read as a stall. The
bubble now shows a small spinner beside the time while play is held. It
comes on once a hold lasts long enough to notice and stays through a run of
slow frames, so neither a quick frame nor the gaps between frames flash it.
With layers of very different reach on together, a month of launches and
a few hours of lightning, the short-range ticks were crushed at the end of
the one shared track. Scroll or pinch on the track now zooms it around the
handle, plus and minus zoom from the keyboard, and zero or a double-click
shows it all again. Everything still plays together; zooming only changes
what the track shows, and the view follows the handle as it plays, steps or
is left behind by now. A scrub's live edge goes by the stretch shown, so a
zoomed track can land minutes from now, and a double-click or pinch puts
back the time its first press moved.
A building word ending the ask ('… Chapel', '… Library', '… Hall') now
passes the last-word rule only when the result is one a building ask
would accept, so a campus, zoo or district whose name merely starts the
ask (Duke University, Old Town) no longer answers it.
- A building word ("Burj Khalifa Tower", "Sagrada Familia Church") passes
the last-word rule on a building-like result; enclosures stay out.
- The anchor chooses only among the best-named outlines, so a worse name
never wins by containment alone.
- An ask without a quadrant takes only the quadrant of the matching street
nearest the point; a leading quadrant ("SE Main St") may go unsaid too.
- On an unbuffered tile edge a fragment shows as an edge along the
boundary, so a whole building touching it at a corner is not cut.
- Drop two orphaned comments and correct the place-name key wording.
Opening Utilities while Data Layers is open no longer forgets that Contact
was folded for the first, and closing one of them leaves Contact folded
while the other is still open. Enter and Esc in a drawn shape's name keep
focus in the field instead of dropping it to the page. The draw tool's
destroy test now checks it stops following the board, and the Draw gate's
amber-count comment sits on its function again.
- One place-name key for server and tiles, so "St. Paul" and "Saint Paul"
agree in Nominatim name checks.
- Tile street names: a lone letter is a direction only beside another name
word ("E Street" is not "East Street"), and a trailing quadrant ("E St NW")
may go unsaid in the ask.
- A building cut at the tile edge that is nearer the point than any whole
one leaves the point instead of a farther neighbour standing in; an edge
without a tile buffer counts as a cut, so a cut area never answers grounds.
- A batch building install re-judges only results that installed; a late
timeout during caption conversion keeps the pending point drawn; a failed
plain recaption keeps the old mark addressable.
- An interrupted Nominatim dispatch is reclaimed by the next lock owner ten
minutes after the start its marker names; node:sqlite loads on first use
without its experimental warning.
- PHOTON_URL accepts only https, which the page's connect-src allows.
- Shared OpenFreeMap tiles decode each layer when a consumer first selects
it; outline selection reuses the shared ring area and simplification, and
ground probes the shared ring centroid; the grounds word list lives once.
qa-draw-tool drives the rows, waits on a mark's state instead of fixed
sleeps, checks that fly-to centres the line, and compares two screenshots to
show a hidden Line's pixels go and come back. Its clicks and pixel clip stay
clear of the Utilities panel beside the left rail. qa-layer-panel checks
that Directions returns to the Utilities list after a reassembly.
Utilities stays in Cockpit for Directions, but only Data Layers folded the
Cockpit context rail away when opened, set its top below the open panel and
took Cyber's Cockpit size. Utilities now does all three, and Contact
expanding closes it as it closes Data Layers.
Draw and Import used to be reachable by voice through DISPLAY. set_panel_open
now takes 'utilities-panel', and "utilities", "draw", "drawing" and "import"
reach it. This adds one value to the Realtime tool schema, so the pinned
payload digest, enum list and serialized length are re-derived with a note.
The show_data_layers_menu test now finds a row only inside the list that
really holds it, so dropping #utilities-toggles from the lookup fails it.
Answers the review of bdbbc3d7 and the IC8 pass after it.
A shape hidden from Draw's list was only set to alpha 0, so the line's
flowing stroke, which keeps its own colour, stayed painted. Now:
- the world renderer's sync() sets every entity of a hidden mark to
show = false, and FlowMaterialProperty follows the mark's alpha;
- the screen renderer does not project a hidden callout, so it is not drawn
and cannot push a visible label aside;
- hiding acts at once, even before a mark's first frame, and with every
mark hidden the annotations render hold is released after one last frame;
- fly_route skips a hidden route.
Hand-drawn shapes are never merged as duplicates: every one is drawn on
purpose and unnamed until its row names it, so a second pin dropped near
the first was dropped, or replaced the first when its colour differed. A
restyle the renderer cannot draw now also syncs the renderer.
Tests cover each: renderer show/hide and flow alpha, the screen skip, the
render hold and its last frame, a show with no fade-in, hidden routes, two
nearby pins, and the restyle rollback.
main moved the panels to Vue views and a one-open-panel rail, and removed
the two templates this branch edited. This merge carries the branch onto
that architecture; it adds no behaviour of its own beyond the port.
- The Utilities panel is a RailPanel in LayerPanelsView with its own rail
item (handyman). LayerPanel renders the Directions rows into its list with
LayerRows, and falls back to Data Layers without it.
- Draw leaves DisplayPanel for DrawControls, DrawMarkList and DrawSwatches,
which render what the draw tool publishes to the display store and call
its actions, as GeoImport does. drawMarks.js words the rows; the DOM-built
drawMarkList.js is gone. A row's fly-to is an immediate navigation like
Import's ('drawing' gets its Cockpit refusal), the name field keeps a
draft through v-field-value, and the colours are one radio group with a
single tab stop.
- UiSegmented draws an option's icon, named by its label, for the shape
buttons.
- The catalogs take main's languages with this branch's strings, Arabic
included, and the tests follow the views to their new homes.
A street ribbon that holds neither its ring mean nor the anchor is centred
on the street's nearest point when the street runs within 100 m.
Nominatim outline selection ranks an exact name, then one holding every
asked word, then an overlap, and refuses an overlap whose name lacks the
ask's last word unless that word is in the result's address or type, so
"Central Park Zoo" is no longer Central Park. The server caches every
accepted outline and answers each ask with the one holding its resolved
anchor (sent as alat/alon), read across the antimeridian.
A duplicate mark that fails to clean up no longer tears down the new
outline it duplicates.
Play no longer runs ahead of the data. The world clock gains a wait, which
holds continuous play where it is while a layer loads what the time needs,
and a gate, which keeps play from passing a time a layer has yet to load.
Weather gates play at its next frame and waits there until the frame shows,
so every frame is seen however fast play runs; it waits no more than eight
seconds on a frame that never comes, and takes a wait only when its frames
change. Play pressed while a frame loads waits for it too. A scrub still
drops what loads and moves on.
The bar loses its long date and the dates at its ends: the time rides in a
bubble over the handle, which pulses while play waits, and the bar runs
left to right in every language. Play starts at real time.
A speed menu beside play offers real time, an hour, a day or a week a
second, a day a second to start with. A speed chosen while playing applies
at once; chosen while weather's own replay steps its frames, it takes
playback over from where it is. Play starts again from the top only at live
or at the very end, so a pause just before the end plays on. On phones the
menu sits beside the track, a full-size target.
Cesium sets no limit, so zooming out left the globe a speck among the stars.
On the globe a hand now stops zooming out where the globe spans 30% of the
screen's height, which follows the screen's shape as the app's own
whole-globe view does. A camera already past it, after a flight or a turned
screen, stops there rather than jumping in at the next zoom. Flat maps keep
zooming out unlimited.
Away from live, a launch shows from its lift-off on: its marker, label,
paths and orbit stay hidden while the world time is before it, so scrubbing
or playing brings the launches up in the order they flew. The selected
mission stays whenever it flies, and live shows every launch as before.
Play on the timeline runs the whole track in about a minute, from its start
when at live, and runs the clock itself past weather's own replay. It goes
live when it reaches now, or holds at an end that lies ahead of now. Going
live puts the rate back to 1.
The timeline shows whenever a layer with time to show is on, live or not.
Its track runs over the time those layers cover, reaching to now, with a
tick for each launch or frame and a handle to drag; arrows step between the
ticks, and a scrub to now goes live. The bar sits along the bottom of the
screen, and the dock and everything anchored above it rise by its height;
clean view, recording and the cockpit put it away with the dock.
Layers whose data has a time of its own follow the page's world clock from
the start; live feeds such as flights, military, vessels and transit stay
live. Launches register the span of the launches loaded, so turning them on
brings up the timeline. Each timed layer registers its coverage once and
reports none while off, telling the clock after each step has settled. The
launch feed keeps its own window, and an orbit ring is drawn again once the
world time has moved far from when it was drawn.
The catalog stored the designator on satrec.intldesg for
internationalDesignatorYear, while the active-TLE lookup parsed the same
columns with tleLineLaunchYear. Each catalog entry now keeps launchYear from
tleLineLaunchYear, which moves to src/sources/tle.js beside the other TLE
field readers, and internationalDesignatorYear goes.
A blank designator now reads as an unknown year in both lookups; the
active-TLE lookup read it as 2000.
The traffic timing test starts a Vite server rooted at the package, which
wrote its dependency cache to node_modules/.vite and kept writing after the
server closed. A host running Core's suite then finds files inside Core and
its check refuses them. The server now uses a temporary cache directory,
removed afterwards, and skips dependency discovery, which the SSR-loaded
modules never need.
The HUD place-lookup changelog note credits its PR, #1459.
Directions, the Contacts marker and panel, placed annotation labels, the
drawn-line measure, the Cockpit and formatRouteDistance each chose the unit
and precision on their own, and most chose before rounding: #1450, #1433 and
formatDistanceText or kilometreDigits from src/data/distanceFormat.js, which
cut after rounding. Each keeps its own metre step and rendering.
That also fixes the 10 km cut the Contacts marker and panel, the drawn-line
measure and the Cockpit still had (9,960 m read 10.0 km). The helper returns
kilometres already rounded, so the plain marker and the localized panel no
longer disagree on half-tenths such as 1,450 m.
A negative annotation distance now leaves the label without a distance, and
a drawn line with a coordinate that is not a number shows no measure, rather
than printing 0 m or NaN km. The HUD altitude tag and the spoken view height
keep their own unit choice.
UiRail takes `collapsible`: while no panel on its side is open, each item
shows its icon and label as a pill, and once one opens the labels fold away
to the icon rail, making room for the panel. The tip is not shown while the
label is on screen; the button keeps its accessible name and the visible
label is hidden from assistive technology. The side panel rails turn it on.
Each rail is now placed by its screen-side edge, at the rail clearance, so a
rail that widens grows toward the middle of the screen; collapsed, it sits
6px outside its stack as before.
The bar now needs more than the world clock being away from live: a layer
that moves with the clock must be on with time to show. In Core that is
weather radar, satellite or lightning with frames; a host adds layers by
following them. Turning the last one off hides the bar; turning it back
on brings it back.
The world clock reports this as `supported` in its state. Weather products
register their frames as coverage one by one, suspended or not, so the way
back to live stays on screen with reduced motion. Weather refreshes and
satellite catalog changes tell the clock their coverage may have changed.
A coverage that throws counts as none, and the clock reads its state once
per notification round.
A bar above the command dock, like a video player's: play and pause on
the left, the world time's date and time in UTC beside it, and Live to
return. It stays hidden while live, so Core looks as before until weather
history is scrubbed or replayed, a host moves the clock, or a link made
away from live is opened. Adds the pause and play_arrow glyphs and the
strings in every catalog.
Since the UI moved to single-file components, every unit-test process
compiled the views it imports from scratch, and the loader ran off-thread
(module.register), adding a round trip to every module a test imports,
including the ones that are not components. Core's unit tests took about
twice as long, and a host running Core's suite on smaller runners passed
its CI time limit.
register-vue now installs synchronous hooks on the loading thread
(module.registerHooks). Compiled components are kept in the system temp
directory, keyed by the file's path and text, the Vue version and the
loader's own code, so each one compiles once across test processes; the
compiler itself loads only on a cache miss. Cached modules run as code, so
the directory is used only when it is a real directory owned by the current
user and writable by no one else; on Windows it lives in the user's local
app data instead of a temp directory that may be shared. The cache never
sits inside the package tree, and GEV_VUE_CACHE=0 compiles every time.
On four cores the full suite went from 441 s to 240 s from a cold cache.
Field 28, worldTime, holds the world time of a link made away from live:
zigzag seconds since 2026-01-01, like field 8, but it may lie ahead of the
link's creation (a forecast moment). Opening such a link pauses the world
clock at that time; a link without it leaves the clock alone, so a page
opens live and Back or Forward does not stop a replay. Times no JS Date
can hold are dropped.
Core told time eight ways: Date.now() in most layers, JulianDate.now() for
flights and military, Cesium's clock frozen at page load for the sky, the
weather clock for observed imagery. They now read one clock,
src/time/worldClock.js, exported as ./time.
Live is a mode, not a timestamp: while live the clock is the wall clock,
so every layer draws as before. Away from live only the layers that follow
the clock move with it. The weather products follow it, as they followed
the weather transport; a host opts others in with follow(id).
- The weather clock takes its time from the world clock and drives its
replay, stepping its frames as before.
- Satellites, launches, flights and military (still 30 s and 15 s behind),
transit, GNSS, Recent Imagery, street level, the HUD and the celestial
ring read worldTimeFor(id). Cadences stay on the wall clock.
- Cesium's sky (stars, sun, moon, atmosphere, lighting) draws at the world
time each frame instead of the moment the page loaded.
- Weather frames and satellites declare coverage() for a host's timeline.
The transit, ALPR, vessel, CCTV and flight/military context fixes each
wrapped 359.5° to north with an inline Math.round(x) % 360. They now call
roundHeadingDegrees in src/data/headingDegrees.js, which also wraps negative
and multi-turn bearings. normalizeHeading moves there with it, and
cockpitMath re-exports it so existing imports keep working. It now adds a
turn only to a negative remainder, so a bearing just under half a degree
keeps its precision.
The awareness, Local ADS-B, cockpit wind, cockpit heading and Street Level
readouts use the same helper. The Cockpit's TRACK ACQUIRED signal rounded
without wrapping and could read COURSE 360°; it now uses
formatHeadingReadout, which shows --- for a track that is not finite.
The panel now renders from the CCTV store, so the meta line is no longer
written to a DOM node by _renderCctvState. Assert on presentCctvPanel's
meta text instead.
The application package boundary owns src/i18n/locales/ar.json beside the
other catalogs, and the style.css shim test checks that the shim holds only
import statements rather than counting its lines, so adding rtl.css to it
does not read as rules.
Selects pad the chevron's side with padding-inline-end, as do the Radio
filter, the SDR gain select and the voice timeout select, which set their
own padding; rtl.css moves the chevron to the left, where an Arabic field's
text ends. A rail legend's range keeps left-to-right order under its colour
ramp, which always runs low to high from left to right.
Main moved the UI to Vue views and Pinia stores, so the Arabic changes to
the old DOM controllers now live in their views: the rail legend range
comes from formatScaleRange (keeping units a source supplies as given),
the voice timeout options are worded by Intl unit formatting in
VoiceControl, and the scene editor's JSON fields stay left to right in
SceneDialog. Main's CCTV status line, launch panel and calibration values
keep the Arabic isolates and Western-digit formatting.
isolate() and keepCodesWithValues() read the locale through getLocale(),
which follows the page's vue-i18n instance, and the Intl formatters take
intlLocale(getLocale()). A language switch now applies in place, and
applyDocumentLanguage sets <html dir> on every switch, so the page mirrors
without a reload.
The icon rails and their tips are placed with logical properties, so they
stay on the outer side of the mirrored stacks; Cyber's new stack offsets,
padding and edge lines are logical too, and rtl.css moves Cyber's right
stack to its new rail clearance. Main removed the Context mini radio, so its
right-to-left rules and test go.
The Arabic catalog gains main's new strings and drops the ones main no
longer uses; the other catalogs keep main's new keys and the branch's
landing labels.
With the panel rails, the left stack's red edge line stood alone beside
the rail whenever no panel was open. It now shows only while a left panel
is open, and the right stack gains the mirrored line for its open panel.
The right stack takes the line and its gap without moving its panels, so
its rail sits outside the line like the left one.
A tall panel moves its stack to the focus or tail layout, and the rail
inside the stack went with it, so the Data Layers and Authoring buttons
jumped up and the right rail followed. Each layout pass now offsets the
rail back to the resting top, with the same transition as the stack, and
the right rail lines up with the left rail's buttons. Cyber pins the left
stack's top, so it flags that and the rail takes no offset.
Choosing a language in the picker now applies it at once. The choice is
stored and ?lang is kept in step through replaceState, and the picker keeps
focus.
Code that writes translated text itself re-presents it on a switch:
onLocaleChange() lets an owner listen, removed at teardown, and status
writers hand over a function of the locale (voice cards, scene director,
SDR, draw hints, imagery split, map sources, Bhote Koshi media) so the
words are rebuilt rather than re-sent. The HUD, cockpit, radio, CCTV,
launch, awareness and credit texts follow the same hook, and
applyDocumentTranslations can be applied again without undoing text code
wrote since.
The unsaved-work registry and persistLocaleAndReload existed only to guard
the reload, so they go, with their catalog entries.
Every data-i18n, data-i18n-title, data-i18n-aria-label and
data-i18n-placeholder marker in the Vue templates becomes a t() call with
the key the boot pass used to look up, contexts included. The three labels
the shell rewrote at boot (Visual Presets, Location and the standby
description) render through t() too, and that rewrite is gone. With the
locale in vue-i18n, view text now follows a locale change, and markup
under v-if or v-for no longer comes back in English when it re-renders.
Rendered text and attributes are unchanged in every locale. The boot pass
still translates static page markup such as the loading screen and a host's
own markup, and npm run i18n rejects data-i18n* in a Vue template.
The application views install one vue-i18n instance that holds the active
locale and the catalogs. src/i18n/index.js stays free of Vue: bindLocale()
keeps its locale in the instance's ref, so t(), tn() and the Intl formatters
read it from there and a view that translates while rendering renders again
when the locale changes. A tag written straight to the ref is normalized.
Message formatting, catalogs and call sites are unchanged, and switching
language still reloads the page.
The instance outlives the views: unmounting one no longer disposes it. The
browser build defines vue-i18n's feature flags and drops its message
compiler, which Core does not use.
The Vue and Pinia migration's entries move out of CHANGELOG.md into
changelog.d/0000-vue-and-pinia.md, newest first, as the contributing guide
asks. 0000 stands in for the pull request number until it is opened. The
migration plan's cleanup step now records where it stands: the template
expansion is gone, the pattern is required for new UI, the page-level
markup tests render the real views, the attribute translation pass waits
for vue-i18n, and a host panel registration API waits for its first host.
CONTRIBUTING gains a Building UI section: new UX elements are Vue
components rendering a Pinia store their owner publishes to, portable
owners stay framework-free behind a publish option, controls come from
src/ui/kit first, controllers don't bind the DOM by hand (Teleport where a
view renders elsewhere; body classes, focus, same-task measured positions
and painted surfaces stay imperative), text goes through t()/tn() with
msg() for stored English, and tests mount components. The README and the
architecture summary drop the stale vanilla-JS claim, the UI ownership doc
points to the guide, and the migration doc's UiPanel note names
v-panel-state as what draws a panel's collapse.
The Cockpit display portal publishes the Display popover's slots to the
gev-display store while Cockpit is on, and the Display panel teleports its
HUD, Detection, Parameters and 3D groups into them and back, each only
while its own slot is published. The portal no longer moves the groups:
it keeps each surface's scroll position, puts back focus the move
dropped, and lays the surfaces out once Vue has moved them. The popover's
uses-shared-display-controls class renders from the store, and the right
rail no longer re-homes the parameters panel the markup already places.
PanelChrome, its collapse-preference restore and the phone shell read a
panel's disclosure state back from the gev-panel-chrome store
(_shownPanel), falling back to the markup's classes before the panel is
first presented, instead of reading the classes v-panel-state draws. The
share link and the phone shell share one rule for a panel the person keeps
closed (prefersCollapsed). The rails, the accordion and the hover
disclosure still read the classes, which the directive draws in the same
task as their layout.
The Cockpit layout pass publishes the utility strip's fit to the
gev-cockpit store: the HUD's --cockpit-utility-* lengths, whether only the
expanded control fits, and whether a collapsed control beside an expanded
one is hidden from assistive technology. CockpitView renders the lengths
on the HUD and CockpitUtilityControls the strip's class and each control's
aria-hidden, which now also follows a popover opened between passes. Focus
leaves a control before the strip hides it. Exit publishes no layout,
which also clears the expanded height, class and aria-hidden the old exit
left behind. The briefing and Contact cards' positions stay imperative:
the same pass measures them straight after writing them.
VisualSettings publishes the active style's parameter rows to the display
store, and the Display panel renders them with the shared row component;
a moved slider goes back through the store's setStyleParameter action.
The readout snaps a value to its step as the browser does, half-steps up
and an off-grid maximum down. A preset that resets the shown style's
uniforms republishes its rows, input for another style's parameters is
ignored, and a stopped owner takes its rows with it unless a newer owner
has published since. createStyleParameters is deprecated and builds its
rows the same way.
RecordingControls publishes the safe frame a recording composes in, 16:9
or 9:16 (null while none records), to the new recording store, and the
scene chrome renders the overlay's active and ratio classes from it
instead of the controls writing them. Any frame but 9:16, even none named,
records in 16:9 as before.
LocationControls presents whether the search field is open and whether a
lookup runs, and the dock renders its expanded and searching classes and
calls toggleSearch(). The lookup, the navigation's settle, Escape and the
location tray's collapse reach the field through the controls
(setSearching, settleSearch, closeSearch) instead of writing its classes.
LocationControls no longer takes searchToggle, LocationSearch takes
setSearching and NavigationController takes settleSearch in place of
searchInput.
The top-center and cockpit reset buttons call the location controls'
new reset() through the globe actions store, and render a reset under
way from it, instead of the location controls binding their clicks and
the navigation writing their labels. The store keeps each owner's reset
under way like its clear, and controls replaced mid-reset keep it on the
buttons. LocationControls no longer takes resetButtons.
The camera orientation controls publish whether the map is tilted and the
camera's whole-degree heading instead of writing the buttons' attributes,
and answer tilt and north-up through the globe actions store. The new
GlobeCompass renders the heading, so a rotating camera re-renders only the
compass. The store drops an unchanged camera state and keeps each clear
owner's busy state, so the buttons show the shell that answers them: a
shell replaced by a newer one, or disposed mid-clear, leaves the live
shell's state alone, and an older shell that answers again shows its own.
createOwners gains owns() and ownerOf().
The top-center bar is now GlobeActions.vue. The shell publishes whether a
clear is under way and answers the clear and share buttons through the
globe actions store, instead of binding listeners and writing attributes on
the buttons. A shell disposed mid-clear resets the busy state, so the next
shell's bar is not left busy. bindClearLayersControl stays exported and is
deprecated.
The Contact and briefing cards' headers repeated the same frame: the title
grid over a hairline, the accent kicker and the square action buttons. It
lives in cockpit-card-header, cockpit-card-kicker and cockpit-card-actions
now, beside each card's own classes, which keep only their spacing and
sizes; the cyber theme targets the shared classes. The computed styles of
every header element are unchanged in the default and cyber themes,
expanded and collapsed.
UiSlider merges a caller's own input, change and blur handlers with its
own, leaves the input alone on a null model, and gains unstyled for a
range drawn as the browser's own. The rail card setting, observation
timeline, imagery opacity, Street Level date and Bhote Koshi split and
progress ranges now render through it. The timeline and the setting
ranges put the handle back once their owner has answered, since a drag
that ends where the slider last rendered leaves its model unchanged.
The radio tuner keeps its own input: its owner moves the handle back to
the selected station without changing the slot.
UiBadge takes a tone and carries Core's gev-status-badge class, with
the density variants and colours left to its callers; the CCTV source and
calibration badges and the rail card badges render through it. UiEmpty takes
a title and hint, with the title as the line itself, since Core's
empty states are one line; the Scene shot list, mission roster and imagery
strip use it.
UiSelect renders the bare native select with v-model, options and
disabled. Options that carry their own attributes come through the slot, a
caller's own change handler still reaches the select, and hold never
rewrites a focused select and takes the model back on blur. Without hold, a
select takes its model back once its change has rendered, so a pick its
owner refuses goes back, as the raw value binding did.
The HUD layout, draw colour, RTL-SDR gain, radio station tag, voice
provider, voice timeout, scene and CCTV camera selects now render through
it.
UiChip is a non-interactive label chip and renders the voice
card's chips. The pressable chips and pills are pressed toggles, so the
rail card chips, map source chips and city and place pills render through
UiButton with pressedClass, keeping their classes, attributes and the
classes other code adds. The pills now report aria-pressed.
UiButton gains pressedClass, Core's class on a pressed button. The pressed
classes toggle through classList, so classes other code adds survive a
press, a caller's class change keeps them, a renamed pressedClass leaves
the button, and server-rendered markup carries them.
The Display, SDR, Radio and Cockpit enable, Cockpit weather and briefing,
mission replay, Street Level, TR-3B, overlay, voice and CCTV toggles now
render through it with the same ids, classes and aria-pressed values.
UiSlider takes v-model, min, max, step and disabled
over Core's native range input, emits numbers, and with `hold` never
rewrites a focused handle and takes the model back on blur. It is a
functional render function, so the portable style parameter rows use it
too. The Display, Radio, RTL-SDR, Cockpit, mission replay and style
parameter sliders render with it; server-rendered markup keeps their
default values.
RadioTuner binds the slider's pointer, key and value events in its
template and calls RadioControls through the radio store; the drag,
its slot and the pointer capture stay with RadioControls, unchanged.
The Radio panel's and Cockpit's Enable, launcher, station tag,
transport and volume controls call RadioControls through the radio
store instead of DOM listeners. A refused station tag gets the kept tag
back from act(), which now returns what the answering action returned.
The Cockpit's station steps keep rotate: false. The unused element
fields go from the shell.
SdrCard calls LocalSdrControls through radio store actions instead of
DOM listeners, and renders its gain options from the receiver's own
R820T gain steps with page-language labels. The display and radio
stores share one owner list (createOwners): the newest connected owner
with an action answers it.
Street Level's fit and panorama filters and Recent Imagery's mode render
through the kit's UiSegmented, and their owners answer the choice
through the Street Level store (setPano, setRenderMode) and the
readout's onSetMode. The imperative segmentedControl.js adapter and its
test go. UiSegmented moves focus at once on a key, a radio group from
the option holding focus and an arrows group from the selection, and
asks for a change only when the key lands somewhere new.
The file import no longer looks up or listens on the Import button, the
picker or the list, and no longer restores focus itself. It connects
importGeoFiles, toggleGeoImport, flyToGeoImport and removeGeoImport to
the display store. A new GeoImport view owns the button, the picker, the
rows and the hint: it opens the picker once the import has presented
itself, sends the pick and the row buttons' actions, and hands keyboard
focus on with v-focus-safe-children before a focused row goes, to the
next row's same button, else the previous row's, else Import.
The draw tool no longer looks up or listens on its controls. It connects
toggleDraw, setDrawShape, setDrawColor, setDrawLabel and clearDraw to the
display store, which now holds several owners and lets the newest owner
of an action answer it, and it publishes the colour and label beside the
shape, state and hint. The Draw panel renders the shape choice with
UiSegmented and binds the label and colour to what the tool publishes.
The label field's own Enter listener goes: the session's capture-phase
key handler already finishes the shape from that field.
The Display panel, the dock's style buttons and the clean-view exit call
the display store's actions, and DisplayBindings connects their handlers
to it, replacing the bindDisplayControls listeners. The detection
allocation and 3D model coverage choices render through a new kit
UiSegmented, which takes v-model, options, label,
stacked and disabled, and adds a keyboard policy, item classes and
per-option attributes.
Shell element fields and facade accessors that only the old listeners
read are removed. The package boundaries now own UiSegmented and the
UiPanel kit files, and the format scope drops the deleted test.
The Authoring panel's and shot list's buttons, the CCTV panel's buttons and
Street Level's nearest button are the kit's UiButton, keeping their classes:
danger buttons take the alert tone and primary ones the primary variant. The
CCTV on/off, coverage, auto-hop, projection and adjust toggles, which showed
their state only by colour, now carry aria-pressed too.
The Data, Authoring, Weather, Recent Imagery, Context, CCTV, Street Level
and Radio panels each wrote the same frame by hand. They are now one kit
component, UiPanel, with a title, badge/actions/default slots and
v-model:collapsed, drawing Core's panel markup and classes beside the kit's;
props cover the differences (sideways, glow, scroll, railScroller, tag,
toggleWords). RailPanel binds it to the panel chrome store: v-panel-state
writes its state classes at once, and its button's clicks reach PanelChrome.
UiPanelToggle is the collapse button UiPanel and PanelCollapseButton share.
UiPanel's collapsed drives the button and ui-panel--collapsed
only; the owner hides the body by writing the root's collapsed class, since
the rails measure a panel in the task that opens it. PanelCollapseButton
takes startCollapsed instead of initial and initialTitle, so a button shows
its state's glyph and words before startup presents the panel. Titles render
through t() instead of data-i18n, and the unused "Expand panel" string is
pruned. v-panel-state redraws the presented state after a re-render, in
case a root's class binding ever changes.
Cockpit presence now carries contextCollapsed and signalCollapsed.
setContextCollapsed and setSignalCollapsed update presence and republish
it only when the value changes, instead of writing the dataset and the
toggle attributes. CockpitContext and CockpitBriefing bind data-collapsed
and their toggles' aria-expanded, aria-label, title and icon from
presence. The Cockpit utility QA script waits a frame for Vue to render
before it reads the toggles.
A panel's collapsed, dock-pinned, dock-pinned-top, layout-auto-collapsed
and accordion-collapsed classes are drawn by its root's v-panel-state from
the gev-panel-chrome store. PanelChrome, its restore, the layout
controller, both rails and the side accordion present each panel's state
there instead of writing its classes; the rails call onCollapse(panel,
{collapsed, auto}). The directive applies a change in a synchronous
watcher, since the rails measure a panel in the task that opens or closes
it.
Destroying the chrome inside Cockpit now puts back the panels Cockpit's
entry closed while it can still present them. Without a shell, the voice
tools' set_panel_open reports that there is no panel to open. QA scripts
present the states they set instead of writing the classes.
PanelChrome no longer writes the panels' collapse buttons, the command
dock tray toggles' names or the dock pins' pressed state. It presents each
panel's open and pinned state to the new gev-panel-chrome store, and the
buttons, now views, call it there: PanelCollapseButton for the panel
headers, bound toggles and pins in the command dock. Clicks take effect
once the chrome is initialized; a new owner starts from its own states.
bindPanelDisclosure binds only a panel's Escape and refuses the retired
buttons and onChange arguments. UiButton joins the component kit.
SceneControls no longer takes element handles or writes the Scene panel's
button states, PLAY label, status line, progress bar or runtime chip: it
presents them to the gev-scenes store, and the panel's buttons, scene
select and IMPORT chooser call its actions there. The chooser opens only
while an owner presents the panel and is cleared once its import settles.
On destroy the panel keeps its last status and buttons, without shots or
the runtime chip, as before.
A refreshed manifest could lose its credit in two ways. A frame still
loading when the refresh arrived had no credit yet, so the refresh was
skipped and the frame installed with the credit of the snapshot it was
staged from. And a host switch restaged the shown frame, then re-applied
the refreshed manifest captured when the restage began, overwriting any
newer refresh that arrived while it loaded.
Each frame now remembers the latest refreshed manifest of its product,
whether or not its imagery is installed yet, and takes its credit from it,
falling back to the snapshot it was staged from. That snapshot still
supplies the time and bounds. A restage passes the newest refreshed
manifest to the new frame up front instead of re-applying a captured one
afterwards, and a refresh that arrives while a frame loads is kept for a
later restage.
initFirstRunExperience no longer writes the launcher card's classes,
attributes and text, wires its listeners or removes it: it presents the
card's visibility, busy state, status line, copy and scroll fade to the
gev-first-run store and connects the controls the welcome view's choices,
DISMISS and "Don't show this again" call. A refused save hands the tick
back to the view to revert. The launcher still reads the rendered card for
its hit test, keyboard trap, focus and overflow measurement.
The owner-chosen copy moves to src/firstRunCopy.js, shared by both. The
source-text tests give way to component tests, and the first-run mutation
QA follows the new code (67/67 caught).
A weather source can describe its own products (credit, units, coverage,
legend) through an optional `display` object in each product manifest:
source, credit, units, coverage, info, legend and extent. The weather
layer uses each valid field in the row summary, legend, info text,
coverage navigation, outside-coverage status, layer stats and imagery
credit, and keeps today's built-in value for any field that is absent or
invalid. Units and legend describe one scale, so they apply only as a
valid pair; otherwise both keep their built-in values, and the radar
info heading names the units in use.
Text is single-line and length-capped. It rejects control characters
(U+0000-U+001F, U+007F-U+009F), line and paragraph separators, and
invisible or format characters: soft hyphen (U+00AD), combining grapheme
joiner (U+034F), Arabic letter mark (U+061C), Mongolian vowel separator
(U+180E), zero-width and direction marks (U+200B-U+200F), bidi embedding
and override (U+202A-U+202E), word joiner, invisible operators and bidi
isolates (U+2060-U+2069), variation selectors (U+FE00-U+FE0F), the
byte-order mark (U+FEFF) and tag characters (U+E0000-U+E007F).
Legends need 2-16 stops whose labels are plain decimal numbers or short
text and whose colours are #rrggbb. A number label must read back
exactly as the legend shows it: within the safe integer range, with no
leading zeros and no digits lost to rounding. Extents must be finite, in
range and non-empty. Supplied source and units text is data: the layer
marks it so the panels show it untranslated, while built-in values are
translated as before.
The imagery credit is escaped as plain text and follows a refreshed
manifest even when the shown frame stays the same. A refresh changes
only the credit: the shown frame keeps the snapshot it was staged from,
so a later host switch restages it with its own time and bounds and the
refreshed credit. Manifests without `display`, including the bundled
nowCOAST ones, render exactly as before.
IntelHUD no longer builds #intel-hud with innerHTML or writes its readouts
by id: it publishes its readouts, typing summary, REC blink, visibility,
layout variant and style colors through its publish option to the gev-hud
store, and the intel-hud application view renders them. The corners render
from mount so the panel layout controllers register them as obstacles.
The vessel layer presents the selected vessel's readout through
src/hudReadouts.js, which IntelHUD follows, instead of writing into the
HUD. The Cyber sonar state answers a HUD toggle from IntelHUD's visibility
rather than the class the view renders on its next update.
The awareness layer presents the compass ring, heading, cardinal labels and
contact bearing markers through subscribeDirectionOverlay(); the application
shell publishes them to the gev-awareness-direction store and the scene
chrome renders them in the viewer's container.
The world overlay presents its focusable mirror of painted cards through
subscribeWorldOverlayActions() and answers activateWorldOverlayAction();
the application shell publishes the list to a store and the scene chrome
renders the buttons and the status, announcing a repeated focus again.
The module stays free of Vue for the portable transit entry, and its
canvases remain painting surfaces.
The screen annotation renderer keeps each mark's SVG parts as attribute
records and presents the board to a store after every rendered frame; a
view teleports it to the page and hands back the callout text and mark
groups the renderer measures. The tracked-entity fade is measured once
the frame is in the document, removed marks fade out before they leave,
and the styles it injected move to annotations.css.
The celestial ring presents its overlay to a store and the scene chrome
renders it in the globe container, handing back the two canvases the
ring paints. The application shell passes the store in so the keyhole
helpers other packages import stay free of Vue. Without its canvases the
ring still tracks whether the globe is framed and turns itself off.
The Bhote Koshi layer keeps its panel controls as plain records and
presents them to a store; the context rail renders the panel as its last
child and marks itself while it is on. Controls are bound to the enable
that published them, and an unchanged frame is not sent again.
The launches layer keeps the replay state, projection and wording and
presents the replay vehicle overlay through subscribeReplayOverlay(),
sending nothing for an unchanged frame, so the portable launches entry
stays free of Vue. The application shell publishes each presentation to
the gev-mission-replay store and the scene chrome teleports the vehicle,
orbit dot and callout into the globe container with the same markup,
classes, mode, transform, rotation and sonar opacities. Hiding keeps the
mode, place and text, as before.
createWindInspectionMarker() moves to src/layers/wind/inspectionMarker.js,
so the portable ./tools entry that reads wind/inspection.js stays free of
Vue. It keeps the frame listener and projection and presents where the
inspected point falls to the gev-wind-inspection store after each
rendered frame, publishing nothing for an unchanged frame; the scene
chrome teleports the marker into the wind layer's container with the
same class, inline style, hidden state and position. The application
guide lists everything leaving out the scene chrome now leaves out.
PointerReticle keeps the turn's timers and tool tracking and presents
where the user pointed to the gev-pointer-reticle store, one entry per
reticle; the command dock teleports each to the body with the same
class, aria-hidden, hidden state, target and position. The voice QA
reads the card and reticle after Vue's flush, and the application guide
says leaving out the command dock leaves out the voice control and its
reticle.
createFrameRateMonitor() keeps the key, frame count and interval and
publishes the readout to the gev-status store; the scene chrome renders
it after the subtitle with the same class, hidden state and title. A
page without the title bar still gets no monitor, and a monitor only
updates or withdraws the readout the store shows.
The data-pack presentations keep the Cesium resources, object URLs and
pick targets and present each pack's attribution card, with its media
player, to the gev-data-packs store; the DataPacks view teleports one
panel per set of presentations into the viewer's container with the same
markup and inline styles, and stops a removed card's player as it goes.
The scene interactions owner keeps the pick handler and the interaction
session and presents the panel to the gev-scene-interactions store; the
SceneInteractions view teleports it into the viewer's container with the
same markup and inline styles. Each activation keys its own panel, each
button carries the owner it was rendered for, and only the owner the
store shows may change or remove it.
LocationControls offers the indicator with showOrbit(active) in its
presented tray instead of building it on the body, and the scene chrome
teleports it there. Orbit toggles, including the voice switch, light it
through the controls.
ShellFeedback and the scene director show toasts through createToast(),
each on its own timer as before, and the scene chrome renders the toast.
Every show rewrites the text, so a repeated message is announced again.
ShellFeedback publishes the loading status and the road network chip, and
CctvControls the camera grid chip, to the gev-status store; the scene
chrome renders them. SplitFlapText plays the departure-board cascade that
splitFlap.js plans, writing only the label's one text node, and rests when
its owner is destroyed. setSplitFlapText and disposeSplitFlap are gone.
createImagerySplit keeps the divider's value, drag, keyboard, CSS position
and scene split, and publishes its line to the imagery split store.
ImagerySplitView, in the scene chrome, teleports each line to the body, so
recent imagery and the Bhote Koshi event no longer build divider DOM.
The dock renders the chips from the map source controls' published view,
so renderMapStackChips, syncMapStackChips and MAP_STACK_CHIP_CLASS have no
callers. Their tests now pin the chip models; the dock test keeps the
rendered chips' button type and native enablement.
createVoiceControl now returns a presenter instead of building the
control with innerHTML: the session, cost, input, card and panel owners
write its state by part and listen for interactions, and VoiceControl.vue
renders it in the command dock, between the location bar and the visual
presets as before. Focus moves wait for the render; the waveform's bars
publish apart from the control and only when they change; the minutes
field keeps a draft until its owner writes it. Owner tests use a real
presenter on their own store, and view tests render the control.
The Vue views apply the gev-action-button and gev-status-badge classes
themselves, so the DOM builders have no users left. The docs describe the
views instead.
createSceneDialog keeps its builder but publishes the dialog's lines,
fields, buttons and status to a scene sharing store; SceneDialog.vue,
teleported to the body from the scene panels view, draws the same modal
with the same styles and opens it with Cancel focused. The EDIT DETAILS /
SHARE SCENE bar is drawn by that view while sharing offers it. The status
directive is shared with Provider Settings, and the two QA scripts that
drove the old DOM read the new dialog.
initKeySetup keeps the setup requests, the ChatGPT OAuth flow, the pasted
field values and the dialog's keyboard, and publishes the POWER UP chip,
the dialog, its key rows and the status line to the gev-provider-settings
store. ProviderSettingsView renders them and sends clicks and typing to
the connected owner; in Cyber the chip is teleported into the top
toolbar. A successful save renders the rows afresh so no field keeps a
key, and the status line announces a repeated message again. The voice
action opens and closes the dialog through the same controls.
LocalePicker renders the existing picker model with the same markup and
listbox behaviour; ProviderSettingsView shows it while the shell publishes
the active language and its choice handler to the gev-locale store. A host
passing localePicker: false publishes nothing. A choice reaches the shell
after the list is closed on screen, and every opening keeps the active
option in view.
index.html now lists each view's host and the loading screen itself, so
the gev:template expansion plugin, its view list, the loading-screen
template and the ./build/html export are gone. createApplication takes
omitViews for views an application leaves out; their hosts are removed,
and a view an earlier call rendered is unmounted.
The FIRMS test that checks a real all-addresses connect failure relied on
the system resolving localhost to both ::1 and 127.0.0.1. On a host whose
localhost resolves only to 127.0.0.1, fetch makes a single attempt, gets a
plain ECONNREFUSED instead of an AggregateError, and the test fails.
Mock dns.lookup for that fetch to return both loopback addresses, so the
real fetch still attempts both families and the error shape check stays
deterministic. Assert the mock was used, so a runtime that stops resolving
through dns.lookup fails loudly instead of silently testing something else.
The Cockpit controller publishes which surfaces are shown (the entry
button and TR-3B chip, the HUD with its exit and reset buttons and
briefing card, and the Contact card) and the views bind them. Focus moves
to the exit button, and back to the entry, after Vue renders them.
Layout scheduling counts the published presence as visible, so entry
still lays out the cards. The Display portal restores only focus its move
dropped. The Missions layer no longer hides the Contact card directly.
Every scrolling surface carries the kit's .ui-scroll, which
draws the thin themed scrollbar from --scroll-thumb and --scroll-track; the
Context panel and others drew the platform's light scrollbar because the
page declares no dark color-scheme. The per-panel scrollbar rules go; Cyber
sets the two tokens and Cockpit's HUD windows keep their accent tint.
Selects draw .gev-field's own chevron instead of the platform arrow, the
voice selects join .gev-field, number fields keep dark spinners, and the
Street Level range uses the shared slider in its green.
RadioTuner renders the band label, readouts, slider and needle from the
Radio store. RadioControls keeps the drag and its slot and publishes what
the tuner shows; the component lays out the tick tape against the dial's
measured width, drawing no ticks before it has one, and lays out the
snapped needle before it drops the drag class so the needle does not
animate from where the drag let go.
The Display and Radio launchers, their popovers and the compact Radio
transport, Enable button and volume now render from the Radio store.
RadioControls owns the disclosure state; Cockpit Escape and the Display
reveal read that state instead of the DOM, and the Enable toggle guards
re-presses synchronously. The compact transport reuses the panel's
transport state and adds only its glyph and title.
The Cockpit controller no longer reconciles the live signal rows by hand.
publishCockpitSignals turns its signal items into rows, keyed by the
signal and its action target as before, with repeats of a key numbered,
and publishes them to the gev-cockpit store; a new CockpitSignalList
component renders them with a keyed v-for inside the briefing card.
Rows reorder several times a second, and a browser blurs any element it
moves. The new v-focus-safe-children directive turns Vue's move of the
row holding focus into moves of the rows around it, reaching the same
order while the focused row stays connected, and before the focused row
is removed it hands focus to the shown briefing page's tab, or the
collapse toggle, as the reconciler did. Selection stays with the
controller's click listener.
The focus test now runs the real components over happy-dom and counts
any move or removal of the focused row as a loss, including across 60
random reorders. The Cockpit QA script publishes and waits a frame
before reading the rows.
The Cockpit controller no longer writes the HUD's instruments, the flight
plan card, the visor's destination arrow or the vision selector. It keeps
them as plain state, starting from cockpitInstrumentState.js, and
publishes copies to the gev-cockpit store: the instruments on every HUD
update, the route only when it changed, the vision selector when the mode
changes. CockpitView renders the callsign, aircraft meta and emergency
mark, speed and altitude rims and ticks, compass tape, clock, position,
HUD layer, destination arrow and vision selector; a new CockpitRoute
component renders the flight plan card inside the briefing card.
The odometer readouts become CockpitRollingValue components. Their
throttle and digit roll move unchanged from setCockpitRollingValue into
cockpitRollingValue.js: every accepted reading restamps the throttle, a
change inside it is dropped, and each accepted change draws fresh glyphs
so the roll animation restarts. A rim tick with no reading still hides
where it stood, and a hidden arrow keeps its last angle and label.
In a dev build the HUD's Vue patch costs about 0.5 ms per update (1.4 ms
at 4x CPU throttle), against 0.4 ms (2.1 ms) for the DOM writes it
replaces, at about 10 updates a second.
The catalog now passes the gnss-integrity source's windowMs to the layer.
A missing, non-finite or non-positive value keeps the 30-minute default.
The row text now names the window the layer uses instead of a fixed 30 min.
The Cockpit controller no longer writes the Contact card's scope,
navigation state, cohort counts, nearest contact, bearing, uncertainty,
update time or weather toggle: it keeps them as plain state, starting
from cockpitContextState.js, and publishes a copy to the gev-cockpit
store, and a new CockpitContext component renders it. A lost contact
still holds its last readout. The weather toggle flips from that state,
not the button's attribute. The card's visibility, collapse and layout
stay with the controller. The controller's publishBrief option becomes
a publish object, one entry per Cockpit view.
The Cockpit controller no longer writes the briefing card's pages, cycle
toggle, regional news and local conditions: it keeps them as plain state,
starting from cockpitBriefPages.js, and publishes a copy to a new
gev-cockpit store. A new CockpitBriefing component, lifted verbatim from
the Cockpit view, renders it. News rows are keyed by story, so a focused
link never turns into another story on refresh. The controls keep the
controller's listeners; the route card and the signal list keep their
owners' bindings for now. The boot-time kicker translation is gone, as
the card translates its own text.
The phone shell no longer builds its tab bar and backdrop or writes their
aria-expanded, aria-hidden and hidden state: it publishes its tabs,
whether it is on and the open sheet to the gev-display store, and a new
PhoneTabBar component in the dock teleports them to the end of the page
from the first publish until the shell is destroyed, so they still follow
the scene's credits. The shell takes tab and backdrop clicks from the
document and returns focus to a tab once the tab bar shows it closed; a
reopen or destroy cancels a pending return.
LocationControls (./ui/location) no longer builds the city and landmark
pills or writes the mini status and the search error: it presents the tray
and hands it to its publish option, so the entry stays Vue-free. The shell
publishes it to a new gev-location store and connects the controls, and
the dock renders it and calls chooseCity() and choosePoi(). The landmark
row still expands a frame after its pills render. The search field, its
toggle and the reset buttons keep their listeners. The location QA harness
reads the rendered elements.
createMapSourceControls() (./ui/maps) no longer builds the chip row or
writes the status badge: it presents both and hands them to its publish
option, so the entry stays Vue-free. The shell publishes them to the
gev-display store and connects the controls, and the dock renders the
chips and calls select() for an available one. renderMapStackChips and
syncMapStackChips are deprecated. The map source QA harnesses wait for
the dock to render and no longer read the removed element refs.
VisualSettings no longer toggles the style buttons' active class or writes
the dock's style mini status and the ACTIVE STYLE corner. It publishes the
active style and its label to the gev-display store; CommandDockView and
SceneChromeView render them. The buttons keep their owner's listeners, and
the initial label renders through t().
The launches layer no longer builds the roster or the selected mission
panel as HTML strings in #space-mission-panel-host. It presents both as a
plain view and publishes it through subscribePanel(); MissionRoster and
MissionDetail render it from the gev-context store and call the layer's
panel commands. Replay controls are presented the same way.
The roster keeps keyboard focus on the same mission across refreshes, or
continues beyond the list when the mission departs, and the row under the
pointer takes the preview back on its next movement. Lines a null view
rebuilds are translated through t(). Disposing the shell clears the
Contacts and Space Missions views.
The awareness layer presents its panel as a plain view and publishes it through subscribePanel() instead of writing markup into #military-awareness-panel. AwarenessPanel renders it from the gev-context store, calls the layer's navigation, and keeps focus on the same control by identity across live repaints, continuing at the note when that control leaves or is disabled.
ContextControls presents the selected mode, the transition busy state and the nearby sites search through a publish option, and ContextView renders them from the gev-context store. ./ui/context stays free of Vue. The Radio QA reads the search button's aria-busy, the state it actually carries.
LocalSdrControls publishes the card to the radio store and SdrCard renders it. The new v-field-value directive never overwrites a focused field, including one a render disables.
RadioPanel, lifted from ContextView, renders the layer state, Enable, the tag filter, station card, transport, volume and playback line from the gev-radio store that RadioControls publishes to. The tuner, the local SDR card and the Cockpit radio controls keep their element bindings. Enable's busy mark and the dragged volume go through the owner so a render never leaves them stale, and the markup's tag option stands until the categories first change. ./ui/radio now imports Vue and Pinia.
The file import publishes its rows and hint to the gev-display store and DisplayPanel renders them, keyed by file, so a toggled row keeps its controls. Keyboard focus is given back once the list has rendered: a removed row's next row, else the previous one, else the Import button; a second change before the render keeps the first target, and focus moved elsewhere meanwhile stays there.
Restore shouldHideCollapsedRightPanels, syncScopeControls and the fadeSliders field, skipping sliders already bound by name. Note the API changes in the changelog.
A desktop side panel hides once collapsed, so Escape hands focus to its visible rail item instead of the hidden disclosure. Rail selection reads the panel's collapsed class rather than the store, which follows it a microtask late.
Re-apply the published scene to the select after every option render, as the imperative renderer did. The scene and visual-input QA scripts click a held shot label after disposal and wait a frame for the bloom row.
The picker's options are data: CctvControls computes the cameras in view
and the closing note for those left out, keeps a focused picker's list and
prepends a camera activated under it, and publishes the options, disabled
state and selection. CctvPicker renders them keyed by camera id and
applies the selection once the options are in place; it updates only when
the controls publish, as the old picker did on layer notifications. A
picker about to be disabled no longer counts as focused.
This completes the CCTV panel: everything it shows is rendered from the
gev-cctv store.
The frame lifecycle keeps its request state in an object instead of the
image's dataset and publishes it; the panel binds the image's src, class
and data attributes and the wrapper's loading and has-frame classes from
it, so preload tokens, kept pixels on a failed refresh and clearing on a
camera change are unchanged. The source badge is the pure
presentCctvSourceBadge(), the live switch and the typed summary are
published too. The video surface still draws into the panel's canvas.
presentCctvCalibration() joins the panel view: ADJUST, SAVE CAL, RESET CAL
and the seven pose chips render from the gev-cctv store. The inline editor
is store state: starting an edit publishes its field, starting value and
finish(), and the CctvCalValue chip renders the number input. The draft is
set once on mount so a refresh while typing keeps it; each edit gets its
own input. Commit and cancel rules, the captured base pose and the camera
guards stay with CctvControls.
formField.js had no other user and is removed.
CctvControls presents the switch, navigation and toggle buttons, the CAL
chip and the meta line with a pure presentCctvPanel() and publishes them
to the gev-cctv store; CctvPanel renders them. The controls keep their
listeners on the same elements, and still write the picker, frame, source
badge, calibration readout and summary, which carry no bindings.
A page that imports ui/cctv with its own markup no longer has these
elements painted for it, as with Street Level.
The panel's markup moves, verbatim, into StreetLevelPanel, which renders
the presentation StreetLevelControls publishes to the gev-street-level
store, with the EXPAND state. The controls keep their listeners, full
screen and popover handling, focus moves, resizing and transitions. The
SINCE readout follows the thumb while dragging and shows the recorded
window once the thumb is there; the thumb never moves under the user's
hand. Text the panel now renders itself drops its data-i18n markers.
Panels lifted out of a view live in src/ui/views/panels/, kept out of
Prettier like the views.
A failed assert.equal or deepEqual prints both operands, and a happy-dom
node reaches the whole page and window: printing one exhausted memory and
took the machine down. assertSameNode() compares identity and fails with
a short description of each node; every node comparison in the happy-dom
tests uses it.
createRecentImageryPanel() computes the readout from the layer snapshot
and renders RecentImageryReadout and its day cards synchronously; its
delegated listeners stay on the stable root and the mode buttons keep their
segmented control. Rail card actions take optional pressed and reserved
states, replacing the owner's patches after render. Thumbnails, focus,
scroll preservation, the DETAILS reveal and export behave as before, and
nothing paints after teardown.
createWeatherPanel() computes the readout it shows (forecast and event
cards, the observed group's scope, the history timeline state and observed
cards) and renders WeatherReadout synchronously into the panel body, so a
newly opened card is still revealed after measuring. The hand-built hosts
and the separate card and timeline builders are gone; markup, open-card
choice, first appearance and teardown are unchanged.
The observed-history transport is RailTimeline in src/ui/rail/, rendered
by createRailTimeline() with the same update and destroy API. Drag preview,
the coalesced 150 ms commit, release, cancel and blur behave as before; the
slider keeps the ticks it is dragged over, an update from inside a preview
cannot move the handle back, and readouts follow the handle within the same
task. utcClock moves to its own module and is still exported here.
The rail cards, card blocks, settings, chips and row lists are Vue
components in src/ui/rail/. createRailCards() and createRailCardBlocks()
keep their update and destroy API and patch the components synchronously
with render(), so Weather and Recent imagery are unchanged. Controls of a
removed block dispatch nothing, a released range keeps its value until the
owner's next update, and range values are written only when they differ.
Weather's remount state is keyed by its panel. Tests of rendered parts run
on happy-dom.
SceneShotList and SceneOptions render the project from a gev-scenes
store instead of scenePresentation building rows with createElement.
SceneControls stays the owner: it publishes the director's state and
attaches the row actions, and a late destroy cannot detach a newer
owner. Rows keep their identity across selection; a rename keeps its
typed text across republishing and ends with its row. The select still
changes its choice only when its options are rendered.
Vue keeps whitespace in every component, so Prettier formats single-file
components with strict whitespace sensitivity, and the test loader parses
templates with the same options as the browser build. The i18n check
reads view text without its interpolations, whose t() calls are scanned
with the sources.
The scene chrome, cockpit, Display controls, command dock, layer panels,
Context rail, welcome card and Provider Settings move verbatim into
src/ui/views and mount into their hosts before the page is translated and
wired. Each view renders the same elements, attributes and text as its
template; Vue keeps whitespace between elements as one space, and no view
markup sits under a white-space: pre rule.
Template asset URLs stay literal, view files are left out of Prettier so
the markup is not reflowed, and the Node component loader moves to build/
and is exported as build/register-vue for host test runners. Hosts that do
not initialize Core's locale pass localePicker: false instead of editing
the markup.
Templates listed in build/applicationViews.js expand to mount hosts, and
mountApplicationViews() renders them before translation and wiring. Markup
tests read the page through a server-side render helper, the i18n scanner
reads view templates, and Vue preserves whitespace so views keep the exact
DOM. No template is converted yet.
Each side stack hosts a Vue rail listing its panels; selecting one opens it
through PanelChrome. The shell owns one Vue runtime and Pinia instance, and a
panel store mirrors each side's panels from the DOM. The rail hides with its
stack, in Cockpit, clean view and recording, and on phones. Panels without a
catalog entry get a generic item, and Data Layers leaves the rail while the
f shortcut hides it.
A panel a feature opens (Street Level's viewer, a selected radio station)
now becomes its side's open panel instead of being collapsed again by the
next layout pass. Cockpit keeps showing its collapsed Data Layers strip,
which Cyber's Cockpit layout aligns to, and Cyber's focus layout no longer
re-shows collapsed left panels. Both sidebars share one accordion helper.
The Cyber accordion now applies to both sidebars and every HUD layout:
opening a side panel collapses its peers, the layout normalizes restored
state to one owner, and collapsed panels are hidden on desktop. Phones keep
showing several panels per sheet.
A building or grounds outline from tiles now needs the tile holding the
anchor: when that tile fails and only neighbours load, no neighbouring
building or area stands in. Nominatim name tags holding several
";"-separated names match each name. Document recovery from a stuck
public-Nominatim dispatch marker, correct the phone credit comment,
drive the slow-device queue test with a mocked performance clock, and
credit GitHub handles in the changelog.
On phones the credit line kept white-space: nowrap, so map plus outline credits ran past the right edge and cut off the Data attribution link. Cesium's credit block is absolutely positioned inside #cesium-credits, so the box needs a real width for the credits to wrap: give it the screen width divided by its 0.8 scale, overriding the dock's width: auto !important the way the phone offset already does. Desktop keeps its one-line rule.
Recheck the caller's signal right after the capability probe on every path, so an ask cleared while the probe is pending starts no outline or last-resort search request, whether the probe says configured, unknown or not configured.
Add GET /api/geocode/status ({configured}), like /api/overpass/status. The outline client and the last-resort place search ask it once per page and send no lookup a server without a Nominatim endpoint would refuse, so a default install no longer logs a 503 on its first named landmark or grounds ask. An older server or a failed probe leaves the capability unknown and lookups proceed as before.
- join street pieces at up to 3 m again (inclusive), as before the shared stitcher
- share one default outline tile source across outline rung instances
- cover Japanese and Thai street names that differ only by a mark
- restore main's text for formatting-only CURRENT-STATE hunks
The server built center from Number(row.lat/lon), which reads a missing value as 0,0, and the client never forwarded it, so the rung's second ordering point was always undefined. Order parts by the request point only.
The rail titles took the dock's rem type inside fixed 176px launchers, so a
larger browser font cut collapsed titles such as Recent Imagery short. Cap
the rail type at its 16px-root size.
The compact type for translated titles now skips Chinese and Japanese,
which fit at full size and keep their 9px floor, and reaches only the rail's
own launchers, so Radio inside Context keeps its type.
Tests now check the launcher divider, gap, toggle margin and flex-shrink
rules by their declarations, not only their selectors.
The shape list rebuilt every row on each board change. A name being typed was
lost when a voice mark landed or expired, and a click right after typing only
committed the name, because the button under the pointer was replaced. Rows
are now updated in place, a re-entrant render is deferred, picking a colour or
Esc returns focus to the swatch, and keys on the list no longer reach Draw.
Fly-to on a line across the antimeridian flew to longitude 0. A drawn mark
and a voice mark are no longer deduplicated into one another, so a voice TTL
cannot expire a drawn pin, and a new pin on a hidden one is drawn.
The engine tests now check effects (renderer calls, alpha, notifications on
clear and expiry, the framed centre), not only return values.
- remove the unused Nominatim reserve(); spacing tests now measure the
actual transport start across processes
- start each queued building's 2 s ground-sampling budget when it
reaches the head of the queue, and request a frame on promotion
- accept Nominatim and grounds outlines that cross the antimeridian
- let an unnamed school, university, hospital, cemetery, stadium or zoo
area answer a grounds ask whose head noun names that kind, using only
the part that holds the point; compare grounds names symmetrically
- charge shared OpenFreeMap consumers their own slice and keep the
charged budget at the previous 64 MiB
- share one place-name fold, point-in-polygon, line stitching and
nextTask helper instead of duplicates
- exclude Who's On First sources licensed only as "CC BY (equivalent)"
or "CC0 (equivalent)"; rebuild the pack from the pinned archives
- whole-word neighbourhood alias guard; correct the outline size-limit
comment
- restore DATA_SOURCES.md table formatting, keeping only real edits
- test behaviour instead of private engine fields
- Add A22_IMAGE_ORIGIN to the pinned frame directories, so a same-host
redirect from www.autobrennero.it/WebCamImg/ cannot leave that directory,
as for the other directory-pinned packs.
- Give Autostrada del Brennero a 3-minute ambient refresh in cctvLod.js,
like the other highway packs, instead of the 5-minute default.
- DATA_SOURCES: the Open Data Hub now tags every A22 record CC0; note that
this is the aggregator's metadata, not Autobrennero's own terms.
- Tests for the redirect pin and the refresh cadence.
Resolve conflicts with the NYC, Toronto, Lithuania, Florida, Taiwan and
Kartverket work on main. The CHANGELOG entry moves to
changelog.d/956-a22-brennero-webcams.md, as main now asks.
- fetchIpv4 builds its Response inside the https response callback, outside
the promise. A 204, 205 or 304, a status above 599 or an invalid reason
phrase made the Response constructor throw there, an uncaught exception
that stops the dev server. Null-body statuses now get a null body and
anything else that cannot become a Response rejects, as the Radio Browser
transport does.
- A non-OK FIRMS or mapkey-status response cancels its body so the IPv4
socket closes at once instead of at the deadline.
- Tests: the crash cases (fail on the old code), a source check that every
upstream request goes through fetchFirms and that fetchSource's body read
shares its deadline (fails if either call reverts to fetch), a real-fetch
contract test for the address-selection error shape, and an explicit skip
in the IPv4-only socket test where the host cannot tell the families apart.
- Changelog entry moves to changelog.d/1078-firms-ipv4-fallback.md per
CONTRIBUTING; CURRENT-STATE says which timeouts are retried and that the
retry does not follow redirects.
Resolve fetchSource onto main's shared deadline: one AbortSignal covers the
fetchFirms request and the capped body read. The CHANGELOG entry moves to a
changelog.d fragment in the next commit.
reconcile voice outlines with main f13c60c, retaining German/Japanese,
Operator HUD retirement and Context/Radio cleanup, named-building
containment, courtyard-hole, non-Latin identity, attribution, cancellation
and antimeridian repairs
keep outline callouts above the open voice preview when an above-panel
slot fits without colliding with another callout,
and clip the compact dock location label on narrow screens
Collapsed Recent Imagery (English) and Data Layers (Portuguese) pushed
their toggles out of the card. Collapsed rail launchers now drop the
divider and pack closer, translated collapsed titles use compact type,
and any title that still does not fit ends in an ellipsis.
The hover now matches the dock trays' hover rather than their pinned
look, only on pointers that can hover, and recolours only the hovered
panel's own header, so Radio and body labels keep their colours.
The cyber theme keeps main's gutters and Display toggle size, the dock
and rails share --dock-label-* tokens, and the dimmed open Context title
rule is gone.
The CCTV layer kept every loaded camera live: each view change touched every
icon, every notification rebuilt every camera's state, and the picker listed
the whole catalog. With the default 4,000 cameras, a continental drag ran at
152-179 fps with 18-24 frames over 30 ms (worst 112 ms). It now runs at
235 fps with none. The layer now:
- shows icons only on or near the screen, and when zoomed out keeps one camera
per world-anchored cell (healthy feeds first), so panning keeps the same
cameras;
- grounds and draws coverage only for the cameras in view, the active camera
and its coverage neighbours, retrying a camera at most twice per view regime;
- reruns the view pass while the camera moves, paced by its own cost, and
projects with one matrix per pass instead of a Cesium call per camera;
- keeps the billboard buffer-usage hint fixed, so showing or hiding a few
icons no longer rewrites the vertex data of every billboard;
- lists the cameras in view (active camera first) in the picker, closing with
the count of cameras outside the view, and builds the full camera list in
the UI state only when it is read.
Adds the Authoring labels and tooltips to the de, fr, it, ja, pt and uk
catalogs that arrived from main, and prunes the retired SCENES-era
entries there. The file row's tighter padding and letter spacing keep
one long translated word per line in the narrow Cyber panel
(IMPORTIEREN, ESECUZIONE, ІМПОРТУВАТИ, EJECUCIÓN).
- PLAY and NEXT continue into the following scenes, so they stay
enabled while any scene in the project has a shot, not only the
selected one.
- Renaming a shot drops the number and ellipsis so the editor is no
longer hidden; qa-scene-controls checks a long rename stays visible.
- PLAY, NEXT, EDIT DETAILS and SHARE SCENE tooltips describe what the
buttons do, translated in every shipped language.
- The primary actions and the file row keep a hover state.
- Docs: the EXPORT rename in DIRECTOR-SHARING and the playback button
states in CURRENT-STATE are accurate.
- Test the voice "authoring" alias through set_panel_open.
main gained de, fr, it, ja, pt and uk catalogs that still carried the 12
strings this change retires, so their catalog-matches-code tests failed
once the branch met main.
The entry now lives in changelog.d/1471-context-panel-cleanup.md with
credit, as CONTRIBUTING asks, and CHANGELOG.md is back to main.
CURRENT-STATE no longer lists a compact Radio power control and scopes
the readability description to what Cyber keeps. The responsive glow
comment no longer cites the removed Radio popover, and qa-radio's
pick-buffer comment is back above settleRadioFrames.
Four test fixtures stop stubbing the removed launcher sync, and
PanelChrome drops the _syncRailPanelCollapseButton pass-through, wiring
the layout controller straight to _syncPanelCollapseButton.
Japanese in the language picker: the full catalog, counters, unit words joined
to their numbers (5分前, 1時間5分), lower-case HUD units as in Chinese, and the
CJK label rules plus strict line breaking for Japanese. Based on translations
by @abiyeenzo (#913).
Co-authored-by: Enzo Abyie <[email protected]>
German in the language picker: the full catalog, neutral infinitive wording,
one/other plurals and the de style guide. Directions keep German nouns
capitalized, and layer names break at word hyphens but not inside codes.
Based on translations by @abiyeenzo (#908).
Co-authored-by: Enzo Abyie <[email protected]>
French in the language picker: the full catalog, French typography, one/many/
other plurals and the fr style guide. Voice plan steps join through a catalog
string so French spaces its colon, and with nine languages the picker shows
its search field. Based on the translation by @abiyeenzo (#907), checked
against @meirm's French catalog (#312).
Co-authored-by: Enzo Abyie <[email protected]>
Co-authored-by: Meir Michanie <[email protected]>
The merge from main kept this branch's side of four conflicts, which dropped the Snow PR's changes: tests expected Snow again, the share-link table lost its retired mark, and the voice pins lost the Snow restore. Keep both retirements, and re-derive the full-payload digest for both enum removals.
Operator was the only layout that displayed the top banner bar. Its markup, styles and keep-out selectors are removed, and the Cyber style test now checks that none remain.
The it, pt and uk catalogs merged from main still carried the two Snow
button strings, which the catalog tests report as stale. They are
pruned with npm run i18n -- --prune.
A Scene Director shot saved with Snow now normalizes to Normal when the
project loads, so the shot list shows NORMAL rather than an untranslated
SNOW for a shot that plays on Normal. The README's style keys read 1-6.
Brazilian Portuguese in the language picker: 2732 strings, Portuguese plural
forms and the pt style guide. The CCTV summary line names its calibration,
source and view in the page language, markup can give text a context with
data-i18n-context, and the cyclone source errors are translated. Based on
translations by @abiyeenzo (#909).
Co-authored-by: Enzo Abyie <[email protected]>
Italian in the language picker: 2730 strings, Italian plural forms, labels
shortened to fit the panels, and the it style guide. Based on the translation
by @abiyeenzo (#912).
Co-authored-by: Enzo Abyie <[email protected]>
Ukrainian in the language picker: 2730 strings, four plural forms, Cyrillic
HUD units and the uk style guide. Based on the Ukrainian translation by
@meirm (#312).
Co-authored-by: Meir Michanie <[email protected]>
Co-authored-by: Meir Michanie <[email protected]>
CI failed at format:check: the format scope still listed the deleted
panelChrome.radioOwnership test, and panelRails.test.mjs needed Prettier
after its glow list shrank.
The new Context title colour outranked Cyber's red panel titles, and the
collapsed Context launcher came out brighter than its siblings; the rule
now applies only to the expanded panel outside Cyber. Cyber's layout-focus
standby card had relied on the old dashed border and centring, so it
drew a 3px frame with its text off the radar art; it now names its own
1px frame, padding and centring. Cyber also drops the shared segmented
track, which drew a third frame around its already framed mode buttons.
Tests now pin the Context header to its title and collapse control, the
segmented control, 10px type, the missing halo and the scoped title
colour, plus the Cyber standby and track overrides. Each fails on the
previous CSS or markup.
qa-radio enables Radio and starts the first Play from the visible
controls, expanding Context and Radio the way a person must, then puts
the panels back. The Escape test and its mutation describe what they
still protect. CURRENT-STATE no longer describes the removed Radio icon
or compact player.
Each shape drawn by hand gets a row under Draw, as Import's files do: its
colour swatch, its name, and show/hide, fly-to and remove. A shape is named
in its row, and its swatch opens the five colours there, so labelling and
colouring happen after a shape is drawn and can be seen, instead of in a
field filled in beforehand. Area, Line and Pin become icon buttons, named
for screen readers and tooltips, and the colour for the next shape is a row
of swatches instead of a dropdown.
The annotation engine gains per-mark remove, setHidden, restyle and frame,
and onChange so the list follows the board. A hidden mark stays on the board
at alpha 0, a renamed line keeps its length, and fly-to frames a line by its
whole path. Voice marks are not listed.
The Draw, form-field and segmented-control gates drive the new controls;
swatches are checked as chips, not segments.
The step headings make the panel taller, so the left stack squeezed the
shot list down to one row at about 1000px of window height. Its minimum
now holds two rows; the panel scrolls when space is short.
Voice show_data_layers_menu opens the panel that holds the row, so asking
for Directions opens UTILITIES and reports that panel, not a hidden row
under Data Layers. In Cockpit the panel stays with Directions alone, as
Data Layers does; only Draw and Import step aside there.
Draw's options sit flat under its name instead of keeping the card that
hung under a DISPLAY button, and its Shape label no longer takes DISPLAY's
label column in other languages. Cyber marks Draw on with its red edge.
LayerPanel routing, the voice panel choice, and the panel's order, mode
and phone-sheet CSS gain tests that fail on the earlier code. The form-field
and segmented-control gates open Utilities for Draw's controls, measuring
Scenes and Utilities in turn since the left rail fits one tall panel.
README, CONTRIBUTING, the PR template, CURRENT-STATE and comments now say
UTILITIES for Draw and Import and list the Audio group, and two files
outside the format scope lose an unrelated reflow.
- Keep the documented 72px / flex-grow 1 action sizing and the 360px
panel width; the larger type applies to the Authoring panel only, so
CCTV and Street Level buttons are unchanged.
- Scope the primary and file-row styles to the panel so Cyber's shared
button surface no longer overrides them.
- Shorten the scene PLAY in Spanish and Russian so it does not break
mid-word, and fix two translation inconsistencies.
- Map the spoken name "authoring" to the panel.
- Clear the has-shots marker when the controls are destroyed.
- Test the idle button states through published changes, the PLAY
translation context, and where the sharing row is placed.
createView threw on style 'snow', so an embed host or a
show_in_gods_eye_view call holding a view from before Snow was retired
was refused outright, camera and layers included. viewFromParams read
style=snow as the recipient's own style, while the app opened the same
link on Normal.
Both now read a retired style as Normal; truly unknown names still
throw. The retired list moves to a browser-free module shared by the
view API, the share codec and visual settings.
The panel reads AUTHORING and groups its controls under SCENE, SHOTS,
PREVIEW and SAVE & SHARE headings, with the EDIT DETAILS / SHARE SCENE
row moved beside EXPORT / IMPORT. Text is larger with more contrast,
shots are numbered, every action has a tooltip and the status line is a
live region. Outside playback, buttons that would do nothing are
disabled. New labels are translated in every shipped language; PLAY
uses its own catalog context apart from the radio's PLAY.
qa-radio cycles Minimal, off, on, then Tactical, so the HUD toggle must
restore a non-default layout, and expects that sequence instead of the
removed Operator step.
The share-link test pins the exact hudVariant codes, so dropping or
moving operator's reserved code fails it. A live IntelHUD test checks
that a retired Operator layout falls back to Tactical, and the Cyber
test stub themes from the normalized value.
The share-link table marks operator as retired, and the zh-CN glossary
drops the removed string.
The Context header carried a Radio icon that opened a second, compact
Radio player on top of the panel, duplicating the Radio section that
already lives inside Context. Remove the icon, the compact player and
everything that kept the two in step. Radio stays in Context as its own
section with one Enable control; Cockpit's compact Radio is unchanged.
Context itself was hard to read: 7-8 px labels at low contrast, a dashed
placeholder box, and a glow halo. The two modes are now one segmented
control, labels and guidance are 10 px at higher contrast, the standby
hint is plain text, section titles use --text-secondary, and the panel
draws no glow.
Unused translations are pruned, tests and QA harnesses that pinned the
compact player are updated or retired, and CURRENT-STATE and the
changelog describe the new header.
Snow is no longer offered anywhere a style can be chosen: the Visual
Presets tray, keyboard shortcut 7, the Cockpit vision carousel, and the
set_visual_style voice action. The shader and its labels go with it;
weather snowfall and the SNOW weather condition are unaffected.
Share links keep working. Snow's enum code (6) stays reserved, so a
compact or long-form link that names it opens on Normal and drops the
Snow shader parameters without reporting them as unknown. A Scene
Director shot saved with Snow plays on Normal too. Snow's frozen share
defaults matched Normal's apart from those parameters, so no other
setting in an old link changes meaning.
The legacy voice-schema pins restore Snow before hashing, so the
unchanged pins still prove nothing else moved; the complete Realtime
payload digest is re-derived.
Mirror the dock trays and their pin, the compact Radio window and the
phone top-bar actions in right-to-left; keep the Cyber cockpit's Data
Layers stack on the left edge. Isolate the Space Missions counter,
landing coordinates and CCTV summary parts, format their numbers with
the locale's numerals, decide pack shot titles by the marker binding,
and place Bhote Koshi media cards between the panels on either side.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
UTILITIES becomes its own panel in the left column, after Data Layers and
CCTV and above Scenes. The layer panel renders the Directions row into the
panel's list instead of Data Layers, and Draw and Import move there from
DISPLAY with their ids unchanged. Radio, the other row under the old
Utilities heading, stays in Data Layers under "Audio".
The panel collapses and remembers its state like the other left panels; share
links do not carry it. It hides in clean view, recording and Cockpit, takes
the Cyber left-rail treatment, and sits collapsed in the phone's Layers sheet.
Voice focuses the Directions row in its new list. The Directions, Draw,
Import and form-field gates open UTILITIES to reach their controls.
Localize a bootstrap shot title without a source pack only when the
Nepal pack marker binds that title to the shot's id, so an operator
shot renamed to a bootstrap title in the built-in scene stays as typed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Localize a legacy Nepal base shot only when the applied pack marker binds
that title to this exact shot id, so a user shot renamed to a built-in
title stays as typed in every locale.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pack marker with shotBindings now vouches only for the shot bound to a
legacy bootstrap title, so a user shot typed with the same title keeps its
text instead of showing the localized built-in name.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Localize a pack shot title only for the shot the pack marker binds to that
title, so an operator's shot named like a pack shot shows as typed. Markers
saved without shotBindings keep the title-only check.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Japanese has no letter case, so the HUD summary keeps units as the
catalog writes them (13 km), matching the cockpit and the ja HUD CSS.
The ja '{value} KM', '{value} KM/H' and '{value} KM/S' strings are now
lower-case like zh-CN.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Display > HUD > Layout and the set_hud voice action now offer Tactical,
Minimal and Cyber. The Operator option, its CSS and its QA coverage are
removed, and the unused "Operator" translations are pruned.
Share-link codes are append-only, so operator keeps its hudVariant code.
A link, scene or saved state that names it restores the default Tactical
layout. The voice schema pins keep their legacy digests by restoring the
retired value before comparing; the full-payload digest is re-derived.
Every rail panel title uses the command dock's label type and colour,
collapsed or open, instead of three sizes at 30% opacity. Panels answer
hover as the dock trays do, with a blue edge, a soft glow and an accent
title. In the default theme each panel's title and collapse button sit
19px from its edge; Context's toggle no longer shrinks beside the radio
button. The cyber theme keeps its own colours and insets.
Conflicts in es, ru, zh-CN and zh-Hant catalogs: both sides appended keys
at the end (cyclone errors from this branch, routing refusals from main);
kept both.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The set_layer_visibility layerId hint listed aliases such as "dams" for
local-dams and "weather radar" for weather-radar, which the id in the
enum already says. Leaving those out, and a layer with no other alias,
cuts the Realtime prompt from 52,000 to 51,695 characters. Aliases the id
does not spell (aqhi, hurricanes, clouds) stay, and the runner still
accepts every alias. Refs #1363.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The test passed URL.pathname to sharp, which is /D:/... on Windows, so the
faces were not found and npm test failed on every Windows clone.
fileURLToPath gives a path that works on every platform.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The HUD builds place context for its AI summary whenever the camera
settles, and each build paid for up to four reverse geocodes plus a Nearby
Places search, even when no OpenAI key was configured to use them.
- Ask GET /api/openai/hud-summary/status once; with no summary configured,
or after an unconfigured reply, make no place lookups for the page.
- Reverse-geocode at most two points: the view center and one viewport
point.
- Reuse a lookup until the view center moves a quarter of the visible
width, the camera's distance to it doubles or halves, or the camera
crosses a lookup height. The reused points are asked again, so only a
timeout, failure or empty answer spends.
- Repaint the last AI reply when a small pan leaves the context unchanged.
Cached-only hosts and voice context lookups are unchanged.
Apply the accepted findings from the fourth three-model French review:
« Fermez d'abord cet outil » in the pointer-busy notices, the colon form
« Outil actif : {name} » for the directions notice, « FOV CONSTRUCTEUR »,
« RAPPORT » for the USGS trigger chip, « CONTEXTE GLOBAL DÉSACTIVÉ », and
wording fixes for the radio, recent imagery, bundle preview and draw label
strings. The guide gains glossary rows for the datasheet FOV and for
rapport versus relevé.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Node.js 24.14.0 carries CVE-2026-111497 ("Node2Shell"), an out-of-bounds
write in the bundled Brotli decoder that can lead to remote code
execution, plus several high-severity DNS, TLS and URL-parsing fixes.
24.22.1 is the first patched 24.x release.
- Dockerfile: node:24.22.1-slim, digest re-pinned to the multi-arch index
- CI: 24.22.1 in the verify matrix and the other two jobs; the
allocation-gate requirement now keys on 24.22.1
- Docs: contributor setup and the dependency baseline name 24.22.1
The engines floor and the setup doctor are unchanged.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
At 900px and narrower the Location and Visual Presets wings shrink to
--dock-side-compact (4.25rem), open or closed, but their text labels kept
their full width. Between 721px and 900px, where the desktop dock is still
shown, LOCATION and VISUAL PRESETS spilled over the idle voice control, and
the LOCATION label took taps on the left half of the mic button. The label
clip that existed was scoped to 620px, which the phone shell (720px) always
covers, so it never applied to a visible dock.
In that band the labels are now font-size 0 and lose their chevrons, so
each wing shows only its icon. The label text is unchanged, so the toggles'
"Expand/Collapse ..." aria-label and title, which panelChrome builds from
it, keep the full name. The Cyber HUD layout sets a wider
--dock-side-compact that fits its labels, so it keeps them. The
unreachable 620px clip is removed.
Found in GEV launch-readiness QA (HAL-728).
Catalog: refresh/updates wording, received SDR aircraft, playing states,
move-start button, impersonal layer note, live-data descriptions,
arrival steps, retry timing, tracking spelling, military chip, dollar
amounts, label: value statuses, upstream source, globe map, stage
recovery, comparison signs as words, ellipses and earlier round-2 fixes.
Code: FIRMS cell cards use the plural entry for their count (FIRES
pruned), translated locator pin titles are isolated in RTL, legend
stops keep their sign, voice timeout minutes agree with the number,
CCTV badges are translated, first-run and referenced-item alignment.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Apply the accepted findings from the third three-model French review:
tool-name agreement (« Outil actif : {name} »), elision before ADS-B,
« GORGE AVAL », « CLASSIQUES », « EN SURVOL », one name for the VIIRS
daily overview, U+202F thousands separator, and wording fixes for the
scene editor, directions, radio tuner and status strings.
The CCTV HUD summary now shows its CAL badge, source kind and view band
through existing catalog entries, so French reads « CAL ESTIM. INITIALE ·
SRC AMONT · CONTEXTE RUE »; English output and unknown codes are
unchanged. presentation.js matches the pt branch's round-3 version byte
for byte, so the two branches merge cleanly.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Catalog fixes from the third review round: booster flight count
({count}回目の飛行), 晴れ for PARTLY CLOUDY, 配信元 for upstream,
部分的 for the PARTIAL feed state, または between provider names,
POS kept as a HUD code, product names in their own case (YouTube,
Facebook, Radio Browser, ChatGPT OAuth, Googleストリートビュー), Esc,
bracketed button names, en-dash ranges, 土砂ダム湖 for both lake
labels and smaller wording fixes. Guide: range rule and new glossary
rows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three-model review (astra, sol, fable), round 3: 17 of 18 findings
applied (some in own wording, one in part); "Missões espac." rejected.
- pt: layer cards «{label}: camada ativada/desativada», pointer and
Directions toasts name «A ferramenta "{owner}"», counters after a
colon where the count can be 1, traffic «sem dados de fluxo» and
«Fluxo: {percent}% cob», «limite de requisições atingido», FECHAR,
ANT., PREDEF. VISUAIS, quoted "Nível da rua", infinitive tooltips,
and more (see docs/translations/pt.md).
- CCTV summary line localizes its CAL, SRC and CONTEXT values through
existing catalog keys (all locales); codes stay underneath.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Catalog fixes from the second review round (squawk 7600/7700 names,
transit occupancy, radio category, cyclone advisory wording, Orbital Watch,
validation and hint texts), cockpit distances keep "km" in Japanese as in
Chinese, the CCTV context line uses the translated view band, and the
Japanese layer status line no longer splits "1分以内".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The trailing visibility frame uses the layer's injectable runtime timer and
is cancelled by resetState as well as disable and destroy.
- An empty-feed regular pass records the camera pose, so later in-cadence
frames do not keep scheduling trailing frames.
- New renderGovernor.test.mjs: a parked camera schedules nothing, a stopped
camera gets exactly one trailing frame and no self-wake, a forced pass
requests its frame, destroy cancels the timer.
- The CHANGELOG entry moves to changelog.d/871-vessels-idle-render.md; docs
say the hold also covers vessels dimmed under a live target.
stopScene now supersedes seeks through _bumpSceneSeekGeneration; the
bhoteKoshiLocator Stop test calls stopScene on a plain object, so it
needs the method too. Without it four tests throw at the PR head.
- Superseding a seek aborts only the seek's own layer reconcile, so a
loadShot that is later rejected (shot-not-found) no longer cancels an
in-flight LOAD. Stop still aborts any LOAD.
- A refused seek rolls back to each layer's effective intent, so a manual
toggle still in flight is kept.
- A cancelled seek leaves layers to whatever superseded it, as a cancelled
LOAD does; only refusal rolls back.
- Move the _seekLoadedShot JSDoc back onto its method.
- Meaning fixes: the radio link opens the broadcaster's website
(SENDER-WEBSITE), a straight lane is «geradeaus führend», not the middle
one, IDLE is INAKTIV (BEREIT is READY), the Gemini errors name the send
queue, traffic percent is live coverage (Live-Abdeckung), the voice radius
is measured from the Bildmitte, the AIS voice error names the Live-Schiffe
layer, and the Bhote landslide is «Erdrutsch» throughout (not «Abbruch»).
- Number-neutral status lines for feed and layer lists, and a payload count
line that also reads correctly for +1.
- Retry wording follows the glossary: «neue Versuche in größeren Abständen».
- Weather history lines are shorter and fit the collapsed row: «Frame fehlt;
letzter Stand bleibt erhalten», «Kein Frame in den {gap} min bis {time}»
(the gap only looks back from the target time).
- Shorter search placeholder «Ort suchen...», which was clipped.
- Smaller wording: Himmelsring, Lagebild-HUD, SCHLEPPVERBAND, Minimum Fuel
(Treibstoff knapp), Bloom / Leuchten, DOWNRANGE GESCHÄTZT, Places-Kontext,
MELDUNG, Rückflug zur Weltübersicht, REALE BILDDATEN and others.
- Glossary and routing notes updated.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The route proxy also refuses with 'routing busy — too many routes at once'
(429) and 'route proxy error'; both showed in English in every locale.
Mark them in ROUTING_REFUSALS and add es, ru, zh-CN and zh-Hant entries.
errorText is now cleared on a new connection, a mode switch and stop(), so
it never outlives the error it describes. Tests cover the two refusals,
'Routing failed: rate limited' (pattern order) and the stop() reset.
When several candidates fit a "place, region" query, take a settlement or
administrative area over a POI of the same name, so a cafe called "Paris"
in Lyon cannot win "Paris, France" from the nearby pass.
Tests: region queries without a camera bias, whole-word and every-word
region matching, city and county regions, the nearby whole-name answer when
the unbiased request fails, and the settlement preference. The new tests sit
with the other Paris, Texas tests. The station fixture uses its real city.
CURRENT-STATE now describes the order the code applies.
A Retry-After longer than the 10 s retry budget used to sleep the whole
budget out and then give up, so the proxy's 429 pause started 10 s late
and every new-point poll in that window still went upstream and logged.
The retry loop now stops at once in that case.
Tests: the retry loop no longer sleeps for an unreachable Retry-After;
fully cached points, including stale ones, are served during the pause
with no upstream call; a 503 does not pause; a shorter concurrent
Retry-After never cuts the pause short. CURRENT-STATE now says which
requests are still served and which log.
Three-model review (astra, sol, fable), round 2: 29 of 30 findings
applied, one of them in part.
- pt: FIRMS «mais recente há», CCTV «cruzamentos», degraded radio
directory, «{layers}: ativação recente» for any layer list, «Sem
imagem», NIVELADO, AJUSTA-SE, quoted Provider Settings, V/S kept on
the ADS-B card, no-break space in «(24 h)» / «(30 d)», and more
(see docs/translations/pt.md).
- Malformed cyclone snapshot is now a catalog message (all locales).
- Markup supports data-i18n-context; the radio STATION TAG option uses
"radio tag|All", so it no longer flips gender when the list loads.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three-model review (astra, sol, Fable), round 1:
- Meaning fixes: post-tropical cyclone, partly cloudy, upload-date basis,
ground track (対地針路), spec FOV, voice credentials, Activate → 押す,
bikeshare counts, NHC/CPHC empty state, absence caveat, regional-news
empty state, route steps (件の案内), minimum fuel, microphone access.
- Consistent terms: 範囲 for WINDOW, カバー範囲 for coverage, 封じ込め率
for fire containment, サイクロン for storms, 交通流 for TomTom flow,
シグナル tab, インポート / エクスポート for projects and presets,
射撃場 for the RANGE class, か所 for sites, full-width colons.
- Short forms where the re-shoot showed a wrap: エクスポート (Scenes),
提供元で操作 (embeds), 該当施設 (Contacts).
- cjk.css: the Recent Imagery slot line wraps at its separators in
Japanese, never inside プレビュー.
- Glossary in docs/translations/ja.md updated to match.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Findings from the astra, sol and fable reviews of the Arabic screenshots:
- Wording: forecast valid time is «عند», not an expiry; acres are آكر, not
feddans; live layers are «تتبّع … المباشر» (not "direct flights"); the
straight-ahead lane, turboprop, call sign, reconcile, proximity, retrying,
count agreement (colon form, fixed counts, ي for compact day ages) and
smaller fixes. Guide and glossary updated.
- Right to left: the scene editor's JSON fields are LTR; canvas map cards
draw with LTR geometry and an Arabic face; whiteboard callouts anchor
inside their cards; HDG/FOV keep their values; the cyclone timestamp no
longer splits its date; bare-number counters read 01 / 25, translated
ones say «من».
- Built-in shot titles of the Nepal shot pack's bootstrap shots localize
(the applied pack vouches for them), in every language.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three-model review (astra, sol, fable): 42 findings accepted, 3 deferred
(unmarked cyclone errors and the Chinese locale descriptors need new keys
in every catalog). The USGS callout now reads as an action (APRI LA FONTE)
because it opens the report. Layer, tool and card-title sentences agree with
any feature name («Il livello {layer} è attivo», «Lo strumento {name}»,
«{label}: attivato»). The store write error is grammatical for both
values. RADO/BILANCIATO agree with DENSO. Also fixes the meaning of
viewpoint, fire incident, trigger record, transit report, FIRMS cell age,
drizzle, widebody and fast-jet classes; Dissolvenza and CH replace the odd
abbreviations; cause/hint dashes become colons.
Fixes the Nepal scene showing shots 01-08 in English in every locale:
displayShotTitle() now also trusts the bootstrap keyframes of the append
recipe that upgrades a built-in scene (src/scenes/recipes.js, with a test).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Meaning fixes: playback connects to the broadcaster when it starts, browser
keys are restricted at the provider, clearing data layers switches them off
(ausschalten, not löschen), cockpit inputs are Quellen, Electronic is
Elektronische Musik, AIS types name vessels (SEGLER, LOTSENBOOT,
SANITÄTSDIENST, SCHNELLSCHIFF), contiguous US excludes Alaska/Hawaii,
wind and weather readings are model values, nearest frames are nearest in
time, cloud imagery and the brightness filter say what they are.
- NHC/CPHC advisories are Lageberichte, not Warnungen; cyclone strings use
Wirbelsturm throughout. Glossary updated.
- Grammar: " oder " keeps its space as a join separator, "{x} konnte nicht
sauber beendet werden", Then: no longer doubles Dann, scene save reasons
continue the sentence in lower case, neutral "Warten auf".
- Spacing between numbers and units in the remaining nine readouts.
- Reroute tooltip: German mode nouns keep their capital (Auto, Fahrrad).
- Built-in Nepal shot titles localize in every language: the legacy scene
upgrade rebuilds its base shots without sourcePackId, so the pack's own
shots in the scene now vouch for them.
- Layout: the layer status column no longer shrinks under a wide chip, and
a mixed-case compound (ALPR-Kameras) may break at its hyphen; only capital
codes (ADS-B, RTL-SDR) stay unbroken. WIRD AKTIVIERT overlapped the name.
The Bhote step meta line may take a third line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three-model review (astra, sol, fable): 51 of 52 findings applied, two
of them in part.
- pt: AQHI bands as health risk, NAVIO-TANQUE, CANDIDATO DO GOOGLE
PLACES, Provider Settings agreement, retry/feed-down wording,
NOTÍCIAS DA ÁREA (fits the briefing kicker), detection modes in the
masculine, GS and REC kept on the HUD, EXPORTAR PREDEF., prepositions
before times, and more (see docs/translations/pt.md).
- Cyclone request timeout is now a catalog message (all locales).
- Built-in Nepal base shots written by the legacy pack bootstrap now
show their localized titles: displayShotTitle also trusts the legacy
keyframes of an append pack the scene has applied.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Fix meaning errors flagged by the three-model review: downed aircraft
(Аварія літака), fast jet, the straight-ahead lane, road traffic,
pilot vessel, nearby contact counts, the low-accuracy score.
- Terms: CRS/TRK → ШК (шляховий кут), ascent → виведення, booster
flight → ПОЛІТ, session → сеанс, sequence → послідовність, place
search → пошук місця, CCTV pitch → нахил, mapped sites →
нанесені обʼєкти everywhere; glossary updated.
- Grammar: case agreement in the radio aria label and the key-store
message, «прицілу», у/в euphony, payload count after a colon,
FIRES as ПОЖ. so any count reads correctly.
- The cockpit current-style button widens to fit ЗВИЧАЙНИЙ; other
languages keep the 58px minimum.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three-model review (astra, sol, fable): 53 findings accepted, 14 rejected
or deferred. Fixes the meaning of "Downed aircraft", "Compilation", the
satellite ground-distance caveat and the browser-key restriction note;
agreement for Full, Provider Settings and Space Missions; fork directions
that compose with every modifier; a radio aria label that reads as a
phrase; imperative tooltips. Shortens the labels that clipped: ESP. A/B,
search placeholder, QUOTA · FT, PREC, Esri imagery note.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The voice context records for a selected flight or military contact
rounded the track without wrapping, so a contact on 359.5° or more was
described to the assistant as heading "360°". Both now wrap after
rounding: 359.6° reads "0°", 359.4° still reads "359°".
Apply the accepted findings from the three-model French review: ground
track reads « route », wildfire containment « circonscrit », downrange
« distance au sol », tankers « navire-citerne », plus wording fixes for
lanes, timestamps, wind sector, radio, CCTV and voice strings.
Shots rebuilt by a pack's legacy bootstrap carry no sourcePackId, so the
first Nepal shot titles stayed English; the scene's applied-pack marker
now vouches for the pack's required titles.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Layer rows read «не оновлювалося» for a feed that never updated, not
«ніколи».
- The dock voice toggle label fits on one line (ПЕРЕМК.), the cockpit
briefing title (НОВИНИ РЕГІОНУ) no longer truncates, Celestial is «Небо».
- No-break spaces keep «24 год», «30 дн» and coordinate abbreviations
(пн. ш., зх. д.) on one line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Global Context panel and its buttons set ui-monospace through the `font`
shorthand, so Arabic fell back to a fixed-pitch face with letters spaced
apart. In Arabic they use the Arabic-aware mono stack.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The mission orbit lookup reads each loaded satellite's launch year from its
international designator, but satellite.js never parses that field, so the
year check never ran and a new payload could take an older namesake's orbit
and position. The loaded catalog now keeps the designator from the TLE line.
- "never" in a layer row reads لم يُحدَّث (not updated), not أبدًا.
- Space Missions (30 يومًا) and Earthquakes (24 ساعة) keep their
parenthesis together with a no-break space instead of wrapping inside it.
- VS BASEMAP is مقابل الخريطة, short enough for its segment.
- The voice pointer label THIS is هذا; two leftover حي (live) become مباشر.
- Latin provider names in Street Level hints lose their «» quotes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A container that only holds a translate="no" span (a provider name in the
Global Context lines) kept its tracking and tore its own Arabic text into
separate glyphs. Only the Latin span itself keeps its letter-spacing now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Labels that ran out of their buttons: EXPORT / EXPORT A stay English
(EXPORTIEREN was cut to EXPORTI…), the imagery mode reads VGL. KARTE,
the scene run log PROTOKOLL (it broke as LAUFPROTOKOL/L), the dock voice
status SPRACHE N. V., and the Esri imagery note fits its line.
- The cyclone layer is Sturmwarnungen (no hyphenated break in the layer
list); Erdbeben (24 h) matches the other hour readouts; retry countdowns
get a space before s; the Mapillary hint names the Straßenebene layer.
- The cyclone source's timeout and malformed-snapshot errors were plain
English in every language: mark them like the wind source does and
translate them in de, es, ru, zh-CN and zh-Hant.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Shorter layer and segment labels that wrapped or broke mid-word in the
narrow panels: ALPRカメラ, 宇宙ミッション 30日, 地図と比較, 対象.
- Window readouts put the value after the label (空/海ウィンドウ 250 km),
so a unit never runs into Japanese text.
- 情勢図 for the GhostMaps intel picture; Googleストリートビュー joined like
any Latin product name in the guide.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
cjk.css now covers Japanese as well as Chinese: tight tracking, the 9px
floor on dock voice labels and layer buttons, whole travel-mode and preset
labels, catalog-case units in the HUD and the taller Recent Imagery cards.
Japanese also gets strict line breaking (no line starts with ー or a small
kana) and unbroken segment labels (すべて, 平面). Chinese rendering is
unchanged: :is(:lang(zh), :lang(ja)) keeps the :lang(zh) specificity.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The cockpit news kicker no longer cuts off: regional news reads «notícias
locais», the cycle button «CICLO DESLIG.», the subtitle «ÚLTIMAS NOTÍCIAS
DO LOCAL».
- Contact header fits in two lines («AR/MAR 250 KM · INSTALAÇÕES: ATÉ
100 KM»); ground speed reads KT like the rest of the HUD.
- A space between a number and s/h/d («nova tentativa em 30 s», «há 5 h»),
and «aproxime-se de uma rua» for zoom-to-street hints.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Shorter wording where the French text ran out of its button, chip or
collapsed card at 1600×1100: the DATA LAYERS launcher (COUCHES), Recent
Imagery buttons and hints (CETTE VUE, VS CARTE, EXP. A), the ENABLING /
DISABLING layer chip, the voice detail line and the location search
placeholder. Also the CCTV range readout gets a space before m, the radio
privacy note reads naturally, and the cockpit air/sea window is shorter.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From the screenshot review: the Recent Imagery buttons (SELEZIONA, USA
VISTA, VS MAPPA BASE) no longer clip or wrap, the layer state chip reads
AVVIO / ARRESTO so a layer name keeps two lines, the cockpit contact header
fits two lines (ARIA/MARE, SITI), and the Bhote Koshi play buttons fit on one
line (AVVIA). The post-event year follows the label, as the reference one does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fixes from the Arabic screenshot review:
- t() isolates every interpolated value in RTL locales (U+2068…U+2069), so
a time (10/28 09:00 UTC), a code or an English name keeps its order inside
an Arabic sentence; coordinates (30.326° ش) are isolated too. No-op in
left-to-right languages.
- The cockpit position readout reads right to left again now that its
coordinates are isolated; gauges, tapes and roll counters stay LTR.
- The start window's arrows point along the reading direction; FM seek and
the glyph-only mini transport stay left to right, the worded PREV / PLAY /
NEXT row follows the page; the Bhote Koshi clock stays a number.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The live flow status derived its wording from the rounded coverage
percent, so 1 matched dot among 300 rounded to 0% and read
"SIMULATED ... (no matches)", and 1 simulated dot among 300 rounded to
100% and dropped "Unmatched: simulated". Coverage now stays within 1-99%
whenever both matched and simulated dots are shown.
Placed route, arrow and drawn-line labels picked metres before rounding
to ten metres, so anything from 995 m to 1 km read "1000 m", even where
the draw hint for the same line read "1.0 km". The label now rounds
first, then picks the unit, and does the same at the 10 km cut, where
9,960 m read "10.0 km".
The draw hint picked m², ha or km² from the raw area and then rounded,
so 9,999.5 m² up to a hectare read "10000 m²" and about 999,500 m² up
to a square kilometre read "100.0 ha". It now rounds first, as the line
measure already does.
dump1090-fa and readsb file an aircraft's vertical rate under geom_rate
instead of baro_rate when its source is GNSS. The decoder-feed adapter
read only baro_rate, so those aircraft showed "V/S —" on the Local ADS-B
card. It now falls back to geom_rate; a sent baro_rate still wins.
Moving the replay-speed slider during the pre-launch hold or countdown
rescaled the time left to liftoff, so 0.25x turned T-8 into T-32 and 4x
into T-2. The speed change now skips a replay that has not lifted off;
mission time already flown is still preserved as before.
routeDistanceText picked one decimal while the raw kilometres were under
10, then rounded them for display, so 9.95 km or more read "10.0 km".
It now picks the unit from the rounded tenth, as it already does at the
metre/kilometre boundary.
The selected ALPR card rounded a mapped camera:direction without
wrapping, so a camera facing 359.5° or more read "DIRECTION 360°". The
rounded bearing now wraps to 0°.
TeleGeography stores 17 cables (2Africa, Echo, Hawaiki...) as several
features under one id, and the tool made a row per feature, so "Echo"
read "3 submarine cables" and area counts ran high. Rows are now unique
by cable id, and a cable still matches when any of its parts does.
Replay Ascent schedules liftoff 15 s out: a 5 s tile-preparation hold,
then a 10 s countdown. The hold ended 5 s before liftoff instead of 10,
so the replay sat on the pad for 10 s and the countdown began at T-5.
The hold now ends at T-10, and the countdown runs T-10 to T-1.
A station whose feed publishes no capacity froze its capacity at the
first status poll's bikes + docks, or at the placeholder 15 when that
poll had no row for it, so later cards read "10 avail · 20 docks · 15
cap" and coloured by the wrong ratio. Each poll now resolves it afresh.
The selected-vehicle card rounded the operator's reported bearing without
wrapping, so a GTFS-Realtime vehicle heading 359.5° or more read
"reported hdg 360°". The card now wraps after rounding: 359.6° reads
"reported hdg 0°", 359.4° still reads "reported hdg 359°".
The CCTV HUD heading token and the camera panel's meta line rounded the
bearing without wrapping, so a camera facing 359.5° or more (a feed
bearing, or a heading calibrated just west of north) read "HDG 360°".
Both now wrap after rounding: 359.6° reads "HDG 0°", 359.4° "HDG 359°".
- docs/translations/ar.md: style, Western digits and direction, units,
the six plural forms and the glossary.
- TRANSLATORS.md and CURRENT-STATE.md list Arabic and say how a
right-to-left language mirrors the chrome.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The title, style indicator and both rails swap sides in RTL through logical
properties (inset-inline-*, margin-inline-*, border-inline-start); LTR
computes the same values. The Display/Context rail keeps its physical
`right` for the credit-clearance model and swaps in rtl.css above 720px.
- rtl.css: no letter-spacing on Arabic (it tears joined letters apart);
Noto Sans Arabic leads the Arabic font stacks so words are not set in a
fixed-pitch fallback; the intel HUD and cockpit instruments stay a
left-to-right instrument overlay; media transport and the radio dial stay
left to right; feed headlines and station names take their own direction;
the moved rail's arrows flip.
- isolate(): a quantity in RTL text (2.7 كم, -18.0°) is wrapped in
first-strong isolates so its digits do not join a neighbouring Latin run.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Register fr: catalog in CATALOGS, LOCALES and LOCALE_NAMES (Français);
any fr-* browser tag (fr-CA, fr-BE, fr-CH) maps to it.
- package-boundaries.json lists fr.json next to the other catalogs.
- Picker and locale tests cover the new row and its local names.
- docs/translations/fr.md: style, typography, plurals and glossary;
TRANSLATORS.md and CURRENT-STATE.md list French as shipped.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Translate the remaining 1,865 strings: aviation, maritime (AIS), weather
and remote-sensing terms, radio, routing, scenes, voice and setup.
- Review the 862 entries from #907: LIVE becomes DIRECT / en direct, stale
becomes obsolète, FIRMS detections are feux actifs, replay is rejeu, the
AIS ship types use the French terms (PILOTINE, SERVITUDE, POLICE), keys
stay Space / Esc / Enter as docs/TRANSLATORS.md asks, and NEXT/PREVIOUS
chips match PREV (SUIV. / PRÉC.).
- French typography: narrow no-break space before : ; ? ! and %, inside
« », and … for ellipsis; plural entries get the many form (exact
millions, "de").
- same-as-english.json: codes, units, genre and product names that French
spells the same as English.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Register ja: CATALOGS, LOCALES, LOCALE_NAMES (日本語) and the package
boundaries; ja and ja-JP tags select it.
- Japanese unit words follow the number directly, as in the catalog:
formatQuantity writes 5分 and 1時間5分 (km and kt keep the space), and
formatRelativeTime drops the space CLDR puts in 5 分前.
- Tests: locale tags, picker order, Intl output, durations and the
ramp/exit/lane wording in Japanese.
- docs/translations/ja.md: style, units, plurals and glossary; ja added to
TRANSLATORS.md and CURRENT-STATE.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Translate the 1,875 strings #913 did not cover (2,727 in all), for a
tactical HUD: aviation, maritime, weather and imagery terms, counters
next to numbers (機, 隻, 基, 件), year-first dates.
- Review the contributor entries: full-width punctuation ():? and …,
no space between Japanese and Latin, one katakana form per term
(ブラウザー, ビューアー), and fixes where the meaning was off:
NO PLACE LEFT BEHIND, LEVEL (horizon), CURATED / RAW PRIOR (CCTV
calibration), STALE (更新遅延, not 古い), rocket stage (段), SAR vessels,
T minus, POWER UP.
- List the codes, units and product names that stay as in English.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Translate the remaining 1,895 strings for the current UI (Bhote Koshi
event, weather, imagery, street level, directions, SDR, voice, scenes,
import) and review meirm's 832 entries from #312: 134 edited for
Ukrainian HUD terms (КУРС/ПЕЛ./ШЛЯХ/ПРИЗН., Л/П, ОНЛАЙН for LIVE,
Cyrillic units КМ, ВУЗ, ФТ, МВт), vessel types, number agreement and
the ʼ apostrophe throughout.
- Register uk: catalog, LOCALES, LOCALE_NAMES (Українська), package
boundaries, same-as-English codes; picker and plural tests cover the
four Ukrainian forms.
- docs/translations/uk.md: style, plurals, units and glossary.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Add ar to LOCALES (picker name العربية) and RTL_LOCALES, so <html dir>
turns rtl; any ar-* tag maps to it.
- Intl formatters get ar-u-nu-latn: numbers and dates keep Western digits
like the catalog, whatever the browser's ICU picks for Arabic regions.
- HUD quantities keep a space before an Arabic unit (2069.0 كم), as in
Chinese, instead of running digits into the word.
- Codes and product names that stay Latin go to same-as-english.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Translate the 1,875 strings #915 did not cover (2,727 in all), with all
six CLDR plural forms where tn() asks for them.
- Review pass on the #915 entries: LIVE reads مباشر (not حي, "alive"),
STALE reads متقادم, ON/OFF pair as مفعّل/متوقف; distances and speeds use
Arabic units (كم، م، قدم، عقدة) instead of Latin KM/FT/KTS; HUD stays HUD;
quotes are «» and a few clumsy count phrasings are rebuilt.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Translate the remaining 1,868 strings in a neutral infinitive style (no
Sie/du), with German compounds, decimal commas and CLDR one/other plurals.
- Review the #908 entries: knots are kn everywhere, the avionics codes BRG,
CRS and DEST stay English like HDG and SPD, expand/collapse is
ausklappen/einklappen, the radio toggle reads EIN/AUS, density SPARSE is
DÜNN, the Thermal style is Wärmebild, German quotes in the shot prompt.
- Register de: catalog, LOCALES, LOCALE_NAMES (Deutsch), package boundaries,
same-as-english (codes, product names, music genres, units).
- Tests: picker order and keyboard model with Deutsch, de/de-AT/de_CH
normalization, browser de-DE resolves to German.
- docs/translations/de.md: style, plurals, units and glossary; TRANSLATORS.md
and CURRENT-STATE.md list German.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Translate the remaining 1,868 strings (pt-BR, as in #909) and review the
contributor entries: HUD codes BRG/SPD/RANGE stay as in the other
languages, sentence-case layer and scene names, Space key named as on the
keyboard, short PREV/NEXT/ON/OFF chips, and the CLDR many form (exact
millions take «de») on every plural.
- Register pt: any pt tag (pt-BR, pt-PT) maps to it; the picker shows
«Português (Brasil)».
- Codes, units and product names identical to English go to
same-as-english.json.
- docs/translations/pt.md: style, plurals, units and glossary.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Register it: catalog in index.js, LOCALES and LOCALE_NAMES (Italiano);
it-IT, it-CH and other it tags select it.
- List it.json with the other catalogs in package-boundaries.json.
- docs/translations/it.md: style, plurals and glossary; Italian in the
shipped lists of TRANSLATORS.md and CURRENT-STATE.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Translate the 1,868 strings #912 does not cover: directions, weather and
wind, cyclones, Recent Imagery, Street Level, GNSS, the Bhote Koshi
reconstruction, scenes and sharing, local SDR, voice and key setup.
- Add the Italian `many` plural form (exact millions) to every plural entry.
- Review the #912 entries: fix RIMORCHIATORE, POWER UP (ACCENDI), DENSO,
minutes as "min", sentence case for layer and scene names, keyboard keys
kept in English (Space, Enter), and the SIG/NOTIZIE/LOCALE tab names.
- List codes, product names and words Italian spells as English in
same-as-english.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The German locale by abiyeenzo (#908), converted from its namespaced JS
catalogs to the English-keyed de.json: 859 entries whose English still
matches the current UI.
The French locale by abiyeenzo (#907), converted from its namespaced JS
catalogs to the English-keyed fr.json: 862 entries whose English still
matches the current UI.
The Brazilian Portuguese locale by abiyeenzo (#909), converted from its
namespaced JS catalogs to the English-keyed pt.json: 859 entries whose
English still matches the current UI.
The Arabic locale by abiyeenzo (#915), converted from its namespaced JS
catalogs to the English-keyed ar.json: 852 entries whose English still
matches the current UI.
The Ukrainian locale by meirm (#312), converted from its namespaced JS
catalogs to the English-keyed uk.json: 832 entries whose English still
matches the current UI.
The Japanese locale by abiyeenzo (#913), converted from its namespaced JS
catalogs to the English-keyed ja.json: 852 entries whose English still
matches the current UI.
The Italian locale by abiyeenzo (#912), converted from its namespaced JS
catalogs to the English-keyed it.json: 859 entries whose English still
matches the current UI.
Correct 年曆史 → 年歷史, 下遊 → 下游, 並道 → 併道 and 發佈 → 發布,
and list these fixes for future reconversions from zh-CN.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rebuild the zh-Hant catalog from zh-CN with OpenCC s2tw glyph
conversion, so wording and terminology match zh-CN and only the
script differs. Drop Taiwan-specific vocabulary and wording notes;
zh-TW/HK/MO browser tags still select zh-Hant.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
use bundled place boundaries and shared OpenFreeMap tiles before a guarded Nominatim fallback
preserve multipart shapes, holes and cancellation,
and keep pending annotations visible while ground sampling finishes across frames
keep non-Latin name distinctions when selecting provider results,
and retain an outline anchor only when it lies in filled geometry
The /api/military-installations endpoint was the last keyless proxy without
the admitSameSite gate that protects terrain, CCTV, traffic, places, and
Celestrak. A cross-site <img>, foreign Origin, opaque origin, or
reverse-proxy-forwarded request now receives 403 before any Overpass query.
Same-origin app requests and bare tool requests continue to work.
Adds 4 integration tests (cross-site refused, same-origin admitted,
bare tool admitted, proxied refused) following the terrainHeightsBounds
test pattern.
readsb reports an aircraft's vertical rate as geom_rate instead of baro_rate
when its source is GNSS. The military normaliser read only baro_rate, so about
one in six live /v2/mil contacts had a null vertical rate in analyst and
tracked-aircraft data. Fall back to geom_rate, as the adsb.lol flights
fallback already does; a sent baro_rate still wins.
The on-globe Contacts markers picked metres before rounding, so a contact
999.5 m or more away read "1000 m" while the Contacts panel read
"1.0 km". The marker label now rounds first, as the panel does.
select_nearest_aircraft passes altitudeM: null for an airborne contact
without a barometric altitude (military rows from adsb.lol can lack
alt_baro). The speech builder coerced it with Number(), and Number(null)
is 0, so the reply said "..., 0 feet." and the voice card showed
"Altitude 0 feet". Read the altitude only when it is a finite number,
so an unknown one is left out as the builder already intends.
Vessel cards and the AIS HUD rounded the direction without wrapping, so
a vessel reporting no heading and a course over ground of 359.5° or more
read "360°" on its cards and "HDG: 360DEG" in the HUD. The rounded
direction now wraps to 0°.
The Street Level viewer caption rounded the photo's bearing without
wrapping, so a view facing 359.5° or more read "360°". The rounded
bearing now wraps to 0°.
Main now listens to camera.moveStart in IntelHUD's constructor (#1384), so
the fake camera in src/hudPageVisibility.test.mjs needs that event too, as
hudAltitudeDatum.test.mjs already has. No change to the fix itself.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The style shaders and visualPresets imported msg() from src/i18n/index.js,
which loads every locale catalog. The share-view codec reads the styles'
defaults, so a server bundling the codec carried about 0.9 MB of
translations it never uses. They now import the catalog-free marker, as
other portable modules do; the codec's bundle drops from about 1 MB to
130 KB.
A location search that found nothing or failed now keeps its query, the open field and focus (text selected), and shows a persistent role=alert message under the search row instead of a toast. Typing, a new search or closing the emptied field clears it. HAL-727.
* feat(share): export the share-view codec for host servers
Add `@gods-eye-view/core/share/view-codec` (src/share/viewCodec.js) as a
package export with its own boundary group. The codec is pure (no DOM, no
Cesium, no external dependencies), so a host's server can decode a
/share/<code> link, for example to describe the view in link previews.
* feat(share): export the share-link address helpers
Add `@gods-eye-view/core/share/link-path` (src/share/shareLinkPath.js), so a
host's server reads /share/<code> addresses the way the app does.
* feat(share): add compact versioned share-view codec
Add a standalone codec for `#s=<base64url>` share links: a version byte
followed by protobuf-style tag-length-value fields. Settings equal to
their default are omitted and refilled from the same table on decode;
unknown fields are skipped by wire type, so new fields never break
older apps. Views are a place (camera) or a thing (an aircraft,
military aircraft, vessel or satellite to follow, with camera offset
and last-known fix); an unknown kind falls back to the camera.
The decoder bounds its input and rejects malformed payloads with a
reason instead of throwing. A converter maps the long form's parsed
state to and from the codec state; the long form is unchanged.
* fix(share): tighten codec decoding after review
Unknown detection mode and allocation names now fall back to their
defaults instead of reaching the lenient long-form migration. Future
panel values and style-parameter tokens are skipped rather than applied
or rejected, and layer options are accepted only under their owner
layer. Wrap longitudes before scaling, normalize negative zero, reuse
the layer-state tracking ID grammar, and align the format doc with the
decoder.
* feat(share): let a host share the link its own way
ShareLinkManager.buildLink() returns the current share link without
copying it, and copyLink() now copies exactly that. The share button
passes the link to an optional services.shareLink(url), which resolves
to 'shared', 'copied', 'cancelled' or false; 'cancelled' shows no toast
and 'shared' shows the new "Link shared" (es, ru, zh-CN). Without the
service the button copies as before.
* feat(share): open and make short /share/<code> links
The share button now makes `<app>/share/<code>` links with the compact
share-view code, and the app opens them at start-up through the same
restore pipeline as long links. Once the app next writes the address, the
share path is replaced (no new history entry) by the app's own address and
the live long-form hash, so a reload follows the live view. A host can pass
the code as `initialShareCode` instead. A code that fails to decode restores
nothing and shows "This share link can't be opened".
The code gains a trailing CRC-8 check byte, so any single mistyped
character is rejected instead of opening a different view. Thing views no
longer store a camera: it is derived from the entity's last-known fix and
the follow offset on a sphere, the entity kind is implied by the id's
field, and the followed entity's layer bit is implied; an aircraft followed
from the app's own camera is a 42-character code. The `#s=` hash form is
gone.
While the camera follows an aircraft, military aircraft or satellite, the
link is a thing view with the camera's offset and the entity's position.
Opening one turns the layer on and follows the entity through the existing
shared-follow restore, then swings the camera round to the shared offset; a
vessel view selects the vessel once the vessel feed has it. If the entity
is not reporting within its window, the view stays at its last-known fix
and says so. The panel and style-parameter tables now come from the codec,
and a host share hook that returns a plain value or throws still gets a
toast.
* fix(share): encode style settings against the style's own look
Settings a style applies when selected (bloom, sharpen, HUD, detection,
celestial ring and the style's shader parameters) now default, per field,
to what that style applies in a fresh session, read from the tables the
app applies (the first-load baseline, the style presets and the shader
defaults). The encoder compares against the view's style and the decoder
fills omitted settings from the decoded style, so a view that keeps its
style's look costs only its camera and style. The previous defaults did not
match a fresh session (sharpen, HUD and detection were off), so every link
paid for them, and a style's own shader parameters were always written.
A test pins the per-style defaults as frozen v1 meanings. A followed
aircraft in Noir is now 44 characters and the real long-link example 39.
* docs: changelog for #1418
* fix(layers): make Clear All clear the saved and shared layer selection
After Clear All every layer control was off, but the share URL could keep
listing layers (for example l=e.f) and a reload switched them back on.
The URL and local preferences are written from the committed layer
state, which only explicit visibility changes update. Clear All issued an
explicit OFF only for layers it found on at the start of the batch and
whose turn was not superseded. A layer already off for a passive reason
(isolated by Contacts or another context mode, or released by Contacts'
own teardown while the batch ran) never received one, so it stayed in the
committed state. Clear All also discards the context snapshot that would
have restored those layers, leaving the screen and the URL out of step.
Clear All now publishes the set of layers still on once its batch
settles, and the layer-state coordinator commits that selection: every
saved layer not in it is dropped and its passive restore is cancelled,
while a layer a newer explicit request turned back on is kept. A context
restore interrupted by Clear All no longer brings its snapshot back after
Contacts settles.
* docs: changelog for #1417
* fix(share): restore a share link opened by hash navigation
Opening a different share link in an already-open tab (pasting it into
the address bar, following a link, or Back/Forward) changed the URL but
left the camera, visual style, map, panels and layers on the old view
until a reload.
The share manager now listens for hashchange and popstate. A new hash
that parses as a share and differs from the last hash the app read or
wrote runs the start-up restore pipeline: lane authority is reserved, URL
writes are held (including a pending debounced write of the old view),
the camera restore takes a new navigation generation, and the layer
coordinator replaces any in-flight layer restore with the new share's
layers through the passive share origin. A newer navigation supersedes an
older restore end to end; only the newest one releases URL writes. The
app's own replaceState writes never restore. Listeners are removed on
destroy.
Returning to the Normal style and resetting an already-active preset now
also apply when a share is restored after start-up, so the result matches
a fresh load of the same link.
* docs: changelog for #1416
Add the shared Voice Preview and Settings panel with browser-local preferences, inactivity controls and visible cost warnings.
Contribution by @manjunath22466.
Once the TomTom tile budget is spent, or TomTom fails, the proxy serves
cached tiles marked STALE-BUDGET / STALE-ERROR. flowSource works out
`stale`, but the layer fetched flow through fetchFlowForBounds, which
returns only the segments, so getStats() and the sync chip still read
"LIVE · Roads: …". #1239 fixed the get_traffic_flow tool the same way; the
layer now uses fetchFlowDetail too, reports stale and oldestTileAt in its
stats, and the chip reads "STALE · Roads: <source> · Flow: cached TomTom
tiles". A source without fetchFlowDetail keeps the previous behaviour.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
When WFIGS was unreachable the proxy served its last snapshot, marked stale
(X-Data-Stale, body.stale), with no age limit, and the client source kept
only the rows. The layer then read healthy with a fresh update time and
get_fire_perimeters reported the perimeters as current; three days into an
outage it still said "1 mapped wildfire perimeter in the requested box."
The source now offers getSnapshotDetail ({rows, stale, fetchedAt}); the
layer reports stale with the snapshot's fetch time, the tool adds stale,
retrieved_at and "(data may be stale)", and the proxy serves a stale
snapshot for at most 24 hours, as the GNSS proxy caps its own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* chore: remove four modules nothing in the app uses
src/app/sourceSlot.js re-exported src/sources/sourceSlot.js, which every
caller imports directly. src/data/flowTiles.js and src/data/issPass.js were
reached only by their own tests. src/data/localGeojson.js wrapped
localGeojsonCore.js with the app's services, which the catalog already
passes itself. The local GeoJSON layer tests now build layers the way the
catalog does, so they keep covering localGeojsonCore.js; only the test of
the wrapper's own default host goes. Boundary lists, format scope and docs
drop the removed paths.
* docs: changelog for #1403
Review follow-ups: the upstream error now carries its HTTP status and the
pause checks error.status instead of the message text; concurrent 429s
extend the pause with Math.max rather than a shorter Retry-After cutting a
longer one short; the per-chunk re-check says it is for multi-chunk
requests; the first 429 test runs its backoff on mocked timers.
CURRENT-STATE describes the pause.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A query with a comma counted as "place, region", so a nearby place whose
own name has a comma ("Bed, Bath & Beyond") no longer won the nearby pass
and the unbiased pass could fly to a namesake far away. Its whole-name
match is now kept as the last choice, after the region checks of both
passes. CURRENT-STATE describes the place-region rule.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A name that only started with the place word also matched, so from Lyon
"Paris, France" picked "Paris Gare de Lyon" in the biased pass, where main
fell through to the unbiased answer and reached Paris. Only an exact name
matches now. Adds a test for the UK alias.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A rate-limited upstream got asked again by every page request for new
points (about once a second while the camera moves), each with its own
retries, which kept the limit in force and logged "refresh incomplete" each
time. After a 429 the proxy now leaves the upstream alone for its
Retry-After (at most 10 minutes), or 60 s when it sends none, serving
cached points meanwhile. The warning that starts the pause says
"(HTTP 429; pausing upstream for N s)"; requests refused during the pause
don't log.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From Denver, 'Paris, Texas' flew to a pub in Budapest at rooftop range. The
camera-biased Photon answer listed the town first, but the pub's name equals
the whole query, and the biased path matched names only. A query with a
region now takes a candidate named for the place whose city, county, state,
country or country code (or USA/UK for US/GB) contains every word of the
region, in both the biased and the unbiased pass; with no such candidate
the existing name rules decide. A query with no region after its comma
keeps the biased name match.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
get_active_fires joined FIRMS acq_date and the unpadded acq_time, so a
detection at 00:45 UTC came back as "2026-01-01 45". The row now uses
acquisitionMsUtc, which pads the time, and reports an ISO UTC timestamp
like the other tool times.
Compare declared visibility with the manager's effective intent, and treat
in-flight transitions as stale so a toggle or superseded LOAD rollback that
is still settling cannot slip past the check. Reconcile only those layers
inside the lightweight seek instead of rebuilding the shot, and report a
refusal without resetting the seek position or camera.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Document visual evidence, category and data licensing requirements for new layers, using the changelog fragment workflow.
Contribution by @asturksever.
formatCockpitWindDirection rounded the bearing and printed that number, so wind from 359.5° or more read "N · 360°". Take the rounded degrees modulo 360 so north reads 0°.
formatMeasure checked the raw length against 1000 but printed it rounded, so a drawn line of 999.5 m or more read "1000 m" in the hint. Compare the rounded metres so it moves to kilometres.
Discard stale HUD summaries when camera movement starts and allow cancelled requests to retry for unchanged place context.
Contribution by @quanticsoul4772.
The Contacts panel checked the raw meters against 1000 but printed them rounded, so 999.5 m or more read "1000 m". Compare the rounded meters so the distance moves to kilometres.
routeDistanceText checked the raw meters against 1000 but printed them rounded to tens, so 995 m or more read "1000 m". Compare the rounded value so it moves to kilometres.
routeDurationText checked the raw seconds against 60 but printed them rounded, so a leg of 59.5 s or more read "60 s". Compare the rounded seconds so it moves to minutes.
Report stale traffic tiles and their fetch time, and release pending freshness metadata when tile requests fail or are cancelled.
Contribution by @quanticsoul4772.
Support spaced hemisphere coordinates while rejecting malformed numeric fragments and ambiguous component assignments. Include regression tests and runtime documentation.
Contribution by @rudycelekli.
Main now has #1282, which pauses periodic layer polling in a hidden page
with its own catch-up and listener cleanup, so this PR drops its duplicate
lifecycle.js change and test in favor of main's.
Review fixes:
- data.js passes panelAwareVisibility(document) (new in app/embed.js) as
the lifecycle's visibility target, so an inline panel whose host reports
it hidden keeps polling. The HUD's 250 ms readout follows the same
document.hidden && !isEmbeddedInline() rule as tools.js.
- Tests: embed.test.mjs (layers poll in an inline panel reported hidden,
pause without the flag) and hudPageVisibility.test.mjs (HUD readout).
Both use mock timers and clean up in finally.
- changelog.d/1344-inline-panel-visibility.md and a note in
docs/CURRENT-STATE.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
add the shared Voice Preview and Settings panel with browser-local preview
and inactivity preferences, keyboard controls and responsive positioning
preserve main’s Rotation and Context behavior, clarify panel routing and
refresh schema pins within the unchanged prompt limit
A hidden tab shows nobody the result, so polled layers (flights, quakes,
weather...) skip their refresh while the page is hidden and catch up with
one refresh when it is shown again. The lifecycle takes an optional
pageVisibility service ({ isHidden, subscribe }) so src/data/lifecycle.js
stays free of browser globals; src/app/data.js supplies the document-backed
one. Without the service, refreshes run as before. Every place that stopped
a layer's interval now goes through one helper that also drops the
catch-up listener.
The HUD's 250 ms camera readout also skips hidden tabs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Add a complete Taiwan Traditional Chinese catalog, locale selection,
Chinese readout formatting, translation notes and regression coverage.
Seed translations from #724 and #652 and adapt the zh-CN catalog.
Co-authored-by: caxer <[email protected]>
Co-authored-by: tbdavid2019 <[email protected]>
Co-authored-by: Enzo Abyie <[email protected]>
Co-authored-by: EDIT-Aeterna <[email protected]>
Once the cache was full, every write sorted all 50,000 entries to find the
oldest. A Map keeps insertion order, so keep the map in fetch-time order
and delete from the front: the resolver re-inserts each point it refetches
(an overwrite would leave it in its old place), and the disk load sorts
once, since a file written before this change, or a migrated raw-batch
cache, is not in age order.
Suggested by asturksever on #975.
A stale point that is refetched becomes the newest entry, and the cache
file's order is not its age order. The ceiling must still evict the
oldest fetch. Passes on the current sort-based eviction.
When the model upstream fails, the wind proxy keeps serving its last
run, however old, with manifest.stale set. The wind layer shows that,
but get_wind ignored it and gave the old run's wind as current.
get_wind now returns stale and adds "(forecast may be stale)" to its
summary, the way get_weather and the space tools flag stale data.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Seven tests built terrainHeightsProxy() with no cachePath, so the proxy fell
back to this checkout's .gev-cache/terrain-heights.json. A point the app had
already cached there answered a request the test expected to reach the
stubbed upstream: after running the dev server near Austin, the cross-site
test failed with 1 !== 2.
isolatedCachePath(t) gives each test a mkdtemp cache file removed in
t.after, as the over-large cache test already did; that test now uses it too.
Fixes#1242.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BNTtHAa65nobzJ7jp2Ltr4
Four open PRs prepended their notes at the top of CURRENT-STATE.md, so
merging any one of them made the others conflict. Place this note in its
own section instead.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The globe renders in the viewer's own browser, so a codespace does not
remove the need for local graphics. Retitle Path 4 accordingly.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
runError doubled as the failure flag, so a phase that threw a falsy
value was still masked by a final release error (ianblenke's nit).
Track failure with a boolean and cover it with a test. Make the
changelog fragment a wrapped bullet like the other fragments.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Reject non-numeric and out-of-range feed coordinates before they reach the
map or bike-share queries. Preserve numeric strings, aliases and zero.
Cover malformed records at the parser and public query boundaries.
Signed-off-by: Bortlesboat <[email protected]>
The card's fire pattern is written by hand against the producers' template,
so a format change upstream would silently show English. Both locale tests
now pass the label the layer actually produced through shownEntityLabel()
under ru, and set the locale inside the try that resets it.
The fire and camera labels in the context store, getStrongestFire() and
getSelectedInfo() were translated at the source, so the voice model got
"Пожар · FRP 12 МВт" or "Cámara ALPR" in track_entity,
get_entity_context and pointer_context while every other entity name in
those payloads is English. Keep them English, as before the interface
was translated; the voice card translates them where it shows them.
The pointer chip, its live-region line and the get_entity_context card
title now translate the English entity labels the FIRMS and ALPR layers
register ("Fire · FRP {frp} MW", "ALPR camera"). Other names, such as
callsigns and place names, show as they came.
summarizeAwarenessCohort() translated `reason`, which is retained in the
awareness context snapshot. Mark the four reasons with msg() instead; the
awareness panel already shows them through translateKnown().
The Local ADS-B layer copied the SDR controller's `message`, which is
Radio card text in the UI language, into its `error` stat. The controller
now publishes an English `errorText` next to the translated `message` for
every receiver error, and the layer reports that. A receiver is any
object with getState(); one that does not publish errorText still shows
its error from `message`, as before. The Radio card text is unchanged,
and the Layers panel still shows the error translated.
The Directions row errors (pointer blocked, no route, routing timed out,
no response from the routing service) and the route request failures
("Routing failed: {error}", "{error} — try again in a moment",
"Routing unavailable (HTTP {status})", the rate-limit and fallback
texts) were translated inside getStats(), so the layer snapshot that
voice tools and the analyst read carried Spanish or Russian text.
Fixed texts are marked with msg() and the interpolated ones are built
in English. The Layers panel translates them where it shows them,
through a ROUTING_ERROR_PATTERNS block in statusText.js that the
proxy's refusal texts move into. English reads as before. In other
locales two things differ slightly: a proxy text inside "Routing
failed: X" that is not one of the known refusals is now looked up in
the catalog instead of shown as given, and the "— try again in a
moment" shape is recognised on every layer's error, not only on
Directions.
Associate expanded cable and landing entities through a copied source index,
retain one reference stem per feature, and cover duplicate IDs, bundled
segments and frozen provider inputs with real Cesium regressions.
Node's network-family auto-selection gives each resolved address a short
attempt timeout. On hosts where IPv6 is unreachable, a slow IPv4 handshake
is abandoned for the IPv6 address, which fails at once, and the request
reports only `fetch failed`. Retry such failures once with a single IPv4
connection within the same deadline, and log the per-address connection
errors (never the keyed URL). Node 26 wraps the AggregateError one level
deeper, so both shapes are recognised.
Fixes#68
Closesbilawalsidhu/gods-eye-view#155. The first Italian cameras: the 13
webcams along the A22 from the Brenner Pass to Modena.
The camera list is one keyless request to the Open Data Hub (NOI Techpark)
tourism API, which republishes Autobrennero's own list with coordinates
and kilometre markers. Frames are the operator's stills, refreshed about
once a minute, and only each camera's own WebCamImg/km<N>.jpg is
registered. The catalog fetch refuses redirects and is size-capped.
The road climbs from ~20 m in the Po valley to ~1,370 m at the Brenner
Pass, so ground elevation comes from a per-kilometre table sampled from
the Copernicus GLO-90 DEM instead of one flat prior. Headings use the
id-hash fallback at low confidence. CCTV_A22_ENABLED=0 disables the pack.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vessels move only when a poll lands, yet enabling the layer held the
continuous render loop for its lifetime, so a parked vessels-only scene
rendered every frame. The layer now:
- requests a frame for every forced visibility pass (new data, selection);
- leaves one trailing frame when the camera moved inside the 800 ms
visibility cadence, so the view the camera stopped on is evaluated. It is
keyed to camera motion alone: the pass's own label publication requests a
frame, and scheduling on that looped at ~1.2 Hz;
- holds 'ais-vessels' only while focus emphasis is animating.
The 80 ms focus pass, which runs while anything is tracked, also skips hidden
sprites whose focus state is at rest (new spriteFocusAtRest) and whose alpha
is already 1: advancing them changes nothing, and with a 12k worldwide feed
they are most of the fleet.
Measured: a parked vessels-only scene (headless SwiftShader) went from
continuous mode and 28 frames in 5 s to idle and 0; horizon visibility had no
mismatches after the camera moved and settled. The focus pass over 12,000
vessels (11,353 hidden) went from ~4.0 ms to ~1.2 ms.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
and Yemen products and the newest incident drop) from
github.com/s2underground/GhostMaps, with one chip per product and report
cards on click. `/api/ghostmaps` watches the repository's branch head every
15 s and pushes each new commit to open pages, which reload only the
products that changed (Black Pine Defense).
- Clear a temporary GhostMaps upstream error and notify open pages when the
repository becomes reachable again, including when no new edition was published.
- Release packed-image resources when a load is cancelled or fails, respect
injected or omitted GhostMaps sources, and wait for background cache warm-up
before removing provider test fixtures.
- Include Simplified Chinese translations and keep the combined voice prompt
within its existing size limit while retaining all spoken aliases.
- A Street Level provider whose `zoomFor` still answers `{zoom, kind}`, the
shape before the core went slim, draws its lines again instead of a silent
blank; an overview level or any answer that is not a whole zoom shows the
zoom-in hint (asturksever, #1288).
- Give ordinary sliders a shared native control and consistent track, thumb,
value, focus and disabled styling while preserving feature behavior and
specialized controls (mazy1998, #1287).
- The Street Level SINCE note sits on the value's line, in its type and a
size smaller: with KartaView lit it reads ANY DATE (by upload date). It
had lost its size variable in the Street Level slim-down and rendered
larger than the value, on a line of its own (asturksever, #1286).
- Keep aircraft route labels on the scheduled airport leg instead of accepting
its distant great-circle extensions, preserving generous endpoint tolerance
across the signed-angle branch cut (rudycelekli, #1284).
- Report cached CelesTrak satellite records as stale in the Data Layers row,
preserving separate core and dense catalog provenance through refresh,
removal and lifecycle cleanup (rudycelekli, #1283).
- Pause periodic network updates in hidden tabs; the overdue update on return restarts the polling schedule, so the next poll follows one interval later even when that update fails (asturksever, #1282).
- Report and guard browser visibility during Transit QA so hidden-page polling pauses cannot masquerade as playback regressions (asturksever, #1282).
- Require each live city to supply its own feed vehicles in Transit QA; retained vehicles cannot qualify another city (asturksever, #1282).
- Bound the Transit card-anchor probe by observed poll coverage instead of a fixed 20-second window (asturksever, #1282).
- Keep visible-page Transit probes focused through the browser testing API (asturksever, #1282).
- Add a real-browser regression gate for hidden-tab periodic work, explicit refresh, resume and teardown (asturksever, #1282).
- Restore minimized test windows before screenshots so native compositor pauses cannot stall browser acceptance (asturksever, #1282).
- Hold rendering only for animated content (asturksever, #1281).
- Exercise current vector road sources and published camera bounds in Traffic fixture QA (asturksever, #1281).
- Establish browser-level foreground visibility for Traffic QA and reject hidden-page samples (asturksever, #1281).
- Supply CORS headers on cross-origin traffic fixtures and report browser exceptions without disabling browser security (asturksever, #1281).
- Isolate the Traffic keyless browser scenario from the keyed page’s workers and intercepted cache (asturksever, #1281).
- Restore minimized test windows before screenshots so native compositor pauses cannot stall browser acceptance (asturksever, #1281).
- Intercept only Traffic provider URLs, leaving Cesium worker and module loading untouched (asturksever, #1281).
- Settle an unfinished Traffic height correction when the last animated point leaves, so the layer stops reporting loading once its render hold is released (asturksever, #1281).
- Throttle mission hover picks with a trailing check for the last pointer position, and clear stale cursors (asturksever, #1279).
- Reserve singleflight before disk cache reads (asturksever, #1278).
- Bound each height request to one upstream chunk (asturksever, #1276).
- Preserve enable during first-load replacement (asturksever, #1275).
- Release disabled weather camera listeners and make teardown resource leaks fail the browser probe (asturksever, #1275).
- Keep visible-page weather teardown probes focused through the browser testing API (asturksever, #1275).
- Scan the kml root delimiter in bounded time (asturksever, #1274).
- Retire unavailable history and keep local trails (asturksever, #1273).
- Normalize geocoder bias across the antimeridian (asturksever, #1272).
- Reject inherited style keys during restore (asturksever, #1271).
- Respect provider cooldowns and bound tle groups (asturksever, #1270).
- Coalesce refreshes and cool down opensky failures (asturksever, #1269).
- Normalize station tags and retain valid mixed rows; skipped malformed rows still report degraded coverage (asturksever, #1268).
- Correct Radio QA push-to-talk setup to target the mounted voice controller (asturksever, #1268).
- Honor Docker Compose trusted-peer and allowed-host settings from the shell
or project `.env`, and let `GEV_KEY_SETUP_TRUSTED_PEERS=none` disable gateway
access to Provider Settings before Vite starts (asturksever, #1265).
- Prevent malformed Overpass filters from blocking the local server, and reject
cross-site or excessive query requests before reading their bodies
(asturksever, #1264; fixes #1248).
- Release unread responses from the optional Google-token probe so keyless
browser validation can settle; the Overpass gate recognizes only that probe's
expected 404 as an unavailable capability (asturksever, #1264).
- Make tracking arrival checks respect the aircraft update cadence while checking
orientation on the first reveal, avoiding false failures at faster render
rates (asturksever, #1264).
- The terrain-heights bounds tests no longer read the checkout's
`.gev-cache/terrain-heights.json`. Each test gets a cache file of its own, so
`npm test` no longer fails after the dev server has cached terrain near
Austin (KnottyDyes, #1244).
- Replace the blocky JPEG sky around the globe with the 9,096 stars of the
Yale Bright Star Catalogue, drawn as sharp points where they are at the
scene time, over a soft Milky Way from ESO/S. Brunier (CC BY 4.0). The Sun
and Moon stay; the anime style hides the Milky Way. Assets: 296 kB (gekh,
#1243).
-`get_traffic_flow` flags stale cached TomTom tiles and reports the oldest
tile fetch time (`stale`, `oldest_tile_at`), with pending freshness metadata
released on failed or cancelled tile reads (quanticsoul4772, #1239).
- Add Taiwan's national freeway cameras from the Freeway Bureau, MOTC to CCTV:
~1,800 cameras read at runtime from the official MOTC 2.0 open-data table,
with on-demand snapshots and attribution. No API key is required; set
`CCTV_TAIWAN_ENABLED=0` to skip it (419vive, #1237).
- Require a geocoded containment anchor for ambiguous analyst region names,
preserve boundary provenance and cancellation safety, and retain exact-name
precedence for natural outlines (manjunath22466, #1235).
- Serve the camera catalog while it refreshes. An expired catalog made every
camera request wait for the slowest pack to reload; it is now answered at
once while one background refresh runs, so only the first load waits. Estonia's
slow Tarktee feeds get 30 s instead of 15 s, so the pack no longer drops out
on about a third of loads (quanticsoul4772, #1234).
- Harden the Tarktee (Estonia) DATEX2 camera catalog fetches: refuse redirects,
cancel both response streams when either response fails or overflows the 4 MB
byte cap, and deduplicate camera IDs before per-pack prioritization
(yaka0007, #1233).
- Skip bike-share stations with missing, blank or invalid coordinates instead
of placing them at coerced zero coordinates or accepting out-of-range values.
Explicit zero coordinates and numeric strings remain supported (Bortlesboat, #1232).
- OpenSky sign-in matches what OpenSky accepts since 2026-03-18. The server
now reads `OPENSKY_CREDENTIALS_FILE` (the `credentials.json` OpenSky's
account page downloads) on every platform, not only through the macOS
launcher, and no longer sends retired username/password Basic auth:
`OPENSKY_AUTH_MODE=basic` loads anonymously with one warning saying what to
set instead (quanticsoul4772, #1229).
- Credential-file warnings omit parser excerpts and filesystem error details;
the path example also works with the macOS launchers.
- Load Ontario 511 cameras again. 511on.ca's camera endpoint now requires a
free developer key; set `ONTARIO_511_API_KEY` to enable the pack, which is
otherwise skipped with one log line saying where to get a key
(quanticsoul4772, #1228).
- The first-run card can be closed on a phone or tablet. "ESC to dismiss" is
now a button that closes the card the same way ESC does; touch screens read
DISMISS, and phones no longer hide it, so choosing a mission is not the only
way out (KnottyDyes, #1226).
- Add continuous globe rotation with direction and speed controls and a globe-fit
check before starting (manjunath22466, #1225).
- Open God's Eye View from Pinokio in the system browser using the supported
`popout` menu option. The application's framing policy is unchanged
(NukeThemAII, #1222).
- Avoid starting or queuing a reply when the local channel rejects a typed OpenAI
command, while preserving an earlier accepted deferred input
(Manjunath, #1220).
- Keep NASA FIRMS fire and ALPR camera labels English in voice tool results
(`track_entity`, `get_entity_context`, pointer context); the voice card still
shows them in the interface language (daikaginza, #1207).
- Report actual outcomes when voice enables or disables CCTV through the shared
layer lifecycle, including failed and cancelled changes (manjunath22466,
#1205).
- Draw CCTV stills at their source aspect on the monitor plane instead of
stretching them to the 16:9 canvas. Most DOT cameras publish 4:3 frames, so
every still was widened; they are now centered and pillarboxed on the dark
the placeholder already ends on (Danielslay86, #1201).
- Color CCTV cameras with a video feed violet. Cameras with an HLS, MP4 or
WebM feed were the same cyan as stills, so a DelDOT or Statens vegvesen
video camera could only be found by opening it. Stills stay cyan and the
active camera stays amber, and focus de-emphasis fades the violet the same
way (Danielslay86, #1200).
- Keep Space Missions marked STALE when Launch Library 2 falls back to cached
launch data, and return to NOMINAL after a fresh response without discarding
retained missions (rudycelekli, #1199).
- Share Launch Library request cache keys across installations refreshing in the
same UTC minute, identify the app to the provider, and explain that standard
space-missions access needs no key. Optional higher-rate LL2 tokens are paid
upgrades; existing local cache and server-only token handling are unchanged
(rudycelekli, #1197).
- Add nine Spanish city bike systems to Bike Share and `get_bike_share`: BiciMAD
(Madrid), Valenbisi (Valencia), Bizi Zaragoza, Sevici (Seville), BIKI
(Valladolid), BiciCoruña, Dbizi (Donostia-San Sebastián), Bilbao Bizi and
BicinRivas, about 1,840 stations. Station names read underscores as word
breaks, and station capacity counts docks rather than PBSC's geofenced area
(ulihhhh, #1190).
- Keep missing launch-pad coordinates unknown so launches use the location
fallback or stay off the map instead of appearing at 0°N 0°E. Missing stage
flight numbers no longer show "FLIGHT 0"
(Rudy Celekli, @rudycelekli, #1186).
- Flights' ground floor works across the antimeridian. The viewer-distance
check behind the 150 km floor radius and its copy in the photoreal mesh-floor
sampler subtracted longitudes without wrapping, so two points 21 km apart
across 180° measured about 38,000 km and low or grounded aircraft just across
the line from the viewer were never floored. A contact's display corridor
walked a segment across 180° the long way round, spending its cell budget on
ground behind the contact, and a contact on the seam lost its cell hysteresis.
SEARCH NEARBY SITES on a view across 180° searched Google Places around
longitude 0, on the far side of the globe, with a radius sized from the view's
height alone; it now searches the view (Aditya, @Rohilalala, #1173).
- Keep missing CCTV numbers unknown instead of reading them as zero. `Number`
turns null, blank strings and booleans into 0 or 1, and the camera source
list sends an unknown number as JSON null, so a source-pack camera that left
out its pose reached the browser as zeros clamped to each field's floor —
facing north, pitch −2°, FOV 20°, range 120 m, mount 6 m and ground 0 m —
instead of the defaults (id-hash heading, −17°, 74°, 700 m, 24 m and the
city's ground); a null or blank position placed the camera at 0,0. The
server's `toFiniteNumber` and the browser's `safeNumber` now treat absent,
blank and boolean values as missing. The same conversion also stops a
Tallinn row with a null heading reading as a confident north-facing camera,
a Caltrans camera with a blank elevation losing its 150 m ground prior, and
a TfL or Catalonia camera with a null or blank coordinate being kept at 0,0
or on the zero meridian (omerbberk, #1172).
- Director shot actions land on the requested shot. A seek to a shot's start
could round a hair below it and select the end of the previous shot for some
shot counts and durations (Aditya, @Rohilalala, #1171).
- Fire-perimeter queries handle outlines and holes crossing the antimeridian:
areas enclosed near ±180° are included, areas at the opposite meridian are
excluded, and returned perimeter coordinates stay near the dateline. Incident
cards use the same circular longitude mean and compare multipart outline
areas along short longitude edges, keeping the card on the largest polygon
(Arianna Method, @ariannamethod, #1142).
- Scenes keep the "Scene not saved" status when a capture, update, append or
import cannot be saved, and an unreadable saved project no longer overwrites
the recovery checkpoint. `startScene()` resolves to its documented result,
`afterShotId` works without an explicit scene, and `scenesRun` counts the
distinct scenes queued (djabhi31, #1135).
- Label camera-tool Street View fallbacks as historical Google imagery rather
than the current camera view; keep unknown frame provenance explicit
(rudycelekli, #1118).
- Release superseded Google Street View open waits and suppress metadata
requests whose viewer was unmounted while the service loaded (rudycelekli,
#1117).
- Exclude aircraft without a reported altitude from requested altitude bands;
unfiltered aircraft queries still retain them (rudycelekli, #1115).
- A failed Esri Satellite or keyless terrain load still shows OSM or flat terrain
at once, but no longer lasts the whole session: while the fallback is on
screen, the real provider is retried in the background (30 seconds, doubling
to a 10-minute cap) and replaces it live once it loads. Map switches never
wait for a retry (Aditya, @Rohilalala, #1111).
- Skip TLE sets whose line 1 and line 2 carry different catalog numbers, so a
spliced catalog cannot pair one satellite's elements with another's; the next
valid set still parses (VG31OP, #1101).
- Overpass data whose element tags contain a failure phrase ("timed out",
"runtime error", "too many requests" and similar, e.g. a place named "Timed
Out Escape Rooms") is returned as data instead of being refused and putting
the endpoint on cooldown; upstream failures are read from the remark (Aditya,
@Rohilalala, #1097).
- Military aircraft keep their last heading and speed when an adsb.lol row omits
`track` or `gs`, instead of turning north and stopping; a grounded row without
a speed still reads 0 (Aditya, @Rohilalala, #1096).
- Update DOMPurify to 3.4.17, Sharp to 0.35.5 and source-map-js to 1.2.2 for
GHSA-p98j-92pf-mc4p, GHSA-6688-9rhm-gjv2, GHSA-wq5f-xc86-pv6w and
GHSA-68fv-2mgg-jv7q; `npm audit` now reports no vulnerabilities (NurstarK,
#1094).
- Location search accepts two-dimensional WGS84 `geo:` URIs offline, with
the existing coordinate framing and label. Unsupported URI fields are passed
to the remaining search providers unchanged (cj-vana, #1071).
- Keyless standalone startup answers an unavailable Google tile-token endpoint
with uncached 204 instead of a browser-visible 404. Existing providers take
precedence, and unrelated API routes behave as before. (dvilrgamerz, #1066)
- On Windows, `npm run doctor` and the Pinokio install no longer print Node's
DEP0190 warning ("Passing args to a child process with shell option true can
lead to security vulnerabilities"): npm.cmd's fixed subcommand is now part of
the shell command string instead of an argument array (quanticsoul4772, #1057).
-`aircraft_in_area` and `vessels_in_area` take `group_by` and `stats`, so an
MCP client can ask which airlines, countries, aircraft types or ship types
dominate an area, or the average altitude or speed, over every match instead
of estimating from the returned rows. Airlines come from the ICAO designator
in each callsign (SWA1234 counts as SWA), so the breakdown works on feeds
that report no operator; operator and type are counted only where the feed
reports them. Ship types count by family, so AIS codes 70 to 79 are all
cargo; type 0 ("not available") and the reserved codes 100 to 255 count as
unknown. Altitude and speed stats cover airborne aircraft only, so parked
aircraft do not pull the means down. Every vessel answer, `find_vessel`
included, now reads AIS ship-type codes as their family and a destination
with no letters as unknown (quanticsoul4772, #1056).
- Add Hong Kong Transport Department traffic snapshot cameras as a keyless CCTV
source pack: the public DATA.GOV.HK English locations XML, frames pinned to
`tdcctv.data.one.gov.hk`, with DATA.GOV.HK Terms of Use attribution. By
default nearly the full TD catalog (~1,013 cameras; cap 1,100) loads,
prioritized across Hong Kong Island, Kowloon and the New Territories. Headings
use the shared id-hash fallback at low confidence. `CCTV_HK_MAX_SOURCES` sets
the cap (8–1,200) and `CCTV_HK_ENABLED=0` turns the pack off (JCY2515, #990).
- Cross-site pages can no longer fill the terrain-height cache or spend the
visitor's own `/api/terrain/heights` rate-limit quota through the visitor's
browser. The route refuses a cross-site `Sec-Fetch-Site` or a foreign or
opaque `Origin` before rate limiting, cache access or upstream work. Unlike
the cost-bearing routes it still answers same-origin requests that arrive
through a reverse proxy, so terrain keeps rendering behind nginx, Caddy or
Cloudflare instead of falling back to the geoid (Sunil56224972, daikaginza,
#975).
- Street Level is rebuilt around one small provider registry: Mapillary,
KartaView, Panoramax and Google Street View share one coverage drawer and
one photo viewer, one source lit at a time. Floating and docked panels,
FOLLOW, surface modes, the legend and the Mapillary proxy's disk cache are
gone; `.gev-cache/mapillary/` from earlier runs is no longer read and can be
deleted (gekh, #974).
- Cross-site browser requests can no longer spend the TomTom traffic-tile
budget through `/api/tomtom/*`. The shared same-site gate runs before status,
cache, and upstream work; same-origin app requests and non-browser local
callers remain supported (Sunil56224972, #973).
- Recent Imagery: opening DETAILS scrolls its card into view when it opens below
the fold. The panel used to restore the previous scroll position right after
the reveal, so the card stayed out of sight (Ian Blenke, #970).
- Harden Vite's filesystem deny list for local-only artifacts: repo `dist/`,
`.gev-logs/`, and `.gev-cache/` are denied without blocking dependency
`dist/` directories, and checkout paths containing glob metacharacters are
escaped before building the root-anchored pattern (blowing-a-raspberry, #948).
- Give each Statens vegvesen camera its own ground height from Kartverket's
keyless point-height API (CC BY 4.0) instead of one flat 150 m prior, so
mountain-pass cameras no longer sit hundreds of metres below the terrain
where neither the point-height prior nor the ground snap resolves. About 850
of ~855 cameras get a height; the rest keep the old prior. Requests carry 20
points, the most the API answers from its 1 m terrain model, eight at a
time; a cold lookup takes about 6–13 s. Cameras over water use the water
surface rather than the sea-floor depth the API returns there; for bridge
cameras that is still below the deck. Heights are cached for good and misses
for 6 h, so a refresh only asks about new or moved cameras. After a
failing request (a 408, 429 or 5xx, a network error, a request over 10 s or
an unreadable body) the lookup stops and backs off (Retry-After, else
30 min, longer than the refresh). The lane waits at most 12 s from its
start, and only on a cold cache; a slower first lookup leaves the cameras it
had not reached on the flat prior until the next 15-minute refresh, and
keeps filling the cache meanwhile.
`CCTV_VEGVESEN_HEIGHTS=0` turns it off. Follow-up to #929 (trygvehhh, #945).
- Extract a reusable model-free World Packet from the existing voice
`get_current_view_state` owner without restoring disabled layers to the
model context. Keep the enabled-only payload, disabled-layer count, source
provenance, action result and view-state contract; read the layer roster once
per packet; voice retains the speech/card envelope at its own boundary.
Credits: original GEV voice/layer contributors and #880 implementation.
- Add optional Gemini Live voice to the standalone app, with a provider chooser,
server-held API key, microphone streaming, shared map actions and captions.
Switching providers ends the old conversation and preserves the input mode
(Manjunath, #872).
- Close the compact Radio disclosure when an adaptive tactical right-rail
restore directly expands Context, before Space Missions can be covered.
A repeated Context-open request also clears stale disclosure state.
Real PanelChrome constructor wiring and the rail reflow path are covered
by focused regressions. Credits: kvnloo (#816), raysung-2026's original
#804 report, asturksever's review, and quanticsoul4772's independent
Windows diagnosis.
- Changelog entries are now one file per pull request. A change adds
`changelog.d/<pull request>-<slug>.md` instead of editing the top of
`CHANGELOG.md`, which two pull requests in flight could not share — it was the
most frequent merge conflict in the open queue, and for six of them the only
one. `npm run changelog` splices the directory into `CHANGELOG.md` at release
time and empties it; `npm run changelog:check` reports the batch and changes
nothing. Fragment text is copied through unchanged (daikaginza, #787).
- Read satellite catalog numbers written in Alpha-5, the TLE spelling of
numbers above 99999: a letter for the first two digits, skipping I and O
(`A0000` is 100000, `Z9999` is 339999). The satellites layer converted such a
field to `NaN`, so every Alpha-5 satellite shared one catalog entry and all
but the first were dropped as duplicates; the satellite tools read the same
field as no number, so such a satellite could not be found by it. The layer
and the tools now decode the field in `src/sources/tle.js`, and reject a
malformed one (a sign, decimal point, hex, embedded space or stray letter)
instead of reading part of it as a number. This is Alpha-5 TLE
compatibility: CelesTrak's TLE feed carries no satellite above 99999, which
needs the OMM feed (#1191) (Honorius Neogy, #767, fixes #751).
- The dev server no longer watches `.gev-cache` and `.gev-logs`. Providers write
cache files and logs there while the app runs, and none of it is source, so
the watcher now skips it; this guards against the DNS-lookup delays heavy
cache writes have caused, though no difference was measured on current main
(monwiss, #761).
- Bound what a caller can grow through `/api/terrain/heights`. Every distinct
5-decimal point was a permanent in-memory entry, a permanent line in an
uncapped `.gev-cache/terrain-heights.json`, and a live upstream call, with
nothing limiting the rate. The point cache now holds at most 50,000 entries
and evicts oldest-first, a cache file above
24 MB is not read back, and the route is limited to 90 requests/minute per
client (270 global). Evicting a point costs one refetch, never a wrong
answer, since terrain does not move. Ordinary use is unaffected: a full
`test:track` run drives 10 requests, peaking at 8/min (daikaginza, #686).
- Add FL511 (Florida DOT) traffic cameras as an opt-in CCTV source pack:
the statewide ArcGIS catalog, read a page at a time under a byte cap with
redirects refused, and frames pinned to FL511's two image hosts over HTTPS.
FL511's terms limit its content to individual, personal use, and any other
use (a public deployment included) needs FDOT's express written consent, so
the pack stays off unless `CCTV_FLORIDA_ENABLED=1`. Headings come from the
catalog's direction codes, and camera ids come from the image channel, since
the catalog's ID column repeats across regions. `CCTV_FLORIDA_MAX_SOURCES`
sets the cap (Danielslay86, #605).
- The client's great-circle distances now come from one shared
- The tracked-aircraft descriptor now publishes `verticalRateMps`, on both the
civil and military layers, so a consumer reading `getTrackedInfo()` no longer
has to reach past it into the raw poll record for climb/descent. Reported,
never derived: an aircraft that has not transmitted a rate reads as `null`
rather than a level-flight `0`.
- Bound how often the GBFS proxy will relay. `/api/gbfs` already restricted
where it could go — https, an allowlisted host, a station feed path, no
redirects, a 12-second deadline and a 5 MB body cap — but nothing restricted
how often, so the dev server would make unlimited requests at a third-party
bikeshare operator on any caller's behalf. The route now admits 120
requests/minute per client address, refusing the rest with 429 and
`Retry-After` before any outbound fetch. The bikeshare layer needs about 15 a
minute with every catalogued system in range, so ordinary use is unchanged.
- Add nine Ontario transit feeds: TTC, MiWay, Hamilton HSR, Durham Region,
YRT/Viva, Guelph, Burlington, Barrie and Kingston, each carrying its own
licence quote and attribution. A feed can now name its own modes, so Toronto's
@@ -282,8 +978,6 @@
point. A slow render gets one bounded second frame request, while hidden,
stale, moved-camera, invalid and oversized captures still fail closed.
## [Unreleased]
- Add a keyless Air quality layer to the Weather group, with Environment and
Climate Change Canada's AQHI as the first registered provider. Readings carry
`{ provider, scale, value, band }` so another network can be added without
@@ -498,6 +1192,28 @@
needs only the Map Tiles API and the Maps JavaScript API (Street View). Keyless sessions and the Photon/Nominatim
fallback order are unchanged.
- Voice can switch on every shipped data layer (the two scene-driven
Bhote Koshi layers stay scene-only), including transit, Recent
Imagery, wind, the weather layers, cyclones and mapped installations, and
understands their spoken names ("buses", "hurricanes", "military bases").
One voice manifest (`src/voice/layerManifest.js`) generates the tool enums
and aliases, and a unit test fails when a new layer has no entry.
-`analyst_query` counts up to 250,000 loaded records per layer instead of the
first 2,000, and says when a layer went past that (`complete: false`). It
also answers over bikeshare stations, transit vehicles, rocket launches,
cyclones, ALPR cameras, mapped installations, local ADS-B aircraft and fire perimeters; fire
records carry their sensor (VIIRS or MODIS).
- Unknown fields, operators, units, value types, scope kinds and invalid
centres are refused with the allowed values instead of answering zero or widening to anywhere. A
layer that is off, still loading or unavailable is reported as such, not
as a count of zero; a partly answerable query is marked `partial`.
Distance sorts honour `sortDir`, a written unit such as `altitudeM(ft)`
converts, and large rankings no longer sort the whole set.
- Results carry `lat`/`lon` for each item and a `display` object with the
scope, the feed window (earthquakes and fires cover the last 24 h) and any
caveat for the screen. The model is asked to answer in one short phrase.
- The weather and Recent Imagery panels can be opened by voice.
## [0.2.1] — 2026-10-02 — God's Eye View through Your Agent (with security fixes and other features)
- Ship each bundled data pack once. The region, marine, admin-boundary,
@@ -780,32 +1496,6 @@
(`src/ui/imagerySplit.js`, `src/maps/imageryComparison.js`), and
`MapSourceController.subscribe()` reports every settled map switch.
## Unreleased — voice reaches every layer; honest analyst answers
- Voice can switch on every shipped data layer (the two scene-driven
Bhote Koshi layers stay scene-only), including transit, Recent
Imagery, wind, the weather layers, cyclones and mapped installations, and
understands their spoken names ("buses", "hurricanes", "military bases").
One voice manifest (`src/voice/layerManifest.js`) generates the tool enums
and aliases, and a unit test fails when a new layer has no entry.
-`analyst_query` counts up to 250,000 loaded records per layer instead of the
first 2,000, and says when a layer went past that (`complete: false`). It
also answers over bikeshare stations, transit vehicles, rocket launches,
cyclones, ALPR cameras, mapped installations, local ADS-B aircraft and fire perimeters; fire
records carry their sensor (VIIRS or MODIS).
- Unknown fields, operators, units, value types, scope kinds and invalid
centres are refused with the allowed values instead of answering zero or widening to anywhere. A
layer that is off, still loading or unavailable is reported as such, not
as a count of zero; a partly answerable query is marked `partial`.
Distance sorts honour `sortDir`, a written unit such as `altitudeM(ft)`
converts, and large rankings no longer sort the whole set.
- Results carry `lat`/`lon` for each item and a `display` object with the
scope, the feed window (earthquakes and fires cover the last 24 h) and any
caveat for the screen. The model is asked to answer in one short phrase.
- The weather and Recent Imagery panels can be opened by voice.
## Unreleased — local receiver feeds
### Local receiver feeds
- The Local ADS-B layer also reads local 1090 MHz and 978 MHz UAT decoder
@@ -948,7 +1638,6 @@
Add feed provenance to analyst/view answers and HUD context while retaining existing response fields and runner ownership (Matt Van Horn, #347).
Analyst records for loaded satellites, datacenters and dams, with explicit bounded count/rank coverage (Matt Van Horn, #351).
- New Fire Perimeters layer (Events group): live NIFC WFIGS interagency
wildfire incident perimeters as ground-clamped polygons with a
containment-colored fire line, refreshed every 5 minutes from the public
@@ -1225,6 +1914,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
### Other changes
- Add ECMWF IFS model selection to Wind (#464, thanks @beneduzi), with model-scoped forecast-step caches, cancellation of replaced requests, and separate issue/valid timestamps.
- Add bounded Director feature actions with accessible controls, explicit camera/layer admission and cancellation; restore pack geometry on same-shot seek. Preserve existing scenes and content attribution.
@@ -2033,3 +2723,4 @@ represent previously published GitHub Releases.
./scripts/dev-fresh.sh # or: npm run dev (keys are optional)
@@ -27,6 +29,25 @@ through Pinokio; the terminal path above remains the contributor path.
Open `http://localhost:4173`. Before sending a PR run `npm run build`, `npm test`, and `npm run test:track` (dev server must be up) — **all three must stay green.**
### Test prerequisites
- **`npm test`** — unit tests. Runs standalone; no dev server or credentials
required. Exit code is the gate.
- **`npm run test:track`** — the 3D flight-tracking regression harness. It
drives the **real app** in headless Chromium, so it **requires a running
dev server** (default `http://localhost:4173`). To target another instance,
run `npm run test:track -- --url <app-url>` (or
`node scripts/track-regression.mjs --url <app-url>`). It does not depend on
live OpenSky / adsb.lol / AISStream (those are optional or rate-limited
locally); it installs a persistent `fetch` shim that returns synthetic
aircraft in the exact upstream payload shapes the layers parse.
- **`npm run build`** — production Vite build. No server or credentials
required; it validates that the client bundle compiles cleanly.
Each feature also has its own headless gate; see
[Feature regression gates](#feature-regression-gates) below. Docs and changelog
rules are under [Pull requests](#pull-requests).
## Checking a built app locally
Run `npm run build` followed by `npm run preview`. Preview serves the built
@@ -108,14 +129,14 @@ prints anything; never fix that by renaming or deleting PR A's published token.
The highest-leverage places to jump in:
- **🌆 Add a CCTV source pack.** Austin is the reference camera source. Adding another city means a clean public camera catalog with coordinates, attribution, and server-registered frame URLs (the proxy only fetches registered URLs — never client-supplied ones, see [SECURITY.md](SECURITY.md)). City packs are the best first lane.
- **🛰️ Add or improve a data layer.** Layer factories live in `src/layers/<family>/`, with source, record, controller and renderer owners implementing the layer interface (`init/enable/disable/update/destroy/getStats`, optional `getDetectableObjects`/`getStats`). Use an existing layer as a template.
- **🛰️ Add or improve a data layer.** Layer factories live in `src/layers/<family>/`, with source, record, controller and renderer owners implementing the layer interface (`init/enable/disable/update/destroy/getStats`, optional `getDetectableObjects`/`getStats`). Use an existing layer as a template, and see [Adding a new layer](#adding-a-new-layer) for what the PR needs.
- **🎙️ Extend voice control.** Voice arguments are defined in `src/voice/actionSchemas.js`, with server-side descriptions in `server/providers/openai/tools.js` and client-side execution in `src/voice/gevActions.js`. Keep the tool surface tight and the responses honest (confirm only what actually happened).
- **🎨 Add a visual style.** Styles are GLSL post-process shaders in `src/styles/`.
- **🐛 Fix bugs / improve the first-run experience.** See [docs/KNOWN-ISSUES.md](docs/KNOWN-ISSUES.md).
- **Vue 3 + Pinia for the interface,** [CesiumJS](https://cesium.com/platform/cesiumjs/) for the globe and [Vite](https://vitejs.dev/) to build. See [Building UI](#building-ui).
- **Assembly lives in `src/app/`; standalone defaults live in `src/standalone/`.** UI controllers live in `src/ui/`, layer factories in `src/layers/`, portable sources in `src/sources/`, and application operations in `src/services/`. Existing `src/data/<layer>.js` entries retain compatibility; new code belongs with its focused owner.
- Sources acquire records; renderers own Cesium resources. Import `gods-eye-view/layers/<family>/source` when only a source factory is needed. Common voice controls consume the session interface; protocol adapters own connection details.
- **Secrets stay server-side.** Anything needing a private key goes through a local proxy under `server/providers/`. The browser only ever sees the Google Maps key (which you restrict) and ephemeral tokens.
@@ -126,9 +147,65 @@ The highest-leverage places to jump in:
- ES modules, **2-space indent, single quotes, semicolons.**
- JSDoc on exported/public functions.
- Match the surrounding code — comment density, naming, and idiom.
- User-visible text goes through `t()`,`tn()` or`msg()`, or a `data-i18n*` attribute in markup, and`npm run i18n` must pass. See [docs/I18N.md](docs/I18N.md).
- User-visible text goes through `t()` or`tn()`, with`msg()` marking English that is stored and translated later. Components call `t()` in their templates, so their text follows the locale; `data-i18n*` attributes are only for static page markup such as the loading screen, and `npm run i18n` rejects them in a template.`npm run i18n` must pass. See [docs/I18N.md](docs/I18N.md).
- Prefer small, reviewable commits. Conventional-commit-style prefixes (`feat:`, `fix:`, `perf:`, `docs:`) are appreciated but not required.
## Building UI
New UX elements are Vue components whose state lives in a Pinia store, and so
are existing ones when a change converts them:
- **Views are Vue components.** Application views live in `src/ui/views/` and
the components they place in `src/ui/views/panels/`; a component that
belongs to one feature sits beside its owner (`src/ui/rail/`,
`src/ui/weather/`). A view renders what its store holds and forwards
clicks and input to the store; it holds no application logic.
- **State lives in a Pinia store** in `src/ui/stores/`. The owner presents a
plain view object and publishes it to the store; the view renders it. User
actions return to the owner through handlers it connects to the store,
which replace an earlier owner's. Where several owners answer different
actions of one store, use `createOwners()` from `src/ui/stores/owners.js`:
`act(name, ...args)` calls the newest connected owner that handles `name`,
and the owner calls `detach(token)` when it stops.
- **Portable owners stay framework-free.** An owner in a portable export
imports neither Vue nor Pinia: it takes a `publish` option or offers a
`subscribe…()`, and the application shell connects it to the store.
`npm run check:boundaries` enforces this.
- **Reuse the kit first.** `src/ui/kit/` has one component per control:
`UiButton` (including pressed toggles and pressable chips), `UiSelect`,
`UiSlider`, `UiSegmented`, `UiChip` (a label, not a control), `UiBadge`,
`UiEmpty`, `UiIcon`, `UiTooltip`, `UiPanel` and `UiRail`. Use these before
writing a control. When the kit lacks something, extend a kit component or
add one that follows the kit's names, props, events and class names (see
[Component kit](docs/VUE-MIGRATION.md#component-kit)), rather than
building a one-off.
- **No hand-written DOM binding.** Don't build elements or write text,
classes or attributes from a controller to show UI state; render it from
the store. New controls take input through the store rather than through
listeners the owner adds. To render outside the view's host (the body, the
globe container, or a slot another surface opens), use `<Teleport>` to
`body`, a fixed id or a container the owner publishes; don't move nodes by
hand. What stays imperative: classes on `<body>` and `<head>`, which sit
outside Vue (`cockpit-mode`, `ui-clean-view`); focus moves; positions a
layout pass measures in the same task that writes them; and surfaces an
owner paints or plays, such as the Cesium canvas, overlay canvases and live
video. A few existing surfaces keep listeners or bindings that
[docs/VUE-MIGRATION.md](docs/VUE-MIGRATION.md) records; keep those
contracts when you change them, and don't copy them into new UI.
- **Text** goes through `t()` or `tn()` where the template renders it.
English kept in a constant or a store is marked with `msg()` and passed to
`t()` when it renders, since a `t()` at module load would fix the English.
- **Test components, not markup strings.** Mount the component over
happy-dom with `@vue/test-utils` or `createApp` and assert what it renders
from its store (see `src/testSupport/vueDom.mjs`, `src/ui/kit/*.test.mjs`
and `src/ui/views/intelHudView.test.mjs`). Run one such file with
nodes to `assert.equal`/`deepEqual`; compare them with `assertSameNode()`
from `src/testSupport/domAssert.mjs`.
[docs/VUE-MIGRATION.md](docs/VUE-MIGRATION.md) records how each surface moved
and what stays imperative, and why.
## Formatting and reusable components
Run `npm run format` before submitting changes, then `npm run format:check`
@@ -153,6 +230,34 @@ ownership and adoption process.
3. If you change runtime behavior, update `docs/CURRENT-STATE.md` in the same PR, and add your changelog entry as `changelog.d/<pr-number>-<slug>.md` instead of editing `CHANGELOG.md` — see [changelog.d/README.md](changelog.d/README.md). Distinct filenames don't conflict; a release assembles the directory into `CHANGELOG.md`. A documentation claim that needs a regression test should assert against `docs/CURRENT-STATE.md` or `README.md`, which a PR still edits directly.
4. If you add or change a data source, update [DATA_SOURCES.md](DATA_SOURCES.md) with its license and attribution. **Don't add data you don't have the right to redistribute** — fetch it at runtime instead.
5. Describe what you changed and how you verified it (screenshots welcome for anything visual).
6. If you add a new layer, fill in the new-layer section of the PR template. See [Adding a new layer](#adding-a-new-layer).
## Adding a new layer
A new layer changes what everyone sees on the globe, so maintainers need to see
it working and know where its data comes from before reviewing the code. PRs
that add a layer must include all four of these in the description. The PR
template has a section for them.
1.**Screenshots.** At least one of the layer on the globe, plus its info card
or panel if it has one.
2.**A screen recording**, preferably 15 to 30 seconds: turn the layer on, move
around, and open a record. Drag an `.mp4` or `.mov` into the PR description.
GitHub accepts videos up to 10 MB on free accounts, so link to a larger file
instead of attaching it.
3.**The proposed category.** Say which layer-panel group the layer belongs in.
The groups are defined in `PANEL_GROUPS` in `src/ui/layerPanel.js` (today:
Movement, Cameras, Infrastructure, Events, Weather and Audio, plus Utilities,
whose rows render in the separate UTILITIES panel). A layer that isn't
listed there falls under "Other layers". If none of the groups
fits, propose a new one and explain why.
4.**The data source and licence.** Name the provider and endpoint, link its
licence or terms, give the attribution shown in the app, and say whether it
needs a key and whether the data is fetched at runtime or bundled. Add the
same details to [DATA_SOURCES.md](DATA_SOURCES.md) and register the credit
in `src/data/dataCredits.js`.
Maintainers may hold a layer PR until all four are there.
| **Environment and Climate Change Canada** (MSC GeoMet OGC API) | Air quality (AQHI) — Canadian station observations | [ECCC Data Servers End-use Licence](https://eccc-msc.github.io/open-data/licence/readme_en/) (open, attribution required, no endorsement implied) | "Contains information licensed under the Environment and Climate Change Canada Data Servers End-use Licence" |
| **NASA GIBS** (WMTS tiles + Worldview Snapshots) and **NASA CMR** (granule catalog) | Recent Imagery layer: HLS S30/L30 (Sentinel-2, Landsat 8/9) and VIIRS true-colour tiles draped on the map, per-day thumbnails and PNG exports; the HLS granule catalog (dates, cloud, footprints) for a selected box | U.S. public domain (NASA data); keyless, browser-direct (CORS `*`); no bulk use — one 30-day catalog query per box, tiles bounded to the box, six tile requests in flight | "We acknowledge the use of imagery provided by services from NASA's Global Imagery Browse Services (GIBS), part of NASA's Earth Science Data and Information System (ESDIS)." — [gibs.earthdata.nasa.gov](https://gibs.earthdata.nasa.gov), [HLS](https://lpdaac.usgs.gov/products/hlss30v002/) |
| **NIFC WFIGS** (Wildland Fire Interagency Geospatial Services, ArcGIS feature service) | Fire Perimeters layer: current interagency wildfire incident perimeters, containment, and incident facts | U.S. public domain (interagency wildland-fire data published through the [NIFC Open Data portal](https://data-nifc.opendata.arcgis.com/)); fetched keyless through `/api/fire-perimeters` (5-minute server cache) with `exceededTransferLimit` paging | "Wildfire perimeters: National Interagency Fire Center (WFIGS)" |
| **InciWeb** (inciweb.wildfire.gov) | Per-incident "InciWeb ↗" card links to official incident pages (narratives, evacuation/closure notices) | U.S. government public incident information. Links are matched best-effort by incident name and state, with incident page origin and update times checked on selection. Same-origin `/api/fire-perimeters/inciweb` routes cache the catalog for 1 hour and publication metadata for 30 minutes; linked pages remain InciWeb content | "Incident information: InciWeb (inciweb.wildfire.gov)" |
| **NYC DOT Traffic Management Center** (`webcams.nyctmc.org`) | CCTV camera catalog + frames, New York City five boroughs | NYC DOT requires developers to sign a [data-sharing agreement](https://www.nyc.gov/html/dot/html/about/datafeeds.shtml) for the camera feed. **Off by default**; enable only under your own agreement | "NYC DOT Traffic Management Center" + NYC DOT's required application disclaimer (in the Data attribution popover) |
| **City of Toronto Open Data** (`opendata.toronto.ca`) | CCTV camera catalog + frames, Toronto city streets | [Open Government Licence – Toronto](https://www.toronto.ca/city-government/data-research-maps/open-data/open-data-licence/) — attribution REQUIRED | "Contains information licensed under the Open Government Licence – Toronto" |
| **GBFS (Lyft / BCycle / Social Bicycles / PBSC)** | Bikeshare availability | Per-feed (attribution-only) | Credit the operator (e.g. Austin BCycle) + its `license_url` |
| **Bike Share Toronto** (`toronto.publicbikesystem.net`) | Bikeshare station availability, Toronto | Operator GBFS feed. No licence is published: the City's open-data package declares none and the feed has no `license_url`. Attribution is given to the operator. | "Bike Share Toronto (Toronto Parking Authority)" |
| **Bike Itaú / Tembici** (`*.publicbikesystem.net`, seven Brazilian cities, Santiago and Bogotá) | Bikeshare station availability: São Paulo, Rio de Janeiro, Salvador, Recife, Porto Alegre, Brasília, Curitiba, Santiago de Chile and Bogotá | Operator GBFS feeds. No licence is published: the feeds' `system_information.json` carries no `license_url`, operator or attribution fields. Attribution is given to the operator. | "Bike Itaú (Tembici)"; "Tembici Bogotá" |
@@ -175,6 +177,7 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei
- **Kartverket ground heights for Statens vegvesen cameras.** The camera feed's points are 2D, so the loader asks Kartverket's keyless point-height API (`ws.geonorge.no/hoydedata/v1/punkt`, CC BY 4.0) for the terrain height at each camera. Requests carry 20 points: the API accepts 50, but above ~20 it answers the extra points from contour lines (`hoydekurver`, metres to ~20 m off) or not at all. Eight requests run at a time, about 36 for the whole country: a cold lookup took 6–13 s in testing (single requests 0.6–9.5 s). Answers are matched by the coordinates the API echoes and recorded only for points that request asked about. Over water the API returns the sea-floor depth (`dybdekurver` or `Havflate`), which is replaced by 0 m, the water surface; for the ~14 bridge cameras that is below the deck, but much closer than the sea floor or the flat prior. Heights are cached in memory by position for the life of the server, and points with no height (a few ferry quays and bridges, or a point a 200 answer leaves out) for 6 h, so the 15-minute catalog refresh only asks about new or moved cameras. The first failing request (a 408, 429 or 5xx, a network error, a request over 10 s, or an unreadable or oversized body) stops the lookup, with no retries, and no new lookup starts until the backoff ends (Retry-After when sent, else 30 min, longer than the refresh; bounded to 1 min–1 h). Any other 4xx marks that request's points as having no height and the lookup carries on. The whole lookup has a 30 s deadline; a lookup cut short by it resumes on the next refresh without a backoff. The lane waits at most 12 s, counted from the start of the loader, and only when more than 10% of the cameras have no answer yet; a few new cameras or expired misses are looked up in the background. A slower lookup keeps filling the cache, and the cameras it had not reached keep the flat prior until the next 15-minute refresh (about 190 of 865 on one slow first run). Only coordinates are sent, with the same User-Agent as the other CCTV requests. `CCTV_VEGVESEN_HEIGHTS=0` turns the lookup off. The Kartverket credit, with a note that heights over water are set to 0 m and heights are rounded to 0.1 m, is shown beside the Statens vegvesen entry in the Data attribution popover.
- **NYC DOT Traffic Management Center cameras.** Off by default. NYC DOT's [developer data feeds page](https://www.nyc.gov/html/dot/html/about/datafeeds.shtml) asks developers who want the camera feed to contact `[email protected]` and sign a data-sharing agreement. This project does not hold one that covers downstream installations, so the pack loads only when an operator who holds their own agreement sets `CCTV_NYC_ENABLED=1`; with the flag unset, empty or any other value, the server makes no request to NYC DOT. The endpoint needs no key. The camera list is the public `webcams.nyctmc.org/api/cameras` catalog (~980 cameras across the five boroughs); frames are stills at `webcams.nyctmc.org/api/cameras/<uuid>/image`, synthesized from a validated UUID rather than taken from the payload, and only that host is registered. Cameras the feed reports as offline are dropped. The catalog publishes no camera facing, so headings use the shared id-hash fallback at low confidence and are corrected with the in-app calibration gizmo. Once enabled, the 250 cameras nearest the borough cores load; `CCTV_NYC_MAX_SOURCES` (8–400) changes the cap. Frames are fetched at request time and never stored or redistributed. NYC DOT asks every application that uses its data to carry its disclaimer; it is registered verbatim in the Data attribution popover.
- **City of Toronto RESCU cameras.** Keyless. The camera list is the City's public `tmcearthcameras.json` on `opendata.toronto.ca` (dataset "Traffic Cameras", Transportation Services), served as JSONP and unwrapped with an anchored pattern. This is the municipal network the Ontario 511 pack does not carry: downtown arterials plus the Gardiner, DVP, Allen and Lake Shore. The list publishes no frame URL, so frame URLs are BUILT from the documented `CameraImages/loc####.jpg` convention and a strictly-digits camera number, pinning the frame proxy to the portal origin by construction; the list fetch refuses redirects and bounds the body it buffers. Frames are fetched at request time and never stored or redistributed. The dataset publishes no camera facing: the `D1`-`D4` fields are the directions of the *comparison* images (static reference stills captured in 2016 under a different path), not a bearing for the live pan-tilt-zoom frame, so headings use the shared id-hash fallback at low confidence and are corrected with the in-app calibration gizmo. A camera that is temporarily out of service still answers 200 with a byte-identical "Image Unavailable" placeholder rather than an error (about 6% of the network at any moment), so such a camera reads as healthy; the frame it shows is the agency's, not a synthesized one. The licence requires the attribution statement above. Disable with `CCTV_TORONTO_ENABLED=0`.
- **Autostrada del Brennero (A22) webcams.** Keyless. The camera list is one request to the Open Data Hub tourism API (`tourism.api.opendatahub.com/v1/WebcamInfo?source=a22`), run by NOI Techpark in South Tyrol, which republishes Autobrennero's own list of 13 motorway webcams with coordinates and kilometre markers. Frames are the operator's stills on `www.autobrennero.it/WebCamImg/km<N>.jpg`, refreshed about once a minute, and only the exact image for each camera's kilometre is registered. The feed names no bearing, so headings use the id-hash fallback at low confidence. The road climbs from about 20 m in the Po valley to about 1,370 m at the Brenner Pass, so each camera's ground elevation comes from a per-kilometre table sampled from the Copernicus GLO-90 DEM (via Open-Meteo's elevation API); a camera not in the table uses a 200 m prior. The Open Data Hub tags every record CC0 (`ClosedData: false`, Autobrennero as licence holder), but that tag is the aggregator's metadata and Autobrennero publishes no terms of its own for the images, so they are credited to the operator as a courtesy and are not redistributed. Frames are fetched at request time and never stored. Disable with `CCTV_A22_ENABLED=0`.
- **Camera frame content.** Public camera frames can contain people, vehicles and license plates, and the camera layer does nothing with them beyond putting them on the map. A successful upstream response is relayed as the provider served it: nothing in the camera pipeline blurs, redacts, enhances or restores it, and nothing there runs face, plate or object detection or otherwise recognises or classifies what is in the picture. (The ALPR layer described below maps where plate-reader cameras are, from locations contributors tagged in OpenStreetMap; it reads nothing from any camera frame.) When an upstream has no frame for a camera, what you see instead is a Google Street View still or the app's own placeholder card; the panel's source badge names which of them you are looking at, and a refresh that fails after a frame has arrived leaves the last good picture on screen rather than replacing it. The browser resamples each frame for display: onto a fixed 1920x1080 surface for the camera's projection plane in the scene, and onto a 192x108 canvas for the small ambient card. That scaling is the only change made to the picture, and the only pixels the camera layer reads back are a 64x36 downscale hashed to tell whether the picture has changed. Live streams are piped through; the still path reads the image into memory to pass it on — TxDOT wraps its JPEG in a JSON envelope, which is decoded there — and no frame is written to disk (the disk cache, `.gev-cache/`, holds feed JSON for other layers and never camera imagery). Outside that pipeline, the voice assistant is the one feature that sends imagery anywhere: when it answers a question about what is in view it may attach a downscaled screenshot of the rendered scene — including any camera plane on screen — to the request it sends to the voice model. Frames are fetched only from server-registered camera URLs — a client cannot hand the proxy a URL (see [SECURITY.md](SECURITY.md)). What the app does stays inside the line README draws: no named-person search, no face recognition, no tracking individuals.
- **Transit (GTFS-Realtime).** The proxy requests only registered operator feeds, validates redirects, bounds responses, and uses conditional requests, short snapshot caching and failure backoff. All sixteen registered regions are live-enabled; each can be disabled independently. Vehicles use delayed playback between reports, with report age and waiting states shown separately. Session-local selected trails are available for every feed. MBTA additionally retains up to 15 minutes of recently observed positions in the running proxy, so available history can survive a browser reload; server restart clears it. History reads never fetch upstream, and other feeds have no proxy retention. Nothing is archived to disk or collected in the background. Operator text credits are included as a courtesy and do not imply endorsement. Heights are aligned to work with Google 3D tiles; subway markers are shown at street level because these feeds do not supply depth. The TTC feed is BusTime, the surface system: it carries buses and streetcars and no subway at all. About four in ten of its vehicles report no route at all (unassigned or deadheading), and those are shown with the feed default rather than a route-established mode. It sends no ETag or Last-Modified, so its conditional requests fall through to the ordinary snapshot cache. Three Golden Horseshoe neighbours run alongside it — MiWay, Hamilton HSR and Durham Region Transit — each bus-only and each polled only near its own region. Durham Region Transit publishes no bearing on any vehicle, so those sprites take their heading from observed movement like any other feed that omits it. Guelph, Kingston and Barrie extend the same pattern eastward and north. Hamilton, Guelph, Burlington and YRT publish route ids that are not the number on the vehicle, so those entries carry a route-name table taken from each operator’s own published GTFS; any id missing from a table renders unchanged rather than blank. A label is never invented: YRT runs a route `9101` that appears nowhere in its published routes, so it stays as the feed sends it. Burlington’s is the one that cannot be shortcut: `351` is route 1 while `3510` is route 10, so the table is a real join rather than a trimmed prefix. GO Transit / Metrolinx is deliberately absent: its REALTIME feed requires an API key issued by manual review, which the keyless admission rule excludes (its static schedule download is keyless, but carries no vehicle positions). Barrie and YRT/Viva each invite acceptance of their licence through a form, and each licence also states that using the data is itself acceptance; this deployment relies on that clause, which is recorded in both entries. Grand River Transit in Waterloo is absent for a different reason: it answers, but its feed header was months stale and it publishes no per-vehicle timestamps, so it would render vehicles that look live and are not. Ottawa’s OC Transpo falls to the same keyless admission rule as GO Transit — its realtime feed requires an API key. London Transit and Thunder Bay Transit publish vehicle positions over plain HTTP with no HTTPS endpoint available. Every other Ontario system — Brampton, Oakville, Niagara, Milton, Windsor, Greater Sudbury, UP Express and a dozen smaller operators — publishes static GTFS schedules only, with no realtime vehicle positions to register. Between the nine included and these exclusions, every Ontario realtime vehicle-position feed catalogued in the Mobility Database is accounted for.
- **Radio Browser.** `/api/radio/stations` discovers official API mirrors, makes bounded and coalesced healthy/geolocated HTTPS-station queries, caches the normalized public-domain directory for 45 minutes, and may serve the last good catalog for up to seven days during an outage. Refreshes must meet minimum accepted-query and station coverage before replacing a warm catalog; schema-valid responses whose rows all fail the product's health policy do not count as successful queries. A usable partial cold catalog is explicitly `DEGRADED`, and non-array or empty successful payloads are rejected atomically. Individual structurally malformed rows are skipped rather than failing the whole query; the response carries their bounded `rejectedStationCount` (at most 10,000), which the client adds to its own rejected rows and reports as degraded coverage without discarding the accepted generation. Every directory and click-count request rejects redirects, validates all resolved addresses as globally routable (including reserved/documentation IPv4 and special/non-global IPv6 exclusions), and pins the TLS connection to a validated address. Only MP3/AAC non-HLS directory rows with public HTTPS stream targets are returned; favicons are intentionally omitted. Pressing play connects one browser audio element directly to the selected broadcaster and calls the directory's click counter through known-ID-only `POST /api/radio/click/:uuid`. GEV never proxies, caches, records, bundles, or redistributes audio. Radio Browser supplies station-level tags, not dependable current-song or upcoming-program metadata, so Radio filtering never claims either. Direct playback exposes the listener's IP address to the broadcaster, whose own stream terms apply.
@@ -183,7 +186,8 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei
- **Re:Earth Terrain.** Keyless (no API key). Used two ways: (1) `src/mapStackController.js` swaps in a `Cesium.CesiumTerrainProvider` pointed at Re:Earth's `cesium-mesh/ellipsoid` quantized-mesh endpoint for globe stacks without a Cesium ion token (e.g. OSM), replacing a flat `EllipsoidTerrainProvider`; falls back to the flat provider if the endpoint can't be reached. (2) The server-side `/api/terrain/heights` proxy (disk-cached, serve-stale) resolves per-point ellipsoidal ground height for entity placement. Both are best-effort with a keyless-safe fallback (bundled EGM96 geoid math) if Re:Earth is unreachable.
- **OSRM on the FOSSGIS routing servers.** Keyless (no API key). Every route is requested by a person: clicking A and B in the Directions layer, or a voice "route from A to B". The server-side `/api/route` proxy sends an identifying `User-Agent`, pins the upstream host, accepts 2-12 coordinates, rejects any leg over 600 km or a total over 2,500 km, caps the buffered response at 8 MB, times the upstream out at 12 s, rate-limits per client, caches each route for 10 minutes, and coalesces identical requests that are in flight at the same time. Turn-by-turn maneuvers are asked for only when a caller wants them (`steps=1`), so the callers that do not read them send the smaller request. The "fix the map" link the policy asks for is in the Data attribution popover beside the OpenStreetMap credit.
- **Mapped installations.** OpenFreeMap military polygons use a bundled Overture/OSM name index; wide views show named points. Optional operator-configured Overpass and explicit Google Places searches keep their own source paths. Mapped data does not establish current activity or complete coverage.
- **Place search.** Named-place fly-to uses Google Geocoding when a Maps key is configured, then Photon's public instance, then `/api/geocode` (Nominatim search) when neither answers. `/api/geocode`uses the shared Nominatim forward-search queue at no more than one request per second, sends an identifying `User-Agent` and `Referer`, caches answers for five minutes, shares one upstream call between identical searches already in flight, bounds the queue so a burst is refused rather than held, and drops a queued search whose caller has given up. It asks for one result per search and is not used for systematic or bulk queries.
- **Place search.** Named-place fly-to uses Google Geocoding when a Maps key is configured, then Photon (`PHOTON_URL`; unset uses the public instance, empty disables it), then `/api/geocode` (Nominatim search) when neither answers. `/api/geocode`and the voice outline route `/api/geocode/outline` share one Nominatim queue (`NOMINATIM_URL`; unset, empty and invalid values disable it; an operator must explicitly select an authorized endpoint): one request at a time at least 1.1 s apart, an identifying `User-Agent` and `Referer`, a short queue that refuses a burst, a pause after 429/403/5xx answers (honouring `Retry-After` in full), at most one retry after a network failure, and a per-install daily cap on public-instance requests (`NOMINATIM_DAILY_CAP`, default 50). Processes using the same installation state file hold one lock through the actual fetch invocation and recording the next public start, with the daily slot persisted before invocation; restarts share that count, spacing and any pause. An unfinished dispatch marker fails closed until operator recovery; it is never automatically reclaimed. Public access fails closed if the state file cannot be read, locked, written or semantically validated. This coordination does not span independent installations or state directories and does not establish an application-wide public-service allowance or authorize public use. An operator-selected non-public endpoint is outside the public cap, spacing and durable-state requirement; an upstream cooldown is kept with the endpoint that returned it rather than leaking across operator URLs. Answers are cached on disk (outlines 90 days, places 30 days, no-results 24 hours); failures are not cached. Outlines are looked up only for an explicit ask, after bundled data and any configured Overpass, and only results whose type suits the ask are used. Administrative outline responses expose country/admin-1/admin-2 identity only when the selected Nominatim row has a recognized, unambiguous address type consistent with its own name; numeric rank, ambiguous types, missing fields and contradictory names do not manufacture identity. It is not used for systematic or bulk queries.
- **Street and building outlines.** Voice street highlights, pointed-at buildings and grounds use the OpenFreeMap vector tiles the traffic layer already loads. Traffic and outlines share one bounded pending fetch/decode owner per immutable XYZ tile; materialized outline geometry is charged to the retained-byte ceiling and capped by coordinate count. Consumer result caches and request lifetimes remain independent, while eviction or explicit teardown cancels queued idle projections and releases their decoded owner. No Overpass request is made unless `OVERPASS_UPSTREAMS` is configured. Named grounds require a returned name matching the selected place, except that an ask whose head noun names a kind of grounds (a school, university or college, hospital, cemetery, stadium or zoo) may use the part of the smallest unnamed tile area of that class that holds the resolved place; a differently named area never answers. Other anonymous landcover polygons cannot establish identity, so unsupported grounds stay unavailable rather than borrowing a nearby POI name.
- **Cockpit regional briefing.** `/api/regional-brief` rounds aircraft coordinates into 0.1° cache cells, caches results for five minutes, and resolves physical regions from bundled public-domain Natural Earth polygons without reverse-geocoding requests. Google News RSS is queried with the resolved locality/region first; GDELT is used only when that RSS query fails or is empty. Google's published Google News terms restrict that source to personal, noncommercial use, so commercial deployments must disable/replace it or obtain separate permission; GDELT permits commercial dataset use with citation. The Data attribution popover identifies the active headline sources; article links retain publisher attribution. Headlines are location-query matches, not verified incidents, risk rankings, or evidence that a location is safe. Empty, partial, stale, and unavailable source states remain distinct. Open-Meteo supplies current conditions independently of the news source. `WX OFF` disables cockpit weather rendering only; the Local Info briefing still fetches its source-backed weather values and displays the required linked Open-Meteo credit.
- **Dynamic weather presentation.** While cockpit mode is active, `/api/weather-effects` requests current Open-Meteo observations for the aircraft/camera location, rounds coordinates into 0.1° cache cells, caches results for five minutes, and may retain a stale observation for up to 30 minutes during a transient outage. WMO condition code selects the visual family; observed cloud cover, precipitation, visibility, wind speed, and wind direction bound its strength and motion. Missing or expired weather renders no synthetic atmospheric effect, and normal globe view never renders the weather overlay.
@@ -205,6 +209,8 @@ The [Bhote Koshi event pack](public/events/bhote-koshi-2026/README.md), under `p
| **DataSF Analysis Neighborhoods** (41 SF neighborhood polygons) | `neighborhoods/` | **PDDL 1.0** (public domain) | ✅ (no restrictions) | "City & County of San Francisco — DataSF" (courtesy — not legally required) |
| **Natural Earth countries, states and provinces** (260 admin-0/map units + 4,587 admin-1 polygons) | `natural_earth/` | **Public domain** | ✅ (no restrictions) | "Made with Natural Earth" (courtesy credit — not legally required) |
| **US Census Bureau counties** (3,235 county polygons) | `us_census_counties/` | **Public domain** | ✅ (no restrictions) | "U.S. Census Bureau" (courtesy — not legally required) |
| **US Census Bureau places** (32,629 city, town and CDP polygons) | `us_census_places/` | **Public domain** | ✅ (no restrictions) | "U.S. Census Bureau" (courtesy — not legally required) |
| **Who's On First neighborhoods** (61,441 polygons) | `wof_neighborhoods/` | Per source: CC BY, ODC-By, Open Government licences, CC0, PDDL, public domain (folder `ATTRIBUTION.md`) | ✅ (attribution) | "Neighborhood boundaries: Who's On First and contributors"; per-source credits in the folder |
| **Yale Bright Star Catalogue** (9,096 stars, sky background) | `sky/` | **Public domain** | ✅ (no restrictions) | "Yale Bright Star Catalogue (Hoffleit & Warren, via CDS)" (courtesy credit) |
| **CCTV ground heights** (3,445 cameras) | `cctv_ground_heights/` | Precomputed camera placement heights, aligned to work with Google Photorealistic 3D Tiles (folder README) | — | — |
@@ -301,6 +307,15 @@ the retrieval date (2026-07-30), exact download URL, license evidence, and the
deterministic transform (`scripts/build-sf-neighborhoods.mjs`: `nhood` → `name`, ~2 m
@@ -61,7 +61,7 @@ Start with the included data sources, then add your own. Each layer is a separat
- **🎯 Click-to-track anything:** Camera locks on, draws a fading trail, surfaces full metadata — and a tracked fire or vessel hands you off to the nearest live camera in one click.
- **🖊️ Voice whiteboard:** Speak annotations onto the world — real boundary polygons, marks, and routes.
- **🛫 3D hangar:** Real per-class aircraft models — 787, ATR-72, Citation, Bell 206, MQ-9 — and a tracked contact swaps from glyph to 3D model as you close in.
- **🎨 Reskin reality:** GLSL sensor looks over the normal globe — CRT, NVG, FLIR/thermal, Noir, Snow.
- **🎨 Reskin reality:** GLSL sensor looks over the normal globe — CRT, NVG, FLIR/thermal, Noir.
- **🟩 Detection overlay:** Screen-space bounding boxes and IDs on everything in view.
- **🎖️ Military HUD:** Tactical heads-up display with intelligence-style telemetry.
- **🌐 Global Context:** Stage the full situational picture with one switch — and get your exact view back when you leave.
@@ -89,7 +89,7 @@ Start with the included data sources, then add your own. Each layer is a separat
## ⚡ Quick Start
**Start without an account or API keys.**Both paths open the same app with
**Start without an account or API keys.**Every path opens the same app with
Esri satellite imagery and keyless terrain. OSM is the fallback if Esri is
unreachable. Flights, military traffic, satellites, earthquakes, public
cameras, radio, and launches are available without keys.
Keep the `env`: bash treats `UID` as read-only, so a plain `UID=1026` in front of a command is silently ignored. If the container can't write the checkout, it says so at start. Docker Desktop on macOS and Windows maps ownership for you.
On SELinux hosts (Fedora, RHEL), compose mounts the checkout with `:z` so the container may read it; Docker may relabel the checkout for container access, which can take a moment on a large checkout. Without that, the start fails with `Cannot find module '/app/scripts/docker-start.mjs'`.
Behind a reverse proxy, put your keys in `.env` or pass them as environment variables - Provider Settings refuses proxied requests on purpose - and add the hostname you serve it on, for example `GEV_ALLOWED_HOSTS=globe.example.lan` (see [Sharing an instance](#-sharing-an-instance)).
</details>
@@ -229,6 +234,41 @@ the container.
</details>
### Path 4 — GitHub Codespaces (cloud, no local install)
Runs the same dev server in a cloud workspace, so Node and the dependencies
never touch your machine. The globe still renders in your own browser, so it
needs the same graphics there as any other path. GitHub Free includes 120
core-hours a month with no card on file — about 60 hours on the default 2-core
machine. Org and Enterprise plans include no free quota; current terms and
quotas apply.
1. Fork this repository, then open **your fork** on GitHub.
2. Click the green **Code** button, the **Codespaces** tab, then **Create codespace on main**.
3. In the codespace terminal:
```bash
nvm install 24 && nvm use 24
npm ci
npm run doctor
GEV_ALLOWED_HOSTS="$CODESPACE_NAME-4173.$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN" npm run dev
```
4. Open the **Ports** tab, find `4173`, and click the globe icon to open it in
the browser. The port is private by default, so the URL asks for your GitHub
login. Keep it private: a public port lets anyone use the app and the keys
in your `.env`.
`GEV_ALLOWED_HOSTS` there is required, not optional. The dev server answers only
the hostnames it trusts, and a forwarded port arrives on
`$CODESPACE_NAME-4173.$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN`. Without it the
page is `Blocked request. This host is not allowed.` Leading-dot wildcards are
ignored on purpose, so pass that exact name.
⚠️ **A codespace is reachable from the internet.** Treat it as the
wider-publish case in [Sharing an instance](#-sharing-an-instance). Provider
Settings does not open on the forwarded URL, so put keys in the workspace
`.env` by hand. `.env` is gitignored — never commit it. [SECURITY.md](SECURITY.md) has the
threat model.
### Then power it up — in the app, not in a file
Keys are upgrades, not prerequisites. When you want one, click the **POWER UP**
@@ -290,14 +330,14 @@ Choose a first-run mission, or try these in order. The GIFs show Google Photorea

6.**Switch the optics.** Tap `1`–`7` — CRT, NVG, FLIR — and the whole live planet re-renders through a different sensor.
6. **Switch the optics.** Tap `1`–`6` — CRT, NVG, FLIR — and the whole live planet re-renders through a different sensor.

7. **Talk to it** _(needs an OpenAI key)_: _"Take me to LAX and select the nearest airborne aircraft."_
8. **Come home.** Hit **Reset Globe** — or just say _"zoom out to a globe view."_
> 🗣️ _"Outline the state of Texas."_ · _"Annotate the Texas State Capitol and its grounds"_ — it draws the **actual enclosing boundary**, not a circle. · _"How far is the Eiffel Tower from the Louvre?"_ — a connector arrow appears and it speaks the distance. Everything persists until you say _"clear the map."_
**✍️ Or draw it yourself** — DISPLAY ▸ **Draw**: pick Area, Line or Pin, click the vertices on the real world, double-click to finish, label it. Same whiteboard, same persistence, no microphone needed.
**✍️ Or draw it yourself** — UTILITIES ▸ **Draw**: pick Area, Line or Pin and a colour, click the vertices on the real world, double-click to finish, then name, recolour, hide or fly to each shape from its row. Same whiteboard, same persistence, no microphone needed.

@@ -395,7 +435,7 @@ Twenty layers and map sources. **Eighteen have a keyless path.** Some offer addi
| 🌍 **Earthquakes** | Global seismic activity, last 24h | USGS | 🟢 |
| ◍ **Air quality** | Live AQHI on the published 1–10+ health-risk scale, with a Weather-panel card showing the band legend and the highest readings in view. **Canada only** — ECCC is the first registered provider | ECCC MSC GeoMet | 🟢 |
| 🚗 **Traffic** | Simulated vehicles on OSM roads. With TomTom, live flow speeds drive the simulation and congestion colors below ~8 km; individual vehicle positions are not live observations | TomTom + OSM | 🟢 simulation · 🟡 live flow speeds |
| 📹 **CCTV Mesh** | ~4,000 public cameras (shared cap) projected _into_ the 3D space — Austin · Texas (TxDOT) · California (Caltrans) · London (TfL) · Ontario (511, free developer key) · Finland (Fintraffic) · British Columbia (DriveBC) · Estonia (Tallinn, Tarktee) · Delaware (DelDOT live video) · New South Wales (Live Traffic NSW) · Calgary · Norway (Statens vegvesen) · Toronto · King County (WA) · Washington (WSDOT) · Catalonia (Servei Català de Trànsit) · Hong Kong (Transport Department) · Taiwan (Freeway Bureau, MOTC), plus opt-in New York City (NYC DOT TMC, needs your own NYC DOT agreement), opt-in Lithuania (Via Lietuva / Eismoinfo, photo licence unconfirmed) and opt-in Florida (FL511, individual personal use only; other use needs FDOT's written consent). Positions are published; poses are estimated priors **you calibrate by dragging a gizmo on the camera itself** | City APIs | 🟢 |
| 📹 **CCTV Mesh** | ~4,000 public cameras (shared cap) projected _into_ the 3D space — Austin · Texas (TxDOT) · California (Caltrans) · London (TfL) · Ontario (511, free developer key) · Finland (Fintraffic) · British Columbia (DriveBC) · Estonia (Tallinn, Tarktee) · Delaware (DelDOT live video) · New South Wales (Live Traffic NSW) · Calgary · Norway (Statens vegvesen) · Toronto · King County (WA) · Washington (WSDOT) · Catalonia (Servei Català de Trànsit) · Hong Kong (Transport Department) · Taiwan (Freeway Bureau, MOTC) · Italy (A22 Brennero motorway), plus opt-in New York City (NYC DOT TMC, needs your own NYC DOT agreement), opt-in Lithuania (Via Lietuva / Eismoinfo, photo licence unconfirmed) and opt-in Florida (FL511, individual personal use only; other use needs FDOT's written consent). Positions are published; poses are estimated priors **you calibrate by dragging a gizmo on the camera itself** | City APIs | 🟢 |
| 📷 **Mapped ALPR Cameras** | License-plate-reader camera locations tagged by OpenStreetMap contributors, loaded one city-sized view at a time, with **SHOW NEAREST**. Locations and tags only: no plate data, no video | OpenStreetMap (incl. DeFlock mapping) | 🟢 |
| 📷 **Street Level** | Street-level photos behind one panel, one chip per provider: coverage on the streets, image cones, an embedded viewer with full-screen **EXPAND**. Mapillary, Google Street View, KartaView and Panoramax | Mapillary · Google · KartaView · Panoramax | 🟢 KartaView, Panoramax · 🟡 Mapillary token · 🔴 Google Maps key |
| 📻 **Radio** | Geolocated world radio with an **analog tuner** — drag the needle across up to 750 stations and the globe flies to each broadcaster | Radio Browser / broadcasters | 🟢 |
@@ -411,7 +451,7 @@ Twenty layers and map sources. **Eighteen have a keyless path.** Some offer addi
**Weather, keyless.** **Wind** animates 10 m forecast flow from NOAA GFS or ECMWF IFS, with an optional color field and a reading at map center. **Rain radar**, **Satellite clouds** and **Lightning density** are observations on one history timeline in the **WEATHER** panel (step back, **Play**, **Latest**); **Cyclone advisories** draw NHC and CPHC positions, forecast tracks and cones. On Google 3D Tiles the observed layers float above the city as translucent shells, sharpened around your view.
In the app, **Data Layers** groups them as Movement, Cameras, Infrastructure, Events, Weather and Utilities.
In the app, **Data Layers** groups them as Movement, Cameras, Infrastructure, Events, Weather and Audio; Directions sits in the separate **Utilities** panel with Draw and Import.
**The basemap ladder — what each tier buys you:**
@@ -487,12 +527,12 @@ How the globe handles live data:
- **Sits on the real ground.** Entity heights are aligned to work with Google 3D tiles, so aircraft park on aprons and cameras stand on street corners instead of floating.
- **Caching and request budgets.** An OpenSky credit governor, a TomTom daily tile budget, and disk-cached TLEs reduce repeated requests. These controls do not replace provider quotas or billing controls.
- **Server-side credentials.** Every API that touches a private key (OpenAI, AISStream, OpenSky OAuth, camera frames) is brokered through a hardened server-side proxy with SSRF protection, response caps, and sanitized errors. The only keys the browser sees are Google Maps and Cesium ion (restrict both at the provider).
- **No framework.** Vanilla JavaScript, **CesiumJS**, and **Vite** — plus **Google Photorealistic 3D Tiles** for the planet and the **OpenAI Realtime API** for voice. Fast to read, fast to hack on.
- **A small stack.** **Vue 3** and **Pinia** for the interface, **CesiumJS** for the globe and **Vite** to build it — plus **Google Photorealistic 3D Tiles** for the planet and the **OpenAI Realtime API** for voice. Fast to read, fast to hack on.
```
src/
├── main.js # Bootstrap: Google 3D tiles, layer registration
God's Eye View is a local-first client for **public** data. It is built for exploration, demos, and learning — not as a hardened production service. This document explains the security model so you can run it safely and report issues responsibly.
Vite's development file server also denies `.gev-logs/`, `.gev-cache/`, and
the package checkout's root `dist/` directory. Dependency `dist/` directories
remain available. This filesystem deny list does not filter `public/` assets or
preview output; never put private artifacts in a public asset directory.
## Reporting a vulnerability
Please report security issues **privately** — do not open a public issue for anything exploitable.
- Hold rendering only for animated content (asturksever, #1281).
- Exercise current vector road sources and published camera bounds in Traffic fixture QA (asturksever, #1281).
- Establish browser-level foreground visibility for Traffic QA and reject hidden-page samples (asturksever, #1281).
- Supply CORS headers on cross-origin traffic fixtures and report browser exceptions without disabling browser security (asturksever, #1281).
- Isolate the Traffic keyless browser scenario from the keyed page’s workers and intercepted cache (asturksever, #1281).
- Restore minimized test windows before screenshots so native compositor pauses cannot stall browser acceptance (asturksever, #1281).
- Intercept only Traffic provider URLs, leaving Cesium worker and module loading untouched (asturksever, #1281).
- Settle an unfinished Traffic height correction when the last animated point leaves, so the layer stops reporting loading once its render hold is released (asturksever, #1281).
- Pause periodic network updates in hidden tabs; the overdue update on return restarts the polling schedule, so the next poll follows one interval later even when that update fails (asturksever, #1282).
- Report and guard browser visibility during Transit QA so hidden-page polling pauses cannot masquerade as playback regressions (asturksever, #1282).
- Require each live city to supply its own feed vehicles in Transit QA; retained vehicles cannot qualify another city (asturksever, #1282).
- Bound the Transit card-anchor probe by observed poll coverage instead of a fixed 20-second window (asturksever, #1282).
- Keep visible-page Transit probes focused through the browser testing API (asturksever, #1282).
- Add a real-browser regression gate for hidden-tab periodic work, explicit refresh, resume and teardown (asturksever, #1282).
- Restore minimized test windows before screenshots so native compositor pauses cannot stall browser acceptance (asturksever, #1282).
- Run Space Missions' Replay Ascent countdown from T−10 after the five-second
tile hold; the hold ran 10 s and the countdown started at T−05 (Aditya,
@Rohilalala, #1443).
Loaded 100 of 1454 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.