mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-09 12:54:01 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f135c675e7 | ||
|
|
4c769daa1a | ||
|
|
4fbbae651f |
No files matched your search
@@ -0,0 +1,4 @@
|
|||||||
|
- A layer that is only waiting for a key now ends the load chip on an amber
|
||||||
|
`KEY REQUIRED` instead of a red `LOAD FAILED`, so a keyless Environmental
|
||||||
|
first run no longer reports a failure while earthquakes load. A real error
|
||||||
|
still wins over a missing key (asturksever, #1600).
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
- MCP tools resolve place names through the keyless Photon geocoder when the
|
|
||||||
server has no geocoder configured, instead of answering "No place matched"
|
|
||||||
for every name (asturksever, #1603).
|
|
||||||
@@ -5081,8 +5081,7 @@
|
|||||||
"src/layers/alpr/records.js",
|
"src/layers/alpr/records.js",
|
||||||
"src/layers/alpr/tileRecords.js",
|
"src/layers/alpr/tileRecords.js",
|
||||||
"src/sources/infrastructureData.js",
|
"src/sources/infrastructureData.js",
|
||||||
"src/sources/eventPacks.js",
|
"src/sources/eventPacks.js"
|
||||||
"src/keylessGeocoder.js"
|
|
||||||
],
|
],
|
||||||
"external": ["@mapbox/vector-tile", "pbf"]
|
"external": ["@mapbox/vector-tile", "pbf"]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -98,15 +98,9 @@ export const FIRST_RUN_MISSIONS = Object.freeze({
|
|||||||
// for the FULLY CONFIGURED experience (owner ruling, 2026-08-23): the tile
|
// for the FULLY CONFIGURED experience (owner ruling, 2026-08-23): the tile
|
||||||
// promises both, so it turns on both, and the subcopy in index.html says so.
|
// promises both, so it turns on both, and the subcopy in index.html says so.
|
||||||
//
|
//
|
||||||
// Keyless, FIRMS is honest where it counts — its own layer row reads
|
// Keyless, FIRMS's own row reads "KEY REQUIRED", and the quakes half of
|
||||||
// "UNAVAILABLE · NASA FIRMS · LIVE · KEY REQUIRED", and the quakes half of
|
// the tile still delivers in full; the global status chip ends on KEY
|
||||||
// the tile still delivers in full. What is NOT honest is the GLOBAL status
|
// REQUIRED too (src/loadingFeedback.js) rather than LOAD FAILED.
|
||||||
// chip, which has no key-required terminal state and folds that row into
|
|
||||||
// "LOAD FAILED". That aggregation is the defect, not this preset: fixing it
|
|
||||||
// means a KEY REQUIRED terminal state in src/loadingFeedback.js, a state
|
|
||||||
// machine shared by every layer and not a thing to refactor the night
|
|
||||||
// before a launch. LEDGERED post-launch. Until it lands, keyless visitors
|
|
||||||
// are judged on the layer row, which tells them the truth.
|
|
||||||
layerIds: Object.freeze(['earthquakes', 'local-firms']),
|
layerIds: Object.freeze(['earthquakes', 'local-firms']),
|
||||||
busyKey: msg('Scanning active events…'),
|
busyKey: msg('Scanning active events…'),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -606,11 +606,10 @@ test('the tile is the FULLY CONFIGURED experience: quakes and fires together', (
|
|||||||
const environmental = FIRST_RUN_MISSIONS.environmental;
|
const environmental = FIRST_RUN_MISSIONS.environmental;
|
||||||
assert.deepEqual(environmental.layerIds, ['earthquakes', 'local-firms']);
|
assert.deepEqual(environmental.layerIds, ['earthquakes', 'local-firms']);
|
||||||
|
|
||||||
// Keyless, the honest surface is the LAYER ROW ("KEY REQUIRED"), which the
|
// Keyless, the LAYER ROW reads "KEY REQUIRED", and the global chip now ends
|
||||||
// FIRMS layer already reports. The misleading part is the GLOBAL chip folding
|
// on KEY REQUIRED as well instead of LOAD FAILED (src/loadingFeedback.js,
|
||||||
// that row into LOAD FAILED — a defect in the shared state machine, ledgered
|
// covered in loadingFeedback.test.mjs). The note says so where the next
|
||||||
// post-launch, and the note must stay where the next editor will read it
|
// editor will read it.
|
||||||
// rather than being re-discovered as a launcher bug.
|
|
||||||
const module = fs.readFileSync(
|
const module = fs.readFileSync(
|
||||||
new URL('./firstRunExperience.js', import.meta.url),
|
new URL('./firstRunExperience.js', import.meta.url),
|
||||||
'utf8',
|
'utf8',
|
||||||
@@ -621,7 +620,7 @@ test('the tile is the FULLY CONFIGURED experience: quakes and fires together', (
|
|||||||
);
|
);
|
||||||
assert.match(table, /KEY REQUIRED/);
|
assert.match(table, /KEY REQUIRED/);
|
||||||
assert.match(table, /src\/loadingFeedback\.js/);
|
assert.match(table, /src\/loadingFeedback\.js/);
|
||||||
assert.match(table, /LEDGERED post-launch/);
|
assert.match(table, /rather than LOAD FAILED/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('every visitor gets the same tile — there is no degraded keyless variant', async () => {
|
test('every visitor gets the same tile — there is no degraded keyless variant', async () => {
|
||||||
|
|||||||
+13
-7
@@ -85,13 +85,18 @@ export function normalizeLayerLoading(layer = {}) {
|
|||||||
function terminalFromParticipantStats(summary, participantIds) {
|
function terminalFromParticipantStats(summary, participantIds) {
|
||||||
if (!participantIds?.length) return null;
|
if (!participantIds?.length) return null;
|
||||||
const participants = new Set(participantIds);
|
const participants = new Set(participantIds);
|
||||||
return summary.records.some(
|
const records = summary.records.filter((record) =>
|
||||||
(record) =>
|
participants.has(record.id),
|
||||||
participants.has(record.id) &&
|
);
|
||||||
(record.error || record.unavailable || record.keyRequired),
|
// A layer waiting for a key did not fail: its row names the key. Only an
|
||||||
|
// actual error makes the whole batch read LOAD FAILED.
|
||||||
|
if (
|
||||||
|
records.some(
|
||||||
|
(record) => !record.keyRequired && (record.error || record.unavailable),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
? 'error'
|
return 'error';
|
||||||
: null;
|
return records.some((record) => record.keyRequired) ? 'key' : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Aggregate all manager layers without changing their lifecycle authority. */
|
/** Aggregate all manager layers without changing their lifecycle authority. */
|
||||||
@@ -282,7 +287,7 @@ function terminalFromEvent(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mergeTerminalOutcome(current, next) {
|
function mergeTerminalOutcome(current, next) {
|
||||||
const severity = { complete: 1, cancelled: 2, error: 3 };
|
const severity = { complete: 1, key: 2, cancelled: 3, error: 4 };
|
||||||
if (!next) return current || null;
|
if (!next) return current || null;
|
||||||
if (!current || severity[next] > severity[current]) return next;
|
if (!current || severity[next] > severity[current]) return next;
|
||||||
return current;
|
return current;
|
||||||
@@ -424,6 +429,7 @@ export function presentLoadingFeedback(state, summary, nowMs) {
|
|||||||
const labels = {
|
const labels = {
|
||||||
complete: 'LOAD COMPLETE',
|
complete: 'LOAD COMPLETE',
|
||||||
cancelled: 'LOAD CANCELLED',
|
cancelled: 'LOAD CANCELLED',
|
||||||
|
key: 'KEY REQUIRED',
|
||||||
error: 'LOAD FAILED',
|
error: 'LOAD FAILED',
|
||||||
};
|
};
|
||||||
const label =
|
const label =
|
||||||
|
|||||||
@@ -247,11 +247,11 @@ test('terminal loading feedback centers its label without an empty detail slot',
|
|||||||
const css = readStylesheet(new URL('../style.css', import.meta.url));
|
const css = readStylesheet(new URL('../style.css', import.meta.url));
|
||||||
assert.match(
|
assert.match(
|
||||||
css,
|
css,
|
||||||
/#global-loading-status:is\(\s*\[data-state='complete'\],\s*\[data-state='cancelled'\],\s*\[data-state='error'\]\s*\)\s*\{[\s\S]*?justify-content:\s*center;[\s\S]*?text-align:\s*center;/,
|
/#global-loading-status:is\(\s*\[data-state='complete'\],\s*\[data-state='cancelled'\],\s*\[data-state='key'\],\s*\[data-state='error'\]\s*\)\s*\{[\s\S]*?justify-content:\s*center;[\s\S]*?text-align:\s*center;/,
|
||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
css,
|
css,
|
||||||
/#global-loading-status:is\(\s*\[data-state='complete'\],\s*\[data-state='cancelled'\],\s*\[data-state='error'\]\s*\)\s*#global-loading-detail\s*\{\s*display:\s*none;/,
|
/#global-loading-status:is\(\s*\[data-state='complete'\],\s*\[data-state='cancelled'\],\s*\[data-state='key'\],\s*\[data-state='error'\]\s*\)\s*#global-loading-detail\s*\{\s*display:\s*none;/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -346,7 +346,7 @@ test('participant stats failure outranks a simultaneous visibility completion',
|
|||||||
name: 'AIS Vessels',
|
name: 'AIS Vessels',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
lifecycleState: 'enabled',
|
lifecycleState: 'enabled',
|
||||||
stats: { loading: false, keyRequired: true },
|
stats: { loading: false, error: 'Upstream closed' },
|
||||||
}]);
|
}]);
|
||||||
state = reduceLoadingFeedback(state, missingKey, 250, {
|
state = reduceLoadingFeedback(state, missingKey, 250, {
|
||||||
type: 'visibility', layerId: 'ais-live-vessels', enabled: true,
|
type: 'visibility', layerId: 'ais-live-vessels', enabled: true,
|
||||||
@@ -355,6 +355,36 @@ test('participant stats failure outranks a simultaneous visibility completion',
|
|||||||
assert.equal(state.terminal, 'error');
|
assert.equal(state.terminal, 'error');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a layer waiting for a key ends the batch as KEY REQUIRED, not LOAD FAILED', () => {
|
||||||
|
const quakes = (stats) => ({ id: 'earthquakes', name: 'Earthquakes', ...stats });
|
||||||
|
const fires = (stats) => ({ id: 'local-firms', name: 'Active Fires', ...stats });
|
||||||
|
const enabling = aggregateLayerLoading([
|
||||||
|
quakes({ lifecycleState: 'enabling', stats: { loading: true } }),
|
||||||
|
fires({ lifecycleState: 'enabling', stats: { loading: true } }),
|
||||||
|
]);
|
||||||
|
let state = reduceLoadingFeedback(createLoadingFeedbackState(), enabling, 0);
|
||||||
|
state = reduceLoadingFeedback(state, enabling, 200);
|
||||||
|
const settled = aggregateLayerLoading([
|
||||||
|
quakes({ enabled: true, stats: { count: 27, lastUpdate: 1 } }),
|
||||||
|
fires({ enabled: true, stats: { status: 'unavailable', keyRequired: true, error: 'KEY REQUIRED' } }),
|
||||||
|
]);
|
||||||
|
state = reduceLoadingFeedback(state, settled, 250);
|
||||||
|
assert.equal(state.terminal, 'key');
|
||||||
|
assert.deepEqual(state.failedEventIds, []);
|
||||||
|
const view = presentLoadingFeedback(state, settled, 260);
|
||||||
|
assert.equal(view.state, 'key');
|
||||||
|
assert.equal(view.label, 'KEY REQUIRED');
|
||||||
|
|
||||||
|
// A real failure beside it still reads as one.
|
||||||
|
const broken = aggregateLayerLoading([
|
||||||
|
quakes({ enabled: true, stats: { error: 'USGS 503' } }),
|
||||||
|
fires({ enabled: true, stats: { status: 'unavailable', keyRequired: true } }),
|
||||||
|
]);
|
||||||
|
let failing = reduceLoadingFeedback(createLoadingFeedbackState(), enabling, 0);
|
||||||
|
failing = reduceLoadingFeedback(failing, enabling, 200);
|
||||||
|
assert.equal(reduceLoadingFeedback(failing, broken, 250).terminal, 'error');
|
||||||
|
});
|
||||||
|
|
||||||
test('retains the worst terminal outcome until every concurrent load drains', () => {
|
test('retains the worst terminal outcome until every concurrent load drains', () => {
|
||||||
const both = aggregateLayerLoading([
|
const both = aggregateLayerLoading([
|
||||||
{ id: 'a', name: 'A', lifecycleState: 'enabling' },
|
{ id: 'a', name: 'A', lifecycleState: 'enabling' },
|
||||||
|
|||||||
+1
-19
@@ -24,7 +24,6 @@ export function createGeocodePlaceService({
|
|||||||
cacheMs = 10 * 60_000,
|
cacheMs = 10 * 60_000,
|
||||||
timeoutMs = 15_000,
|
timeoutMs = 15_000,
|
||||||
now = () => Date.now(),
|
now = () => Date.now(),
|
||||||
keylessGeocoder = null,
|
|
||||||
} = {}) {
|
} = {}) {
|
||||||
// Recent answers by name, so tools that combine others resolve a place to
|
// Recent answers by name, so tools that combine others resolve a place to
|
||||||
// the same point once instead of once per section. A pending lookup is
|
// the same point once instead of once per section. A pending lookup is
|
||||||
@@ -34,17 +33,8 @@ export function createGeocodePlaceService({
|
|||||||
async function lookup(name, signal) {
|
async function lookup(name, signal) {
|
||||||
const query = new URLSearchParams({ q: name });
|
const query = new URLSearchParams({ q: name });
|
||||||
const response = await fetchImpl(`/api/geocode?${query}`, { signal });
|
const response = await fetchImpl(`/api/geocode?${query}`, { signal });
|
||||||
const payload = await response.json().catch(() => null);
|
|
||||||
// No server geocoder (no Google key, no Nominatim): ask the keyless one
|
|
||||||
// the search box uses, rather than reporting every name as unmatched.
|
|
||||||
if (
|
|
||||||
keylessGeocoder &&
|
|
||||||
(payload?.configured === false || payload?.code === 'NOMINATIM_DISABLED')
|
|
||||||
) {
|
|
||||||
const { place } = await keylessGeocoder.geocode(name, { signal });
|
|
||||||
return place ? placeFromKeylessResult(place) : null;
|
|
||||||
}
|
|
||||||
if (!response.ok) throw new Error(`Geocode HTTP ${response.status}`);
|
if (!response.ok) throw new Error(`Geocode HTTP ${response.status}`);
|
||||||
|
const payload = await response.json();
|
||||||
return placeFromGeocodeResult(payload?.results?.[0]);
|
return placeFromGeocodeResult(payload?.results?.[0]);
|
||||||
}
|
}
|
||||||
function start(key, name) {
|
function start(key, name) {
|
||||||
@@ -110,14 +100,6 @@ export function createGeocodePlaceService({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Convert one geocode result to a place with bounds, or null. */
|
/** Convert one geocode result to a place with bounds, or null. */
|
||||||
/** A keyless (Photon) answer in the same shape as a geocode result's. */
|
|
||||||
function placeFromKeylessResult({ lat, lng, label, name, viewport }) {
|
|
||||||
return placeFromGeocodeResult({
|
|
||||||
formatted_address: label || name || null,
|
|
||||||
geometry: { location: { lat, lng }, ...(viewport ? { viewport } : {}) },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function placeFromGeocodeResult(result) {
|
export function placeFromGeocodeResult(result) {
|
||||||
const location = result?.geometry?.location;
|
const location = result?.geometry?.location;
|
||||||
if (!Number.isFinite(location?.lat) || !Number.isFinite(location?.lng))
|
if (!Number.isFinite(location?.lat) || !Number.isFinite(location?.lng))
|
||||||
|
|||||||
@@ -93,49 +93,3 @@ test('a shared lookup outlives one cancelled caller, and times out on its own',
|
|||||||
await places.resolve('Slow').catch(() => {});
|
await places.resolve('Slow').catch(() => {});
|
||||||
assert.equal(signals.length, 3, 'a timed-out lookup is not cached');
|
assert.equal(signals.length, 3, 'a timed-out lookup is not cached');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('without a server geocoder, names resolve through the keyless geocoder', async () => {
|
|
||||||
const asked = [];
|
|
||||||
const places = createGeocodePlaceService({
|
|
||||||
fetchImpl: async () =>
|
|
||||||
Response.json(
|
|
||||||
{
|
|
||||||
error: 'Place search is not configured',
|
|
||||||
code: 'NOMINATIM_DISABLED',
|
|
||||||
retryable: false,
|
|
||||||
},
|
|
||||||
{ status: 503 },
|
|
||||||
),
|
|
||||||
keylessGeocoder: {
|
|
||||||
geocode: async (name) => {
|
|
||||||
asked.push(name);
|
|
||||||
return name === 'Paris'
|
|
||||||
? {
|
|
||||||
place: {
|
|
||||||
lat: 48.8566,
|
|
||||||
lng: 2.3522,
|
|
||||||
name: 'Paris',
|
|
||||||
label: 'Paris, France',
|
|
||||||
viewport: {
|
|
||||||
southwest: { lat: 48.81, lng: 2.22 },
|
|
||||||
northeast: { lat: 48.9, lng: 2.47 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
answered: true,
|
|
||||||
}
|
|
||||||
: { place: null, answered: true };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const paris = await places.resolve('Paris');
|
|
||||||
assert.equal(paris.name, 'Paris, France');
|
|
||||||
assert.deepEqual(paris.point, { lat: 48.8566, lon: 2.3522 });
|
|
||||||
assert.deepEqual(paris.bounds, {
|
|
||||||
west: 2.22,
|
|
||||||
south: 48.81,
|
|
||||||
east: 2.47,
|
|
||||||
north: 48.9,
|
|
||||||
});
|
|
||||||
assert.equal(await places.resolve('Nowhere-at-all'), null);
|
|
||||||
assert.deepEqual(asked, ['Paris', 'Nowhere-at-all']);
|
|
||||||
});
|
|
||||||
@@ -35,7 +35,6 @@ import {
|
|||||||
createVesselSource,
|
createVesselSource,
|
||||||
createFlightSource,
|
createFlightSource,
|
||||||
} from '../sources/live/standalone.js';
|
} from '../sources/live/standalone.js';
|
||||||
import { createPhotonGeocoder } from '../keylessGeocoder.js';
|
|
||||||
import {
|
import {
|
||||||
createGeocodePlaceService,
|
createGeocodePlaceService,
|
||||||
createPlaceSearchService,
|
createPlaceSearchService,
|
||||||
@@ -87,10 +86,7 @@ export function createToolServices({ fetchImpl, appUrl, panelKey }) {
|
|||||||
fetchImpl,
|
fetchImpl,
|
||||||
tileFetchImpl: fetchImpl,
|
tileFetchImpl: fetchImpl,
|
||||||
}),
|
}),
|
||||||
places: createGeocodePlaceService({
|
places: createGeocodePlaceService({ fetchImpl }),
|
||||||
fetchImpl,
|
|
||||||
keylessGeocoder: createPhotonGeocoder({ fetchImpl }),
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const STATUS_LABEL_KEYS = Object.freeze({
|
|||||||
'LOAD COMPLETE': msg('LOAD COMPLETE'),
|
'LOAD COMPLETE': msg('LOAD COMPLETE'),
|
||||||
'LOAD CANCELLED': msg('LOAD CANCELLED'),
|
'LOAD CANCELLED': msg('LOAD CANCELLED'),
|
||||||
'LOAD FAILED': msg('LOAD FAILED'),
|
'LOAD FAILED': msg('LOAD FAILED'),
|
||||||
|
'KEY REQUIRED': msg('KEY REQUIRED'),
|
||||||
'LIVE DATA OFF': msg('LIVE DATA OFF'),
|
'LIVE DATA OFF': msg('LIVE DATA OFF'),
|
||||||
'MAPPED SITES LOADED': msg('MAPPED SITES LOADED'),
|
'MAPPED SITES LOADED': msg('MAPPED SITES LOADED'),
|
||||||
'RETRYING MAPPED SITES': msg('RETRYING MAPPED SITES'),
|
'RETRYING MAPPED SITES': msg('RETRYING MAPPED SITES'),
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ html:not([lang='en']) #global-loading-detail {
|
|||||||
#global-loading-status:is(
|
#global-loading-status:is(
|
||||||
[data-state='complete'],
|
[data-state='complete'],
|
||||||
[data-state='cancelled'],
|
[data-state='cancelled'],
|
||||||
|
[data-state='key'],
|
||||||
[data-state='error']
|
[data-state='error']
|
||||||
) {
|
) {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -139,6 +140,7 @@ html:not([lang='en']) #global-loading-detail {
|
|||||||
#global-loading-status:is(
|
#global-loading-status:is(
|
||||||
[data-state='complete'],
|
[data-state='complete'],
|
||||||
[data-state='cancelled'],
|
[data-state='cancelled'],
|
||||||
|
[data-state='key'],
|
||||||
[data-state='error']
|
[data-state='error']
|
||||||
)
|
)
|
||||||
#global-loading-detail {
|
#global-loading-detail {
|
||||||
@@ -155,11 +157,13 @@ html:not([lang='en']) #global-loading-detail {
|
|||||||
#global-loading-status[data-state='cancelled'] {
|
#global-loading-status[data-state='cancelled'] {
|
||||||
color: #ffd488;
|
color: #ffd488;
|
||||||
}
|
}
|
||||||
#global-loading-status[data-state='retry'] {
|
#global-loading-status:is([data-state='retry'], [data-state='key']) {
|
||||||
border-color: rgba(255, 212, 136, 0.55);
|
border-color: rgba(255, 212, 136, 0.55);
|
||||||
}
|
}
|
||||||
#global-loading-status[data-state='retry'] #global-loading-label,
|
#global-loading-status:is([data-state='retry'], [data-state='key'])
|
||||||
#global-loading-status[data-state='retry'] #global-loading-detail {
|
#global-loading-label,
|
||||||
|
#global-loading-status:is([data-state='retry'], [data-state='key'])
|
||||||
|
#global-loading-detail {
|
||||||
color: #ffd488;
|
color: #ffd488;
|
||||||
}
|
}
|
||||||
#global-loading-status[data-state='cancelled'] #global-loading-label,
|
#global-loading-status[data-state='cancelled'] #global-loading-label,
|
||||||
|
|||||||
Reference in new issue
Block a user