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>
This commit is contained in:
Janpeter Visser 2026-05-06 04:26:56 +02:00
parent 555ed8fe89
commit 35835c693c
6 changed files with 88 additions and 7 deletions

View file

@ -64,7 +64,17 @@ type WorkerPayload = {
product_id?: string
}
type NotifyPayload = EntityPayload | JobPayload | WorkerPayload
// M13: per-iteration quota-rapport van de worker. Geen product-scope —
// elke heartbeat geldt voor alle producten waar deze user toegang toe heeft.
type WorkerHeartbeatPayload = {
type: 'worker_heartbeat'
user_id: string
token_id: string
last_quota_pct: number
last_quota_check_at: string
}
type NotifyPayload = EntityPayload | JobPayload | WorkerPayload | WorkerHeartbeatPayload
function isJobPayload(p: NotifyPayload): p is JobPayload {
return 'type' in p && (p.type === 'claude_job_enqueued' || p.type === 'claude_job_status')
@ -74,6 +84,10 @@ function isWorkerPayload(p: NotifyPayload): p is WorkerPayload {
return 'type' in p && (p.type === 'worker_connected' || p.type === 'worker_disconnected')
}
function isWorkerHeartbeatPayload(p: NotifyPayload): p is WorkerHeartbeatPayload {
return 'type' in p && p.type === 'worker_heartbeat'
}
function shouldEmit(
payload: NotifyPayload,
productId: string,
@ -90,6 +104,10 @@ function shouldEmit(
return payload.user_id === userId
}
if (isWorkerHeartbeatPayload(payload)) {
return payload.user_id === userId
}
// M11 (ST-1104): question-events horen op /api/realtime/notifications, niet hier.
if (payload.entity === 'question') return false