9.8 KiB
Etapa 3 — Limpieza técnica y diagnóstico de auth
Sprint: 6 Fecha: 2026-06-24 Alcance: fix confirmado de localStorage key en ProfileSheet, eliminación de setTimeout(350), spec E2E de auth, actualización de TECH_DEBT.md.
Contexto
Esta etapa cierra deuda técnica documentada y resuelve una contradicción activa en el manejo de auth que podría ser un bug real de persistencia de perfil.
Parte A — Bug potencial: localStorage key en ProfileSheet
El problema
ProfileSheet.getAccessToken() lee de la clave 'supabase.auth.token':
// ProfileSheet.tsx línea 14-23
function getAccessToken(): string | null {
try {
const raw = localStorage.getItem('supabase.auth.token') // ← clave potencialmente incorrecta
...
}
}
Pero auth-setup.ts documenta y confirma empíricamente que @supabase/supabase-js v2's createClient() escribe la sesión bajo sb-${projectRef}-auth-token, no bajo supabase.auth.token. El projectRef se deriva de la URL del proyecto.
Si en producción la sesión está bajo sb-${projectRef}-auth-token, entonces localStorage.getItem('supabase.auth.token') retorna null → getAccessToken() retorna null → if (!token) return en línea 67 silencia el error → el persist a Supabase de nombre y empresa nunca se ejecuta. El usuario ve el cambio en UI (vía updateUser() en memoria) pero no se persiste.
Diagnóstico requerido
Antes de modificar código, confirmar el estado real con un spec o con console.log en dev:
Opción A (spec): agregar un spec E2E tests/e2e/auth-localStorage-key.spec.ts que:
- Navega con
storageStatede auth - Evalúa
document.cookieolocalStorageen el contexto del browser - Afirma qué clave de auth existe realmente (
supabase.auth.tokenvssb-${projectRef}-auth-token) - Afirma que la clave correcta contiene un
access_tokenválido (string no vacío)
Fix según diagnóstico:
Si la clave real es sb-${projectRef}-auth-token (probable):
// Reemplazar getAccessToken() completa:
function getAccessToken(): string | null {
try {
// supabase-js v2 calcula la clave como sb-{projectRef}-auth-token
// donde projectRef = hostname.split('.')[0] de la URL del proyecto.
const projectRef = new URL(import.meta.env.VITE_SUPABASE_URL as string).hostname.split('.')[0]
const storageKey = `sb-${projectRef}-auth-token`
const raw = localStorage.getItem(storageKey)
if (!raw) return null
const parsed = JSON.parse(raw) as { access_token?: string }
return parsed.access_token ?? null
} catch {
return null
}
}
Si en producción ambas claves coexisten (posible si hay versiones mixtas de @supabase/auth-js), leer con fallback:
const raw = localStorage.getItem(storageKey) ?? localStorage.getItem('supabase.auth.token')
Actualizar el comentario en ProfileSheet.tsx para reflejar la clave real y el diagnóstico confirmado. El comentario actual dice "STORAGE_KEY = 'supabase.auth.token'" — eso era la documentación de auth-js v1/v2-old, no de supabase-js v2.
Criterios de Parte A
- Spec E2E confirma qué clave existe en localStorage en producción bajo auth real
getAccessToken()lee la clave correcta- Comentario en ProfileSheet.tsx refleja la clave real y la razón
- El save de perfil (nombre, empresa) persiste en Supabase — verificar manualmente en Studio o via spec
Parte B — Eliminar setTimeout(350) de ProfileSheet
El problema
ProfileSheet.handleSave() difiere updateUser() 350ms:
// ProfileSheet.tsx líneas 57-60
onOpenChange(false)
setTimeout(() => {
updateUser({ name: savedName, company: savedCompany })
}, 350)
El comentario dice que es para evitar que @radix-ui/react-remove-scroll deje pointer-events:none permanente en el body. Pero src/components/ui/sheet.tsx ya tiene el fix definitivo (useEffect sobre prop open que restaura pointer-events defensivamente). El setTimeout es redundante.
Fix
- Reemplazar el bloque con llamada directa:
onOpenChange(false)
updateUser({ name: savedName, company: savedCompany })
-
Verificar manualmente que después de guardar el perfil, la UI responde (no hay pointer-events:none atascado en body). Si el fix de sheet.tsx es suficiente, el comportamiento es idéntico. Si reaparece el bug, restaurar el setTimeout y documentar que el fix de sheet.tsx no es suficiente solo.
-
Actualizar el comentario para explicar que el fix vive en sheet.tsx.
Criterios de Parte B
- setTimeout(350) eliminado de ProfileSheet.tsx
- Verificación manual: guardar perfil → UI responde correctamente, sin pointer-events bloqueados
- Comentario actualizado explicando que el fix está en sheet.tsx
Parte C — Spec E2E de validación de auth real
Independientemente de la resolución de Parte A, agregar un spec que valide que el auth E2E funciona realmente (no solo que el storageState se carga, sino que Supabase devuelve datos reales del usuario).
Archivo: tests/e2e/auth-real.spec.ts
import { test, expect } from '@playwright/test'
test.use({ storageState: 'tests/e2e/setup/auth-state.json' })
test('auth E2E: Supabase devuelve datos reales del usuario autenticado', async ({ page }) => {
// Navegar a la app autenticada
await page.goto('http://localhost:4173/')
// Esperar a que AppHeader muestre el avatar/nombre (confirma que AuthContext cargó la sesión)
await expect(page.getByTestId('app-header-user-avatar')).toBeVisible({ timeout: 10000 })
// Verificar que el email del usuario de test aparece en el dropdown de perfil
// (o bien hacer una query directa a Supabase REST para confirmar que el token es válido)
const email = process.env.TEST_USER_EMAIL
if (email) {
await page.getByTestId('app-header-user-avatar').click()
await expect(page.getByText(email)).toBeVisible()
}
})
Adaptar según los data-testid reales del AppHeader (consultarlos antes de escribir el spec).
Si TEST_USER_EMAIL no está en .env.test, el spec hace skip con mensaje descriptivo (mismo patrón de export-pdf.spec.ts).
Criterios de Parte C
- Spec en
tests/e2e/auth-real.spec.tsque pasa cuando auth funciona - Spec hace
test.skip()descriptivo siTEST_USER_EMAILno está configurado - Si el spec falla, el diagnóstico es definitivo: el auth E2E no funciona realmente → fix en auth-setup.ts o en AuthContext
Parte D — Actualización de TECH_DEBT.md
Al terminar A, B y C, actualizar TECH_DEBT.md:
-
Marcar
[INVESTIGATING] Clave de localStorage del auth E2E vs produccióncomo[RESOLVED]con el resultado real del diagnóstico. -
Marcar
[BACKLOG] setTimeout(350) en ProfileSheetcomo[RESOLVED]. -
Agregar nuevo ítem:
### [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.
Parte E — Opcional: animación de expansión en ActivityPanel
Solo implementar si las partes A-D no consumen el slot completo.
La sección de automatización en ActivityPanel usa max-height para expandir/colapsar. La aceleración no es lineal en los últimos píxeles (se nota el "freno" al final).
Fix: usar useRef + scrollHeight para animar a la altura real del contenido:
const contentRef = useRef<HTMLDivElement>(null)
// Al expandir:
contentRef.current.style.maxHeight = contentRef.current.scrollHeight + 'px'
// Al colapsar:
contentRef.current.style.maxHeight = '0'
Con transición CSS transition-[max-height] duration-300 ease-in-out.
Verificar visualmente antes de commitear.
Orden de ejecución recomendado
- Parte A (diagnóstico y fix de localStorage key) — puede revelar un bug real, prioridad más alta
- Parte B (setTimeout cleanup) — depende de confirmar que sheet.tsx fix es suficiente
- Parte C (spec E2E) — complementa Parte A
- Parte D (TECH_DEBT.md) — al final, cuando A-C estén resueltos
- Parte E (animación) — solo si hay tiempo
Criterios de validación del conjunto
getAccessToken()en ProfileSheet.tsx usa la clave correcta de localStorage- Save de perfil persiste en Supabase (nombre y empresa visibles al recargar)
- setTimeout(350) eliminado, UI funciona correctamente después de guardar perfil
- Spec E2E de auth pasa o hace skip descriptivo
- TECH_DEBT.md actualizado: ítems resueltos marcados, nueva nota de CumulativeSavingsChart
npm run lintsin erroresnpm run buildlimpionpm run test→ 552+ tests verdes- Validación del director sobre el estado de la documentación antes del OK formal
Archivos a modificar
src/components/ProfileSheet.tsx— clave localStorage + eliminar setTimeouttests/e2e/auth-localStorage-key.spec.ts— spec de diagnóstico (nuevo)tests/e2e/auth-real.spec.ts— spec de validación E2E (nuevo)TECH_DEBT.md— marcar resueltos + agregar nota CumulativeSavingsChart
Opcional:
src/features/workspace/ActivityPanel.tsx— animación scrollHeight
No modificar:
tests/e2e/setup/auth-setup.ts— ya documentado correctamente- Ningún otro archivo fuera de esta lista sin consulta previa