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