Merge feature/verwachting-vandaag: verwachting toont nu ook vandaag (6 dagen)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-13 10:01:20 +02:00
co-authored by Claude Sonnet 5
6 changed files with 22 additions and 21 deletions
+5 -4
View File
@@ -1,6 +1,6 @@
# Weerscherm: bouwspecificatie v1 # 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 5 dagen. 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. **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.
@@ -26,7 +26,8 @@ Werkwijze: lees eerst dit document en de mockup. Stel daarna een plan voor per f
- Buitentemperatuur, met de laagste en hoogste temperatuur van vandaag (inclusief tijdstippen) en de trend. - 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. - 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. - Regen: vandaag, de afgelopen 3 dagen per dag, en het totaal van de huidige maand.
- Verwachting voor de komende 5 dagen (vanaf morgen): max/min-temperatuur, neerslag in mm, neerslagkans en een weericoon. - 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. - 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. **Niet in v1:** nachtmodus (ontwerp C), andere meetwaarden (CO₂, luchtvochtigheid, wind), meerdere stations, een instellingenscherm, pushmeldingen.
@@ -255,7 +256,7 @@ De iconen `sun`, `partly`, `rain` en `showers` staan in de mockup. Maak `cloud`,
- "Vandaag" groot, in mm met 1 decimaal. - "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. - 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". - Daaronder "September 38,5 mm".
- **Verwachting (rechtsonder):** 5 kolommen met dagafkorting ("Za"), icoon (58px), "max° min°" als hele getallen (min in `--muted`) en "x,x mm" plus neerslagkans in %. Geen neerslag toont "0 mm". - **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.
### 6.4 Verversen ### 6.4 Verversen
@@ -342,7 +343,7 @@ Hangt af van de iPadOS-versie in §0:
- [ ] Op de iPad mini, gestart vanaf het beginscherm, draait de app fullscreen zonder Safari-balken en klopt de layout visueel met ontwerp A. - [ ] 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. - [ ] 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 5 dagen vanaf morgen met het juiste icoon per WMO-code. - [ ] De verwachting toont 6 dagen (vandaag t/m 5 dagen verder) met het juiste icoon per WMO-code.
- [ ] De data ververst zichzelf. Na middernacht staat de nieuwe datum er binnen 5 minuten. - [ ] 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. - [ ] 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. - [ ] De Netatmo-token ververst meerdere dagen achter elkaar zonder handmatig ingrijpen.
+2 -2
View File
@@ -129,11 +129,11 @@ html, body {
.a-bar.now span { color: var(--ink); font-weight: 600; } .a-bar.now span { color: var(--ink); font-weight: 600; }
/* Verwachting */ /* Verwachting */
.a-fc { display: grid; grid-template-columns: repeat(5, 1fr); text-align: center; } .a-fc { display: grid; grid-template-columns: repeat(6, 1fr); text-align: center; }
.a-day { display: flex; flex-direction: column; align-items: center; gap: 6px; border-left: 1px solid var(--line); } .a-day { display: flex; flex-direction: column; align-items: center; gap: 6px; border-left: 1px solid var(--line); }
.a-day:first-child { border-left: 0; } .a-day:first-child { border-left: 0; }
.a-dname { font-size: 19px; font-weight: 600; } .a-dname { font-size: 19px; font-weight: 600; }
.a-icon { width: 58px; height: 58px; } .a-icon { width: 50px; height: 50px; }
.a-hl { font-size: 28px; font-weight: 500; } .a-hl { font-size: 28px; font-weight: 500; }
.a-hl span { font-size: 22px; color: var(--muted); font-weight: 400; margin-left: 6px; } .a-hl span { font-size: 22px; color: var(--muted); font-weight: 400; margin-left: 6px; }
.a-fr { font-size: 16px; color: var(--rain); } .a-fr { font-size: 16px; color: var(--rain); }
+1 -1
View File
@@ -1,6 +1,6 @@
/* Service worker: app-shell cache-first, API network-first met timeout en fallback op de laatste response. */ /* Service worker: app-shell cache-first, API network-first met timeout en fallback op de laatste response. */
const VERSION = 'weerscherm-v3'; const VERSION = 'weerscherm-v4';
const SHELL = [ const SHELL = [
'./', './',
'./index.html', './index.html',
+3 -3
View File
@@ -4,10 +4,10 @@ declare(strict_types=1);
namespace Weerscherm; namespace Weerscherm;
/** Zet het "daily"-blok van Open-Meteo om naar 5 dagen vanaf morgen (index 0 is vandaag). */ /** Zet het "daily"-blok van Open-Meteo om naar 6 dagen, inclusief vandaag (index 0). */
final class ForecastMapper final class ForecastMapper
{ {
private const DAYS = 5; private const DAYS = 6;
/** /**
* @param array<string, mixed> $daily * @param array<string, mixed> $daily
@@ -17,7 +17,7 @@ final class ForecastMapper
{ {
$dates = is_array($daily['time'] ?? null) ? $daily['time'] : []; $dates = is_array($daily['time'] ?? null) ? $daily['time'] : [];
$out = []; $out = [];
for ($i = 1; $i <= self::DAYS && isset($dates[$i]); $i++) { for ($i = 0; $i < self::DAYS && isset($dates[$i]); $i++) {
if (!is_string($dates[$i])) { if (!is_string($dates[$i])) {
continue; continue;
} }
+2 -2
View File
@@ -63,8 +63,8 @@ final class DashboardServiceTest extends TestCase
self::assertSame(['2026-09-08', '2026-09-09', '2026-09-10'], array_column($p['rain']['previous_days'], 'date')); self::assertSame(['2026-09-08', '2026-09-09', '2026-09-10'], array_column($p['rain']['previous_days'], 'date'));
self::assertEqualsWithDelta(38.5, $p['rain']['month_total'], 0.0001); self::assertEqualsWithDelta(38.5, $p['rain']['month_total'], 0.0001);
self::assertFalse($p['rain']['stale']); self::assertFalse($p['rain']['stale']);
self::assertCount(5, $p['forecast']); self::assertCount(6, $p['forecast']);
self::assertSame('2026-09-12', $p['forecast'][0]['date']); self::assertSame('2026-09-11', $p['forecast'][0]['date']);
self::assertFalse($p['forecast_stale']); self::assertFalse($p['forecast_stale']);
self::assertSame([], $p['errors']); self::assertSame([], $p['errors']);
} }
+9 -9
View File
@@ -15,16 +15,16 @@ final class ForecastMapperTest extends TestCase
return json_decode((string) file_get_contents(__DIR__ . '/fixtures/forecast.json'), true)['daily']; return json_decode((string) file_get_contents(__DIR__ . '/fixtures/forecast.json'), true)['daily'];
} }
public function testSkipsTodayAndReturnsFiveDays(): void public function testIncludesTodayAndReturnsSixDays(): void
{ {
$f = ForecastMapper::map($this->daily()); $f = ForecastMapper::map($this->daily());
self::assertCount(5, $f); self::assertCount(6, $f);
self::assertSame([ self::assertSame([
'date' => '2026-09-12', 'weather_code' => 2, 'temp_max' => 18.3, 'temp_min' => 10.1, 'date' => '2026-09-11', 'weather_code' => 3, 'temp_max' => 17.9, 'temp_min' => 11.0,
'precip_mm' => 0.4, 'precip_prob' => 20, 'precip_mm' => 0.0, 'precip_prob' => 10,
], $f[0]); ], $f[0]);
self::assertSame('2026-09-16', $f[4]['date']); self::assertSame('2026-09-16', $f[5]['date']);
} }
public function testNullValuesStayNull(): void public function testNullValuesStayNull(): void
@@ -35,9 +35,9 @@ final class ForecastMapperTest extends TestCase
$f = ForecastMapper::map($daily); $f = ForecastMapper::map($daily);
self::assertNull($f[2]['weather_code']); self::assertNull($f[3]['weather_code']);
self::assertNull($f[2]['precip_prob']); self::assertNull($f[3]['precip_prob']);
self::assertSame(17.4, $f[2]['temp_max']); self::assertSame(17.4, $f[3]['temp_max']);
} }
public function testShorterResponseGivesFewerDays(): void public function testShorterResponseGivesFewerDays(): void
@@ -47,7 +47,7 @@ final class ForecastMapperTest extends TestCase
$daily[$key] = array_slice($values, 0, 3); $daily[$key] = array_slice($values, 0, 3);
} }
self::assertCount(2, ForecastMapper::map($daily)); self::assertCount(3, ForecastMapper::map($daily));
} }
public function testMissingTimeArrayGivesEmptyList(): void public function testMissingTimeArrayGivesEmptyList(): void