Scrum4Me/docs/specs/functional.md
Janpeter Visser 3d52fe4958
docs: Product Backlog page workflow & states (PBI-88) (#208)
* 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>
2026-05-14 22:31:36 +02:00

74 KiB
Raw Permalink Blame History

title status audience language last_updated
Scrum4Me — Functionele Specificatie active
maintainer
contributor
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 12 seconden ingelogd. De flow is bedoeld om typen op vreemde toetsenborden, shoulder-surfing en autofill-history te vermijden.

Verloop:

  1. 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) en desktopToken (HttpOnly cookie voor de desktop).
  2. De desktop toont een QR-code. De code bevat een URL met mobileSecret in het URL-fragment (#s=…) — dit fragment wordt door browsers nooit naar servers gestuurd, dus belandt niet in access logs of analytics.
  3. 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 mobileSecret in de body naar de approve-endpoint.
  4. 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 12 seconden via een SSE-stream die geauthenticeerd is met het HttpOnly desktop-cookie.
  5. De desktop claimt de sessie atomisch (eenmalig consumeerbaar), krijgt zijn iron-session cookie en wordt naar /dashboard doorgestuurd.

Acceptatiecriteria:

  • Knop "Inloggen via mobiel" zichtbaar op /login naast 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)
  • mobileSecret komt nergens in een GET-URL voor (alleen in URL-fragment of POST-body); desktopToken staat alleen in een HttpOnly cookie met Path=/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/pair redirectt naar /login met return-URL
  • Gebruiker logt zichzelf uit op de mobiel terwijl de pairing nog pending is → 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_pairings publiceert via pg_notify('scrum4me_pairing', …)
  • Sessie-payload: nieuwe optionele paired: boolean en pairedExpiresAt: 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 bytea
  • GET /api/profile/avatar?v=<timestamp> — serveert avatar met Cache-Control: private, max-age=3600; timestamp in de URL zorgt voor cache-invalidatie na upload
  • updateProfileAction Server Action — slaat bio en bio_detail op

