docs(ST-1139): mobile-shell sync in functional spec + architectuur (T-334/T-335/T-336)

- docs/specs/functional.md: nieuwe sectie "Mobile shell" met routestructuur,
  acceptance-criteria, bekende iOS-limiet; desktop-first-clausule herzien naar
  "desktop-first hoofdpad + mobile-shell voor /m/*"
- docs/architecture/project-structure.md: route-tree onder app/(mobile)/,
  components/mobile/ in tree, vier nieuwe sleutelbeslissingen (route group,
  UA-redirect, gedeelde dialog-classes, gescheiden cookie-key)
- docs/INDEX.md regenerated, doc-links 86/86 valid
- T-336 E2E: lint/test/build groen; manuele DevTools/PWA-checks gelogd

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-04 10:56:15 +02:00
parent b327fbdf09
commit 19724eac5a
2 changed files with 68 additions and 3 deletions

View file

@ -15,8 +15,8 @@ scrum4me/
│ ├── (auth)/
│ │ ├── login/page.tsx
│ │ └── register/page.tsx
│ ├── (app)/ # Beschermde routes
│ │ ├── layout.tsx # Auth-check + navigatie
│ ├── (app)/ # Beschermde routes (desktop + tablets)
│ │ ├── layout.tsx # Auth-check (requireSession) + navigatie
│ │ ├── dashboard/page.tsx # Productenlijst
│ │ ├── products/
│ │ │ ├── new/page.tsx
@ -31,6 +31,16 @@ scrum4me/
│ │ └── settings/
│ │ ├── page.tsx # Profiel, account, PB-overzicht, rollen, tokens
│ │ └── tokens/page.tsx
│ ├── (mobile)/ # Mobile-shell route group (telefoon-UA)
│ │ ├── layout.tsx # Auth via gedeelde requireSession; geen NavBar/StatusBar
│ │ └── m/
│ │ ├── settings/page.tsx # Account + product-selector + QR-instructie + logout
│ │ ├── pair/ # QR-pairing (verhuisd uit (app)/ — URL ongewijzigd)
│ │ │ ├── page.tsx
│ │ │ └── pair-confirmation.tsx
│ │ └── products/[id]/
│ │ ├── page.tsx # Mobile Product Backlog (tab-mode op <1024px)
│ │ └── solo/page.tsx # Mobile Solo (3-koloms-kanban)
│ ├── api/ # REST API voor Claude Code
│ │ ├── products/
│ │ │ └── [id]/
@ -54,6 +64,7 @@ scrum4me/
│ ├── sprint/ # Sprint-componenten
│ ├── products/ # ProductForm, TeamManager, ArchiveProductButton
│ ├── settings/ # RoleManager, ProfileEditor, LeaveProductButton
│ ├── mobile/ # LandscapeGuard, MobileTabBar, LogoutButton
│ └── dnd/ # dnd-kit wrappers
├── lib/
│ ├── prisma.ts # Prisma Client singleton
@ -107,6 +118,26 @@ scrum4me/
**Rationale:** De gesplitste schermen met dnd-kit vereisen client-side staat die twee panelen tegelijk aanstuurt. `useState` per component leidt tot prop drilling; Context API veroorzaakt onnodige re-renders bij 60fps drag-events. Zustand's selector-gebaseerde subscriptions updaten alleen de componenten die de gewijzigde slice observeren. De gouden regel: Zustand beheert uitsluitend ephemere UI-staat — nooit server-data. Server-data blijft in Server Components en wordt opgehaald via Prisma.
**Trade-off:** Extra abstractielaag die geïnitialiseerd moet worden vanuit server-data. Opgelost via een patroon waarbij het Server Component de initiële ids doorgeeft aan een Client Component dat de store hydrateert.
### Beslissing: Eigen route group `(mobile)` voor mobile-shell (PBI-11)
**Keuze:** Telefoon-routes leven onder `app/(mobile)/m/*` met eigen `layout.tsx`, niet als nested directory in `(app)/m/*`.
**Rationale:** Next.js layouts erven naar binnen — een nested layout in `(app)/m/` zou de NavBar/StatusBar/MinWidthBanner/SoloRealtimeBridge/NotificationsBridge erven van `(app)/layout.tsx` zonder die te kunnen onderdrukken. De mobile-shell heeft die chrome niet nodig (alleen bottom-tab-bar). Een eigen route group geeft een schone parent-layout. De auth-check is geëxtraheerd naar `lib/auth-guard.ts` `requireSession()` zodat `(app)/layout.tsx` en `(mobile)/layout.tsx` dezelfde guard delen.
**Trade-off:** Twee layouts om te onderhouden, maar elk met een duidelijk afgebakende verantwoordelijkheid. Content-componenten (PbiList, StoryPanel, TaskPanel, SoloBoard, alle entity-dialogen) blijven volledig gedeeld — geen dubbele implementatie.
### Beslissing: UA-redirect via `Mobi`-substring (PBI-11)
**Keuze:** `lib/user-agent.ts` `isPhoneUA()` test op `Mobi` in de UA-string. `loginAction` (`actions/auth.ts`) leest de header na `session.save()`; phone-UA → `/m/products/[active]/solo` (zonder actief product → `/m/settings`); tablet-UA en desktop → `/dashboard`.
**Rationale:** `Mobi` is de standaard-heuristiek — aanwezig in iPhone Safari Mobile en Android Chrome op telefoons, afwezig op iPad en Android-tablet. Exact wat we willen: alleen telefoons krijgen de mobile-shell, tablets behouden de desktop-flow.
**Trade-off:** Heuristieken zijn nooit 100%; wie via een mobile-emulatie (DevTools) wil testen kan UA spoofen.
### Beslissing: Gedeelde `entityDialogContentClasses` voor mobile-fullscreen (PBI-11)
**Keuze:** Eén Tailwind-class-string in `components/shared/entity-dialog-layout.ts` met `max-sm:w-screen max-sm:h-screen max-sm:max-w-none max-sm:rounded-none` dekt alle entity-dialogen (PbiDialog, StoryDialog, TaskDialog, TaskDetailDialog).
**Rationale:** Dialog-fullscreen op mobile op vier plekken bewaken zou drift introduceren. De gedeelde constant geeft één bron van waarheid. Het regressie-vangnet (`__tests__/components/shared/entity-dialog-layout.test.ts`) verifieert dat elke dialog deze constant blijft gebruiken.
**Trade-off:** Eén dialog kan niet afwijken zonder de constant te verlaten — bewuste keuze voor consistentie.
### Beslissing: Gescheiden SplitPane cookie-key voor mobile (PBI-11)
**Keuze:** `BacklogSplitPane` op `app/(mobile)/m/products/[id]/page.tsx` gebruikt `cookieKey={\`backlog-${id}-mobile\`}` (versus desktop `backlog-${id}`).
**Rationale:** Op mobile rendert de `SplitPane` in tab-mode (`<1024px`), waar split-percentages niet aangepast worden. Zonder gescheiden key zou dezelfde cookie hergebruikt worden — telefoon-rotaties of orientatie-wisselingen hadden anders ongewenste interactie met de desktop-split-state.
**Trade-off:** Gebruikers die zowel mobile als desktop gebruiken hebben twee onafhankelijke split-instellingen, wat juist gewenst is.
---
## Zustand stores

