mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Merge branch 'main' into feat/voice-geometry
Picks up the HUD summary locale work (#1488). No conflicts; it changes no voice prompt or tool description.
This commit is contained in:
commit
d18fdfa47f
28 files changed
+1251
-136
No files matched your search
@@ -17,6 +17,7 @@ export const LAYER_FEED_STATES = Object.freeze([
|
||||
'loading',
|
||||
'degraded',
|
||||
'stale',
|
||||
'partial',
|
||||
'fallback',
|
||||
'unavailable',
|
||||
]);
|
||||
@@ -27,6 +28,7 @@ export const LAYER_FEED_STATE_LABELS = Object.freeze({
|
||||
loading: 'LOADING',
|
||||
degraded: 'DEGRADED',
|
||||
stale: 'STALE',
|
||||
partial: 'PARTIAL',
|
||||
fallback: 'FALLBACK',
|
||||
unavailable: 'UNAVAILABLE',
|
||||
});
|
||||
@@ -48,8 +50,9 @@ export const FEED_STATE_SEVERITY = Object.freeze({
|
||||
degraded: 2,
|
||||
stale: 3,
|
||||
fallback: 4,
|
||||
nominal: 5,
|
||||
off: 6,
|
||||
partial: 5,
|
||||
nominal: 6,
|
||||
off: 7,
|
||||
});
|
||||
|
||||
export { layerFeedState } from './feedState.js';
|
||||
@@ -90,7 +93,7 @@ export function snapshotAgeLabel(lastUpdate, now = Date.now()) {
|
||||
/**
|
||||
* Pick the most severe feed-state from a list.
|
||||
* @param {Iterable<string|null|undefined>} states Feed-state strings.
|
||||
* @returns {'nominal'|'loading'|'degraded'|'stale'|'fallback'|'unavailable'|'off'|null}
|
||||
* @returns {'nominal'|'loading'|'degraded'|'stale'|'partial'|'fallback'|'unavailable'|'off'|null}
|
||||
*/
|
||||
export function worstFeedState(states) {
|
||||
let worst = null;
|
||||
@@ -205,6 +208,8 @@ export function feedProvenanceNote(layers = [], overall = null) {
|
||||
return `${s.name || s.id} is ${String(s.feedState).toUpperCase()}${extras ? ` (${extras})` : ''}`;
|
||||
});
|
||||
const body = bits.join('; ') || `${String(overall).toUpperCase()} feed`;
|
||||
if (overall === 'partial')
|
||||
return `${body}. Narrate that coverage is incomplete with any count; do not claim full coverage.`;
|
||||
return `${body}. Narrate that provenance with any count; do not describe this as live.`;
|
||||
}
|
||||
|
||||
@@ -251,3 +256,63 @@ export function viewStateLayerRow(layer, options) {
|
||||
lastUpdate: snap.lastUpdate,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The feed state and up to two layer names of a provenance envelope that is
|
||||
* not nominal, or null; the worst layers come first, so the names match the
|
||||
* state. Values stay English (`state` is a feed-state id, `names` are
|
||||
* `layer.name`); the HUD translates them for display.
|
||||
* @param {{ overall?: string|null, layers?: Array<object> }|null} envelope
|
||||
* @returns {{ state: string, names: string[] }|null}
|
||||
*/
|
||||
export function provenanceTag(envelope) {
|
||||
const overall = envelope?.overall;
|
||||
if (!overall || overall === 'nominal' || overall === 'off') return null;
|
||||
const names = (envelope.layers || [])
|
||||
.filter(
|
||||
(s) => s.feedState && s.feedState !== 'nominal' && s.feedState !== 'off',
|
||||
)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
FEED_STATE_SEVERITY[a.feedState] - FEED_STATE_SEVERITY[b.feedState],
|
||||
)
|
||||
.map((s) => String(s.name || s.id || 'LAYER'))
|
||||
.slice(0, 2);
|
||||
return { state: overall, names };
|
||||
}
|
||||
|
||||
const upperCase = (word) => word.toUpperCase();
|
||||
|
||||
/**
|
||||
* `STATE | text | LAYERS`: the state leads so a narrow readout never clips
|
||||
* it; the layer names trail so they never push out the text.
|
||||
* @param {string} text
|
||||
* @param {{ state: string, names: string[] }|null} tag From `provenanceTag`.
|
||||
* @param {(state: string) => string} [stateLabel]
|
||||
* @param {(name: string) => string} [layerLabel]
|
||||
* @returns {string}
|
||||
*/
|
||||
export function provenanceLine(
|
||||
text,
|
||||
tag,
|
||||
stateLabel = upperCase,
|
||||
layerLabel = upperCase,
|
||||
) {
|
||||
if (!tag) return text;
|
||||
return [stateLabel(tag.state), text, tag.names.map(layerLabel).join('/')]
|
||||
.filter(Boolean)
|
||||
.join(' | ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Require a matching feedState for warning states: a view that is not
|
||||
* nominal falls back on a missing or different state.
|
||||
* @param {{ feedState?: string|null }|null} response The proxy's reply.
|
||||
* @param {{ overall?: string|null }|null} provenance The context's envelope.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function hudSummaryMatchesProvenance(response, provenance) {
|
||||
const state = provenance?.overall;
|
||||
if (!state || state === 'nominal' || state === 'off') return true;
|
||||
return response?.feedState === state;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
layerFeedState,
|
||||
layerSnapshot,
|
||||
layerSnapshots,
|
||||
provenanceTag,
|
||||
snapshotAgeLabel,
|
||||
snapshotAgeSec,
|
||||
viewStateLayerRow,
|
||||
@@ -90,7 +91,15 @@ test('layerFeedState matches the chip classifier for every honesty class', () =>
|
||||
test('chip labels and severity cover every feed-state the envelope can emit', () => {
|
||||
assert.deepEqual(
|
||||
[...LAYER_FEED_STATES],
|
||||
['nominal', 'loading', 'degraded', 'stale', 'fallback', 'unavailable'],
|
||||
[
|
||||
'nominal',
|
||||
'loading',
|
||||
'degraded',
|
||||
'stale',
|
||||
'partial',
|
||||
'fallback',
|
||||
'unavailable',
|
||||
],
|
||||
);
|
||||
assert.deepEqual([...SNAPSHOT_FEED_STATES], [...LAYER_FEED_STATES, 'off']);
|
||||
for (const state of LAYER_FEED_STATES) {
|
||||
@@ -107,6 +116,33 @@ test('chip labels and severity cover every feed-state the envelope can emit', ()
|
||||
assert.equal(worstFeedState([]), null);
|
||||
});
|
||||
|
||||
test('partial is a feed-state of its own: above nominal, below every fault', () => {
|
||||
const layer = (id, stats) => ({ id, name: id, enabled: true, stats });
|
||||
const partial = layer('flights', {
|
||||
partial: true,
|
||||
count: 4,
|
||||
lastUpdate: NOW,
|
||||
});
|
||||
const nominal = layer('vessels', { count: 9, lastUpdate: NOW });
|
||||
const stale = layer('cctv', { stale: true, count: 2, lastUpdate: NOW });
|
||||
const overall = (layers) =>
|
||||
feedProvenanceEnvelope(layers, { now: NOW }).overall;
|
||||
assert.equal(overall([partial]), 'partial');
|
||||
assert.equal(overall([partial, nominal]), 'partial');
|
||||
assert.equal(overall([nominal, partial]), 'partial');
|
||||
assert.equal(overall([partial, stale]), 'stale');
|
||||
assert.equal(overall([stale, partial]), 'stale');
|
||||
assert.equal(LAYER_FEED_STATE_LABELS.partial, 'PARTIAL');
|
||||
const note = feedProvenanceEnvelope([partial], { now: NOW }).note;
|
||||
assert.match(note, /coverage is incomplete/);
|
||||
assert.doesNotMatch(note, /do not describe this as live/);
|
||||
assert.equal(
|
||||
FEED_STATE_SEVERITY.fallback < FEED_STATE_SEVERITY.partial &&
|
||||
FEED_STATE_SEVERITY.partial < FEED_STATE_SEVERITY.nominal,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('a disabled layer is off even when leftover stats would read stale', () => {
|
||||
const snap = layerSnapshot(
|
||||
{
|
||||
@@ -258,3 +294,18 @@ test('age labels match the Data Layers meta clock', () => {
|
||||
assert.equal(snapshotAgeLabel(NOW - 240_000, NOW), '4m ago');
|
||||
assert.equal(snapshotAgeLabel(NOW - 7_200_000, NOW), '2h ago');
|
||||
});
|
||||
|
||||
test('the warning tag names the layers in the reported state first', () => {
|
||||
const tag = provenanceTag({
|
||||
overall: 'unavailable',
|
||||
layers: [
|
||||
{ name: 'Live Flights', feedState: 'partial' },
|
||||
{ name: 'Live AIS Vessels', feedState: 'partial' },
|
||||
{ name: 'CCTV', feedState: 'unavailable' },
|
||||
],
|
||||
});
|
||||
assert.deepEqual(tag, {
|
||||
state: 'unavailable',
|
||||
names: ['CCTV', 'Live Flights'],
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user