Migra dal codice salvato in locale al codice condiviso

This commit is contained in:
Giuseppe Raffa
2026-01-06 17:36:58 +01:00
parent 8a88c31c75
commit ff1566d36b
30 changed files with 8985 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
const fs = require("fs");
const path = require("path");
// Coda di scrittura per gestire backpressure
let writeQueue = [];
let isDraining = false;
/**
* Inizializza il dataset e lo prepara per essere salvato.
*
* @param {String[]} headers Un array di stringhe che rappresentano i tipi di dati.
* @param {WriteStream} streamer Lo stream di scrittura del file.
* @returns {boolean} True se l'inizializzazione ha successo
*/
function datasetInit(headers, streamer) {
if (!streamer || streamer.destroyed) {
console.error('[DatasetCore] Stream non valido per inizializzazione');
return false;
}
if (!Array.isArray(headers) || headers.length === 0) {
console.error('[DatasetCore] Headers non validi');
return false;
}
writeQueue = [];
isDraining = false;
streamer.write(headers.join(',') + '\n');
return true;
}
/**
* Aggiunge una riga di dati al dataset con gestione backpressure.
*
* @param {Object} data I dati da scrivere
* @param {String[]} headers Gli header delle colonne
* @param {WriteStream} streamer Lo stream di scrittura
* @returns {boolean} True se la scrittura è andata a buon fine
*/
function appendData(data, headers, streamer) {
if (!streamer || streamer.destroyed) {
console.error('[DatasetCore] Stream non disponibile o distrutto');
return false;
}
if (!data || typeof data !== 'object') {
console.warn('[DatasetCore] Dati non validi, skip scrittura');
return false;
}
// Escape valori che contengono virgole o newline per CSV valido
const escapeCSV = (val) => {
if (val === undefined || val === null) return '';
const str = String(val);
if (str.includes(',') || str.includes('\n') || str.includes('"')) {
return `"${str.replace(/"/g, '""')}"`;
}
return str;
};
const row = headers.map(header => escapeCSV(data[header])).join(',');
// Gestione backpressure con coda
const canWrite = streamer.write(row + '\n');
if (!canWrite) {
if (!isDraining) {
isDraining = true;
console.warn('[DatasetCore] Buffer saturo, attendo drain...');
streamer.once('drain', () => {
isDraining = false;
// Processa coda pendente
while (writeQueue.length > 0 && !streamer.destroyed) {
const pendingRow = writeQueue.shift();
if (!streamer.write(pendingRow + '\n')) {
writeQueue.unshift(pendingRow);
break;
}
}
});
}
// Aggiungi alla coda solo se non troppo piena (max 1000 entries)
if (writeQueue.length < 1000) {
writeQueue.push(row);
} else {
console.error('[DatasetCore] Coda piena, scarto dati');
return false;
}
}
return true;
}
/**
* Ottiene la dimensione della coda di scrittura pendente
* @returns {number} Numero di righe in attesa
*/
function getPendingWrites() {
return writeQueue.length;
}
module.exports = {
datasetInit,
appendData,
getPendingWrites
};

View File

