refactor: remove rules endpoint and related logic

- Deleted the rules routes and associated logic from the API.
- Removed rules-related functionality from params.sensor.js.
- Updated dashboard and rulesets HTML to remove references to rulesets.
- Removed force update button and related functionality from rulesets page.
- Cleaned up styles related to the force update button.
- Removed unused WebSocket client example.
- Updated realtime server to eliminate rules pushing logic.
- Refactored WebSocket handler to streamline data processing.
This commit is contained in:
Giuseppe Raffa
2026-04-16 14:27:27 +02:00
parent edd7226966
commit 5912c00a82
11 changed files with 32 additions and 1097 deletions

View File

@@ -96,10 +96,6 @@ app.use('/params', paramsRoutes)
const settingsRoutes = require('./routes/settings')
app.use('/settings', settingsRoutes)
const rulesRoutes = require('./routes/rules')
app.use('/rules', rulesRoutes)
app.listen(PORT, '0.0.0.0', () => {
console.log(`Started on port ${PORT}`);
});

View File

@@ -61,55 +61,4 @@ router.get('/:sensorCode/active', authenticateSensor, async (req, res) => {
}
});
// --- Mapping tipo rules → tabelle ---
const RULES_TYPE_MAP = {
weather: { rules: 'weather', items: 'weatheritems' },
laterforecasts: { rules: 'laterforecasts', items: 'laterforecastitems' },
data: { rules: 'dataread', items: 'datareaditems' },
logs: { rules: 'logs', items: 'logitems' }
};
/**
* GET /params/sensor/:sensorCode/rules?type=weather
* Ritorna la rule attiva con items per il tipo specificato.
* Autenticazione tramite sensor code (non richiede JWT/API key).
*/
router.get('/:sensorCode/rules', authenticateSensor, async (req, res) => {
const { type } = req.query;
if (!type || !RULES_TYPE_MAP[type]) {
return res.status(400).json({ error: `invalid type, must be one of: ${Object.keys(RULES_TYPE_MAP).join(', ')}` });
}
const tables = RULES_TYPE_MAP[type];
try {
const { rows: ruleRows } = await query(
`SELECT * FROM ${tables.rules} WHERE active = true AND archived = false LIMIT 1`,
[], 'rules'
);
if (ruleRows.length === 0) {
return res.status(404).json({ error: `no active ${type} rule found` });
}
const rule = ruleRows[0];
const { rows: items } = await query(
`SELECT * FROM ${tables.items} WHERE rule_id = $1 AND enabled = true`,
[rule.id], 'rules'
);
res.json({
id: rule.id,
version: rule.version,
description: rule.description,
tags: rule.tags,
items
});
} catch (err) {
console.error('[PARAMS/SENSOR] Rules error:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
module.exports = router;

View File

@@ -1,562 +0,0 @@
const router = require('express').Router();
const db = require('../storage/postgres');
const { encode } = require('@msgpack/msgpack');
// Mapping tipo → tabelle
const TYPE_MAP = {
weather: { rules: 'weather', items: 'weatheritems' },
laterforecasts: { rules: 'laterforecasts', items: 'laterforecastitems' },
data: { rules: 'dataread', items: 'datareaditems' },
logs: { rules: 'logs', items: 'logitems' }
};
const VALID_TYPES = Object.keys(TYPE_MAP);
/**
* GET /rules — Lista tutte le rules (id, version, active) per ogni tipo
*/
router.get('/', async (req, res) => {
try {
const result = {};
for (const [type, tables] of Object.entries(TYPE_MAP)) {
const { rows } = await db.query(
`SELECT * FROM ${tables.rules} ORDER BY created_at DESC`,
[], 'rules'
);
result[type] = rows;
}
res.json(result);
} catch (err) {
console.error('Error fetching rules', err);
res.status(500).json({ error: 'internal server error' });
}
});
/**
* GET /rules/versions — Ritorna solo id+version delle 3 rules attive (check rapido)
*/
router.get('/versions', async (req, res) => {
try {
const versions = {};
for (const [type, tables] of Object.entries(TYPE_MAP)) {
const { rows } = await db.query(
`SELECT id, version FROM ${tables.rules} WHERE active = true AND archived = false LIMIT 1`,
[], 'rules'
);
versions[type] = rows[0] || null;
}
res.json(versions);
} catch (err) {
console.error('Error fetching versions', err);
res.status(500).json({ error: 'internal server error' });
}
});
/**
* GET /rules/active?type=weather — Ritorna la rule attiva completa con items
* Supporta Accept: application/msgpack per formato compatto
*/
router.get('/active', async (req, res) => {
const { type } = req.query;
if (!type || !VALID_TYPES.includes(type)) {
return res.status(400).json({ error: `invalid type, must be one of: ${VALID_TYPES.join(', ')}` });
}
const tables = TYPE_MAP[type];
try {
const { rows: ruleRows } = await db.query(
`SELECT * FROM ${tables.rules} WHERE active = true AND archived = false LIMIT 1`,
[], 'rules'
);
if (ruleRows.length === 0) {
return res.status(404).json({ error: `no active ${type} rule found` });
}
const rule = ruleRows[0];
const { rows: items } = await db.query(
`SELECT * FROM ${tables.items} WHERE rule_id = $1 AND enabled = true`,
[rule.id], 'rules'
);
const payload = {
id: rule.id,
version: rule.version,
description: rule.description,
tags: rule.tags,
items
};
// Se il client accetta msgpack, rispondi in binario
if (req.accepts('application/msgpack')) {
res.set('Content-Type', 'application/msgpack');
return res.send(Buffer.from(encode(payload)));
}
res.json(payload);
} catch (err) {
console.error('Error fetching active rule', err);
res.status(500).json({ error: 'internal server error' });
}
});
/**
* GET /rules/:type/:id — Dettaglio di una rule specifica con items
*/
router.get('/:type/:id', async (req, res) => {
const { type, id } = req.params;
if (!VALID_TYPES.includes(type)) {
return res.status(400).json({ error: 'invalid rule type' });
}
const tables = TYPE_MAP[type];
try {
const { rows: ruleRows } = await db.query(
`SELECT * FROM ${tables.rules} WHERE id = $1`,
[id], 'rules'
);
if (ruleRows.length === 0) {
return res.status(404).json({ error: 'rule not found' });
}
const rule = ruleRows[0];
const { rows: items } = await db.query(
`SELECT * FROM ${tables.items} WHERE rule_id = $1`,
[rule.id], 'rules'
);
res.json({ ...rule, items });
} catch (err) {
console.error('Error fetching rule', err);
res.status(500).json({ error: 'internal server error' });
}
});
/**
* PATCH /rules/update?type=weather&from=1.0.0
* Ritorna le rules con versione > from (per aggiornamenti incrementali)
*/
router.patch('/update', async (req, res) => {
const { type, from } = req.query;
if (!type || !VALID_TYPES.includes(type)) {
return res.status(400).json({ error: 'invalid rule type' });
}
if (!from) {
return res.status(400).json({ error: 'missing from parameter' });
}
const tables = TYPE_MAP[type];
try {
const { rows } = await db.query(
`SELECT * FROM ${tables.rules} WHERE version > $1 AND archived = false ORDER BY version ASC`,
[from], 'rules'
);
if (rows.length === 0) {
return res.status(404).json({ error: 'no rules found with version greater than specified' });
}
// Per ogni rule, allegare gli items
const results = [];
for (const rule of rows) {
const { rows: items } = await db.query(
`SELECT * FROM ${tables.items} WHERE rule_id = $1`,
[rule.id], 'rules'
);
results.push({ ...rule, items });
}
if (req.accepts('application/msgpack')) {
res.set('Content-Type', 'application/msgpack');
return res.send(Buffer.from(encode(results)));
}
res.json(results);
} catch (err) {
console.error('Error fetching rules update', err);
res.status(500).json({ error: 'internal server error' });
}
});
// --- ID Generation ---
const TYPE_PREFIX = { weather: 'w', laterforecasts: 'f', data: 'd', logs: 'l' };
function generateId(type) {
const prefix = TYPE_PREFIX[type] || 'x';
const num = Math.floor(1000 + Math.random() * 9000);
// Pad to 8 chars with trailing zeros
return (prefix + num).padEnd(8, '0');
}
// --- ITEM FIELD DEFINITIONS per tipo ---
const ITEM_FIELDS = {
weather: ['group_name', 'ref', 'name', 'unit', 'enabled'],
laterforecasts: ['group_name', 'ref', 'name', 'unit', 'enabled'],
data: ['category', 'path', 'unit', 'enabled'],
logs: ['path', 'ref', 'unit', 'measurement', 'enabled']
};
// Campi rule aggiornabili
const RULE_UPDATE_FIELDS = {
weather: ['version', 'tags', 'description'],
laterforecasts: ['version', 'tags', 'description'],
data: ['version', 'tags'],
logs: ['version', 'tags', 'description', 'browser_rule_id']
};
/**
* POST /rules/:type — Crea una nuova rule
*/
router.post('/:type', async (req, res) => {
const { type } = req.params;
if (!VALID_TYPES.includes(type)) {
return res.status(400).json({ error: 'invalid rule type' });
}
const tables = TYPE_MAP[type];
const id = generateId(type);
const { version, tags, description, browser_rule_id } = req.body;
if (!version) {
return res.status(400).json({ error: 'version is required' });
}
try {
let sql, params;
if (type === 'weather') {
sql = `INSERT INTO ${tables.rules} (id, version, tags, description) VALUES ($1, $2, $3, $4) RETURNING *`;
params = [id, version, tags || [], description || null];
} else if (type === 'logs') {
sql = `INSERT INTO ${tables.rules} (id, version, tags, description, browser_rule_id) VALUES ($1, $2, $3, $4, $5) RETURNING *`;
params = [id, version, tags || [], description || null, browser_rule_id || null];
} else {
sql = `INSERT INTO ${tables.rules} (id, version, tags) VALUES ($1, $2, $3) RETURNING *`;
params = [id, version, tags || []];
}
const { rows } = await db.query(sql, params, 'rules');
res.status(201).json(rows[0]);
} catch (err) {
console.error('Error creating rule', err);
res.status(500).json({ error: 'internal server error' });
}
});
/**
* PUT /rules/:type/:id — Aggiorna una rule
*/
router.put('/:type/:id', async (req, res) => {
const { type, id } = req.params;
if (!VALID_TYPES.includes(type)) {
return res.status(400).json({ error: 'invalid rule type' });
}
const tables = TYPE_MAP[type];
const allowed = RULE_UPDATE_FIELDS[type];
const sets = [];
const params = [];
let idx = 1;
for (const field of allowed) {
if (req.body[field] !== undefined) {
sets.push(`${field} = $${idx}`);
params.push(req.body[field]);
idx++;
}
}
if (sets.length === 0) {
return res.status(400).json({ error: 'no valid fields to update' });
}
sets.push(`updated_at = NOW()`);
params.push(id);
try {
const sql = `UPDATE ${tables.rules} SET ${sets.join(', ')} WHERE id = $${idx} RETURNING *`;
const { rows } = await db.query(sql, params, 'rules');
if (rows.length === 0) return res.status(404).json({ error: 'rule not found' });
res.json(rows[0]);
} catch (err) {
console.error('Error updating rule', err);
res.status(500).json({ error: 'internal server error' });
}
});
/**
* DELETE /rules/:type/:id — Elimina una rule (cascade items)
*/
router.delete('/:type/:id', async (req, res) => {
const { type, id } = req.params;
if (!VALID_TYPES.includes(type)) {
return res.status(400).json({ error: 'invalid rule type' });
}
const tables = TYPE_MAP[type];
try {
const { rowCount } = await db.query(
`DELETE FROM ${tables.rules} WHERE id = $1`, [id], 'rules'
);
if (rowCount === 0) return res.status(404).json({ error: 'rule not found' });
res.json({ status: 'ok' });
} catch (err) {
console.error('Error deleting rule', err);
res.status(500).json({ error: 'internal server error' });
}
});
/**
* PATCH /rules/:type/:id/active — Toggle active
* Disattiva tutte le altre, poi attiva questa (o disattiva se già attiva)
*/
router.patch('/:type/:id/active', async (req, res) => {
const { type, id } = req.params;
if (!VALID_TYPES.includes(type)) {
return res.status(400).json({ error: 'invalid rule type' });
}
const tables = TYPE_MAP[type];
try {
// Check current state
const { rows: current } = await db.query(
`SELECT active FROM ${tables.rules} WHERE id = $1`, [id], 'rules'
);
if (current.length === 0) return res.status(404).json({ error: 'rule not found' });
const isActive = current[0].active;
if (isActive) {
// Disattiva
await db.query(`UPDATE ${tables.rules} SET active = false, updated_at = NOW() WHERE id = $1`, [id], 'rules');
} else {
// Disattiva tutte, poi attiva questa
await db.query(`UPDATE ${tables.rules} SET active = false, updated_at = NOW() WHERE active = true`, [], 'rules');
await db.query(`UPDATE ${tables.rules} SET active = true, updated_at = NOW() WHERE id = $1`, [id], 'rules');
}
res.json({ status: 'ok', active: !isActive });
} catch (err) {
console.error('Error toggling active', err);
res.status(500).json({ error: 'internal server error' });
}
});
/**
* PATCH /rules/:type/:id/archive — Toggle archived
*/
router.patch('/:type/:id/archive', async (req, res) => {
const { type, id } = req.params;
if (!VALID_TYPES.includes(type)) {
return res.status(400).json({ error: 'invalid rule type' });
}
const tables = TYPE_MAP[type];
try {
const { rows } = await db.query(
`UPDATE ${tables.rules} SET archived = NOT archived, updated_at = NOW() WHERE id = $1 RETURNING archived`,
[id], 'rules'
);
if (rows.length === 0) return res.status(404).json({ error: 'rule not found' });
res.json({ status: 'ok', archived: rows[0].archived });
} catch (err) {
console.error('Error toggling archive', err);
res.status(500).json({ error: 'internal server error' });
}
});
/**
* POST /rules/:type/:id/items — Aggiungi item
*/
router.post('/:type/:id/items', async (req, res) => {
const { type, id } = req.params;
if (!VALID_TYPES.includes(type)) {
return res.status(400).json({ error: 'invalid rule type' });
}
const tables = TYPE_MAP[type];
const fields = ITEM_FIELDS[type].filter(f => f !== 'enabled');
const values = fields.map(f => req.body[f]);
// Validate required fields present
for (let i = 0; i < fields.length; i++) {
if (values[i] === undefined || values[i] === null || values[i] === '') {
return res.status(400).json({ error: `${fields[i]} is required` });
}
}
const allFields = ['rule_id', ...fields];
const allValues = [id, ...values];
const placeholders = allValues.map((_, i) => `$${i + 1}`).join(', ');
try {
const sql = `INSERT INTO ${tables.items} (${allFields.join(', ')}) VALUES (${placeholders}) RETURNING *`;
const { rows } = await db.query(sql, allValues, 'rules');
res.status(201).json(rows[0]);
} catch (err) {
console.error('Error creating item', err);
if (err.code === '23505') {
return res.status(409).json({ error: 'duplicate item (unique constraint)' });
}
res.status(500).json({ error: 'internal server error' });
}
});
/**
* PUT /rules/:type/:id/items/:itemId — Aggiorna item
*/
router.put('/:type/:id/items/:itemId', async (req, res) => {
const { type, id, itemId } = req.params;
if (!VALID_TYPES.includes(type)) {
return res.status(400).json({ error: 'invalid rule type' });
}
const tables = TYPE_MAP[type];
const allowed = ITEM_FIELDS[type];
const sets = [];
const params = [];
let idx = 1;
for (const field of allowed) {
if (req.body[field] !== undefined) {
sets.push(`${field} = $${idx}`);
params.push(req.body[field]);
idx++;
}
}
if (sets.length === 0) {
return res.status(400).json({ error: 'no valid fields to update' });
}
params.push(itemId, id);
try {
const sql = `UPDATE ${tables.items} SET ${sets.join(', ')} WHERE id = $${idx} AND rule_id = $${idx + 1} RETURNING *`;
const { rows } = await db.query(sql, params, 'rules');
if (rows.length === 0) return res.status(404).json({ error: 'item not found' });
res.json(rows[0]);
} catch (err) {
console.error('Error updating item', err);
res.status(500).json({ error: 'internal server error' });
}
});
/**
* DELETE /rules/:type/:id/items/:itemId — Elimina item
*/
router.delete('/:type/:id/items/:itemId', async (req, res) => {
const { type, id, itemId } = req.params;
if (!VALID_TYPES.includes(type)) {
return res.status(400).json({ error: 'invalid rule type' });
}
const tables = TYPE_MAP[type];
try {
const { rowCount } = await db.query(
`DELETE FROM ${tables.items} WHERE id = $1 AND rule_id = $2`, [itemId, id], 'rules'
);
if (rowCount === 0) return res.status(404).json({ error: 'item not found' });
res.json({ status: 'ok' });
} catch (err) {
console.error('Error deleting item', err);
res.status(500).json({ error: 'internal server error' });
}
});
/**
* PATCH /rules/:type/:id/items/:itemId/toggle — Toggle enabled
*/
router.patch('/:type/:id/items/:itemId/toggle', async (req, res) => {
const { type, id, itemId } = req.params;
if (!VALID_TYPES.includes(type)) {
return res.status(400).json({ error: 'invalid rule type' });
}
const tables = TYPE_MAP[type];
try {
const { rows } = await db.query(
`UPDATE ${tables.items} SET enabled = NOT enabled WHERE id = $1 AND rule_id = $2 RETURNING enabled`,
[itemId, id], 'rules'
);
if (rows.length === 0) return res.status(404).json({ error: 'item not found' });
res.json({ status: 'ok', enabled: rows[0].enabled });
} catch (err) {
console.error('Error toggling item', err);
res.status(500).json({ error: 'internal server error' });
}
});
/**
* POST /rules/force-update — Forza l'invio delle rules attive a tutti i sensori connessi.
* Legge le 3 rules attive dal DB, poi chiama il realtime server che le pushta via WebSocket.
*/
router.post('/force-update', async (req, res) => {
try {
const payload = {};
for (const [type, tables] of Object.entries(TYPE_MAP)) {
const { rows: ruleRows } = await db.query(
`SELECT * FROM ${tables.rules} WHERE active = true AND archived = false LIMIT 1`,
[], 'rules'
);
if (ruleRows.length === 0) continue;
const rule = ruleRows[0];
const { rows: items } = await db.query(
`SELECT * FROM ${tables.items} WHERE rule_id = $1 AND enabled = true`,
[rule.id], 'rules'
);
payload[type] = {
id: rule.id,
version: rule.version,
description: rule.description,
tags: rule.tags,
items
};
}
if (Object.keys(payload).length === 0) {
return res.status(404).json({ error: 'no active rules found' });
}
// Invia al realtime server per il push ai sensori connessi
const REALTIME_URL = process.env.REALTIME_INTERNAL_URL || 'http://realtime:3000';
const API_KEY = process.env.INTERNAL_API_KEY;
const rtRes = await fetch(`${REALTIME_URL}/push-rules`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY
},
body: JSON.stringify(payload)
});
if (!rtRes.ok) {
const err = await rtRes.json().catch(() => ({}));
console.error('[RULES] Force-update: realtime error', err);
return res.status(502).json({ error: 'realtime server error', detail: err.error });
}
const result = await rtRes.json();
res.json({ status: 'ok', pushed: Object.keys(payload), sensors: result.sensors || 0 });
} catch (err) {
console.error('Error force-updating rules', err);
res.status(500).json({ error: 'internal server error' });
}
});
module.exports = router;