Data:

  • users.bio — VarChar(160), nullable
  • users.bio_detail — VarChar(2000), nullable
  • users.avatar_data — Bytes (bytea), nullable; altijd WebP na verwerking
  • users.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 (14) 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 (14), 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 (14, 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 (14), 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 document
    • test_result (passed): groen, icoon vinkje
    • test_result (failed): rood, icoon kruis
    • commit: 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_READY toont de gegenereerde PBI/story/task-structuur als preview; bevestigen materialiseert ze in de productbacklog en zet de idea op PLANNED
  • Idea's zijn user-private (geen productAccessFilter); secundaire producten via idea_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:


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_date en end_date op 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 CLOSED en 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 ook sprint_runs + sprint_task_executions in 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 (14)
  • 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 (plus FAILED en EXCLUDED gezet door agent-flows; zie lib/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 (14), 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=1 voor DB-ping (geen auth)
  • GET /api/products — lijst van actieve producten waarvoor de tokengebruiker eigenaar of teamlid is
  • GET /api/products/:id/next-story — hoogst geprioriteerde open story van de actieve sprint
  • GET /api/products/:id/claude-context — bundled context (product / sprint / story / tasks) voor MCP
  • GET /api/sprints/:id/tasks?limit=10 — eerste N taken in huidige volgorde
  • PATCH /api/stories/:id/tasks/reorder — accepteert geordende lijst van taak-id's
  • POST /api/stories/:id/log — vastleggen van implementatieplan, testresultaat of commit
  • PATCH /api/tasks/:id — status bijwerken (todo → in_progress → review → done) en/of implementation_plan opslaan
  • GET / POST /api/ideas en GET / PATCH /api/ideas/:id — idea CRUD (vervangt voormalig POST /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-story geeft 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:

  1. Claude heeft een vraag → roept MCP-tool ask_user_question aan met { story_id, question, options?, wait_seconds? }. Tool schrijft een rij naar claude_questions met status open, vervaltijd 24 u.
  2. Postgres-trigger emit op het bestaande scrum4me_changes-kanaal met entity: 'question'. De Scrum4Me-app heeft een user-scoped SSE-stream die filter't op product-toegang.
  3. NavBar-bell krijgt een badge met de count van open vragen voor deze gebruiker. Story-assignee ziet een visuele "wacht op jou"-emphase.
  4. 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.
  5. Trigger fired opnieuw, alle SSE-clients zien het item verdwijnen. Claude's tool-poller (als wait_seconds was meegegeven) krijgt het antwoord direct terug; anders haalt Claude het later op via get_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 hij status: '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 expired gezet
  • 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_questions of get_question_answer op
  • 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's get_question_answer retourneert status: '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_questions publiceert via pg_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 dev start de app lokaal met Neon-database
  • DATABASE_URL in .env.local verwijst naar Neon connection string
  • npx prisma db push initialiseert het schema
  • next build slaagt voor Vercel-deployment zonder aanpassingen
  • .env.example documenteert 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 ClaudeJob in status FAILED, CANCELLED of SKIPPED toont een "Opnieuw starten"-knop in de JobDetailPane.
  • 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 bijbehorende SprintTaskExecution-rows in dezelfde transactie teruggezet naar PENDING.
  • Tijdens de server-action is de knop disabled (loading-state). De UI updatet via SSE zonder handmatige refresh.
  • Demo-sessies zien een DemoTooltip op de knop en kunnen niet restarten (drie-laagse policy: knop disabled + server action session.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/SKIPPED is, 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 <1024px rendert in tab-mode (tabs: PBI's | Stories | Taken) met click-cascade auto-switch
  • Entity-dialogen (PBI, Story, Task, Task-detail) renderen full-screen op <640px via gedeelde entityDialogContentClasses
  • 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)

  1. Lars opent de app op zijn telefoon → wordt via UA-redirect naar /m/settings gestuurd (geen actief product) of keert terug via de tab-bar Settings-icoon.
  2. Hij ziet zijn accountnaam en rol (read-only). Geen avatar-upload op mobiel in v1.
  3. Via de product-selector activeert hij een product — app redirect naar /m/products/[id]/solo.
  4. 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.
  5. Logout-knop met bevestigingsstap; na bevestiging → /login.

Backlog (/m/products/:id)

  1. Lars tikt op het Backlog-icoon in de tab-bar.
  2. Scherm toont drie tabs bovenaan: PBI's | Stories | Taken.
  3. In de PBI's-tab selecteert hij een PBI → app wisselt automatisch naar de Stories-tab met de bijbehorende stories.
  4. In de Stories-tab selecteert hij een story → app wisselt automatisch naar de Taken-tab.
  5. Tikken op een taak opent de TaskDetailDialog full-screen (<640px via entityDialogContentClasses).
  6. Terugnavigatie via ← in de tab-header of via de tab-bar.

Solo (/m/products/:id/solo)

  1. Lars tikt op het Solo-icoon in de tab-bar.
  2. Scherm toont het 3-koloms kanban-bord (TO_DO / IN_PROGRESS / DONE) met horizontal scroll — geen 1-koloms-mode.
  3. Hij scrollt horizontaal om DONE-kolom te bereiken.
  4. Tikken op een taakkaart opent de TaskDetailDialog full-screen.
  5. Drag-and-drop tussen kolommen werkt via PointerSensor (touch-events); status persisteert met optimistische UI en rollback bij fout.
  6. 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 (14), 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)

  1. Lars ziet zijn productenlijst — vier producten, elk met naam en beschrijving
  2. Hij klikt op "Factuur-tool"
  3. Product Backlog opent — PBI's links, stories rechts
  4. Hij ziet drie open PBI's; klikt op "Betalingsverwerking"
  5. Vijf stories verschijnen rechts als blokken; twee hebben badge "In Sprint"
  6. Hij navigeert naar Sprint Planning
  7. Selecteert een story links, ziet de bijbehorende taken rechts
  8. Maakt een nieuwe taak aan: "Stripe webhook valideren" → prioriteit 1
  9. Opent terminal, start Claude Code
  10. Claude haalt de taak op via API, stelt implementatieplan op
  11. 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

  1. claude devplanner next — haalt hoogst geprioriteerde open story op via GET /api/products/:id/next-story
  2. Claude toont storytitel en acceptatiecriteria
  3. Claude haalt eerste 10 taken op via GET /api/sprints/:id/tasks?limit=10
  4. Claude beoordeelt volgorde en past aan via PATCH /api/stories/:id/tasks/reorder
  5. Claude stelt implementatieplan op → POST /api/stories/:id/log (type: IMPLEMENTATION_PLAN)
  6. Claude voert implementatie uit
  7. Claude draait tests → POST /api/stories/:id/log (type: TEST_RESULT, status: PASSED)
  8. Claude maakt commit → POST /api/stories/:id/log (type: COMMIT, hash, message)
  9. Lars opent de story in de app — ziet plan, testresultaat en commit-hash in de activiteitenlog
  10. 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)

  1. Lars maakt een idea aan: "Voeg rate limiting toe aan de API"
  2. Hij koppelt het aan product "Factuur-tool" en start een grill-job
  3. Een agent claimt de IDEA_GRILL-job en stelt vragen via het claude-question-kanaal — Lars antwoordt in de UI
  4. Agent eindigt met update_idea_grill_md; status → GRILLED
  5. Lars start een make-plan-job; agent produceert strict yaml-frontmatter
  6. Status → PLAN_READY; Lars bekijkt de preview (PBI + stories + tasks)
  7. 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_id voor alle leden die dit product actief hadden automatisch op null gezet (atomisch via $transaction).
  • Product verlaten: wanneer een lid het product verlaat, wordt hun active_product_id gecleard.
  • Lid verwijderen: wanneer een eigenaar een lid verwijdert, wordt dat lid's active_product_id gecleard.
  • Stale referentie: als bij een request active_product_id verwijst naar een gearchiveerd of onbereikbaar product (bijv. toegang ingetrokken in een andere sessie), cleared de layout de referentie server-side en redirect naar /dashboard met 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_id veld.

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.md over 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 fout
  • requireWriter — ingelogd én niet-demo
  • requireProductAccess — 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.all parallelliseert de twee onafhankelijke queries
  • select projectie houdt payload klein

'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 opclaimStoryAction — zichtbaar als ongeclaimd of niet-jij
  • Geef terug aan teamunclaimStoryAction — 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?: boolean prop (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
  • 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:

  1. Server ActionsrequireProductWriter (en requireWriter) throwt early met "Niet beschikbaar in demo-modus". Doe je niets, dan kan een demo-user via gespoofte requests toch wijzigen.
  2. UI-knoppen — disabled + tooltip "Niet beschikbaar in demo-modus" (ST-604 conventie). Pass isDemo als prop door vanaf de Server Component.
  3. DndContext — wrap kaarten zonder useDraggable als isDemo, of zet disabled op 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.id en 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
  • /solo redirect 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

  1. Sortering binnen kolom — drag binnen kolom is in v1 een no-op. Toekomstige uitbreiding via solo_sort_order veld of een aparte UserTaskOrder-tabel.
  2. Markdown-rendering implementatieplan — v2; v1 is plain textarea.
  3. Multi-product Solo bord — alle producten in één bord. Component is hier al op voorbereid via optionele showProduct prop op task card.
  4. REVIEW-status — bewuste scope-uitstel; voegt later kolom + enum-migratie toe.

Klaar om te valideren en aan Claude Code te geven.