feat/atlas-sat
needs attentionviewing older commit744d655 · incrementalPR #353reviewed 2026-07-28 02:04 UTC0H · 1M · 5L · 8I- Purpose
- Public atlas of Mexico's energy infrastructure (generation plants, transmission lines, interconnections) embedded as an iframe on batuenergy.com/atlas. Serves siting/market research for solar integrators.
- Goal
- Add satellite basemap toggle + searchable owner combobox to improve atlas usability
- Sub-goals
- SG-1: Satellite basemap toggle (Esri World Imagery with CARTO label overlay + 25% scrim)
- SG-2: Searchable owner/group combobox replacing plain <select> (79 owners, 51 with 1 plant)
- SG-3: Attribution control — moved from top-left to bottom-right, collapsed by default
- What
- Single file changed (apps/web/public/atlas/index.html, +87/-7 over the whole branch). Added: Esri raster source, basemap segment control, CARTO label preservation on satellite, scrim overlay, original-visibility restore on switch-back; replaced owner <select> with custom searchable combobox with keyboard nav and NFD normalization; attribution moved to bottom-right and collapsed on load.
- Why
- Satellite view gives substantive siting context (terrain, existing plants, industrial footprint) under grid layers. Owner filter was unusable with 79 entries and no search — 51 owners have a single plant so the tail was unreachable.
- Areas
- apps/web/public/atlas/index.html+87−7
- Blast
- 1 file, +87/-7. No dependencies, no data files, no backend changes. Risk is confined to the atlas iframe.
Findings · 13
correctness4
Attribution collapse: querySelector may run before MapLibre applies compact-show class
apps/web/public/atlas/index.html
The block `{const _a=document.querySelector('.maplibregl-ctrl-attrib');if(_a)_a.classList.remove('maplibregl-compact-show');}` runs synchronously after addControl(). While addControl() inserts the control element synchronously, MapLibre may apply `maplibregl-compact-show` via a ResizeObserver callback (async). If so, the classList.remove fires before the class exists — the guard `if(_a)` masks the failure silently. A safer placement is inside a `map.once('load', …)` or as the first step of the existing _initFn callback. If this works today it's because the class is added synchronously on compact:true — but this is an undocumented timing dependency.
Enter with no highlighted item picks first owner (items[1]), not Todos (items[0])
apps/web/public/atlas/index.html
In onkeydown: `pick(hl>=0?hl:(items.length>1?1:0))`. When hl=-1 and multiple items exist, fallback is index 1 (first real owner) not index 0 (Todos/empty). A user who opens the list and immediately presses Enter expects to see 'Todos' selected (clearing the filter), not the top owner. Fallback should be 0.
NFD diacritic regex uses raw combining-char literal instead of \u0300-\u036f
apps/web/public/atlas/index.html
norm() uses `/[\u0300-\u036f]/g` written as invisible combining characters in the source literal. If the file is re-saved by an editor that normalizes or re-encodes the source, the range could silently break and diacritic stripping stops working. Prefer the explicit `/[\u0300-\u036f]/g` form.
Reset button does not close the combobox dropdown if open
apps/web/public/atlas/index.html
The reset onclick clears f_grp_in and f.own but does not call close(). If the dropdown is open when reset fires, it stays open showing the full unfiltered list.
security2
esc() escapes double-quotes only; latent risk if template switches to single-quoted attrs
apps/web/public/atlas/index.html
esc() escapes &, <, >, " but not '. Current templates use double-quoted HTML attributes, so this is safe. No active vulnerability, but a note for future template changes.
data-i converted with unary + without bounds check
apps/web/public/atlas/index.html
lst.onmousedown reads `+d.dataset.i`. data-i is only written by the render() function with the loop index (always a non-negative integer), so there is no injection path. Using parseInt(d.dataset.i, 10) with a bounds check would be more defensive but is not required.
conventions3
Combobox clear button is a <span> with no role, tabindex, or aria-label
apps/web/public/atlas/index.html
The `×` clear button (`<span class="cb-x">`) has only a `title="Limpiar"` tooltip. WAI-ARIA patterns require interactive controls to be reachable by keyboard and announced by screen readers. It should be `<button aria-label="Limpiar">` or at minimum `<span role="button" tabindex="0" aria-label="Limpiar">`.
aria-activedescendant not updated on keyboard navigation
apps/web/public/atlas/index.html
The combobox input has `role="combobox"` and `aria-controls="f_grp_list"` (listbox), but when ArrowDown/Up moves the highlight (hl), `aria-activedescendant` on the input is never set to point to the highlighted option's id. Screen readers won't announce the active item. Fix: add `id="f_grp_opt_N"` to each rendered option div and set `inp.setAttribute('aria-activedescendant', 'f_grp_opt_'+hl)` on navigation.
Future data sources without built-in attribution must be added back to customAttribution
apps/web/public/atlas/index.html
OSM/CARTO/Esri were correctly removed from customAttribution since MapLibre emits them from each source. But this is an implicit convention: any new data source added without a source-level attribution field must have its credit explicitly added to customAttribution to maintain legal compliance.
tests1
No automated tests for combobox keyboard navigation or NFD normalization
apps/web/public/atlas/index.html
Acceptable for a standalone static asset. If a Playwright harness ever covers apps/web/public, ArrowDown/Enter and 'garcia'→'García' matching are the highest-value cases to add.
improvement2
onfocus always opens with empty query, discarding display text context
apps/web/public/atlas/index.html
inp.onfocus calls open('') unconditionally. When an owner is already selected and the user refocuses, the full unfiltered list opens. Consider open(f.own||'') to pre-filter to the active selection, giving the user context about what's already selected.
norm() called on all 79 owners on every keystroke — could be cached
apps/web/public/atlas/index.html
Negligible perf impact at 79 items, but pre-computing `const ownersNorm = owners.map(norm)` once after owners is built makes the intent clearer and removes ~79 string allocations per keystroke.
seo1
Attribution now injected by map library — verify runtime rendering compliance
apps/web/public/atlas/index.html
OSM/CARTO/Esri credits removed from hardcoded HTML and now expected from MapLibre source attribution. File is an iframe embed (not crawlable), so SEO impact is nil. Worth confirming the attributions visually appear in the rendered ⓘ panel for legal compliance (OSM ODbL, Esri ToU).