25 KiB
Weerscherm Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Een PWA voor een iPad mini (liggend) die Netatmo-stationsdata en een 5-daagse Open-Meteo-verwachting toont, gevoed door een PHP-backend met cache en tokenbeheer.
Architecture: Een PHP 8.3-backend zonder framework serveert GET /api/data.php als één JSON-payload. DashboardService combineert drie bronnen (stationsdata, regenmetingen, verwachting) via een WeatherSource-interface met een Live- en een Fixture-implementatie, cachet per bron en valt bij storing terug op stale cache. Pure mappers (StationMapper, RainCalculator, ForecastMapper) zijn los testbaar. De frontend is vanilla ES-modules op een vast canvas van 1133×744 dat geschaald wordt; pure formatteerfuncties staan in format.js en worden met node --test getest.
Tech Stack: PHP 8.3 (ext-curl, ext-json), PHPUnit 11 (dev), vanilla HTML/CSS/JS (ES-modules), Node 24 node:test (dev), service worker, webmanifest.
Spec: docs/weerscherm-spec.md (mockup: docs/design/weerscherm-layouts.html, ontwerp A).
Global Constraints
- PHP 8.3, geen framework, geen runtime-dependencies; alleen
ext-curlenext-json. PHPUnit als dev-dependency. - Frontend zonder build-stap, externe fonts of CDN. Alles lokaal geserveerd.
- Browserdoel iPadOS 15+: moderne JS/CSS en ES-modules zijn toegestaan.
- Canvas 1133 × 744, geschaald met
transform: scale(min(innerWidth/1133, innerHeight/744)), gecentreerd, alleen liggend. - Design tokens:
--bg #E8EDF1,--ink #1B2A35,--muted #5F707C,--line #C6D0D8,--cold #3A75A3,--warm #B8761C,--rain #2E6A99,--sun #D39A2C. - Notatie
nl-NL, decimale komma, tijden in de stationstijdzone, overalfont-variant-numeric: tabular-nums. - API: ruwe getallen, ISO 8601 met offset,
nullvoor onbekend,Cache-Control: no-store,X-Robots-Tag: noindex, HTTP 200 zolang er iets te tonen is. - Cache-TTL's: stationsdata 300 s, getmeasure 900 s, forecast 3600 s.
- Secrets alleen in
config.phpenstorage/; beide in.gitignore,storage/buiten de docroot, tokenbestandchmod 600. - Projectroot is deze map (
home-display), docroot ispublic/. - Commits klein en per taak;
config.phpenstorage/nooit committen.
File Structure
config.example.php # voorbeeldconfig (in git); config.php (niet in git) heeft dezelfde vorm
composer.json # autoload psr-4 "Weerscherm\\" => src/, phpunit dev, scripts test
phpunit.xml
package.json # alleen "test": "node --test tests/js"
.gitignore # config.php, storage/, vendor/, node_modules/
public/
index.html # app-shell, iOS-meta, manifest-link, module script
styles.css # ontwerp A, tokens, canvas-schaal, portret-melding, foutstaten
app.js # ophalen, renderen, timers, foutafhandeling, SW-registratie
format.js # pure functies (formatteren, marker, staven, WMO, datums)
icons.js # icon(name) => SVG-string, 9 iconen
sw.js # app-shell cache-first, API network-first met timeout
manifest.webmanifest
robots.txt
icons/icon-180.png, icon-192.png, icon-512.png, icon.svg
api/data.php # bootstrap + JSON
src/
Config.php # laadt config.php, defaults
Http.php # HttpClient-interface + curl-implementatie + HttpResponse
Cache.php # bestandscache per key, met ttl en stale-lezen
TokenStore.php # atomisch lezen/schrijven, flock
NetatmoClient.php # oauth refresh, getstationsdata, getmeasure
OpenMeteoClient.php # daily forecast
WeatherSource.php # interface + LiveSource + FixtureSource
StationMapper.php # getstationsdata-body -> station/outdoor/indoor
RainCalculator.php # getmeasure-body -> today/previous_days/month_total
ForecastMapper.php # open-meteo daily -> 5 dagen
DashboardService.php # orkestratie, cache, stale-fallback, errors
UpstreamException.php, NetatmoAuthException.php
bin/netatmo-init.php # slaat refresh token op in storage/netatmo-token.json
tests/
bootstrap.php
fixtures/getstationsdata.json, getmeasure.json, getmeasure-dst.json, forecast.json
StationMapperTest.php, RainCalculatorTest.php, ForecastMapperTest.php
TokenStoreTest.php, NetatmoClientTest.php, DashboardServiceTest.php, CacheTest.php
js/format.test.js
docs/weerscherm-spec.md, docs/design/weerscherm-layouts.html
README.md
Interfaces (gedeeld door alle taken)
namespace Weerscherm;
interface HttpClient {
/** @param array<string,string|int|float> $query */
public function get(string $url, array $query = [], array $headers = []): HttpResponse;
public function postForm(string $url, array $fields): HttpResponse;
}
final class HttpResponse { public function __construct(public readonly int $status, public readonly string $body) {} public function json(): array; }
final class Config { public static function load(string $file): self; public function get(string $key, mixed $default = null): mixed; }
final class Cache {
public function __construct(string $dir);
/** @return array{data: array, stored_at: int}|null */
public function read(string $key): ?array;
public function isFresh(string $key, int $ttl, int $now): bool;
public function write(string $key, array $data, int $now): void;
}
final class TokenStore {
public function __construct(string $file);
/** @return array{access_token:string, refresh_token:string, expires_at:int}|null */
public function read(): ?array;
public function write(array $token): void; // tmp + rename, chmod 600
public function withLock(callable $fn): mixed; // flock LOCK_EX op <file>.lock
}
interface WeatherSource {
public function stationsData(): array; // body van getstationsdata
public function measure(string $deviceId, string $moduleId, int $begin, int $end): array; // body van getmeasure
public function forecast(float $lat, float $lon, string $tz): array; // "daily" van open-meteo
}
final class StationMapper {
/** @return array{timezone:string, lat:?float, lon:?float, device_id:?string, measured_at:?int,
* outdoor:array, indoor:list<array>, rain_module_id:?string, sum_rain_24:?float} */
public static function map(array $body, ?string $deviceId, array $indoorModules): array;
}
final class RainCalculator {
public static function dateBegin(\DateTimeImmutable $now): \DateTimeImmutable;
/** @return array{today:?float, previous_days:list<array{date:string,mm:?float}>, month_total:?float} */
public static function compute(array $body, \DateTimeImmutable $now, ?float $sumRain24): array;
}
final class ForecastMapper { /** @return list<array> */ public static function map(array $daily): array; }
final class DashboardService {
public function __construct(WeatherSource $source, Cache $cache, array $ttl, string $tzFallback, ?float $lat, ?float $lon, array $indoorModules, ?string $deviceId, ?\DateTimeImmutable $now = null);
public function build(): array; // payload volgens spec §5
}
Payloadvorm: exact spec §5.
Fase 1: backend met fixtures
Task 1: Projectskelet, composer, PHPUnit
Files: composer.json, phpunit.xml, tests/bootstrap.php, .gitignore, config.example.php, src/Config.php, tests/ConfigTest.php
composer.jsonmet"autoload": {"psr-4": {"Weerscherm\\": "src/"}},"require": {"php": ">=8.3", "ext-curl": "*", "ext-json": "*"},"require-dev": {"phpunit/phpunit": "^11"}, scripttest.composer install.- Test:
Config::load()geeft defaults (timezone=Europe/Berlin,data_source=fixture,cache_ttl=[stations=>300, measure=>900, forecast=>3600]) en overschrijft met waarden uit het bestand. - Implementatie
Config. composer testgroen. Commit.
Task 2: StationMapper
Files: src/StationMapper.php, tests/fixtures/getstationsdata.json, tests/StationMapperTest.php
Fixture: 1 NAMain (_id 70:ee:50:00:00:01, module_name "Woonkamer", place.timezone Europe/Berlin, place.location [6.55, 51.82]), NAModule1 buiten (Temperature 16.4, min_temp 9.2, max_temp 19.8, date_min_temp, date_max_temp, temp_trend "down", time_utc), 2× NAModule4 ("Slaapkamer" reachable, "Kantoor" reachable:false zonder dashboard_data), NAModule3 regenmeter met sum_rain_24 2.4.
Tests:
timezone,lat,lonuitplace(let op: location is[lon, lat]).outdoorbevattemp 16.4, trend "down", min 9.2, min_at <unix>, max 19.8, max_at <unix>.indoorheeft 3 items in volgorde NAMain, dan modules; "Kantoor" heefttemp/min/max null, reachable false.indoor_modules-config['70:ee:50:00:00:03' => 'Studeerkamer', '70:ee:50:00:00:01' => 'Living']bepaalt volgorde en labels; modules die niet in de lijst staan komen daarna in oorspronkelijke volgorde.- Zonder NAModule3:
rain_module_id null, sum_rain_24 null. - Onbekend
device_id→ eerste station. Leegdevices→ alle velden null, geen exception.
Task 3: RainCalculator
Files: src/RainCalculator.php, tests/fixtures/getmeasure.json, tests/fixtures/getmeasure-dst.json, tests/RainCalculatorTest.php
Body-vorm van getmeasure met optimize=false: {"body": {"<unix>": [mm], ...}}. Sleutels zijn timestamps van dagwaarden, niet per se middernacht.
Regels:
dateBegin(now)= 00:00 stationstijd vanmin(1e van de maand, vandaag − 3 dagen).- Elke timestamp → lokale datum
Y-m-din de tijdzone van$now. today= waarde van vandaag, anders$sumRain24.previous_days= 3 datums vóór vandaag, oudste eerst, ontbrekende dagmm: null.month_total= som van alle dagen in de huidige kalendermaand inclusief vandaag;nullals er geen enkele dag is.
Tests (now = 2026-09-11 14:32 Europe/Berlin):
- Normale week: today 2.4, previous [08→1.2, 09→6.8, 10→0.0], month_total = som van 1 t/m 11.
- Timestamps om 03:17 lokale tijd mappen op de juiste datum.
- Ontbrekende 9 september →
mm null, month_total telt de rest. - now = 2 oktober: previous_days = 29, 30 sep en 1 okt; month_total = 1 + 2 okt.
- Ontbrekende vandaag →
today= sumRain24 (2.4); zonder sumRain24 → null. - DST: now =
2026-10-25 12:00(wissel om 03:00). Timestamp2026-10-24 23:30 UTC(= 01:30 lokaal 25 okt) telt als 25 okt;2026-10-25 23:30 UTC(= 00:30 lokaal 26 okt) niet als 25 okt. dateBeginop 2 okt = 29 sep 00:00; op 15 sep = 1 sep 00:00.
Task 4: ForecastMapper
Files: src/ForecastMapper.php, tests/fixtures/forecast.json, tests/ForecastMapperTest.php
- Input: Open-Meteo
dailymet arraystime, weather_code, temperature_2m_max, temperature_2m_min, precipitation_sum, precipitation_probability_maxvan 6 elementen. - Output: 5 items (index 1..5):
date, weather_code, temp_max, temp_min, precip_mm, precip_prob. Ontbrekende (null) waarden blijvennull. Minder dan 6 dagen → zoveel als beschikbaar.
Task 5: Cache
Files: src/Cache.php, tests/CacheTest.php
writeschrijft<dir>/<key>.jsonmet{stored_at, data}via tmp + rename.readgeeft null bij ontbreken of corrupt JSON.isFresh(key, ttl, now):now - stored_at < ttl.- Key wordt gesaneerd tot
[a-z0-9_-].
Task 6: WeatherSource + FixtureSource
Files: src/WeatherSource.php, src/FixtureSource.php, src/Exceptions.php, tests/FixtureSourceTest.php
FixtureSourcebouwt in PHP realistische data relatief aan nu (zodat de UI altijd "vandaag" laat zien): stationsdata metdate_min_temp= vandaag 06:10,date_max_temp= vandaag 13:05,time_utc= nu − 2 min; measure met 11+ dagen tot vandaag; forecast met 6 dagen vanaf vandaag, codes[1, 2, 0, 61, 80, 3].- Test:
measure()bevat een sleutel die op vandaag valt;forecast()['time'][1]is morgen.
Task 7: DashboardService
Files: src/DashboardService.php, tests/DashboardServiceTest.php
Gedrag (per bron):
- Cache fresh → gebruik cache,
stale false. - Anders
source->…(); succes → cache write,stale false. - Exception → cache stale (ongeacht leeftijd) →
stale true+ errorcode; geen cache → velden null + errorcode. NetatmoAuthException→ codenetatmo_auth; andere Netatmo-foutennetatmo_unavailable; forecastforecast_unavailable.- Regen:
date_beginuitRainCalculator::dateBegin,date_end = now; zonderrain_module_id→rainalle null zonder call. - Forecast-locatie: config lat/lon, anders station-lat/lon; beide onbekend →
forecast_unavailable. - Tijden als ISO 8601 met offset in de stationstijdzone (
timezoneuit stationsdata, anders fallback).
Tests met een FakeSource (arrays of exceptions per methode) en een tijdelijke cache-dir:
- Happy path: payload heeft alle sleutels uit §5,
errors == [],outdoor.min_at=2026-09-11T06:10:00+02:00. - Stations-exception zonder cache →
outdoor.temp null,station.stale true, errors[netatmo_unavailable]. - Stations-exception mét cache → oude waarden,
station.stale true. - Auth-exception →
netatmo_auth. - Forecast-exception →
forecast_stale true,forecast_unavailable, rest intact. - Tweede
build()binnen de TTL doet geen source-call (teller in FakeSource).
Task 8: public/api/data.php
Files: public/api/data.php, public/robots.txt
- Laadt
vendor/autoload.php,config.php(valt terug opconfig.example.phpmet een 500 en tekst "config.php ontbreekt" als die er niet is), kiestFixtureSourceofLiveSource(LiveSource komt in fase 3; tot danthrowbijlive). - Headers:
Content-Type: application/json; charset=utf-8,Cache-Control: no-store,X-Robots-Tag: noindex. robots.txt:User-agent: */Disallow: /.- Check:
php -S 127.0.0.1:8080 -t publicencurlgeeft geldige JSON meterrors: []. - Commit. Fase 1 klaar.
Fase 2: frontend
Task 9: format.js met tests
Files: public/format.js, tests/js/format.test.js, package.json
Exports en gedrag:
f1(v):16.4 → "16,4",0 → "0,0",null → "–".fInt(v):18.3 → "18",null → "–"(afronden metMath.round).fmm(v):0 → "0 mm",0.4 → "0,4 mm",null → "–".markerPos(temp, min, max):(16.4, 9.2, 19.8) ≈ 67.9;< min → 0;> max → 100;max == min → 50; null → 50.barHeights(values, maxPx = 70, minPx = 2):[1.2, 6.8, 0, 2.4] → [12.35…, 70, 2, 24.7…]; alle 0 → alle 2; null telt als 0.wmoIcon(code): tabel uit spec §4; onbekend →cloud.trendLabel(t):up → "↗ stijgend",down → "↘ dalend",stable → "→ stabiel", anders"".formatDate(date, tz):"Vrijdag 11 september"(eerste letter hoofdletter).formatTime(iso, tz):"06:10"; null →"–".dayAbbr(dateStr, tz):"2026-09-12" → "Za".monthName(date, tz):"September".isOlderThan(iso, now, minutes).nextMidnight(now, tz)ennextTimeAt(now, tz, hour)geven ms tot het volgende moment.
package.json: {"private": true, "type": "module", "scripts": {"test": "node --test tests/js"}}. Tests importeren ../../public/format.js.
Task 10: icons.js
Files: public/icons.js
export function icon(name)→<svg class="ic" viewBox="0 0 48 48" aria-hidden="true">…</svg>.sun,partly,rain,showersletterlijk uit de mockup.- Nieuw in dezelfde stijl (stroke 2.4, ronde uiteinden,
i-sun/i-cloud/i-drop):cloud: alleen de wolkvorm vanrainmaar gecentreerd (y +5).fog: wolk plus 2 horizontale lijnen eronder (M14 36h20 M18 42h14, klassei-drop).drizzle: wolk plus 3 korte streepjes (M18 35l-1 2.5 M26 35l-1 2.5 M34 35l-1 2.5).snow: wolk plus 3 kleine sterretjes (elk 2 kruisende lijnen van 5 px) ini-drop.thunder: wolk plus bliksemM25 32l-4 7h5l-3 6ini-sun.
- Onbekende naam →
cloud.
Task 11: index.html + styles.css (ontwerp A)
Files: public/index.html, public/styles.css
<html lang="nl">, viewportwidth=device-width, initial-scale=1, viewport-fit=cover, iOS-meta (§7),<link rel="manifest">,<script type="module" src="app.js">.- Structuur:
<div id="stage">(1133×744,transform-origin: top left) met daarin dezelfde secties als de mockup:header.a-top,section.a-hero,section.a-inside,section.a-bottom(regen +.a-fc). Alle dynamische velden hebbenid's ofdata-*. - Extra elementen:
#status(klein, in.a-right, voor "geen verbinding sinds HH:MM"),#banner(over de bovenzone, verborgen; voornetatmo_authen de eerste-load-fout),#rotate(portret-melding, alleen zichtbaar via@media (orientation: portrait)),#loading("Gegevens laden…"). - CSS: de
.da-tokens en alle.a-*-klassen uit de mockup overgenomen;bodyheeftbackground: var(--bg)enoverflow: hidden;#stagegecentreerd via JS-schaal;paddinghoudt rekening metenv(safe-area-inset-*). .a-upd.stale { color: var(--warm); }.
Task 12: app.js
Files: public/app.js
Functies:
fit():scale = Math.min(innerWidth/1133, innerHeight/744); stage krijgttransform: translate(x,y) scale(s). Aanresizeenorientationchange.render(data): vult alle velden metformat.js; regenstaven viabarHeights; forecast viaicon(wmoIcon(code)); indoor onbereikbaar → "–" in--muted;station.staleof meting ouder dan 30 min →.a-updwarm + " verouderd";errorsbevatnetatmo_auth→ banner met herstelinstructie.refresh():fetch('api/data.php', {cache: 'no-store'}); bij succeslastDatabewaren,lastOk = now, status leeg; bij fout: laatste data blijft, status "geen verbinding sinds HH:MM" (tijd van laatste succes); eerste load zonder data → banner met foutmelding.- Timers:
setInterval(refresh, 5 min);visibilitychange→ refresh als zichtbaar; klok elke minuut;setTimeouttot middernacht → refresh + datum;setTimeouttot 04:00 →location.reload(). navigator.wakeLock?.request('screen')in try/catch, opnieuw bijvisibilitychange.- SW-registratie en
controllerchange→location.reload()(fase 5; hier alvast metif ('serviceWorker' in navigator)). - Visuele check:
php -S+ browser op 1133×744 naast de mockup (tab A). Commit. Fase 2 klaar.
Fase 3: Netatmo live
Task 13: Http + HttpResponse
Files: src/Http.php
Http implements HttpClient, curl metCURLOPT_CONNECTTIMEOUT 5,CURLOPT_TIMEOUT 8,RETURNTRANSFER,FOLLOWLOCATION false. Curl-fout →UpstreamException.HttpResponse::json()→json_decode(..., true, 512, JSON_THROW_ON_ERROR), gooitUpstreamExceptionbij ongeldige JSON.
Task 14: TokenStore
Files: src/TokenStore.php, tests/TokenStoreTest.php
write:json_encodenaar<file>.tmp.<uniqid>,chmod 0600,rename.read: null als bestand ontbreekt of ongeldig.withLock(fn): opent<file>.lock,flock(LOCK_EX), voert fn uit,LOCK_UN, geeft resultaat terug; ook bij exception ontgrendelen (finally).- Tests: write/read round-trip; bestand heeft mode 0600;
withLockgeeft de returnwaarde terug en laat de lock los na een exception; twee processen (proc_openvan een klein PHP-script dat 300 ms lockt) serialiseren: het tweede wacht.
Task 15: NetatmoClient + tokentests
Files: src/NetatmoClient.php, tests/NetatmoClientTest.php, tests/FakeHttp.php
__construct(HttpClient $http, TokenStore $store, string $clientId, string $clientSecret, ?callable $now = null).accessToken(): onderwithLock: lees token; alsexpires_at - now < 300→refresh().refresh()POSThttps://api.netatmo.com/oauth2/tokenmetgrant_type=refresh_token, refresh_token, client_id, client_secret; responseaccess_token, refresh_token, expires_in→write(['access_token', 'refresh_token' => nieuwe, 'expires_at' => now + expires_in]). Status 400 met bodyerror: invalid_grant→NetatmoAuthException. Ontbrekend tokenbestand →NetatmoAuthException.apiGet(path, query): GET met Bearer; bij 401/403 eenmaligrefresh()+ retry; daarnaNetatmoAuthException. Andere status ≠ 200 →UpstreamException. Responsebodyteruggeven.getStationsData():GET /api/getstationsdata;getMeasure($deviceId, $moduleId, $begin, $end):GET /api/getmeasuremetscale=1day, type=sum_rain, optimize=false, real_time=false.- Tests met
FakeHttp(script van responses, registreert requests):- verlopen token → eerst POST refresh, daarna GET met nieuwe Bearer; store bevat nieuwe refresh token.
- geldig token → geen refresh.
- GET 401 → refresh + retry; tweede 401 →
NetatmoAuthException. - refresh met
invalid_grant→NetatmoAuthException. - gelijktijdigheid: twee clients op dezelfde store, de refresh van de eerste schrijft; de tweede leest binnen de lock het verse token en refresht niet (FakeHttp telt 1 POST).
Task 16: LiveSource, bin/netatmo-init.php, data.php live
Files: src/LiveSource.php, bin/netatmo-init.php, public/api/data.php
LiveSource(NetatmoClient, OpenMeteoClient)(OpenMeteoClient komt in Task 17; hier alvast als constructorparameter metforecast()doorgifte).bin/netatmo-init.php <refresh_token> [access_token]: schrijftstorage/netatmo-token.jsonmetexpires_at 0(dwingt refresh bij eerste call), meldt pad en mode.data.php:data_source === 'live'→LiveSourcemetHttp,TokenStore(storage/netatmo-token.json).config.phpaanmaken met de echte gegevens,storage/aanmaken, init draaien,curlop de live API: echte waarden zichtbaar. Commit (zonder config.php/storage). Fase 3 klaar.
Fase 4: Open-Meteo live
Task 17: OpenMeteoClient
Files: src/OpenMeteoClient.php, tests/OpenMeteoClientTest.php
dailyForecast(float $lat, float $lon, string $tz): array→ GEThttps://api.open-meteo.com/v1/forecastmetlatitude, longitude, daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max, timezone=$tz, forecast_days=6; geeft['daily']terug; status ≠ 200 of ontbrekendedaily→UpstreamException.- Test met FakeHttp: query bevat alle parameters; foutstatus → exception.
- Live check via
curlop de API:forecastheeft 5 dagen vanaf morgen. Commit. Fase 4 klaar.
Fase 5: PWA
Task 18: manifest, iconen, iOS-meta
Files: public/manifest.webmanifest, public/icons/icon.svg, public/icons/icon-{180,192,512}.png, public/index.html
- Manifest volgens §7 (
name Weerscherm, short_name Weer, display standalone, orientation landscape, start_url "./", scope "./", background_color/theme_color #E8EDF1, icons 192 + 512). icon.svg: zon/wolk in de iconstijl op--bg, 512×512. PNG's renderen metrsvg-convertofsips/qlmanage(macOS); fallback: een klein Node-script metcanvasis NIET toegestaan (geen deps) → gebruikqlmanage -t -s 512 -o . icon.svgensips -zvoor 192/180.index.html:apple-touch-icon180,apple-mobile-web-app-capable,apple-mobile-web-app-status-bar-style default,apple-mobile-web-app-title Weer.
Task 19: service worker
Files: public/sw.js, public/app.js
CACHE = 'weerscherm-v1';SHELL = ['./', './index.html', './styles.css', './app.js', './format.js', './icons.js', './manifest.webmanifest', './icons/icon-192.png', './icons/icon-512.png', './icons/icon-180.png'].install:cache.addAll(SHELL)+skipWaiting().activate: oude caches weg +clients.claim().fetch: URL bevat/api/data.php→ network-first metAbortControllertimeout 8000 ms; succes → response klonen in cache; fout → cached response met extra headerX-From-Cache: 1(vianew Response(body, {headers})), of503JSON{"errors":["offline"]}. Overige GET → cache-first, netwerk als fallback.app.js: registreert./sw.js;controllerchange→location.reload(); response metX-From-Cache→ behandelen als "geen verbinding" (status tonen, data wel renderen).- Check: in Chrome DevTools offline zetten → laatste data blijft, status verschijnt. Commit. Fase 5 klaar.
Fase 6: deploy en README
Task 20: OrbStack-vhost, README, deploy
Files: README.md, orbstack/… (buiten deze repo, in /Volumes/development/sites/orbstack/divers/weerscherm/), deploy/ notities in README
- OrbStack:
Dockerfileopphp:8.3-fpm-alpine(curl-ext zit erin),docker-compose.ymlinshared-network, vhostweerscherm.testmet docroot/var/www/html/weerscherm/public, mount in shared compose,/etc/hosts-regel. Alleen uitvoeren als OrbStack draait; anders de bestanden klaarzetten en in de README beschrijven. - README: installatie,
config.php, Netatmo-koppeling (§3.1),bin/netatmo-init.php, tests (composer test,npm test), lokaal draaien (php -Sen OrbStack), deploy (Gitea + DeployPad,storage/als gedeelde map buiten de release), productie-URLhttps://homedisplay.bahne-drei.de/, iPad-instellingen (beginscherm, Automatisch vergrendelen → Nooit, Begeleide toegang), acceptatiecriteria uit §11 als checklist. - Commit. Fase 6 klaar.