# 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. `guest` → `isClientRole = 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`

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

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