Author SHA1 Message Date
theoandClaude Opus 5 ca2fc76552 fix: modulekolom past zich aan het aantal modules aan
Er is een vierde binnenmodule bijgekomen. De kaart verscheen al vanzelf
(StationMapper leest alle NAModule4-modules uit), maar de kolom was op
drie kaarten ontworpen: vier kaarten van 120px duwden de rij op 481px en
daarmee de onderste strook 39px voorbij de onderrand van het canvas.

De kolom heeft nu een vaste hoogte van 412px met overflow verborgen, en
fitRooms() verkleint de inhoud met een schaal zodra die niet past. Bij
drie modules verandert er niets (schaal 1), bij vier wordt het 0,86 en
met een vastgezette module 0,78. Zo blijft het scherm kloppen, ongeacht
hoeveel modules erbij komen.

De dev-fixture heeft er een vierde module bij, zodat dit lokaal te zien
is zonder Netatmo-koppeling. Servicewerker naar v12.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:44:38 +02:00
theoandClaude Opus 5 75111a5571 Merge feature/module-grafiekjes: verloop van vandaag bij elke binnenmodule
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 12:10:40 +02:00
theoandClaude Opus 5 bfc96acb5a feat: temperatuurverloop van vandaag ook bij de binnenmodules
Naast het getal in elke modulekaart staat nu hetzelfde soort lijntje als
bij buiten (200x40). Zonder labels: daar is in zo'n kaart geen ruimte
voor, de schaal staat al op de regel eronder en het venster is hetzelfde
"vandaag" als bij het grafiekje van buiten ernaast.

Per module een eigen getmeasure, gecachet onder temperature-<module> met
een gedeelde TTL; fetch() heeft daarvoor een losse ttlKey gekregen, want
anders zou de TTL-lookup per module mislukken en zou er helemaal niet
gecachet worden. Het basisstation meet zelf temperatuur en wordt zonder
module_id opgevraagd, dus die parameter is optioneel geworden. In de
payload als indoor[].history; mislukt er een, dan blijft die lijst leeg
en verdwijnt alleen dat lijntje, met de foutcode in errors. Een
stale-vlag per kamer zou het contract nodeloos zwaar maken.

De hoogte is overal 40px, ook bij een vastgezette module: gelijke hoogte
houdt de kamers onderling vergelijkbaar, en een hoger lijntje zou de
toch al krappe speling onder de kolom opeten. De grafiekjes voegen geen
hoogte toe aan de kaarten.

