Scrum4Me/components/solo/nav-status-indicators.tsx
Madhura68 35835c693c feat(M13 T-519b): SSE worker_heartbeat + NavBar stand-by badge
Aanvulling op scrum4me-mcp PR #25 (worker_heartbeat MCP-tool).

- app/api/realtime/solo/route.ts: WorkerHeartbeatPayload type +
  isWorkerHeartbeatPayload guard + shouldEmit-routing op user_id.
- stores/solo-store.ts: workerQuotaPct + workerQuotaCheckAt state +
  setWorkerQuota action. Reset bij decrementWorkers naar 0.
- lib/realtime/use-solo-realtime.ts: handle worker_heartbeat-event,
  roep setWorkerQuota.
- components/solo/nav-status-indicators.tsx: stand-by badge wanneer
  workerQuotaPct < minQuotaPct + tooltip met drempel.
- components/shared/nav-bar.tsx + app/(app)/layout.tsx: minQuotaPct
  prop plumbing van User.min_quota_pct naar NavStatusIndicators.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 04:26:56 +02:00

99 lines
2.8 KiB
TypeScript

'use client'
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'
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({
hasActiveProduct,
minQuotaPct,
}: {
hasActiveProduct: boolean
minQuotaPct: number
}) {
const realtimeStatus = useSoloStore((s) => s.realtimeStatus)
const showConnectingIndicator = useSoloStore((s) => s.showConnectingIndicator)
const connectedWorkers = useSoloStore((s) => s.connectedWorkers)
const workerQuotaPct = useSoloStore((s) => s.workerQuotaPct)
if (!hasActiveProduct) return null
// M13: stand-by als alle workers low quota hebben (workerQuotaPct geldt
// voor de laatste-rapporterende worker; bij N>1 workers is dit een
// benadering — server-side aggregate is een v2-verbetering).
const isStandby =
connectedWorkers > 0 &&
workerQuotaPct !== null &&
workerQuotaPct < minQuotaPct
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',
isStandby
? 'bg-warning'
: connectedWorkers > 0
? 'bg-status-done'
: 'bg-muted-foreground/40'
)} />
{isStandby ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={<span>Stand-by ({workerQuotaPct}%)</span>}
/>
<TooltipContent>
Worker wacht tot Anthropic-quota stijgt boven {minQuotaPct}%
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : connectedWorkers > 0 ? (
'Agent verbonden'
) : (
'Geen agent'
)}
</div>
</div>
)
}