Files
weerscherm/docs/weerscherm-spec.md
theoandClaude Opus 5 107978b265 feat: klein grafiekje van het temperatuurverloop van vandaag
In de lege ruimte rechts van de grote buitentemperatuur staat nu het
verloop van vandaag: onderschrift "Vandaag", de lijn, en tijdlabels op
hele uren met "nu" rechts. Geen y-as, want de laagste en hoogste waarde
van vandaag staan al onder de balk en dat is precies de schaal.

Het getal en het grafiekje staan in een flexrij, zodat het grafiekje de
resterende ruimte pakt en meebeweegt als het getal smaller of breder
wordt. Vlak na middernacht zijn er nog geen twee meetpunten; dan blijft
het blokje verborgen in plaats van een leeg kader te tonen.

Data uit een extra getmeasure (scale=30min, type=Temperature, op de
buitenmodule) vanaf middernacht lokale tijd, cache 15 minuten, in de
payload als outdoor_history: {points, stale} zodat een storing daar de
rest niet raakt. PressureHistory is generiek gemaakt tot MeasureSeries
met de naam van het waardeveld als parameter, in plaats van er een bijna
identieke tweede klasse naast te zetten. StationMapper geeft nu ook het
id van de buitenmodule door.

Onderweg gevonden: FixtureSource deed een modulo op een float, wat een
PHP-deprecation vóór de JSON in de response zette. Dat verklaart de
kapotte JSON die eerder in de browserconsole langskwam. Opgelost met
intdiv. Servicewerker naar v10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 11:50:40 +02:00

21 KiB
Raw Permalink Blame History

Weerscherm: bouwspecificatie v1

Opdracht voor Claude Code: bouw een PWA die fullscreen draait op een iPad mini (liggend) in de woonkamer. Het scherm toont actuele gegevens van een Netatmo-weerstation en een weersverwachting voor 6 dagen.

Ontwerp A in docs/design/weerscherm-layouts.html is leidend voor de layout. Dat bestand is een statische mockup met voorbeelddata. De CSS van ontwerp A (klasse .da en alle .a-*-klassen) is getest op 1133 × 744 punten en dient als uitgangspunt. De tabbladen B en C zijn niet in scope.

Werkwijze: lees eerst dit document en de mockup. Stel daarna een plan voor per fase (zie §12) en bouw pas na akkoord.


0. Vooraf in te vullen (Theo)

Wat Waarde
iPad mini-generatie en iPadOS-versie …
Productie-hostname en pad …
Netatmo client_id / client_secret in config.php, nooit in git
Volgorde en labels van de 3 binnenruimtes … (standaard: module_name uit Netatmo)
Locatie voor de verwachting standaard de coördinaten van het Netatmo-station

1. Scope

In v1:

  • Buitentemperatuur, met de laagste en hoogste temperatuur van vandaag (inclusief tijdstippen) en de trend.
  • De 3 binnensensoren, elk met huidige, laagste en hoogste temperatuur van vandaag.
  • Regen: vandaag, de afgelopen 3 dagen per dag, en het totaal van de huidige maand.
  • Verwachting voor 6 dagen (vandaag t/m 5 dagen verder): max/min-temperatuur, neerslag in mm, neerslagkans en een weericoon. Wijziging 2026-09-13: was oorspronkelijk 5 dagen vanaf morgen; op verzoek van Theo toegevoegd: vandaag als eerste kolom.
  • Installeerbaar als PWA op het beginscherm, blijft werken met de laatst bekende data als de verbinding wegvalt.

Niet in v1: nachtmodus (ontwerp C), andere meetwaarden (CO₂, luchtvochtigheid, wind), meerdere stations, een instellingenscherm, pushmeldingen.


2. Architectuur

iPad (PWA) ──GET /api/data.php──▶ PHP-backend ──▶ Netatmo API
                                      │      └──▶ Open-Meteo API
                                      └── storage/ (tokens + cache)

De backend is nodig om drie redenen:

  • Het Netatmo client_secret en de roterende refresh token mogen de browser nooit bereiken.
  • Een cache houdt het aantal API-calls laag.
  • De frontend krijgt één JSON-bestand waarin alle waarden al zijn omgerekend.

