mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
* feat(energy-atlas): promote Atlas map layers to FULL variant (§R #3 = B)
Per plan §R/#3 decision B: the Redis-backed evidence registries
(75 gas + 75 oil pipelines, 200 storage facilities, 29 fuel shortages)
are now toggleable on the main worldmonitor.app map. Previously they
were hardcoded energy-variant-only, and FULL users who toggled
`pipelines: true` got the ~20-entry legacy static PIPELINES list.
Changes:
- `src/components/DeckGLMap.ts`: drop the `SITE_VARIANT === 'energy'`
gates at :1511-1541. The pipelines layer now always uses
`createEnergyPipelinesLayer()` (Redis-backed evidence registry);
`createPipelinesLayer` (legacy static) is left in the file as dead
code pending a separate cleanup PR that also retires
`src/config/pipelines.ts`. Storage and fuel-shortage layers are
now gated only on the variant's `mapLayers.storageFacilities` /
`mapLayers.fuelShortages` booleans.
- `src/config/panels.ts`: add `storageFacilities: false` +
`fuelShortages: false` to FULL_MAP_LAYERS (desktop + mobile) so
the keys exist for toggle dispatch; default off so users opt in.
- `src/config/map-layer-definitions.ts`: extend the `full` variant's
VARIANT_LAYER_ORDER to include `storageFacilities` and
`fuelShortages`, so `getAllowedLayerKeys('full')` admits them and
the layer picker surfaces them.
- `src/config/commands.ts`: add CMD+K toggles
`layer:storageFacilities` and `layer:fuelShortages` next to the
existing `layer:pipelines`.
Finance + commodity variants already had `pipelines: true`; they
now render the more comprehensive Redis-backed 150-entry dataset
instead of the ~20-entry legacy list. If a variant doesn't want
this, they set `pipelines: false` in their MAP_LAYERS config.
Part of docs/internal/energy-atlas-registry-expansion.md §R.
* fix(energy-atlas): restrict storageFacilities + fuelShortages to flat renderer
Reviewer (Codex) found two gaps in PR #3366:
1. GlobeMap 3D toggles did nothing. LAYER_REGISTRY declared both new
layers with the default ['flat', 'globe'] renderers, so the toggle
showed up in globe mode. But GlobeMap.ts has no rendering support:
ensureStaticDataForLayer (:2160) only handles cables/pipelines/etc.,
and the layer-channel map (:2484) has no entries for either. Users
in globe mode saw the toggle and got silent no-ops.
2. SVG/mobile fallback (Map.ts fullLayers at :381) also has no render
path for these data types. The existing cyberThreats precedent at
:387 documents this as an intentional DeckGL-only pattern.
Fix:
- Restrict both LAYER_REGISTRY entries to ['flat'] explicitly. The
layer picker hides the toggle in globe mode instead of exposing a
no-op. Comment points to the GlobeMap gap so a future globe-rendering
PR knows what to undo.
- Extend the existing cyberThreats note in Map.ts:387 to cover
storageFacilities + fuelShortages too, noting they're already
hidden from globe mode via the LAYER_REGISTRY restriction.
This is the smallest possible fix consistent with the pre-existing
pattern. Full globe-mode rendering for these layers is out of scope —
tracked separately as a follow-up.
* fix(energy-atlas): gate layer:* CMD+K by current renderer + DeckGL state
Reviewer follow-up on PR #3366: the previous fix restricted
LAYER_REGISTRY renderers to ['flat'] so the globe-mode layer picker
hides storageFacilities / fuelShortages toggles. But CMD+K was still
callable — SearchModal.matchCommands didn't filter `layer:*` commands
by renderer, so a user could CMD+K "storage layer" in globe or SVG
mode and trigger a silent no-op.
Fix — centralize "can this layer render right now?" in one helper:
- Add `deckGLOnly?: boolean` to LayerDefinition. `renderers: ['flat']`
is not enough because `'flat'` covers both DeckGL-flat and SVG-flat,
and the SVG/mobile fallback has no render path for either layer.
Mark both as `deckGLOnly: true`.
- New `isLayerExecutable(key, renderer, isDeckGLActive)` helper in
map-layer-definitions.ts. Returns true iff renderers include the
current renderer AND (if deckGLOnly) DeckGL is active.
- `SearchModal.setLayerExecutableFn(fn)`: caller-supplied predicate
used in both `matchCommands` (search results) and
`renderAllCommandsList` (full picker).
- `search-manager` wires the predicate using `ctx.map.isGlobeMode()`
+ `ctx.map.isDeckGLActive()`, and also adds a symmetric guard in
the `layer:` dispatch case so direct activations (keyboard
accelerator, programmatic invocation) bail the same way.
Pre-existing resilienceScore DeckGL gate at search-manager:494 kept as
a belt-and-suspenders — the new isLayerExecutable check already
covers it since resilienceScore has `renderers: ['flat']` (though it
lacks deckGLOnly). Left the specific check in place to avoid scope
creep on a working guard.
Typecheck clean, 6694/6694 tests pass.
* fix(energy-atlas): filter CMD+K layer commands by variant too
Greptile P2 on commit 3f7a40036: `layer:storageFacilities` and
`layer:fuelShortages` still surface in CMD+K on tech / finance /
commodity / happy variants (where they're not in VARIANT_LAYER_ORDER).
Renderer + DeckGL filter was passing because those variants run flat
DeckGL. Dispatch silently failed at the `variantAllowed` guard in
handleCommand (:491), producing an invisible no-op from the user's
POV.
Fix: extend `setLayerExecutableFn` predicate to also check
`getAllowedLayerKeys(SITE_VARIANT).has(key)` before the renderer
checks. SearchModal now hides these commands on non-full/non-energy
variants where they can't execute.
This also cleans up the pre-existing pattern for other
variant-specific layer commands flagged by Greptile as "consistent
with how other variant-specific layer commands (e.g. layer:nuclear
on tech variant) already behave today" — they now all route through
the same predicate.
* fix(energy-atlas): gate layers:* presets + add isLayerExecutable tests (review P2)
Two Codex P2 findings on this PR:
1. `layers:*` presets bypassed the renderer/DeckGL gate.
`search-manager.ts:481` checked only `allowed.has(layer)` before
flipping a preset layer on. A user in globe mode or on SVG
fallback who ran `layers:all` or `layers:infra` would silently
set `deckGLOnly` layers (storageFacilities, fuelShortages) to
true — toggles with no rendered output, and since the picker
hides those layers under the current renderer the user had no
way to toggle them back off without switching modes.
Fix: funnel presets through the same `isLayerExecutable`
predicate per-layer CMD+K already uses. `executable(k)` combines
the existing `allowed.has` variant check with the renderer + DeckGL
gate, so presets now match the per-layer dispatch behavior exactly.
2. No regression tests for the `deckGLOnly` / `isLayerExecutable`
contract, despite it being behavior-critical renderer gating.
Fix: added `tests/map-layer-executable.test.mts` — 16 cases:
- Flag assertions: storageFacilities + fuelShortages carry
`deckGLOnly: true` and renderers: ['flat']. Layers without the
flag (pipelines, conflicts, cables) have it `undefined`, not
accidentally `false`.
- Renderer-gate cases: deckGLOnly layers pass only on flat + DeckGL
active, not on SVG fallback, not on globe. Flat-only non-deckGLOnly
layers (ciiChoropleth) pass on flat regardless of DeckGL status.
Dual-renderer layers (pipelines) pass on both flat and globe.
Unknown layer keys return false.
- Exhaustive 2×2×2 matrix across (renderer, isDeckGL, deckGLOnly)
using representative layer keys for each shape.
All 16 new tests pass. Full test:data suite still green. Typecheck clean.
* fix(energy-atlas): add pipeline-status to finance + commodity panel sets (review P1)
Codex P1: FINANCE_MAP_LAYERS and COMMODITY_MAP_LAYERS both carry
`pipelines: true`, and PR #3366 unified all variants on
`createEnergyPipelinesLayer` which dispatches
`energy:open-pipeline-detail` on row click. The listener for that
event lives in PipelineStatusPanel.
`PanelLayoutManager.createPanel()` only instantiates panels whose keys
are present in `panelSettings`, which derives from FULL_PANELS /
FINANCE_PANELS / etc. — so on finance and commodity variants the
listener never existed, and pipeline clicks were a silent no-op.
Fix: add `pipeline-status` to both FINANCE_PANELS and COMMODITY_PANELS
with `enabled: false` (panel slot not auto-opened; users invoke it by
clicking a pipeline on the map or via CMD+K). The panel now
instantiates on both variants and the click-through works end to end.
FULL_PANELS + ENERGY_PANELS already had the key from earlier PRs;
no change there.
Typecheck clean, test:data 6696/6696 pass.
140 lines
7.3 KiB
TypeScript
140 lines
7.3 KiB
TypeScript
// Regression guards for src/config/map-layer-definitions.ts:
|
|
//
|
|
// - `deckGLOnly` flag on LayerDefinition
|
|
// - `isLayerExecutable(key, renderer, isDeckGLActive)` predicate
|
|
//
|
|
// Both gate whether a `layer:*` toggle (per-layer CMD+K, `layers:*`
|
|
// preset, or programmatic dispatch) is allowed to flip a layer on
|
|
// under the active renderer + DeckGL state. Getting them wrong means
|
|
// toggles can set `mapLayers[key] = true` for layers that can't
|
|
// render — silent no-op state the user can't toggle back off if the
|
|
// picker hides the command under the current renderer.
|
|
//
|
|
// Closes the PR #3366 Codex P2 about missing regression tests for
|
|
// the `deckGLOnly` / `isLayerExecutable` contract.
|
|
|
|
import { strict as assert } from 'node:assert';
|
|
import { test, describe } from 'node:test';
|
|
import {
|
|
LAYER_REGISTRY,
|
|
isLayerExecutable,
|
|
} from '../src/config/map-layer-definitions';
|
|
|
|
describe('LAYER_REGISTRY — deckGLOnly flag', () => {
|
|
test('storageFacilities and fuelShortages are marked deckGLOnly', () => {
|
|
// These two layers ship in PR #3366 with DeckGL-only render paths.
|
|
// GlobeMap has no branch for them in ensureStaticDataForLayer, and
|
|
// Map.ts SVG fallback has no render code. The `deckGLOnly: true`
|
|
// flag is the signal that non-DeckGL contexts must not flip them on.
|
|
assert.equal(LAYER_REGISTRY.storageFacilities.deckGLOnly, true,
|
|
'storageFacilities must be marked deckGLOnly');
|
|
assert.equal(LAYER_REGISTRY.fuelShortages.deckGLOnly, true,
|
|
'fuelShortages must be marked deckGLOnly');
|
|
});
|
|
|
|
test('storageFacilities and fuelShortages are flat-only (no globe)', () => {
|
|
// Renderer restriction is belt to the deckGLOnly suspenders — it
|
|
// hides the toggle from the globe picker, while deckGLOnly also
|
|
// blocks dispatch on the SVG fallback even though SVG is "flat".
|
|
assert.deepEqual(LAYER_REGISTRY.storageFacilities.renderers, ['flat']);
|
|
assert.deepEqual(LAYER_REGISTRY.fuelShortages.renderers, ['flat']);
|
|
});
|
|
|
|
test('layers without deckGLOnly do not accidentally set the flag to false', () => {
|
|
// Spot-check: layers that existed before PR #3366 should have
|
|
// deckGLOnly unset (undefined), not explicitly `false`. An
|
|
// accidentally-introduced `deckGLOnly: false` would technically
|
|
// type-check but signals confusion about the contract (absence
|
|
// means "no opinion", not "forbids DeckGL").
|
|
assert.equal(LAYER_REGISTRY.pipelines.deckGLOnly, undefined,
|
|
'pipelines is not deckGLOnly — renders on flat + globe');
|
|
assert.equal(LAYER_REGISTRY.conflicts.deckGLOnly, undefined);
|
|
assert.equal(LAYER_REGISTRY.cables.deckGLOnly, undefined);
|
|
});
|
|
});
|
|
|
|
describe('isLayerExecutable — renderer gate', () => {
|
|
test('deckGLOnly layer returns true only on flat + DeckGL active', () => {
|
|
// The intended ship state: DeckGL desktop can render, nothing else.
|
|
assert.equal(isLayerExecutable('storageFacilities', 'flat', true), true,
|
|
'flat + DeckGL should execute');
|
|
assert.equal(isLayerExecutable('storageFacilities', 'flat', false), false,
|
|
'flat + SVG-fallback (no DeckGL) must NOT execute');
|
|
assert.equal(isLayerExecutable('storageFacilities', 'globe', true), false,
|
|
'globe mode must NOT execute (no GlobeMap render path)');
|
|
assert.equal(isLayerExecutable('storageFacilities', 'globe', false), false,
|
|
'globe + SVG is impossible in practice but must also not execute');
|
|
});
|
|
|
|
test('flat-only non-deckGLOnly layer returns true on flat regardless of DeckGL', () => {
|
|
// `ciiChoropleth` is renderers:['flat'] but NOT deckGLOnly — it
|
|
// renders via a different flat path (choropleth). The gate should
|
|
// admit it on flat regardless of DeckGL status.
|
|
assert.equal(isLayerExecutable('ciiChoropleth', 'flat', true), true);
|
|
// SVG fallback with ciiChoropleth: the renderer gate admits it
|
|
// because 'flat' is in its renderers list. CII-specific rendering
|
|
// is handled by whatever renders flat-mode layers — that's outside
|
|
// isLayerExecutable's scope. deckGLOnly is the only "needs DeckGL
|
|
// even on flat" signal.
|
|
assert.equal(isLayerExecutable('ciiChoropleth', 'flat', false), true);
|
|
assert.equal(isLayerExecutable('ciiChoropleth', 'globe', true), false,
|
|
'ciiChoropleth has no globe renderer');
|
|
});
|
|
|
|
test('dual-renderer layer admits both flat and globe', () => {
|
|
// `pipelines` has renderers:['flat', 'globe'] (default) — it
|
|
// renders on both flat DeckGL/SVG and globe mode.
|
|
assert.equal(isLayerExecutable('pipelines', 'flat', true), true);
|
|
assert.equal(isLayerExecutable('pipelines', 'flat', false), true);
|
|
assert.equal(isLayerExecutable('pipelines', 'globe', true), true);
|
|
assert.equal(isLayerExecutable('pipelines', 'globe', false), true);
|
|
});
|
|
|
|
test('unknown layer key returns false', () => {
|
|
// Typo or stale key -> must not accidentally pass the gate.
|
|
// @ts-expect-error — intentionally passing a key outside the union
|
|
assert.equal(isLayerExecutable('nonexistentLayer', 'flat', true), false);
|
|
});
|
|
});
|
|
|
|
describe('isLayerExecutable — matrix of renderer x DeckGL x deckGLOnly', () => {
|
|
// Exhaustive 2x2x2 matrix to lock down the truth table. Future edits
|
|
// to the predicate that accidentally widen the allowed set get
|
|
// caught here rather than in production.
|
|
const cases: Array<{
|
|
renderers: Array<'flat' | 'globe'>;
|
|
deckGLOnly: boolean;
|
|
renderer: 'flat' | 'globe';
|
|
isDeckGL: boolean;
|
|
expect: boolean;
|
|
why: string;
|
|
}> = [
|
|
// deckGLOnly:true — only flat + DeckGL active passes
|
|
{ renderers: ['flat'], deckGLOnly: true, renderer: 'flat', isDeckGL: true, expect: true, why: 'flat + DeckGL passes deckGLOnly' },
|
|
{ renderers: ['flat'], deckGLOnly: true, renderer: 'flat', isDeckGL: false, expect: false, why: 'flat + SVG fails deckGLOnly' },
|
|
{ renderers: ['flat'], deckGLOnly: true, renderer: 'globe', isDeckGL: true, expect: false, why: 'globe not in renderers list' },
|
|
{ renderers: ['flat'], deckGLOnly: true, renderer: 'globe', isDeckGL: false, expect: false, why: 'globe not in renderers list' },
|
|
// deckGLOnly:false/undefined — renderer list is the only gate
|
|
{ renderers: ['flat'], deckGLOnly: false, renderer: 'flat', isDeckGL: true, expect: true, why: 'flat-only layer on flat' },
|
|
{ renderers: ['flat'], deckGLOnly: false, renderer: 'flat', isDeckGL: false, expect: true, why: 'flat-only layer on SVG (no deckGLOnly requirement)' },
|
|
{ renderers: ['flat'], deckGLOnly: false, renderer: 'globe', isDeckGL: true, expect: false, why: 'flat-only layer rejects globe' },
|
|
// dual-renderer layers
|
|
{ renderers: ['flat', 'globe'], deckGLOnly: false, renderer: 'flat', isDeckGL: true, expect: true, why: 'dual-renderer on flat' },
|
|
{ renderers: ['flat', 'globe'], deckGLOnly: false, renderer: 'globe', isDeckGL: true, expect: true, why: 'dual-renderer on globe' },
|
|
];
|
|
|
|
for (const c of cases) {
|
|
test(`${c.why}`, () => {
|
|
// Pick a representative key matching the (renderers, deckGLOnly) shape.
|
|
// storageFacilities = ['flat'] + deckGLOnly:true
|
|
// ciiChoropleth = ['flat'] + deckGLOnly:undefined
|
|
// pipelines = ['flat','globe'] + deckGLOnly:undefined
|
|
let key: keyof typeof LAYER_REGISTRY;
|
|
if (c.deckGLOnly) key = 'storageFacilities';
|
|
else if (c.renderers.length === 1) key = 'ciiChoropleth';
|
|
else key = 'pipelines';
|
|
assert.equal(isLayerExecutable(key, c.renderer, c.isDeckGL), c.expect, c.why);
|
|
});
|
|
}
|
|
});
|