a ver si ahora sí guarda el perfil
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled

This commit is contained in:
2026-06-20 19:06:58 -03:00
parent 1c5329915a
commit 882edd230d
3 changed files with 675 additions and 0 deletions

View File

@@ -0,0 +1,224 @@
# Sprint 5 — Etapa 4F: Fix definitivo save de perfil + ETAPA_4E_PROMPT pendiente
## Contexto
InQ ROI — React 18 + Vite + TypeScript + TanStack Router + Zustand + shadcn/ui + Supabase v2.106.2.
Esta etapa resuelve DOS issues simultáneos:
1. **Save de perfil produce "sin llamadas de red + sin mensajes en consola"** al hacer click en Guardar
2. **Los Cambios 1-4 del ETAPA_4E_PROMPT.md nunca fueron aplicados al código** — solo se commitió el archivo del prompt
---
## Estado actual del código (ya verificado — no requiere re-lectura inicial)
- `src/components/ProfileSheet.tsx`: ya usa `fetch()` nativo con token de localStorage. ✅ NO tocar este archivo salvo el Cambio 3 de abajo.
- `src/lib/supabase.ts`: `getSessionSafe()` sigue presente, `lockAcquireTimeout` NO configurado. ❌
- `src/store/library-store.ts`: sigue con `await getSessionSafe()`. ❌
- `src/store/process-store.ts`: sigue con `await getSessionSafe()` (2 ocurrencias). ❌
- `src/features/report/useReportData.ts`: sigue con `await getSessionSafe()`. ❌
---
## Causa raíz: botón deshabilitado por `name.trim() === ''`
### Diagnóstico
`ProfileSheet.tsx` tiene:
```tsx
<Button onClick={handleSave} disabled={!name.trim() || saving}>
```
"Sin llamadas de red Y sin mensajes en consola" = el botón está deshabilitado. Click en un botón disabled no dispara ningún event handler — comportamiento nativo del browser, sin eventos, sin logs.
El botón está deshabilitado cuando `name.trim() === ''`. El `name` state se inicializa en `useEffect([open])` con `setName(user.name)`.
### Por qué `user.name` es cadena vacía
**En `SupabaseAuthService.buildAuthUser()`:**
```ts
name: supabaseUser.user_metadata?.full_name as string ?? email,
```
El operador `??` solo hace fallback con `null` o `undefined`. NO hace fallback con cadena vacía.
Si Google devuelve `full_name: ""`, entonces `"" ?? email` = `""``user.name = ""`.
**La cadena de propagación:**
```
buildAuthUser() → name = "" (Google devuelve full_name: "")
upsertUserOnLogin() → inserta name="" en tabla users (ignoreDuplicates: true, primer login)
syncProfileFromDB() → lee name="" del DB, lo retorna como ""
AuthContext setUser → profile.name ?? prev.name = "" ?? prev.name = "" ← ?? no hace fallback!
ProfileSheet useEffect → setName("") → name state = "" → botón disabled
```
Mismo patrón en `getUserFromStorage()` del `AuthContext.tsx`.
---
## Cambios a aplicar — 7 archivos
### Cambio 1: `src/auth/SupabaseAuthService.ts`
**A) En `buildAuthUser`**, cambiar `?? email` por `|| email`:
```ts
// ANTES:
name: supabaseUser.user_metadata?.full_name as string ?? email,
// DESPUÉS:
name: (supabaseUser.user_metadata?.full_name as string | undefined) || email,
```
El operador `||` trata `""`, `null` y `undefined` como ausentes, haciendo fallback al email.
**B) En `syncProfileFromDB`**, cambiar `?? undefined` por `|| undefined`:
```ts
// ANTES:
name: (data.name as string | null) ?? undefined as unknown as string,
// DESPUÉS:
name: ((data.name as string | null | undefined) || undefined) as unknown as string,
```
Esto trata tanto `null` como `""` como ausentes (retorna `undefined`), evitando que un nombre vacío en DB se propague al estado del usuario.
### Cambio 2: `src/auth/AuthContext.tsx`
**A) En `getUserFromStorage()`**, mismo fix `|| email`:
```ts
// ANTES:
name: (u.user_metadata?.full_name as string) ?? email,
// DESPUÉS:
name: (u.user_metadata?.full_name as string | undefined) || email,
```
**B) En el `useEffect([user?.id])` IIFE**, cambiar `??` por `||` para que cadena vacía del DB también haga fallback:
```ts
// ANTES:
? { ...prev, company: profile.company, name: profile.name ?? prev.name }
// DESPUÉS:
? { ...prev, company: profile.company, name: profile.name || prev.name }
```
### Cambio 3: `src/components/ProfileSheet.tsx`
**En `useEffect([open])`**, agregar fallback a email:
```ts
// ANTES:
setName(user.name)
// DESPUÉS:
setName(user.name || user.email || '')
```
Aunque los Cambios 1-2 previenen que `user.name` llegue vacío en sesiones nuevas, este fallback protege a usuarios cuya fila en DB ya tiene `name = ""` de sesiones anteriores. Usando el email como fallback, el campo siempre tiene un valor no-vacío → botón habilitado.
### Cambio 4: `src/lib/supabase.ts`
Dos cambios (del ETAPA_4E_PROMPT.md Cambio 1):
**A) Agregar `lockAcquireTimeout: 30000`:**
```ts
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
lock: processLock,
// Aumentado de 5000 (default) a 30000ms.
// Con 5s, un token refresh lento dispara ProcessLockAcquireTimeoutError,
// cuyo handler hace await previousOperation (colgada) → chain bloqueado permanente.
// 30s da margen para refresh en redes lentas o cold starts de Supabase Auth.
lockAcquireTimeout: 30000,
},
})
```
**B) Eliminar TODA la función `getSessionSafe()`** — declaración y export. No dejar ni stub ni comentario.
La función abandona la Promise de `getSession()` cuando gana el timeout. La Promise abandonada deja una operación en `PROCESS_LOCKS[name]` que bloquea el chain.
### Cambio 5: `src/store/library-store.ts`
```ts
// ELIMINAR este import:
import { getSessionSafe } from '@/lib/supabase'
// ELIMINAR esta línea en load():
await getSessionSafe()
```
No reemplazar con nada. Las queries a Supabase llaman `getSession()` internamente vía `_getAccessToken()` que espera `initializePromise` automáticamente.
### Cambio 6: `src/store/process-store.ts`
```ts
// ELIMINAR este import:
import { getSessionSafe } from '@/lib/supabase'
// ELIMINAR ambas líneas (en loadProcess() y en loadActivities()):
await getSessionSafe()
```
### Cambio 7: `src/features/report/useReportData.ts`
```ts
// ELIMINAR este import:
import { getSessionSafe } from '@/lib/supabase'
// ELIMINAR la línea:
await getSessionSafe()
```
---
## Lo que NO entra
- NO agregar logs de debug
- NO cambiar la estrategia de ProfileSheet (fetch nativo con localStorage token — es correcta, mantener)
- NO implementar el "Cambio 5" del ETAPA_4E_PROMPT.md (ProfileSheet con supabase client + timeout) — ya está mejor con fetch nativo
- NO cambios en simulación, heatmap, ComparisonTable, ni features nuevas
- NO cambios en `AuthContext.tsx` más allá de los especificados en Cambio 2
---
## Orden de ejecución
1. Leer los 7 archivos antes de editar para confirmar estado actual
2. Aplicar Cambio 1 (`SupabaseAuthService.ts`)
3. Aplicar Cambio 2 (`AuthContext.tsx`)
4. Aplicar Cambio 3 (`ProfileSheet.tsx`)
5. Aplicar Cambio 4 (`supabase.ts`)
6. Aplicar Cambio 5 (`library-store.ts`)
7. Aplicar Cambio 6 (`process-store.ts`)
8. Aplicar Cambio 7 (`useReportData.ts`)
9. `npm run build` — debe dar 0 errores TypeScript
10. `npm run test` — todos los Vitest verdes
11. `git add -A && git commit -m "fix(sprint-5/etapa-4f): || fallback en name vacío + eliminar getSessionSafe + lockAcquireTimeout 30s"`
12. `git push origin main`**OBLIGATORIO antes de validar en producción**
---
## Criterios de validación
- [ ] `npm run build` limpio (0 errores TypeScript)
- [ ] `npm run test` — todos los Vitest verdes
- [ ] **`git push origin main` ejecutado**
- [ ] **Validación visual del director antes del OK formal**
Validación funcional (post-deploy, a cargo del director):
- [ ] Guardar nombre en ProfileSheet → app NO se congela → nombre actualizado en dropdown
- [ ] Guardar empresa → app NO se congela → empresa aparece en dropdown
- [ ] Cmd+R después de guardar → valores persisten (Supabase)
- [ ] Biblioteca carga correctamente (sin getSessionSafe)
- [ ] Workspace carga correctamente (sin getSessionSafe)
- [ ] Reporte carga correctamente (sin getSessionSafe)