retro css

This commit is contained in:
2026-10-23 11:37:55 +02:00
parent 02f3df8c41
commit 6a782c1e9b
6 changed files with 78 additions and 15 deletions
+6 -2
View File
@@ -168,8 +168,12 @@ def create_app():
if type_: if type_:
query = query.filter(SensorReadout.type == type_) query = query.filter(SensorReadout.type == type_)
rows = query.order_by(SensorReadout.timestamp).all() rows = query.order_by(SensorReadout.timestamp).all()
# Zakresy today i 24h: wszystkie pomiary jako punkty, bez uśredniania. # Zakresy today i 24h domyślnie zwracają wszystkie pomiary bez
if range_ in ("today", "24h"): # 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( return jsonify(
[ [
{ {
+27
View File
@@ -63,6 +63,27 @@ h1 {
h2 { color: var(--mc-bright); letter-spacing: 2px; } 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; } } @keyframes mcblink { 50% { opacity: 0; } }
/* --- Menu bar (navigation) --- */ /* --- Menu bar (navigation) --- */
@@ -166,6 +187,12 @@ button:disabled { opacity: 0.5; cursor: default; }
#refresh-button { margin-top: 0.6rem; } #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 { .error {
background: var(--mc-red); background: var(--mc-red);
color: var(--mc-bright); color: var(--mc-bright);
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

+1
View File
@@ -13,6 +13,7 @@
<a href="{{ url_for('home') }}">Home</a> <a href="{{ url_for('home') }}">Home</a>
<a href="{{ url_for('sensors_page') }}">Sensors</a> <a href="{{ url_for('sensors_page') }}">Sensors</a>
<a href="{{ url_for('logout') }}">Log out</a> <a href="{{ url_for('logout') }}">Log out</a>
<img class="nav-logo" src="{{ url_for('static', filename='yggdrasil-logo.png') }}" alt="Yggdrasil logo">
</nav> </nav>
<h1>Sensor Readouts &mdash; API Documentation</h1> <h1>Sensor Readouts &mdash; API Documentation</h1>
+2
View File
@@ -14,6 +14,7 @@
<a href="{{ url_for('sensors_page') }}">Sensors</a> <a href="{{ url_for('sensors_page') }}">Sensors</a>
<a href="{{ url_for('api_docs') }}">API</a> <a href="{{ url_for('api_docs') }}">API</a>
<a href="{{ url_for('logout') }}">Log out</a> <a href="{{ url_for('logout') }}">Log out</a>
<img class="nav-logo" src="{{ url_for('static', filename='yggdrasil-logo.png') }}" alt="Yggdrasil logo">
</nav> </nav>
<h1>Yggdrasil</h1> <h1>Yggdrasil</h1>
<div class="panel"> <div class="panel">
@@ -26,6 +27,7 @@
</ul> </ul>
<p class="hint">Tip: press <b>1</b>, <b>2</b> or <b>0</b> to navigate.</p> <p class="hint">Tip: press <b>1</b>, <b>2</b> or <b>0</b> to navigate.</p>
</div> </div>
<img class="logo" src="{{ url_for('static', filename='yggdrasil-logo.png') }}" alt="Yggdrasil logo">
<footer class="statusbar"> <footer class="statusbar">
<span>{{ user.username }}@yggdrasil:~$</span> <span>{{ user.username }}@yggdrasil:~$</span>
</footer> </footer>
+42 -13
View File
@@ -14,6 +14,7 @@
<a href="{{ url_for('home') }}">Home</a> <a href="{{ url_for('home') }}">Home</a>
<a href="{{ url_for('api_docs') }}">API</a> <a href="{{ url_for('api_docs') }}">API</a>
<a href="{{ url_for('logout') }}">Log out</a> <a href="{{ url_for('logout') }}">Log out</a>
<img class="nav-logo" src="{{ url_for('static', filename='yggdrasil-logo.png') }}" alt="Yggdrasil logo">
</nav> </nav>
<h1>Sensor Readouts</h1> <h1>Sensor Readouts</h1>
@@ -40,6 +41,9 @@
<option value="month">Month</option> <option value="month">Month</option>
<option value="all" selected>All</option> <option value="all" selected>All</option>
</select> </select>
<label id="avg-label" class="hidden">
<input type="checkbox" id="avg-checkbox"> hourly average
</label>
<button id="refresh-button" type="button">[ Refresh ]</button> <button id="refresh-button" type="button">[ Refresh ]</button>
</div> </div>
@@ -77,6 +81,8 @@
const readoutsSection = document.getElementById('readouts-section'); const readoutsSection = document.getElementById('readouts-section');
const readoutsBody = document.getElementById('readouts-body'); const readoutsBody = document.getElementById('readouts-body');
const chartInfo = document.getElementById('chart-info'); const chartInfo = document.getElementById('chart-info');
const avgCheckbox = document.getElementById('avg-checkbox');
const avgLabel = document.getElementById('avg-label');
let chart = null; let chart = null;
// Strefa czasowa przeglądarki — backend liczy "today" od północy // Strefa czasowa przeglądarki — backend liczy "today" od północy
@@ -97,20 +103,35 @@
return d.toLocaleString('pl-PL', { timeZone: userTz }); return d.toLocaleString('pl-PL', { timeZone: userTz });
} }
function formatChartLabel(iso) { function formatChartLabel(iso, timeOnly) {
const d = new Date(iso + 'Z'); 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) { function chartInfoText(range, averaged) {
if (range === 'today') return `Raw measurements since local midnight (${userTz}) — no averaging.`; if (!averaged) {
if (range === '24h') return 'Raw measurements from the last 24 hours — no averaging.'; if (range === 'today') return `Raw measurements since local midnight (${userTz}) — no averaging.`;
const period = range === 'week' ? 'the last 7 days' 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' : range === 'month' ? 'the last 30 days'
: 'all stored data'; : 'all stored data';
return `Hourly averages over ${period} — each point is the mean of all measurements in that local hour (${userTz}).`; 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), // Ciśnienie jest składowane w bazie w paskalach (tak wysyła firmware),
// a wyświetlane w hektopaskalach z jednym miejscem po przecinku. // a wyświetlane w hektopaskalach z jednym miejscem po przecinku.
function displayValue(type, value) { function displayValue(type, value) {
@@ -176,14 +197,16 @@
try { try {
// fetch hourly data for chart // 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'); 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)); 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(); if (chart) chart.destroy();
const ctx = document.getElementById('chart').getContext('2d'); const ctx = document.getElementById('chart').getContext('2d');
@@ -216,7 +239,7 @@
// The chart was created while the container was hidden, // The chart was created while the container was hidden,
// so force a re-measure now that it is visible. // so force a re-measure now that it is visible.
chart.resize(); chart.resize();
chartInfo.textContent = '> ' + chartInfoText(range); chartInfo.textContent = '> ' + chartInfoText(range, averaged);
} catch (err) { } catch (err) {
chartError.textContent = '! Chart error: ' + err.message; chartError.textContent = '! Chart error: ' + err.message;
chartError.classList.remove('hidden'); chartError.classList.remove('hidden');
@@ -265,8 +288,14 @@
} }
}); });
rangeSelect.addEventListener('change', loadChart); rangeSelect.addEventListener('change', () => {
updateAvgVisibility();
loadChart();
});
avgCheckbox.addEventListener('change', loadChart);
updateAvgVisibility();
loadSensors(); loadSensors();
</script> </script>
</body> </body>