Commit graph

29 commits

Author SHA1 Message Date
Janpeter Visser
bd7478861b
PBI-58: Developer manual + in-app /manual page (#148)
* docs(PBI-58): add developer manual chapters under docs/manual/

Adds a 7-file English-language manual targeted at new human contributors:
index, overview, statuses & transitions (with mermaid state diagrams),
git workflow, MCP integration, docker, and troubleshooting. The manual
is the *map* — it cross-references existing runbooks/ADRs/architecture
docs rather than duplicating their content.

Regenerates docs/INDEX.md and validates with check-doc-links.mjs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(PBI-58): add markdown rendering deps + manual:build script

Adds mermaid, rehype-slug, rehype-autolink-headings for the in-app
/manual page. Wires manual:build into prebuild so production builds
always regenerate the chapter TOC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(PBI-58): codegen script for in-app manual TOC

scripts/build-manual.mjs walks docs/manual/, parses YAML front-matter,
strips it from the body, and emits lib/manual.generated.ts with a typed
ManualEntry[] containing slug, title, description, filePath, and the
embedded markdown body. Pure Node 20, mirrors generate-docs-index.mjs.

Inlining the markdown at build time keeps runtime serverless functions
free of filesystem reads, which avoids whole-project NFT tracing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(PBI-58): /manual route renders developer manual chapters in-app

Catch-all route at app/(app)/manual/[[...slug]]/page.tsx with
generateStaticParams covering every TOC entry. Server-side
MarkdownView uses react-markdown with remark-gfm, rehype-slug, and
rehype-autolink-headings; mermaid code blocks are routed to a
client-only MermaidBlock that dynamic-imports mermaid on mount.

ManualSidebar (client) reads the typed TOC and highlights the active
chapter via usePathname.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(PBI-58): add Manual link to main nav bar

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-07 18:00:10 +02:00
Janpeter Visser
dc8557308b
chore: typecheck-script + dependency-cascade-grep in Make-Plan-prompt (#130)
Twee preventieve aanpassingen na de mislukte ST-2wj8mw8q-run, waarbij een
schema-edit (Todo-model verwijderd) groen door lint en vitest kwam maar
op `next build` brak vanwege 16 ongelinkte `prisma.todo`-references in 4
bestanden.

package.json:
- "typecheck": "tsc --noEmit" — losse script voor snelle full-project
  type-check, los van eslint en next build.
- "verify": "npm run lint && npm run typecheck && npm test" — umbrella
  voor agents/CI om voor de eind-build te valideren.

lib/idea-prompts/make-plan.md:
- Werkwijze-stap 3 toegevoegd: "Bij removal/refactor: doe een
  dependency-cascade-grep". Voor de strikte format-sectie staat nu een
  verplicht protocol:
  - Removal van Prisma-model: grep `prisma.x` in actions/app/components/lib
  - Removal van component/utility/type: grep paden + exports
  - Hernoemen: per geraakt bestand een edit-taak
  - Veld-wijziging in create/update: grep op die call-sites
  - Eind-taak: `npm run typecheck` als sanity-check los van
    lint/test/build.
- Toelichting waarom: eslint en vitest+esbuild slaan diepe type-check
  over; next build is de eerste step die alles type-checkt en zit aan
  het eind van de pijp.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 09:31:02 +02:00
Scrum4Me Agent
b760ec625e feat(ST-l9kkxh2m): CLI-script scripts/create-admin.ts voor admin-bootstrap
- Maakt user aan als die niet bestaat, anders upgrade bestaande user
- Upsert ADMIN in user_roles (idempotent)
- Helder foutbericht als argumenten ontbreken (process.exit(1))
- package.json scripts: "create-admin": "tsx scripts/create-admin.ts"
2026-05-05 14:34:10 +02:00
dfee518996 lib: idea-code generator + plan_md yaml-frontmatter parser (M12 T-494)
- lib/idea-code.ts: pure formatIdeaCode helper (client-safe, no prisma)
- lib/idea-code-server.ts: atomic nextIdeaCode via Prisma row-lock,
  accepts optional TransactionClient for combining with idea.create
- lib/idea-plan-parser.ts: parsePlanMd extracts ---yaml---/body, runs
  yaml.parse + ideaPlanMdFrontmatterSchema, returns line-info on failure;
  CRLF-tolerant
- adds yaml@^2.8.4 dependency
- 8 unit tests (parser happy/missing/yaml-error/zod-error/empty-stories/CRLF;
  formatIdeaCode pad-3 + overflow)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 19:40:39 +02:00
90343573f3 chore: bump version to 1.0.0
Eerste stabiele release. v1-readiness checklist (Now + Before-launch) is
afgerond, smoke-test groen, lint/tests/build/doc-links allemaal groen.

CHANGELOG.md [Unreleased] gepromoveerd naar [1.0.0] — 2026-05-04 met
volledige Added/Changed/Fixed/Security-secties uit PR #85 t/m #89.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 14:25:15 +02:00
ac11483c68 feat(ops): Sentry error-monitoring (v1-readiness item 2)
Vier config-files volgens Next.js 15+ conventie:
- instrumentation.ts (root) → koppelt server/edge config aan runtime-hook
- instrumentation-client.ts → client-init + onRouterTransitionStart
- sentry.server.config.ts → node-runtime
- sentry.edge.config.ts → edge-runtime (proxy.ts)

next.config.ts gewrapped met withSentryConfig:
- Source-map-upload ALLEEN als SENTRY_AUTH_TOKEN gezet is
- Tunnel /monitoring omzeilt ad-blockers (*.sentry.io)
- Silent buiten CI

SDK is no-op zonder NEXT_PUBLIC_SENTRY_DSN — geen network/overhead in
dev of bij ontbrekende creds. Sample-rates conservatief: errors 100%,
performance 10% in productie / 100% in dev. Geen Replay (privacy-review
nodig + overkill voor MVP). sendDefaultPii uit.

.env.example gedocumenteerd; architectuur-doc bijgewerkt met nieuwe
sleutelbeslissing en file-tree-aanvulling. v1-readiness #1 verschoven
naar 'done', #2 hiermee in flight.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 13:24:19 +02:00
9bfa732a6a chore: bump version to 0.9.0
Release omvat ondermeer PBI-11 (mobile-shell met landscape-lock):
mobile-shell onder /m/* met UA-redirect, route group (mobile)/, gedeelde
auth-guard, gedeelde mobile-fullscreen-dialog-classes, gescheiden
SplitPane cookie-key voor mobile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 11:09:11 +02:00
4ff50cb87e feat(landing): rewrite around local-first proposition + architecture diagram
Reframe the landing page around what makes Scrum4Me unique: code execution
stays on the developer's own hardware (laptop, NAS or VM). The Vercel UI and
Neon DB are a metadata coordination layer; source code never leaves the local
worker. Adds a mermaid-rendered architecture diagram (two-zone: Scrum4Me-stack
vs Jouw kant) with light/dark variants generated via mmdc.

Page changes (app/page.tsx):
- Hero: H1 "Plannen in de cloud. Uitvoeren op je eigen machine." + new
  subhead; CTA "Hoe het werkt" replaces "Demo bekijken" (anchors to
  #architectuur).
- New section §3 "Architectuur" with light/dark SVG and 4 callout-cards
  (Vercel · Neon · Lokale worker · GitHub) honestly describing what each
  component stores or runs.
- Feature grid: 6 cards (set C) — combines Sprint Board + Solo Paneel,
  adds Lokale Claude-agents, Realtime updates, Async vraagkanaal, Todo's.
  LIVE callout removed (folded into Realtime card).
- "Twee manieren"-section replaced by a Quickstart with concrete git-clone
  snippet for the MCP-server.
- Handleiding: 9 → 10 steps; MCP recommended in step 8; new step 9
  "Story laten uitvoeren" describing the Voer-uit / job-queue flow;
  step 1 mentions QR-pairing as alternative login.
- Footer: adds link to madhura68/scrum4me-mcp alongside the app repo.

Tooling:
- New docs/diagrams/architecture.mmd (mermaid source).
- New npm script "diagrams" generates light + dark SVG via mmdc; output
  committed to public/diagrams/. No prebuild hook (manual regenerate, like
  prisma generate / docs:index).
- Plan + grilling outcomes captured in docs/plans/landing-local-first.md.

Tracked under Scrum4Me story cmoq2qoik0001qa175iynfnaa
(PBI Marketing & Landingspagina, cmoq2q50s0000qa174rmrjove).

Verified: npm run lint (0 new errors), npm test (379/379), npm run build OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 20:24:10 +02:00
Janpeter Visser
7e45bbdbc0
docs: AI-optimized docs restructure (Phases 1–8) (#61)
* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* Added pdevelopment docs

* docs(plans): add docs-restructure plan for AI-optimized lookup

Audit of existing 39 doc files (~10.700 lines) and a phased restructure
proposal aimed at minimising the tokens an AI agent has to read to find
the right reference. Captures resolved decisions on language (English),
ADR template (Nygard default with MADR escape-hatch), index generator
(node script), and folder taxonomy. Proposal status — fase 1 to follow.

* docs(adr): add ADR scaffolding (templates, README, meta-ADR)

Set up docs/adr/ as the canonical home for architecture decisions:

- templates/nygard.md — default four-section format (Status, Context,
  Decision, Consequences) for one-way-door decisions.
- templates/madr.md — MADR v4 with YAML front-matter and explicit
  Considered Options for decisions where rejected alternatives matter.
- README.md — naming convention (NNNN-kebab-case), template-selection
  guidance (Nygard default; MADR for auth, queue mechanics, agent
  integration), status lifecycle, and ADR roster.
- 0000-record-architecture-decisions.md — meta-ADR establishing the
  practice itself, in Nygard format.

Backfilling existing implicit decisions (base-ui-over-radix, float
sort_order, demo-user three-layer policy, etc.) is fase 6 of the
docs-restructure plan.

* feat(docs): add docs index generator + initial INDEX.md

scripts/generate-docs-index.mjs walks docs/**/*.md, parses YAML
front-matter (or first H1 fallback) and a Nygard-style ## Status
section, then writes docs/INDEX.md with grouped tables for ADRs,
Specs, Plans (with archive subsection), Patterns, and Other.

Pure Node 20 (no external deps); idempotent — running it twice
produces byte-identical output. Excludes adr/templates/, the ADR
README, INDEX.md itself, and any *_*.md sidecar file.

Wire-up:
- package.json: docs:index → node scripts/generate-docs-index.mjs

Initial run indexed 35 docs across the existing structure; the
generated INDEX.md is committed so the table is reviewable in the
PR before hooking generation into a pre-commit step.

* chore: ignore Obsidian vault and personal sidecar files

Add .obsidian/ (Obsidian vault config) and _*.md (personal sidecar
notes) to .gitignore so the docs/ tree can serve as canonical source
of truth while still being usable as an Obsidian vault for personal
authoring. The docs index generator already excludes the same _*.md
pattern from INDEX.md.

* docs(plans): add PBI bulk-create spec for docs-restructure

Machine-parseable spec for an executor that calls the scrum4me MCP
(create_pbi → create_story → create_task) to seed the docs-restructure
work into the DB.

- Section 1 (Context) is the PBI description; serves as task-context
  via mcp__scrum4me__get_claude_context.
- Section 2 lists the 6 resolved decisions (English, MD3+styling
  merged, solo-paneel merged, .Plans archived, Nygard ADR default,
  node index script).
- Section 3 records what already shipped on this branch so the
  executor doesn't duplicate the ADR scaffolding or index generator.
- Section 4 carries the structured YAML graph: 1 PBI, 8 stories
  (one per phase), 39 tasks. product_id is REPLACE_ME — fill before
  running.
- YAML validated with PyYAML; field schema sanity-checked.

* docs(junk-cleanup): remove stub patterns/test.md

* docs(junk-cleanup): archive .Plans/ to docs/plans/archive/

* docs(front-matter): add YAML front-matter to docs/ root

* docs(front-matter): add YAML front-matter to patterns/

* docs(front-matter): add YAML front-matter to plans + agent files

* docs(index): regenerate INDEX.md after front-matter pass

* docs(naming): drop scrum4me- prefix from doc filenames

* docs(naming): lowercase API.md and MD3 filenames

* docs(naming): rename plan file to kebab-case ASCII

* docs(naming): rename middleware.md to proxy.md (next 16)

* docs(naming): polish CLAUDE.md doc-index after renames

* docs(taxonomy): scaffold topical folders under docs/

* docs(taxonomy): move spec files into docs/specs/

* docs(taxonomy): move design/api/qa/backlog/assets into folders

* docs(taxonomy): move agent-instruction-audit into decisions/

* docs(split): break architecture.md into 6 topical files

* docs(split): merge solo-paneel-spec into specs/functional.md

* docs(split): merge md3-color-scheme into design/styling

* docs(trim): extract branch/commit rules into runbook

* docs(trim): extract MCP integration into runbook

* docs(adr): add 0001-base-ui-over-radix

* docs(adr): add 0002-float-sort-order

* docs(adr): add 0003-one-branch-per-milestone

* docs(adr): add 0004-status-enum-mapping

* docs(adr): add 0005-iron-session-over-nextauth

* docs(adr): add 0006-demo-user-three-layer-policy

* docs(adr): add 0007-claude-question-channel-design

* docs(adr): add 0008-agent-instructions-in-claude-md + update README index

* docs(index): regenerate after ADR 0001-0008

* docs(glossary): add docs/glossary.md

* chore(docs): regenerate INDEX.md in pre-commit hook

* docs(readme): link INDEX + glossary + agent instructions

* feat(docs): add doc-link checker script

* chore(docs): wire docs:check-links and docs npm scripts

* ci(docs): block merge on broken doc links

* docs(links): fix broken cross-references after restructure

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 03:21:59 +02:00
1df1ea48ad
Component: BurndownChart (Recharts LineChart) (#35)
* feat: getBurndownData helper + computeBurndownDays (lib/insights/burndown.ts)

Server-side aggregatie per active sprint: bouwt time-series met remaining en ideal per dag.
Inclusief 4 Vitest-unit-tests voor de pure computeBurndownDays functie.

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

* chore: voeg recharts toe aan dependencies

Vereist door BurndownChart component in lib/insights.

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

* feat: BurndownChart component (Recharts LineChart, ideal vs remaining)

LineChart met two series: ideal (grijs, dashed) en remaining (status-in-progress blauw).
ResponsiveContainer 100% x 240px, empty-state bij lege days-array.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 16:41:15 +02:00
8877ea469d
feat(M14): 3-pane backlog — generic SplitPane, BacklogStore, SSE realtime, card-grid TaskPanel (#22)
* feat(split-pane): refactor to generic n-pane SplitPane with cookie persistence

New API: panes[], defaultSplit[], cookieKey, tabLabels. Supports arbitrary
number of panes with n-1 draggable dividers and JSON cookie persistence.
Replaces TriplePane; mobile renders tabs.

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

* feat(split-pane): migrate callers to new panes[] API

Backlog page and sprint board now use generic SplitPane.
TriplePane removed; sprint board uses 3-pane with defaultSplit=[28,35,37].

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

* test(split-pane): add unit tests for 2/3-pane, cookie-restore, mobile tabs

Added jsdom + @testing-library/react devDeps for component testing.
7 cases: render, divider count, cookie restore, invalid cookie fallback,
mobile tab render/switch, and no-dividers-on-mobile.

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

* feat(backlog): add BacklogStore Zustand store with applyChange reducer

State: pbis, storiesByPbi, tasksByStory. setInitialData for server
hydration; applyChange(entity, op, data) handles I/U/D for SSE events.

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

* feat(backlog): server-fetch tasks + hydrate BacklogStore on page load

Page now fetches tasks parallel to stories and groups by story_id.
BacklogHydrationWrapper calls setInitialData on mount so the store
is ready for downstream SSE consumers.

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

* feat(backlog): add EmptyPanel shared component, replace inline empty states

EmptyPanel takes title?, message, and optional action with DemoTooltip.
Replaces duplicate inline empty-state markup in pbi-list and story-panel.

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

* feat(backlog): add TaskPanel with sortable rows and TaskDialog wiring

Reads selectedStoryId + tasksByStory from stores. DnD reorder via
reorderTasksAction. Row click → ?editTask, + button → ?newTask&storyId.
DemoTooltip on drag handles and + button.

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

* feat(backlog): wire TaskPanel + TaskDialog into backlog page

3-pane SplitPane [20,45,35]. searchParams for newTask/editTask.
TaskDialog and EditTaskLoader render on ?newTask and ?editTask.

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

* test(backlog): add TaskPanel tests for render states and click handlers

7 cases: no-story empty, no-tasks empty+action, tasks render, + button
router.push, row click router.push, demo disabled button, demo disabled handles.

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

* feat(backlog): migrate PbiList to store-driven via useBacklogStore

Removes pbis prop; reads from useBacklogStore(s => s.pbis) so SSE
updates reflect in real-time without prop drilling.

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

* feat(backlog): migrate StoryPanel to store-driven + selectStory on click

Removes storiesByPbi prop; reads from useBacklogStore. Card click now
dispatches selectStory(id) + shows isSelected highlight. Edit moved to
inline pencil button. page.tsx drops pbis/storiesByPbi props.

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

* test(backlog): add 3-pane integration tests for click-cascade flow

Covers: empty states, PBI→stories, story→tasks, cascade-reset,
isSelected highlight. localStorage mocked for sort-mode persistence.

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

* feat(ST-1115): SSE backlog realtime — endpoint, hook, hydration mount, tests

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

* feat(ST-1116): mobile auto-switch tabs + back button in BacklogSplitPane

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

* docs(ST-1116): update functional-spec (3-pane backlog + mobile) and architecture (backlog SSE + backlog-store)

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

* feat(ST-1117): TaskPanel card-grid — BacklogCard + rectSortingStrategy, tests updated

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

* fix(tests): correct PbiStatusApi type and remove duplicate mock keys

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 18:16:07 +02:00
6cd98129f2
M14: TaskDialog (create/edit) + story auto-promotion (#21)
* chore(ST-1112): add deps for task dialog

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

* feat(ST-1112): add shared zod schema for task dialog

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

* feat(ST-1112): add missing MD3 tokens for task dialog

outline-variant, on-error-container, status-review (light + dark)

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

* feat(ST-1112): add saveTask and deleteTask server actions for TaskDialog

Unified create/edit action (saveTask) replaces separate formData-based
actions for the new TaskDialog. Uses shared zod schema, structured
SaveTaskResult union type, and context-aware revalidatePath for both
sprint and backlog routes.

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

* feat(ST-1112): add TaskDialog component (create & edit mode)

Builds the full TaskDialog on top of the existing @base-ui/react
Dialog primitive. Covers create mode, edit mode (status field +
created_at metadata + delete), dirty-check AlertDialog, delete
confirm AlertDialog, Cmd+Enter submit, and per-field char counters.
Uses react-hook-form + zodResolver against the shared taskSchema.
Priority and status are extracted to PrioritySegmented and
StatusSelect sub-components using MD3 tokens throughout.

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

* feat(ST-1112): refactor task-list to open TaskDialog via URL params

Replaces inline create/edit forms with router.push navigation:
- Clicking a task row → ?editTask=<id>
- "+ Taak" button → ?newTask=1&storyId=<storyId>
Removes CreateTaskForm, EditSubmitButton, updateTaskAction, and
createTaskAction from the component. Status toggle and DnD remain
unchanged. Rows now have cursor-pointer and keyboard a11y.

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

* feat(ST-1112): wire TaskDialog into sprint page via searchParams

Sprint page now reads ?newTask, ?storyId, and ?editTask query params.
For edit mode: fetches the task server-side with productAccessFilter
scope (invalid/foreign IDs redirect to closePath). Renders TaskDialog
when either param is present. closePath is the sprint route without
query params.

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

* feat(ST-1112): add Suspense skeleton for edit-mode task loading

Extracts task fetch into EditTaskLoader (async server component) so
the sprint board renders immediately while the task loads.
TaskDialogSkeleton shows 3 grey bars during the fetch. Invalid or
out-of-scope task IDs redirect to closePath.

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

* feat(ST-1112): render description as markdown in task-detail-dialog

Solo task detail now renders description via react-markdown +
remark-gfm with prose styling. Sanitizes script/iframe elements.

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

* test(ST-1112): add saveTask/deleteTask server action tests

Covers all three demo-policy layers and cross-tenant scope:
demo blocked (403), unauthenticated blocked, validation 422,
edit cross-tenant forbidden, create cross-tenant forbidden,
and happy-path for both edit and create.

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

* feat(ST-1112): add updateTaskStatusWithStoryPromotion helper

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

* feat(ST-1112): wire story-promotion into saveTask and PATCH /api/tasks/:id

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

* docs(ST-1112): add task-dialog doc and architecture note

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

* chore: extend allowed tools in settings.local.json

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

* feat(ST-1113): add 200ms animation-delay to TaskDialogSkeleton to prevent flicker

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

* feat(ST-1114): add DirtyCloseGuard reusable component for dirty-form close confirmation

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

* feat(ST-1114): add shared Markdown wrapper, apply to task-detail and story-dialog

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

* chore: allow grep -E pattern in settings.local.json

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 16:55:20 +02:00
74616432d2
M10: Password-loze inlog via QR-pairing (ST-1001..ST-1008) (#12)
* chore(M10): swap demo-active sprint from M3.5 to M10

M3.5 was de demo-actieve sprint zolang er geen recentere milestone in progress
was. Nu M10 het actieve werk is, willen we dat get_claude_context (en
implement_next_story) ST-1001 als next-story teruggeven i.p.v. ST-350.

Vereist een herhaling van npx prisma db seed na deze commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(M10): gate Solo demo-stories on M3.5 active milestone

De hardcoded Solo Paneel-demoset uit M3.5 (priority=2) schreeuwt over de
parser-driven M10-stories heen (priority=4) en laat get_claude_context op
"Gebruikersauthenticatie opzetten" wijzen i.p.v. ST-1001.

Sluit het blok nu alleen open als de actieve sprint van het Scrum4Me-product
M3.5 betreft. Voor M10+ leveren de parser-stories zelf de bord-content; de
demo-set blijft beschikbaar als M3.5 ooit weer ACTIVE wordt voor demo-doeleinden.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(M10): drop hardcoded Solo Paneel demo data from seed

DB wordt voortaan leidend voor de werkstaat; testdata voor andere projecten /
demo-scenario's komt elders. Deze hardgecodeerde set was specifiek gemaakt voor
de M3.5 Solo Paneel-demo en raakt nu het next_story-resultaat: priority=2 won
van de M10 parser-stories (priority=4) waardoor get_claude_context op
'Gebruikersauthenticatie opzetten' bleef hangen i.p.v. ST-1001.

Vervangt de eerdere M3.5-gating-aanpak (commit 0e3228d) — schoner om het
helemaal weg te halen dan met een conditional aanwezig te houden.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(M10): add npm run seed shortcut

Wrapt prisma db seed (die de bestaande prisma.seed-config in package.json gebruikt)
zodat re-seeden één korte invocatie wordt zonder de prisma-CLI-syntaxis te onthouden.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-1001): add LoginPairing model + pg_notify trigger via migration

Schema (prisma/schema.prisma):
- model LoginPairing met id (cuid), secret_hash + desktop_token_hash (beide NOT
  NULL — scheiden mobiel- en desktop-bewijs), status (pending|approved|consumed
  |cancelled), optionele user_id met onDelete: SetNull, desktop_ua VarChar(255),
  desktop_ip VarChar(45) voor IPv6, created_at + expires_at + approved_at +
  consumed_at, indexes op (expires_at) en (status, expires_at)
- back-relation login_pairings LoginPairing[] op User

Migratie (20260427200734_add_login_pairing):
- Prisma-gegenereerde DDL voor login_pairings + indexes + FK
- Toegevoegde notify_pairing_change() functie + login_pairings_notify trigger
  op AFTER INSERT/UPDATE; emit pg_notify('scrum4me_pairing', payload) met
  { op: 'I'|'U', pairing_id, status }
- DELETE niet ondersteund — pairings gaan naar consumed/cancelled, niet weg
- Channel naam analoog aan scrum4me_changes uit ST-801

Verification: Node pg-client roundtrip-test via DATABASE_URL toonde notifies bij
INSERT (op=I) en UPDATE (op=U) met correcte payload-shape.

Bouwt voort op M8 LISTEN/NOTIFY-infra. SSE-route /api/auth/pair/stream/[id] in
ST-1004 abonneert hierop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-1002): add pairing helpers, pre-auth cookie + paired-session guard

lib/auth/pairing.ts: pure crypto-helpers voor de QR-pairing flow.
- generateMobileSecret() / generateDesktopToken() — beide 32 bytes base64url, los
  zodat ze elkaar niet onthullen
- hashToken(t) — sha256-hex
- verifyToken(t, hash) — timingSafeEqual met length-guard
- isPairedSessionExpired(session) — geëxtraheerde helper zodat de Server-
  Component-render Date.now() niet rechtstreeks aanroept (React Compiler-flag)

lib/auth/pair-cookie.ts: HttpOnly pre-auth cookie helpers (s4m_pair).
- Path=/api/auth/pair, Max-Age=120s (gelijk aan pending-TTL pairing),
  SameSite=Lax, Secure in productie

lib/session.ts: SessionData uitgebreid met optionele paired + pairedExpiresAt.

app/(app)/layout.tsx: guard die paired-sessies vernietigt zodra
pairedExpiresAt verstreken is en redirect naar /login.

Tests: 14 unit-tests in __tests__/lib/auth/pairing.test.ts dekken hash-
determinisme, timing-safe verify (true/false/length-mismatch), generator-
uniciteit en vier expiry-scenario's voor isPairedSessionExpired.

Quality gates: npm run lint (0 errors), tsc --noEmit clean, vitest 111/111.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-1003): add /api/auth/pair/start with rate-limit + pre-auth cookie

POST /api/auth/pair/start (anon, runtime: 'nodejs'):
- Geen authenticateApiRequest — desktop heeft nog geen sessie
- Genereert los mobileSecret + desktopToken via lib/auth/pairing
- Persisteert alleen sha256-hashes in login_pairings; status='pending', expires_at = now + 2 min
- Slaat user-agent + best-effort IP op (afgekapt op kolom-grootte)
- Set-Cookie via setPairCookie helper: HttpOnly, Path=/api/auth/pair, Max-Age=120, SameSite=Lax
- Response body: { pairingId, mobileSecret, expiresAt, qrUrl } met qrUrl = origin/m/pair#id=…&s=…
  → secret reist alleen via fragment (#…), nooit in querystring of access logs

Rate-limit: 'pair-start' expliciet aan lib/rate-limit.ts CONFIGS toegevoegd
voor self-documentatie (10/min, gelijk aan login).

Tests __tests__/api/pair-start.test.ts (6 cases):
- 200 met body-shape (pairingId, mobileSecret 43-char base64url, qrUrl met
  fragment, expiresAt ISO)
- alleen hashes in DB, geen plaintext
- cookie set met juiste opties
- UA + IP afgekapt op kolom-grootte
- IP=null als x-forwarded-for ontbreekt
- 11e POST levert 429 met NL foutmelding

Quality gates: lint 0 errors, tsc clean (na prisma generate), vitest 117/117.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-1004): add SSE /api/auth/pair/stream with cookie auth

GET /api/auth/pair/stream/[pairingId]:
- runtime: 'nodejs', maxDuration: 300, dynamic: 'force-dynamic'
- Auth via s4m_pair HttpOnly cookie (readPairCookie + verifyToken tegen
  desktop_token_hash); 401 zonder cookie of bij hash-mismatch, 404 als pairing
  onbekend, 410 als verlopen — geen geheim materiaal in URL of querystring
- Hergebruikt LISTEN/NOTIFY-pattern uit app/api/realtime/solo/route.ts:
  ReadableStream + dedicated pg.Client + heartbeat 25s + hard-close 240s
- Channel: scrum4me_pairing; filter notifies op pairing_id-match
- Initial 'state'-event direct na connect met huidige status (voorkomt race
  waarbij approve net vóór SSE-open landt — desktop ziet 'm alsnog)
- Auto-close zodra status consumed/cancelled binnenkomt
- Fallback DIRECT_URL → DATABASE_URL (de eerste staat lokaal op een placeholder)

Tests __tests__/api/pair-stream.test.ts (4 cases — auth-paden):
- 401 zonder cookie (en geen DB-call gedaan)
- 404 op onbekende pairingId
- 410 op verlopen pairing
- 401 op cookie/hash-mismatch

Full-stream-test (LISTEN+notify-roundtrip) is een handmatige acceptatietest in
ST-1008 — niet zinvol te mocken voor v1.

Quality gates: lint 0 errors, tsc clean, vitest 121/121.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-1005): add pairing server actions + mobile confirmation page

actions/pairing.ts (Server Actions, volgt docs/patterns/server-action.md):
- getPairingForApproval(pairingId, mobileSecret): auth + Zod + lookup + status
  + expiry + verifyToken-check; retourneert UA/IP/username voor de
  bevestigingspagina. Demo MAG aanroepen (read-only).
- approvePairing: zelfde checks PLUS demo-blokkade (session.isDemo). Update
  status pending→approved, zet user_id + approved_at, bumpt expires_at +5min.
  Postgres-trigger emit pg_notify automatisch — desktop-SSE pikt het op.
- cancelPairing: status pending→cancelled. Demo mag annuleren.
- Tagged-union return-type uit loadPendingPairing voor schone discriminatie.

app/(app)/m/pair/page.tsx (Server Component, achter (app)/layout-guard):
- Geen searchParams uitlezen — page leest URL niet. Alleen statische uitleg +
  PairConfirmation client-island.

app/(app)/m/pair/pair-confirmation.tsx (Client Component):
- useEffect parseert window.location.hash voor #id=…&s=… (server ziet de
  fragment nooit)
- Roept getPairingForApproval om UA/IP/username op te halen
- Toont kaart "Inloggen als <username> op dit apparaat?" met UA + IP +
  expliciete waarschuwing tegen phishing-QR; Bevestig/Annuleer-knoppen
- Na approve: window.history.replaceState wist de hash zodat back/forward de
  secret niet meer onthult; transitioneert naar success-state
- queueMicrotask voor synchrone setState om React-Compiler "cascading renders"
  warning te vermijden

Tests __tests__/actions/pairing.test.ts (11 cases):
- getPairingForApproval: ok + 5 fail-paths (geen sessie, approved, verlopen,
  verkeerd secret, ongeldige cuid)
- approvePairing: happy + demo-block + verkeerd secret (geen DB-write)
- cancelPairing: happy + demo mag annuleren

Quality gates: lint 0 errors, tsc clean, vitest 132/132.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-1006): add /api/auth/pair/claim with atomic consume + iron-session

POST /api/auth/pair/claim (cookie-auth, runtime: 'nodejs'):
- Auth via s4m_pair HttpOnly cookie alleen — body bevat enkel pairingId, geen
  secret. Het cookie-token is het bewijs.
- Atomic state-transitie via prisma.loginPairing.updateMany met composite
  WHERE (id + status='approved' + desktop_token_hash + expires_at > now);
  PostgreSQL row-locking garandeert dat concurrent dubbele claims slechts één
  count=1 zien — de rest 410.
- Bij geen rij geüpdate: tweede findFirst om te disambigueren tussen 401
  (cookie matcht geen pairing) en 410 (al consumed/cancelled). Cookie altijd
  gecleared bij faalpaden om herhaalde verwerking te voorkomen.
- Bij succes: getIronSession schrijft scrum4me-session-cookie met userId +
  isDemo (uit user-record als vangnet) + paired=true + pairedExpiresAt = now+8h
  (kortere TTL voor publieke desktops). s4m_pair wordt gecleared.
- Logging onder NODE_ENV !== 'production' alleen pairingId, nooit cookie of
  mobileSecret.

Tests __tests__/api/pair-claim.test.ts (7 cases):
- 200 happy: updateMany met juiste WHERE, iron-session payload (userId, isDemo,
  paired, pairedExpiresAt ~8h), save() called, s4m_pair cleared
- demo-vangnet: isDemo=true wordt doorgezet
- 401 zonder cookie (geen DB-call)
- 400 op malformed body
- 400 zonder pairingId
- 410 op tweede claim (al consumed, cookie cleared, geen session.save)
- 401 op cookie/hash-mismatch (cookie cleared)

Quality gates: lint 0 errors, tsc clean, vitest 139/139.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(M10): bump pending-TTL to 5min + repair MD3 contrast on pair page

TTL: 2 min was te kort voor handmatig curl-paste-confirm-testen — gebruiker
zag 'Pairing verlopen' voor hij kon bevestigen. Bumpt naar 5 min (gelijk aan
approved-TTL): nog steeds tight voor security, ruim voor menselijke reactie.
- app/api/auth/pair/start/route.ts: PENDING_TTL_MS 120s → 300s
- lib/auth/pair-cookie.ts: MAX_AGE_SECONDS 120 → 300
- __tests__/api/pair-start.test.ts: maxAge en expires_at-window meegegroeid

Kleuren: bevestigingspagina gebruikte bg-destructive/10 + text-destructive-
foreground — beide lichte kleuren, te weinig contrast. Vervangen door MD3
container-tokens (zelfde patroon als components/auth/auth-form.tsx):
- error-state: bg-error-container + text-error-container-foreground + border-l-4 border-error
- approved-state: bg-success-container + foreground + accent-border
- cancelled-state: bg-surface-container-high + neutral foreground

Quality gates: lint 0 errors, tsc clean, vitest 139/139.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-1007): add QR login button on /login with SSE listener

