Files

380 lines
17 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<!-- =========================================================================
IoT Dashboard — «Умный дом»
Визуализация данных из Redis через Chart.js
Пайплайн данных:
Producer → RabbitMQ → Consumer → Redis + MongoDB → Dashboard (этот файл)
Данные получает через API (server.js):
GET /api/latest — последние значения датчиков (bar chart)
GET /api/counters — счётчики по типам (doughnut chart)
GET /api/recent — лента событий (таблица)
Автообновление каждые 3 секунды (setInterval).
Chart.js v4 загружается из CDN.
========================================================================= -->
<html lang="ru">
<head>
<meta charset="UTF-8">
<!-- viewport для мобильных: масштабирование под ширину экрана -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IoT Dashboard — Умный дом</title>
<!-- Chart.js v4 из CDN (jsDelivr) — библиотека графиков -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
<style>
/* ===================================================================
Стили: тёмная тема в стиле Tailwind/shadcn
Цветовая схема: Slate (фон #0f172a, карточки #1e293b, бордер #334155)
=================================================================== */
/* Сброс отступов и box-sizing для всех элементов */
* { margin: 0; padding: 0; box-sizing: border-box; }
/* Основной фон — тёмный slate-900 */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #0f172a; /* slate-900 */
color: #e2e8f0; /* slate-200 — светлый текст */
min-height: 100vh; /* на весь экран */
}
/* Шапка: тёмная полоса вверху */
.header {
background: #1e293b; /* slate-800 */
padding: 20px 30px;
border-bottom: 1px solid #334155; /* slate-700 */
}
.header h1 {
font-size: 24px;
color: #38bdf8; /* sky-400 — голубой заголовок */
}
.header span {
color: #94a3b8; /* slate-400 — серый подзаголовок */
font-size: 14px;
}
/* Сетка карточек: автоматически подстраивается под ширину экрана */
.grid {
display: grid;
/* auto-fit: карточки переносятся на новую строку если не влезают */
/* minmax(400px, 1fr): минимум 400px, максимум — равные доли */
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
padding: 20px;
}
/* Карточка с графиком или таблицей */
.card {
background: #1e293b; /* slate-800 */
border-radius: 12px; /* скруглённые углы */
padding: 20px;
border: 1px solid #334155; /* slate-700 */
}
.card h2 {
font-size: 16px;
color: #94a3b8; /* slate-400 */
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; /* slate-500 — заголовки таблицы */
font-weight: 600;
}
/* Бейджи для типов устройств (цветовое кодирование) */
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
}
.badge-temp { background: #f97316; color: #fff; } /* orange-500 — температура */
.badge-hum { background: #06b6d4; color: #fff; } /* cyan-500 — влажность */
.badge-power { background: #eab308; color: #000; } /* yellow-500 — энергия */
/* Подсказка об автообновлении (серая, мелкая) */
.refresh {
text-align: right;
font-size: 12px;
color: #64748b;
margin-bottom: 10px;
}
</style>
</head>
<body>
<!-- =====================================================================
Шапка дашборда
===================================================================== -->
<div class="header">
<h1>🏠 IoT Dashboard — Умный дом</h1>
<!-- Описание пайплайна: от RabbitMQ через Redis до отображения -->
<span>RabbitMQ → Redis → MongoDB | Автообновление каждые 3 сек</span>
</div>
<!-- =====================================================================
Сетка: 3 карточки (графики + таблица)
===================================================================== -->
<div class="grid">
<!-- =================================================================
Карточка 1: Bar Chart — последние значения датчиков
Данные: GET /api/latest
================================================================= -->
<div class="card">
<h2>📊 Последние значения датчиков</h2>
<div class="chart-wrap">
<!-- Canvas для Chart.js — рисуется программно -->
<canvas id="latestChart"></canvas>
</div>
</div>
<!-- =================================================================
Карточка 2: Doughnut Chart — счётчики по типам устройств
Данные: GET /api/counters
================================================================= -->
<div class="card">
<h2>📈 Счётчики по типам устройств</h2>
<div class="chart-wrap">
<canvas id="countersChart"></canvas>
</div>
</div>
<!-- =================================================================
Карточка 3: Таблица последних событий
Данные: GET /api/recent
================================================================= -->
<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>
<!-- Заполняется динамически через JS -->
</tbody>
</table>
</div>
</div>
</div>
<!-- =====================================================================
Карточка с описанием демо (вне сетки, на всю ширину)
===================================================================== -->
<div class="card" style="margin: 0 20px 20px 20px; line-height: 1.8;">
<h2>О демонстрации</h2>
<p>
<b>Эмуляция:</b> IoT-датчики «Умного дома» — температура, влажность, энергопотребление (8 датчиков, 3 типа).<br>
<b>Пайплайн данных:</b><br>
1. <b>Producer</b> (Node.js) — генерирует события → отправляет в RabbitMQ<br>
2. <b>RabbitMQ</b> — очередь сообщений (iot-events)<br>
3. <b>Consumer</b> (Node.js) — читает очередь → сохраняет в Redis + MongoDB<br>
4. <b>Redis</b> — кэш: последние значения, счётчики, лента событий<br>
5. <b>MongoDB</b> — постоянное хранение всех событий<br>
6. <b>Dashboard</b> (Node.js + Chart.js) — читает Redis → отображает графики<br>
<b>Сервисы Nubes Cloud:</b> RabbitMQ, Redis, MongoDB, Node.js ×3<br>
<b>Развёрнуто через Terraform</b> на тестовом стенде (k8s-4-sandbox-nubes-ru)
</p>
</div>
<!-- =====================================================================
JavaScript: логика дашборда
===================================================================== -->
<script>
// -------------------------------------------------------------------
// Переменные для Chart.js-инстансов (создаются один раз, потом обновляются)
// -------------------------------------------------------------------
let latestChart, countersChart;
// -------------------------------------------------------------------
// getBadge(type) — возвращает CSS-класс бейджа по типу устройства
// Используется для цветового кодирования в таблице событий
// -------------------------------------------------------------------
function getBadge(type) {
if (type === 'temperature') return 'badge-temp'; // оранжевый
if (type === 'humidity') return 'badge-hum'; // голубой
return 'badge-power'; // жёлтый (power_meter)
}
// -------------------------------------------------------------------
// refresh() — основная функция обновления данных
// Вызывается при загрузке страницы и каждые 3 секунды.
//
// Порядок работы:
// 1. Запрашивает /api/latest → обновляет Bar Chart
// 2. Запрашивает /api/counters → обновляет Doughnut Chart
// 3. Запрашивает /api/recent → обновляет таблицу (первые 15 строк)
//
// При первом вызове создаёт Chart.js-инстансы,
// при последующих — обновляет данные через .update()
// (это эффективнее, чем пересоздавать canvas каждый раз).
// -------------------------------------------------------------------
async function refresh() {
try {
// ===============================================================
// 1. Bar Chart — последние значения датчиков
// ===============================================================
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); // числовые значения
// Цвет столбца зависит от единицы измерения:
// °C → оранжевый, % → голубой, kW → жёлтый
const colors = latest.map(d => {
if (d.unit === '°C') return '#f97316'; // orange-500
if (d.unit === '%') return '#06b6d4'; // cyan-500
return '#eab308'; // yellow-500
});
if (!latestChart) {
// === Первый вызов: создаём новый Chart.js-инстанс ===
latestChart = new Chart(
document.getElementById('latestChart'),
{
type: 'bar', // столбчатая диаграмма
data: {
labels, // подписи по оси X (имена датчиков)
datasets: [{
label: 'Значение',
data: values,
backgroundColor: colors, // цвет каждого столбца
}],
},
options: {
responsive: true, // адаптивный размер
maintainAspectRatio: false, // можно менять пропорции
scales: {
y: { ticks: { color: '#94a3b8' } }, // цвет шкалы Y
x: { ticks: { color: '#94a3b8' } }, // цвет шкалы X
},
plugins: {
legend: { display: false }, // скрываем легенду (избыточно)
},
},
}
);
} else {
// === Последующие вызовы: обновляем существующий график ===
latestChart.data.labels = labels;
latestChart.data.datasets[0].data = values;
latestChart.data.datasets[0].backgroundColor = colors;
latestChart.update(); // перерисовываем без моргания
}
// ===============================================================
// 2. Doughnut Chart — счётчики по типам устройств
// ===============================================================
const counters = await fetch('/api/counters').then(r => r.json());
// Ключи — типы устройств (temperature, humidity, power_meter)
const cLabels = Object.keys(counters);
// Значения — счётчики (строки из Redis, преобразуем в Number)
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();
}
// ===============================================================
// 3. Таблица последних событий
// ===============================================================
const recent = await fetch('/api/recent').then(r => r.json());
const tbody = document.querySelector('#recentTable tbody');
// Берём первые 15 событий, формируем HTML-строки таблицы
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(''); // объединяем массив строк в одну HTML-строку
} catch (e) {
// Сетевая ошибка или API недоступен — логируем,
// дашборд продолжит попытки через 3 секунды
console.error(e);
}
}
// -------------------------------------------------------------------
// Запуск: первый рендер + автообновление каждые 3 секунды
// -------------------------------------------------------------------
refresh(); // немедленный первый рендер
setInterval(refresh, 3000); // затем каждые 3000 мс (3 секунды)
</script>
</body>
</html>