@@ -0,0 +1,274 @@
const fs = require('fs');
const path = require('path');
/**
* Searches for a directory. If not found, creates it.
* @param {string} dirPath - The absolute or relative path to the directory.
* @returns {string} - The absolute path to the directory.
*/
function getDirectory(dirPath) {
const absolutePath = path.resolve(dirPath);
if (!fs.existsSync(absolutePath)) {
fs.mkdirSync(absolutePath, { recursive: true });
}
return absolutePath;
}
/**
* Searches for a file. If not found, creates it with initialData.
* @param {string} filePath - The absolute or relative path to the file.
* @param {object} [initialData={}] - The initial data to write if the file is created.
* @returns {object} - The content of the file as an object.
*/
function write(filePath, initialData = {}) {
const absolutePath = path.resolve(filePath);
const dir = path.dirname(absolutePath);
getDirectory(dir);
if (!fs.existsSync(absolutePath)) {
fs.writeFileSync(absolutePath, JSON.stringify(initialData, null, 2), 'utf-8');
return initialData;
}
try {
const content = fs.readFileSync(absolutePath, 'utf-8');
return JSON.parse(content);
} catch (error) {
console.error(`Error reading or parsing JSON file at ${absolutePath}:`, error);
throw error;
}
}
/**
* Scrive dati in un file JSON.
* @param {string} filePath - Il path assoluto o relativo al file JSON.
* @param {object} data - Gli elementi da aggiungere nel file JSON.
*/
function update(filePath, data) {
const absolutePath = path.resolve(filePath);
const dir = path.dirname(absolutePath);
getDirectory(dir);
fs.writeFileSync(absolutePath, JSON.stringify(data, null, 2), 'utf-8');
}
/**
* Aggiunge un elemento all'array del file specificato
* Se il file non esiste, lo crea con un array contenente l'elemento.
* Se il file esiste ma non è un array, genera un errore.
* @param {string} filePath - Il path del file JSON.
* @param {any} element - L'elemento da aggiungere all'array.
* @returns {array} - L'array aggiornato.
*/
function appendTo(filePath, element) {
const absolutePath = path.resolve(filePath);
let data = [];
if (fs.existsSync(absolutePath)) {
try {
const content = fs.readFileSync(absolutePath, 'utf-8');
data = JSON.parse(content);
} catch (error) {
console.error(`Error reading or parsing JSON file at ${absolutePath}:`, error);
throw error;
}
} else {
// Ensure directory exists if we are creating the file
const dir = path.dirname(absolutePath);
getDirectory(dir);
}
if (!Array.isArray(data)) {
throw new Error(`File at ${absolutePath} exists but is not a JSON array.`);
}
data.push(element);
fs.writeFileSync(absolutePath, JSON.stringify(data, null, 2), 'utf-8');
return data;
}
/**
* Aggiunge un elemento a un array specifico all'interno di un oggetto JSON
* Es: JSON = {date: "now", elements: [], security: false}
* appendToElement(filePath, 'elements', {title: "", description: ""})
*
* @param {string} filePath - Il path del file JSON.
* @param {string} arrayKey - La chiave dell'array nell'oggetto JSON (es: 'elements').
* @param {any} element - L'elemento da aggiungere all'array specificato.
* @returns {boolean} - Se l'operazione è andata a buon fine, restituisce true.
*/
function appendToElement(filePath, arrayKey, element) {
const absolutePath = path.resolve(filePath);
let data = {};
if (fs.existsSync(absolutePath)) {
try {
const content = fs.readFileSync(absolutePath, 'utf-8');
data = JSON.parse(content);
} catch (error) {
console.error(`Error reading or parsing JSON file at ${absolutePath}:`, error);
throw error;
}
} else {
const dir = path.dirname(absolutePath);
getDirectory(dir);
data = {};
}
if (!data.hasOwnProperty(arrayKey)) {
data[arrayKey] = [];
}
if (!Array.isArray(data[arrayKey])) {
throw new Error(`Property '${arrayKey}' in file at ${absolutePath} exists but is not an array.`);
}
data[arrayKey].push(element);
fs.writeFileSync(absolutePath, JSON.stringify(data, null, 2), 'utf-8');
return true
}
/**
* Rimuove un elemento da un array specifico all'interno di un oggetto JSON
* cercando per proprietà "name"
* Es: JSON = {date: "now", elements: [{name: "item1"}, {name: "item2"}], security: false}
* removeFromElement(filePath, 'elements', 'item1')
*
* @param {string} filePath - Il path del file JSON.
* @param {string} arrayKey - La chiave dell'array nell'oggetto JSON (es: 'elements').
* @param {string} nameToRemove - Il valore della proprietà "name" dell'elemento da rimuovere.
* @returns {object} - Oggetto con {success: boolean, removed: object|null, remaining: number}
*/
function removeFromElement(filePath, arrayKey, nameToRemove) {
const absolutePath = path.resolve(filePath);
let data = {};
if (fs.existsSync(absolutePath)) {
try {
const content = fs.readFileSync(absolutePath, 'utf-8');
data = JSON.parse(content);
} catch (error) {
console.error(`Error reading or parsing JSON file at ${absolutePath}:`, error);
throw error;
}
} else {
throw new Error(`File at ${absolutePath} does not exist.`);
}
if (!data.hasOwnProperty(arrayKey)) {
throw new Error(`Property '${arrayKey}' does not exist in file at ${absolutePath}.`);
}
if (!Array.isArray(data[arrayKey])) {
throw new Error(`Property '${arrayKey}' in file at ${absolutePath} is not an array.`);
}
const initialLength = data[arrayKey].length;
const indexToRemove = data[arrayKey].findIndex(item => item.name === nameToRemove);
if (indexToRemove === -1) {
return {
success: false,
removed: null,
remaining: initialLength,
message: `Element with name '${nameToRemove}' not found in array '${arrayKey}'.`
};
}
const removedElement = data[arrayKey].splice(indexToRemove, 1)[0];
fs.writeFileSync(absolutePath, JSON.stringify(data, null, 2), 'utf-8');
return true
}
function findInElement(filePath, arrayKey, name) {
const absolutePath = path.resolve(filePath);
let data = {};
if (fs.existsSync(absolutePath)) {
try {
const content = fs.readFileSync(absolutePath, 'utf-8');
data = JSON.parse(content);
} catch (error) {
console.error(`Error reading or parsing JSON file at ${absolutePath}:`, error);
throw error;
}
} else {
throw new Error(`File at ${absolutePath} does not exist.`);
}
if (!data.hasOwnProperty(arrayKey)) {
throw new Error(`Property '${arrayKey}' does not exist in file at ${absolutePath}.`);
}
if (!Array.isArray(data[arrayKey])) {
throw new Error(`Property '${arrayKey}' in file at ${absolutePath} is not an array.`);
}
const index = data[arrayKey].findIndex(item => item.name === name);
return data[arrayKey][index]
}
/**
* Aggiorna un elemento in un array specifico all'interno di un oggetto JSON
* cercando per proprietà "name" e sostituendolo con un nuovo elemento
* Es: JSON = {date: "now", elements: [{name: "item1", value: 10}, {name: "item2", value: 20}]}
* updateInElement(filePath, 'elements', 'item1', {name: "item1", value: 99})
*
* @param {string} filePath - Il path del file JSON.
* @param {string} arrayKey - La chiave dell'array nell'oggetto JSON (es: 'elements').
* @param {string} nameToUpdate - Il valore della proprietà "name" dell'elemento da aggiornare.
* @param {any} newElement - Il nuovo elemento che sostituirà quello trovato.
* @returns {boolean} - True se l'operazione ha successo, false se l'elemento non è stato trovato.
*/
function updateInElement(filePath, arrayKey, nameToUpdate, newElement) {
const absolutePath = path.resolve(filePath);
let data = {};
if (fs.existsSync(absolutePath)) {
try {
const content = fs.readFileSync(absolutePath, 'utf-8');
data = JSON.parse(content);
} catch (error) {
console.error(`Error reading or parsing JSON file at ${absolutePath}:`, error);
throw error;
}
} else {
throw new Error(`File at ${absolutePath} does not exist.`);
}
if (!data.hasOwnProperty(arrayKey)) {
throw new Error(`Property '${arrayKey}' does not exist in file at ${absolutePath}.`);
}
if (!Array.isArray(data[arrayKey])) {
throw new Error(`Property '${arrayKey}' in file at ${absolutePath} is not an array.`);
}
const index = data[arrayKey].findIndex(item => item.name === nameToUpdate);
if (index === -1) {
return false;
}
data[arrayKey][index] = newElement;
fs.writeFileSync(absolutePath, JSON.stringify(data, null, 2), 'utf-8');
return true;
}
module.exports = {
getDirectory,
write,
update,
appendToElement,
findInElement,
removeFromElement,
updateInElement
};

