import { randomUUID } from "node:crypto"; import { getPool } from "../db/pool"; function nowIso() { return new Date().toISOString(); } function mapHeartbeat(row) { return { id: row.id, device_id: row.device_id, timestamp: row.timestamp, received_at: row.received_at, firmware_version: row.firmware_version || undefined, network_strength: row.network_strength, battery_level: row.battery_level, state: row.state || undefined }; } export async function createDeviceHeartbeat(payload) { const now = nowIso(); const id = randomUUID(); const { rows } = await getPool().query(`INSERT INTO device_heartbeats ( id, device_id, timestamp, received_at, firmware_version, network_strength, battery_level, state, payload_json ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING *`, [ id, payload.device_id, payload.timestamp, now, payload.firmware_version, payload.network_strength, payload.battery_level, payload.state, payload.payload_json || {} ]); return mapHeartbeat(rows[0]); } export async function getLatestHeartbeatByDeviceId(deviceId) { const { rows } = await getPool().query(`SELECT * FROM device_heartbeats WHERE device_id = $1 ORDER BY received_at DESC LIMIT 1`, [deviceId]); return rows[0] ? mapHeartbeat(rows[0]) : null; } export async function getHeartbeatCountForDeviceLastHours(deviceId, hours = 24) { const { rows } = await getPool().query(`SELECT COUNT(*)::INT AS cnt FROM device_heartbeats WHERE device_id = $1 AND (NOW() AT TIME ZONE 'utc' - timestamp) <= ($2 || ' hours')::interval`, [deviceId, hours]); return Number(rows[0]?.cnt || 0); } export async function listHeartbeats(filter) { const clauses = []; const params = []; let i = 1; if (filter?.device_id) { clauses.push(`device_id = $${i++}`); params.push(filter.device_id); } if (filter?.state) { clauses.push(`state = $${i++}`); params.push(filter.state); } if (filter?.from) { clauses.push(`timestamp >= $${i++}`); params.push(filter.from); } if (filter?.to) { clauses.push(`timestamp <= $${i++}`); params.push(filter.to); } const whereSql = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : ""; const limitSql = filter?.limit ? `LIMIT ${Number(filter.limit)}` : ""; const { rows } = await getPool().query(`SELECT * FROM device_heartbeats ${whereSql} ORDER BY received_at DESC ${limitSql}`, params); return rows.map(mapHeartbeat); } export function deriveDeviceStatus(input) { const now = Date.now(); const lastSeen = Date.parse(input?.last_seen_at || ""); if (!Number.isFinite(lastSeen)) { return "offline"; } const ageSeconds = (now - lastSeen) / 1000; if (ageSeconds > 900) { return "offline"; } if (ageSeconds > 90) { return "stale"; } if ((typeof input?.network_strength === "number" && input.network_strength < 40) || (typeof input?.battery_level === "number" && input.battery_level < 20)) { return "degraded"; } return "online"; }