mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Merge commit '1a9cfb180914fc9c2b06088a836b45dbf27254d5' into review/bilawal-status-371
This commit is contained in:
commit
8eb6d09105
8 files changed
+200
-21
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import { governorRequestRender } from '../renderGovernor.js';
|
||||
import { GUIDANCE_STATUSES } from '../loadingFeedback.js';
|
||||
import { markDetectionSourcesChanged } from './detection.js';
|
||||
function cloneLayerParams(value) {
|
||||
if (Array.isArray(value)) return value.map(cloneLayerParams);
|
||||
@@ -2233,6 +2234,12 @@ export class DataLayerManager {
|
||||
}
|
||||
return `${stateLabel} · ${source} · ${presentedError}`;
|
||||
}
|
||||
// A guidance status carries its prompt in `statusMessage`, not `error`, so
|
||||
// the row still tells the operator what to do without reporting a fault.
|
||||
if (GUIDANCE_STATUSES.includes(String(stats.status || '').toLowerCase())
|
||||
&& typeof stats.statusMessage === 'string' && stats.statusMessage.trim()) {
|
||||
return `${source} · ${stats.statusMessage.trim()}`;
|
||||
}
|
||||
const ago = stats.lastUpdate ? this._timeAgo(stats.lastUpdate) : 'never';
|
||||
if (stats.loading) {
|
||||
const loadingLabel = typeof stats.loadingLabel === 'string' && stats.loadingLabel.trim()
|
||||
|
||||
@@ -3256,3 +3256,14 @@ test('a layer that surrenders its row controls hides the block entirely', async
|
||||
else globalThis.document = originalDocument;
|
||||
}
|
||||
});
|
||||
|
||||
test('layer metadata shows a guidance prompt without reporting it as a fault', () => {
|
||||
const mgr = new DataLayerManager({});
|
||||
const text = mgr._buildMetaText({
|
||||
enabled: true,
|
||||
source: 'OpenStreetMap',
|
||||
stats: { status: 'zoom-in', error: null, count: 0, statusMessage: 'Zoom in to search mapped installations' },
|
||||
});
|
||||
assert.match(text, /Zoom in to search mapped installations$/);
|
||||
assert.doesNotMatch(text, /UNAVAILABLE|DEGRADED/);
|
||||
});
|
||||
@@ -425,12 +425,18 @@ function scheduleLoad() {
|
||||
async function loadInstallations() {
|
||||
if (!state.enabled || !state.viewer) return;
|
||||
const box = viewportBox(state.viewer);
|
||||
// Guidance, not a fault: the layer chose not to query because the view is
|
||||
// unbounded (a global view, or Cockpit looking at the horizon). Keep it out
|
||||
// of `error` — the manager derives refresh failures and the global status
|
||||
// chip derives LOAD FAILED from that field, and a "zoom in" prompt turned
|
||||
// every Cockpit refresh into a reported failure. The row and toast read the
|
||||
// prompt from `statusMessage` (installationFeedback) instead.
|
||||
if (!box) {
|
||||
state.abort?.abort();
|
||||
state.abort = null;
|
||||
state.loading = false;
|
||||
clearUnavailableRetry();
|
||||
setInstallationStatus('zoom-in', 'Zoom in to load mapped installation context');
|
||||
setInstallationStatus('zoom-in');
|
||||
return;
|
||||
}
|
||||
state.abort?.abort();
|
||||
|
||||
@@ -758,6 +758,8 @@ test('zoom-out aborts an active installation request and returns non-loading gui
|
||||
assert.equal(observedSignal.aborted, true);
|
||||
assert.equal(militaryInstallationsLayer.getStats().loading, false);
|
||||
assert.equal(militaryInstallationsLayer.getStats().status, 'zoom-in');
|
||||
assert.equal(militaryInstallationsLayer.getStats().error, null, 'guidance is not a fault');
|
||||
assert.match(militaryInstallationsLayer.getStats().statusMessage, /zoom in/i);
|
||||
} finally {
|
||||
militaryInstallationsLayer.destroy(viewer);
|
||||
globalThis.fetch = originalFetch;
|
||||
|
||||
@@ -5,6 +5,8 @@ export const LOADING_TERMINAL_DWELL_MS = 2200;
|
||||
export const LOADING_FAILURE_DWELL_MS = 5000;
|
||||
export const LOADING_LONG_THRESHOLD_MS = 30000;
|
||||
export const TRAFFIC_SYNC_CONFIRM_MS = 1500;
|
||||
/** Layer statuses that are user guidance, not feed faults (see manager.js layerFeedState). */
|
||||
export const GUIDANCE_STATUSES = Object.freeze(['zoom-in', 'empty', 'idle']);
|
||||
|
||||
function finiteCount(value) {
|
||||
const number = Number(value);
|
||||
@@ -20,7 +22,13 @@ export function normalizeLayerLoading(layer = {}) {
|
||||
const loading = lifecycleState === 'enabling' || disabling || stats.loading === true || stats.refreshing === true;
|
||||
const count = finiteCount(stats.count);
|
||||
const stoppingInstallations = layer.id === 'military-installations' && disabling;
|
||||
const error = stoppingInstallations ? null : stats.error || stats.lastError || stats.managerRefreshError || null;
|
||||
// Guidance statuses ask the user to act (zoom in, run a search). They are
|
||||
// normal operation, never a batch failure — mirrors layerFeedState's carve-out
|
||||
// so a prompt stored alongside the status cannot turn the chip red.
|
||||
const guidance = GUIDANCE_STATUSES.includes(status);
|
||||
const error = stoppingInstallations || guidance
|
||||
? null
|
||||
: stats.error || stats.lastError || stats.managerRefreshError || null;
|
||||
const unavailable = !stoppingInstallations && (stats.unavailable === true
|
||||
|| stats.available === false
|
||||
|| ['unavailable', 'offline', 'down', 'error'].includes(status));
|
||||
|
||||
@@ -685,3 +685,17 @@ test('the loading ticker never runs hidden and stops after loading and notices s
|
||||
'the resample handler must be removed on teardown',
|
||||
);
|
||||
});
|
||||
|
||||
test('a guidance status such as zoom-in never counts as a participant failure', () => {
|
||||
const zoomIn = { id: 'military-installations', name: 'Mapped Installations', enabled: true,
|
||||
stats: { status: 'zoom-in', error: 'Zoom in to load mapped installation context', loading: true, count: 0 } };
|
||||
const loading = aggregateLayerLoading([zoomIn]);
|
||||
assert.equal(loading.records[0].error, null);
|
||||
assert.equal(loading.records[0].degraded, false);
|
||||
let state = reduceLoadingFeedback(createLoadingFeedbackState(), loading, 1000);
|
||||
state = reduceLoadingFeedback(state, loading, 1200);
|
||||
const settled = aggregateLayerLoading([{ ...zoomIn, stats: { ...zoomIn.stats, loading: false } }]);
|
||||
state = reduceLoadingFeedback(state, settled, 1500);
|
||||
assert.equal(state.terminal, 'complete');
|
||||
assert.equal(presentLoadingFeedback(state, settled, 1500).label, 'MAPPED SITES LOADED');
|
||||
});
|
||||
@@ -230,3 +230,62 @@ test('AIS preview route ingests through the socket, returns tracks and disposes
|
||||
for (let i = 0; i < 100 && sockets.length < 2; i++) await delay(10);
|
||||
assert.equal(sockets.length, 2);
|
||||
});
|
||||
|
||||
test('military aircraft route serves stale cache on an upstream 429 and cools down for Retry-After', async (t) => {
|
||||
let now = Date.now();
|
||||
let calls = 0;
|
||||
t.mock.method(Date, 'now', () => now);
|
||||
t.mock.method(console, 'warn', () => {});
|
||||
t.mock.method(globalThis, 'fetch', async () => {
|
||||
calls += 1;
|
||||
if (calls === 1) return Response.json({ ac: [{ hex: 'abc123' }] });
|
||||
return new Response(JSON.stringify({ error: 'rate limited' }), {
|
||||
status: 429,
|
||||
headers: { 'Retry-After': '20' },
|
||||
});
|
||||
});
|
||||
const request = install(providers.adsbLolProxy());
|
||||
const first = await request('/api/adsblol/mil');
|
||||
assert.equal(first.statusCode, 200);
|
||||
now += 13_000;
|
||||
const limited = await request('/api/adsblol/mil');
|
||||
assert.equal(calls, 2);
|
||||
assert.equal(
|
||||
limited.statusCode,
|
||||
200,
|
||||
'a 429 with a cached body is never relayed',
|
||||
);
|
||||
assert.equal(limited.body, first.body);
|
||||
assert.equal(limited.headers['x-ads-b-cache'], 'STALE');
|
||||
assert.equal(limited.headers['x-ads-b-upstream-status'], '429');
|
||||
now += 5_000;
|
||||
const cooling = await request('/api/adsblol/mil');
|
||||
assert.equal(calls, 2, 'no upstream call inside the Retry-After window');
|
||||
assert.equal(cooling.headers['x-ads-b-cache'], 'STALE');
|
||||
now += 16_000;
|
||||
await request('/api/adsblol/mil');
|
||||
assert.equal(calls, 3, 'upstream is retried once Retry-After elapses');
|
||||
});
|
||||
|
||||
test('military aircraft route relays an upstream 429 when nothing is cached', async (t) => {
|
||||
t.mock.method(console, 'warn', () => {});
|
||||
let calls = 0;
|
||||
t.mock.method(globalThis, 'fetch', async () => {
|
||||
calls += 1;
|
||||
return new Response('{"error":"rate limited"}', { status: 429 });
|
||||
});
|
||||
const request = install(providers.adsbLolProxy());
|
||||
const limited = await request('/api/adsblol/mil');
|
||||
assert.equal(limited.statusCode, 429);
|
||||
const again = await request('/api/adsblol/mil');
|
||||
assert.equal(again.statusCode, 429);
|
||||
assert.equal(
|
||||
calls,
|
||||
1,
|
||||
'the cooldown still protects upstream with nothing cached',
|
||||
);
|
||||
assert.ok(
|
||||
again.headers['retry-after'],
|
||||
'a cooling-down miss carries Retry-After',
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user