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 @@
HomeSensorsLog 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 @@
SensorsAPILog 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 @@
HomeAPILog 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();