Files
inq-roi-simulador-web/sprints/sprint-7/ETAPA_7_PROMPT.md
Marcos Benítez 9b89cb8669
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
La política simulations_update está activa en la DB de desarrollo. El fix B está completo.
Resumen de todo lo entregado en Etapa 7:

Fix A1 (src/auth/AuthContext.tsx): isProfileLoaded: boolean agregado como estado. Arranca en false, se pone true en el finally del sync effect (después de syncProfileFromDB), y cuando onAuthStateChange devuelve null (usuario no autenticado). Se resetea a false en cada SIGN_IN para gatear el re-sync.

Fix A2 (src/features/library/LibraryPage.tsx): Guard temprano después de todos los hooks — si !user || !isProfileLoaded devuelve un skeleton de tres columnas animado, idéntico al que usa HomeView. El contenido rol-dependiente solo se renderiza cuando el perfil está sincronizado.

Fix A3 (GlobalSettingsPanel.tsx + WorkspacePage.tsx): El campo "Cliente" es texto estático (<p>) para client_editor y client_viewer. En el topbar de WorkspacePage se oculta el input inline y el ícono de lápiz; si hay un clientName se muestra como texto inerte.

Fix B (supabase/migrations/016_add_simulations_update_policy.sql): Migración creada + aplicada vía Management API a inq-roi-desa. pg_policies confirma las 4 políticas: simulations_select, simulations_insert, simulations_update, simulations_delete.

Tests: tests/features/library/library-ui.test.tsx actualizado — mock incluye isProfileLoaded, 2 tests nuevos para isProfileLoaded: false. Total: 586 tests, todos verdes. Build limpio.
2026-07-07 12:27:36 -03:00

8.2 KiB

Etapa 7 — Fix: race condition de roles en LibraryPage + simulations UPDATE policy

Sprint: 7 Fecha: 2026-07-07 Alcance: AuthContext (loading state), LibraryPage (guard), nueva migración SQL (simulations_update), campo client read-only para client_editor.


Contexto del bug

msebem@gmail.com tiene platform_role = 'client_editor' confirmado en DB.
Sin embargo al cargar LibraryPage muestra elementos que solo debería ver InQuality:

  • Botón "Importar BPMN"
  • Panel de grupos ("Clientes")
  • Ícono de configuración

Al navegar a otra ruta y volver, el comportamiento es correcto (elementos ocultos).


Causa raíz confirmada — DOS problemas independientes

Problema A: race condition de roles en LibraryPage

Flujo actual:

  1. Login → onAuthStateChange SIGNED_IN → buildAuthUser(supabaseUser)platformRole: 'guest' (heurística email; msebem@gmail.com no es @inquality.com.py)
  2. guestisClientRole = false → LibraryPage renderiza con todos los elementos visibles
  3. syncProfileFromDB completa → platformRole: 'client_editor' → store actualizado
  4. Navegar y volver → re-render correcto

La UI "miente" durante la ventana entre login y sync de DB. El fix es no renderizar contenido rol-dependiente hasta que syncProfileFromDB haya completado al menos una vez en la sesión.

Problema B: política simulations_update inexistente

En migration 015_client_roles_and_rls.sql existe simulations_insert pero NO existe simulations_update.

Cuando "Guardar configuración" intenta hacer UPDATE sobre una simulación ya existente → PostgreSQL no encuentra política → operación bloqueada silenciosamente. El usuario no recibe error explícito pero los cambios no se persisten.


Cambios requeridos

ANTES de empezar: leer src/auth/AuthContext.tsx (o el archivo que gestiona onAuthStateChange y llama a syncProfileFromDB) y src/features/library/LibraryPage.tsx para entender el estado actual. Leer también src/features/workspace/WorkspacePage.tsx para detectar si tiene el mismo problema.


Fix A1: agregar isProfileLoaded al AuthContext

En el contexto de autenticación (el que expone user, maneja onAuthStateChange, llama a syncProfileFromDB):

Agregar un estado isProfileLoaded: boolean (inicialmente false).

Lógica:

  • Se pone en false cuando empieza el proceso de login (SIGNED_IN event)
  • Se pone en true después de que syncProfileFromDB completa (tanto si retorna datos como si retorna null)
  • Cuando el evento es SIGNED_OUT, volver a false
  • Cuando el token se refresca (TOKEN_REFRESHED) o el usuario ya estaba logueado al cargar la app, también completar el sync antes de poner true

Exponer isProfileLoaded en el contexto junto a user.

Importante: isProfileLoaded: true con user: null es un estado válido (usuario no logueado, pero ya se verificó). isProfileLoaded: false con user: null es "todavía verificando".


Fix A2: guard en LibraryPage

ANTES de empezar este fix: leer el archivo completo de LibraryPage para entender dónde se usa isClientRole y qué elementos condicionan.

En LibraryPage, antes de renderizar contenido rol-dependiente (import button, groups panel, settings gear, y cualquier sección condicionada por isClientRole), verificar isProfileLoaded.

