Files
inq-roi-simulador-web/TECH_DEBT.md
2026-07-07 17:29:48 -03:00

221 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TECH_DEBT — InQ ROI
> Deuda técnica, refactors postergados y decisiones arquitectónicas pendientes.
> Requiere consulta entre director y técnico para resolver.
> No incluye features de producto (ver BACKLOG.md) ni hallazgos de sprint actual (ver OBSERVATIONS.md).
---
## Convención de etiquetas
- `[SPRINT-2]` — Comprometido para resolución en el próximo sprint
- `[BACKLOG]` — Disponible para asignar
- `[DEFERRED]` — Pospuesto, requiere decisión arquitectónica
- `[INVESTIGATING]` — Necesita análisis técnico antes de planear
Cada ítem incluye:
- Estado actual del problema
- Causa raíz hipotetizada
- Impacto si no se resuelve
- Solución propuesta
- Archivos afectados (cuando aplica)
---
## Deuda crítica — afecta calidad del entregable
### [RESOLVED] Truncamiento de líneas largas en notas metodológicas
**Estado actual:** los caracteres Unicode rotos (Σ → £, → ") fueron
resueltos en Etapa 8 mediante reescritura a ASCII puro (SUM, x, -).
Decisión arquitectónica: la notación ASCII se mantiene como solución
permanente porque alinea con terminología financiera/Excel.
**Pendiente:** verificar si todavía hay líneas largas que se truncan en
margen derecho cuando una fórmula excede el ancho de página portrait.
**Acción:** test E2E que valide ancho de cada línea de notas metodológicas
en los 3 PDFs. Si hay truncamiento, evaluar:
- Opción A: usar `doc.splitTextToSize()` para wrap automático
- Opción B: reformular fórmulas problemáticas en múltiples líneas en el
texto fuente
**Resultado:** auditado en Sprint 2 Etapa 5-6. Dos líneas variables corregidas con `splitTextToSize` (`topResource.resourceName` en `drawAnalysisSection` y top3 en análisis ROI). Resto sin riesgo confirmado. 7 tests nuevos cubren el comportamiento.
**Sprint cerrado en:** Sprint 2 Etapas 5-6.
---
## Deuda media — afecta DX o evolución del producto
### [RESOLVED] Clave de localStorage del auth E2E vs producción
**Resultado:** la "contradicción" era un bug real, no solo una discrepancia de documentación.
`tests/e2e/auth-localStorage-key.spec.ts` (Sprint 6 Etapa 3) confirmó empíricamente que la
única clave presente en `localStorage` con sesión real es `sb-${projectRef}-auth-token`
(`sb-xgqbkxcliyjzisoccsur-auth-token` en este proyecto) — `'supabase.auth.token'` (STORAGE_KEY
default de `@supabase/auth-js`'s `GoTrueClient` en aislamiento) **no existe**, porque
`@supabase/supabase-js`'s `createClient()` sobrescribe ese default calculando la clave
`sb-`-prefijada desde la URL del proyecto.
`ProfileSheet.getAccessToken()` leía `'supabase.auth.token'` — la clave vieja, inexistente.
Esto significaba que **el guardado de perfil (nombre, empresa) nunca llegaba a Supabase**:
`getAccessToken()` retornaba `null`, el `if (!token) return` silenciaba el error, y el usuario
veía el cambio solo en memoria (`updateUser()`) sin persistir. Bug real, no solo deuda de
documentación.
**Fix:** `getAccessToken()` ahora calcula `sb-${projectRef}-auth-token` igual que
`auth-setup.ts`. Verificado end-to-end: PATCH a `/rest/v1/users` devuelve 204, y tras un
reload completo de página el valor guardado se relee correctamente desde la DB.
**Sprint cerrado en:** Sprint 6 Etapa 3.
### [BACKLOG] Guards de rol via `useEffect` en lugar de `beforeLoad` (3 componentes)
**Estado actual:** `AdminLayout`, `WorkspacePage`, e `ImportPage` usan `useEffect` para detectar el rol y redirigir. El componente monta brevemente antes del redirect — posible flash de contenido.
**Causa raíz:** el pattern surgió de prompts que describieron el guard en términos de comportamiento de componente ("si el rol no es X, redirigir") en lugar de especificar la sección de router.
**Impacto:** no es bug de seguridad (RLS protege los datos en Supabase), pero es anti-patrón para TanStack Router y puede causar flash visual breve en conexiones lentas.
**Solución propuesta:** migrar a `beforeLoad` de TanStack Router. Ejemplo: `beforeLoad: ({ context }) => { if (context.auth.user?.platformRole !== 'platform_admin') throw redirect({ to: '/library' }) }`.
**Archivos afectados:** `src/features/admin/AdminLayout.tsx`, `src/features/workspace/WorkspacePage.tsx`, `src/features/import/ImportPage.tsx` (aprox. línea 65-70 en WorkspacePage).
**Aprendizaje de método:** en prompts futuros que requieran guards de rol, especificar siempre el código exacto de `beforeLoad` en la sección de router — no solo el comportamiento esperado del componente.
**Origen:** Sprint 7 Etapa 5 — patrón recurrente detectado en 3 componentes.
### [BACKLOG] Doble suscripción `onAuthStateChange` en AuthContext
**Estado actual:** `AuthContext.tsx` tiene dos listeners de `onAuthStateChange`: uno principal (SIGNED_IN, SIGNED_OUT, TOKEN_REFRESHED, INITIAL_SESSION) y uno secundario para `PASSWORD_RECOVERY`. El secundario fue implementado para evitar importación circular entre `AuthContext` y `SupabaseAuthService`.
**Causa raíz:** `PASSWORD_RECOVERY` necesita llamar a `supabase.auth.updateUser()` directamente desde el contexto — si se delega a `SupabaseAuthService`, este importa desde `AuthContext` creando un ciclo.
**Impacto:** ambos listeners se registran en cada mount del `AuthProvider`. En la mayoría de los casos no genera bugs, pero el patrón es frágil y puede causar doble-dispatch de eventos en casos edge de remount.
**Solución propuesta:** refactorizar para un único switch/case en el listener principal. Resolver la importación circular moviendo `supabase` client a un módulo compartido sin dependencia de AuthContext.
**Archivos afectados:** `src/auth/AuthContext.tsx`.
**Origen:** Sprint 7 Etapa 3 — documentado en OBSERVATIONS.md.
### [BACKLOG] `getCurrentUser()` en SupabaseAuthService — sin callers activos
**Estado actual:** `SupabaseAuthService.getCurrentUser()` usa `getSession()` directamente (sin timeout, sin processLock workaround). Es la única excepción documentada a la regla — justificada porque necesita el objeto sesión completo, no solo una barrera. Sin callers activos hoy.
**Impacto si no se resuelve:** ninguno mientras no tenga callers. Si en el futuro algún flujo llama `getCurrentUser()`, podría colgar el processLock si el servidor no responde.
**Solución propuesta:** cuando adquiera callers reales, evaluar si `supabase.auth.getUser()` (token de localStorage, sin lock) es suficiente para ese caso.
**Archivos afectados:** `src/auth/SupabaseAuthService.ts` (~línea 27, comentario explicativo presente).
**Origen:** OBSERVATIONS Sprint 5 Etapa 1 — ítem prometido que faltaba registrar acá.
### [RESOLVED] `setTimeout(350)` en ProfileSheet — candidato a limpieza
**Resultado:** `setTimeout` eliminado — `updateUser()` se llama directamente tras
`onOpenChange(false)`. Verificado end-to-end (Sprint 6 Etapa 3): el fix definitivo en
`src/components/ui/sheet.tsx` (`useEffect` sobre el prop `open`, limpieza defensiva de
`pointer-events`) es suficiente por sí solo — `document.body.style.pointerEvents` queda
vacío tras guardar, y la navegación posterior (click en "Importar BPMN") funciona sin
bloqueos.
**Sprint cerrado en:** Sprint 6 Etapa 3.
### [RESOLVED] E2E specs con Dexie directo — reescribir para Supabase
**Resultado:** `export-pdf.spec.ts` (test "Heatmap con gradiente real") y `validate-dod-etapa4.spec.ts` reescritos para inyectar datos vía `@supabase/supabase-js` con `SUPABASE_SERVICE_ROLE_KEY` (bypasa RLS), en lugar de IndexedDB directo. `afterAll` borra el proceso creado por cada test (cascada en `activities`/`simulations`). Si `SUPABASE_SERVICE_ROLE_KEY` no está configurada en `.env.test`, ambos tests hacen `test.skip()` con mensaje descriptivo en vez de fallar — ver `.env.example`. Diagnóstico previo de `auth-setup.ts` confirmó que la storageKey (`sb-${projectRef}-auth-token`) ya era correcta (no era parte del problema): `@supabase/supabase-js`'s `createClient()` sobrescribe el `STORAGE_KEY` default de `auth-js` con esa clave calculada desde la URL.
**Sprint cerrado en:** Sprint 5 Etapa 6.
### [RESOLVED] E2E flakiness — "Guardar cambios" button timing en ActivityPanel
**Resultado:** agregado `data-testid="save-activity-btn"` al botón en `ActivityPanel.tsx`. Specs afectados (`payback-etapa-2.spec.ts`, `export-roi.spec.ts`) reemplazan el click directo por `waitForSelector(':not([disabled])')` → click → `waitForSelector('[disabled]')` (confirma que el guardado se procesó). Confirmado estable en 3 corridas consecutivas sin fallos. `GlobalSettingsPanel.tsx` tiene el mismo patrón `isDirty`/disabled en su botón "Guardar configuración" pero no se tocó — fuera del scope de Etapa 6; queda como ítem nuevo abajo.
**Sprint cerrado en:** Sprint 5 Etapa 6.
### [BACKLOG] GlobalSettingsPanel — mismo patrón de flakiness potencial que ActivityPanel
**Estado actual:** `GlobalSettingsPanel.tsx` tiene un botón "Guardar configuración" con el mismo guard `disabled={!isDirty}` que `ActivityPanel.tsx` tenía antes de Etapa 6. La función `configureGlobal()` en `payback-etapa-2.spec.ts` y `export-roi.spec.ts` sigue usando `getByRole('button', { name: /Guardar/i }).click()` sin esperar a que se habilite — el mismo riesgo de flakiness, no confirmado como activo todavía.
**Impacto:** potencial, no confirmado. Estos specs pasaron 3/3 en Etapa 6 sin tocar esta función.
**Solución propuesta:** agregar `data-testid="save-global-btn"` a `GlobalSettingsPanel.tsx` y aplicar el mismo patrón de `waitForSelector` que se usó para `save-activity-btn`.
**Costo:** bajo. Mismo patrón ya validado.
**Origen:** Sprint 5 Etapa 6 — detectado durante el diagnóstico, explícitamente fuera del scope del prompt (solo `ActivityPanel.tsx` listado en archivos a modificar).
### [BACKLOG] 9 specs adicionales con el mismo patrón flaky de "Guardar" sin data-testid
**Estado actual:** además de los 2 specs corregidos en Etapa 6, otros 9 archivos (`sprint-1-5-final.spec.ts`, `screenshots-etapa-3.spec.ts`, `etapa-4-pdfs.spec.ts` a `etapa-9-badge.spec.ts`, `etapa-11-polish.spec.ts`) usan el mismo `getByRole('button', { name: /Guardar/i }).click()` sin esperar a `isDirty`. No confirmados como flaky activamente — fuera del scope explícito de Etapa 6 (que listaba solo 2 archivos a modificar).
**Impacto:** riesgo de flakiness latente, no confirmado.
**Solución propuesta:** aplicar el mismo patrón `waitForSelector('[data-testid="save-activity-btn"]:not([disabled])')` ahora que el testid existe en producción.
**Costo:** bajo, mecánico — mismo cambio repetido 9 veces.
**Origen:** Sprint 5 Etapa 6 — identificado durante el grep de diagnóstico, no aplicado por respeto al scope explícito del prompt.
### [BACKLOG] CumulativeSavingsChart — derivación de inversión base desde cumulativeSavingsHorizon
**Estado actual:** cuando `roiBase` está presente (modo sensibilidad activo), el componente
recupera la inversión original algebraicamente: `investment_base = roiBase.annualSavings * horizonYears - roiBase.cumulativeSavingsHorizon`.
Es correcto mientras `horizonYears` del chart coincida con el usado en `calculateRoi` (hoy siempre es el mismo).
**Riesgo:** si en el futuro el chart recibe un horizonte diferente al del cálculo de ROI, el valor
de referencia del eje Y para la línea base será incorrecto, sin error visible.
**Solución propuesta:** agregar prop `investmentBase?: number` a `CumulativeSavingsChartProps`
y pasar `process.automationInvestment` directamente desde `ReportPage`.
**Costo:** bajo. **Origen:** Sprint 6 Etapa 2 — detectado en auditoría post-implementación.
### [INVESTIGATING] Dexie live queries (sincronización store ↔ db)
**Estado actual:** la invalidación del resultado de simulación se garantiza vía ESLint rule (`no-restricted-imports`) + convención: todas las mutaciones pasan por `useProcessStore`.
**Riesgo a futuro:** si en Sprint 2 la grilla de edición masiva requiere escrituras de alta frecuencia directas a IndexedDB (por performance), la convención actual no escala.
**Solución a evaluar:** migración a live queries de Dexie que invaliden el store automáticamente.
**Acción pendiente:** análisis técnico de costo/beneficio cuando se planee la grilla de edición masiva.
**Origen:** auditoría de Mayo 2026.
### [BACKLOG] Indicador visual en reporte de actividades automatable
**Estado:** Sprint 1 usa solo leyenda de texto para diferenciar actividades automatables. **Acción:** evaluar si el cliente necesita más claridad visual (borde punteado o ícono diferenciador) cuando compara Actual vs Automatizado en reuniones. Decidir con feedback de uso real.
### [BACKLOG] Paleta del heatmap — saturado vs pastel
**Estado:** los colores saturados actuales (`#10B981`, `#F59E0B`, `#EF4444`) funcionan bien en pantallas grandes de talleres InQuality. El brief de Sprint 1.5 propuso pasteles "más consultivos" (`#BBF7D0`, `#FED7AA`, `#FECACA`).
**Decisión actual:** mantener saturados (Sprint 1.5 Etapa 1).
**Impacto del cambio futuro:** requiere recalibrar tests E2E de análisis de píxeles (umbrales de verde/ámbar/rojo).
**Acción:** decidir con feedback real de uso en talleres InQuality. Sprint 3-4.
---
## Refinamientos UX
### [BACKLOG] Mejorar animación de expansión de sección "Automatización" en ActivityPanel
**Estado:** actualmente usa max-h trick (funciona, pero la aceleración no es lineal en los últimos píxeles).
**Solución sofisticada:** `useRef + scrollHeight` para animar a la altura real del contenido.
**Costo:** medio. **Beneficio:** micro-pulido visual.
### [RESOLVED] Label "Payback" superpuesto con eje Y en CumulativeSavingsChart (web)
**Resultado:** offset ajustado con `padding: [0,0,0,8]` y `offset: [4,0]`. Sin colisión con eje Y.
**Sprint cerrado en:** Sprint 2 Etapa 3.
### [RESOLVED] CumulativeSavingsChart — empezar en `-inversión`, no en 0 (web)
**Resultado:** verificado en Sprint 2 Etapa 3 — la línea ya arrancaba en `-investment` (`roi.annualSavings * y - investment`, y=0 → `-investment`). Ítem cerrado sin cambio de código.
**Sprint cerrado en:** Sprint 2 Etapa 3.
---
## Decisiones arquitectónicas tomadas en Sprint 1.5 (cerradas, archivadas)
> Sección informativa. Estas decisiones ya están aplicadas en el código. Se mantienen acá como referencia.
### Subtítulo "Análisis de retorno de inversión" solo en página 1 ROI
Decidido: página 1 tiene subtítulo descriptivo, páginas 2-4 no. Patrón estándar de reportes financieros (página 1 con título extendido).
### KPI cards en página 1 de PDFs Actual/Automatizado — color naranja
Decidido: el KPI "Costo total" se migró a naranja durante refactor de Sprint 1.5. Antes era azul. Decisión retroactiva del audit.
### ComparisonTable — actividades no automatizables muestran em dash
Decidido: en filas donde `isAutomatable === false`, mostrar "—" (em dash) en columnas Ahorro y % Ahorro en lugar de "USD 0,00" y "0,0%". Aplicado en Sprint 1.5.
### Versión "v0.1.0" en footer — fuera de PDFs, presente en web
Decidido: la versión es útil para usuarios internos en web. En PDFs entregados a clientes genera dudas de estabilidad. Aplicado en Sprint 1.5.
---
### Anotaciones rápidas antes de pasar a Etapa 10
### [RESOLVED] Tamaño del badge ⚡ — evaluar aumentar
**Resultado:** evaluado en Sprint 2 Etapa 3 (16px) y ajustado a 20×20px en fix post-validación visual. Sin colisión en nodos de evento. Director aprobó visualmente.
**Sprint cerrado en:** Sprint 2 Etapa 3 + fix.
### [INVESTIGATING] Toggle automatable + Guardar requerido para badge
**Estado:** comportamiento intencional. Switch del ActivityPanel cambia
visualmente, pero el badge en BpmnCanvas solo aparece después de "Guardar".
**Decisión actual (Sprint 1.5):** mantener como está. Es UX deliberada.
**Acción:** revisar en Sprint 2 con feedback real de uso en talleres.
Si genera confusión recurrente, evaluar Opción B (indicador visual de
"pendiente de guardar") o Opción C (auto-save).
### [RESOLVED] Badge ⚡ naranja en BpmnCanvas — Etapa 9
**Resultado:** badge rediseñado con identidad InQ, hex hardcodeados eliminados, 526 tests verdes.
**Sprint cerrado en:** Sprint 1.5 Etapa 9.
### [RESOLVED] Refinamientos página 1 PDF ROI — Etapa 11
**Resultado:** KPI Hero 26pt, banner con bold naranja oscuro, grid 2×2 uniforme 9/18/9pt, Top 3 con título 11pt. Esquinas redondeadas via canvas clip con fondo blanco opaco.
**Sprint cerrado en:** Sprint 1.5 Etapa 11.