feat: open SoloRealtimeBridge globaal voor active product

SoloRealtimeBridge gated nu op active-product i.p.v. /solo-pad. Live-dot
en worker-presence werken daardoor op alle (app)-pagina's
(Producten/PB/Sprint/Solo/Todo's). Buiten /solo is de solo-store leeg en
zijn task-events no-ops, dus de stream gedraagt zich automatisch als
lichte presence-stream tot SoloBoard mount.

- realtime-bridge: productId-prop i.p.v. usePathname
- (app)/layout: activeProduct?.id doorgegeven aan bridge
- nav-status-indicators: pathname-check vervangen door hasActiveProduct prop
- nav-bar: hasActiveProduct={!!activeProduct} doorgegeven
- architecture-doc: realtime connection lifecycle bijgewerkt

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-01 10:22:13 +02:00
parent bf464bfc31
commit 1e48eed459
5 changed files with 12 additions and 20 deletions

View file

@ -92,7 +92,7 @@ export default async function AppLayout({ children }: { children: React.ReactNod
{children}
</main>
<StatusBar />
<SoloRealtimeBridge />
<SoloRealtimeBridge productId={activeProduct?.id ?? null} />
<NotificationsBridge userId={session.userId} />
<Suspense>
<AlertToast />

View file

@ -183,7 +183,7 @@ export function NavBar({
{/* Rechts: solo-status + notifications + account-menu */}
<div className="flex items-center gap-2 flex-1 justify-end">
<SoloNavStatusIndicators />
<SoloNavStatusIndicators hasActiveProduct={!!activeProduct} />
<NotificationsBell currentUserId={userId} isDemo={isDemo} />
<UserMenu userId={userId} username={username} email={email} roles={roles} />
</div>

View file

@ -1,13 +1,10 @@
'use client'
import { usePathname } from 'next/navigation'
import { useSoloStore } from '@/stores/solo-store'
import type { RealtimeStatus } from '@/stores/solo-store'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
const SOLO_PATH_RE = /^\/products\/[^/]+\/solo$/
function RealtimeIndicator({
status,
showConnectingIndicator,
@ -43,13 +40,12 @@ function RealtimeIndicator({
)
}
export function SoloNavStatusIndicators() {
const pathname = usePathname()
export function SoloNavStatusIndicators({ hasActiveProduct }: { hasActiveProduct: boolean }) {
const realtimeStatus = useSoloStore((s) => s.realtimeStatus)
const showConnectingIndicator = useSoloStore((s) => s.showConnectingIndicator)
const connectedWorkers = useSoloStore((s) => s.connectedWorkers)
if (!pathname || !SOLO_PATH_RE.test(pathname)) return null
if (!hasActiveProduct) return null
return (
<div className="flex items-center gap-3 px-2">

View file

@ -1,21 +1,17 @@
// SoloRealtimeBridge — mount in de (app)-layout zodat de SSE-verbinding
// blijft staan over Server Action-refreshes van de Solo-page heen.
// blijft staan over Server Action-refreshes heen.
//
// Leest het huidige product-id uit de URL (`/products/[id]/solo`).
// Wanneer de gebruiker niet op het Solo Paneel zit, wordt de stream
// gesloten — geen onnodige verbinding open houden.
// Stream opent zodra er een actief product is (ongeacht het pad), zodat
// de Live-status-dot en worker-presence-indicator in de NavBar overal
// werken. Buiten /solo is de solo-store leeg en zijn task-events no-ops
// (zie stores/solo-store.ts handleRealtimeEvent), dus de stream gedraagt
// zich automatisch als lichte presence-stream tot SoloBoard mount.
'use client'
import { usePathname } from 'next/navigation'
import { useSoloRealtime } from '@/lib/realtime/use-solo-realtime'
const SOLO_PATH_RE = /^\/products\/([^/]+)\/solo$/
export function SoloRealtimeBridge() {
const pathname = usePathname()
const match = pathname?.match(SOLO_PATH_RE)
const productId = match?.[1] ?? null
export function SoloRealtimeBridge({ productId }: { productId: string | null }) {
useSoloRealtime(productId)
return null
}

View file

@ -986,7 +986,7 @@ Niet-matchende events worden server-side gedropt zodat de browser geen irrelevan
### Connection lifecycle
- **Open**: `EventSource('/api/realtime/solo?product_id=...')` zodra de gebruiker op `/solo` is.
- **Open**: `EventSource('/api/realtime/solo?product_id=...')` zodra de gebruiker een actief product heeft. `SoloRealtimeBridge` mount in `(app)/layout` en krijgt het `productId` via prop, zodat de stream over de hele app open staat — niet alleen op `/solo`. Zo kunnen de Live-status-dot en worker-presence-indicator in de NavBar overal werken. Buiten `/solo` is de solo-store leeg en zijn binnenkomende task-events no-ops (`stores/solo-store.ts handleRealtimeEvent` skipt onbekende ids), dus de stream gedraagt zich automatisch als lichte presence-stream tot `SoloBoard` mount.
- **Reconnect**: exponential backoff bij `onerror` (1s → 30s, reset bij `ready` event).
- **Pause op tab-hidden**: `document.visibilityState === 'hidden'` sluit de stream actief. Bij `visible` wordt opnieuw verbonden. Dit voorkomt dat inactieve tabs DB-connecties open houden.
- **Hard close**: server sluit zelf na 240s (Vercel `maxDuration` is 300s); client herconnect transparant.