2025-03-30 16:43:20 +02:00
|
|
|
const path = require('path');
|
2025-03-30 16:17:26 +02:00
|
|
|
const express = require('express');
|
|
|
|
|
const router = express.Router();
|
2025-03-30 16:43:20 +02:00
|
|
|
const protobufjs = require("protobufjs");
|
2025-03-30 16:17:26 +02:00
|
|
|
|
|
|
|
|
// create prisma db client
|
2025-09-25 20:24:21 +02:00
|
|
|
const { Prisma, PrismaClient } = require("@prisma/client");
|
2025-03-30 16:17:26 +02:00
|
|
|
const prisma = new PrismaClient();
|
|
|
|
|
|
2025-03-30 16:43:20 +02:00
|
|
|
// load protobufs
|
|
|
|
|
const root = new protobufjs.Root();
|
2025-08-03 21:05:33 +02:00
|
|
|
root.resolvePath = (origin, target) => path.join(__dirname, "protobufs", target);
|
2025-03-30 16:43:20 +02:00
|
|
|
root.loadSync('meshtastic/mqtt.proto');
|
|
|
|
|
const HardwareModel = root.lookupEnum("HardwareModel");
|
2025-04-09 10:03:46 +02:00
|
|
|
const PortNum = root.lookupEnum("PortNum");
|
2025-04-07 20:52:59 +02:00
|
|
|
|
|
|
|
|
|
2025-03-30 16:17:26 +02:00
|
|
|
router.get('/hardware-models', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
|
|
// get nodes from db
|
|
|
|
|
const results = await prisma.node.groupBy({
|
|
|
|
|
by: ['hardware_model'],
|
|
|
|
|
orderBy: {
|
|
|
|
|
_count: {
|
|
|
|
|
hardware_model: 'desc',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
_count: {
|
|
|
|
|
hardware_model: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const hardwareModelStats = results.map((result) => {
|
|
|
|
|
return {
|
|
|
|
|
count: result._count.hardware_model,
|
|
|
|
|
hardware_model: result.hardware_model,
|
|
|
|
|
hardware_model_name: HardwareModel.valuesById[result.hardware_model] ?? "UNKNOWN",
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
hardware_model_stats: hardwareModelStats,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
} catch(err) {
|
|
|
|
|
console.error(err);
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
message: "Something went wrong, try again later.",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.get('/messages-per-hour', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const hours = 168;
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const startTime = new Date(now.getTime() - hours * 60 * 60 * 1000);
|
|
|
|
|
|
|
|
|
|
const messages = await prisma.textMessage.findMany({
|
|
|
|
|
where: { created_at: { gte: startTime } },
|
|
|
|
|
select: { packet_id: true, created_at: true },
|
|
|
|
|
distinct: ['packet_id'], // Ensures only unique packet_id entries are counted
|
|
|
|
|
orderBy: { created_at: 'asc' }
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-09 16:13:56 +02:00
|
|
|
// Pre-fill `uniqueCounts` with zeros for all hours, including the current hour
|
2025-03-30 16:17:26 +02:00
|
|
|
const uniqueCounts = Object.fromEntries(
|
|
|
|
|
Array.from({ length: hours }, (_, i) => {
|
2025-08-09 16:13:56 +02:00
|
|
|
const hourTime = new Date(now.getTime() - (hours - 1 - i) * 60 * 60 * 1000);
|
2025-08-09 16:33:49 +02:00
|
|
|
const hourString = hourTime.toISOString().slice(0, 13) + ":00:00.000Z"; // zero out the minutes and seconds
|
2025-03-30 16:17:26 +02:00
|
|
|
return [hourString, 0];
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Populate actual message counts
|
|
|
|
|
messages.forEach(({ created_at }) => {
|
2025-08-09 16:33:49 +02:00
|
|
|
const hourString = created_at.toISOString().slice(0, 13) + ":00:00.000Z"; // zero out the minutes and seconds
|
2025-08-09 16:13:56 +02:00
|
|
|
uniqueCounts[hourString] = (uniqueCounts[hourString] ?? 0) + 1;
|
2025-03-30 16:17:26 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Convert to final result format
|
|
|
|
|
const result = Object.entries(uniqueCounts).map(([hour, count]) => ({ hour, count }));
|
|
|
|
|
|
|
|
|
|
res.json(result);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error fetching messages:', error);
|
|
|
|
|
res.status(500).json({ error: 'Internal Server Error' });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-03-31 21:11:49 +02:00
|
|
|
router.get('/most-active-nodes', async (req, res) => {
|
2025-04-16 18:52:51 +02:00
|
|
|
try {
|
|
|
|
|
const result = await prisma.$queryRaw`
|
|
|
|
|
SELECT n.long_name, COUNT(*) AS count
|
|
|
|
|
FROM (
|
|
|
|
|
SELECT DISTINCT \`from\`, packet_id
|
|
|
|
|
FROM service_envelopes
|
|
|
|
|
WHERE
|
|
|
|
|
created_at >= NOW() - INTERVAL 1 DAY
|
|
|
|
|
AND packet_id IS NOT NULL
|
|
|
|
|
AND portnum != 73
|
2025-04-20 11:38:48 +02:00
|
|
|
AND \`to\` != 1
|
2025-04-16 18:52:51 +02:00
|
|
|
) AS unique_packets
|
|
|
|
|
JOIN nodes n ON unique_packets.from = n.node_id
|
|
|
|
|
GROUP BY n.long_name
|
|
|
|
|
ORDER BY count DESC
|
|
|
|
|
LIMIT 25;
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
res.set('Cache-Control', 'public, max-age=600'); // 10 min cache
|
|
|
|
|
res.json(result);
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error fetching data:', error);
|
|
|
|
|
res.status(500).json({ error: 'Internal Server Error' });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-04-07 20:52:59 +02:00
|
|
|
router.get('/portnum-counts', async (req, res) => {
|
|
|
|
|
const nodeId = req.query.nodeId ? parseInt(req.query.nodeId, 10) : null;
|
|
|
|
|
const hours = 24;
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const startTime = new Date(now.getTime() - hours * 60 * 60 * 1000);
|
|
|
|
|
|
2025-04-16 18:52:51 +02:00
|
|
|
try {
|
|
|
|
|
const envelopes = await prisma.serviceEnvelope.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
created_at: { gte: startTime },
|
|
|
|
|
...(Number.isInteger(nodeId) ? { from: nodeId } : {}),
|
|
|
|
|
packet_id: { not: null },
|
2025-04-20 11:38:48 +02:00
|
|
|
to: { not: 1 }, // Filter out NODENUM_BROADCAST_NO_LORA
|
2025-04-16 19:26:02 +02:00
|
|
|
OR: [
|
|
|
|
|
{ portnum: { not: 73 } }, // Exclude portnum 73 (e.g. map reports)
|
|
|
|
|
{ portnum: null } // But include PKI packages, they have no portnum
|
|
|
|
|
]
|
2025-04-16 18:52:51 +02:00
|
|
|
},
|
|
|
|
|
select: {from: true, packet_id: true, portnum: true, channel_id: true}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Ensure uniqueness based on (from, packet_id)
|
|
|
|
|
const seen = new Set();
|
|
|
|
|
const counts = {};
|
|
|
|
|
|
|
|
|
|
for (const envelope of envelopes) {
|
|
|
|
|
const uniqueKey = `${envelope.from}-${envelope.packet_id}`;
|
|
|
|
|
if (seen.has(uniqueKey)) continue;
|
|
|
|
|
seen.add(uniqueKey);
|
|
|
|
|
|
|
|
|
|
// Override portnum to 512 if channel_id is "PKI"
|
|
|
|
|
const portnum = envelope.channel_id === "PKI" ? 512 : (envelope.portnum ?? 0);
|
|
|
|
|
counts[portnum] = (counts[portnum] || 0) + 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = Object.entries(counts).map(([portnum, count]) => ({
|
|
|
|
|
portnum: parseInt(portnum, 10),
|
|
|
|
|
count: count,
|
|
|
|
|
label: parseInt(portnum, 10) === 512 ? "PKI" : (PortNum.valuesById[portnum] ?? "UNKNOWN"),
|
|
|
|
|
})).sort((a, b) => a.portnum - b.portnum);
|
|
|
|
|
|
|
|
|
|
res.json(result);
|
|
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Error in /portnum-counts:", err);
|
|
|
|
|
res.status(500).json({ message: "Internal server error" });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-04-19 09:32:32 +02:00
|
|
|
router.get('/battery-stats', async (req, res) => {
|
2025-04-19 09:28:49 +02:00
|
|
|
const days = parseInt(req.query.days || '1', 10);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const stats = await prisma.$queryRaw`
|
|
|
|
|
SELECT id, recorded_at, avg_battery_level
|
|
|
|
|
FROM battery_stats
|
2025-04-19 09:34:40 +02:00
|
|
|
WHERE recorded_at >= NOW() - INTERVAL ${days} DAY
|
2025-04-19 09:28:49 +02:00
|
|
|
ORDER BY recorded_at DESC;
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
res.json(stats);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Error fetching battery stats:', err);
|
|
|
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-04-20 22:19:07 +02:00
|
|
|
router.get('/channel-utilization-stats', async (req, res) => {
|
2025-09-25 20:17:42 +02:00
|
|
|
const days = parseInt(req.query.days || '1', 10);
|
|
|
|
|
const channelId = req.query.channel_id; // optional string
|
|
|
|
|
|
2025-04-20 22:19:07 +02:00
|
|
|
try {
|
2025-09-25 20:17:42 +02:00
|
|
|
const stats = await prisma.$queryRaw(
|
|
|
|
|
Prisma.sql`
|
|
|
|
|
SELECT recorded_at, channel_id, avg_channel_utilization
|
|
|
|
|
FROM channel_utilization_stats
|
|
|
|
|
WHERE recorded_at >= NOW() - INTERVAL ${days} DAY
|
|
|
|
|
${channelId ? Prisma.sql`AND channel_id = ${channelId}` : Prisma.sql``}
|
|
|
|
|
ORDER BY recorded_at DESC;
|
|
|
|
|
`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
res.json(stats);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Error fetching channel utilization stats:', err);
|
|
|
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-04-20 22:19:07 +02:00
|
|
|
|
2025-09-25 20:17:42 +02:00
|
|
|
router.get('/channel-utilization', async (req, res) => {
|
|
|
|
|
const channelId = req.query.channel_id;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const snapshot = await prisma.$queryRaw(
|
|
|
|
|
Prisma.sql`
|
|
|
|
|
SELECT recorded_at, channel_id, avg_channel_utilization
|
|
|
|
|
FROM channel_utilization_stats
|
|
|
|
|
WHERE recorded_at = (
|
|
|
|
|
SELECT MAX(recorded_at) FROM channel_utilization_stats
|
|
|
|
|
)
|
|
|
|
|
${channelId ? Prisma.sql`AND channel_id = ${channelId}` : Prisma.sql``}
|
|
|
|
|
ORDER BY channel_id;
|
|
|
|
|
`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
res.json(snapshot);
|
2025-04-20 22:19:07 +02:00
|
|
|
} catch (err) {
|
2025-09-25 20:17:42 +02:00
|
|
|
console.error('Error fetching latest channel utilization:', err);
|
|
|
|
|
res.status(500).json({ error: 'Internal server error' });
|
2025-04-20 22:19:07 +02:00
|
|
|
}
|
2025-09-25 20:17:42 +02:00
|
|
|
});
|
2025-03-31 21:11:49 +02:00
|
|
|
|
2025-03-30 16:17:26 +02:00
|
|
|
module.exports = router;
|