← all branches

feat/ui-impact

needs attentionviewing older commit
25516f7 · incrementalpre-PRreviewed 2026-07-25 03:29 UTC5H · 7M · 8L · 4I
The branch
Purpose
Entitlement-aware soft-launch gate + energia module UI — make the gate consult per-org module entitlements, extend to sites/assets modules, and build the full energia dashboard (findings, comparativo YoY, pagos, demanda, ESG).
Goal
Entitlement-aware gate live; energia module fully scaffolded with like-for-like YoY, anomaly detection, findings API, and CFE clarification workflow.
Sub-goals
  • SG-1: Domain + schema — ORG_MODULES + OrgModuleKeySchema widened
  • SG-2: soft-launch.ts entitlement-aware; guard + layout + sidebar wiring; admin toggles; tests
  • SG-3: Preview test bed (pending)
The changes (whole branch)
What
Fix commit addressing adversarial review findings: re-gates /energia (cross-tenant exposure), fixes NaN KPI from median([]), corrects bill supersession vs accumulation logic in calendarMonths, rewrites operacion_discontinua closure detection to use per-day intensity and contiguous runs, fixes resolveWindow to maximize comparable stores instead of latest-possible, makes exclusion reasons non-exclusive, removes hardcoded PERIODO from CFE letter, fixes 'Recuperado' to use verifiedMxn only, adds DEMO- provenance markers to synthetic data surfaces.
Why
Adversarial live review of the preview found cross-tenant data exposure, a NaN rendering on the portfolio KPI, and multiple places where the code made false claims (hardcoded periods, fabricated XML-validation claims for non-billing findings, estimated amounts labelled as verified recoveries).
Areas
apps/platform/src/app/[locale]/(dashboard)/energia+56000domains/utility/src/finding+28000apps/platform/src/api+4200apps/platform/src/lib+202packages/api/src/schemas+50packages/database+50
Blast
68 files, ~8500 net additions across the branch — primarily new energia module UI, finding domain entity (FCIS), finding API contract+handler+mapper, and soft-launch gate entitlement wiring.
real-customer-data-in-bundle security-gate-client-side-only no-anomaly-unit-tests
ci· No open PR — push-triggered review on pre-PR branchcoderabbit· No .coderabbit.yaml presenttypecheck· Not run in this review pass

Findings · 23

correctness4

high

Level-shift detection fires on recovering stores — incorrectly excludes them from YoY

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/_lib/likeForLike.ts:242

analyzeOne builds vals from [...w.cur, ...w.prev] — current-year months FIRST, prior-year SECOND. Then lo=median(first half)=current, hi=median(second half)=prior. The shifted check fires when min(lo,hi)/max(lo,hi) < 0.25, which triggers equally for 'store went dark in current year' AND 'store recovered (low prior, high current)'. A legitimately growing store is incorrectly labelled operacion_discontinua and excluded from the comparable set.

medium

Conflict-day perDay values are summed (double-counted) into MonthCoverage.value

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/_lib/likeForLike.ts:93

When two distinct periods claim the same day, byDay accumulates both perDay values. The resulting MonthCoverage.value is inflated. Currently safe because complete:false gates all downstream consumers, but any future reader of raw MonthCoverage.value on non-complete months gets a silently corrupted number.

low

Two median functions with divergent empty-input semantics across sibling files

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/_lib/likeForLike.ts

likeForLike.ts median() returns 0 for empty; anomaly.ts median() returns null. Both are correct in their current call sites, but a future refactor extracting a shared utility must pick one contract or will introduce a silent regression.

info

alternatives sort in resolveWindow is consistent — most-recent-first is correct intent

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/_lib/likeForLike.ts:164

Sort (a.end < b.end ? 1 : -1) puts newer ends first. Consistent with the primary sort's secondary criterion. Non-finding.

security5

high

Real Grupo Axo production consumption data embedded in client JS bundle — reachable by any authenticated user

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/_lib/realConsumption.ts

realConsumption.ts contains real kWh values, real MXN bill totals, period dates, and state-level geographic data for 24 stores pulled from production Supabase. It is imported by comparativo/page.tsx ('use client') and compiled into the browser bundle. The soft-launch gate prevents rendering, but cannot prevent bundle inspection. Any authenticated user can read this data via DevTools regardless of energia module entitlement.

