Files
inq-roi-simulador-web/sprints/sprint-5/ETAPA_4F_PROMPT.md
Marcos Benítez 882edd230d
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
a ver si ahora sí guarda el perfil
2026-06-20 19:06:58 -03:00

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:

  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:

<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.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 mainOBLIGATORIO 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)