Voltooit de desktop-zijde van de QR-pairing-flow. Gebruiker klikt "Inloggen
via mobiel" naast het wachtwoord-formulier → krijgt een QR-code → telefoon
scant en bevestigt → desktop wordt automatisch ingelogd zonder dat er ooit
een wachtwoord is getypt op het publieke apparaat.

app/(auth)/login/qr-login-button.tsx (Client Component):
- Phase-state: idle | starting | showing | expired | claiming
- klik → POST /api/auth/pair/start (credentials:'same-origin' voor s4m_pair)
- QRCodeSVG met fragment-URL als value (level=M, 200px); aria-label
- EventSource('/api/auth/pair/stream/<id>', { withCredentials: true })
  vereist voor cookie-auth — standaard verstuurt EventSource geen credentials
- bij data.status === 'approved': es.close → POST /pair/claim → router.push('/dashboard')
- aftellende timer (mm:ss); bij 0s → 'expired' state met Vernieuwen-knop
- cleanup bij unmount: removeEventListener + close
- A11y: <details> sectie toont fragment-URL als kopieerbare tekst voor screenreaders en gebruikers zonder camera

app/(auth)/login/page.tsx: QrLoginButton onder het bestaande wachtwoord-form
met "of"-divider, achter de bestaande surface-container-low styling.

