Files
weerscherm/docs/superpowers/plans/2026-09-11-weerscherm.md
2026-09-11 14:34:39 +02:00

25 KiB
Raw Permalink Blame History

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-curl en ext-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, overal font-variant-numeric: tabular-nums.
  • API: ruwe getallen, ISO 8601 met offset, null voor 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.php en storage/; beide in .gitignore, storage/ buiten de docroot, tokenbestand chmod 600.
  • Projectroot is deze map (home-display), docroot is public/.
  • Commits klein en per taak; config.php en storage/ 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.json met "autoload": {"psr-4": {"Weerscherm\\": "src/"}}, "require": {"php": ">=8.3", "ext-curl": "*", "ext-json": "*"}, "require-dev": {"phpunit/phpunit": "^11"}, script test.
  • 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 test groen. 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, lon uit place (let op: location is [lon, lat]).
  • outdoor bevat temp 16.4, trend "down", min 9.2, min_at <unix>, max 19.8, max_at <unix>.
  • indoor heeft 3 items in volgorde NAMain, dan modules; "Kantoor" heeft temp/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. Leeg devices → 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 van min(1e van de maand, vandaag − 3 dagen).
  • Elke timestamp → lokale datum Y-m-d in de tijdzone van $now.
  • today = waarde van vandaag, anders $sumRain24.
  • previous_days = 3 datums vóór vandaag, oudste eerst, ontbrekende dag mm: null.
  • month_total = som van alle dagen in de huidige kalendermaand inclusief vandaag; null als 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). Timestamp 2026-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.
  • dateBegin op 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 daily met arrays time, weather_code, temperature_2m_max, temperature_2m_min, precipitation_sum, precipitation_probability_max van 6 elementen.
  • Output: 5 items (index 1..5): date, weather_code, temp_max, temp_min, precip_mm, precip_prob. Ontbrekende (null) waarden blijven null. Minder dan 6 dagen → zoveel als beschikbaar.

Task 5: Cache

