Add app icon assets and project notes
This commit is contained in:
parent
f5b459dadb
commit
899c1af824
10 changed files with 1040 additions and 0 deletions
84
CLAUDE.md
Normal file
84
CLAUDE.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Inspannings Monitor** — a Dutch-language wellness web app for energy planning, daily check-ins, reflection, and self-evaluation. UI and all user-facing text are in Dutch (nl-NL). Release 1 targets individuals only; no sharing, AI features, or medical claims.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
npm run dev # Start dev server (localhost:3000)
|
||||
npm run build # Production build
|
||||
npm run lint # ESLint
|
||||
```
|
||||
|
||||
No test framework is configured yet.
|
||||
|
||||
Node version: `20.9.0` (see `.nvmrc`).
|
||||
|
||||
## Environment Setup
|
||||
|
||||
Copy `.env.example` to `.env.local` and fill in your Supabase project values:
|
||||
|
||||
```
|
||||
NEXT_PUBLIC_SUPABASE_URL=
|
||||
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=
|
||||
```
|
||||
|
||||
Supabase project must have email/password auth enabled with email confirmation. Apply migrations from `supabase/migrations/` to your local/remote DB.
|
||||
|
||||
## Architecture
|
||||
|
||||
**Stack:** Next.js (App Router) + React 19 + TypeScript + Supabase (Auth + PostgreSQL) + shadcn/ui + Tailwind CSS. Deployed on Vercel.
|
||||
|
||||
### Route structure
|
||||
|
||||
| Route | Purpose |
|
||||
|---|---|
|
||||
| `/` | Public landing page |
|
||||
| `/login`, `/sign-up` | Auth pages |
|
||||
| `/auth/confirm` | Email confirmation callback |
|
||||
| `/onboarding` | Mandatory first-time setup |
|
||||
| `/dashboard` | Main protected page |
|
||||
| `/settings` | User preferences |
|
||||
|
||||
### Auth & data flow
|
||||
|
||||
- `lib/auth/` — `getAuthState()` validates the session server-side from SSR cookies. All protected routes call this and redirect unauthenticated users to `/login`.
|
||||
- New users are redirected to `/onboarding`; dashboard redirects there if onboarding is incomplete.
|
||||
- On first protected page load, `profiles` and `user_settings` rows are auto-created with defaults if missing.
|
||||
- Server Actions (`app/**/actions.ts`) handle form mutations; client components call these directly.
|
||||
|
||||
### Database
|
||||
|
||||
Two tables with Row Level Security (users see only their own rows):
|
||||
|
||||
- **`profiles`** — display name, locale, timezone, onboarding completion flags
|
||||
- **`user_settings`** — reminder preferences, energy point visibility
|
||||
|
||||
Migrations live in `supabase/migrations/`.
|
||||
|
||||
### Key lib modules
|
||||
|
||||
- `lib/supabase/` — Supabase client setup (server-side SSR client + proxy config)
|
||||
- `lib/auth/` — session helpers, navigation utilities, Dutch error messages
|
||||
- `lib/profile/service.ts` — CRUD for profiles and user_settings
|
||||
- `lib/profile/types.ts` — shared TypeScript types for profile/settings data
|
||||
- `lib/onboarding/` — onboarding options and timezone lists
|
||||
|
||||
### UI components
|
||||
|
||||
`components/ui/` contains shadcn/ui primitives (button, card, input, select, alert, etc.). Feature-level components live in `components/auth/`, `components/onboarding/`, and `components/settings/`. Path alias `@/*` resolves from the repo root.
|
||||
|
||||
## CI/CD
|
||||
|
||||
GitHub Actions (`.github/workflows/ci.yml`) runs lint + build on every PR and push to `main`. Vercel auto-deploys previews on branches and production on `main`. Production domain: `inspannings-monitor.jp-visser.nl`.
|
||||
|
||||
## Planned next work
|
||||
|
||||
From the backlog (tracked in Linear):
|
||||
- **ST-201** — Morning check-in feature
|
||||
- **ST-203** — Energy budget logic
|
||||
- **ST-301** — Activities data model
|
||||
136
aanbeveling-claude.md
Normal file
136
aanbeveling-claude.md
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
# Aanbevelingen voor Inspannings Monitor
|
||||
|
||||
Dit bestand bevat aanbevelingen gebaseerd op analyse van de broncode, documentatie en backlog (peildatum: 2026-04-18). Bedoeld als leidraad voor toekomstige werksessies.
|
||||
|
||||
---
|
||||
|
||||
## Huidige status
|
||||
|
||||
De volgende onderdelen zijn volledig geïmplementeerd en van goede kwaliteit:
|
||||
|
||||
| Onderdeel | Status |
|
||||
|---|---|
|
||||
| Authenticatie (login, signup, e-mailbevestiging, uitloggen) | ✅ Af |
|
||||
| Beveiligde routes met server-side sessiecontrole | ✅ Af |
|
||||
| Onboarding flow (3 stappen, profiel- en instellingenopslag) | ✅ Af |
|
||||
| Instellingenbeheer (tijdzone, herinneringen, energiepunten) | ✅ Af |
|
||||
| Databaseschema met RLS-beleid | ✅ Af |
|
||||
| CI/CD (GitHub Actions + Vercel) | ✅ Af |
|
||||
| Branchbeveiliging op `main` | ✅ Af |
|
||||
|
||||
---
|
||||
|
||||
## Volgende prioriteiten (volgorde uit backlog)
|
||||
|
||||
### 1. ST-201 — Ochtendcheck-in UI (P0, EPIC-03)
|
||||
Het hart van de applicatie. Zonder check-in heeft het dashboard geen inhoud. Bouwen als:
|
||||
- Energieschuifregelaar (1–10)
|
||||
- Slaapkwaliteitsinput (goed / matig / slecht)
|
||||
- Opslaan in nieuwe tabel `morning_check_ins`
|
||||
- Check-in status tonen op het dashboard (al ingevoerd of nog niet)
|
||||
|
||||
### 2. ST-203 — Budgetberekening (P0, EPIC-03)
|
||||
Zodra de check-in werkt, berekent de app automatisch het dagbudget op basis van de energiescore. Vereist:
|
||||
- Score-naar-budget mapping (formule vastleggen vóór implementatie)
|
||||
- Edge cases: score = 1, score = 10, geen check-in
|
||||
- Eenheidstests voor de berekeningslogica — dit is de enige plek in het project waar tests nu echt urgent zijn
|
||||
|
||||
### 3. ST-301 t/m ST-305 — Dagplanning en energiemeter (P0, EPIC-04)
|
||||
- Activiteiteninvoer met categorie, duur en energiepuntschatting
|
||||
- Lopende energiemeter (resterend budget)
|
||||
- Waarschuwing bij overschrijding (niet-blokkerend)
|
||||
- Vereist nieuwe tabellen: `activities`, `activity_instances`, `activity_categories`
|
||||
|
||||
### 4. ST-401 t/m ST-405 — Evaluatie en dagoverzicht (P0, EPIC-05)
|
||||
- Activiteiten afvinken als voltooid, overgeslagen of aangepast
|
||||
- Dagelijkse samenvatting: gepland vs. werkelijk
|
||||
- Sluit de plan-do-evaluate-lus
|
||||
|
||||
### 5. ST-105 — RLS hardening (P0, EPIC-08, parallel uitvoeren)
|
||||
RLS-beleid is aangemaakt maar nog niet grondig getest. Voer dit parallel uit aan de feature-bouw:
|
||||
- Testscripts schrijven die proberen om andermans rijen te lezen/schrijven
|
||||
- Bevestigen dat `service_role`-key nergens in de frontend of Vercel-configuratie staat
|
||||
|
||||
---
|
||||
|
||||
## Technische schuld
|
||||
|
||||
De huidige code is van goede kwaliteit, maar bevat een aantal verbeterpunten die bij de volgende feature-bouwfase opgepakt kunnen worden — niet nu, maar vóór launch.
|
||||
|
||||
### Matig urgent
|
||||
|
||||
| Probleem | Bestand(en) | Oplossing |
|
||||
|---|---|---|
|
||||
| `getParamValue()` 4× gedupliceerd | `app/*/page.tsx` | Verplaats naar `lib/auth/params.ts` |
|
||||
| `onboarding-flow.tsx` is 343 regels | `components/onboarding/onboarding-flow.tsx` | Splits in drie stapcomponenten |
|
||||
| `settings-form.tsx` dupliceert toestandslogica van onboarding | `components/settings/settings-form.tsx` | Extraheer gedeelde hook |
|
||||
|
||||
### Laag urgent (vóór launch)
|
||||
|
||||
| Probleem | Bestand(en) | Oplossing |
|
||||
|---|---|---|
|
||||
| Geen laadstatus tijdens server actions | `onboarding-flow.tsx`, `settings-form.tsx` | Gebruik `useTransition` + pending-state |
|
||||
| Geen toast/melding na formulieropslaan | Alle clientcomponenten | shadcn/ui `toast` toevoegen |
|
||||
| Booleaanse extractie uit FormData stil faalbaar | `app/**/actions.ts` | Expliciete validatie toevoegen |
|
||||
|
||||
---
|
||||
|
||||
## Risico's en aandachtspunten vóór launch
|
||||
|
||||
### Security
|
||||
- **Oud `service_role`-geheim in git-history**: Sleutel is als gecompromitteerd behandeld en niet meer in gebruik. Git-history opschonen is nog niet gedaan. Voer dit uit vóór publieke launch als extra voorzorgsmaatregel (`git filter-repo` of BFG Repo Cleaner).
|
||||
- **RLS nog niet gehard (ST-105)**: Blokkeer launch totdat dit getest is.
|
||||
- **Rate limiting ontbreekt (ST-701)**: Sign-up en sign-in eindpunten zijn momenteel niet beperkt op applicatieniveau (Supabase heeft eigen throttling, maar expliciete applicatielaag ontbreekt).
|
||||
|
||||
### Privacy
|
||||
- **DPIA nog niet gedaan (ST-803)**: Verplicht vóór launch omdat gezondheidsdata wordt verwerkt, ook al zijn het welzijnsgegevens.
|
||||
- **Copyreview (ST-803)**: Alle teksten moeten door een copycheck — geen medische claims, geen diagnoses, geen therapeutisch advies. Dit is een harde launchpoort.
|
||||
- **Gegevensretentie**: Nog geen beleid of implementatie voor het verwijderen van oude daggegevens.
|
||||
|
||||
### Kwaliteit
|
||||
- **Geen testinfrastructuur**: Er zijn geen tests. Minimaal de budgetberekening (ST-203) en RLS-beleid vereisen geautomatiseerde tests vóór launch.
|
||||
- **Geen toegankelijkheidsaudit (ST-802)**: WCAG 2.1 AA is de norm; nog niet gecontroleerd.
|
||||
|
||||
---
|
||||
|
||||
## Architectuuradvies voor de volgende bouwfase
|
||||
|
||||
### Databaseschema uitbreiden
|
||||
Voeg tabellen toe in deze volgorde (met migraties in `/supabase/migrations/`):
|
||||
1. `morning_check_ins` — energiescore, slaapkwaliteit, berekend budget, datum
|
||||
2. `activity_categories` — referentiedata (naam, standaard energiepunten)
|
||||
3. `activities` — geplande activiteiten per dag per gebruiker
|
||||
4. `activity_instances` — werkelijk uitgevoerd, overgeslagen of aangepast
|
||||
5. `skip_reasons` — optionele referentiedata
|
||||
|
||||
Alle tabellen krijgen RLS-beleid op `user_id = auth.uid()`.
|
||||
|
||||
### Servicelaag uitbreiden (`lib/`)
|
||||
Volg het bestaande patroon in `lib/profile/service.ts`:
|
||||
- Maak `lib/checkin/service.ts` voor ochtendcheck-in logica
|
||||
- Maak `lib/planning/service.ts` voor activiteitenbeheer
|
||||
- Houd serveracties (`app/**/actions.ts`) dun — valideren, delegeren naar service, redirecten
|
||||
|
||||
### Componentstrategie
|
||||
- Gebruik het bestaande shadcn/ui-fundament (`components/ui/`)
|
||||
- Voeg geen nieuwe UI-bibliotheek toe
|
||||
- Bouw feature-componenten in eigen mappen (`components/checkin/`, `components/planning/`, `components/evaluation/`)
|
||||
- Voeg `useTransition` toe zodra een form meer dan één server roundtrip kost
|
||||
|
||||
### Wanneer testen toevoegen
|
||||
- Start met tests bij ST-203 (budgetberekening) — pure functie, makkelijk te testen
|
||||
- Voeg RLS-integratietests toe bij ST-105
|
||||
- Gebruik Vitest (past bij de huidige toolchain, geen extra configuratie nodig naast het toevoegen van het pakket)
|
||||
|
||||
---
|
||||
|
||||
## Samenvatting prioriteitsvolgorde
|
||||
|
||||
```
|
||||
Nu: ST-201 (ochtendcheck-in UI)
|
||||
Dan: ST-203 (budgetlogica + eerste tests)
|
||||
Daarna: ST-301–305 (dagplanning)
|
||||
Daarna: ST-401–405 (evaluatie)
|
||||
Parallel: ST-105 (RLS), ST-701 (rate limiting)
|
||||
Vóór launch: ST-802 (toegankelijkheid), ST-803 (copy + DPIA)
|
||||
```
|
||||
BIN
app/apple-icon.png
Normal file
BIN
app/apple-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
27
app/icon.svg
Normal file
27
app/icon.svg
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="20" y="20" width="472" height="472" rx="116" fill="#F4EFE5"/>
|
||||
<circle
|
||||
cx="256"
|
||||
cy="256"
|
||||
r="144"
|
||||
fill="none"
|
||||
stroke="#20493B"
|
||||
stroke-width="58"
|
||||
stroke-linecap="round"
|
||||
stroke-dasharray="660 210"
|
||||
transform="rotate(-34 256 256)"
|
||||
/>
|
||||
<path
|
||||
d="M164 294C189 274 221 264 256 264C291 264 323 274 348 294"
|
||||
stroke="#D8C3A5"
|
||||
stroke-width="30"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M192 320C210 304 233 296 256 296C279 296 302 304 320 320"
|
||||
stroke="#A7C957"
|
||||
stroke-width="18"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<circle cx="362" cy="171" r="24" fill="#A7C957"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 734 B |
694
docs/generate_testplan.py
Normal file
694
docs/generate_testplan.py
Normal file
|
|
@ -0,0 +1,694 @@
|
|||
from pathlib import Path
|
||||
|
||||
from docx import Document
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.oxml import OxmlElement
|
||||
from docx.oxml.ns import qn
|
||||
from docx.opc.constants import RELATIONSHIP_TYPE as RT
|
||||
from docx.shared import Inches, Pt
|
||||
|
||||
|
||||
BASE_DIR = Path("/Users/janpetervisser/Development/third/docs")
|
||||
PRODUCT_NAME = "Inspannings Monitor"
|
||||
DATE_TEXT = "18 april 2026"
|
||||
POSITIONING = "Wellness/self-management"
|
||||
HOSTING = "Vercel"
|
||||
DATABASE = "Supabase PostgreSQL"
|
||||
AUTH = "Supabase Auth"
|
||||
|
||||
|
||||
def init_doc(title_text: str, subtitle_text: str) -> Document:
|
||||
doc = Document()
|
||||
section = doc.sections[0]
|
||||
section.top_margin = Inches(0.8)
|
||||
section.bottom_margin = Inches(0.8)
|
||||
section.left_margin = Inches(0.8)
|
||||
section.right_margin = Inches(0.8)
|
||||
|
||||
styles = doc.styles
|
||||
styles["Normal"].font.name = "Aptos"
|
||||
styles["Normal"].font.size = Pt(10.5)
|
||||
for style_name in ["Title", "Subtitle", "Heading 1", "Heading 2", "Heading 3"]:
|
||||
styles[style_name].font.name = "Aptos"
|
||||
styles["Title"].font.size = Pt(22)
|
||||
styles["Subtitle"].font.size = Pt(11)
|
||||
styles["Heading 1"].font.size = Pt(15)
|
||||
styles["Heading 2"].font.size = Pt(12.5)
|
||||
styles["Heading 3"].font.size = Pt(11)
|
||||
styles["Heading 1"].font.bold = True
|
||||
styles["Heading 2"].font.bold = True
|
||||
styles["Heading 3"].font.bold = True
|
||||
|
||||
title = doc.add_paragraph(style="Title")
|
||||
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
title.add_run(title_text)
|
||||
subtitle = doc.add_paragraph(style="Subtitle")
|
||||
subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
subtitle.add_run(subtitle_text)
|
||||
doc.add_paragraph("")
|
||||
return doc
|
||||
|
||||
|
||||
def add_hyperlink(paragraph, text: str, url: str) -> None:
|
||||
part = paragraph.part
|
||||
rel_id = part.relate_to(url, RT.HYPERLINK, is_external=True)
|
||||
hyperlink = OxmlElement("w:hyperlink")
|
||||
hyperlink.set(qn("r:id"), rel_id)
|
||||
run = OxmlElement("w:r")
|
||||
run_props = OxmlElement("w:rPr")
|
||||
color = OxmlElement("w:color")
|
||||
color.set(qn("w:val"), "0563C1")
|
||||
run_props.append(color)
|
||||
underline = OxmlElement("w:u")
|
||||
underline.set(qn("w:val"), "single")
|
||||
run_props.append(underline)
|
||||
run.append(run_props)
|
||||
text_elem = OxmlElement("w:t")
|
||||
text_elem.text = text
|
||||
run.append(text_elem)
|
||||
hyperlink.append(run)
|
||||
paragraph._p.append(hyperlink)
|
||||
|
||||
|
||||
def p(doc: Document, text: str = "", style: str = "Normal") -> None:
|
||||
doc.add_paragraph(text, style=style)
|
||||
|
||||
|
||||
def bullets(doc: Document, items) -> None:
|
||||
for item in items:
|
||||
doc.add_paragraph(item, style="List Bullet")
|
||||
|
||||
|
||||
def numbered(doc: Document, items) -> None:
|
||||
for item in items:
|
||||
doc.add_paragraph(item, style="List Number")
|
||||
|
||||
|
||||
def table(doc: Document, headers, rows) -> None:
|
||||
tbl = doc.add_table(rows=1, cols=len(headers))
|
||||
tbl.style = "Table Grid"
|
||||
for idx, header in enumerate(headers):
|
||||
cell = tbl.rows[0].cells[idx]
|
||||
cell.text = header
|
||||
for para in cell.paragraphs:
|
||||
for run in para.runs:
|
||||
run.bold = True
|
||||
for row in rows:
|
||||
cells = tbl.add_row().cells
|
||||
for idx, value in enumerate(row):
|
||||
cells[idx].text = value
|
||||
doc.add_paragraph("")
|
||||
|
||||
|
||||
def set_footer(doc: Document, text: str) -> None:
|
||||
footer = doc.sections[0].footer.paragraphs[0]
|
||||
footer.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
footer.text = text
|
||||
|
||||
|
||||
def build_testplan() -> None:
|
||||
doc = init_doc(
|
||||
f"{PRODUCT_NAME} Testplan v0.2",
|
||||
f"Risicogebaseerde teststrategie, traceability, cybersecurity en QMS-fundament\n{DATE_TEXT}",
|
||||
)
|
||||
|
||||
# ── 1. Documentdoel ──────────────────────────────────────────────────────
|
||||
p(doc, "1. Documentdoel", "Heading 1")
|
||||
p(
|
||||
doc,
|
||||
f"Dit document beschrijft hoe {PRODUCT_NAME} getest wordt. De strategie is verschoven van "
|
||||
"'werkt de feature?' naar 'is het risico beheerst?'. "
|
||||
"Dat onderscheid is relevant zelfs voor een wellness-first product: de app verwerkt gezondheidsgerelateerde "
|
||||
"gegevens van mensen met chronische vermoeidheid, en een fout in de budgetberekening of een lek in de "
|
||||
"datatoegang kan directe gevolgen hebben voor het vertrouwen en zelfmanagement van de gebruiker. "
|
||||
"Dit document combineert risicoanalyse (ISO 14971-principe), verificatie versus validatie, "
|
||||
"een traceability matrix, cybersecurity testing (NEN 7510), ISO 13485-voorbereiding "
|
||||
"en de praktische testpiramide met tooling.",
|
||||
)
|
||||
|
||||
# ── 2. Van feature-test naar risicobeheer ────────────────────────────────
|
||||
p(doc, "2. Teststrategie: van feature-test naar risicobeheer", "Heading 1")
|
||||
p(
|
||||
doc,
|
||||
"Traditionele teststrategie stelt de vraag: 'doet de code wat de developer verwacht?' "
|
||||
"Een risicogebaseerde aanpak stelt de vraag: 'wat zijn de gevolgen als deze functie faalt, en zijn die gevolgen acceptabel?' "
|
||||
"De testinspanning wordt verdeeld op basis van de risicoscore van een functie, niet op basis van complexiteit of backlog-prioriteit.",
|
||||
)
|
||||
table(
|
||||
doc,
|
||||
["Risicoscore", "Betekenis", "Vereiste testdekking"],
|
||||
[
|
||||
["Kritiek", "Fout leidt tot verkeerde zelfmanagementbeslissing of datadiefstal", "100% — unit, integratie én E2E verplicht"],
|
||||
["Hoog", "Fout leidt tot onjuiste weergave of verlies van gebruikersdata", "Volledige unit- en integratietestdekking, E2E aanbevolen"],
|
||||
["Middel", "Fout leidt tot verminderd gebruiksgemak of niet-kritieke weergavefout", "Minimaal unit tests op grenzen, handmatige verificatie"],
|
||||
["Laag", "Cosmetische of zeldzame randgevallen zonder impact op data of beslissingen", "Handmatige QA voldoende"],
|
||||
],
|
||||
)
|
||||
|
||||
# ── 3. Verificatie versus validatie ──────────────────────────────────────
|
||||
p(doc, "3. Verificatie en validatie", "Heading 1")
|
||||
p(
|
||||
doc,
|
||||
"Dit onderscheid is cruciaal bij gezondheidsgerelateerde software en vormt de basis voor "
|
||||
"MDR-documentatieplicht en ISO 13485-conformiteit zodra het product richting een medisch track gaat. "
|
||||
"Ook voor de huidige wellness-first versie geldt dit als kwalitatief fundament.",
|
||||
)
|
||||
table(
|
||||
doc,
|
||||
["Type", "Centrale vraag", "Activiteiten", "Wie"],
|
||||
[
|
||||
[
|
||||
"Verificatie",
|
||||
"Hebben we het product goed gebouwd?",
|
||||
"Unit tests, integratietests, RLS-tests, code review, statische analyse, traceability matrix",
|
||||
"Engineers",
|
||||
],
|
||||
[
|
||||
"Validatie",
|
||||
"Hebben we het juiste product gebouwd voor de gebruiker?",
|
||||
"Usability tests met echte gebruikers, acceptatietests op functionele requirements, klinische evaluatie (toekomstig medisch track)",
|
||||
"Product, UX, (toekomstig) klinisch evaluator",
|
||||
],
|
||||
],
|
||||
)
|
||||
p(
|
||||
doc,
|
||||
"Validatie is niet hetzelfde als E2E-testen. Een geautomatiseerde Playwright-test verifieert dat de app "
|
||||
"technisch correct werkt, maar valideert niet of de gebruiker de energieslider begrijpt of de juiste "
|
||||
"beslissing neemt op basis van weergegeven informatie. Usability tests met echte gebruikers zijn nodig "
|
||||
"voor validatie — dit is een geplande activiteit vóór launch (ST-801).",
|
||||
)
|
||||
|
||||
# ── 4. ISO 13485 en QMS-voorbereiding ────────────────────────────────────
|
||||
p(doc, "4. ISO 13485 en kwaliteitsmanagementsysteem (QMS)", "Heading 1")
|
||||
p(
|
||||
doc,
|
||||
"ISO 13485 is de internationale norm voor kwaliteitsmanagementsystemen voor medische hulpmiddelen. "
|
||||
f"In de huidige wellness-first fase is {PRODUCT_NAME} geen medisch hulpmiddel en is ISO 13485-certificering "
|
||||
"niet vereist. De norm wordt hier als leidraad gebruikt om de kwaliteitsborging al in de goede richting "
|
||||
"op te zetten, zodat de drempel naar een eventuele medische track zo laag mogelijk blijft.",
|
||||
)
|
||||
|
||||
p(doc, "4.1 Relevante ISO 13485-vereisten als leidraad", "Heading 2")
|
||||
table(
|
||||
doc,
|
||||
["ISO 13485-element", "Toepassing in wellness-first fase", "Status"],
|
||||
[
|
||||
["Documentbeheer (par. 4.2)", "Alle specificaties, testplannen en besluitlogs worden beheerd en voorzien van versienummer en datum", "Ingericht via docs/ en git"],
|
||||
["Risicomanagement (par. 7.1, ref. ISO 14971)", "Risicoanalyse per functie uitgevoerd (zie sectie 5)", "In dit document"],
|
||||
["Software-ontwikkelingsproces (par. 7.3)", "Backlog, epics, definition of done en code review zijn aanwezig", "Ingericht via Linear en GitHub"],
|
||||
["Verificatie en validatie (par. 7.3.6 / 7.3.7)", "Testpiramide met traceability matrix per requirement", "In dit document"],
|
||||
["Traceability (par. 7.3.4)", "Traceability matrix koppelt elke FR aan een test en resultaat", "Sectie 6 van dit document"],
|
||||
["Correctieve maatregelen / CAPA (par. 8.5)", "Bugs worden bijgehouden in Linear; ernstige fouten krijgen een root-cause analyse", "Aanbevolen werkwijze"],
|
||||
["Interne audit (par. 8.2.2)", "Periodieke review van testresultaten en traceability matrix vóór elke release", "Aanbevolen als releasepoort"],
|
||||
],
|
||||
)
|
||||
|
||||
p(doc, "4.2 Wanneer is formele ISO 13485-certificering nodig?", "Heading 2")
|
||||
p(
|
||||
doc,
|
||||
"Formele certificering is verplicht wanneer het product wordt geclassificeerd als medisch hulpmiddel "
|
||||
"onder de EU Medical Device Regulation (MDR 2017/745). "
|
||||
"De decision gate hiervoor staat beschreven in het Roadmap-document (doc 04, Gate D en E). "
|
||||
"Zolang het product binnen de wellness-positionering blijft, is certificering niet vereist maar is "
|
||||
"de gestructureerde aanpak uit dit testplan voldoende als kwaliteitsborging.",
|
||||
)
|
||||
bullets(
|
||||
doc,
|
||||
[
|
||||
"Stel nu al een documentregister in (versies, goedkeuringen, archief) — dit is de kern van elk QMS.",
|
||||
"Documenteer afwijkingen en bugs als 'non-conformities' in Linear met root-cause en correctieve actie.",
|
||||
"Voer vóór elke release een interne review uit van de traceability matrix en de testresultaten.",
|
||||
"Zodra het product richting medisch track gaat: stel een formeel QMS-handboek op en start een gap-analyse tegen ISO 13485.",
|
||||
],
|
||||
)
|
||||
|
||||
# ── 5. Risicoanalyse per functie (ISO 14971) ─────────────────────────────
|
||||
p(doc, "5. Risicoanalyse per functie (ISO 14971-principe)", "Heading 1")
|
||||
p(
|
||||
doc,
|
||||
"Voor elke kritieke functie is bepaald wat de ernstigste consequentie van een fout is, "
|
||||
"wat de risicoscore is en welke testdekking verplicht is. "
|
||||
"Risico = kans op optreden × ernst van de gevolgen. "
|
||||
"Kans wordt bepaald door code-complexiteit en het ontbreken van type-veiligheid of validatie.",
|
||||
)
|
||||
table(
|
||||
doc,
|
||||
["Functie / module", "Ernstigste fout", "Ernst", "Kans zonder tests", "Risicoscore", "Vereiste dekking"],
|
||||
[
|
||||
[
|
||||
"Budgetberekening (score → energyLevel + dailyBudget)",
|
||||
"Gebruiker krijgt verkeerd budget; plant meer dan verantwoord is",
|
||||
"Hoog",
|
||||
"Middel",
|
||||
"Kritiek",
|
||||
"100% unit tests op alle grenswaarden + integratietest op opgeslagen output",
|
||||
],
|
||||
[
|
||||
"RLS-beleid (owner-only datatoegang)",
|
||||
"Gebruiker A leest of overschrijft data van gebruiker B",
|
||||
"Kritiek",
|
||||
"Middel",
|
||||
"Kritiek",
|
||||
"100% pgTAP-tests op alle tabellen, alle operaties, alle rollen",
|
||||
],
|
||||
[
|
||||
"Authenticatie en sessiebeheer",
|
||||
"Niet-ingelogde gebruiker krijgt toegang tot dashboard of data",
|
||||
"Hoog",
|
||||
"Laag",
|
||||
"Hoog",
|
||||
"E2E-test op elke beveiligde route; integratietest op getAuthState()",
|
||||
],
|
||||
[
|
||||
"Zod-invoervalidatie (server actions)",
|
||||
"Ongeldige of kwaadaardige invoer bereikt de database",
|
||||
"Hoog",
|
||||
"Middel",
|
||||
"Hoog",
|
||||
"Unit tests op schema-grenzen; integratietest op server action met ongeldige invoer",
|
||||
],
|
||||
[
|
||||
"Insightregels en weekpatronen",
|
||||
"Gebruiker trekt verkeerde conclusie op basis van onjuist patroon",
|
||||
"Middel",
|
||||
"Middel",
|
||||
"Hoog",
|
||||
"Unit tests op aggregatiefuncties; datadrempellogica getest op minimum-threshold",
|
||||
],
|
||||
[
|
||||
"Reflectieprompt-planning (T+1/T+2 job)",
|
||||
"Dubbele of gemiste prompts",
|
||||
"Laag",
|
||||
"Middel",
|
||||
"Middel",
|
||||
"Integratietest op idempotentie van joblogica",
|
||||
],
|
||||
[
|
||||
"Navigatie-sanitisatie (open redirect)",
|
||||
"Gebruiker wordt doorgestuurd naar externe kwaadaardige URL",
|
||||
"Hoog",
|
||||
"Laag",
|
||||
"Hoog",
|
||||
"Unit tests inclusief double-slash en externe URL-patronen",
|
||||
],
|
||||
[
|
||||
"Copy en insighttekst (wellness vs. medisch)",
|
||||
"Regulatoire interpretatie als medisch hulpmiddel",
|
||||
"Hoog (regulatoir)",
|
||||
"Middel",
|
||||
"Hoog",
|
||||
"Handmatige copyreview vóór elke release; geautomatiseerde woordlijstcheck overwegen",
|
||||
],
|
||||
],
|
||||
)
|
||||
|
||||
# ── 6. Traceability matrix ───────────────────────────────────────────────
|
||||
p(doc, "6. Traceability matrix", "Heading 1")
|
||||
p(
|
||||
doc,
|
||||
"De traceability matrix koppelt elke functionele requirement (FR-ID) aan de tests die aantonen dat de eis "
|
||||
"is geverifieerd. Dit is een kernvereiste voor MDR-conformiteit, ISO 13485 (par. 7.3.4) en voor "
|
||||
"auditeerbare kwaliteitsborging. De matrix wordt bij elke release bijgewerkt met het testresultaat. "
|
||||
"Mislukte tests blokkeren de release. Overgeslagen tests vereisen een expliciete risicoafweging.",
|
||||
)
|
||||
table(
|
||||
doc,
|
||||
["Requirement ID", "Omschrijving (kort)", "Testtype", "Test ID / bestand", "Risicoscore", "Resultaat"],
|
||||
[
|
||||
["FR-CHK-001", "Check-in opslaan met energiescore en slaapkwaliteit", "E2E", "e2e/checkin.spec.ts — happy path", "Middel", "—"],
|
||||
["FR-CHK-002", "Dagbudget afleiden uit energiescore", "Unit + Integratie", "lib/checkin/__tests__/budget.test.ts", "Kritiek", "—"],
|
||||
["FR-PLAN-001", "Activiteit plannen met verplichte velden", "E2E", "e2e/planning.spec.ts — aanmaken", "Middel", "—"],
|
||||
["FR-PLAN-002", "Lopend totaal bijwerken na mutatie", "Integratie + E2E", "lib/planning/__tests__/meter.test.ts", "Hoog", "—"],
|
||||
["FR-PLAN-003", "Niet-blokkerende waarschuwing bij overschrijding", "E2E", "e2e/planning.spec.ts — budget overschrijden", "Middel", "—"],
|
||||
["FR-ACT-001", "Activiteit als uitgevoerd markeren", "E2E", "e2e/planning.spec.ts — uitgevoerd", "Middel", "—"],
|
||||
["FR-ACT-002", "Activiteit als geskipt markeren met reden", "E2E", "e2e/planning.spec.ts — geskipt", "Middel", "—"],
|
||||
["FR-ACT-003", "Activiteit als aangepast markeren", "E2E", "e2e/planning.spec.ts — aangepast", "Middel", "—"],
|
||||
["FR-ACT-005", "Ongeplande activiteit toevoegen", "Integratie + E2E", "lib/planning/__tests__/service.test.ts", "Middel", "—"],
|
||||
["FR-DAY-001", "Gepland versus uitgevoerd tonen in dagoverzicht", "E2E", "e2e/planning.spec.ts — dagoverzicht", "Middel", "—"],
|
||||
["FR-WEEK-001", "Weekoverzicht met gemiddelde energie en adherence", "Unit + Integratie", "lib/insights/__tests__/week.test.ts", "Hoog", "—"],
|
||||
["FR-INS-001", "Inzicht alleen tonen bij minimale data", "Unit", "lib/insights/__tests__/thresholds.test.ts", "Hoog", "—"],
|
||||
["FR-REM-001", "Reflectieprompts per gebruiker aan/uit", "Integratie", "lib/reflection/__tests__/service.test.ts", "Middel", "—"],
|
||||
["FR-SET-001", "Instellingen opslaan en direct actief", "E2E", "e2e/settings.spec.ts", "Middel", "—"],
|
||||
["SEC-001", "TLS op alle communicatie", "Handmatig / infra", "SSL Labs check op productiedomain", "Hoog", "—"],
|
||||
["SEC-004", "Rate limiting op auth-routes", "Integratie", "lib/auth/__tests__/ratelimit.test.ts", "Hoog", "—"],
|
||||
["SEC-RLS-001", "Owner-only SELECT op profiles", "pgTAP", "supabase/tests/test_profiles_rls.sql", "Kritiek", "—"],
|
||||
["SEC-RLS-002", "Owner-only SELECT op user_settings", "pgTAP", "supabase/tests/test_user_settings_rls.sql", "Kritiek", "—"],
|
||||
["SEC-RLS-003", "Owner-only SELECT op morning_check_ins", "pgTAP", "supabase/tests/test_morning_check_ins_rls.sql", "Kritiek", "—"],
|
||||
["SEC-RLS-004", "Owner-only SELECT op activities", "pgTAP", "supabase/tests/test_activities_rls.sql", "Kritiek", "—"],
|
||||
["SAFE-001", "Geen medische taal in UI-copy", "Handmatig copyreview", "Checklist ST-803", "Hoog", "—"],
|
||||
["SAFE-003", "Inzichten tonen minimale-dataguardrail", "Unit", "lib/insights/__tests__/thresholds.test.ts", "Hoog", "—"],
|
||||
],
|
||||
)
|
||||
|
||||
# ── 7. Cybersecurity testing (NEN 7510) ──────────────────────────────────
|
||||
p(doc, "7. Cybersecurity testing (NEN 7510)", "Heading 1")
|
||||
p(
|
||||
doc,
|
||||
"NEN 7510 is de Nederlandse norm voor informatiebeveiliging in de zorg. "
|
||||
f"Hoewel {PRODUCT_NAME} een wellness-product is, verwerkt de app gezondheidsgerelateerde persoonsgegevens. "
|
||||
"De NEN 7510-baseline wordt als toetssteen gebruikt om privacyrisico's te beheersen en de "
|
||||
"drempel naar een toekomstige medische track te verlagen.",
|
||||
)
|
||||
|
||||
p(doc, "7.1 Encryptie en datatransport", "Heading 2")
|
||||
table(
|
||||
doc,
|
||||
["Eis", "Norm", "Testmethode", "Acceptatiecriterium"],
|
||||
[
|
||||
["TLS 1.2 of hoger op alle routes", "NEN 7510 / SEC-001", "SSL Labs op productiedomain", "A-rating, geen TLS 1.0/1.1"],
|
||||
["Data at rest versleuteld (AES-256)", "NEN 7510 / SEC-002", "Supabase-dashboard encryptie-instellingen", "AES-256 bevestigd"],
|
||||
["Geen gevoelige data in URL-parameters", "OWASP / NEN 7510", "Handmatige review alle redirects", "Geen tokens of gezondheidsdata in URL"],
|
||||
["HTTPS-only, geen mixed content", "SEC-001", "Content-Security-Policy header check", "Geen HTTP-requests in productie"],
|
||||
["Veilige cookie-attributen", "OWASP Session Management", "Browser-devtools of Playwright-test", "Secure, HttpOnly, SameSite aanwezig"],
|
||||
],
|
||||
)
|
||||
|
||||
p(doc, "7.2 Authenticatie en toegangscontrole", "Heading 2")
|
||||
table(
|
||||
doc,
|
||||
["Eis", "Norm", "Testmethode", "Acceptatiecriterium"],
|
||||
[
|
||||
["Brute-force bescherming op login", "NEN 7510 / SEC-004", "Integratietest: >10 snelle pogingen triggert rate limit", "429-respons of vertraging"],
|
||||
["Sessie vervalt na inactiviteit", "NEN 7510 / SEC-003", "Handmatig: sessie na 24 uur controleren", "Gebruiker moet opnieuw inloggen"],
|
||||
["Geen sessietokens in localStorage", "OWASP", "Browser-devtools na login", "Geen tokens in localStorage"],
|
||||
["Beveiligde routes zonder sessie", "SEC-003", "Playwright-test: dashboard zonder cookie", "Redirect naar /login"],
|
||||
["Owner-only datatoegang (RLS)", "NEN 7510 / SEC-002", "pgTAP-tests op alle tabellen", "Zie traceability SEC-RLS-001 t/m 004"],
|
||||
],
|
||||
)
|
||||
|
||||
p(doc, "7.3 Penetratietest", "Heading 2")
|
||||
table(
|
||||
doc,
|
||||
["Testvorm", "Scope", "Timing", "Uitvoerder"],
|
||||
[
|
||||
["Geautomatiseerde OWASP Top 10 scan", "Alle publieke en beveiligde routes", "Vóór launch R1", "OWASP ZAP of Burp Suite Community"],
|
||||
["Handmatig: SQL-injectie via formulieren", "Alle invoervelden die server actions aanroepen", "Vóór launch R1", "Engineer of security reviewer"],
|
||||
["Handmatig: IDOR (cross-user data access)", "Alle calls met user-specifieke IDs", "Vóór launch R1", "Engineer"],
|
||||
["Formele pentest door externe partij", "Volledige applicatie", "Vóór medische track", "Gecertificeerde pentest-partij"],
|
||||
],
|
||||
)
|
||||
p(
|
||||
doc,
|
||||
"IDOR-testprocedure: log in als gebruiker A, kopieer een record-ID (bijv. activity ID), "
|
||||
"log in als gebruiker B en probeer dat record op te halen of te muteren via directe API-aanroep. "
|
||||
"Verwacht resultaat: 404 of 403, nooit de data van gebruiker A.",
|
||||
)
|
||||
|
||||
p(doc, "7.4 Security headers", "Heading 2")
|
||||
table(
|
||||
doc,
|
||||
["Header", "Aanbevolen waarde", "Testmethode"],
|
||||
[
|
||||
["Content-Security-Policy", "Strikte policy, inline scripts beperkt", "securityheaders.com"],
|
||||
["Strict-Transport-Security", "max-age=31536000; includeSubDomains", "curl -I op productiedomain"],
|
||||
["X-Frame-Options", "DENY of SAMEORIGIN", "securityheaders.com"],
|
||||
["X-Content-Type-Options", "nosniff", "securityheaders.com"],
|
||||
["Referrer-Policy", "strict-origin-when-cross-origin", "securityheaders.com"],
|
||||
],
|
||||
)
|
||||
p(doc, "Security headers worden geconfigureerd in next.config.ts via de headers()-functie.", "Normal")
|
||||
|
||||
p(doc, "7.5 Logging en auditability (NEN 7510)", "Heading 2")
|
||||
table(
|
||||
doc,
|
||||
["Te loggen event", "Minimale informatie", "Testmethode"],
|
||||
[
|
||||
["Mislukte loginpoging", "Tijdstip, IP, e-mailadres (geanonimiseerd)", "Integratietest: mislukte login triggert logentry"],
|
||||
["Succesvolle login", "Tijdstip, userId", "Integratietest: succesvolle login triggert logentry"],
|
||||
["Accountverwijdering", "Tijdstip, userId", "Handmatige verificatie"],
|
||||
["Sessie-timeout", "Tijdstip, userId", "Handmatige verificatie"],
|
||||
],
|
||||
)
|
||||
|
||||
# ── 8. Testpiramide en tooling ───────────────────────────────────────────
|
||||
p(doc, "8. Testpiramide en tooling", "Heading 1")
|
||||
table(
|
||||
doc,
|
||||
["Laag", "Wat wordt getest", "Framework", "Wanneer"],
|
||||
[
|
||||
["Unit", "Pure functies, berekeningslogica, Zod-schema's, hulpfuncties", "Vitest", "Bij elke commit"],
|
||||
["Integratie", "Servicelaag, server actions, Supabase-queries", "Vitest + echte Supabase", "Bij elke commit"],
|
||||
["Database / RLS", "RLS-beleid direct in PostgreSQL", "pgTAP via supabase test db", "Bij elke commit"],
|
||||
["End-to-end", "Volledige gebruikersflows in echte browser", "Playwright", "Bij PR naar main"],
|
||||
["Cybersecurity", "OWASP Top 10, headers, encryptie, IDOR", "ZAP + handmatig", "Vóór elke release"],
|
||||
["Handmatig / validatie", "Usability, toegankelijkheid, copy, regressie", "Checklist", "Vóór elke release"],
|
||||
],
|
||||
)
|
||||
|
||||
p(doc, "8.1 Vitest — unit en integratietests", "Heading 2")
|
||||
p(doc, "npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/dom vite-tsconfig-paths", "Normal")
|
||||
p(doc, "npx vitest — watch mode", "Normal")
|
||||
p(doc, "npx vitest run — eenmalig alle tests", "Normal")
|
||||
p(doc, "npx vitest run lib/checkin/__tests__/budget.test.ts — één bestand", "Normal")
|
||||
|
||||
p(doc, "8.2 Playwright — end-to-end tests", "Heading 2")
|
||||
p(
|
||||
doc,
|
||||
"Authenticatie verloopt programmatisch via de Supabase Auth REST API (global-setup.ts). "
|
||||
"Het token wordt eenmalig opgehaald en hergebruikt als cookie-state voor alle tests.",
|
||||
)
|
||||
p(doc, "npm install -D @playwright/test && npx playwright install", "Normal")
|
||||
p(doc, "npx playwright test — alle E2E-tests", "Normal")
|
||||
p(doc, "npx playwright test --workers=1 — bij Supabase connection-limiet in CI", "Normal")
|
||||
|
||||
p(doc, "8.3 Zod — runtime-validatie en testschema's", "Heading 2")
|
||||
p(doc, "npm install zod && npm install -D zod-fixture", "Normal")
|
||||
p(
|
||||
doc,
|
||||
"Zod-schema's in lib/*/schemas.ts worden hergebruikt in server actions én client-componenten. "
|
||||
"zod-fixture genereert automatisch testfixtures vanuit het schema.",
|
||||
)
|
||||
|
||||
p(doc, "8.4 pgTAP — RLS en database-tests", "Heading 2")
|
||||
p(doc, "supabase test db — voert alle .sql-testbestanden in supabase/tests/ uit", "Normal")
|
||||
p(
|
||||
doc,
|
||||
"pgTAP draait direct in PostgreSQL op hetzelfde uitvoerniveau als productieverkeer. "
|
||||
"Dit is de enige methode die RLS-gedrag betrouwbaar verifieert.",
|
||||
)
|
||||
|
||||
# ── 9. Unit tests ────────────────────────────────────────────────────────
|
||||
p(doc, "9. Unit tests", "Heading 1")
|
||||
|
||||
p(doc, "9.1 Budgetberekening (risicoscore: Kritiek)", "Heading 2")
|
||||
table(
|
||||
doc,
|
||||
["Testgeval", "Input", "Verwacht resultaat"],
|
||||
[
|
||||
["Minimale score", "energyScore = 1", "energyLevel = 'very_low', dailyBudget = minimumwaarde"],
|
||||
["Maximale score", "energyScore = 10", "energyLevel = 'high', dailyBudget = maximumwaarde"],
|
||||
["Elke grenswaarde", "Score op elke overgangswaarde", "Correct niveau en budget"],
|
||||
["Deterministisch", "Zelfde score twee keer", "Altijd identiek resultaat"],
|
||||
["Ongeldige invoer", "energyScore = 0, 11, -1, 'hoog'", "ZodError vóór berekening"],
|
||||
],
|
||||
)
|
||||
|
||||
p(doc, "9.2 Zod-schema's per domein", "Heading 2")
|
||||
table(
|
||||
doc,
|
||||
["Schema", "Locatie", "Te testen gevallen"],
|
||||
[
|
||||
["MorningCheckInSchema", "lib/checkin/schemas.ts", "Geldige check-in, score buiten bereik, te lange notitie"],
|
||||
["OnboardingSubmissionSchema", "lib/onboarding/schemas.ts", "Geldige onboarding, ongeldige tijdzone"],
|
||||
["SettingsSubmissionSchema", "lib/profile/schemas.ts", "Geldige settings, ongeldige herinneringstijd"],
|
||||
["PlannedActivitySchema", "lib/planning/schemas.ts", "Geldige activiteit, negatieve energiepunten"],
|
||||
],
|
||||
)
|
||||
|
||||
p(doc, "9.3 Navigatie-utilities (risicoscore: Hoog)", "Heading 2")
|
||||
table(
|
||||
doc,
|
||||
["Functie", "Bestand", "Te testen gevallen"],
|
||||
[
|
||||
["sanitizeNextPath()", "lib/auth/navigation.ts", "Geldig pad, dubbele slash (//evil.com), externe URL, leeg pad"],
|
||||
["buildPathWithQuery()", "lib/auth/navigation.ts", "Geen params, meerdere params, speciale tekens"],
|
||||
["getAuthNotice()", "lib/auth/messages.ts", "Bekende foutcode, onbekende code, statuscode"],
|
||||
],
|
||||
)
|
||||
|
||||
# ── 10. Integratietests ──────────────────────────────────────────────────
|
||||
p(doc, "10. Integratietests", "Heading 1")
|
||||
p(
|
||||
doc,
|
||||
"Integratietests gebruiken een echte Supabase-testdatabase. Mocks worden niet ingezet voor de databaselaag: "
|
||||
"mock-gedrag verschilt van productiegedrag en verbergt RLS- en queryfouten.",
|
||||
)
|
||||
table(
|
||||
doc,
|
||||
["Test", "Doel", "Risicoscore"],
|
||||
[
|
||||
["getProfileBundleForCurrentUser()", "Retourneert gecombineerd profiel en settings", "Hoog"],
|
||||
["ensureProfileBundleForCurrentUser()", "Maakt records aan als ze niet bestaan (bootstrap)", "Hoog"],
|
||||
["createMorningCheckIn() — geldig", "Check-in opgeslagen, budget berekend en teruggegeven", "Kritiek"],
|
||||
["createMorningCheckIn() — ongeldige invoer", "Zod fout vóór databaseschrijf", "Kritiek"],
|
||||
["Rate limit: >10 snelle loginpogingen", "429-respons of vertraging aantoonbaar", "Hoog"],
|
||||
["Reflectie-job idempotentie", "Dubbel uitvoeren geeft geen dubbele prompts", "Middel"],
|
||||
],
|
||||
)
|
||||
|
||||
# ── 11. RLS en security tests ────────────────────────────────────────────
|
||||
p(doc, "11. RLS en security tests (pgTAP)", "Heading 1")
|
||||
p(
|
||||
doc,
|
||||
"Elke tabel met gebruikersdata krijgt een eigen testbestand. "
|
||||
"Tests worden uitgevoerd als de 'authenticated'-rol met een gesimuleerd JWT. "
|
||||
"De SQL Editor-rol mag nooit worden gebruikt, omdat die RLS omzeilt.",
|
||||
)
|
||||
table(
|
||||
doc,
|
||||
["Testgeval", "Te verifiëren"],
|
||||
[
|
||||
["SELECT eigen rij", "Gebruiker A ziet alleen zijn eigen record"],
|
||||
["SELECT andermans rij (IDOR)", "Gebruiker A ziet 0 rijen van gebruiker B"],
|
||||
["INSERT voor zichzelf", "Aanmaken eigen record lukt"],
|
||||
["INSERT voor een ander", "RLS-fout of 0 rows inserted"],
|
||||
["UPDATE eigen rij", "Eigen record aanpassen lukt"],
|
||||
["UPDATE andermans rij", "0 rows updated"],
|
||||
["DELETE eigen rij", "Verwijderen eigen record lukt"],
|
||||
["DELETE andermans rij", "0 rows deleted"],
|
||||
["Unauthenticated (geen JWT)", "0 rijen, geen informatielekking in foutmelding"],
|
||||
],
|
||||
)
|
||||
|
||||
# ── 12. E2E tests ────────────────────────────────────────────────────────
|
||||
p(doc, "12. End-to-end tests (Playwright)", "Heading 1")
|
||||
table(
|
||||
doc,
|
||||
["Flow", "Kritieke assertions", "Risicoscore"],
|
||||
[
|
||||
["Registratie + e-mailbevestiging", "Dashboard bereikbaar na bevestiging", "Hoog"],
|
||||
["Inloggen", "Dashboard zichtbaar, profiel aanwezig", "Hoog"],
|
||||
["Beveiligde route zonder sessie", "Redirect naar /login", "Kritiek"],
|
||||
["IDOR-poging (cross-user)", "Gebruiker B kan data van A niet ophalen", "Kritiek"],
|
||||
["Ochtendcheck-in", "Budget en energieniveau zichtbaar na opslaan", "Kritiek"],
|
||||
["Activiteit plannen + energiemeter", "Meter update direct, activiteit in overzicht", "Hoog"],
|
||||
["Activiteit uitgevoerd / geskipt / aangepast", "Status correct in dagoverzicht", "Middel"],
|
||||
["Instellingen wijzigen", "Succesbericht, instelling persistent na herlaad", "Middel"],
|
||||
["Uitloggen", "Dashboard onbereikbaar na uitloggen", "Hoog"],
|
||||
],
|
||||
)
|
||||
|
||||
# ── 13. Testdata-management ──────────────────────────────────────────────
|
||||
p(doc, "13. Testdata-management", "Heading 1")
|
||||
bullets(
|
||||
doc,
|
||||
[
|
||||
"Elke E2E-test maakt eigen testdata aan of hergebruikt een dedicated testaccount. Nooit productiedata.",
|
||||
"Unit- en integratietests zijn stateless: geen gedeelde databaserecords.",
|
||||
"Gebruik zod-fixture om valide testobjecten te genereren vanuit Zod-schema's.",
|
||||
"Na integratietests worden records opgeruimd in afterEach of afterAll.",
|
||||
"Seed-scripts voor statische referentiedata staan in supabase/seed.sql.",
|
||||
"Gebruik een apart Supabase-testproject voor CI.",
|
||||
],
|
||||
)
|
||||
|
||||
# ── 14. CI/CD-integratie ─────────────────────────────────────────────────
|
||||
p(doc, "14. CI/CD-integratie", "Heading 1")
|
||||
table(
|
||||
doc,
|
||||
["Job", "Trigger", "Stappen", "Blokkeerend voor merge"],
|
||||
[
|
||||
["Lint en build", "PR en push naar main", "npm ci, npm run lint, npm run build", "Ja"],
|
||||
["Unit en integratie", "PR en push naar main", "npm ci, npx vitest run, supabase test db", "Ja"],
|
||||
["E2E", "PR naar main", "npm ci, npx playwright install, npx playwright test --workers=1", "Ja"],
|
||||
["Security headers check", "PR naar main", "curl-check op staging-URL", "Ja"],
|
||||
],
|
||||
)
|
||||
p(
|
||||
doc,
|
||||
"Mislukte tests blokkeren de merge. Overgeslagen tests vereisen expliciete goedkeuring "
|
||||
"inclusief gedocumenteerde risicoafweging (ISO 13485-principe: non-conformity met CAPA).",
|
||||
)
|
||||
|
||||
# ── 15. Bestandsstructuur ────────────────────────────────────────────────
|
||||
p(doc, "15. Bestandsstructuur", "Heading 1")
|
||||
table(
|
||||
doc,
|
||||
["Pad", "Inhoud"],
|
||||
[
|
||||
["lib/checkin/__tests__/budget.test.ts", "Unit tests budgetberekening (risico: Kritiek)"],
|
||||
["lib/checkin/schemas.ts", "Zod-schema MorningCheckIn"],
|
||||
["lib/auth/__tests__/navigation.test.ts", "Unit tests open-redirect-preventie (risico: Hoog)"],
|
||||
["lib/profile/__tests__/service.test.ts", "Integratietests profileservice"],
|
||||
["supabase/tests/test_profiles_rls.sql", "pgTAP RLS-tests profiles"],
|
||||
["supabase/tests/test_user_settings_rls.sql", "pgTAP RLS-tests user_settings"],
|
||||
["supabase/tests/test_morning_check_ins_rls.sql", "pgTAP RLS-tests check-ins"],
|
||||
["supabase/tests/test_activities_rls.sql", "pgTAP RLS-tests activiteiten"],
|
||||
["e2e/auth.spec.ts", "Playwright: registratie, login, uitloggen, IDOR-poging"],
|
||||
["e2e/checkin.spec.ts", "Playwright: ochtendcheck-in"],
|
||||
["e2e/planning.spec.ts", "Playwright: activiteiten plannen en evalueren"],
|
||||
["e2e/settings.spec.ts", "Playwright: instellingen"],
|
||||
["playwright/global-setup.ts", "Programmatische Supabase-login, sessiestatus opslaan"],
|
||||
["playwright.config.ts", "Playwright-configuratie incl. auth-setup en workers"],
|
||||
["docs/traceability-matrix.md", "Levend document: FR-ID → test → resultaat per release"],
|
||||
],
|
||||
)
|
||||
|
||||
# ── 16. Acceptatiecriteria en Definition of Done ─────────────────────────
|
||||
p(doc, "16. Acceptatiecriteria en Definition of Done", "Heading 1")
|
||||
table(
|
||||
doc,
|
||||
["Laag", "Minimale eis voor launch"],
|
||||
[
|
||||
["Unit (Kritiek)", "Budgetberekening: 100% dekking op alle grenswaarden en ongeldige invoer."],
|
||||
["Unit (Hoog)", "Zod-schema's getest op geldige en ongeldige invoer. Navigatie-utilities getest op open-redirect."],
|
||||
["Integratie", "Profileservice: happy path en bootstrap. Check-in service: opslaan + budgetoutput. Rate limiting aantoonbaar actief."],
|
||||
["RLS (pgTAP)", "Alle tabellen: SELECT/INSERT/UPDATE/DELETE als owner, als andere gebruiker en zonder sessie getest."],
|
||||
["E2E", "Login, check-in, planning, evaluatie en uitloggen geautomatiseerd. IDOR-poging geeft geen data. Beveiligde route zonder sessie redirect."],
|
||||
["Cybersecurity", "OWASP Top 10 scan zonder kritieke bevindingen. Security headers A-rating. AES-256 data at rest bevestigd."],
|
||||
["Traceability (ISO 13485)", "Alle FR-IDs zijn gekoppeld aan een test. Kritieke FR's hebben gedocumenteerd testresultaat."],
|
||||
["QMS", "Mislukte tests gedocumenteerd als non-conformity in Linear. Interne release-review van traceability matrix uitgevoerd."],
|
||||
["Validatie (handmatig)", "Kernflows geverifieerd op mobiel. Usability test met minimaal 1 echte gebruiker. Copy getoetst op niet-medische formulering."],
|
||||
],
|
||||
)
|
||||
|
||||
# ── 17. Bewuste keuzes ───────────────────────────────────────────────────
|
||||
p(doc, "17. Bewuste keuzes en afwegingen", "Heading 1")
|
||||
table(
|
||||
doc,
|
||||
["Keuze", "Alternatief", "Reden"],
|
||||
[
|
||||
["Echte Supabase in integratietests", "Gemockte client", "Mocks verbergen RLS- en querygedrag. Mock/prod-divergentie is een bewezen risico."],
|
||||
["pgTAP voor RLS", "Applicatielaag-tests", "RLS draait in de database; pgTAP test op hetzelfde uitvoerniveau."],
|
||||
["Risicogebaseerde prioritering (ISO 14971)", "Gelijke dekking per module", "Testcapaciteit wordt ingezet waar de gevolgen van een fout het grootst zijn."],
|
||||
["Traceability matrix (ISO 13485)", "Geen formele koppeling", "Vereiste voor auditeerbare kwaliteitsborging en MDR-voorbereiding."],
|
||||
["NEN 7510 als toetssteen nu al", "Alleen OWASP", "Verlaagt drempel naar medische track, vermindert privacyrisico's bij gezondheidsdata."],
|
||||
["ISO 13485 als leidraad (niet gecertificeerd)", "Geen QMS-structuur", "Bouwt documentregister en non-conformity-aanpak op zonder onnodige overhead."],
|
||||
["Programmatische Playwright-login", "UI-login per test", "Sneller, minder foutgevoelig, scheidt auth-test van feature-test."],
|
||||
],
|
||||
)
|
||||
|
||||
# ── 18. Externe referenties ──────────────────────────────────────────────
|
||||
p(doc, "18. Externe referenties", "Heading 1")
|
||||
references = [
|
||||
("Next.js Testing Guide — Vitest", "https://nextjs.org/docs/app/guides/testing/vitest"),
|
||||
("Next.js Testing Guide — Playwright", "https://nextjs.org/docs/app/guides/testing/playwright"),
|
||||
("Supabase Testing Overview", "https://supabase.com/docs/guides/local-development/testing/overview"),
|
||||
("pgTAP documentatie", "https://pgtap.org/"),
|
||||
("Zod documentatie", "https://zod.dev/"),
|
||||
("zod-fixture — testdata genereren vanuit Zod-schema's", "https://github.com/timdeschryver/zod-fixture"),
|
||||
("Playwright — Supabase auth via REST API", "https://mokkapps.de/blog/login-at-supabase-via-rest-api-in-playwright-e2e-test"),
|
||||
("Playwright — opslaan en hergebruiken van auth-state", "https://playwright.dev/docs/auth"),
|
||||
("ISO 14971 — Risicomanagement voor medische hulpmiddelen", "https://www.iso.org/standard/72704.html"),
|
||||
("ISO 13485 — Kwaliteitsmanagementsystemen voor medische hulpmiddelen", "https://www.iso.org/standard/59752.html"),
|
||||
("NEN 7510 — Informatiebeveiliging in de zorg", "https://www.nen.nl/nen-7510-1-2017-nl-237552"),
|
||||
("OWASP Top 10 — Meest kritieke webapplicatierisico's", "https://owasp.org/www-project-top-ten/"),
|
||||
("OWASP ZAP — Geautomatiseerde securityscanner", "https://www.zaproxy.org/"),
|
||||
("SSL Labs — TLS-configuratiecheck", "https://www.ssllabs.com/ssltest/"),
|
||||
("securityheaders.com — HTTP security headers checker", "https://securityheaders.com/"),
|
||||
("EU MDR 2017/745 — Medical Device Regulation", "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32017R0745"),
|
||||
]
|
||||
for name, url in references:
|
||||
para = doc.add_paragraph(style="List Bullet")
|
||||
para.add_run(f"{name}: ")
|
||||
add_hyperlink(para, url, url)
|
||||
|
||||
set_footer(doc, f"{PRODUCT_NAME} Testplan v0.2")
|
||||
doc.save(BASE_DIR / "inspannings-monitor-07-testplan-v01.docx")
|
||||
print("Testplan v0.2 gegenereerd.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
BASE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
build_testplan()
|
||||
29
docs/icon-concepts/README.md
Normal file
29
docs/icon-concepts/README.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Icon-concepten voor Inspannings Monitor
|
||||
|
||||
Deze map bevat drie eerste SVG-concepten voor de gekozen richting
|
||||
`rustige energiering`.
|
||||
|
||||
## Gekozen richting
|
||||
|
||||
`Concept 03` is gekozen als basis voor de app. De uitgewerkte master staat nu
|
||||
in [app/icon.svg](/Users/janpetervisser/Development/third/app/icon.svg).
|
||||
|
||||
## Concepten
|
||||
|
||||
- `concept-01-open-ring.svg`
|
||||
Een heldere open ring met een zacht accentpunt. Dit is de meest directe en
|
||||
rustige variant.
|
||||
|
||||
- `concept-02-double-orbit.svg`
|
||||
Een open ring met een tweede lichte baan. Dit voelt iets dynamischer en meer
|
||||
als pacing of beweging in lagen.
|
||||
|
||||
- `concept-03-horizon-ring.svg`
|
||||
Een open ring met een zachte binnenboog. Dit legt meer nadruk op ritme en een
|
||||
kalme daglijn.
|
||||
|
||||
## Opmerking
|
||||
|
||||
Dit zijn nog geen definitieve app-icon exports, maar SVG-masters op
|
||||
`512x512` formaat. Vanuit de gekozen richting kunnen later `favicon`,
|
||||
`apple-icon` en `app icon` varianten worden geëxporteerd.
|
||||
16
docs/icon-concepts/concept-01-open-ring.svg
Normal file
16
docs/icon-concepts/concept-01-open-ring.svg
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="24" y="24" width="464" height="464" rx="112" fill="#F4EFE5"/>
|
||||
<circle
|
||||
cx="256"
|
||||
cy="256"
|
||||
r="142"
|
||||
fill="none"
|
||||
stroke="#1F5C49"
|
||||
stroke-width="60"
|
||||
stroke-linecap="round"
|
||||
stroke-dasharray="710 190"
|
||||
transform="rotate(-42 256 256)"
|
||||
/>
|
||||
<circle cx="372" cy="149" r="28" fill="#A7C957"/>
|
||||
<circle cx="256" cy="256" r="30" fill="#1F5C49" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 510 B |
27
docs/icon-concepts/concept-02-double-orbit.svg
Normal file
27
docs/icon-concepts/concept-02-double-orbit.svg
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="24" y="24" width="464" height="464" rx="112" fill="#F4EFE5"/>
|
||||
<circle
|
||||
cx="256"
|
||||
cy="256"
|
||||
r="136"
|
||||
fill="none"
|
||||
stroke="#234B3F"
|
||||
stroke-width="54"
|
||||
stroke-linecap="round"
|
||||
stroke-dasharray="600 180"
|
||||
transform="rotate(-48 256 256)"
|
||||
/>
|
||||
<circle
|
||||
cx="256"
|
||||
cy="256"
|
||||
r="178"
|
||||
fill="none"
|
||||
stroke="#C9D9B3"
|
||||
stroke-width="18"
|
||||
stroke-linecap="round"
|
||||
stroke-dasharray="310 820"
|
||||
transform="rotate(20 256 256)"
|
||||
/>
|
||||
<circle cx="351" cy="162" r="24" fill="#B7CF6B"/>
|
||||
<circle cx="256" cy="256" r="18" fill="#234B3F"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 695 B |
27
docs/icon-concepts/concept-03-horizon-ring.svg
Normal file
27
docs/icon-concepts/concept-03-horizon-ring.svg
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="24" y="24" width="464" height="464" rx="112" fill="#F4EFE5"/>
|
||||
<circle
|
||||
cx="256"
|
||||
cy="256"
|
||||
r="144"
|
||||
fill="none"
|
||||
stroke="#20493B"
|
||||
stroke-width="58"
|
||||
stroke-linecap="round"
|
||||
stroke-dasharray="660 210"
|
||||
transform="rotate(-34 256 256)"
|
||||
/>
|
||||
<path
|
||||
d="M162 292C188 272 221 262 256 262C291 262 324 272 350 292"
|
||||
stroke="#D8C3A5"
|
||||
stroke-width="28"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M188 318C208 302 232 294 256 294C280 294 304 302 324 318"
|
||||
stroke="#B7CF6B"
|
||||
stroke-width="16"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<circle cx="364" cy="169" r="24" fill="#B7CF6B"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 734 B |
BIN
docs/inspannings-monitor-07-testplan-v01.docx
Normal file
BIN
docs/inspannings-monitor-07-testplan-v01.docx
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue