feat: verplaats Live + agent-status indicators naar NavBar

Live-dot (SSE-status) en "Agent verbonden / Geen agent" indicator zijn
verhuisd van de SoloBoard-header naar de NavBar (rechts, voor de
notifications-bell). Data blijft uit useSoloStore komen, gevoed door
SoloRealtimeBridge in de (app)-layout. Indicators tonen alleen op
/products/[id]/solo — buiten die route is de SSE-stream inactief.

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

View file

@ -17,6 +17,7 @@ import {
import { AppIcon } from '@/components/shared/app-icon'
import { UserMenu } from '@/components/shared/user-menu'
import { NotificationsBell } from '@/components/shared/notifications-bell'
import { SoloNavStatusIndicators } from '@/components/solo/nav-status-indicators'
import { cn } from '@/lib/utils'
import { setActiveProductAction } from '@/actions/active-product'
@ -180,8 +181,9 @@ export function NavBar({
)}
</div>
{/* Rechts: notifications + account-menu */}
{/* Rechts: solo-status + notifications + account-menu */}
<div className="flex items-center gap-2 flex-1 justify-end">
<SoloNavStatusIndicators />
<NotificationsBell currentUserId={userId} isDemo={isDemo} />
<UserMenu userId={userId} username={username} email={email} roles={roles} />
</div>

View file

@ -0,0 +1,69 @@
'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,
}: {
status: RealtimeStatus
showConnectingIndicator: boolean
}) {
let color = 'bg-status-done'
let label = 'Live'
if (showConnectingIndicator) {
if (status === 'disconnected') {
color = 'bg-priority-critical'
label = 'Verbroken — opnieuw proberen…'
} else {
color = 'bg-muted-foreground'
label = 'Verbinden…'
}
}
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={
<span
aria-label={label}
className={cn('inline-block h-2 w-2 rounded-full shrink-0 transition-colors', color)}
/>
}
/>
<TooltipContent>{label}</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
export function SoloNavStatusIndicators() {
const pathname = usePathname()
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
return (
<div className="flex items-center gap-3 px-2">
<RealtimeIndicator
status={realtimeStatus}
showConnectingIndicator={showConnectingIndicator}
/>
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<span className={cn(
'size-2 rounded-full',
connectedWorkers > 0 ? 'bg-status-done' : 'bg-muted-foreground/40'
)} />
{connectedWorkers > 0 ? 'Agent verbonden' : 'Geen agent'}
</div>
</div>
)
}

View file

@ -7,55 +7,13 @@ import {
} from '@dnd-kit/core'
import { toast } from 'sonner'
import { useSoloStore } from '@/stores/solo-store'
import type { RealtimeStatus } from '@/stores/solo-store'
import { taskStatusToApi } from '@/lib/task-status'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import { SplitPane } from '@/components/split-pane/split-pane'
import { SoloColumn, type ColumnStatus } from './solo-column'
import { SoloTaskCardOverlay } from './solo-task-card'
import { TaskDetailDialog } from './task-detail-dialog'
import { UnassignedStoriesSheet, type UnassignedStory } from './unassigned-stories-sheet'
// ST-805: kleine status-dot in de header — groen wanneer SSE-stream open
// is, grijs/rood pas zichtbaar als de connectie >4s niet open is (animatie B
// zit in useSoloRealtime). Default groen tijdens de eerste 4s zodat micro-
// disconnects geen flikker geven.
function RealtimeIndicator({
status,
showConnectingIndicator,
}: {
status: RealtimeStatus
showConnectingIndicator: boolean
}) {
let color = 'bg-status-done'
let label = 'Live'
if (showConnectingIndicator) {
if (status === 'disconnected') {
color = 'bg-priority-critical'
label = 'Verbroken — opnieuw proberen…'
} else {
color = 'bg-muted-foreground'
label = 'Verbinden…'
}
}
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={
<span
aria-label={label}
className={cn('inline-block h-2 w-2 rounded-full shrink-0 transition-colors', color)}
/>
}
/>
<TooltipContent>{label}</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
export interface SoloTask {
id: string
title: string
@ -91,9 +49,6 @@ export function SoloBoard({
productId, productName, sprintGoal, tasks: initialTasks, unassignedStories: initialUnassigned, isDemo,
}: SoloBoardProps) {
const { tasks, initTasks, optimisticMove, rollback, markPending, clearPending } = useSoloStore()
const realtimeStatus = useSoloStore((s) => s.realtimeStatus)
const showConnectingIndicator = useSoloStore((s) => s.showConnectingIndicator)
const connectedWorkers = useSoloStore((s) => s.connectedWorkers)
const [activeDragId, setActiveDragId] = useState<string | null>(null)
const [selectedTask, setSelectedTask] = useState<SoloTask | null>(null)
const [sheetOpen, setSheetOpen] = useState(false)
@ -174,20 +129,7 @@ export function SoloBoard({
<div className="flex flex-col h-full p-4 gap-4 min-h-0">
<div className="flex items-start justify-between gap-4 shrink-0">
<div className="min-w-0">
<div className="flex items-center gap-2">
<h1 className="text-base font-semibold text-foreground truncate">{productName}</h1>
<RealtimeIndicator
status={realtimeStatus}
showConnectingIndicator={showConnectingIndicator}
/>
<div className="flex items-center gap-1 text-xs text-muted-foreground ml-1">
<span className={cn(
'size-2 rounded-full',
connectedWorkers > 0 ? 'bg-status-done' : 'bg-muted-foreground/40'
)} />
{connectedWorkers > 0 ? 'Agent verbonden' : 'Geen agent'}
</div>
</div>
<h1 className="text-base font-semibold text-foreground truncate">{productName}</h1>
{sprintGoal && (
<p className="text-sm text-muted-foreground mt-0.5 line-clamp-2">{sprintGoal}</p>
)}