Files
signalk-plugin/plugin/cores/logs.local.js
Giuseppe Raffa c2c1598226 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.
2026-05-12 10:17:54 +02:00

159 lines
5.7 KiB
JavaScript

/**
* 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 skFlow = require('../config/skFlow');
const realtime = require('./realtime/core');
const rulesets = require('./rulesets');
const logsDirectory = pth.join(__dirname, '../../data/logs/');
const WRITE_INTERVAL = 1000;
let session = null;
let writeInterval = null;
let logPaths = [];
const SPECIAL_RESOLVERS = {
'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);
}
/**
* 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 init(fallback) {
logPaths = rulesets.getEnabledPaths('logs');
if ((!logPaths || logPaths.length === 0) && Array.isArray(fallback)) {
logPaths = fallback;
}
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);
}
});
}
async function ensureDir() {
try { await fs.mkdir(logsDirectory, { recursive: true }); } catch (e) {}
}
async function startRecording(name) {
if (session) await stopRecording();
if (!name) name = new Date().toISOString().replace(/[:.]/g, '-');
if (logPaths.length === 0) console.warn('[LOGS] nessun path configurato');
await ensureDir();
const header = ['timestamp', ...logPaths].join(',') + '\n';
const filePath = pth.join(logsDirectory, `${name}.csv`);
await fs.writeFile(filePath, header);
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;
}
async function writeLog() {
if (!session) return;
try {
const timestamp = new Date().toISOString();
const fields = {};
const csvValues = [];
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);
}
const row = [timestamp, ...csvValues].join(',') + '\n';
await fs.appendFile(session.filePath, row);
session.elements++;
if (realtime.isConnected()) realtime.send([Date.now(), 'logs', fields]);
} catch (err) {
console.error('[LOGS] write error:', err.message);
}
}
async function stopRecording() {
if (writeInterval) { clearInterval(writeInterval); writeInterval = null; }
if (session) {
console.log(`[LOGS] stopped ${session.name} (${session.elements} righe)`);
session = null;
}
}
async function getLog(name) {
try { return await fs.readFile(pth.join(logsDirectory, `${name}.csv`), 'utf-8'); }
catch (err) { console.error('[LOGS] read err:', err.message); return null; }
}
function getLogFile(name) {
const p = pth.join(logsDirectory, `${name}.csv`);
return fsSync.existsSync(p) ? p : null;
}
async function listLogs() {
await ensureDir();
try {
const files = await fs.readdir(logsDirectory);
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,
});
}
return out.sort((a, b) => b.modified - a.modified);
} catch (err) {
console.error('[LOGS] list err:', err.message);
return [];
}
}
function getSession() {
if (!session) return null;
return {
name: session.name, paths: session.paths, startTime: session.startTime,
elements: session.elements, delay: WRITE_INTERVAL,
};
}
module.exports = { init, startRecording, stopRecording, getLog, getLogFile, getSession, listLogs };