diff --git a/uiserver/webpack.config.js b/uiserver/webpack.config.js index 10901c4..80f8941 100644 --- a/uiserver/webpack.config.js +++ b/uiserver/webpack.config.js @@ -32,8 +32,15 @@ const agentClients = new Map(); // devicePrefix -> Set function validateApiKey(apiKey) { if (!db) return null; try { - const stmt = db.prepare('SELECT * FROM api_keys WHERE key = ? AND enabled = 1'); - return stmt.get(apiKey); + const stmt = db.prepare('SELECT id, name, device_prefix FROM api_keys WHERE key = ?'); + const result = stmt.get(apiKey); + + if (result) { + // Update last_used_at timestamp + db.prepare("UPDATE api_keys SET last_used_at = datetime('now') WHERE id = ?").run(result.id); + } + + return result || null; } catch (err) { console.error('[WS] Error validating API key:', err.message); return null; @@ -43,59 +50,77 @@ function validateApiKey(apiKey) { function insertReadingsSmart(devicePrefix, readings) { if (!db) throw new Error('Database not connected'); - let inserted = 0; - let updated = 0; + const isoTimestamp = new Date().toISOString(); - const insertStmt = db.prepare(` - INSERT INTO sensor_events (timestamp, channel, device, value, data, data_type) - VALUES (?, ?, ?, ?, ?, ?) + const stmtLast = db.prepare(` + SELECT id, value, data, data_type + FROM sensor_events + WHERE device = ? AND channel = ? + ORDER BY timestamp DESC + LIMIT 1 `); - const updateUntilStmt = db.prepare(` + const stmtUpdate = db.prepare(` UPDATE sensor_events SET until = ? WHERE id = ? `); - const getLastStmt = db.prepare(` - SELECT id, value, data FROM sensor_events - WHERE device = ? AND channel = ? - ORDER BY timestamp DESC LIMIT 1 + const stmtInsert = db.prepare(` + INSERT INTO sensor_events (timestamp, until, device, channel, value, data, data_type) + VALUES (?, NULL, ?, ?, ?, ?, ?) `); - const now = new Date().toISOString(); + const transaction = db.transaction((items) => { + let inserted = 0; + let updated = 0; - for (const reading of readings) { - const device = `${devicePrefix}${reading.device}`; - const channel = reading.channel; - const value = reading.value ?? null; - const data = reading.data ? JSON.stringify(reading.data) : null; - const dataType = value !== null ? 'number' : 'json'; + for (const reading of items) { + const fullDevice = `${devicePrefix}${reading.device}`; + const channel = reading.channel; - // Check last value for RLE - const last = getLastStmt.get(device, channel); + // Determine type and values + let dataType = 'number'; + let value = null; + let data = null; - if (last) { - const lastValue = last.value; - const lastData = last.data; - - // If same value, just update 'until' - if (value !== null && lastValue === value) { - updateUntilStmt.run(now, last.id); - updated++; - continue; + if (reading.value !== undefined && reading.value !== null) { + dataType = 'number'; + value = reading.value; + } else if (reading.data !== undefined) { + dataType = 'json'; + data = typeof reading.data === 'string' ? reading.data : JSON.stringify(reading.data); + } else { + continue; // Skip invalid } - if (data !== null && lastData === data) { - updateUntilStmt.run(now, last.id); + + // Check last reading for RLE + const last = stmtLast.get(fullDevice, channel); + let isDuplicate = false; + + if (last && last.data_type === dataType) { + if (dataType === 'number') { + if (Math.abs(last.value - value) < Number.EPSILON) { + isDuplicate = true; + } + } else { + // Compare JSON strings + if (last.data === data) { + isDuplicate = true; + } + } + } + + if (isDuplicate) { + stmtUpdate.run(isoTimestamp, last.id); updated++; - continue; + } else { + stmtInsert.run(isoTimestamp, fullDevice, channel, value, data, dataType); + inserted++; } } + return { inserted, updated }; + }); - // Insert new reading - insertStmt.run(now, channel, device, value, data, dataType); - inserted++; - } - - return { inserted, updated }; + return transaction(readings); } function createAgentWebSocketServer() { @@ -108,9 +133,17 @@ function createAgentWebSocketServer() { const clientState = { authenticated: false, devicePrefix: null, - name: null + name: null, + lastPong: Date.now() }; + // Set up ping/pong for keepalive + ws.isAlive = true; + ws.on('pong', () => { + ws.isAlive = true; + clientState.lastPong = Date.now(); + }); + ws.on('message', (data) => { try { const message = JSON.parse(data.toString()); @@ -133,6 +166,22 @@ function createAgentWebSocketServer() { }); }); + // Ping interval to detect dead connections + const pingInterval = setInterval(() => { + wss.clients.forEach((ws) => { + if (ws.isAlive === false) { + console.log('[WS] Terminating unresponsive client'); + return ws.terminate(); + } + ws.isAlive = false; + ws.ping(); + }); + }, 30000); + + wss.on('close', () => { + clearInterval(pingInterval); + }); + console.log(`[WS] WebSocket server listening on port ${WS_PORT}`); return wss; }