161 lines
5.8 KiB
HTML
161 lines
5.8 KiB
HTML
<!DOCTYPE html>
|
|||
|
|
<html lang="en">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
|
<title>Sensor Readouts</title>
|
||
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
|
||
|
|
<style>
|
||
|
|
body { font-family: sans-serif; max-width: 800px; margin: 2rem auto; }
|
||
|
|
select, table { width: 100%; margin-top: 0.5rem; }
|
||
|
|
select { padding: 0.4rem; }
|
||
|
|
table { border-collapse: collapse; margin-top: 1rem; }
|
||
|
|
th, td { border: 1px solid #ccc; padding: 0.4rem 0.6rem; text-align: left; }
|
||
|
|
th { background: #f5f5f5; }
|
||
|
|
label { font-weight: bold; }
|
||
|
|
.hidden { display: none; }
|
||
|
|
.chart-box { margin-top: 1rem; }
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<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>
|
||
|
|
|
||
|
|
<div id="chart-section" class="hidden chart-box">
|
||
|
|
<canvas id="chart"></canvas>
|
||
|
|
</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>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
const sensorSelect = document.getElementById('sensor-select');
|
||
|
|
const typeSelect = document.getElementById('type-select');
|
||
|
|
const typeSection = document.getElementById('type-section');
|
||
|
|
const chartSection = document.getElementById('chart-section');
|
||
|
|
const readoutsSection = document.getElementById('readouts-section');
|
||
|
|
const readoutsBody = document.getElementById('readouts-body');
|
||
|
|
let chart = null;
|
||
|
|
|
||
|
|
function formatTimestamp(iso) {
|
||
|
|
const d = new Date(iso);
|
||
|
|
const pad = (n) => String(n).padStart(2, '0');
|
||
|
|
const months = ['Jan','Feb','Mar','Apr','May','Jun',
|
||
|
|
'Jul','Aug','Sep','Oct','Nov','Dec'];
|
||
|
|
return `${d.getDate()} ${months[d.getMonth()]} ${d.getFullYear()}, ` +
|
||
|
|
`${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
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');
|
||
|
|
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');
|
||
|
|
});
|
||
|
|
|
||
|
|
typeSelect.addEventListener('change', async () => {
|
||
|
|
const sensorId = sensorSelect.value;
|
||
|
|
const type = typeSelect.value;
|
||
|
|
chartSection.classList.add('hidden');
|
||
|
|
readoutsSection.classList.add('hidden');
|
||
|
|
readoutsBody.innerHTML = '';
|
||
|
|
|
||
|
|
if (!type) return;
|
||
|
|
|
||
|
|
// fetch hourly data for chart
|
||
|
|
const hourly = await fetchJSON(`/api/sensors/${sensorId}/readouts/hourly?type=${type}`);
|
||
|
|
const labels = hourly.map(r => r.timestamp.replace('T', ' '));
|
||
|
|
const values = hourly.map(r => r.value);
|
||
|
|
|
||
|
|
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: '#2563eb',
|
||
|
|
backgroundColor: 'rgba(37,99,235,0.1)',
|
||
|
|
fill: true,
|
||
|
|
tension: 0.3,
|
||
|
|
pointRadius: 2,
|
||
|
|
}]
|
||
|
|
},
|
||
|
|
options: {
|
||
|
|
responsive: true,
|
||
|
|
plugins: { legend: { display: false } },
|
||
|
|
scales: {
|
||
|
|
x: { title: { display: true, text: 'Time (hourly avg)' } },
|
||
|
|
y: { title: { display: true, text: type } }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
chartSection.classList.remove('hidden');
|
||
|
|
|
||
|
|
// fetch raw readouts for table
|
||
|
|
const readouts = await fetchJSON(`/api/sensors/${sensorId}/readouts?type=${type}`);
|
||
|
|
readouts.forEach(r => {
|
||
|
|
const tr = document.createElement('tr');
|
||
|
|
tr.innerHTML = `<td>${formatTimestamp(r.timestamp)}</td><td>${r.value}</td>`;
|
||
|
|
readoutsBody.appendChild(tr);
|
||
|
|
});
|
||
|
|
readoutsSection.classList.remove('hidden');
|
||
|
|
});
|
||
|
|
|
||
|
|
loadSensors();
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|