Add app icon assets and project notes

This commit is contained in:
Janpeter Visser 2026-04-18 18:50:27 +02:00
parent f5b459dadb
commit 899c1af824
10 changed files with 1040 additions and 0 deletions

84
CLAUDE.md Normal file
View 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
View 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 (110)
- 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-301305 (dagplanning)
Daarna: ST-401405 (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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

27
app/icon.svg Normal file
View 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
View 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()

View 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.

View 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

View 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

View 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

Binary file not shown.