View file

@ -27,7 +27,7 @@ v1 is een desktop-first fullstack webapplicatie waarmee een solo developer of kl
- Integratie met externe tools (GitHub Issues, Linear, Jira) — v2
- Notificaties en reminders — v2
- Native mobiele app — web-first; een toekomstige mobiele variant richt zich uitsluitend op taken afvinken
- Responsive layout voor schermen smaller dan 1024px — desktop-first in v1
- Responsive layout voor schermen smaller dan 1024px — desktop-first hoofdpad. Voor telefoons (UA met `Mobi`) is er een aparte mobile-shell onder `/m/*` met drie schermen — zie sectie *Mobile shell* hieronder.
---
@ -534,10 +534,44 @@ De app is deployable op Vercel + Neon PostgreSQL en lokaal draaibaar met een Neo
/todos (todo-lijst)
/settings (profiel, account, product backlogs, rollen, API-tokens)
/settings/tokens (API-tokenbeheer)
# Mobile-shell (telefoon-UA)
/m/settings (account + product-selector + QR-instructie + logout)
/m/products/:id (Product Backlog — tab-mode op <1024px)
/m/products/:id/solo (Solo Paneel — 3-koloms-kanban met horizontal scroll)
/m/pair (QR-pairing bevestiging — verhuisd uit (app)/ naar (mobile)/)
```
---
## Mobile shell
**Prioriteit:** v1 — voor on-the-go gebruik (PBI-11)
**Persona:** Lars onderweg / tussendoor
**Omschrijving:**
Telefoon-gebruikers (UA met `Mobi`-substring) krijgen een minimale mobile-shell met drie schermen onder `/m/*`. Tablets (iPad, Android-tablet zonder `Mobi`) en desktop blijven het bestaande `/dashboard`-pad volgen. De mobile-shell hergebruikt zoveel mogelijk content-componenten van de desktop-app (PbiList, StoryPanel, TaskPanel, SoloBoard, alle entity-dialogen) — er is geen aparte mobile-implementatie van de business-logica.
**Architectuur in één regel:** eigen route group `app/(mobile)/` met eigen `layout.tsx` (zonder NavBar/StatusBar/MinWidthBanner) — een nested layout in `(app)/m/*` zou de NavBar erven. Auth via gedeelde `lib/auth-guard.ts` `requireSession()`. Zie [`docs/architecture/project-structure.md`](../architecture/project-structure.md) voor de volledige architectuur.
**Acceptatiecriteria:**
- [ ] Phone-UA bij login → `/m/products/[active]/solo` (zonder actief product → `/m/settings`)
- [ ] Tablet-UA en desktop-UA blijven naar `/dashboard`
- [ ] `/m/*` rendert geen NavBar, AppIcon, MinWidthBanner of StatusBar — alleen tab-bar onderaan
- [ ] Portrait-modus toont rotate-overlay; landscape verbergt overlay
- [ ] PWA-manifest verzoekt `landscape`-orientatie (iOS Safari kan dit niet 100% afdwingen — CSS-overlay als fallback)
- [ ] Tab-bar onderaan: Backlog (ListTree), Solo (Activity), Settings — alleen iconen, geen labels, tap-target ≥44×44px
- [ ] Backlog op `<1024px` rendert in tab-mode (tabs: PBI's | Stories | Taken) met click-cascade auto-switch
- [ ] Entity-dialogen (PBI, Story, Task, Task-detail) renderen full-screen op `<640px` via gedeelde `entityDialogContentClasses`
- [ ] Solo-paneel behoudt 3-koloms-kanban met horizontal scroll (geen 1-koloms-mode)
- [ ] Settings: account-info read-only, product-selector activeert + redirect, QR-instructie naar desktop, logout met bevestiging
- [ ] `/m/pair` (QR-pairing-bevestiging) blijft werken — alleen filesystem-locatie verhuisd, URL onveranderd
- [ ] Demo-user op mobile: read-only werkt; logout staat toe
**Bekende limiet:** iOS Safari respecteert `manifest.orientation` niet altijd in PWA-modus — de CSS-overlay (`<LandscapeGuard>`) is de feitelijke afdwinging.
---
## Datamodel (schets)
| Entiteit | Sleutelvelden | Relaties / opmerkingen |