← all branches

claude/vigorous-stonebraker-41cc2d

needs attentionviewing older commit
d28c6da · incrementalpre-PRreviewed 2026-07-10 16:59 UTC0H · 3M · 10L · 3I
The branch
Purpose
Atlas is a public-facing data visualization map for Mexico's energy grid (CFE network, distributed generation, tariffs). This branch extends the 'Evolución' panel to support a rich DG (generación distribuida) sub-panel.
Goal
Add multi-dimensional DG visualization: MW/contracts/avg-system-size metrics, cumulative vs annual mode, size-band breakdown (bar chart) and region/state map breakdown — giving users a complete picture of Mexico's solar DG deployment evolution.
Sub-goals
  • SG-1: Add DG panel HTML controls (break/metric/mode/state selectors)
  • SG-2: Implement DG computation helpers (dgSizeVal, dgStateVal, dgNatAvg, etc.)
  • SG-3: Implement drawDGSizeChart and drawDGRegionChart with interactive bar charts
  • SG-4: Wire region map coloring for DG mode
  • SG-5: Update popup handler to show DG-appropriate units
The changes (whole branch)
What
Replaced the single drawDGSize() function (annual MW only) with a full DG sub-system: EVO state expanded with dgBreak/dgMetric/dgMode/dgBand/dgState; 15 new helper functions; buildDG() dispatcher; two new draw functions for size and region charts; HTML controls for all new dimensions; popup handler updated.
Why
Previous DG visualization only showed annual MW additions by size band. The new panel exposes 3 metric types × 2 modes × 2 breakdown views = 12 chart configurations, matching the depth of the users/energy panels.
Areas
apps/web/public/atlas/index.html+12838apps/web/public/atlas/data/atlas.json+11
Blast
2 files, +129/−39 total. Fully self-contained in the Atlas static HTML visualization — no TypeScript, no Next.js, no API changes. Zero impact on platform or backend.
standalone-html data-viz no-tests xss-pattern-present
ci· no PR — CI not triggeredcoderabbit· no .coderabbit.yaml in repo

Findings · 17

correctness3

medium

Negative kW/system in annual avg mode renders point outside SVG viewBox

apps/web/public/atlas/index.html

dgStateVal() returns raw dm/dc*1000 in annual avg mode even when dm<0 (MW decreased due to retroactive data corrections). The Y function Y=v=>DGmT+DGph-v/max*DGph places negative values above the chart's top edge (y > DGH=190), so they fall outside the viewBox and disappear, creating a misleading visual drop toward zero. dgNatAvgState has the same issue — dm=mw-pmw is not clamped. Fix: return Math.max(0, dm/dc*1000) or null when dm<0.

low

drawDGRegionMap ignores its keys parameter — implicit coupling to EVO.idx

apps/web/public/atlas/index.html

drawDGRegionMap(keys) receives a keys array but never uses it; all lookups go through dgStateVal(nm, EVO.idx) which reads EVO.data.dgstate.periods internally. Not a crash today since EVO.idx is clamped before the call and buildDG always passes dgstate.periods. But a future caller passing a different keys array expecting control over the displayed period will be silently ignored. Remove the parameter or use it.

low

regs.indexOf(r) in inner loop — color mismatch if region names contain duplicates

apps/web/public/atlas/index.html

In drawDGRegionChart, regs.indexOf(r) is called inside regs.forEach() for both bar color and legend color. If two regions ever share a name, indexOf always returns the first index for both, causing the second region's bars to get the wrong color while the legend has the correct one. Fix: capture the forEach callback index (r, ri) and use ri directly.

security4

medium

XSS via unsanitized GeoJSON properties in map popups (new evo-fill handler)

apps/web/public/atlas/index.html

The new evo-fill click handler interpolates p.name (from GeoJSON feature properties) directly into setHTML() without escaping. Pre-existing handlers do the same for demand, generation, and tariff popups. Data comes from static same-origin JSON files today, but ATLAS_DEMAND_URL and ATLAS_PND_URL are fetched from raw.githubusercontent.com — a supply-chain compromise of those files or the atlas-data branch could inject arbitrary HTML into popups. Fix: escape p.name and all string properties before interpolation into setHTML.

medium

XSS via unsanitized JSON keys inserted into dropdown innerHTML

apps/web/public/atlas/index.html

State names from dg_by_state.json (new in this commit), zone names from demand JSON, and other JSON-keyed strings are inserted into <select> innerHTML via template literals without escaping. A malicious JSON value like '</option><script>...' would execute. The state dropdown populates from Object.keys(EVO.data.dgstate.byState) — those are static data today, but the pattern is fragile. Fix: build option elements via document.createElement/textContent, or escape <, >, &, " before inserting.

low

External cross-origin fetches (GitHub raw) without integrity verification

apps/web/public/atlas/index.html

ATLAS_DEMAND_URL and ATLAS_PND_URL point to raw.githubusercontent.com and are fetched without SRI or signature verification. A compromise of the batuenergy/atlas-sen data branch would allow injecting values into rendered HTML. Lower severity since the repo is trusted infrastructure, but worth noting as a persistent external dependency for an HTML file that renders fetched strings into the DOM.

low

SVG innerHTML injection pattern is fragile — region name strings not escaped

apps/web/public/atlas/index.html

evoLegend() builds <span> elements with region names from dgstate JSON and sets them via echart.innerHTML. If a region name contained </span><img src=x onerror=...> it would execute. Values are currently static data, but the pattern has no escape layer. SVG innerHTML also permits <script> and inline handlers in some browser contexts.

