feat(notes): Notes onder het Memory-menu (IDEA-226, PBI-167) #270
No reviewers
Labels
No labels
severity/s3
severity/s4
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
janpeter/Scrum4Me!270
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/notes-pbi-167"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Notes — privé snippets onder het Memory-menu (IDEA-226, PBI-167)
Sprint S-2026-09-28-1. Spec
specs/idea-226-spec(r3), planplans/idea-226-plan(r2).Wat
5f9afaa):Note,NoteKeyword,NoteKeywordLink;lib/note-schema.ts(limieten,countBodyChars, keyword-normalisatie, zod-schema's, veld-/ordercontracten,planKeywordResolution(userId, …)). Gitlink op shared main.20260928060000_add_notes: tabellen, partiële unieke indexnote_keywords_default_name_key, 10 defaults idempotent (ON CONFLICT DO NOTHING). Geen GRANTs — rechten komen uit de policy-apply-fase.api-token-usage-transition,schema-preflight,provision; de drie notes-contracten inscripts/db-access/profiles/scrum4me.json.lib/notes-server.tsis de enigeprisma.note*-toegang; alle queries user-scoped, productaccess → "Product niet gevonden".saveNoteAction/deleteNoteAction— auth 403 → demo 403 → rate limit → id/zod 422 → adapter./notesmet filters, pager, loading-skeleton; URL-gedrevenNoteDialog(?newNote=1/?editNote=<id>), KeywordPicker, dialogprofieldocs/specs/dialogs/note.md./notesinprotectedRoutes; proxy-demo-guard blijft/api/*-only (besluit JP) — demo-grens voor notes = action-403 + disabled knoppen.docs/runbooks/notes-rollout.md, data-model, CLAUDE.md (drift-regelnote_keywords, proxy-hardstop).Uitrol (volgorde, zie
docs/runbooks/notes-rollout.md)Verificatie
npm run verify: lint 0 errors (7 bestaande warnings), typecheck groen, 3057 passed / 29 skipped. Schema-drift 0,npm run docsschoon.npm run buildniet lokaal draaibaar in de worktree (env ontbreekt). Per-taak reviews + whole-branch review (opus) Approved na één fixronde.Beslissingen (rulings)
listNotesweigert een productfilter buiten toegang/scope met "Product niet gevonden" (web filtert alleen op eigen notes) — spec § 2.5.resolveKeywordsblijft intern (pariteit met web).Uitgestelde minors
?q=a&q=bgeeft 500 op/notes; productfilter toont "Alle producten" bij gearchiveerd product; kopieer-toast meldt succes bij mislukte copy; titel/keyword tellen UTF-16, body codepoints; demo Cmd/Ctrl+Enter stuurt save (server 403).product_id); product-fielderror blijft staan tot volgende submit;PRODUCT_NOT_FOUND_TEXTdupliceert servertekst.NoteDialogPropsgeen discriminated union; Cmd/Ctrl+Enter in "Nieuw keyword" voegt toe én submit;startsWith-prefix in nav;proxy.md:96-97formulering.🤖 Generated with Claude Code
updateNote relied on an earlier ownership findFirst inside the transaction, then wrote via note.update({ where: { id } }) without user_id. A note deleted concurrently between the two statements made update() throw P2025 (500) instead of returning the same 404 as every other not-found/not-yours path. The write itself now carries the scope via note.updateMany({ where: { id, user_id } }); count !== 1 returns the existing 404, and the keyword link delete/relink only run once that scoped write has confirmed ownership. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>T-1882: server component app/(app)/notes/page.tsx met loading-skeleton, NotesFilterBar (zoek + productselect + keyword-chips via URL-searchparams), NoteList (rijen naar ?editNote=<id>, "Geen product"/lege staten, geen cuid als label per ADR-0015) en NotesPager ("Vorige"/"Volgende" + "x-y van total", filters blijven behouden). Component-tests voor de lege staten en het behoud van filters in pager-links. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>Fix round 1 op T-1882 (review: 2 CRITICAL, 1 IMPORTANT, 1 MINOR), TDD per finding (rode test eerst): - buildNotesHref stapelde newNote/editNote i.p.v. ze uit te sluiten — het zetten van de een wist nu expliciet de ander. - NotesFilterBar's render-time q-sync verloor toetsaanslagen wanneer een server-rerender de zojuist zelf gepushte q bevestigde; een lastPushedQ- state (gezet in de debounce-callback, niet in een ref — react-hooks/refs verbiedt refs lezen tijdens render) onderscheidt nu "eigen push bevestigd" van "externe wijziging" (terug/vooruit, andere link). - NotesPager toonde een omgekeerd bereik ("226–5 van 5") bij een page voorbij het einde met total>0; toont nu "0 van {total}", Vorige blijft werken. - "Nieuwe note"-knop (render={<Link/>}) kreeg nativeButton={false}, conform het precedent in products/[id]/docs/[folder]/[slug]/page.tsx — voorkomt de Base UI dev-warning en de foutieve type="button" op de <a>. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>Minor 3 uit de final-review. Twee losstaande hardening-fixes: - actions/notes.ts: `saveNoteAction`/`deleteNoteAction` valideren nu expliciet dat `id` een niet-lege string is vóór de adapter wordt aangeroepen. Server-action-argumenten zijn op de wire ongetypeerd — een aangepaste client kan `id` als object aanleveren (bv. `{ not: '' }`), wat anders ongefilterd in een Prisma `where` belandt. - lib/notes-server.ts `updateNote`: een `count !== 1` in de scoped `updateMany` gooit nu binnen de `$transaction`-callback i.p.v. er stil uit te `return`en. Een `return` liet Prisma de transactie gewoon committen, dus een `updateMany` die (door zo'n type-confused id) meerdere rijen matchte zou zijn title/body-overschrijving laten staan terwijl de caller "niet gevonden" te horen kreeg. Tests: __tests__/actions/notes.test.ts (id-type-confusion + lege string, beide paden), __tests__/lib/notes-server.test.ts (count=2 rolt de transactie terug — bewezen via een reject van de mock-$transaction-callback). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Important 2 uit de final-review. Een keyword langer dan NOTE_KEYWORD_NAME_MAX (40) gaf een zod-fout op `keywords[i]` die react-hook-form als een per-index array-fout terugzet, zonder top-level `.message` op `errors.keywords` zelf. De render-conditie keek alleen naar dat top-level `.message`, dus verscheen er niets — de gebruiker klikte Save en zag geen enkele feedback. - components/notes/note-dialog.tsx: `keywordsErrorMessage` valt terug op de eerste element-fout wanneer `errors.keywords` een array is (de max-10-array-fout blijft via het bestaande top-level `.message` werken). - components/notes/keyword-picker.tsx: het invoerveld draagt nu `maxLength={NOTE_KEYWORD_NAME_MAX}`, geïmporteerd uit `@/lib/schemas/note` (geen hard-coded 40). Test: __tests__/components/notes/note-dialog.test.tsx — een 41-teken keyword toont de foutmelding onder het keywords-veld en `saveNoteAction` wordt niet aangeroepen; het invoerveld draagt `maxLength={40}`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Verdict: REQUEST_CHANGES
Findings
/api/(return pathname.startsWith('/api/')). Next Server Actions voorsaveNoteActionendeleteNoteActionzijn POSTs naar/notesmet deNext-Action-header en vallen daardoor buiten deze laag. Daarmee bevat de Notes-writeflow slechts de UI- en action-check, terwijl de verplichte drielaagse demo-policy uit de dialog-standaard een proxy/middleware-guard voor elke write vereist. Breid de guard uit voor Server Actions (of routeer de writes via een beschermde API-route) en voeg een regressietest voor een demo-POST metNext-Actiontoe.De overige beoordeelde Notes-contracten (user-scoping, body-uitsluiting uit lijstqueries, schema/migratie, navigatie en documentatie) sluiten aan op het gekoppelde plan en de productstandaarden.
Reactie op de REQUEST_CHANGES (proxy.ts:16) — bewust niet uitgebreid, besluit JP; de norm waarop de bevinding steunt is in
c2c2bfb5rechtgezet.Waarom geen proxy-guard voor server actions: een server action is een
POSTnaar de URL van de pagina waarop hij draait, met een opaqueNext-Action-id. Een guard op/notesblokkeert daarom ooklogoutActionen andere layout-actions die vanaf die pagina posten — de proxy kan ze niet onderscheiden. Die variant is in deze PR gebouwd (22faaf89) en in review juist daarom teruggedraaid (8bc8b0d0); JP koos 'proxy-laag weg voor pagina-actions'.De norm klopte nooit met de code: op
mainvóór deze PR (1d766431) matchte de demo-guard al alleenpathname.startsWith('/api/'). Geen enkele bestaande dialog met server actions (issues, ideas, …) had dus een laag 1. ADR-0006 endocs/patterns/dialog.md§6 beloofden 'alle non-GET vóór elke server action'; beide zijn nu bijgewerkt: laag 1 =/api/*, voor server actions is laag 2 de handhavende grens, laag 3 blijft verplicht.Demo-bescherming van de notes-writes:
saveNoteAction/deleteNoteActionweigeren demo met 403 vóór elke write (guard-volgorde auth → demo → rate limit → validatie; tests__tests__/actions/notes.test.ts:41en:143), en de knoppen zijn disabled metDemoTooltip. De gevraagde regressietest metNext-Actionbestaat:__tests__/proxy/demo-guard.test.ts:120legt vast dat de proxy zo'n POST doorlaat en waarom.APPROVED
Findings