Initial commit
This commit is contained in:
102
dist/shared/store/heartbeatStore.js
vendored
Normal file
102
dist/shared/store/heartbeatStore.js
vendored
Normal file
@ -0,0 +1,102 @@
|
||||
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";
|
||||
}
|
||||
Reference in New Issue
Block a user