Layer Manager: flag a layer that has no data at the current time
Parent: #409
Motivation
Once the core stops requesting a time-enabled layer outside its declared data coverage (#409), a layer that is switched on can be absent from the map for a good reason. Without a signal, that looks exactly like a broken layer: the checkbox says on, the map shows nothing, and the user has no way to tell "no data on this day" from "the tiles failed". The layer needs to explain itself.
How it should work
A suppressed layer's row in the Layer Manager stays exactly as it is — no dimming, checkbox untouched, every other control in place — plus a small calendar warning icon beside the layer's name. It appears only on time-enabled layers, and only while the core is suppressing that layer.
Hovering or focusing the icon opens a popover that says No data at this time, the instant being asked for, and the coverage in words:
- continuous: "Data available 2020-01-01 to 2020-03-01", or with an open bound "Data available from 2020-01-01" / "Data available until 2020-03-01"
- sparse, one entry, named as precisely as it was listed: "Data available in 2020", "in March 2020", "on 2020-03-04", or "on 2020-03-04 at 14:30 UTC"
- sparse, several entries: "Data available for 12 listed periods, March 2020 to 2020-11-30", with the first and last each named at their own precision
Listed entries can be a year, a month, a day or an hour, and the core says which for each one, so the popover never guesses an entry's precision from how long its span is. All times are UTC. The instant is read when the popover opens, so it names the time being asked for now rather than the time of the last change.
The panel is a consumer. It reads the coverage record the core serves over the message bus and words it itself; it computes no coverage and reaches into no core state for it. The portable half of the plugin stays free of the host: it is handed what it renders, and never asks the bus itself.
Done when
Out of scope
Draft implementation plan — written as of 93c37cc on 2026-09-11. Rough guide; re-verify against latest code.
Depends on #409 (PR #421, branch feature/layer-data-coverage-request-gating); stack on it until it merges. Tasks 8–10 of the local plan docs/superpowers/plans/2026-09-09-layer-data-coverage-request-gating.md were written for the older Layers tool and are superseded by this section.
Current behavior
src/essence/Tools/LayerManager/ is a React plugin split in two: lib/ is the portable, host-free half (components, utilities, its own types.ts), and adapters/ plus MMGISLayerManagerAdapter.tsx talk to core over the bus. __tests__/libBoundary.spec.js fails any lib/ file that imports outside lib/, names an MMGIS module, or mentions mmgisAPI / mmgisglobal. Tests run under vitest.
The adapter builds rows in getVisibleLayersWithLegends (from layers:getAllConfigs and friends, keyed by layer UUID), refreshes on bus events via useMMGISEvent, and waits for layers:getAll before the first refresh. Each row is lib/geo/LayerLegend/LayerLegend.tsx; its popovers are FloatingPopovers portaled per row and controlled by isOpen. The row has no warning icon of any kind today. LayerManagerTool.destroy unmounts the React root, which releases every useMMGISEvent subscription.
The bus surface already exists on the #409 branch: layers:getDataCoverage (one layer by UUID or name, or all keyed by UUID) and layers:dataCoverageChanged, with typed wrappers mmgisGetDataCoverage, mmgisGetLayerDataCoverage, mmgisOnDataCoverageChanged and the LayerDataCoverage / CoverageSpan types in src/essence/Tools/_shared/adapters/mmgisAPI.ts.
Where the change lands & rough plan
- A pure wording function in
lib/ (e.g. lib/utils/dataCoverageWording.ts) — record in, { title, instant, coverage } out — with a spec. Because lib/ cannot import the shared adapter types, declare the structural coverage shape it needs in lib/types.ts. Sparse wording switches on each span's unit and formats its at at that precision (YYYY, MMMM YYYY, YYYY-MM-DD, YYYY-MM-DD HH:mm UTC); continuous wording uses start/end, treating ±Infinity as open.
lib/: Layer gains an optional coverage field; LayerLegend renders a calendar warning icon beside the title while it says out-of-range, with a hover/focus FloatingPopover. The instant is fetched on open through a prop callback (same shape as canZoomToLayer), so lib/ stays host-free. Styles in lib/styles/components-geo/layer-legend.scss.
- Adapter: fetch the bulk coverage map with the row data so an already-suppressed layer shows on first render; subscribe to
layers:dataCoverageChanged with a stable handler and patch that layer's row; supply the on-open callback via mmgisGetLayerDataCoverage. Show the icon only where the record says outOfDataRange on a time-enabled layer.
tests/unit/dataCoverageBoundary.spec.js (or alongside libBoundary.spec.js): walks src/essence/Tools/ and fails on any import of layerDataCoverage or any read of layers.dataCoverage.
⚠️ Gotcha: the coverage record's requestedWindow is stored on every time step but the change event fires only when the verdict or coverage changes, so a popover that cached the event payload would name a stale instant. Read the record on show.
⚠️ Gotcha: lib/ must not reach the bus or import the shared mmgisAPI types — libBoundary.spec.js enforces it. Anything the row needs from core arrives as a prop or a callback.
References
Layer Manager: flag a layer that has no data at the current time
Parent: #409
Motivation
Once the core stops requesting a time-enabled layer outside its declared data coverage (#409), a layer that is switched on can be absent from the map for a good reason. Without a signal, that looks exactly like a broken layer: the checkbox says on, the map shows nothing, and the user has no way to tell "no data on this day" from "the tiles failed". The layer needs to explain itself.
How it should work
A suppressed layer's row in the Layer Manager stays exactly as it is — no dimming, checkbox untouched, every other control in place — plus a small calendar warning icon beside the layer's name. It appears only on time-enabled layers, and only while the core is suppressing that layer.
Hovering or focusing the icon opens a popover that says No data at this time, the instant being asked for, and the coverage in words:
Listed entries can be a year, a month, a day or an hour, and the core says which for each one, so the popover never guesses an entry's precision from how long its span is. All times are UTC. The instant is read when the popover opens, so it names the time being asked for now rather than the time of the last change.
The panel is a consumer. It reads the coverage record the core serves over the message bus and words it itself; it computes no coverage and reaches into no core state for it. The portable half of the plugin stays free of the host: it is handed what it renders, and never asks the bus itself.
Done when
Out of scope
Draft implementation plan — written as of 93c37cc on 2026-09-11. Rough guide; re-verify against latest code.
Depends on #409 (PR #421, branch
feature/layer-data-coverage-request-gating); stack on it until it merges. Tasks 8–10 of the local plandocs/superpowers/plans/2026-09-09-layer-data-coverage-request-gating.mdwere written for the older Layers tool and are superseded by this section.Current behavior
src/essence/Tools/LayerManager/is a React plugin split in two:lib/is the portable, host-free half (components, utilities, its owntypes.ts), andadapters/plusMMGISLayerManagerAdapter.tsxtalk to core over the bus.__tests__/libBoundary.spec.jsfails anylib/file that imports outsidelib/, names an MMGIS module, or mentionsmmgisAPI/mmgisglobal. Tests run under vitest.The adapter builds rows in
getVisibleLayersWithLegends(fromlayers:getAllConfigsand friends, keyed by layer UUID), refreshes on bus events viauseMMGISEvent, and waits forlayers:getAllbefore the first refresh. Each row islib/geo/LayerLegend/LayerLegend.tsx; its popovers areFloatingPopovers portaled per row and controlled byisOpen. The row has no warning icon of any kind today.LayerManagerTool.destroyunmounts the React root, which releases everyuseMMGISEventsubscription.The bus surface already exists on the #409 branch:
layers:getDataCoverage(one layer by UUID or name, or all keyed by UUID) andlayers:dataCoverageChanged, with typed wrappersmmgisGetDataCoverage,mmgisGetLayerDataCoverage,mmgisOnDataCoverageChangedand theLayerDataCoverage/CoverageSpantypes insrc/essence/Tools/_shared/adapters/mmgisAPI.ts.Where the change lands & rough plan
lib/(e.g.lib/utils/dataCoverageWording.ts) — record in,{ title, instant, coverage }out — with a spec. Becauselib/cannot import the shared adapter types, declare the structural coverage shape it needs inlib/types.ts. Sparse wording switches on each span'sunitand formats itsatat that precision (YYYY,MMMM YYYY,YYYY-MM-DD,YYYY-MM-DD HH:mm UTC); continuous wording usesstart/end, treating ±Infinity as open.lib/:Layergains an optional coverage field;LayerLegendrenders a calendar warning icon beside the title while it says out-of-range, with a hover/focusFloatingPopover. The instant is fetched on open through a prop callback (same shape ascanZoomToLayer), solib/stays host-free. Styles inlib/styles/components-geo/layer-legend.scss.layers:dataCoverageChangedwith a stable handler and patch that layer's row; supply the on-open callback viammgisGetLayerDataCoverage. Show the icon only where the record saysoutOfDataRangeon a time-enabled layer.tests/unit/dataCoverageBoundary.spec.js(or alongsidelibBoundary.spec.js): walkssrc/essence/Tools/and fails on any import oflayerDataCoverageor any read oflayers.dataCoverage.References
src/essence/Tools/_shared/adapters/mmgisAPI.tscanZoomToLayerinLayerLegend.tsxandMMGISLayerManagerAdapter.tsx