feat/energia-ui
needs attentionviewing older commitd563614 · incrementalPR #303reviewed 2026-07-13 04:06 UTC3H · 5M · 7L · 6I- Purpose
- Build and preview the Energia enterprise energy management module — a new product tier for multi-site corporate energy consumers in Mexico.
- Goal
- Demo-ready energy dashboard screens for enterprise prospects, showing real Grupo Axo and Tiendas Neto customer data with a live org switcher.
- Sub-goals
- SG-1: Module scaffold — per-org toggleable, greyed pre-launch (commit 3716a44a)
- SG-2: Port axo-rfp demo pages into energia route for preview (commit 30ee8dc9)
- SG-3: Reground money screens to minimal design + real Axo/Neto data + org switcher (commit d5636141)
- What
- Latest commit replaces ~2,700 lines of complex analytics built on axoMockData with ~800 lines of clean minimal UI. Introduces EnergiaOrgContext and OrgSwitcher for switching between two real customer orgs; adds realData.ts with hardcoded production figures; simplifies hallazgos, pagos, and resumen pages to consume real data.
- Why
- The axoMockData-based analytics showed synthetic data. Client demos need real impact numbers to close deals. This commit trades analytical depth for clarity and authenticity in the money screens.
- Areas
- apps/platform+4245−1packages/api+1−1domains/core+1−1
- Blast
- 23 files, +4247/-3 across 3 areas; entirely within apps/platform/src/app/(dashboard)/energia/ (21 files) plus minor touches to soft-launch.ts, DashboardSidebar.tsx, messages/*.json, organization.type.ts, and organization.schemas.ts.
Findings · 22
correctness6
Lifecycle stepper: en_proceso marks wrong step (i<=1 instead of i<=2)
apps/platform/src/app/[locale]/(dashboard)/energia/hallazgos/page.tsx:141
The `reached` expression for `en_proceso` is `i <= 1`, marking steps 0 and 1 ('Aclaracion generada') as complete. Step 2 is 'En seguimiento con CFE' — the exact phase en_proceso represents — yet it renders as an empty circle. Should be `i <= 2`. For Neto's featured finding (status en_proceso) this shows checkmark checkmark empty empty instead of correct checkmark checkmark checkmark empty — a visible error in the demo's primary CTA card.
Hardcoded 'Un solo sitio vencido' copy not data-driven
apps/platform/src/app/[locale]/(dashboard)/energia/pagos/page.tsx
CTA subtitle is hardcoded and only accurate for Axo (p.overdue === 1). Would be factually wrong if a new org has 0 or 2 overdues with !isUrgent.
Neto value bar fills ~98.7% — 507k MXN gap in data design
apps/platform/src/app/[locale]/(dashboard)/energia/resumen/page.tsx
For Tiendas Neto: recovered(53k) + inProcess(110k) + operational(14,850k) = 15,013k vs identified(15,520k). Three segments fill only 98.7%, leaving a visible muted gap. Cosmetic; legend values are correct.
pct() floor can cause segments to sum > 100% with extreme data
apps/platform/src/app/[locale]/(dashboard)/energia/resumen/page.tsx:16
Math.max(1.5, ...) applies a 1.5% floor per segment. If any two values are very small, three segments can sum > 100%. overflow-hidden prevents layout break but legend would misrepresent proportions. Latent with current data.
SVG gradient id='spendFill' is global — fragile if chart ever multi-instanced
apps/platform/src/app/[locale]/(dashboard)/energia/resumen/page.tsx:116
Static SVG gradient id is document-global. Not a bug in current single-chart layout, but would cause incorrect fills if rendered more than once simultaneously.
{featured && ...} guard correctly handles undefined cfeFindings[0]
apps/platform/src/app/[locale]/(dashboard)/energia/hallazgos/page.tsx
When cfeFindings is empty, cfeFindings[0] returns undefined. React JSX {undefined && ...} renders nothing. All featured.* refs are inside the guard. Correct.
security5
Real customer financials hardcoded in client-side JS bundle
apps/platform/src/app/[locale]/(dashboard)/energia/_lib/realData.ts
The file's own comment says 'INTERNAL: real customer data — do not expose outside the app,' but every consumer carries 'use client', so the entire ENERGIA_DATA object is bundled and readable in DevTools Sources. This includes Grupo Axo annual spend (45M MXN), month-by-month series, payment amounts, CFE finding values, carbon emissions, and fee structure; and Tiendas Neto: 309M MXN annual spend, 20.8M pending, 6.4M unknown-status payments, per-entity revenue splits, RENE obligation flag. Anyone with the preview URL can exfiltrate both clients' complete financial profiles. Fix: move real figures server-side (server component or API call gated on org membership), or replace with anonymized stand-ins in the client bundle.
Cross-tenant disclosure: org switcher has no auth or membership check
apps/platform/src/app/[locale]/(dashboard)/energia/_lib/orgContext.tsx
OrgSwitcher lets any authenticated user freely toggle between Grupo Axo and Tiendas Neto data with no org-membership check. In a multi-tenant SaaS this is a cross-tenant disclosure: a Neto employee could view Axo's energy spend, payment posture, and CFE dispute details, and vice versa. Even for an internal demo, if both clients log in to the preview they see each other's data by clicking the switcher.
Soft-launch gate bypassed — all authenticated users can reach /energia
apps/platform/src/lib/soft-launch.ts
The gate has a 'TEMP: energia live for preview demo — revert before merge' comment but is already commented out, making the route reachable for every authenticated user. If this branch merges without reverting, all production users get unrestricted access to both customers' real financial data. There is no CI check, lint rule, or test assertion that would catch a missed revert.
Confidential per-client pricing data exposed in bundle
apps/platform/src/app/[locale]/(dashboard)/energia/_lib/realData.ts
Bundle exposes each customer's negotiated per-site monthly fee (Axo: 130 MXN; Neto: 25 MXN), annual contract values (414,960 MXN and 554,100 MXN), and value-multiple figures (3.7x and 28x). Competitors or clients who inspect the bundle can learn Batu's pricing strategy and discount depth.
'revert before merge' comment is the only guard against prod data leak
apps/platform/src/app/[locale]/(dashboard)/energia/_lib/realData.ts:1
No CI check, lint rule, or test assertion catches a merge with real data still present. Consider a lint rule that fails if customer names appear in the client bundle, or replace real data with anonymized stand-ins.
conventions6
Em-dash in user-facing JSX copy (against ui-patterns rule)
apps/platform/src/app/[locale]/(dashboard)/energia/resumen/page.tsx
ui-patterns.md says no em dashes in user-facing copy. Found in resumen/page.tsx and pagos/page.tsx. Use a comma, colon, or en-dash instead.
Native <button> without type="button" in pagos
apps/platform/src/app/[locale]/(dashboard)/energia/pagos/page.tsx:147
Raw <button> without type="button" defaults to submit in form contexts and bypasses the shared <Button> component's focus ring and active scale. Use <Button variant="ghost" size="sm"> instead.
JSDoc comment on EnergiaHeader describes what not why
apps/platform/src/app/[locale]/(dashboard)/energia/layout.tsx:21
Per CLAUDE.md: only comment when WHY is non-obvious. '/** Demo header: active org + the customer switcher (Axo / Neto). */' is readable from the code. Remove it.
Button size=xs and Badge variant=warning/success are valid design-system variants
apps/platform/src/app/[locale]/(dashboard)/energia/hallazgos/page.tsx
size=xs is defined in packages/ui/src/components/button.tsx. Badge warning/success variants are documented and used throughout. No issue.
TailwindCSS v4 classes (border-l-red-500, from-emerald-500/[0.06]) are safe
apps/platform/src/app/[locale]/(dashboard)/energia/hallazgos/page.tsx
Severity border classes are full string literals in SEVERITY_BORDER record — Tailwind v4 content scanner detects them. Arbitrary opacity /[0.06] is valid v4 syntax. No purging risk.
No ADR-016 (FCIS) concerns — pure UI/data change
apps/platform/src/app/[locale]/(dashboard)/energia/
No imports from domain shells, queries, or decisions. All data consumed from _lib/realData.ts (hardcoded const). FCIS boundaries fully respected.
tests1
fmtMxn() branching logic worth a unit test before production promotion
apps/platform/src/app/[locale]/(dashboard)/energia/_lib/realData.ts
fmtMxn() has two code paths with numeric thresholds and edge cases. A short Vitest spec would lock the contract before reuse in production screens. Not a blocker for the demo branch.
improvement4
Goal-gradient bar JSX and pct() duplicated in resumen and hallazgos
apps/platform/src/app/[locale]/(dashboard)/energia/resumen/page.tsx:16
Identical pct() helper and the 20-line goal-gradient bar block appear verbatim in both pages. Extract pct as valueBarPct() in _lib/realData.ts and the bar into _components/ValueGradientBar.tsx. Removes ~45 lines of duplication.
Two parallel MXN formatters: fmtMxn and formatMxn from axoMockData
apps/platform/src/lib/demo/axoMockData.ts
axoMockData.ts exports formatMxn() (no compact option); _lib/realData.ts exports fmtMxn() (superset). demanda/ and sitios/ pages still use the axoMockData version. Unify on fmtMxn and remove the duplicate.
Lifecycle stepper reached logic has noisy '&& true'
apps/platform/src/app/[locale]/(dashboard)/energia/hallazgos/page.tsx:139
(featured.status === 'verificado' && true) is redundant. Cleaner: const reachedUpTo = featured.status === 'verificado' ? Infinity : featured.status === 'en_proceso' ? 2 : 0; const reached = i <= reachedUpTo;
All user-facing strings hardcoded Spanish — pre-launch i18n extraction needed
apps/platform/src/app/[locale]/(dashboard)/energia/resumen/page.tsx
Acceptable for demo branch. The layout nav already uses useTranslations('energia.nav'). All page copy will need extraction into messages/es.json before productionizing.