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:
@@ -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
@@ -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
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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']);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user