- Implemented HTML pages for datasets, models, training, testing, and results. - Created API endpoints for managing repositories, results, tests, and training sessions. - Added functionality for streaming training progress via Server-Sent Events (SSE). - Introduced a Dockerfile for the ML runner with necessary dependencies. - Developed an SDK for user code execution within the runner container. - Enhanced CSS styles for improved UI layout and navigation. - Established a layout template for consistent HTML structure across pages. - Added JavaScript for dynamic interactions on the models page. - Implemented WebSocket handling for real-time communication with kiosk devices and controllers. - Implemented model registration and management API at /api/models - Added Gitea proxy API for repository interactions at /api/repos - Created results API for listing and comparing training results at /api/results - Developed training management API for enqueueing and retrieving training jobs at /api/trainings - Introduced SSE endpoint for live training progress updates - Added HTML pages for models, datasets, and training management - Created a Dockerfile for the ML runner with necessary dependencies - Developed SDK for user code execution within the runner container - Enhanced CSS styles for improved UI/UX - Implemented WebSocket communication for real-time device and controller interactions in the kiosk system
194 lines
6.2 KiB
JavaScript
194 lines
6.2 KiB
JavaScript
const { InfluxDB, Point } = require('@influxdata/influxdb-client');
|
|
|
|
const url = process.env.INFLX_URL;
|
|
const token = process.env.INFLX_TOKEN;
|
|
const org = process.env.INFLX_ORG;
|
|
|
|
const boatTelemetry = "boat"
|
|
|
|
const client = new InfluxDB({ url, token })
|
|
const write = client.getWriteApi(org, boatTelemetry);
|
|
const querying = client.getQueryApi(org);
|
|
|
|
console.log("InfluxDB client initialized with config:", { url, org, token });
|
|
|
|
async function append(measurement, sensor, data) {
|
|
const point = new Point(measurement)
|
|
.tag("sensor", sensor)
|
|
.floatField('temperature', data.temperature)
|
|
.floatField('humidity', data.humidity)
|
|
|
|
write.writePoint(point);
|
|
await write.flush();
|
|
|
|
}
|
|
|
|
async function writeBatch(datas) {
|
|
datas.forEach(data => {
|
|
append(data.measurement, data.sensor, data.data);
|
|
})
|
|
}
|
|
|
|
async function query(bucket, relativeTime, measurement, sensor, field) {
|
|
|
|
const fluxTimeRange = relativeTime || "-1h";
|
|
let fluxQuery = `
|
|
from(bucket: "${bucket}")
|
|
|> range(start: ${fluxTimeRange})
|
|
|> filter(fn: (r) => r._measurement == "${measurement}")`;
|
|
|
|
if (sensor) {
|
|
fluxQuery += `\n |> filter(fn: (r) => r.sensor == "${sensor}")`;
|
|
}
|
|
|
|
if (field) {
|
|
fluxQuery += `\n |> filter(fn: (r) => r._field == "${field}")`;
|
|
}
|
|
|
|
fluxQuery += `\n |> yield(name: "data")`;
|
|
|
|
try {
|
|
const data = [];
|
|
for await (const { values, tableMeta } of querying.iterateRows(fluxQuery)) {
|
|
data.push(tableMeta.toObject(values));
|
|
}
|
|
return data;
|
|
} catch (error) {
|
|
console.error("Error in query:", error);
|
|
return [];
|
|
}
|
|
|
|
}
|
|
|
|
// Sorgente di verità per i logs di sessione: stesso bucket usato da
|
|
// realtime/store/influx.js. Sovrascrivibile via env per ambiente.
|
|
const sessionBucket = process.env.INFLX_BUCKET_LOGS || 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';
|
|
}
|
|
|
|
/**
|
|
* 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"])
|
|
`;
|
|
const [firstRows, lastRows] = await Promise.all([
|
|
runFlux(base + '|> first() |> keep(columns: ["_time", "sensor", "session"])'),
|
|
runFlux(base + '|> last() |> keep(columns: ["_time", "sensor", "session"])'),
|
|
]);
|
|
|
|
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(
|
|
`from(bucket: "boat") |> range(start: -1s) |> limit(n:1)`
|
|
);
|
|
console.log('InfluxDB: OK');
|
|
return { ok: true };
|
|
} catch (error) {
|
|
console.error('InfluxDB check failed:', {
|
|
message: error.message,
|
|
statusCode: error.statusCode ?? 'N/A',
|
|
body: error.body ?? 'N/A'
|
|
});
|
|
return false
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
write: append,
|
|
writeBatch,
|
|
query,
|
|
listInfluxSessions,
|
|
querySessionHistory,
|
|
exportSessionCSV,
|
|
checkInflux
|
|
}
|
|
|