Stack:

  • PHP 8.3 zonder framework en zonder runtime-dependencies. Alleen ext-curl en ext-json; PHPUnit als dev-dependency.
  • Frontend in vanilla HTML/CSS/JS, zonder build-stap, externe fonts of CDN. Alles wordt lokaal geserveerd.
  • Uitgangspunt: simpel en onderhoudbaar, ook voor iemand anders die het later overneemt.

Mappenstructuur (voorstel):

weerscherm/
├── public/                  # document root
│   ├── index.html
│   ├── styles.css
│   ├── app.js
│   ├── icons.js             # weericonen (inline SVG-strings)
│   ├── sw.js
│   ├── manifest.webmanifest
│   ├── icons/               # app-iconen 180, 192, 512 px
│   └── api/data.php
├── src/
│   ├── Config.php
│   ├── Http.php             # dunne curl-wrapper met timeouts
│   ├── TokenStore.php       # atomisch lezen/schrijven + flock
│   ├── NetatmoClient.php
│   ├── OpenMeteoClient.php
│   ├── Cache.php
│   └── DashboardService.php # bouwt de JSON-payload
├── bin/netatmo-init.php     # eenmalig refresh token opslaan
├── storage/                 # NIET in git, niet in docroot, schrijfbaar voor www-data
├── tests/                   # PHPUnit + JSON-fixtures
├── docs/design/weerscherm-layouts.html
├── config.example.php       # config.php staat in .gitignore
└── README.md

3. Netatmo

3.1 App en tokens

  1. Maak een app aan op dev.netatmo.com. Dat levert een client_id en client_secret op.
  2. Genereer met de token generator op de app-pagina een token met scope read_station.
  3. Sla de token op met php bin/netatmo-init.php <refresh_token>. Dat schrijft naar storage/netatmo-token.json.

Tokenbeheer:

  • Een access token is ongeveer 3 uur geldig. Ververs hem via POST https://api.netatmo.com/oauth2/token met grant_type=refresh_token.
  • De refresh token roteert. Het antwoord bevat een nieuwe refresh token. Sla die direct op en gebruik vanaf dan altijd de nieuwste.
  • Schrijf atomisch (tijdelijk bestand + rename) en onder een flock. Anders kunnen twee gelijktijdige requests dubbel verversen en elkaars token ongeldig maken.
  • Ververs proactief als de access token binnen 5 minuten verloopt, en eenmalig opnieuw bij een 401/403.
  • Geeft Netatmo invalid_grant terug, dan retourneert de API de fout netatmo_auth. De frontend toont dan "Netatmo-koppeling verlopen: voer bin/netatmo-init.php opnieuw uit".

3.2 Stationsdata

GET https://api.netatmo.com/api/getstationsdata met Authorization: Bearer <token>. Kies het station via device_id uit de config, of standaard het eerste station.

Nodig Bron in response
Tijdzone, locatie body.devices[i].place.timezone, place.location (= [lon, lat])
Buiten module met type = NAModule1 → dashboard_data.Temperature, min_temp, max_temp, date_min_temp, date_max_temp (unix), temp_trend (up/down/stable), time_utc
Binnen (3×) het basisstation zelf (type = NAMain, eigen dashboard_data) plus modules met type = NAModule4; naam uit module_name. Volgorde en labels zijn te overschrijven in de config op basis van _id
Regenmeter module met type = NAModule3: de _id is nodig voor §3.3; dashboard_data.sum_rain_24 dient alleen als fallback

Is een module onbereikbaar (reachable: false of geen dashboard_data), zet dan alle waarden van die module op null. De backend mag hier niet op crashen.

3.3 Regen per dag

Eén call levert vandaag, de vorige 3 dagen en het maandtotaal op:

GET https://api.netatmo.com/api/getmeasure
  device_id  = <_id van NAMain>
  module_id  = <_id van NAModule3>
  scale      = 1day
  type       = sum_rain
  date_begin = 00:00 lokale tijd van min(1e van de maand, vandaag − 3 dagen)
  date_end   = nu
  optimize   = false
  real_time  = false

Verwerking:

  • Koppel elke timestamp aan een lokale datum in de tijdzone van het station. Bij real_time=false liggen de timestamps van dagwaarden niet op middernacht, dus ga daar niet van uit.
  • today: de waarde voor de datum van vandaag. Ontbreekt die, gebruik dan sum_rain_24.
  • previous_days: de 3 datums vóór vandaag, oudste eerst. Een ontbrekende dag wordt null en de frontend toont dan "–".
  • month_total: de som van alle dagen in de huidige kalendermaand, inclusief vandaag. Op de 1e t/m 3e van de maand vallen de vorige dagen in de vorige maand. Die tellen wél mee in previous_days, maar niet in het maandtotaal.

