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:
@@ -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}`);
|
||||
});
|
||||
|
||||
88
api/src/routes/sessions.js
Normal file
88
api/src/routes/sessions.js
Normal 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;
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user