diff --git a/docs/weerscherm-spec.md b/docs/weerscherm-spec.md index 7e90cb5..0967e91 100644 --- a/docs/weerscherm-spec.md +++ b/docs/weerscherm-spec.md @@ -266,7 +266,11 @@ 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-` maar met diff --git a/public/app.js b/public/app.js index b65076d..519edfe 100644 --- a/public/app.js +++ b/public/app.js @@ -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'); @@ -112,6 +113,19 @@ function renderRooms(rooms) { `; }); $('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) { diff --git a/public/format.js b/public/format.js index c0b0bf8..303c244 100644 --- a/public/format.js +++ b/public/format.js @@ -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); +} diff --git a/public/index.html b/public/index.html index 3a3f844..8d1d5b3 100644 --- a/public/index.html +++ b/public/index.html @@ -62,7 +62,7 @@ -
+
diff --git a/public/styles.css b/public/styles.css index f3834a6..67473d9 100644 --- a/public/styles.css +++ b/public/styles.css @@ -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; } diff --git a/public/sw.js b/public/sw.js index ee387a5..4dba4a4 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,6 +1,6 @@ /* Service worker: app-shell cache-first, API network-first met timeout en fallback op de laatste response. */ -const VERSION = 'weerscherm-v11'; +const VERSION = 'weerscherm-v12'; const SHELL = [ './', './index.html', diff --git a/src/FixtureSource.php b/src/FixtureSource.php index 2320769..d5469eb 100644 --- a/src/FixtureSource.php +++ b/src/FixtureSource.php @@ -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]], ], @@ -77,6 +79,7 @@ final class FixtureSource implements WeatherSource '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 }; diff --git a/tests/js/format.test.js b/tests/js/format.test.js index 5604b44..107d5a8 100644 --- a/tests/js/format.test.js +++ b/tests/js/format.test.js @@ -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); +});