M13: Claude job queue — 'Voer uit'-knop + worker presence (ST-1111) (#18)

* feat(ST-1111.1): add ClaudeJob model and state-machine enum

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1111.2): add ClaudeJob status API mappers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1111.3): add enqueue/cancel ClaudeJob server actions with idempotency + NOTIFY

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1111.4): forward ClaudeJob events on solo SSE stream + initial state

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1111.6): add 'Voer uit' + cancel buttons to task detail dialog

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1111.7): add job status pill with spinner on solo task cards

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(ST-1111.8): cover job-status mappers and enqueue/cancel actions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(ST-1111.9): document Claude job queue architecture and agent flow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1111.10a): add ClaudeWorker presence model

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1111.10c): forward worker presence events on solo SSE + initial count

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1111.10d): show worker presence indicator and gate 'Voer uit' on connected workers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-29 19:51:48 +02:00 committed by GitHub
parent 1cb5772edd
commit 73087e9705
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 921 additions and 27 deletions

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 414 KiB

After

Width:  |  Height:  |  Size: 488 KiB

Before After
Before After

View file

@ -0,0 +1,69 @@
# ST-1111 — 'Voer uit'-knop met Claude Code job queue
**Story:** Als developer wil ik op het solo-scherm per task een 'Voer uit'-knop, zodat ik mijn lokale Claude Code-sessie kan inschakelen om de taak uit te voeren.
**Branch:** `feat/M13-claude-job-queue`
---
## Sub-tasks en commits
| Task | Commit |
|---|---|
| ST-1111.1 DB: ClaudeJob model + enum + migration | `5274e1e` |
| ST-1111.2 API: ClaudeJob status mappers | `a1b1f69` |
| ST-1111.3 Server actions: enqueue + cancel | `9d9fb4b` |
| ST-1111.4 SSE: ClaudeJob events op solo-stream + initial state | `ece0aa9` |
| ST-1111.5 MCP-tools (scrum4me-mcp repo — aparte PR) | — |
| ST-1111.6 UI: 'Voer uit' + cancel in TaskDetailDialog | `b9c65eb` |
| ST-1111.7 UI: status-pill op SoloTaskCard | `dace427` |
| ST-1111.8 Tests: mappers + actions | `2c2a246` |
| ST-1111.9 Docs | dit bestand |
---
## Architectuur
### State machine
```
QUEUED → CLAIMED → RUNNING → DONE
→ FAILED
→ CANCELLED (cancel-knop of server action)
CLAIMED → QUEUED (stale cleanup, >30min, via wait_for_job)
```
### NOTIFY-pijplijn
Omdat `claude_jobs` geen row-trigger heeft (zoals `tasks` en `stories`), stuurt de **server action** zelf `pg_notify` via `prisma.$executeRaw`:
```ts
await prisma.$executeRaw`SELECT pg_notify('scrum4me_changes', ${JSON.stringify(payload)}::text)`
```
Voordeel: expliciete controle over het payload-shape (met `type` i.p.v. `entity`). Nadeel: MCP-tools in de `scrum4me-mcp`-repo moeten hun eigen NOTIFY-aanroep hebben bij `update_job_status`.
### SSE-routing
De bestaande `/api/realtime/solo`-route herkent nu twee payload-shapes:
- `entity: 'task'|'story'` — bestaande trigger-events
- `type: 'claude_job_enqueued'|'claude_job_status'` — nieuwe job-events
Job-events worden gefilterd op `user_id + product_id`. Bij connect stuurt de route een `claude_jobs_initial`-event met alle actieve + recente (vandaag) jobs.
### Idempotency
`enqueueClaudeJobAction` weigert als `claude_jobs WHERE task_id=X AND status IN (QUEUED, CLAIMED, RUNNING)` bestaat. De client ontvangt `{ error, jobId }` zodat de UI naar de actieve job kan linken in plaats van een nieuw venstertje te openen.
---
## Beslissingen
**Waarom geen DB-trigger voor NOTIFY?**
De MCP-server claimt jobs via raw SQL (FOR UPDATE SKIP LOCKED); die schrijft ook direct naar de DB. Een trigger zou clean zijn, maar de MCP-tools moeten hoe dan ook hun eigen NOTIFY-payload bouwen voor `update_job_status`. Applicatie-NOTIFY houdt de payloads consistent en expliciet.
**Waarom `cancelled` verwijderd uit de store?**
Geannuleerde jobs zijn terminaal; het pill-element zou "Geannuleerd" tonen tot de gebruiker een refresh doet. In plaats daarvan wist `handleJobEvent` de entry bij `status === 'cancelled'` zodat de kaart teruggaat naar de "Voer uit"-staat.
**Auto-clear DONE/FAILED?**
Niet geïmplementeerd in v1. De pill blijft staan totdat de SSE-connectie herstart (refresh, tab-hidden+visible). Acceptabel voor de eerste iteratie.

View file

@ -1047,6 +1047,56 @@ Patroon:
**Let op:** drag-and-drop handles (`⠿`) blijven verborgen voor demo (`{!isDemo && <span {...listeners} />}`) — dragging is geen UI-showcase maar zou nep-optimistische updates triggeren.
---
## Claude job queue (M13 — ST-1111)
Developers kunnen vanuit de Task Detail Dialog een lokale Claude Code-sessie inschakelen. De job queue zorgt voor coördinatie en realtime-status.
### State machine
```
QUEUED → CLAIMED → RUNNING → DONE
→ FAILED
→ CANCELLED (door user)
CLAIMED → QUEUED (stale claim cleanup, >30min)
```
### ClaudeJob model
```
claude_jobs
id, user_id, product_id, task_id
status: ClaudeJobStatus (QUEUED|CLAIMED|RUNNING|DONE|FAILED|CANCELLED)
claimed_by_token_id (FK → api_tokens, nullable)
claimed_at, started_at, finished_at
branch, summary, error
@@index([user_id, status])
@@index([task_id, status])
@@index([status, claimed_at]) — voor stale-claim cleanup
```
### NOTIFY/LISTEN flow
```
UI klikt 'Voer uit'
→ enqueueClaudeJobAction() Server Action
→ prisma.claudeJob.create(QUEUED)
→ prisma.$executeRaw pg_notify('scrum4me_changes', {type:'claude_job_enqueued',...})
→ /api/realtime/solo SSE server-side filter: user_id + product_id
→ EventSource.onmessage browser: handleJobEvent()
→ useSoloStore.claudeJobsByTaskId map
→ SoloTaskCard pill + dialog-footer update
```
### Idempotency
`enqueueClaudeJobAction` weigert een tweede enqueue als er al een job bestaat met `status IN (QUEUED, CLAIMED, RUNNING)`. Teruggestuurde fout bevat het bestaande `jobId` zodat de UI ernaar kan linken.
### Hybride-ready
De huidige implementatie verwacht een lokale Claude Code-sessie die `wait_for_job` aanroept vanuit `madhura68/scrum4me-mcp`. Toekomstige uitbreiding naar Vercel Sandbox (serverless agent) vereist alleen een nieuw claim-endpoint — het datamodel en SSE-flow zijn ongewijzigd.
## Environment variables
| Variabele | Doel | Waar te vinden |