Add Rulesets page with HTML structure and CSS styles

- Created a new HTML file for the Rulesets page, including a header, toolbar, rules grid, and rule detail popup.
- Implemented JavaScript functionality for loading, filtering, sorting, and managing rules.
- Added CSS styles for the layout, components, and responsive design of the Rulesets page.
This commit is contained in:
Giuseppe Raffa
2026-04-15 08:06:29 +02:00
parent c9402de2e4
commit 3094c06467
13 changed files with 2010 additions and 27 deletions

View File

@@ -1,9 +1,9 @@
const router = require('express').Router();
const { queryAll, query } = require('../store/redis');
const { queryAll, query, hset } = require('../store/redis');
const { connectedSensors } = require('../ws/handler');
/**
* GET /sessions — Lista tutte le sessioni attive dei sensori.
* Legge da Redis le chiavi sensors:* (scritte da handler.js alla connessione)
* GET /sessions — Lista tutte le sessioni dei sensori con metadata e rules versions
*/
router.get('/', async (req, res) => {
try {
@@ -16,7 +16,13 @@ router.get('/', async (req, res) => {
name,
connectedAt: info.timestamp || null,
session: info.session || null,
sessionLabel: info.sessionLabel || info.session || null,
status: info.status || 'unknown',
rules: {
weather: info.rules_weather || null,
data: info.rules_data || null,
logs: info.rules_logs || null,
}
};
}
res.json(sessions);
@@ -27,8 +33,7 @@ router.get('/', async (req, res) => {
});
/**
* GET /sessions/pending — Lista token di connessione pendenti.
* Legge da Redis le chiavi sensors_pending:* (create da createConnectionToken)
* GET /sessions/pending — Lista token di connessione pendenti
*/
router.get('/pending', async (req, res) => {
try {
@@ -41,8 +46,7 @@ router.get('/pending', async (req, res) => {
});
/**
* GET /sessions/connected — Lista sensori attualmente connessi.
* Legge da Redis le chiavi sensor:* (scritte da appendAsConnection in handler.js)
* GET /sessions/connected — Lista sensori attualmente connessi
*/
router.get('/connected', async (req, res) => {
try {
@@ -63,7 +67,7 @@ router.get('/connected', async (req, res) => {
});
/**
* GET /sessions/connected/:id — Verifica se un sensore specifico è connesso.
* GET /sessions/connected/:id — Verifica se un sensore specifico è connesso
*/
router.get('/connected/:id', async (req, res) => {
const { id } = req.params;
@@ -79,4 +83,38 @@ router.get('/connected/:id', async (req, res) => {
}
});
/**
* POST /sessions/:id/label — Cambia il label della sessione per un sensore connesso.
* Non interrompe il flusso dati. I nuovi punti InfluxDB avranno il nuovo tag.
*/
router.post('/:id/label', async (req, res) => {
const { id } = req.params;
const { label } = req.body;
if (!label || typeof label !== 'string' || label.trim().length === 0) {
return res.status(400).json({ error: 'label is required' });
}
const trimmedLabel = label.trim();
// Trova il WS client connesso
const ws = connectedSensors.get(id);
if (!ws) {
return res.status(404).json({ error: 'sensor not connected' });
}
// Aggiorna in memoria (effetto immediato sui prossimi punti InfluxDB)
ws.sessionLabel = trimmedLabel;
// Aggiorna in Redis per persistenza
try {
await hset(`sensors:${id}`, 'sessionLabel', trimmedLabel);
} catch (err) {
console.error('Error updating session label in Redis', err);
}
console.log(`[${id}] Session label changed to: ${trimmedLabel}`);
res.json({ status: 'ok', label: trimmedLabel });
});
module.exports = router;

View File

@@ -3,8 +3,9 @@ const { decode } = require('@msgpack/msgpack');
const { consumeConnectionToken, appendAsConnection, query, hset, del } = require('../store/redis');
const { writeSensorData, queryHistory } = require('../store/influx');
// In-memory map: sensorName → Set<WebSocket>
const sensorWatchers = new Map();
// In-memory registries
const sensorWatchers = new Map(); // sensorName → Set<WebSocket> (watchers)
const connectedSensors = new Map(); // sensorName → WebSocket (sensor clients)
function generateSessionId() {
const num = Math.floor(1000 + Math.random() * 9000);
@@ -71,13 +72,13 @@ function setup(server) {
wss.handleUpgrade(req, socket, head, (ws) => {
ws.sensorName = sensor;
ws.sessionId = generateSessionId();
ws.sessionLabel = ws.sessionId; // default label = sessionId
ws.connectedAt = new Date().toISOString();
ws.rulesVersions = null; // populated by _t:init message
handleSensorConnection(ws);
});
} else if (path === '/live') {
// Accept upgrade without requiring query params.
// The console sends { action: 'watch', sensorId } after connecting.
wss.handleUpgrade(req, socket, head, (ws) => {
handleWatcherConnection(ws);
});
@@ -90,11 +91,14 @@ function setup(server) {
}
function handleSensorConnection(ws) {
const { sensorName, sessionId, connectedAt } = ws;
const { sensorName, sessionId, sessionLabel, connectedAt } = ws;
console.log(`Sensor connected: ${sensorName} (session: ${sessionId})`);
// Register in global registry
connectedSensors.set(sensorName, ws);
appendAsConnection(sensorName, 'connected', connectedAt);
hset(`sensors:${sensorName}`, 'session', sessionId);
hset(`sensors:${sensorName}`, 'session', sessionId, 'sessionLabel', sessionLabel);
const pingInterval = setInterval(() => {
if (ws.readyState === ws.OPEN) ws.ping();
@@ -103,11 +107,28 @@ function handleSensorConnection(ws) {
ws.on('message', (data) => {
try {
const packet = decode(data);
// Messaggio di inizializzazione con versioni rulesets
if (packet._t === 'init') {
ws.rulesVersions = packet.rules || {};
console.log(`[${sensorName}] Rules versions:`, ws.rulesVersions);
// Salva in Redis
const rulesFields = [];
for (const [type, ver] of Object.entries(ws.rulesVersions)) {
rulesFields.push(`rules_${type}`, ver);
}
if (rulesFields.length > 0) {
hset(`sensors:${sensorName}`, ...rulesFields);
}
return; // non scrivere su InfluxDB
}
const { ts, _m, ...fields } = packet;
writeSensorData(fields, sensorName, sessionId, ts);
// Usa sessionLabel (puo' cambiare a runtime dalla console)
writeSensorData(fields, sensorName, ws.sessionLabel, ts);
// Broadcast to watchers as JSON messages grouped by measurement
// Broadcast to watchers
const watchers = sensorWatchers.get(sensorName);
if (watchers && watchers.size > 0) {
const messages = transformPacket(packet);
@@ -128,6 +149,7 @@ function handleSensorConnection(ws) {
ws.on('close', () => {
console.log(`Sensor disconnected: ${sensorName}`);
clearInterval(pingInterval);
connectedSensors.delete(sensorName);
appendAsConnection(sensorName, 'disconnected', new Date().toISOString());
del(`sensors:${sensorName}`);
});
@@ -145,7 +167,6 @@ function handleWatcherConnection(ws) {
const msg = JSON.parse(data.toString());
if (msg.action === 'watch' && msg.sensorId) {
// Unwatch previous sensor if any
if (ws.sensorName) {
sensorWatchers.get(ws.sensorName)?.delete(ws);
if (sensorWatchers.get(ws.sensorName)?.size === 0) {
@@ -155,7 +176,6 @@ function handleWatcherConnection(ws) {
ws.sensorName = msg.sensorId;
// Register as watcher
if (!sensorWatchers.has(msg.sensorId)) {
sensorWatchers.set(msg.sensorId, new Set());
}
@@ -163,14 +183,12 @@ function handleWatcherConnection(ws) {
console.log(`Watcher now watching sensor: ${msg.sensorId}`);
// Send history since sensor connected
try {
const sensorInfo = await query(msg.sensorId, 'sensors');
if (sensorInfo && sensorInfo.timestamp && sensorInfo.session) {
const history = await queryHistory(msg.sensorId, sensorInfo.session, sensorInfo.timestamp);
for (const row of history) {
const ts = new Date(row._time).getTime();
// Send each historical row as individual messages grouped by measurement
const rebuilt = { ts };
for (const [short, { key }] of Object.entries(fieldMapping)) {
const influxField = { t: 'temperature', h: 'humidity', spd: 'speed', cog: 'cog', sog: 'sog', hdg: 'headingTrue', lat: 'latitude', lon: 'longitude' }[short];
@@ -217,4 +235,4 @@ function handleWatcherConnection(ws) {
});
}
module.exports = { setup };
module.exports = { setup, connectedSensors };