Files
inq-roi-simulador-web/sprints/sprint-5/ETAPA_1_PROMPT.md
Marcos Benítez a2763ab755
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
Sprint 5. Etapa 1.
2026-06-19 15:31:03 -03:00

193 lines
7.8 KiB
Markdown

# Sprint 5 — Etapa 1: Fix crítico workspace loading
## Contexto
InQ ROI — React 18 + Vite + TypeScript + TanStack Router + Zustand + shadcn/ui + Tailwind + Supabase.
Sprint 4 completado. La app tiene auth (Google OAuth + Supabase), PostgreSQL, RLS, catálogo de recursos global.
**Bug en producción:** al navegar desde la biblioteca (`/`) a un proceso (`/workspace/:processId`), la pantalla del workspace queda cargando indefinidamente. Solo carga correctamente después de un refresh manual de la página.
---
## Causa raíz — ya diagnosticada, no requiere investigación adicional
El diagnóstico es preciso. No es necesario "investigar el flujo" — el problema está localizado.
**`src/store/process-store.ts`** llama a `supabase.auth.getSession()` directamente como barrera de sesión antes del fetch. Esta llamada no tiene timeout: si Supabase tarda en responder (token refresh en curso, red lenta, lock interno), `getSession()` se bloquea indefinidamente → `loadProcess` nunca resuelve → `isLoading` queda `true` para siempre.
**`src/store/library-store.ts`** hace lo mismo pero usa `getSessionSafe()` — una función en `src/lib/supabase.ts` que hace race entre `getSession()` y un timeout de 4 segundos, garantizando que siempre resuelve. Por eso la biblioteca carga y el workspace no.
**La inconsistencia es sistémica:** el mismo patrón puede existir en otros stores. Esta etapa lo audita y corrige en todos.
---
## Parte 1 — Fix en process-store.ts
En `src/store/process-store.ts`, localizar la llamada a `supabase.auth.getSession()` (usada como barrera antes del query principal) y reemplazarla por `getSessionSafe()`:
```typescript
// Antes:
await supabase.auth.getSession()
// Después:
await getSessionSafe()
```
Agregar el import si no está:
```typescript
import { supabase, getSessionSafe } from '@/lib/supabase'
```
Verificar que `getSessionSafe` ya existe en `src/lib/supabase.ts`. Si por alguna razón no existe, crearla siguiendo el mismo patrón que se usa en `library-store.ts`.
---
## Parte 2 — Auditar todos los stores
Buscar en `src/store/` y `src/persistence/` cualquier archivo que llame a `supabase.auth.getSession()` directamente (sin pasar por `getSessionSafe`). Reportar qué archivos tienen este patrón.
Para **cada archivo** que lo use:
- Si es una barrera de sesión antes de un query → reemplazar por `getSessionSafe()`
- Si es otro uso (ej: verificar si hay sesión activa para tomar una decisión, no como barrera) → evaluar caso por caso y documentar la decisión
---
## Parte 3 — Estado de error en el workspace
**Problema adicional:** actualmente, si `getSessionSafe()` agota su timeout, `loadProcess` lanza un error, pero el workspace no tiene un estado `error` propio. El resultado es que `isLoading` puede quedar `true` o la pantalla queda vacía sin mensaje al usuario.
**Fix requerido:** en `process-store.ts` (y el hook/componente que lo consume), agregar manejo de estado de error:
```typescript
// En el store, el estado debe incluir:
{
isLoading: boolean
error: string | null // 🆕
process: Process | null
}
// En loadProcess, al catch:
set({ isLoading: false, error: 'No se pudo cargar el proceso. Revisá tu conexión.' })
```
**En WorkspacePage.tsx** (o el componente raíz del workspace), manejar el estado `error`:
```tsx
if (error) {
return (
<div className="flex flex-col items-center justify-center h-full gap-4">
<p className="text-sm text-muted-foreground">{error}</p>
<Button variant="outline" onClick={() => loadProcess(processId)}>
Reintentar
</Button>
</div>
)
}
```
No usar `alert()` ni logs crudos. El error debe ser visible, accionable, y en español.
---
## Parte 4 — Skeleton screen en el workspace
**Estado actual:** el workspace muestra un spinner genérico mientras carga. En una app moderna esto es insuficiente — el usuario no sabe qué está cargando ni cuánto ocupa la pantalla.
**Requerimiento:** reemplazar el spinner de carga del workspace por un skeleton screen que preserve el layout de la página.
El workspace tiene una estructura aproximada de: barra superior + panel BPMN (zona grande central/izquierda) + panel lateral (ActivityPanel/ResourcesPanel).
Crear un componente `WorkspaceSkeleton` que muestre:
- Header con altura igual al real (sin contenido)
- Zona del canvas BPMN: rectángulo con `animate-pulse` en gris claro, misma altura aproximada
- Panel lateral: 3-4 filas de skeleton (títulos y texto en gris)
Usar `shadcn/ui Skeleton` si ya está disponible en el proyecto, o implementar con Tailwind `animate-pulse`:
```tsx
<div className="animate-pulse bg-muted rounded-md h-full w-full" />
```
El skeleton se muestra cuando `isLoading === true` y no hay `process` todavía. Una vez que `process` está disponible, el skeleton desaparece y el workspace real se monta.
---
## Parte 5 — React Error Boundary en el workspace
Una app de nueva generación no deja que un error de renderizado en el workspace blankee la pantalla completa. Agregar un Error Boundary al nivel del workspace.
Si ya existe un `ErrorBoundary` en el proyecto (`src/components/ErrorBoundary.tsx` o similar), usarlo. Si no existe, crear uno mínimo:
```tsx
// src/components/ErrorBoundary.tsx
import { Component, ReactNode } from 'react'
interface Props { children: ReactNode; fallback?: ReactNode }
interface State { hasError: boolean; error: Error | null }
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null }
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error }
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? (
<div className="flex flex-col items-center justify-center h-full gap-2 p-8">
<p className="text-sm font-medium text-destructive">Algo salió mal al cargar el workspace.</p>
<p className="text-xs text-muted-foreground">{this.state.error?.message}</p>
<button
className="text-xs underline text-muted-foreground"
onClick={() => window.location.reload()}
>
Recargar la página
</button>
</div>
)
}
return this.props.children
}
}
```
Envolver `WorkspacePage` (o el contenido del workspace) con `<ErrorBoundary>` en el router o en `App.tsx`.
---
## Qué NO entra en esta etapa
- Cambios en el diseño visual del workspace (paneles, BPMN canvas, ActivityPanel)
- Cambios en la lógica de simulación
- Cambios en recursos, reporte, o PDF
- Nuevas features de producto
- Cambios en auth (login, logout, OAuth)
---
## Orden de ejecución
1. Abrir `src/store/process-store.ts` — reemplazar `getSession()` por `getSessionSafe()`
2. Grep en `src/store/` y `src/persistence/` para `getSession()` — auditar y corregir
3. Agregar campo `error` al store y manejo en `WorkspacePage`
4. Crear `WorkspaceSkeleton` y usarlo mientras `isLoading === true`
5. Crear `ErrorBoundary` (si no existe) y envolver el workspace
6. `npm run build` — 0 errores TypeScript
7. `npm run lint` — 0 warnings en archivos tocados
8. `npm run test` — mantener conteo anterior de tests Vitest (sin regresiones)
9. Commit: `fix(sprint-5/etapa-1): workspace loading — getSessionSafe + error state + skeleton`
---
## Criterios de validación
- [ ] Navegar desde la biblioteca a cualquier proceso carga el workspace en el primer intento (sin refresh)
- [ ] Si el proceso no carga en ~5s, se muestra mensaje de error con botón "Reintentar"
- [ ] El estado de carga muestra un skeleton screen que preserva el layout (no spinner en blanco)
- [ ] Si hay un error de renderizado en el workspace, la app muestra el Error Boundary (no pantalla en blanco)
- [ ] Todos los stores auditados — ninguno usa `getSession()` como barrera de sesión sin timeout
- [ ] `npm run build` limpio
- [ ] `npm run test` — sin regresiones
- [ ] **Validación visual del director** antes del OK formal