From 0da4d390eaf5b54cac0efeb269879d92e6a562bd Mon Sep 17 00:00:00 2001 From: daikaginza Date: Sat, 26 Sep 2026 17:36:59 +0700 Subject: [PATCH 01/15] docs: add the realtime backend to the voice ownership table The ownership table lists every owner except the backend, though realtimeBackend.js owns the two requests, their separate transports, the abort/timeout scope around both, and client-secret expiry. The method signatures are already documented with application construction, so link there rather than repeat them, and state only the ownership facts written nowhere: the connection is the backend's only consumer in the application, the resolved credential is opaque to it and is handed to negotiate whole, and client-secret expiry is refused inside the backend. --- docs/VOICE-OWNERSHIP.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/VOICE-OWNERSHIP.md b/docs/VOICE-OWNERSHIP.md index d5553df00..be7a6c26b 100644 --- a/docs/VOICE-OWNERSHIP.md +++ b/docs/VOICE-OWNERSHIP.md @@ -8,6 +8,7 @@ The session adapter, action runner and backend interfaces remain unchanged. | Owner | State and responsibility | | --- | --- | | `realtimeConnection.js` | Start generation, request cancellation, peer/data channel, acquired microphone stream and playback element | +| `realtimeBackend.js` | Token and SDP requests, their separate transports, the abort/timeout scope around both, and client-secret expiry | | `realtimeTurns.js` | Active and superseded responses, pending confirmations, deduplicated calls and action cancellation | | `realtimeRadio.js` | Speaker ducking, prepared playback, stronger-action reservations and playback observers | | `realtimeInput.js` | Physical Space gesture, microphone mute, input/output meters and their frame/listener lifetime | @@ -15,6 +16,15 @@ The session adapter, action runner and backend interfaces remain unchanged. | `realtimeViewport.js` | One retained image, bounded deletion identities and capture generation | | `realtimeDiagnostics.js` | Bounded error history and sanitized optional diagnostics | +The backend is replaceable per deployment: `realtimeController.js` accepts it as a +`backend` option and falls back to `createRealtimeBackend()`. Its two method +signatures live with [application construction](APPLICATION.md); what belongs here +is the ownership. The connection is the backend's only consumer in the application. +The resolved credential is opaque to the connection and is handed to `negotiate` +whole. Client-secret expiry is refused inside the backend, not by the connection. +`realtimeOwners.test.mjs` drives whole sessions through a substitute that +implements only those two methods. + Pure input policy, preferences and protocol-response policy have separate modules. Owners receive named readers, operations or focused collaborators. Cross-owner cancellation uses operations; response handling does not own Radio's reservations From 993b1bcdb1d7871a1f39b7cabc7c16387d93dd5d Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Sun, 27 Sep 2026 22:29:19 -0500 Subject: [PATCH 02/15] docs: add Pinokio browser performance workaround --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 88690d52f..0ff6c32e1 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,14 @@ locked dependencies, finds a free local port, and opens the app. 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 +compatibility or minimum-hardware problem, copy the local server URL shown by +Pinokio and open the same URL in your normal hardware-accelerated browser. A +Pinokio 8.2.0 Linux report reproduced severe stutter in Pinokio's embedded view +while the same running server was smooth in an external browser ([#365](https://github.com/bilawalsidhu/gods-eye-view/issues/365)). +That isolates the browser/compositor path from the app server and your machine; +it does not establish a minimum GPU requirement. + ### Path 2 — Terminal / coding agent Use **Node.js 24.x (24.14.0 or later) or 26.x**. The setup doctor warns about From 4ab530b8a83c6f56b34612c1ea5c9bc07c34e3ea Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Sun, 27 Sep 2026 22:29:21 -0500 Subject: [PATCH 03/15] docs: distinguish embedded browser performance --- docs/PERFORMANCE.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index 34b207f79..873ef8768 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -128,6 +128,12 @@ Use the same controls before attributing a difference to the application: 6. Record live object counts before attributing a difference to the client. 7. Treat a live-source outage as missing coverage, not as evidence of low client rendering cost. +8. Record whether the app is running in Pinokio's embedded view or in an + external browser. The same Pinokio 8.2.0 Linux server was reported as + severely stuttery in the embedded view and smooth when its local URL was + opened directly in a normal browser ([#365](https://github.com/bilawalsidhu/gods-eye-view/issues/365)). + Treat that as a browser/compositor-path difference until the GPU renderer + proves otherwise. ## What is not established yet From c87f007e34a627bc9554c4bfe8070267f9c5e0b5 Mon Sep 17 00:00:00 2001 From: daikaginza Date: Mon, 28 Sep 2026 11:39:19 +0700 Subject: [PATCH 04/15] fix(build): discover unit tests under server/, not only src/ npm test is the only suite CI runs, and its discovery walked src/ alone, so server/providers/transitHistory.test.mjs has never executed since it was added. Its 8 tests pass, so this was silent coverage loss rather than a hidden failure: the file reads like covered code in review and never ran anywhere. Add server/ to the searched roots, and pin the property with a test that walks the repository itself and compares the two sets, so a tree added later without updating the roots fails loudly instead of going quiet. Suite goes from 5090 to 5099 tests, 0 failures: the 8 tests in that file plus the regression test added here. --- scripts/run-unit-tests.mjs | 14 ++++++++-- src/unitTestRunner.test.mjs | 51 ++++++++++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/scripts/run-unit-tests.mjs b/scripts/run-unit-tests.mjs index 9bd7e70e0..7024ea38c 100644 --- a/scripts/run-unit-tests.mjs +++ b/scripts/run-unit-tests.mjs @@ -21,9 +21,19 @@ export function assertNode24AllocationRuntime(version = process.versions.node) { return version; } +/** + * Directories this runner searches for `*.test.mjs`, in stable order. + * + * `server/` belongs here because provider modules own tests of their own: a + * suite placed beside the module it covers must run, and `npm test` is the only + * thing CI invokes, so a directory missing from this list is a suite that never + * runs anywhere. `unitTestRunner.test.mjs` asserts the list covers every test + * file in the repository, so adding a tree without adding it here fails. + */ +export const UNIT_TEST_ROOTS = Object.freeze(['src', 'server']); + /** Discover repository unit tests in stable path order. */ export function discoverUnitTestFiles(root = process.cwd()) { - const sourceRoot = path.join(root, 'src'); const files = []; const visit = (directory) => { for (const entry of readdirSync(directory, { withFileTypes: true })) { @@ -34,7 +44,7 @@ export function discoverUnitTestFiles(root = process.cwd()) { } } }; - visit(sourceRoot); + for (const testRoot of UNIT_TEST_ROOTS) visit(path.join(root, testRoot)); return files.sort(); } diff --git a/src/unitTestRunner.test.mjs b/src/unitTestRunner.test.mjs index 002dcb796..201f6ed8e 100644 --- a/src/unitTestRunner.test.mjs +++ b/src/unitTestRunner.test.mjs @@ -1,14 +1,40 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import { readFileSync } from 'node:fs'; +import { readFileSync, readdirSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; import { ALLOCATION_TEST_FILES, + UNIT_TEST_ROOTS, allocationTestArgs, assertNode24AllocationRuntime, buildUnitTestPlan, + discoverUnitTestFiles, isCalibratedAllocationRuntime, } from '../scripts/run-unit-tests.mjs'; +const repositoryRoot = fileURLToPath(new URL('..', import.meta.url)); + +/** Every `*.test.mjs` a contributor could add, found without consulting the runner. */ +function testFilesOnDisk(root) { + // Build outputs and dependencies are not places a contributor writes a test, + // and walking them would be slow enough to matter. Everything else counts. + const skip = new Set(['node_modules', 'dist', 'build', 'public']); + const found = []; + const visit = (directory) => { + for (const entry of readdirSync(directory, { withFileTypes: true })) { + if (entry.name.startsWith('.') || skip.has(entry.name)) continue; + const absolute = path.join(directory, entry.name); + if (entry.isDirectory()) visit(absolute); + else if (entry.isFile() && entry.name.endsWith('.test.mjs')) { + found.push(path.relative(root, absolute).split(path.sep).join('/')); + } + } + }; + visit(root); + return found.sort(); +} + test('unit runner serializes only GC-bracketed allocation microbenchmarks', () => { const ordinary = [ 'src/data/manager.test.mjs', @@ -37,6 +63,29 @@ test('unit runner serializes only GC-bracketed allocation microbenchmarks', () = ); }); +test('the runner discovers every test file in the repository', () => { + // `npm test` is the only suite CI runs, and it runs exactly what this + // function returns. A `*.test.mjs` outside the searched roots therefore never + // executes anywhere, while still reading — in review and to its author — like + // covered code. Compare the two sets rather than their sizes, so the failure + // names the files instead of only disagreeing about a count. + const discovered = discoverUnitTestFiles(repositoryRoot); + const onDisk = testFilesOnDisk(repositoryRoot); + const undiscovered = onDisk.filter((file) => !discovered.includes(file)); + assert.deepEqual( + undiscovered, + [], + `test files the runner never executes: ${undiscovered.join(', ')}`, + ); + assert.deepEqual(discovered, onDisk); + for (const file of discovered) { + assert.ok( + UNIT_TEST_ROOTS.some((root) => file.startsWith(`${root}/`)), + `${file} should sit under one of ${UNIT_TEST_ROOTS.join(', ')}`, + ); + } +}); + test('allocation runtime calibration is explicit and pinned to Node 24', () => { assert.equal(assertNode24AllocationRuntime('24.19.0'), '24.19.0'); assert.throws( From f93755af488585bd745f75c7baa43bebbed5dc3e Mon Sep 17 00:00:00 2001 From: Bruno Gomes Date: Tue, 6 Oct 2026 16:13:45 -0300 Subject: [PATCH 05/15] fix(cctv): start live HLS on plain-HTTP LAN pages The client lease id came from crypto.randomUUID, which browsers only expose in secure contexts. Opened over http://:4173 (the HOST=0.0.0.0 opt-in), starting a live camera feed threw before playback. Fall back to a v4 UUID from crypto.getRandomValues, which the server's lease check accepts. --- CHANGELOG.md | 6 ++++ docs/CURRENT-STATE.md | 4 ++- src/layers/cctv/videoPlayback.js | 28 +++++++++++++++++- src/layers/cctv/videoPlayback.test.mjs | 39 +++++++++++++++++++++++++- 4 files changed, 74 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b433f8c06..9fc367d84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,12 @@ largest cities) and `CCTV_VEGVESEN_VIDEO=0` keeps stills only. Attributed under NLOD 2.0. +- Live HLS cameras play on a page opened over plain HTTP from another machine + (`HOST=0.0.0.0`). The client lease id came from `crypto.randomUUID`, which + browsers only expose in secure contexts, so starting a live feed threw + before playback. The id now falls back to a v4 UUID built from + `crypto.getRandomValues`, which the server's lease check accepts. + ## [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, diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index d4fe022a8..fde1373af 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -4814,7 +4814,9 @@ its paint loop. Camera deactivation destroys the decoder, and fatal playback errors revert to the labeled frame fallback. Camera changes clear the old panel frame. hls.js is imported only when an HLS camera starts. Two live sessions may coexist with at most eight independent consumer leases each; no-access lease -expiry is 15 seconds. Closing one consumer leaves other consumers running. Encrypted/fMP4/byte-range playlists fail +expiry is 15 seconds. Closing one consumer leaves other consumers running. Lease +ids use `crypto.randomUUID` where the page is a secure context and an equivalent +v4 id from `crypto.getRandomValues` on plain-HTTP LAN pages. Encrypted/fMP4/byte-range playlists fail closed. Performance and live agency reliability require recorded validation. ## Wind forecast models diff --git a/src/layers/cctv/videoPlayback.js b/src/layers/cctv/videoPlayback.js index d59143b5a..48988c17c 100644 --- a/src/layers/cctv/videoPlayback.js +++ b/src/layers/cctv/videoPlayback.js @@ -1,3 +1,28 @@ +/** + * A v4 UUID for the client lease. `crypto.randomUUID` only exists in secure + * contexts, so a page opened over plain HTTP from another machine (the + * HOST=0.0.0.0 LAN opt-in) has none; `getRandomValues` exists in every context + * and yields the same shape the server's lease check accepts. + */ +export function createLeaseId(cryptoImpl = globalThis.crypto) { + if (typeof cryptoImpl.randomUUID === 'function') { + return cryptoImpl.randomUUID(); + } + const bytes = cryptoImpl.getRandomValues(new Uint8Array(16)); + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')); + return [ + hex.slice(0, 4), + hex.slice(4, 6), + hex.slice(6, 8), + hex.slice(8, 10), + hex.slice(10), + ] + .map((group) => group.join('')) + .join('-'); +} + /** One decoder per active camera; both surfaces consume this video element. */ export function attachCctvVideo( video, @@ -7,13 +32,14 @@ export function attachCctvVideo( loadHls = () => import('hls.js'), onFailure = () => {}, fetchImpl = globalThis.fetch, + cryptoImpl = globalThis.crypto, } = {}, ) { // Preserve replay for existing finite video feeds; live HLS must not loop. video.loop = feedType !== 'hls'; let disposed = false; let hls = null; - const leaseId = feedType === 'hls' ? globalThis.crypto.randomUUID() : null; + const leaseId = feedType === 'hls' ? createLeaseId(cryptoImpl) : null; const mediaUrl = feedType === 'hls' ? `${url}${url.includes('?') ? '&' : '?'}lease=${leaseId}` diff --git a/src/layers/cctv/videoPlayback.test.mjs b/src/layers/cctv/videoPlayback.test.mjs index 1f8c1c0bb..d1ef02539 100644 --- a/src/layers/cctv/videoPlayback.test.mjs +++ b/src/layers/cctv/videoPlayback.test.mjs @@ -1,6 +1,15 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import { attachCctvVideo } from './videoPlayback.js'; +import { webcrypto } from 'node:crypto'; +import { attachCctvVideo, createLeaseId } from './videoPlayback.js'; + +const LEASE_V4 = + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; + +/** What an insecure context exposes: getRandomValues but no randomUUID. */ +const insecureCrypto = { + getRandomValues: (array) => webcrypto.getRandomValues(array), +}; function video() { const v = new EventTarget(); Object.assign(v, { @@ -69,6 +78,34 @@ test('native HLS releases its client lease without response-header access', asyn assert.equal(releases[0].init.method, 'DELETE'); }); +test('lease ids are v4 UUIDs with or without crypto.randomUUID', () => { + assert.match(createLeaseId(), LEASE_V4); + const ids = new Set(); + for (let i = 0; i < 64; i++) ids.add(createLeaseId(insecureCrypto)); + assert.equal(ids.size, 64); + for (const id of ids) assert.match(id, LEASE_V4); +}); + +test('HLS starts on a plain-HTTP LAN page without crypto.randomUUID', async () => { + const source = video(); + source.canPlayType = () => 'probably'; + const playback = attachCctvVideo(source, '/api/cctv/media/a', 'hls', { + loadHls: async () => ({ default: { isSupported: () => false } }), + fetchImpl: async () => {}, + cryptoImpl: insecureCrypto, + }); + try { + await playback.ready; + const lease = new URL( + source.src, + 'http://192.168.1.10:4173/', + ).searchParams.get('lease'); + assert.match(lease, LEASE_V4); + } finally { + playback.dispose(); + } +}); + test('finite video feeds retain looping while live HLS does not loop', async () => { for (const feedType of ['mp4', 'webm', 'hls']) { const source = video(); From 6cc05a7fcbf89a6a581e8a94cd5b42b87baad8be Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:14:22 -0500 Subject: [PATCH 06/15] fix(ui): recut #918 src/ui/panelRails.test.mjs on current main --- src/ui/panelRails.test.mjs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/ui/panelRails.test.mjs b/src/ui/panelRails.test.mjs index 016de58fa..de5b3997b 100644 --- a/src/ui/panelRails.test.mjs +++ b/src/ui/panelRails.test.mjs @@ -704,6 +704,25 @@ test('narrow-screen rails pin every hosted panel glow inside its panel box', () } }); +test('right layout ignores a CSS-hidden expanded panel when deciding exclusivity', () => { + const f = fixture('right'); + const weather = element('weather-panel', { height: 300 }); + weather.computed.display = 'none'; + f.stack.children.push(weather); + weather.parentElement = f.stack; + + f.run(); + + assert.equal(f.stack.classList.contains('layout-exclusive'), false); + assert.equal(f.stack.dataset.expandedCount, '0'); + assert.equal(f.first.getAttribute('aria-hidden'), undefined); + assert.equal(f.second.getAttribute('aria-hidden'), undefined); + assert.equal( + weather.style.getPropertyValue('--right-panel-allocated-height'), + '', + ); +}); + test('right layout ignores a hidden panel: no lane, no gap, no auto-collapse', () => { const f = fixture('right'); const imagery = element('recent-imagery-panel', { height: 0 }); From 21a90b41442af3a1f7c56d86c95c376a174e92dd Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:14:25 -0500 Subject: [PATCH 07/15] fix(ui): recut #918 src/ui/rightPanelRail.js on current main --- src/ui/rightPanelRail.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ui/rightPanelRail.js b/src/ui/rightPanelRail.js index 3b85bb896..b62be511c 100644 --- a/src/ui/rightPanelRail.js +++ b/src/ui/rightPanelRail.js @@ -72,7 +72,9 @@ export function layoutRightPanelRail({ // A floating (lifted-out) panel is not laid out by the rail. const panels = [...stack.children].filter( (panel) => - panel.matches('[data-panel-id]:not(.panel-floating)') && !panel.hidden, + panel.matches('[data-panel-id]:not(.panel-floating)') && + !panel.hidden && + getComputedStyle(panel).display !== 'none', ); if (!hud.visible || hud.variant !== 'tactical') { for (const panel of panels.filter((item) => From fa53eec53dd81622e68244d47c29471565e3d349 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Sun, 11 Oct 2026 09:33:25 -0400 Subject: [PATCH 08/15] fix(search): refuse empty coordinate components instead of silently dropping them --- CHANGELOG.md | 2 ++ docs/CURRENT-STATE.md | 5 +++++ src/search/coordinate-empty-components.test.mjs | 12 ++++++++++++ src/search/coordinateParser.js | 4 ++-- 4 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 src/search/coordinate-empty-components.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index b24ac95ed..598588ffa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +- Coordinate searches refuse empty latitude or longitude components instead of navigating to an unintended location. + - Treat explicitly current, complete empty vessel coverage as a successful update, clearing obsolete contacts while retaining stale or incomplete snapshots. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 26d56e66b..b490d308f 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -5127,3 +5127,8 @@ datacenters, dams and the Nepal locator retain that shared credit while displaye Military fragments stitch by source identity. Different mapped parcels merge only when their footprints touch or contain one another, never across a seam tolerance gap. + + +### fix(search): refuse empty coordinate components instead of silently dropping them + +Require exactly two nonempty components while preserving comma, semicolon and whitespace pairs. Malformed separator input falls through to geocoding. diff --git a/src/search/coordinate-empty-components.test.mjs b/src/search/coordinate-empty-components.test.mjs new file mode 100644 index 000000000..48bca54ed --- /dev/null +++ b/src/search/coordinate-empty-components.test.mjs @@ -0,0 +1,12 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { parseCoordinateQuery } from './coordinateParser.js'; +test('search refuses omitted components and repeated separators', () => { + for (const q of ['30,, -97', ',30,-97', '30,-97,', '30;; -97', '30,; -97']) + assert.equal(parseCoordinateQuery(q), null, q); + for (const q of ['30,-97', '30; -97', '30 -97', 'N 30, W 97']) + assert.deepEqual( + [parseCoordinateQuery(q).lat, parseCoordinateQuery(q).lon], + [30, -97], + ); +}); diff --git a/src/search/coordinateParser.js b/src/search/coordinateParser.js index 261e2394c..cb8c85524 100644 --- a/src/search/coordinateParser.js +++ b/src/search/coordinateParser.js @@ -49,8 +49,8 @@ function splitComponents(query) { const parts = /[,;]/.test(text) ? text.split(/[,;]/) : text.split(/\s+(?=[+-]?[.\d]|[NSEW]\s*[+-]?[.\d])/i); - const trimmed = parts.map((part) => part.trim()).filter(Boolean); - return trimmed.length === 2 ? trimmed : null; + const trimmed = parts.map((part) => part.trim()); + return trimmed.length === 2 && trimmed.every(Boolean) ? trimmed : null; } /** Present a parsed pair the way a coordinate is normally written. */ From 7bbf0aa55af84a6382283eff4640f2ddecde5f2e Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Mon, 12 Oct 2026 11:12:41 -0400 Subject: [PATCH 09/15] fix: address review feedback for PR #1022 --- docs/CURRENT-STATE.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index b490d308f..8e4f58af1 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -4143,6 +4143,8 @@ and unreachable upstream (502/504) separately from road geometry. ### Place-search providers +Coordinate input requires exactly two nonempty components; extra comma or semicolon separators decline to geocoding rather than silently choosing a location. + Location search/fly-to, annotations and Radio location lookup receive one `placeSearch.geocode(query, { bias, signal })` service. `src/standalone` composes two offline providers first — a strict decimal-degree coordinate reader and an @@ -5127,8 +5129,3 @@ datacenters, dams and the Nepal locator retain that shared credit while displaye Military fragments stitch by source identity. Different mapped parcels merge only when their footprints touch or contain one another, never across a seam tolerance gap. - - -### fix(search): refuse empty coordinate components instead of silently dropping them - -Require exactly two nonempty components while preserving comma, semicolon and whitespace pairs. Malformed separator input falls through to geocoding. From 6f0d1bb35f121ec69b64d136901e60416a339a05 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Wed, 14 Oct 2026 11:07:18 -0700 Subject: [PATCH 10/15] fix(ui): keep panels the right rail hides itself in its layout Exclusive presentation hides collapsed right-rail panels with display: none. Skipping every display: none panel also skipped those, so panels the rail had auto-collapsed stayed collapsed after the HUD was hidden or left Tactical. Only panels hidden by other CSS are skipped now. Adds the changelog and current-state notes. --- CHANGELOG.md | 4 ++++ docs/CURRENT-STATE.md | 3 +++ src/ui/panelRails.test.mjs | 26 ++++++++++++++++++++++++++ src/ui/rightPanelRail.js | 9 +++++++-- 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7acdedf6b..7c7ada1b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +- The right rail ignores panels that other CSS hides, such as during an event + view, so they no longer force one-panel-at-a-time layout or collapse visible + panels. Panels the rail collapses itself are still restored. + - Bing Aerial and Bing Labels request HTTPS tiles on HTTP localhost through their ion-backed providers, retaining ion and per-tile attribution without widening the Content Security Policy. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index c77d1d8e3..59ba91202 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1047,6 +1047,9 @@ Automatic collapse remains presentation only, prioritizes the latest explicit panel, and honors keyboard focus on the right. Mobile layout still releases desktop allocation styles. Stable Display allocation avoids unnecessary style writes. This extraction does not change panel positions or layout defaults. +The right rail skips panels hidden by other CSS (computed `display: none`), but +keeps the collapsed panels its own exclusive presentation hides, so automatic +collapse can still be undone. ## Surface keyboard lifecycle diff --git a/src/ui/panelRails.test.mjs b/src/ui/panelRails.test.mjs index de5b3997b..7015c89e2 100644 --- a/src/ui/panelRails.test.mjs +++ b/src/ui/panelRails.test.mjs @@ -723,6 +723,32 @@ test('right layout ignores a CSS-hidden expanded panel when deciding exclusivity ); }); +test('right layout still restores panels that exclusive presentation hides', () => { + const f = fixture('right'); + // Model layers.css: an exclusive rail hides its collapsed panels. + f.options.windowRef.getComputedStyle = (node) => ({ + ...node.computed, + display: + f.stack.classList.contains('layout-exclusive') && + node.classList.contains('collapsed') + ? 'none' + : node.computed.display, + }); + f.expand(f.first, 250); + f.second.classList.add('layout-auto-collapsed'); + f.stack.classList.add('layout-exclusive'); + + f.run(); + assert.equal(f.stack.classList.contains('layout-exclusive'), true); + assert.equal(f.second.getAttribute('aria-hidden'), 'true'); + + f.options.hud.visible = false; + f.run(); + assert.equal(f.second.classList.contains('collapsed'), false); + assert.equal(f.second.classList.contains('layout-auto-collapsed'), false); + assert.deepEqual(f.collapsed, ['second']); +}); + test('right layout ignores a hidden panel: no lane, no gap, no auto-collapse', () => { const f = fixture('right'); const imagery = element('recent-imagery-panel', { height: 0 }); diff --git a/src/ui/rightPanelRail.js b/src/ui/rightPanelRail.js index b62be511c..be49999fd 100644 --- a/src/ui/rightPanelRail.js +++ b/src/ui/rightPanelRail.js @@ -69,12 +69,17 @@ export function layoutRightPanelRail({ // A collapse may schedule one follow-up, which only measures and allocates. const isCollapseRetry = pendingCollapseRetries.delete(stack); - // A floating (lifted-out) panel is not laid out by the rail. + // A floating (lifted-out) panel is not laid out by the rail. Neither is one + // that other CSS hides, such as an event view. Exclusive presentation hides + // collapsed panels itself; those stay in the rail so they can be restored. + const hiddenByRail = (panel) => + stack.classList.contains('layout-exclusive') && + panel.classList.contains('collapsed'); const panels = [...stack.children].filter( (panel) => panel.matches('[data-panel-id]:not(.panel-floating)') && !panel.hidden && - getComputedStyle(panel).display !== 'none', + (hiddenByRail(panel) || getComputedStyle(panel).display !== 'none'), ); if (!hud.visible || hud.variant !== 'tactical') { for (const panel of panels.filter((item) => From 750f3da6eba3b115399ab50d8fadb9144fb7a846 Mon Sep 17 00:00:00 2001 From: HEOJUNFO <100739943+HEOJUNFO@users.noreply.github.com> Date: Thu, 15 Oct 2026 03:48:51 +0900 Subject: [PATCH 11/15] fix(styles): keep Snow wind sway bounded over time (#782) The horizontal offset multiplied elapsed time by a sine, so flakes swung faster the longer the app ran. It is now the integral of the intended sway velocity, which stays bounded at any session age; Wind still scales the amplitude. Co-authored-by: Sameh Khamis --- CHANGELOG.md | 3 +++ docs/CURRENT-STATE.md | 2 ++ src/styles/snow.js | 6 ++++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69a7f6823..1ef811a5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +- Snow flakes sway within a fixed range instead of drifting sideways faster the + longer the app runs; the Wind setting still scales the sway. + - The right rail ignores panels that other CSS hides, such as during an event view, so they no longer force one-panel-at-a-time layout or collapse visible panels. Panels the rail collapses itself are still restored. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 9281f4a40..ebf7495f5 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1013,6 +1013,8 @@ and sharpen. Display supplies actions and renders settings; effect execution has no DOM dependency. Existing presets and the 500 ms transition remain unchanged. Stopping the controller revokes animation before asynchronous UI teardown; final destruction removes its stages and restores the borrowed bloom configuration. +Snow's sideways sway stays within a fixed range at any session age; Wind scales +its amplitude. ## Display control ownership diff --git a/src/styles/snow.js b/src/styles/snow.js index e2ada6ad7..b23f9bd71 100644 --- a/src/styles/snow.js +++ b/src/styles/snow.js @@ -29,11 +29,13 @@ export const snowShader = { float depth = 0.5 + layer * 0.5; float speed = 0.4 + layer * 0.3; float size = mix(0.01, 0.025, layer); - float windForce = sin(time * 0.5 + layer * 3.14) * (0.05 + wind * 0.2); + float windForce = 0.05 + wind * 0.2; vec2 snowUV = uv * vec2(1.0, 0.5) * (4.0 + layer * 4.0); snowUV.y += time * speed; - snowUV.x += time * windForce; + // Offset is the integral of a swaying velocity sin(time * 0.5 + phase) * windForce, + // so the sway stays bounded however long the app has been running. + snowUV.x -= 2.0 * cos(time * 0.5 + layer * 3.14) * windForce; vec2 cell = floor(snowUV); vec2 f = fract(snowUV); From 85754bcf50c7b52fe3c753d34c653b4ae6826960 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Wed, 14 Oct 2026 11:59:14 -0700 Subject: [PATCH 12/15] test: say which CI job and which files the discovery guard covers --- scripts/run-unit-tests.mjs | 9 +++++---- src/unitTestRunner.test.mjs | 11 ++++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/scripts/run-unit-tests.mjs b/scripts/run-unit-tests.mjs index 7024ea38c..6cc6aaa94 100644 --- a/scripts/run-unit-tests.mjs +++ b/scripts/run-unit-tests.mjs @@ -25,10 +25,11 @@ export function assertNode24AllocationRuntime(version = process.versions.node) { * Directories this runner searches for `*.test.mjs`, in stable order. * * `server/` belongs here because provider modules own tests of their own: a - * suite placed beside the module it covers must run, and `npm test` is the only - * thing CI invokes, so a directory missing from this list is a suite that never - * runs anywhere. `unitTestRunner.test.mjs` asserts the list covers every test - * file in the repository, so adding a tree without adding it here fails. + * suite placed beside the module it covers must run, and CI's unit-test job runs + * `npm test`, so a directory missing from this list is a suite that never runs + * there. `unitTestRunner.test.mjs` asserts the list covers every `*.test.mjs` + * outside dependencies, build output and dot-directories, so adding a tree + * without adding it here fails. */ export const UNIT_TEST_ROOTS = Object.freeze(['src', 'server']); diff --git a/src/unitTestRunner.test.mjs b/src/unitTestRunner.test.mjs index 201f6ed8e..80d9091e0 100644 --- a/src/unitTestRunner.test.mjs +++ b/src/unitTestRunner.test.mjs @@ -64,11 +64,12 @@ test('unit runner serializes only GC-bracketed allocation microbenchmarks', () = }); test('the runner discovers every test file in the repository', () => { - // `npm test` is the only suite CI runs, and it runs exactly what this - // function returns. A `*.test.mjs` outside the searched roots therefore never - // executes anywhere, while still reading — in review and to its author — like - // covered code. Compare the two sets rather than their sizes, so the failure - // names the files instead of only disagreeing about a count. + // CI's unit-test job runs `npm test`, which runs exactly what this function + // returns. A `*.test.mjs` outside the searched roots therefore never runs + // there, while still reading — in review and to its author — like covered + // code. Compare the two sets rather than their sizes, so the failure names the + // files instead of only disagreeing about a count. The final equality also + // catches the reverse: files the runner finds that this walk skips. const discovered = discoverUnitTestFiles(repositoryRoot); const onDisk = testFilesOnDisk(repositoryRoot); const undiscovered = onDisk.filter((file) => !discovered.includes(file)); From afb3a92914b4d1bc19fa62d708456a331e0db274 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Wed, 14 Oct 2026 11:59:51 -0700 Subject: [PATCH 13/15] docs: say what the connection reads from the voice credential It reads the token, model, transcription model and tier for diagnostics and cost accounting, so the credential is not opaque to it. Also lists requestToken's authMode option in APPLICATION.md. --- docs/APPLICATION.md | 2 +- docs/VOICE-OWNERSHIP.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/APPLICATION.md b/docs/APPLICATION.md index deafe9168..c5a2642e6 100644 --- a/docs/APPLICATION.md +++ b/docs/APPLICATION.md @@ -158,7 +158,7 @@ routing and regional caches. Request parameters cannot override these endpoints. Voice controls accept an action runner and a controller factory through `createVoiceCommands` (`./voice/commands`). The default Realtime controller owns microphone tracks, playback, push-to-talk, tool cancellation and radio handoff. -Its backend supplies `requestToken({ tier, signal })` and +Its backend supplies `requestToken({ tier, authMode, signal })` and `negotiate({ offerSdp, credential, signal })`. The Realtime-compatible adapter (`./voice/realtime-backend`) accepts separate token and connection transports and endpoints. Only the short-lived client secret reaches the SDP endpoint. diff --git a/docs/VOICE-OWNERSHIP.md b/docs/VOICE-OWNERSHIP.md index 98b6b0c5c..bb28eb3e9 100644 --- a/docs/VOICE-OWNERSHIP.md +++ b/docs/VOICE-OWNERSHIP.md @@ -26,8 +26,8 @@ The backend is replaceable per deployment: `realtimeController.js` accepts it as `backend` option and falls back to `createRealtimeBackend()`. Its two method signatures live with [application construction](APPLICATION.md); what belongs here is the ownership. The connection is the backend's only consumer in the application. -The resolved credential is opaque to the connection and is handed to `negotiate` -whole. Client-secret expiry is refused inside the backend, not by the connection. +The connection reads credential metadata for diagnostics and cost accounting, +then passes the complete credential unchanged to `negotiate`. Client-secret expiry is refused inside the backend, not by the connection. `realtimeOwners.test.mjs` drives whole sessions through a substitute that implements only those two methods. From 4cd7200fe445200968109b2a65cb6a98a485deb3 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Wed, 14 Oct 2026 12:00:16 -0700 Subject: [PATCH 14/15] docs: attribute the Pinokio stutter report accurately The workaround came from one Pinokio 8.2.0 user who did not state an OS, and a single report suggests rather than isolates the embedded-browser cause. --- README.md | 10 +++++----- docs/PERFORMANCE.md | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 6c6b1c238..50c5a4003 100644 --- a/README.md +++ b/README.md @@ -119,11 +119,11 @@ Version 8.2 fixes the launcher installation issue; **Pinokio opens but 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. A -Pinokio 8.2.0 Linux report reproduced severe stutter in Pinokio's embedded view -while the same running server was smooth in an external browser ([#365](https://github.com/bilawalsidhu/gods-eye-view/issues/365)). -That isolates the browser/compositor path from the app server and your machine; -it does not establish a minimum GPU requirement. +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 +away when the same running server was opened in an external browser ([#365](https://github.com/bilawalsidhu/gods-eye-view/issues/365)). +That points to the embedded browser rather than the app server or your +hardware; it does not establish a minimum GPU requirement. ### Path 2 — Terminal / coding agent diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index 873ef8768..65d0abae9 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -129,11 +129,11 @@ Use the same controls before attributing a difference to the application: 7. Treat a live-source outage as missing coverage, not as evidence of low client rendering cost. 8. Record whether the app is running in Pinokio's embedded view or in an - external browser. The same Pinokio 8.2.0 Linux server was reported as - severely stuttery in the embedded view and smooth when its local URL was - opened directly in a normal browser ([#365](https://github.com/bilawalsidhu/gods-eye-view/issues/365)). - Treat that as a browser/compositor-path difference until the GPU renderer - proves otherwise. + external browser. One Pinokio 8.2.0 user reported severe stutter in the + embedded view that went away when the same local URL was opened in a normal + browser ([#365](https://github.com/bilawalsidhu/gods-eye-view/issues/365)). + Do not compare embedded-view and external-browser measurements directly; + compare the reported GPU renderer and matched scene settings first. ## What is not established yet From 91ac5c119084fdb200362b2c6dd07cb2324fb081 Mon Sep 17 00:00:00 2001 From: quanticsoul4772 <71096528+quanticsoul4772@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:23:49 -0700 Subject: [PATCH 15/15] test(codex): match the installed-executable stub on Windows (#997) The test's access stub matched a forward-slash suffix, so on Windows the resolver's backslash path never matched and it fell back to PATH. The stub and the expected value now use the same path.join result. The Windows CI job also runs this test file now. Co-authored-by: Sameh Khamis --- .github/workflows/ci.yml | 2 +- src/codexOauthRealtime.test.mjs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 375bc9077..491f2468d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -150,7 +150,7 @@ jobs: run: npm run check:boundaries - name: Run focused onboarding tests - run: node --test src/setupDoctor.test.mjs src/pinokioEnvironment.test.mjs src/pinokioPreflight.test.mjs src/pinokioLauncherContract.test.mjs src/keySetupHardening.test.mjs src/mapStartup.test.mjs src/hudSummaryResponse.test.mjs + run: node --test src/setupDoctor.test.mjs src/pinokioEnvironment.test.mjs src/pinokioPreflight.test.mjs src/pinokioLauncherContract.test.mjs src/keySetupHardening.test.mjs src/mapStartup.test.mjs src/hudSummaryResponse.test.mjs src/codexOauthRealtime.test.mjs - name: Build production bundle run: npm run build diff --git a/src/codexOauthRealtime.test.mjs b/src/codexOauthRealtime.test.mjs index 07e1a7dcc..4a577ac70 100644 --- a/src/codexOauthRealtime.test.mjs +++ b/src/codexOauthRealtime.test.mjs @@ -145,17 +145,20 @@ test('Codex OAuth reader enforces expiry margin and bounds tokens without exp by test('Codex login resolves an installed executable before PATH and starts browser auth detached', async () => { const checked = []; + // Built with path.join so the stub matches the resolver on Windows too, + // where the separators are backslashes. + const installed = path.join('/home/fixture', '.local', 'bin', 'codex'); assert.equal( resolveCodexExecutable({ environment: {}, home: '/home/fixture', access(candidate) { checked.push(candidate); - if (candidate.endsWith('/.local/bin/codex')) return; + if (candidate === installed) return; throw new Error('missing'); }, }), - path.join('/home/fixture', '.local', 'bin', 'codex'), + installed, ); assert.equal(checked.length, 1);