Files
yggdrasil/server/templates/sensors.html
T

304 lines
12 KiB
HTML
Raw Normal View History

2026-09-13 18:14:19 +02:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
2026-10-03 20:24:37 +02:00
<title>Sensor Readouts — Yggdrasil</title>
2026-09-13 18:14:19 +02:00
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
2026-10-03 20:24:37 +02:00
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='retro.css') }}">
2026-09-13 18:14:19 +02:00
</head>
<body>
2026-10-03 19:31:38 +02:00
<nav>
2026-10-03 20:24:37 +02:00
<a href="{{ url_for('home') }}">Home</a>
2026-11-01 17:17:58 +01:00
<a href="{{ url_for('notes_page') }}">Notes</a>
2026-10-03 20:24:37 +02:00
<a href="{{ url_for('api_docs') }}">API</a>
<a href="{{ url_for('logout') }}">Log out</a>
2026-10-23 11:37:55 +02:00
<img class="nav-logo" src="{{ url_for('static', filename='yggdrasil-logo.png') }}" alt="Yggdrasil logo">
2026-10-03 19:31:38 +02:00
</nav>
2026-09-13 18:14:19 +02:00
<h1>Sensor Readouts</h1>
<div>
<label for="sensor-select">Sensor:</label>
<select id="sensor-select">
<option value="">-- select sensor --</option>
</select>
</div>
<div id="type-section" class="hidden">
<label for="type-select">Type:</label>
<select id="type-select">
<option value="">-- select type --</option>
</select>
</div>
2026-10-03 20:24:37 +02:00
<div id="range-section" class="hidden">
<label for="range-select">Range:</label>
<select id="range-select">
<option value="today">Today</option>
2026-10-05 19:28:37 +02:00
<option value="24h">Last 24h</option>
2026-10-03 20:24:37 +02:00
<option value="week">Week</option>
<option value="month">Month</option>
<option value="all" selected>All</option>
</select>
2026-10-23 11:37:55 +02:00
<label id="avg-label" class="hidden">
<input type="checkbox" id="avg-checkbox"> hourly average
</label>
2026-10-03 20:24:37 +02:00
<button id="refresh-button" type="button">[ Refresh ]</button>
</div>
2026-09-13 18:14:19 +02:00
<div id="chart-section" class="hidden chart-box">
<canvas id="chart"></canvas>
2026-10-05 19:28:37 +02:00
<p id="chart-info" class="desc"></p>
2026-10-03 20:24:37 +02:00
<p id="chart-error" class="error hidden"></p>
2026-09-13 18:14:19 +02:00
</div>
<div id="readouts-section" class="hidden">
<table>
<thead>
<tr>
<th>Timestamp</th>
<th>Value</th>
</tr>
</thead>
<tbody id="readouts-body"></tbody>
</table>
</div>
2026-10-03 20:24:37 +02:00
<footer class="statusbar">
<span>{{ user.username }}@yggdrasil:~/sensors$</span>
</footer>
2026-09-13 18:14:19 +02:00
<script>
const sensorSelect = document.getElementById('sensor-select');
const typeSelect = document.getElementById('type-select');
const typeSection = document.getElementById('type-section');
2026-10-03 20:24:37 +02:00
const rangeSelect = document.getElementById('range-select');
const rangeSection = document.getElementById('range-section');
2026-09-13 18:14:19 +02:00
const chartSection = document.getElementById('chart-section');
2026-10-03 20:24:37 +02:00
const chartError = document.getElementById('chart-error');
const refreshButton = document.getElementById('refresh-button');
2026-09-13 18:14:19 +02:00
const readoutsSection = document.getElementById('readouts-section');
const readoutsBody = document.getElementById('readouts-body');
2026-10-05 19:28:37 +02:00
const chartInfo = document.getElementById('chart-info');
2026-10-23 11:37:55 +02:00
const avgCheckbox = document.getElementById('avg-checkbox');
const avgLabel = document.getElementById('avg-label');
2026-09-13 18:14:19 +02:00
let chart = null;
2026-10-05 19:28:37 +02:00
// Strefa czasowa przeglądarki — backend liczy "today" od północy
// w tej strefie (np. Europe/Warsaw = GMT+2 latem).
const userTz = Intl.DateTimeFormat().resolvedOptions().timeZone || 'Europe/Warsaw';
2026-10-03 20:24:37 +02:00
// The page must keep working (dropdown + table) even if the
// Chart.js CDN is unreachable — the chart is then simply skipped.
if (window.Chart) {
Chart.defaults.color = '#d7d7d7';
Chart.defaults.borderColor = 'rgba(215,215,215,0.18)';
Chart.defaults.font.family = "'VT323', 'Courier New', monospace";
Chart.defaults.font.size = 20;
}
2026-09-13 18:14:19 +02:00
function formatTimestamp(iso) {
const d = new Date(iso);
2026-10-05 19:28:37 +02:00
return d.toLocaleString('pl-PL', { timeZone: userTz });
2026-09-20 18:24:32 +02:00
}
2026-10-23 11:37:55 +02:00
function formatChartLabel(iso, timeOnly) {
2026-09-20 18:24:32 +02:00
const d = new Date(iso + 'Z');
2026-10-23 11:37:55 +02:00
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' });
2026-10-05 19:28:37 +02:00
}
2026-10-23 11:37:55 +02:00
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'
2026-10-05 19:28:37 +02:00
: 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}).`;
}
2026-10-23 11:37:55 +02:00
function isRawRange(range) {
return range === 'today' || range === '24h';
}
function updateAvgVisibility() {
avgLabel.classList.toggle('hidden', !isRawRange(rangeSelect.value));
}
2026-10-05 19:28:37 +02:00
// 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) {
if (type === 'pressure') return Math.round(value / 10) / 10;
return value;
}
function formatTableValue(type, value) {
if (type === 'pressure') return (value / 100).toFixed(1) + ' hPa';
return value;
}
function yAxisTitle(type) {
if (type === 'pressure') return 'pressure (hPa)';
return type;
2026-09-13 18:14:19 +02:00
}
async function fetchJSON(url) {
const res = await fetch(url);
return res.json();
}
async function loadSensors() {
const sensors = await fetchJSON('/api/sensors');
sensors.forEach(s => {
const opt = document.createElement('option');
opt.value = s.id;
opt.textContent = s.name;
sensorSelect.appendChild(opt);
});
}
sensorSelect.addEventListener('change', async () => {
const sensorId = sensorSelect.value;
typeSection.classList.add('hidden');
2026-10-03 20:24:37 +02:00
rangeSection.classList.add('hidden');
2026-09-13 18:14:19 +02:00
chartSection.classList.add('hidden');
readoutsSection.classList.add('hidden');
typeSelect.innerHTML = '<option value="">-- select type --</option>';
readoutsBody.innerHTML = '';
if (!sensorId) return;
const types = await fetchJSON(`/api/sensors/${sensorId}/types`);
types.forEach(t => {
const opt = document.createElement('option');
opt.value = t;
opt.textContent = t;
typeSelect.appendChild(opt);
});
typeSection.classList.remove('hidden');
2026-10-03 20:24:37 +02:00
rangeSection.classList.remove('hidden');
2026-09-13 18:14:19 +02:00
});
2026-10-03 20:24:37 +02:00
async function loadChart() {
2026-09-13 18:14:19 +02:00
const sensorId = sensorSelect.value;
const type = typeSelect.value;
2026-10-03 20:24:37 +02:00
const range = rangeSelect.value;
2026-09-13 18:14:19 +02:00
chartSection.classList.add('hidden');
2026-10-03 20:24:37 +02:00
chartError.classList.add('hidden');
if (!sensorId || !type || !window.Chart) return;
try {
// fetch hourly data for chart
2026-10-23 11:37:55 +02:00
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'}`);
2026-10-03 20:24:37 +02:00
if (!hourly.length) throw new Error('no data for this range');
2026-10-23 11:37:55 +02:00
// 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)));
2026-10-05 19:28:37 +02:00
const values = hourly.map(r => displayValue(type, r.value));
2026-10-03 20:24:37 +02:00
if (chart) chart.destroy();
const ctx = document.getElementById('chart').getContext('2d');
chart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: type,
data: values,
borderColor: '#55ffff',
backgroundColor: 'rgba(85,255,255,0.12)',
pointBackgroundColor: '#ffff55',
pointBorderColor: '#55ffff',
fill: true,
tension: 0.3,
2026-10-05 19:28:37 +02:00
pointRadius: dense ? 1.5 : 3,
2026-10-03 20:24:37 +02:00
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: {
2026-10-05 19:28:37 +02:00
x: { title: { display: true, text: dense ? 'Time' : 'Time (hourly avg)' } },
y: { title: { display: true, text: yAxisTitle(type) } }
2026-10-03 20:24:37 +02:00
}
}
});
chartSection.classList.remove('hidden');
// The chart was created while the container was hidden,
// so force a re-measure now that it is visible.
chart.resize();
2026-10-23 11:37:55 +02:00
chartInfo.textContent = '> ' + chartInfoText(range, averaged);
2026-10-03 20:24:37 +02:00
} catch (err) {
chartError.textContent = '! Chart error: ' + err.message;
chartError.classList.remove('hidden');
2026-10-05 19:28:37 +02:00
chartInfo.textContent = '';
2026-10-03 20:24:37 +02:00
chartSection.classList.remove('hidden');
}
}
async function loadTable() {
const sensorId = sensorSelect.value;
const type = typeSelect.value;
2026-09-13 18:14:19 +02:00
readoutsSection.classList.add('hidden');
readoutsBody.innerHTML = '';
2026-10-03 20:24:37 +02:00
if (!sensorId || !type) return;
2026-09-13 18:14:19 +02:00
// fetch raw readouts for table
const readouts = await fetchJSON(`/api/sensors/${sensorId}/readouts?type=${type}`);
readouts.forEach(r => {
const tr = document.createElement('tr');
2026-10-05 19:28:37 +02:00
tr.innerHTML = `<td>${formatTimestamp(r.timestamp)}</td><td>${formatTableValue(type, r.value)}</td>`;
2026-09-13 18:14:19 +02:00
readoutsBody.appendChild(tr);
});
readoutsSection.classList.remove('hidden');
2026-10-03 20:24:37 +02:00
}
typeSelect.addEventListener('change', async () => {
if (!typeSelect.value) {
chartSection.classList.add('hidden');
readoutsSection.classList.add('hidden');
readoutsBody.innerHTML = '';
return;
}
await loadChart();
await loadTable();
2026-09-13 18:14:19 +02:00
});
2026-10-03 20:24:37 +02:00
refreshButton.addEventListener('click', async () => {
if (!sensorSelect.value || !typeSelect.value || refreshButton.disabled) return;
refreshButton.disabled = true;
try {
await loadChart();
await loadTable();
} finally {
refreshButton.disabled = false;
}
});
2026-10-23 11:37:55 +02:00
rangeSelect.addEventListener('change', () => {
updateAvgVisibility();
loadChart();
});
avgCheckbox.addEventListener('change', loadChart);
2026-10-03 20:24:37 +02:00
2026-10-23 11:37:55 +02:00
updateAvgVisibility();
2026-09-13 18:14:19 +02:00
loadSensors();
</script>
</body>
</html>