diff --git a/docs/INDEX.md b/docs/INDEX.md index 4ee9fa0..599e83c 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -2,7 +2,7 @@ # Documentation Index -Auto-generated on 2026-05-09 from front-matter and headings. +Auto-generated on 2026-05-10 from front-matter and headings. ## Architecture Decision Records diff --git a/lib/realtime/use-notifications-realtime.ts b/lib/realtime/use-notifications-realtime.ts index bbb23c5..36430ce 100644 --- a/lib/realtime/use-notifications-realtime.ts +++ b/lib/realtime/use-notifications-realtime.ts @@ -191,21 +191,30 @@ export function useNotificationsRealtime() { }) } + // PBI-74: stream blijft open op hidden. Reconnect alleen als hij door + // netwerkfout/server-close weg is. Bij visible-overgang en bij online + // triggeren we router.refresh() zodat de notifications-bel verse state + // pakt — gemiste vraag-events via NOTIFY-throttling worden hierdoor + // alsnog zichtbaar. const onVisibilityChange = () => { - if (document.visibilityState === 'visible') { - if (!sourceRef.current || sourceRef.current.readyState === EventSource.CLOSED) { - connect() - } - } else { - close() + if (document.visibilityState !== 'visible') return + if (!sourceRef.current || sourceRef.current.readyState === EventSource.CLOSED) { + connect() } + router.refresh() + } + + const onOnline = () => { + router.refresh() } connect() document.addEventListener('visibilitychange', onVisibilityChange) + window.addEventListener('online', onOnline) return () => { document.removeEventListener('visibilitychange', onVisibilityChange) + window.removeEventListener('online', onOnline) close() } }, [router]) diff --git a/lib/realtime/use-solo-realtime.ts b/lib/realtime/use-solo-realtime.ts index cf93361..50a837b 100644 --- a/lib/realtime/use-solo-realtime.ts +++ b/lib/realtime/use-solo-realtime.ts @@ -6,7 +6,12 @@ // - Opent EventSource('/api/realtime/solo?product_id=...') wanneer // productId niet null is; sluit de stream als productId null wordt. // - Reconnect met exponential backoff (1s → 30s, reset bij ready). -// - Pauseert bij document.visibilityState === 'hidden', resumes bij visible. +// - PBI-74: stream blijft open op tab hidden (geen close meer). Bij +// hidden→visible en bij window 'online' triggeren we router.refresh() +// zodat gemiste events alsnog binnenkomen via een verse server-render +// (re-fetcht initialTasks → initTasks reset solo-store). Postgres NOTIFY +// heeft geen replay, dus zonder deze resync zouden hidden-tab events +// permanent verloren zijn — zelfde fix als Story 5 voor backlog-realtime. // - Cleanup op unmount. // - Connection-status (status, showConnectingIndicator) wordt naar de // solo-store geschreven; UI-componenten lezen daar uit. @@ -19,6 +24,7 @@ import { useEffect, useRef } from 'react' import { flushSync } from 'react-dom' +import { useRouter } from 'next/navigation' import { useSoloStore } from '@/stores/solo-store' import type { ClaudeJobEvent, JobState, RealtimeEvent, RealtimeStatus } from '@/stores/solo-store' @@ -27,10 +33,12 @@ const BACKOFF_MAX_MS = 30_000 const CONNECTING_INDICATOR_DELAY_MS = 4_000 export function useSoloRealtime(productId: string | null) { + const router = useRouter() const sourceRef = useRef(null) const backoffRef = useRef(BACKOFF_START_MS) const reconnectTimerRef = useRef | null>(null) const indicatorTimerRef = useRef | null>(null) + const readyCountRef = useRef(0) useEffect(() => { const setStatus = useSoloStore.getState().setRealtimeStatus @@ -88,6 +96,12 @@ export function useSoloRealtime(productId: string | null) { source.addEventListener('ready', () => { backoffRef.current = BACKOFF_START_MS scheduleIndicator('open') + readyCountRef.current += 1 + // PBI-74: latere ready = post-reconnect → resync via router.refresh() + // zodat gemiste tasks-state via re-render initial-prop binnenkomt. + if (readyCountRef.current > 1) { + router.refresh() + } }) source.addEventListener('claude_jobs_initial', (e) => { @@ -173,25 +187,33 @@ export function useSoloRealtime(productId: string | null) { } } + // PBI-74: stream blijft open op hidden. Reconnect alleen als de stream + // door netwerkfout/server-close weg is en de tab visible is. Bij iedere + // visible-overgang triggeren we router.refresh() — gemiste events tijdens + // throttling/freeze worden via een verse server-render alsnog opgepakt. const onVisibility = () => { - if (document.visibilityState === 'hidden') { - close() - scheduleIndicator('disconnected') - } else if (sourceRef.current === null) { + if (document.visibilityState !== 'visible') return + if (sourceRef.current === null) { backoffRef.current = BACKOFF_START_MS connect() } + router.refresh() } - if (document.visibilityState === 'visible') { - connect() + const onOnline = () => { + router.refresh() } + + connect() document.addEventListener('visibilitychange', onVisibility) + window.addEventListener('online', onOnline) return () => { document.removeEventListener('visibilitychange', onVisibility) + window.removeEventListener('online', onOnline) if (indicatorTimerRef.current) clearTimeout(indicatorTimerRef.current) close() + readyCountRef.current = 0 } - }, [productId]) + }, [productId, router]) }