css
This commit is contained in:
+118
-48
@@ -3,24 +3,17 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sensor Readouts</title>
|
||||
<title>Sensor Readouts — Yggdrasil</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>
|
||||
<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') }}">
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<a href="{{ url_for('home') }}">Dom</a>
|
||||
<a href="{{ url_for('logout') }}">Wyloguj</a>
|
||||
<a href="{{ url_for('home') }}">Home</a>
|
||||
<a href="{{ url_for('api_docs') }}">API</a>
|
||||
<a href="{{ url_for('logout') }}">Log out</a>
|
||||
</nav>
|
||||
<h1>Sensor Readouts</h1>
|
||||
|
||||
@@ -38,8 +31,20 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="range-section" class="hidden">
|
||||
<label for="range-select">Range:</label>
|
||||
<select id="range-select">
|
||||
<option value="today">Today</option>
|
||||
<option value="week">Week</option>
|
||||
<option value="month">Month</option>
|
||||
<option value="all" selected>All</option>
|
||||
</select>
|
||||
<button id="refresh-button" type="button">[ Refresh ]</button>
|
||||
</div>
|
||||
|
||||
<div id="chart-section" class="hidden chart-box">
|
||||
<canvas id="chart"></canvas>
|
||||
<p id="chart-error" class="error hidden"></p>
|
||||
</div>
|
||||
|
||||
<div id="readouts-section" class="hidden">
|
||||
@@ -54,15 +59,32 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<footer class="statusbar">
|
||||
<span>{{ user.username }}@yggdrasil:~/sensors$</span>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const sensorSelect = document.getElementById('sensor-select');
|
||||
const typeSelect = document.getElementById('type-select');
|
||||
const typeSection = document.getElementById('type-section');
|
||||
const rangeSelect = document.getElementById('range-select');
|
||||
const rangeSection = document.getElementById('range-section');
|
||||
const chartSection = document.getElementById('chart-section');
|
||||
const chartError = document.getElementById('chart-error');
|
||||
const refreshButton = document.getElementById('refresh-button');
|
||||
const readoutsSection = document.getElementById('readouts-section');
|
||||
const readoutsBody = document.getElementById('readouts-body');
|
||||
let chart = null;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
function formatTimestamp(iso) {
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleString('pl-PL', { timeZone: 'Europe/Warsaw' });
|
||||
@@ -91,6 +113,7 @@
|
||||
sensorSelect.addEventListener('change', async () => {
|
||||
const sensorId = sensorSelect.value;
|
||||
typeSection.classList.add('hidden');
|
||||
rangeSection.classList.add('hidden');
|
||||
chartSection.classList.add('hidden');
|
||||
readoutsSection.classList.add('hidden');
|
||||
typeSelect.innerHTML = '<option value="">-- select type --</option>';
|
||||
@@ -106,48 +129,71 @@
|
||||
typeSelect.appendChild(opt);
|
||||
});
|
||||
typeSection.classList.remove('hidden');
|
||||
rangeSection.classList.remove('hidden');
|
||||
});
|
||||
|
||||
typeSelect.addEventListener('change', async () => {
|
||||
async function loadChart() {
|
||||
const sensorId = sensorSelect.value;
|
||||
const type = typeSelect.value;
|
||||
const range = rangeSelect.value;
|
||||
chartSection.classList.add('hidden');
|
||||
chartError.classList.add('hidden');
|
||||
|
||||
if (!sensorId || !type || !window.Chart) return;
|
||||
|
||||
try {
|
||||
// fetch hourly data for chart
|
||||
const hourly = await fetchJSON(`/api/sensors/${sensorId}/readouts/hourly?type=${type}&range=${range}`);
|
||||
if (!hourly.length) throw new Error('no data for this range');
|
||||
|
||||
const labels = hourly.map(r => formatChartLabel(r.timestamp));
|
||||
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: '#55ffff',
|
||||
backgroundColor: 'rgba(85,255,255,0.12)',
|
||||
pointBackgroundColor: '#ffff55',
|
||||
pointBorderColor: '#55ffff',
|
||||
fill: true,
|
||||
tension: 0.3,
|
||||
pointRadius: 3,
|
||||
}]
|
||||
},
|
||||
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');
|
||||
// The chart was created while the container was hidden,
|
||||
// so force a re-measure now that it is visible.
|
||||
chart.resize();
|
||||
} catch (err) {
|
||||
chartError.textContent = '! Chart error: ' + err.message;
|
||||
chartError.classList.remove('hidden');
|
||||
chartSection.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTable() {
|
||||
const sensorId = sensorSelect.value;
|
||||
const type = typeSelect.value;
|
||||
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 => formatChartLabel(r.timestamp));
|
||||
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');
|
||||
if (!sensorId || !type) return;
|
||||
|
||||
// fetch raw readouts for table
|
||||
const readouts = await fetchJSON(`/api/sensors/${sensorId}/readouts?type=${type}`);
|
||||
@@ -157,8 +203,32 @@
|
||||
readoutsBody.appendChild(tr);
|
||||
});
|
||||
readoutsSection.classList.remove('hidden');
|
||||
}
|
||||
|
||||
typeSelect.addEventListener('change', async () => {
|
||||
if (!typeSelect.value) {
|
||||
chartSection.classList.add('hidden');
|
||||
readoutsSection.classList.add('hidden');
|
||||
readoutsBody.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
await loadChart();
|
||||
await loadTable();
|
||||
});
|
||||
|
||||
refreshButton.addEventListener('click', async () => {
|
||||
if (!sensorSelect.value || !typeSelect.value || refreshButton.disabled) return;
|
||||
refreshButton.disabled = true;
|
||||
try {
|
||||
await loadChart();
|
||||
await loadTable();
|
||||
} finally {
|
||||
refreshButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
rangeSelect.addEventListener('change', loadChart);
|
||||
|
||||
loadSensors();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user