add graphs for historical device metrics

This commit is contained in:
liamcottle
2024-03-14 02:39:41 +13:00
parent 9bceb14c4a
commit ed89da90b7
2 changed files with 329 additions and 18 deletions

View File

@ -84,6 +84,49 @@ app.get('/api/v1/nodes', async (req, res) => {
} }
}); });
app.get('/api/v1/nodes/:nodeId/device-metrics', async (req, res) => {
try {
const nodeId = parseInt(req.params.nodeId);
const count = req.query.count ? parseInt(req.query.count) : undefined;
// find node
const node = await prisma.node.findFirst({
where: {
node_id: nodeId,
},
});
// make sure node exists
if(!node){
res.status(404).json({
message: "Not Found",
});
}
// get latest device metrics
const deviceMetrics = await prisma.deviceMetric.findMany({
where: {
node_id: node.node_id,
},
orderBy: {
id: 'desc',
},
take: count,
});
res.json({
device_metrics: deviceMetrics,
});
} catch(err) {
console.error(err);
res.status(500).json({
message: "Something went wrong, try again later.",
});
}
});
app.get('/api/v1/stats/hardware-models', async (req, res) => { app.get('/api/v1/stats/hardware-models', async (req, res) => {
try { try {

View File

@ -28,6 +28,9 @@
<script src="https://unpkg.com/vue@3"></script> <script src="https://unpkg.com/vue@3"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<!-- chart js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style> <style>
.icon-online { .icon-online {
@ -368,7 +371,7 @@
<div class="relative flex items-center"> <div class="relative flex items-center">
<div class="block flex-1 px-4 py-2"> <div class="block flex-1 px-4 py-2">
<div class="relative flex min-w-0 flex-1 items-center"> <div class="relative flex min-w-0 flex-1 items-center">
<div class="truncate"> <div class="w-full">
<p class="truncate text-sm font-medium text-gray-900">Battery Level</p> <p class="truncate text-sm font-medium text-gray-900">Battery Level</p>
<p class="truncate text-sm text-gray-700"> <p class="truncate text-sm text-gray-700">
<span v-if="selectedNode.battery_level"> <span v-if="selectedNode.battery_level">
@ -378,6 +381,9 @@
<span v-else class="text-gray-500">???</span> <span v-else class="text-gray-500">???</span>
</p> </p>
</div> </div>
<div>
<canvas id="batteryLevelChart" style="width:150px;height:50px;"></canvas>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -387,28 +393,15 @@
<div class="relative flex items-center"> <div class="relative flex items-center">
<div class="block flex-1 px-4 py-2"> <div class="block flex-1 px-4 py-2">
<div class="relative flex min-w-0 flex-1 items-center"> <div class="relative flex min-w-0 flex-1 items-center">
<div class="truncate"> <div class="w-full">
<p class="truncate text-sm font-medium text-gray-900">Voltage</p> <p class="truncate text-sm font-medium text-gray-900">Voltage</p>
<p class="truncate text-sm text-gray-700"> <p class="truncate text-sm text-gray-700">
<span v-if="selectedNode.voltage">{{ Number(selectedNode.voltage).toFixed(2) }}V</span> <span v-if="selectedNode.voltage">{{ Number(selectedNode.voltage).toFixed(2) }}V</span>
<span v-else class="text-gray-500">???</span> <span v-else class="text-gray-500">???</span>
</p> </p>
</div> </div>
</div> <div>
</div> <canvas id="voltageChart" style="width:150px;height:50px;"></canvas>
</div>
</li>
<li>
<div class="relative flex items-center">
<div class="block flex-1 px-4 py-2">
<div class="relative flex min-w-0 flex-1 items-center">
<div class="truncate">
<p class="truncate text-sm font-medium text-gray-900">Channel Utilization</p>
<p class="truncate text-sm text-gray-700">
<span v-if="selectedNode.channel_utilization">{{ Number(selectedNode.channel_utilization).toFixed(2) }}%</span>
<span v-else class="text-gray-500">???</span>
</p>
</div> </div>
</div> </div>
</div> </div>
@ -419,13 +412,35 @@
<div class="relative flex items-center"> <div class="relative flex items-center">
<div class="block flex-1 px-4 py-2"> <div class="block flex-1 px-4 py-2">
<div class="relative flex min-w-0 flex-1 items-center"> <div class="relative flex min-w-0 flex-1 items-center">
<div class="truncate"> <div class="w-full">
<p class="truncate text-sm font-medium text-gray-900">Channel Utilization</p>
<p class="truncate text-sm text-gray-700">
<span v-if="selectedNode.channel_utilization">{{ Number(selectedNode.channel_utilization).toFixed(2) }}%</span>
<span v-else class="text-gray-500">???</span>
</p>
</div>
<div>
<canvas id="channelUtilizationChart" style="width:150px;height:50px;"></canvas>
</div>
</div>
</div>
</div>
</li>
<li>
<div class="relative flex items-center">
<div class="block flex-1 px-4 py-2">
<div class="relative flex min-w-0 flex-1 items-center">
<div class="w-full">
<p class="truncate text-sm font-medium text-gray-900">Air Util Tx</p> <p class="truncate text-sm font-medium text-gray-900">Air Util Tx</p>
<p class="truncate text-sm text-gray-700"> <p class="truncate text-sm text-gray-700">
<span v-if="selectedNode.air_util_tx">{{ Number(selectedNode.air_util_tx).toFixed(2) }}%</span> <span v-if="selectedNode.air_util_tx">{{ Number(selectedNode.air_util_tx).toFixed(2) }}%</span>
<span v-else class="text-gray-500">???</span> <span v-else class="text-gray-500">???</span>
</p> </p>
</div> </div>
<div>
<canvas id="airUtilTxChart" style="width:150px;height:50px;"></canvas>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -512,6 +527,7 @@
hardwareModelStats: null, hardwareModelStats: null,
selectedNode: null, selectedNode: null,
selectedNodeDeviceMetrics: [],
moment: window.moment, moment: window.moment,
@ -525,6 +541,7 @@
// handle node callback from outside of vue // handle node callback from outside of vue
window._onNodeClick = (node) => { window._onNodeClick = (node) => {
this.selectedNode = node; this.selectedNode = node;
this.loadNodeDeviceMetrics(node.node_id);
}; };
}, },
@ -536,6 +553,257 @@
// do nothing // do nothing
}); });
}, },
loadNodeDeviceMetrics: function(nodeId) {
window.axios.get(`/api/v1/nodes/${nodeId}/device-metrics`, {
params: {
count: 100,
},
}).then((response) => {
this.selectedNodeDeviceMetrics = response.data.device_metrics;
this.renderDeviceMetricCharts();
}).catch(() => {
this.selectedNodeDeviceMetrics = [];
this.renderDeviceMetricCharts();
});
},
renderDeviceMetricCharts: function() {
this.updateBatteryLevelChart();
this.updateVoltageChart();
this.updateChannelUtilizationChart();
this.updateAirUtilTxChart();
},
updateBatteryLevelChart: function() {
// get chart context
const ctx = window.document.getElementById('batteryLevelChart')?.getContext('2d');
if(!ctx){
return;
}
new window.Chart(ctx, {
type: 'line',
data: {
labels: this.selectedNodeDeviceMetrics.map((deviceMetric) => {
return new Date(deviceMetric.created_at).toLocaleTimeString();
}),
datasets: [{
label: 'Battery Level',
data: this.selectedNodeDeviceMetrics.map((deviceMetric) => {
return deviceMetric.battery_level;
}),
borderColor: '#22c55e',
backgroundColor: '#e5e7eb',
fill: true,
}]
},
options: {
responsive: true,
scales: {
x: {
display: false, // Hide x-axis labels
},
y: {
display: false, // Hide y-axis labels
min: 0,
max: 100,
},
},
plugins: {
legend: {
display: false, // Hide the legend
},
tooltip: {
yAlign: 'top',
intersect: false,
displayColors: false,
callbacks: {
label: (item) => item.formattedValue + "%",
},
},
},
elements: {
point: {
radius: 0, // Set the radius to 0 to hide the dots
},
},
}
});
},
updateVoltageChart: function() {
// get chart context
const ctx = window.document.getElementById('voltageChart')?.getContext('2d');
if(!ctx){
return;
}
new window.Chart(ctx, {
type: 'line',
data: {
labels: this.selectedNodeDeviceMetrics.map((deviceMetric) => {
return new Date(deviceMetric.created_at).toLocaleTimeString();
}),
datasets: [{
label: 'Voltage',
data: this.selectedNodeDeviceMetrics.map((deviceMetric) => {
return deviceMetric.voltage;
}),
borderColor: '#22c55e',
backgroundColor: '#e5e7eb',
fill: true,
}]
},
options: {
responsive: true,
scales: {
x: {
display: false, // Hide x-axis labels
},
y: {
display: false, // Hide y-axis labels
min: 0,
max: 5,
},
},
plugins: {
legend: {
display: false, // Hide the legend
},
tooltip: {
yAlign: 'top',
intersect: false,
displayColors: false,
callbacks: {
label: (item) => item.formattedValue + "V",
},
},
},
elements: {
point: {
radius: 0, // Set the radius to 0 to hide the dots
},
},
}
});
},
updateChannelUtilizationChart: function() {
// get chart context
const ctx = window.document.getElementById('channelUtilizationChart')?.getContext('2d');
if(!ctx){
return;
}
new window.Chart(ctx, {
type: 'line',
data: {
labels: this.selectedNodeDeviceMetrics.map((deviceMetric) => {
return new Date(deviceMetric.created_at).toLocaleTimeString();
}),
datasets: [{
label: 'Channel Utilization',
data: this.selectedNodeDeviceMetrics.map((deviceMetric) => {
return deviceMetric.channel_utilization;
}),
borderColor: '#22c55e',
backgroundColor: '#e5e7eb',
fill: true,
}]
},
options: {
responsive: true,
scales: {
x: {
display: false, // Hide x-axis labels
},
y: {
display: false, // Hide y-axis labels
min: -5,
max: 100,
},
},
plugins: {
legend: {
display: false, // Hide the legend
},
tooltip: {
yAlign: 'top',
intersect: false,
displayColors: false,
callbacks: {
label: (item) => item.formattedValue + "%",
},
},
},
elements: {
point: {
radius: 0, // Set the radius to 0 to hide the dots
},
},
}
});
},
updateAirUtilTxChart: function() {
// get chart context
const ctx = window.document.getElementById('airUtilTxChart')?.getContext('2d');
if(!ctx){
return;
}
new window.Chart(ctx, {
type: 'line',
data: {
labels: this.selectedNodeDeviceMetrics.map((deviceMetric) => {
return new Date(deviceMetric.created_at).toLocaleTimeString();
}),
datasets: [{
label: 'Air Util Tx',
data: this.selectedNodeDeviceMetrics.map((deviceMetric) => {
return deviceMetric.air_util_tx;
}),
borderColor: '#22c55e',
backgroundColor: '#e5e7eb',
fill: true,
}]
},
options: {
responsive: true,
scales: {
x: {
display: false, // Hide x-axis labels
},
y: {
display: false, // Hide y-axis labels
min: -5,
max: 100,
},
},
plugins: {
legend: {
display: false, // Hide the legend
},
tooltip: {
yAlign: 'top',
intersect: false,
displayColors: false,
callbacks: {
label: (item) => item.formattedValue + "%",
},
},
},
elements: {
point: {
radius: 0, // Set the radius to 0 to hide the dots
},
},
}
});
},
}, },
}).mount('#app'); }).mount('#app');
</script> </script>