mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
commit
702ecc38e2
8 files changed
+154
-7
No files matched your search
@@ -5,6 +5,12 @@
|
|||||||
Repeated Context-open calls and rail-driven expansion both close the mini,
|
Repeated Context-open calls and rail-driven expansion both close the mini,
|
||||||
with a regression for auto-collapse restoration (raysung-2026, #804).
|
with a regression for auto-collapse restoration (raysung-2026, #804).
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
- Coordinate searches refuse empty latitude or longitude components instead of navigating to an unintended location.
|
||||||
|
|
||||||
- Bing Aerial and Bing Labels request HTTPS tiles on HTTP localhost through
|
- Bing Aerial and Bing Labels request HTTPS tiles on HTTP localhost through
|
||||||
their ion-backed providers, retaining ion and per-tile attribution without
|
their ion-backed providers, retaining ion and per-tile attribution without
|
||||||
widening the Content Security Policy.
|
widening the Content Security Policy.
|
||||||
@@ -198,6 +204,12 @@
|
|||||||
largest cities) and `CCTV_VEGVESEN_VIDEO=0` keeps stills only. Attributed
|
largest cities) and `CCTV_VEGVESEN_VIDEO=0` keeps stills only. Attributed
|
||||||
under NLOD 2.0.
|
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)
|
## [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,
|
- Ship each bundled data pack once. The region, marine, admin-boundary,
|
||||||
|
|||||||
@@ -1054,6 +1054,10 @@ disclosure before revealing Context content. The compact launcher still works
|
|||||||
while Context remains collapsed. This is an ownership invariant, not a
|
while Context remains collapsed. This is an ownership invariant, not a
|
||||||
z-index workaround (#804).
|
z-index workaround (#804).
|
||||||
|
|
||||||
|
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
|
## Surface keyboard lifecycle
|
||||||
|
|
||||||
`ui/surfaces` owns the capture-phase keyboard listener, Tab cycling and return
|
`ui/surfaces` owns the capture-phase keyboard listener, Tab cycling and return
|
||||||
@@ -4326,6 +4330,8 @@ and unreachable upstream (502/504) separately from road geometry.
|
|||||||
|
|
||||||
### Place-search providers
|
### 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
|
Location search/fly-to, annotations and Radio location lookup receive one
|
||||||
`placeSearch.geocode(query, { bias, signal })` service. `src/standalone` composes
|
`placeSearch.geocode(query, { bias, signal })` service. `src/standalone` composes
|
||||||
two offline providers first — a strict decimal-degree coordinate reader and an
|
two offline providers first — a strict decimal-degree coordinate reader and an
|
||||||
@@ -5226,7 +5232,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
|
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
|
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
|
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.
|
closed. Performance and live agency reliability require recorded validation.
|
||||||
|
|
||||||
## Wind forecast models
|
## Wind forecast models
|
||||||
|
|||||||
@@ -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. */
|
/** One decoder per active camera; both surfaces consume this video element. */
|
||||||
export function attachCctvVideo(
|
export function attachCctvVideo(
|
||||||
video,
|
video,
|
||||||
@@ -7,13 +32,14 @@ export function attachCctvVideo(
|
|||||||
loadHls = () => import('hls.js'),
|
loadHls = () => import('hls.js'),
|
||||||
onFailure = () => {},
|
onFailure = () => {},
|
||||||
fetchImpl = globalThis.fetch,
|
fetchImpl = globalThis.fetch,
|
||||||
|
cryptoImpl = globalThis.crypto,
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
// Preserve replay for existing finite video feeds; live HLS must not loop.
|
// Preserve replay for existing finite video feeds; live HLS must not loop.
|
||||||
video.loop = feedType !== 'hls';
|
video.loop = feedType !== 'hls';
|
||||||
let disposed = false;
|
let disposed = false;
|
||||||
let hls = null;
|
let hls = null;
|
||||||
const leaseId = feedType === 'hls' ? globalThis.crypto.randomUUID() : null;
|
const leaseId = feedType === 'hls' ? createLeaseId(cryptoImpl) : null;
|
||||||
const mediaUrl =
|
const mediaUrl =
|
||||||
feedType === 'hls'
|
feedType === 'hls'
|
||||||
? `${url}${url.includes('?') ? '&' : '?'}lease=${leaseId}`
|
? `${url}${url.includes('?') ? '&' : '?'}lease=${leaseId}`
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
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() {
|
function video() {
|
||||||
const v = new EventTarget();
|
const v = new EventTarget();
|
||||||
Object.assign(v, {
|
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');
|
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 () => {
|
test('finite video feeds retain looping while live HLS does not loop', async () => {
|
||||||
for (const feedType of ['mp4', 'webm', 'hls']) {
|
for (const feedType of ['mp4', 'webm', 'hls']) {
|
||||||
const source = video();
|
const source = video();
|
||||||
|
|||||||
@@ -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],
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -49,8 +49,8 @@ function splitComponents(query) {
|
|||||||
const parts = /[,;]/.test(text)
|
const parts = /[,;]/.test(text)
|
||||||
? text.split(/[,;]/)
|
? text.split(/[,;]/)
|
||||||
: text.split(/\s+(?=[+-]?[.\d]|[NSEW]\s*[+-]?[.\d])/i);
|
: text.split(/\s+(?=[+-]?[.\d]|[NSEW]\s*[+-]?[.\d])/i);
|
||||||
const trimmed = parts.map((part) => part.trim()).filter(Boolean);
|
const trimmed = parts.map((part) => part.trim());
|
||||||
return trimmed.length === 2 ? trimmed : null;
|
return trimmed.length === 2 && trimmed.every(Boolean) ? trimmed : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Present a parsed pair the way a coordinate is normally written. */
|
/** Present a parsed pair the way a coordinate is normally written. */
|
||||||
|
|||||||
@@ -704,6 +704,51 @@ 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 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', () => {
|
test('right layout ignores a hidden panel: no lane, no gap, no auto-collapse', () => {
|
||||||
const f = fixture('right');
|
const f = fixture('right');
|
||||||
const imagery = element('recent-imagery-panel', { height: 0 });
|
const imagery = element('recent-imagery-panel', { height: 0 });
|
||||||
|
|||||||
@@ -69,10 +69,17 @@ export function layoutRightPanelRail({
|
|||||||
// A collapse may schedule one follow-up, which only measures and allocates.
|
// A collapse may schedule one follow-up, which only measures and allocates.
|
||||||
const isCollapseRetry = pendingCollapseRetries.delete(stack);
|
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(
|
const panels = [...stack.children].filter(
|
||||||
(panel) =>
|
(panel) =>
|
||||||
panel.matches('[data-panel-id]:not(.panel-floating)') && !panel.hidden,
|
panel.matches('[data-panel-id]:not(.panel-floating)') &&
|
||||||
|
!panel.hidden &&
|
||||||
|
(hiddenByRail(panel) || getComputedStyle(panel).display !== 'none'),
|
||||||
);
|
);
|
||||||
if (!hud.visible || hud.variant !== 'tactical') {
|
if (!hud.visible || hud.variant !== 'tactical') {
|
||||||
for (const panel of panels.filter((item) =>
|
for (const panel of panels.filter((item) =>
|
||||||
|
|||||||
Reference in new issue
Block a user