Dependency: qrcode.react ^4.2.0 (client-side SVG; geen extra round-trip;
mobileSecret blijft op desktop in JS-geheugen).

Quality gates: lint 0 errors, tsc clean, vitest 139/139, next build slaagt
(login-route static, m/pair en pair/* dynamic).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ST-1008): document QR-pairing endpoints, flow, threat-model + pattern

docs/API.md — nieuwe sectie 'Auth — QR-pairing (M10)' met alle drie endpoints
(start, stream, claim), cookie-mechaniek, foutcodes (400/401/410/429),
curl-voorbeelden inclusief --cookie-jar.

docs/scrum4me-architecture.md — sectie 'QR-pairing flow' met:
- Mermaid sequence-diagram (start → QR → scan → approve → claim)
- Threat-model (replay, phishing-QR, demo-block, rate-limit, secret-leak,
  long-lived sessie) met expliciete mitigaties
- TTL-rationale voor de drie tijden (5min pending / +5min approved / 8u paired)
- Subsectie 'Waarom geen secret in URL' — fragment-eigenschap + HttpOnly
  cookie + twee gescheiden hashes

docs/patterns/qr-login.md — herbruikbaar pattern 'QR-pairing via unauth-SSE +
pre-auth cookie' met de drie endpoints, vier security-uitgangspunten,
sjabloon-bestanden, TTL-richtlijn, en wanneer NIET te gebruiken.

CLAUDE.md — extra rij in Implementatiepatronen-tabel die naar het nieuwe
pattern-doc verwijst.

Acceptatie ST-1008 (zeven scenario's):
- Happy path: gedekt door manuele E2E in vorige stories (gebruiker bevestigde
  dat M10-stories op Solo bord verschijnen + curl-roundtrip werkt)
- Demo-block: actions/pairing.test.ts → approvePairing demo → Niet beschikbaar
- Replay: pair-claim.test.ts → 410 op tweede claim
- Expiry tijdens pending: pair-stream.test.ts + pairing.test.ts → 410/error
- Expiry tussen approve+claim: pair-claim.test.ts → 410
- Cookie-mismatch op SSE/claim: pair-stream.test.ts + pair-claim.test.ts → 401
- Secret niet in URL/logs: per ontwerp — fragment + cookie reizen niet via
  URL-paden of querystrings (gedocumenteerd in architecture.md)

Quality gates: lint 0 errors, tsc clean, vitest 139/139 (16 files).

M10 is hiermee compleet — feat/M10-qr-login bevat 13 commits klaar voor
gebruiker-acceptatie en PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: move logout form outside DropdownMenuContent so requestSubmit fires

UserMenu's hidden logout-form zat binnen <DropdownMenuContent>. Wanneer een
DropdownMenuItem onSelect vuurt, sluit base-ui de menu en unmount het
content-portal in dezelfde tick — waardoor de form verdwijnt voordat
requestSubmit() wordt aangeroepen, en logoutFormRef.current null is.

Form naar top-level van het component verplaatsen (als sibling van DropdownMenu,
binnen Fragment) houdt de ref geldig. Geen DOM-side-effecten — form is hidden,
zat nooit visueel in het menu.

Quality gates: lint 0 errors, tsc clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: call logoutAction directly via useTransition instead of form-ref submit

De form-ref-dance werkte niet betrouwbaar in de huidige base-ui:
- onSelect vuurde requestSubmit() op een hidden form
- Form zat eerst binnen DropdownMenuContent (form geunmount → ref null)
- Form daarna naar top-level verplaatst — vuurde nog steeds geen request af,
  vermoedelijk doordat onSelect in deze base-ui-build niet (consistent) een
  click-event genereerde dat de form-API trigger'de

Vervang door directe call: Server Actions kunnen sinds Next.js 14 als async
functie worden aangeroepen vanuit Client Components. useTransition voorkomt
dat de UI bevriest tijdens de redirect.

Naast onSelect ook onClick als veiligheid voor het geval base-ui later weer
van event-prop wisselt — beide handlers wijzen naar dezelfde idempotente
function (handleLogout via startTransition).

Pendingstate ('Uitloggen…' label, disabled item) zodat dubbele klikken niet
dubbele logoutAction-calls afvuren.

Quality gates: lint 0 errors, tsc clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ST-1007): listen for SSE 'state' event so approve-during-connect resolves

De SSE-route in ST-1004 stuurt de catch-up payload als `event: state\ndata: …`
om een race te dichten: tussen pair/start en SSE-open kan de mobiel approven,
de pg_notify fired vóór onze LISTEN actief is en gaat verloren (Postgres
queuet niet). De server compenseert door direct na connect een `state`-event
te sturen met de huidige status uit de DB.

Maar de client luisterde alleen op 'message'. EventSource routeert events met
`event: <name>` enkel naar listeners voor die exacte naam — het catch-up event
werd dus genegeerd. Gevolg bij een (zeldzame) race: QR blijft hangen tot
expiry omdat noch de notify noch de catch-up doorkomt.

Fix: dezelfde onMessage-handler ook aan 'state' binden (en netjes
unsubscriben bij cleanup). Geen server-side wijziging nodig — protocol bleef
bewust om de semantische scheiding 'initial state' vs 'live notify' te
behouden voor toekomstige clients die er onderscheid in willen maken.

Severity: middel-laag — kleine race-window, geen data/security-impact, alleen
"QR doet niks" tot user op Vernieuwen klikt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(M10): close pair/stream race + demo-block on cancelPairing

Twee P1's uit code-review:

(1) pair/stream race: de findUnique die de pairing-status leest gebeurde vóór
LISTEN actief was. Als de mobiel approvet tussen die query en LISTEN: pg_notify
fired in dat venster gaat verloren (Postgres queuet niet voor abonnees die
nog niet listen) én was de eerder gelezen status stale. De catch-up state-
event emitte dus 'pending' terwijl de DB inmiddels 'approved' was, en de
desktop bleef hangen tot expiry.

Tweede findUnique toegevoegd ná LISTEN actief is: het venster sluit, omdat
elke approve na dat punt via de notify-handler doorkomt. Aanvullend op de
eerdere client-side fix die 'state' events nu ook routeert (commit d6e71f9).

(2) cancelPairing demo-block: cancel was een DB-write zonder demo-guard,
in tegenspraak met de "demo = 403 op writes"-regel. Demo-blokkade
toegevoegd; bestaande test omgedraaid naar 'wordt geblokkeerd, geen DB-write'.

Quality gates: lint 0 errors, tsc clean, vitest 139/139.

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-04-28 00:01:04 +02:00
88dca4102c
feat(M9): active product backlog — persistent active PB, NavBar splits, sprint card styling (#10)
* feat(tooling): extend backlog parser to support PBI-x milestone headers

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

* chore(backlog): mark ST-801–806 as done

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

* feat(backlog): sorteer PBI's en stories op prio/code/datum, onthoud keuze in localStorage; vergroot sprint-afronden dialoog

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

* feat(ST-901): add user.active_product_id with FK to Product

- Nullable relation User → Product with onDelete: SetNull
- Index on active_product_id for join performance
- Migration: 20260427165329_add_user_active_product_id
- Install @tanstack/react-table (was missing from node_modules)
- Fix PRIORITY_COLORS ref removed in earlier refactor
- Note: User schema change affects vendor/scrum4me-mcp submodule — run prisma generate + tsc --noEmit there after merge

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

* fix: restore priority color on PBI filter pill

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

* feat(ST-902): add setActiveProduct + clearActiveProduct server actions

- actions/active-product.ts: setActiveProductAction validates access via
  productAccessFilter, rejects archived products and demo users
- archiveProductAction: clears active_product_id for all affected users in transaction
- removeProductMemberAction: clears active_product_id for removed member
- leaveProductAction: clears active_product_id for leaving user

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

* feat(ST-903): load active product in layout, replace cookie with DB lookup in solo

- layout.tsx: fetch active_product_id, resolve product, clear stale ref server-side
- NavBar: add activeProduct prop (rendering changes in ST-904)
- solo/page.tsx: redirect via user.active_product_id instead of lastProductId cookie
- proxy.ts: remove lastProductId cookie logic
- lib/cookies.ts: deleted (no longer used)

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

* feat(ST-904): split NavBar into 5 tabs with disabled-states and product-switcher dropdown

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

* feat(ST-905): add Activeer button per product row in dashboard and product header

* feat(ST-906): redirect to dashboard with toast when active product becomes inaccessible

* feat(ST-907): tests for active-product actions and functional spec update for M9

* docs(M9): add implementation plan document and link from backlog

* feat: active PB indicator, Maak actief button and new product link in settings

* feat: apply priority-color card style to sprint story rows

* fix: move add-to-sprint click from entire card to + Toevoegen button

* feat: apply priority-color card style to sprint task rows

* fix(sprint-backlog): prevent text selection on PBI collapse button

* chore: bump version to 0.4.0 (M9 active product backlog)

* fix(landing): align logged-in nav left to match app NavBar

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 20:25:13 +02:00
2460eae9df
feat(M8): Realtime Solo Paneel via Postgres LISTEN/NOTIFY (ST-801..ST-806) (#8)
* feat(ST-801): pg_notify triggers on tasks and stories

Add notify_task_change() and notify_story_change() PL/pgSQL functions
plus AFTER INSERT/UPDATE/DELETE triggers on tasks and stories. Each
write emits a JSONB payload on the 'scrum4me_changes' channel with
op, entity, id, product_id, sprint_id, assignee_id and (for UPDATE)
the list of changed columns. Tasks resolve product/sprint/assignee
via their parent story so the SSE handler can filter without an extra
DB roundtrip.

The migration is a side-effect-only change (no Prisma model/schema
diff) so the Prisma Client and TypeScript types are unaffected.

Verified locally with a node-pg LISTEN client: both task and story
mutations produce the expected payload within milliseconds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-802): SSE route /api/realtime/solo for Solo Paneel updates

Node.js-runtime route handler die een dedicated pg-Client opent op
DIRECT_URL en LISTEN't op het scrum4me_changes-kanaal. Per inkomende
NOTIFY-payload filtert het server-side op product (uit query-param),
sprint (active sprint van het product) en persoonlijke relevantie
(assignee_id == userId, of assignee_id IS NULL voor stories voor de
claim-lijst).

Auth via iron-session cookie:
- 401 als sessie ontbreekt
- 400 als product_id query-param ontbreekt
- 403 als de user geen toegang heeft tot het product
- demo-tokens mogen lezen (geen write-tools op deze route)

Stream-bouw:
- text/event-stream met juiste headers (no-cache, no-transform,
  X-Accel-Buffering: no voor proxy-vrije buffering)
- ready-event bij connect met product_id en active sprint_id
- heartbeat (SSE-comment) elke 25s
- hard-close na 240s als safety-net onder Vercel maxDuration; client
  herconnect via EventSource
- cleanup op request.signal abort (tab dicht / refresh)

Cleanup-pad sluit de pg-client en de stream-controller idempotent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-803): useSoloRealtime hook with EventSource lifecycle

Client-only hook die de SSE-stream van /api/realtime/solo opent en
voor de UI twee statussen exposed:
  - status: 'connecting' | 'open' | 'disconnected'
  - showConnectingIndicator: pas true als status !== 'open' >2s duurt
    (ST-805 animatie B — voorkomt flikker bij microscopische
    disconnects)

Lifecycle-gedrag:
- Reconnect met exponential backoff start 1s, plafond 30s, reset op
  'ready'-event
- Page Visibility API: bij hidden sluit de hook de connectie en stopt
  reconnect-pogingen; bij visible reopent direct
- onerror van EventSource wordt onderschept zodat we eigen backoff
  kunnen voeren ipv de browser-default
- Volledige cleanup op unmount

Voert events naar useSoloStore.getState().handleRealtimeEvent — de
echte dispatch-logica met pendingOps en gedifferentieerde
apply{Task,Story}{Update,Create,Delete} landt in ST-804. Hier is dat
nog een stub zodat dit zelf-staand kan compileren.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-804): solo-store realtime dispatch + pendingOps

Wire de SSE-events uit /api/realtime/solo door naar de Zustand-store
zodat het Solo Paneel zonder refresh meebeweegt met DB-mutaties uit
welke bron dan ook (web, REST, MCP).

Migratie 20260427000216_extend_realtime_payload: voegt new-state
velden aan de pg_notify-payload toe (task_status, task_sort_order,
task_title, story_status, story_sort_order, story_title, story_code)
zodat de client geen extra fetch nodig heeft per event.

Store-uitbreiding (stores/solo-store.ts):
- pendingOps: Set<task-id> die optimistic-writes markeert; realtime
  echos voor die ids worden onderdrukt zodat eigen UI-mutaties niet
  twee keer toegepast worden of door een latere echo overschreven
- handleRealtimeEvent: dispatch op entity + op
  - task UPDATE/INSERT: bestaande tasks krijgen status/title/sort_order
    bijgewerkt; onbekende tasks worden genegeerd (story-context
    ontbreekt — gebruiker ziet ze pas na refresh)
  - task DELETE: verwijdert uit store
  - story UPDATE: werkt story_title/story_code bij op alle child-tasks
    in de store
  - story DELETE: verwijdert alle child-tasks (cascade reflectie)

Unit-test: 7 scenario's (status update, pendingOps echo-suppression,
DELETE, story-rename cascade, story-delete cascade, unknown task
no-op).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-805): wire useSoloRealtime + live indicator + column-move animatie

- SoloBoard roept useSoloRealtime(productId) aan, zo komt elke task/
  story-mutatie uit web/REST/MCP binnen via SSE en wordt door de
  store-dispatcher (ST-804) verwerkt
- markPending/clearPending rond de drag-drop optimistic write zodat
  de echo van de eigen Server Action de store niet dubbel beweegt
- RealtimeIndicator: kleine status-dot in de header
  - groen ('Live') wanneer SSE-stream open OF tijdens de eerste 2s
    grace-period (animatie B in de hook — voorkomt micro-flikker)
  - grijs ('Verbinden…') na 2s in connecting-state
  - rood ('Verbroken') na 2s in disconnected-state
- Animatie A (kolom-move): bij task UPDATE-events wikkelt de hook de
  store-dispatch in document.startViewTransition + flushSync. SoloTask-
  cards krijgen view-transition-name `solo-task-<id>` (alleen wanneer
  niet aan het draggen) zodat de browser de positie-shift soepel
  animeert van bezig naar klaar (en omgekeerd)

Bestaande 89 tests blijven groen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: pin dev to port 3000 + predev hook to clear stale processes

Voorkomt dat een stale next-dev op 3000 ervoor zorgt dat een tweede
'npm run dev' op 3001 start — wat sessies, cookies en MCP-config
inconsistent maakt.

- dev: '-p 3000' expliciet
- predev: lsof/kill alles op 3000 (idempotent — falen is ok)
- CLAUDE.md: regel toegevoegd onder Conventies

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(M8): make SSE-stream survive Solo Paneel mutations

Symptoom op feat/ST-801-realtime-triggers initial implementation:
elke task-update sloot de open SSE-stream af en triggerde een
herverbinding met backoff. In de tussentijd gemiste events.

Oorzaak: Server Actions in App Router doen een impliciete
route-tree refresh die client components remount; daarmee killt
React de useEffect die de EventSource beheert.

Fix in twee delen:

1. Hef de realtime-hook op naar de (app)-layout via een nieuwe
   `SoloRealtimeBridge`-component. Layouts overleven Server-
   Action-refreshes beter dan pages, en de bridge leest het
   product-id uit de URL via usePathname. Connection-status
   (status, showConnectingIndicator) gaat naar de solo-store
   zodat SoloBoard 'm uit een gedeelde plek kan lezen.

2. Vervang updateTaskStatusAction en updateTaskPlanAction in de
   Solo-componenten door fetch naar de bestaande Route Handler
   `PATCH /api/tasks/[id]`. Route Handlers triggeren geen
   page-refresh, dus de SSE-stream blijft staan. lib/api-auth.ts
   accepteert nu naast Bearer-tokens ook iron-session cookies
   zodat browser-fetches zonder token werken.

Bijkomend: actions/tasks.ts laat /solo bewust niet meer
revalideren (wordt nu via realtime gedekt). Sprint/planning blijft
wel revalidaten — geen realtime daar.

Toegevoegd:
- components/solo/realtime-bridge.tsx — mount in (app) layout
- scripts/realtime-mutate.ts — handige test-helper voor externe
  mutaties (alsof MCP/REST schrijft) tijdens acceptance

Debug-logs in app/api/realtime/solo/route.ts staan nog aan voor
ST-806 acceptance; worden later gestript.

Bekend issue: Chrome op localhost (HTTP/1.1) cycle't EventSource
om de paar seconden vanwege de 6-connectie-limiet en retry-
heuristiek. Safari werkt stabiel. Productie op Vercel (HTTP/2
multiplexing) zou beide browsers stabiel moeten houden — Vercel
preview test is volgende stap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(dev): disable reactStrictMode for stable SSE testing locally

Strict Mode dubbel-mount maakt langlopende connecties tijdens
ontwikkeling moeilijk te observeren — de mount/unmount cycles in
dev rondom Hot Reload + Turbopack triggeren herhaalde EventSource-
reconnects die verbergen of de productie-flow stabiel is.

Productie kent dit gedrag niet (Strict Mode is dev-only). Heroverwogen
als M8 acceptance rond is — kan dan weer aan voor andere effects-
discipline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(M8): unguard debug logs in /api/realtime/solo for Vercel diagnose

Maakt de connect/listen/notify-logs zichtbaar in Vercel function-logs
op preview zodat we kunnen zien waarom events niet doorkomen ondanks
DIRECT_URL-fix. Logt ook hostname (gemaskeerd) + sprint id bij connect,
en pg client errors + end-events voor closed connections.

Wordt gestript in ST-806 voordat de PR uit draft gaat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(debug): add /debug-realtime page + bare SSE endpoint

Tijdelijke debug-tooling voor M8-acceptance op Vercel preview.

- app/api/debug/realtime-stream/route.ts — geen auth, geen filtering;
  dropt elke pg_notify-event op scrum4me_changes rauw door als SSE
- app/debug-realtime/page.tsx — open zonder login op de root, toont
  binnenkomende events in een simpele <table>

Doel: isoleren of de SSE + Postgres LISTEN-pipe op Vercel überhaupt
events laat zien, los van iron-session, productfilter of solo-store.
Als ook deze niets binnen krijgt: probleem zit in pg connection of
Vercel function lifecycle. Als deze wel events toont: probleem zit
hoger in de stack (filter, store, hook).

VERWIJDEREN voordat de PR uit draft gaat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(debug): extend /debug-realtime with stats, emit-button and filters

Bouwt de basale luister-tabel uit met diagnostische tooling om de
SSE+LISTEN-pipe stress-vrij te kunnen valideren.

Toegevoegd:
- POST /api/debug/emit-test-notify — vuurt een handmatige pg_notify
  op scrum4me_changes met een synthetic payload (debug:true) zonder
  een echte DB-UPDATE te doen. Isoleert de SSE-route van Prisma/triggers.
- DebugRealtimeClient: stats-grid (status, reconnects, total events,
  since last event met >30s rood-warning, largest gap, first-event-
  time), emit-button, reset-stats, filters op type en entity
  (incl. "debug only").
- Type/entity kolom in de tabel met kleuring per type.

Geen impact op productie- of solo-flow. Tijdelijke testtooling;
verwijderen wanneer we deze pagina niet meer nodig hebben.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(debug): add Layer 2 — mini Zustand-store + dispatch toggles

Test of SSE-event → store → render-pipeline werkt buiten de Solo
Paneel context. Mirrort het patroon van solo-store maar minimaal.

- debug-store.ts: kleine Zustand-store met tasks + applyEvent +
  applyCount/skipCount-tellers
- store-panel.tsx: rendert store-state in een tabel met statuskleuring
- client.tsx: drie layer-toggles (dispatch / flushSync / startView-
  Transition) + lift dispatch in onmessage. Zo kunnen we elke
  combinatie isoleren

Bevestigd: alle drie de toggles werken op het bare /debug-realtime
endpoint. Volgende laag is Server Action revalidation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(ST-806): cleanup debug-pages, document realtime, restore strict mode

M8 acceptance heeft de end-to-end pipeline bevestigd: trigger → NOTIFY →
SSE → store → View Transition. De cycling-symptomen waren een artefact
van testen via terminal (alt-tab triggert visibility-pause-by-design),
geen bug. Tijd om de tijdelijke instrumentatie en debug-pagina's weg te
halen en de architectuur op te schrijven.

- Verwijder /debug-realtime, /(app)/debug-realtime-app, /api/debug/*
- Strip debug-logs uit /api/realtime/solo (closed-reden alleen in dev)
- reactStrictMode weer aan
- CONNECTING_INDICATOR_DELAY_MS 2s → 4s (minder flikker bij micro-disconnects)
- Nieuwe sectie "Realtime updates (M8)" in scrum4me-architecture.md:
  diagram, NOTIFY-bron, server-filter, connection lifecycle inclusief
  visibility-pause + bekende beperking, animatie, auth
- DIRECT_URL env-rij uitgebreid met realtime-doel
- GET /api/realtime/solo gedocumenteerd in API.md

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-04-27 13:59:32 +02:00
f6132960a5
feat(tooling): add db:insert-milestone for idempotent backlog inserts (#7)
Een herbruikbaar script dat één milestone (PBI + stories + tasks) uit
docs/scrum4me-backlog.md leest en idempotent toevoegt aan de DB,
zonder bestaande data of sprint-toewijzingen te raken.

  npm run db:insert-milestone -- M8                 # standaard SCRUM4ME
  npm run db:insert-milestone -- M8 --product CODE  # ander product
  npm run db:insert-milestone -- M8 --dry-run       # niets schrijven

Parser-uitbreiding (parse-backlog.ts):
- M7 en M8 toegevoegd aan priority/goal/sprint_status maps
- Onbekende milestones krijgen defaults (priority 4, sprint COMPLETED,
  goal = header-titel) zodat M9..M∞ vanzelf werken
- Optionele { strict: false } skipt de "≥ 8 milestones / ≥ 60 stories"
  asserts; de bestaande seed-flow behoudt strict: true (default)

Idempotency:
- Pbi upsert by (product_id, code = milestone.key)
- Story upsert by (product_id, code = ST-XXX)
- Tasks alleen aangemaakt als de story op dit moment 0 tasks heeft

Sprint-toewijzing wordt expliciet niet meegenomen — stories landen in
de backlog en moeten via Sprint Planning in een sprint geplaatst worden.

Geverifieerd: M8 inserted (PBI + 6 stories + 6 tasks), tweede run laat
zien "PBI reused, 0 created" — geen duplicaten.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 01:25:05 +02:00
5dc8033b85 feat(ST-509): rebuild todo list as TanStack Data Table
- @tanstack/react-table voor kolommen, paginering en rij-selectie
- Kolommen: multi-select checkbox, titel (line-clamp-2), productnaam-badge, datum
- Toolbar: product-filter dropdown, bulk-archiveer knop (telt selectie), + knop
- Paginering: 10 rijen per pagina met paginatelling (x–y van n)
- Rij-klik opent detail-kaart (placeholder; volgt in ST-510)
- Promote dialogs behouden voor gebruik in ST-510

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 19:55:36 +02:00
ecc05dda37 chore: bump version to 0.3.1
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 18:47:20 +02:00
8e299915a8 Limit Prisma generation in CI 2026-04-25 17:17:35 +02:00
4ec0683f88 Document Prisma ERD generation 2026-04-25 17:08:55 +02:00
b5e967d8d3 Add analytics and documentation updates 2026-04-25 15:11:51 +02:00
e0efb65efb Fix scoped access checks 2026-04-25 14:36:55 +02:00
b541379964 chore: SQLite verwijderd — alleen PostgreSQL via Neon
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:15:19 +02:00
32fa2e8c1b chore: middleware.ts verwijderd, icon-bron toegevoegd, versie 0.2.0
- middleware.ts verwijderd (hernoemd naar proxy.ts in vorige commit)
- icon-master-light.svg toegevoegd als bronbestand voor app-iconen
- package.json versie bijgewerkt naar 0.2.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 23:05:00 +02:00
f43dcd0f64 chore: postinstall script voor automatische prisma generate 2026-04-24 14:48:56 +02:00
b2e84d043e chore: husky pre-commit hook met lint-staged 2026-04-24 14:14:25 +02:00
d11b114fc1 feat: ST-601-ST-612 M6 polish, beveiliging en launch-ready
- ST-601/602: loading skeletons en error boundary
- ST-603: Sonner toasts op alle CRUD-operaties
- ST-604: DemoTooltip op uitgeschakelde knoppen
- ST-605: KeyboardSensor dnd-kit, Escape sluit modals
- ST-606: min-width banner < 1024px
- ST-607: WCAG AA aria-labels en skip link
- ST-608: rate limiting login (10/min) en registratie (5/uur)
- ST-609: security integratietests cross-user toegang (7 tests)
- ST-610: GitHub Actions CI/CD workflow
- ST-611: README met quickstart, deployment en API-docs
- ST-612: Lars-flow acceptatiechecklist
- fix: settings toont gebruikersnaam i.p.v. interne id
- fix: seed idempotent, testdata altijd gekoppeld aan demo-gebruiker

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 12:36:23 +02:00
7f94bb6359 feat: ST-001–ST-005 foundation — scaffolding, Prisma, schema, seed, env
- ST-001: Next.js 16 + React 19 + TypeScript strict + Tailwind + shadcn/ui + all deps
- ST-002: Prisma v7 setup with better-sqlite3 adapter (local) and pg adapter (cloud)
- ST-003: Full schema migration (users, pbis, stories, sprints, tasks, todos, api_tokens)
- ST-004: Seed with 9 PBIs, ~40 stories, demo user (demo/demo1234), lars user
- ST-005: Zod-validated env vars, .env.example, lib/session, lib/auth, lib/api-auth

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 21:04:48 +02:00
4cf5833c1d Initial commit from Create Next App 2026-04-22 20:25:19 +02:00