3.4 Cache en frequentie

Het station stuurt ongeveer elke 10 minuten nieuwe metingen. Cache getstationsdata 5 minuten en getmeasure 15 minuten. Dat blijft ruim binnen de rate limits van Netatmo.


4. Open-Meteo (verwachting)

Gratis voor niet-commercieel gebruik, geen API-key nodig.

GET https://api.open-meteo.com/v1/forecast
  latitude, longitude   = config, anders stationslocatie
  daily                 = weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max
  timezone              = Europe/Berlin
  forecast_days         = 6

Gebruik index 1 t/m 5 (index 0 is vandaag) en cache 60 minuten.

WMO-code → icoon:

Code Betekenis Icoon
0 onbewolkt sun
1, 2 licht / half bewolkt partly
3 bewolkt cloud
45, 48 mist fog
51–57 motregen drizzle
61–67 regen rain
80–82 regenbuien showers
71–77, 85, 86 sneeuw snow
95–99 onweer thunder

De iconen sun, partly, rain en showers staan in de mockup. Maak cloud, fog, drizzle, snow en thunder in dezelfde stijl:

  • viewBox 0 0 48 48, stroke 2.4, ronde uiteinden.
  • Zon-delen krijgen de klasse i-sun, wolk-vullingen i-cloud en druppels of vlokken i-drop.

5. API-contract: GET /api/data.php

{
  "generated_at": "2026-09-11T14:32:05+02:00",
  "timezone": "Europe/Berlin",
  "station": { "measured_at": "2026-09-11T14:30:12+02:00", "stale": false },
  "outdoor": {
    "temp": 16.4, "trend": "down",
    "min": 9.2,  "min_at": "2026-09-11T06:10:00+02:00",
    "max": 19.8, "max_at": "2026-09-11T13:05:00+02:00"
  },
  "indoor": [
    { "id": "70:ee:50:…", "name": "Woonkamer", "temp": 21.3, "min": 20.4, "max": 22.1, "reachable": true }
  ],
  "rain": {
    "today": 2.4,
    "previous_days": [
      { "date": "2026-09-08", "mm": 1.2 },
      { "date": "2026-09-09", "mm": 6.8 },
      { "date": "2026-09-10", "mm": 0.0 }
    ],
    "month_total": 38.5,
    "stale": false
  },
  "forecast": [
    { "date": "2026-09-12", "weather_code": 2, "temp_max": 18.3, "temp_min": 10.1,
      "precip_mm": 0.4, "precip_prob": 20 }
  ],
  "forecast_stale": false,
  "errors": []
}

Regels:

  • Geef ruwe getallen door. De frontend rondt af en formatteert.
  • Tijden zijn ISO 8601 met offset, in de tijdzone van het station. Onbekende waarden zijn null.
  • Bij een storing bij één bron: serveer de laatst gecachte data voor die bron, zet stale: true en voeg een code toe aan errors (netatmo_auth, netatmo_unavailable, forecast_unavailable). De HTTP-status blijft 200 zolang er iets te tonen is.
  • Stuur Cache-Control: no-store mee.
  • Met DATA_SOURCE=fixture in de config levert de API vaste voorbeelddata. Dan kan de frontend gebouwd worden zonder Netatmo-koppeling.

6. Frontend

6.1 Canvas en schaal

  • Ontwerp op een vast canvas van 1133 × 744, het formaat van de iPad mini 6 in liggende stand.
  • Schaal het canvas met transform: scale() op min(innerWidth/1133, innerHeight/744) en centreer het. Vul de ruimte eromheen op met de achtergrondkleur. Zo klopt de layout ook op een mini 5 (1024 × 768).
  • Alleen liggend. In portretstand verschijnt de melding "Draai de iPad een kwartslag".
  • Gebruik viewport-fit=cover en houd rekening met env(safe-area-inset-*).

6.2 Design tokens (uit de mockup)

