From 55a1ee035c8e528ee4d3d262d4e5e28b4e9a18a3 Mon Sep 17 00:00:00 2001 From: Janpeter Visser Date: Sat, 2 May 2026 13:09:25 +0200 Subject: [PATCH 01/17] docs: introduce generic entity-dialog pattern + entity-profiles (#45) * docs(dialog-pattern): add generic entity-dialog spec Introduceert docs/patterns/dialog.md als bron-of-truth voor elke create/edit/detail-dialog in Scrum4Me, ongeacht het achterliggende dataobject. Bevat 14 secties: uitgangspunten, stack, component- architectuur, layout, validatie, drielaagse demo-policy, submission, dialog-gedrag, theming, footer, triggers/URL-state, per-entiteit profile-template, out-of-scope, en een verificatie-checklist. Registreert het patroon in CLAUDE.md "Implementatiepatronen"-tabel zodat Claude (en mensen) de spec verplicht raadplegen voor elke nieuwe dialog. Co-Authored-By: Claude Opus 4.7 (1M context) * docs(dialog-pattern): convert task spec + add pbi/story entity-profiles Reduceert docs/scrum4me-task-dialog.md van 507 naar ~140 regels: alle gedeelde regels verhuisd naar docs/patterns/dialog.md, dit document bevat nu alleen Task-specifieke velden, URL-pattern, status-veld, server actions, triggers en bewuste out-of-scope-keuzes. Voegt twee nieuwe entity-profielen toe voor bestaande dialogen: - docs/scrum4me-pbi-dialog.md (PbiDialog: state-based, code+title-rij, PbiStatusSelect, geen delete in v1) - docs/scrum4me-story-dialog.md (StoryDialog: state-based, header met status/priority badges, inline activity-log, demo-readonly-fallback, inline-delete-confirm i.p.v. AlertDialog) Beide profielen documenteren expliciet de "Bekende gaps t.o.v. generieke spec" zodat opvolgende PR's de afwijkingen kunnen rechtzetten of bewust kunnen accorderen. Co-Authored-By: Claude Opus 4.7 (1M context) * Added pdevelopment docs --------- Co-authored-by: Claude Opus 4.7 (1M context) --- .gitignore | 3 +- CLAUDE.md | 1 + docs/patterns/dialog.md | 387 ++++++++++++ docs/plans/ST-1114-copilot-reviews.md | 133 +++++ .../Tweede Claude Agent — Planning Agent.md | 346 +++++++++++ docs/scrum4me-pbi-dialog.md | 120 ++++ docs/scrum4me-story-dialog.md | 163 +++++ docs/scrum4me-task-dialog.md | 557 +++--------------- 8 files changed, 1241 insertions(+), 469 deletions(-) create mode 100644 docs/patterns/dialog.md create mode 100644 docs/plans/ST-1114-copilot-reviews.md create mode 100644 docs/plans/Tweede Claude Agent — Planning Agent.md create mode 100644 docs/scrum4me-pbi-dialog.md create mode 100644 docs/scrum4me-story-dialog.md diff --git a/.gitignore b/.gitignore index e3c508b..9c8093c 100644 --- a/.gitignore +++ b/.gitignore @@ -71,4 +71,5 @@ jp.sh .codex/ # Lokale scratch-bestanden -Brainstro \ No newline at end of file +Brainstro +/graphify-out \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 65be9e6..010083b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -111,6 +111,7 @@ Lees het relevante patroon vóór je begint. Nooit uit het hoofd schrijven. | Middleware (route protection) | `docs/patterns/middleware.md` | | QR-pairing (unauth-SSE + pre-auth cookie) | `docs/patterns/qr-login.md` | | Bidirectionele async-comms MCP-agent ↔ user | `docs/patterns/claude-question-channel.md` | +| **Entity Dialog (verplicht voor élke create/edit/detail-dialog)** | `docs/patterns/dialog.md` — bron-of-truth; per entiteit één profile-doc (bv. `docs/scrum4me-task-dialog.md`) | | Status-enum mapping (DB ↔ API) | `lib/task-status.ts` | | Client/server module-boundary | `*-server.ts` bevat DB-calls of node-only deps; `*.ts` is pure (client-safe). Nooit `import { ... } from '@/lib/foo-server'` in een client-component, anders krijg je `Module not found: 'dns'`/`'pg'`-style runtime fouten | diff --git a/docs/patterns/dialog.md b/docs/patterns/dialog.md new file mode 100644 index 0000000..9bf4682 --- /dev/null +++ b/docs/patterns/dialog.md @@ -0,0 +1,387 @@ +# Pattern — Entity Dialog + +Deze pagina is **bindend** voor elke create/edit/detail-dialog in Scrum4Me, ongeacht het achterliggende dataobject (PBI, Story, Task, Todo, Sprint, Product, User, of toekomstige entiteiten). Een nieuwe dialog die hier niet aan voldoet, hoort niet gemerged te worden. + +> **Doel:** elke dialog voelt identiek aan voor de gebruiker, hergebruikt dezelfde primitives, en heeft de drielaagse demo-policy + auth-scoping standaard ingebakken. + +Voor entity-specifieke afwijkingen of velden: schrijf één begeleidende doc per entiteit (zie sectie [§ Per-entiteit profile](#per-entiteit-profile-verplicht)). Voorbeeld: `docs/scrum4me-task-dialog.md` is het Task-profiel. + +--- + +## 1 — Verplichte uitgangspunten + +| # | Regel | Bron / waarom | +|---|---|---| +| 1.1 | Bouw op `components/ui/dialog.tsx` (de bestaande shadcn/`@base-ui/react`-wrapper). **Geen** directe imports van dialog-primitives uit `@base-ui/react`. | Voorkomt twee parallelle dialog-implementaties met inconsistente animatie/focus-trap/theming | +| 1.2 | Gebruik composition via de **`render`-prop** (zie `CLAUDE.md` "UI Library Conventions"). Nooit Radix' `asChild`. | Project gebruikt `@base-ui/react`, niet Radix | +| 1.3 | Mode (`create` vs `edit` vs `detail`) wordt afgeleid uit één input — een prop, een `state`-object of een `searchParam`. **Niet** twee aparte componenten. | Voorkomt code-duplicatie en inconsistente labels/footer-layouts | +| 1.4 | Auth-scoping op elke server action via `productAccessFilter(userId)` (of het scope-helper-equivalent). Cross-tenant writes mogen onmogelijk zijn. | `CLAUDE.md` "Toegangsmodel" + `docs/patterns/server-action.md` | +| 1.5 | **Drielaagse demo-policy** (verplicht — zie § 6) op elke write-actie. | `CLAUDE.md` "Demo-check" + `docs/scrum4me-architecture.md#demo-user-policy` | +| 1.6 | Validatie via één gedeeld zod-schema (`lib/schemas/.ts`) — gebruikt door zowel form als server action. | `CLAUDE.md` "Validatie" | +| 1.7 | Foutcodes volgen de project-conventie (§ 5). | `CLAUDE.md` "Foutcodes API" | +| 1.8 | Geen willekeurige Tailwind-kleuren (`bg-blue-500` etc.). Alleen MD3-tokens uit `app/styles/theme.css`. | `docs/scrum4me-styling.md` | + +--- + +## 2 — Stack & dependencies + +Toegestane runtime-deps voor dialog-werk (al aanwezig of standaard pattern): + +| Doel | Voorkeur | Acceptabele alternatief | +|---|---|---| +| Form-state | `react-hook-form` + `@hookform/resolvers/zod` | `useActionState` + `useFormStatus` (Server Actions, native React) | +| Auto-grow textarea | `react-textarea-autosize` | — | +| Markdown-rendering (preview) | `react-markdown` + `remark-gfm` (via bestaande ``-wrapper) | — | +| Toasts | `sonner` | — | +| Iconen | `lucide-react` | — | + +> **Per-dialog mag je kiezen tussen `react-hook-form` of `useActionState`.** Beide patronen draaien al in deze codebase. Kies één per dialog en blijf consistent binnen dat bestand. Mix ze niet. + +Verboden in dialog-context (v1): +- `material-color-utilities` (dynamic color valt buiten v1) +- Nieuwe form-libraries — geen `formik`, `final-form`, etc. + +--- + +## 3 — Component-architectuur + +### 3.1 Reusables (`components/entity-dialog/` of `components/shared/`) + +Deze primitives kennen géén entity-specifieke types en mogen door élke dialog gebruikt worden: + +| Primitive | Locatie | Verantwoordelijkheid | +|---|---|---| +| `` + `` etc. | `components/ui/dialog.tsx` | Shell, motion, focus-trap, backdrop | +| `` / `` | `components/shared/priority-select.tsx` | P1-P4 — identiek over alle entiteiten | +| `` | `components/shared/demo-tooltip.tsx` | Wrapper rond write-knoppen voor demo-modus (laag 3 van 3) | +| Auto-grow textarea | (toe te voegen wanneer nodig in `components/shared/`) | Wrapper rond `react-textarea-autosize` met char-counter + markdown-hint | +| Dirty-close-guard | (gedeelde AlertDialog-flow) | "Wijzigingen niet opgeslagen — weggooien?" | +| `` | `components/markdown.tsx` | `react-markdown` + `remark-gfm` voor description/criteria-preview | + +> Wanneer je een primitive twee keer kopieert tussen entity-dialogs, **promote 'm meteen** naar `components/shared/` (of `components/entity-dialog/`). Drie keer is te laat. + +### 3.2 Entity-specifieke laag (`components//-dialog.tsx`) + +Per entiteit één wrapper-bestand dat: +1. De juiste form/body rendert +2. De juiste server actions koppelt (`saveAction`, `deleteAction`) +3. Entiteit-specifieke labels levert ("Story bewerken", "PBI aanmaken", etc.) + +Een entity-dialog bevat **geen** layout-mechanica, motion-config of dirty-check zelf — die komen uit § 3.1. + +--- + +## 4 — Layout & responsive gedrag + +Identiek voor élke dialog (geen entity-specifieke variaties tenzij expliciet beargumenteerd in het entity-profile): + +| Breakpoint | Breedte | Hoogte | +|---|---|---| +| Mobiel (< 640px) | full-screen | full-screen | +| Tablet (640–1024px) | `90vw` | `max-h-[85vh]` | +| Desktop (≥ 1024px) | `max-w-[50vw]`, `min-w-[480px]` | `max-h-[85vh]` | + +Verplicht: +- Padding `p-6` rondom (24px) +- Veld-spacing in body `space-y-6` (24px) +- **Sticky** header (titel + close) en **sticky** footer (knoppen) +- Body scrollt onafhankelijk; geneste scrolls vermijden +- Footer heeft `border-t` in `outline-variant` + +--- + +## 5 — Validatie & foutcodes + +### 5.1 zod-schema + +Eén `lib/schemas/.ts` per entiteit. Geïmporteerd door zowel form als server action — geen aparte definities. + +### 5.2 Foutcodes (verplicht) + +| Code | Wanneer | UI-respons | +|---|---|---| +| **422** | zod-validatiefout (server-side dubbelcheck) | `fieldErrors` mappen naar `form.setError()`, géén toast | +| **403** | demo-sessie probeert te schrijven, of cross-tenant write geblokkeerd | toast "Niet toegestaan in demo-modus" of "Geen toegang", form blijft open | +| **400** | malformed JSON-body (`request.json()` faalt) — alleen bij REST-route-handlers | toast "Ongeldige aanvraag" | +| **500** | onverwachte serverfout | toast met "Opnieuw proberen"-knop, form-state behouden | + +> Field-level errors zijn **alleen** geldig bij `code: 422`. Bij andere codes is `fieldErrors` ongedefinieerd. + +### 5.3 Field-level rendering + +- Errors **onder** het veld, in `text-error`, met `border-error` op het input-element +- Géén toast voor field-level errors +- Submit-knop **blijft enabled** bij errors — klik scrollt naar eerste error-veld + focus +- react-hook-form mode: `onTouched` (eerste validatie bij blur, daarna onChange) + +--- + +## 6 — Drielaagse demo-policy (verplicht voor write-dialogs) + +Elke dialog die schrijft (create / edit / delete) MOET door alle drie de lagen heen: + +1. **Middleware-guard** in `proxy.ts` — blokkeert demo-sessies op write-routes vóór de server action loopt. Returnt **403**. +2. **`session.isDemo`-check** binnen elke `saveAction` / `deleteAction` zelf — defense-in-depth voor het geval een actie buiten een proxy-route loopt. Returnt **403**. +3. **``** rond de submit- en delete-knoppen — UI-laag: knoppen `disabled` met tooltip "Demo-modus: opslaan uitgeschakeld". + +> Eén laag missen = bug. Reviewers moeten alle drie de lagen kunnen aanwijzen in de PR. + +--- + +## 7 — Submission-flow + +### 7.1 Server Action (template) + +```ts +// actions/.ts +'use server' + +export async function saveAction( + input: Input, + context: { /* ids voor revalidatePath en scope */ }, +): PromiseResult> { + const session = await getSession() + if (!session.userId) return { ok: false, code: 403, error: 'forbidden' } + if (session.isDemo) return { ok: false, code: 403, error: 'demo_readonly' } + + const scope = await productAccessFilter(session.userId) // verplicht + const parsed = Schema.safeParse(input) + if (!parsed.success) { + return { ok: false, code: 422, error: 'validation', fieldErrors: parsed.error.flatten().fieldErrors } + } + // ... Prisma write binnen `scope` ... + // revalidatePath(...) op de context-route + return { ok: true, : row } +} + +type SaveResult = + | { ok: true; : } + | { ok: false; code: 422; error: 'validation'; fieldErrors: Record } + | { ok: false; code: 403; error: 'demo_readonly' | 'forbidden' } + | { ok: false; code: 500; error: 'server_error' } +``` + +### 7.2 Revalidation + +`revalidatePath` op de **context-route** waarin de dialog werd geopend, niet op een statisch path. Context wordt door de aanroepende client meegegeven (geen hard-coded paths in de action). + +### 7.3 Submit-flow + +- Synchroon (geen optimistic update in v1, behalve waar het store-patroon `usePlannerStore` al bestaat) +- Tijdens submit: cancel- en submit-knop disabled, spinner of "…" in submit-knop, velden **blijven enabled** +- Server saniteert en valideert opnieuw met hetzelfde zod-schema + +--- + +## 8 — Dialog-gedrag (UX-regels) + +### 8.1 Sluiten met dirty state + +- Form niet aangeraakt → Esc / backdrop-klik / Cancel sluiten **direct** +- Form `isDirty` → Esc / backdrop-klik / Cancel triggeren `AlertDialog`: *"Wijzigingen niet opgeslagen — weggooien?"* + +### 8.2 Keyboard shortcuts + +| Toets | Actie | +|---|---| +| **Esc** | Sluiten (met dirty-check) | +| **Cmd/Ctrl + Enter** | Submit vanuit elk veld | +| **Enter in ``** | **Geen** submit (alleen Cmd/Ctrl+Enter) | +| **Enter in `