158 lines
7.8 KiB
JavaScript
158 lines
7.8 KiB
JavaScript
// =============================================================================
|
||
// IoT Dashboard — Веб-интерфейс для визуализации IoT-данных
|
||
// =============================================================================
|
||
// Читает данные ТОЛЬКО из Redis (не трогает MongoDB — это зона Consumer'а).
|
||
// Отдаёт статический HTML (public/index.html) и JSON API для графиков.
|
||
//
|
||
// Redis-ключи (заполняются Consumer'ом):
|
||
// iot:latest (hash) — последние значения всех датчиков
|
||
// iot:counters (hash) — счётчики событий по типам устройств
|
||
// iot:recent (zset) — лента последних событий (сортирована по времени)
|
||
//
|
||
// API-эндпоинты:
|
||
// GET /api/latest — последние значения датчиков [{sensor_id, value, unit, ...}]
|
||
// GET /api/counters — счётчики по типам {temperature: N, humidity: N, ...}
|
||
// GET /api/recent — последние 50 событий (в обратном хронологическом порядке)
|
||
// GET / — HTML-дашборд (static из public/)
|
||
//
|
||
// Переменные окружения (задаются Terraform'ом через json_env):
|
||
// REDIS_HOST — хост Redis (K8s service.cluster.local)
|
||
// REDIS_PORT — порт Redis (6379)
|
||
// REDIS_PASS — пароль Redis
|
||
// PORT — порт HTTP-сервера (3000)
|
||
// =============================================================================
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Зависимости
|
||
// ---------------------------------------------------------------------------
|
||
const express = require("express"); // HTTP-фреймворк
|
||
const { createClient } = require("redis"); // Redis-клиент (v4+, Promise API)
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Конфигурация из переменных окружения
|
||
// ---------------------------------------------------------------------------
|
||
const PORT = process.env.PORT || 3000; // HTTP-порт
|
||
const REDIS_HOST = process.env.REDIS_HOST || "127.0.0.1"; // Redis хост
|
||
const REDIS_PORT = parseInt(process.env.REDIS_PORT || "6379"); // Redis порт
|
||
const REDIS_PASS = process.env.REDIS_PASS || undefined; // пароль
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Express-приложение
|
||
// ---------------------------------------------------------------------------
|
||
const app = express();
|
||
|
||
// Раздаём статику из папки public/ (index.html, Chart.js подгружается из CDN)
|
||
app.use(express.static("public"));
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Redis-клиент (один на всё приложение)
|
||
// ---------------------------------------------------------------------------
|
||
const redis = createClient({
|
||
socket: { host: REDIS_HOST, port: REDIS_PORT },
|
||
password: REDIS_PASS,
|
||
});
|
||
|
||
// Логируем ошибки Redis, но не крашим сервер
|
||
redis.on("error", (err) => console.error("Redis error:", err.message));
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// start() — подключение к Redis и регистрация API-маршрутов
|
||
// ---------------------------------------------------------------------------
|
||
async function start() {
|
||
await redis.connect();
|
||
console.log("Redis connected");
|
||
|
||
// -----------------------------------------------------------------------
|
||
// GET /api/latest — последние значения всех датчиков
|
||
// -----------------------------------------------------------------------
|
||
// Читает Redis-хэш iot:latest (sensor_id → JSON),
|
||
// десериализует JSON и возвращает массив объектов.
|
||
//
|
||
// Redis-структура:
|
||
// iot:latest = {
|
||
// "temp_living": '{"value":26.95,"unit":"°C","location":"living_room",...}',
|
||
// "humidity_bed": '{"value":52.1,"unit":"%","location":"bedroom",...}',
|
||
// ...
|
||
// }
|
||
//
|
||
// Ответ:
|
||
// [{sensor_id: "temp_living", value: 26.95, unit: "°C", location: "living_room", ...}, ...]
|
||
// -----------------------------------------------------------------------
|
||
app.get("/api/latest", async (req, res) => {
|
||
try {
|
||
// hGetAll возвращает объект {sensor_id: json_string, ...}
|
||
const data = await redis.hGetAll("iot:latest");
|
||
|
||
// Преобразуем: {key: json} → [{sensor_id: key, ...parsedJson}]
|
||
const items = Object.entries(data).map(([id, json]) => ({
|
||
sensor_id: id,
|
||
...JSON.parse(json), // разворачиваем JSON в поля объекта
|
||
}));
|
||
|
||
res.json(items);
|
||
} catch (e) {
|
||
// При ошибке Redis — возвращаем ошибку в JSON (не 500,
|
||
// чтобы фронтенд не упал)
|
||
res.json({ error: e.message });
|
||
}
|
||
});
|
||
|
||
// -----------------------------------------------------------------------
|
||
// GET /api/counters — счётчики событий по типам устройств
|
||
// -----------------------------------------------------------------------
|
||
// Redis-структура (заполняется HINCRBY в consumer):
|
||
// iot:counters = {
|
||
// "temperature": "3847",
|
||
// "humidity": "2563",
|
||
// "power_meter": "3851"
|
||
// }
|
||
//
|
||
// Ответ: {temperature: "3847", humidity: "2563", power_meter: "3851"}
|
||
// Значения — строки (Redis hash), фронтенд преобразует в Number.
|
||
// -----------------------------------------------------------------------
|
||
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 });
|
||
}
|
||
});
|
||
|
||
// -----------------------------------------------------------------------
|
||
// GET /api/recent — последние 50 событий (лента)
|
||
// -----------------------------------------------------------------------
|
||
// Redis-структура (заполняется ZADD score=timestamp в consumer):
|
||
// iot:recent (sorted set, до 1000 элементов)
|
||
//
|
||
// ZRANGE 0 49 REV — последние 50 элементов (REV = обратный порядок)
|
||
// Каждый элемент — JSON-строка события, десериализуем.
|
||
//
|
||
// Ответ: массив до 50 объектов событий в обратном хронологическом порядке
|
||
// -----------------------------------------------------------------------
|
||
app.get("/api/recent", async (req, res) => {
|
||
try {
|
||
// ZRANGE с REV: true — от большего score к меньшему (сначала новые)
|
||
const items = await redis.zRange("iot:recent", 0, 49, { REV: true });
|
||
|
||
// Каждый элемент в zset — JSON-строка, парсим
|
||
res.json(items.map(JSON.parse));
|
||
} catch (e) {
|
||
res.json({ error: e.message });
|
||
}
|
||
});
|
||
|
||
// -----------------------------------------------------------------------
|
||
// GET / — заглушка (статику отдаёт express.static выше)
|
||
// -----------------------------------------------------------------------
|
||
app.get("/", (req, res) => {
|
||
res.send("IoT Dashboard OK");
|
||
});
|
||
|
||
// Запуск HTTP-сервера
|
||
app.listen(PORT, () => console.log(`Dashboard on port ${PORT}`));
|
||
}
|
||
|
||
// Стартуем приложение
|
||
start();
|