Token Waarde Gebruik
--bg #E8EDF1 achtergrond, ook de vulling van wolk-iconen
--ink #1B2A35 hoofdtekst
--muted #5F707C labels, secundaire tekst
--line #C6D0D8 scheidingslijnen
--cold #3A75A3 minimumtemperaturen
--warm #B8761C maximumtemperaturen
--rain #2E6A99 regen
--sun #D39A2C zon in iconen
  • Font: -apple-system, system-ui, sans-serif (SF Pro op de iPad), met overal font-variant-numeric: tabular-nums.
  • Grid: padding 30px 44px, kolommen 1fr 330px, rijen 40px 1fr 232px, column-gap: 56px.
  • Onderste zone: kolommen 390px 1fr, column-gap: 36px. Deze waarden zijn in de mockup al gecorrigeerd.

6.3 Zones en formattering

Algemene notatie: nl-NL, decimale komma, tijden in de tijdzone van het station via Intl.DateTimeFormat.

  • Kopregel: datum ("Vrijdag 11 september") links. Rechts "meting HH:MM" (uit station.measured_at) en de klok (apparaattijd, per minuut bijgewerkt).
  • Buiten:
    • De temperatuur staat groot, met 1 decimaal ("16,4°"). Het label heeft een trend-aanduiding: up → "↗ stijgend", down → "↘ dalend", stable → "→ stabiel".
    • Daaronder de gradiëntbalk van --cold naar --warm. De marker staat op (temp − min) / (max − min), begrensd op 0–100%. Bij max == min staat hij op 50%.
    • Onder de balk: "9,2° laagst om 06:10" en "19,8° hoogst om 13:05".
    • Rechts van het grote getal een klein grafiekje van het temperatuurverloop van vandaag. Toegevoegd 2026-09-13: onderschrift "Vandaag", de lijn, en tijdlabels op hele uren met "nu" rechts (zelfde hourTicks als de luchtdruk). Geen y-as: de laagste en hoogste waarde van vandaag staan al onder de balk en dat is precies de schaal van de lijn. Data uit een extra getmeasure (scale=30min, type=Temperature, buitenmodule) vanaf middernacht lokale tijd, in de payload als outdoor_history: {points, stale}. Vlak na middernacht zijn er nog geen twee punten; dan blijft het blokje verborgen in plaats van een leeg kader te tonen.
    • Daaronder de luchtdruk: "Luchtdruk 1014 hPa ↗ stijgend" met een grafiek van de afgelopen 24 uur. Assen toegevoegd 2026-09-13: links de hoogste en laagste waarde van het venster in hele hPa, onder de lijn tijdlabels op hele uren die op de klok zijn uitgelijnd (elke 6 uur vanaf middernacht, dus 00:00, 06:00, 12:00, 18:00) op hun echte tijdspositie, met "nu" op de rechterrand, plus drie lichte verticale hulplijnen. Een markering te dicht bij het einde valt weg omdat "nu" daar staat. De losse tekst "1009 – 1015 hPa in 24 u" in de kopregel is daarmee vervallen. Zie hourTicks() in public/format.js; die loopt uur voor uur door het venster en raadpleegt telkens de lokale klok, zodat de zomertijdwissel vanzelf goed gaat.
  • Binnen (rechts): 3 rijen met naam, temperatuur (1 decimaal) en "↓ min° ↑ max°". Een onbereikbare module toont "–" in --muted. Toegevoegd 2026-09-13: een module is tikbaar. Een tik zet die module bovenaan de lijst (de overige twee behouden hun onderlinge volgorde) en vergroot alleen het temperatuurgetal van 50 naar 92px. Nogmaals tikken op de bovenste (vastgezette) module maakt hem weer los. De keuze blijft staan bij de 5-minuten-verversing en reset bij een volledige herlaad. Zie pinRoom() in public/format.js.
  • Regen (linksonder):
    • "Vandaag" groot, in mm met 1 decimaal.
    • Daarnaast 4 staafjes: de 3 vorige dagen (labels "Di", "Wo", "Do") en vandaag, uitgelicht. De hoogte is mm / max(4 waarden) × 70px, met een minimum van 2px. Zijn alle waarden 0, dan zijn alle staafjes 2px.
    • Daaronder "September 38,5 mm". Toegevoegd 2026-09-13: het regenblok is tikbaar en opent de regenradar van Buienradar (geanimeerde GIF, 550x512, met de tijd in het beeld), naast de regencijfers van vandaag, de 3 vorige dagen en het maandtotaal. Sluit na 20 seconden of bij een tik. Het beeld loopt via /api/radar.php op onze eigen server (cache 4 min, bij storing het laatst opgehaalde beeld), zodat de iPad niet zelf met een derde partij praat en het beeld ook offline blijft staan. Bronvermelding "bron: Buienradar.nl" staat in de overlay; dat vraagt Buienradar bij vrij gebruik. Zie src/BuienradarClient.php en src/RadarService.php.
  • Verwachting (rechtsonder): 6 kolommen met dagafkorting ("Za"), icoon (50px), "max° min°" als hele getallen (min in --muted) en "x,x mm" plus neerslagkans in %. Geen neerslag toont "0 mm". Eerste kolom is vandaag. Toegevoegd 2026-09-13: elke dagkolom is tikbaar. Een tik opent een uuroverzicht voor die dag (temperatuur, regen en wind, elk om de 2 uur — 12 punten), dat na 20 seconden vanzelf sluit of eerder bij een tik erop. De uurdata komt in dezelfde Open-Meteo-call mee (hourly=temperature_2m,precipitation,wind_speed_10m, wind_direction_10m), dus geen extra netwerkverzoek bij het openen. Windpijl wijst waarheen de wind waait (richting + 180°). Zie src/HourlyForecastMapper.php en renderHourlyCharts()/openHourOverlay() in public/app.js.