medium

CFE letter template splices user-controlled fields without newline stripping

apps/platform/src/app/[locale]/(dashboard)/energia/_components/AclaracionSheet.tsx:56

buildLetter() interpolates finding.title, finding.desc, finding.rpu, and periodo directly into the letter body. finding.desc flows from the open details JSONB blob (storeName). A CRLF sequence in storeName could inject fabricated paragraphs into a downloadable document bearing the account's real RPU. Risk is limited to parties who can write to the findings details column, but no sanitization exists.

medium

SOFT_LAUNCH=true is a shared kill-switch — disabling it bypasses the energia gate for all orgs

apps/platform/src/lib/soft-launch.ts:34

Setting SOFT_LAUNCH=false lifts ALL module gates including energia. The module contains real customer data; it should arguably use a dedicated flag rather than sharing a kill-switch with commercial modules (sites, tarifas, assets) that carry no similar data-sensitivity.

low

storeName from untyped details JSONB blob spliced into letter without newline stripping

apps/platform/src/app/[locale]/(dashboard)/energia/_lib/findingView.ts

typeof storeName === 'string' guard exists but no length cap or newline stripping. Flows into finding.desc which appears in the CFE letter body.

info

Server-side ensureModuleEntitled correctly wired for findings API

apps/platform/src/api/handlers/finding.handler.ts

Both listFindingsHandler and getFindingHandler call ensureModuleEntitled('energia'). The live findings API is correctly gated server-side. The client-bundle exposure concern is specific to realConsumption.ts hardcoded demo data, not the API-backed findings.

conventions3

medium

StoreYoY.reason is a redundant derived field — no caller reads it, but dual-field structure invites bugs

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/_lib/likeForLike.ts:184

StoreYoY now carries both reason: ExclusionReason (first reason, for 'compact display') and reasons: ExclusionReason[]. No call site in comparativo/page.tsx reads r.reason — all display uses r.reasons.map(...). The field is dead weight; a future reader who reads reason and misses reasons will silently miss multi-reason stores.

info

No TEMP/FIXME/TODO markers found in any changed file

The two TEMP: energia live for preview demo comments were correctly removed. Clean.

info

LfLWindow.alternatives is typed and used consistently across type, implementation, and tests

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/_lib/likeForLike.ts

alternatives: { end: string; comparable: number }[] is defined, populated in resolveWindow, read in comparativo/page.tsx, and asserted in tests. No type inconsistency.

tests5

high

operacion_discontinua rewrite is entirely untested — contiguous-run, per-day intensity, and level-shift all unexercised

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/_lib/__tests__/likeForLike.test.ts

The closure-detection rewrite (per-day intensity, CLOSED_MIN_RUN contiguous months, lo/hi level shift) is one of the largest behavioral changes in the commit but no test in the suite triggers operacion_discontinua. The new logic could regress silently.

high

resolveWindow maximize-comparable behavior not pinned — test cannot distinguish from latest-wins

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/_lib/__tests__/likeForLike.test.ts:97

The new test only checks Array.isArray(w.alternatives) and that each alt.comparable <= chosen. On the real STORES data, the chosen window may also happen to be the latest, so the test cannot distinguish the new maximize algorithm from the old MIN_STORES_FOR_WINDOW=3 (latest-possible) one.

high

No anomaly.test.ts — all three anomaly.ts fixes (median null, isFinite guard, minPeers) have zero coverage

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/_lib/anomaly.ts

The median([])→NaN bug was the highest-visibility user-facing defect (rendered 'NaN kWh/día' on the portfolio KPI). The fix (return null on empty, Number.isFinite guards, peers.length < 2 early return) has no regression test. A unit test calling analyzeStore with < 2 peers would cover all three guards.

low

Accumulation test only verifies total kWh, not per-month distribution

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/_lib/__tests__/likeForLike.test.ts:250

The ACCUMULATES test asserts total ~= 6200 across all months but does not check that January and February each receive their correct allocated shares. A bug that distributes incorrectly but conserves the total would pass.

low