Si !isProfileLoaded: mostrar un estado de carga mínimo. No es necesario un skeleton complejo — basta con el mismo spinner o skeleton que ya usa LibraryPage en su estado de carga (isLoading && groups.length === 0). Reutilizar ese patrón si existe.

Si isProfileLoaded: renderizar normalmente, el isClientRole ahora tiene el valor correcto.

La idea es que el usuario vea un spinner breve (< 200ms típicamente) en lugar de una UI incompleta que luego cambia. Preferible UX.


Fix A3: campo "cliente" read-only para client_editor (en WorkspacePage / GlobalPanel)

ANTES de empezar: leer el componente que renderiza el campo "cliente" (texto libre) en el sidebar o panel global del workspace. Puede estar en WorkspacePage.tsx, GlobalPanel.tsx, o similar.

Para isClientRole = true: mostrar el nombre del cliente como texto estático (no como input editable). El campo sigue visible pero no editable. No es necesario guardarlo o enviar cambios al hacer click en él.

Para InQuality (!isClientRole): comportamiento actual sin cambios.


Fix B: nueva migración — simulations_update policy

Crear el archivo de migración con el número siguiente a 015 (verificar qué existe en supabase/migrations/ y usar el siguiente).

Nombre: 016_add_simulations_update_policy.sql

-- Fix: faltaba política UPDATE en simulations.
-- simulations_insert existe en 015 pero simulations_update no fue creada.
-- Sin esta policy, client_editor no puede guardar configuración sobre simulaciones existentes.

CREATE POLICY "simulations_update" ON public.simulations
  FOR UPDATE USING (
    EXISTS (SELECT 1 FROM public.processes WHERE id = simulations.process_id)
    AND public.current_platform_role() NOT IN ('client_viewer', 'guest')
  );

Verificar antes de crear: que no exista ya una simulations_update policy en la DB (puede haberse creado manualmente). Si ya existe, no crearla (o usar DROP POLICY IF EXISTS + recrear para idempotencia).


Fix B2: verificar y corregir rename de proceso para client_editor

ANTES de empezar: en WorkspacePage (o donde se maneja el rename del proceso), encontrar la lógica que guarda el nombre cuando el usuario lo edita.

La política update_processes en migration 015 ya permite client_editor AND org_id = current_org(). Si el rename no funciona, el problema es frontend (el input no llama al save, o el save usa el método incorrecto).

Verificar:

  1. ¿El input de nombre del proceso llama a la función de save en onBlur o onEnter?
  2. ¿Esa función usa supabase.from('processes').update() con el id correcto?
  3. Si ambos están bien pero sigue fallando → verificar en Supabase SQL Editor que UPDATE public.processes SET name = 'test' WHERE id = '<process_id>' funcione como el usuario client_editor (hint: no se puede hacer directamente en SQL Editor como el usuario — verificar la condición de la policy manualmente)

Si es un bug frontend (el save no se dispara), arreglarlo.


Tests nuevos

// Mínimo requerido:

// 1. LibraryPage no renderiza elements rol-dependientes hasta isProfileLoaded = true
//    (simular: isProfileLoaded: false → assert import button no visible, groups panel no visible)

// 2. LibraryPage renderiza correctamente para client_editor una vez isProfileLoaded = true
//    (simular: isProfileLoaded: true, platformRole: 'client_editor' → assert import button no visible, groups panel no visible)

// 3. LibraryPage renderiza correctamente para platform_admin una vez isProfileLoaded = true
//    (simular: isProfileLoaded: true, platformRole: 'platform_admin' → assert import button visible)

Criterios de validación

  • Loguear como msebem@gmail.com → LibraryPage NO muestra Import BPMN ni groups panel en ningún momento (ni durante la carga inicial)
  • "Guardar configuración" en workspace funciona y persiste los cambios (no silent fail)
  • El campo "cliente" en el sidebar del workspace es read-only para msebem@gmail.com
  • Un usuario InQuality (platform_admin/member) sigue viendo todos los elementos sin regresión
  • npm run test → tests verdes (incluidos los 3 nuevos mínimos)
  • npm run build limpio
  • Validación visual del director antes del OK formal

NO modificar

  • Edge Functions (supabase/functions/)
  • Migraciones 001-015 (no editar, solo crear 016 nueva)
  • Panel de admin (/admin/*)
  • Motor de simulación, dominio, ROI, PDF, CSV
  • ConfirmInvitePage (Etapa 6)
  • ReportPage
  • Lógica de cálculo en domain/

Flujo esperado post-fix

  1. msebem@gmail.com loguea
  2. LibraryPage muestra spinner breve (~200ms)
  3. syncProfileFromDB completa → client_editor
  4. LibraryPage renderiza: lista flat de procesos de Solar Banco, sin import button, sin groups panel, sin settings gear
  5. Entra al workspace de su proceso
  6. Edita parámetros de actividades y configuración global
  7. Hace click en "Guardar configuración" → persiste correctamente
  8. Puede ver reporte y exportar PDF