7.6 KiB
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:
- Save de perfil produce "sin llamadas de red + sin mensajes en consola" al hacer click en Guardar
- 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 usafetch()nativo con token de localStorage. ✅ NO tocar este archivo salvo el Cambio 3 de abajo.src/lib/supabase.ts:getSessionSafe()sigue presente,lockAcquireTimeoutNO configurado. ❌src/store/library-store.ts: sigue conawait getSessionSafe(). ❌src/store/process-store.ts: sigue conawait getSessionSafe()(2 ocurrencias). ❌src/features/report/useReportData.ts: sigue conawait getSessionSafe(). ❌
Causa raíz: botón deshabilitado por name.trim() === ''
Diagnóstico
ProfileSheet.tsx tiene:
<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():
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:
// 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:
// 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:
// 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:
// 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:
// 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:
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
// 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
// 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
// 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.tsxmás allá de los especificados en Cambio 2
Orden de ejecución
- Leer los 7 archivos antes de editar para confirmar estado actual
- Aplicar Cambio 1 (
SupabaseAuthService.ts) - Aplicar Cambio 2 (
AuthContext.tsx) - Aplicar Cambio 3 (
ProfileSheet.tsx) - Aplicar Cambio 4 (
supabase.ts) - Aplicar Cambio 5 (
library-store.ts) - Aplicar Cambio 6 (
process-store.ts) - Aplicar Cambio 7 (
useReportData.ts) npm run build— debe dar 0 errores TypeScriptnpm run test— todos los Vitest verdesgit add -A && git commit -m "fix(sprint-5/etapa-4f): || fallback en name vacío + eliminar getSessionSafe + lockAcquireTimeout 30s"git push origin main— OBLIGATORIO antes de validar en producción
Criterios de validación
npm run buildlimpio (0 errores TypeScript)npm run test— todos los Vitest verdesgit push origin mainejecutado- 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)