diff --git a/server/app.py b/server/app.py index c968e2f..9f93e9c 100644 --- a/server/app.py +++ b/server/app.py @@ -168,8 +168,12 @@ def create_app(): if type_: query = query.filter(SensorReadout.type == type_) rows = query.order_by(SensorReadout.timestamp).all() - # Zakresy today i 24h: wszystkie pomiary jako punkty, bez uśredniania. - if range_ in ("today", "24h"): + # Zakresy today i 24h domyślnie zwracają wszystkie pomiary bez + # uśredniania; checkbox "hourly average" wymusza średnie godzinowe. + averaged = range_ not in ("today", "24h") or request.args.get( + "avg", "0" + ).lower() in ("1", "true", "yes") + if not averaged: return jsonify( [ { diff --git a/server/static/retro.css b/server/static/retro.css index 49b1889..71b672e 100644 --- a/server/static/retro.css +++ b/server/static/retro.css @@ -63,6 +63,27 @@ h1 { h2 { color: var(--mc-bright); letter-spacing: 2px; } +/* Yggdrasil logo — pixel art, so keep it crisp when scaled down. + * On the home page it spans the full width of the blue band. */ +.logo { + display: block; + width: 100%; + height: auto; + margin: 1rem auto; + image-rendering: pixelated; + border: 3px double var(--mc-bright); + box-shadow: 8px 8px 0 rgba(0, 0, 0, 0.45); +} + +/* Small logo pinned to the right side of the black navbar. */ +nav { display: flex; align-items: center; } +.nav-logo { + height: 68px; + width: auto; + margin-left: auto; + image-rendering: pixelated; +} + @keyframes mcblink { 50% { opacity: 0; } } /* --- Menu bar (navigation) --- */ @@ -166,6 +187,12 @@ button:disabled { opacity: 0.5; cursor: default; } #refresh-button { margin-top: 0.6rem; } +/* Sensors page: the "hourly average" checkbox must stay a small inline + * box next to its text — the global `input { width: 100% }` would + * otherwise stretch it full-width and break the row layout. */ +#avg-label { display: flex; align-items: center; gap: 0.5em; margin-top: 0.6rem; } +#avg-label input { width: auto; margin: 0; } + .error { background: var(--mc-red); color: var(--mc-bright); diff --git a/server/static/yggdrasil-logo.png b/server/static/yggdrasil-logo.png new file mode 100644 index 0000000..3b5be7f Binary files /dev/null and b/server/static/yggdrasil-logo.png differ diff --git a/server/templates/docs.html b/server/templates/docs.html index e91883b..cbd0d09 100644 --- a/server/templates/docs.html +++ b/server/templates/docs.html @@ -13,6 +13,7 @@ Home Sensors Log out +

Sensor Readouts — API Documentation

diff --git a/server/templates/home.html b/server/templates/home.html index abf544b..4558f39 100644 --- a/server/templates/home.html +++ b/server/templates/home.html @@ -14,6 +14,7 @@ Sensors API Log out +

Yggdrasil

@@ -26,6 +27,7 @@

Tip: press 1, 2 or 0 to navigate.

+ diff --git a/server/templates/sensors.html b/server/templates/sensors.html index 0271a96..f0c0435 100644 --- a/server/templates/sensors.html +++ b/server/templates/sensors.html @@ -14,6 +14,7 @@ Home API Log out +

Sensor Readouts

