From dc8cbee8da96f967f932f40a7565ae6e7e98fe4a Mon Sep 17 00:00:00 2001 From: Theo Weijmer Date: Fri, 11 Sep 2026 14:49:39 +0200 Subject: [PATCH] docs: README, OrbStack notes, autoload fallback for deploys without composer Co-Authored-By: Claude Fable 5.1 --- .phpunit.result.cache | 2 +- README.md | 154 ++++++++++++++++++++++++++++++++++++++++++ bin/netatmo-init.php | 2 +- public/.htaccess | 11 +++ public/api/data.php | 2 +- src/autoload.php | 24 +++++++ 6 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 README.md create mode 100644 public/.htaccess create mode 100644 src/autoload.php diff --git a/.phpunit.result.cache b/.phpunit.result.cache index 9785b34..db970fc 100644 --- a/.phpunit.result.cache +++ b/.phpunit.result.cache @@ -1 +1 @@ -{"version":2,"defects":{"Weerscherm\\Tests\\DashboardServiceTest::testForecastUsesStationLocationUnlessConfigured":7,"Weerscherm\\Tests\\DashboardServiceTest::testMeasureFailureFallsBackToSumRain24":7,"Weerscherm\\Tests\\NetatmoClientTest::testRejectedTokenAlreadyReplacedByOtherProcessIsReusedWithoutRefresh":8},"times":{"Weerscherm\\Tests\\ConfigTest::testDefaultsApplyWhenKeysAreMissing":0,"Weerscherm\\Tests\\ConfigTest::testFileValuesOverrideDefaults":0,"Weerscherm\\Tests\\ConfigTest::testUnknownKeyReturnsGivenDefault":0,"Weerscherm\\Tests\\ConfigTest::testFileMustReturnArray":0,"Weerscherm\\Tests\\StationMapperTest::testStationMetadata":0,"Weerscherm\\Tests\\StationMapperTest::testOutdoor":0,"Weerscherm\\Tests\\StationMapperTest::testIndoorDefaultOrderAndUnreachableModule":0,"Weerscherm\\Tests\\StationMapperTest::testIndoorConfigDeterminesOrderAndLabels":0,"Weerscherm\\Tests\\StationMapperTest::testRainModule":0,"Weerscherm\\Tests\\StationMapperTest::testWithoutRainModule":0,"Weerscherm\\Tests\\StationMapperTest::testUnreachableOutdoorModuleGivesNulls":0,"Weerscherm\\Tests\\StationMapperTest::testUnknownDeviceIdFallsBackToFirstStation":0,"Weerscherm\\Tests\\StationMapperTest::testEmptyDevicesGivesNullsWithoutCrashing":0,"Weerscherm\\Tests\\RainCalculatorTest::testNormalWeek":0,"Weerscherm\\Tests\\RainCalculatorTest::testMissingPreviousDayIsNullAndExcludedFromTotal":0,"Weerscherm\\Tests\\RainCalculatorTest::testMissingTodayFallsBackToSumRain24":0,"Weerscherm\\Tests\\RainCalculatorTest::testMonthBoundaryKeepsPreviousDaysButNotInMonthTotal":0,"Weerscherm\\Tests\\RainCalculatorTest::testDaylightSavingTransitionMapsTimestampsToLocalDates":0,"Weerscherm\\Tests\\RainCalculatorTest::testEmptyBodyGivesNulls":0,"Weerscherm\\Tests\\RainCalculatorTest::testDateBeginIsMinOfFirstOfMonthAndThreeDaysAgo":0,"Weerscherm\\Tests\\CacheTest::testWriteThenReadRoundTrip":0.003,"Weerscherm\\Tests\\CacheTest::testMissingKeyReadsNull":0.004,"Weerscherm\\Tests\\CacheTest::testCorruptFileReadsNull":0,"Weerscherm\\Tests\\CacheTest::testFreshness":0.001,"Weerscherm\\Tests\\CacheTest::testKeyIsSanitised":0,"Weerscherm\\Tests\\CacheTest::testWriteCreatesDirectory":0,"Weerscherm\\Tests\\ForecastMapperTest::testSkipsTodayAndReturnsFiveDays":0,"Weerscherm\\Tests\\ForecastMapperTest::testNullValuesStayNull":0,"Weerscherm\\Tests\\ForecastMapperTest::testShorterResponseGivesFewerDays":0,"Weerscherm\\Tests\\ForecastMapperTest::testMissingTimeArrayGivesEmptyList":0,"Weerscherm\\Tests\\DashboardServiceTest::testHappyPathMatchesContract":0.001,"Weerscherm\\Tests\\DashboardServiceTest::testMeasureIsRequestedForRainModuleFromMidnightOfDateBegin":0.001,"Weerscherm\\Tests\\DashboardServiceTest::testForecastUsesStationLocationUnlessConfigured":0.001,"Weerscherm\\Tests\\DashboardServiceTest::testStationsFailureWithoutCacheGivesNullsAndError":0,"Weerscherm\\Tests\\DashboardServiceTest::testStationsFailureWithCacheServesStaleData":0.001,"Weerscherm\\Tests\\DashboardServiceTest::testAuthFailureGivesNetatmoAuthError":0.001,"Weerscherm\\Tests\\DashboardServiceTest::testMeasureFailureFallsBackToSumRain24":0.001,"Weerscherm\\Tests\\DashboardServiceTest::testForecastFailureLeavesRestIntact":0.001,"Weerscherm\\Tests\\DashboardServiceTest::testSecondBuildWithinTtlUsesCache":0.001,"Weerscherm\\Tests\\TokenStoreTest::testWriteReadRoundTripAndMode":0,"Weerscherm\\Tests\\TokenStoreTest::testMissingOrInvalidFileReadsNull":0,"Weerscherm\\Tests\\TokenStoreTest::testWithLockReturnsValueAndReleasesAfterException":0,"Weerscherm\\Tests\\TokenStoreTest::testConcurrentProcessesAreSerialised":0.443,"Weerscherm\\Tests\\NetatmoClientTest::testValidTokenIsUsedWithoutRefresh":0.001,"Weerscherm\\Tests\\NetatmoClientTest::testExpiringTokenIsRefreshedAndRotatedRefreshTokenStored":0.001,"Weerscherm\\Tests\\NetatmoClientTest::testFreshlyInitialisedStoreWithoutAccessTokenRefreshesFirst":0.001,"Weerscherm\\Tests\\NetatmoClientTest::testUnauthorizedResponseTriggersOneRefreshAndRetry":0.001,"Weerscherm\\Tests\\NetatmoClientTest::testSecondUnauthorizedResponseIsAuthError":0.001,"Weerscherm\\Tests\\NetatmoClientTest::testInvalidGrantIsAuthError":0,"Weerscherm\\Tests\\NetatmoClientTest::testMissingTokenFileIsAuthError":0,"Weerscherm\\Tests\\NetatmoClientTest::testServerErrorIsUpstreamError":0,"Weerscherm\\Tests\\NetatmoClientTest::testRefreshServerErrorIsUpstreamNotAuth":0,"Weerscherm\\Tests\\NetatmoClientTest::testConcurrentClientsShareOneRefresh":0.001,"Weerscherm\\Tests\\NetatmoClientTest::testRejectedTokenAlreadyReplacedByOtherProcessIsReusedWithoutRefresh":0.001,"Weerscherm\\Tests\\NetatmoClientTest::testGetMeasureQuery":0,"Weerscherm\\Tests\\OpenMeteoClientTest::testRequestsSixDaysWithAllDailyVariables":0,"Weerscherm\\Tests\\OpenMeteoClientTest::testErrorStatusIsUpstreamException":0,"Weerscherm\\Tests\\OpenMeteoClientTest::testMissingDailyIsUpstreamException":0}} \ No newline at end of file +{"version":2,"defects":{"Weerscherm\\Tests\\DashboardServiceTest::testForecastUsesStationLocationUnlessConfigured":7,"Weerscherm\\Tests\\DashboardServiceTest::testMeasureFailureFallsBackToSumRain24":7,"Weerscherm\\Tests\\NetatmoClientTest::testRejectedTokenAlreadyReplacedByOtherProcessIsReusedWithoutRefresh":8},"times":{"Weerscherm\\Tests\\ConfigTest::testDefaultsApplyWhenKeysAreMissing":0,"Weerscherm\\Tests\\ConfigTest::testFileValuesOverrideDefaults":0,"Weerscherm\\Tests\\ConfigTest::testUnknownKeyReturnsGivenDefault":0,"Weerscherm\\Tests\\ConfigTest::testFileMustReturnArray":0,"Weerscherm\\Tests\\StationMapperTest::testStationMetadata":0,"Weerscherm\\Tests\\StationMapperTest::testOutdoor":0,"Weerscherm\\Tests\\StationMapperTest::testIndoorDefaultOrderAndUnreachableModule":0,"Weerscherm\\Tests\\StationMapperTest::testIndoorConfigDeterminesOrderAndLabels":0,"Weerscherm\\Tests\\StationMapperTest::testRainModule":0,"Weerscherm\\Tests\\StationMapperTest::testWithoutRainModule":0,"Weerscherm\\Tests\\StationMapperTest::testUnreachableOutdoorModuleGivesNulls":0,"Weerscherm\\Tests\\StationMapperTest::testUnknownDeviceIdFallsBackToFirstStation":0,"Weerscherm\\Tests\\StationMapperTest::testEmptyDevicesGivesNullsWithoutCrashing":0,"Weerscherm\\Tests\\RainCalculatorTest::testNormalWeek":0,"Weerscherm\\Tests\\RainCalculatorTest::testMissingPreviousDayIsNullAndExcludedFromTotal":0,"Weerscherm\\Tests\\RainCalculatorTest::testMissingTodayFallsBackToSumRain24":0,"Weerscherm\\Tests\\RainCalculatorTest::testMonthBoundaryKeepsPreviousDaysButNotInMonthTotal":0,"Weerscherm\\Tests\\RainCalculatorTest::testDaylightSavingTransitionMapsTimestampsToLocalDates":0,"Weerscherm\\Tests\\RainCalculatorTest::testEmptyBodyGivesNulls":0,"Weerscherm\\Tests\\RainCalculatorTest::testDateBeginIsMinOfFirstOfMonthAndThreeDaysAgo":0,"Weerscherm\\Tests\\CacheTest::testWriteThenReadRoundTrip":0.003,"Weerscherm\\Tests\\CacheTest::testMissingKeyReadsNull":0.004,"Weerscherm\\Tests\\CacheTest::testCorruptFileReadsNull":0,"Weerscherm\\Tests\\CacheTest::testFreshness":0.001,"Weerscherm\\Tests\\CacheTest::testKeyIsSanitised":0,"Weerscherm\\Tests\\CacheTest::testWriteCreatesDirectory":0,"Weerscherm\\Tests\\ForecastMapperTest::testSkipsTodayAndReturnsFiveDays":0,"Weerscherm\\Tests\\ForecastMapperTest::testNullValuesStayNull":0,"Weerscherm\\Tests\\ForecastMapperTest::testShorterResponseGivesFewerDays":0,"Weerscherm\\Tests\\ForecastMapperTest::testMissingTimeArrayGivesEmptyList":0,"Weerscherm\\Tests\\DashboardServiceTest::testHappyPathMatchesContract":0.002,"Weerscherm\\Tests\\DashboardServiceTest::testMeasureIsRequestedForRainModuleFromMidnightOfDateBegin":0.001,"Weerscherm\\Tests\\DashboardServiceTest::testForecastUsesStationLocationUnlessConfigured":0.001,"Weerscherm\\Tests\\DashboardServiceTest::testStationsFailureWithoutCacheGivesNullsAndError":0,"Weerscherm\\Tests\\DashboardServiceTest::testStationsFailureWithCacheServesStaleData":0.001,"Weerscherm\\Tests\\DashboardServiceTest::testAuthFailureGivesNetatmoAuthError":0.001,"Weerscherm\\Tests\\DashboardServiceTest::testMeasureFailureFallsBackToSumRain24":0,"Weerscherm\\Tests\\DashboardServiceTest::testForecastFailureLeavesRestIntact":0.001,"Weerscherm\\Tests\\DashboardServiceTest::testSecondBuildWithinTtlUsesCache":0.001,"Weerscherm\\Tests\\TokenStoreTest::testWriteReadRoundTripAndMode":0,"Weerscherm\\Tests\\TokenStoreTest::testMissingOrInvalidFileReadsNull":0,"Weerscherm\\Tests\\TokenStoreTest::testWithLockReturnsValueAndReleasesAfterException":0,"Weerscherm\\Tests\\TokenStoreTest::testConcurrentProcessesAreSerialised":0.438,"Weerscherm\\Tests\\NetatmoClientTest::testValidTokenIsUsedWithoutRefresh":0.001,"Weerscherm\\Tests\\NetatmoClientTest::testExpiringTokenIsRefreshedAndRotatedRefreshTokenStored":0.001,"Weerscherm\\Tests\\NetatmoClientTest::testFreshlyInitialisedStoreWithoutAccessTokenRefreshesFirst":0.001,"Weerscherm\\Tests\\NetatmoClientTest::testUnauthorizedResponseTriggersOneRefreshAndRetry":0.001,"Weerscherm\\Tests\\NetatmoClientTest::testSecondUnauthorizedResponseIsAuthError":0.001,"Weerscherm\\Tests\\NetatmoClientTest::testInvalidGrantIsAuthError":0,"Weerscherm\\Tests\\NetatmoClientTest::testMissingTokenFileIsAuthError":0,"Weerscherm\\Tests\\NetatmoClientTest::testServerErrorIsUpstreamError":0,"Weerscherm\\Tests\\NetatmoClientTest::testRefreshServerErrorIsUpstreamNotAuth":0.001,"Weerscherm\\Tests\\NetatmoClientTest::testConcurrentClientsShareOneRefresh":0.001,"Weerscherm\\Tests\\NetatmoClientTest::testRejectedTokenAlreadyReplacedByOtherProcessIsReusedWithoutRefresh":0.001,"Weerscherm\\Tests\\NetatmoClientTest::testGetMeasureQuery":0,"Weerscherm\\Tests\\OpenMeteoClientTest::testRequestsSixDaysWithAllDailyVariables":0,"Weerscherm\\Tests\\OpenMeteoClientTest::testErrorStatusIsUpstreamException":0,"Weerscherm\\Tests\\OpenMeteoClientTest::testMissingDailyIsUpstreamException":0}} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d02d79a --- /dev/null +++ b/README.md @@ -0,0 +1,154 @@ +# Weerscherm + +Een PWA voor een iPad mini (liggend) in de woonkamer. Toont de actuele gegevens van een +Netatmo-weerstation (buiten, drie binnenruimtes, regen) en een verwachting voor vijf dagen van Open-Meteo. + +- Spec: [docs/weerscherm-spec.md](docs/weerscherm-spec.md) +- Mockup (ontwerp A is leidend): [docs/design/weerscherm-layouts.html](docs/design/weerscherm-layouts.html) +- Implementatieplan: [docs/superpowers/plans/2026-09-11-weerscherm.md](docs/superpowers/plans/2026-09-11-weerscherm.md) + +``` +iPad (PWA) ──GET /api/data.php──▶ PHP-backend ──▶ Netatmo API + │ └──▶ Open-Meteo API + └── storage/ (tokens + cache) +``` + +## Vereisten + +- PHP 8.3 met `ext-curl` en `ext-json`. Geen framework, geen runtime-dependencies. +- Composer alleen voor PHPUnit (dev). Node 20+ alleen voor de JS-tests (dev). +- Een webserver met `public/` als document root. `storage/` ligt daarbuiten en moet schrijfbaar zijn voor de webserver. + +## Installatie + +```bash +git clone weerscherm +cd weerscherm +composer install # alleen nodig voor tests; de app draait ook zonder vendor/ +cp config.example.php config.php +mkdir -p storage && chmod 750 storage +``` + +Vul `config.php` in. De sleutels: + +| Sleutel | Betekenis | +|---|---| +| `data_source` | `fixture` (voorbeelddata, geen koppeling nodig) of `live` | +| `netatmo_client_id`, `netatmo_client_secret` | uit de app op dev.netatmo.com | +| `netatmo_device_id` | optioneel, `_id` van het basisstation; leeg = eerste station | +| `timezone` | fallback als het station geen tijdzone meldt (standaard `Europe/Berlin`) | +| `forecast_lat`, `forecast_lon` | optioneel; leeg = locatie van het station | +| `indoor_modules` | optioneel, `_id => label` in de gewenste volgorde; leeg = Netatmo-namen | +| `cache_ttl` | seconden per bron: `stations` 300, `measure` 900, `forecast` 3600 | +| `storage_path` | map voor tokens en cache, buiten de docroot | + +`config.php` en `storage/` staan in `.gitignore` en komen nooit in git. + +## Netatmo-koppeling + +1. Maak een app aan op . Zet `client_id` en `client_secret` in `config.php`. +2. Genereer met de token generator op de app-pagina een token met scope `read_station`. +3. Sla de refresh token op: + +```bash +php bin/netatmo-init.php '' +``` + +Dat schrijft `storage/netatmo-token.json` (mode 600). Bij het eerste verzoek wordt het access token ververst. +De refresh token roteert bij elke verversing; de nieuwste wordt automatisch bewaard. Gebruik een oude nooit opnieuw. + +Toont het scherm "Netatmo-koppeling verlopen", genereer dan een nieuwe token en voer stap 3 opnieuw uit. + +## Tests + +```bash +composer test # PHPUnit: mapping, regenberekening, tokenrotatie, cache, stale-fallback +npm test # node --test: formattering, WMO-mapping, service worker +``` + +## Lokaal draaien + +Snel, zonder Docker: + +```bash +php -S 127.0.0.1:8080 -t public +``` + +Open . Met `data_source = fixture` is er geen koppeling nodig. +Visuele check: een browservenster van exact 1133 × 744 naast de mockup (tab A). + +Via de OrbStack shared-Apache-setup (`weerscherm.test`): + +```bash +# eenmalig: hostnaam +sudo sh -c 'echo "127.0.0.1 weerscherm.test" >> /etc/hosts' +# shared apache herstarten (vhost en mount staan al in orbstack/shared) +cd /Volumes/development/sites/orbstack/shared && docker-compose up -d --build +# php-container van dit project +cd /Volumes/development/sites/orbstack/divers/weerscherm && docker-compose up -d --build +``` + +De bestanden staan in `orbstack/divers/weerscherm/` (Dockerfile, compose) en `orbstack/shared/apache/vhosts/weerscherm.conf`. + +## Deploy (productie) + +Productie-URL: `https://homedisplay.bahne-drei.de/` + +- Git op de self-hosted Gitea, squash-and-merge naar `main`, deploy via DeployPad. +- Document root is `public/`. `src/`, `storage/`, `config.php` en `bin/` zijn dan niet via HTTP bereikbaar. +- `storage/` moet deploys overleven: maak er in DeployPad een gedeelde map van (shared folder) of zet + `storage_path` in `config.php` naar een map buiten de release-map, bijvoorbeeld `/var/www/weerscherm-storage`. + De map moet schrijfbaar zijn voor de webserver-gebruiker (`www-data`). +- `config.php` is per omgeving en staat niet in git; zet hem ook in de gedeelde map en symlink, of maak hem + eenmalig aan in de release-root. +- `composer install` is op de server niet nodig: `src/autoload.php` laadt de klassen zelf als `vendor/` ontbreekt. +- HTTPS is vereist voor de service worker en "Zet op beginscherm". +- Na de eerste deploy: `php bin/netatmo-init.php ''` op de server, en controleer + `https://homedisplay.bahne-drei.de/api/data.php` op `"errors": []`. +- De app staat bewust zonder login. Kies een niet-voor-de-hand-liggend (sub)domein; `robots.txt` en + `X-Robots-Tag: noindex` houden zoekmachines buiten de deur. + +Nieuwe versie van de frontend? Verhoog `VERSION` in `public/sw.js`; de app herlaadt zichzelf bij `controllerchange` +en in elk geval dagelijks rond 04:00. + +## iPad-instellingen + +1. Open de URL in Safari, tik op Delen → **Zet op beginscherm**. Start de app daarna altijd vanaf het beginscherm + (fullscreen, zonder Safari-balken). +2. Instellingen → Scherm en helderheid → **Automatisch vergrendelen → Nooit**. +3. Optioneel: Instellingen → Toegankelijkheid → **Begeleide toegang** inschakelen en starten met drie keer op de + zijknop, zodat het scherm niet per ongeluk verlaten wordt. +4. Houd de iPad liggend; in portretstand verschijnt "Draai de iPad een kwartslag". + +## Gedrag bij storingen + +| Situatie | Wat het scherm doet | +|---|---| +| Meting ouder dan 30 minuten of Netatmo tijdelijk onbereikbaar | "meting HH:MM · verouderd" in oranje, laatste data blijft staan | +| Server of wifi onbereikbaar | laatste data blijft staan, kopregel toont "geen verbinding sinds HH:MM"; herstelt vanzelf | +| Refresh token ongeldig (`netatmo_auth`) | melding over de bovenzone met de herstelinstructie | +| Open-Meteo onbereikbaar | laatst gecachte verwachting, `forecast_stale: true` | +| Allereerste start zonder data | "Gegevens laden…", daarna een concrete foutmelding | + +## Acceptatiecriteria (spec §11) + +- [ ] Op de iPad mini, gestart vanaf het beginscherm, draait de app fullscreen en klopt de layout met ontwerp A. +- [ ] Alle waarden komen overeen met de Netatmo-app: buiten, 3× binnen, min/max met tijdstippen, regen vandaag, + de afgelopen 3 dagen en het maandtotaal. +- [ ] De verwachting toont 5 dagen vanaf morgen met het juiste icoon per WMO-code. +- [ ] De data ververst zichzelf. Na middernacht staat de nieuwe datum er binnen 5 minuten. +- [ ] Wifi uit: de laatste data blijft staan met "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. +- [ ] `composer test` en `npm test` slagen. + +## Structuur + +``` +public/ document root: index.html, styles.css, app.js, format.js, icons.js, sw.js, manifest, icons/, api/data.php +src/ Config, Http, TokenStore, NetatmoClient, OpenMeteoClient, Cache, mappers, DashboardService +bin/ netatmo-init.php +storage/ tokens en cache (niet in git, niet in docroot) +tests/ PHPUnit + JSON-fixtures; tests/js voor node --test +docs/ spec, mockup, plan +``` diff --git a/bin/netatmo-init.php b/bin/netatmo-init.php index c591e71..f4edf06 100755 --- a/bin/netatmo-init.php +++ b/bin/netatmo-init.php @@ -16,7 +16,7 @@ declare(strict_types=1); use Weerscherm\Config; use Weerscherm\TokenStore; -require dirname(__DIR__) . '/vendor/autoload.php'; +require dirname(__DIR__) . '/src/autoload.php'; $refresh = $argv[1] ?? ''; $access = $argv[2] ?? ''; diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..5029711 --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,11 @@ +# Alleen relevant als de productieserver AllowOverride toestaat. De vhost-config heeft voorrang. + + Header set X-Robots-Tag "noindex, nofollow" + + Header set Cache-Control "no-cache" + + + + AddType application/manifest+json .webmanifest + +Options -Indexes diff --git a/public/api/data.php b/public/api/data.php index aa2cb7a..d9a5214 100644 --- a/public/api/data.php +++ b/public/api/data.php @@ -14,7 +14,7 @@ header('Content-Type: application/json; charset=utf-8'); header('Cache-Control: no-store'); header('X-Robots-Tag: noindex'); -require $root . '/vendor/autoload.php'; +require $root . '/src/autoload.php'; $configFile = $root . '/config.php'; if (!is_file($configFile)) { diff --git a/src/autoload.php b/src/autoload.php new file mode 100644 index 0000000..3177a76 --- /dev/null +++ b/src/autoload.php @@ -0,0 +1,24 @@ +