feat+fix: changed the primary data source for the data analysis and added the relative card in the dashboard page.
This commit is contained in:
@@ -1,19 +1,39 @@
|
|||||||
const router = require('express').Router();
|
const router = require('express').Router();
|
||||||
const { query: dbQuery } = require('../storage/postgres');
|
const { query: dbQuery } = require('../storage/postgres');
|
||||||
const { querySessionHistory, exportSessionCSV } = require('../storage/influx');
|
const { listInfluxSessions, querySessionHistory, exportSessionCSV } = require('../storage/influx');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /sessions/history
|
* GET /sessions/history
|
||||||
* Lista tutte le sessioni di registrazione storiche da PostgreSQL (sessiondataref).
|
* Fonte primaria: InfluxDB (tag sensor + session sul measurement "logs").
|
||||||
|
* Arricchisce con i metadati opzionali da PostgreSQL (sessiondataref): nome, tags, descrizione.
|
||||||
*/
|
*/
|
||||||
router.get('/history', async (req, res) => {
|
router.get('/history', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const sessions = await listInfluxSessions();
|
||||||
|
|
||||||
|
// Arricchisci con dati PostgreSQL (opzionale — può fallire senza bloccare)
|
||||||
|
let pgMap = {};
|
||||||
try {
|
try {
|
||||||
const result = await dbQuery(
|
const result = await dbQuery(
|
||||||
`SELECT * FROM sessiondataref ORDER BY created_at DESC LIMIT 100`,
|
`SELECT * FROM sessiondataref`,
|
||||||
[],
|
[],
|
||||||
'sensors'
|
'sensors'
|
||||||
);
|
);
|
||||||
res.json(result.rows);
|
result.rows.forEach(r => { pgMap[r.session_id] = r; });
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
const enriched = sessions.map(s => ({
|
||||||
|
session_id: s.session,
|
||||||
|
sensor_name: s.sensor,
|
||||||
|
startTime: s.startTime,
|
||||||
|
endTime: s.endTime,
|
||||||
|
// campi opzionali da PostgreSQL
|
||||||
|
name: pgMap[s.session]?.name || null,
|
||||||
|
description: pgMap[s.session]?.description || null,
|
||||||
|
tags: pgMap[s.session]?.tags || [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json(enriched);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[sessions] history error:', err.message);
|
console.error('[sessions] history error:', err.message);
|
||||||
res.status(500).json({ error: 'internal server error' });
|
res.status(500).json({ error: 'internal server error' });
|
||||||
|
|||||||
@@ -118,6 +118,51 @@ async function exportSessionCSV(sensor, session, since, until = null) {
|
|||||||
return header + '\n' + csvRows.join('\n') + '\n';
|
return header + '\n' + csvRows.join('\n') + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility interna: esegue una Flux query e restituisce le righe come array di oggetti.
|
||||||
|
*/
|
||||||
|
function runFlux(fluxQuery) {
|
||||||
|
const rows = [];
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
querying.queryRows(fluxQuery, {
|
||||||
|
next(row, tableMeta) { rows.push(tableMeta.toObject(row)); },
|
||||||
|
error: reject,
|
||||||
|
complete() { resolve(rows); },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elenca tutte le sessioni presenti in InfluxDB, con primo e ultimo timestamp.
|
||||||
|
* Sorgente di verità: tag sensor + session sul measurement "logs".
|
||||||
|
* @param {string} [lookback='-5y'] - range di ricerca (es. '-365d', '-5y')
|
||||||
|
* @returns {Promise<Array<{session, sensor, startTime, endTime}>>}
|
||||||
|
*/
|
||||||
|
async function listInfluxSessions(lookback = '-5y') {
|
||||||
|
const base = `
|
||||||
|
from(bucket: "${sessionBucket}")
|
||||||
|
|> range(start: ${lookback})
|
||||||
|
|> filter(fn: (r) => r._measurement == "logs")
|
||||||
|
|> group(columns: ["sensor", "session"])
|
||||||
|
|> keep(columns: ["_time", "sensor", "session"])
|
||||||
|
`;
|
||||||
|
const [firstRows, lastRows] = await Promise.all([
|
||||||
|
runFlux(base + '|> first()'),
|
||||||
|
runFlux(base + '|> last()'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const map = {};
|
||||||
|
firstRows.forEach(r => {
|
||||||
|
if (!r.session) return;
|
||||||
|
map[r.session] = { session: r.session, sensor: r.sensor, startTime: r._time };
|
||||||
|
});
|
||||||
|
lastRows.forEach(r => {
|
||||||
|
if (map[r.session]) map[r.session].endTime = r._time;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.values(map).sort((a, b) => new Date(b.startTime) - new Date(a.startTime));
|
||||||
|
}
|
||||||
|
|
||||||
async function checkInflux() {
|
async function checkInflux() {
|
||||||
try {
|
try {
|
||||||
const result = await querying.collectRows(
|
const result = await querying.collectRows(
|
||||||
@@ -139,6 +184,7 @@ module.exports = {
|
|||||||
write: append,
|
write: append,
|
||||||
writeBatch,
|
writeBatch,
|
||||||
query,
|
query,
|
||||||
|
listInfluxSessions,
|
||||||
querySessionHistory,
|
querySessionHistory,
|
||||||
exportSessionCSV,
|
exportSessionCSV,
|
||||||
checkInflux
|
checkInflux
|
||||||
|
|||||||
@@ -38,6 +38,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</a> -->
|
</a> -->
|
||||||
|
|
||||||
|
<a class="card" href="/sessions" title="Previsioni">
|
||||||
|
<div>
|
||||||
|
<h3>Analisi Dati</h3>
|
||||||
|
<p>Analizza i dati raccolti sulle performance e sulla navigazione.</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
<a class="card" href="/forecasts" title="Previsioni">
|
<a class="card" href="/forecasts" title="Previsioni">
|
||||||
<div>
|
<div>
|
||||||
<h3>Previsioni</h3>
|
<h3>Previsioni</h3>
|
||||||
|
|||||||
@@ -464,7 +464,7 @@ function nearestIdx(ts) {
|
|||||||
// --- Load sessions list ---
|
// --- Load sessions list ---
|
||||||
async function loadSessionsList() {
|
async function loadSessionsList() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/sessions/history`);
|
const res = await fetch(`${API_URL}/sessions/history`, { credentials: 'include' });
|
||||||
const sessions = await res.json();
|
const sessions = await res.json();
|
||||||
renderSessionGrid(sessions);
|
renderSessionGrid(sessions);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -496,8 +496,8 @@ function filterSessionGrid() {
|
|||||||
if (!filtered.length) { grid.innerHTML = '<div class="sess-empty">Nessuna sessione trovata.</div>'; return; }
|
if (!filtered.length) { grid.innerHTML = '<div class="sess-empty">Nessuna sessione trovata.</div>'; return; }
|
||||||
grid.innerHTML = '';
|
grid.innerHTML = '';
|
||||||
filtered.forEach(s => {
|
filtered.forEach(s => {
|
||||||
const start = s.created_at ? new Date(s.created_at).getTime() : null;
|
const start = s.startTime ? new Date(s.startTime).getTime() : null;
|
||||||
const end = s.disconnected_at ? new Date(s.disconnected_at).getTime() : null;
|
const end = s.endTime ? new Date(s.endTime).getTime() : null;
|
||||||
const dur = start && end ? fmtDuration(end - start) : (start ? 'In corso' : '—');
|
const dur = start && end ? fmtDuration(end - start) : (start ? 'In corso' : '—');
|
||||||
const tags = Array.isArray(s.tags) ? s.tags : [];
|
const tags = Array.isArray(s.tags) ? s.tags : [];
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
@@ -506,8 +506,8 @@ function filterSessionGrid() {
|
|||||||
<div class="sess-card-name">${s.name || s.session_id || '—'}</div>
|
<div class="sess-card-name">${s.name || s.session_id || '—'}</div>
|
||||||
<div class="sess-card-id">${s.session_id || ''}</div>
|
<div class="sess-card-id">${s.session_id || ''}</div>
|
||||||
<div class="sess-card-sensor">${s.sensor_name || '—'}</div>
|
<div class="sess-card-sensor">${s.sensor_name || '—'}</div>
|
||||||
<div class="sess-card-dates">${fmtDate(s.created_at)}</div>
|
<div class="sess-card-dates">${fmtDate(s.startTime)}</div>
|
||||||
${end ? `<div class="sess-card-dates">${fmtDate(s.disconnected_at)}</div>` : ''}
|
${end ? `<div class="sess-card-dates">${fmtDate(s.endTime)}</div>` : ''}
|
||||||
<div class="sess-card-duration">${dur}</div>
|
<div class="sess-card-duration">${dur}</div>
|
||||||
${tags.length ? `<div class="sess-tags">${tags.map(t=>`<span class="sess-tag">${t}</span>`).join('')}</div>` : ''}
|
${tags.length ? `<div class="sess-tags">${tags.map(t=>`<span class="sess-tag">${t}</span>`).join('')}</div>` : ''}
|
||||||
`;
|
`;
|
||||||
@@ -549,10 +549,10 @@ async function loadSessionData(meta) {
|
|||||||
document.getElementById('loadingText').textContent = 'Caricamento dati sessione...';
|
document.getElementById('loadingText').textContent = 'Caricamento dati sessione...';
|
||||||
document.getElementById('loadingOverlay').classList.add('visible');
|
document.getElementById('loadingOverlay').classList.add('visible');
|
||||||
try {
|
try {
|
||||||
const from = meta.created_at ? new Date(meta.created_at).toISOString() : null;
|
const from = meta.startTime ? new Date(meta.startTime).toISOString() : null;
|
||||||
const params = new URLSearchParams({ session: meta.session_id });
|
const params = new URLSearchParams({ session: meta.session_id });
|
||||||
if (from) params.set('from', from);
|
if (from) params.set('from', from);
|
||||||
const res = await fetch(`${API_URL}/sessions/${encodeURIComponent(meta.sensor_name)}/data?${params}`);
|
const res = await fetch(`${API_URL}/sessions/${encodeURIComponent(meta.sensor_name)}/data?${params}`, { credentials: 'include' });
|
||||||
sessionRows = await res.json();
|
sessionRows = await res.json();
|
||||||
|
|
||||||
if (!sessionRows.length) { showToast('Nessun dato trovato per questa sessione'); return; }
|
if (!sessionRows.length) { showToast('Nessun dato trovato per questa sessione'); return; }
|
||||||
@@ -894,7 +894,7 @@ document.getElementById('downloadBtn').onclick = async () => {
|
|||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({ session: currentSessionId, from: String(tStart) });
|
const params = new URLSearchParams({ session: currentSessionId, from: String(tStart) });
|
||||||
if (restrictMode) params.set('to', String(restrictEnd));
|
if (restrictMode) params.set('to', String(restrictEnd));
|
||||||
const res = await fetch(`${API_URL}/sessions/${encodeURIComponent(currentSensorId)}/csv?${params}`);
|
const res = await fetch(`${API_URL}/sessions/${encodeURIComponent(currentSensorId)}/csv?${params}`, { credentials: 'include' });
|
||||||
if (!res.ok) { showToast('Errore durante il download'); return; }
|
if (!res.ok) { showToast('Errore durante il download'); return; }
|
||||||
const blob = await res.blob();
|
const blob = await res.blob();
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
|||||||
Reference in New Issue
Block a user