6.4 Verversen

  • Haal /api/data.php op bij het laden, daarna elke 5 minuten, en direct bij visibilitychange naar zichtbaar.
  • Om 00:00 lokale tijd: opnieuw ophalen en de datum bijwerken.
  • Dagelijks rond 04:00 een volledige location.reload(). Dat voorkomt geheugenlekken en pakt nieuwe deploys op.
  • Optioneel: vraag een navigator.wakeLock aan als die beschikbaar is. De primaire oplossing is Automatisch vergrendelen op "Nooit" in de iPad-instellingen.

6.5 Verouderde data en fouten

  • Is station.measured_at ouder dan 30 minuten, dan wordt "meting HH:MM" --warm met de toevoeging "verouderd".
  • Is de API niet bereikbaar, laat dan de laatste data staan en toon klein in de kopregel "geen verbinding sinds HH:MM".
  • Bij netatmo_auth: toon een duidelijke melding over de hele bovenzone met de herstelinstructie uit §3.1.
  • Nooit een leeg of wit scherm. Bij de allereerste load zonder data: toon "Gegevens laden…" en daarna een concrete foutmelding.

6.6 Browserdoel

Hangt af van de iPadOS-versie in §0:

  • iPadOS 15 of nieuwer: moderne JS/CSS is prima.
  • Ouder: schrijf ES2017 (geen optional chaining of ??). In CSS geen gap op flexbox (gebruik margins), geen inset en geen aspect-ratio. Grid-gap mag wel.

7. PWA

  • manifest.webmanifest:
    • name: "Weerscherm", short_name: "Weer", display: "standalone", orientation: "landscape".
    • start_url en scope relatief ("./").
    • background_color en theme_color: #E8EDF1.
    • Iconen van 192 en 512 px.
  • iOS-meta: apple-mobile-web-app-capable, apple-mobile-web-app-status-bar-style (default), een apple-touch-icon van 180 px, apple-mobile-web-app-title.
  • Service worker:
    • De app-shell (html, css, js, iconen) is cache-first met een geversioneerde cachenaam.
    • /api/data.php is network-first met een timeout van 8 s. Val terug op de laatst gecachte response en markeer die in de UI als verouderd.
    • Gebruik skipWaiting + clients.claim. De pagina herlaadt zichzelf bij controllerchange.
  • App-icoon: een eenvoudig zon/wolk-icoon in de stijl van de weericonen, op --bg.

8. Configuratie en beveiliging

config.example.php staat in git, config.php niet. Sleutels:

  • netatmo_client_id, netatmo_client_secret
  • netatmo_device_id (optioneel)
  • timezone (standaard Europe/Berlin)
  • forecast_lat / forecast_lon (optioneel)
  • indoor_modules: optioneel, een lijst van _id → label in de gewenste volgorde
  • data_source (live | fixture)
  • cache_ttl per bron

