Initial — Node.js IoT Dashboard with Chart.js
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "tf-iot-dashboard",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "IoT Dashboard — Node.js + Redis + Chart.js",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.21.0",
|
||||||
|
"redis": "^4.7.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>IoT Dashboard — Умный дом</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; }
|
||||||
|
.header { background: #1e293b; padding: 20px 30px; border-bottom: 1px solid #334155; }
|
||||||
|
.header h1 { font-size: 24px; color: #38bdf8; }
|
||||||
|
.header span { color: #94a3b8; font-size: 14px; }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px; padding: 20px; }
|
||||||
|
.card { background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; }
|
||||||
|
.card h2 { font-size: 16px; color: #94a3b8; margin-bottom: 15px; }
|
||||||
|
.chart-wrap { position: relative; height: 250px; }
|
||||||
|
.table-wrap { max-height: 300px; overflow-y: auto; }
|
||||||
|
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||||
|
th, td { padding: 8px 12px; text-align: left; border-bottom: 1px solid #334155; }
|
||||||
|
th { color: #64748b; font-weight: 600; }
|
||||||
|
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; }
|
||||||
|
.badge-temp { background: #f97316; color: #fff; }
|
||||||
|
.badge-hum { background: #06b6d4; color: #fff; }
|
||||||
|
.badge-power { background: #eab308; color: #000; }
|
||||||
|
.refresh { text-align: right; font-size: 12px; color: #64748b; margin-bottom: 10px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>🏠 IoT Dashboard — Умный дом</h1>
|
||||||
|
<span>Kafka → Redis → MongoDB | Автообновление каждые 3 сек</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card">
|
||||||
|
<h2>📊 Последние значения датчиков</h2>
|
||||||
|
<div class="chart-wrap"><canvas id="latestChart"></canvas></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>📈 Счётчики по типам устройств</h2>
|
||||||
|
<div class="chart-wrap"><canvas id="countersChart"></canvas></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>📋 Последние события</h2>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table id="recentTable">
|
||||||
|
<thead><tr><th>Время</th><th>Датчик</th><th>Значение</th><th>Локация</th></tr></thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let latestChart, countersChart;
|
||||||
|
|
||||||
|
function getBadge(type) {
|
||||||
|
if (type === 'temperature') return 'badge-temp';
|
||||||
|
if (type === 'humidity') return 'badge-hum';
|
||||||
|
return 'badge-power';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
try {
|
||||||
|
// Latest values
|
||||||
|
const latest = await fetch('/api/latest').then(r => r.json());
|
||||||
|
const labels = latest.map(d => d.sensor_id);
|
||||||
|
const values = latest.map(d => d.value);
|
||||||
|
const colors = latest.map(d => {
|
||||||
|
if (d.unit === '°C') return '#f97316';
|
||||||
|
if (d.unit === '%') return '#06b6d4';
|
||||||
|
return '#eab308';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!latestChart) {
|
||||||
|
latestChart = new Chart(document.getElementById('latestChart'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: { labels, datasets: [{ label: 'Значение', data: values, backgroundColor: colors }] },
|
||||||
|
options: { responsive: true, maintainAspectRatio: false,
|
||||||
|
scales: { y: { ticks: { color: '#94a3b8' } }, x: { ticks: { color: '#94a3b8' } } },
|
||||||
|
plugins: { legend: { display: false } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
latestChart.data.labels = labels;
|
||||||
|
latestChart.data.datasets[0].data = values;
|
||||||
|
latestChart.data.datasets[0].backgroundColor = colors;
|
||||||
|
latestChart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Counters
|
||||||
|
const counters = await fetch('/api/counters').then(r => r.json());
|
||||||
|
const cLabels = Object.keys(counters);
|
||||||
|
const cValues = Object.values(counters).map(Number);
|
||||||
|
|
||||||
|
if (!countersChart) {
|
||||||
|
countersChart = new Chart(document.getElementById('countersChart'), {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: { labels: cLabels, datasets: [{ data: cValues, backgroundColor: ['#f97316','#06b6d4','#eab308'] }] },
|
||||||
|
options: { responsive: true, maintainAspectRatio: false,
|
||||||
|
plugins: { legend: { labels: { color: '#94a3b8' } } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
countersChart.data.labels = cLabels;
|
||||||
|
countersChart.data.datasets[0].data = cValues;
|
||||||
|
countersChart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recent events
|
||||||
|
const recent = await fetch('/api/recent').then(r => r.json());
|
||||||
|
const tbody = document.querySelector('#recentTable tbody');
|
||||||
|
tbody.innerHTML = recent.slice(0, 15).map(e => `
|
||||||
|
<tr>
|
||||||
|
<td>${new Date(e.timestamp).toLocaleTimeString()}</td>
|
||||||
|
<td>${e.sensor_id}</td>
|
||||||
|
<td><span class="badge ${getBadge(e.device_type)}">${e.value} ${e.unit}</span></td>
|
||||||
|
<td>${e.location}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
setInterval(refresh, 3000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const { createClient } = require("redis");
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
const REDIS_HOST = process.env.REDIS_HOST || "127.0.0.1";
|
||||||
|
const REDIS_PORT = parseInt(process.env.REDIS_PORT || "6379");
|
||||||
|
const REDIS_PASSWORD = process.env.REDIS_PASSWORD || undefined;
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.static("public"));
|
||||||
|
|
||||||
|
const redis = createClient({
|
||||||
|
socket: { host: REDIS_HOST, port: REDIS_PORT },
|
||||||
|
password: REDIS_PASSWORD,
|
||||||
|
});
|
||||||
|
|
||||||
|
redis.on("error", (err) => console.error("Redis error:", err.message));
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
await redis.connect();
|
||||||
|
console.log("Redis connected");
|
||||||
|
|
||||||
|
// API: latest sensor values
|
||||||
|
app.get("/api/latest", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const data = await redis.hGetAll("iot:latest");
|
||||||
|
const items = Object.entries(data).map(([id, json]) => ({
|
||||||
|
sensor_id: id,
|
||||||
|
...JSON.parse(json),
|
||||||
|
}));
|
||||||
|
res.json(items);
|
||||||
|
} catch (e) {
|
||||||
|
res.json({ error: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// API: counters by device type
|
||||||
|
app.get("/api/counters", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const data = await redis.hGetAll("iot:counters");
|
||||||
|
res.json(data);
|
||||||
|
} catch (e) {
|
||||||
|
res.json({ error: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// API: recent events
|
||||||
|
app.get("/api/recent", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const items = await redis.zRange("iot:recent", 0, 49, { REV: true });
|
||||||
|
res.json(items.map(JSON.parse));
|
||||||
|
} catch (e) {
|
||||||
|
res.json({ error: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
app.get("/", (req, res) => {
|
||||||
|
res.send("IoT Dashboard OK");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(PORT, () => console.log(`Dashboard on port ${PORT}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
start();
|
||||||
Reference in New Issue
Block a user