Naast het getal in elke modulekaart staat nu hetzelfde soort lijntje als bij buiten (200x40). Zonder labels: daar is in zo'n kaart geen ruimte voor, de schaal staat al op de regel eronder en het venster is hetzelfde "vandaag" als bij het grafiekje van buiten ernaast. Per module een eigen getmeasure, gecachet onder temperature-<module> met een gedeelde TTL; fetch() heeft daarvoor een losse ttlKey gekregen, want anders zou de TTL-lookup per module mislukken en zou er helemaal niet gecachet worden. Het basisstation meet zelf temperatuur en wordt zonder module_id opgevraagd, dus die parameter is optioneel geworden. In de payload als indoor[].history; mislukt er een, dan blijft die lijst leeg en verdwijnt alleen dat lijntje, met de foutcode in errors. Een stale-vlag per kamer zou het contract nodeloos zwaar maken. De hoogte is overal 40px, ook bij een vastgezette module: gelijke hoogte houdt de kamers onderling vergelijkbaar, en een hoger lijntje zou de toch al krappe speling onder de kolom opeten. De grafiekjes voegen geen hoogte toe aan de kaarten. FakeSource houdt nu alle aanroepen bij in plaats van alleen de laatste, zodat een test de volgorde van de vier temperatuur-calls kan nalopen. Servicewerker naar v11. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
22 KiB
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_secreten 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-curlenext-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
- Maak een app aan op dev.netatmo.com. Dat levert een
client_idenclient_secretop. - Genereer met de token generator op de app-pagina een token met scope
read_station. - Sla de token op met
php bin/netatmo-init.php <refresh_token>. Dat schrijft naarstorage/netatmo-token.json.
Tokenbeheer:
- Een access token is ongeveer 3 uur geldig. Ververs hem via
POST https://api.netatmo.com/oauth2/tokenmetgrant_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 eenflock. 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_grantterug, dan retourneert de API de foutnetatmo_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=falseliggen de timestamps van dagwaarden niet op middernacht, dus ga daar niet van uit. today: de waarde voor de datum van vandaag. Ontbreekt die, gebruik dansum_rain_24.previous_days: de 3 datums vóór vandaag, oudste eerst. Een ontbrekende dag wordtnullen 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 inprevious_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-vullingeni-clouden druppels of vlokkeni-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: trueen voeg een code toe aanerrors(netatmo_auth,netatmo_unavailable,forecast_unavailable). De HTTP-status blijft 200 zolang er iets te tonen is. - Stuur
Cache-Control: no-storemee. - Met
DATA_SOURCE=fixturein 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()opmin(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=coveren houd rekening metenv(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 overalfont-variant-numeric: tabular-nums. - Grid: padding
30px 44px, kolommen1fr 330px, rijen40px 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
--coldnaar--warm. De marker staat op(temp − min) / (max − min), begrensd op 0–100%. Bijmax == minstaat 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
hourTicksals 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 extragetmeasure(scale=30min,type=Temperature, buitenmodule) vanaf middernacht lokale tijd, in de payload alsoutdoor_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()inpublic/format.js; die loopt uur voor uur door het venster en raadpleegt telkens de lokale klok, zodat de zomertijdwissel vanzelf goed gaat.
- De temperatuur staat groot, met 1 decimaal ("16,4°"). Het label heeft een trend-aanduiding:
-
Binnen (rechts): 3 rijen met naam, temperatuur (1 decimaal) en "↓ min° ↑ max°". Een onbereikbare module toont "–" in
--muted. Toegevoegd 2026-09-13: rechts van elk temperatuurgetal staat het verloop van vandaag als klein lijntje (200x40, zonder labels: de schaal staat op de regel eronder en het venster is hetzelfde "vandaag" als bij buiten). Data per module uit een eigengetmeasure, apart gecachet ondertemperature-<module>maar met één gedeelde TTL; het basisstation meet zelf en wordt zondermodule_idopgevraagd. In de payload alsindoor[].history. Mislukt er één, dan blijft die lijst leeg, verdwijnt alleen dat lijntje en staat de foutcode inerrors; een stale-vlag per kamer zou het contract nodeloos zwaar maken. De hoogte is overal gelijk, ook bij een vastgezette module, zodat de kamers vergelijkbaar blijven.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()inpublic/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.phpop 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. Ziesrc/BuienradarClient.phpensrc/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°). Ziesrc/HourlyForecastMapper.phpenrenderHourlyCharts()/openHourOverlay()inpublic/app.js.
6.4 Verversen
- Haal
/api/data.phpop bij het laden, daarna elke 5 minuten, en direct bijvisibilitychangenaar 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.wakeLockaan 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_atouder dan 30 minuten, dan wordt "meting HH:MM"--warmmet 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 geengapop flexbox (gebruik margins), geeninseten geenaspect-ratio. Grid-gapmag wel.
7. PWA
manifest.webmanifest:name: "Weerscherm",short_name: "Weer",display: "standalone",orientation: "landscape".start_urlenscoperelatief ("./").background_colorentheme_color:#E8EDF1.- Iconen van 192 en 512 px.
- iOS-meta:
apple-mobile-web-app-capable,apple-mobile-web-app-status-bar-style(default), eenapple-touch-iconvan 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.phpis 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 bijcontrollerchange.
- 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_secretnetatmo_device_id(optioneel)timezone(standaardEurope/Berlin)forecast_lat/forecast_lon(optioneel)indoor_modules: optioneel, een lijst van_id→ label in de gewenste volgordedata_source(live|fixture)cache_ttlper bron
Beveiliging:
storage/staat buiten de document root en wordt nooit geserveerd. Tokenbestanden krijgenchmod 600.- Er komen geen secrets of tokens in responses, logs of de frontend.
- Stuur
X-Robots-Tag: noindexmee en zet eenrobots.txtdie 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 metdata_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
getstationsdatanaar 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_grantgeeft de foutnetatmo_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
- Backend met fixtures: mappenstructuur, config,
DashboardServiceop fixtures,/api/data.php, tests voor de mapping en de regenberekening. - Frontend: ontwerp A op basis van de fixture-API, met schaal, formattering, verversen en de foutstatussen.
- Netatmo live:
bin/netatmo-init.php, TokenStore met lock,getstationsdataengetmeasure, cache, stale-fallback, tokentests. - Open-Meteo live: client, cache, WMO-mapping en de ontbrekende iconen.
- PWA: manifest, iOS-meta, service worker, app-iconen.
- 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.