Add initial KioskCore and API endpoint for data analysis

- Created a new CSS file for kiosk styles, defining variables, typography, and layout for cards and toolbars.
- Implemented new routes for data anlaysis page
This commit is contained in:
Giuseppe Raffa
2026-04-18 12:32:32 +02:00
parent ef62bb5da0
commit b6c2a7e904
12 changed files with 2287 additions and 4 deletions

View File

@@ -96,6 +96,9 @@ app.use('/params', paramsRoutes)
const settingsRoutes = require('./routes/settings')
app.use('/settings', settingsRoutes)
const sessionsRoutes = require('./routes/sessions')
app.use('/sessions', sessionsRoutes)
app.listen(PORT, '0.0.0.0', () => {
console.log(`Started on port ${PORT}`);
});

View File

@@ -0,0 +1,88 @@
const router = require('express').Router();
const { query: dbQuery } = require('../storage/postgres');
const { querySessionHistory, exportSessionCSV } = require('../storage/influx');
/**
* GET /sessions/history
* Lista tutte le sessioni di registrazione storiche da PostgreSQL (sessiondataref).
*/
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);
} catch (err) {
console.error('[sessions] history error:', err.message);
res.status(500).json({ error: 'internal server error' });
}
});
/**
* GET /sessions/:sensorId/data?session=sXXXX&from=ISO&to=ISO
* Restituisce i dati storici di una sessione come JSON (righe pivotate da InfluxDB).
*/
router.get('/:sensorId/data', async (req, res) => {
const { sensorId } = req.params;
const { session, from, to } = req.query;
if (!session) return res.status(400).json({ error: 'session param required' });
try {
let since = from || null;
if (!since) {
const result = await dbQuery(
`SELECT created_at FROM sessiondataref WHERE session_id = $1`,
[session],
'sensors'
);
since = result.rows[0]?.created_at?.toISOString() || '-30d';
}
const until = to ? new Date(parseInt(to)).toISOString() : null;
const rows = await querySessionHistory(sensorId, session, since, until);
res.json(rows);
} catch (err) {
console.error('[sessions] data error:', err.message);
res.status(500).json({ error: 'internal server error' });
}
});
/**
* GET /sessions/:sensorId/csv?session=sXXXX&from=ms&to=ms
* Esporta i dati di una sessione come CSV (supporta intervallo opzionale).
*/
router.get('/:sensorId/csv', async (req, res) => {
const { sensorId } = req.params;
const { session, from, to } = req.query;
if (!session) return res.status(400).json({ error: 'session param required' });
try {
let since = from ? new Date(parseInt(from)).toISOString() : null;
if (!since) {
const result = await dbQuery(
`SELECT created_at FROM sessiondataref WHERE session_id = $1`,
[session],
'sensors'
);
since = result.rows[0]?.created_at?.toISOString() || '-30d';
}
const until = to ? new Date(parseInt(to)).toISOString() : null;
const csv = await exportSessionCSV(sensorId, session, since, until);
if (!csv) return res.status(404).json({ error: 'No data found for this session' });
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', `attachment; filename="session_${session}_${sensorId}.csv"`);
res.send(csv);
} catch (err) {
console.error('[sessions] csv error:', err.message);
res.status(500).json({ error: 'CSV export failed' });
}
});
module.exports = router;

View File

@@ -60,6 +60,64 @@ async function query(bucket, relativeTime, measurement, sensor, field) {
}
const sessionBucket = process.env.INFLX_BUCKET || 'logs';
/**
* Query storica per una sessione di registrazione.
* @param {string} sensor - nome sensore
* @param {string} session - session_id (tag InfluxDB)
* @param {string} since - ISO timestamp o duration (es. "-30d")
* @param {string|null} until - ISO timestamp fine (opzionale)
* @returns {Promise<Array<Object>>}
*/
async function querySessionHistory(sensor, session, since, until = null) {
const rangeStr = until ? `start: ${since}, stop: ${until}` : `start: ${since}`;
const fluxQuery = `
from(bucket: "${sessionBucket}")
|> range(${rangeStr})
|> filter(fn: (r) => r._measurement == "logs")
|> filter(fn: (r) => r.sensor == "${sensor}")
|> filter(fn: (r) => r.session == "${session}")
|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
|> sort(columns: ["_time"])
`;
const rows = [];
return new Promise((resolve, reject) => {
querying.queryRows(fluxQuery, {
next(row, tableMeta) { rows.push(tableMeta.toObject(row)); },
error: reject,
complete() { resolve(rows); },
});
});
}
/**
* Esporta i dati di una sessione come stringa CSV.
* @param {string} sensor
* @param {string} session
* @param {string} since
* @param {string|null} until
* @returns {Promise<string>}
*/
async function exportSessionCSV(sensor, session, since, until = null) {
const rows = await querySessionHistory(sensor, session, since, until);
if (rows.length === 0) return '';
const metaKeys = new Set(['result', 'table', '_start', '_stop', '_measurement', 'sensor', 'session', '']);
const fieldNames = new Set();
for (const row of rows) {
for (const key of Object.keys(row)) {
if (!metaKeys.has(key) && key !== '_time') fieldNames.add(key);
}
}
const fields = Array.from(fieldNames).sort();
const header = ['timestamp', ...fields].join(',');
const csvRows = rows.map(row => {
const values = fields.map(f => { const v = row[f]; return (v == null) ? '' : v; });
return [row._time || '', ...values].join(',');
});
return header + '\n' + csvRows.join('\n') + '\n';
}
async function checkInflux() {
try {
const result = await querying.collectRows(
@@ -78,9 +136,11 @@ async function checkInflux() {
}
module.exports = {
write:append,
write: append,
writeBatch,
query,
querySessionHistory,
exportSessionCSV,
checkInflux
}