feat/energia-ui
needs attentionviewing older commit30ee8dc · fullPR #303reviewed 2026-07-13 03:43 UTC5H · 9M · 10L · 7I- Purpose
- Port the axo-rfp demo pages into the energia route so the 7-screen energía module runs as real Next.js pages in a Vercel preview — enabling an RFP demo for Grupo Axo without needing to seed an entitled org.
- Goal
- 7-screen energía module (resumen, hallazgos, demanda, pagos, conciliación, esg, medición, sitios) mounted as real Next.js pages under /energia/* with a shared sub-nav, using illustrative mock data from the axoMockData dataset.
- Sub-goals
- SG-1: Wire energia as a real OrgModuleKey (domain type + API schema + admin panel toggle)
- SG-2: New route tree — layout with sub-nav + 7 page files with recharts + @batu/ui + mock data
- SG-3: TEMP soft-launch bypass — energia removed from GATED_PATH_MODULES so any authenticated user can reach the preview (marked for revert before merge)
- SG-4: i18n nav keys in en.json + es.json; sidebar entry wired
- What
- Added 'energia' to ORG_MODULES + OrgModuleKeySchema (domain + API schema lockstep). Built a 7-page route tree under /energia/* with layout, sub-nav, and mock data pages. Added DashboardSidebar wiring, admin panel toggle, and i18n nav keys. TEMP: two soft-launch.ts edits force energia live for all authenticated users on this branch.
- Why
- RFP demo for Grupo Axo — needed the feature running live in-app on a Vercel preview without seeding a prod org. Stacked on feat/mod-gates (#299) which established the per-org module entitlement system.
- Areas
- apps/platform/energia route tree (8 files)+4414−0apps/platform/lib/demo (3 files)+651−0apps/platform/lib/soft-launch + tests+17−2apps/platform/components + messages (4 files)+38−0domains/core + packages/api (2 files)+2−2
- Blast
- 23 files, +5358/-3. Concentrated in apps/platform energia route tree (new) and demo lib (new). Only 4 files modified: soft-launch.ts (+2/-2), soft-launch.test.ts (+15/-1), organization.type.ts (+1/-1), organization.schemas.ts (+1/-1).
Findings · 23
correctness4
TEMP bypass: isGated('/energia') returns false but tests assert true
apps/platform/src/lib/soft-launch.ts:50
The TEMP edit comments out the `['/energia', 'energia']` entry from GATED_PATH_MODULES, so isGated('/energia') returns false for all callers including SoftLaunchGuard. The test at soft-launch.test.ts:35 asserts `isGated('/energia').toBe(true)` — it will FAIL against the current code. Intentional for preview, but CI is running a broken test suite.
TEMP bypass: navState('energia') returns 'live' but tests assert 'greyed'
apps/platform/src/lib/soft-launch.ts:58
'energia' is absent from GREYED, so navState('energia') falls through to the default 'live'. Tests at soft-launch.test.ts:100 and the nav↔route invariant block assert 'greyed' — both fail against the current code.
Unentitled orgs can reach /energia via direct URL (soft-launch gate bypassed)
apps/platform/src/lib/soft-launch.ts:44
With energia absent from GATED_PATH_MODULES, SoftLaunchGuard does not redirect unentitled orgs. Any authenticated user can access /energia directly. This is intentional for the preview demo (mock data only, no real data exposed), but must be reverted before merge.
navState('energia')='live' makes energia invisible in sidebar for unentitled orgs
apps/platform/src/components/DashboardSidebar.tsx:129
Because navState returns 'live', requiredModule='energia' is set, and the sidebar filter drops the item for orgs without the entitlement — so unentitled orgs see NO energia entry (not even a greyed teaser). The route is still reachable via URL.
security2
All /energia/* routes have no server-side auth guard
apps/platform/src/app/[locale]/(dashboard)/energia/layout.tsx:1
Every energia page is 'use client' and relies entirely on client-side SoftLaunchGuard + dashboard layout redirect for auth. This matches the existing (dashboard) pattern and only serves mock data today. Flag so it is not forgotten when real data is wired — server-side auth guard will be required then.
PapaParse error.message interpolated into UI state
apps/platform/src/app/[locale]/(dashboard)/energia/pagos/conciliacion/page.tsx:106
Error message is set into a React text state and rendered as text node (not innerHTML) — no XSS risk. PapaParse error messages are generic strings in practice. Low severity.
conventions6
4 pages use next/link + manual /${locale} prefix instead of @/i18n/navigation
apps/platform/src/app/[locale]/(dashboard)/energia/resumen/page.tsx:4
resumen, pagos, sitios, and pagos/conciliacion import Link from 'next/link' and manually prefix hrefs with `/${locale}/…`. Convention is `import { Link } from '@/i18n/navigation'` with locale-free paths (as correctly done in energia/layout.tsx). These 4 pages bypass next-intl routing and are fragile if locales are added.
All energia page body strings are hardcoded Spanish — not i18n-ready
apps/platform/src/app/[locale]/(dashboard)/energia/resumen/page.tsx:205
Every page body renders inline Spanish string literals. Only energia/layout.tsx uses useTranslations. Acceptable for a preview branch, but the entire surface needs extraction before production.
lib/demo/ is a shared location for route-scoped demo utilities
apps/platform/src/lib/demo/TimeFrameControl.tsx:1
axoMockData.ts, TimeFrameControl.tsx, and timeFrame.ts are consumed exclusively by the energia route tree. Convention puts route-scoped utilities in route/_lib/. Acceptable here as a quarantine zone for deletion-bound demo assets.
JSX structural comments explain the obvious in layout.tsx
apps/platform/src/app/[locale]/(dashboard)/energia/layout.tsx:43
{/* Navigation Tabs */} and {/* Page Content */} are what-comments — convention allows only why-comments.
Multiple what-comments across energia page files
apps/platform/src/app/[locale]/(dashboard)/energia/resumen/page.tsx:88
Lines 88, 95, 114, 132+ are what-comments restating what the code below obviously does. Similar patterns in hallazgos and demanda. Low severity on demo code.
layout.tsx has a multi-paragraph JSDoc
apps/platform/src/app/[locale]/(dashboard)/energia/layout.tsx:20
The EnergiaLayout JSDoc (lines 20–26) is multi-paragraph explaining implementation detail. Convention says no multi-paragraph docstrings.
tests4
soft-launch tests are broken by the TEMP bypass
apps/platform/src/lib/__tests__/soft-launch.test.ts:35
Tests at lines 35, 89–93, 100, and the full nav↔route invariant block (lines 124–156) assert energia IS gated/greyed. With the TEMP bypass active, these fail. CI is running a broken suite on this branch.
reconcile.ts has no unit tests
apps/platform/src/app/[locale]/(dashboard)/energia/pagos/conciliacion/_lib/reconcile.ts:53
reconcile() is a pure function with non-trivial logic: three-way status classification, ±$1 float tolerance boundary, sort ordering, and summary aggregation. Neither reconcile() nor exportDiscrepanciesCsv() has a test file. The ±$1 tolerance is a product decision with real edge cases worth pinning.
timeFrame.ts pure utilities untested
apps/platform/src/lib/demo/timeFrame.ts:32
currentWindow(), compareWindow(), sumInWindow(), pctChange(), describeWindow() are pure with deterministic output and edge cases worth pinning (pctChange(x, 0) → null, ytd slice, startIdx clamped). Low severity — demo code.
axoMockData.ts deterministic generators untested
apps/platform/src/lib/demo/axoMockData.ts:89
Seeded RNG guarantees stable output — snapshot tests would catch regressions cheaply. Acceptable to skip given the PR's deletion intent.
improvement7
reconcile.ts types are entangled with demo types — not a clean swap for real data
apps/platform/src/app/[locale]/(dashboard)/energia/pagos/conciliacion/_lib/reconcile.ts:1
reconcile() accepts AxoBill/AxoSite as parameter types. When wiring real data the function needs a type-signature rewrite, not just an import swap. The algorithm is worth keeping; generalize the type signature or move to domains/utility for the next iteration.
soft-launch.ts TEMP comments are the only merge guard — easy to miss
apps/platform/src/lib/soft-launch.ts:50
Two TEMP comments mark what must be reverted before any merge. If merged as-is, all Batu orgs get energia fully live, bypassing the per-org rollout. The PR title says DO NOT MERGE but TEMP comments are the only in-code safeguard.
Format helpers duplicated in resumen/page.tsx instead of shared _lib/formatters.ts
apps/platform/src/app/[locale]/(dashboard)/energia/resumen/page.tsx:48
formatKwhShort, formatSignedMxn, formatSignedKwh, formatSignedTons are defined inline but not exported. portfolioHealth already has formatMxn/formatKwh. Move to energia/_lib/formatters.ts for sharing across the module in the next iteration.
All energia pages are full client components — complicates real-data wiring
apps/platform/src/app/[locale]/(dashboard)/energia/resumen/page.tsx:1
Every page starts with 'use client' and reads mock data at module scope. Standard pattern is a server component page passing prefetched data to interactive client components. Extracting computation into _hooks/useEnergiaData.ts per page will make the mock→real swap cleaner.
Large pages (630–769 lines) lack _components extraction
apps/platform/src/app/[locale]/(dashboard)/energia/hallazgos/page.tsx:1
hallazgos (769 lines), conciliacion (667 lines), resumen (630 lines) each contain 3–6 distinct sections inline. Extracting natural boundaries (KPI bar, chart, table/drill panel) into _components/ will make per-section data wiring easier in the next iteration.
reconcile.ts business logic belongs in a domain lib, not a route _lib/
apps/platform/src/app/[locale]/(dashboard)/energia/pagos/conciliacion/_lib/reconcile.ts:53
reconcile() implements substantive logic (CSV-vs-bills diff). Canonical form puts pure business logic in domains/utility/src/bills/. Route _lib/ placement pressures callers to run it client-side; at 2,000+ RPUs real scale this becomes a performance issue.
Artificial setTimeout in conciliacion demo should not be mistaken for real debounce
apps/platform/src/app/[locale]/(dashboard)/energia/pagos/conciliacion/page.tsx:72
setTimeout(resolve, 1400) simulates processing latency. Remove when wiring to a real endpoint — don't mistake for a debounce or rate-limiting mechanism.