docs(cleanup): archief verouderde plannen, backlog en root-duplicaten
- 6 plans naar docs/old/plans/ (PBI-11/75/78, user-settings-store, Local github setup, lees-de-readme — laatste was verkeerde repo)
- docs/backlog/ naar docs/old/backlog/ (pre-MCP statische registry; live werk loopt via Scrum4Me-MCP)
- 6 root-level duplicaten naar docs/old/ (functional, {pbi,story,task}-dialog, product-backlog, backlog)
- 2 landing plans (niet uitgevoerd) krijgen archived: true frontmatter — blijven op plek maar uit INDEX
- scripts/generate-docs-index.mjs: skip docs/old/** + skip archived: true
- CLAUDE.md: rijen docs/backlog/, docs/plans/<key>-*.md, docs/manual/ weg; Track B-sectie verwijderd
- README.md / CHANGELOG.md / docs/plans/v1-readiness.md: link-fixes naar nieuwe locaties
Verify groen (lint + typecheck + 718 tests). docs/INDEX.md geregenereerd.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bf7162a5fc
commit
0a265b96eb
23 changed files with 11 additions and 29 deletions
371
docs/old/plans/Local github setup.md
Normal file
371
docs/old/plans/Local github setup.md
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
# Advies — Zelf een Git-platform hosten naast of in plaats van GitHub
|
||||
|
||||
## Situatie
|
||||
|
||||
Je wilt onderzoeken of je lokaal of op een eigen server een Git repository-platform kunt draaien vergelijkbaar met GitHub.
|
||||
|
||||
In jouw situatie spelen mee:
|
||||
|
||||
- Next.js/Vercel apps
|
||||
- AI-workers / automation
|
||||
- batch processing
|
||||
- deploy pipelines
|
||||
- private code
|
||||
- mogelijk draaien op NAS of VPS
|
||||
- integratie met Claude Code / Codex / agents
|
||||
|
||||
Het antwoord is: ja, dit kan uitstekend.
|
||||
|
||||
---
|
||||
|
||||
# Architectuur-opties
|
||||
|
||||
## Optie 1 — Alleen een centrale Git remote
|
||||
|
||||
De lichtste oplossing.
|
||||
|
||||
Je draait alleen een zogenaamde "bare repo" op een Linux server.
|
||||
|
||||
### Voordelen
|
||||
|
||||
- extreem simpel
|
||||
- weinig resources
|
||||
- volledige controle
|
||||
- SSH push/pull
|
||||
|
||||
### Nadelen
|
||||
|
||||
- geen webinterface
|
||||
- geen PR’s
|
||||
- geen issues
|
||||
- geen gebruikersbeheer
|
||||
- geen CI/CD UI
|
||||
|
||||
### Setup
|
||||
|
||||
Server:
|
||||
|
||||
```bash
|
||||
mkdir -p /srv/git/myapp.git
|
||||
cd /srv/git/myapp.git
|
||||
git init --bare
|
||||
```
|
||||
|
||||
Client:
|
||||
|
||||
```bash
|
||||
git remote add origin ssh://user@server:/srv/git/myapp.git
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Optie 2 — Self-hosted GitHub alternatief
|
||||
|
||||
Dit is meestal de beste keuze.
|
||||
|
||||
Software opties:
|
||||
|
||||
| Software | Omschrijving |
|
||||
|---|---|
|
||||
| Gitea | Lichtgewicht GitHub alternatief |
|
||||
| Forgejo | Community fork van Gitea |
|
||||
| GitLab | Zeer compleet maar zwaar |
|
||||
| OneDev | Moderne alles-in-één oplossing |
|
||||
|
||||
---
|
||||
|
||||
# Aanbevolen keuze: Gitea
|
||||
|
||||
## Waarom
|
||||
|
||||
Voor jouw situatie is Gitea waarschijnlijk de beste balans tussen:
|
||||
|
||||
- eenvoud
|
||||
- performance
|
||||
- features
|
||||
- beheerlast
|
||||
|
||||
Je krijgt:
|
||||
|
||||
- Git hosting
|
||||
- web UI
|
||||
- pull requests
|
||||
- issues
|
||||
- SSH support
|
||||
- webhooks
|
||||
- CI integratie
|
||||
- Docker support
|
||||
- private repos
|
||||
- multi-user support
|
||||
|
||||
---
|
||||
|
||||
# Aanbevolen architectuur voor jouw setup
|
||||
|
||||
## Huidige richting
|
||||
|
||||
```text
|
||||
MacBook
|
||||
↓
|
||||
GitHub
|
||||
↓
|
||||
Vercel deploy
|
||||
```
|
||||
|
||||
## Uitgebreide AI workflow
|
||||
|
||||
```text
|
||||
MacBook
|
||||
↓
|
||||
Gitea / GitHub
|
||||
↓ webhook
|
||||
AI Worker Server
|
||||
↓
|
||||
Repo clone
|
||||
↓
|
||||
Code generatie
|
||||
↓
|
||||
Commit + push
|
||||
↓
|
||||
PR creation
|
||||
↓
|
||||
Merge
|
||||
↓
|
||||
Vercel deploy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Beste strategie voor jouw situatie
|
||||
|
||||
## Advies: hybride model
|
||||
|
||||
Gebruik:
|
||||
|
||||
| Component | Platform |
|
||||
|---|---|
|
||||
| publieke repos | GitHub |
|
||||
| deploys | Vercel |
|
||||
| AI worker orchestration | eigen server |
|
||||
| interne experimenten | Gitea |
|
||||
| automation | self-hosted |
|
||||
|
||||
Waarom:
|
||||
|
||||
- GitHub ecosystem blijft beschikbaar
|
||||
- recruiters herkennen GitHub
|
||||
- Copilot integratie blijft optimaal
|
||||
- minder beheer
|
||||
- sneller stabiel
|
||||
|
||||
---
|
||||
|
||||
# Wanneer volledig self-hosted interessant wordt
|
||||
|
||||
Volledig self-hosted wordt interessant als:
|
||||
|
||||
- privacy belangrijk is
|
||||
- AI agents autonoom moeten kunnen werken
|
||||
- je volledige controle wilt
|
||||
- je GitHub limieten wilt vermijden
|
||||
- je meerdere workers wilt draaien
|
||||
|
||||
Dan bouw je:
|
||||
|
||||
```text
|
||||
Gitea
|
||||
+ Postgres
|
||||
+ Docker Registry
|
||||
+ CI Runners
|
||||
+ Reverse Proxy
|
||||
+ Backups
|
||||
+ Monitoring
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Aanbevolen infrastructuur
|
||||
|
||||
## Lichtgewicht setup
|
||||
|
||||
### Hardware
|
||||
|
||||
- Synology NAS of mini-PC
|
||||
- 8–16 GB RAM
|
||||
- SSD opslag
|
||||
|
||||
### Software stack
|
||||
|
||||
| Component | Advies |
|
||||
|---|---|
|
||||
| OS | Ubuntu Server |
|
||||
| Containers | Docker Compose |
|
||||
| Git platform | Gitea |
|
||||
| Reverse proxy | Traefik |
|
||||
| Database | Postgres |
|
||||
| SSL | Let's Encrypt |
|
||||
| Deploys | Vercel |
|
||||
|
||||
---
|
||||
|
||||
# Docker Compose voorbeeld
|
||||
|
||||
```yaml
|
||||
services:
|
||||
gitea:
|
||||
image: gitea/gitea:latest
|
||||
container_name: gitea
|
||||
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "222:22"
|
||||
|
||||
volumes:
|
||||
- ./gitea:/data
|
||||
|
||||
restart: always
|
||||
```
|
||||
|
||||
Starten:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Daarna bereikbaar via:
|
||||
|
||||
```text
|
||||
http://server-ip:3000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Belangrijke aandachtspunten
|
||||
|
||||
## Backups
|
||||
|
||||
Bij self-hosting moet je zelf regelen:
|
||||
|
||||
- database backups
|
||||
- repo backups
|
||||
- disaster recovery
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
Je bent zelf verantwoordelijk voor:
|
||||
|
||||
- updates
|
||||
- SSH security
|
||||
- firewall
|
||||
- SSL certificaten
|
||||
- gebruikersbeheer
|
||||
|
||||
---
|
||||
|
||||
## CI/CD
|
||||
|
||||
GitHub Actions vervang je mogelijk door:
|
||||
|
||||
- Gitea Actions
|
||||
- Drone CI
|
||||
- Woodpecker CI
|
||||
- self-hosted runners
|
||||
|
||||
---
|
||||
|
||||
# Integratie met jouw AI-worker ideeën
|
||||
|
||||
Dit sluit zeer goed aan op jouw eerdere ideeën:
|
||||
|
||||
- Neon database events
|
||||
- worker servers
|
||||
- auto-generated PR’s
|
||||
- selective deploys
|
||||
- batch execution
|
||||
|
||||
Je kunt bijvoorbeeld:
|
||||
|
||||
1. story wordt aangemaakt
|
||||
2. worker krijgt event via SSE/webhook
|
||||
3. repo wordt gecloned
|
||||
4. AI implementeert wijziging
|
||||
5. commit + push
|
||||
6. PR automatisch aangemaakt
|
||||
7. review pipeline start
|
||||
8. merge → deploy
|
||||
|
||||
Dit wordt veel eenvoudiger wanneer je volledige controle hebt over de Git infrastructuur.
|
||||
|
||||
---
|
||||
|
||||
# Concrete roadmap
|
||||
|
||||
## Fase 1 — huidige setup stabiliseren
|
||||
|
||||
Hou:
|
||||
|
||||
- GitHub
|
||||
- Vercel
|
||||
- Neon
|
||||
|
||||
Voeg toe:
|
||||
|
||||
- AI worker server
|
||||
- webhooks
|
||||
- automation pipeline
|
||||
|
||||
---
|
||||
|
||||
## Fase 2 — interne Git infrastructuur
|
||||
|
||||
Installeer:
|
||||
|
||||
- Gitea
|
||||
- Docker
|
||||
- Postgres
|
||||
|
||||
Gebruik dit voor:
|
||||
|
||||
- experimenten
|
||||
- AI-generated branches
|
||||
- interne repos
|
||||
- automation testing
|
||||
|
||||
---
|
||||
|
||||
## Fase 3 — geavanceerde automation
|
||||
|
||||
Later toevoegen:
|
||||
|
||||
- self-hosted runners
|
||||
- preview environments
|
||||
- deploy approvals
|
||||
- selective deployments
|
||||
- agent orchestration
|
||||
|
||||
---
|
||||
|
||||
# Eindadvies
|
||||
|
||||
Voor jouw situatie:
|
||||
|
||||
## Niet meteen GitHub vervangen
|
||||
|
||||
Dat levert nu vooral extra beheerlast op.
|
||||
|
||||
## Wel nu al beginnen met:
|
||||
|
||||
- eigen AI worker server
|
||||
- webhook automation
|
||||
- lokale Git orchestration
|
||||
- Gitea testomgeving
|
||||
|
||||
Dat sluit perfect aan op:
|
||||
|
||||
- Scrum4Me
|
||||
- AI-assisted development
|
||||
- batch story execution
|
||||
- autonome pipelines
|
||||
198
docs/old/plans/PBI-11-mobile-shell.md
Normal file
198
docs/old/plans/PBI-11-mobile-shell.md
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
# PBI-11 — Mobile-shell met landscape-lock (settings + backlog + solo)
|
||||
|
||||
> **Status:** READY · priority 3 · sort_order 8
|
||||
> **Stories:** ST-1133 (TaskDialog full-screen) · ST-1134 (foundation) · ST-1135 (UA-redirect) · ST-1136 (settings) · ST-1137 (backlog) · ST-1138 (solo) · ST-1139 (docs + E2E)
|
||||
|
||||
## Doel
|
||||
|
||||
Scrum4Me bruikbaar maken op een mobiele telefoon, beperkt tot drie schermen — Settings (account + product-selector + QR-pairing-instructie + logout), Product Backlog (PBI/Story/Task aanmaken), Solo Paneel (voortgang vastleggen). Landscape-orientatie afgedwongen via PWA-manifest + CSS-overlay. App-naam en -icoon onderdrukken op `/m/*`. Desktop-app blijft ongewijzigd.
|
||||
|
||||
## Drie architectuur-beslissingen
|
||||
|
||||
### Beslissing A — gedeelde dialog-classes (raakt ST-1133 + ST-1138)
|
||||
|
||||
Alle entity-dialogen (PbiDialog, StoryDialog, TaskDialog, TaskDetailDialog) delen dezelfde class-string in [components/shared/entity-dialog-layout.ts](../../components/shared/entity-dialog-layout.ts):
|
||||
|
||||
```ts
|
||||
export const entityDialogContentClasses = cn(
|
||||
'flex flex-col p-0 gap-0',
|
||||
'max-h-[90vh] w-full max-w-[calc(100%-2rem)]',
|
||||
'sm:max-w-[90vw] sm:max-h-[85vh]',
|
||||
'lg:max-w-[50vw] lg:min-w-[480px]',
|
||||
)
|
||||
```
|
||||
|
||||
→ Mobile-fullscreen wordt via één edit op deze constant geregeld:
|
||||
|
||||
```ts
|
||||
'max-sm:w-screen max-sm:h-screen max-sm:max-w-none max-sm:rounded-none'
|
||||
```
|
||||
|
||||
**Gevolg voor stories:**
|
||||
- ST-1133 T-317 muteert `entity-dialog-layout.ts`, niet `task-dialog.tsx` rechtstreeks
|
||||
- ST-1138 T-332 vervalt als file-edit — wordt verify-only (controleer dat TaskDetailDialog mee-erft)
|
||||
- PBI/Story-dialogen krijgen mobile-fullscreen "voor niets" (handig voor ST-1137)
|
||||
|
||||
### Beslissing B — eigen route group `app/(mobile)/`
|
||||
|
||||
Parent layout `app/(app)/layout.tsx` rendert NavBar, MinWidthBanner, StatusBar, SoloRealtimeBridge, NotificationsBridge. Een nested layout in `(app)/m/` kan deze parent-output **niet** verwijderen (Next.js layouts erven naar binnen, niet vervangen).
|
||||
|
||||
**Keuze:** verplaats `/m/*` naar een eigen route group `app/(mobile)/m/{settings,pair,products}/...` met eigen `app/(mobile)/layout.tsx`.
|
||||
|
||||
**Auth-guard duplicatie voorkomen** door `getSession()`-check te extraheren naar `lib/auth-guard.ts`:
|
||||
|
||||
```ts
|
||||
// lib/auth-guard.ts
|
||||
export async function requireSession() {
|
||||
const session = await getSession()
|
||||
if (!session.userId) redirect('/login')
|
||||
return session
|
||||
}
|
||||
```
|
||||
|
||||
Beide layouts (`(app)/layout.tsx` en `(mobile)/layout.tsx`) roepen deze helper aan. Bestaande `/m/pair/page.tsx` (M10 QR-pairing) verhuist mee naar `app/(mobile)/m/pair/page.tsx` — geen URL-wijziging, alleen filesystem-move.
|
||||
|
||||
**Gevolg voor stories:**
|
||||
- ST-1134 T-321 schrijft `app/(mobile)/layout.tsx`, niet `app/(app)/m/layout.tsx`
|
||||
- ST-1136 page wordt `app/(mobile)/m/settings/page.tsx`
|
||||
- ST-1137 page wordt `app/(mobile)/m/products/[id]/page.tsx`
|
||||
- ST-1138 page wordt `app/(mobile)/m/products/[id]/solo/page.tsx`
|
||||
- M10's `/m/pair` verhuist naar `app/(mobile)/m/pair/` — URL ongewijzigd, geen redirect-migratie nodig
|
||||
|
||||
### Beslissing C — gescheiden SplitPane cookie-key
|
||||
|
||||
ST-1137 hergebruikt `BacklogSplitPane` (drie panelen). Op mobile rendert die in tab-mode (auto-switch + back-button uit ST-1116). De SplitPane bewaart split-percentages in een cookie.
|
||||
|
||||
**Keuze:** gescheiden cookie-key voor mobile — `split-pane:backlog-3-mobile:<id>` — zodat mobile-gebruikers (die in tab-mode geen split-percentages bewerken maar wel terug kunnen schakelen) de desktop-split niet beïnvloeden.
|
||||
|
||||
**Gevolg voor stories:**
|
||||
- ST-1137 T-328 geeft expliciete `cookieKey`-prop aan `BacklogSplitPane` op de mobile-route
|
||||
|
||||
## Hergebruik (al aanwezig)
|
||||
|
||||
| Wat | Bron |
|
||||
|---|---|
|
||||
| Mobile tab-mode in `SplitPane` (incl. `tabLabels`, `mobileBreakpoint`, `activeTab`) | ST-1116 — [components/split-pane/split-pane.tsx](../../components/split-pane/split-pane.tsx) |
|
||||
| Click-cascade auto-switch in `BacklogSplitPane` | ST-1116 commit `3e86a8d` |
|
||||
| QR-pairing route `/m/pair` | M10 commit `625221f` |
|
||||
| `/m/pair` confirmation page | bestaand |
|
||||
| Functional-spec mobile-tabs sectie | `docs/specs/functional.md:234-235` |
|
||||
|
||||
## Stories
|
||||
|
||||
### ST-1133 — TaskDialog full-screen op mobile (verifieer en fix)
|
||||
|
||||
**Doel:** entity-dialogen renderen 100vw × 100vh op viewport `<640px`.
|
||||
|
||||
**Acceptance:**
|
||||
- `entityDialogContentClasses` in `components/shared/entity-dialog-layout.ts` bevat `max-sm:w-screen max-sm:h-screen max-sm:max-w-none max-sm:rounded-none`
|
||||
- Sticky header en footer blijven bereikbaar; body scrollt
|
||||
- Werkt voor TaskDialog, TaskDetailDialog, PbiDialog, StoryDialog (alle gebruiken de constant)
|
||||
- Tests dekken mobile-render via `window.innerWidth`-mock voor minstens TaskDialog en TaskDetailDialog
|
||||
- Geen regressie op desktop (`sm:max-w-[90vw]` blijft op `>=640px`)
|
||||
|
||||
**Tasks:**
|
||||
- T-316 inventariseer huidige render
|
||||
- T-317 fix de gedeelde constant
|
||||
- T-318 tests
|
||||
|
||||
### ST-1134 — Mobile shell foundation (route group + landscape-guard + tab-bar + manifest)
|
||||
|
||||
**Doel:** route group `(mobile)`, landscape-overlay, bottom tab-bar, PWA-manifest.
|
||||
|
||||
**Acceptance:**
|
||||
- `app/(mobile)/layout.tsx` rendert zonder NavBar / AppIcon / MinWidthBanner / StatusBar
|
||||
- Auth-guard via gedeelde `lib/auth-guard.ts` helper; `(app)/layout.tsx` gebruikt dezelfde helper
|
||||
- `<LandscapeGuard>` toont rotate-overlay in portrait (window.matchMedia)
|
||||
- `<MobileTabBar>` bottom-fixed met 3 lucide-iconen (ListTree, Activity, Settings); tap-targets ≥44×44 px
|
||||
- `public/manifest.json` bevat `"orientation": "landscape"`
|
||||
- M10 `/m/pair` verhuist filesystem-only naar `app/(mobile)/m/pair/` — URL onveranderd
|
||||
- Tests: LandscapeGuard render-states, TabBar route-active, auth-guard helper
|
||||
|
||||
**Tasks:**
|
||||
- T-319 LandscapeGuard
|
||||
- T-320 MobileTabBar
|
||||
- T-321 `(mobile)/layout.tsx` + manifest + auth-guard extractie + filesystem-move van `/m/pair`
|
||||
|
||||
### ST-1135 — Mobile UA-redirect bij login
|
||||
|
||||
**Acceptance:**
|
||||
- `lib/user-agent.ts` exporteert `isPhoneUA(ua: string | null): boolean` op basis van `Mobi`-substring
|
||||
- `actions/auth.ts` `loginAction` redirect bij phone-UA naar `/m/products/[active]/solo`; zonder actief product naar `/m/settings`
|
||||
- Tablet-UA en desktop-UA blijven op `/dashboard`
|
||||
- Demo-user volgt zelfde routing
|
||||
- Tests dekken alle paden (phone met/zonder product, tablet, desktop, null UA, demo)
|
||||
|
||||
**Tasks:** T-322 helper · T-323 loginAction integratie · T-324 tests
|
||||
|
||||
### ST-1136 — Mobile Settings-pagina
|
||||
|
||||
**Acceptance:**
|
||||
- `app/(mobile)/m/settings/page.tsx`
|
||||
- Toont username, isDemo-badge, actief-product-naam
|
||||
- Product-selector — klik → `setActiveProductAction` + redirect `/m/products/[id]/solo`
|
||||
- QR-pairing-instructie — link "Open scrum4me.app/login op je desktop om in te loggen via QR"
|
||||
- Logout-knop met AlertDialog "Uitloggen?" → `logoutAction`
|
||||
- Geen avatar-upload, geen bio-edit
|
||||
- Tests render-states + logout-flow
|
||||
|
||||
**Tasks:** T-325 layout · T-326 logout-flow · T-327 tests
|
||||
|
||||
### ST-1137 — Mobile Product Backlog-pagina
|
||||
|
||||
**Acceptance:**
|
||||
- `app/(mobile)/m/products/[id]/page.tsx` hergebruikt PbiList/StoryPanel/TaskPanel + backlog-store
|
||||
- `BacklogSplitPane` rendert in tab-mode op `<1024px`; auto-switch op selectie blijft werken
|
||||
- TaskDialog-searchParams wiring (`?newTask=`, `?editTask=`, `?storyId=`) werkt
|
||||
- Cookie-key gescheiden: `split-pane:backlog-3-mobile:<id>`
|
||||
- + knoppen voor PBI/Story/Task werken; demo blijft read-only
|
||||
- Tests: page-rendering met initial state, tab-mode, click-cascade-flow
|
||||
|
||||
**Tasks:** T-328 page wrapper + cookie-key · T-329 TaskDialog wiring · T-330 tests
|
||||
|
||||
### ST-1138 — Mobile Solo Paneel
|
||||
|
||||
**Acceptance:**
|
||||
- `app/(mobile)/m/products/[id]/solo/page.tsx` hergebruikt SoloBoard
|
||||
- 3 kanban-kolommen blijven; horizontal scroll
|
||||
- TaskDetailDialog rendert 100vw × 100vh op `<640px` — **gedekt door beslissing A** (entityDialogContentClasses)
|
||||
- "Voer uit"-knop bereikbaar
|
||||
- SSE-stream blijft werken
|
||||
- Tests: solo-page rendert, TaskDetailDialog erft mobile-classes (zonder eigen file-edit)
|
||||
|
||||
**Tasks:**
|
||||
- T-331 page wrapper
|
||||
- T-332 verify-only (geen file-edit; controleer dat shared constant uit ST-1133 doorwerkt)
|
||||
- T-333 tests
|
||||
|
||||
### ST-1139 — Docs sync + end-to-end verificatie
|
||||
|
||||
**Acceptance:**
|
||||
- `docs/specs/functional.md` heeft "Mobile shell"-sectie; desktop-first-clausule herzien
|
||||
- `docs/architecture.md` beschrijft route group `(mobile)`, manifest landscape, UA-redirect, gedeelde auth-guard
|
||||
- `npm run lint && npm test && npm run build` slagen
|
||||
- E2E checklist (11 punten — zie hieronder)
|
||||
- Bekende limiet: iOS Safari PWA-orientation-lock werkt niet 100% — CSS-overlay als fallback
|
||||
|
||||
**Tasks:** T-334 functional-spec · T-335 architecture-doc · T-336 E2E-verificatie
|
||||
|
||||
## Verificatie (E2E checklist uit T-336)
|
||||
|
||||
1. `npm run lint && npm test && npm run build` slagen
|
||||
2. DevTools mobile-emulatie iPhone 12 landscape: `/m/products/[id]` rendert tab-mode, geen NavBar, tab-bar onderaan
|
||||
3. Portrait → rotate-overlay zichtbaar; landscape → overlay verdwijnt
|
||||
4. Tab-bar 3 iconen werken (Backlog/Solo/Settings)
|
||||
5. Login phone-UA → redirect `/m/products/[id]/solo`; desktop-UA → `/dashboard`
|
||||
6. Backlog-flow: + PBI, + Story, + Task in TaskDialog
|
||||
7. Solo-flow: tap task → TaskDetailDialog full-screen, "Voer uit"-knop bereikbaar
|
||||
8. TaskDialog full-screen op `<640px` (via shared constant)
|
||||
9. PWA-installatie test op echte mobile (Android of iOS)
|
||||
10. `/m/pair` QR-flow intact na route-group-verhuizing
|
||||
11. Demo op mobile read-only; logout via `/m/settings` werkt; geen Scrum4Me-tekst of AppIcon op `/m/*`
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Tablets (geen Mobi-UA) blijven desktop-flow gebruiken
|
||||
- iOS PWA full-orientation-lock (CSS-overlay is fallback)
|
||||
- Avatar/bio editor op mobile-settings
|
||||
- 1-koloms-kanban (3-koloms blijft, swipe horizontaal)
|
||||
128
docs/old/plans/PBI-75-sprint-task-edit-store.md
Normal file
128
docs/old/plans/PBI-75-sprint-task-edit-store.md
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
# PBI-75 — Sprint task-edit client-side via workspace-store
|
||||
|
||||
## Context
|
||||
|
||||
In het Sprint-scherm (`/products/<id>/sprint/<sprintId>`) duurt het bewerken van een taak onevenredig lang. Klik op een taakregel of het potlood-icoon roept `router.push(?editTask=<id>)` aan vanuit [`components/sprint/task-list.tsx:226`](../../components/sprint/task-list.tsx). Dat triggert:
|
||||
|
||||
- **Volledige server re-render** van [`app/(app)/products/[id]/sprint/[sprintId]/page.tsx`](../../app/(app)/products/[id]/sprint/[sprintId]/page.tsx) met zware queries (sprint, alle sprintStories+tasks, productMembers, alle PBIs+stories voor het backlog-paneel)
|
||||
- **Tweede Prisma-query** in [`EditTaskLoader`](../../app/_components/tasks/edit-task-loader.tsx) voor task-detail (incl. `implementation_plan`)
|
||||
- **Na save**: `router.push(closePath)` + `revalidatePath` → opnieuw alle queries
|
||||
|
||||
De [`sprint-workspace-store`](../../stores/sprint-workspace/store.ts) (sinds PBI-74 Story 9) bevat al alles voor een client-side edit-flow:
|
||||
|
||||
- [`setActiveTask(taskId)`](../../stores/sprint-workspace/store.ts) (regel 337) — zet `context.activeTaskId` + roept `ensureTaskLoaded()` aan
|
||||
- [`ensureTaskLoaded(taskId)`](../../stores/sprint-workspace/store.ts) (regel 414) — `GET /api/tasks/{id}` → upsert met `_detail: true`
|
||||
- [`selectActiveTask`](../../stores/sprint-workspace/selectors.ts) (regel 91) — bestaat, nog geen consumer
|
||||
- [`applyTaskEvent`](../../stores/sprint-workspace/store.ts) (regel 748) — SSE-events propageren idempotent na server-save
|
||||
- Optimistic-mutation primitives (`applyOptimisticMutation` / `settleMutation` / `rollbackMutation`)
|
||||
|
||||
Het patroon "URL-param → store" bestaat al voor product-workspace in [`components/backlog/url-task-sync.tsx`](../../components/backlog/url-task-sync.tsx) — we volgen dat als precedent.
|
||||
|
||||
**Doel**: klik op een taak opent de edit-dialoog client-side via store-state binnen ~100ms. Geen URL-navigatie, geen server re-render, alleen `GET /api/tasks/{id}` voor het detail.
|
||||
|
||||
## Aanpak
|
||||
|
||||
**Architectuur**: store-mounted dialog + URL-sync component voor deeplinks.
|
||||
|
||||
1. **Klik-flow**: `TaskList.openEditDialog` roept `setActiveTask(taskId)` aan op de store. Geen `router.push`.
|
||||
2. **Render-flow**: nieuwe client-component `SprintTaskDialogMount` zit binnen `SprintHydrationWrapper`, subscribet `selectActiveTask`, en rendert `<TaskDialog>` zodra de active task `_detail === true` is.
|
||||
3. **Save-flow**: `TaskDialog` krijgt optionele `onClose`/`onSaved` callbacks (backwards compatible met bestaande `closePath`). Mount geeft `onClose = () => setActiveTask(null)`. Server action `saveTask` blijft `revalidatePath` doen voor server-cache; SSE-event update store via `applyTaskEvent`.
|
||||
4. **Deeplink-flow**: nieuwe `SprintUrlTaskSync` leest `?editTask=<id>` en roept `setActiveTask(id)` aan (analoog aan `url-task-sync.tsx`).
|
||||
|
||||
## Bestanden + wijzigingen
|
||||
|
||||
### Nieuw — `components/sprint/sprint-task-dialog-mount.tsx`
|
||||
Client component. Subscribet `selectActiveTask` (single-value, geen `useShallow`). Wanneer task aanwezig is en `isDetail(task)` true, mappt naar `TaskDialogTask`-shape:
|
||||
- `status`: via `taskStatusFromApi` uit [`lib/task-status.ts`](../../lib/task-status.ts) (lowercase API → Prisma UPPER_SNAKE)
|
||||
- `implementation_plan: task.implementation_plan ?? null`
|
||||
- `created_at: new Date(task.created_at)`
|
||||
|
||||
Rendert `<TaskDialog task={mapped} productId={productId} onClose={() => setActiveTask(null)} isDemo={isDemo} />`. Geen render tussen `setActiveTask` en `_detail: true` (detail-fetch <100ms).
|
||||
|
||||
### Nieuw — `components/sprint/sprint-url-task-sync.tsx`
|
||||
Kopie van [`components/backlog/url-task-sync.tsx`](../../components/backlog/url-task-sync.tsx) maar tegen `useSprintWorkspaceStore` en `writeTaskHint` uit [`stores/sprint-workspace/restore`](../../stores/sprint-workspace/restore.ts).
|
||||
|
||||
### Wijziging — `components/sprint/task-list.tsx` (regels 225-227)
|
||||
Vervang:
|
||||
```ts
|
||||
function openEditDialog(taskId: string) {
|
||||
router.push(`${pathname}?editTask=${taskId}`)
|
||||
}
|
||||
```
|
||||
door:
|
||||
```ts
|
||||
function openEditDialog(taskId: string) {
|
||||
useSprintWorkspaceStore.getState().setActiveTask(taskId)
|
||||
}
|
||||
```
|
||||
`openCreateDialog` (regel 222) blijft URL-gebaseerd — out-of-scope.
|
||||
|
||||
### Wijziging — `app/(app)/products/[id]/sprint/[sprintId]/page.tsx`
|
||||
- Verwijder `editTask` uit searchParams-destructuring (regel 36)
|
||||
- Verwijder `editTask &&`-block met `<Suspense><EditTaskLoader>` (regels 250-260)
|
||||
- Verwijder ongebruikte imports (`EditTaskLoader`, `TaskDialogSkeleton`, evt. `Suspense`)
|
||||
- Mount binnen `SprintHydrationWrapper`:
|
||||
```tsx
|
||||
<SprintHydrationWrapper ...>
|
||||
<SprintBoardClient ... />
|
||||
<SprintTaskDialogMount productId={id} isDemo={isDemo} />
|
||||
<SprintUrlTaskSync />
|
||||
</SprintHydrationWrapper>
|
||||
```
|
||||
- `newTask`-block (regels 241-248) blijft ongemoeid — out-of-scope.
|
||||
|
||||
### Wijziging — `app/_components/tasks/task-dialog.tsx`
|
||||
Maak `closePath` optioneel + voeg `onClose`/`onSaved` toe (backwards compatible):
|
||||
```ts
|
||||
interface TaskDialogProps {
|
||||
task?: TaskDialogTask
|
||||
storyId?: string
|
||||
productId: string
|
||||
closePath?: string
|
||||
onClose?: () => void
|
||||
onSaved?: (taskId: string) => void
|
||||
isDemo?: boolean
|
||||
}
|
||||
```
|
||||
Refactor de drie `router.push(closePath)`-calls (regels 104, 120, 155) naar één helper:
|
||||
```ts
|
||||
function close() {
|
||||
if (onClose) { onClose(); return }
|
||||
if (closePath) router.push(closePath)
|
||||
}
|
||||
```
|
||||
Bestaande callers (`EditTaskLoader`, mobile, product-page, sprint `newTask`-block) blijven werken via `closePath`. Nieuwe `SprintTaskDialogMount` gebruikt `onClose`.
|
||||
|
||||
### Geen wijziging
|
||||
- `stores/sprint-workspace/selectors.ts` — `selectActiveTask` bestaat al
|
||||
- `app/_components/tasks/edit-task-loader.tsx` — nog gebruikt door product-page en mobile
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Status-enum mapping**: store API-lowercase → Prisma UPPER_SNAKE via `taskStatusFromApi`, fallback `'TO_DO'`
|
||||
- **`_detail: true` race**: mount rendert pas wanneer `isDetail(task)` true is — geen flash met undefined velden
|
||||
- **Demo-mode**: prop blijft via server doorlopen, dialog respecteert al `isDemo`
|
||||
- **Dirty-close-guard**: ingebouwd in dialog (regels 107, 172) — werkt via `onClose`
|
||||
- **SSE na save**: `applyTaskEvent` updatet store automatisch
|
||||
- **Deeplink + task niet bestaat**: `GET /api/tasks/{id}` 404 → store doet niets, dialog opent niet (huidige `redirect()` verdwijnt — acceptabel)
|
||||
|
||||
## Verificatie
|
||||
|
||||
1. **Browser** (`npm run dev`): klik op task in takenlijst → dialog opent <100ms, geen URL-verandering, alleen `GET /api/tasks/<id>` in Network
|
||||
2. **Save**: wijzig titel → Opslaan → dialog sluit → store toont nieuwe titel via SSE
|
||||
3. **Deeplink**: `?editTask=<id>` → dialog opent via `SprintUrlTaskSync`
|
||||
4. **Bestaande flows ongebroken**: product-page edit, mobile edit, sprint `?newTask=1`
|
||||
5. **`npm run verify && npm run build`**
|
||||
6. **Vitest**: `__tests__/components/sprint/sprint-task-dialog-mount.test.tsx` — hydreer store, mock fetch, `setActiveTask(id)`, assert UPPER_SNAKE status + `onClose` clear
|
||||
|
||||
## Risico's
|
||||
|
||||
- Andere mounts (mobile, product-backlog, sprint `newTask`) blijven URL-gebaseerd — `closePath?` optional houdt ze werkend
|
||||
- Geen `redirect()` bij not-found-deeplink (klein UX-verschil)
|
||||
- SSE-latency 100-500ms na save — eventueel later mitigeren via `applyOptimisticMutation` in `onSaved`-callback
|
||||
|
||||
## Out-of-scope (follow-up PBIs)
|
||||
|
||||
- `?newTask=1`-flow naar store
|
||||
- Mobile + product-backlog mounts
|
||||
- `EditTaskLoader` verwijderen wanneer alle callers over zijn
|
||||
186
docs/old/plans/PBI-78-cost-analysis-widget.md
Normal file
186
docs/old/plans/PBI-78-cost-analysis-widget.md
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
# PBI-78 — Cost-analyse widget op Insights-pagina
|
||||
|
||||
## Context
|
||||
|
||||
De insights-pagina heeft al een `TokenUsageCard` die KPI's + per-job tabel toont, maar **alleen voor de actieve sprint van een gefilterd product**. Daardoor mis je het globale plaatje: hoeveel geef je deze maand uit, welk model is de grootste kostenpost, hoe goed werkt prompt-caching, en welke job-kinds (IDEA_MAKE_PLAN met Opus vs TASK_IMPLEMENTATION met Sonnet) trekken het budget.
|
||||
|
||||
We voegen een nieuwe sectie **"Cost analyse"** toe (tussen Sprint Health en Plan-quality). Eén shared periode-selector (7d/30d/90d/MTD) stuurt vier visualisaties aan op basis van best practices uit de Anthropic Console + LLM-observability tools (Datadog, Portkey):
|
||||
|
||||
1. Trend-chart over tijd
|
||||
2. Breakdown per model
|
||||
3. Breakdown per job-kind
|
||||
4. Cache efficiency
|
||||
|
||||
De bestaande `TokenUsageCard` blijft staan als sprint-detail (top duurste jobs voor de actieve sprint).
|
||||
|
||||
## Bestaande infrastructuur (hergebruik)
|
||||
|
||||
**Reeds aanwezig in DB:**
|
||||
|
||||
- [prisma/schema.prisma](../../prisma/schema.prisma) — `ClaudeJob` heeft `input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_write_tokens`, `actual_thinking_tokens`, `model_id`, `kind`, `finished_at`
|
||||
- `ModelPrice` tabel met prijzen per 1M tokens (input/output/cache_read/cache_write)
|
||||
- Prijzen worden gesynced via [scripts/sync-model-prices.ts](../../scripts/sync-model-prices.ts)
|
||||
|
||||
**Hergebruikbare patronen:**
|
||||
|
||||
- KPI-strip stijl: zie [app/(app)/insights/components/token-usage.tsx](../../app/(app)/insights/components/token-usage.tsx) (regels 43-64)
|
||||
- URL-param-gestuurde filter met `useTransition` + `router.replace`: zie [app/(app)/insights/components/agent-throughput.tsx](../../app/(app)/insights/components/agent-throughput.tsx) (regels 38-62)
|
||||
- Recharts BarChart pattern: zie [app/(app)/insights/components/agent-throughput.tsx](../../app/(app)/insights/components/agent-throughput.tsx) (regels 110-130)
|
||||
- Cost-formule (zelfde overal): zie [lib/insights/token-stats.ts](../../lib/insights/token-stats.ts) (regels 73-79) — input + output + cache_read + cache_write + thinking, allemaal `× price_per_1m / 1_000_000`
|
||||
- Server component → parallel data-fetch via `Promise.all`: zie [app/(app)/insights/page.tsx](../../app/(app)/insights/page.tsx) (regels 46-80)
|
||||
|
||||
## Te bouwen
|
||||
|
||||
### Taak 1 — Data-laag — `lib/insights/cost-analysis.ts` (nieuw)
|
||||
|
||||
Eén bestand met vijf functies, allemaal `(userId, period)` als parameters. Period wordt naar `WHERE cj.finished_at >= NOW() - INTERVAL '<n> days'` vertaald (MTD = `>= date_trunc('month', NOW())`).
|
||||
|
||||
```ts
|
||||
export type Period = '7d' | '30d' | '90d' | 'mtd'
|
||||
|
||||
export interface CostKpi {
|
||||
totalCostUsd: number
|
||||
totalTokens: number
|
||||
jobCount: number
|
||||
avgPerDayUsd: number
|
||||
cacheSavingsUsd: number // (input_price - cache_read_price) × cache_read_tokens
|
||||
topModelId: string | null
|
||||
topModelCostUsd: number
|
||||
}
|
||||
|
||||
export interface CostByDayRow { day: string; costUsd: number }
|
||||
export interface CostByModelRow { modelId: string; costUsd: number; jobCount: number }
|
||||
export interface CostByKindRow { kind: string; costUsd: number; jobCount: number }
|
||||
export interface CacheEfficiency {
|
||||
cacheReadTokens: number
|
||||
uncachedInputTokens: number
|
||||
cacheHitRatio: number // cache_read / (cache_read + input)
|
||||
savingsUsd: number
|
||||
spentOnCacheWriteUsd: number
|
||||
}
|
||||
|
||||
export async function getCostKpi(userId: string, period: Period): Promise<CostKpi>
|
||||
export async function getCostByDay(userId: string, period: Period): Promise<CostByDayRow[]>
|
||||
export async function getCostByModel(userId: string, period: Period): Promise<CostByModelRow[]>
|
||||
export async function getCostByKind(userId: string, period: Period): Promise<CostByKindRow[]>
|
||||
export async function getCacheEfficiency(userId: string, period: Period): Promise<CacheEfficiency>
|
||||
```
|
||||
|
||||
**Belangrijke details:**
|
||||
|
||||
- Alle queries: `WHERE cj.user_id = ${userId} AND cj.status = 'DONE' AND cj.finished_at >= <periodStart>`
|
||||
- Geen `productAccessFilter` nodig — `cj.user_id = ${userId}` filtert al op de eigenaar
|
||||
- `getCostByDay` vult ontbrekende dagen op met `0` (anders breekt de chart-x-as) — vul aan client- of server-side, kies één
|
||||
- Periode → days mapping inline: `7d`→7, `30d`→30, `90d`→90, `mtd`→huidige dag-van-maand
|
||||
- Cache savings: `cache_read_tokens × (input_price - cache_read_price) / 1_000_000` — "wat je betaald zou hebben zonder cache, minus wat je betaalde mét cache"
|
||||
|
||||
### Taak 2 — UI — `app/(app)/insights/components/cost-analysis.tsx` (nieuw)
|
||||
|
||||
Eén client-component die de hele sectie rendert. Structuur:
|
||||
|
||||
```
|
||||
[Period selector rechtsboven]
|
||||
[KPI strip: Totaal | Cache savings | Avg/dag | Top model ($X op claude-opus-4-7)]
|
||||
[grid grid-cols-1 md:grid-cols-2 gap-4]
|
||||
[Daily cost line/bar chart] [Model breakdown - horizontal bar of donut]
|
||||
[Job-kind breakdown - bar] [Cache efficiency - donut + label "X% hit, $Y bespaard"]
|
||||
```
|
||||
|
||||
**Period selector:** kopieer pattern uit [agent-throughput.tsx](../../app/(app)/insights/components/agent-throughput.tsx) (regels 50-61) — `useTransition` + `router.replace` met `?period=` in URL. Default tonen als "30d".
|
||||
|
||||
**Charts:** Recharts (al gebruikt in `BurndownChart`, `AgentThroughputCard`, `VelocityChart`):
|
||||
|
||||
- Daily: `<BarChart>` met één bar (cost in USD), x-as = dag (`MM-DD`), tooltip toont `$X.XXXX`
|
||||
- Model: `<BarChart layout="vertical">` met model_id labels — beperkt tot top 5
|
||||
- Kind: `<BarChart layout="vertical">` met kind labels — beperkt tot top 5
|
||||
- Cache: `<PieChart>` met twee segmenten (cached / uncached input) + tekst "X% cache hit · $Y bespaard"
|
||||
|
||||
**Empty state:** als `kpi.jobCount === 0`: render één regel "Geen jobs in deze periode."
|
||||
|
||||
### Taak 3 — Integratie — `app/(app)/insights/page.tsx` (edit)
|
||||
|
||||
Wijzigingen:
|
||||
|
||||
```diff
|
||||
interface InsightsPageProps {
|
||||
- searchParams: Promise<{ product?: string }>
|
||||
+ searchParams: Promise<{ product?: string; period?: string }>
|
||||
}
|
||||
```
|
||||
|
||||
```diff
|
||||
- const { product: filterProductId } = await searchParams
|
||||
+ const { product: filterProductId, period: rawPeriod } = await searchParams
|
||||
+ const period = (['7d','30d','90d','mtd'].includes(rawPeriod ?? '') ? rawPeriod : '30d') as Period
|
||||
```
|
||||
|
||||
In de `Promise.all`, voeg toe:
|
||||
|
||||
```ts
|
||||
getCostKpi(userId, period),
|
||||
getCostByDay(userId, period),
|
||||
getCostByModel(userId, period),
|
||||
getCostByKind(userId, period),
|
||||
getCacheEfficiency(userId, period),
|
||||
```
|
||||
|
||||
Nieuwe sectie tussen Sprint Health en Plan-quality:
|
||||
|
||||
```tsx
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-lg font-medium text-foreground">Cost analyse</h2>
|
||||
<CostAnalysisCard
|
||||
period={period}
|
||||
kpi={costKpi}
|
||||
byDay={costByDay}
|
||||
byModel={costByModel}
|
||||
byKind={costByKind}
|
||||
cache={cacheEff}
|
||||
/>
|
||||
</section>
|
||||
```
|
||||
|
||||
De bestaande "Token gebruik" sectie blijft staan (sprint-detail tabel).
|
||||
|
||||
## Bestanden
|
||||
|
||||
**Nieuw:**
|
||||
|
||||
- `lib/insights/cost-analysis.ts` — 5 query-functies + types
|
||||
- `app/(app)/insights/components/cost-analysis.tsx` — client-component met period-selector + 4 charts
|
||||
|
||||
**Edit:**
|
||||
|
||||
- `app/(app)/insights/page.tsx` — period uit searchParams, parallel-fetch, nieuwe sectie
|
||||
|
||||
**Geen wijzigingen aan:**
|
||||
|
||||
- Prisma schema (alle data is er al)
|
||||
- MCP server (token-data wordt al weggeschreven via `update_job_status`)
|
||||
- `TokenUsageCard` (blijft als sprint-detail tabel)
|
||||
|
||||
## Verificatie
|
||||
|
||||
```bash
|
||||
npm run verify && npm run build
|
||||
```
|
||||
|
||||
**Handmatig:**
|
||||
|
||||
1. Open `/insights` zonder query — period default `30d`, sectie toont KPI + 4 charts
|
||||
2. Wissel period via selector → URL updatet `?period=7d`, charts laden nieuwe data via `router.replace`
|
||||
3. Check empty state: kies periode zonder jobs → "Geen jobs in deze periode."
|
||||
4. Sanity-check KPI's tegen ruwe DB-query:
|
||||
```sql
|
||||
SELECT SUM(input_tokens * mp.input_price_per_1m / 1e6
|
||||
+ output_tokens * mp.output_price_per_1m / 1e6
|
||||
+ cache_read_tokens * mp.cache_read_price_per_1m / 1e6
|
||||
+ cache_write_tokens * mp.cache_write_price_per_1m / 1e6
|
||||
+ COALESCE(actual_thinking_tokens, 0) * mp.input_price_per_1m / 1e6)
|
||||
FROM claude_jobs cj
|
||||
LEFT JOIN model_prices mp ON mp.model_id = cj.model_id
|
||||
WHERE cj.user_id = '<id>' AND cj.status = 'DONE'
|
||||
AND cj.finished_at >= NOW() - INTERVAL '30 days';
|
||||
```
|
||||
5. Cache savings sanity: `cacheSavingsUsd ≈ cache_read_tokens × 0.9 × input_price / 1M`
|
||||
(cache_read prijs = 0.1× input prijs, dus savings is 90%)
|
||||
205
docs/old/plans/lees-de-readme-md-validated-book.md
Normal file
205
docs/old/plans/lees-de-readme-md-validated-book.md
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
# Scrum4Me-Research — Zustand rearchitecture (reset + execute)
|
||||
|
||||
> **Scope:** dit plan is geschreven voor de research-repo
|
||||
> [`madhura68/Scrum4Me-Research`](https://github.com/madhura68/Scrum4Me-Research),
|
||||
> niet voor dit hoofdproject. Bestandsverwijzingen die naar
|
||||
> `stores/data-store.ts`, `hooks/use-event-stream.ts`,
|
||||
> `components/*-select.tsx` etc. wijzen, bestaan in de research-repo —
|
||||
> niet hier. Ze staan in `code`-tags zodat de doc-link-checker ze niet
|
||||
> probeert te resolven.
|
||||
|
||||
## Context
|
||||
|
||||
Het bestaande [zustand-store-rearchitecture.md](./zustand-store-rearchitecture.md) beschrijft een doel-architectuur (`product-workspace-store` met genormaliseerde entities, race-safe loaders, resync-laag, optimistic mutations). De research-repo is dé plek om dat eerst te testen voordat het in `Scrum4Me/` belandt.
|
||||
|
||||
Probleem nu: de research-repo wijkt af van het hoofdproject. Mijn custom `data-store.ts` lijkt qua vorm op de doel-architectuur, maar springt over de baseline heen. We willen aantonen dat de migratie *vanaf* de huidige Scrum4Me-patronen werkt, niet vanaf een verzonnen tussenvorm.
|
||||
|
||||
Dus: eerst de research-repo terugbrengen naar dezelfde stores/hooks/routes als Scrum4Me nu heeft, dan de rearchitecture daarop uitvoeren.
|
||||
|
||||
## Bron-documenten
|
||||
|
||||
- **Doel-architectuur**: [zustand-store-rearchitecture.md](./zustand-store-rearchitecture.md) (in research-repo). Dit plan voert dat document uit; herhaalt het niet.
|
||||
- **Conventies**: [CLAUDE.md](../../CLAUDE.md) hoofdproject. Taal NL, MD3 tokens, `@base-ui/react` render-prop, `*-server.ts`, enum UPPER_SNAKE↔lowercase via `lib/task-status.ts`.
|
||||
|
||||
## Drie-faseplan
|
||||
|
||||
### Fase A — Reset naar Scrum4Me-patronen
|
||||
|
||||
Doel: onze research-pagina werkt op exact dezelfde store/hook/route-vorm als het hoofdproject, met identiek gedrag.
|
||||
|
||||
**Verwijderen** (research-repo):
|
||||
- `stores/data-store.ts` (research-repo) — mijn megastore
|
||||
- `hooks/use-event-stream.ts` (research-repo) — vervangen door `use-backlog-realtime.ts`
|
||||
- `hooks/use-browser-presence.ts` (research-repo) — niet in main, drop voor reset
|
||||
- `app/api/realtime/events/route.ts` (research-repo) — vervangen door `app/api/realtime/backlog/route.ts`
|
||||
- Mijn custom `loadX/resyncAll`-paden in selectie-componenten
|
||||
|
||||
**Kopiëren uit `/Users/janpetervisser/Development/Scrum4Me/`** (1-op-1 of stripped van auth):
|
||||
| Bron | Doel |
|
||||
|---|---|
|
||||
| `stores/backlog-store.ts` | `stores/backlog-store.ts` (`pbis`, `storiesByPbi`, `tasksByStory`; `setInitialData`, `applyChange`) |
|
||||
| `stores/planner-store.ts` | `stores/planner-store.ts` (DnD-order; voor research nog niet gebruikt maar we zetten 'm klaar) |
|
||||
| `stores/selection-store.ts` | overschrijf bestaand (state: `selectedPbiId`, `selectedStoryId`, geen taskId/productId in main; add `selectedTaskId` + `productId` als research-uitbreiding) |
|
||||
| `stores/product-store.ts` | `stores/product-store.ts` (`currentProduct`) |
|
||||
| `stores/products-store.ts` | `stores/products-store.ts` (lijst, voor pulldown) |
|
||||
| `lib/realtime/use-backlog-realtime.ts` | `lib/realtime/use-backlog-realtime.ts` (SSE-client → `applyChange` op backlog-store) |
|
||||
| `lib/task-status.ts` | `lib/task-status.ts` (enum-converters) |
|
||||
| `app/api/realtime/backlog/route.ts` | `app/api/realtime/backlog/route.ts` (SSE+LISTEN, **research-only: strip auth/session/getAccessibleProduct** — vraagt enkel `product_id` querystring) |
|
||||
|
||||
> ⚠️ **Auth-strip is research-only.** Het hoofdproject MOET sessie + `getAccessibleProduct()`-check op SSE en read-routes behouden. Bij backport vanaf de research-repo nooit de geknipte route 1-op-1 overnemen. Dit geldt voor zowel `app/api/realtime/backlog/route.ts` als alle read-routes onder `app/api/products/...`, `/pbis/...`, `/stories/...`, `/tasks/...`.
|
||||
|
||||
**API-routes (read)** — bestaande paden behouden, alleen `force-dynamic` blijft:
|
||||
- `GET /api/products` (list voor pulldown)
|
||||
- `GET /api/products/[id]/pbis` (open: READY+BLOCKED)
|
||||
- `GET /api/pbis/[id]/stories`
|
||||
- `GET /api/stories/[id]/tasks`
|
||||
- `GET /api/tasks/[id]`
|
||||
|
||||
**Componenten herschrijven**:
|
||||
- `components/product-select.tsx` (research-repo) → leest `useProductsStore`, schrijft naar `useProductStore.setCurrentProduct`
|
||||
- `components/pbi-select.tsx` (research-repo) → leest `useBacklogStore` (filter op currentProduct), `useSelectionStore.selectPbi`. Triggert fetch op product-mount via een `useBacklogLoader`-helper die initial data binnenhaalt.
|
||||
- `components/story-select.tsx` (research-repo) → idem voor stories
|
||||
- `components/tasks-table.tsx` (research-repo) → leest `tasksByStory[selectedStoryId]`. **Max 10 rijen, scrollbaar** (al ingebouwd, behouden)
|
||||
- `components/task-detail-card.tsx` (research-repo) → fetcht task detail apart (geen full-fat backlog veld; matcht main's `tasks/[id]` route)
|
||||
- `components/event-stream-panel.tsx` (research-repo) → blijft bestaan voor research-doel (event-tap), maar luistert nu mee op dezelfde EventSource via `use-backlog-realtime` (of een tweede readonly listener); selecteerbare events met JSON-detail rechts blijven. Twee checkboxes (Postgres / Browser). Truncate met ellipsis in de lijst.
|
||||
|
||||
**Werkwijzen (verifiëren tijdens reset)**:
|
||||
- Comments en UI-tekst NL
|
||||
- Geen `bg-blue-500` etc; enkel MD3 tokens (`bg-primary`, `bg-card`, `bg-status-done`, ...)
|
||||
- shadcn-componenten al `base-nova` style
|
||||
- Server-only files krijgen `*-server.ts` suffix waar van toepassing (in deze fase niet nodig — alle DB-toegang loopt via `lib/prisma.ts` in route handlers)
|
||||
- TaskStatus-mapping via `lib/task-status.ts` als de UI lowercase wil
|
||||
|
||||
**Acceptatie Fase A**:
|
||||
- `npx tsc --noEmit` schoon
|
||||
- Pagina rendert, cascading werkt, tabel toont taken, detail-card vult, events stromen door (preview-verificatie)
|
||||
- Stores matchen het hoofdproject qua vorm (vergelijking via `diff` uitvoerbaar voor backlog-store etc.)
|
||||
|
||||
### Fase B — Rearchitecture uitvoeren
|
||||
|
||||
Volgt de 15 stappen uit [zustand-store-rearchitecture.md](./zustand-store-rearchitecture.md) §Implementatiepad. Concreet voor de research-repo:
|
||||
|
||||
1. **Map** `stores/product-workspace/` aanmaken (factory + provider + selectors).
|
||||
2. **`activeProduct`** wordt nu nog gespiegeld vanuit `useProductStore`; voor de research-pagina geen layout/server-side bepaling — we lezen het uit de pulldown-state.
|
||||
3. **Selection migreren** — `selection-store` → `context.{activePbiId, activeStoryId, activeTaskId}` + `productId`. Setters cascaden de reset naar children (zoals doc beschrijft).
|
||||
4. **Backlog naar entities + relations** — `pbisById`, `storiesById`, `tasksById`, `pbiIds`, `storyIdsByPbi`, `taskIdsByStory`. Selectors:
|
||||
- `selectVisiblePbis(productId)`
|
||||
- `selectStoriesForActivePbi(state)`
|
||||
- `selectTasksForActiveStory(state)`
|
||||
- `selectActivePbi/Story/Task(state)`
|
||||
5. **Planner-state** in dezelfde workspace-store landen (`relations` slice); voor research: niet actief gebruikt, wel structureel meekoppen.
|
||||
6. **Race-safe loaders** — `ensureProductLoaded`, `ensurePbiLoaded`, `ensureStoryLoaded`, `ensureTaskLoaded` met `requestId`-guard. Implementatie:
|
||||
```ts
|
||||
setActivePbi(pbiId) {
|
||||
const requestId = crypto.randomUUID()
|
||||
set({ context: { ..., activePbiId: pbiId, ... }, loading: { ..., activeRequestId: requestId } })
|
||||
void get().ensurePbiLoaded(pbiId, requestId)
|
||||
}
|
||||
// in ensure: if (get().loading.activeRequestId !== requestId) return
|
||||
```
|
||||
7. **localStorage = restore hints** — `lastActivePbiIdByProduct`, `lastActiveStoryIdByProduct`, `lastActiveTaskIdByProduct`. Niet de waarheid, alleen hint die getoetst wordt aan toegankelijkheid.
|
||||
8. **`use-backlog-realtime` dispatcht naar `applyRealtimeEvent`** — store interpreteert pbi/story/task I|U|D events, doet upsert + parent-id move + sort.
|
||||
9. **Hidden tab beleid** — `EventSource` openhouden bij `hidden`. Op `visible` → `resyncActiveScopes('visible')`.
|
||||
10. **Reconnect resync** — bij `ready` na disconnect of na exponential backoff: `resyncActiveScopes('reconnect')`.
|
||||
11. **Unknown-event fallback** — onbekend event met `payload.product_id === activeProductId` → `resyncActiveScopes('unknown-event')`. Dit is wat het "veel events maar geen update" issue oplost.
|
||||
12. **`force-dynamic` + `cache: 'no-store'`** — al gedaan in mijn fixes; behouden bij reset en versterken.
|
||||
13. **Componenten naar selectors** — backlog-componenten lezen via `selectStoriesForActivePbi` etc., niet via raw store-velden.
|
||||
14. **Tests** (Vitest, conform main):
|
||||
- hydrate snapshot
|
||||
- active selectie cascade
|
||||
- race-safe ensure (laat trage promise van oude selectie geen nieuwe data overschrijven)
|
||||
- SSE I|U|D voor pbi/story/task
|
||||
- parent-move (story verandert van pbi)
|
||||
- hidden→visible resync
|
||||
- reconnect resync
|
||||
- unknown-event resync
|
||||
- delete-cleanup van actieve selectie
|
||||
- localStorage restore-hint validatie tegen toegankelijkheid
|
||||
15. **Sprint-workspace** — buiten scope; flag voor latere herhaling.
|
||||
|
||||
**Optimistic mutations** (§Optimistic in doc): voor research geen DnD, dus alleen het patroon dropunten en niet bouwen. Wel: `applyOptimisticMutation`-action wel klaarzetten in de store-API zodat het patroon zichtbaar is.
|
||||
|
||||
### Fase C — Werkwijzen verweven en doortrekken
|
||||
|
||||
**Tijdens Fase A én B respecteren**:
|
||||
1. **Plan mode workflow** — eerst Plan, ExitPlanMode, dan code. Bij grote wendingen opnieuw plannen.
|
||||
2. **TodoWrite** voor multi-step werk; markeer immediate completion.
|
||||
3. **Verify via preview** voor elke observable verandering (de hook reminder doet dit al).
|
||||
4. **`tsc --noEmit`** voor afronden van een stap.
|
||||
5. **Comments/Dutch** consistent. WHY-comments over de invariant; geen WHAT-comments.
|
||||
6. **MD3 tokens** alleen.
|
||||
7. **Geen secrets in chat** — `.env.local` blijft lokaal.
|
||||
8. **Niet schrijven naar shared DB** zonder expliciete user-toestemming (geen `pg_notify` op shared channel).
|
||||
9. **Source of truth = DB**. Zustand is projectie. localStorage = hint.
|
||||
10. **Vóór elke fase**: kort statusrapport in de chat met wat er aankomt en waarom.
|
||||
|
||||
**Doortrekken naar hoofdproject** (out-of-scope deze run, maar geflagd):
|
||||
- Na bewezen werking in research-repo: backport `product-workspace-store` + selectors + realtime-apply + resync-laag naar `Scrum4Me/stores/product-workspace/`.
|
||||
- **Niet backporten**: de auth-stripped routes uit research. Main behoudt iron-session, `getAccessibleProduct()`, en alle product-access/sprint/personal filters in z'n SSE- en read-routes.
|
||||
- **Wel backporten**: store-shape, selectors, race-safe `ensure*Loaded`, hidden-tab beleid, `resyncActiveScopes`, unknown-event fallback, restore-hint patroon, `force-dynamic` + `cache: 'no-store'`.
|
||||
- Migratie main-project zal langer duren (DnD, sprint, jobs, tests). Apart plan.
|
||||
|
||||
## Bestandsmutaties (overzicht)
|
||||
|
||||
### Verwijderen na Fase A
|
||||
- `stores/data-store.ts` (research-repo)
|
||||
- `hooks/use-event-stream.ts` (research-repo)
|
||||
- `hooks/use-browser-presence.ts` (research-repo) — komt deels terug in Fase B als helper voor visibility/online resync trigger
|
||||
- `app/api/realtime/events/route.ts` (research-repo)
|
||||
|
||||
### Toevoegen Fase A (uit Scrum4Me)
|
||||
- `stores/backlog-store.ts`
|
||||
- `stores/planner-store.ts`
|
||||
- `stores/selection-store.ts` (overschrijf)
|
||||
- `stores/product-store.ts`
|
||||
- `stores/products-store.ts`
|
||||
- `lib/realtime/use-backlog-realtime.ts`
|
||||
- `lib/task-status.ts`
|
||||
- `app/api/realtime/backlog/route.ts` (zonder auth)
|
||||
|
||||
### Toevoegen Fase B (nieuw, conform doc)
|
||||
- `stores/product-workspace/store.ts` (zustand factory)
|
||||
- `stores/product-workspace/selectors.ts`
|
||||
- `stores/product-workspace/types.ts`
|
||||
- `stores/product-workspace/restore.ts` (localStorage hints)
|
||||
- `stores/product-workspace/realtime-apply.ts` (SSE event → store)
|
||||
- `stores/product-workspace/resync.ts` (`resyncActiveScopes`, `resyncLoadedScopes`)
|
||||
- `tests/product-workspace/*.test.ts` (Vitest, install vitest als devDep)
|
||||
|
||||
### Te aanpassen in Fase B
|
||||
- Alle `components/*.tsx` (nu shadcn select/table/card panels) → consumeren via selectors uit workspace-store
|
||||
- `lib/realtime/use-backlog-realtime.ts` → dispatcht `applyRealtimeEvent` naar workspace-store i.p.v. `applyChange` naar backlog-store
|
||||
- `event-stream-panel.tsx` → blijft bestaan (research-tap), maar leest events ook uit workspace-store of via een dunne `event-log-store` ernaast (in bounded-context-stijl: aparte log-store voor pure observatie hoort er niet thuis in de workspace-store)
|
||||
|
||||
## Verificatie
|
||||
|
||||
### Na Fase A (baseline)
|
||||
1. `npm run dev` op port 3001
|
||||
2. Pagina laadt, cascading werkt: product → PBI → story → tasks
|
||||
3. Detail-card vult bij klik op task
|
||||
4. Event-paneel toont realtime events (truncate + JSON-detail)
|
||||
5. `npx tsc --noEmit` schoon
|
||||
6. Vergelijk: `diff Scrum4Me/stores/backlog-store.ts Scrum4Me-Research/stores/backlog-store.ts` → identiek (modulo lokale interface-uitbreidingen waar gedocumenteerd)
|
||||
|
||||
### Na Fase B (target)
|
||||
Alle acceptatiecriteria uit [zustand-store-rearchitecture.md §Acceptatiecriteria](./zustand-store-rearchitecture.md):
|
||||
- Eén waarheid per entity in de store ✓
|
||||
- Selectors als enige UI-leesweg ✓
|
||||
- SSE patcht zonder full-page refresh ✓
|
||||
- Hidden→visible herstelt missers binnen één resync-cyclus ✓
|
||||
- Reconnect resync werkt zonder NOTIFY-replay ✓
|
||||
- Directe task-edits zonder `entity:'task'` NOTIFY worden via unknown-event fallback zichtbaar ✓
|
||||
- LocalStorage = hint, geen forced state ✓
|
||||
- `force-dynamic` + `cache: 'no-store'` overal ✓
|
||||
|
||||
### Manuele preview-verificatie (na elke fase)
|
||||
- TODO via TodoWrite tijdens uitvoer; preview-screenshot na grote stappen
|
||||
- Tab-switch test: open page, switch tab, doe een wijziging via een ander mechanisme (psql na user-akkoord, of UI in main-project), keer terug → verwacht: zonder warnings + data gerefresht
|
||||
|
||||
## Open vragen / risico's
|
||||
|
||||
1. **Reset-import** uit hoofdproject: voor de research-repo strippen we auth/session-deps uit de gekopieerde routes (research-repo heeft geen auth-laag, draait lokaal). Belangrijk: **dit is een research-repo-keuze; main behoudt de volledige auth-filters**. Zie de waarschuwing onder "API-routes (read)" hierboven.
|
||||
2. **`use-backlog-realtime` heeft mogelijk auth-headers/session-checks**: bevestigen tijdens copy. Indien zo: research-versie gebruikt geen auth, route is publiek-bereikbaar binnen lokale dev. Geldt alleen lokaal — geen wijziging aan main.
|
||||
3. **Tests-deps** (vitest, @testing-library/react) toevoegen tijdens Fase B. Of pas in Fase B step 14 vanwege scope.
|
||||
4. **Event-paneel toekomst**: blijft het in research-repo of stoten we het af zodra de workspace-store af is? Voorstel: behouden als observatie-tool, maar er aparte `event-log-store` (kleine UI store) voor maken zodat het niet meelift in de workspace-store.
|
||||
5. **README.md** update na Fase B (optioneel) — kort beschrijven dat dit nu het canonical migratie-pad demonstreert.
|
||||
212
docs/old/plans/user-settings-store.md
Normal file
212
docs/old/plans/user-settings-store.md
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
---
|
||||
title: "User-settings store (DB-backed user prefs)"
|
||||
status: draft
|
||||
audience: [contributor, ai-agent]
|
||||
language: nl
|
||||
last_updated: 2026-05-10
|
||||
---
|
||||
|
||||
# User-settings store (DB-backed user prefs)
|
||||
|
||||
> **Locatie na approval:** verhuis dit bestand naar `docs/plans/user-settings-store.md` in de repo.
|
||||
> Trigger voor dit plan: zichtbare hydratie-flits op het sprint-scherm in v1.3.3 ([PR #184](https://github.com/madhura68/Scrum4Me/pull/184)).
|
||||
> De fix daar (useEffect-hydratie + `prefsLoaded`-gate) is een tijdelijke patch; deze migratie elimineert de flits volledig.
|
||||
|
||||
## Context
|
||||
|
||||
Filter- en view-prefs zitten nu verspreid over `localStorage` (en deels cookies).
|
||||
Bij SSR weet de server niets van `localStorage`, dus bij users met saved-state ≠
|
||||
default ontstaat één render-flits direct na hydratie. Daarnaast werken die prefs
|
||||
alleen per browser — geen cross-device, en cross-tab-sync vereist `storage`-events.
|
||||
|
||||
Doel: **één `User.settings` JSON-veld** als single source of truth, met:
|
||||
|
||||
- Server-component leest het veld bij elke page-render → SSR-correct, geen flits
|
||||
- Zustand-store met optimistic updates patroon (zoals `product-workspace-store`)
|
||||
- Cross-tab sync via bestaande `LISTEN/NOTIFY` + SSE-bridge
|
||||
- Cross-device persistence (login op andere browser/laptop ziet zelfde prefs)
|
||||
|
||||
---
|
||||
|
||||
## Scope (gefaseerd)
|
||||
|
||||
### Fase 0 — Infrastructuur
|
||||
|
||||
Aparte PR. Geen UI-wijziging; legt het fundament. Resultaat is een werkende store
|
||||
zonder migraties; bestaande localStorage-flow blijft intact tot Fase 1.
|
||||
|
||||
| # | Bestand | Wat |
|
||||
|---|---|---|
|
||||
| 0.1 | `prisma/schema.prisma` | `settings Json @default("{}")` op `User` model + migration |
|
||||
| 0.2 | `lib/user-settings.ts` | Zod-schema + types + `mergeSettings(prev, patch)` deep-merge helper + defaults |
|
||||
| 0.3 | `actions/user-settings.ts` | `updateUserSettingsAction(patch: Partial<UserSettings>)` — auth-guard, Zod-validate, deep-merge in DB transactie, `NOTIFY scrum4me_changes 'user_settings:${userId}'` |
|
||||
| 0.4 | `stores/user-settings/store.ts` | Zustand met `entities.settings: UserSettings`, `hydrate(initial)`, generieke `setPref(path, value)` met optimistic + rollback. Zelfde mutation-flow als `product-workspace-store` |
|
||||
| 0.5 | `app/api/realtime/user-settings/route.ts` | SSE-route per user, `LISTEN user_settings:${userId}`, push patches |
|
||||
| 0.6 | `components/shared/user-settings-bridge.tsx` | Server reads `prisma.user.findUnique({select:{settings:true}})`, geeft door als prop, client mount roept `store.hydrate()` aan + opent SSE |
|
||||
| 0.7 | Mount in `app/(app)/layout.tsx` | Bridge bovenin de app-layout zodat de store altijd beschikbaar is voor alle authenticated pagina's |
|
||||
| 0.8 | Tests | `__tests__/lib/user-settings.test.ts` (merge-logic), `__tests__/actions/user-settings.test.ts` (auth + validation), `__tests__/stores/user-settings.test.ts` (optimistic flow) |
|
||||
|
||||
**Demo/anon-fallback:** `useUserSettingsStore` detecteert `session.isDemo` of geen `userId`
|
||||
en valt terug op in-memory state (geen server-write). Bridge wordt voor demo niet
|
||||
gemount — defaults blijven actief, geen persistence-verwachting.
|
||||
|
||||
### Fase 1 — Migreer huidige flits-bronnen
|
||||
|
||||
| Component | localStorage-keys | → `settings`-pad |
|
||||
|---|---|---|
|
||||
| `components/sprint/sprint-backlog.tsx` | `scrum4me:sprint_pb_*` (6) | `views.sprintBacklog.{filterPriority,filterStatus,sort,sortDir,collapsedPbis,filterPopoverOpen}` |
|
||||
| `components/backlog/pbi-list.tsx` | `scrum4me:pbi_*` (4) | `views.pbiList.{sort,filterPriority,filterStatus,sortDir}` |
|
||||
| `components/backlog/story-panel.tsx` | `scrum4me:story_sort` (1) | `views.storyPanel.sort` |
|
||||
| `components/jobs/jobs-column.tsx` | `${prefix}_filter_kind`, `${prefix}_filter_status` (2 dyn.) | `views.jobsColumns[prefix].{kinds,statuses}` |
|
||||
| `stores/debug-store.ts` (via `status-bar-debug-toggle`) | `scrum4me:debug-mode` (1) | `devTools.debugMode` |
|
||||
|
||||
Per component:
|
||||
- Verwijder `useState` + `useEffect`-hydratie + `useEffect`-write
|
||||
- Vervang door `useUserSettingsStore(s => s.entities.settings.views.sprintBacklog?.filterStatus ?? 'OPEN')`
|
||||
- Setter wordt `useUserSettingsStore.getState().setPref(['views','sprintBacklog','filterStatus'], value)`
|
||||
- `prefsLoaded`-state en helpers (`readLocalStoragePref`) verdwijnen
|
||||
- `lib/use-local-storage-pref.ts` wordt verwijderd (niet meer in gebruik)
|
||||
|
||||
**Migratie-pad voor bestaande users:** bij eerste mount, voor de eerste `setPref`-call,
|
||||
leest een one-shot `useEffect` de oude localStorage-keys en pusht ze als één bulk-patch
|
||||
naar de server. Daarna `localStorage.removeItem(...)` om geen verwarring te wekken.
|
||||
Idempotent: als `settings.views.sprintBacklog.filterStatus` al gezet is, sla over.
|
||||
|
||||
### Fase 2 — Cookie-consolidatie (optioneel, later PR)
|
||||
|
||||
| Bron | Huidig | → `settings`-pad |
|
||||
|---|---|---|
|
||||
| `components/shared/split-pane.tsx` | `document.cookie` (`sp:` prefix) | `layout.splitPanePositions[cookieKey]` |
|
||||
| `lib/active-sprint.ts` + `actions/active-sprint.ts` | server-side cookie per product | `layout.activeSprints[productId]` |
|
||||
|
||||
Server-component-lezers veranderen — apart traject met meer regression-risico.
|
||||
Niet onderdeel van de eerste user-settings-PR.
|
||||
|
||||
### Fase 3 — Skip / al persistent
|
||||
|
||||
- `idea-md-editor.tsx` drafts — werk-in-progress, geen pref
|
||||
- `iron-session` cookies — auth, andere zorg
|
||||
- `User.active_product_id` — al in DB (kolom op model)
|
||||
- Modal/popover open-state behalve `filterPopoverOpen` — ephemeral
|
||||
|
||||
---
|
||||
|
||||
## JSON-shape (Fase 1)
|
||||
|
||||
```ts
|
||||
// lib/user-settings.ts
|
||||
import { z } from 'zod'
|
||||
|
||||
export const UserSettingsSchema = z.object({
|
||||
views: z.object({
|
||||
sprintBacklog: z.object({
|
||||
filterPriority: z.union([z.number().int().min(1).max(4), z.literal('all')]).optional(),
|
||||
filterStatus: z.enum(['OPEN', 'IN_SPRINT', 'DONE', 'all']).optional(),
|
||||
sort: z.enum(['priority', 'status', 'code']).optional(),
|
||||
sortDir: z.enum(['asc', 'desc']).optional(),
|
||||
collapsedPbis: z.array(z.string()).optional(),
|
||||
filterPopoverOpen: z.boolean().optional(),
|
||||
}).optional(),
|
||||
pbiList: z.object({
|
||||
sort: z.enum(['priority', 'code', 'date']).optional(),
|
||||
filterPriority: z.union([z.number().int().min(1).max(4), z.literal('all')]).optional(),
|
||||
filterStatus: z.enum(['ready', 'blocked', 'done', 'all']).optional(),
|
||||
sortDir: z.enum(['asc', 'desc']).optional(),
|
||||
}).optional(),
|
||||
storyPanel: z.object({
|
||||
sort: z.enum(['priority', 'code', 'date']).optional(),
|
||||
}).optional(),
|
||||
jobsColumns: z.record(z.string(), z.object({
|
||||
kinds: z.array(z.string()),
|
||||
statuses: z.array(z.string()),
|
||||
})).optional(),
|
||||
}).optional(),
|
||||
devTools: z.object({
|
||||
debugMode: z.boolean().optional(),
|
||||
}).optional(),
|
||||
}).strict()
|
||||
|
||||
export type UserSettings = z.infer<typeof UserSettingsSchema>
|
||||
```
|
||||
|
||||
Defaults zijn impliciet (alle keys optioneel). Selectors in de store geven
|
||||
fallback-waardes terug zodat consumers niet `?? 'OPEN'` hoeven te schrijven —
|
||||
maar het mag, geen big deal.
|
||||
|
||||
---
|
||||
|
||||
## Realtime-notificatie
|
||||
|
||||
Bestaand kanaal `scrum4me_changes` blijft. Payload-conventie:
|
||||
|
||||
```json
|
||||
{ "kind": "user_settings", "userId": "...", "patch": { "views": { ... } } }
|
||||
```
|
||||
|
||||
`/api/realtime/user-settings/route.ts` filtert payloads op `userId === session.userId`.
|
||||
Andere tabs van zelfde user krijgen patches binnen, store roept `applyServerPatch(patch)`
|
||||
aan zonder optimistic flow.
|
||||
|
||||
---
|
||||
|
||||
## Verificatie (per fase)
|
||||
|
||||
### Fase 0
|
||||
- [ ] `npm run verify && npm run build` groen
|
||||
- [ ] Migration draait op fresh + bestaande DB zonder data-verlies
|
||||
- [ ] `updateUserSettingsAction` weigert auth-loze calls (test)
|
||||
- [ ] Zod-validatie geeft 422 bij invalid patch (test)
|
||||
- [ ] Optimistic update + rollback gedraagt zich zoals `product-workspace-store` (test)
|
||||
- [ ] SSE-route levert patches alleen aan zelfde user (manueel: open twee tabs als A, schrijf, zie update; tab van user B blijft stil)
|
||||
|
||||
### Fase 1
|
||||
- [ ] Geen `localStorage.getItem` of `localStorage.setItem` meer in de gemigreerde componenten
|
||||
- [ ] Sprint screen: refresh → filter direct correct, geen flits, geen hydration error in console
|
||||
- [ ] Product backlog screen: idem
|
||||
- [ ] Jobs page: idem (per kolom-instance)
|
||||
- [ ] Two-tab test: filter wijzigen in tab A → tab B updatet binnen ~100ms
|
||||
- [ ] Demo-user: filter wijzigen werkt binnen sessie, niet gepersisteerd na refresh (verwacht)
|
||||
- [ ] One-shot localStorage-migratie: bestaande user met oude keys ziet bij eerste login zijn waardes terug; na refresh zijn de localStorage-keys leeg
|
||||
|
||||
### Fase 2
|
||||
- [ ] Split-pane positie persistent en SSR-correct
|
||||
- [ ] Active-sprint per product werkt zonder cookie
|
||||
|
||||
---
|
||||
|
||||
## Schatting
|
||||
|
||||
| Fase | Tijd |
|
||||
|---|---|
|
||||
| 0 — Infra | ~3 uur |
|
||||
| 1 — Migratie | ~2 uur |
|
||||
| 2 — Cookies | ~2 uur (apart) |
|
||||
| Totaal Fase 0 + 1 | **~5 uur**, 1 PR (of 2 als we 0 en 1 splitsen) |
|
||||
|
||||
Aanbevolen: **Fase 0 + 1 in één PR** als de infra klein blijft, anders splitsen
|
||||
per fase. Fase 2 is altijd een aparte PR.
|
||||
|
||||
---
|
||||
|
||||
## Open vragen
|
||||
|
||||
1. **Cross-device merge-conflict.** Twee tabs van zelfde user op verschillende
|
||||
devices wijzigen tegelijk. Server-side: `last-write-wins` of `JSON_PATCH`-merge?
|
||||
Voorstel: deep-merge per top-level path, dus `views.sprintBacklog.filterStatus`
|
||||
en `views.pbiList.sort` botsen niet — laatste schrijver per veld wint.
|
||||
2. **Storage-grens.** PostgreSQL JSON kolom kan ~1GB; we zitten op <5KB per user.
|
||||
Geen concern.
|
||||
3. **Schema-versionering.** Als we het JSON-schema later wijzigen: voorzichtig
|
||||
migreren via Zod `.catch()` voor onbekende keys. Voor v1: start klein.
|
||||
4. **One-shot localStorage-migratie weglaten?** Voor solo-dev-tool kan het
|
||||
acceptabel zijn dat users hun saved filters verliezen bij de migratie. Scheelt
|
||||
~30 minuten implementatie + tests.
|
||||
|
||||
---
|
||||
|
||||
## Eerste stappen na approval
|
||||
|
||||
1. Verhuis dit plan naar `docs/plans/user-settings-store.md` in een nieuwe branch (bv. `feat/user-settings-store`)
|
||||
2. Maak via Scrum4Me-MCP een PBI met story + taken voor Fase 0 (volgens CLAUDE.md werkflow)
|
||||
3. Start met taken in `sort_order`; commit per laag
|
||||
4. Fase 1 als opvolg-PBI (of in dezelfde sprint, los gelabeld)
|
||||
Loading…
Add table
Add a link
Reference in a new issue