View File

@@ -0,0 +1,350 @@
const fs = require('fs');
const path = require('path');
const ARCHIVE_FILE = path.join(__dirname, 'hourly_archive.json');
// Cache dati OpenMeteo condivisi (evita chiamate duplicate)
let sharedWeatherData = {
forecast: null,
waves: null,
units: null, // Unità di misura globali
lastUpdate: null,
updateInterval: 2 * 60 * 1000 // 2 minuti
};
// Archivio dati orari
let hourlyArchive = {
temperature: [],
windSpeed: [],
windDirection: [],
waveHeight: [],
wavePeriod: [],
waveDirection: [],
humidity: [],
pressure: []
};
/**
* Carica l'archivio da file
*/
function loadArchive() {
try {
if (fs.existsSync(ARCHIVE_FILE)) {
const data = fs.readFileSync(ARCHIVE_FILE, 'utf8');
const parsed = JSON.parse(data);
// Valida struttura archivio
if (parsed && typeof parsed === 'object') {
hourlyArchive = {
temperature: Array.isArray(parsed.temperature) ? parsed.temperature : [],
windSpeed: Array.isArray(parsed.windSpeed) ? parsed.windSpeed : [],
windDirection: Array.isArray(parsed.windDirection) ? parsed.windDirection : [],
waveHeight: Array.isArray(parsed.waveHeight) ? parsed.waveHeight : [],
wavePeriod: Array.isArray(parsed.wavePeriod) ? parsed.wavePeriod : [],
waveDirection: Array.isArray(parsed.waveDirection) ? parsed.waveDirection : [],
humidity: Array.isArray(parsed.humidity) ? parsed.humidity : [],
pressure: Array.isArray(parsed.pressure) ? parsed.pressure : []
};
console.log('[GraphsCore] Archivio caricato');
}
}
} catch (error) {
console.error('[GraphsCore] Errore caricamento archivio:', error.message);
// Resetta archivio se corrotto
hourlyArchive = {
temperature: [], windSpeed: [], windDirection: [],
waveHeight: [], wavePeriod: [], waveDirection: [],
humidity: [], pressure: []
};
}
}
/**
* Salva l'archivio su file
*/
function saveArchive() {
try {
fs.writeFileSync(ARCHIVE_FILE, JSON.stringify(hourlyArchive, null, 2));
} catch (error) {
console.error('[GraphsCore] Errore salvataggio archivio:', error.message);
}
}
/**
* Aggiorna i dati meteo condivisi
* @param {object} forecastData - Dati forecast da OpenMeteo
* @param {object} wavesData - Dati onde da OpenMeteo
*/
function updateSharedWeatherData(forecastData, wavesData) {
if (forecastData) {
sharedWeatherData.forecast = forecastData;
}
if (wavesData) {
sharedWeatherData.waves = wavesData;
}
// Aggiorna unità se disponibili
if (forecastData?.units || wavesData?.units) {
sharedWeatherData.units = {
forecast: forecastData?.units || sharedWeatherData.units?.forecast || {},
waves: wavesData?.units || sharedWeatherData.units?.waves || {}
};
}
sharedWeatherData.lastUpdate = Date.now();
}
/**
* Ottiene i dati meteo condivisi
* @returns {object} Dati meteo attuali
*/
function getSharedWeatherData() {
return {
forecast: sharedWeatherData.forecast,
waves: sharedWeatherData.waves,
units: sharedWeatherData.units,
lastUpdate: sharedWeatherData.lastUpdate,
isValid: sharedWeatherData.lastUpdate &&
(Date.now() - sharedWeatherData.lastUpdate) < sharedWeatherData.updateInterval * 2
};
}
/**
* Ottiene le unità di misura globali
*/
function getUnits() {
return sharedWeatherData.units || {
forecast: {
temperature: '°C',
humidity: '%',
pressure: 'hPa',
windSpeed: 'km/h',
windDirection: '°'
},
waves: {
waveHeight: 'm',
wavePeriod: 's',
waveDirection: '°'
}
};
}
/**
* Formatta un valore con la sua unità
*/
function formatValue(value, unitKey, category = 'forecast') {
if (value === null || value === undefined) return 'n/d';
const units = getUnits();
const unit = units[category]?.[unitKey] || '';
return `${value}${unit}`;
}
/**
* Verifica se i dati condivisi sono ancora validi
*/
function isWeatherDataValid() {
if (!sharedWeatherData.lastUpdate) return false;
return (Date.now() - sharedWeatherData.lastUpdate) < sharedWeatherData.updateInterval;
}
/**
* Archivia un punto dati orario
*/
function archiveHourlyData(data) {
if (!data || typeof data !== 'object') {
console.warn('[GraphsCore] archiveHourlyData: dati non validi');
return;
}
const timestamp = new Date().toISOString();
const maxPoints = 168; // 7 giorni di dati orari
const addPoint = (arr, value) => {
if (value === null || value === undefined || Number.isNaN(value)) return;
arr.push({ timestamp, value });
if (arr.length > maxPoints) arr.shift();
};
addPoint(hourlyArchive.temperature, data.temperature);
addPoint(hourlyArchive.windSpeed, data.windSpeed);
addPoint(hourlyArchive.windDirection, data.windDirection);
addPoint(hourlyArchive.waveHeight, data.waveHeight);
addPoint(hourlyArchive.wavePeriod, data.wavePeriod);
addPoint(hourlyArchive.waveDirection, data.waveDirection);
addPoint(hourlyArchive.humidity, data.humidity);
addPoint(hourlyArchive.pressure, data.pressure);
saveArchive();
console.log('[GraphsCore] Dati orari archiviati');
}
/**
* Ottiene i dati per un grafico specifico
* @param {string} parameter - temperatura, vento, onde, etc.
* @param {number} hours - ultimi N ore (default 24)
*/
function getGraphData(parameter, hours = 24) {
const paramMap = {
'temperature': hourlyArchive.temperature,
'windSpeed': hourlyArchive.windSpeed,
'windDirection': hourlyArchive.windDirection,
'waveHeight': hourlyArchive.waveHeight,
'wavePeriod': hourlyArchive.wavePeriod,
'waveDirection': hourlyArchive.waveDirection,
'humidity': hourlyArchive.humidity,
'pressure': hourlyArchive.pressure
};
const data = paramMap[parameter] || [];
const cutoff = Date.now() - (hours * 60 * 60 * 1000);
return data.filter(point => new Date(point.timestamp).getTime() > cutoff);
}
/**
* Genera dati formattati per Chart.js
*/
function formatForChart(parameter, hours = 24) {
const data = getGraphData(parameter, hours);
return {
labels: data.map(p => {
const d = new Date(p.timestamp);
return `${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`;
}),
datasets: [{
label: getParameterLabel(parameter),
data: data.map(p => p.value),
borderColor: getParameterColor(parameter),
backgroundColor: getParameterColor(parameter, 0.2),
tension: 0.3,
fill: true
}]
};
}
/**
* Label leggibili per i parametri
*/
function getParameterLabel(param) {
const labels = {
'temperature': 'Temperatura (°C)',
'windSpeed': 'Velocità Vento (km/h)',
'windDirection': 'Direzione Vento (°)',
'waveHeight': 'Altezza Onde (m)',
'wavePeriod': 'Periodo Onde (s)',
'waveDirection': 'Direzione Onde (°)',
'humidity': 'Umidità (%)',
'pressure': 'Pressione (hPa)'
};
return labels[param] || param;
}
/**
* Colori per i grafici
*/
function getParameterColor(param, alpha = 1) {
const colors = {
'temperature': `rgba(255, 99, 132, ${alpha})`,
'windSpeed': `rgba(54, 162, 235, ${alpha})`,
'windDirection': `rgba(75, 192, 192, ${alpha})`,
'waveHeight': `rgba(153, 102, 255, ${alpha})`,
'wavePeriod': `rgba(255, 159, 64, ${alpha})`,
'waveDirection': `rgba(255, 205, 86, ${alpha})`,
'humidity': `rgba(201, 203, 207, ${alpha})`,
'pressure': `rgba(100, 149, 237, ${alpha})`
};
return colors[param] || `rgba(128, 128, 128, ${alpha})`;
}
/**
* Ottiene tutti i dati disponibili per dashboard
*/
function getAllGraphsData(hours = 24) {
return {
temperature: formatForChart('temperature', hours),
windSpeed: formatForChart('windSpeed', hours),
waveHeight: formatForChart('waveHeight', hours),
humidity: formatForChart('humidity', hours)
};
}
/**
* Statistiche sull'archivio
*/
function getArchiveStats() {
return {
temperature: hourlyArchive.temperature.length,
windSpeed: hourlyArchive.windSpeed.length,
waveHeight: hourlyArchive.waveHeight.length,
oldestData: getOldestTimestamp(),
newestData: getNewestTimestamp()
};
}
function getOldestTimestamp() {
const all = [
...hourlyArchive.temperature,
...hourlyArchive.windSpeed,
...hourlyArchive.waveHeight
];
if (all.length === 0) return null;
return all.reduce((oldest, p) =>
new Date(p.timestamp) < new Date(oldest.timestamp) ? p : oldest
).timestamp;
}
function getNewestTimestamp() {
const all = [
...hourlyArchive.temperature,
...hourlyArchive.windSpeed,
...hourlyArchive.waveHeight
];
if (all.length === 0) return null;
return all.reduce((newest, p) =>
new Date(p.timestamp) > new Date(newest.timestamp) ? p : newest
).timestamp;
}
/**
* Pulisce l'archivio
*/
function clearArchive() {
hourlyArchive = {
temperature: [],
windSpeed: [],
windDirection: [],
waveHeight: [],
wavePeriod: [],
waveDirection: [],
humidity: [],
pressure: []
};
saveArchive();
console.log('[GraphsCore] Archivio pulito');
}
// Carica archivio all'avvio
loadArchive();
module.exports = {
// Gestione dati condivisi
updateSharedWeatherData,
getSharedWeatherData,
isWeatherDataValid,
// Unità di misura
getUnits,
formatValue,
// Archivio orario
archiveHourlyData,
getGraphData,
formatForChart,
getAllGraphsData,
getArchiveStats,
clearArchive,
// Utility
getParameterLabel,
getParameterColor
};