--- title: "ST-1114 — Copilot reviews op dashboard" status: active audience: [maintainer, contributor] language: nl last_updated: 2026-05-03 applies_to: [ST-1114] --- # 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 `
`? (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)