FakeSource houdt nu alle aanroepen bij in plaats van alleen de laatste,
zodat een test de volgorde van de vier temperatuur-calls kan nalopen.
Servicewerker naar v11.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 12:10:40 +02:00
theoandClaude Opus 5 4c4881fd11 Merge feature/temperatuurverloop: verloop van vandaag naast de buitentemperatuur
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 11:50:40 +02:00
14 changed files with 172 additions and 26 deletions
+13 -1
View File
@@ -266,7 +266,19 @@ De iconen `sun`, `partly`, `rain` en `showers` staan in de mockup. Maak `cloud`,
"1009 – 1015 hPa in 24 u" in de kopregel is daarmee vervallen. Zie `hourTicks()` in `public/format.js`;
die loopt uur voor uur door het venster en raadpleegt telkens de lokale klok, zodat de zomertijdwissel
vanzelf goed gaat._
- **Binnen (rechts):** 3 rijen met naam, temperatuur (1 decimaal) en "↓ min° ↑ max°". Een onbereikbare module toont "–" in `--muted`.
- **Binnen (rechts):** een rij per module met naam, temperatuur (1 decimaal) en "↓ min° ↑ max°". Een onbereikbare module toont "–" in `--muted`.
_Gewijzigd 2026-09-14: het aantal modules ligt niet vast op 3. De kolom heeft een vaste hoogte van 412px
(744 − 2×30 padding − 40 kopregel − 232 onderstrook) en `fitRooms()` verkleint de inhoud met een schaal
zodra die niet past. Zonder dat duwde een vierde module de onderste strook 39px van het scherm af. Bij
drie modules verandert er niets (schaal 1); vier geeft ongeveer 0,86, en met een vastgezette module 0,78._
_Toegevoegd 2026-09-13: rechts van elk temperatuurgetal staat het verloop van vandaag als klein lijntje
(200x40, zonder labels: de schaal staat op de regel eronder en het venster is hetzelfde "vandaag" als bij
buiten). Data per module uit een eigen `getmeasure`, apart gecachet onder `temperature-<module>` maar met
één gedeelde TTL; het basisstation meet zelf en wordt zonder `module_id` opgevraagd. In de payload als
`indoor[].history`. Mislukt er één, dan blijft die lijst leeg, verdwijnt alleen dat lijntje en staat de
foutcode in `errors`; een stale-vlag per kamer zou het contract nodeloos zwaar maken. De hoogte is overal
gelijk, ook bij een vastgezette module, zodat de kamers vergelijkbaar blijven._
_Toegevoegd 2026-09-13: een module is tikbaar. Een tik zet die module bovenaan de lijst (de overige twee
behouden hun onderlinge volgorde) en vergroot alleen het temperatuurgetal van 50 naar 92px. Nogmaals tikken
op de bovenste (vastgezette) module maakt hem weer los. De keuze blijft staan bij de 5-minuten-verversing en
+21 -2
View File
@@ -2,7 +2,7 @@ import {
f1, fInt, fmm, markerPos, barHeights, wmoIcon, trendLabel,
formatDate, formatTime, dayAbbr, monthName, isOlderThan, msUntil,
co2Level, fPct, fHpa, seriesRange, sparklinePath,
windArrowDeg, fKmh, formatHour, formatFullDate, pinRoom, hourTicks,
windArrowDeg, fKmh, formatHour, formatFullDate, pinRoom, hourTicks, scaleToFit,
} from './format.js';
import { icon, windArrow } from './icons.js';
@@ -17,6 +17,7 @@ const FALLBACK_TZ = 'Europe/Berlin';
const OVERLAY_MS = 20000; // hoe lang een overlay blijft staan zonder tik
const RADAR_BUCKET_MS = 240000; // hergebruikvenster voor het radarbeeld, gelijk aan de servercache
const HOURLY_WIDTH = 1045; // = stage-breedte 1133 min 2x 44px padding, zelfde marge als de rest van het scherm
const INSIDE_H = 412; // hoogte van de middelste rij: 744 - 2x30 padding - 40 kopregel - 232 onderstrook
const $ = (id) => document.getElementById(id);
const stage = $('stage');
@@ -100,13 +101,31 @@ function renderRooms(rooms) {
const co2 = co2Level(off ? null : r.co2);
const pill = `<span class="pill ${co2.level} num" title="${co2.label}">${co2.level === 'unknown' ? '–' : r.co2 + ' ppm'}</span>`;
const pinned = r.id === state.pinnedRoomId;
const points = r.history || [];
// Zonder twee punten valt er niets te tekenen; dan blijft de plek naast het getal gewoon leeg.
const spark = points.length < 2 ? '' : `<svg class="a-rspark" viewBox="0 0 200 40" preserveAspectRatio="none" aria-hidden="true">
<path class="a-spark-line" d="${sparklinePath(points.map((p) => p.temp), 200, 40, 3)}"/>
</svg>`;
return `<div class="a-room${off ? ' off' : ''}${pinned ? ' pinned' : ''}" data-id="${esc(r.id)}">
<p class="a-rname"><span>${esc(r.name)}</span>${pill}</p>
<p class="a-rtemp num">${off ? '–' : f1(r.temp) + '°'}</p>
<div class="a-rrow"><p class="a-rtemp num">${off ? '–' : f1(r.temp) + '°'}</p>${spark}</div>
<p class="a-rmm num"><span class="cold">↓ ${off ? '–' : f1(r.min) + '°'}</span><span class="warm">↑ ${off ? '–' : f1(r.max) + '°'}</span><span class="rv">RV ${off ? '–' : fPct(r.humidity)}</span></p>
</div>`;
});
$('rooms').innerHTML = html.join('');
fitRooms();
}
/**
* Houdt de modulekolom binnen zijn vaste hoogte. Bij drie modules past alles op ware grootte; komt er een
* module bij (of groeit er een doordat je hem vastzet), dan wordt het geheel net genoeg verkleind. Zonder
* dit duwt een vierde kaart de onderste strook van het scherm af.
*/
function fitRooms() {
const el = $('rooms');
el.style.transform = 'none';
const scale = scaleToFit(el.scrollHeight, INSIDE_H);
el.style.transform = scale < 1 ? `scale(${scale.toFixed(4)})` : '';
}
function renderRain(rain, now) {
+6
View File
@@ -246,3 +246,9 @@ export function hourTicks(startIso, endIso, tz, stepHours = 6, minGapPercent = 9
}
return ticks;
}
/** Schaal om `natural` px binnen `available` px te passen; nooit vergroten, nooit negatief. */
export function scaleToFit(natural, available) {
if (!(natural > 0) || !(available > 0)) return 1;
return Math.min(1, available / natural);
}
+1 -1
View File
@@ -62,7 +62,7 @@
</div>
</section>
<section class="a-inside" id="rooms"></section>
<section class="a-inside"><div id="rooms"></div></section>
<section class="a-bottom">
<div id="rain-block">
+9 -3
View File
@@ -109,8 +109,10 @@ html, body {
.a-spark-grid { stroke: var(--line); stroke-width: 1; vector-effect: non-scaling-stroke; opacity: .6; }
.a-spark-line { fill: none; stroke: var(--rain); stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
/* Binnen */
.a-inside { align-self: center; }
/* Binnen. Vaste hoogte met overflow verborgen: hoeveel modules er ook zijn, de kolom duwt nooit de
onderste strook van het scherm af. Past de inhoud niet, dan verkleint fitRooms() hem met een schaal. */
.a-inside { align-self: center; height: 412px; display: flex; align-items: center; overflow: hidden; }
#rooms { width: 100%; transform-origin: center center; }
.a-room { padding: 12px 0; border-top: 1px solid var(--line); }
.a-room:first-child { border-top: 0; }
.a-rname { font-size: 19px; color: var(--muted); display: flex; justify-content: space-between; align-items: center; }
@@ -119,7 +121,11 @@ html, body {
.pill.medium { background: var(--warm); }
.pill.bad { background: var(--bad); }
.pill.unknown { background: var(--line); color: var(--muted); }
.a-rtemp { font-size: 50px; font-weight: 300; line-height: 1.05; transition: font-size .25s ease; }
.a-rrow { display: flex; align-items: center; gap: 16px; }
.a-rtemp { font-size: 50px; font-weight: 300; line-height: 1.05; transition: font-size .25s ease; flex: none; }
/* Overal even hoog: zo zijn de kamers onderling vergelijkbaar, en groeit een vastgezette kaart niet
zo ver door dat de speling onder de kolom opraakt. */
.a-rspark { flex: 1; min-width: 0; height: 40px; overflow: visible; }
.a-room.off .a-rtemp { color: var(--muted); }
.a-room.pinned .a-rtemp { font-size: 92px; }
.a-rmm { font-size: 17px; display: flex; gap: 18px; }
+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. */
const VERSION = 'weerscherm-v10';
const VERSION = 'weerscherm-v12';
const SHELL = [
'./',
'./index.html',
+38 -5
View File
@@ -70,7 +70,7 @@ final class DashboardService
'humidity' => $station['outdoor']['humidity'],
],
'outdoor_history' => $outdoorHistory,
'indoor' => $station['indoor'],
'indoor' => $this->indoorWithHistory($station['indoor'], $station['device_id'], $now),
'rain' => $rain,
'pressure' => $pressure,
'forecast' => $forecast['days'],
@@ -128,11 +128,22 @@ final class DashboardService
if ($station['device_id'] === null || $moduleId === null) {
return ['points' => [], 'stale' => false];
}
return $this->temperatureHistory($station['device_id'], $moduleId, $now);
}
/**
* Temperatuurverloop van vandaag voor één module, apart gecachet per module.
*
* @return array{points: list<array{at: string, temp: float}>, stale: bool}
*/
private function temperatureHistory(string $deviceId, ?string $moduleId, \DateTimeImmutable $now): array
{
$begin = $now->setTime(0, 0, 0)->getTimestamp();
$measure = $this->fetch(
'temperature',
fn() => $this->source->temperatureHistory($station['device_id'], $moduleId, $begin, $now->getTimestamp()),
'temperature-' . ($moduleId ?? $deviceId),
fn() => $this->source->temperatureHistory($deviceId, $moduleId, $begin, $now->getTimestamp()),
self::ERR_NETATMO,
'temperature',
);
return [
'points' => $measure['data'] === null ? [] : MeasureSeries::map($measure['data'], $now->getTimezone(), 'temp'),
@@ -140,6 +151,27 @@ final class DashboardService
];
}
/**
* Zet het temperatuurverloop van vandaag bij elke binnenmodule. Mislukt er één, dan blijft die lijst
* leeg (het grafiekje verdwijnt) en staat de foutcode al in errors; een eigen stale-vlag per kamer
* zou het contract nodeloos zwaar maken.
*
* @param list<array<string, mixed>> $rooms
* @return list<array<string, mixed>>
*/
private function indoorWithHistory(array $rooms, ?string $deviceId, \DateTimeImmutable $now): array
{
if ($deviceId === null) {
return $rooms;
}
foreach ($rooms as &$room) {
$moduleId = $room['id'] === $deviceId ? null : $room['id'];
$room['history'] = $this->temperatureHistory($deviceId, $moduleId, $now)['points'];
}
unset($room);
return $rooms;
}
/** @return array{days: list<array>, stale: bool} */
private function forecast(array $station, \DateTimeZone $tz): array
{
@@ -175,12 +207,13 @@ final class DashboardService
* Cache-first per bron. Bij een fout: stale cache (ongeacht leeftijd) en een foutcode, anders null.
*
* @param callable(): array $fetcher
* @param ?string $ttlKey losse TTL-sleutel wanneer de cachesleutel per module verschilt
* @return array{data: ?array, stale: bool}
*/
private function fetch(string $key, callable $fetcher, string $errorCode): array
private function fetch(string $key, callable $fetcher, string $errorCode, ?string $ttlKey = null): array
{
$now = ($this->now ?? new \DateTimeImmutable('now'))->getTimestamp();
$ttl = $this->ttl[$key] ?? 0;
$ttl = $this->ttl[$ttlKey ?? $key] ?? 0;
if ($ttl > 0 && $this->cache->isFresh($key, $ttl, $now)) {
return ['data' => $this->cache->read($key)['data'], 'stale' => false];
}
+14 -3
View File
@@ -38,6 +38,8 @@ final class FixtureSource implements WeatherSource
'dashboard_data' => ['time_utc' => $at('-4 minutes'), 'Temperature' => 18.9, 'CO2' => 1180, 'Humidity' => 52, 'min_temp' => 18.1, 'max_temp' => 19.6, 'temp_trend' => 'up']],
['_id' => '03:00:00:00:00:03', 'type' => 'NAModule4', 'module_name' => 'Kantoor', 'reachable' => true,
'dashboard_data' => ['time_utc' => $at('-4 minutes'), 'Temperature' => 20.2, 'CO2' => 1640, 'Humidity' => 44, 'min_temp' => 19.0, 'max_temp' => 21.0, 'temp_trend' => 'stable']],
['_id' => '03:00:00:00:00:05', 'type' => 'NAModule4', 'module_name' => 'Zolder', 'reachable' => true,
'dashboard_data' => ['time_utc' => $at('-5 minutes'), 'Temperature' => 17.4, 'CO2' => 780, 'Humidity' => 55, 'min_temp' => 16.2, 'max_temp' => 18.0, 'temp_trend' => 'up']],
['_id' => '05:00:00:00:00:04', 'type' => 'NAModule3', 'module_name' => 'Regenmeter', 'reachable' => true,
'dashboard_data' => ['time_utc' => $at('-3 minutes'), 'Rain' => 0, 'sum_rain_1' => 0, 'sum_rain_24' => 2.4]],
],
@@ -69,9 +71,18 @@ final class FixtureSource implements WeatherSource
return $body;
}
public function temperatureHistory(string $deviceId, string $moduleId, int $begin, int $end): array
public function temperatureHistory(string $deviceId, ?string $moduleId, int $begin, int $end): array
{
// Dagcurve tussen de min en max uit stationsData(): koudst rond 06:00, warmst rond 15:00.
// Dagcurve tussen de min en max die stationsData() voor deze module meldt: koudst rond 06:00,
// warmst rond 15:00. Binnen beweegt minder en later dan buiten, zodat de grafiekjes verschillen.
[$min, $max, $shift] = match ($moduleId) {
'02:00:00:00:00:01' => [9.2, 19.8, 10], // buiten
'03:00:00:00:00:02' => [18.1, 19.6, 13], // slaapkamer
'03:00:00:00:00:03' => [19.0, 21.0, 14], // kantoor
'03:00:00:00:00:05' => [16.2, 18.0, 15], // zolder
default => [20.4, 22.1, 12], // basisstation
};
$body = [];
for ($ts = $begin - ($begin % 1800); $ts <= $end; $ts += 1800) {
if ($ts < $begin) {
@@ -79,7 +90,7 @@ final class FixtureSource implements WeatherSource
}
$at = (new \DateTimeImmutable('@' . $ts))->setTimezone($this->now->getTimezone());
$hour = (int) $at->format('G') + (int) $at->format('i') / 60;
$t = 9.2 + (19.8 - 9.2) * (0.5 + 0.5 * sin((($hour - 10) / 24) * 2 * M_PI));
$t = $min + ($max - $min) * (0.5 + 0.5 * sin((($hour - $shift) / 24) * 2 * M_PI));
$body[(string) $ts] = [round($t, 1)];
}
return $body;
+1 -1
View File
@@ -39,7 +39,7 @@ final class LiveSource implements WeatherSource
return $this->netatmo->getPressureHistory($deviceId, $begin, $end);
}
public function temperatureHistory(string $deviceId, string $moduleId, int $begin, int $end): array
public function temperatureHistory(string $deviceId, ?string $moduleId, int $begin, int $end): array
{
return $this->netatmo->getTemperatureHistory($deviceId, $moduleId, $begin, $end);
}
+10 -3
View File
@@ -63,10 +63,17 @@ final class NetatmoClient
return $this->measureSeries('Pressure', ['device_id' => $deviceId], $begin, $end);
}
/** @return array<string|int, mixed> timestamp => [°C], per 30 minuten, van de opgegeven module */
public function getTemperatureHistory(string $deviceId, string $moduleId, int $begin, int $end): array
/**
* @param ?string $moduleId null meet het basisstation zelf; Netatmo verwacht dan geen module_id
* @return array<string|int, mixed> timestamp => [°C], per 30 minuten
*/
public function getTemperatureHistory(string $deviceId, ?string $moduleId, int $begin, int $end): array
{
return $this->measureSeries('Temperature', ['device_id' => $deviceId, 'module_id' => $moduleId], $begin, $end);
$target = ['device_id' => $deviceId];
if ($moduleId !== null) {
$target['module_id'] = $moduleId;
}
return $this->measureSeries('Temperature', $target, $begin, $end);
}
/**
+5 -2
View File
@@ -16,8 +16,11 @@ interface WeatherSource
/** @return array<string|int, mixed> de "body" van Netatmo getmeasure (timestamp => [hPa]) voor het basisstation */
public function pressureHistory(string $deviceId, int $begin, int $end): array;
/** @return array<string|int, mixed> de "body" van Netatmo getmeasure (timestamp => [°C]) voor de buitenmodule */
public function temperatureHistory(string $deviceId, string $moduleId, int $begin, int $end): array;
/**
* @param ?string $moduleId null meet het basisstation zelf
* @return array<string|int, mixed> de "body" van Netatmo getmeasure (timestamp => [°C])
*/
public function temperatureHistory(string $deviceId, ?string $moduleId, int $begin, int $end): array;
/** @return array{daily: array<string, mixed>, hourly: array<string, mixed>} de "daily"- en "hourly"-blokken van Open-Meteo */
public function forecast(float $lat, float $lon, string $timezone): array;
+34 -2
View File
@@ -87,7 +87,7 @@ final class DashboardServiceTest extends TestCase
{
$p = $this->service()->build();
[$deviceId, $moduleId, $begin, $end] = $this->source->lastArgs['temperature'];
[$deviceId, $moduleId, $begin, $end] = $this->source->allArgs['temperature'][0];
self::assertSame('70:ee:50:00:00:01', $deviceId);
self::assertSame('02:00:00:00:00:01', $moduleId, 'de buitenmodule, niet het basisstation');
self::assertSame((new \DateTimeImmutable('2026-09-11 00:00', new \DateTimeZone('Europe/Berlin')))->getTimestamp(), $begin);
@@ -98,6 +98,37 @@ final class DashboardServiceTest extends TestCase
self::assertFalse($p['outdoor_history']['stale']);
}
public function testEveryIndoorModuleGetsItsOwnHistory(): void
{
$p = $this->service()->build();
foreach ($p['indoor'] as $room) {
self::assertNotEmpty($room['history'], "kamer {$room['name']} heeft een verloop");
self::assertSame(['at', 'temp'], array_keys($room['history'][0]));
}
// Het basisstation meet zelf; dat vraag je op zonder module_id.
$modules = array_map(fn(array $args) => $args[1], $this->source->allArgs['temperature']);
self::assertSame(
['02:00:00:00:00:01', null, '03:00:00:00:00:02', '03:00:00:00:00:03'],
$modules,
'buiten, dan het basisstation zonder module_id, dan de twee losse modules',
);
}
public function testIndoorHistoryFailureOnlyEmptiesThatList(): void
{
$this->source->responses['temperature'] = new UpstreamException('500');
$p = $this->service()->build();
foreach ($p['indoor'] as $room) {
self::assertSame([], $room['history']);
}
self::assertSame(21.3, $p['indoor'][0]['temp'], 'de actuele waarden komen uit stationsdata en blijven staan');
self::assertSame(['netatmo_unavailable'], $p['errors']);
}
public function testOutdoorHistoryFailureLeavesTheRestIntact(): void
{
$this->source->responses['temperature'] = new UpstreamException('500');
@@ -231,10 +262,11 @@ final class DashboardServiceTest extends TestCase
$this->service()->build();
$this->service('2026-09-11 12:36:05 UTC')->build();
self::assertSame(['stations' => 1, 'measure' => 1, 'pressure' => 1, 'temperature' => 1, 'forecast' => 1], $this->source->calls);
self::assertSame(['stations' => 1, 'measure' => 1, 'pressure' => 1, 'temperature' => 4, 'forecast' => 1], $this->source->calls);
$this->service('2026-09-11 12:38:05 UTC')->build();
self::assertSame(2, $this->source->calls['stations'], 'stations na 5 min opnieuw');
self::assertSame(4, $this->source->calls['temperature'], 'verloop per module nog binnen 15 min');
self::assertSame(1, $this->source->calls['measure'], 'measure nog binnen 15 min');
}
}
+5 -1
View File
@@ -18,6 +18,9 @@ final class FakeSource implements WeatherSource
/** @var array<string, array> */
public array $lastArgs = [];
/** @var array<string, list<array>> alle aanroepen per soort, in volgorde */
public array $allArgs = [];
public static function fromFixtures(): self
{
$s = new self();
@@ -49,7 +52,7 @@ final class FakeSource implements WeatherSource
return $this->answer('pressure', func_get_args());
}
public function temperatureHistory(string $deviceId, string $moduleId, int $begin, int $end): array
public function temperatureHistory(string $deviceId, ?string $moduleId, int $begin, int $end): array
{
return $this->answer('temperature', func_get_args());
}
@@ -63,6 +66,7 @@ final class FakeSource implements WeatherSource
{
$this->calls[$key]++;
$this->lastArgs[$key] = $args;
$this->allArgs[$key][] = $args;
$r = $this->responses[$key] ?? new \RuntimeException("geen response voor $key");
if ($r instanceof \Throwable) {
throw $r;
+14 -1
View File
@@ -121,7 +121,7 @@ test('msUntil computes the wait until the next local time', () => {
import {
co2Level, fPct, fHpa, seriesRange, sparklinePath, sparklinePoints,
windArrowDeg, fKmh, formatHour, formatFullDate, pinRoom, hourTicks,
windArrowDeg, fKmh, formatHour, formatFullDate, pinRoom, hourTicks, scaleToFit,
} from '../../public/format.js';
test('co2Level uses the Netatmo thresholds', () => {
@@ -244,3 +244,16 @@ test('hourTicks gives nothing for an empty or invalid window', () => {
assert.deepEqual(hourTicks('kapot', '2026-09-13T10:00:00+02:00', TZ), []);
assert.deepEqual(hourTicks(undefined, undefined, TZ), []);
});
test('scaleToFit shrinks to fit but never enlarges', () => {
assert.equal(scaleToFit(481, 412), 412 / 481);
assert.equal(scaleToFit(361, 412), 1, 'past al: niet vergroten');
assert.equal(scaleToFit(412, 412), 1);
});
test('scaleToFit is safe with nonsense input', () => {
assert.equal(scaleToFit(0, 412), 1);
assert.equal(scaleToFit(-10, 412), 1);
assert.equal(scaleToFit(481, 0), 1);
assert.equal(scaleToFit(undefined, 412), 1);
});