diff --git a/sprints/sprint-4/ETAPA_7B_PROMPT.md b/sprints/sprint-4/ETAPA_7B_PROMPT.md index d3a1a33..c9d5069 100644 --- a/sprints/sprint-4/ETAPA_7B_PROMPT.md +++ b/sprints/sprint-4/ETAPA_7B_PROMPT.md @@ -133,14 +133,56 @@ async signInWithGoogle(): Promise { --- +## Fix 4 — simulations: 406 por `.single()` sin filas + re-fetch en loop + +### Problema + +En el network tab se observan múltiples requests con 406 Not Acceptable: +``` +GET simulations?select=*&process_id=eq.{id}&order=executed_at.desc&limit=1 → 406 +``` + +**Causa:** supabase-js v2 `.single()` envía `Accept: application/vnd.pgrst.object+json`, que exige exactamente 1 fila. Si el proceso no tiene simulaciones aún (0 filas), PostgREST responde 406. El mismo query aparece ~6 veces para los mismos `processId` — señal de un `useEffect` re-fetching por dependencia incorrecta. + +### Solución + +**Parte A — Fix del 406:** en `src/persistence/supabase/simulation-repo.ts`, en el método que busca la última simulación por proceso, cambiar `.single()` por `.maybeSingle()`: + +```typescript +// Antes: +const { data, error } = await supabase + .from('simulations') + .select('*') + .eq('process_id', processId) + .order('executed_at', { ascending: false }) + .limit(1) + .single() // ← lanza 406 cuando no hay filas + +// Después: +const { data, error } = await supabase + .from('simulations') + .select('*') + .eq('process_id', processId) + .order('executed_at', { ascending: false }) + .limit(1) + .maybeSingle() // ← retorna null sin error cuando no hay filas +``` + +El caller debe manejar `data === null` como "sin simulación" (no como error). + +**Parte B — Fix del re-fetch en loop:** localizar el `useEffect` que dispara esta query (probablemente en `LibraryPage.tsx` o en un hook de simulación). Revisar que sus dependencias no incluyan un objeto o array que se re-crea en cada render (patrón clásico de loop infinito). Si el array de dependencias contiene `processes` (que es un array nuevo en cada render del store), estabilizarlo con `useMemo` o extraer solo los IDs que realmente cambian. + +--- + ## Orden de ejecución -1. Fix 3 — `SupabaseAuthService.signInWithGoogle()` (1 línea, más fácil de verificar primero) -2. Fix 2 — función `formatCostPerHour` con precisión dinámica + aplicar en todos los contextos -3. Fix 1 — query param en ResourcesPanel + banner + botón en ResourceCatalogPage -4. `npm run build` — debe pasar sin errores TypeScript -5. `npm run test` — debe mantener 521+ tests verdes -6. Commit: `fix(sprint-4/etapa-7b): selector cuenta Google + redondeo costo/hora + contexto catálogo` +1. Fix 3 — `SupabaseAuthService.signInWithGoogle()` (1 línea) +2. Fix 4 — `.maybeSingle()` en simulation-repo + revisar re-fetch loop +3. Fix 2 — función `formatCostPerHour` con precisión dinámica + aplicar en todos los contextos +4. Fix 1 — query param en ResourcesPanel + banner + botón en ResourceCatalogPage +5. `npm run build` — debe pasar sin errores TypeScript +6. `npm run test` — debe mantener 521+ tests verdes +7. Commit: `fix(sprint-4/etapa-7b): selector cuenta Google + redondeo costo/hora + contexto catálogo + simulations maybeSingle` --- @@ -153,6 +195,8 @@ async signInWithGoogle(): Promise { - [ ] Botón "← Usar en proceso" aparece en cada fila cuando hay contexto de proceso - [ ] Click en "← Usar en proceso" navega a `/workspace/:processId` - [ ] Navegar a `/recursos` directamente (sin `fromProcess`) no muestra banner ni botón extra +- [ ] Network tab sin 406 en requests de `simulations` +- [ ] Network tab: el query de simulaciones no se repite en loop para el mismo `processId` - [ ] Build limpio sin errores TypeScript - [ ] 521+ tests verdes - [ ] **Validación visual del director** antes del OK formal diff --git a/src/persistence/supabase/simulation-repo.ts b/src/persistence/supabase/simulation-repo.ts index c54ebcb..33315e7 100644 --- a/src/persistence/supabase/simulation-repo.ts +++ b/src/persistence/supabase/simulation-repo.ts @@ -32,8 +32,8 @@ export const supabaseSimulationRepo = { .eq('process_id', processId) .order('executed_at', { ascending: false }) .limit(1) - .single() - if (error) return undefined + .maybeSingle() + if (error || !data) return undefined return fromRow(data as Record) },