Beveiliging:

  • storage/ staat buiten de document root en wordt nooit geserveerd. Tokenbestanden krijgen chmod 600.
  • Er komen geen secrets of tokens in responses, logs of de frontend.
  • Stuur X-Robots-Tag: noindex mee en zet een robots.txt die alles blokkeert.
  • Standaard geen login. Dat is proportioneel voor weerdata, en HTTP basic auth werkt slecht in iOS-standalone-PWA's. Plaats de app wel op een niet-voor-de-hand-liggend (sub)domein of pad.

9. Lokaal ontwikkelen en deployen

  • Lokaal: via de bestaande OrbStack shared-Apache-setup met een PHP 8.3-container, onder weerscherm.test. Begin met data_source = fixture.
  • Visuele check: een browservenster van exact 1133 × 744 naast docs/design/weerscherm-layouts.html (tab A).
  • Deploy: Git op de self-hosted Gitea (squash-and-merge), deploy via DeployPad. storage/ moet deploys overleven: zet hem buiten de release-map of maak er een gedeelde map van.
  • README: installatie, Netatmo-koppeling (§3.1), config, deploy, en de iPad-instellingen:
    • toevoegen aan beginscherm
    • Automatisch vergrendelen → Nooit
    • Begeleide toegang

10. Tests

PHPUnit, met JSON-fixtures van echte responses (geanonimiseerde MAC-adressen):

  • Mapping van getstationsdata naar de payload, inclusief een onbereikbare module en een ontbrekende regenmeter.
  • Regenberekening: datum-mapping in de stationstijdzone, de maandgrens (1e–3e van de maand), een ontbrekende dag, en de dag van de zomertijdwissel.
  • Token-refresh: de nieuwe refresh token wordt opgeslagen, een gelijktijdige refresh verloopt goed dankzij de lock, en invalid_grant geeft de fout netatmo_auth.
  • Stale-fallback: bij een upstream-fout wordt de gecachte data geserveerd met stale: true.

Frontend: houd formatteer- en rekenfuncties puur en los testbaar. Denk aan de decimale komma, de markerpositie, de staafhoogtes en de WMO-mapping. Vitest mag als dev-dependency, maar zonder build-stap voor productie.


11. Acceptatiecriteria

  • Op de iPad mini, gestart vanaf het beginscherm, draait de app fullscreen zonder Safari-balken en klopt de layout visueel met ontwerp A.
  • Alle waarden komen overeen met de Netatmo-app: buiten, 3× binnen, min/max met tijdstippen, regen vandaag en de afgelopen 3 dagen, en het maandtotaal.
  • De verwachting toont 6 dagen (vandaag t/m 5 dagen verder) met het juiste icoon per WMO-code.
  • Tikken op een dag opent het uuroverzicht (temperatuur, regen, wind) en sluit na 20 seconden vanzelf of bij een tik erop.
  • Tikken op het regenblok opent de Buienradar-animatie en sluit op dezelfde manier.
  • De data ververst zichzelf. Na middernacht staat de nieuwe datum er binnen 5 minuten.
  • Wifi uit: de laatste data blijft staan, met een melding "geen verbinding sinds". Wifi aan: herstelt vanzelf.
  • De Netatmo-token ververst meerdere dagen achter elkaar zonder handmatig ingrijpen.
  • Geen secrets in git, in de browser of in responses. storage/ is niet via HTTP bereikbaar.
  • De PHPUnit-tests uit §10 slagen.

12. Werkvolgorde

  1. Backend met fixtures: mappenstructuur, config, DashboardService op fixtures, /api/data.php, tests voor de mapping en de regenberekening.
  2. Frontend: ontwerp A op basis van de fixture-API, met schaal, formattering, verversen en de foutstatussen.
  3. Netatmo live: bin/netatmo-init.php, TokenStore met lock, getstationsdata en getmeasure, cache, stale-fallback, tokentests.
  4. Open-Meteo live: client, cache, WMO-mapping en de ontbrekende iconen.
  5. PWA: manifest, iOS-meta, service worker, app-iconen.
  6. Deploy en README: productieconfig, DeployPad, een test op de iPad aan de hand van de acceptatiecriteria.

Sluit elke fase af met iets dat te testen is, en meld afwijkingen van deze spec voordat je ze doorvoert.