feat: Implement rulesets and layout management for kiosk plugin
- Added rulesets manager to handle various data types and updates via HTTP and WebSocket. - Introduced layout store for managing kiosk layouts with caching and server synchronization. - Enhanced dashboard and data routes to support new layout and ruleset features. - Updated kiosk HTML and JavaScript to utilize new layout rendering and data binding. - Removed obsolete map route and integrated map functionality into the new tile renderer. - Improved Telegram commands to reflect changes in data structure and logging. - Refactored weather fetching intervals to prevent multiple instances. - Added SSE stream for real-time layout updates in the kiosk.
This commit is contained in:
@@ -1,250 +1,158 @@
|
||||
/**
|
||||
* Logger locale + invio realtime.
|
||||
*
|
||||
* Scrive una riga ogni secondo in un CSV locale (1 sessione = 1 file) e
|
||||
* contemporaneamente invia i field via WebSocket al realtime per InfluxDB.
|
||||
*
|
||||
* Path-list dinamica: i path letti ad ogni tick vengono presi dal ruleset
|
||||
* 'logs' (cores/rulesets). Quando il ruleset cambia (push WS o cambio remoto):
|
||||
* - la sessione corrente viene chiusa (CSV finalizzato)
|
||||
* - si manda `session_reset` al server (nuovo session_id su Influx)
|
||||
* - si apre un nuovo CSV con i nuovi path
|
||||
* - lo storico Influx resta coerente: tag `session` cambia, tag
|
||||
* `ruleset_version` riflette la nuova versione applicata.
|
||||
*/
|
||||
const fs = require('fs').promises;
|
||||
const fsSync = require('fs');
|
||||
const pth = require('path');
|
||||
const os = require('os');
|
||||
const skFlow = require('../config/skFlow');
|
||||
const realtime = require('./realtime/core');
|
||||
const rulesets = require('./rulesets');
|
||||
|
||||
const logsDirectory = pth.join(__dirname, '../../data/logs/');
|
||||
|
||||
// Intervallo di scrittura fisso: 1 secondo
|
||||
const WRITE_INTERVAL = 1000;
|
||||
|
||||
// Stato della sessione attiva
|
||||
let session = null;
|
||||
let writeInterval = null;
|
||||
|
||||
// Paths da registrare (impostati da init)
|
||||
let logPaths = [];
|
||||
|
||||
/**
|
||||
* Risolutori speciali per path che non sono direttamente accessibili via skFlow.get().
|
||||
* Per ogni path speciale, definisce una funzione che restituisce il valore.
|
||||
*/
|
||||
const SPECIAL_RESOLVERS = {
|
||||
'navigation.position.latitude': () => {
|
||||
const pos = skFlow.get('navigation.position');
|
||||
return pos?.latitude ?? null;
|
||||
},
|
||||
'navigation.position.longitude': () => {
|
||||
const pos = skFlow.get('navigation.position');
|
||||
return pos?.longitude ?? null;
|
||||
},
|
||||
'system.uptime': () => Math.floor(process.uptime())
|
||||
'navigation.position.latitude': () => skFlow.get('navigation.position')?.latitude ?? null,
|
||||
'navigation.position.longitude': () => skFlow.get('navigation.position')?.longitude ?? null,
|
||||
'system.uptime': () => Math.floor(process.uptime()),
|
||||
};
|
||||
|
||||
function resolveValue(p) {
|
||||
if (SPECIAL_RESOLVERS[p]) return SPECIAL_RESOLVERS[p]();
|
||||
return skFlow.get(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Risolve il valore di un path, gestendo i casi speciali.
|
||||
* @param {String} path - il path da risolvere
|
||||
* @returns {*} il valore
|
||||
* Init: prende i path iniziali dal ruleset attivo e si sottoscrive ai cambi.
|
||||
* @param {Array<String>} [fallback] - opzionale, lista path se non c'e' ruleset
|
||||
*/
|
||||
function resolveValue(path) {
|
||||
// Controlla se c'e un risolutore speciale
|
||||
if (SPECIAL_RESOLVERS[path]) {
|
||||
return SPECIAL_RESOLVERS[path]();
|
||||
function init(fallback) {
|
||||
logPaths = rulesets.getEnabledPaths('logs');
|
||||
if ((!logPaths || logPaths.length === 0) && Array.isArray(fallback)) {
|
||||
logPaths = fallback;
|
||||
}
|
||||
// Path standard: leggi dal databrowser Signal K
|
||||
return skFlow.get(path);
|
||||
console.log(`[LOGS] init: ${logPaths.length} path da ruleset v=${rulesets.versionStr('logs') || '?'}`);
|
||||
|
||||
rulesets.onUpdateOf('logs', async (next) => {
|
||||
const newPaths = (next.content || []).filter(it => it.enabled !== false).map(it => it.path);
|
||||
console.log(`[LOGS] ruleset update: ${newPaths.length} path → restart recording`);
|
||||
const wasRecording = !!session;
|
||||
try {
|
||||
if (wasRecording) await stopRecording();
|
||||
logPaths = newPaths;
|
||||
if (realtime.isConnected()) realtime.sendRaw({ _t: 'session_reset' });
|
||||
if (wasRecording) await startRecording();
|
||||
} catch (err) {
|
||||
console.error('[LOGS] restart on ruleset update failed:', err.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inizializza i path da registrare.
|
||||
* @param {Array<String>} paths - array di path da rules.js LOG_PATHS
|
||||
*/
|
||||
function init(paths) {
|
||||
logPaths = paths;
|
||||
console.log(`[LOGS] Inizializzati ${paths.length} path`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assicura che la cartella logs esista
|
||||
*/
|
||||
async function ensureDir() {
|
||||
try {
|
||||
await fs.mkdir(logsDirectory, { recursive: true });
|
||||
} catch (e) {}
|
||||
try { await fs.mkdir(logsDirectory, { recursive: true }); } catch (e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Avvia la registrazione: crea un nuovo file CSV e inizia a scrivere ogni secondo.
|
||||
* @param {String} name - nome del file (opzionale, default: data/ora corrente)
|
||||
*/
|
||||
async function startRecording(name) {
|
||||
// Se c'e gia una sessione attiva, fermala prima
|
||||
if (session) {
|
||||
await stopRecording();
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
const now = new Date();
|
||||
name = now.toISOString().replace(/[:.]/g, '-');
|
||||
}
|
||||
|
||||
if (logPaths.length === 0) {
|
||||
console.warn('[LOGS] Nessun path configurato, la registrazione non catturera dati');
|
||||
}
|
||||
if (session) await stopRecording();
|
||||
if (!name) name = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
if (logPaths.length === 0) console.warn('[LOGS] nessun path configurato');
|
||||
|
||||
await ensureDir();
|
||||
|
||||
// Header CSV: timestamp + tutti i path
|
||||
const header = ['timestamp', ...logPaths].join(',') + '\n';
|
||||
const filePath = pth.join(logsDirectory, `${name}.csv`);
|
||||
await fs.writeFile(filePath, header);
|
||||
|
||||
session = {
|
||||
name: name,
|
||||
paths: logPaths,
|
||||
startTime: new Date(),
|
||||
elements: 0,
|
||||
filePath: filePath
|
||||
};
|
||||
|
||||
// Scrivi ogni secondo
|
||||
writeInterval = setInterval(() => {
|
||||
writeLog();
|
||||
}, WRITE_INTERVAL);
|
||||
|
||||
console.log(`[LOGS] Registrazione avviata: ${name} (${logPaths.length} colonne, intervallo ${WRITE_INTERVAL}ms)`);
|
||||
session = { name, paths: logPaths.slice(), startTime: new Date(), elements: 0, filePath };
|
||||
writeInterval = setInterval(() => { writeLog(); }, WRITE_INTERVAL);
|
||||
console.log(`[LOGS] started: ${name} (${logPaths.length} cols)`);
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrive una riga nel CSV e invia i dati al server via WebSocket.
|
||||
*/
|
||||
async function writeLog() {
|
||||
if (!session) return;
|
||||
|
||||
try {
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
// Risolvi tutti i valori
|
||||
const fields = {};
|
||||
const csvValues = [];
|
||||
|
||||
for (const path of session.paths) {
|
||||
const val = resolveValue(path);
|
||||
fields[path] = val;
|
||||
|
||||
// Formatta per CSV
|
||||
if (val === null || val === undefined) {
|
||||
csvValues.push('');
|
||||
} else if (typeof val === 'object') {
|
||||
csvValues.push(JSON.stringify(val).replace(/,/g, ';'));
|
||||
} else {
|
||||
csvValues.push(val);
|
||||
}
|
||||
for (const p of session.paths) {
|
||||
const val = resolveValue(p);
|
||||
fields[p] = val;
|
||||
if (val === null || val === undefined) csvValues.push('');
|
||||
else if (typeof val === 'object') csvValues.push(JSON.stringify(val).replace(/,/g, ';'));
|
||||
else csvValues.push(val);
|
||||
}
|
||||
|
||||
// Scrivi riga CSV nel file locale
|
||||
const row = [timestamp, ...csvValues].join(',') + '\n';
|
||||
await fs.appendFile(session.filePath, row);
|
||||
session.elements++;
|
||||
|
||||
// Invia al server via WebSocket (se connesso)
|
||||
if (realtime.isConnected()) {
|
||||
realtime.send([Date.now(), 'logs', fields]);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[LOGS] Errore scrittura:', error.message);
|
||||
if (realtime.isConnected()) realtime.send([Date.now(), 'logs', fields]);
|
||||
} catch (err) {
|
||||
console.error('[LOGS] write error:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interrompe la registrazione e chiude il file.
|
||||
*/
|
||||
async function stopRecording() {
|
||||
if (writeInterval) {
|
||||
clearInterval(writeInterval);
|
||||
writeInterval = null;
|
||||
}
|
||||
|
||||
if (writeInterval) { clearInterval(writeInterval); writeInterval = null; }
|
||||
if (session) {
|
||||
console.log(`[LOGS] Registrazione fermata: ${session.name} (${session.elements} righe)`);
|
||||
console.log(`[LOGS] stopped ${session.name} (${session.elements} righe)`);
|
||||
session = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ottieni i dati del file come stringa CSV.
|
||||
* @param {String} name - il nome del file (senza estensione)
|
||||
* @returns {String|null}
|
||||
*/
|
||||
async function getLog(name) {
|
||||
try {
|
||||
const filePath = pth.join(logsDirectory, `${name}.csv`);
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
return content;
|
||||
} catch (error) {
|
||||
console.error('[LOGS] Errore lettura log:', error.message);
|
||||
return null;
|
||||
}
|
||||
try { return await fs.readFile(pth.join(logsDirectory, `${name}.csv`), 'utf-8'); }
|
||||
catch (err) { console.error('[LOGS] read err:', err.message); return null; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Ottieni il percorso del file CSV.
|
||||
* @param {String} name - il nome del file (senza estensione)
|
||||
* @returns {String|null}
|
||||
*/
|
||||
function getLogFile(name) {
|
||||
const filePath = pth.join(logsDirectory, `${name}.csv`);
|
||||
if (fsSync.existsSync(filePath)) {
|
||||
return filePath;
|
||||
}
|
||||
return null;
|
||||
const p = pth.join(logsDirectory, `${name}.csv`);
|
||||
return fsSync.existsSync(p) ? p : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ottieni la lista di tutti i file di log disponibili.
|
||||
* @returns {Array}
|
||||
*/
|
||||
async function listLogs() {
|
||||
await ensureDir();
|
||||
try {
|
||||
const files = await fs.readdir(logsDirectory);
|
||||
const csvFiles = files.filter(f => f.endsWith('.csv'));
|
||||
|
||||
const result = [];
|
||||
for (const file of csvFiles) {
|
||||
const filePath = pth.join(logsDirectory, file);
|
||||
const stat = await fs.stat(filePath);
|
||||
result.push({
|
||||
const csv = files.filter(f => f.endsWith('.csv'));
|
||||
const out = [];
|
||||
for (const file of csv) {
|
||||
const p = pth.join(logsDirectory, file);
|
||||
const stat = await fs.stat(p);
|
||||
out.push({
|
||||
name: file.replace('.csv', ''),
|
||||
filename: file,
|
||||
size: (stat.size / (1024 * 1024)).toFixed(2),
|
||||
created: stat.birthtime,
|
||||
modified: stat.mtime
|
||||
modified: stat.mtime,
|
||||
});
|
||||
}
|
||||
|
||||
return result.sort((a, b) => b.modified - a.modified);
|
||||
} catch (error) {
|
||||
console.error('[LOGS] Errore lista log:', error.message);
|
||||
return out.sort((a, b) => b.modified - a.modified);
|
||||
} catch (err) {
|
||||
console.error('[LOGS] list err:', err.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ottieni informazioni sulla sessione di registrazione attiva.
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
function getSession() {
|
||||
if (!session) return null;
|
||||
return {
|
||||
name: session.name,
|
||||
paths: session.paths,
|
||||
startTime: session.startTime,
|
||||
elements: session.elements,
|
||||
delay: WRITE_INTERVAL
|
||||
name: session.name, paths: session.paths, startTime: session.startTime,
|
||||
elements: session.elements, delay: WRITE_INTERVAL,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init,
|
||||
startRecording,
|
||||
stopRecording,
|
||||
getLog,
|
||||
getLogFile,
|
||||
getSession,
|
||||
listLogs
|
||||
};
|
||||
module.exports = { init, startRecording, stopRecording, getLog, getLogFile, getSession, listLogs };
|
||||
|
||||
Reference in New Issue
Block a user