# 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'`:

```ts
// 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:
1. Navega con `storageState` de auth
2. Evalúa `document.cookie` o `localStorage` en el contexto del browser
3. Afirma qué clave de auth existe realmente (`supabase.auth.token` vs `sb-${projectRef}-auth-token`)
4. Afirma que la clave correcta contiene un `access_token` válido (string no vacío)

**Fix según diagnóstico:**

Si la clave real es `sb-${projectRef}-auth-token` (probable):

```ts
// 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:
```ts
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:

```ts
// 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

1. Reemplazar el bloque con llamada directa:

```ts
onOpenChange(false)
updateUser({ name: savedName, company: savedCompany })
```

2. 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.

3. 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`

```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.ts` que pasa cuando auth funciona
- [ ] Spec hace `test.skip()` descriptivo si `TEST_USER_EMAIL` no 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:

1. Marcar `[INVESTIGATING] Clave de localStorage del auth E2E vs producción` como `[RESOLVED]` con el resultado real del diagnóstico.

2. Marcar `[BACKLOG] setTimeout(350) en ProfileSheet` como `[RESOLVED]`.

3. Agregar nuevo ítem:

```markdown
### [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:

```tsx
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

1. Parte A (diagnóstico y fix de localStorage key) — puede revelar un bug real, prioridad más alta
2. Parte B (setTimeout cleanup) — depende de confirmar que sheet.tsx fix es suficiente
3. Parte C (spec E2E) — complementa Parte A
4. Parte D (TECH_DEBT.md) — al final, cuando A-C estén resueltos
5. 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 lint` sin errores
- [ ] `npm run build` limpio
- [ ] `npm 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 setTimeout
- `tests/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
