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

@@ -1,171 +0,0 @@
-- ============================================================
-- Schema completo per il database "rules"
-- ============================================================
-- ============ DATA / BROWSER ============
CREATE TABLE IF NOT EXISTS dataread (
id CHAR(8) PRIMARY KEY,
version VARCHAR(20) NOT NULL,
tags TEXT[] DEFAULT '{}',
active BOOLEAN NOT NULL DEFAULT false,
archived BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS datareaditems (
id BIGSERIAL PRIMARY KEY,
rule_id CHAR(8) NOT NULL REFERENCES dataread(id) ON DELETE CASCADE,
category VARCHAR(50) NOT NULL,
path VARCHAR(200) NOT NULL,
unit VARCHAR(20),
enabled BOOLEAN NOT NULL DEFAULT true,
UNIQUE(rule_id, path)
);
CREATE INDEX IF NOT EXISTS idx_datareaditems_rule ON datareaditems(rule_id);
-- ============ WEATHER (current, ogni 5 min) ============
CREATE TABLE IF NOT EXISTS weather (
id CHAR(8) PRIMARY KEY,
version VARCHAR(20) NOT NULL,
description TEXT,
tags TEXT[] DEFAULT '{}',
active BOOLEAN NOT NULL DEFAULT false,
archived BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS weatheritems (
id BIGSERIAL PRIMARY KEY,
rule_id CHAR(8) NOT NULL REFERENCES weather(id) ON DELETE CASCADE,
group_name VARCHAR(50) NOT NULL,
ref VARCHAR(50) NOT NULL,
name VARCHAR(50) NOT NULL,
unit VARCHAR(20) NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT true,
UNIQUE(rule_id, ref)
);
CREATE INDEX IF NOT EXISTS idx_weatheritems_rule ON weatheritems(rule_id);
-- ============ LATERFORECASTS (hourly 7gg, ogni 1 ora) ============
CREATE TABLE IF NOT EXISTS laterforecasts (
id CHAR(8) PRIMARY KEY,
version VARCHAR(20) NOT NULL,
description TEXT,
tags TEXT[] DEFAULT '{}',
active BOOLEAN NOT NULL DEFAULT false,
archived BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS laterforecastitems (
id BIGSERIAL PRIMARY KEY,
rule_id CHAR(8) NOT NULL REFERENCES laterforecasts(id) ON DELETE CASCADE,
group_name VARCHAR(50) NOT NULL,
ref VARCHAR(50) NOT NULL,
name VARCHAR(50) NOT NULL,
unit VARCHAR(20) NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT true,
UNIQUE(rule_id, ref)
);
CREATE INDEX IF NOT EXISTS idx_laterforecastitems_rule ON laterforecastitems(rule_id);
-- ============ LOGS ============
CREATE TABLE IF NOT EXISTS logs (
id CHAR(8) PRIMARY KEY,
version VARCHAR(20) NOT NULL,
description TEXT,
tags TEXT[] DEFAULT '{}',
active BOOLEAN NOT NULL DEFAULT false,
archived BOOLEAN NOT NULL DEFAULT false,
browser_rule_id CHAR(8) REFERENCES dataread(id),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS logitems (
id BIGSERIAL PRIMARY KEY,
rule_id CHAR(8) NOT NULL REFERENCES logs(id) ON DELETE CASCADE,
path VARCHAR(200) NOT NULL,
ref VARCHAR(4) NOT NULL,
unit VARCHAR(20) NOT NULL,
measurement VARCHAR(50) NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT true,
UNIQUE(rule_id, ref),
UNIQUE(rule_id, path),
CHECK (LENGTH(ref) <= 4)
);
CREATE INDEX IF NOT EXISTS idx_logitems_rule ON logitems(rule_id);
-- ============ KIOSK ============
CREATE TABLE IF NOT EXISTS kiosktemplates (
id CHAR(8) PRIMARY KEY NOT NULL,
name VARCHAR(50) NOT NULL,
tags TEXT[] DEFAULT '{}',
active BOOLEAN NOT NULL DEFAULT false,
archived BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS kioskelements (
id BIGSERIAL PRIMARY KEY,
template_id CHAR(8) NOT NULL REFERENCES kiosktemplates(id) ON DELETE CASCADE,
font INT NOT NULL,
label VARCHAR(100) NOT NULL,
x INT NOT NULL,
y INT NOT NULL,
width INT NOT NULL,
height INT NOT NULL,
color VARCHAR(20) NOT NULL,
UNIQUE(template_id)
);
CREATE INDEX IF NOT EXISTS idx_kioskelements_template ON kioskelements(template_id);
-- ============ VINCOLI: una sola rule attiva per tipo ============
CREATE UNIQUE INDEX IF NOT EXISTS idx_one_active_dataread
ON dataread(active) WHERE active = true AND archived = false;
CREATE UNIQUE INDEX IF NOT EXISTS idx_one_active_weather
ON weather(active) WHERE active = true AND archived = false;
CREATE UNIQUE INDEX IF NOT EXISTS idx_one_active_laterforecasts
ON laterforecasts(active) WHERE active = true AND archived = false;
CREATE UNIQUE INDEX IF NOT EXISTS idx_one_active_logs
ON logs(active) WHERE active = true AND archived = false;
-- ============ FIX per schema esistente ============
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'datareaditems' AND column_name = 'enables'
) THEN
ALTER TABLE datareaditems RENAME COLUMN enables TO enabled;
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'weather' AND column_name = 'description'
) THEN
ALTER TABLE weather ADD COLUMN description TEXT;
END IF;
END $$;

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;