Files: src/Cache.php, tests/CacheTest.php

  • write schrijft <dir>/<key>.json met {stored_at, data} via tmp + rename.
  • read geeft 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

  • FixtureSource bouwt in PHP realistische data relatief aan nu (zodat de UI altijd "vandaag" laat zien): stationsdata met date_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):

  1. Cache fresh → gebruik cache, stale false.
  2. Anders source->…(); succes → cache write, stale false.
  3. Exception → cache stale (ongeacht leeftijd) → stale true + errorcode; geen cache → velden null + errorcode.
  4. NetatmoAuthException → code netatmo_auth; andere Netatmo-fouten netatmo_unavailable; forecast forecast_unavailable.
  5. Regen: date_begin uit RainCalculator::dateBegin, date_end = now; zonder rain_module_id → rain alle null zonder call.
  6. Forecast-locatie: config lat/lon, anders station-lat/lon; beide onbekend → forecast_unavailable.
  7. Tijden als ISO 8601 met offset in de stationstijdzone (timezone uit 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 op config.example.php met een 500 en tekst "config.php ontbreekt" als die er niet is), kiest FixtureSource of LiveSource (LiveSource komt in fase 3; tot dan throw bij live).
  • 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 public en curl geeft geldige JSON met errors: [].
  • 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 met Math.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) en nextTimeAt(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, showers letterlijk uit de mockup.
  • Nieuw in dezelfde stijl (stroke 2.4, ronde uiteinden, i-sun/i-cloud/i-drop):
    • cloud: alleen de wolkvorm van rain maar gecentreerd (y +5).
    • fog: wolk plus 2 horizontale lijnen eronder (M14 36h20 M18 42h14, klasse i-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) in i-drop.
    • thunder: wolk plus bliksem M25 32l-4 7h5l-3 6 in i-sun.
  • Onbekende naam → cloud.

Task 11: index.html + styles.css (ontwerp A)

Files: public/index.html, public/styles.css

  • <html lang="nl">, viewport width=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 hebben id's of data-*.
  • Extra elementen: #status (klein, in .a-right, voor "geen verbinding sinds HH:MM"), #banner (over de bovenzone, verborgen; voor netatmo_auth en 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; body heeft background: var(--bg) en overflow: hidden; #stage gecentreerd via JS-schaal; padding houdt rekening met env(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 krijgt transform: translate(x,y) scale(s). Aan resize en orientationchange.
  • render(data): vult alle velden met format.js; regenstaven via barHeights; forecast via icon(wmoIcon(code)); indoor onbereikbaar → "–" in --muted; station.stale of meting ouder dan 30 min → .a-upd warm + " verouderd"; errors bevat netatmo_auth → banner met herstelinstructie.
  • refresh(): fetch('api/data.php', {cache: 'no-store'}); bij succes lastData bewaren, 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; setTimeout tot middernacht → refresh + datum; setTimeout tot 04:00 → location.reload().
  • navigator.wakeLock?.request('screen') in try/catch, opnieuw bij visibilitychange.
  • SW-registratie en controllerchange → location.reload() (fase 5; hier alvast met if ('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 met CURLOPT_CONNECTTIMEOUT 5, CURLOPT_TIMEOUT 8, RETURNTRANSFER, FOLLOWLOCATION false. Curl-fout → UpstreamException.
  • HttpResponse::json() → json_decode(..., true, 512, JSON_THROW_ON_ERROR), gooit UpstreamException bij ongeldige JSON.

Task 14: TokenStore

Files: src/TokenStore.php, tests/TokenStoreTest.php

  • write: json_encode naar <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; withLock geeft de returnwaarde terug en laat de lock los na een exception; twee processen (proc_open van 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(): onder withLock: lees token; als expires_at - now < 300 → refresh(). refresh() POST https://api.netatmo.com/oauth2/token met grant_type=refresh_token, refresh_token, client_id, client_secret; response access_token, refresh_token, expires_in → write(['access_token', 'refresh_token' => nieuwe, 'expires_at' => now + expires_in]). Status 400 met body error: invalid_grant → NetatmoAuthException. Ontbrekend tokenbestand → NetatmoAuthException.
  • apiGet(path, query): GET met Bearer; bij 401/403 eenmalig refresh() + retry; daarna NetatmoAuthException. Andere status ≠ 200 → UpstreamException. Response body teruggeven.
  • getStationsData(): GET /api/getstationsdata; getMeasure($deviceId, $moduleId, $begin, $end): GET /api/getmeasure met scale=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 met forecast() doorgifte).
  • bin/netatmo-init.php <refresh_token> [access_token]: schrijft storage/netatmo-token.json met expires_at 0 (dwingt refresh bij eerste call), meldt pad en mode.
  • data.php: data_source === 'live' → LiveSource met Http, TokenStore(storage/netatmo-token.json).
  • config.php aanmaken met de echte gegevens, storage/ aanmaken, init draaien, curl op 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 → GET https://api.open-meteo.com/v1/forecast met latitude, 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 ontbrekende daily → UpstreamException.
  • Test met FakeHttp: query bevat alle parameters; foutstatus → exception.
  • Live check via curl op de API: forecast heeft 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 met rsvg-convert of sips/qlmanage (macOS); fallback: een klein Node-script met canvas is NIET toegestaan (geen deps) → gebruik qlmanage -t -s 512 -o . icon.svg en sips -z voor 192/180.
  • index.html: apple-touch-icon 180, 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 met AbortController timeout 8000 ms; succes → response klonen in cache; fout → cached response met extra header X-From-Cache: 1 (via new Response(body, {headers})), of 503 JSON {"errors":["offline"]}. Overige GET → cache-first, netwerk als fallback.
  • app.js: registreert ./sw.js; controllerchange → location.reload(); response met X-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: Dockerfile op php:8.3-fpm-alpine (curl-ext zit erin), docker-compose.yml in shared-network, vhost weerscherm.test met 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 -S en OrbStack), deploy (Gitea + DeployPad, storage/ als gedeelde map buiten de release), productie-URL https://homedisplay.bahne-drei.de/, iPad-instellingen (beginscherm, Automatisch vergrendelen → Nooit, Begeleide toegang), acceptatiecriteria uit §11 als checklist.
  • Commit. Fase 6 klaar.