store() test helper return type is not annotated as Store — divergence risk if Store gains required fields

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/_lib/__tests__/likeForLike.test.ts:107

The helper returns an object literal, not typed as Store. If Store gains a required field the helper silently diverges and tests continue passing against a stale shape.

improvement6

medium

lo/hi variable names in level-shift detection are inverted relative to time order

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/_lib/likeForLike.ts:242

lo = median(vals.slice(0, half)) holds current-year values; hi holds prior-year. In a typical growth scenario lo > hi, opposite of the names. Logic is symmetric so no bug, but a future maintainer reading 'lo' and 'hi' will expect magnitude-ordered values and may introduce a directional bug.

medium

resolveWindow allows a 1-store comparable window — single-store portfolio delta is statistically meaningless

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/_lib/likeForLike.ts:159

scored.filter(s => s.n > 0) allows a window where exactly 1 store qualifies. The old MIN_STORES_FOR_WINDOW=3 was removed intentionally (misapplied), but no floor replaced it. The headline like-for-like delta shown to the user would be a single-store number presented as a portfolio metric.

medium

byPeriod supersession relies on store.series insertion order — undocumented invariant, fragile for real data

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/_lib/likeForLike.ts:77

byPeriod.set(key, p) last-write-wins on loop order. For hardcoded demo data this is deterministic, but when connected to live DB query results without an explicit ORDER BY issued_at, a withdrawn bill could beat its correction. MonthPoint has no issuance timestamp field. The old code sorted explicitly; the new code trusts upstream ordering silently.

low

periodo is redundant in AclaracionSheet useMemo dependency array

apps/platform/src/app/[locale]/(dashboard)/energia/_components/AclaracionSheet.tsx:84

periodo = finding.periodo ?? 'por confirmar' is derived from finding. Listing both [finding, periodo] double-counts the dep; ESLint exhaustive-deps will flag this. Change to [finding].

low

'Ilustrativo' badge copy-pasted in 4+ locations — missing shared component

apps/platform/src/app/[locale]/(dashboard)/energia/pagos/page.tsx

The badge <Badge variant='outline' className='text-[10px] font-normal text-muted-foreground'>Ilustrativo</Badge> appears verbatim in pagos/page.tsx, resumen/page.tsx, and likely others. A shared IlustrativoBadge component would make future wording changes a single-line edit.

low

energia in GREYED (shows as 'Pronto') vs HIDDEN — document the intent

apps/platform/src/lib/soft-launch.ts:60

The commit comment says the page 'must never be reachable by an unentitled org.' GREYED shows unentitled orgs a nav teaser, making the module discoverable. If intent is strict entitlement-only (invited demos), HIDDEN is more consistent with the data-sensitivity rationale. Document the product choice so the next person doesn't change it assuming it was an oversight.

History · 15 commits

  1. 96ca7d7needs attentionincremental0H · 9M · 12L2026-07-25 16:40
  2. 25516f7needs attentionincremental5H · 7M · 8L2026-07-25 03:29current
  3. a7f8d64needs attentionincremental0H · 4M · 5L2026-07-25 01:56
  4. 7fc4ef0needs attentionincremental2H · 6M · 8L2026-07-24 21:04
  5. 027e5eaneeds attentionincremental4H · 9M · 8L2026-07-24 20:04
  6. 95a101eneeds attentionincremental3H · 6M · 5L2026-07-24 16:09
  7. 5d0d186needs attentionincremental1H · 3M · 5L2026-07-24 15:36
  8. c3c5121needs attentionincremental2H · 1M · 5L2026-07-24 15:17
  9. ded4e61needs attentionincremental2H · 3M · 9L2026-07-24 14:22
  10. 312a1f4needs attentionincremental1H · 4M · 4L2026-07-24 04:01
  11. b48af56needs attentionincremental1H · 6M · 6L2026-07-24 03:14
  12. 6d07cc8needs attentionincremental2H · 4M · 5L2026-07-24 00:50
  13. 261b55eneeds attentionincremental5H · 11M · 6L2026-07-24 00:38
  14. 5b8a252needs attentionincremental3H · 6M · 9L2026-07-24 00:19
  15. f29cc5bneeds attentionfull9H · 17M · 11L2026-07-23 23:07