* docs(T-1014): PB-workflow doc — skelet + as-is architectuur-lagen en stores Eerste laag van het Product Backlog page workflow-doc (PBI-88 / ST-1363): frontmatter, Context & scope, de architectuur-lagen (PG-triggers -> SSE -> Zustand -> React) en de drie voedende Zustand-stores. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(T-1015): PB-workflow doc — as-is workflow-states, transitions en diagram Tweede laag van het Product Backlog page workflow-doc (PBI-88 / ST-1363): de zeven impliciete workflow-states met preconditie en UI-gedrag, de transition-tabel, en een Mermaid stateDiagram-v2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(T-1016): PB-workflow doc — to-be expliciete state machine Derde laag van het Product Backlog page workflow-doc (PBI-88 / ST-1363): canonieke state-set met mapping op de as-is werkelijkheid, transitietabel, en het ontwerp van een dunne deriveScreenState()-afleidingslaag bovenop de bestaande PBI-74 stores (geen nieuwe store). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(T-1017): PB-workflow doc — gap-analyse, aanbevelingen en docs-wiring Slotlaag van het Product Backlog page workflow-doc (PBI-88 / ST-1363): gap-analyse (G1-G6, incl. de oorspronkelijke switcher-FOUT), niet-bindende aanbevelingen, en verwante-docs sectie. Haakt het doc in via de architecture.md breadcrumb en een cross-link vanuit functional.md F-04. npm run docs groen: INDEX geregenereerd, alle doc-links valide. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
74 KiB
| title | status | audience | language | last_updated | ||
|---|---|---|---|---|---|---|
| Scrum4Me — Functionele Specificatie | active |
|
nl | 2026-05-08 |
Scrum4Me — Functionele Specificatie
Versie: 0.2 — april 2026 Volgt op: Brainstorm v0.3, Personas v0.1
MVP-scopeverklaring
v1 is een desktop-first fullstack webapplicatie waarmee een solo developer of klein Scrum Team meerdere softwareprojecten hiërarchisch kan plannen (product → PBI → story → taak), Sprints kan beheren via gesplitste schermen met drag-and-drop, en Claude Code kan integreren voor geautomatiseerde implementatieflows waarbij elk resultaat wordt vastgelegd in de story. Een Product Owner kan Developers (via gebruikersnaam) aan een product backlog koppelen; zij krijgen dan schrijfrechten op stories, taken en sprints van dat product. De app is deployable op Vercel + Neon én volledig lokaal draaibaar.
Expliciet buiten scope voor v1
- Uitnodigingsflow voor teams — Developers toevoegen via gebruikersnaam is beschikbaar; e-mailuitnodiging of link-gebaseerde onboarding komt in v2
- E-mailverificatie bij registratie — gebruikersnaam/wachtwoord volstaat voor v1
- Daily Scrum, Sprint Review en Sprint Retrospective schermen — v2
- Automatische statusupdate van stories na commit — handmatige update via UI in v1
- Tijdregistratie, urenverantwoording en burndown-charts — buiten positionering
- Integratie met externe tools (GitHub Issues, Linear, Jira) — v2
- Notificaties en reminders — v2
- Native mobiele app — web-first; een toekomstige mobiele variant richt zich uitsluitend op taken afvinken
- Responsive layout voor schermen smaller dan 1024px — desktop-first hoofdpad. Voor telefoons (UA met
Mobi) is er een aparte mobile-shell onder/m/*met drie schermen — zie sectie Mobile shell hieronder.
Feature-specificaties
F-01: Authenticatie
Prioriteit: v1 — Kritiek Persona: Lars, Dina, Remi
Omschrijving: Gebruikers kunnen een account aanmaken en inloggen met gebruikersnaam en wachtwoord. Er is een ingebouwde demo-gebruiker met alleen leesrechten. Geen e-mailverificatie vereist in v1.
Acceptatiecriteria:
- Registratie vereist gebruikersnaam (uniek, min. 3 tekens) en wachtwoord (min. 8 tekens)
- Dubbele gebruikersnaam geeft duidelijke foutmelding bij registratie
- Inloggen met incorrecte combinatie geeft generieke foutmelding (geen onderscheid naam/wachtwoord)
- Na inloggen wordt de gebruiker doorgestuurd naar het dashboard
- Sessie blijft actief totdat de gebruiker uitlogt
- Demo-gebruiker kan inloggen met vaste credentials (zichtbaar op de loginpagina)
- Demo-gebruiker kan niets aanmaken, aanpassen of verwijderen
- Alle schrijfknoppen zijn zichtbaar maar uitgeschakeld voor de demo-gebruiker, met tooltip "Niet beschikbaar in demo-modus"
- Uitlogknop is altijd zichtbaar in de navigatie
Randgevallen:
- Gebruiker probeert beschermde route te bezoeken zonder sessie → redirect naar /login
- Demo-gebruiker probeert via de API te schrijven → 403 Forbidden
Data:
- Opgeslagen:
users(id, username, password_hash, role[], is_demo, created_at) - Sessie: JWT of server-side sessie met user_id en is_demo vlag
F-01b: Inloggen via mobiel (QR-pairing)
Prioriteit: v1 — Belangrijk Persona: Lars (publieke demo-laptops), Dina (klantapparatuur)
Omschrijving: Een gebruiker kan op een (publieke of gedeelde) desktop inloggen zonder zijn wachtwoord te typen, door op zijn al-ingelogde mobiele apparaat een QR-code te scannen die de desktop toont. Na een expliciete tap op "Bevestig" op de mobiel raakt de desktop binnen 1–2 seconden ingelogd. De flow is bedoeld om typen op vreemde toetsenborden, shoulder-surfing en autofill-history te vermijden.
Verloop:
- Op het login-scherm klikt de desktop-gebruiker op "Inloggen via mobiel". De server maakt een eenmalige pairing-rij aan (status
pending, vervalt na 2 minuten) met twee gescheiden geheimen:mobileSecret(voor de mobiel) endesktopToken(HttpOnly cookie voor de desktop). - De desktop toont een QR-code. De code bevat een URL met
mobileSecretin het URL-fragment (#s=…) — dit fragment wordt door browsers nooit naar servers gestuurd, dus belandt niet in access logs of analytics. - De gebruiker scant met zijn telefoon. De OS-camera opent de URL in de mobiele Scrum4Me-tab. Een Client Component leest het fragment en POST't
mobileSecretin de body naar de approve-endpoint. - De mobiele bevestigingspagina toont "Inloggen op {browser-omschrijving} ({IP}) als {jouw-gebruikersnaam}?" met een Bevestig- en Annuleer-knop. Na een tap op Bevestig wordt de pairing approved (status
approved, vervaltijd verlengd naar 5 minuten); de desktop ontvangt dit binnen 1–2 seconden via een SSE-stream die geauthenticeerd is met het HttpOnly desktop-cookie. - De desktop claimt de sessie atomisch (eenmalig consumeerbaar), krijgt zijn iron-session cookie en wordt naar
/dashboarddoorgestuurd.
Acceptatiecriteria:
- Knop "Inloggen via mobiel" zichtbaar op
/loginnaast het wachtwoord-formulier - QR-code vernieuwt automatisch na 2 minuten via een "Vernieuwen"-knop
- Mobiele bevestigingspagina toont browser/UA en best-effort IP van de desktop
- Demo-gebruiker kan niet als approver fungeren — duidelijke foutmelding "Niet beschikbaar in demo-modus"
- Paired-sessie heeft een eigen TTL van 8 uur (korter dan reguliere wachtwoord-login) en is herkenbaar via een
paired-vlag in het session-payload - Een tweede claim met dezelfde pairing geeft
410 Gone(one-time use) mobileSecretkomt nergens in een GET-URL voor (alleen in URL-fragment of POST-body);desktopTokenstaat alleen in een HttpOnly cookie metPath=/api/auth/pair,Max-Age=120,SameSite=Lax- In
nginx/Vercel access logs is geen secret-materiaal terug te vinden (acceptatietest)
Randgevallen:
- QR vervalt voordat mobiel scant → mobiele pagina toont "Pairing verlopen, vraag een nieuwe QR-code op"; desktop toont "Vernieuwen"-knop
- Pairing approved maar desktop claimt niet binnen 5 minuten → atomic update faalt; pairing-rij wordt automatisch genegeerd; gebruiker start opnieuw
- Gebruiker scant een phishing-QR vanaf een willekeurige website → mobiele bevestiging toont onbekende UA/IP; expliciete bevestiging vereist; de gebruiker kan annuleren
- Gebruiker is op de mobiel niet ingelogd → middleware-guard van
/m/pairredirectt naar/loginmet return-URL - Gebruiker logt zichzelf uit op de mobiel terwijl de pairing nog
pendingis → approve faalt op auth-check
Data:
- Nieuw:
login_pairings(id, secret_hash, desktop_token_hash, status, user_id?, desktop_ua?, desktop_ip?, created_at, expires_at, approved_at?, consumed_at?) - Postgres-trigger op
login_pairingspubliceert viapg_notify('scrum4me_pairing', …) - Sessie-payload: nieuwe optionele
paired: booleanenpairedExpiresAt: number
F-02: Roltoewijzing
Prioriteit: v1 — Fundament voor v2 Persona: Remi (v2), Lars en Dina (impliciet)
Omschrijving: Een gebruiker kan bij registratie of in instellingen één of meerdere Scrum-rollen aannemen: Product Owner, Scrum Master, Developer. De rol Developer is relevant voor teambeheer: alleen gebruikers met de rol Developer kunnen aan een product backlog worden gekoppeld door de eigenaar.
Acceptatiecriteria:
- Gebruiker kan bij registratie of achteraf in instellingen rollen selecteren
- Minimaal één rol is verplicht
- Alle drie de rollen tegelijk zijn toegestaan
- Geselecteerde rollen zijn zichtbaar in de profielbalk
- Alleen gebruikers met de rol Developer kunnen als teamlid aan een product backlog worden gekoppeld
- Demo-gebruiker heeft een vaste rol (Developer) die niet gewijzigd kan worden
Data:
- Opgeslagen:
user_roles[]als array op het gebruikersobject (enum: PRODUCT_OWNER, SCRUM_MASTER, DEVELOPER)
F-02b: Gebruikersprofiel
Prioriteit: v1 Persona: Lars, Dina, Remi
Omschrijving: Een gebruiker kan een profielfoto, korte omschrijving (bio) en uitgebreide beschrijving toevoegen via de instellingenpagina. De profielfoto wordt server-side verwerkt met Sharp en opgeslagen als WebP bytea in PostgreSQL. Het profiel is niet publiek zichtbaar in v1 maar vormt de basis voor v2-teamweergaven.
Acceptatiecriteria:
- Gebruiker kan een foto uploaden (JPEG, PNG of WebP, max 12 MB)
- Validatie op MIME-type en bestandsgrootte vóór verwerking — ongeldige bestanden worden geweigerd met een foutmelding
- Server converteert afbeelding naar WebP, maximaal 700×700 px (fit inside, niet uitrekken)
- Profielfoto wordt weergegeven in de instellingenpagina na upload
- Gebruiker kan een korte omschrijving invoeren (max 160 tekens)
- Gebruiker kan een uitgebreide beschrijving invoeren (max 2000 tekens)
- Opslaan van bio-velden is los van de foto-upload (apart formulier)
- Demo-gebruiker ziet de profiel-sectie niet (uitgeschakeld)
Implementatie:
POST /api/profile/avatar— upload + Sharp-verwerking + opslag als byteaGET /api/profile/avatar?v=<timestamp>— serveert avatar metCache-Control: private, max-age=3600; timestamp in de URL zorgt voor cache-invalidatie na uploadupdateProfileActionServer Action — slaat bio en bio_detail op
Data:
users.bio— VarChar(160), nullableusers.bio_detail— VarChar(2000), nullableusers.avatar_data— Bytes (bytea), nullable; altijd WebP na verwerkingusers.updated_at— wordt bijgewerkt bij elke wijziging; gebruikt als versienummer in de avatar-URL
F-02c: Product Backlog-overzicht in instellingen
Prioriteit: v1 Persona: Lars, Dina, Remi
Omschrijving: De instellingenpagina toont een gecombineerde lijst van alle product backlogs waarbij de gebruiker betrokken is: producten waarvan hij/zij eigenaar is, en producten waarbij hij/zij als Developer is toegevoegd. Vanuit deze lijst kan een team-lidmaatschap worden beëindigd.
Acceptatiecriteria:
- Alle actieve (niet-gearchiveerde) producten van de ingelogde gebruiker zijn zichtbaar met badge "Eigenaar"
- Alle producten waarbij de gebruiker als Developer is toegevoegd zijn zichtbaar met badge "Developer" en de naam van de eigenaar
- Klikken op een productnaam navigeert naar de product backlog
- Bij een Developer-lidmaatschap is een "Verlaten"-knop zichtbaar met bevestigingsstap
- Na verlaten verdwijnt het product uit de lijst
- Eigenaar-producten hebben geen verlaat-actie
- Lege staat toont een link naar "Product aanmaken"
- Demo-gebruiker ziet de lijst maar heeft geen verlaat-knop
F-03: Productbeheer
Prioriteit: v1 — Kritiek Persona: Lars (meerdere eigen projecten), Dina (per klant), Remi (per team-product)
Omschrijving: Gebruikers kunnen producten aanmaken, bewerken en archiveren. Een product is het hoogste niveau in de hiërarchie en bevat een naam, beschrijving, git-repo URL en de Definition of Done. Alle andere entiteiten (PBI's, stories, taken) horen bij een product.
Acceptatiecriteria:
- Product aanmaken vereist een naam (uniek per gebruiker, verplicht)
- Beschrijving is optioneel (vrije tekst, max. 1000 tekens)
- Git-repo URL is optioneel; wordt gevalideerd als geldige URL bij invullen
- Definition of Done is een vaste tekst, instelbaar per product (verplicht bij aanmaken, max. 500 tekens)
- Product verschijnt direct in de productenlijst na aanmaken
- Naam en alle andere velden zijn bewerkbaar na aanmaken
- Archiveren is omkeerbaar; gearchiveerde producten zijn standaard verborgen
- Productenlijst toont: naam, beschrijving (ingekort tot 80 tekens), git-repo link (indien aanwezig)
- Klikken op een product opent de Product Backlog van dat product
- Lege staat toont een duidelijke prompt om een eerste product aan te maken
Randgevallen:
- Gebruiker probeert naam leeg te maken bij bewerken → validatiefout, opslaan geblokkeerd
- Git-repo URL zonder
https://→ validatiefout met suggestie
Data:
- Opgeslagen:
products(id, user_id, name, description, repo_url, definition_of_done, archived, created_at, updated_at)
F-04: Product Backlog — 3-paneels gesplitst scherm
Prioriteit: v1 — Kritiek Persona: Lars, Dina, Remi
Omschrijving: De Product Backlog wordt weergegeven als een 3-paneels gesplitst scherm: PBI's (links) | Stories (midden) | Taken (rechts). De splitters zijn versleepbaar. Selectie cascadeert: klikken op een PBI toont de bijbehorende stories; klikken op een story toont de bijbehorende taken. Elk paneel heeft een eigen navigatiebar met acties.
Workflow & states: dit spec beschrijft de layout. Voor het gedrag — architectuur-lagen, de impliciete workflow-states, transitions en de to-be state machine — zie architecture/product-backlog-workflow.md.
Acceptatiecriteria:
- Standaard splitverhouding is 20/45/35 (PBI's / Stories / Taken)
- Splitters zijn versleepbaar; positie wordt opgeslagen in een cookie (
sp:backlog-{id}) - Selecteren van een PBI toont de bijbehorende stories in het middenpaneel
- Geselecteerd PBI is visueel gemarkeerd (achtergrondkleur of rand)
- Selecteren van een story toont de bijbehorende taken in het rechterpaneel
- Geselecteerde story is visueel gemarkeerd
- Cascade-reset: selecteren van een ander PBI wist de geselecteerde story en taken
- PBI-paneel navigatiebar bevat: [+ PBI aanmaken]
- Stories-paneel navigatiebar bevat: [+ Story aanmaken], [sorteer], [filter status]
- Taken-paneel navigatiebar bevat: [+ Nieuwe taak]
- Lege staat PBI-paneel: prompt om eerste PBI aan te maken
- Lege staat Stories-paneel (geen PBI geselecteerd): instructie om een PBI te selecteren
- Lege staat Stories-paneel (PBI geselecteerd, geen stories): prompt om eerste story aan te maken
- Lege staat Taken-paneel (geen story geselecteerd): instructie om een story te selecteren
- Lege staat Taken-paneel (story geselecteerd, geen taken): prompt om eerste taak aan te maken
- Taak aanmaken opent TaskDialog via
?newTask=1&storyId={id} - Taak bewerken opent TaskDialog via
?editTask={id}
Randgevallen:
- Scherm smaller dan 1024px → 3-paneels scherm schakelt over naar 3 tabbladen (PBI's | Stories | Taken)
- Mobile tab-navigatie: klikken op PBI schakelt automatisch naar Stories-tab; klikken op story schakelt naar Taken-tab
- Mobile ← terug-knop in tab-header op tabs 2 en 3 navigeert naar het vorige tabblad
F-05: PBI-beheer
Prioriteit: v1 — Kritiek Persona: Lars, Dina, Remi
Omschrijving: Product Backlog Items kunnen worden aangemaakt, bewerkt, geprioriteerd (1–4) en gerangschikt via drag-and-drop binnen dezelfde prioriteitsgroep. PBI's worden gegroepeerd per prioriteit met een visuele scheiding.
Acceptatiecriteria:
- PBI aanmaken vereist een titel (verplicht, max. 200 tekens)
- Omschrijving is optioneel (vrije tekst, max. 2000 tekens)
- Prioriteit is verplicht (1 = Kritiek, 2 = Hoog, 3 = Middel, 4 = Laag)
- PBI's worden gegroepeerd per prioriteit; elke groep heeft een visueel label en scheidingslijn
- Binnen een prioriteitsgroep is de volgorde instelbaar via drag-and-drop (dnd-kit)
- Slepen over een prioriteitsgrens wijzigt de prioriteit van het PBI
- Volgorde en prioriteit worden direct opgeslagen na loslaten
- PBI bewerken (titel, omschrijving, prioriteit) via inline bewerkingsmodus
- PBI verwijderen vereist bevestiging; cascade-verwijdering van gekoppelde stories en taken
- Bevestigingsdialoog vermeldt expliciet dat stories en taken ook verwijderd worden
- Filter op prioriteit werkt realtime; actief filter is visueel zichtbaar en eenvoudig te wissen
- Drag-and-drop placeholder toont de doelpositie tijdens het slepen
Randgevallen:
- Prioriteitsgroep is leeg na verplaatsing → groep verdwijnt uit de weergave
- PBI met stories verwijderen → bevestigingsdialoog toont het aantal gekoppelde stories
Data:
- Opgeslagen:
pbis(id, product_id, title, description, priority (1–4), sort_order, created_at, updated_at)
F-06: Story-beheer
Prioriteit: v1 — Kritiek Persona: Lars, Dina, Remi
Omschrijving: Stories worden weergegeven als compacte blokken (~10% schermbreedte) in het rechterpaneel van de Product Backlog, gerangschikt op prioriteit. Ze kunnen worden aangemaakt, bewerkt, geprioriteerd en gerangschikt via drag-and-drop.
Acceptatiecriteria:
- Story aanmaken vereist een titel (verplicht, max. 200 tekens)
- Omschrijving is optioneel (vrije tekst, max. 2000 tekens)
- Acceptatiecriteria zijn optioneel (vrije tekst, max. 2000 tekens)
- Prioriteit is verplicht (1–4, zelfde schaal als PBI's)
- Stories worden weergegeven als blokken van ~10% schermbreedte, horizontaal gerangschikt
- Blokken tonen: storytitel (ingekort), prioriteit-badge, status-badge
- Elke prioriteitsgroep heeft een visuele scheiding (gekleurde band of scheidingslijn)
- Volgorde binnen een prioriteitsgroep is instelbaar via drag-and-drop (dnd-kit)
- Slepen over een prioriteitsgrens wijzigt de prioriteit van de story
- Klikken op een storyblok opent de story-detailweergave (slide-over of modal)
- Story verwijderen vereist bevestiging; cascade-verwijdering van gekoppelde taken
- Stories die aan een Sprint gekoppeld zijn tonen een badge "In Sprint [naam]"
Randgevallen:
- Storytitel is langer dan past in het blok → afgekapt met ellipsis, volledige titel zichtbaar bij hover
Data:
- Opgeslagen:
stories(id, pbi_id, product_id, title, description, acceptance_criteria, priority (1–4), sort_order, status (OPEN | IN_SPRINT | DONE), sprint_id?, created_at, updated_at)
F-07: Story-activiteitenlog
Prioriteit: v1 — Kern van Claude Code-integratie Persona: Lars
Omschrijving: Elke story heeft een activiteitenlog die alle door Claude Code vastgelegde stappen toont: implementatieplannen, testresultaten en commits. De log is zichtbaar in de story-detailweergave en is read-only vanuit de UI.
Acceptatiecriteria:
- Log toont alle entries in chronologische volgorde (oudste eerst)
- Elk entry-type heeft een eigen visuele stijl:
implementation_plan: blauw, icoon documenttest_result (passed): groen, icoon vinkjetest_result (failed): rood, icoon kruiscommit: paars, icoon git-branch
- Commit-entries tonen de hash (ingekort, bijv.
a1b2c3d) en commit-bericht - Commit-hash is klikbaar als de git-repo URL is ingesteld op het product; opent in nieuw tabblad
- Log is leeg bij nieuwe stories → lege staat toont "Nog geen activiteit vastgelegd"
- Log is niet bewerkbaar via de UI
Data:
- Opgeslagen:
story_logs(id, story_id, type (IMPLEMENTATION_PLAN | TEST_RESULT | COMMIT), content, status? (PASSED | FAILED), commit_hash?, commit_message?, created_at)
F-08: Ideeën-laag (Idea capture, grill, plan)
Prioriteit: v1 — Hoog (M12 — vervangt Todo-lijst) Persona: Lars (snelle vastlegging), Dina (losse klantnotities)
Omschrijving:
Een idea is een gestructureerde voorganger van een PBI. De gebruiker maakt een idea snel aan (titel + optioneel product). Een agent grilt het idea via een interactieve Q&A-loop (IDEA_GRILL-job → grill_md); daarna materialiseert een tweede agent het idea in een deterministisch plan (IDEA_MAKE_PLAN-job → plan_md) dat parsed wordt naar PBI + stories + tasks. De todos-tabel uit eerdere versies is per migratie ST-1239 gedropt en volledig vervangen door ideas.
Acceptatiecriteria:
- Idea aanmaken via snel-invoerveld (Enter om op te slaan); titel verplicht, product optioneel
- Idea-lijst toont items per status (
DRAFT,GRILLING,GRILLED,PLAN_READY,PLANNED, …) - Per idea kan de gebruiker een grill-job starten — agent stelt vragen via het claude-question-kanaal
- Per idea kan de gebruiker (na succesvol grillen) een make-plan-job starten — agent produceert strict yaml-frontmatter; parse-fail =
PLAN_FAILED PLAN_READYtoont de gegenereerde PBI/story/task-structuur als preview; bevestigen materialiseert ze in de productbacklog en zet de idea opPLANNED- Idea's zijn user-private (geen
productAccessFilter); secundaire producten viaidea_products - Demo-gebruiker kan idea's lezen maar niet schrijven of grillen
Randgevallen:
- Idea heeft geen primair product → make-plan vraagt eerst om producttoekenning voordat materialisatie kan
- Grill-job time-out / agent crash → status valt terug naar
GRILL_FAILED; gebruiker kan opnieuw grillen - Plan-output past niet in het strict yaml-format →
PLAN_FAILED+IdeaLog{JOB_EVENT}met de parse-error
Data:
- Opgeslagen:
ideas,idea_products,idea_logs,user_questions— zie data-model en het M12 plan. Het profiel voor de IdeaDialog staat in docs/specs/dialogs/idea.md.
F-09: Sprint aanmaken en beheren
Prioriteit: v1 — Hoog Persona: Lars, Dina, Remi
Omschrijving:
Het Scrum Team kan meerdere Sprints per product aanmaken (PBI-63), elk met een eigen Sprint Goal en stabiele code (SP-N). Een sprint-switcher in de product-header schakelt tussen sprints. Stories worden via een gesplitst scherm vanuit de Product Backlog naar de Sprint Backlog gesleept.
Acceptatiecriteria:
- Sprint aanmaken vereist een Sprint Goal (verplicht, max. 500 tekens)
- Sprint is gekoppeld aan een product en krijgt automatisch een
code(SP-1,SP-2, …) sequentieel per product - Een product mag tegelijk meerdere
OPEN-sprints hebben; de sprint-switcher in de product-header bepaalt welke actief is in de UI - Optionele
start_dateenend_dateop een sprint (puur planningsmetadata) - Sprint Backlog scherm is gesplitst: Sprint Backlog links, stories per PBI rechts
- Rechterpaneel toont alle PBI's inklapbaar, met hun stories eronder
- Stories die al in de Sprint zitten zijn visueel gemarkeerd en niet opnieuw sleepbaar
- Story naar Sprint slepen via drag-and-drop (dnd-kit) van rechts naar links
- Story in de Sprint Backlog is herrangschikbaar via drag-and-drop
- Story uit Sprint verwijderen via contextmenu of verwijderknop → story keert terug in Product Backlog
- Sprint Goal is bewerkbaar na aanmaken
- Sprint afronden zet status op
CLOSEDen past stories aan volgens de afsluit-keuze (DONE of terug naar OPEN, per story in afronden-dialoog)
Randgevallen:
- Story wordt uit Sprint verwijderd terwijl er taken aan hangen → taken blijven bestaan maar worden losgekoppeld van de Sprint
- Sprint wordt afgerond met openstaande stories → afsluit-dialoog dwingt een keuze per story; geen impliciete defaults
Data:
- Opgeslagen:
sprints(id, product_id, code, sprint_goal, status (OPEN | CLOSED | ARCHIVED | FAILED), start_date?, end_date?, created_at, completed_at?). Voor uitvoering door agents zie ooksprint_runs+sprint_task_executionsin data-model.
F-10: Sprint Planning — taken aanmaken
Prioriteit: v1 — Hoog Persona: Lars, Remi
Omschrijving: In het Sprint Planning scherm worden stories uit de Sprint Backlog opgedeeld in taken. Het scherm is gesplitst: stories links, taken van de geselecteerde story rechts. Taken kunnen worden geprioriteerd en gerangschikt via drag-and-drop.
Acceptatiecriteria:
- Sprint Planning scherm toont Sprint Backlog stories links in volgorde
- Selecteren van een story links toont de bijbehorende taken rechts
- Taak aanmaken vereist een titel (verplicht, max. 200 tekens)
- Omschrijving is optioneel (max. 1000 tekens)
- Prioriteit is verplicht (1–4)
- Taken zijn gerangschikt op prioriteit en volgorde; volgorde instelbaar via drag-and-drop (dnd-kit)
- Taakstatus is instelbaar via de UI:
TO_DO | IN_PROGRESS | REVIEW | DONE(plusFAILEDenEXCLUDEDgezet door agent-flows; zielib/task-status.ts) - Story toont een voortgangsindicator (bijv. "2/5 taken Done"); auto-promotie van story naar DONE wanneer alle tasks DONE zijn
- Taak verwijderen vereist bevestiging
Randgevallen:
- Story heeft geen taken → lege staat rechts met prompt om eerste taak aan te maken
- Alle taken van een story zijn Done → story promoot automatisch naar DONE in dezelfde transactie
Data:
- Opgeslagen:
tasks(id, story_id, product_id, sprint_id?, code, title, description?, implementation_plan?, priority (1–4), sort_order, status (TO_DO | IN_PROGRESS | REVIEW | DONE | FAILED | EXCLUDED), verify_only, verify_required, repo_url?, created_at, updated_at)
F-11: Claude Code REST API
Prioriteit: v1 — Kern-differentiator Persona: Lars
Omschrijving: Een REST API waarmee Claude Code stories en taken kan ophalen, de taakvolgorde kan beoordelen en aanpassen, en implementatieplannen, testresultaten en commits kan vastleggen. Alle endpoints zijn beveiligd via een API-token.
Acceptatiecriteria:
Endpoints:
GET /api/health— liveness, optioneel?db=1voor DB-ping (geen auth)GET /api/products— lijst van actieve producten waarvoor de tokengebruiker eigenaar of teamlid isGET /api/products/:id/next-story— hoogst geprioriteerde open story van de actieve sprintGET /api/products/:id/claude-context— bundled context (product / sprint / story / tasks) voor MCPGET /api/sprints/:id/tasks?limit=10— eerste N taken in huidige volgordePATCH /api/stories/:id/tasks/reorder— accepteert geordende lijst van taak-id'sPOST /api/stories/:id/log— vastleggen van implementatieplan, testresultaat of commitPATCH /api/tasks/:id— status bijwerken (todo → in_progress → review → done) en/ofimplementation_planopslaanGET / POST /api/ideasenGET / PATCH /api/ideas/:id— idea CRUD (vervangt voormaligPOST /api/todos)GET /api/jobs/:id/sub-tasks— sprint-task-executions van een SPRINT_IMPLEMENTATION-job
Authenticatie:
- Alle endpoints vereisen
Authorization: Bearer <token>header - Ontbrekend of ongeldig token geeft 401 Unauthorized
- Demo-gebruiker API-token geeft 403 op alle schrijf-endpoints
Responsformaat:
- Alle responses zijn JSON
- Foutresponses bevatten
{ "error": "omschrijving" } GET /api/products/:id/next-storygeeft 404 als er geen open stories zijn
Data:
- Opgeslagen:
api_tokens(id, user_id, token_hash, label, created_at, revoked_at?)
F-11b: Vraag-antwoord-kanaal Claude ↔ user
Prioriteit: v1 — Verdiept de Claude-integratie (richting B uit strategisch overleg) Persona: Lars (primair), Dina (idem voor klant-werk)
Omschrijving: Wanneer Claude Code tijdens het implementeren van een story een keuze niet uit de acceptance-criteria kan afleiden, post hij een gestructureerde vraag naar Scrum4Me via een MCP-tool. De Scrum4Me-app toont een notificatie-badge voor iedereen met toegang tot het product. Een gebruiker beantwoordt de vraag in de UI; Claude leest het antwoord (sync via een polling-tool of in een latere sessie) en gaat door zonder te raden of te wachten in de Claude Code-sessie.
Verloop:
- Claude heeft een vraag → roept MCP-tool
ask_user_questionaan met{ story_id, question, options?, wait_seconds? }. Tool schrijft een rij naarclaude_questionsmet statusopen, vervaltijd 24 u. - Postgres-trigger emit op het bestaande
scrum4me_changes-kanaal metentity: 'question'. De Scrum4Me-app heeft een user-scoped SSE-stream die filter't op product-toegang. - NavBar-bell krijgt een badge met de count van open vragen voor deze gebruiker. Story-assignee ziet een visuele "wacht op jou"-emphase.
- Klik op bell → slide-over met lijst → klik op item → modal met de volledige vraag, story-context-link en (optionele) keuze-opties. Submit verstuurt het antwoord via Server Action.
- Trigger fired opnieuw, alle SSE-clients zien het item verdwijnen. Claude's tool-poller (als
wait_secondswas meegegeven) krijgt het antwoord direct terug; anders haalt Claude het later op viaget_question_answer.
Acceptatiecriteria:
- Claude kan via MCP een vraag stellen (
ask_user_question); demo-tokens krijgen permission-denied - Bell-icon in NavBar toont badge met aantal open vragen voor de ingelogde gebruiker
- Iedere gebruiker met product-toegang kan antwoorden; story-assignee krijgt visuele markering
- Demo-gebruiker kan vragen lezen maar de Verstuur-knop is uitgeschakeld met tooltip
- Optionele
wait_seconds(max 600) laat de MCP-tool blijven pollen; bij timeout retourneert hijstatus: 'pending' - Concurrent dubbele submit op zelfde vraag: één wint via atomic
updateMany, ander krijgt foutmelding "al beantwoord" - Vragen ouder dan 24 u worden via een Vercel cron op
expiredgezet - Cross-product-isolatie: een gebruiker ziet alleen vragen van producten waar hij toegang toe heeft
Randgevallen:
- Claude vraagt iets en is daarna offline (Claude Code-sessie afgesloten) → vraag blijft in DB; volgende sessie roept
list_open_questionsofget_question_answerop - Story-assignee verandert nadat de vraag is gesteld → de vraag blijft beantwoordbaar door iedereen met product-toegang; visuele emphase volgt de actuele assignee
- Vraag verloopt voordat iemand antwoord geeft → cron zet 'm op
expired; Claude'sget_question_answerretourneertstatus: 'expired' - Phishing/abuse: alleen geverifieerde Claude-tokens kunnen vragen stellen; Scrum4Me-gebruikers zien alleen vragen van hun eigen producten
Data:
- Nieuw:
claude_questions(id, story_id, task_id?, product_id, asked_by, question, options?, status, answer?, answered_by?, answered_at?, created_at, expires_at) - Postgres-trigger op
claude_questionspubliceert viapg_notify('scrum4me_changes', ...) - Nieuwe MCP-tools in mcp:
ask_user_question,get_question_answer,list_open_questions,cancel_question
F-12: API-tokenbeheer
Prioriteit: v1 — Vereiste voor Claude Code-integratie Persona: Lars
Omschrijving: Gebruikers kunnen API-tokens aanmaken, labelen en intrekken via de instellingenpagina. Een token wordt eenmalig in klare tekst getoond en daarna niet meer.
Acceptatiecriteria:
- Gebruiker kan een token aanmaken met een optioneel label (bijv. "Claude Code — laptop")
- Token wordt eenmalig getoond na aanmaken in een kopieerbaar veld
- Na sluiten van het dialoog is de tokennwaarde niet meer opvraagbaar
- Tokenoverzicht toont: label, aanmaakdatum, status (actief / ingetrokken)
- Gebruiker kan een token intrekken; ingetrokken tokens werken direct niet meer
- Maximaal 10 actieve tokens per gebruiker
Randgevallen:
- Gebruiker sluit aanmaakmeldingsdialoog per ongeluk → token bestaat al, maar waarde is verloren; gebruiker moet een nieuw token aanmaken
F-13: Lokale en cloud deployment
Prioriteit: v1 — Kritiek Persona: Lars (lokaal, geen vendor lock-in), Dina (cloud)
Omschrijving: De app is deployable op Vercel + Neon PostgreSQL en lokaal draaibaar met een Neon-database. Eén codebase, PostgreSQL via Prisma.
Acceptatiecriteria:
npm run devstart de app lokaal met Neon-databaseDATABASE_URLin.env.localverwijst naar Neon connection stringnpx prisma db pushinitialiseert het schemanext buildslaagt voor Vercel-deployment zonder aanpassingen.env.exampledocumenteert alle vereiste en optionele environment variables- README bevat stap-voor-stap instructies voor zowel lokale als cloud setup
F-14: Job-queue inzicht en beheer (/jobs)
Prioriteit: v1 — Operationele controle Persona: Lars
Omschrijving:
De /jobs-pagina geeft een overzicht van alle ClaudeJob-records voor het actieve product. Vanuit de JobDetailPane kan de gebruiker een mislukte, geannuleerde of overgeslagen job opnieuw in de wachtrij zetten.
Acceptatiecriteria:
Mislukte job opnieuw starten
- Een
ClaudeJobin statusFAILED,CANCELLEDofSKIPPEDtoont een "Opnieuw starten"-knop in deJobDetailPane. - De knop reset de bestaande job (geen nieuwe job aanmaken):
status → QUEUED,retry_count + 1, alle run-velden gecleared. - Bij
SPRINT_IMPLEMENTATION-jobs worden alle bijbehorendeSprintTaskExecution-rows in dezelfde transactie teruggezet naarPENDING. - Tijdens de server-action is de knop disabled (loading-state). De UI updatet via SSE zonder handmatige refresh.
- Demo-sessies zien een
DemoTooltipop de knop en kunnen niet restarten (drie-laagse policy: knop disabled + server actionsession.isDemo-check + HTTP 403).
Randgevallen:
- Job is ondertussen al door een andere actie opnieuw gestart (race condition) → server-action controleert de huidige status vóór de update; als de status niet meer
FAILED/CANCELLED/SKIPPEDis, retourneert de action een foutmelding. - Demo-token probeert via directe API-aanroep te restarten → 403 Forbidden.
Navigatiestructuur
/ (landingspagina — app-uitleg, Scrum-samenvatting, gebruikershandleiding, API-overzicht)
/login
/register
/dashboard (productenlijst)
/products/new (product aanmaken)
/products/:id (Product Backlog — gesplitst scherm)
/products/:id/sprint (Sprint Backlog — gesplitst scherm; sprint-switcher in product-header)
/products/:id/sprint/planning (Sprint Planning — gesplitst scherm)
/solo (Solo board — Kanban per ingelogde gebruiker, top-level)
/ideas (Idea-laag, vervangt voormalige /todos)
/ideas/:id (Idea-detail met grill / make-plan)
/jobs (Job-queue inzicht)
/insights (Tokenkosten + run-statistieken)
/manual (In-app developer manual)
/settings (profiel, account, product backlogs, rollen, API-tokens)
/settings/tokens (API-tokenbeheer)
# Mobile-shell (telefoon-UA)
/m/settings (account + product-selector + QR-instructie + logout)
/m/products/:id (Product Backlog — tab-mode op <1024px)
/m/products/:id/solo (Solo Paneel — 3-koloms-kanban met horizontal scroll)
/m/pair (QR-pairing bevestiging — verhuisd uit (app)/ naar (mobile)/)
Mobile shell
Prioriteit: v1 — voor on-the-go gebruik (PBI-11) Persona: Lars onderweg / tussendoor
Omschrijving:
Telefoon-gebruikers (UA met Mobi-substring) krijgen een minimale mobile-shell met drie schermen onder /m/*. Tablets (iPad, Android-tablet zonder Mobi) en desktop blijven het bestaande /dashboard-pad volgen. De mobile-shell hergebruikt zoveel mogelijk content-componenten van de desktop-app (PbiList, StoryPanel, TaskPanel, SoloBoard, alle entity-dialogen) — er is geen aparte mobile-implementatie van de business-logica.
Architectuur in één regel: eigen route group app/(mobile)/ met eigen layout.tsx (zonder NavBar/StatusBar/MinWidthBanner) — een nested layout in (app)/m/* zou de NavBar erven. Auth via gedeelde lib/auth-guard.ts requireSession(). Zie docs/architecture/project-structure.md voor de volledige architectuur.
Acceptatiecriteria:
- Phone-UA bij login →
/m/products/[active]/solo(zonder actief product →/m/settings) - Tablet-UA en desktop-UA blijven naar
/dashboard /m/*rendert geen NavBar, AppIcon, MinWidthBanner of StatusBar — alleen tab-bar onderaan- Portrait-modus toont rotate-overlay; landscape verbergt overlay
- PWA-manifest verzoekt
landscape-orientatie (iOS Safari kan dit niet 100% afdwingen — CSS-overlay als fallback) - Tab-bar onderaan: Backlog (ListTree), Solo (Activity), Settings — alleen iconen, geen labels, tap-target ≥44×44px
- Backlog op
<1024pxrendert in tab-mode (tabs: PBI's | Stories | Taken) met click-cascade auto-switch - Entity-dialogen (PBI, Story, Task, Task-detail) renderen full-screen op
<640pxvia gedeeldeentityDialogContentClasses - Solo-paneel behoudt 3-koloms-kanban met horizontal scroll (geen 1-koloms-mode)
- Settings: account-info read-only, product-selector activeert + redirect, QR-instructie naar desktop, logout met bevestiging
/m/pair(QR-pairing-bevestiging) blijft werken — alleen filesystem-locatie verhuisd, URL onveranderd- Demo-user op mobile: read-only werkt; logout staat toe
Bekende limiet: iOS Safari respecteert manifest.orientation niet altijd in PWA-modus — de CSS-overlay (<LandscapeGuard>) is de feitelijke afdwinging.
Flow per scherm:
Settings (/m/settings)
- Lars opent de app op zijn telefoon → wordt via UA-redirect naar
/m/settingsgestuurd (geen actief product) of keert terug via de tab-bar Settings-icoon. - Hij ziet zijn accountnaam en rol (read-only). Geen avatar-upload op mobiel in v1.
- Via de product-selector activeert hij een product — app redirect naar
/m/products/[id]/solo. - Onderaan staat de QR-pairing-instructie: "Scan een QR-code op de desktop om in te loggen zonder wachtwoord." Knop "Inloggen op desktop via QR" opent
/m/pair. - Logout-knop met bevestigingsstap; na bevestiging →
/login.
Backlog (/m/products/:id)
- Lars tikt op het Backlog-icoon in de tab-bar.
- Scherm toont drie tabs bovenaan: PBI's | Stories | Taken.
- In de PBI's-tab selecteert hij een PBI → app wisselt automatisch naar de Stories-tab met de bijbehorende stories.
- In de Stories-tab selecteert hij een story → app wisselt automatisch naar de Taken-tab.
- Tikken op een taak opent de TaskDetailDialog full-screen (
<640pxviaentityDialogContentClasses). - Terugnavigatie via ← in de tab-header of via de tab-bar.
Solo (/m/products/:id/solo)
- Lars tikt op het Solo-icoon in de tab-bar.
- Scherm toont het 3-koloms kanban-bord (TO_DO / IN_PROGRESS / DONE) met horizontal scroll — geen 1-koloms-mode.
- Hij scrollt horizontaal om DONE-kolom te bereiken.
- Tikken op een taakkaart opent de TaskDetailDialog full-screen.
- Drag-and-drop tussen kolommen werkt via PointerSensor (touch-events); status persisteert met optimistische UI en rollback bij fout.
- Knop bovenaan toont ongeclaimde stories; tik op "Pak op" claimt een story direct.
Datamodel (schets)
Volledige tabeldefinities staan in data-model. Onderstaande tabel is een korte schets per entiteit.
| Entiteit | Sleutelvelden | Relaties / opmerkingen |
|---|---|---|
users |
id, username, email?, password_hash, is_demo, must_reset_password, active_product_id?, idea_code_counter, min_quota_pct, bio?, bio_detail?, avatar_data?, created_at | Profielvelden optioneel; avatar als WebP bytea |
user_roles |
id, user_id, role (PRODUCT_OWNER | SCRUM_MASTER | DEVELOPER | ADMIN) |
Meervoudige rollen per gebruiker |
api_tokens |
id, user_id, token_hash, label, revoked_at | Max. 10 actief per gebruiker; gekoppeld aan max. 1 ClaudeWorker |
products |
id, user_id, name, code?, description, repo_url, definition_of_done, auto_pr, pr_strategy, archived | Hoogste niveau; eigenaar + members |
pbis |
id, product_id, code, title, description, priority (1–4), sort_order, status (READY | BLOCKED | FAILED | DONE), pr_url?, pr_merged_at? |
Geordend binnen prioriteitsgroep; auto-DONE bij sprint-close |
stories |
id, pbi_id, product_id, sprint_id?, assignee_id?, code, title, description, acceptance_criteria, priority, sort_order, status (OPEN | IN_SPRINT | DONE | FAILED) |
Auto-promotie als alle tasks DONE |
story_logs |
id, story_id, type (IMPLEMENTATION_PLAN | TEST_RESULT | COMMIT), content, status?, commit_hash?, commit_message?, metadata?, created_at |
Aangemaakt via API; read-only in UI |
sprints |
id, product_id, code, sprint_goal, status (OPEN | CLOSED | ARCHIVED | FAILED), start_date?, end_date?, created_at, completed_at? |
Meerdere sprints per product (PBI-63) |
sprint_runs |
id, sprint_id, started_by_id, status (QUEUED | RUNNING | PAUSED | DONE | FAILED | CANCELLED), pr_strategy, branch?, pr_url?, pause_context?, previous_run_id? |
Eén run per uitvoering; chained retries |
tasks |
id, story_id, product_id, sprint_id?, code, title, description?, implementation_plan?, priority, sort_order, status (TO_DO | IN_PROGRESS | REVIEW | DONE | FAILED | EXCLUDED), verify_only, verify_required, repo_url? |
code blijft stabiel bij re-parenting |
claude_jobs |
id, user_id, product_id, task_id?, idea_id?, sprint_run_id?, kind, status, claimed_by_token_id?, model_id?, tokens, plan_snapshot?, base/head_sha?, branch?, pr_url?, summary?, error?, retry_count, lease_until? | Job-queue voor agents |
sprint_task_executions |
id, sprint_job_id, task_id, order, plan_snapshot, verify_required_snapshot, verify_only_snapshot, status (PENDING | RUNNING | DONE | FAILED | SKIPPED), verify_result?, verify_summary?, skip_reason? |
Bevroren scope per SPRINT_IMPLEMENTATION-claim |
claude_workers |
id, user_id, token_id (unique), product_id?, started_at, last_seen_at, last_quota_pct?, last_quota_check_at? | Live-presence per actieve agent |
model_prices |
id, model_id (unique), input/output/cache_read/cache_write_price_per_1m, currency | Prijslookup voor jobs-pagina |
ideas / idea_products / idea_logs / user_questions |
zie data-model | Idea-laag (M12); vervangt voormalige todos |
claude_questions |
id, story_id?, task_id?, idea_id?, product_id, asked_by, question, options?, status, answer?, answered_by?, answered_at?, created_at, expires_at | Agent ↔ user vraag-kanaal (M11) |
login_pairings |
id, secret_hash, desktop_token_hash, status, user_id?, desktop_ua?, desktop_ip?, expires_at, approved_at?, consumed_at? | QR-pairing-flow (M10) |
push_subscriptions |
id, user_id, endpoint (unique), p256dh, auth, user_agent?, last_used_at | Web-push subscriptions |
product_members |
id, product_id, user_id, created_at | Many-to-many; alleen Developers; eigenaar via products.user_id |
Niet-functionele vereisten
| Vereiste | Waarde |
|---|---|
| Platform | Desktop-first web (minimale breedte 1024px); toekomstige mobiele variant beperkt tot taken afvinken |
| Authenticatie | Gebruikersnaam + wachtwoord; geen e-mailverificatie in v1 |
| Drag-and-drop | dnd-kit (useDraggable / useDroppable); 60fps vereist bij lijsten tot 100 items |
| API-beveiliging | Bearer token op alle /api/* routes |
| Offline support | Niet vereist voor v1 |
| Taal | Nederlands voor UI-teksten; Engels voor code, API en technische documentatie |
| Toegankelijkheid | Keyboard-navigatie voor alle primaire acties; WCAG AA voor de sprint planning flow |
| Database | Prisma ORM; PostgreSQL via Neon |
| Deployment | Vercel (cloud) of lokaal via npm run dev |
| Analytics | Vercel Analytics via @vercel/analytics/next in de root layout |
| Sessiebeheer | Server-side sessie of JWT; geen opslag van gevoelige data in localStorage |
Sleutel-user-flows
Flow 1: Lars start zijn avond
Startpunt: Dashboard (na inloggen)
- Lars ziet zijn productenlijst — vier producten, elk met naam en beschrijving
- Hij klikt op "Factuur-tool"
- Product Backlog opent — PBI's links, stories rechts
- Hij ziet drie open PBI's; klikt op "Betalingsverwerking"
- Vijf stories verschijnen rechts als blokken; twee hebben badge "In Sprint"
- Hij navigeert naar Sprint Planning
- Selecteert een story links, ziet de bijbehorende taken rechts
- Maakt een nieuwe taak aan: "Stripe webhook valideren" → prioriteit 1
- Opent terminal, start Claude Code
- Claude haalt de taak op via API, stelt implementatieplan op
- Plan verschijnt direct in de story-activiteitenlog in de app
Resultaat: Lars heeft in minder dan vijf minuten overzicht en Claude Code is aan het werk.
Flow 2: Claude Code volledige cyclus
Startpunt: Claude Code in terminal
claude devplanner next— haalt hoogst geprioriteerde open story op viaGET /api/products/:id/next-story- Claude toont storytitel en acceptatiecriteria
- Claude haalt eerste 10 taken op via
GET /api/sprints/:id/tasks?limit=10 - Claude beoordeelt volgorde en past aan via
PATCH /api/stories/:id/tasks/reorder - Claude stelt implementatieplan op →
POST /api/stories/:id/log(type: IMPLEMENTATION_PLAN) - Claude voert implementatie uit
- Claude draait tests →
POST /api/stories/:id/log(type: TEST_RESULT, status: PASSED) - Claude maakt commit →
POST /api/stories/:id/log(type: COMMIT, hash, message) - Lars opent de story in de app — ziet plan, testresultaat en commit-hash in de activiteitenlog
- Lars zet de story handmatig op DONE via de UI
Resultaat: Volledige traceerbaarheid van beslissing tot commit, zonder extra handmatige invoer.
Flow 3: Idea grillen en materialiseren
Startpunt: Idea-lijst (/ideas)
- Lars maakt een idea aan: "Voeg rate limiting toe aan de API"
- Hij koppelt het aan product "Factuur-tool" en start een grill-job
- Een agent claimt de
IDEA_GRILL-job en stelt vragen via het claude-question-kanaal — Lars antwoordt in de UI - Agent eindigt met
update_idea_grill_md; status →GRILLED - Lars start een make-plan-job; agent produceert strict yaml-frontmatter
- Status →
PLAN_READY; Lars bekijkt de preview (PBI + stories + tasks) - Bevestigen materialiseert de structuur in de Product Backlog; idea-status →
PLANNED
Resultaat: Een ruwe gedachte wordt via één gestructureerde dialoog een complete PBI-tak met stories en tasks — zonder handmatig opnieuw te tikken.
Actief Product Backlog
Concept
Een gebruiker kan één product als "actief" markeren. Dit actieve product wordt in de NavBar centraal getoond en bepaalt welke tabs (Product Backlog, Sprint, Solo) navigeerbaar zijn. Het actieve product wordt opgeslagen in user.active_product_id in de database — niet in een cookie.
Menugedrag
- Producten — altijd bereikbaar, toont alle producten van de gebruiker
- Product Backlog — alleen klikbaar als er een actief product is
- Sprint — alleen klikbaar als er een actief product is én minimaal één sprint bestaat; sprint-switcher in de product-header bepaalt welke
- Solo — alleen klikbaar als er een actief product is
- Ideeën — altijd bereikbaar (vervangt voormalig "Todo's")
In het midden van de NavBar staat een dropdown met de naam van het actieve product. Via deze dropdown kan de gebruiker wisselen tussen producten of naar "Producten beheren" navigeren.
Activeren
- Dashboard: elke productrij toont een "Activeer"-knop (verborgen voor het al actieve product). Het actieve product krijgt een "Actief"-badge. Klikken → actief product instellen + navigeer naar Product Backlog.
- Product Backlog header: als dit product nog niet actief is, staat er een "Activeer"-knop in de header.
Demo-gebruikers zien de knoppen maar krijgen een toast "Niet beschikbaar in demo-modus" bij het klikken.
Edge cases
- Archiveren: wanneer een eigenaar een product archiveert, wordt
active_product_idvoor alle leden die dit product actief hadden automatisch opnullgezet (atomisch via$transaction). - Product verlaten: wanneer een lid het product verlaat, wordt hun
active_product_idgecleard. - Lid verwijderen: wanneer een eigenaar een lid verwijdert, wordt dat lid's
active_product_idgecleard. - Stale referentie: als bij een request
active_product_idverwijst naar een gearchiveerd of onbereikbaar product (bijv. toegang ingetrokken in een andere sessie), cleared de layout de referentie server-side en redirect naar/dashboardmet de toast "Je actieve product is niet meer beschikbaar".
Solo Panel
Doel: een persoonlijk Kanban-bord per product dat de taken toont van stories die geclaimd zijn door de ingelogde developer. Bord werkt met drie kolommen (TO_DO / IN_PROGRESS / DONE), drag-and-drop tussen kolommen, en koppelt aan een nieuw
Story.assignee_idveld.Scope v1: geen REVIEW-status, geen multi-product aggregatie, geen taak-level overrides. Story-level assignment volstaat. Desktop-first conform ST-606 — onder 1024px tonen we dezelfde "te smal scherm"-melding als de rest van de app.
Versie: v2 — verwerkt antwoorden uit
backlog.mdover sessie-flag, bestaande Server Actions en desktop-first scope.
Wat veranderde t.o.v. v1
| Onderdeel | v1 aanname | v2 (op basis van backlog) |
|---|---|---|
isDemo toegang |
DB-lookup of session, ambivalent | Komt uit session.isDemo (ST-006, ST-604) — geen DB-call |
| Implementation_plan editen | Bestaande Server Action of API | Nieuwe updateTaskPlanAction (gericht, optimistisch-vriendelijk) |
| Mobiel | Optionele chunk 13 (tab-strip) | Geen mobile UI; volg ST-606 desktop-first patroon |
| Toast | Algemeen genoemd | Sonner is geïnstalleerd (ST-603) — gebruik consistent |
| Pending states | Niet uitgewerkt | useFormStatus of useTransition zoals ST-601 voorschrijft |
| Demo-tooltip tekst | "Read-only in demo-modus" | "Niet beschikbaar in demo-modus" zoals ST-604 |
| Sprint Board referentie | Generieke "sprint board" | ST-313 drie-panelen Sprint Board — assignee-UI komt in middenpaneel |
1. Datamodel — Prisma migratie
Eén veld erbij, één index erbij. Geen enum-wijzigingen.
model Story {
// ... bestaande velden ongewijzigd ...
assignee_id String?
assignee User? @relation("StoryAssignee", fields: [assignee_id], references: [id], onDelete: SetNull)
@@index([sprint_id, assignee_id]) // hot path: solo-bord query
// bestaande indexen ongewijzigd
}
model User {
// ... bestaande velden ongewijzigd ...
assigned_stories Story[] @relation("StoryAssignee")
}
Migratie:
npx prisma migrate dev --name add_story_assignee
onDelete-keuze: SetNull zodat verwijderen van een user de stories behoudt (assignee valt terug naar "team"). Cascade zou stories vernietigen — niet wat we willen.
Named relation "StoryAssignee": voorkomt botsing met andere mogelijke User↔Story relations in de toekomst.
2. Auth-helper (lib/auth.ts uitbreiding)
isDemo zit al in de sessiecookie sinds ST-006 — geen DB-lookup nodig.
import { getIronSession } from 'iron-session'
import { cookies } from 'next/headers'
import { sessionOptions, type SessionData } from '@/lib/session'
import { prisma } from '@/lib/prisma'
export async function getSession() {
return getIronSession<SessionData>(await cookies(), sessionOptions)
}
export async function requireUser() {
const session = await getSession()
if (!session?.userId) throw new Error('Niet ingelogd')
return session
}
export async function requireWriter() {
const session = await requireUser()
if (session.isDemo) throw new Error('Niet beschikbaar in demo-modus')
return session
}
export async function requireProductAccess(productId: string) {
const session = await requireUser()
const product = await prisma.product.findFirst({
where: {
id: productId,
OR: [
{ user_id: session.userId }, // owner
{ members: { some: { user_id: session.userId } } }, // member
],
},
select: { id: true },
})
if (!product) throw new Error('Geen toegang tot dit product')
return session
}
export async function requireProductWriter(productId: string) {
const session = await requireProductAccess(productId)
if (session.isDemo) throw new Error('Niet beschikbaar in demo-modus')
return session
}
Patroon-uitleg:
requireUser— ingelogd, anders foutrequireWriter— ingelogd én niet-demorequireProductAccess— ingelogd én lid (read)requireProductWriter— ingelogd én lid én niet-demo (write)
Afhankelijkheid: controleer of bestaande actions/*.ts een eigen lokale getSession definiëren. Zo ja, optioneel migreren bij gelegenheid (geen blocker).
3. Server Actions
3a. Story-claim acties (actions/stories.ts uitbreiding)
'use server'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'
import { prisma } from '@/lib/prisma'
import { requireProductWriter } from '@/lib/auth'
// ---------------------------------------------------------------------------
const claimSchema = z.object({
storyId: z.string().cuid(),
productId: z.string().cuid(),
})
export async function claimStoryAction(input: z.infer<typeof claimSchema>) {
const { storyId, productId } = claimSchema.parse(input)
const session = await requireProductWriter(productId)
await prisma.story.update({
where: { id: storyId, product_id: productId }, // tenant-guard
data: { assignee_id: session.userId },
})
revalidatePath(`/products/${productId}/sprint`)
revalidatePath(`/products/${productId}/solo`)
}
// ---------------------------------------------------------------------------
export async function unclaimStoryAction(input: z.infer<typeof claimSchema>) {
const { storyId, productId } = claimSchema.parse(input)
await requireProductWriter(productId)
await prisma.story.update({
where: { id: storyId, product_id: productId },
data: { assignee_id: null },
})
revalidatePath(`/products/${productId}/sprint`)
revalidatePath(`/products/${productId}/solo`)
}
// ---------------------------------------------------------------------------
const reassignSchema = z.object({
storyId: z.string().cuid(),
productId: z.string().cuid(),
targetUserId: z.string().cuid(),
})
export async function reassignStoryAction(input: z.infer<typeof reassignSchema>) {
const { storyId, productId, targetUserId } = reassignSchema.parse(input)
await requireProductWriter(productId)
// Valideer dat target-user lid is van het product (anders cross-tenant assignment)
const isMember = await prisma.product.findFirst({
where: {
id: productId,
OR: [
{ user_id: targetUserId },
{ members: { some: { user_id: targetUserId } } },
],
},
select: { id: true },
})
if (!isMember) throw new Error('Doel-gebruiker is geen lid van dit product')
await prisma.story.update({
where: { id: storyId, product_id: productId },
data: { assignee_id: targetUserId },
})
revalidatePath(`/products/${productId}/sprint`)
revalidatePath(`/products/${productId}/solo`)
}
// ---------------------------------------------------------------------------
const bulkClaimSchema = z.object({ productId: z.string().cuid() })
export async function claimAllUnassignedInActiveSprintAction(
input: z.infer<typeof bulkClaimSchema>,
) {
const { productId } = bulkClaimSchema.parse(input)
const session = await requireProductWriter(productId)
const activeSprint = await prisma.sprint.findFirst({
where: { product_id: productId, status: 'OPEN' },
select: { id: true },
})
if (!activeSprint) throw new Error('Geen actieve sprint gevonden')
const result = await prisma.story.updateMany({
where: {
sprint_id: activeSprint.id,
product_id: productId,
assignee_id: null,
},
data: { assignee_id: session.userId },
})
revalidatePath(`/products/${productId}/sprint`)
revalidatePath(`/products/${productId}/solo`)
return { claimed: result.count }
}
3b. Implementation plan editen (actions/tasks.ts uitbreiding)
Bestaande updateTaskStatus (ST-310) en updateTask (ST-311) blijven ongewijzigd. We voegen één nieuwe gerichte action toe:
'use server'
const planSchema = z.object({
taskId: z.string().cuid(),
productId: z.string().cuid(), // voor tenant-guard
implementationPlan: z.string().max(20000),
})
export async function updateTaskPlanAction(input: z.infer<typeof planSchema>) {
const { taskId, productId, implementationPlan } = planSchema.parse(input)
await requireProductWriter(productId)
// Tenant-guard via geneste relatie
await prisma.task.update({
where: {
id: taskId,
story: { product_id: productId }, // verifieer dat task bij product hoort
},
data: { implementation_plan: implementationPlan },
})
revalidatePath(`/products/${productId}/solo`)
revalidatePath(`/products/${productId}/sprint`)
}
Waarom een aparte action: korter, optimistisch-vriendelijk (kleine payload, lage latency), past bij save-on-blur in de detail-dialoog. De bestaande updateTask is voor volledige edits via een formulier.
Toast/UX: geen success-toast (te frequent bij save-on-blur). Wel error-toast bij fout. Indicator in dialoog ("Bezig met opslaan…" / "Opgeslagen").
4. Routes en pagina's
app/
├── solo/
│ └── page.tsx # /solo → redirect of picker
└── products/
└── [id]/
├── sprint/page.tsx # bestaand (ST-313 drie-panelen) — krijgt UI-uitbreidingen
└── solo/
└── page.tsx # /products/[id]/solo → het bord
4a. /solo — Redirect-pagina
Server Component. Leest cookie lastProductId, valideert toegang, redirect.
// app/solo/page.tsx
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { prisma } from '@/lib/prisma'
import { requireUser } from '@/lib/auth'
import { ProductPicker } from '@/components/solo/product-picker'
export default async function SoloRedirectPage() {
const session = await requireUser()
const lastProductId = (await cookies()).get('lastProductId')?.value
if (lastProductId) {
const valid = await prisma.product.findFirst({
where: {
id: lastProductId,
archived: false,
OR: [
{ user_id: session.userId },
{ members: { some: { user_id: session.userId } } },
],
},
select: { id: true },
})
if (valid) redirect(`/products/${valid.id}/solo`)
}
// Geen valide cookie → toon picker
const products = await prisma.product.findMany({
where: {
archived: false,
OR: [
{ user_id: session.userId },
{ members: { some: { user_id: session.userId } } },
],
},
select: { id: true, name: true },
orderBy: { updated_at: 'desc' },
})
return <ProductPicker products={products} basePath="/solo" />
}
4b. /products/[id]/solo — Het Solo Bord
Server Component. Doet alle queries en geeft data door aan een client-side <SoloBoard>.
// app/products/[id]/solo/page.tsx
import { notFound } from 'next/navigation'
import { prisma } from '@/lib/prisma'
import { requireUser } from '@/lib/auth'
import { setLastProductCookie } from '@/lib/cookies'
import { SoloBoard } from '@/components/solo/solo-board'
import { NoActiveSprint } from '@/components/solo/no-active-sprint'
export default async function SoloPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const session = await requireUser()
await setLastProductCookie(id)
const product = await prisma.product.findFirst({
where: {
id,
OR: [
{ user_id: session.userId },
{ members: { some: { user_id: session.userId } } },
],
},
select: { id: true, name: true },
})
if (!product) notFound()
const activeSprint = await prisma.sprint.findFirst({
where: { product_id: id, status: 'OPEN' },
select: { id: true, sprint_goal: true },
})
if (!activeSprint) return <NoActiveSprint product={product} />
// Parallel: eigen taken + count ongeclaimde stories
const [tasks, unassignedStoryCount] = await Promise.all([
prisma.task.findMany({
where: {
sprint_id: activeSprint.id,
story: { assignee_id: session.userId },
},
select: {
id: true,
title: true,
priority: true,
sort_order: true,
status: true,
description: true,
implementation_plan: true,
story: { select: { id: true, title: true } },
},
orderBy: [{ priority: 'desc' }, { sort_order: 'asc' }],
}),
prisma.story.count({
where: { sprint_id: activeSprint.id, assignee_id: null },
}),
])
return (
<SoloBoard
product={product}
sprint={activeSprint}
tasks={tasks}
unassignedStoryCount={unassignedStoryCount}
isDemo={session.isDemo}
/>
)
}
Performance:
- Query gebruikt
[sprint_id, assignee_id]index die we toevoegen → snelle filter Promise.allparallelliseert de twee onafhankelijke queriesselectprojectie houdt payload klein
5. Cookie-helper (lib/cookies.ts)
'use server'
import { cookies } from 'next/headers'
const ONE_MONTH = 60 * 60 * 24 * 30
export async function setLastProductCookie(productId: string) {
const store = await cookies()
store.set('lastProductId', productId, {
httpOnly: true,
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
maxAge: ONE_MONTH,
path: '/',
})
}
6. Sprint Board (ST-313) uitbreidingen
In het middenpaneel (Sprint Backlog) van het drie-panelen Sprint Board komen de assignee-UI elementen.
6a. Story-kaart op het Sprint Backlog paneel
Nieuwe elementen op elke story-kaart:
┌──────────────────────────────────────────────────┐
│ ⚡ Story title [···] │ ← actie-menu rechts
│ Some PBI · 3 taken │
│ ───────────────────────────────────────────── │
│ [👤 jan.visser] of [— Niet geclaimd] │ ← assignee-chip
└──────────────────────────────────────────────────┘
Assignee-chip: klein component met <UserAvatar size="xs"> + username, of een muted badge (bg-muted text-muted-foreground) als assignee_id === null.
Actie-menu (shadcn DropdownMenu):
- Pak op →
claimStoryAction— zichtbaar als ongeclaimd of niet-jij - Geef terug aan team →
unclaimStoryAction— zichtbaar als geclaimd - Wijs toe aan ▶ (submenu met members) →
reassignStoryAction
Demo-modus: hele dropdown disabled met tooltip "Niet beschikbaar in demo-modus" (consistent met ST-604).
6b. Bovenaan het Sprint Backlog paneel
<div className="flex items-center justify-between">
<h2>Sprint Backlog</h2>
<Button
onClick={handleClaimAll}
disabled={unassignedCount === 0 || isDemo}
variant="outline"
>
Claim alle ongeclaimde stories ({unassignedCount})
</Button>
</div>
Na succes: Sonner-toast "X stories geclaimd" (gewone success-toast, niet drag-and-drop frequentie). Bij demo: knop disabled met tooltip "Niet beschikbaar in demo-modus".
7. Solo Paneel componenten
components/solo/
├── solo-board.tsx # Client root, dnd context, layout
├── solo-column.tsx # Drop target per status
├── solo-task-card.tsx # Draggable kaart (bestaande task-card hergebruiken)
├── task-detail-dialog.tsx # Shadcn Dialog
├── unassigned-stories-sheet.tsx # Shadcn Sheet
├── no-active-sprint.tsx # Empty state
└── product-picker.tsx # Voor /solo zonder cookie
7a. <SoloBoard> — root component
'use client'
interface Props {
product: { id: string; name: string }
sprint: { id: string; sprint_goal: string }
tasks: TaskWithStory[]
unassignedStoryCount: number
isDemo: boolean
}
export function SoloBoard({ product, sprint, tasks, unassignedStoryCount, isDemo }: Props) {
// Zustand store gehydrateerd met initiële taken
// DndContext (overslaan als isDemo) met sensor + collision detection
// Header: productnaam, sprint goal, knop "Toon openstaande stories (N)"
// Drie kolommen in een grid (md:grid-cols-3)
}
7b. Zustand store (stores/solo-store.ts)
Volgt het patroon van usePlannerStore (ST-201): init*, optimistic*, rollback*.
import { create } from 'zustand'
import type { TaskStatus } from '@prisma/client'
interface SoloState {
tasks: TaskWithStory[]
initTasks: (tasks: TaskWithStory[]) => void
optimisticMove: (taskId: string, toStatus: TaskStatus) => TaskStatus | null // returns prev for rollback
rollback: (taskId: string, prevStatus: TaskStatus) => void
updatePlan: (taskId: string, plan: string) => void
}
export const useSoloStore = create<SoloState>((set, get) => ({
tasks: [],
initTasks: (tasks) => set({ tasks }),
optimisticMove: (taskId, toStatus) => {
const task = get().tasks.find(t => t.id === taskId)
if (!task) return null
const prev = task.status
set({ tasks: get().tasks.map(t => t.id === taskId ? { ...t, status: toStatus } : t) })
return prev
},
rollback: (taskId, prevStatus) => {
set({ tasks: get().tasks.map(t => t.id === taskId ? { ...t, status: prevStatus } : t) })
},
updatePlan: (taskId, plan) => {
set({ tasks: get().tasks.map(t => t.id === taskId ? { ...t, implementation_plan: plan } : t) })
},
}))
7c. Drag-and-drop (dnd-kit)
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event
if (!over) return
const taskId = String(active.id)
const toStatus = String(over.id) as TaskStatus // kolom-id = status enum-value
if (!['TO_DO', 'IN_PROGRESS', 'DONE'].includes(toStatus)) return
const prev = useSoloStore.getState().optimisticMove(taskId, toStatus)
if (prev === null || prev === toStatus) return
startTransition(async () => {
try {
await updateTaskStatusAction(taskId, toStatus)
} catch (err) {
useSoloStore.getState().rollback(taskId, prev)
toast.error('Status bijwerken mislukt — taak teruggeplaatst')
}
})
}
Sensor-keuze: PointerSensor met activationConstraint: { distance: 5 } om accidentele drags op klik te voorkomen. Klik = open dialog, drag = verplaats.
Collision detection: closestCorners voor kolom-niveau drops; geen sortering binnen kolom in v1.
Toast-strategie: consistent met ST-603 — geen success-toast bij drag (te frequent), wél error-toast bij rollback.
Demo-user: sla de hele DndContext over en wrap kaart-componenten zonder draggable. Klik werkt nog wel (lezen mag).
7d. <SoloColumn>
Status-token mapping (briefing):
| Status | Header background |
|---|---|
TO_DO |
bg-status-todo/15 text-status-todo border-status-todo/30 |
IN_PROGRESS |
bg-status-in-progress/15 text-status-in-progress border-status-in-progress/30 |
DONE |
bg-status-done/15 text-status-done border-status-done/30 |
7e. <SoloTaskCard> — hergebruik bestaande task-card
Bestaande task-card uit het sprint board (ST-313 rechterpaneel) hergebruiken. Pas zo nodig aan:
- Linker-rand of dot met
bg-priority-{level}voor prioriteit - Taaktitel (
font-medium,truncate) - Story-titel (
text-sm text-muted-foreground,truncate) - Optionele
showProduct?: booleanprop (off op product-specifieke pagina; reservering voor toekomstig multi-product bord)
Klik → opent <TaskDetailDialog> met deze taak.
7f. <TaskDetailDialog>
Shadcn Dialog. Inhoud:
- Header: taaktitel + statusbadge (gekleurd via MD3 tokens)
- Sectie Beschrijving (read-only
<p>of formatted block — volg bestaand task-detailpatroon) - Sectie Implementatieplan:
<Textarea>met save-on-blur- On blur:
updateTaskPlanAction({ taskId, productId, implementationPlan }) - Indicator rechtsonder: "Bezig met opslaan…" tijdens transition, "Opgeslagen" daarna (vervaagt na 2s)
- Bij fout: error-toast + waarde rollback in store
- On blur:
- Footer: link "Open in Sprint Board ↗" naar
/products/[id]/sprint?storyId=... - Demo-modus: textarea heeft
readOnly+ tooltip "Niet beschikbaar in demo-modus"
function handleBlur(plan: string) {
if (plan === task.implementation_plan) return // geen no-op call
startTransition(async () => {
try {
await updateTaskPlanAction({ taskId: task.id, productId, implementationPlan: plan })
useSoloStore.getState().updatePlan(task.id, plan)
} catch (err) {
toast.error('Opslaan mislukt')
}
})
}
Markdown-rendering voor implementatieplan kan in v2; voor v1 plain text in textarea — sneller te bouwen, past bij scope.
7g. <UnassignedStoriesSheet>
Shadcn Sheet (slide-out van rechts). Trigger: knop bovenaan het bord met badge (N).
Inhoud:
- Lijst van ongeclaimde stories in actieve sprint, met titel + taakaantal
- Per item: knop "Pak op" →
claimStoryAction→ revalidate → Sonner toast - Sheet blijft open tot user 'm sluit (zodat meerdere achter elkaar claimen kan)
- Lege staat: "Geen ongeclaimde stories. Lekker bezig!"
useFormStatus op de claim-knoppen voor pending state (ST-601).
7h. <NoActiveSprint> — empty state
Geen OPEN sprint: nette empty-state met titel, korte uitleg en link naar productpagina om er een te starten (ST-302 stappen).
8. <UserAvatar> component (nieuw, herbruikbaar)
components/ui/user-avatar.tsx
interface Props {
userId: string
username: string
size?: 'xs' | 'sm' | 'md' | 'lg'
className?: string
}
export function UserAvatar({ userId, username, size = 'md', className }: Props) {
const sizeClasses = {
xs: 'h-5 w-5 text-[10px]',
sm: 'h-6 w-6 text-xs',
md: 'h-8 w-8 text-sm',
lg: 'h-10 w-10 text-base',
}
const initials = username.slice(0, 2).toUpperCase()
return (
<Avatar className={cn(sizeClasses[size], className)}>
<AvatarImage
src={`/api/users/${userId}/avatar`}
alt={username}
/>
<AvatarFallback className="bg-primary-container text-primary">
{initials}
</AvatarFallback>
</Avatar>
)
}
Gebaseerd op shadcn <Avatar>. Fallback in MD3-token (bg-primary-container).
Aandachtspunt: als /api/users/[id]/avatar 404 returnt (user heeft geen avatar gezet), valt shadcn automatisch terug op <AvatarFallback> met initialen. Test dit gedrag — anders forceer je via onError.
Hergebruik: dit component is ook nuttig in toekomstige plekken (story-detail, instellingen, sprint board kaarten) — geen Solo-specifieke component.
9. Demo-modus
Eenvoudig nu we weten dat isDemo in de sessiecookie zit:
Drie plekken waar isDemo ertoe doet:
- Server Actions —
requireProductWriter(enrequireWriter) throwt early met "Niet beschikbaar in demo-modus". Doe je niets, dan kan een demo-user via gespoofte requests toch wijzigen. - UI-knoppen — disabled + tooltip "Niet beschikbaar in demo-modus" (ST-604 conventie). Pass
isDemoals prop door vanaf de Server Component. - DndContext — wrap kaarten zonder
useDraggablealsisDemo, of zetdisabledop de hele context.
Seed-vereiste: in prisma/seed.ts (ST-004) zorgen dat de demo-user (is_demo = true) een product heeft met:
- Een OPEN sprint
- Stories met
assignee_id = demoUser.iden bijbehorende taken in alle drie statussen (om bord werkend te tonen) - Minstens 1 ongeclaimde story (om "Toon openstaande" te demonstreren — demo-user kan niet claimen, ziet wel hoe het werkt)
10. Navbar
// components/navbar.tsx (uitbreiding)
<NavLink href="/solo" icon={<UserSquare className="h-4 w-4" />}>
Solo
</NavLink>
Plek: tussen "Producten" en "Ideeën" (of zoals layout het bepaalt). Altijd zichtbaar voor ingelogde users — geen product-context nodig, die kiest de redirect-handler zelf.
11. Werkvolgorde voor Claude Code (chunks)
Elke chunk komt overeen met een story uit M3.5 in de backlog en is afzonderlijk reviewbaar en commitbaar.
| # | Story | Inhoud | Verifiëer met |
|---|---|---|---|
| 1 | ST-350 | Schema-migratie + auth-helpers | prisma migrate dev slaagt; helpers werken vanuit testbestand |
| 2 | ST-351 | <UserAvatar> component |
Visuele check op 4 sizes; fallback bij ontbrekende avatar |
| 3 | ST-352 | Story-claim Server Actions (4 acties) | Aanroepen vanuit Sprint Board of test-route; demo-guard werkt |
| 4 | ST-353 | Sprint Board: assignee-chip + dropdown | Klikken claimt; demo-user krijgt disabled tooltip |
| 5 | ST-354 | Sprint Board: bulk-claim knop + count | Werkt in regular/demo (disabled) sessie + toast |
| 6 | ST-355 | Solo route + queries + empty states + cookie | /solo redirect werkt; pagina toont juiste taken |
| 7 | ST-356 | Solo Kanban + Zustand + DnD | Sleep tussen kolommen, status persisteert; netwerk-fail → rollback |
| 8 | ST-357 | Task detail-dialoog + updateTaskPlanAction |
Edit, blur, refresh: persisteert; demo: read-only |
| 9 | ST-358 | Openstaande stories sheet | Sheet opent met N items; claimen werkt; lege staat correct |
| 10 | ST-359 | Navbar-link "Solo" | Klik gaat naar /solo (en redirect verder) |
| 11 | ST-360 | Demo-seed uitbreiden | Login als demo, Solo bord toont werkende staat |
Bouwvolgorde-inzicht: chunks 1-5 leveren al op het Sprint Board (ST-313) een werkend assignment-systeem. Daar is een natuurlijke release-grens. Chunks 6-9 vormen het Solo Paneel zelf. Chunks 10-11 zijn polish & demo.
12. Acceptatiecriteria (volledig v1)
Functioneel:
- Een user kan op het Sprint Board een story claimen, teruggeven, of aan een andere member toewijzen
- Een user kan met één klik alle ongeclaimde stories in de actieve sprint claimen
/soloredirect naar laatst-bezochte product, met fallback naar product-picker- Solo-bord toont alle taken van geclaimde stories in de actieve sprint, gegroepeerd in 3 kolommen
- Drag-and-drop tussen kolommen update status, met optimistische UI en rollback bij fout
- Klik op taakkaart opent dialoog met bewerkbaar implementatieplan (save-on-blur)
- Knop bovenaan toont openstaande stories en laat ze individueel claimen
- Navbar-link "Solo" altijd zichtbaar voor ingelogde users
Niet-functioneel:
- Demo-user kan lezen maar niets muteren — alle Server Actions throwen, alle knoppen disabled met tooltip "Niet beschikbaar in demo-modus"
- Membership-check werkt voor zowel owner (
Product.user_id) als members (ProductMember) - Reassignment kan alleen naar geldige product-members
- Foutberichten in het Nederlands voor eindgebruikers
- Stylingregels uit briefing (MD3-tokens) consistent toegepast
- Desktop-first; volgt ST-606 melding bij < 1024px
Performance:
- Solo-pagina laadt < 500ms voor sprint met 50 taken (lokaal)
- Optimistische update voelt direct (< 50ms)
13. Nog open / mogelijke v1.1
- Sortering binnen kolom — drag binnen kolom is in v1 een no-op. Toekomstige uitbreiding via
solo_sort_orderveld of een aparteUserTaskOrder-tabel. - Markdown-rendering implementatieplan — v2; v1 is plain textarea.
- Multi-product Solo bord — alle producten in één bord. Component is hier al op voorbereid via optionele
showProductprop op task card. - REVIEW-status — bewuste scope-uitstel; voegt later kolom + enum-migratie toe.
Klaar om te valideren en aan Claude Code te geven.