@@ -40,6 +41,9 @@ + @@ -77,6 +81,8 @@ const readoutsSection = document.getElementById('readouts-section'); const readoutsBody = document.getElementById('readouts-body'); const chartInfo = document.getElementById('chart-info'); + const avgCheckbox = document.getElementById('avg-checkbox'); + const avgLabel = document.getElementById('avg-label'); let chart = null; // Strefa czasowa przeglądarki — backend liczy "today" od północy @@ -97,20 +103,35 @@ return d.toLocaleString('pl-PL', { timeZone: userTz }); } - function formatChartLabel(iso) { + function formatChartLabel(iso, timeOnly) { const d = new Date(iso + 'Z'); - return d.toLocaleString('pl-PL', { timeZone: userTz, hour: '2-digit', minute: '2-digit', day: '2-digit', month: '2-digit', year: 'numeric' }); + if (timeOnly) { + return d.toLocaleString('pl-PL', { timeZone: userTz, hour: '2-digit', minute: '2-digit' }); + } + return d.toLocaleString('pl-PL', { timeZone: userTz, day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }); } - function chartInfoText(range) { - if (range === 'today') return `Raw measurements since local midnight (${userTz}) — no averaging.`; - if (range === '24h') return 'Raw measurements from the last 24 hours — no averaging.'; - const period = range === 'week' ? 'the last 7 days' + function chartInfoText(range, averaged) { + if (!averaged) { + if (range === 'today') return `Raw measurements since local midnight (${userTz}) — no averaging.`; + return 'Raw measurements from the last 24 hours — no averaging.'; + } + const period = range === 'today' ? 'today (since local midnight)' + : range === '24h' ? 'the last 24 hours' + : range === 'week' ? 'the last 7 days' : range === 'month' ? 'the last 30 days' : 'all stored data'; return `Hourly averages over ${period} — each point is the mean of all measurements in that local hour (${userTz}).`; } + function isRawRange(range) { + return range === 'today' || range === '24h'; + } + + function updateAvgVisibility() { + avgLabel.classList.toggle('hidden', !isRawRange(rangeSelect.value)); + } + // Ciśnienie jest składowane w bazie w paskalach (tak wysyła firmware), // a wyświetlane w hektopaskalach z jednym miejscem po przecinku. function displayValue(type, value) { @@ -176,14 +197,16 @@ try { // fetch hourly data for chart - const hourly = await fetchJSON(`/api/sensors/${sensorId}/readouts/hourly?type=${encodeURIComponent(type)}&range=${encodeURIComponent(range)}&tz=${encodeURIComponent(userTz)}`); + const averaged = !isRawRange(range) || avgCheckbox.checked; + const hourly = await fetchJSON(`/api/sensors/${sensorId}/readouts/hourly?type=${encodeURIComponent(type)}&range=${encodeURIComponent(range)}&tz=${encodeURIComponent(userTz)}&avg=${averaged ? '1' : '0'}`); if (!hourly.length) throw new Error('no data for this range'); - const labels = hourly.map(r => formatChartLabel(r.timestamp)); + // Surowe punkty today/24h (~1/min) — mniejsze markery, + // żeby wykres był czytelny. W obrębie doby na osi X + // wystarczy sama godzina (też po uśrednieniu). + const dense = !averaged; + const labels = hourly.map(r => formatChartLabel(r.timestamp, isRawRange(range))); const values = hourly.map(r => displayValue(type, r.value)); - // Przy zakresach today/24h punktów jest dużo (~1/min), - // więc mniejsze markery żeby wykres był czytelny. - const dense = range === 'today' || range === '24h'; if (chart) chart.destroy(); const ctx = document.getElementById('chart').getContext('2d'); @@ -216,7 +239,7 @@ // The chart was created while the container was hidden, // so force a re-measure now that it is visible. chart.resize(); - chartInfo.textContent = '> ' + chartInfoText(range); + chartInfo.textContent = '> ' + chartInfoText(range, averaged); } catch (err) { chartError.textContent = '! Chart error: ' + err.message; chartError.classList.remove('hidden'); @@ -265,8 +288,14 @@ } }); - rangeSelect.addEventListener('change', loadChart); + rangeSelect.addEventListener('change', () => { + updateAvgVisibility(); + loadChart(); + }); + avgCheckbox.addEventListener('change', loadChart); + + updateAvgVisibility(); loadSensors();