diff --git a/.env.example b/.env.example index 55cb367a0..c9700c5be 100644 --- a/.env.example +++ b/.env.example @@ -115,7 +115,8 @@ GEMINI_LIVE_MODEL=gemini-3.8-live # Client Token). Client-exposed by design, like GOOGLE_MAPS_API_KEY. MAPILLARY_CLIENT_TOKEN= -# Optional: OpenSky auth mode (oauth | basic | auto | anon) +# Optional: OpenSky auth mode (oauth | auto | anon). OpenSky retired +# username/password (basic) sign-in on 2026-03-18; "basic" now loads anonymously. # Default: oauth — requires client credentials below # Set to "anon" to skip aircraft auth (rate-limited but works) OPENSKY_AUTH_MODE=oauth @@ -128,7 +129,9 @@ OPENSKY_CLIENT_SECRET= # Optional paid Patreon tokens raise request limits; there is no free API key. LL2_API_TOKEN= -# Optional: OpenSky credentials JSON file path (alternative to above) +# Optional: OpenSky credentials JSON file path, used when the two values above +# are empty: the credentials.json your OpenSky account page downloads when you +# create an API client. Use an absolute path for compatibility with launchers. # OPENSKY_CREDENTIALS_FILE=/path/to/credentials.json # Optional: dev server config @@ -253,6 +256,8 @@ AISSTREAM_API_KEY= # CCTV_TAIWAN_ENABLED=1 # set 0 to skip Taiwan Freeway Bureau (MOTC) road cameras # CCTV_TAIWAN_MAX_SOURCES=600 # cap Taiwan freeway cameras (1-2000); no API key required # CCTV_TAIWAN_URL= # override the Freeway Bureau MOTC 2.0 CCTV.xml URL +# CCTV_A22_ENABLED=1 # set 0 to disable the Autostrada del Brennero (A22, Italy) webcams +# CCTV_A22_URL= # override the Open Data Hub A22 webcam catalog URL # CCTV_MAX_SOURCES=4000 # catalog-wide ceiling, shared round-robin across packs # CCTV_PREFER_AUSTIN=1 # prioritize Austin cameras in the registry # CCTV_FORCE_AUSTIN= # force live default sources when a source-pack JSON is configured diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..e8c9f8bdf --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,59 @@ +## What this changes + + + +## How I verified it + +- [ ] `npm run build` +- [ ] `npm test` +- [ ] `npm run test:track` (dev server running) +- [ ] Feature gate for the area I touched: + + + +## New layer checklist + + + +**Screenshots** + + + +**Recording** + + + +**Proposed category** + + + +Category: + +**Data source and licence** + +- Source (provider and endpoint): +- Licence or terms (link): +- Attribution shown in the app: +- Needs a key? (none / optional / required): +- Fetched at runtime, or bundled in the repo: + + + +- [ ] Added a row to `DATA_SOURCES.md` +- [ ] Credit is registered in `src/data/dataCredits.js` +- [ ] Share-link token assigned with `npm run layer-token:next`, if the layer is shareable + +## Docs + +- [ ] `docs/CURRENT-STATE.md` updated and a `changelog.d/-.md` fragment added, or this PR doesn't change runtime behaviour diff --git a/.prettierignore b/.prettierignore index 1aaad8a03..2b2cc755a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -20,3 +20,8 @@ node_modules .vite .vite-* *.min.js + +# Application views, and panels lifted from them, hold the page markup +# verbatim; reflowing it changes the DOM +src/ui/views/*View.vue +src/ui/views/panels/ diff --git a/.prettierrc.json b/.prettierrc.json index b1e6b2baf..095bf4e98 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -2,5 +2,11 @@ "singleQuote": true, "tabWidth": 2, "semi": true, - "endOfLine": "lf" + "endOfLine": "lf", + "overrides": [ + { + "files": "*.vue", + "options": { "htmlWhitespaceSensitivity": "strict" } + } + ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ad3a7368..356289841 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,707 @@ # Changelog +## [Unreleased] + +## [0.3.0] — 2026-10-27 — Street Level, On Your Phone, in Your Language + +- Show a Street Level view facing 359.5° or more as 0°. The viewer caption + rounded the bearing without wrapping, so a view facing 359.6° read "360°" + (Aditya, @Rohilalala, #1431). + +- The visual styles and their presets mark their labels with the catalog-free + `msg()`, so the share-link codec, and any server that loads it, no longer + bundles every translation catalog (about 0.9 MB) (samehkhamis, #1421). + +- Keep a location search that found nothing, or failed, in the field so it can + be corrected: the query stays, focused and selected, and a persistent + announced message under the search row says what to do next, instead of a + toast followed by a cleared and closed field (samehkhamis, #1420). + +- Hosts can read share links on their server: the share-link codec and the + `/share/` address helpers are now package exports, + `@gods-eye-view/core/share/view-codec` and `@gods-eye-view/core/share/link-path` + (samehkhamis, #1419). + +- Share links are now short: the share button makes `/share/` links + that open on any copy of the app, follow the aircraft, military aircraft or + satellite you were following, and fall back to its last-known position with + a notice if it isn't reporting. A mistyped link is refused instead of + opening a different view. Long `#v=2` links keep working (samehkhamis, #1418). + +- Make Clear All clear the saved and shared layer selection. Layers that a + context mode had already switched off no longer stay in the share link and + come back on reload (samehkhamis, #1417). + +- Restore a share link opened in an already-open tab. Pasting a link into the + address bar, following one, or using Back/Forward now moves the camera and + restores the style, map, panels and layers instead of changing only the URL + (samehkhamis, #1416). + +- Add a Scope checkbox and extend feathering to 200%, retaining scope settings + across HUD layouts, share links and scenes. First run opens with Scope ON at + 100% feather; explicit settings and historical links retain their values. Give + Tactical, Operator and Minimal text black backplates only when Scope is OFF, + including vertical metadata, without changing the original HUD text color, glow or + opacity. Scope ON retains its original HUD presentation (bilawalsidhu, #1410). + Keep title and tagline positions stable when toggling Scope, include the eye + in the title backing, and brighten the two secondary labels only with Scope OFF. + +- Remove four modules nothing in the app imports: `src/app/sourceSlot.js`, + `src/data/flowTiles.js`, `src/data/issPass.js` and `src/data/localGeojson.js`. + The local GeoJSON layer tests now build layers the way the catalog does, so + they keep covering `localGeojsonCore.js`. Package exports are unchanged + (samehkhamis, #1403). + +- Aircraft on the ground keep getting ground-height samples after the + computer's clock steps back, instead of waiting for it to catch up + (quanticsoul4772, #1399). + +- The Sharpen slider at 0 now turns the sharpen pass off instead of still + sharpening at a low amount (quanticsoul4772, #1398). + +- Show a locally heard aircraft tracking 359.5° or more as TRK 0°. The Local + ADS-B card rounded the track without wrapping, so 359.7° read "TRK 360°" + (Rudy Celekli, @rudycelekli, #1396). + +- Report a fire detection's acquisition time as a UTC timestamp in + `get_active_fires`. The tool joined FIRMS's unpadded date and time, so a + 00:45 UTC detection read "2026-01-01 45"; it now reads + `2026-01-01T00:45:00.000Z`. Missing or invalid dates and times remain + unknown rather than rolling into another calendar day + (Rudy Celekli, @rudycelekli, #1395). + +- Report Docker container health with a local HTTP check using Node, with the + same port parsing as the launcher (John Rogers, @14traits, #1394). + +- Start the Docker install on SELinux hosts such as Fedora and RHEL. Compose now + mounts the checkout with `:z`; without it the container could not read `/app` + and stopped with "Cannot find module '/app/scripts/docker-start.mjs'" + (Anıl Güvenç, @anilguvenc, #1391). + +- Render awareness-overlay bearings and courses that round through north as + `000°`, rather than `360°` (Rudy Celekli, #1390). + +- Show cockpit wind from 359.5° or more as 0°, not 360°. Rounding 359.6° + printed "N · 360°" on the local wind direction + (Rudy Celekli, @rudycelekli, #1389). + +- Show a drawn line of 999.5 m or more as 1.0 km. The draw hint kept metres + below 1000, then rounded 999.6 m up to "1000 m" + (Rudy Celekli, @rudycelekli, #1388). + +- Show a Contacts entry 999.5 m or more away as 1.0 km. The panel kept meters + below 1000, then rounded 999.6 m up to "1000 m" + (Rudy Celekli, @rudycelekli, #1387). + +- Show a route leg of 995 m or more as 1.0 km. The route summary and step + cards kept meters below 1000, then rounded 996 m up to "1000 m" + (Rudy Celekli, @rudycelekli, #1386). + +- Show a route leg of 59.5 seconds or more as 1 min. The route summary and step + cards kept seconds below 60, then rounded 59.6 s up to "60 s" + (Rudy Celekli, @rudycelekli, #1385). + +- The HUD's AI place summary drops a reply that arrives after the camera has + started moving, so a summary asked for before a flight no longer names the + old place mid-flight. Cancelled or partially typed summaries retry even if the place + context stays the same (quanticsoul4772, #1384). + +- Reduce repeated work in aircraft detection candidates by caching unchanged + callsign/registration text and reusing resolved fleet models. Metadata updates, + live positions and tracked-aircraft/model handoffs remain immediate + (samehkhamis, #1379). + +- Increase touch-first globe zoom sensitivity to three times the default while + retaining Cesium collision and distance limits. +- Reuse one delayed display timestamp per civil-aircraft fleet update and cache + extrapolation coordinate frames until their fix coordinates change. Preserve + contact counts, movement cadence, tracked motion and ground corrections. + +- Turning a layer off frees the memory its entities held. Cesium kept + removed layers' draw commands in reused per-pass arrays, so Data Centers + kept 116 MiB after it was switched off, submarine cables 74 MiB and dams + 30 MiB; they now keep 13, 9 and 9 MiB (quanticsoul4772, #1367). + +- Aircraft 3D models (Flights, Military Flights, Local ADS-B) skip Cesium's + dynamic environment map, a sky render and GPU readback per model. With + ~150 models in the scene at Atlanta on Google 3D, GPU time per frame at + rest falls from 14.3-14.8 ms to 12.1-12.5 ms; a same-model comparison + showed no visible change (quanticsoul4772, #1366). + +- Drop the decimal when fire radiative power rounds to 10 MW. A 9.96 MW + detection was labeled 10.0 MW because the one-decimal cutoff ran before + rounding (Rudy Celekli, @rudycelekli, #1354). + +- Count a transit report that is 90 seconds old as one minute. The card kept + seconds through 89s, then rounded 90s up to "Reported 2 min ago" + (Rudy Celekli, @rudycelekli, #1353). + +- Read a destination heading that rounds to north as 000. 359.6° was shown as + 360° on the destination bearing while the compass tape already uses 000 + (Rudy Celekli, @rudycelekli, #1352). + +- Keep a tracked vessel's speed, course, heading and fix time when AISStream + static data arrives; the static message only merges name, type, destination + and IMO (rudycelekli, #1351). +- Apply changed static metadata immediately, retaining known fields when a + later static report omits them, without refreshing the vessel's position + (samehkhamis, #1351). + +- Convert NHC cyclone movement speed from miles per hour to knots, so + `movement.speedKt` in cyclone query results is no longer about 15% high + (rudycelekli, #1350). + +- Label NHC cyclone classifications by their published meanings: `PTC` is a + post-tropical cyclone, `PC` a potential tropical cyclone, and subtropical + systems arrive as `STS` and `STD` (rudycelekli, #1349). + +- Wake staged weather imagery on a 150 ms timer while waiting for readiness, + avoiding a continuous render loop and cancelling the wake when work ends + (@nu-stats, #1345). + +- Phrase slight and sharp ramps as directions, so a ramp reads "slightly left" + instead of "on the slightly left", and a lane names its side in both + the proxy response and localized browser instructions + (Rudy Celekli, @rudycelekli, #1343). + +- Keep an impossible wildfire acquisition date off the detection clock, so a + fire is not labeled with a later day it did not happen + (Rudy Celekli, @rudycelekli, #1342). + +- Reject impossible KartaView photo timestamps, so 31 February is unknown + instead of rolling forward into March and passing the imagery date filter + (Rudy Celekli, @rudycelekli, #1341). + +- Read a hemisphere letter that is separated from its number, so a search for + `40.7128 N 74.0060 W` flies there instead of falling through to the geocoders + (Rudy Celekli, @rudycelekli, #1340). + +- Carry HUD LAT/LON seconds that round up to 60.00" into the minutes and + degrees, so longitude -122.35 reads `122°21'00.00"W` instead of + `122°20'60.00"W` (rudycelekli, #1339). + +- Improve touch selection of aircraft and vessels through the shared contact + picker, reveal tracked aircraft details when opening Context, support downward + swipe dismissal of phone drawers while preserving scrolling and controls, and + enlarge the phone logo (samehkhamis, #1338). + +- Evict terrain-height cache entries in fetch-time order without repeatedly + sorting the full cache, while keeping refreshed points newest and ordering + restored entries by their fetch time (daikaginza, #1326). + +- Add a phone layout for viewports 720px and narrower: the globe stays full + screen and a bottom tab bar (Layers, Cameras, Context, Search, More) opens one + panel sheet at a time, keeping attribution visible and scene STOP reachable. + Adapted from the phone tab-shell in #203 by blearymoth (samehkhamis, #1325). + +- Export the KartaView and Panoramax Street Level providers, so a host composing + its own provider list can offer the keyless providers without Mapillary + (samehkhamis, #1324). +- A Mapillary source can take a `graphEndpoint`, a Graph API base that adds the + token itself (such as a same-origin proxy). Its sequence lookups go there + through the source's transport, and the photo viewer uses MapillaryJS's + Graph provider at that endpoint, so no client token is needed in the page + (samehkhamis, #1324). + +- Remove dead code across the repository: three modules nothing imports + (`src/ui.js`, `src/standalone/ui.js`, `src/data/localLayers.js`), exports + and locals nothing reads, a duplicate copy of the bundled dams data and an + unused ship model. The app no longer builds an unused default wind layer + and Bhote Koshi locator layer when it loads. Package exports are unchanged + (samehkhamis, #1321). + +- Validate provider response formats, strengthen camera source checks, and apply consistent API response protections (samehkhamis, #1317). + +- Clarify shared field CSS ownership: CCTV owns calibration editor sizing, + while Radio and Cyber supply explicit field palettes without changing the + current appearance or native control behavior (mazy1998, #1309). + +- Share read-only status badge presentation across CCTV source/frame labels, + calibration provenance and Weather coverage, preserving status meanings, + palettes, translations and announcements while preparing the view contract + for a future Vue migration (mazy1998, #1306). + +- Share native dropdown and numeric-field styling across Scene, CCTV, Display + and Radio controls, with a shared CCTV number-input builder that preserves + existing precision, validation and commit behavior (mazy1998, #1305). + +- Point distances wrap longitude differences across the antimeridian, in voice + proximity checks and `places_nearby` (quanticsoul4772, #1304). + +- `get_wind` flags stale forecasts (`stale`) when the wind proxy serves a + cached model run (quanticsoul4772, #1303). + +- Give ordinary Scene and CCTV actions shared native buttons with consistent + default, destructive and compact styling while preserving their existing + behavior and specialized controls (mazy1998, #1302). + +- Unify segmented selectors for model coverage, detection allocation, drawing + shape, Street View and imagery comparison while preserving their existing + behavior. Keep compact labels such as FILL and FLAT on one line, and let + collapsed Weather release space to the open rail panel. Reserve the full mode + button height in Recent Imagery so keyboard focus is not clipped (mazy1998, #1301). + +- Add a shared Voice Preview and Settings panel with browser-local preferences, + inactivity controls and accessible floating provider controls on compact layouts; + recover inactivity after confirmed audio-less replies, release retired audio + observers and keep cost warnings visible outside Settings + (manjunath22466, #1299). + +- Add Simplified Chinese (zh-CN) to the language picker, with localized dates + and readable Chinese labels. Based on translations by @abiyeenzo (#914) + and @EDIT-Aeterna (#979). + (German, @gekh, #1298). + +- Add the S2 GhostMaps layer: S2 Underground's public KMZ intel products + (Common Intelligence Picture, 30-day kinetic pull, Border Crisis Map, Iran + 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/-.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 + `src/geoDistance.js` (`greatCircleKm`/`greatCircleMeters`, `{lat, lon}` + points). The Earth radius is standardized on 6371.0088 km, so distances shift + by about 1.4 ppm and region areas by about 2.8 ppm, and missing or + out-of-range points return `Infinity` (onionviolet, #287; Rajojha-1, #749). + - 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. ## [0.1.0] — 2026-02-09 - Initial project version. + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 442b9c1bf..d8e569d3a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -108,15 +108,15 @@ 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//`, 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//`, 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). ## Architecture in one minute -- **No framework.** Vanilla JS + [CesiumJS](https://cesium.com/platform/cesiumjs/) + [Vite](https://vitejs.dev/). -- **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/ui.js` and `src/data/.js` entries retain compatibility; new code belongs with its focused owner. +- **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/.js` entries retain compatibility; new code belongs with its focused owner. - Sources acquire records; renderers own Cesium resources. Import `gods-eye-view/layers//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. - `docs/CURRENT-STATE.md` is the authoritative runtime reference — read it first. @@ -126,9 +126,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 `` to + `body`, a fixed id or a container the owner publishes; don't move nodes by + hand. What stays imperative: classes on `` and ``, 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 + `node --import=./build/registerVue.js --test `. Don't pass happy-dom + 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 +209,33 @@ 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/-.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 Utilities). 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. ## Maintainers diff --git a/DATA_SOURCES.md b/DATA_SOURCES.md index 6d2ee498b..a4035f175 100644 --- a/DATA_SOURCES.md +++ b/DATA_SOURCES.md @@ -38,6 +38,7 @@ How to read this: | **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)" | +| **S2 Underground GhostMaps** (`github.com/s2underground/GhostMaps`) | S2 GhostMaps layer: the current edition of S2's KMZ intel products (Common Intelligence Picture, 30-day kinetic pull, Border Crisis Map, Iran and Yemen products, newest incident drop). Keyless. `/api/ghostmaps` reads the branch head from git's ref advertisement every 15 s (`GHOSTMAPS_POLL_SECONDS`, minimum 5 s), lists the tree through the GitHub REST API only when the head changes (optional `GITHUB_TOKEN`), downloads files by commit-pinned raw URLs into `.gev-cache/ghostmaps/` and pushes new catalogs to browsers over server-sent events | [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/) (repository README: "S2 Underground © 2025 by S2A1"); non-commercial, attribution required; files are fetched at runtime, never stored in this repository | "S2 GhostMaps intel products: S2 Underground © by S2A1, CC BY-NC-SA 4.0" — shown in-app | | **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)" | | **OpenFreeMap / OpenMapTiles** | Vector tiles | OpenStreetMap data under ODbL 1.0 | OpenFreeMap © OpenMapTiles | | **Overture Maps Foundation** | Military area names pack | [ODbL 1.0](https://opendatacommons.org/licenses/odbl/1-0/) | Overture Maps Foundation | @@ -68,6 +69,7 @@ How to read this: | **Kartverket** (`ws.geonorge.no/hoydedata/v1/punkt`) | Terrain heights (NN2000) used as the ground prior for Statens vegvesen cameras | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) — attribution REQUIRED | "© Kartverket", modified (heights over water set to 0 m, rounded to 0.1 m) | | **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" | +| **Autostrada del Brennero (A22)** (catalog `tourism.api.opendatahub.com`, frames `www.autobrennero.it/WebCamImg/`) | CCTV camera catalog + frames, A22 Brenner–Modena motorway, Italy | Public Autobrennero webcam images; camera list republished by the Open Data Hub, whose records are tagged CC0 (`ClosedData: false`, licence holder Autobrennero); Autobrennero publishes no terms of its own for the images | "Webcam images © Autostrada del Brennero S.p.A., via the Open Data Hub (NOI Techpark)" (courtesy) | | **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,9 +177,10 @@ 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 `tmcdot@dot.nyc.gov` 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//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.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 malformed or empty successful payloads are rejected atomically. 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. +- **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. - **TomTom Traffic.** Optional and BYOK: without `TOMTOM_API_KEY` the traffic layer runs its built-in simulation and no TomTom data (or attribution) appears. With a key, flow vector tiles are fetched through the server-side `/api/tomtom` proxy (120 s cache + a daily tile-budget governor — `TOMTOM_DAILY_TILE_BUDGET`, default 6,000 — sized so a 31-day month stays inside TomTom's 200K monthly free allowance; still a configurable application safety ceiling, not a guarantee; TomTom's [current pricing](https://docs.tomtom.com/pricing/) lists 200K free tile requests per month) and the "Traffic flow data © TomTom" credit is registered in the Data attribution popover the moment live mode activates. TomTom data is served live and cached only transiently (≤120 s TTL under `.gev-cache/`, gitignored) — it is not bundled or redistributed. One 23 KB point-in-time tile snapshot is committed as a decode-test fixture (`src/data/fixtures/`, © TomTom, never served to the app). - **Routing (OSRM / FOSSGIS).** `/api/route` proxies the public OSRM instances FOSSGIS e.V. runs for the OpenStreetMap community (car, bike, foot profiles). They are keyless and intended for fair, interactive use — the proxy rate-limits clients (60/min, 200/min total), caps route span and response size, caches each route for 10 minutes, and identifies itself in the User-Agent. Heavy or automated use must run its own OSRM. Routes are computed from OpenStreetMap data (ODbL); the Directions layer and voice routes never redistribute them. A route the router cannot find is reported as such; the Directions layer draws nothing in that case, and voice routes fall back to a clearly labeled straight line. - **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. diff --git a/Dockerfile b/Dockerfile index d34f4887a..433d170a8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,6 +39,11 @@ COPY --chown=node:node . . ENV HOST=0.0.0.0 ENV PORT=4173 EXPOSE 4173 +# node:slim has no curl, so probe with Node's built-in fetch. 127.0.0.1 is +# always on the Host allowlist, and the generous start period covers Vite's +# first-run dependency optimisation. +HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ + CMD ["node", "-e", "fetch('http://127.0.0.1:'+(Number.parseInt(process.env.PORT,10)||4173)+'/').then((r)=>process.exit(r.ok?0:1),()=>process.exit(1))"] # The launcher is `npm run dev` plus one step: it trusts the container's # gateway address for Provider Settings, because that is where Docker delivers # the host's own browser from (never 127.0.0.1). See scripts/docker-start.mjs. diff --git a/README.md b/README.md index 00fb9c24f..3a4c5fc43 100644 --- a/README.md +++ b/README.md @@ -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. @@ -111,13 +111,15 @@ app's **POWER UP** panel; [Keys & Costs](#-api-keys) explains the options. Available on **Windows, macOS, and Linux**. The Pinokio maintainer reports cross-platform testing of the fixed installer. The launcher installs the -locked dependencies, finds a free local port, and opens the app. +locked dependencies, finds a free local port, and opens the app in your system +browser. The Server menu item keeps the launcher console available. **Tried before and installation failed?** Update Pinokio and try again. Version 8.2 fixes the launcher installation issue; [details from the Pinokio maintainer](https://pinokio.co/posts/01m1m4p9xxm3qw7dnnpj2wr93g). -**Pinokio opens but the globe is very choppy?** Before treating that as a GPU +**Using an older launcher or opening the embedded view manually?** If the globe +is very choppy, before treating that as a GPU compatibility or minimum-hardware problem, copy the local server URL shown by Pinokio and open the same URL in your normal hardware-accelerated browser. One Pinokio 8.2.0 user reported severe stutter in Pinokio's embedded view that went @@ -177,6 +179,16 @@ Publishing it wider is the [Sharing an instance](#-sharing-an-instance) opt-in, When you widen the publish, also set `GEV_KEY_SETUP_TRUSTED_PEERS=none` so the launcher trusts no gateway address. +```bash +GEV_KEY_SETUP_TRUSTED_PEERS=none docker compose up +``` + +Compose forwards `GEV_KEY_SETUP_TRUSTED_PEERS` and `GEV_ALLOWED_HOSTS` from your +shell or the project's `.env`; an exported shell value takes precedence. You can +also put `GEV_KEY_SETUP_TRUSTED_PEERS=none` in `.env` for a persistent setting. +Unset or blank trusted peers retain automatic gateway discovery; `none` explicitly +disables it. The startup log confirms that no additional peers are trusted. +
Running on Linux, a NAS, or behind a reverse proxy @@ -197,6 +209,8 @@ env UID=1026 GID=100 docker compose up -d 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)).
@@ -280,14 +294,14 @@ Choose a first-run mission, or try these in order. The GIFs show Google Photorea ![Tracking the ISS along its orbital path as it crosses over Ukraine](docs/media/14-iss-over-ukraine.gif) -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. ![Cycling a dense live globe through CRT, FLIR, and NVG in one continuous view](docs/media/01-style-sweep.gif) 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."_ -**Keyboard:** `1`–`7` visual styles · `H` HUD · `D` detection · `C` cockpit · `` ` `` frame rate · `Esc` out. Trackpad pinch zooms the globe. +**Keyboard:** `1`–`6` visual styles · `H` HUD · `D` detection · `C` cockpit · `` ` `` frame rate · `Esc` out. Trackpad pinch zooms the globe. --- @@ -333,6 +347,10 @@ Thirty tools, four jobs — the commands below come straight from the product's **🎥 Direct it** — drone-operator camera verbs: +“Rotate the globe” spins Earth to the right continuously; ask for left, a speed, +or stop. If it refuses, follow its reset-and-rotate or further-zoom guidance; +camera input stops rotation. + > 🗣️ _"Take me to Tokyo."_ · _"Orbit around this area slowly."_ · _"Draw the walking route from the Capitol to Zilker Park."_ → _"Fly the route we just drew."_ · _"Zoom out to a globe view."_ **🖊️ Annotate it** — a whiteboard over the real world: @@ -381,7 +399,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 | 🟢 | @@ -473,12 +491,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 -├── ui.js # Runtime UI — panels, HUD, styles, control facade +├── ui/ # Runtime UI — Vue views, Pinia stores, kit, controllers, styles ├── hud.js # Intelligence HUD + AI scene summary ├── keySetup.js # POWER UP panel — in-app provider keys (dev server only) ├── mapStackController.js # Basemap switching — Google 3D / Esri / OSM / ion stacks diff --git a/SECURITY.md b/SECURITY.md index 708bb1583..4423eead3 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,6 +2,11 @@ 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. @@ -100,6 +105,11 @@ The dev server is a **key broker**: every server-side key above is spendable by `compose.yaml` publishes on `127.0.0.1`: **widen that mapping and every LAN client arrives from the same gateway address, which hands them Provider Settings too.** Treat publishing beyond loopback as the LAN opt-in above. + Compose forwards `GEV_KEY_SETUP_TRUSTED_PEERS` and `GEV_ALLOWED_HOSTS` from + the shell or project `.env` before the launcher runs. Set trusted peers to + `none` when widening the publish: it becomes an explicit empty list before + Vite loads configuration, so the gateway cannot regain Provider Settings + access. Unset or blank values preserve automatic gateway discovery. - **Provider-side budgets are the real backstop.** For hard spend protection, configure limits where the money is: OpenAI platform usage limits, Google Cloud budget alerts + per-API quotas, and equivalent controls for any other keyed provider. - **Pinokio LAN and Cloudflare sharing are refused.** The current supported Pinokio release re-reads sharing state when an app registers its Open URL and diff --git a/build/application-html.js b/build/application-html.js deleted file mode 100644 index b14210a56..000000000 --- a/build/application-html.js +++ /dev/null @@ -1,40 +0,0 @@ -import { readFileSync } from 'node:fs'; - -export const APPLICATION_TEMPLATES = Object.freeze([ - 'scene-chrome', - 'cockpit', - 'display-controls', - 'command-dock', - 'layer-panels', - 'context', - 'welcome', - 'provider-settings', - 'hud-loading', -]); -const allowed = new Set(APPLICATION_TEMPLATES); - -/** Expand only known component templates; markers cannot name filesystem paths. */ -export function expandApplicationHtml(html) { - return html.replace( - /^[ \t]*\r?\n?/gm, - (_, name) => { - if (!allowed.has(name)) - throw new Error(`Unknown application template: ${name}`); - return readFileSync( - new URL(`../src/ui/templates/${name}.html`, import.meta.url), - 'utf8', - ); - }, - ); -} - -/** Assemble static application markup before Vite processes scripts and assets. */ -export function applicationHtmlPlugin() { - return { - name: 'application-component-templates', - transformIndexHtml: { - order: 'pre', - handler: expandApplicationHtml, - }, - }; -} diff --git a/build/registerVue.js b/build/registerVue.js new file mode 100644 index 000000000..6db7d017c --- /dev/null +++ b/build/registerVue.js @@ -0,0 +1,8 @@ +import { registerHooks } from 'node:module'; +import { load } from './vueLoader.js'; + +// Lets Node import single-file components: `node --import=`. +// Hosts reach it as `/build/register-vue`. The hooks run on the +// loading thread: off-thread hooks (module.register) would add a round trip +// to every module a test imports, not only the components. +registerHooks({ load }); diff --git a/build/vite.js b/build/vite.js index a02f2340f..fe90501dd 100644 --- a/build/vite.js +++ b/build/vite.js @@ -1,9 +1,30 @@ -import { applicationHtmlPlugin } from './application-html.js'; +import { fileURLToPath } from 'node:url'; import cesium from 'vite-plugin-cesium'; +import vue from '@vitejs/plugin-vue'; +import { VUE_PLUGIN_OPTIONS } from './vueOptions.js'; import { DEFAULT_ALLOWED_HOSTS, hostCheckPlugin } from './allowedHosts.js'; import { embedFramingPlugin } from './embed-framing.js'; import { panelBuildPlugin } from './panel.js'; +/** + * The repository root (this file lives at `/build/vite.js`), as a + * forward-slash path for glob matching. Vite's `fs.deny` patterns are + * matched against the full absolute file path, and a bare `**\/dist/**` + * matches a `dist` directory at *any* depth — including dependencies that + * ship their own, such as `node_modules/vite/dist/client/client.mjs` (the + * dev client itself). Anchoring to this repository's root denies only the + * project's own build output. + */ +const REPO_ROOT = fileURLToPath(new URL('..', import.meta.url)) + .replace(/\\/g, '/') + .replace(/\/$/, ''); + +export function escapeFsDenyPath(path) { + return path.replace(/[()[\]{}*?!+@|]/g, '\\$&'); +} + +const REPO_ROOT_GLOB = escapeFsDenyPath(REPO_ROOT); + /** * Content-Security-Policy for every document the dev/preview server serves. * No inline script is permitted, and the only foreign script origins are the @@ -135,7 +156,8 @@ export function createBrowserViteConfig({ // Next, so no provider route answers a Host the server does not allow. hostCheckPlugin(), cesium(), - applicationHtmlPlugin(), + // Application views are Vue single-file components. + vue(VUE_PLUGIN_OPTIONS), ...plugins, embedFramingPlugin(), panelBuildPlugin(), @@ -165,7 +187,19 @@ export function createBrowserViteConfig({ // build/allowedHosts.js); IP addresses are always accepted. allowedHosts: [...allowedHosts], fs: { - deny: ['.env', '.env.*', '*.{crt,pem}', '**/.git/**', '**/ENVIRONMENT'], + deny: [ + '.env', + '.env.*', + '*.{crt,pem}', + '**/.git/**', + '**/ENVIRONMENT', + '**/.gev-logs/**', + '**/.gev-cache/**', + // Root-anchored: a bare '**/dist/**' would also deny every + // dependency's own dist/ (e.g. node_modules/vite/dist/client), + // breaking /@vite/client and other dev-client assets. + `${REPO_ROOT_GLOB}/dist/**`, + ], }, // These headers protect the document containing Provider Settings and // give the whole page a real Content-Security-Policy (BROWSER_CSP). @@ -177,6 +211,12 @@ export function createBrowserViteConfig({ // framing + CSP hardening (one constant, no drift). preview: { headers: BROWSER_HEADERS }, define: { + // vue-i18n holds the locale and catalogs; Core formats the messages + // (src/ui/i18n.js), so the build drops vue-i18n's message compiler. + __VUE_I18N_FULL_INSTALL__: 'false', + __VUE_I18N_LEGACY_API__: 'false', + __INTLIFY_PROD_DEVTOOLS__: 'false', + __INTLIFY_DROP_MESSAGE_COMPILER__: 'true', 'import.meta.env.GOOGLE_MAPS_API_KEY': JSON.stringify(googleApiKey), 'import.meta.env.CESIUM_ION_TOKEN': JSON.stringify(cesiumToken), // Mapillary client tokens are public by design (see SECURITY.md). diff --git a/build/vueLoader.js b/build/vueLoader.js new file mode 100644 index 000000000..13026dc04 --- /dev/null +++ b/build/vueLoader.js @@ -0,0 +1,170 @@ +import { createHash, randomBytes } from 'node:crypto'; +import { + lstatSync, + mkdirSync, + readFileSync, + renameSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import { createRequire } from 'node:module'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { VUE_TEMPLATE_OPTIONS } from './vueOptions.js'; + +const require = createRequire(import.meta.url); + +// The compiler loads on the first component that is not cached, so a test +// process that imports no views, or only cached ones, never pays for it. +let compiler = null; +const sfcCompiler = () => (compiler ??= require('vue/compiler-sfc')); + +/** + * Compile a single-file component to an ES module for Node, so unit tests (the + * application's and a host's) can import components without a bundler. + * Styles are dropped: tests read the global stylesheet separately. + */ +export function compileVueModule(source, filename) { + const { compileScript, compileTemplate, parse } = sfcCompiler(); + // The template is parsed with the SFC; parse it as the browser build does. + const { descriptor, errors } = parse(source, { + filename, + templateParseOptions: VUE_TEMPLATE_OPTIONS.compilerOptions, + }); + if (errors.length) throw errors[0]; + const id = filename; + if (descriptor.scriptSetup) { + return compileScript(descriptor, { + id, + inlineTemplate: true, + templateOptions: VUE_TEMPLATE_OPTIONS, + }).content; + } + const script = descriptor.script + ? compileScript(descriptor, { id, genDefaultAs: '__sfc__' }).content + : 'const __sfc__ = {};'; + let template = ''; + if (descriptor.template) { + const compiled = compileTemplate({ + source: descriptor.template.content, + filename, + id, + ...VUE_TEMPLATE_OPTIONS, + }); + if (compiled.errors.length) throw compiled.errors[0]; + template = compiled.code; + } + return `${script}\n${template}\nif (typeof render === 'function') __sfc__.render = render;\nexport default __sfc__;\n`; +} + +// The loader's own code is part of every key, so a change to how components +// compile never reuses output from before it. +let loaderFingerprint = null; +const fingerprint = () => + (loaderFingerprint ??= createHash('sha256') + .update(readFileSync(fileURLToPath(import.meta.url))) + .update(readFileSync(new URL('./vueOptions.js', import.meta.url))) + .update(require('vue/package.json').version) + .digest('hex')); + +/** + * Where compiled components are kept between test processes, never a + * package's own tree (a host's checks refuse files inside Core): the system + * temp directory, or on Windows the per-user local app data directory, since + * a temp directory there may be shared and its ownership is not checked. + * GEV_VUE_CACHE=0 compiles every time. + */ +export function vueCacheDirectory( + env = process.env, + platform = process.platform, +) { + if (env.GEV_VUE_CACHE === '0') return null; + if (platform === 'win32') { + return env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, 'gev-vue-sfc') : null; + } + return path.join(tmpdir(), `gev-vue-sfc-${process.getuid()}`); +} + +/** The cache entry for one component: its path, its text and the loader. */ +export function vueCacheKey(source, filename) { + return createHash('sha256') + .update(JSON.stringify([fingerprint(), filename])) + .update('\0') + .update(source) + .digest('hex'); +} + +/** + * Compile a component, reusing a previous process's result for the same text. + * A cache that cannot be read or written only costs the compile. + */ +export function cachedVueModule( + source, + filename, + directory = vueCacheDirectory(), +) { + if (!directory || !ownCacheDirectory(directory)) { + return compileVueModule(source, filename); + } + const entry = path.join(directory, `${vueCacheKey(source, filename)}.mjs`); + try { + return readFileSync(entry, 'utf8'); + } catch { + // Not compiled yet. + } + const compiled = compileVueModule(source, filename); + // Concurrent test processes and threads may compile the same component; + // each writes its own new file and the rename makes the entry appear whole. + const partial = `${entry}.${randomBytes(6).toString('hex')}.tmp`; + try { + writeFileSync(partial, compiled, { flag: 'wx', mode: 0o600 }); + renameSync(partial, entry); + } catch { + // An unwritable directory, or a rename another process holds open, + // leaves the cache without this entry. + try { + rmSync(partial, { force: true }); + } catch { + // A partial file that cannot be removed is only clutter. + } + } + return compiled; +} + +/** + * Cached modules run as code, so the directory must be this user's own: a + * real directory (not a link), owned by the current user and writable by no + * one else. In a shared temp directory another user could otherwise create it + * first and plant modules. On Windows it sits in the user's own profile. + */ +const checkedDirectories = new Map(); +export function ownCacheDirectory(directory) { + if (!checkedDirectories.has(directory)) { + checkedDirectories.set(directory, isOwnDirectory(directory)); + } + return checkedDirectories.get(directory); +} + +function isOwnDirectory(directory) { + try { + mkdirSync(directory, { recursive: true, mode: 0o700 }); + const stats = lstatSync(directory); + if (!stats.isDirectory()) return false; + if (process.platform === 'win32') return true; + return stats.uid === process.getuid() && (stats.mode & 0o022) === 0; + } catch { + return false; + } +} + +/** Synchronous module hook (module.registerHooks): `.vue` loads compiled. */ +export function load(url, context, nextLoad) { + if (!url.endsWith('.vue')) return nextLoad(url, context); + const filename = fileURLToPath(url); + return { + format: 'module', + source: cachedVueModule(readFileSync(filename, 'utf8'), filename), + shortCircuit: true, + }; +} diff --git a/build/vueOptions.js b/build/vueOptions.js new file mode 100644 index 000000000..bd8be66fd --- /dev/null +++ b/build/vueOptions.js @@ -0,0 +1,16 @@ +/** + * Template options shared by the browser build, the boundary check and the + * unit-test loader. Whitespace between nodes is kept (as a single space) so + * an application view renders as its static markup did; Prettier formats + * single-file components with strict whitespace sensitivity to match. Asset + * URLs such as `/pin.svg` stay literal instead of becoming module imports. + */ +export const VUE_TEMPLATE_OPTIONS = Object.freeze({ + compilerOptions: Object.freeze({ whitespace: 'preserve' }), + transformAssetUrls: false, +}); + +/** `@vitejs/plugin-vue` options for application builds. */ +export const VUE_PLUGIN_OPTIONS = Object.freeze({ + template: VUE_TEMPLATE_OPTIONS, +}); diff --git a/changelog.d/1014-director-callback-failures.md b/changelog.d/1014-director-callback-failures.md new file mode 100644 index 000000000..f3877eca4 --- /dev/null +++ b/changelog.d/1014-director-callback-failures.md @@ -0,0 +1,4 @@ +- Director clock listeners, interaction observers and scene release failures + no longer leave the director inconsistent: each scene is released once, and + a phase error is kept (with the release failure logged) when cleanup also + fails (deepeshbind, #1014). diff --git a/changelog.d/1056-area-tool-breakdowns.md b/changelog.d/1056-area-tool-breakdowns.md deleted file mode 100644 index 3ca857a45..000000000 --- a/changelog.d/1056-area-tool-breakdowns.md +++ /dev/null @@ -1,12 +0,0 @@ -- `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). diff --git a/changelog.d/1057-windows-npm-dep0190.md b/changelog.d/1057-windows-npm-dep0190.md deleted file mode 100644 index 1e8e1da55..000000000 --- a/changelog.d/1057-windows-npm-dep0190.md +++ /dev/null @@ -1,4 +0,0 @@ -- 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). diff --git a/changelog.d/1071-geo-uri-search.md b/changelog.d/1071-geo-uri-search.md deleted file mode 100644 index eba251948..000000000 --- a/changelog.d/1071-geo-uri-search.md +++ /dev/null @@ -1,3 +0,0 @@ -- 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). diff --git a/changelog.d/1078-firms-ipv4-fallback.md b/changelog.d/1078-firms-ipv4-fallback.md new file mode 100644 index 000000000..ff4e60e1b --- /dev/null +++ b/changelog.d/1078-firms-ipv4-fallback.md @@ -0,0 +1,4 @@ +- FIRMS fire data loads on hosts where IPv6 is unreachable and IPv4 is slow to + connect: a request whose every address fails to connect retries once over + IPv4, and FIRMS warnings name the underlying connection errors instead of + only `fetch failed` (jeffblakely, #1078). diff --git a/changelog.d/1094-dependency-security-patches.md b/changelog.d/1094-dependency-security-patches.md deleted file mode 100644 index 058359efc..000000000 --- a/changelog.d/1094-dependency-security-patches.md +++ /dev/null @@ -1,4 +0,0 @@ -- 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). diff --git a/changelog.d/1096-military-hold-track-speed.md b/changelog.d/1096-military-hold-track-speed.md deleted file mode 100644 index adc1793c4..000000000 --- a/changelog.d/1096-military-hold-track-speed.md +++ /dev/null @@ -1,3 +0,0 @@ -- 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). diff --git a/changelog.d/1097-overpass-remark-failure-phrases.md b/changelog.d/1097-overpass-remark-failure-phrases.md deleted file mode 100644 index 4543710da..000000000 --- a/changelog.d/1097-overpass-remark-failure-phrases.md +++ /dev/null @@ -1,5 +0,0 @@ -- 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). diff --git a/changelog.d/1101-tle-catalog-number-match.md b/changelog.d/1101-tle-catalog-number-match.md deleted file mode 100644 index d1e2aa407..000000000 --- a/changelog.d/1101-tle-catalog-number-match.md +++ /dev/null @@ -1,3 +0,0 @@ -- 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). diff --git a/changelog.d/1111-map-fallback-retry.md b/changelog.d/1111-map-fallback-retry.md deleted file mode 100644 index 45086eab1..000000000 --- a/changelog.d/1111-map-fallback-retry.md +++ /dev/null @@ -1,5 +0,0 @@ -- 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). diff --git a/changelog.d/1115-aircraft-altitude-band-unknown.md b/changelog.d/1115-aircraft-altitude-band-unknown.md deleted file mode 100644 index a6b69472d..000000000 --- a/changelog.d/1115-aircraft-altitude-band-unknown.md +++ /dev/null @@ -1,2 +0,0 @@ -- Exclude aircraft without a reported altitude from requested altitude bands; - unfiltered aircraft queries still retain them (rudycelekli, #1115). diff --git a/changelog.d/1117-google-viewer-lifecycle.md b/changelog.d/1117-google-viewer-lifecycle.md deleted file mode 100644 index 66d73d4a2..000000000 --- a/changelog.d/1117-google-viewer-lifecycle.md +++ /dev/null @@ -1,3 +0,0 @@ -- Release superseded Google Street View open waits and suppress metadata - requests whose viewer was unmounted while the service loaded (rudycelekli, - #1117). diff --git a/changelog.d/1118-cctv-snapshot-fallback-provenance.md b/changelog.d/1118-cctv-snapshot-fallback-provenance.md deleted file mode 100644 index 648891785..000000000 --- a/changelog.d/1118-cctv-snapshot-fallback-provenance.md +++ /dev/null @@ -1,3 +0,0 @@ -- Label camera-tool Street View fallbacks as historical Google imagery rather - than the current camera view; keep unknown frame provenance explicit - (rudycelekli, #1118). diff --git a/changelog.d/1135-director-save-status-run-results.md b/changelog.d/1135-director-save-status-run-results.md deleted file mode 100644 index aa3c6f0e6..000000000 --- a/changelog.d/1135-director-save-status-run-results.md +++ /dev/null @@ -1,5 +0,0 @@ -- 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). diff --git a/changelog.d/1142-antimeridian-fire-perimeters.md b/changelog.d/1142-antimeridian-fire-perimeters.md deleted file mode 100644 index 8b847163e..000000000 --- a/changelog.d/1142-antimeridian-fire-perimeters.md +++ /dev/null @@ -1,6 +0,0 @@ -- 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). diff --git a/changelog.d/1171-director-goto-shot.md b/changelog.d/1171-director-goto-shot.md deleted file mode 100644 index 4c08351ef..000000000 --- a/changelog.d/1171-director-goto-shot.md +++ /dev/null @@ -1,3 +0,0 @@ -- 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). diff --git a/changelog.d/1172-cctv-missing-numbers.md b/changelog.d/1172-cctv-missing-numbers.md deleted file mode 100644 index d401021e9..000000000 --- a/changelog.d/1172-cctv-missing-numbers.md +++ /dev/null @@ -1,13 +0,0 @@ -- 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). diff --git a/changelog.d/1173-antimeridian-ground-floor.md b/changelog.d/1173-antimeridian-ground-floor.md deleted file mode 100644 index 939559bb5..000000000 --- a/changelog.d/1173-antimeridian-ground-floor.md +++ /dev/null @@ -1,10 +0,0 @@ -- 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). diff --git a/changelog.d/1186-launch-unknown-coordinates.md b/changelog.d/1186-launch-unknown-coordinates.md deleted file mode 100644 index d03c2d902..000000000 --- a/changelog.d/1186-launch-unknown-coordinates.md +++ /dev/null @@ -1,4 +0,0 @@ -- 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). diff --git a/changelog.d/1190-bikeshare-spain-cities.md b/changelog.d/1190-bikeshare-spain-cities.md deleted file mode 100644 index f07faa813..000000000 --- a/changelog.d/1190-bikeshare-spain-cities.md +++ /dev/null @@ -1,6 +0,0 @@ -- 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). diff --git a/changelog.d/1197-launch-library-provider-guidance.md b/changelog.d/1197-launch-library-provider-guidance.md deleted file mode 100644 index 34cbe299e..000000000 --- a/changelog.d/1197-launch-library-provider-guidance.md +++ /dev/null @@ -1,5 +0,0 @@ -- 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). diff --git a/changelog.d/1199-launch-cache-provenance.md b/changelog.d/1199-launch-cache-provenance.md deleted file mode 100644 index 04f33cb7f..000000000 --- a/changelog.d/1199-launch-cache-provenance.md +++ /dev/null @@ -1,3 +0,0 @@ -- 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). diff --git a/changelog.d/1200-cctv-live-icon-color.md b/changelog.d/1200-cctv-live-icon-color.md deleted file mode 100644 index 30d67e1d4..000000000 --- a/changelog.d/1200-cctv-live-icon-color.md +++ /dev/null @@ -1,5 +0,0 @@ -- 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). diff --git a/changelog.d/1201-cctv-stills-aspect.md b/changelog.d/1201-cctv-stills-aspect.md deleted file mode 100644 index fdb560064..000000000 --- a/changelog.d/1201-cctv-stills-aspect.md +++ /dev/null @@ -1,4 +0,0 @@ -- 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). diff --git a/changelog.d/1205-cctv-tool-routing.md b/changelog.d/1205-cctv-tool-routing.md deleted file mode 100644 index ed7552591..000000000 --- a/changelog.d/1205-cctv-tool-routing.md +++ /dev/null @@ -1,3 +0,0 @@ -- Report actual outcomes when voice enables or disables CCTV through the shared - layer lifecycle, including failed and cancelled changes (manjunath22466, - #1205). diff --git a/changelog.d/1209-layer-errors-english-in-data.md b/changelog.d/1209-layer-errors-english-in-data.md new file mode 100644 index 000000000..00360147a --- /dev/null +++ b/changelog.d/1209-layer-errors-english-in-data.md @@ -0,0 +1,5 @@ +- Keep Directions route errors, the Local ADS-B receiver error and Global + Context cohort reasons English in layer data; the Layers panel, Radio card + and awareness panel still show them in the interface language. The route + proxy's "routing busy" and "route proxy error" refusals are now translated + too (daikaginza, #1209). diff --git a/changelog.d/1220-openai-typed-send.md b/changelog.d/1220-openai-typed-send.md deleted file mode 100644 index cd1b5b470..000000000 --- a/changelog.d/1220-openai-typed-send.md +++ /dev/null @@ -1,3 +0,0 @@ -- Avoid starting or queuing a reply when the local channel rejects a typed OpenAI - command, while preserving an earlier accepted deferred input - (Manjunath, #1220). diff --git a/changelog.d/1228-cctv-ontario-511-key.md b/changelog.d/1228-cctv-ontario-511-key.md deleted file mode 100644 index d9dc7986f..000000000 --- a/changelog.d/1228-cctv-ontario-511-key.md +++ /dev/null @@ -1,4 +0,0 @@ -- 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). diff --git a/changelog.d/1233-tarktee-datex-catalog-hardening.md b/changelog.d/1233-tarktee-datex-catalog-hardening.md deleted file mode 100644 index 39f184526..000000000 --- a/changelog.d/1233-tarktee-datex-catalog-hardening.md +++ /dev/null @@ -1,4 +0,0 @@ -- 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). diff --git a/changelog.d/1234-cctv-catalog-background-refresh.md b/changelog.d/1234-cctv-catalog-background-refresh.md deleted file mode 100644 index 2d3706b14..000000000 --- a/changelog.d/1234-cctv-catalog-background-refresh.md +++ /dev/null @@ -1,5 +0,0 @@ -- 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). diff --git a/changelog.d/1235-location-resolution.md b/changelog.d/1235-location-resolution.md deleted file mode 100644 index 160688005..000000000 --- a/changelog.d/1235-location-resolution.md +++ /dev/null @@ -1,3 +0,0 @@ -- 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). diff --git a/changelog.d/1237-taiwan-freeway-cctv.md b/changelog.d/1237-taiwan-freeway-cctv.md deleted file mode 100644 index 1578971d4..000000000 --- a/changelog.d/1237-taiwan-freeway-cctv.md +++ /dev/null @@ -1,4 +0,0 @@ -- 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). diff --git a/changelog.d/1243-starfield-sky.md b/changelog.d/1243-starfield-sky.md deleted file mode 100644 index 04d0cfbf8..000000000 --- a/changelog.d/1243-starfield-sky.md +++ /dev/null @@ -1,5 +0,0 @@ -- 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). diff --git a/changelog.d/1264-overpass-request-admission.md b/changelog.d/1264-overpass-request-admission.md deleted file mode 100644 index c5e763e30..000000000 --- a/changelog.d/1264-overpass-request-admission.md +++ /dev/null @@ -1,9 +0,0 @@ -- 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). diff --git a/changelog.d/1283-satellite-cache-provenance.md b/changelog.d/1283-satellite-cache-provenance.md deleted file mode 100644 index c9b0dd7dc..000000000 --- a/changelog.d/1283-satellite-cache-provenance.md +++ /dev/null @@ -1,3 +0,0 @@ -- 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). diff --git a/changelog.d/1284-route-plausibility-segment.md b/changelog.d/1284-route-plausibility-segment.md deleted file mode 100644 index 4ab211957..000000000 --- a/changelog.d/1284-route-plausibility-segment.md +++ /dev/null @@ -1,3 +0,0 @@ -- 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). diff --git a/changelog.d/1286-since-note-inline.md b/changelog.d/1286-since-note-inline.md deleted file mode 100644 index f66467d67..000000000 --- a/changelog.d/1286-since-note-inline.md +++ /dev/null @@ -1,4 +0,0 @@ -- 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). diff --git a/changelog.d/1287-shared-sliders.md b/changelog.d/1287-shared-sliders.md deleted file mode 100644 index b5e5fb215..000000000 --- a/changelog.d/1287-shared-sliders.md +++ /dev/null @@ -1,3 +0,0 @@ -- 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). diff --git a/changelog.d/1301-shared-segmented-selectors.md b/changelog.d/1301-shared-segmented-selectors.md deleted file mode 100644 index 379b8c716..000000000 --- a/changelog.d/1301-shared-segmented-selectors.md +++ /dev/null @@ -1,5 +0,0 @@ -- Unify segmented selectors for model coverage, detection allocation, drawing - shape, Street View and imagery comparison while preserving their existing - behavior. Keep compact labels such as FILL and FLAT on one line, and let - collapsed Weather release space to the open rail panel. Reserve the full mode - button height in Recent Imagery so keyboard focus is not clipped (mazy1998, #1301). diff --git a/changelog.d/1302-shared-action-buttons.md b/changelog.d/1302-shared-action-buttons.md deleted file mode 100644 index 3c8318488..000000000 --- a/changelog.d/1302-shared-action-buttons.md +++ /dev/null @@ -1,3 +0,0 @@ -- Give ordinary Scene and CCTV actions shared native buttons with consistent - default, destructive and compact styling while preserving their existing - behavior and specialized controls (mazy1998, #1302). diff --git a/changelog.d/1305-shared-native-form-fields.md b/changelog.d/1305-shared-native-form-fields.md deleted file mode 100644 index 129d81bcf..000000000 --- a/changelog.d/1305-shared-native-form-fields.md +++ /dev/null @@ -1,3 +0,0 @@ -- Share native dropdown and numeric-field styling across Scene, CCTV, Display - and Radio controls, with a shared CCTV number-input builder that preserves - existing precision, validation and commit behavior (mazy1998, #1305). diff --git a/changelog.d/1306-shared-status-badges.md b/changelog.d/1306-shared-status-badges.md deleted file mode 100644 index 007a028d1..000000000 --- a/changelog.d/1306-shared-status-badges.md +++ /dev/null @@ -1,4 +0,0 @@ -- Share read-only status badge presentation across CCTV source/frame labels, - calibration provenance and Weather coverage, preserving status meanings, - palettes, translations and announcements while preparing the view contract - for a future Vue migration (mazy1998, #1306). diff --git a/changelog.d/1309-shared-field-css-ownership.md b/changelog.d/1309-shared-field-css-ownership.md deleted file mode 100644 index 1b8817bac..000000000 --- a/changelog.d/1309-shared-field-css-ownership.md +++ /dev/null @@ -1,3 +0,0 @@ -- Clarify shared field CSS ownership: CCTV owns calibration editor sizing, - while Radio and Cyber supply explicit field palettes without changing the - current appearance or native control behavior (mazy1998, #1309). diff --git a/changelog.d/1317-provider-response-hardening.md b/changelog.d/1317-provider-response-hardening.md deleted file mode 100644 index 398688fc0..000000000 --- a/changelog.d/1317-provider-response-hardening.md +++ /dev/null @@ -1 +0,0 @@ -- Validate provider response formats, strengthen camera source checks, and apply consistent API response protections (samehkhamis, #1317). diff --git a/changelog.d/1344-inline-panel-visibility.md b/changelog.d/1344-inline-panel-visibility.md new file mode 100644 index 000000000..7d0fd6689 --- /dev/null +++ b/changelog.d/1344-inline-panel-visibility.md @@ -0,0 +1,4 @@ +- Pause the HUD's camera readout while the page is hidden, as polled layers + already do. An app embedded inline in a panel keeps both updating, because the + panel's host can report the page as hidden while it is on screen (nu-stats, + #1344). diff --git a/changelog.d/1374-director-seek-visibility.md b/changelog.d/1374-director-seek-visibility.md new file mode 100644 index 000000000..d4f93a1bd --- /dev/null +++ b/changelog.d/1374-director-seek-visibility.md @@ -0,0 +1,4 @@ +- Restore a shot's declared layer visibility when seeking within it after manual + layer changes, including toggles still in flight. Only changed layers are + reconciled; refused restoration reports a failed seek and keeps the last + position, while undeclared operator layers are preserved (@oneill9, #1374). diff --git a/changelog.d/1401-geocode-place-region.md b/changelog.d/1401-geocode-place-region.md new file mode 100644 index 000000000..d3aa5c6b8 --- /dev/null +++ b/changelog.d/1401-geocode-place-region.md @@ -0,0 +1,2 @@ +- A "place, region" search such as "Paris, Texas" now lands on the place in + that region instead of a namesake elsewhere (quanticsoul4772, #1401). diff --git a/changelog.d/1402-terrain-429-pause.md b/changelog.d/1402-terrain-429-pause.md new file mode 100644 index 000000000..05b18680b --- /dev/null +++ b/changelog.d/1402-terrain-429-pause.md @@ -0,0 +1,5 @@ +- The terrain proxy stops asking the upstream for a while after a 429 + (its Retry-After, at most 10 minutes, or 60 s) instead of retrying on every + page request, and a Retry-After longer than its 10 s retry budget now ends + the retries at once instead of sleeping the budget out (quanticsoul4772, + #1402). diff --git a/changelog.d/1404-perimeters-stale.md b/changelog.d/1404-perimeters-stale.md new file mode 100644 index 000000000..5105aed2b --- /dev/null +++ b/changelog.d/1404-perimeters-stale.md @@ -0,0 +1,3 @@ +- Fire Perimeters and `get_fire_perimeters` say when WFIGS is unreachable and + the last snapshot is being shown (STALE, its age, "data may be stale"), and + that snapshot is served for at most 24 hours (quanticsoul4772, #1404). diff --git a/changelog.d/1405-traffic-layer-stale.md b/changelog.d/1405-traffic-layer-stale.md new file mode 100644 index 000000000..dc564ac76 --- /dev/null +++ b/changelog.d/1405-traffic-layer-stale.md @@ -0,0 +1,3 @@ +- The Traffic layer reads STALE instead of LIVE when the TomTom proxy serves + cached flow tiles (budget spent or TomTom failing), and the row dates them by + the oldest cached tile (quanticsoul4772, #1405). diff --git a/changelog.d/1430-traditional-chinese.md b/changelog.d/1430-traditional-chinese.md new file mode 100644 index 000000000..e644829fc --- /dev/null +++ b/changelog.d/1430-traditional-chinese.md @@ -0,0 +1,4 @@ +- Add Traditional Chinese (zh-Hant) with language selection and localized + Chinese readouts: the Simplified Chinese catalog written in Traditional + characters, based on translations by @abiyeenzo (#914) and @EDIT-Aeterna + (#979) (German, @gekh, #1430). diff --git a/changelog.d/1432-vessel-course-north.md b/changelog.d/1432-vessel-course-north.md new file mode 100644 index 000000000..45d9e5845 --- /dev/null +++ b/changelog.d/1432-vessel-course-north.md @@ -0,0 +1,3 @@ +- Show a vessel course of 359.5° or more as 0°. Vessel cards and the AIS HUD + rounded the course without wrapping, so a ship with no reported heading on + a 359.7° course read "360°" and "HDG: 360DEG" (Aditya, @Rohilalala, #1432). diff --git a/changelog.d/1433-awareness-marker-kilometre.md b/changelog.d/1433-awareness-marker-kilometre.md new file mode 100644 index 000000000..a62cdd90c --- /dev/null +++ b/changelog.d/1433-awareness-marker-kilometre.md @@ -0,0 +1,6 @@ +- Show a Contacts marker 999.5 m or more away as 1.0 km. The on-globe marker + chose metres before rounding, so 999.6 m read "1000 m" while the panel read + "1.0 km" (Aditya, @Rohilalala, #1433). +- Distance labels share one formatter that takes each unit cut after rounding, + so the Contacts panel and markers, the drawn-line measure and the Cockpit + show 9.96 km as 10 km rather than 10.0 km (#1433). diff --git a/changelog.d/1434-voice-unknown-altitude.md b/changelog.d/1434-voice-unknown-altitude.md new file mode 100644 index 000000000..229d31800 --- /dev/null +++ b/changelog.d/1434-voice-unknown-altitude.md @@ -0,0 +1,3 @@ +- Selecting the nearest aircraft by voice no longer says "0 feet" for a contact + whose feed reported no altitude; the reply and card leave the altitude out + (Aditya, @Rohilalala, #1434). diff --git a/changelog.d/1436-military-geom-vertical-rate.md b/changelog.d/1436-military-geom-vertical-rate.md new file mode 100644 index 000000000..ddeb5b4aa --- /dev/null +++ b/changelog.d/1436-military-geom-vertical-rate.md @@ -0,0 +1,3 @@ +- Military aircraft whose transponders report a GNSS vertical rate now carry it + in analyst and tracked-aircraft data instead of null (Aditya, @Rohilalala, + #1436). diff --git a/changelog.d/1441-cctv-heading-north.md b/changelog.d/1441-cctv-heading-north.md new file mode 100644 index 000000000..d65e5c2d8 --- /dev/null +++ b/changelog.d/1441-cctv-heading-north.md @@ -0,0 +1,3 @@ +- Show a CCTV camera facing 359.5° or more as HDG 0°. The HUD heading token + and the camera panel rounded the bearing without wrapping, so a camera + facing 359.6° read "HDG 360°" (Aditya, @Rohilalala, #1441). diff --git a/changelog.d/1442-traffic-coverage-edges.md b/changelog.d/1442-traffic-coverage-edges.md new file mode 100644 index 000000000..63890cc35 --- /dev/null +++ b/changelog.d/1442-traffic-coverage-edges.md @@ -0,0 +1,4 @@ +- Keep Street Traffic's flow status honest at the edges. A few TomTom-matched + dots among hundreds rounded to 0% and read "(no matches)", and a few + simulated dots rounded to "100% cov"; coverage now stays within 1–99% while + both are shown (Aditya, @Rohilalala, #1442). diff --git a/changelog.d/1443-replay-countdown-ten.md b/changelog.d/1443-replay-countdown-ten.md new file mode 100644 index 000000000..4780f26bf --- /dev/null +++ b/changelog.d/1443-replay-countdown-ten.md @@ -0,0 +1,3 @@ +- 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). diff --git a/changelog.d/1444-replay-speed-countdown.md b/changelog.d/1444-replay-speed-countdown.md new file mode 100644 index 000000000..fb35d008b --- /dev/null +++ b/changelog.d/1444-replay-speed-countdown.md @@ -0,0 +1,3 @@ +- Keep Space Missions' replay countdown in real time when the replay speed + changes before liftoff; 0.25× had stretched T−8 to T−32 (Aditya, + @Rohilalala, #1444). diff --git a/changelog.d/1445-annotation-label-units.md b/changelog.d/1445-annotation-label-units.md new file mode 100644 index 000000000..494c663db --- /dev/null +++ b/changelog.d/1445-annotation-label-units.md @@ -0,0 +1,3 @@ +- Label a route, arrow or drawn line of 995 m or more as 1.0 km, and of + 9,950 m or more as 10 km. The placed label picked the unit before rounding, + so it read "1000 m" and "10.0 km" (Aditya, @Rohilalala, #1445). diff --git a/changelog.d/1446-draw-area-units.md b/changelog.d/1446-draw-area-units.md new file mode 100644 index 000000000..ef82d6628 --- /dev/null +++ b/changelog.d/1446-draw-area-units.md @@ -0,0 +1,3 @@ +- Show a drawn area of 9,999.5 m² or more as 1.0 ha, and of 999,500 m² or + more as 1.00 km². The draw hint picked the unit before rounding, so it read + "10000 m²" and "100.0 ha" (Aditya, @Rohilalala, #1446). diff --git a/changelog.d/1448-local-adsb-geom-rate.md b/changelog.d/1448-local-adsb-geom-rate.md new file mode 100644 index 000000000..a4a0c54f8 --- /dev/null +++ b/changelog.d/1448-local-adsb-geom-rate.md @@ -0,0 +1,6 @@ +- Local ADS-B decoder feeds now carry a GNSS-sourced vertical rate + (`geom_rate`) when no barometric rate is sent, so the card shows V/S + instead of a dash (Aditya, @Rohilalala, #1448). +- Local ADS-B, military and the adsb.lol flight fallback read the vertical + rate through one shared readsb field reader, so all three prefer the + barometric rate and fall back to the GNSS rate the same way (#1448). diff --git a/changelog.d/1449-cable-tool-duplicates.md b/changelog.d/1449-cable-tool-duplicates.md new file mode 100644 index 000000000..e3cc8df96 --- /dev/null +++ b/changelog.d/1449-cable-tool-duplicates.md @@ -0,0 +1,3 @@ +- List each submarine cable once in find_submarine_cables. TeleGeography + splits 17 cables (2Africa, Echo, Hawaiki) into several features, so a + search for Echo read "3 submarine cables" (Aditya, @Rohilalala, #1449). diff --git a/changelog.d/1450-directions-ten-km.md b/changelog.d/1450-directions-ten-km.md new file mode 100644 index 000000000..1b348493e --- /dev/null +++ b/changelog.d/1450-directions-ten-km.md @@ -0,0 +1,3 @@ +- Show a route of 9.95 km or more as 10 km. The Directions row and step + cards kept one decimal until the raw distance reached 10 km, so 9.96 km + read "10.0 km" (Aditya, @Rohilalala, #1450). diff --git a/changelog.d/1451-alpr-direction-north.md b/changelog.d/1451-alpr-direction-north.md new file mode 100644 index 000000000..74f04b7ab --- /dev/null +++ b/changelog.d/1451-alpr-direction-north.md @@ -0,0 +1,2 @@ +- Show an ALPR camera's mapped bearing of 359.5° or more as DIRECTION 0° + rather than DIRECTION 360° (Aditya, @Rohilalala, #1451). diff --git a/changelog.d/1452-transit-heading-north.md b/changelog.d/1452-transit-heading-north.md new file mode 100644 index 000000000..95c81a080 --- /dev/null +++ b/changelog.d/1452-transit-heading-north.md @@ -0,0 +1,3 @@ +- Show a transit vehicle's reported bearing of 359.5° or more as 0°. The + selected-vehicle card rounded it without wrapping, so a bus heading 359.6° + read "reported hdg 360°" (Aditya, @Rohilalala, #1452). diff --git a/changelog.d/1453-satellite-launch-year.md b/changelog.d/1453-satellite-launch-year.md new file mode 100644 index 000000000..6b51770c5 --- /dev/null +++ b/changelog.d/1453-satellite-launch-year.md @@ -0,0 +1,6 @@ +- Space Missions no longer shows an older satellite with a similar name and a + different known launch year as a new payload's orbit: the launch-year check + on loaded satellites now runs (Aditya, @Rohilalala, #1453). +- Satellite launch years come from one TLE designator parser for both the + loaded catalog and the active-TLE fallback; a blank designator is an + unknown year instead of 2000 (#1453). diff --git a/changelog.d/1454-voice-context-heading-north.md b/changelog.d/1454-voice-context-heading-north.md new file mode 100644 index 000000000..5653c4785 --- /dev/null +++ b/changelog.d/1454-voice-context-heading-north.md @@ -0,0 +1,5 @@ +- Describe a selected flight or military contact on 359.5° or more to the + voice assistant as heading 0°, not 360° (Aditya, @Rohilalala, #1454). +- Heading and bearing readouts share one rounding helper, so the Cockpit's + TRACK ACQUIRED course no longer reads 360°, and a negative bearing wraps + instead of printing a minus sign (#1454). diff --git a/changelog.d/1459-hud-place-lookups.md b/changelog.d/1459-hud-place-lookups.md new file mode 100644 index 000000000..023e2f2e6 --- /dev/null +++ b/changelog.d/1459-hud-place-lookups.md @@ -0,0 +1,5 @@ +- Cut the Google requests the HUD's AI summary makes on its own. It no longer + looks up place names when no OpenAI key is configured, reverse-geocodes at + most two points instead of up to four, and looks up again only once the view + shows a different area, not after every small pan, so a small pan no longer + asks OpenAI for a new summary either (samehkhamis, #1459). diff --git a/changelog.d/1463-dev-tool-updates.md b/changelog.d/1463-dev-tool-updates.md new file mode 100644 index 000000000..94e71ae79 --- /dev/null +++ b/changelog.d/1463-dev-tool-updates.md @@ -0,0 +1,4 @@ +- Update the development tools to Vite 6.4.4 and Puppeteer 25.16.0, which + clears the open npm advisories for the dev server and the browser test + tooling, and remove the unused local screenshot helper `scripts/shot-sink.mjs` + (samehkhamis, #1463). diff --git a/changelog.d/1466-panel-chrome-consistency.md b/changelog.d/1466-panel-chrome-consistency.md new file mode 100644 index 000000000..c7f1f28a1 --- /dev/null +++ b/changelog.d/1466-panel-chrome-consistency.md @@ -0,0 +1,5 @@ +- Give the side panels one look. Every panel title now uses the command dock's + label type in a brighter colour; a panel's edge and title light up on hover, + as the dock's trays do; and in the default theme each collapse button sits + 19px from its panel's edge (plus the scrollbar's width in an open, scrolling + panel where the system always shows scrollbars) (asturksever, #1466). diff --git a/changelog.d/1467-remove-operator-hud-layout.md b/changelog.d/1467-remove-operator-hud-layout.md new file mode 100644 index 000000000..556d551ee --- /dev/null +++ b/changelog.d/1467-remove-operator-hud-layout.md @@ -0,0 +1,3 @@ +- Remove the Operator HUD layout. Display > HUD > Layout and the HUD voice + action now offer Tactical, Minimal and Cyber; share links, scenes and saved + views that name Operator open in Tactical (asturksever, #1467). diff --git a/changelog.d/1468-remove-snow-preset.md b/changelog.d/1468-remove-snow-preset.md new file mode 100644 index 000000000..2bd1cb161 --- /dev/null +++ b/changelog.d/1468-remove-snow-preset.md @@ -0,0 +1,4 @@ +- Remove the Snow visual preset. Visual Presets, keyboard shortcuts, the + Cockpit vision carousel and the visual-style voice action now offer Normal, + CRT, NVG, FLIR, Anime and Noir; share links and scene shots that name Snow + open on Normal (asturksever, #1468). diff --git a/changelog.d/1470-ukrainian.md b/changelog.d/1470-ukrainian.md new file mode 100644 index 000000000..9f64e3081 --- /dev/null +++ b/changelog.d/1470-ukrainian.md @@ -0,0 +1,3 @@ +- Add Ukrainian (uk) to the language picker: the whole interface in Ukrainian, + with its four plural forms and Cyrillic HUD units. Based on the Ukrainian + translation by @meirm (#312) (German, @gekh, #1470). diff --git a/changelog.d/1471-context-panel-cleanup.md b/changelog.d/1471-context-panel-cleanup.md new file mode 100644 index 000000000..0d97cc397 --- /dev/null +++ b/changelog.d/1471-context-panel-cleanup.md @@ -0,0 +1,7 @@ +- Remove the Radio icon from the Context panel header, along with the compact + Radio player it opened; Radio stays in Context as its own section, and + Cockpit's compact Radio is unchanged. Context is also easier to read: the + Contacts / Space Missions modes are one segmented control, labels and + guidance are larger and higher contrast, the standby hint is plain text + instead of a dashed box, and the panel draws no glow halo. Cyber keeps its + own framed buttons and red titles (asturksever, #1471). diff --git a/changelog.d/1472-authoring-panel.md b/changelog.d/1472-authoring-panel.md new file mode 100644 index 000000000..cec54b18e --- /dev/null +++ b/changelog.d/1472-authoring-panel.md @@ -0,0 +1,4 @@ +- Rename the Scenes panel to Authoring and lay it out in steps (Scene, Shots, + Preview, Save & share) with larger, higher-contrast text, numbered shots, a + tooltip on every action and buttons that are disabled when they would do + nothing (asturksever, #1472). diff --git a/changelog.d/1473-italian.md b/changelog.d/1473-italian.md new file mode 100644 index 000000000..c1af8f021 --- /dev/null +++ b/changelog.d/1473-italian.md @@ -0,0 +1,3 @@ +- Add Italian (it) to the language picker: the whole interface in Italian, + with Italian plural forms and labels shortened to fit the panels. Based on + the translation by @abiyeenzo (#912) (German, @gekh, #1473). diff --git a/changelog.d/1475-brazilian-portuguese.md b/changelog.d/1475-brazilian-portuguese.md new file mode 100644 index 000000000..1705d628a --- /dev/null +++ b/changelog.d/1475-brazilian-portuguese.md @@ -0,0 +1,4 @@ +- Add Brazilian Portuguese (pt) to the language picker, with every screen + translated and Portuguese plural forms. The CCTV summary line now names its + calibration, source and view in the page language. Based on translations by + @abiyeenzo (#909) (German, @gekh, #1475). diff --git a/changelog.d/1476-french.md b/changelog.d/1476-french.md new file mode 100644 index 000000000..30d0b8c89 --- /dev/null +++ b/changelog.d/1476-french.md @@ -0,0 +1,4 @@ +- Add French (fr) to the language picker, with French typography and plural + forms. With nine languages the picker now shows its search field. Based on + the translation by @abiyeenzo (#907), checked against @meirm's French + catalog (#312) (German, @gekh, #1476). diff --git a/changelog.d/1477-german.md b/changelog.d/1477-german.md new file mode 100644 index 000000000..77d7717b6 --- /dev/null +++ b/changelog.d/1477-german.md @@ -0,0 +1,3 @@ +- Add German (de) to the language picker, with a full translation of the + interface and a German style guide in docs/translations/de.md. Based on + translations by @abiyeenzo (#908) (German, @gekh, #1477). diff --git a/changelog.d/1478-japanese.md b/changelog.d/1478-japanese.md new file mode 100644 index 000000000..376853e24 --- /dev/null +++ b/changelog.d/1478-japanese.md @@ -0,0 +1,4 @@ +- Add Japanese (ja) to the language picker, with year-first dates, Japanese + counters, unit words joined to their numbers (5分前, 1時間5分) and CJK label + rules for Japanese text. Based on translations by @abiyeenzo (#913) + (German, @gekh, #1478). diff --git a/changelog.d/1479-arabic.md b/changelog.d/1479-arabic.md new file mode 100644 index 000000000..d9282a52d --- /dev/null +++ b/changelog.d/1479-arabic.md @@ -0,0 +1,5 @@ +- Add Arabic (ar), the first right-to-left language: the title, rails, dock and + corner row mirror, numbers keep Western digits, and plurals use all six + Arabic forms. FIRMS cards and Space Missions landing labels are now + translated in every language. Based on translations by @abiyeenzo (#915) + (German, @gekh, #1479). diff --git a/changelog.d/1485-cctv-view-thinning.md b/changelog.d/1485-cctv-view-thinning.md new file mode 100644 index 000000000..307da84ad --- /dev/null +++ b/changelog.d/1485-cctv-view-thinning.md @@ -0,0 +1,4 @@ +- Draw only the CCTV cameras in view, and thin them to one per ground cell when + zoomed out. Dragging the globe with the default 4,000-camera catalog no + longer stalls, and the camera picker lists the cameras in view with a count + of the ones outside it (samehkhamis, #1485). diff --git a/changelog.d/1491-gnss-source-window.md b/changelog.d/1491-gnss-source-window.md new file mode 100644 index 000000000..108aa50b2 --- /dev/null +++ b/changelog.d/1491-gnss-source-window.md @@ -0,0 +1,3 @@ +- Let a GNSS integrity source set the layer's evidence window (`windowMs`). A + missing or invalid value keeps the 30-minute default, and the layer row now + names the window in use instead of a fixed "30 min" (samehkhamis, #1491). diff --git a/changelog.d/1494-weather-product-display.md b/changelog.d/1494-weather-product-display.md new file mode 100644 index 000000000..3aabc5b6a --- /dev/null +++ b/changelog.d/1494-weather-product-display.md @@ -0,0 +1,4 @@ +- Let a weather source describe its own products through an optional + `display` object in each manifest (source, credit, units, coverage, info, + legend and extent). Each valid field replaces the built-in value, and + manifests without it render as before (samehkhamis, #1494). diff --git a/changelog.d/1499-vue-and-pinia.md b/changelog.d/1499-vue-and-pinia.md new file mode 100644 index 000000000..dc93aecdd --- /dev/null +++ b/changelog.d/1499-vue-and-pinia.md @@ -0,0 +1,205 @@ +- The language picker switches language in place instead of reloading the + page. Views re-render; code that publishes translated text words it again + through `onLocaleChange` (`src/i18n/index.js`), and ``/`dir` and + the page's `data-i18n` markup follow. `persistLocale` replaces + `persistLocaleAndReload`, and `registerUnsavedWork` and the switch's + unsaved-work prompt are removed, since a switch no longer loses anything + (samehkhamis, #1499). + +- A map source's `unavailableReason` may be a function, read when the reason + is shown so it follows the locale; the sources from `createDefaultMapSources` + (`./maps/defaults`) give functions. `MapSourceController` reads either form + (samehkhamis, #1499). + +- The application views translate their text with `t()` as they render, + so it follows the active locale; they no longer carry `data-i18n*` + attributes, and `npm run i18n` rejects those in a Vue template. The boot + pass (`applyDocumentTranslations`) still translates static page markup + such as the loading screen and a host's own markup (samehkhamis, #1499). + +- The page's locale lives in a vue-i18n instance installed in every + application view, and `t()`, `tn()` and the Intl formatters read it from + there, so a view that translates while rendering renders again when the + locale changes. `bindLocale` (`src/i18n/index.js`) keeps the locale in a + given ref; Core's Vite config builds vue-i18n without its message + compiler. Catalogs, messages and call sites are unchanged + (samehkhamis, #1499). + +- Cockpit's Display popover shows the Display panel's own HUD, Detection, + Parameters and 3D groups through Vue teleports: `CockpitDisplayPortal` + publishes the popover's slots (`publish`, with `afterRender` to lay out once + they have moved) instead of moving the groups itself (samehkhamis, #1499). + +- The Cockpit utility strip's measured layout renders from the `gev-cockpit` + store: the layout pass publishes `utilityLayout` (the HUD's + `--cockpit-utility-*` lengths, `primaryOnly` and `hideCollapsed`), and a host + constructing `CockpitController` passes `publish.utilityLayout` beside its + other publishers (samehkhamis, #1499). + +- The Cockpit Contact and briefing cards' collapse renders from the + `gev-cockpit` store: the controller publishes `contextCollapsed` and + `signalCollapsed` in its presence, and each card's `data-collapsed` and its + toggle's state, words and icon follow it (samehkhamis, #1499). + +- The panels' own state classes (`collapsed`, `dock-pinned`, `dock-pinned-top`, + `layout-auto-collapsed`, `accordion-collapsed`) render from the + `gev-panel-chrome` store: the shell, the rails and the accordion present each + panel's state there instead of writing its classes. `layoutLeftPanelRail` and + `layoutRightPanelRail` (`./ui/layout`) now call + `onCollapse(panel, {collapsed, auto})` and leave drawing it to the host. + Without a shell to own the panels, the voice tools' `set_panel_open` now + reports that there is no panel to open instead of writing its classes + (samehkhamis, #1499). + +- The shell's panel chrome no longer writes the panels' collapse buttons, the + dock tray toggles' names or the dock pins' pressed state: it presents each + panel's open and pinned state to the `gev-panel-chrome` store, and the + buttons, now views, call it there. `bindPanelDisclosure` (`./ui/panels`) binds + only a panel's Escape; it no longer takes `buttons` or `onChange`. `UiButton` + joins the component kit (samehkhamis, #1499). + +- `SceneControls` (`./ui/scenes`) no longer takes `elements` or writes the Scene + panel's buttons, status, progress or runtime chip: it presents them to the + `gev-scenes` store and the panel's buttons call its actions there. + `sceneElements`, `presentSceneButtons`, `presentSceneProgress` and + `presentSceneRuntime` give way to `sceneButtonsDisabled` and `scenePercent`. + It no longer stands down when the page has no Scene panel: ESC still stops a + playing scene and the runtime chip still shows (samehkhamis, #1499). + +- `initFirstRunExperience` (via `./application/chrome`) no longer writes the + launcher's classes, attributes or text, or removes the card: it presents them + to the welcome view's `gev-first-run` store (its new `store` option) and + connects the controls the view's buttons call (samehkhamis, #1499). + +- `IntelHUD` (`./ui/hud`) no longer builds or writes `#intel-hud`: it hands its + readouts, visibility, layout variant and style colors to its `publish` option, + and Core's `intel-hud` view renders them. A page places that view with + `
` instead of a bare + `#intel-hud` element. `./layers/vessels` shows the selected vessel's HUD + readout through `services.readouts.presentSelectedVessel()` instead of writing + `#hud-ais-vessel`; the application passes `src/hudReadouts.js`, which IntelHUD + follows (samehkhamis, #1499). + +- `LocationControls` (`./ui/location`) no longer builds the city and landmark + pills or writes the mini status: it presents the tray, including the search + error (`setSearchError()`), and hands it to its `publish` option, and Core's + dock renders it and calls `chooseCity()` and `choosePoi()`. Its `elements` are + now only `search`: `resetButtons` and `searchToggle` are gone, and the reset + buttons and the search toggle call its new `reset()` and `toggleSearch()`. It + presents the search field's open and searching state too (`searchExpanded`, + `searching`; a `searchExpanded` option starts it open), set through + `setSearching()`, `settleSearch()` and `closeSearch()`, and a host renders the + field's `expanded` and `searching` classes from it; `LocationSearch` takes a + `setSearching` option in place of writing the field's class, and + `NavigationController` a `settleSearch` option in place of `searchInput`. + `createOrbitIndicator()` is replaced by `showOrbit(active)`, which adds the + ORBIT indicator to the presented tray as `orbit: { active }` (null until + offered) (samehkhamis, #1499). + +- `createMapSourceControls()` (`./ui/maps`) no longer renders the map source + chips into a `container` or writes a `statusElement`: it presents the chip row + and status badge and hands them to its `publish` option, and Core's dock + renders them and calls `select()`. `renderMapStackChips`, `syncMapStackChips` + and `MAP_STACK_CHIP_CLASS` are removed (samehkhamis, #1499). + +- `./layers/launches` no longer writes the Space Missions roster or the selected + mission panel into `#space-mission-panel-host`: the layer publishes both + through `subscribePanel(listener)` and Core's Space Missions views render + them, calling the layer's panel commands (`selectMissionAt`, + `selectPreviousMission`, `selectNextMission`, `deselectMission`, + `focusSelectedLaunchSite`, `startSelectedReplay`, `toggleReplayPause`, + `cancelReplay`, `setReplaySpeed` and the `roster*` preview inputs). The + `bindMissionRosterItemKeyboardPreview`, `captureMissionRosterFocus` and + `restoreMissionRosterFocus` helpers are gone; the roster keeps focus itself + (samehkhamis, #1499). + +- `./layers/awareness` no longer writes the Contacts panel into + `#military-awareness-panel`: the layer publishes a plain panel view through + `subscribePanel(listener)` and Core's Contacts view renders it, calling the + layer's `navigatePrevious`, `focusCurrent`, `navigateNext` and `focusTarget`. + The `awarenessPanelControlKey`, `captureAwarenessPanelFocus` and + `restoreAwarenessPanelFocus` helpers are gone; the view keeps focus itself + (samehkhamis, #1499). + +- `./ui/cctv` and `./ui/radio` import Vue and Pinia: their controllers publish + the panel's state to a store that the CCTV and Radio views render, instead of + writing the panel's markup themselves (samehkhamis, #1499). + +- `./ui/display`'s `bindDisplayControls` is gone: the Display panel's controls + and the dock's style buttons call their owner through the display store's + `connect`/`act`, and its allocation and 3D model choices are the kit's + `UiSegmented`. `initDrawTool` takes `connect(actions)`, which returns their + disconnect, and no longer listens on the Draw controls; its view publishes + `color` and `label` too, and the shape choice is a `UiSegmented`. + `initGeoFileImport` takes `connect(actions)` the same way and no longer + listens on the Import button, picker or list, nor restores focus itself: the + `GeoImport` view does both, and its `publish` need not settle once the list + renders. Street Level's fit and panorama filters and Recent Imagery's mode are + `UiSegmented` too: the Street Level store's owner answers `setPano` and + `setRenderMode`, and `RecentImageryReadout` takes `onSetMode`. The Radio + panel's and Cockpit's internet radio controls and the tuner slider's events + call `RadioControls`, and the local RTL-SDR card's controls + `LocalSdrControls`, through the radio store's new `connect`/`act`; the card + labels its gain options itself. `act` returns what the answering action + returned. The Display, Radio, RTL-SDR, Cockpit, mission replay, style + parameter, rail card, observation timeline, imagery opacity, Street Level date + and Bhote Koshi sliders are the kit's `UiSlider`, a native range input that + emits numbers, and the pressed toggles are the kit's `UiButton`, which gains + `pressedClass`; so are the rail card chips, map source chips and location + pills (which now report `aria-pressed`), and the voice card's chips are the + kit's new `UiChip`. The selects are the kit's new `UiSelect`, the status + badges its new `UiBadge` and the one-line empty states its new `UiEmpty`. The + top-center clear, share, tilt, north-up and reset buttons, and the cockpit's + reset, call the shell through the new globe actions store, which also carries + the camera's tilt and compass heading. The recording safe frame renders what + `RecordingControls` publishes to the new recording store. `syncScopeControls` + (`./ui/display`), `shouldHideCollapsedRightPanels` (`./ui/layout`), + `bindClearLayersControl` (`./ui/layers`) and `createStyleParameters` + (`./ui/input`; the Display panel renders the style parameters VisualSettings + publishes to the display store) are deprecated: the Display view, the rail and + the globe actions view now own what they did. `./ui/input` imports Vue + (samehkhamis, #1499). + +- `./scenes` presents the scene sharing dialogs and the EDIT DETAILS / SHARE + SCENE bar through a store that the scene panels view renders, so a page that + omits `layer-panels` shows neither (samehkhamis, #1499). + +- `./voice/control`'s `createVoiceControl({ panel })` returns the voice + control's presenter instead of element handles: owners write it by part + (`update('detail', { text })`) and listen with `on('button', fn)`, and the + command dock renders it from a store. `createVoiceCommands`' `createControl` + and `VoiceCardControls` (`{ ui }` instead of `{ root }`) take the same + presenter; the `reset` option is gone, since a newer control replaces the + previous one (samehkhamis, #1499). + +- Pages place each view with a `
` + host and write their own loading screen; the `build/html` template expansion + and its `gev:template` markers are gone. `createApplication` takes `omitViews` + (for example `['provider-settings']`) for views an application leaves out + (samehkhamis, #1499). + +- The page's panels, dock, cockpit and dialogs are Vue views with the same + markup as before. Hosts that do not initialize Core's locale pass + `localePicker: false` to `createApplicationControls` to drop the language + picker, and Node tests that import application modules register the component + loader with `--import=/build/register-vue` (samehkhamis, #1499). + +- The Data, Authoring, Weather, Recent Imagery, Context, CCTV, Street Level and + Radio panels are one component, `UiPanel` (`src/ui/kit/`), with the same + markup plus the kit's `ui-panel` classes, their titles translated by `t()` + rather than `data-i18n`; `RailPanel` binds it to the panel chrome store. + `PanelCollapseButton` drops its `initial` and `initialTitle` props and takes + `startCollapsed`, so before startup presents a panel its button already shows + that state's glyph, words and `aria-expanded` (samehkhamis, #1499). + +- The Authoring, shot list and CCTV buttons and Street Level's nearest button + are the kit's `UiButton` (danger buttons in the `alert` tone, primary ones the + `primary` variant), keeping their classes; the CCTV on/off, coverage, + auto-hop, projection and adjust toggles now say their state with + `aria-pressed` (samehkhamis, #1499). + +- Each desktop sidebar has an icon rail that lists every panel on that side and + opens one with a click. One panel is open per side in every HUD layout, so the + collapsed panel headers no longer take up the sidebars. Phones keep the bottom + tab bar. The rail is the first view built with Vue and Pinia, which now ship + with the app; see docs/VUE-MIGRATION.md (samehkhamis, #1499). diff --git a/changelog.d/1500-vue-loader-cache.md b/changelog.d/1500-vue-loader-cache.md new file mode 100644 index 000000000..da2ff7c13 --- /dev/null +++ b/changelog.d/1500-vue-loader-cache.md @@ -0,0 +1,4 @@ +- `build/register-vue` loads components on the test thread and keeps compiled + components between test processes in a per-user cache directory, so Node + tests that import views run about twice as fast. `GEV_VUE_CACHE=0` compiles + every time (samehkhamis, #1500). diff --git a/changelog.d/1501-rail-pills.md b/changelog.d/1501-rail-pills.md new file mode 100644 index 000000000..61d23a38b --- /dev/null +++ b/changelog.d/1501-rail-pills.md @@ -0,0 +1,3 @@ +- The side panel rails show each panel as a labelled pill while no panel on + that side is open, and fold to icons once one opens. `UiRail` takes + `collapsible` for this (samehkhamis, #1501). diff --git a/changelog.d/287-shared-great-circle-distance.md b/changelog.d/287-shared-great-circle-distance.md deleted file mode 100644 index c2db9d54d..000000000 --- a/changelog.d/287-shared-great-circle-distance.md +++ /dev/null @@ -1,5 +0,0 @@ -- The client's great-circle distances now come from one shared - `src/geoDistance.js` (`greatCircleKm`/`greatCircleMeters`, `{lat, lon}` - points). The Earth radius is standardized on 6371.0088 km, so distances shift - by about 1.4 ppm and region areas by about 2.8 ppm, and missing or - out-of-range points return `Infinity` (onionviolet, #287; Rajojha-1, #749). diff --git a/changelog.d/605-fl511-florida-cameras.md b/changelog.d/605-fl511-florida-cameras.md deleted file mode 100644 index 76f840198..000000000 --- a/changelog.d/605-fl511-florida-cameras.md +++ /dev/null @@ -1,9 +0,0 @@ -- 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). diff --git a/changelog.d/686-terrain-heights-bounds.md b/changelog.d/686-terrain-heights-bounds.md deleted file mode 100644 index 7e0004069..000000000 --- a/changelog.d/686-terrain-heights-bounds.md +++ /dev/null @@ -1,9 +0,0 @@ -- 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). diff --git a/changelog.d/761-dev-unwatched-runtime-dirs.md b/changelog.d/761-dev-unwatched-runtime-dirs.md deleted file mode 100644 index d3aaf0a91..000000000 --- a/changelog.d/761-dev-unwatched-runtime-dirs.md +++ /dev/null @@ -1,5 +0,0 @@ -- 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). diff --git a/changelog.d/767-alpha5-catalog-numbers.md b/changelog.d/767-alpha5-catalog-numbers.md deleted file mode 100644 index 8a96698a1..000000000 --- a/changelog.d/767-alpha5-catalog-numbers.md +++ /dev/null @@ -1,11 +0,0 @@ -- 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). diff --git a/changelog.d/787-changelog-fragments.md b/changelog.d/787-changelog-fragments.md deleted file mode 100644 index 41509c046..000000000 --- a/changelog.d/787-changelog-fragments.md +++ /dev/null @@ -1,7 +0,0 @@ -- Changelog entries are now one file per pull request. A change adds - `changelog.d/-.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). diff --git a/changelog.d/816-radio-owner-restoration.md b/changelog.d/816-radio-owner-restoration.md deleted file mode 100644 index df38617ee..000000000 --- a/changelog.d/816-radio-owner-restoration.md +++ /dev/null @@ -1,7 +0,0 @@ -- 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. diff --git a/changelog.d/871-vessels-idle-render.md b/changelog.d/871-vessels-idle-render.md new file mode 100644 index 000000000..928a73591 --- /dev/null +++ b/changelog.d/871-vessels-idle-render.md @@ -0,0 +1,7 @@ +- Let the scene idle with AIS vessels enabled. Vessels no longer hold the + continuous render loop; they request frames for new data and selection, one + trailing frame after the camera stops, and hold the loop only while focus + emphasis animates or a vessel stays dimmed under a tracked target. A parked + vessels-only scene went from continuous rendering to 0 frames in 5 s. While + anything is tracked, the vessel focus pass skips hidden sprites at rest: + about 4 ms to 1.2 ms per pass over 12,000 vessels (waar19, #871). diff --git a/changelog.d/872-gemini-live-voice.md b/changelog.d/872-gemini-live-voice.md deleted file mode 100644 index 519e4d37b..000000000 --- a/changelog.d/872-gemini-live-voice.md +++ /dev/null @@ -1,4 +0,0 @@ -- 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). diff --git a/changelog.d/880-world-packet-boundary.md b/changelog.d/880-world-packet-boundary.md deleted file mode 100644 index 7ee574e35..000000000 --- a/changelog.d/880-world-packet-boundary.md +++ /dev/null @@ -1,6 +0,0 @@ -- 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. diff --git a/changelog.d/945-cctv-vegvesen-kartverket-heights.md b/changelog.d/945-cctv-vegvesen-kartverket-heights.md deleted file mode 100644 index 1572c0316..000000000 --- a/changelog.d/945-cctv-vegvesen-kartverket-heights.md +++ /dev/null @@ -1,17 +0,0 @@ -- 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). diff --git a/changelog.d/956-a22-brennero-webcams.md b/changelog.d/956-a22-brennero-webcams.md new file mode 100644 index 000000000..bee2545de --- /dev/null +++ b/changelog.d/956-a22-brennero-webcams.md @@ -0,0 +1,8 @@ +- Add the Autostrada del Brennero (A22, Brenner–Modena) motorway webcams as a + CCTV pack, the first Italian cameras. The 13-camera list is one keyless + request to the Open Data Hub (NOI Techpark) tourism API, which republishes + Autobrennero's own list with coordinates; frames are the operator's stills, + refreshed about once a minute and pinned to each camera's own `km.jpg`. + Each camera's ground elevation comes from a per-kilometre DEM table, since + the road climbs from the Po valley to the Brenner Pass. `CCTV_A22_ENABLED=0` + disables the pack (Jibran Tahir, @jibraaan, #956). Closes #155. diff --git a/changelog.d/970-recent-imagery-details-scroll.md b/changelog.d/970-recent-imagery-details-scroll.md deleted file mode 100644 index 761e265c0..000000000 --- a/changelog.d/970-recent-imagery-details-scroll.md +++ /dev/null @@ -1,3 +0,0 @@ -- 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). diff --git a/changelog.d/973-tomtom-cross-site-gate.md b/changelog.d/973-tomtom-cross-site-gate.md deleted file mode 100644 index e4a029406..000000000 --- a/changelog.d/973-tomtom-cross-site-gate.md +++ /dev/null @@ -1,4 +0,0 @@ -- 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). diff --git a/changelog.d/974-street-level-slim.md b/changelog.d/974-street-level-slim.md deleted file mode 100644 index 8bc2b5346..000000000 --- a/changelog.d/974-street-level-slim.md +++ /dev/null @@ -1,6 +0,0 @@ -- 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). diff --git a/changelog.d/975-terrain-cross-site-gate.md b/changelog.d/975-terrain-cross-site-gate.md deleted file mode 100644 index 6c0e42156..000000000 --- a/changelog.d/975-terrain-cross-site-gate.md +++ /dev/null @@ -1,8 +0,0 @@ -- 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). diff --git a/changelog.d/990-hong-kong-cctv.md b/changelog.d/990-hong-kong-cctv.md deleted file mode 100644 index a150b66ae..000000000 --- a/changelog.d/990-hong-kong-cctv.md +++ /dev/null @@ -1,7 +0,0 @@ -- 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). diff --git a/compose.yaml b/compose.yaml index 7826bf376..38cf438ca 100644 --- a/compose.yaml +++ b/compose.yaml @@ -5,6 +5,11 @@ services: # lands in the bind-mounted repo, so run as you, not root. UID/GID are # unset on Docker Desktop, where the 1000 fallback is ignored anyway. user: "${UID:-1000}:${GID:-1000}" + # Forward explicit shell/.env policy before the launcher discovers a gateway. + # Unset values stay unset so the default loopback-only install still works. + environment: + - GEV_KEY_SETUP_TRUSTED_PEERS + - GEV_ALLOWED_HOSTS ports: # Publish to loopback only. Docker's default ("4173:4173") would bind # every host interface, which quietly turns the LAN warning in the README @@ -17,5 +22,8 @@ services: # UI land in the .env on your host instead of vanishing with the # container. node_modules stays in the image: sharp and friends are # compiled for Linux and must not be shadowed by a host install. - - .:/app + # `:z` relabels the checkout for containers on SELinux hosts (Fedora, + # RHEL); without it the container cannot read /app at all and Node + # reports the launcher as "Cannot find module". Elsewhere it is ignored. + - .:/app:z - /app/node_modules diff --git a/docs/APPLICATION.md b/docs/APPLICATION.md index c5a2642e6..8848601d1 100644 --- a/docs/APPLICATION.md +++ b/docs/APPLICATION.md @@ -200,6 +200,24 @@ its pending results. Source disposal does not silently reinstate a default. `ui/composition` supplies the default engines to the shell. Applications can supply a HUD implementation or request policy through control services. The shared chrome owns the welcome/loading transition; standalone composition adds -Provider Settings. `build/html` expands an allowlist of component markers from -`src/ui/templates`; unknown names cannot read arbitrary filesystem paths. The -standalone document expands to the same markup as before this extraction. +Provider Settings. A page places each Vue view in `src/ui/views` with a host, +`
`, and writes its own loading +screen (`#loading-screen` with a `.loader-status` line) as static markup so it +paints before any script runs. `createApplication()` renders the views before +anything reads the page; `omitViews` names views the application leaves out, +such as `provider-settings`, and their hosts are removed. Leaving out +`layer-panels` also leaves out the scene sharing dialogs it draws, and leaving +out `scene-chrome` the before/after imagery dividers (the imagery still splits, +with no line or handle to move it), the status chips, the toast, the ORBIT +indicator, the FPS readout, the Scene actions panel, the data-pack attribution +cards, the wind inspection marker, the Space Missions replay vehicle, the +full-globe celestial ring, the screen-space map annotations, the visible +map target list and the Global Context compass; +leaving out `command-dock` the voice control and its pointer reticle; +leaving out `context` the Bhote Koshi reconstruction panel; and leaving out +`intel-hud` the Intel HUD overlay (`#intel-hud`), whose readouts the HUD +implementation publishes to the `gev-hud` store. An unknown +view name is an error. A host that does not initialize Core's locale passes +`localePicker: false` to `createApplicationControls` instead of editing the +markup. Node tests that import application modules register the component +loader with `node --import=/build/register-vue`. diff --git a/docs/CODE-BOUNDARIES.md b/docs/CODE-BOUNDARIES.md index a23ba3e0f..f0bb9e979 100644 --- a/docs/CODE-BOUNDARIES.md +++ b/docs/CODE-BOUNDARIES.md @@ -79,17 +79,13 @@ moves it. Do not label all ingestion modules platform-independent by filename. | Retained entry | Owner and current reason | | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -| `src/ui.js` | UI facade; direct callers retain standalone StyleManager defaults. Normal assembly uses `src/ui/composition.js` | | `src/data/manager.js` | Lifecycle/panel compatibility and existing tests; normal assembly constructs lifecycle and presentation separately | | `src/data/.js` | Legacy default instances; new assembly calls layer factories with supplied sources | -| `src/data/localGeojson.js` | Infrastructure compatibility factories; catalog imports services from `src/app/localGeojsonServices.js` directly | -| `src/app/sourceSlot.js` | Re-export for direct callers/tests; portable slot ownership is `src/sources/sourceSlot.js` | | `src/app/sources.js`, `src/services/application.js` | Existing default/override API; current application construction supplies its own instances | | `server/providers/local.js` | Existing Node composition barrel and provider helper re-exports | -The two exact standalone-import exceptions are `src/ui.js` to -`src/standalone/catalog.js` and `server/providers/local.js` to -`server/standalone/key-setup.js`. They do not authorize new compatibility +The one exact standalone-import exception is `server/providers/local.js` to +`server/standalone/key-setup.js`. It does not authorize new compatibility back-edges. Voice actions use plain `src/data/feedState.js`, not the manager facade. Settings filesystem hardening belongs in `server/standalone/key-setup-hardening.mjs`; its settings policy is unchanged. @@ -237,7 +233,7 @@ DOM elements and callbacks; importing it creates no browser state. `destroy()` removes owned listeners and cancels pending work without changing saved state or moving focus. Call it before removing or replacing the controls. -`src/ui.js` retains panel layout, persistence, share restoration and application +`src/ui/applicationShell.js` retains panel layout, persistence, share restoration and application reactions to state changes. Map Source selection and Location draft cleanup are provided through callbacks. The component imports no globe, data, application or server modules. Package checks and scoped formatting cover this entry. @@ -280,7 +276,7 @@ compatible through re-exports. ### Adding a right-rail readout panel -Add the shell in `src/ui/templates/context.html` inside `#right-context-rail`: +Add the shell in `src/ui/views/ContextView.vue` inside `#right-context-rail`: a `.panel-collapsible` with a stable `id` and matching `data-panel-id`, the existing `.panel-header`, collapse button and a dedicated body. Register it with the existing panel chrome/layout owners; do not allocate rail space in @@ -301,13 +297,13 @@ compactStatus, blocks }` in place. Their full-width native button headers expose adapter owns accordion state. Generic cards do not select data or enable layers. `blocks` is an ordered array of loose, optional `{ id, type, ... }` descriptors, -rendered by `railCardBlocks.js`. There are no per-section disclosures: +rendered by `rail/RailCardBlocks.vue`. There are no per-section disclosures: - `summary`: `text`, a single summary line. -- `list`: `list: { ariaLabel, items }`, reconciled by `syncRowList` in `rowList.js`. +- `list`: `list: { ariaLabel, items }`, keyed by `rail/RailRowList.vue`. - `lines`: keyed `lines: [{ id, text, muted? }]` for details and status. - `settings`: `settings: [{ id, label, chips }]`; each labelled row has a 56 px - label column and a wrapping `syncChipGroup` from `chipGroup.js`. + label column and wrapping keyed chips from `rail/RailChips.vue`. - `legend`: `legend: { colors, labels, units?, zeroIndex?, categorical? }` for a ramp/scale (including a physical freezing anchor) or separate keyed swatches. - `actions`: keyed `actions: [{ id, label, params?, href?, onClick?, disabled?, @@ -376,12 +372,10 @@ facade's asynchronous teardown can yield. ## Ordinary action buttons -`src/ui/actionButton.js` is a native, listener-free view helper used by Scene -presentation and sharing-toolbar owners. It receives translated text and explicit -presentation/accessibility properties. Feature controllers own action dispatch, -state, persistence and cleanup. Static Scenes/CCTV templates use the same CSS -surface and preserve IDs and layout hooks. The helper imports no application, -renderer, storage or services; it belongs to the consuming package closures. +The scene panels view and the CCTV panel draw their action buttons with the shared +`gev-action-button` classes in `src/ui/styles/action-button.css`. Feature +controllers own action dispatch, state, persistence and cleanup, and the views +preserve IDs and layout hooks. ## Display controls @@ -405,9 +399,9 @@ remain in their callers. `ui/maps` owns source-chip presentation, selection feedback and its subscription lifetime. It receives the existing controller and explicit state/action callbacks; it imports no renderer or application. Source construction and availability policy -remain with the map controller. Rebuilding controls removes their previous chip -listeners, and destruction suppresses late completions without owning or destroying -the supplied controller. +remain with the map controller. The dock renders the chips; rebuilding controls +drops their previous subscription, and destruction suppresses late completions +without owning or destroying the supplied controller. ## Layer panel @@ -470,8 +464,8 @@ release listeners; pending action feedback is limited to its current owner. ## UI assembly and styles `ui/shell` assembles controls from supplied existing layer, navigation, terrain, -rendering, HUD and share operations. `src/standalone/ui.js` provides the running -application's instances; `src/ui.js` remains the compatibility entry. The shell +rendering, HUD and share operations. `src/standalone/controls.js` provides the running +application's instances. The shell imports no standalone bootstrap or concrete live layer implementation. Panel layout, position/drag, notices, recording and deferred UI work have separate owners with synchronous cleanup. Existing scene, share and HUD engines retain @@ -602,6 +596,10 @@ state subscriptions retain their existing event contract. tracking handoff. It reads Cockpit admission and uses supplied tracking operations. - `ui/share-restoration` owns the initial restore transaction, layer coordinator, status notices and gesture/timer cleanup. +- `share/view-codec` is the share-link codec (`decodeShareView`, + `encodeShareView`). It is pure (no DOM, no Cesium), so a host's server can + read a `/share/` link, for example to describe it in link previews. + `share/link-path` reads and builds the `/share/` address itself. - `ui/visual-settings` owns style preferences, detection overrides, display inputs and IR cleanup. Its engine services and panel operations are explicit. - `ui/panel-chrome` owns disclosure, docking, collapse preferences and temporary @@ -741,13 +739,13 @@ DOM and platform middleware. ### Native form fields -`src/ui/formField.js` constructs native numeric inputs without listeners or -application state. Static selects and number inputs use the same `gev-field` -surface, with explicit compact and inline densities. Feature owners retain +Native numeric inputs are rendered by the views that own them; the CCTV pose +chip renders its inline input in `src/ui/cctv/CctvCalValue.vue`. Static +selects and number inputs use the same `gev-field` surface, with explicit +compact and inline densities. Feature owners retain option identity/order, bounds, parsing, draft values, input/change timing and teardown. CCTV effective-pose conversion and tuner/device operations remain -feature responsibilities. A Vue replacement can reuse this contract and CSS; -it replaces DOM construction, rather than adding a second state owner. +feature responsibilities. The shared stylesheet owns field surfaces and disabled opacity; feature and theme palettes use `--gev-field-background`, `--gev-field-border-color`, @@ -759,11 +757,9 @@ field rules separate, so Radio's palette remains an intentional exception. ### Read-only status badges -`src/ui/statusBadge.js` constructs/updates ordinary badges without listeners, -status inference or live-region policy. It consumes translated text and controlled -visibility; appearance variants share CSS rather than mapping machine states to -universal severity. CCTV owns frame/source and calibration rules and their data -attributes, while Weather owns coverage labels. The keyed rail-card owner keeps -node identity, header focus, disclosure and teardown. The shared adapter never -creates another state store or announces all card updates. A Vue view can replace -this adapter and retain these presentation inputs and styles. +Views draw read-only badges with the `gev-status-badge` classes, without +listeners, status inference or live-region policy. Appearance variants share +CSS rather than mapping machine states to universal severity. CCTV owns +frame/source and calibration rules and their data attributes, while Weather +owns coverage labels. The rail card keeps node identity, header focus, +disclosure and teardown, and does not announce every card update. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 9a3f5a18e..6d6a52f09 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1,5 +1,49 @@ # God's Eye View Current State +Docker Compose mounts the checkout with the shared SELinux `:z` label so +containers can read it on enforcing SELinux hosts. The separate image-owned +`node_modules` volume and loopback-only port publishing remain unchanged. + +The HUD's AI summary line looks up place names for the view only when the +summary can use them. The page asks `GET /api/openai/hud-summary/status` once; +a server without an OpenAI key answers `{ configured: false }`, and the HUD then +makes no reverse-geocode or Nearby Places request until the page reloads. A +server without the route is asked once, and an unconfigured summary reply +stops the lookups the same way. Each lookup reverse-geocodes at most two +points: the view center alone below 10 km, the center and one viewport point +up to 750 km, and two viewport points up to 3000 km; Nearby Places is still +added below 25 km. A lookup is reused until the view center moves a quarter of +the visible width, the camera's distance to the center doubles or halves, or +the camera crosses one of those heights. The width is measured from the +camera's distance to the view center, so terrain and tilt scale it. A reused +lookup asks again only for what it did not get (a timeout, a failure, an empty +answer); everything else is a cache hit. A small pan over a reused lookup +repaints the last AI reply instead of asking for a new one. A host passing +`cachedOnly` keeps reading every cached sample. Voice context lookups are +unchanged. + +The HUD's LAT/LON degrees-minutes-seconds readout rounds the coordinate once, to +hundredths of a second, and then splits it. A value within 0.005" of the next +whole minute carries into the minutes and degrees, so `-122.35` reads +`122°21'00.00"W` rather than `122°20'60.00"W`. The field widths and hemisphere +letters are unchanged. + +Aircraft detection candidates reuse normalized display text until callsign or +registration changes, including in-place enrichment. Each candidate resolves its +fleet model once per collection. Positions, model ownership and tracked-aircraft +anchors remain live on every collection; contact admission is unchanged. + +Civil aircraft share one delayed render timestamp per fleet update. Extrapolated +fixes reuse their local coordinate frame until their coordinates change, including +in-place ground-height corrections. Update cadence, contact counts, interpolation, +turn rates and tracked-aircraft motion are unchanged. Cached frames are weakly +keyed by each position fix and do not retain retired history. + +Touch-first devices use three times the default Cesium zoom sensitivity, so +pinching traverses globe and street scales with fewer gestures. Cesium retains +its distance-aware zoom, collision handling and minimum/maximum distance limits. +Fine-pointer desktop mouse and trackpad sensitivity is unchanged. + Analyst region queries resolve unique Natural Earth names offline without geocoding, so Gold Coast and Ivory Coast name the West African natural regions. Duplicate names require a geocoded containment anchor; when several entries for @@ -9,6 +53,15 @@ broader suffix variants. Follow-ups retain boundary provenance, and cancelled lookups preserve the previous analyst cohort and boundary source. Navigation search keeps a successful geocode, with no search or view-based region recovery. +Manager-owned periodic network updates pause while the document is hidden. Returning to a visible tab runs one overdue update per enabled layer without overlapping an active refresh and restarts that layer's polling schedule from the return, so the next poll follows one interval later whether or not the catch-up update succeeds; explicit refresh requests remain available. Transit also defers camera-driven feed admission while hidden and rechecks coverage on return. Visibility listeners are released with their polling intervals. + +Terrain height requests accept at most 64 points, matching the browser service's +existing batch size and one bounded upstream chunk. Larger requests return HTTP +413 before disk-cache loading or upstream work; malformed coordinates remain 400. +The browser continues to split larger workloads into sequential 64-point requests. + +Traffic holds continuous rendering only while enabled with animated dots or a pulsing jam heat-line. Empty and high-altitude views release the hold. A failed load leaves no hold only when no animated content is retained: if an earlier load succeeded, ingestion keeps its animated points and the hold stays. Population admission reacquires the hold and resets animation time. Clear, disable, and destroy also release it. Retained fading dots keep animation alive until they are removed. When the last point leaves mid height correction, the correction is settled at once, before the hold is released, so it cannot keep the layer reporting loading. + Shared distance checks use `src/geoDistance.js` with `{ lat, lon }` points and the IUGG mean spherical Earth radius, 6371.0088 km. `greatCircleKm` is canonical; `greatCircleMeters` is its exact metre conversion. Missing, @@ -144,6 +197,9 @@ catalog, loaded on first use. See [tools and the MCP server](TOOLS.md). Aircraft altitude-band queries require a reported barometric or geometric altitude for each match, so a grounded contact that reports none is left out of a band too. Queries without an altitude band retain unknown-altitude contacts. +`find_submarine_cables` lists and counts each cable once: TeleGeography splits +a few cables (2Africa, Echo) into several features under one id, and such a +cable matches when any of its parts crosses the area. Catalog calls check their cancellation signal immediately before executing a tool. Already-cancelled queries do not start; cancellation after execution starts @@ -161,6 +217,32 @@ Regression coverage lives in `src/tools/area.test.mjs`, `src/tools/queries/environment.test.mjs` and `src/layers/perimeters/cards.test.mjs`. +## Scope display controls + +Display's native Scope checkbox defaults ON with feather at 100%, adjustable +from 0–200%. OFF disables the slider while retaining its value. HUD layout +changes preserve both settings; share links and scenes restore them. The +historical missing-`scf` fallback remains 35%; explicit 0% keeps the hard edge. + +With Scope OFF, Tactical and Minimal add text-sized black backplates +at 70% opacity behind telemetry, vertical metadata and scene chrome. Original +HUD text color, glow and opacity are preserved. Scope OFF brightens the tagline +and Active Style label with the shared secondary text color. The title plate +covers the eye and title without moving the title or tagline. Scope ON retains +the original HUD presentation; Cyber retains its existing skin. + +`node scripts/qa-scope-hud.mjs` checks native input, range, layout persistence, +restoration and both Scope presentations. `QA_SCREENSHOT_DIR` saves screenshots. + +## HUD layouts — October 29, 2026 + +Display > HUD > Layout offers Tactical, Minimal and Cyber; the `set_hud` voice +action accepts the same three. The Operator layout is retired. Share links +keep its enum code so older links still parse, and a link, scene or saved state +that names Operator restores the default Tactical layout. Operator was the +only layout that showed the HUD's top banner bar, so its markup and styles are +removed too. + ## Cyber HUD — September 30, 2026 Display > HUD > Layout includes Cyber, also available through the HUD voice @@ -176,8 +258,8 @@ red hover border in both map and cockpit, so clipping does not hide the highligh Cyber's map-side right rail opens one panel at a time while keeping collapsed launchers available. Display, CCTV and Context scroll their contents inside fixed headers and decorative frames. The narrow-screen rail remains scrollable to reach -each panel. Radio retains the shared nested Context player and compact -header disclosure, without relocating playback controls on theme changes. +each panel. Radio retains the shared nested Context player without relocating +playback controls on theme changes. Voice help/error popups and Location/Visual Presets pins extend beyond their Cyber frames without being clipped: the shapes decorate non-interactive @@ -270,6 +352,11 @@ catalog construction owns each instance. Wind animates one forecast without advancing forecast time. Separate Weather observation layers provide radar and satellite history; none claims measured cloud volume. +Weather imagery releases its camera move listener on disable and registers it once on re-enable. Disabling also clears active imagery work. +A staged weather frame with loaded tiles and no pending requests rechecks +readiness on a single 150 ms wake timer instead of requesting every render. +The existing readiness conditions and two settled frames still govern promotion; +completion, cancellation, replacement and teardown clear the wake timer. Weather imagery drapes onto the globe basemap. On photorealistic 3D Tiles each observed product is instead a raised, translucent shell: one rectangle over the product bounds at a fixed height (global infrared 5.5 km, regional infrared @@ -391,10 +478,40 @@ not worldwide detections; units are strikes/km²/min ×10³. It is neither raw G flashes nor a live strike counter. Weather imagery orders scalar context below satellite, radar and lightning consistently across enable and history order. +A weather source can describe its own products (credit, units, coverage, +legend) through an optional `display` object in each product manifest: +`source` (≤60 characters, the row's source name), `credit` (≤200, the imagery +credit in Data attribution, shown as plain text and updated when a refreshed +manifest changes it), `units` (≤40), `coverage` (≤48, the coverage badge and +info line), `info` (≤600, the info tooltip), `legend` (2–16 `{ label, color }` +stops; label a plain decimal number or text, at most 16 characters either way, +a number reading back exactly as the legend shows it: within ±2^53−1, with +no leading zeros and no digits lost to rounding; color `#rrggbb`) and `extent` +(`{ west, south, east, north }` degrees, non-empty; west greater than east +crosses the antimeridian; used for coverage navigation and the +outside-coverage status). `units` and `legend` describe one scale and +apply only together: if either is absent or invalid, both keep the built-in +values, and the radar info heading names the units in use. Text must be +single-line, without control characters (U+0000–U+001F, U+007F–U+009F), line +or paragraph separators, or invisible and 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), byte-order mark (U+FEFF) and tag characters +(U+E0000–U+E007F). Each other field is validated on its own; an absent or +invalid field keeps the built-in value, so the bundled nowCOAST manifests, +which carry no `display`, look exactly as before. Accepted source +and units text is source data and is shown untranslated. The static Data +attribution entries remain the application's `credits` list. + Cyclones combine fixed NHC/CPHC status and summary GIS endpoints through a bounded same-origin provider. The status endpoint lacks browser CORS; each installation fetches keyless public sources at runtime, with no centralized ingestion service. The five-minute snapshot covers Atlantic and eastern/central North Pacific only. +Classification labels follow NHC status codes: `PC` is a potential tropical +cyclone, `PTC` a post-tropical cyclone, and `STS`/`STD` a subtropical +storm/depression. Unknown codes remain visible unchanged. Current positions, advisory issue time and position time stay distinct. NHC may release an advisory before its nominal time (routinely up to about 30 minutes, rarely about an hour), so advisory issue and position times up to and including @@ -407,6 +524,8 @@ than relabeling older geometry. The cone represents forecast center uncertainty, not storm size or the full hazard area. Successful empty and unavailable states are distinct. One owned native Cesium data source preserves polygon holes and geographic seams and releases on disable; forecast animation is not implied. +NHC movement speed is converted from miles per hour to rounded knots by the +proxy; advisory wind intensity is already in knots and is passed through. Cyclone markers, labels, tracks and cones are hidden beyond the horizon on every map source. Consult the linked official advisory for safety decisions. @@ -441,13 +560,23 @@ those reports arrive as 102.3 knots and 360 degrees. Both are stored as unknown, and vessel cards, the HUD and analyst queries show a missing measurement rather than a reading. Heading keeps its existing separate check for its own sentinel. Genuine readings are unchanged, including a stopped vessel's zero and the -highest speed and course the fields can encode. +highest speed and course the fields can encode. Vessel cards and the HUD wrap +the direction after rounding to whole degrees, so a course of 359.5° or more +reads `0°` (`0DEG` in the HUD) rather than `360°`. + +For an already tracked vessel, AISStream static messages update name, type, +destination and IMO without changing its position, motion, fix timestamps, +position freshness or trail. Non-empty static values replace earlier values; +omitted, empty or unavailable fields retain known metadata across Class B's +separate reports and in the cache used by the next position update. Position +reports still prefer their own metadata name. Invalid Class B parts are ignored; +absent fields prefer the latest live value over the static cache. Native ` ``` -`data-i18n` replaces the element's text, so put it on the element that directly holds the text, never on a parent with child elements. `data-i18n-title`, `data-i18n-aria-label` and `data-i18n-placeholder` translate that attribute. Markup is translated once at boot, and switching language reloads the page. A module that holds work a reload would lose (an unsaved edit, an open session) registers it with `registerUnsavedWork(() => busy ? msg('unsaved …') : null)` from `src/i18n/index.js`, and the language picker asks before reloading. +`data-i18n` replaces the element's text, so put it on the element that directly holds the text, never on a parent with child elements. `data-i18n-title`, `data-i18n-aria-label` and `data-i18n-placeholder` translate that attribute. `data-i18n-context="radio tag"` gives the element's text a context, the catalog entry `radio tag|All`, as `t('All', { context: 'radio tag' })` does in code. Markup is translated at boot and again after every language switch. + +## Switching language + +The language picker switches in place, without a reload. A view that calls `t()` while it renders follows by itself. Code that translates text and hands it on (to a store, a published view object, a layer's state, a DOM node) must word it again when the language changes: + +- **Present again.** `onLocaleChange(listener)` from `src/i18n/index.js` calls the listener after each change and returns the function that removes it; call that at teardown. The listener presents the view again from what it keeps, as `layerPanel.js` and `cockpitController.js` do. +- **Keep the English, translate when presenting.** Store `msg()` text or a function that words it, not its translation: the voice control takes `update(part, { text: () => t('MIC') })`, the scene director's status and the SDR receiver's messages keep such functions, and `imagerySplit.js` takes its titles that way. +- **Translated constants in a view** go in `computed`, not in a plain `const` set up once. + +A toast already on screen keeps its words. ## Rules @@ -66,16 +88,16 @@ An empty translation is reported, and so is one left as the English, plural form `npm run i18n` and `npm test` fail on UI text that is not marked: -- **Markup:** element text without `data-i18n`, and `title`, `aria-label` or `placeholder` without the matching `data-i18n-*`. +- **Markup:** element text without `data-i18n`, and `title`, `aria-label` or `placeholder` without the matching `data-i18n-*`. In a Vue template, static text or a static `title`, `aria-label` or `placeholder` (rather than `t()`), and any `data-i18n*` attribute. - **Code:** string literals that reach the page without `t()`, `tn()` or `msg()`: assigned to `textContent`, `title` and the like; passed to `setAttribute('title' | 'aria-label' | …)`, `createTextNode`, `showToast` and other notice helpers; assigned to a layer's `error`, `lastError`, `loadingLabel`, `statusMessage` or `status` (a plain `error = …` too); or kept in `label:`, `title:`, `status:` and similar properties. Not reported: declarations (`let status = …`), default values, ternary branches and lower-case codes (`'auth-failed'`). The full list is `JS_SINKS` in `scripts/i18n.mjs`. Fix it by marking the text. When the text must stay as it is, say so where it lives: -| Case | How | -| --------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -| Codes, units, product names in markup (`0.9 dB`, `ADS-B · 1090`, `OpenSky`) | `translate="no"` on the element; it covers everything inside, as in HTML | -| Markup whose text code sets through `t()` (a status that starts as `OFF`) | `data-i18n-runtime` on the element | -| A literal in code that is not UI copy (a product name, sample data) | `// i18n-ignore: ` on the line or the line above | +| Case | How | +| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------ | +| Codes, units, product names in markup (`0.9 dB`, `ADS-B · 1090`, `OpenSky`) | `translate="no"` on the element; it covers everything inside, as in HTML | +| Page markup whose text code sets through `t()` (a status that starts as `OFF`) | `data-i18n-runtime` on the element (not in a Vue template) | +| A literal in code that is not UI copy (a product name, sample data) | `// i18n-ignore: ` on the line or the line above | Icons (`aria-hidden="true"`, Material Symbols), comments, CSS values and lowercase identifiers are skipped automatically, and so is `src/tools/`, whose text is written for AI clients rather than the page. @@ -84,6 +106,7 @@ Icons (`aria-hidden="true"`, Material Symbols), comments, CSS values and lowerca | File | What it holds | | ------------------------------- | ------------------------------------------------------------------------------ | | `src/i18n/index.js` | `t`, `tn`, `msg`, the Intl formatters, markup translation, switching language | +| `src/ui/i18n.js` | the page's vue-i18n instance: the active locale and the catalogs | | `src/i18n/marker.js` | `msg` alone, for modules the server or tools import (no catalogs) | | `src/i18n/locale.js` | shipped languages, their names, choosing the language at boot | | `src/i18n/locales/.json` | one catalog per language, keyed by English text | diff --git a/docs/INFRASTRUCTURE-LAYERS.md b/docs/INFRASTRUCTURE-LAYERS.md index 76567e605..31d3cb4cb 100644 --- a/docs/INFRASTRUCTURE-LAYERS.md +++ b/docs/INFRASTRUCTURE-LAYERS.md @@ -53,9 +53,8 @@ Cesium installation shared with its viewer; it must not bundle separate copies. `gods-eye-view/infrastructure/geojson` exports the lower-level `createLocalGeoJsonLayer(options, services)` and existing infrastructure overlay helpers. `gods-eye-view/infrastructure/lod` exports the existing LOD policy helpers. -The standalone app's `src/data/localGeojson.js` keeps its original single-argument -factory and supplies its existing functions. Other consumers should use the package -exports, which do not import standalone application globals. +Consumers use the package exports, which do not import standalone application +globals. Dataset files and source/license notices remain under `src/data/local_data/`. Their input counts are 4,351 datacenter features and 704 dam features. Cesium may diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index 65d0abae9..7bf6d3751 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -97,7 +97,8 @@ operational sample contained 3,909 observations and two selected labels, but its live aircraft and traffic rows were empty, so it remains a limited stress case. Snow, Noir, dense detection, and text-heavy combined layers are the clearest -controlled comparison points for later optimization work. +controlled comparison points for later optimization work. Snow has since been +retired as a visual preset; its row records the measurement as taken. ## Keyed live sources diff --git a/docs/SCENE-DOCUMENT.md b/docs/SCENE-DOCUMENT.md index f6dc66636..b96896024 100644 --- a/docs/SCENE-DOCUMENT.md +++ b/docs/SCENE-DOCUMENT.md @@ -1,6 +1,6 @@ # Scene document format -Scenes exports a UTF-8 JSON project. Version 5 adds [data packs](DIRECTOR-DATA-PACKS.md) +Authoring exports a UTF-8 JSON project. Version 5 adds [data packs](DIRECTOR-DATA-PACKS.md) and is the write format. Versions 1, 2, 3, 4 and unversioned legacy files migrate on import without converting their existing shots into authored moves. Unknown versions or unsupported fields are rejected with a field path before the current project, selection or saved bytes change. diff --git a/docs/SHARE-LINK-FORMAT.md b/docs/SHARE-LINK-FORMAT.md new file mode 100644 index 000000000..5728d1ec3 --- /dev/null +++ b/docs/SHARE-LINK-FORMAT.md @@ -0,0 +1,370 @@ +# Share-link format + +A share link restores a view: where the camera is, what it follows, the visual +style, data layers and panel layout. There are two forms. + +- **Long form** — `#v=2&lat=30.2672&lon=-97.7431&…`. Parsed by + `src/sharelink.js`. The address bar follows the live view in this form, and + links already in the wild keep working; the long form is never removed. +- **Short form** — `/share/`, where `` is wherever the app is + served (an origin's root, or a base path) and `` is encoded and + decoded by `src/share/viewCodec.js`, which is pure (no DOM, no Cesium) and + needs no server. Hosts import it as `@gods-eye-view/core/share/view-codec` + (a server can read a code to describe the view in a link preview). The share button makes this form. The same code opens on + any copy of the app. `src/share/shareLinkPath.js` reads and builds the + address, and `src/share/legacyShareState.js` converts between the long + form's parsed state and the code's state. + +The same view in both forms (a real session's link): + +| Form | Characters | +| ----- | ----------------- | +| Long | 302 (the hash) | +| Short | 39 (the ``) | + +For example, a camera-only place view is a 24-character code (31 with its +created-at time). +Following an aircraft is 42–50: 42 when the link is made from the app's own +follow camera with the fix taken as the link is made, 44 with a fix four +seconds older, and 50 after orbiting to another heading and pitch. A view in +any other style that keeps the style's own look adds only the style (two +bytes): the 42-character followed aircraft in Noir is 44. + +## Opening a short link + +At start-up the app reads `` from an address whose path ends in +`share/` (a host can pass the code instead, as `initialShareCode` to +`createApplicationControls`). A code that decodes runs the same restore as a +long link; once the app next writes the address, `/share/` is replaced +by the app's own address and the long form, without a new history entry, so +a reload follows the live view. A code that is rejected restores nothing: the +app starts as usual and says the link can't be opened. + +A thing view turns on its entity's layer and follows the entity by id, using +the same restore as a long link's followed flight or satellite (and its +expiry windows); a vessel is selected once the vessel feed has it. Opening is +best effort: until the entity is found, and if it is not found in time, the +camera is where the link was made, looking at the entity's last-known fix, +and the app says that the entity isn't reporting and when the fix was taken. +The share button makes a thing view while the camera follows an aircraft, +military aircraft or satellite. A selected vessel is not followed (the camera +stays where the user puts it), so a link made then is a place view; vessel +thing views open, but the app does not make them yet. + +## Payload + +The code is unpadded base64url (`A–Z a–z 0–9 - _`). The decoded bytes are: + +1. one **version byte**, currently `1`; +2. a sequence of **fields**, each a varint tag `field_number << 3 | wire_type` + followed by its value; +3. one **check byte**: the CRC-8 of everything before it. + +| Wire type | Value | +| --------- | ----------------------------------------------------------- | +| 0 varint | unsigned LEB128, at most 8 bytes, at most 2^53 − 1 | +| 1 fixed64 | 8 bytes (not used by v1; skipped) | +| 2 LEN | varint length, then that many bytes (text or a sub-message) | +| 5 fixed32 | 4 bytes (not used by v1; skipped) | + +Signed values are zigzag varints (0, −1, 1, −2 → 0, 1, 2, 3). Text is printable +ASCII. Fields are written in ascending order; the decoder accepts any order. + +A field whose value equals its default is **omitted**. The decoder fills every +omitted field from the same defaults table the encoder reads (for a +style-dependent field, the decoded style's defaults), so an empty +payload (code `AQc`: the version byte and its check byte) is the all-default +view. + +### Check byte + +The check byte is CRC-8 with polynomial x⁸ + x² + x + 1 (`0x07`), initial +value 0, no reflection and no final XOR (CRC-8/SMBUS; the check value of the +ASCII digits `123456789` is `0xF4`). A decoder that computes a different value +rejects the code (`checksum`) before reading any field, so a mistyped code +never opens a different view. A degree-8 CRC catches every error burst of up +to 8 bits; one base64url character is 6 bits, so **every single-character +typo is rejected** (a test tries every substitution at every position). Two +swapped neighbours, a dropped or an extra character are caught with a +probability of about 255 in 256 (many of them already fail as base64url). + +## Field registry (top level) + +| # | State key | Type | Default | Encoding | +| --- | ---------------------------- | ----- | ----------- | ----------------------------------------------------------------- | +| 1 | `view.kind` | enum | see below | 0 place, 1 thing | +| 2 | `lat` | coord | 0 | zigzag of degrees × 10⁴ | +| 3 | `lon` | coord | 0 | zigzag of degrees × 10⁴ | +| 4 | `alt` | sint | 800 | whole metres | +| 5 | `heading` | angle | 0 | whole degrees, see angles | +| 6 | `pitch` | sint | −35 | whole degrees | +| 7 | `roll` | angle | 0 | whole degrees | +| 8 | `sharedAtMs` | epoch | none | zigzag seconds since 2026-01-01T00:00:00Z | +| 9 | booleans | flags | 0 | bit set when the value differs from default | +| 10 | `layerState.enabledLayerIds` | LEN | none | layer bitset, see layers | +| 11 | `layerState.options` | LEN | none | packed option triples, see layers | +| 12 | `panelState` | LEN | none | packed panel entries, see panels | +| 13 | thing | LEN | none | sub-message, see thing views | +| 14 | `style` | enum | `normal` | normal, retro, surveillance, thermal, anime, noir, snow (retired) | +| 15 | `mapStack` | enum | `photoreal` | photoreal, bing-aerial, bing-labels, esri-imagery, osm | +| 16 | `hudVariant` | enum | style | tactical, operator (retired), minimal, cyber | +| 17 | `bloomIntensity` | uint | style | | +| 18 | `bloomVersion` | uint | 2 | | +| 19 | `sharpenIntensity` | uint | style | | +| 20 | `detectionMode` | enum | style | OFF, SPARSE, BALANCED, DENSE | +| 21 | `detectionDensity` | uint | style | one of 0, 25, 50, 75, 100 | +| 22 | `detectionAllocation` | enum | style | ELASTIC, WEIGHTED | +| 23 | `detectionFadePct` | uint | style | 0–40 | +| 24 | `detectionOutsideOpacityPct` | uint | style | 0–100 | +| 25 | `scopeFeatherPct` | uint | 100 | 0–200 | +| 26 | `scopeTerminusPct` | uint | none | 94–100; absent is the adaptive terminus | +| 27 | `styleParams` | LEN | style | packed `(token, value × 100)` pairs | + +Field 1 absent means `thing` when field 13 is present and `place` otherwise, +so v1 never writes it; a later view kind writes its code. + +Enum codes are the list position (0, 1, 2…). A name not in the list is written +as LEN text in the same field, so the app can add a style or map before this +table learns its code. A new style's settings are encoded against its own +defaults, while a decoder that does not know the style uses Normal's; adding a +style also extends the pinned style-defaults table. + +`hudVariant` code 1 (`operator`) is retired: the app no longer offers that +layout and restores it as `tactical`. The code stays reserved. + +Flag bits (field 9): 0 `bloom`, 1 `sharpen`, 2 `hudVisible`, 3 +`celestialRing` (each defaulting by style), 4 `scopeEnabled` (default on). + +### Style defaults + +A default marked **style** is what the view's style applies when it is +selected in a fresh session (`SHARE_VIEW_STYLE_DEFAULTS`): the app's +first-load baseline (`GLOBAL_POST_DEFAULTS` in `src/ui/visualPresets.js`), +with the style's preset on top (`STYLE_PRESET_DEFAULTS`), and the style's +shader parameters at the preset's values, else the shader's own. The encoder +compares against the defaults of the view's style, and the decoder fills +omitted settings from the defaults of the style it decoded (an unknown style +uses Normal's). So a view that keeps its style's look costs its camera and +style alone. (A recipient-side preference the app remembers, such as the +detection allocation, is not a style default: a sender who changed it pays +for it.) In v1: + +| Setting | Every style | +| ------------------------- | ------------- | +| bloom, intensity | off, 0 | +| sharpen, intensity | on, 49 | +| HUD visible, variant | on, tactical | +| detection mode, density | DENSE, 75 | +| allocation, fade, outside | ELASTIC, 7, 1 | +| celestial ring | off | + +| Style | Shader parameters (field 27 defaults) | +| ------------ | --------------------------------------------------------------- | +| normal | none | +| retro | pixelation 1, distortion 0, instability 0.42 | +| surveillance | gain 0.18, bloom 0.22, scanlineStr 0.96, pixelation 1 | +| thermal | sensitivity 0.85, bloom 0.2, mode 0.33, pixelation 1, palette 0 | +| anime | saturation 1, edgeThick 0.5 | +| noir | contrastAmt 1.2, grainAmt 0.5, vignetteAmt 0.5 | +| snow | retired: decodes as normal; its parameters are dropped | + +These are frozen like every other default: the codec reads them from the +app's tables today, and a test pins them, so if a table moves the old values +are frozen in the codec rather than changing what shared links mean. + +### Precision + +| Value | Stored as | Matches the long form | +| --------------------------- | ------------------------- | --------------------- | +| Latitude, longitude | 10⁻⁴ degree (about 11 m) | `toFixed(4)` | +| Altitude, range | whole metres | `Math.round` | +| Heading, pitch, roll | whole degrees | `Math.round` | +| Intensities and percentages | integers | `Math.round` | +| Style parameters | hundredths after clamping | `sp` (× 100) | +| Created-at and observed-at | whole seconds | `at` | + +Angles: heading and roll are normalized into [0, 360) — the long form can +write `roll=360` for a camera at 359.6°, which the compact form stores as 0 — +and written as the nearer signed turn, so 350° costs one byte (−10). +When a state is encoded, out-of-range input is clamped (latitude, pitch) or +wrapped (longitude). A payload that stores a latitude or longitude beyond +±90/±180 cannot come from the encoder and is rejected. + +### Layers (fields 10 and 11) + +A layer's number is derived from its permanent layer-state token +(`src/data/layerStateTokenReservations.json`): a one-character token is its +index in `0–9a–z`, a two-character token is 36 + first × 36 + second. The +token ledger never reassigns a token, so layer numbers never change and new +layers need no change here. + +Field 10 is a bitset: bit _n_ of byte ⌊n/8⌋ (least significant first) is set +when layer _n_ is on. Present-but-empty means "every layer off"; an absent +field leaves the recipient's layers alone (the long form's `l=` versus no `l`). +A thing view's entity layer is always on, so its bit is not written: in a +thing view, present-but-empty means "only the entity's layer", and an absent +field opens with only the entity's layer on (the app writes the field +whenever it has a layer set). + +Field 11 repeats `(layer number, option key, value)` for each option that +differs from its frozen absent meaning (`SHARE_VIEW_LAYER_OPTION_ABSENT`). The +option key is `option token index × 2 + kind`: kind 0 means the value is a +zigzag varint (an option whose long-form spelling is a canonical integer), +kind 1 means a varint length and ASCII text (the long-form spelling). Values +are decoded by the layer-state codec's own option decoders. + +A followed aircraft, military aircraft or satellite is a thing view in the +compact form, not a tracking option: the encoder moves it out of the layer +options, and the converter moves it back for the long form. + +### Panels (field 12) + +Each entry is one varint: `panel number × 16 + collapsed × 4 + pinned`, where +collapsed and pinned are 0 (no value), 1 (false) or 2 (true). Only panels that +differ from the frozen default layout are written, plus a collapsed-0 entry +for each panel the state has no opinion about (an older long-form link that +predates the panel); a panel with no entry takes its default. Values of 3 are +reserved: a decoder ignores that entry, as for collapsed 0. + +| # | Panel | Long-form token | Default | +| --- | ---------------------- | --------------- | ------------------- | +| 0 | `control-panel` | `c` | collapsed, unpinned | +| 1 | `location-bar` | `l` | collapsed, unpinned | +| 2 | `data-panel` | `d` | collapsed | +| 3 | `cctv-panel` | `v` | collapsed | +| 4 | `recent-imagery-panel` | `i` | expanded | +| 5 | `street-level-panel` | `t` | collapsed | +| 6 | `radio-panel` | `r` | collapsed | +| 7 | `scene-panel` | `s` | collapsed | +| 8 | `global-context-panel` | `g` | collapsed | +| 9 | `pp-toggles` | `p` | collapsed | +| 10 | `param-slider-panel` | `m` | expanded | + +### Style parameters (field 27) + +Pairs of varints `(token index, value × 100)` for the parameters of the +view's style that differ from its defaults, using the long form's `sp` tokens +(`SHARE_VIEW_STYLE_PARAMS`). The decoded state always lists every parameter +of the style: the stored ones over the style's defaults. A long link without +`sp` converts to the style's defaults. + +## View kinds + +A **place** view (field 13 absent) opens at its camera, fields 2–7. + +A **thing** view (field 13 present) follows an entity. Field 13 is a +sub-message: + +| # | Field | Encoding | Default | +| --- | ----------- | -------------------------------------------------------- | ------- | +| 1 | aircraft | entity id: varint (24-bit ICAO address) or LEN text | | +| 2 | military | entity id: varint (24-bit ICAO address) or LEN text | | +| 3 | vessel | entity id: varint (MMSI) or LEN text | | +| 4 | satellite | entity id: varint (NORAD catalog number) or LEN text | | +| 5 | range | uint metres from the entity to the camera | 2000 | +| 6 | heading | angle, the camera's heading while following | 0 | +| 7 | pitch | sint whole degrees, the camera's pitch while following | −35 | +| 8 | fix lat | coord, the entity's last-known latitude | none | +| 9 | fix lon | coord, its last-known longitude | none | +| 10 | fix alt | sint metres | 0 | +| 11 | fix age | sint seconds the fix predates the created-at time | 0 | +| 12 | observed-at | epoch, when the fix was taken; only without a created-at | none | + +The entity kind is the field that carries the id (its code in +`SHARE_VIEW_ENTITY_KINDS`), so it costs nothing; exactly one of fields 1–4 is +present (two is malformed). Entity ids are the ones the layers look up: an +aircraft's ICAO 24-bit address (six hex digits, stored as a number; other +forms such as TIS-B `~abc123` as text), a vessel's MMSI and a satellite's +NORAD catalog number (decimal numbers; an MMSI with a leading zero stays +text). Ids are lowercase and match `[0-9a-z~_-]{1,16}`. + +Fields 5–7 are the follow offset: the camera looks along (heading, pitch) at +the entity from `range` metres away, heading clockwise from north, in the +entity's local east-north-up frame. The defaults match the app's own follow +camera, which looks north at about −35°, so only its range is written. + +Fields 8–12 are the last-known fix, the entity's position when the link was +made. Fields 8 and 9 are both written whenever there is a fix; their presence +marks it. Its time is the created-at time (field 8 at the top level) minus +field 11, or field 12 when the view has no created-at time. + +**The camera of a thing view with a fix is derived, not stored**: fields 2–7 +are not written, and the camera is where following the fix from the offset +puts it (`followCameraFor` in `src/share/follow.js`): the camera sits +`−r·cos p·sin h`, `−r·cos p·cos h` and `−r·sin p` metres east, north and up +of the fix (heading h, pitch p, range r), on a sphere of radius 6 371 000 m, +and looks back at the fix (its heading and pitch are those of that direction +at the camera, roll 0; at follow distances they equal h and p). A +decoder ignores fields 2–7 when the fix is present. A thing view without a +fix (one converted from a long link, which records neither offset nor fix) +writes its camera in fields 2–7 like a place view. + +A decoder that meets a view kind or an entity kind it does not know (an id in +a thing field it skips) opens the view as a place at its camera — the derived +one when there is a fix — and reports `fallback`. + +## Decoding rules + +`decodeShareView(code)` takes the bare code and never throws. It returns +`{ ok: true, state, unknownFields?, fallback? }` or `{ ok: false, reason }`. + +Rejected (the whole link): input over 2048 characters, bad base64url +(padding, foreign characters, impossible length, stray bits), fewer than two +bytes, a check byte that does not match (`checksum`), an unknown version byte, +a truncated value, a varint over 8 bytes or above 2⁵³ − 1, an +overlong varint, a LEN longer than what remains (at any level, including +inside the thing sub-message), a reserved wire type, field number 0, a known +field with the wrong wire type or repeated, two entity ids in one thing, a +duplicate panel/option/style entry, non-ASCII text, and a latitude or +longitude (camera or fix) beyond ±90/±180. + +Tolerated and reported in `unknownFields`: unknown fields at any level +(skipped by wire type), unknown enum codes and names (the default is used), +unknown flag bits, layers, panel values and style parameters, and layer +options the layer-state codec rejects or that are filed under a layer other +than their owner. Detection mode and allocation names outside their lists +fall back to the default rather than reaching the long form's lenient +migration. The map stack and HUD variant are open: a name outside their lists +that matches `[A-Za-z0-9_-]{1,32}` is kept as written (the app decides what +it means) and is not reported; only a name outside that grammar is. A +created-at time in the future is dropped, as the long form's `at` is. + +## Changing the format + +These rules keep every link that was ever shared working, in every version of +the app that can open it. + +1. **Field numbers are permanent.** Never renumber, never reuse. A field that + is no longer written stays in `SHARE_VIEW_FIELDS` marked `retired: true`. +2. **A field's type and wire type are permanent.** To change one, add a new + field. +3. **Defaults are frozen meanings.** A default is what an omitted field meant + when links were written. If the app's default moves, leave the table alone: + the encoder will start writing the new value explicitly. Style defaults + are read from the app's style tables; when those move, freeze the old + values in the codec (see Style defaults). The same applies + to `SHARE_VIEW_LAYER_OPTION_ABSENT` and the panel defaults. +4. **Codes are append-only.** Enum lists, entity kinds, flag bits, panel + numbers and style parameter tokens only grow. A style the app no longer + offers keeps its code: `snow` (6) is retired, so a link that names it + opens as Normal and the parameters written for it are dropped. +5. **Add, don't change.** A new setting takes the next free field number + (28 onwards; fields 1–15 have one-byte tags and are all taken). A new + boolean takes the next flag bit instead, which costs nothing when it is at + its default. +6. **New view kinds and entity kinds** get the next code. A new entity kind's + id takes the next free thing field (13 onwards), so older apps skip it and + fall back to the camera. A thing that needs more than an id (for example a + CCTV camera, a radio station or a transit vehicle) adds further thing + fields the same way. +7. **A new layer option** needs an entry in `SHARE_VIEW_LAYER_OPTION_ABSENT` + (a test fails until it has one). +8. **Bump the version byte only** for a change older decoders must refuse. + Decoders reject any version they do not know. The trailing CRC-8 check + byte belongs to every version. +9. **Derivations are frozen too.** `followCameraFor` and the follow-offset + defaults are part of the format: changing them moves the camera of every + thing link already shared. + +Add a test for every change: a round trip, and a payload written with the new +field that the previous decoder still reads. diff --git a/docs/TOOLS.md b/docs/TOOLS.md index 45ee62546..80fd80b13 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -92,7 +92,8 @@ pitch), data layers, visual style, map imagery, and an aircraft, military aircraft or satellite to follow. `createView` builds and bounds one, `viewToParams` and `viewFromParams` write and read it in the share-link format the app restores, and `viewUrl` gives the address that opens it. The -style names are the ones share links use. Ships cannot be followed from a +style names are the ones share links use; a retired style (`snow`) is read +as `normal`. Ships cannot be followed from a link yet. An aircraft's `follow` may ask for its cockpit view (`cockpit: true`); links cannot carry that and open the app following it. diff --git a/docs/TRANSLATORS.md b/docs/TRANSLATORS.md index 2131ab7a3..f83316c7a 100644 --- a/docs/TRANSLATORS.md +++ b/docs/TRANSLATORS.md @@ -1,6 +1,6 @@ # Translating God's Eye View -Shipped languages: **English** (source and fallback), **Spanish** (`es`) and **Russian** (`ru`). +Shipped languages: **English** (source and fallback), **Arabic** (`ar`), **German** (`de`), **Spanish** (`es`), **French** (`fr`), **Italian** (`it`), **Japanese** (`ja`), **Brazilian Portuguese** (`pt`), **Russian** (`ru`), **Ukrainian** (`uk`), **Simplified Chinese** (`zh-CN`) and **Traditional Chinese** (`zh-Hant`). ## How it works @@ -22,7 +22,7 @@ Every other language is one flat JSON file, `src/i18n/locales/.json`, keye A `tn()` entry is keyed by the English plural form and holds one string per `Intl.PluralRules` category of that language. A missing entry falls back to English. -The page picks its language from `?lang=`, then the choice stored by the language picker (the globe button in the bottom-right corner), then the browser languages, then English. `?lang=` is not stored, and copied share links leave it out. Switching reloads the page. +The page picks its language from `?lang=`, then the choice stored by the language picker (the globe button in the bottom-right corner), then the browser languages, then English. `?lang=` is not stored, and copied share links leave it out. Switching applies at once, without a reload. ## The check @@ -44,7 +44,7 @@ Edit `src/i18n/locales/.json` and run `npm run i18n`. To see your change, 1. Create `src/i18n/locales/.json`, fill it from `npm run i18n -- --missing ` (start from `{}`), and translate every entry. 2. Import it in `src/i18n/index.js` and add it to `CATALOGS`. -3. Add the code to `LOCALES` and its own-language name to `LOCALE_NAMES` in `src/i18n/locale.js`. Right-to-left languages also go in `RTL_LOCALES`. +3. Add the code to `LOCALES` and its own-language name to `LOCALE_NAMES` in `src/i18n/locale.js`. Right-to-left languages also go in `RTL_LOCALES`; the chrome mirrors through logical CSS properties (`inset-inline-*`, `margin-inline-*`), and `src/ui/styles/rtl.css` holds what those cannot express (see the Arabic notes). If the language should keep Western digits, map it in `INTL_TAGS` in `locale.js`. 4. Add `src/i18n/locales/.json` next to `ru.json` in `scripts/package-boundaries.json`. 5. Write `docs/translations/.md`: glossary, plural forms, units and style (copy the structure of `ru.md`). 6. Run `npm run i18n` and `npm test`, then check the panels in the app at narrow widths. @@ -63,4 +63,4 @@ These are contracts, not copy: layer ids and registry keys, status enums and oth ## Language notes -Each language keeps its glossary, plural forms, units and style in its own file, `docs/translations/.md`: [Spanish](./translations/es.md), [Russian](./translations/ru.md). Read it before translating, and add a term there when you settle on one. +Each language keeps its glossary, plural forms, units and style in its own file, `docs/translations/.md`: [Arabic](./translations/ar.md), [German](./translations/de.md), [Spanish](./translations/es.md), [French](./translations/fr.md), [Italian](./translations/it.md), [Japanese](./translations/ja.md), [Brazilian Portuguese](./translations/pt.md), [Russian](./translations/ru.md), [Ukrainian](./translations/uk.md), [Simplified Chinese](./translations/zh-CN.md), [Traditional Chinese](./translations/zh-Hant.md). Read it before translating, and add a term there when you settle on one. diff --git a/docs/UI-OWNERSHIP.md b/docs/UI-OWNERSHIP.md index 8d24215c7..46d0d5a2b 100644 --- a/docs/UI-OWNERSHIP.md +++ b/docs/UI-OWNERSHIP.md @@ -11,12 +11,18 @@ scene playback and voice. It delegates; new behavior belongs with its owner. | `cockpitCoordinator.js` | Cockpit entry/rollback, readouts and the single Display portal | | `visualSettings.js` | Effects, detection overrides, visual restoration and render-loop lifetime | | `panelChrome.js` | Disclosure, docking, layout and Cockpit panel restoration | +| `panelNavigation.js` | Desktop panel rails: each side's offered panels and the open one | | `aircraftDisplay.js` | Shared commercial/military 3D display preferences and controls | | `layerBindings.js` | Data-manager attachment, selected-tracker adoption and camera-entry listeners | | `displayBindings.js` | Keyboard/display subscriptions and frame-rate monitor lifetime | | `shareRestoration.js` | Initial share/layer restoration and cancellation | | `shellFeedback.js` | Loading feedback, notices and toast lifetime | +Owners that present UI do so through Vue: they publish plain view objects to a +Pinia store in `src/ui/stores/`, views in `src/ui/views/` render them, and user +actions return through the handlers the owner connects to that store. See +[Building UI](../CONTRIBUTING.md#building-ui). + Owners receive named services, elements, operations and readers. They do not receive the application shell itself. Readers resolve a replaceable collaborator at use time; operations cross an ownership boundary without copying its state. diff --git a/docs/VUE-MIGRATION.md b/docs/VUE-MIGRATION.md new file mode 100644 index 000000000..3dcd7f63b --- /dev/null +++ b/docs/VUE-MIGRATION.md @@ -0,0 +1,368 @@ +# Vue migration + +The application UI moves from static templates plus imperative DOM wiring to +Vue 3 components with Pinia stores. The migration keeps the experience the +same: element ids, classes, the global stylesheet, translations, keyboard +shortcuts, voice actions, share links and the phone layout all behave as +before. The one intended change is the panel rail described below. + +## Goals + +- Views are Vue single-file components; state that several views read lives in + Pinia stores rather than in DOM classes. +- The owners listed in [UI-OWNERSHIP.md](UI-OWNERSHIP.md) keep their + responsibilities. A component replaces DOM construction; it does not add a + second owner of the same state. +- Public methods used by layers, scene playback, voice and host applications + (`setPanelCollapsed`, `setContextMode`, `setStyle`, share and location + subscriptions) keep their signatures. +- Ids and classes used by browser QA and host applications stay stable until a + host can use an explicit extension point instead. + +## The panel rail + +Each desktop sidebar gets a floating icon rail that lists every panel available +on that side. Selecting an icon opens that panel; selecting it again closes it. +One panel is open per side at a time, so the rail replaces the accordion +stacking. Panels that are not available in the current mode (for example +Weather before a weather layer is active) are shown disabled or omitted as the +panel's own availability rule dictates. The rail hides with the rest of the +chrome in Cockpit, scene playback, recording and the clean view. + +Phones keep the existing bottom tab bar and sheets; the rail is desktop only. + +Voice and keyboard openings go through the same panel store, so they follow the +one-panel rule too. Share links continue to record which panels are open. + +## Component kit + +`src/ui/kit/` holds the generic components: `UiRail`, `UiTooltip`, `UiIcon`, +`UiButton`, `UiPanel`, `UiSegmented`, `UiSlider`, `UiChip`, `UiSelect`, +`UiBadge` and `UiEmpty` so far, with more joining as the controls they serve +are converted. One component +serves every use of a control, with props for the differences (`UiPanel`'s +`sideways`, `glow`, `scroll`), rather than a copy per panel. The components +are styled by the existing stylesheet and custom properties; a kit class such as +`ui-button--icon` carries no Core style yet, and a converted control keeps +its own classes beside it. They contain no application state. +`UiPanel` takes a `title`, slots and `v-model:collapsed`, but its +`collapsed` drives only the button and `ui-panel--collapsed`: the root's +`collapsed` class, which hides the body, is drawn at once by `v-panel-state` +from the panel chrome store, and there is no `closable`, since Core's panels collapse rather than close. +`UiSegmented` adds `keyboard` (`none`, `arrows`, or `radio` with +Home and End), since each of Core's groups keeps its own keyboard policy, and +`itemClass`/`activeClass` for Core's segment classes. A key moves focus at +once and asks for a change only when it lands on another option. Every +segmented group in Core is a `UiSegmented`; the old `segmentedControl.js` +adapter is gone. +`UiButton` adds `pressedClass`, Core's class on a pressed toggle (most use +`active`); its pressed classes toggle through `classList`, so a class other +code adds (a voice highlight) survives a press. Every pressed toggle in the +views is a `UiButton` except Bhote Koshi's cinematic button and the +tilt-map button, whose owners still write their state into the DOM. Core's +pressable chips and pills (rail card chips, map sources, cities and places) +are `UiButton`s too; `UiChip` is a non-interactive label chip, used by +the voice card. +`UiSelect` renders the bare native select (Core's labels sit outside it or +come as an `aria-label`) with `v-model`, `options` and `disabled`; +options that carry their own attributes come through the slot, a caller's +`@change` still reaches the select, and `hold` works as in `UiSlider`. +Without `hold`, a select takes its model back after its own change renders, +so a pick its owner refuses goes back; an owner that answers later (the SDR +gain) takes `hold`. Every select in the views is a `UiSelect`. +`UiBadge` takes a `tone` and carries Core's `gev-status-badge`; its +density variants and colours stay caller classes, and it renders the CCTV +source and calibration badges and the rail card badges. `UiEmpty` takes +a `title` and `hint`, but Core's empty states are one line, so the title +is the text itself rather than a bold heading; the Scene shot list, mission +roster and imagery strip use it. The locale picker's and the mission tables' +empty rows stay a paragraph and table cells. +`UiSlider` keeps the native range input rather than drawing a +`role="slider"` track, so Core's keyboard, form behaviour and styling stay; +it takes `v-model`, `min`, `max`, `step` and `disabled`, emits a +number, and adds `hold` (never rewrite a focused handle, and take the model +back on blur) and `unstyled` (the browser's own range, as Bhote Koshi's +are, without Core's slider surface). A caller's own input, change and blur +handlers still reach the input, and a null model leaves it as it is. It is +a functional render-function component, so the portable `./ui/input` rows +use it without a component compiler. Every range in the views is a +`UiSlider` except the radio tuner's: its owner moves the handle back to the +selected station on a drag's first input without changing the slot, which +a functional component would not render again. + +Controls share one skin, so every panel reads the same. Every scrolling +surface carries the kit's `.ui-scroll`, which draws the thin +themed scrollbar from `--scroll-thumb` and `--scroll-track`; the Cyber theme +sets those two tokens, and Cockpit's HUD windows keep their accent tint. A +surface without it would draw the platform's light scrollbar, because the +page declares no dark `color-scheme`. Selects and number fields are +`.gev-field`, which draws its own chevron in place of the platform's arrow +and keeps native spinners dark; ranges are `.gev-quantitative-slider`, with +a feature supplying only its accent. + +## Phases + +1. **Foundation.** Vue, Pinia and the Vite Vue plugin join the browser build + (`build/vite.js`, so host applications receive them too). One Vue runtime is + created by the shell (`src/ui/vueRuntime.js`) with one Pinia instance that + every view shares; disposing the shell unmounts them. Stores are added + when a view needs one, mirroring state an owner already publishes, so + there is never a second owner of the same state. +2. **Rail.** `PanelChrome` enforces one panel per side; the panel store + mirrors each side's panels and the rail renders from it. The rail panels + are `RailPanel`s: the kit's `UiPanel` drawing Core's panel markup, bound to the `gev-panel-chrome` + store, with their titles rendered by `t()` rather than `data-i18n`. Each panel's + collapse button, the dock tray toggles and the + dock pins render the open and pinned state PanelChrome presents to the + `gev-panel-chrome` store, and their clicks call it there. Each panel's + root draws its own state from there too (`v-panel-state`: `collapsed`, + `dock-pinned`, `dock-pinned-top`, `layout-auto-collapsed`, + `accordion-collapsed`): PanelChrome, the rails and the accordion present + it rather than writing classes. The directive applies it in a synchronous + watcher, not on Vue's next render, because the rails measure a panel in + the same task that opens or closes it. PanelChrome and its preference + restore, and the phone shell, read a panel's state back from the store + (`_shownPanel`; the markup's classes before its first presentation), so + once a panel is presented a class written outside the store no longer + counts. The rails, the accordion and the hover disclosure + (`panelDisclosure.js`), which act within the same task as a layout, read + the classes the directive has just drawn. Done. +3. **Panels.** Each template and its wiring become a component, keeping ids, + classes and CSS; text a template renders with `t()` drops its `data-i18n` + marker. The page places each view with a + `.gev-view` host (`display: contents`), and `mountApplicationViews()` + (`src/ui/views/index.js`) renders the view into it before the page is + translated and wired. `createApplication()` calls it too, removing the + hosts of any views its `omitViews` names. Views belong to the page and are + unmounted only when a later call omits them. The loading screen is static markup in the page so it + paints before any script runs. Markup + tests read `renderApplicationHtml()` from `src/testSupport`, which renders + the views on the server. Templates compile with Vue's `preserve` + whitespace, which keeps the whitespace between elements as a single + space, so a view renders as its template did; no view markup sits under a + `white-space: pre` rule. Application views (`src/ui/views/*View.vue`) are + kept out of Prettier; other components are formatted with strict + whitespace sensitivity, so their source spacing is what renders. The + markup has moved, unchanged: + every template except the loading screen is a view. + + Next each view takes over its wiring: markup an owner builds with + `createElement` becomes component markup that renders from a Pinia store + the owner publishes to, and user actions come back to the owner through + the store, so the owner keeps every decision. Component text uses + `t()`, which follows the locale as the view renders; the views carry no + `data-i18n*` markers, which `npm run i18n` rejects in a template. Shared builders become components first and + keep their imperative entry points until their owners are components: + the rail cards, card blocks and timeline (`src/ui/rail/`) render through + Vue's synchronous `render()` behind `createRailCards()`, + `createRailCardBlocks()` and `createRailTimeline()`. Order: Scenes (done: `SceneOptions` and + `SceneShotList` over the `gev-scenes` store; the panel's buttons, status + line, progress bar and the runtime chip render the panel SceneControls + presents there, and the buttons and file chooser call its actions), Weather (done: the owner + renders `WeatherReadout` from the view it computes), Recent imagery + (done: `RecentImageryReadout` and its day cards, the same way), + Street level (done: `StreetLevelPanel` renders the `gev-street-level` + store that `StreetLevelControls` publishes to; the controls keep their + listeners, EXPAND, focus and resizing), CCTV (done: `CctvPanel` renders + the whole panel from the `gev-cctv` store, including the camera picker, + the inline pose editor, the frame preview, source badge and typed + summary; `CctvControls` keeps its listeners, the frame requests and the + live video surface), Layers (done: `LayerPanel` presents each row and + renders `LayerRows` synchronously; keyed chips and list items keep focus + through the refresh their own click triggers, and clicks re-read the live + descriptor before acting), Display (in progress: `DisplayPanel` renders + the HUD, Sonar, Scope, Celestial, detection, bloom, sharpen and the + active style's parameter sliders from the `gev-display` store, which + VisualSettings publishes to and answers the sliders through, and the 3D + aircraft group, the Draw controls and the imported file list, which + AircraftDisplay, the draw tool and the file import publish beside it; + the controls call DisplayBindings, the draw tool and the file import + through the store's `connect`/`act`, the newest owner of an action + answering it; `GeoImport` hands keyboard focus on before a focused row + goes, with `v-focus-safe-children`; the + style parameter rows render through Vue behind `createStyleParameters()`, + as a render function so the portable `./ui/input` entry needs no + component compiler; the other groups follow one owner at a time), + Context and Radio (in progress: `RadioPanel` renders the directory row, + station card, transport, volume and playback line from the `gev-radio` + store that RadioControls publishes to and whose controls it answers + through the store's `connect`/`act` (the shared `createOwners()`; a + refused station tag gets the kept tag back from `act`), and `SdrCard` the + local RTL-SDR card that LocalSdrControls publishes beside it and answers + the same way; fields a person edits take their value through + `v-field-value`, which never writes a focused field; + `RadioTuner` renders the directory tuner, where RadioControls keeps the + drag and its slot, takes the slider's pointer, key and value events + through the store and publishes the band, readouts, slider and needle, + and the component lays out the tick tape against the dial's measured + width and lays out the snapped needle before it drops the drag class; + `ContextView` renders the mode switch and the nearby sites + search from the `gev-context` store, which ContextControls reaches only + through its `publish` option, so `./ui/context` needs no Vue; + `AwarenessPanel` renders the Contacts panel that the awareness layer + publishes through `subscribePanel()`, the first layer to hand its rail + content to a view, and `MissionRoster` and `MissionDetail` the Space + Missions roster and selected mission that the launches layer publishes + the same way), the command dock (in progress: the style buttons, the + dock's style mini status and the ACTIVE STYLE corner render the style + VisualSettings publishes to the `gev-display` store; the buttons keep + their owner's listeners; the map source chips and status badge render + what `createMapSourceControls()` publishes beside it, so `./ui/maps` + needs no Vue; the location tray's city and landmark pills, mini status + and search error render what `LocationControls` publishes to the + `gev-location` store, and the search field keeps its listeners; beside + the voice control, the dock teleports each voice pointer reticle to the + body where its `PointerReticle` presents it in the `gev-pointer-reticle` + store), the phone shell (`PhoneTabBar` renders the tabs and open sheet the shell + publishes to the `gev-display` store, teleporting the tab bar and sheet + backdrop to the end of the page from the first publish until the shell + is destroyed; the shell takes their clicks from the document), the + imagery dividers (`ImagerySplitView`, in the scene chrome, teleports each + before/after divider that `createImagerySplit` publishes to the + `gev-imagery-split` store to the body; the split keeps the + drag, keyboard, CSS position and scene split), the status chips + (the top-center loading status and the road network and camera grid + chips render what ShellFeedback and CctvControls publish to the + `gev-status` store; `SplitFlapText` plays the departure-board flip that + `splitFlap.js` plans, writing only the label's one text node; the toast + renders what each owner's `createToast()` publishes there, on that + owner's own timer, and the title bar's FPS readout what + `createFrameRateMonitor()` publishes), the globe actions + (`GlobeActions` renders the top-center bar; its clear, share, tilt, + north-up and reset buttons, and the cockpit's reset, call the shell + through the `gev-globe-actions` store, which also carries a clear or a + reset under way and the camera's tilt and heading + that `bindCameraOrientationControls` publishes each drawn frame it + changes, which only `GlobeCompass` renders; an owner publishes only + while it answers the button, so a shell replaced by a newer one leaves + its state alone), the recording safe frame (the scene chrome renders + the frame `RecordingControls` publishes to the `gev-recording` store), + the Scene actions panel (`SceneInteractions` + teleports what the scene interactions owner publishes to the + `gev-scene-interactions` store into the viewer's container; the owner + keeps the pick handler and focuses the picked action), the data-pack + attribution cards (`DataPacks` teleports what the data-pack presentations + publish to the `gev-data-packs` store into the viewer's container and + stops a removed card's player), the wind inspection marker + (`WindInspectionMarker` teleports the marker into the wind layer's + container where `createWindInspectionMarker()`, now in + `src/layers/wind/inspectionMarker.js` so the portable `./tools` entry + needs no Vue, places it after each rendered frame; an unchanged frame + publishes nothing), the Space Missions replay vehicle + (`MissionReplayOverlay` teleports into the globe container what the + launches layer presents through `subscribeReplayOverlay()`, which the + application shell publishes to the `gev-mission-replay` store, so the + portable `./layers/launches` entry needs no Vue), the Bhote Koshi + reconstruction panel (`BhoteKoshiPanel`, last in the context rail, + renders what the event layer publishes to the `gev-bhote-koshi-panel` + store: each control's text, hidden, disabled, value and attributes as + the layer writes them, unchanged frames sending nothing; the rail takes + `bhote-event-active` while it shows), the full-globe celestial ring + (`CelestialRing` teleports into the globe container what `CelestialRing` + in `src/celestialRing.js` publishes to the `gev-celestial-ring` store and + hands its two canvases back for the owner to paint; the application shell + passes the store in, so the keyhole helpers other packages import need no + Vue), the screen-space map annotations (`ScreenAnnotations` teleports to + the body the board the screen annotation renderer publishes to the + `gev-screen-annotations` store after each rendered frame: each mark's SVG + parts as attribute records, the fading marks until they leave; the view + hands back the callout text and mark groups the renderer measures, and + the tracked-entity fade is measured once the frame is in the document; + the styles it injected now live in `src/ui/styles/annotations.css`), the + visible map target list (the scene chrome renders the buttons and status + the world overlay presents through `subscribeWorldOverlayActions()`, which + the application shell publishes to the `gev-world-overlay-actions` store + and answers with `activateWorldOverlayAction()`; the module needs no Vue + for the portable transit entry, and its card and detection canvases stay + painting surfaces it draws on, like the Cesium canvas), the Global + Context compass and contact bearings (`AwarenessDirection` teleports into + the globe container what the awareness layer presents through + `subscribeDirectionOverlay()`, which the application shell publishes to + the `gev-awareness-direction` store, so the portable `./layers/awareness` + entry needs no Vue), the Intel HUD (the `intel-hud` view renders + `#intel-hud`, its corners, edges and bottom bar from the `gev-hud` + store, which IntelHUD publishes to through its `publish` option: the + readouts, the typing summary, the REC blink, visibility, layout variant + and style colors; the vessel layer presents the selected vessel's + readout through `src/hudReadouts.js` instead of writing into the HUD), + the first-run mission launcher (the welcome view renders the card's + visibility, busy state, status line, copy and scroll fade from the + `gev-first-run` store that `initFirstRunExperience` publishes to, and its + choices, DISMISS and "Don't show this again" call the launcher's + controls; 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), then + Cockpit (in progress: `CockpitBriefing` renders the briefing card's + pages, cycle toggle, regional news and local conditions, and + `CockpitContext` the Contact card's scope, navigation, counts, nearest + contact and weather toggle, `CockpitView` the instruments, destination + arrow and vision selector, and `CockpitRoute` the flight plan card, from + the `gev-cockpit` store that the Cockpit controller publishes to, + starting from `cockpitBriefPages.js`, `cockpitContextState.js` and + `cockpitInstrumentState.js`, which both read; the odometer readouts + (`CockpitRollingValue`) keep their throttle and roll in + `cockpitRollingValue.js`. `CockpitSignalList` renders the live signal + rows; `v-focus-safe-children` turns Vue's move of the focused row into + moves of the rows around it, and hands focus to the shown briefing tab + before the focused row is removed, so a keyed list reorders several times + a second without dropping keyboard focus. The instruments publish on + every HUD update (about 10 Hz); in a dev build their Vue patch costs + about 0.5 ms, close to the DOM writes it replaced. The controller also + publishes which surfaces are shown: the entry button and TR-3B chip in + the Context panel, the HUD with its exit and reset buttons and briefing + card, and the Contact card. Focus moves to the exit button, and back to + the entry, only after Vue renders them. The cards' collapse is part of + that presence too: each card's `data-collapsed` and its toggle's words + and icon render from it. The controls keep the controller's listeners, + and the cards' layout stays with the controller, which reads their + visibility when it lays them out. Its layout pass publishes the utility + strip's measured fit (the HUD's anchor and height limits, whether only the + expanded control fits and which control is hidden from assistive + technology), which `CockpitView` and `CockpitUtilityControls` render; the + briefing and Contact cards' positions stay imperative, because the same + pass measures them straight after writing them. + `CockpitUtilityControls` renders the Display + and Radio launchers, which popover is open and the compact Radio + controls from the `gev-radio` store that RadioControls publishes to and + answers through. The Cockpit display portal publishes the Display + popover's slots to the `gev-display` store while Cockpit is on, and + `DisplayPanel` teleports its HUD, Detection, Parameters and 3D groups into + them and back; the portal keeps each surface's scroll position and puts + back focus the move dropped). + Layers that write into the right rail directly gain a slot instead. + +4. **Cleanup.** The build-time template expansion is retired: `index.html` + lists the view hosts and the loading screen itself. Every owner that + showed UI state through hand-written DOM writes now publishes to a store + that a view renders, apart from the imperative parts this document + records surface by surface; [Building UI](../CONTRIBUTING.md#building-ui) + now requires the pattern for new UI. The page-level markup tests read + `renderApplicationHtml()`, the real views rendered on the server, and new + tests mount components; three tests still read a `.vue` file's source + (`cyberTheme`, `panelStorageDocs`, `geoFileImport`). The views translate with `t()` + while rendering, and vue-i18n holds the locale they read; the attribute + translation pass is left for static page markup (the loading screen and a + host's own markup). An explicit API for hosts to register panels and rail items is + deferred until a host adds one: no host does today (the hosted + application's account interface is its own markup), and the first such + panel, the hosted timeline, will shape it. + +## Verification + +- Unit tests: stores and components run under `npm test`. Single-file + components are compiled by a test loader; component tests render with + `@vue/test-utils` and `happy-dom`. `npm test` registers the loader; to run + one file that reaches a component (including anything importing the + shell), use `node --import=./build/registerVue.js --test `. +- Never pass happy-dom nodes to `assert.equal` or `deepEqual`: a failure + prints both operands, and a node reaches the whole page and window, which + runs the process out of memory. Compare nodes with `assertSameNode()` + (`src/testSupport/domAssert.mjs`). +- Panels lifted out of a view keep its verbatim markup in + `src/ui/views/panels/`, which Prettier leaves alone like the views. +- Visual parity: before/after screenshots of every panel at desktop and phone + widths, in the default and Cyber themes, with the rail as the only expected + difference. +- Browser QA: the selector-based `scripts/qa-*.mjs` suites keep passing. +- `npm run format:check`, `npm run check:boundaries`, `npm run i18n` and + `npm run build`. diff --git a/docs/opensky-auth.md b/docs/opensky-auth.md index 67844f912..c5f0a387b 100644 --- a/docs/opensky-auth.md +++ b/docs/opensky-auth.md @@ -3,10 +3,11 @@ God's Eye View uses explicit auth modes for `/api/flights`: - `OPENSKY_AUTH_MODE=oauth` (default, recommended) -- `OPENSKY_AUTH_MODE=auto` (OAuth first, then Basic fallback) -- `OPENSKY_AUTH_MODE=basic` +- `OPENSKY_AUTH_MODE=auto` (OAuth when configured, otherwise anonymous) - `OPENSKY_AUTH_MODE=anon` +`OPENSKY_AUTH_MODE=basic` is retired (see below). + Reference: OpenSky REST API docs recommend OAuth2 Client Credentials flow. ## Quick Start (OAuth Client JSON) @@ -41,13 +42,18 @@ Launchers resolve OAuth creds in this order: 2. `OPENSKY_CREDENTIALS_FILE` 3. Keychain (`opensky-network` / `client_id`, `client_secret`) -## Basic Auth (Legacy) +The server itself reads 1 and 2, so `npm run dev`, `npm run preview` and +Pinokio also work with only `OPENSKY_CREDENTIALS_FILE` set in `.env`, on any +platform. It reads the file on each token refresh. -Basic auth is still supported for now, but OpenSky is moving away from it. +## Basic Auth (Retired) -```bash -OPENSKY_AUTH_MODE=basic ./scripts/dev-fresh.sh -``` +OpenSky stopped accepting username/password (HTTP Basic) sign-in on +2026-03-18; its API now takes only OAuth2 client credentials. With +`OPENSKY_AUTH_MODE=basic` the server no longer sends `OPENSKY_USERNAME` / +`OPENSKY_PASSWORD`: it requests anonymously (reason `basic_retired`) and +logs once what to set instead. Create an API client on your OpenSky +account page and use its `credentials.json` as above. ## Troubleshooting @@ -61,6 +67,6 @@ Useful reasons: - `oauth_invalid_or_missing`: OAuth client not found/usable - `oauth_invalid_credentials`: OAuth client rejected by OpenSky -- `basic_invalid_credentials`: username/password rejected -- `missing_basic_creds`: basic mode missing user/pass +- `missing_oauth_creds`: auto mode found no OAuth client +- `basic_retired`: basic mode requested; answered anonymously - `rate_limited`: OpenSky throttling diff --git a/docs/panel-surfaces.md b/docs/panel-surfaces.md index 2d18023e7..30f1be88c 100644 --- a/docs/panel-surfaces.md +++ b/docs/panel-surfaces.md @@ -12,7 +12,7 @@ A rail panel uses an outer owner with its existing stable `id`, matching `data-panel-id`, and `panel-collapsible` class. Its first non-glow child is the surface. Keep the header and any outward popup outside the scrolling body: -```html +```vue

Example

- +
@@ -92,15 +89,27 @@ Do not copy the rail example's `.panel-collapse-btn` class into the dock: existing dock rules intentionally hide that class. The generic surface skin does not replace the dock's disclosure policy. -The existing `PanelChrome` binds `.panel-collapse-btn[data-collapse-target]` -at initialization and routes state through `setPanelCollapsed`. For additions +A panel with Core's standard header (glow, title, badges, divider, actions +and collapse button) is a `RailPanel` (`src/ui/rail/`): the kit's `UiPanel` +bound to the `gev-panel-chrome` store, with its state classes drawn by +`v-panel-state` and its button's click asking `PanelChrome` to toggle it. +A panel with its own header uses `PanelCollapseButton`, which renders the +collapse button (`.panel-collapse-btn`) from the open state `PanelChrome` +presents to that store; until then, both show their `startCollapsed` state, +which should match the markup's `collapsed` class. +Either way, the click reaches `PanelChrome`'s `setPanelCollapsed`. `PanelChrome` collects the buttons' +targets at initialization and binds each panel's Escape. For additions present at startup, keep those hooks and the existing chrome lifecycle. A -dynamically mounted panel must bind and dispose its own disclosure using -`bindPanelDisclosure` / `collapsePanelOnEscape` from `panelDisclosure.js`, with -the application owning state changes. Do not rerun the whole application chrome +dynamically mounted panel must own its disclosure: its own state, button and +Escape, bound and disposed with `bindPanelDisclosure` / +`collapsePanelOnEscape` from `panelDisclosure.js` (which bind Escape only). Do not rerun the whole application chrome initializer just to add one panel. `layoutLeftPanelRail` and `layoutRightPanelRail` accept arbitrary direct `data-panel-id` children; their -host owns layout scheduling, observations and collapse callbacks. Dynamic +host owns layout scheduling, observations and collapse callbacks. A rail +never writes a panel's classes: it calls `onCollapse(panel, {collapsed, +auto})`, and the host draws that state (the application presents it to the +panel chrome store, which each root's `v-panel-state` draws at once, so the +rail can measure the panel in the same task). Dynamic content that changes size may require an owner-scheduled layout pass or an owned `ResizeObserver`; adding CSS attributes does not register new observers. diff --git a/docs/translations/ar.md b/docs/translations/ar.md new file mode 100644 index 000000000..22d5e5e35 --- /dev/null +++ b/docs/translations/ar.md @@ -0,0 +1,117 @@ +# Arabic (`ar`) + +Catalog: `src/i18n/locales/ar.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md). + +Translation sources: abiyeenzo (#915). + +## Style + +- Modern Standard Arabic, neutral across regions. Address the user with plain imperatives: «انقر»، «فعّل»، «أعد المحاولة». +- Arabic has no letter case: an UPPERCASE English HUD label becomes the same short Arabic label. Keep labels as short as the English: «مباشر»، «متقادم»، «متوقف». +- Status words in progress use «جارٍ …» («جارٍ التحميل»، «جارٍ الضبط»، «… جارٍ تشغيل اللقطة»), so a status chip never reads as the command next to it («ضبط» is the TUNE button); finished actions use the passive («تم الحفظ»، «أُلغي التحميل»). Data that refreshes itself «يُحدَّث» (يتحدّث means "speaks"). A sentence about a `{placeholder}` whose gender and number vary (tool, layer and cohort names, call signs, vessel names) stays impersonal: «{layers}: تم التفعيل للتو»، ««{name}» قيد الاستخدام»، «{id}: لا بيانات واردة الآن»، «لم يعد {name} ضمن البيانات المحمّلة». +- A label followed by its value takes a colon, so it does not read as a sentence: «الصوت: نشط»، «السياق: {value}»، «الاحتواء: {percent}%». A named mode or control quoted in a sentence takes «»: «يظهر في وضع «كثيف» فقط». +- Arabic punctuation in Arabic text: ، ؛ ؟ and «» for quotes. Keep `·` as the HUD separator, `—` with spaces, and `…`. +- Arrows that show a sequence point the way Arabic reads: «الإشارات ← الأخبار ← المحلي», «{origin} ← {destination}». +- Product names, provider names and codes stay in Latin letters: Google, Cesium ion, CelesTrak, NOAA, FIRMS, AIS, ADS-B, GNSS, CCTV, HUD, CRT, NVG, FLIR, WHOT/BHOT, Ironbow, Street View, Mapillary. Aviation and camera abbreviations stay Latin too: HDG, FOV, BRG, CRS, GS, TRK, V/S, SQUAWK. Local place names from the Bhote Koshi scene (Rasuwagadhi, Bidur, Timure…) stay Latin, and a site known by two names takes one Arabic noun in front: «جسر Mailung / Upper Trishuli»; well-known cities and countries are written in Arabic (أوستن، طوكيو، نيبال). +- Months are the international names used across the Arab world: يناير، فبراير … أغسطس. + +## Digits and direction + +- **Western digits (0–9)**, as in the catalog. The HUD reads like an instrument panel, and mixing ٠–٩ with Latin codes and coordinates misreads. `intlLocale('ar')` in `src/i18n/locale.js` passes `ar-u-nu-latn` to every Intl formatter, so browsers that default to Arabic-Indic digits for an Arabic region still show 0–9. +- **Right to left.** `ar` is in `RTL_LOCALES`, so the page gets ``. The title, style indicator, both panel stacks and their icon rails swap sides; the dock and the corner row reverse. The intel HUD (classification block, MGRS, coordinates, GSD) stays a left-to-right instrument overlay; its translated lines read right to left inside it. FM seek and the radio dial stay left to right, as on any receiver. The rails' screen-reader names follow the mirrored layout: «Left panels» is «اللوحات اليمنى». Layout rules: `src/ui/styles/rtl.css`. +- Latin runs inside Arabic sentences (product names, codes, `{placeholders}` filled with English data) are isolated by the bidi algorithm; write them as plain text, without manual direction marks. +- Position counters: a translated counter says «من» («القناة {index} من {total}», «الإطار {index} من {total}»); a spaced slash between two isolated numbers swaps them in right-to-left text. Counters the code builds from bare numbers (`01 / 25`, `1 / 3`) are shown left to right by `rtl.css`. +- Code stays left to right: the JSON fields of the scene editor are LTR in every language. +- Comparison signs `<` and `>` are mirrored glyphs in right-to-left text («< 2%» shows as «> 2%»). Write them as words: «أقل من 2%»، «أكثر من 10%»، «أقل من ساعة». A legend's open upper bound («300+», «-40») is isolated by `formatScaleLabel()`, so its sign stays where it is written; `formatScaleRange()` isolates the unit after the range too («°C», not «C°»). + +## Units + +Distances and speeds take Arabic abbreviations after the number, with a space: كم، م، كم/س، كم/ث، م/ث، ميل/س، عقدة (knots)، قدم (feet)، مم. Time: ث (seconds)، د (minutes)، س (hours)، يوم; compact age labels («3d», «5D AGO») use ي: «{count} ي»، «قبل {count} ي», since a plain string cannot make يوم agree with the count. Area: آكر (acres, the value is not converted; فدان is a different unit). Money: the word دولار after the amount, not a bare `$`: «{amount} مليون دولار». Scientific and radio units stay Latin: MHz، dB، dBFS، dBZ، hPa، MW، MiB. `°` and `%` stay. Hemisphere letters in coordinates: ش (N)، ج (S)، ق (E)، غ (W), as in `21.33ش`. + +## Plurals + +`Intl.PluralRules('ar')` uses all six CLDR categories, and a `tn()` entry needs every one: + +| Form | Counts | Example | +| ------- | --------------------------- | ------------- | +| `zero` | 0 | 0 مصدر | +| `one` | 1 | مصدر واحد (1) | +| `two` | 2 | مصدران (2) | +| `few` | 3–10, 103–110… | 3 مصادر | +| `many` | 11–99, 111–199… | 11 مصدرًا | +| `other` | 100–102, 200–202, fractions | 100 مصدر | + +Every form must keep `{count}`. For one and two, Arabic names the quantity with the noun itself (مصدر واحد، مصدران), so the number follows in parentheses. `few` takes the plural noun; `many` the accusative singular (مصدرًا، مفتاحًا); `zero` and `other` the genitive singular. When a word next to the number cannot agree in every form, or a short chip has no room, put the number after a colon instead: «القريبة: {count}»، «الملفات المحددة: {count}». Where neither fits, «من أصل {total} من السجلات» agrees with any number. A standalone unit label («الدقائق») is never glued after a number: the voice-timeout options («3 دقائق»، «15 دقيقة») come from `Intl.NumberFormat` unit formatting, not from the catalog. + +## Glossary + +| English | العربية | +| ------------------------------------------ | ---------------------------------------------------------- | +| layer / data layer | طبقة / طبقة بيانات | +| feed / source | مصدر بيانات (short: مصدر) / مصدر | +| LIVE (status) / live data | مباشر / البيانات المباشرة (not حي) | +| Live ships, live detections (descriptions) | بيانات مباشرة عن السفن، رصد مباشر (not the adverb مباشرةً) | +| track, tracking | تتبّع (always with the shadda) | +| SIMULATED | محاكى (status) / محاكاة | +| stale / degraded / unavailable | متقادم / متدهور / غير متاح | +| ON / OFF (state) | مفعّل / متوقف | +| retry | إعادة المحاولة | +| key (API key) / POWER UP | مفتاح (مفتاح API) / تشغيل | +| Provider Settings | إعدادات المزوّدين | +| globe / map / map source / basemap | الكرة الأرضية / الخريطة / مصدر الخريطة / الخريطة الأساسية | +| globe map (map source drawn on the globe) | خريطة على الكرة الأرضية (not خريطة كروية) | +| tiles | بلاطات | +| aircraft / flight / military flights | طائرة / رحلة / الرحلات العسكرية | +| Live Flights / Live Vessels (layers) | تتبّع الرحلات / السفن المباشر (not الرحلات المباشرة) | +| call sign | رمز النداء | +| vessel, ship | سفينة | +| satellite | قمر صناعي | +| launch (rocket) / space mission | إطلاق / مهمة فضائية | +| contact (tracked object) | هدف | +| installation, site / mapped | منشأة، موقع / مرسوم | +| camera / CCTV camera | كاميرا / كاميرا CCTV | +| earthquake / active fire / fire perimeter | زلزال / حريق نشط / محيط حريق | +| containment (wildfire) | احتواء (not السيطرة) | +| cyclone advisory | نشرة إعصار | +| scene / shot / scene recipe | مشهد / لقطة / قالب المشهد | +| briefing / panel / dock | إحاطة / لوحة / الشريط السفلي | +| preset / visual preset | إعداد مسبق / نمط بصري | +| Scope (circular viewport mask) / keyhole | المنظار / الفتحة | +| Clean UI / EXIT CLEAN VIEW | واجهة نظيفة / الخروج من الواجهة النظيفة (not العرض) | +| cockpit | القمرة (قمرة القيادة) | +| heading / altitude / ground speed | الاتجاه (HDG) / الارتفاع / السرعة الأرضية | +| Street Level / Street View | مستوى الشارع / Street View | +| Recent Imagery | الصور الحديثة | +| Global Context / Context | السياق العالمي / السياق | +| radio station / tuner / gain | محطة / موالف / الكسب | +| broadcaster (radio stream) | محطة البث (not المحطة المُذيعة) | +| VOLUME (slider) / VOICE | مستوى الصوت / الصوت | +| decoder (SDR) | مفكّك | +| transit / bike share | النقل العام / الدراجات المشتركة | +| Previous / Next | السابق / التالي | +| compass points (N, NNE, …) | شمال، شمال شمال شرق، … | +| forecast / valid (time) / issued | توقع / عند، وقت التوقع (not صالح حتى) / صدر | +| observation / observed | رصد / مرصود | +| heard (aircraft received by the SDR) | الملتقطة | +| upstream (camera feed) | المصدر الأصلي (in every CCTV message, not bare المصدر) | +| field (weather) / model | حقل / نموذج | +| parameters / ellipsoid | المعلمات / المجسّم الإهليلجي | +| scene cloud | غيوم المشهد | +| box (Recent Imagery area) / pin | منطقة / تثبيت | +| width (box size) / view (current camera) | اتساع / العرض الحالي (عرض alone reads as display) | +| N/A | غير متاح (no abbreviation) | +| METRO (view band) / REGIONAL | حضري (not حاضرة) / إقليمي | +| THIS (voice pointer) | هذا | +| outline / mark (map annotation) | مخطط / علامة، تعليم | +| share (a view or link) | مشاركة | +| AI agent | وكيل ذكاء اصطناعي | +| spacecraft | مركبة فضائية (مركبة alone is any vehicle) | +| stage (rocket) / downrange | مرحلة / على امتداد المسار | +| landing position: CONFIRMED / PAD (RTLS) | مؤكَّد / منصة الإطلاق | +| recovery (rocket stage) | استعادة (not استرجاع) | +| Arrive at your destination (route step) | الوصول إلى وجهتك (a step, not وصلت) | +| forecast lead hours | مدة التوقع بالساعات (not ساعات المهلة) | +| cel-shaded | بتظليل كرتوني | +| (debris-)dammed lake | بحيرة خلف سدّ (ركامي) | +| frame (bring into view) | إظهار في الإطار | +| story beat | مرحلة القصة | diff --git a/docs/translations/de.md b/docs/translations/de.md new file mode 100644 index 000000000..86032a238 --- /dev/null +++ b/docs/translations/de.md @@ -0,0 +1,108 @@ +# German (`de`) + +Catalog: `src/i18n/locales/de.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md). + +Translation source: abiyeenzo (#908). + +## Style + +- Standard German that reads naturally in Germany, Austria and Switzerland; ß where it belongs (Straße, schließen), no Swiss ss. +- No direct address: neither «Sie» nor «du». Actions and hints use the neutral infinitive: «Erneut versuchen», «Kamera anklicken zum Aktivieren», «Zuerst eine Aufnahme wählen», «Mikrofonzugriff erlauben und erneut versuchen». "Your" becomes the article or «eigene»: «Eigener RTL-SDR-Empfänger», «Die IP-Adresse ist für diesen Sender sichtbar». +- Nouns are capitalized, as German requires; otherwise sentence case, not Title Case: "Live Flights" → «Live-Flüge», "Provider Settings" → «Anbieter-Einstellungen». UPPERCASE English stays UPPERCASE: «LIVE-DATEN AUS», «KEINE ENTWARNUNG». In UPPERCASE, ß is written SS: «SCHLIESSEN», «STRASSENEBENE». +- Compounds are written as one word or with a hyphen next to a name, code or English term: «Datenebene», «Wetterkacheln», «TomTom-Schlüssel», «AIS-Schiffe», «Street-View-Fallback». Do not split them with spaces. +- Short labels, chips and HUD readouts stay short. German runs about a third longer than English, so prefer a short word or a standard abbreviation with a period over a wrapped line: REICHW., GESCHW., NIEDERSCHL., SONST., NACHR., VERST., VGL., KAL., KOMM. (a bare «KOMM» reads as the imperative) +- The avionics codes HDG, BRG, CRS, DEST, SPD, ALT, GS, TRK, V/S, FOV and SQUAWK stay in English, as in the cockpit. Words used in a sentence are translated: heading → «Kurs», bearing → «Peilung», range → «Reichweite». +- A dash between clauses is a spaced en dash « – »; a colon is fine where it explains: «Feed ausgefallen – neue Versuche in größeren Abständen», «SIMULIERT: Verkehrsdienst nicht erreichbar». Quotes are „…“. +- Status and waiting messages use the neutral infinitive too: «Warten auf Aktualisierung», not «Warte auf …». Reasons inserted after a dash stay lower case: «Szene nicht gespeichert – das Projekt ist größer als 5 MiB». +- A layer named in a sentence takes its panel name in quotes: «Die Ebene „Live-Flüge“ ist nicht aktiviert», «die Ebene „Live-Flüge“ lädt Daten für den jeweiligen Bildausschnitt». +- Rolling time windows say their length, so they cannot read as the previous calendar period: LAST YEAR → «LETZTE 12 MONATE», LAST MONTH → «LETZTE 30 TAGE». +- Keyboard keys are named as on German keyboards where they differ: Leertaste, Rücktaste, Umschalt; Esc, Enter, Tab and letter keys stay. +- Decimal comma in fixed text: «M4,4», «28,2710° N». Thousands separator is a period: «1.000 km». Formatted numbers come from `Intl` and are already German. +- Units: km, m, km/h, m/s, kn (knots, also in the cockpit), ft (feet), hPa, MW, h, min, s, T (days, in short readouts); in UPPERCASE text KM, KM/H, KN, FT, MIN, H, T («VOR {count} H»). A space between number and unit, in compact readouts too: «{value} m», «{count} min», «50 %», «{speed} KN», «{count} / 30 T». `°` sticks to the number. +- "ago" goes before the time: «vor {count} min», «vor {value}». When a placeholder already holds a localized "ago" text (`{age}` in `STALE · cached {age}`, `{ago}`), do not add «vor» again. +- Compass points use O for Ost: N, NNO, NO, ONO, O, OSO, SO, SSO, S, SSW, SW, WSW, W, WNW, NW, NNW; hemisphere letters N, S, O, W. +- Routing: direction words are lowercase adverbs (links, leicht rechts, scharf links, geradeaus), so a sentence never starts with one: «Auf {road} {direction} abbiegen», «Hier {direction} abbiegen», «Weiter {direction}». «Hier», not «Dann»: the step card already says «dann {leg}» and «Nächster Schritt: …». At a fork the verb is «weiter», which also works with «geradeaus»: «An der Gabelung {direction} weiter». Roundabout exits are ordinals with a period: «die {exit} Ausfahrt» → «die 2. Ausfahrt». A lane is named by where it leads, not by its position: «Die geradeaus führende Spur benutzen», «die leicht nach links führende», «die scharf nach rechts führende» (not «halblinke» or «äußerst linke»). + +## Plurals + +A `tn()` entry needs two forms. `Intl.PluralRules('de')` picks: + +| Form | Counts | Example | +| ------- | --------------------------- | --------- | +| `one` | 1 | 1 Kamera | +| `other` | everything else: 0, 2, 1,5… | 3 Kameras | + +Keep the verb in agreement: «{count} Station meldet» / «{count} Stationen melden». Abbreviated units and codes do not change: «{count} NACHR.», «{count} EMPFANGEN». + +## Glossary + +| English | Deutsch | +| ---------------------------------------------- | ---------------------------------------------------------------------- | +| layer / data layer | Ebene / Datenebene | +| feed / source / data source / input (cockpit) | Feed / Quelle / Datenquelle / Quelle | +| LIVE (status) / live data | LIVE / Live-Daten | +| SIMULATED | SIMULIERT | +| stale / degraded / unavailable | veraltet / eingeschränkt / nicht verfügbar | +| retry / retrying / retrying slowly | neuer Versuch / erneut versuchen / neue Versuche in größeren Abständen | +| fallback | Fallback | +| key (API key) / POWER UP / POWERED UP | Schlüssel / STARTKLAR / HOCHGEFAHREN | +| Provider Settings | Anbieter-Einstellungen | +| ON / OFF (state chips) | EIN / AUS | +| enable / disable | aktivieren / deaktivieren | +| globe / map / map source / map stack / basemap | Globus / Karte / Kartenquelle / Kartenstapel / Basiskarte | +| aircraft / flight / military flights | Luftfahrzeug (HUD and short labels: Flugzeug) / Flug / Militärflüge | +| vessel, ship | Schiff | +| satellite / shell (Starlink) / DENSE | Satellit / Schale / DICHT | +| launch (rocket) / launch site / space mission | Start / Startplatz / Raummission | +| ascent / replay / payload / stage / downrange | Aufstieg / Replay, Wiedergabe / Nutzlast / Stufe / Downrange | +| contact / tracking | Kontakt / Verfolgung | +| installation / site | Anlage / Standort | +| mapped / observed | kartiert / beobachtet (Bhote chip OBSERVED IMAGERY: REALE BILDDATEN) | +| camera / CCTV camera / camera badge | Kamera / CCTV-Kamera / Kamera-Markierung | +| earthquake / active fire / fire perimeter | Erdbeben / aktiver Brand / Brandgrenze | +| cyclone / advisory (NHC/CPHC) / cone | Wirbelsturm / Lagebericht (not «Warnung») / Kegel | +| issued (advisory, forecast) | ausgegeben | +| radio station | Sender | +| broadcast FM | UKW | +| road / flow (traffic) / flow coverage | Straße / Verkehrsfluss / Live-Abdeckung | +| transit / bike share | ÖPNV / Bikesharing | +| scene / shot / scene recipe | Szene / Aufnahme / Szenenrezept | +| capture shot / camera move | Aufnahme speichern / Kameraflug | +| anchor / data pack / bundle | Anker / Datenpaket / Bundle | +| story beat / evidence beat | Story-Schritt / Beweisschritt | +| briefing / panel / dock | Briefing / Bereich / Dock | +| cockpit | Cockpit | +| preset / visual preset | Preset / visuelles Preset | +| Scope (circular viewport mask) / keyhole | Scope / Schlüsselloch | +| feather / fade | Weichzeichnung / Ausblenden | +| overlay / detection overlay | Overlay / Erkennungs-Overlay | +| Clean UI | Reine Ansicht | +| Night vision / Thermal / Snow (cockpit styles) | Nachtsicht / Wärmebild / Schnee | +| heading / altitude / ground speed | Kurs / Höhe / Geschwindigkeit über Grund (short: GESCHW. ÜBER GRUND) | +| HDG / BRG / CRS / DEST / SPD / FOV | unchanged | +| RANGE / PITCH / HGT (CCTV calibration chips) | REICHW. / NEIGUNG / HÖHE | +| PREV / NEXT | ZURÜCK / WEITER | +| RESUME (radio transport button) | FORTSETZEN (not «WEITER», which is NEXT in the same row) | +| nearest (spatial) | nächstgelegen (always; «nächste» is NEXT) | +| expand / collapse (sections) | ausklappen / einklappen | +| L / R (relative bearing) | L / R | +| SIG / NEWS / LOCAL (briefing tabs) | SIG / NEWS / LOKAL | +| NOT AN ALL-CLEAR | KEINE ENTWARNUNG | +| Street Level / Street View (Google) | Straßenebene / Street View | +| Recent Imagery / box / pin (a day) | Aktuelle Bilddaten / Bereich / fixieren | +| frame (bring into view) | ins Bild nehmen | +| mark (map annotation) / outline | Markierung / Umriss | +| voice (control) / voice session | Sprachsteuerung / Sprachsitzung | +| YOU / THIS / HERE (voice card labels) | EINGABE / DIES / HIER | +| click / click to … | anklicken / … klicken | +| tap (touch) | Antippen (not «Tipp», which is a hint) | +| press / tap a key | Taste drücken / kurz drücken (never «tippen» for keys) | +| tilted view | Schrägansicht | +| card (host card) / voice card | Eintrag (in hints), Infokarte / Sprachkarte | +| label (on the map) / clear (switch off layers) | Beschriftung / ausschalten (not «löschen») | +| station tag (radio filter) | Kategorie | +| DIRECTORY BAND / {category} BAND (radio dial) | SENDERSKALA / SENDERSKALA · {category} (not «-BAND»: «ROCK-BAND») | +| MIC (dock button) | MIKRO | +| frame (weather, video) / image | Frame / Bild | +| collapse / landslide (Bhote Koshi) | Erdrutsch (not «Abbruch», which reads as "cancel") | +| website (radio station) | Website (SENDER-WEBSITE; «Standort» is a physical site) | diff --git a/docs/translations/fr.md b/docs/translations/fr.md new file mode 100644 index 000000000..e2f506988 --- /dev/null +++ b/docs/translations/fr.md @@ -0,0 +1,105 @@ +# French (`fr`) + +Catalog: `src/i18n/locales/fr.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md). + +Translation source: abiyeenzo (#907); the French part of meirm's #312 was used for comparison. + +## Style + +- Standard French that reads naturally in France, Belgium, Switzerland and Canada; avoid regionalisms. +- Address the user with "vous" imperatives: « Cliquez », « Activez », « Réessayez ». Tooltips that name an action use the infinitive: « Réduire le panneau », « Réinitialiser la caméra ». UPPERCASE HUD instructions also use the infinitive: « CLIQUER SUR UNE CAMÉRA POUR L'ACTIVER ». +- Sentence case, not Title Case: "Live Flights" → « Vols en direct ». UPPERCASE English stays UPPERCASE, accents included: « ÉTAT », « PRÉC. ». +- Typography: a narrow no-break space (U+202F) goes before `:` `;` `?` `!` and `%`, and inside « » quotes: `Supprimer le plan « {shot} » ?`. It keeps the sign on the same line as the word before it. Do not type a plain space there. The same narrow no-break space separates thousands in a number written into the text: « 1 000 km ». Ellipsis is `…`, not `...`. Dashes keep their spaces: « flux indisponible — réessayez ». +- Short labels, chips and HUD readouts stay short. A standard abbreviation with a period beats a wrapped line: PRÉC., SUIV., RÉINIT., ENREG., INDISP., DIST. +- The HUD codes HDG, BRG, CRS, FOV, DEST, SPD and ALT stay in English everywhere, as in cockpit avionics. Words are translated: RANGE → « PORTÉE », heading in a sentence → « cap », ground-track course → « route » (« ROUTE 270° »). +- ON and OFF stay as they are on HUD toggles and chips (« CCTV ON », « PROJECTION OFF »); in a sentence use « activé » / « désactivé ». +- Keyboard keys stay in English: Space, Esc, Enter, Tab, Shift, Backspace, H, 1–9 (« Maintenez Space pour parler », « ESC pour fermer »). +- Units: km, m, km/h, m/s, nd (knots), ft (feet), h, min, s, with a space before the unit: « 250 km », « il y a 5 min »; in UPPERCASE text KM, KM/S, ND, FT, MIN. MW, dBZ, hPa and `°` stay. Decimal comma in fixed numbers: « 28,2710° N », « M4,4 ». Money: « 12 k$ », « 3 M$ », « 2 Md$ ». +- "ago" goes before the time: « il y a {count} min ». When a placeholder already holds a localized "ago" text (`{age}`, `{ago}`), do not add « il y a » again. +- Round-about exit ordinals: 1re, 2e, 3e… (« prenez la 1re sortie »). + +## Plurals + +A `tn()` entry needs three forms. `Intl.PluralRules('fr')` picks: + +| Form | Counts | Example | +| ------- | -------------------------------------- | -------------------- | +| `one` | 0 and 1, fractions below 2 (1,5) | 1 caméra, 0 caméra | +| `many` | 1 000 000, 2 000 000… (exact millions) | 1 000 000 de caméras | +| `other` | everything else: 2–999 999, 2,5… | 3 caméras | + +The `many` form is the `other` text with « de » (« d' » before a vowel) after the number: « {count} de couches effacées », « {count} D'ENTRÉES INCONNUES ». When the words after the number cannot agree (an UPPERCASE readout, a list), put the number after a colon in every form: « CAPTÉS : {count} ». The same goes for a layer name whose gender and number vary: « Couche activée : {label} », not « {label} activé », or a tool name: « Outil actif : {name} », not « {name} est actif ». + +## Glossary + +| English | Français | +| ------------------------------------------------------------ | ----------------------------------------------------------------------- | +| layer / data layer / DATA LAYERS (panel title) | couche / couche de données / COUCHES | +| feed / source / data source | flux / source / source de données | +| LIVE (status) / live data | DIRECT / données en direct | +| SIMULATED | SIMULÉ | +| stale / degraded / unavailable | obsolète / dégradé / indisponible | +| retry / retrying (status: RETRYING X) | nouvel essai / réessayez (NOUVEL ESSAI : X) | +| rate-limited | limite de requêtes atteinte | +| key (API key) / POWER UP / POWERED UP | clé (clé API) / ALLUMER / ALLUMÉ | +| Provider Settings | paramètres des fournisseurs | +| enable / disable | activer / désactiver | +| globe / map / map source / basemap / map stack | globe / carte / source de carte / fond de carte / fond de carte | +| tile (map) | tuile | +| aircraft / flight / military flights | aéronef (avion in short copy) / vol / vols militaires | +| vessel, ship / tanker (AIS type) | navire / navire-citerne | +| satellite / shell (Starlink) / DENSE | satellite / couche de la constellation / DENSE | +| launch / launch site / space mission | lancement / site de lancement / mission spatiale | +| ascent / replay / payload / stage / downrange | ascension / rejeu / charge utile / étage / distance au sol (DIST. SOL) | +| contact / tracking / track (HUD) | contact / suivi / piste | +| brackets (HUD corner brackets around contacts) | cadres de ciblage | +| installation / site | installation / site | +| mapped / observed | cartographié / observé | +| camera / CCTV camera / ALPR camera | caméra / caméra CCTV / caméra LAPI | +| earthquake | séisme | +| active fire (FIRMS detection) / wildfire | feu actif / incendie | +| contained / containment (wildfire) | circonscrit / part circonscrite (not « maîtrisé », which is controlled) | +| advisory (NHC/CPHC cyclone) | bulletin | +| forecast / lead hours / valid / issued | prévision / échéances / valide / émise | +| AQHI (Canada) | CQAS (cote air santé) | +| radio station / tune | station / régler | +| bikeshare / transit | vélos partagés / transports en commun | +| route (directions) / turn (maneuver) | itinéraire / manœuvre | +| scene / shot / scene recipe | scène / plan / recette de scène | +| bundle / data pack / anchor | archive / paquet de données / ancre | +| import / export (noun: file, scene, project) | importation / exportation (not « import » / « export ») | +| capture camera as anchor / set move start (scene editor) | ancre à la position de la caméra / vue actuelle comme départ | +| story beat | étape du récit | +| briefing / panel / dock | briefing / panneau / dock | +| cockpit | cockpit | +| preset / visual preset | préréglage / préréglage visuel | +| Scope (circular viewport mask) / keyhole | lunette / ouverture | +| detection overlay | surcouche de détection | +| Clean UI | vue épurée | +| Street Level (layer) / Street View (Google) | Niveau rue / Street View | +| Recent Imagery / box | imagerie récente / zone | +| Daily overview / overview (Recent Imagery VIIRS product) | vue quotidienne (one name for the product, not « vue d'ensemble ») | +| pin (Draw tool) / pin (Recent Imagery) | épingle / épingler | +| Cyber (HUD layout, effect) | Cyber | +| heading / course (ground track) / altitude / ground speed | cap / route / altitude / vitesse sol | +| HDG / BRG / CRS / FOV / DEST / SPD / ALT | unchanged | +| RANGE / PITCH / HGT (CCTV calibration chips) | PORTÉE / INCL. / HAUT. | +| PREV / PREVIOUS / NEXT (chips) | PRÉC. / PRÉC. / SUIV. | +| Previous / Next (button text) | Précédent / Suivant | +| L / R (relative bearing) | G / D | +| SIG / NEWS / LOCAL (briefing tabs) | SIG / ACTUS / LOCAL | +| compass points (N, NNE, …, W, NW) | N, NNE, …, O, NO | +| NOT AN ALL-CLEAR | PAS UN FEU VERT | +| AI agent | agent IA | +| share (a view or link) | partager | +| click / click to … | cliquez / cliquer pour … | +| map heading (compass, north-up control) | orientation (de la carte) | +| subject (cockpit / shared contact) | cible | +| cloud cover / CLOUD (briefing readout) | nébulosité / NÉBULOSITÉ | +| LEVEL (cockpit horizon guide) | HORIZON | +| CALIBRATED / CURATED / RAW PRIOR (CCTV pose) | CALIBRÉE / VÉRIFIÉE / ESTIM. INITIALE | +| DATASHEET / SPEC FOV (CCTV lens range) | FICHE TECHNIQUE / FOV CONSTRUCTEUR | +| PORT TENDER (AIS type) | SERVITUDE PORT. (navire de servitude portuaire) | +| ChatGPT sign-in (action) / opening sign-in / sign-in expired | connexion / page de connexion / la session a expiré | +| authored shot (scene) | plan scénarisé | +| report / record (USGS event card) / readout, reading | rapport / relevé (a measured value only) | diff --git a/docs/translations/it.md b/docs/translations/it.md new file mode 100644 index 000000000..722916101 --- /dev/null +++ b/docs/translations/it.md @@ -0,0 +1,108 @@ +# Italian (`it`) + +Catalog: `src/i18n/locales/it.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md). + +Translation source: abiyeenzo (#912). + +## Style + +- Standard Italian as in Italian software UIs. Address the user with informal "tu" imperatives: «Aggiungi», «Attiva», «Scegli», «Incolla», «Riprova». Tooltips that name an action use the same imperative: «Espandi il pannello», «Ripristina la camera». +- Sentence case, not Title Case: "Live Flights" → «Voli live», "Space Missions" → «Missioni spaziali». UPPERCASE English stays UPPERCASE: «LIVE», «NON DISPONIBILE». +- Quotes inside Italian text are straight double quotes: «Eliminare lo scatto "{shot}"?». Use the accented capitals (È, À) in UPPERCASE text: «VELOCITÀ», «CITTÀ». +- Errors read as a statement, not a past participle on its own: "Search failed" → «Ricerca non riuscita»; "Could not …" → «Impossibile …». A dash that joins a cause and a hint becomes a colon: «Chiave API rifiutata: controlla AISSTREAM_API_KEY». The same goes for a label and its hint in a tooltip: «Direzione (bussola °): clic per digitare». +- Short labels, chips and HUD readouts stay short. A standard abbreviation with a period beats a wrapped line: REGOLAZ., RILEV., INCL., ALT., VEL., STIM., PROIEZ., ESP.; PREC / SUCC for PREV / NEXT and PREVIOUS / NEXT. +- The HUD codes HDG, BRG, CRS, FOV and DEST stay in English, as in cockpit avionics. RANGE is «PORTATA». Words used in a sentence are translated: heading → «prua» (aircraft, ships) or «direzione» (camera, buses, trams, trains), course → «rotta». +- Keyboard keys stay in English: Space, Enter, Esc, Tab, Shift, Backspace, H, 1–9 («Tieni premuto Space per parlare», «Premi Enter per riprovare»). +- Decimal comma in numbers written in the text, magnitudes included: «28,2710° N», «M5,2». Numbers formatted by the code follow `Intl` and need nothing. +- Amounts: thousands, millions and billions are written out or abbreviated in Italian, not with K/M/B: «{amount} mila $», «{amount} mln $», «{amount} mld $». +- Units: km, m, km/h, m/s, kn (knots), ft (feet), h, min, s; in UPPERCASE text KM, KM/S, KT, FT, MIN, H. "m" is always metres, so minutes are «min» («5 min fa»). MW stays MW. `°` and `%` stay. +- "ago" goes after the time: «{count} min fa». When a placeholder already holds a localized "ago" text (`{age}`, `{ago}`), do not add «fa» again. +- A feature name used as a sentence subject gets a head noun so the verb agrees: «La modalità Missioni spaziali isola…», «Il pannello Impostazioni provider è disattivato…», «Il livello {layer} è attivo…», «Lo strumento {name} è attivo…». A short card title with a placeholder name puts the state after a colon: «{label}: attivato». The same goes for a placeholder list of names: «Attivazione recente: {layers}». A count in a plain `t()` entry (no plural forms) that can be 1 goes after a label, so nothing has to agree with it: «Record accettati: {accepted} su {raw}». +- English loanwords that Italian UIs use as they are stay in English: feed, live, cockpit, briefing, preset, overlay, layout, snapshot, tile, provider, timeout, payload, bloom, scope. + +## Plurals + +A `tn()` entry needs three forms. `Intl.PluralRules('it')` picks: + +| Form | Counts | Example | +| ------- | -------------------------------------- | ----------------------- | +| `one` | 1 | 1 telecamera | +| `many` | 1 000 000, 2 000 000… (exact millions) | 1 000 000 di telecamere | +| `other` | everything else: 0, 2–999 999, 1,5… | 3 telecamere | + +The `many` form is the `other` text with «di» after the number when a noun follows: «{count} di telecamere caricate», «{count} DI CHIAVI IN ATTESA». When no noun follows the number («{count} vicine», «{count} RICEVUTI»), `many` repeats `other`. Nouns that do not change in the plural keep one form for all three (`{count} file`, `{count} data center`), with «di» in `many`. + +## Glossary + +| English | Italiano | +| --------------------------------------------- | ------------------------------------------------------------ | +| layer / data layer | livello / livello dati | +| feed / source / data source | feed / fonte / fonte dati | +| LIVE (status) / live data / live (feed state) | LIVE / dati live / live | +| SIMULATED | SIMULATO | +| stale / degraded / unavailable | obsoleto / degradato / non disponibile | +| retry / retrying | riprova / nuovo tentativo | +| rate limited | limite di richieste (raggiunto) | +| key (API key) / POWER UP / POWERED UP | chiave / ACCENDI / ACCESO | +| Provider Settings | Impostazioni provider | +| ON / OFF (state chips) | ON / OFF | +| enable / disable | attivare / disattivare | +| globe / map / map source / map stack | globo / mappa / fonte mappa / stack mappa | +| basemap / box (Recent Imagery area) | mappa di base / riquadro | +| aircraft, plane / flight / military flights | aereo / volo / voli militari | +| vessel, ship | nave | +| satellite / shell (Starlink) / DENSE | satellite / shell / DENSO | +| SPARSE / BALANCED / DENSE (detection modes) | RADO / BILANCIATO / DENSO (agree with «overlay») | +| broadband (Starlink shell) | a banda larga | +| VISUAL (CelesTrak group) | VISIBILI | +| launch (rocket) / launch site / space mission | lancio / sito di lancio / missione spaziale | +| ascent / replay / payload / stage | salita / replay / payload / stadio | +| contact / tracking | contatto / tracciamento | +| installation / site | installazione / sito | +| mapped / observed | mappato / osservato | +| camera (view) / camera, CCTV camera | camera / telecamera, telecamera CCTV | +| earthquake / active fire / fire perimeter | terremoto / incendio attivo / perimetro dell'incendio | +| lightning strike / cloud (cover, %) | fulmine / nuvole («non singoli fulmini», «20% di nuvole») | +| incident (wildfire, Type N) / scene incident | evento (Evento di tipo N, Incendio senza nome) / evento | +| trigger record (USGS card) / OPEN SOURCE link | scheda / APRI LA FONTE | +| advisory (NHC/CPHC cyclone) / cone | bollettino / cono di incertezza | +| forecast lead hours (cyclone points) | scadenze di previsione in ore | +| AIR / SEA (radio category) / ground station | ARIA / MARE / stazione di terra | +| radio station / broadcaster | stazione radio / emittente | +| CH (radio channel readout) | CH (unchanged) | +| road / flow (traffic) | strada / flusso | +| transit / bike share | trasporto pubblico / bike sharing | +| last report (vehicle position) | ultima segnalazione | +| scene / shot / scene recipe | scena / scatto / ricetta della scena | +| anchor / data pack / bundle (scene) | ancora / pacchetto dati / pacchetto | +| story beat | tappa del racconto | +| briefing / panel / dock | briefing / pannello / dock | +| cockpit | cockpit | +| preset / visual preset | preset / preset visivo | +| Tactical / Minimal / Cyber | Tattico / Minimale / Cyber | +| Scope (circular viewport mask) / keyhole | scope / foro | +| feather / fade | sfumatura / dissolvenza | +| detection overlay | overlay di rilevamento | +| pin (Draw tool) / mark | segnaposto / segno | +| pin (Recent Imagery) | fissare | +| Draw / Directions (tools) | Disegno / Indicazioni | +| Street Level (layer) / Street View (Google) | Vista stradale / Street View | +| Recent Imagery | Immagini recenti | +| Global Context / Context (panel) | Contesto globale / Contesto | +| heading, course / altitude / ground speed | prua, rotta / altitudine / velocità al suolo (VEL. AL SUOLO) | +| altitude in short HUD captions / Alt unknown | QUOTA (QUOTA · FT) / Quota sconosciuta | +| fallback (FALLBACK, Street View fallback) | riserva / di riserva (SUPERFICIE DI RISERVA) | +| forecast track / advisory center (cyclones) | traccia prevista / centro del ciclone da bollettino | +| witness source / authored (scene) | fonte testimoniale / predefinito, definito dall'autore | +| explore manually | esplora manualmente | +| HDG / BRG / CRS / FOV / DEST | HDG / BRG / CRS / FOV / DEST (unchanged) | +| RANGE / PITCH / HGT (CCTV calibration) | PORTATA / INCL. / ALT. | +| PREV / NEXT | PREC / SUCC | +| Previous / Next (button text) | Precedente / Successivo | +| Focus {target} (action, aria label) | Inquadra {target} (the FOCUS button stays FOCUS) | +| L / R (relative bearing) | S / D | +| SIG / NEWS / LOCAL (briefing tabs) | SIG / NOTIZIE / LOCALE | +| compass points (N, NNE, …, W, NW) | N, NNE, …, O, NO (west is «O», ovest) | +| NOT AN ALL-CLEAR | NON È UN VIA LIBERA | +| weather field / model / valid / issued | campo / modello / valida / emessa | +| click / click to … | fai clic / clic per … | diff --git a/docs/translations/ja.md b/docs/translations/ja.md new file mode 100644 index 000000000..9e612e8e4 --- /dev/null +++ b/docs/translations/ja.md @@ -0,0 +1,112 @@ +# Japanese (`ja`) + +Catalog: `src/i18n/locales/ja.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md). + +Translation source: abiyeenzo (#913). + +## Style + +- Sentences use です/ます: 「キーを保存しました」, 「もう一度お試しください」. Labels, buttons and HUD chips are short nouns or verb stems without a final 。: 「再試行」, 「保存」, 「ライブ」, 「オフ」. +- Japanese has no case: an UPPERCASE English label becomes the same short Japanese label. Codes, units and names stay in their own case (HDG, ADS-B, km). +- Full-width punctuation in Japanese text: 、。:?!()「」. Quote names and titles with 「」: 「{shot}」を削除しますか? Keep `·` as the HUD separator, `…` for an ellipsis and `—`, `→`, `↗` as in English. A dash inside a sentence becomes 。 or :. +- Button names cited in running text go in []: [次へ]を使ってください. +- No space between Japanese and Latin letters, digits or a `{placeholder}`: 「AIS船舶」, 「Spaceを長押しで話す」, 「{count}件」, 「{name}を表示」. +- Two exceptions keep the space: a Japanese label before its value in a readout (「雲量 {pct}%」, 「ステーション {id}」, 「高度 {altitude}」), and the state words in toggles (「CCTV オン」, 「ラジオ オフ」). +- Waiting states are 「X待ち」, not 「Xを待機中」: 「観測データ待ち」, 「進路/予報円は第{number}報待ち」, 「RTL-SDRデバイスの接続待ち…」. 待機中 is for something on standby with no object: 「フィード待機中」, 「音声待機中」. +- Key names keep their own case in running text and hints: Tab, Enter, Space, Esc (「Tabでプレビュー · Enter / Spaceで選択」). Text that CSS upper-cases shows them in capitals, as in English. +- Loanwords are katakana with the long-vowel mark, as in Microsoft and Apple style: ユーザー, ブラウザー, ビューアー, プロバイダー, レイヤー, フィルター, モニター, パラメーター. Pick one katakana form per term and keep it (see the glossary). +- Product, provider and dataset names stay in Latin script: Google Maps, Cesium ion, OpenSky, CelesTrak, NASA FIRMS, Sentinel-2, KartaView. Google's own product names use Google's Japanese, joined like any Latin word: Googleストリートビュー. Nepal place names in the Bhote Koshi scenes stay in Latin script, as in zh-CN; country and well-known city names are translated (ネパール, 東京, オースティン). + +## Numbers, units and dates + +- Units: km, m, km/h, m/s, kt, ft, mph, hPa, MW, dB, °, % stay as written, with the English spacing (「250 km」, 「{frp} MW」, 「95 kt」). Code-built cockpit distances follow this too (`cockpitUnitCase` skips the HUD upper-casing for ja, as for zh). The cockpit's own speed caption is the HUD code KT, in 対地速度 · KT and under the readout. +- Number ranges keep the English en dash, not ~: 「2–6日ごと」, 「50–99%」, 「南緯25°–北緯80°」. +- Time units are words written straight after the number: 秒, 分, 時間, 日 (「5分前」, 「{count}時間」, 「1時間5分」). `formatQuantity` and `formatRelativeTime` drop the space for Japanese unit words, so code-built values match the catalog. +- Counters follow the thing counted: 機 (aircraft), 隻 (ships), 基 (satellites), 台 (cameras, vehicles), 局 (stations), 件 (records, results, events), か所 (sites), 個 (generic). Prefer a counter to a bare number. +- Dates come from `Intl` and read year first: 2026年8月26日. Coordinates in prose are 北緯 / 東経; HUD hemisphere letters stay N, S, E, W. + +## Plurals + +`Intl.PluralRules('ja')` has one category, `other`: a `tn()` entry has only that form. Put the counter next to the number: 「航空機{n}機」, 「船舶{n}隻」, 「メッセージ {count}件」. + +## Glossary + +| English | 日本語 | +| ----------------------------------------- | --------------------------------------- | +| layer / data layer | レイヤー / データレイヤー | +| feed | フィード | +| source / data source / attribution | ソース / データソース / 出典 | +| LIVE (status) / live data | ライブ / ライブデータ | +| SIMULATED | シミュレーション | +| stale / degraded / unavailable | 更新遅延 / 性能低下 / 利用不可 | +| cached (status) | キャッシュの(キャッシュの予報) | +| PARTIAL (feed state) | 部分的 | +| upstream (remote data source) | 配信元 | +| retry | 再試行 | +| key (API key) / POWER UP | キー(APIキー)/ パワーアップ | +| Provider Settings | プロバイダー設定 | +| globe / map / map source / basemap | 地球儀 / 地図 / マップソース / ベースマップ | +| mapped (OSM-mapped sites) | マップ済み | +| aircraft / flight / military flights | 航空機 / フライト / 軍用機 | +| FLIGHT {count} (a rocket stage's nth flight) | {count}回目の飛行 | +| vessel, ship | 船舶 | +| satellite | 衛星 | +| launch (rocket) / space mission | 打ち上げ / 宇宙ミッション | +| stage (rocket) / downrange | 段 / ダウンレンジ | +| contact (tracked object) | コンタクト | +| installation, site | 施設 | +| camera / CCTV camera | カメラ / CCTVカメラ | +| earthquake / active fire / fire perimeter | 地震 / 火災検知 / 延焼範囲 | +| contained / containment (fire) | 封じ込め / 封じ込め率 | +| scene / shot | シーン / ショット | +| briefing / panel / dock | ブリーフィング / パネル / ドック | +| preset / visual preset | プリセット / ビジュアルプリセット | +| Tactical / Minimal / Cyber | タクティカル / ミニマル / サイバー | +| Scope (circular viewport mask) / keyhole | スコープ / キーホール | +| Clean UI | クリーンUI | +| cockpit | コックピット | +| heading / altitude / ground speed | 機首方位 / 高度 / 対地速度 | +| track (TRK, direction over the ground) | 対地針路 | +| HDG / BRG / CRS / FOV / SPD / POS | そのまま(HUDコード) | +| squawk | スコーク | +| radio failure / emergency (7600 / 7700) | 通信機故障 / 緊急事態 | +| map heading / North up | 方位 / 北を上に | +| Street Level / Street View (Google) | ストリートレベル / ストリートビュー | +| Recent Imagery | 最近の衛星画像 | +| Global Context / Context (panel) | グローバルコンテキスト / コンテキスト | +| radio (station) / tune / seek | ラジオ(局)/ 選局・同調 / シーク | +| Public Safety / SAFETY (radio category) | 警察・消防 | +| transit / bikeshare | 公共交通 / シェアサイクル | +| occupancy: empty / standing room only / crushed | 乗客なし / 立ち乗りのみ / 非常に混雑 | +| traffic flow (TomTom) | 交通流 | +| Previous / Next | 前へ / 次へ | +| SIG / NEWS / LOCAL (briefing tabs) | シグナル / ニュース / ローカル | +| compass points (N, NNE, NE, ENE, …) | 北, 北北東, 北東, 東北東, … | +| weather field / FIELD (selector) | 要素 | +| weather model / forecast lead hours | モデル / 予報時間 | +| valid (forecast time) / issued | 対象時刻 / 発表 | +| radar reflectivity / lightning density | レーダー反射強度 / 雷密度 | +| cyclone, storm (NHC/CPHC) / advisory | サイクロン / 情報(第N報); alone: サイクロン情報 | +| potential tropical cyclone (NHC PC) | サイクロン化のおそれがある擾乱 | +| forecast uncertainty cone | 予報円 | +| tropical storm / tropical depression | 熱帯暴風雨 / 熱帯低気圧 | +| scene cloud / mosaic / daily overview | シーン雲量 / モザイク / 日次概観 | +| box (Recent Imagery area) | 範囲 | +| WINDOW (search radius around a subject) | 範囲 | +| coverage / coverage % / full, partial | カバー範囲 / カバー率(%のみ)/ 全体, 一部 | +| pin (Recent Imagery) | 固定 | +| clear day (Recent Imagery) | 雲の少ない日 | +| outline / mark (map annotation) | 輪郭 / マーク | +| share (a view or link) | 共有 | +| import / export (project, presets) | インポート / エクスポート | +| EXPORT (image, PNG) | 書き出し | +| voice (the voice assistant) | 音声アシスタント / 音声機能 | +| activate (a control, screen-reader text) | 押す(「押すと…」) | +| AI agent | AIエージェント | +| frame (bring into view) | 視野に収める | +| story beat / evidence beat | ストーリー区切り / 証拠の区切り | +| camera pose / camera move | カメラの姿勢 / 移動 | +| roundabout / ramp / exit | ロータリー / ランプ / 出口 | +| reduced motion | 動きを減らす | +| NOT AN ALL-CLEAR | 安全の保証ではありません | +| … Watch (scene titles: Earth, Orbital) | 監視(地球監視, 軌道監視) | diff --git a/docs/translations/pt.md b/docs/translations/pt.md new file mode 100644 index 000000000..682c0a3b9 --- /dev/null +++ b/docs/translations/pt.md @@ -0,0 +1,107 @@ +# Brazilian Portuguese (`pt`) + +Catalog: `src/i18n/locales/pt.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md). + +Translation source: abiyeenzo (#909). + +## Style + +- Brazilian Portuguese (pt-BR): câmera, tela, compartilhar, arquivo, baixar, ônibus, metrô. Every `pt` tag, `pt-PT` included, gets this catalog, so avoid slang and regionalisms. +- Address the user with «você» imperatives: «Clique no mapa», «Ative o CCTV», «Tente novamente». Tooltips that name an action use the infinitive: «Expandir painel», «Redefinir a câmera». +- Sentence case, not Title Case: "Live Flights" → «Voos ao vivo». UPPERCASE English stays UPPERCASE: «AO VIVO», «SEM DADOS». +- Quotes inside Portuguese text are straight double quotes: «Excluir o plano "{shot}"?». A sentence that names a panel quotes it: «o painel "Configurações de provedores"» as the subject, so the verb agrees, and «adicione em "Configurações de provedores"» after a preposition. +- A dash between clauses is a spaced em dash « — », even where the English runs it in. +- A date or time after a participle takes a preposition: «emitido em {time}», «válida para {valid}», «Posição em {time}», «onde estava em {time}». Use «em», not «às», wherever {time} can include a date. +- A count in a string with no plural form (no `tn()`) goes after a colon, so it reads right for 1: «sincronizando feeds de cidades: {count}...», «CÂMERAS EM ESPERA: {count}». +- A placeholder that holds a layer or tool name of unknown gender and number gets a noun to agree with: «{label}: camada ativada», «A ferramenta "{owner}" está usando o ponteiro — feche-a primeiro». +- Short labels, chips and HUD readouts stay short. A standard abbreviation with a period beats a wrapped line: ANT., PRÓX., DESLIG., EXCL., INCL., ALT., VEL., câm. +- The HUD codes HDG, BRG, CRS, FOV, DEST, SPD, RANGE, ALT, GS, TRK and V/S stay in English, as in cockpit avionics («ALT {altitude} FT · GS {speed} KT · TRK {track}», «V/S {rate} FT/MIN»), and so does the HUD recording mark REC. Words used in a sentence are translated: heading → «rumo», range → «alcance». +- Keyboard keys stay in English: Space, Esc, Enter, Tab, Shift, Backspace, H, 1–9 («Segure Space para falar»). +- Common English tech words used in Brazil stay: feed, stream, snapshot, briefing, dock, layout, bloom, status, app, link, token. +- Units: km, m, km/h, m/s, kt (knots), ft (feet), h, min, s, always spaced from the number («Terremotos (24 h)», «Missões espaciais (30 d)»; in the layer names the space is a no-break space, U+00A0, so the unit never wraps away from the number); in UPPERCASE text KM, KM/S, KT, FT, MIN, H. "m" is always metres, so minutes are «min» («há 5 min»). MW stays MW. `°` and `%` stay. Decimal comma in written numbers («M4,4»). +- "ago" goes before the time: «há {count} min», «mais recente há {value}». When a placeholder already holds a localized "ago" text (`{age}`, `{ago}`), do not add «há» again. +- Money: «US$ {amount} mil / mi / bi» for thousands, millions and billions. +- Compass points use Leste/Oeste: N, NNE, NE, ENE, L, ESE, SE, SSE, S, SSO, SO, OSO, O, ONO, NO, NNO; hemisphere letters N, S, L, O. + +## Plurals + +A `tn()` entry needs three forms. `Intl.PluralRules('pt')` follows Brazilian usage: + +| Form | Counts | Example | +| ------- | -------------------------------------- | ------------------ | +| `one` | 0, 1 and anything below 2: 0, 1, 1.5 | 0 câmera, 1 câmera | +| `many` | 1 000 000, 2 000 000… (exact millions) | 1000000 de câmeras | +| `other` | everything else: 2–999 999, 2.5… | 3 câmeras | + +The `many` form is the `other` text with «de» after the number: «{count} de câmeras carregadas», «{count} DE CHAVES PENDENTES». A `one` form is also read for 0, so keep it natural with zero («0 arquivo selecionado» is standard Brazilian usage). + +## Glossary + +| English | Português | +| --------------------------------------------- | ------------------------------------------------------------ | +| layer / data layer | camada / camada de dados | +| feed / source / data source | feed / fonte / fonte de dados | +| LIVE (status) / live data | AO VIVO / dados ao vivo | +| SIMULATED | SIMULADO | +| stale / degraded / unavailable | desatualizado / degradado / indisponível | +| retry / retrying | nova tentativa / tentando de novo | +| retrying (a fetch) / retrying slowly | buscando de novo / tentando de novo em intervalos maiores | +| rate-limited | «X: limite de requisições atingido» | +| dismiss (window, card, error) | fechar | +| key (API key) / POWER UP / POWERED UP | chave / ATIVAR / ATIVADO | +| Provider Settings | Configurações de provedores | +| ON / OFF (state chips) | ATIVO / INATIVO | +| enable / disable | ativar / desativar | +| globe / map / map source / map stack | globo / mapa / fonte do mapa / pilha de mapas | +| basemap / tile | mapa base / bloco | +| aircraft, plane / flight / military flights | aeronave, avião / voo / voos militares | +| vessel, ship / TANKER (AIS class) | embarcação / NAVIO-TANQUE | +| satellite / shell (Starlink) / DENSE | satélite / camada orbital / DENSO | +| SPARSE / BALANCED / DENSE (detection modes) | ESPARSO / EQUILIBRADO / DENSO (o modo) | +| launch (rocket) / launch site / space mission | lançamento / local de lançamento / missão espacial | +| KM DOWNRANGE / EST. DOWNRANGE | KM DO LOCAL DE LANÇAMENTO / EST. PELA DIST. DO LANÇAMENTO | +| ascent / replay / payload / stage | subida / reprise / carga útil / estágio | +| contact / tracking / track | contato / rastreamento / rastro | +| installation / site | instalação / local | +| PLACES CANDIDATE (installation class) | CANDIDATO DO GOOGLE PLACES | +| mapped / observed | mapeado / observado | +| camera / CCTV camera | câmera / câmera de CCTV | +| CALIBRATED / CURATED (CCTV badge) | CALIBRADA / COM CURADORIA | +| AUTO HOP ON / OFF (CCTV) | TROCA AUTO. LIGADA / DESLIG. (as the other CCTV toggles) | +| earthquake / active fire | terremoto / incêndio ativo | +| advisory (NHC/CPHC cyclone) | boletim | +| E/C Pacific (NHC/CPHC coverage) | Pacífico Leste/Central (no «L/C») | +| weather (layer, card) / weather satellite | clima / meteorologia, meteorológico | +| FOG / cloud unknown | NEVOEIRO / nebulosidade desconhecida | +| AQHI bands Low … Very High | Risco baixo … Risco muito alto | +| radio station / broadcaster | estação de rádio / emissora | +| Oldies / World (radio tags) | Flashback / Músicas do mundo | +| radio genre tags agree with «música» | Alternativa, Clássica, Eletrônica, Latina | +| road / flow (traffic) | via / fluxo | +| Street Level (layer) | Nível da rua | +| scene / shot / scene recipe | cena / plano / receita de cena | +| briefing / panel / dock | briefing / painel / dock | +| cockpit | cabine | +| preset / visual preset / EXPORT PRESETS | predefinição / predefinição visual / EXPORTAR PREDEF. | +| VISUAL PRESETS (dock caption) | PREDEF. VISUAIS | +| Scope (circular viewport mask) / keyhole | mira / visor | +| feather / fade | suavização / esmaecimento | +| detection overlay | sobreposição de detecção | +| pin (Draw tool) / box (Recent Imagery) | marcador / área | +| Cyber (HUD layout, effect) | Ciber | +| heading / course / altitude / ground speed | rumo / curso / altitude / velocidade solo (VELOC. SOLO) | +| map heading / bearing (compass, north up) | orientação (do mapa) | +| HDG / BRG / CRS / FOV / DEST / SPD / RANGE | HDG / BRG / CRS / FOV / DEST / SPD / RANGE (unchanged) | +| V/S (vertical speed code) | V/S (unchanged) | +| PREV / NEXT | ANT. / PRÓX. | +| Previous / Next (button text) | Anterior / Próximo (Próxima) | +| L / R (relative bearing) | E / D | +| SIG / NEWS / LOCAL (briefing tabs) | SIG / NOTÍCIAS / LOCAL | +| REGIONAL NEWS (fits the 16-character kicker) | NOTÍCIAS DA ÁREA | +| PITCH / HGT (CCTV calibration chips) | INCL. / ALT. | +| NOT AN ALL-CLEAR | NÃO GARANTE SEGURANÇA | +| Last report (transit vehicle position) | Último registro | +| intersection (CCTV) | cruzamento | +| {layers} just turned on (any list of layers) | «{layers}: ativação recente» (no agreement needed) | +| {label} on / off (one layer) | «{label}: camada ativada / desativada» | +| turn-by-turn: turn left / keep left / lane | vire à esquerda / mantenha-se à esquerda / faixa da esquerda | diff --git a/docs/translations/uk.md b/docs/translations/uk.md new file mode 100644 index 000000000..673748f42 --- /dev/null +++ b/docs/translations/uk.md @@ -0,0 +1,106 @@ +# Ukrainian (`uk`) + +Catalog: `src/i18n/locales/uk.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md). + +Translation source: meirm (#312). + +## Style + +- Address the user with polite imperatives: «Натисніть», «Увімкніть», «Спробуйте ще раз». +- Sentence case, not Title Case: "Live Flights" → «Рейси онлайн». +- Ukrainian norms, not Russian calques: «у радіусі», «за посиланням», «надто довгий», «потім», «вигляд» (a camera view: «вигляд згори»), «стеження» (tracking); prepositions alternate у/в and і/й by euphony. +- The apostrophe is always ʼ (U+02BC): обʼєкт, звʼязок, памʼять. Not ' or ’. +- Quotes inside Ukrainian text are «ялинки»; a button or layer name inside a sentence goes in them: Увімкніть «Вуличні знімки». +- In short labels, a standard abbreviation with a period beats a wrapped line: ПОПЕР., ДАЛЬН., КАЛІБР. +- Units: км, м, км/год, м/с, вуз (knots), фт (feet), год, хв, с, МГц, дБ, гПа; in UPPERCASE text КМ, М, КМ/С, ВУЗ, ФТ. MW is МВт in any case. `°` and `%` stay. Decimal comma in coordinates written as text: 28,2710° пн. ш. +- Coordinates: пн. ш. / пд. ш. / сх. д. / зх. д.; the HUD hemisphere letters are Пн, Пд, Сх, Зх. + +## Plurals + +A `tn()` entry needs four forms. `Intl.PluralRules('uk')` picks: + +| Form | Counts | Example | +| ------- | ------------------------ | ---------- | +| `one` | 1, 21, 31, 101… | 1 камера | +| `few` | 2–4, 22–24, 32–34… | 3 камери | +| `many` | 0, 5–20, 25–30, 111–114… | 5 камер | +| `other` | fractions: 1.5, 2.25… | 1,5 камери | + +`other` takes the genitive singular: 1,5 кадру, 1,5 літака, 1,5 ключа. When a word next to a number cannot agree in every form, move the number after a colon instead: «поруч: {count}», «відповідей: {responses}». The same trick keeps place names and tool names out of a preposition's case: «звідки: {origin}», «Активно: {name}». + +## Glossary + +| English | Українська | +| ----------------------------------------------- | ---------------------------------------------------------------- | +| layer / data layer | шар / шар даних | +| feed | потік даних (short: потік) | +| source / data source | джерело / джерело даних | +| LIVE (status) / live data | ОНЛАЙН / у реальному часі (status chip: ДАНІ ОНЛАЙН) | +| SIMULATED / SYNTHETIC | ІМІТАЦІЯ | +| stale / degraded / unavailable | застарів / з перебоями (chip: ПЕРЕБОЇ) / недоступний | +| retry | повтор | +| key (API key) / POWER UP | ключ / ЗАПУСК | +| Provider Settings / provider | Налаштування постачальників / постачальник | +| globe / map / map source / basemap | глобус / карта / джерело карти / підкладка | +| aircraft / flight / military flights | літак (short HUD: борт) / рейс / військові рейси | +| vessel, ship / vessels | судно / судна (gen. pl. суден) | +| satellite | супутник | +| launch (rocket) / space mission | запуск / космічна місія | +| ascent (rocket) / booster FLIGHT {n} | виведення / ПОЛІТ {n} (not рейс) | +| contact / tracking | контакт / стеження | +| installation, site / mapped | обʼєкт / нанесений (обʼєкт, контакт) | +| camera / CCTV camera | камера / камера CCTV | +| earthquake / active fire / fire perimeter | землетрус / активна пожежа / контур пожежі | +| advisory (NHC/CPHC cyclone) | зведення | +| scene / shot / scene recipe | сцена / кадр / рецепт сцени | +| briefing / panel / dock | брифінг / панель / док | +| preset / visual preset | пресет / візуальний пресет | +| Scope, keyhole (circular viewport mask) | приціл | +| overlay (detection overlay) / world overlay | накладка / накладка на глобусі | +| heading / altitude / ground speed | курс / висота / шляхова швидкість | +| map bearing, map heading | азимут карти | +| HDG / BRG / CRS, TRK / FOV / DEST / SPD / RANGE | КУРС / ПЕЛ. / ШК (шляховий кут) / ОГЛЯД / ПРИЗН. / ШВИД / ДАЛЬН. | +| Street View (Google) | Перегляд вулиць (as in Ukrainian Google Maps) | +| Street Level (layer) | Вуличні знімки | +| imagery / recent imagery | знімки / свіжі знімки | +| radar reflectivity / lightning density | радіолокаційна відбивність / щільність блискавок | +| infrared | інфрачервоний (short: ІЧ) | +| receiver / gain / decoder | приймач / підсилення / декодер | +| broadcaster | мовник | +| CONUS, WHOT/BHOT, Ironbow, CRT, NVG, FLIR | без перекладу | +| PREV / NEXT (HUD) / Previous / Next | ПОПЕР. / ДАЛІ / Попередній / Наступний | +| anchor / data pack / bundle (scene) | якір / пакет даних / архів | +| box (Recent Imagery area) | область | +| camera move | переліт | +| SIG / NEWS / LOCAL (briefing tabs) | СИГН. / НОВИНИ / МІСЦ. | +| compass points (N, NNE, NE, …) | Пн, ПнПнСх, ПнСх, … | +| L / R (relative bearing) | Л / П | +| click / tap | клацнути / натиснути | +| session (voice, OAuth) | сеанс | +| sequence (street-level photos) | послідовність | +| search (place search) | пошук місця | +| pitch (CCTV camera pose) | нахил | +| traffic (road traffic layer) | дорожній рух | +| phosphor (CRT) | люмінофор | +| view (map extent) / view centre / in view | видима область (карти) / центр карти / у кадрі | +| select (verb) / selected | вибрати / вибраний (not обрати) | +| ping (cockpit proximity contacts) | контакт | +| merge (turn-by-turn) / on ramp | влитися в потік / вʼїзд (off ramp: зʼїзд) | +| watch (Earth/Orbital Watch) | спостереження | +| timeout | тайм-аут | +| icon | значок (not іконка) | +| run (scene playback) / RUN LOG | відтворення / ЖУРНАЛ ВІДТВОРЕННЯ (запуск is a rocket launch) | +| Preview (voice panel replies) | відповіді | +| Marine (radio tag) / pleasure craft | море (not флот) / прогулянкове судно | +| emergency (ADS-B) / Emergency (radio tag) | аварійний стан / надзвичайні ситуації | +| confidence (FIRMS fire detection) | достовірність (short: достов.) | +| thermal (FLIR-style imaging) | тепловий (not термальний) | +| map stack | набір карт | +| stage (rocket) re-entry / recovery | вхід ступеня в атмосферу / повернення | +| ON / OFF (toggle chips) | УВІМК. / ВИМК. | +| Clean UI / EXIT CLEAN VIEW | чистий інтерфейс / ПОВЕРНУТИ ІНТЕРФЕЙС | +| full globe view | вигляд всього глобуса | +| inputs (cockpit context chip) | потоки (not вхідні дані) | +| downrange (rocket stage) | дальність | +| resolution (spatial) | роздільна здатність (not роздільність) | +| shared follow expired | посилання на стеження застаріло | diff --git a/docs/translations/zh-CN.md b/docs/translations/zh-CN.md new file mode 100644 index 000000000..b3a3f973a --- /dev/null +++ b/docs/translations/zh-CN.md @@ -0,0 +1,77 @@ +# Simplified Chinese (`zh-CN`) + +Catalog: `src/i18n/locales/zh-CN.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md). + +Translation sources: abiyeenzo (#914) and EDIT-Aeterna (#979). + +## Style + +- Mainland usage and Simplified characters. Address the user neutrally, without 您/你 where a plain imperative works: 「点击地图」, 「重试」. +- Chinese has no case: an UPPERCASE English HUD label becomes the same short Chinese label. Keep labels as short as the English: 「实时」, 「过期」, 「关」. +- Full-width punctuation in Chinese text: ,。:;?!()“”. A dash is 「——」 without spaces; keep `·` as the HUD separator and `…` for ellipsis. +- One space between Chinese and Latin letters or digits: 「AIS 船舶」, 「24 小时」, 「{count} 个」. No space next to full-width punctuation. +- Units: km, m, km/h, m/s, kn, ft, MW, °, % stay as written; time words are 小时, 分钟, 秒 after a number (「5 分钟前」). +- Product names, provider names and codes stay as they are: Google, Cesium, CelesTrak, NOAA, FIRMS, AIS, ADS-B, GNSS, CCTV, HUD, CRT, NVG, FLIR, WHOT/BHOT, Ironbow, CONUS. + +## Plurals + +`Intl.PluralRules('zh-CN')` has one category, `other`: a `tn()` entry has only that form. Use a measure word next to the number where Chinese needs one: 「{count} 个密钥」, 「{count} 架飞机」, 「{count} 艘船」. + +## Glossary + +| English | 中文 | +| ------------------------------------- | --------------------------- | +| layer / data layer | 图层 / 数据图层 | +| feed | 数据源 | +| source / data source | 来源 / 数据源 | +| LIVE (status) / live data | 实时 / 实时数据 | +| SIMULATED | 模拟 | +| stale / degraded / unavailable | 过期 / 降级 / 不可用 | +| retry | 重试 | +| key (API key) / POWER UP | 密钥(API 密钥)/ 增强 | +| Provider Settings | 服务商设置 | +| globe / map / map source / basemap | 地球 / 地图 / 地图源 / 底图 | +| aircraft / flight / military flights | 飞机 / 航班 / 军用航班 | +| vessel, ship | 船舶 | +| satellite | 卫星 | +| launch (rocket) / space mission | 发射 / 太空任务 | +| contact (tracked object) | 目标 | +| installation, site | 设施 | +| camera / CCTV camera | 摄像头 / CCTV 摄像头 | +| earthquake / active fire | 地震 / 活跃火点 | +| scene / shot | 场景 / 镜头 | +| briefing / panel / dock | 简报 / 面板 / 停靠栏 | +| preset / visual preset | 预设 / 视觉预设 | +| Display / HUD layout | 显示 / HUD 布局 | +| Tactical / Minimal / Cyber | 战术 / 精简 / 赛博 | +| Scope (circular viewport mask) / keyhole | 视域 | +| Clean UI | 纯净界面 | +| cockpit | 座舱 | +| heading / altitude / ground speed | 航向 / 高度 / 地速 | +| map heading / North up | 方位 / 正北朝上 | +| Street Level / Street View (Google) | 街景 / 街景(Google) | +| Recent Imagery | 近期影像 | +| Global Context / Context (panel) | 全局态势 / 态势 | +| radio (station) | 电台 | +| transit / bikeshare | 公共交通 / 共享单车 | +| Previous / Next | 上一个 / 下一个 | +| compass points (N, NNE, …) | 北, 北东北, … | +| weather field / FIELD (selector) | 气象场 / 要素 | +| weather model / forecast lead hours | 模式 / 预报时效 | +| valid (forecast time) | 有效时间 | +| scene cloud / mosaic | 整景云量 / 镶嵌图 | +| pin (Recent Imagery) | 固定 | +| THIS (voice pointer) | 所指 | +| outline / trace an outline | 轮廓 / 绘制轮廓 | +| mark (map annotation) | 标注 | +| camera badge | 摄像头标记 | +| provider / embedded provider controls | 服务商 / 平台控件 | +| share (a view or link) | 分享 | +| AI agent | AI 智能体 | +| stage (rocket) / downrange | 子级 / 航程 | +| capture shot / camera move | 保存镜头 / 运镜 | +| story beat / evidence beat | 事件节点 / 证据节点 | +| camera pose | 位姿 | +| forecast uncertainty cone | 预报不确定范围 | +| seed (catalog data) | 预置 | +| frame (bring into view) | 将…纳入视野 | diff --git a/docs/translations/zh-Hant.md b/docs/translations/zh-Hant.md new file mode 100644 index 000000000..ac355516b --- /dev/null +++ b/docs/translations/zh-Hant.md @@ -0,0 +1,17 @@ +# Traditional Chinese (`zh-Hant`) + +Catalog: `src/i18n/locales/zh-Hant.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md). + +`zh-Hant` is the Simplified Chinese catalog (`zh-CN`, credited to abiyeenzo, #914, and EDIT-Aeterna, #979) written in Traditional characters with OpenCC character conversion. Wording and terminology are the same as in `zh-CN`: change a term in [zh-CN.md](./zh-CN.md) and the `zh-CN` catalog first, then convert. Only the script differs. + +After `s2tw` conversion, fix the characters OpenCC picks wrongly: 年曆史 → 年歷史, 下遊 → 下游, 並道 → 併道, 發佈 → 發布, 公佈 → 公布. + +## Style + +- Follow [zh-CN.md](./zh-CN.md) for wording, punctuation, spacing, units and the glossary, written in Traditional characters. +- Relative-time placeholders may already include 前. In `LIVE · updated {age}`, the caller supplies the complete relative age; do not append another 前. +- The picker displays 中文(繁體) / Chinese (Traditional); explicit Hans tags select Simplified Chinese. + +## Plurals + +`Intl.PluralRules('zh-Hant')` uses only `other`. Keep the measure words of the `zh-CN` entry: 「{count} 架飛機」、「{count} 艘船」. diff --git a/index.html b/index.html index 8a01415b6..803bc2417 100644 --- a/index.html +++ b/index.html @@ -21,31 +21,33 @@ - + - + - +
+
+
+
+
+
+
+
+ +
- - - - - - - - - - - - - - - + +
+
+ +

GOD'S EYE VIEW

+

Initializing photorealistic world...

+
+
diff --git a/package-lock.json b/package-lock.json index 7b6d3476f..dd60d3383 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gods-eye-view", - "version": "0.2.1", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gods-eye-view", - "version": "0.2.1", + "version": "0.3.0", "license": "MIT", "dependencies": { "@jtarrio/signals": "^0.10.0", @@ -19,13 +19,20 @@ "mapillary-js": "^4.1.2", "mgrs": "^2.1.0", "pbf": "^5.1.2", - "satellite.js": "^6.0.2" + "pinia": "^2.3.1", + "satellite.js": "^6.0.2", + "vue": "^3.5.44", + "vue-i18n": "^11.4.15" }, "devDependencies": { + "@happy-dom/global-registrator": "^20.14.6", + "@vitejs/plugin-vue": "^5.2.4", + "@vue/test-utils": "^2.5.1", + "happy-dom": "^20.14.6", "prettier": "3.9.6", - "puppeteer": "^25.10.0", + "puppeteer": "^25.16.0", "sharp": "^0.35.4", - "vite": "^6.0.0", + "vite": "^6.4.4", "vite-plugin-cesium": "^1.2.23", "ws": "^8.21.0" }, @@ -33,6 +40,52 @@ "node": ">=24.14.0 <25 || >=26 <27" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz", + "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.12", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.12.tgz", + "integrity": "sha512-WJ7JBp/Q4+z/ImydtI7jS6NVco5++632ExBemSX+Ysb+G1lCwQ5BVo3SY+jGGOcZXI4ZccbZSUSJLNBW6eEpJg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.11" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", + "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.11", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@cesium/engine": { "version": "22.3.0", "resolved": "https://registry.npmjs.org/@cesium/engine/-/engine-22.3.0.tgz", @@ -537,6 +590,20 @@ "node": ">=18" } }, + "node_modules/@happy-dom/global-registrator": { + "version": "20.14.6", + "resolved": "https://registry.npmjs.org/@happy-dom/global-registrator/-/global-registrator-20.14.6.tgz", + "integrity": "sha512-Xd2FwCdm0cAZ+EG0upp69tpxBT9jtvVxaZv1lnbgWsNaZyEuPs0PojYgvCmtQ1RRdRCYCyWTeLjn3Z0aSxDchA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">=20.0.0", + "happy-dom": "^20.14.6" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@img/colour": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", @@ -1064,6 +1131,73 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@intlify/core-base": { + "version": "11.4.15", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.15.tgz", + "integrity": "sha512-yHNg7+M6VKQuj+KUO/CrtcBMm3iAspvfFe1H/eKI0TP0pn2T2srBF1gEb/TL3c8RA/BZZGOIySwtZD0i+9/hhw==", + "license": "MIT", + "dependencies": { + "@intlify/devtools-types": "11.4.15", + "@intlify/message-compiler": "11.4.15", + "@intlify/shared": "11.4.15" + }, + "engines": { + "node": ">= 22" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/devtools-types": { + "version": "11.4.15", + "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.15.tgz", + "integrity": "sha512-NFV9LsP720IkR6BHNfUYBSkd/abocWimcOWAS63D3spwczy8zDvXCf9WMyhCVQ7FcYkWyECZke18x8D6UNVBFw==", + "license": "MIT", + "dependencies": { + "@intlify/core-base": "11.4.15", + "@intlify/shared": "11.4.15" + }, + "engines": { + "node": ">= 22" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/message-compiler": { + "version": "11.4.15", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.15.tgz", + "integrity": "sha512-dV6apms3MXAG3jZ/6bAq+IW2IKDGasmjA/Aoi0JVKCStVi6GbzrPruCVbU/Djn1n41w5EiM3KdnpTuo6I1qe5w==", + "license": "MIT", + "dependencies": { + "@intlify/shared": "11.4.15", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 22" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/shared": { + "version": "11.4.15", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.15.tgz", + "integrity": "sha512-jlTJkM4bMdr267e1iUE2RMNz1LyoYU6IPReKjnQM6RvipEBUVwmMHbeUckDcOmR+WeungEGjaNmSQJdQzJcOfQ==", + "license": "MIT", + "engines": { + "node": ">= 22" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, "node_modules/@jtarrio/signals": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@jtarrio/signals/-/signals-0.10.1.tgz", @@ -1115,14 +1249,21 @@ "node": ">=24.0.0" } }, + "node_modules/@one-ini/wasm": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.2.1.tgz", + "integrity": "sha512-TUqERXGNTifZ9y2g3wPxQrw3HpHv/02DsW3D90T9x0hhonrL1ZqpSmNrU2XkoIq0fP1N6gZfVQzy2Fw1ZvGBNg==", + "dev": true, + "license": "MIT" + }, "node_modules/@puppeteer/browsers": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.2.tgz", - "integrity": "sha512-q2BU4YfO9h/Wt7IcWPcggpOOqLk2Tbs1hDwolvKZrweRjy751OJBKMN9zO5bfD0pzU7X/tvKw/exQds4pM/LOg==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.6.tgz", + "integrity": "sha512-MJP29gVoHznmqmwhH1tlZqzfeQ3aylTpSSa6WKZXtkwYUH02i9imBmWGTyZrbnAou5paX78SE4jMAUqnEPf8ug==", "dev": true, "license": "Apache-2.0", "dependencies": { - "modern-tar": "^0.8.4", + "modern-tar": "^0.8.6", "yargs": "^18.0.0" }, "bin": { @@ -1556,6 +1697,16 @@ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", "license": "MIT" }, + "node_modules/@types/node": { + "version": "26.6.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.6.tgz", + "integrity": "sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, "node_modules/@types/pako": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/pako/-/pako-1.0.7.tgz", @@ -1605,6 +1756,171 @@ "integrity": "sha512-Qu3Nn6JFuF4+sHKYl+IcX9vYiI40ogleXzFFSxoE1W94rG98o/kXs8uJ0QSfFzuwBCZWlGfUGpPkgwuuX4PchA==", "license": "MIT" }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.44.tgz", + "integrity": "sha512-k34nCytbK8PQ1gR2mGROYOw6Pnv/mumqIdrZ3x2tbyXZDWm7ViKGLTfdB+DRrWqEQsLgGJvn0WN9jBif5zCTLQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.9", + "@vue/shared": "3.5.44", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.2" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.44.tgz", + "integrity": "sha512-W5AoBFWdw1lQAKiHowkfgFKFab82oSRT6sGsPfw/q1GRjsy/ZJFLHGhJyBRIFN9APLTUduDH9U2JAiPOIyKuYg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.44", + "@vue/shared": "3.5.44" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.44.tgz", + "integrity": "sha512-dzHmQ4jXTpdr5FjIQUeykVakN/uzqFbUo9/C5ikBFg3a3PeM6uRxeczx1ZbXBML0/zJstEh3ygYipzR5Z7b2Pw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.9", + "@vue/compiler-core": "3.5.44", + "@vue/compiler-dom": "3.5.44", + "@vue/compiler-ssr": "3.5.44", + "@vue/shared": "3.5.44", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.29", + "source-map-js": "^1.2.2" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.44.tgz", + "integrity": "sha512-xr5gb1JVHlfXJwSlRz1FeP6uxoazdzHuMTVLE7up+4Vrqxk219CvxLPxD9NXBt47yvZW7+p4DXfyp4O1fkByIA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.44", + "@vue/shared": "3.5.44" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.44.tgz", + "integrity": "sha512-u+QJdKeBBEADkPdevsL+6E12QlYVfBD8Zcw5qjDQKtamI6l14KyqazJMobhCKelhWN5zpyKUiEDoTyHE93cqWw==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.44" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.44.tgz", + "integrity": "sha512-uEa/ndwjhrrhnOfb2p+sZy6zeoamJOISqaLSJGk/8R6nABT1ZldpzcJoPzKj+zHVomVBPcmQYyC2r4LtvDV6CQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.44", + "@vue/shared": "3.5.44" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.44.tgz", + "integrity": "sha512-3fc4ARcXVhVBaJ3ujWiNZJARD6pkyRcwG+ZYhMzjKtKUp7MAY0HLwr7r3Smep3Li6ZaWT6ewdmx8XqdaD5oNZg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.44", + "@vue/runtime-core": "3.5.44", + "@vue/shared": "3.5.44", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.44.tgz", + "integrity": "sha512-R4S1UoH4h5Frrauv8Z5XE5TxV9oBiUSgWIC4TyVK1FK+RocRnOLwFA5Xcmo2011RlgJp+AbZCnw6afrhlgRuUQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.44", + "@vue/runtime-dom": "3.5.44", + "@vue/shared": "3.5.44" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.44.tgz", + "integrity": "sha512-H82XLH0DjmSOdbEwO15x3QTo1f5PWEtv237eLpYKc0mqLu+B8hnq16lFWmuBmNurkeiG3wr+oM1FhOll/YVFRg==", + "license": "MIT" + }, + "node_modules/@vue/test-utils": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.5.1.tgz", + "integrity": "sha512-V/4a99odJ7hapr0bRt35XxljGk8hefq31gSOnlKTcEq2fUeoj2qb+FcpJYfQu4qfYhW9Ug+O2+CZ3QmZ7gXDzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-beautify": "^2.0.0", + "vue-component-type-helpers": "^3.0.0" + }, + "peerDependencies": { + "@vue/compiler-dom": "3.x", + "@vue/server-renderer": "3.x", + "vue": "3.x" + }, + "peerDependenciesMeta": { + "@vue/server-renderer": { + "optional": true + } + } + }, "node_modules/@zip.js/zip.js": { "version": "2.8.18", "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.18.tgz", @@ -1619,10 +1935,20 @@ "node": ">=18.0.0" } }, + "node_modules/abbrev": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-5.0.0.tgz", + "integrity": "sha512-/XrFJgzQQQHpti1raDJC6m4ws6aNktmjBlhk8Fdlk7LwCEuDoieEJJY9OFHjfiFJFFRM2tK+Ky/IsfbbmlMu1w==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, "node_modules/ansi-regex": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", - "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", "dev": true, "license": "MIT", "engines": { @@ -1667,18 +1993,54 @@ "pnpm": ">=10.10.0" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/bitmap-sdf": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/bitmap-sdf/-/bitmap-sdf-1.0.4.tgz", "integrity": "sha512-1G3U4n5JE6RAiALMxu0p1XmeZkTeCwGKykzsLTCqVzfSDaN6S7fKnkIkfejogz+iwqBWc0UYAIKnKHNN7pSfDg==", "license": "MIT" }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/browser-split": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/browser-split/-/browser-split-0.0.1.tgz", "integrity": "sha512-JhvgRb2ihQhsljNda3BI8/UcRHVzrVwo3Q+P8vDtSiyobXuFpuZ9mq+MbRGMnC22CjW3RrfXdg6j6ITX8M+7Ow==", "license": "MIT" }, + "node_modules/buffer-image-size": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz", + "integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/camelize": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", @@ -1707,14 +2069,14 @@ } }, "node_modules/chromium-bidi": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", - "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", + "version": "157.0.8090-0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-157.0.8090-0.tgz", + "integrity": "sha512-KJw7IQgqXwxpJgJlEIPg5mr2ZoqA+jkTRtc5EABI7sUJ3bu3cpI7NuEBoj4Gn1AsF78CaT9So5WQWspK3/wYag==", "dev": true, "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", - "zod": "^3.24.1" + "zod": "^4.6.5" }, "engines": { "node": ">=20.19.0 <22.0.0 || >=22.12.0" @@ -1723,16 +2085,6 @@ "devtools-protocol": "*" } }, - "node_modules/chromium-bidi/node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/cliui": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", @@ -1772,6 +2124,23 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1821,9 +2190,9 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1666840", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1666840.tgz", - "integrity": "sha512-gCcO42XCHKEs7Ag0S7aGYsnJ7hlgrO3qderYqeiY0Eqk+0GFfuvT13IA0hHreJTa2KCdDVyGMeOhdMNmrrTjVg==", + "version": "0.0.1704951", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1704951.tgz", + "integrity": "sha512-GQhQOs9u/Mvm1zMtJIH872JS4AskuZdUSsPNPk4ChahtWAUqgz6atEtIsMt+nO7MqF1Ic4HxII/k8Cu36JivNg==", "dev": true, "license": "BSD-3-Clause" }, @@ -1853,6 +2222,35 @@ "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", "license": "ISC" }, + "node_modules/editorconfig": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-3.0.2.tgz", + "integrity": "sha512-T0ix8GhtxyKVfUFEcvdNDt3YGqlwkFHbD4/5bgFUDgFmxhI/cSRAeJ87/Sz//Cq8Eam6JX/e23RkoFO71P7aAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.2.1", + "commander": "^14.0.3", + "minimatch": "~10.2.4", + "semver": "^7.7.4" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1890,6 +2288,18 @@ "node": ">= 0.8" } }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/error/-/error-4.4.0.tgz", @@ -1963,7 +2373,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, "license": "MIT" }, "node_modules/etag": { @@ -2054,9 +2463,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", - "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", "dev": true, "license": "MIT", "engines": { @@ -2066,6 +2475,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/global": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", @@ -2089,6 +2516,25 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "license": "MIT" }, + "node_modules/happy-dom": { + "version": "20.14.6", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.14.6.tgz", + "integrity": "sha512-5DDsf4QmEIlRL6NkCv/fxSjYHHkZceL/IaYQdJc2h3G7DRTEai++VTjbfL0LB9geW2VrA3+vrjF3Efx+OTtX0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">=20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "@types/ws": "^8.18.1", + "buffer-image-size": "^0.6.4", + "entities": "^7.0.1", + "whatwg-mimetype": "^3.0.0", + "ws": "^8.21.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/hls.js": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.7.3.tgz", @@ -2148,6 +2594,13 @@ "dev": true, "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, "node_modules/is-object": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", @@ -2167,6 +2620,35 @@ "@types/estree": "*" } }, + "node_modules/js-beautify": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-2.0.3.tgz", + "integrity": "sha512-cyFbh3tkPhknnTD/0bLf0T0yy2ZIbqL05mttzbt4y1Zfr7NxqXQZ62dkBLKs3oHH/lpjmDRAnciJiSUyOy8XwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^3.0.2", + "glob": "^13.0.6", + "js-cookie": "^3.0.8", + "nopt": "^10.0.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-cookie": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz", + "integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==", + "dev": true, + "license": "MIT" + }, "node_modules/jsep": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", @@ -2226,6 +2708,16 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/magic-string": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", @@ -2353,6 +2845,32 @@ "dom-walk": "^0.1.0" } }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", @@ -2361,9 +2879,9 @@ "license": "MIT" }, "node_modules/modern-tar": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz", - "integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==", + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.6.tgz", + "integrity": "sha512-LYSks0tNVqbtez/9OB0YFP6qLRSqkdAxJWOQpVaR67lNTbj2zNLyYpowkRMia5lXLCZKzxwoxkxH2MbbBR2aEg==", "dev": true, "license": "MIT", "engines": { @@ -2378,10 +2896,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.19", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", - "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", - "dev": true, + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", "funding": [ { "type": "github", @@ -2402,6 +2919,22 @@ "integrity": "sha512-f7h4svPtl+QidoBv4taKXUjJ70G2asaZ8G28nS0OkqaalX8dwwrtWtyxEDPK62AC00ur/+/E0pUwBwY5EPn15Q==", "license": "MIT" }, + "node_modules/nopt": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-10.0.1.tgz", + "integrity": "sha512-df3sBr/6ax9hSGuC3CspvLlbnX8cP5L5nZwXF8cGN8l0zSWR6BvzmQ6jPUKjvo6+/xdpkNvEcucBNUdBeeV13g==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^5.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, "node_modules/nosleep.js": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/nosleep.js/-/nosleep.js-0.12.0.tgz", @@ -2437,6 +2970,23 @@ "node": ">= 0.8" } }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/pbf": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/pbf/-/pbf-5.1.2.tgz", @@ -2453,7 +3003,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -2469,6 +3018,28 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/polylabel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/polylabel/-/polylabel-1.1.0.tgz", @@ -2485,10 +3056,9 @@ "license": "ISC" }, "node_modules/postcss": { - "version": "8.5.28", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", - "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", - "dev": true, + "version": "8.5.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz", + "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==", "funding": [ { "type": "opencollective", @@ -2505,9 +3075,9 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.18", + "nanoid": "^3.3.20", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" @@ -2538,6 +3108,13 @@ "node": ">= 0.6.0" } }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, "node_modules/protobufjs": { "version": "8.8.0", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.8.0.tgz", @@ -2557,19 +3134,19 @@ "license": "MIT" }, "node_modules/puppeteer": { - "version": "25.10.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.10.0.tgz", - "integrity": "sha512-9ZfkiaZDQWpGPJp9XTS+Bkn/D78hPvYmtjPfIBeybn05oeY6Jj7aiSbYdfcSQD2UMvC0vE7Yi9PSDo179euRzw==", + "version": "25.16.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.16.0.tgz", + "integrity": "sha512-LOr3reYnxAYYfQkpopz1fQnL0LdvqhpR5U2Fu+QnwzTX2gpVTHsn91UETo5NNC1md2/oC54Zu1ChBXgZDNSP1A==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "3.2.2", - "chromium-bidi": "17.0.2", - "devtools-protocol": "0.0.1666840", + "@puppeteer/browsers": "3.2.6", + "chromium-bidi": "157.0.8090-0", + "devtools-protocol": "0.0.1704951", "lilconfig": "^3.1.3", - "puppeteer-core": "25.10.0", - "typed-query-selector": "^2.12.2" + "puppeteer-core": "25.16.0", + "typed-query-selector": "^2.12.3" }, "bin": { "puppeteer": "lib/puppeteer/node/cli.js" @@ -2579,18 +3156,18 @@ } }, "node_modules/puppeteer-core": { - "version": "25.10.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.10.0.tgz", - "integrity": "sha512-Hy5eMQshOEMil4JUUx03h5pw1HYkYCso1RG/gcpPlFSd4cYPOcopxcXEAxpLPOkOPJb9LIJtwxuj66bSdvknFg==", + "version": "25.16.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.16.0.tgz", + "integrity": "sha512-VA5Pf54N8shfopNkZZXCdiiwfnnY8ZJYAMqFQRhlH5mGUhfwf53Ar770GPtdpTDLUqvQeYpIBtC9/G2Jij2IJw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "3.2.2", - "chromium-bidi": "17.0.2", - "devtools-protocol": "0.0.1666840", - "typed-query-selector": "^2.12.2", - "webdriver-bidi-protocol": "0.4.3", - "ws": "^8.21.3" + "@puppeteer/browsers": "3.2.6", + "chromium-bidi": "157.0.8090-0", + "devtools-protocol": "0.0.1704951", + "typed-query-selector": "^2.12.3", + "webdriver-bidi-protocol": "1.0.1", + "ws": "^8.22.0" }, "engines": { "node": ">=22.12.0" @@ -2835,7 +3412,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -2871,9 +3447,9 @@ "integrity": "sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==" }, "node_modules/string-width": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", - "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2963,9 +3539,16 @@ "license": "0BSD" }, "node_modules/typed-query-selector": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", - "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", "dev": true, "license": "MIT" }, @@ -3002,9 +3585,9 @@ } }, "node_modules/vite": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", - "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.4.tgz", + "integrity": "sha512-xGjU/SyjeNyb0E4KxXBvYz/1C+3JXuujvf2hXNOtnlcE+58o5ajd9N2/n2VhkiJvpCP2qQasnUpx5PXNeI+p1Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3125,6 +3708,81 @@ "rollup": "^2.25.0" } }, + "node_modules/vue": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.44.tgz", + "integrity": "sha512-dvvmaOCG7lAMkJTPqPwWeGMsPS6DIsFsfZDUgBp9V6mHf9b3+/wyVwYR4Es+WV6uJC0FKRxp81Obcq0ZwUcGFQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.44", + "@vue/compiler-sfc": "3.5.44", + "@vue/runtime-dom": "3.5.44", + "@vue/server-renderer": "3.5.44", + "@vue/shared": "3.5.44" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "3.3.14", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.14.tgz", + "integrity": "sha512-Pq3+OnbCiPCHk0R7KSZrx+j/XXaUn2kSQ1epxq60w8CPbByLjC1EJgV51n4WY851sTXUVzsNMuitywdkdVyKjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-i18n": { + "version": "11.4.15", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.15.tgz", + "integrity": "sha512-bOoNYoEd/ta4g2EelI9HKxPyMZvTDFoykTNYujDTOGoKDRy5ehKJg8WmMDU7hWqNM5efO7fEGNRuwrBXnwHyDg==", + "license": "MIT", + "dependencies": { + "@intlify/core-base": "11.4.15", + "@intlify/devtools-types": "11.4.15", + "@intlify/shared": "11.4.15", + "@vue/devtools-api": "^6.5.0" + }, + "engines": { + "node": ">= 22" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, "node_modules/web-worker": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", @@ -3132,12 +3790,22 @@ "license": "Apache-2.0" }, "node_modules/webdriver-bidi-protocol": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz", - "integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-1.0.1.tgz", + "integrity": "sha512-WXWs/aXF2a6Al1Z2/V/7Gp2oPZOAAmHjJOTmv4KCXRJ122IMj3tSSMc6a4q3K4QOmhQbse/Ahr+oTVJOU2dUiw==", "dev": true, "license": "Apache-2.0" }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/wrap-ansi": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", @@ -3175,9 +3843,9 @@ } }, "node_modules/ws": { - "version": "8.21.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", - "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", "dev": true, "license": "MIT", "engines": { @@ -3226,9 +3894,9 @@ } }, "node_modules/yargs": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz", - "integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", "dev": true, "license": "MIT", "dependencies": { @@ -3252,6 +3920,16 @@ "engines": { "node": "^20.19.0 || ^22.12.0 || >=23" } + }, + "node_modules/zod": { + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 241c59e59..7dda05144 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "gods-eye-view", "private": true, - "version": "0.2.1", + "version": "0.3.0", "description": "A real-time intelligence console for planet Earth — photorealistic 3D globe, live aircraft/ships/satellites/earthquakes/CCTV, and hands-free voice control. Runs in a browser.", "type": "module", "license": "MIT", @@ -66,13 +66,20 @@ "mapillary-js": "^4.1.2", "mgrs": "^2.1.0", "pbf": "^5.1.2", - "satellite.js": "^6.0.2" + "pinia": "^2.3.1", + "satellite.js": "^6.0.2", + "vue": "^3.5.44", + "vue-i18n": "^11.4.15" }, "devDependencies": { + "@happy-dom/global-registrator": "^20.14.6", + "@vitejs/plugin-vue": "^5.2.4", + "@vue/test-utils": "^2.5.1", + "happy-dom": "^20.14.6", "prettier": "3.9.6", - "puppeteer": "^25.10.0", + "puppeteer": "^25.16.0", "sharp": "^0.35.4", - "vite": "^6.0.0", + "vite": "^6.4.4", "vite-plugin-cesium": "^1.2.23", "ws": "^8.21.0" }, @@ -168,6 +175,7 @@ "./layers/recent-imagery/source": "./src/layers/recentImagery/source.js", "./layers/perimeters": "./src/layers/perimeters/index.js", "./layers/gnss": "./src/layers/gnss/index.js", + "./layers/ghostmaps": "./src/layers/ghostMaps/index.js", "./sources/firms-csv": "./src/data/firmsCsv.js", "./layers/firms": "./src/layers/firms/index.js", "./layers/satellites": "./src/layers/satellites/index.js", @@ -212,8 +220,8 @@ "./voice/actions": "./src/voice/gevActions.js", "./annotations": "./src/annotations/index.js", "./context": "./src/data/contextStore.js", - "./build/html": { - "node": "./build/application-html.js" + "./build/register-vue": { + "node": "./build/registerVue.js" }, "./application/catalog": "./src/app/catalog.js", "./standalone/catalog": "./src/standalone/catalog.js", @@ -222,6 +230,8 @@ "./data/lifecycle": "./src/data/lifecycle.js", "./ui/navigation": "./src/ui/navigationController.js", "./ui/share-restoration": "./src/ui/shareRestoration.js", + "./share/view-codec": "./src/share/viewCodec.js", + "./share/link-path": "./src/share/shareLinkPath.js", "./ui/visual-settings": "./src/ui/visualSettings.js", "./ui/panel-chrome": "./src/ui/panelChrome.js", "./layers/flights/records": "./src/layers/flights/records.js", @@ -244,6 +254,7 @@ "./layers/earthquakes/source": "./src/layers/earthquakes/source.js", "./layers/perimeters/source": "./src/layers/perimeters/source.js", "./layers/gnss/source": "./src/layers/gnss/source.js", + "./layers/ghostmaps/source": "./src/layers/ghostMaps/source.js", "./layers/firms/source": "./src/layers/firms/source.js", "./layers/installations/source": "./src/layers/installations/source.js", "./layers/launches/source": "./src/layers/launches/source.js", @@ -272,6 +283,8 @@ "./layers/street-level/mapillary": "./src/layers/streetLevel/providers/mapillary/index.js", "./layers/street-level/mapillary/source": "./src/layers/streetLevel/providers/mapillary/source.js", "./layers/street-level/google": "./src/layers/streetLevel/providers/google/index.js", + "./layers/street-level/kartaview": "./src/layers/streetLevel/providers/kartaview/index.js", + "./layers/street-level/panoramax": "./src/layers/streetLevel/providers/panoramax/index.js", "./tools": "./src/tools/index.js", "./tools/mcp": "./src/tools/mcp/index.js", "./tools/panel": "./src/tools/panel.js", diff --git a/pinokio/pinokio.js b/pinokio/pinokio.js index d781c1a58..49c42a361 100644 --- a/pinokio/pinokio.js +++ b/pinokio/pinokio.js @@ -22,8 +22,10 @@ module.exports = { if (starting) { const local = info.local('start.js'); if (local?.url) { + // Open in the system browser without changing the app's framing policy. + // This also avoids relying on Pinokio's embedded browser behavior. return [ - { default: true, icon: 'fa-solid fa-earth-americas', text: 'Open God\'s Eye View', href: local.url }, + { default: true, icon: 'fa-solid fa-earth-americas', text: 'Open God\'s Eye View', href: local.url, popout: true }, { icon: 'fa-solid fa-terminal', text: 'Server', href: 'start.js' }, ]; } diff --git a/public/models/README.md b/public/models/README.md index 7e9c089be..aa7f40fbd 100644 --- a/public/models/README.md +++ b/public/models/README.md @@ -8,7 +8,6 @@ under the license listed below. |---|---|---|---|---| | `airplane.glb` | “boeing 747” by [zairiq-123](https://sketchfab.com/zairiq-123) | [Sketchfab model](https://sketchfab.com/3d-models/boeing-747-9b16672038ba48f98e6d80a159044ed9) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | Substantially modified and optimized for God's Eye View, including geometry/material simplification and coordinate/orientation preparation. The former 24× runtime calibration is baked into the mesh; location, rotation, and scale are applied; the bounding-box centre is at the origin; and the model uses glTF +Y-up with its nose toward local −X. Geometry remains uncompressed so the aircraft asset does not compete with photogrammetry for Draco worker capacity. | | `jet.glb` | “Private Jet” by [Nick the Name](https://sketchfab.com/Nick_The_Name) | [Sketchfab model](https://sketchfab.com/3d-models/private-jet-cbdd1de6ced9461e950eafaa302cc82b) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | Repackaged as a glTF binary for the project's military-flight visualization. Imported hierarchy transforms are baked into the meshes; location, rotation, and scale are applied; the bounding-box centre is at the origin; and the model uses meter scale, glTF +Y-up, and the shared nose −X convention. Existing materials are preserved. | -| `ship.glb` | “Low Poly Cargo Ship” by [Javier_Fernandez](https://sketchfab.com/Javier.Fernandez) | [Sketchfab model](https://sketchfab.com/3d-models/low-poly-cargo-ship-4c22cbaf01c1427f8ab60b3a07b1b32c) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | Optimized and repackaged as a glTF binary for the project's vessel visualization. | | `bell206.glb` | “Bell 206 JetRanger” by [terran4627](https://sketchfab.com/terran4627) | [Sketchfab model](https://sketchfab.com/3d-models/bell-206-jetranger-d2f7ba1d671549d4b26aaf834139a1dd) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | Optimized for God's Eye View: geometry/material simplification, textures resized to 256 px WebP, orientation/scale vertex-baked to real-world meters (Y-up, nose −X), origin centered. | | `c172.glb` | “Cessna 172” by [e737](https://sketchfab.com/e0057537) | [Sketchfab model](https://sketchfab.com/3d-models/cessna-172-64cddaee5aff470682659a8c08525046) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | Optimized for God's Eye View: geometry/material simplification, textures resized to 256 px WebP, orientation/scale vertex-baked to real-world meters (Y-up, nose −X), origin centered. | | `citation2.glb` | “1990 Cessna Citation, Texture Detailed, Exterior” by [BlenderCommunityHead](https://sketchfab.com/aboodgoudagad) | [Sketchfab model](https://sketchfab.com/3d-models/1990-cessna-citation-texture-detailed-exterior-a78839624fe64900a8352cb23462350a) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | Optimized for God's Eye View: geometry/material simplification, textures resized to 256 px WebP, orientation/scale vertex-baked to real-world meters (Y-up, nose −X), origin centered. | diff --git a/public/models/ship.glb b/public/models/ship.glb deleted file mode 100644 index e52d13b2c..000000000 Binary files a/public/models/ship.glb and /dev/null differ diff --git a/scripts/check-import-directions.mjs b/scripts/check-import-directions.mjs index ac1a3f732..b6d8dba67 100644 --- a/scripts/check-import-directions.mjs +++ b/scripts/check-import-directions.mjs @@ -24,11 +24,10 @@ const renderer = (file) => /^src\/layers\/[^/]+\/(?:index|rendering|snapshotRenderer|overlay|presentation|cards|controls)\.js$/.test( file, ); -// Two compatibility composition entries intentionally select standalone defaults. +// One compatibility composition entry intentionally selects standalone defaults. const compatibilityEdge = (from, to) => - (from === 'server/providers/local.js' && - to === 'server/standalone/key-setup.js') || - (from === 'src/ui.js' && to === 'src/standalone/catalog.js'); + from === 'server/providers/local.js' && + to === 'server/standalone/key-setup.js'; const portableExport = (key) => key.startsWith('./sources/') || /\/source$/.test(key) || @@ -154,14 +153,6 @@ export function checkImportDirections(root) { file, 'Actions must consume feed state without the manager facade', ); - if ( - file === 'src/app/constructCatalog.js' && - to === 'src/data/localGeojson.js' - ) - report( - file, - 'Catalog must use the services owner without compatibility layer construction', - ); } return record; } diff --git a/scripts/check-package-boundaries.mjs b/scripts/check-package-boundaries.mjs index 28627e4ac..700dc9a1a 100644 --- a/scripts/check-package-boundaries.mjs +++ b/scripts/check-package-boundaries.mjs @@ -1,6 +1,8 @@ import { readFile, realpath } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; +import vue from '@vitejs/plugin-vue'; +import { VUE_PLUGIN_OPTIONS } from '../build/vueOptions.js'; import { build, normalizePath } from 'vite'; /** Build each browser or Node export group and reject imports outside its declared ownership. */ @@ -99,15 +101,20 @@ export async function checkPackageBoundaries(root) { ssr: node ? { noExternal: true } : undefined, logLevel: 'silent', plugins: [ + vue(VUE_PLUGIN_OPTIONS), { name: 'check-package-ownership', moduleParsed(info) { - if (!allowed.has(info.id)) { + // Plugin helpers are virtual; a single-file component's style and + // template blocks belong to the component file itself. + if (info.id.startsWith('\0')) return; + const id = info.id.split('?')[0]; + if (!allowed.has(id)) { throw new Error( - `Package boundary ${name} imports an unowned module: ${path.relative(root, info.id)}`, + `Package boundary ${name} imports an unowned module: ${path.relative(root, id)}`, ); } - seen.add(info.id); + seen.add(id); }, }, ], diff --git a/scripts/docker-start.mjs b/scripts/docker-start.mjs index 3935f2b3b..c77d6335f 100644 --- a/scripts/docker-start.mjs +++ b/scripts/docker-start.mjs @@ -48,7 +48,8 @@ export function defaultGatewayFromRouteTable(text) { /** * Decide this launch's trusted-peer list. An operator value always wins, so a - * compose override stays authoritative; otherwise the gateway alone is + * compose override stays authoritative; `none` trusts no additional peers. + * An unset or blank value means the gateway alone is * trusted. No gateway trusts nothing and Provider Settings stays loopback-only, * exactly as under `npm run dev`. * @param {{env?: NodeJS.ProcessEnv, routeTable?: string}} input @@ -56,6 +57,7 @@ export function defaultGatewayFromRouteTable(text) { */ export function resolveTrustedPeers({ env = process.env, routeTable = '' } = {}) { const explicit = String(env.GEV_KEY_SETUP_TRUSTED_PEERS ?? '').trim(); + if (explicit.toLowerCase() === 'none') return ''; if (explicit) return explicit; return defaultGatewayFromRouteTable(routeTable) ?? ''; } @@ -109,15 +111,17 @@ async function start() { String(process.env.GEV_KEY_SETUP_TRUSTED_PEERS ?? '').trim(), ); const trustedPeers = resolveTrustedPeers({ routeTable: readRouteTable() }); + // Set even an empty list before Vite loads .env: the resolved launcher policy + // must remain authoritative when there is no trusted gateway. + process.env.GEV_KEY_SETUP_TRUSTED_PEERS = trustedPeers; if (trustedPeers) { - // Set before Vite is imported: the dev server snapshots process.env during - // configuration, and the admission gate reads it per request. - process.env.GEV_KEY_SETUP_TRUSTED_PEERS = trustedPeers; console.log( fromOperator ? `[Docker] Provider Settings trusted peers: ${trustedPeers} (GEV_KEY_SETUP_TRUSTED_PEERS).` : `[Docker] Provider Settings trusts the container gateway (${trustedPeers}).`, ); + } else if (fromOperator) { + console.log('[Docker] Provider Settings trusts no additional peers (GEV_KEY_SETUP_TRUSTED_PEERS=none).'); } else { console.log('[Docker] No default gateway; Provider Settings stays loopback-only.'); } diff --git a/scripts/format-scope.json b/scripts/format-scope.json index bfab00fde..11134114f 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -6,6 +6,7 @@ "scripts/qa-form-fields.mjs", "scripts/package-boundaries.json", "scripts/format.mjs", + "scripts/qa-layer-visibility.mjs", "scripts/next-layer-state-token.mjs", "scripts/check-layer-state-tokens.mjs", "scripts/qa-layer-token-twochar.mjs", @@ -67,6 +68,7 @@ "server/standalone/vite.config.js", "vite.config.js", "src/tooling/viteBuild.test.mjs", + "src/tooling/vueLoader.test.mjs", "src/tooling/allowedHosts.test.mjs", "server/providers/common/http.js", "server/providers/common/query.js", @@ -179,6 +181,8 @@ "src/ui/panelDisclosure.js", "src/ui/imagerySplit.js", "src/ui/imagerySplit.test.mjs", + "src/ui/imagerySplitView.test.mjs", + "src/ui/stores/imagerySplit.js", "src/ui/surfaceKeyboard.js", "src/ui/surfaceKeyboard.test.mjs", "src/ui/panelRails.js", @@ -193,11 +197,10 @@ "src/ui/visualInput.js", "src/ui/visualInput.test.mjs", "scripts/qa-visual-input.mjs", + "scripts/qa-scope-hud.mjs", "scripts/qa-segmented-controls.mjs", "src/ui/displayControls.js", - "src/ui/segmentedControl.test.mjs", "src/ui/styles/segmented-control.css", - "src/ui/displayControls.test.mjs", "src/ui/cameraOrientationControls.js", "src/ui/cameraOrientationControls.test.mjs", "src/ui/effects.js", @@ -210,7 +213,6 @@ "src/styles/thermal.js", "src/styles/anime.js", "src/styles/noir.js", - "src/styles/snow.js", "scripts/qa-visual-effects.mjs", "scripts/qa-draw-tool.mjs", "scripts/qa-geo-import.mjs", @@ -285,8 +287,6 @@ "src/ui/sceneControls.test.mjs", "scripts/qa-scene-controls.mjs", "scripts/qa-action-buttons.mjs", - "src/ui.js", - "src/standalone/ui.js", "src/ui/applicationShell.js", "src/ui/panelLayoutController.js", "src/ui/panelPositionControls.js", @@ -298,6 +298,8 @@ "src/ui/styles.js", "src/ui/shellLifecycle.test.mjs", "src/testSupport/readStylesheet.mjs", + "src/testSupport/voiceUi.mjs", + "src/testSupport/voiceView.mjs", "src/testSupport/streetLevelFakes.mjs", "src/splitFlap.js", "style.css", @@ -519,7 +521,6 @@ "src/layers/bikeshare/selection.js", "src/data/traffic.js", "src/data/bikeshare.js", - "src/data/flowTiles.js", "src/layers/traffic/source.test.mjs", "src/layers/bikeshare/source.test.mjs", "src/layers/bikeshare/proxyRoute.test.mjs", @@ -528,6 +529,7 @@ "scripts/qa-traffic-navigation.mjs", "src/ui/frameRateMonitor.js", "src/ui/frameRateMonitor.test.mjs", + "src/ui/frameRateView.test.mjs", "src/layers/cctv/lifecycle.js", "src/layers/cctv/controls.js", "src/layers/cctv/geometry.js", @@ -563,6 +565,7 @@ "src/data/cctvCataloniaSource.test.mjs", "src/data/cctvHk.test.mjs", "src/data/cctvVegvesen.test.mjs", + "src/data/cctvA22.test.mjs", "src/data/cctvNyc.test.mjs", "src/data/cctvNswSource.test.mjs", "src/data/cctvTxdotSource.test.mjs", @@ -651,8 +654,6 @@ "src/voice/realtimeBackend.test.mjs", "src/agent/worldPacket.test.mjs", "src/voice/layerSummary.js", - "build/application-html.js", - "src/app/sourceSlot.js", "src/app/sources.js", "src/ui/composition.js", "src/services/application.js", @@ -705,7 +706,6 @@ "src/ui/shareRestoration.js", "src/ui/visualSettings.js", "src/ui/panelChrome.js", - "src/ui/panelChrome.radioOwnership.test.mjs", "src/testSupport/readShellSource.mjs", "src/ui/stateOwners.test.mjs", "src/layers/flights/records.js", @@ -743,6 +743,8 @@ "server/providers/openai/toolDescriptions.js", "src/voice/session.js", "src/voice/sessionCommands.js", + "src/ui/stores/pointerReticle.js", + "src/voice/pointerReticleView.test.mjs", "src/voice/realtimeSession.js", "src/voice/realtimeEvents.js", "src/voice/session.test.mjs", @@ -874,6 +876,18 @@ "src/layers/gnss/source.test.mjs", "src/layers/gnss/ownership.test.mjs", "scripts/qa-gnss.mjs", + "server/providers/ghostMaps.js", + "server/providers/ghostMaps.test.mjs", + "src/app/layers/ghostMaps.js", + "src/layers/ghostMaps/index.js", + "src/layers/ghostMaps/catalog.js", + "src/layers/ghostMaps/kml.js", + "src/layers/ghostMaps/records.js", + "src/layers/ghostMaps/source.js", + "src/layers/ghostMaps/catalog.test.mjs", + "src/layers/ghostMaps/records.test.mjs", + "src/layers/ghostMaps/lifecycle.test.mjs", + "scripts/qa-ghostmaps.mjs", "src/layers/cyclones/index.js", "src/layers/cyclones/rendering.js", "src/layers/cyclones/labels.js", @@ -888,6 +902,7 @@ "scripts/qa-weather-swap.mjs", "scripts/qa-weather-teardown.mjs", "src/layers/weather/imageryHost.test.mjs", + "src/layers/weather/display.test.mjs", "src/sdr/adsbDecoder.test.mjs", "src/sdr/controller.test.mjs", "src/sdr/decoderDiagnostics.test.mjs", @@ -900,6 +915,8 @@ "src/sources/adsbRecords.test.mjs", "src/layers/localAdsb/index.test.mjs", "src/ui/localSdrControls.test.mjs", + "src/ui/sdrCardActions.test.mjs", + "src/ui/radioPanelActions.test.mjs", "src/ui/localSdrMarkup.test.mjs", "src/ui/localSdrPresentation.test.mjs", "src/data/localReceiversProxy.test.mjs", @@ -1085,7 +1102,87 @@ "server/providers/overpass/requestAdmission.test.mjs", "src/tooling/trackArrivalObservation.test.mjs", "src/ui/styles/status-badge.css", - "src/ui/statusBadge.test.mjs", "src/ui/cctvBadges.test.mjs", - "scripts/qa-status-badges.mjs" + "scripts/qa-status-badges.mjs", + "src/dockerTrustedPeers.test.mjs", + "src/cameraGlobeRotation.test.mjs", + "src/voice/globeResetConfirmation.test.mjs", + "src/hudDms.test.mjs", + "src/hudSummaryGate.test.mjs", + "src/voice/hudPlaceLabels.test.mjs", + "src/voice/panel/preferences.test.mjs", + "src/voice/panel/preferencesView.test.mjs", + "src/voice/voiceControlView.test.mjs", + "src/voice/panel/host.test.mjs", + "src/voice/panel/manualPanel.test.mjs", + "src/voice/panel/nativeActivity.test.mjs", + "src/voice/panel/inactivity.test.mjs", + "src/voice/panel/actionObserver.test.mjs", + "src/voice/realtimeCostPanel.test.mjs", + "docs/SHARE-LINK-FORMAT.md", + "src/share/wire.test.mjs", + "src/share/viewCodec.test.mjs", + "src/share/legacyShareState.test.mjs", + "src/testSupport/shareViewFixtures.mjs", + "src/ui/shareLinks.test.mjs", + "src/ui/shareButton.test.mjs", + "src/share/shareLinkPath.test.mjs", + "src/ui/kit/UiRail.test.mjs", + "src/ui/kit/UiSegmented.vue", + "src/ui/kit/UiSegmented.test.mjs", + "src/ui/kit/UiSlider.js", + "src/ui/kit/UiSlider.test.mjs", + "src/ui/kit/UiButton.vue", + "src/ui/kit/UiButton.test.mjs", + "src/ui/kit/UiChip.vue", + "src/ui/kit/UiChip.test.mjs", + "src/ui/globeActions.test.mjs", + "src/ui/safeFrameView.test.mjs", + "src/ui/kit/UiSelect.js", + "src/ui/kit/UiSelect.test.mjs", + "src/ui/kit/UiBadge.vue", + "src/ui/kit/UiBadge.test.mjs", + "src/ui/kit/UiEmpty.vue", + "src/ui/rail/RailChips.vue", + "src/ui/displayPanelActions.test.mjs", + "src/testSupport/vueDom.mjs", + "build/vueLoader.js", + "build/registerVue.js", + "src/ui/panelNavigation.test.mjs", + "src/ui/stores/owners.js", + "src/ui/stores/owners.test.mjs", + "src/ui/stores/panels.test.mjs", + "src/ui/stores/status.js", + "src/ui/statusChipsView.test.mjs", + "src/ui/statusChipOwners.test.mjs", + "src/ui/toast.js", + "src/ui/toastView.test.mjs", + "src/ui/stores/sceneInteractions.js", + "src/scenes/dataPacks/presentationView.test.mjs", + "src/ui/stores/dataPacks.js", + "src/scenes/interactionsView.test.mjs", + "src/layers/wind/inspectionMarker.js", + "src/layers/wind/inspectionMarker.test.mjs", + "src/ui/stores/windInspection.js", + "src/ui/stores/missionReplay.js", + "src/layers/launches/replayOverlay.test.mjs", + "src/ui/stores/bhoteKoshiPanel.js", + "src/data/bhoteKoshiPanelView.test.mjs", + "src/ui/stores/celestialRing.js", + "src/celestialRingView.test.mjs", + "src/ui/stores/screenAnnotations.js", + "src/ui/stores/worldOverlayActions.js", + "src/overlays/worldOverlayActionsView.test.mjs", + "src/ui/stores/awarenessDirection.js", + "src/layers/awareness/directionOverlay.test.mjs", + "src/hudReadouts.js", + "src/ui/stores/hud.js", + "src/ui/views/intelHudView.test.mjs", + "src/testSupport/hudReadouts.mjs", + "src/ui/stores/firstRun.js", + "src/firstRunLauncherView.test.mjs", + "src/firstRunCopy.js", + "src/data/headingDegrees.test.mjs", + "src/data/distanceFormat.test.mjs", + "src/sources/adsbFields.test.mjs" ] diff --git a/scripts/format.mjs b/scripts/format.mjs index 25311ba29..e91cf527b 100644 --- a/scripts/format.mjs +++ b/scripts/format.mjs @@ -39,7 +39,7 @@ export async function discoverRuntimeFormatFiles(root) { const files = []; for (const name of new Set(candidates)) { if ( - !/\.[cm]?js$/.test(name) || + !/\.([cm]?js|vue)$/.test(name) || /\.test\.[cm]?js$/.test(name) || !existsSync(path.join(root, name)) ) diff --git a/scripts/i18n.mjs b/scripts/i18n.mjs index 0ee5d60ec..c424666a2 100644 --- a/scripts/i18n.mjs +++ b/scripts/i18n.mjs @@ -8,7 +8,7 @@ // lists it. // // Messages are the English literals passed to t(), tn() and msg() in src/, -// plus the markup opted in with data-i18n* in src/ui/templates and index.html. +// plus the markup opted in with data-i18n* in index.html and the Vue views. // It also lists UI text that is not marked at all (see findUnmarked). import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { execFileSync } from 'node:child_process'; @@ -119,14 +119,17 @@ export function extractMessages() { } } - const markup = trackedFiles('index.html', 'src/ui/templates/*.html'); + const markup = markupFiles(); const TAG = /<([a-zA-Z][\w-]*)(\s[^>]*?)\/?>([^<]*)/gs; for (const file of markup) { - const html = readFileSync(path.join(ROOT, file), 'utf8'); + const html = readMarkup(file); for (const [, tag, attrs, text] of html.matchAll(TAG)) { const where = `${file} <${tag}>`; - if (/\sdata-i18n(\s|=|$)/.test(attrs)) - add(normalize(decodeHtml(text)), where); + if (/\sdata-i18n(\s|=|$)/.test(attrs)) { + const context = attrs.match(/\sdata-i18n-context="([^"]*)"/); + const message = normalize(decodeHtml(text)); + add(context ? `${decodeHtml(context[1])}|${message}` : message, where); + } for (const [, name] of attrs.matchAll( /\sdata-i18n-(title|aria-label|placeholder)\b/g, )) { @@ -153,7 +156,7 @@ const SKIP_TAGS = new Set(['script', 'style', 'svg', 'code', 'pre', 'title']); /** * User-visible markup text without data-i18n*. Skipped, with everything * inside: translate="no" (text that stays as is, as in HTML), - * data-i18n-runtime (code sets this text through t()), and icons + * data-i18n-runtime (page markup whose text code sets through t()), and icons * (aria-hidden, Material Symbols). */ export function unmarkedMarkup(file, html) { @@ -401,9 +404,12 @@ export function unmarkedScript(file, original) { */ export function findUnmarked() { const found = []; - for (const file of trackedFiles('index.html', 'src/ui/templates/*.html')) + for (const file of markupFiles()) { + found.push(...unmarkedMarkup(file, readMarkup(file))); + } + for (const file of trackedFiles('src/**/*.vue')) found.push( - ...unmarkedMarkup(file, readFileSync(path.join(ROOT, file), 'utf8')), + ...markedTemplate(file, readFileSync(path.join(ROOT, file), 'utf8')), ); for (const file of sourceFiles().filter( (name) => !NOT_UI.some((pattern) => pattern.test(name)), @@ -420,6 +426,57 @@ export function findUnmarked() { }); } +/** + * data-i18n* in a Vue template: views translate with t() as they render, so + * their text follows the locale; the attributes are for static page markup. + * Reads the whole single-file component, so lines are the file's own. + */ +export function markedTemplate(file, source) { + const start = source.indexOf(''); + // Comments and attribute values are blanked, keeping every offset. + const blank = (text) => text.replace(/[^\n]/g, ' '); + const template = source + .slice(0, end === -1 ? undefined : end) + .replace(//g, blank); + const found = []; + const TAG = /<[a-zA-Z][\w-]*((?:"[^"]*"|'[^']*'|[^>"'])*)>/g; + TAG.lastIndex = start + '