conventions4

medium

DGph/DGpw use literal arithmetic instead of named constants

apps/web/public/atlas/index.html

const DGph=190-10-18 and DGpw=860-46-12 repeat the raw numbers rather than referencing DGH, DGmT, DGW, DGmL, DGmR. The literal 18 is a bottom margin with no named constant (DGmB never defined). The existing demand chart uses ph=Hh-mT-mB with all four symbols in scope — the same pattern should apply here. If DGH or any margin is adjusted, DGph/DGpw silently become stale.

low

DG_REGPAL breaks the EVO_ prefix convention for EVO-panel constants

apps/web/public/atlas/index.html

All sibling constants are EVO_-prefixed (EVO_TF, EVO_SZ, EVO_SZC, EVO_RAMP, EVO_UNIT). The new region palette DG_REGPAL uses a different prefix. Rename to EVO_REGPAL to stay consistent.

low

e_metric handler not updated to use the new setSeg helper

apps/web/public/atlas/index.html

The new setSeg(id,b) helper is used for e_break, e_dgmetric, and e_mode, but the pre-existing e_metric handler still uses the old inlined toggle pattern. This leaves an inconsistency — a future maintainer is unsure which idiom is canonical.

low

regs.indexOf(r) called twice per region — forEach index already available

apps/web/public/atlas/index.html

regs.forEach(r => { ... DG_REGPAL[regs.indexOf(r)%DG_REGPAL.length] ... }) calls indexOf inside the callback when (r, ri) => { DG_REGPAL[ri%DG_REGPAL.length] } would be O(1) and match the forEach((sz, i)) pattern already used in the size-stacked-bar path.

tests3

low

Annual-mode delta not clamped in dgNatAvgState — negative national average silently masked

apps/web/public/atlas/index.html

dgNatAvgState computes dm = mw - pmw without Math.max(0,...). A data revision reducing national total MW in a period produces a negative dm, and therefore negative kW/system for that period. The SVG rect height is clamped via Math.max(0.4,...) so it renders as a near-zero bar, hiding the underlying negative value. dgSizeVal has the clamp; dgNatAvgState should match.

low

Division-by-zero guard pattern duplicated across three functions with different logic

apps/web/public/atlas/index.html

dgNatAvg, dgNatAvgState, and dgStateVal each implement the divide-by-contracts guard with slightly different conditions (dc>0 vs truthy c vs truthy s[key].contratos). No test validates all three return null (not NaN) on dc=0. A future edit may break one while the others pass visual inspection.

info

Pure computation functions are extraction candidates for unit testing

apps/web/public/atlas/index.html

dgSizeVal, dgNatAvg, dgStateVal, dgRegionVal, dgNatAvgState are pure computations over a data structure with no DOM access. They could be extracted to atlas-dg.js and tested with a simple Vitest fixture. The 12 logical branches (3 metric types × 2 modes × 2 break types) make manual correctness verification unreliable as the dataset evolves.

improvement3

low

Dropdown re-init guard may not re-apply selected value on re-entry

apps/web/public/atlas/index.html

buildDG() sets stSel.value=EVO.dgState inside the options.length<=1 guard. On subsequent calls (e.g. switch away from region break and back), the guard skips re-population and therefore also skips re-asserting stSel.value. Works today because onchange keeps EVO.dgState in sync, but silently breaks if EVO.dgState is reset externally. Move stSel.value=EVO.dgState outside the guard.

info

Shared chart setup duplicated verbatim in drawDGSizeChart and drawDGRegionChart

apps/web/public/atlas/index.html

Both functions open with identical const n=keys.length,cur=Math.min(EVO.idx,n-1),slot=DGpw/n,cx=i=>DGmL+(i+0.5)*slot,bw=Math.max(2,slot*0.62). A dgChartSetup(keys) helper returning {n,cur,slot,cx,bw} would ensure a change to the bar-width coefficient (0.62) applies to both charts and reduce noise in both functions.

info

statesInRegion called on every dgRegionVal invocation — pre-computable

apps/web/public/atlas/index.html

drawDGRegionChart computes series[r]=keys.map((k,i)=>dgRegionVal(r,i)), and dgRegionVal calls statesInRegion(r) each time. statesInRegion does Object.keys(B).filter(...) — a full scan of all states. Pre-computing a regionToStates map once per drawDGRegionChart call would remove n_regions×n_periods redundant scans. Not a real perf issue at current data sizes, but it's free to fix.

History · 10 commits

  1. a081002safeincremental0H · 0M · 0L2026-07-14 01:23
  2. 64dc83dneeds attentionincremental0H · 1M · 7L2026-07-13 20:03
  3. ec4eea3needs attentionincremental1H · 3M · 7L2026-07-13 18:42
  4. f4b4129safeincremental0H · 0M · 3L2026-07-13 04:34
  5. 8ea0c8asafeincremental0H · 0M · 3L2026-07-13 04:21
  6. 37ce28asafeincremental0H · 2M · 3L2026-07-10 17:22
  7. d28c6daneeds attentionincremental0H · 3M · 10L2026-07-10 16:59current
  8. 754a65dneeds attentionincremental0H · 3M · 4L2026-07-10 16:39
  9. a9190b2safeincremental0H · 0M · 5L2026-07-10 16:14
  10. 0bf12a0blockedfull1H · 2M · 4L2026-07-10 16:06