feat/ui-impact
needs attentionviewing older commit312a1f4 · incrementalpre-PRreviewed 2026-07-24 04:01 UTC1H · 4M · 4L · 3I- Purpose
- UI impact branch for the energía module — shipping a polished, demo-safe Demanda page with attributed color coding and real RPU wiring for the CFE aclaración letter generator.
- Goal
- Replace illustrative/hash-derived RPUs with real API RPUs in the aclaración letter, fix severity attribution so demand and FP are colored by their own driver (not OR'd into one opaque badge), and add a DriverLegend that explains every color and its action.
- Sub-goals
- SG-1: Real RPU from API flows to Finding view model and AclaracionSheet
- SG-2: Attributed color helpers + DriverLegend explain colors by driver
- SG-3: Severity ranking tracks worst driver separately per site
- SG-4: Sub-chart annotations and 'Ilustrativo' badges frame demo data clearly
- SG-5: Nav label updated to 'Demanda y FP' in both locales
- What
- Removed illustrativeRpu() hash function; added rpu?: string to Finding interface and rpu passthrough in toFindingView(); updated AclaracionSheet to use finding.rpu ?? 'por confirmar'; added demandTextColor/fpTextColor helpers, DriverLegend component, per-driver severity ranking, sub-chart annotations, Ilustrativo badges, and updated page title/description.
- Why
- Demo safety: the previous letter used a hash-derived fake RPU with a 'datos ilustrativos' disclaimer; the real RPU is now in the API so the letter can use it. UX clarity: a red badge on a site must always resolve to a single cause (demand rebase vs FP penalty), and the legend must name that cause and its corrective action.
- Areas
- apps/platform/src/app/[locale]/(dashboard)/energia+96−42apps/platform/src/messages+2−2
- Blast
- 6 files, +98/-44 lines; scope is fully contained to the energía Demanda UI and the Finding view model type. No backend, domain, or API contract changes.
Findings · 13
correctness2
avgFp3m NaN when site has no recent bills
apps/platform/src/app/[locale]/(dashboard)/energia/demanda/page.tsx:137
`avgFp3m = recentBills.reduce(...) / recentBills.length` produces NaN when recentBills is empty. Not triggered today on static mock data (all AXO sites have 24 billed periods), but if ever fed real API data a site with < 3 billed periods will silently produce NaN for avgFp3m, making every downstream comparison (`avgFp3m < 0.9`, fpTextColor, fpSeverity) return false. Fix: `recentBills.length > 0 ? sum / recentBills.length : 1.0`.
f.rpu ?? undefined is a no-op (rpu is non-nullable in FindingResponse)
apps/platform/src/app/[locale]/(dashboard)/energia/_lib/findingView.ts:150
FindingResponse.rpu is typed as `string` (never null), so `f.rpu ?? undefined` always evaluates to `f.rpu`. The nullish coalescing is harmless but misleading — implies the field could be undefined at runtime when the schema guarantees it cannot.
conventions4
demanda/page.tsx has zero i18n coverage — all display strings hardcoded Spanish
apps/platform/src/app/[locale]/(dashboard)/energia/demanda/page.tsx
Page title, subtitle, DriverLegend labels, sub-chart annotations, badge copy ('En vivo · ledger de hallazgos', 'Ilustrativo') are all hardcoded Spanish literals. The project convention requires all user-facing strings in messages/{locale}.json via next-intl. This commit adds several new hardcoded strings on an already-uncovered page. The nav label ('Demanda y FP') IS correctly in both locale files — the gap inside the page is more visible.
rpu comment in realData.ts ('from the API') is misleading against the SECURITY annotation
apps/platform/src/app/[locale]/(dashboard)/energia/_lib/realData.ts:44
The file's SECURITY header (lines 3-4) says it must contain NO real RPUs. The rpu?: string field is type-level only and PLACEHOLDER_FINDINGS never sets it — no real data ships in this file. But the comment '// the finding's real RPU (from the API)' implies the opposite to a future contributor, suggesting it's acceptable to populate rpu here. The comment should clarify that this field is only populated by findingView.ts (the API projection path), not by synthetic data.
Arbitrary Tailwind font-size values (text-[10px], text-[11px]) bypass the design-system scale
apps/platform/src/app/[locale]/(dashboard)/energia/demanda/page.tsx
TailwindCSS v4 with a design-system scale — arbitrary values circumvent consistent typography. Closest equivalent is text-xs (12px); if these micro-label sizes are intentional, add them to the theme config.
WHAT-style comments on storeId/storeName fields in realData.ts
apps/platform/src/app/[locale]/(dashboard)/energia/_lib/realData.ts:45
Comments '// details.storeId — links a consumption finding to its real series' and '// details.storeName — display label when present' describe what the fields are, not why a non-obvious decision was made. Project convention restricts comments to WHY reasoning.
tests4
toFindingView rpu passthrough not asserted in findingView.test.ts
apps/platform/src/app/[locale]/(dashboard)/energia/_lib/__tests__/findingView.test.ts:66
The existing test calls toFindingView() but only checks v.impactKind and v.title. The new rpu field is never asserted — a regression that drops or misnames it would be invisible. Add: expect(v.rpu).toBe('123456789012') plus a case with rpu: null to verify the ?? undefined coercion.
demandTextColor and fpTextColor branch logic untested
apps/platform/src/app/[locale]/(dashboard)/energia/demanda/page.tsx:38
Both are pure functions with distinct threshold boundaries (pct: 100, 85; fp: 0.9) but are unexported module-level consts inside a 'use client' page. Cheapest fix: extract to demanda/_lib/metrics.ts, export, and unit-test boundary values. The boundary at pct === 100 (not > 100) and pct === 85 are silent correctness questions that tests would pin.
buildSiteMetrics severity tie-break logic untested
apps/platform/src/app/[locale]/(dashboard)/energia/demanda/page.tsx:131
The rank comparison has three meaningful paths: demand wins outright, FP wins outright, and tie (both critico — rank equal, demand wins by >=). None is covered. Function is unexported; extraction to demanda/_lib/metrics.ts would allow unit testing.
illustrativeRpu deletion leaves no orphaned tests; buildLetter fallback untested
illustrativeRpu() had no tests before deletion — nothing to clean up. The new 'por confirmar' fallback path in buildLetter() (when finding.rpu is undefined) is also untested, but low urgency given the demo context.
improvement3
rank lookup object recreated inside the per-site loop — hoist to module level
apps/platform/src/app/[locale]/(dashboard)/energia/demanda/page.tsx:150
const rank: Record<Sev, number> = { critico: 2, atencion: 1, saludable: 0 } is allocated fresh inside the per-site map body of buildSiteMetrics(). It contains no site-specific data. Hoist to module level alongside demandTextColor/fpTextColor — consistent with the file's existing pattern for static helpers.
rpu: f.rpu ?? undefined — simplify to rpu: f.rpu
apps/platform/src/app/[locale]/(dashboard)/energia/_lib/findingView.ts:150
FindingResponse.rpu is string (non-optional). The ?? undefined is a no-op that misleads the reader. Write rpu: f.rpu to match the actual type contract.
Extract metrics helpers from page.tsx to testable module
apps/platform/src/app/[locale]/(dashboard)/energia/demanda/page.tsx
demandTextColor, fpTextColor, and the rank logic in buildSiteMetrics are pure functions co-located in a 'use client' page component. Moving them to demanda/_lib/metrics.ts would make them exportable and unit-testable without mounting the component. This also resolves the tests lens findings on the same functions.
History · 15 commits
- 96ca7d7needs attentionincremental0H · 9M · 12L2026-07-25 16:40
- 25516f7needs attentionincremental5H · 7M · 8L2026-07-25 03:29
- a7f8d64needs attentionincremental0H · 4M · 5L2026-07-25 01:56
- 7fc4ef0needs attentionincremental2H · 6M · 8L2026-07-24 21:04
- 027e5eaneeds attentionincremental4H · 9M · 8L2026-07-24 20:04
- 95a101eneeds attentionincremental3H · 6M · 5L2026-07-24 16:09
- 5d0d186needs attentionincremental1H · 3M · 5L2026-07-24 15:36
- c3c5121needs attentionincremental2H · 1M · 5L2026-07-24 15:17
- ded4e61needs attentionincremental2H · 3M · 9L2026-07-24 14:22
- 312a1f4needs attentionincremental1H · 4M · 4L2026-07-24 04:01current
- b48af56needs attentionincremental1H · 6M · 6L2026-07-24 03:14
- 6d07cc8needs attentionincremental2H · 4M · 5L2026-07-24 00:50
- 261b55eneeds attentionincremental5H · 11M · 6L2026-07-24 00:38
- 5b8a252needs attentionincremental3H · 6M · 9L2026-07-24 00:19
- f29cc5bneeds attentionfull9H · 17M · 11L2026-07-23 23:07