diff --git a/api/src/routes/sessions.js b/api/src/routes/sessions.js index 125e646..4f23c71 100644 --- a/api/src/routes/sessions.js +++ b/api/src/routes/sessions.js @@ -1,19 +1,39 @@ const router = require('express').Router(); const { query: dbQuery } = require('../storage/postgres'); -const { querySessionHistory, exportSessionCSV } = require('../storage/influx'); +const { listInfluxSessions, querySessionHistory, exportSessionCSV } = require('../storage/influx'); /** * 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) => { try { - const result = await dbQuery( - `SELECT * FROM sessiondataref ORDER BY created_at DESC LIMIT 100`, - [], - 'sensors' - ); - res.json(result.rows); + const sessions = await listInfluxSessions(); + + // Arricchisci con dati PostgreSQL (opzionale — può fallire senza bloccare) + let pgMap = {}; + try { + const result = await dbQuery( + `SELECT * FROM sessiondataref`, + [], + 'sensors' + ); + 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) { console.error('[sessions] history error:', err.message); res.status(500).json({ error: 'internal server error' }); diff --git a/api/src/storage/influx.js b/api/src/storage/influx.js index 0944bdd..a27709e 100644 --- a/api/src/storage/influx.js +++ b/api/src/storage/influx.js @@ -118,6 +118,51 @@ async function exportSessionCSV(sensor, session, since, until = null) { 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>} + */ +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() { try { const result = await querying.collectRows( @@ -139,6 +184,7 @@ module.exports = { write: append, writeBatch, query, + listInfluxSessions, querySessionHistory, exportSessionCSV, checkInflux diff --git a/console/src/pages/dashboard.html b/console/src/pages/dashboard.html index 4b88301..523df8a 100644 --- a/console/src/pages/dashboard.html +++ b/console/src/pages/dashboard.html @@ -38,6 +38,13 @@ --> + +
+

Analisi Dati

+

Analizza i dati raccolti sulle performance e sulla navigazione.

+
+
+

Previsioni

diff --git a/console/src/pages/sessions.html b/console/src/pages/sessions.html index 7265e1e..0992c78 100644 --- a/console/src/pages/sessions.html +++ b/console/src/pages/sessions.html @@ -464,7 +464,7 @@ function nearestIdx(ts) { // --- Load sessions list --- async function loadSessionsList() { try { - const res = await fetch(`${API_URL}/sessions/history`); + const res = await fetch(`${API_URL}/sessions/history`, { credentials: 'include' }); const sessions = await res.json(); renderSessionGrid(sessions); } catch (err) { @@ -496,8 +496,8 @@ function filterSessionGrid() { if (!filtered.length) { grid.innerHTML = '
Nessuna sessione trovata.
'; return; } grid.innerHTML = ''; filtered.forEach(s => { - const start = s.created_at ? new Date(s.created_at).getTime() : null; - const end = s.disconnected_at ? new Date(s.disconnected_at).getTime() : null; + const start = s.startTime ? new Date(s.startTime).getTime() : null; + const end = s.endTime ? new Date(s.endTime).getTime() : null; const dur = start && end ? fmtDuration(end - start) : (start ? 'In corso' : '—'); const tags = Array.isArray(s.tags) ? s.tags : []; const card = document.createElement('div'); @@ -506,8 +506,8 @@ function filterSessionGrid() {
${s.name || s.session_id || '—'}
${s.session_id || ''}
${s.sensor_name || '—'}
-
${fmtDate(s.created_at)}
- ${end ? `
${fmtDate(s.disconnected_at)}
` : ''} +
${fmtDate(s.startTime)}
+ ${end ? `
${fmtDate(s.endTime)}
` : ''}
${dur}
${tags.length ? `
${tags.map(t=>`${t}`).join('')}
` : ''} `; @@ -549,10 +549,10 @@ async function loadSessionData(meta) { document.getElementById('loadingText').textContent = 'Caricamento dati sessione...'; document.getElementById('loadingOverlay').classList.add('visible'); 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 }); 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(); if (!sessionRows.length) { showToast('Nessun dato trovato per questa sessione'); return; } @@ -894,7 +894,7 @@ document.getElementById('downloadBtn').onclick = async () => { try { const params = new URLSearchParams({ session: currentSessionId, from: String(tStart) }); 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; } const blob = await res.blob(); const url = URL.createObjectURL(blob);