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) <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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-02 13:09:25 +02:00 committed by GitHub
parent 6c6c8b96b7
commit 55a1ee035c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1241 additions and 469 deletions

View file

@ -0,0 +1,133 @@
# Plan — ST-1114 · Copilot reviews op dashboard
## Context
Als ontwerper wil je een overzicht zien van GitHub Copilot's PR-reviews om per stuk te beslissen of je 'm implementeert of overslaat. De codebase heeft nu **nul** GitHub-integratie — alleen `product.repo_url` als string voor hyperlinks. We bouwen een minimale, hobby-vriendelijke architectuur.
## Architectuurkeuzes (via AskUserQuestion bevestigd)
- **Auth**: lokaal script met `GITHUB_TOKEN` — webapp heeft GEEN GitHub-credentials. Het script draai je lokaal wanneer je wil verversen.
- **Fetch**: on-demand op dashboard-load (server-side `prisma.copilotReview.findMany`, geen externe call)
- **Decision**: alleen visuele toggle in `localStorage` (geen DB-persistentie)
- **Scope**: MVP — tonen + lokale toggle. Geen cron, geen webhook, geen GitHub-auth in productie.
## Architectuur
```
┌──────────────┐ octokit ┌────────────┐ API token ┌─────────────┐
│ scripts/ │ ──────────▶ │ GitHub │ │ Scrum4Me │
│ sync-copilot │ │ REST API │ │ /api/ │
│ -reviews.ts │ ◀────────── │ │ │ copilot- │
└──────────────┘ reviews └────────────┘ POST batch │ reviews │
│ │ │
└──────────────────────────────────────────────────▶ DB upsert │
└──────┬──────┘
┌──────▼──────┐
│ /dashboard │
│ server-side │
│ findMany │
└─────────────┘
```
Het script is de enige plek waar GitHub-credentials nodig zijn. Productie kent alleen Scrum4Me-data.
## Datamodel
```prisma
model CopilotReview {
id String @id @default(cuid())
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
product_id String
pr_number Int
pr_title String
pr_url String
pr_state String // 'open' | 'closed' | 'merged'
author_login String // bv. 'copilot-pull-request-reviewer[bot]'
review_state String // 'COMMENTED' | 'APPROVED' | 'CHANGES_REQUESTED'
body String @db.Text
submitted_at DateTime
html_url String // directe link naar de review
fetched_at DateTime @default(now())
@@unique([product_id, pr_number, submitted_at])
@@index([product_id, submitted_at])
@@map("copilot_reviews")
}
```
`@@unique` zorgt voor idempotency — script kan herhaald draaien zonder dupes. Geen `decision`-veld: dat staat in `localStorage`.
## Script
`scripts/sync-copilot-reviews.ts` — TypeScript via `tsx`, leest env, gebruikt Octokit, POST naar eigen API.
```bash
GITHUB_TOKEN=ghp_... \
SCRUM4ME_API_URL=http://localhost:3000 \
SCRUM4ME_API_TOKEN=s4m_... \
npx tsx scripts/sync-copilot-reviews.ts
```
Stappen:
1. `GET /api/products` (Bearer-auth) — lijst toegankelijke producten met `repo_url`
2. Per product: parse `owner/repo` uit `repo_url`, `octokit.pulls.list({state: 'all', per_page: 50})`
3. Per PR: `octokit.pulls.listReviews(...)`, filter op `user.type === 'Bot' && user.login.includes('copilot')`
4. `POST /api/copilot-reviews` met `{ product_id, reviews: [...] }` — endpoint doet `deleteMany` + `createMany` per product (atomic replace)
5. Print samenvatting: aantal reviews per product + totale runtime
## API endpoint
`app/api/copilot-reviews/route.ts`:
- **POST**: Bearer-auth, demo-block, payload `{ product_id, reviews: CopilotReview[] }`. Atomic transaction: delete-all-for-product → createMany. Validatie via Zod.
- **GET**: niet nodig — dashboard leest direct via Prisma server-side. Endpoint kan komen voor toekomstige clients.
## Dashboard widget
Boven of onder de bestaande product-grid een nieuwe sectie "Copilot reviews".
`components/dashboard/copilot-reviews.tsx` (client component):
- Props: `reviews: CopilotReview[]` (server-fetched)
- Lijst met cards: PR-titel + nummer (link naar PR), Copilot's body (truncated of accordion), state-badge, "Implementeer" / "Skip"-knoppen
- Decision-state in `localStorage` keyed op `review.id`: `'implement' | 'skip' | undefined` (default: ongezien)
- Cards met decision='skip' visueel gedimmed; cards met 'implement' krijgen een groen randje
- Filter-toggles bovenaan: "Alle / Te beoordelen / Implementeren / Skip"
- Empty state: "Geen Copilot-reviews gevonden — draai het sync-script."
`app/(app)/dashboard/page.tsx` past `prisma.copilotReview.findMany({ where: { product_id: { in: accessibleIds } }, orderBy: { submitted_at: 'desc' } })` en geeft door.
## Voorgestelde sub-tasks
| Code | Onderwerp |
|---|---|
| ST-1114.2 | DB: `CopilotReview` model + migration |
| ST-1114.3 | API: `POST /api/copilot-reviews` (Bearer-auth + demo-block + replace-by-product) |
| ST-1114.4 | Script: `scripts/sync-copilot-reviews.ts` met octokit |
| ST-1114.5 | UI: dashboard-widget met cards, localStorage-decision, filter-toggle |
| ST-1114.6 | Tests: API endpoint (auth, demo-block, validation), dashboard-widget snapshot |
| ST-1114.7 | Docs: README-sectie over script + env-vars; CLAUDE.md-update |
## M11-keuzes voor de implementerende sessie
Drie open beslissingen die niet kritiek zijn voor het plan zelf:
1. **PR-state filter**: alle PR's of alleen `state=open`? (closed-PRs hebben oude reviews die misschien niet meer relevant zijn)
2. **Markdown-rendering**: react-markdown, of plain `<pre>`? (react-markdown is +35KB bundle)
3. **localStorage-key-vorm**: `scrum4me:copilot-decision:{review_id}` per review, of één map-object onder één key?
## Branch + PR
- Branch: `feat/M14-copilot-reviews` (M14 = nieuwe milestone)
- 6 commits (.2 t/m .7), één per laag
- PR pas openen na handmatige test door gebruiker
## Verificatie (end-to-end)
1. `npm run dev`
2. `GITHUB_TOKEN=... SCRUM4ME_API_TOKEN=... npx tsx scripts/sync-copilot-reviews.ts` — toont `n reviews opgeslagen`
3. Browser refresht dashboard → "Copilot reviews"-sectie toont cards met PR-titels
4. Klik "Implementeer" → kaart krijgt groen randje, decision in localStorage
5. Refresh → state blijft (localStorage)
6. Filter toggle "Alleen te beoordelen" → cards met decision verdwijnen
7. Demo-user: kan reviews zien, maar `POST /api/copilot-reviews` weigert (al via middleware-guard van ST-1110)