capa adicional sobre lo que ya deployaste
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled

This commit is contained in:
2026-06-18 21:54:44 -03:00
parent b3b6fbe792
commit c20624399c
3 changed files with 50 additions and 15 deletions

View File

@@ -381,7 +381,7 @@ function HomeView({
currentUserId: string currentUserId: string
onShared: () => void onShared: () => void
}) { }) {
const { groups, processes, settings, createGroup, isLoading } = useLibraryStore() const { groups, processes, settings, createGroup, isLoading, error, load } = useLibraryStore()
const { user } = useAuth() const { user } = useAuth()
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [activeTags, setActiveTags] = useState<string[]>([]) const [activeTags, setActiveTags] = useState<string[]>([])
@@ -422,6 +422,24 @@ function HomeView({
) )
} }
// Error state: el load() falló o se colgó (timeout) — distinto de "biblioteca vacía"
if (error && groups.length === 0 && processes.length === 0) {
return (
<div className="flex flex-col items-center justify-center py-24 gap-4">
<Building2 className="h-16 w-16 text-destructive/40" />
<p className="text-base font-medium">No se pudo cargar la biblioteca</p>
<p className="text-[13px] text-muted-foreground max-w-sm text-center">{error}</p>
<button
onClick={() => load()}
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium text-white transition-opacity hover:opacity-90"
style={{ background: '#F59845' }}
>
Reintentar
</button>
</div>
)
}
// Empty state: sin grupos ni procesos (ya terminó de cargar) // Empty state: sin grupos ni procesos (ya terminó de cargar)
if (groups.length === 0 && processes.length === 0) { if (groups.length === 0 && processes.length === 0) {
return ( return (

View File

@@ -8,3 +8,14 @@ if (!supabaseUrl || !supabaseAnonKey) {
} }
export const supabase = createClient(supabaseUrl, supabaseAnonKey) export const supabase = createClient(supabaseUrl, supabaseAnonKey)
// supabase.auth.getSession() puede colgarse indefinidamente si el cliente queda
// esperando un lock interno (navigator.locks) que nunca se libera. Esta barrera
// nunca bloquea más de 4s: si no resuelve a tiempo, sigue de largo sin sesión confirmada
// en vez de dejar la UI con un spinner/skeleton infinito.
export async function getSessionSafe(): Promise<void> {
await Promise.race([
supabase.auth.getSession(),
new Promise((resolve) => setTimeout(resolve, 4000)),
])
}

View File

@@ -1,6 +1,6 @@
import { create } from 'zustand' import { create } from 'zustand'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { supabase } from '@/lib/supabase' import { getSessionSafe } from '@/lib/supabase'
import { supabaseProcessRepo } from '@/persistence/supabase/process-repo' import { supabaseProcessRepo } from '@/persistence/supabase/process-repo'
import { supabaseGroupRepo } from '@/persistence/supabase/group-repo' import { supabaseGroupRepo } from '@/persistence/supabase/group-repo'
import { supabaseSettingsRepo } from '@/persistence/supabase/settings-repo' import { supabaseSettingsRepo } from '@/persistence/supabase/settings-repo'
@@ -12,6 +12,7 @@ interface LibraryState {
processes: Process[] processes: Process[]
settings: AppSettings settings: AppSettings
isLoading: boolean isLoading: boolean
error: string | null
load: () => Promise<void> load: () => Promise<void>
@@ -36,27 +37,32 @@ export const useLibraryStore = create<LibraryState>((set, get) => ({
processes: [], processes: [],
settings: DEFAULT_SETTINGS, settings: DEFAULT_SETTINGS,
isLoading: false, isLoading: false,
error: null,
load: async () => { load: async () => {
set({ isLoading: true }) set({ isLoading: true, error: null })
try { try {
// Barrera única: garantiza que el cliente Supabase tenga la sesión restaurada // Barrera única: garantiza que el cliente Supabase tenga la sesión restaurada
// antes de salir a buscar datos en paralelo (evita 0 filas por RLS en el primer load) // antes de salir a buscar datos en paralelo (evita 0 filas por RLS en el primer load).
const { data: sessionData } = await supabase.auth.getSession() // Con timeout defensivo: si el cliente se cuelga, no bloquea la UI más de 4s.
console.log('[library-store] load session:', { await getSessionSafe()
hasSession: !!sessionData.session,
userId: sessionData.session?.user?.id ?? 'NULL',
})
const [groups, processes, settings] = await Promise.all([ // Timeout global: si el cliente Supabase queda colgado (ej. lock interno nunca liberado),
// isLoading nunca debe quedar en true para siempre — la UI debe poder mostrar error y reintentar.
const [groups, processes, settings] = await Promise.race([
Promise.all([
supabaseGroupRepo.getAll(), supabaseGroupRepo.getAll(),
supabaseProcessRepo.getAll(), supabaseProcessRepo.getAll(),
supabaseSettingsRepo.get(), supabaseSettingsRepo.get(),
]),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Timeout: Supabase no respondió en 15s')), 15_000)
),
]) ])
set({ groups, processes, settings, isLoading: false }) set({ groups, processes, settings, isLoading: false, error: null })
} catch (err) { } catch (err) {
console.error('Error al cargar la biblioteca:', err) console.error('Error al cargar la biblioteca:', err)
set({ isLoading: false }) set({ isLoading: false, error: err instanceof Error ? err.message : 'Error al cargar la biblioteca' })
} }
}, },