feat(M13 PBI-31 T-519b/T-520b): NavBar stand-by badge + quota-check runbook (#119)

* 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>

* docs(M13 T-520b): pre-flight quota-check sectie in mcp-integration

Documenteert de batch-loop-uitbreiding:
1. get_worker_settings → min_quota_pct
2. bin/worker-quota-probe.sh → pct + reset
3. worker_heartbeat naar server (NavBar stand-by-badge)
4. Sleep tot reset bij low quota; anders wait_for_job

Verwijst naar bin/worker-quota-probe.sh in scrum4me-docker (zie
PR daar).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-06 04:34:48 +02:00 committed by GitHub
parent 555ed8fe89
commit 31dc429b61
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 126 additions and 8 deletions

View file

@ -64,6 +64,11 @@ interface SoloStore {
claudeJobsByTaskId: Record<string, JobState>
connectedWorkers: number
// M13: laatste quota-rapport van een actieve worker. null = geen
// worker actief of nog geen heartbeat met quota ontvangen.
workerQuotaPct: number | null
workerQuotaCheckAt: string | null
initTasks: (tasks: SoloTask[]) => void
optimisticMove: (taskId: string, toStatus: TaskStatus) => TaskStatus | null
rollback: (taskId: string, prevStatus: TaskStatus) => void
@ -82,6 +87,7 @@ interface SoloStore {
setWorkers: (count: number) => void
incrementWorkers: () => void
decrementWorkers: () => void
setWorkerQuota: (pct: number, checkAt: string) => void
handleRealtimeEvent: (event: RealtimeEvent) => void
}
@ -93,6 +99,8 @@ export const useSoloStore = create<SoloStore>((set, get) => ({
showConnectingIndicator: false,
claudeJobsByTaskId: {},
connectedWorkers: 0,
workerQuotaPct: null,
workerQuotaCheckAt: null,
initTasks: (tasks) =>
set({ tasks: Object.fromEntries(tasks.map(t => [t.id, t])) }),
@ -145,7 +153,15 @@ export const useSoloStore = create<SoloStore>((set, get) => ({
setWorkers: (count) => set({ connectedWorkers: Math.max(0, count) }),
incrementWorkers: () => set(s => ({ connectedWorkers: s.connectedWorkers + 1 })),
decrementWorkers: () => set(s => ({ connectedWorkers: Math.max(0, s.connectedWorkers - 1) })),
decrementWorkers: () =>
set((s) => ({
connectedWorkers: Math.max(0, s.connectedWorkers - 1),
// Reset quota-state als alle workers weg zijn — pct van een vertrokken
// worker is niet meer actueel.
workerQuotaPct: s.connectedWorkers - 1 <= 0 ? null : s.workerQuotaPct,
workerQuotaCheckAt: s.connectedWorkers - 1 <= 0 ? null : s.workerQuotaCheckAt,
})),
setWorkerQuota: (pct, checkAt) => set({ workerQuotaPct: pct, workerQuotaCheckAt: checkAt }),
handleJobEvent: (event) => {
const { job_id, task_id } = event