Compare commits

..

1 commit

Author SHA1 Message Date
dependabot[bot]
ff8ee2e01d
Bump the npm_and_yarn group across 1 directory with 2 updates
Bumps the npm_and_yarn group with 2 updates in the / directory: [glob](https://github.com/isaacs/node-glob) and [qs](https://github.com/ljharb/qs).


Updates `glob` from 10.4.5 to 10.5.0
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v10.4.5...v10.5.0)

Updates `qs` from 6.14.0 to 6.14.2
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.14.0...v6.14.2)

---
updated-dependencies:
- dependency-name: glob
  dependency-version: 10.5.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: qs
  dependency-version: 6.14.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-14 10:24:53 +00:00
17 changed files with 3108 additions and 2998 deletions

View file

@ -65,11 +65,6 @@ git clone https://github.com/liamcottle/meshtastic-map
cd meshtastic-map
```
Install Meshtastic protobufs definitions
```
git clone https://github.com/meshtastic/protobufs src/protobufs
```
Install NodeJS dependencies
```

16
package-lock.json generated
View file

@ -69,7 +69,6 @@
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@ -2010,7 +2009,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001737",
"electron-to-chromium": "^1.5.211",
@ -3306,9 +3304,10 @@
}
},
"node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"dev": true,
"license": "ISC",
"dependencies": {
@ -5150,7 +5149,6 @@
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@prisma/config": "6.16.2",
"@prisma/engines": "6.16.2"
@ -5239,9 +5237,9 @@
"license": "MIT"
},
"node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"version": "6.14.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 MiB

After

Width:  |  Height:  |  Size: 270 KiB

Before After
Before After

View file

@ -1,115 +0,0 @@
/* used to prevent ui flicker before vuejs loads */
[v-cloak] {
display: none;
}
.icon-longfast {
background-color: #009016;
border-radius: 25px;
border: 1px solid #2C2D3C;
}
.icon-mediumfast {
background-color: #326be7;
border-radius: 25px;
border: 1px solid #2C2D3C;
}
.icon-shortslow {
background-color: #0077e6;
border-radius: 25px;
border: 1px solid #2C2D3C;
}
.icon-mqtt-connected {
background-color: #2563eb; /* Change to use same color as disconnected // #16a34a; */
border-radius: 25px;
border: 1px solid #2C2D3C;
}
.icon-mqtt-disconnected {
background-color: #2563eb;
border-radius: 25px;
border: 1px solid #2C2D3C;
}
.icon-offline {
background-color: #e2286c;
border-radius: 25px;
border: 1px solid #2C2D3C;
}
.icon-position-history {
background-color: #a855f7;
border-radius: 25px;
border: 1px solid #2C2D3C;
}
.icon-traceroute-start {
background-color: #16a34a; /* green */
border-radius: 25px;
border: 1px solid #2C2D3C;
}
.icon-traceroute-end {
background-color: #dc2626; /* red */
border-radius: 25px;
border: 1px solid #2C2D3C;
}
.waypoint-label {
font-size: 26px;
background-color: transparent;
}
.link {
color: #2563eb;
}
.link:hover {
text-decoration: underline;
}
.tooltip {
position: relative;
display: inline-block;
}
.tooltip .tooltip-text {
visibility: hidden;
width: 80px;
background-color: black;
color: #fff;
text-align: center;
padding: 4px 0;
border-radius: 6px;
position: absolute;
z-index: 10000;
top: 100%;
left: 50%;
margin-top: 8px;
margin-left: -40px; /* Use half of the width (120/2 = 60), to center the tooltip */
}
.tooltip .tooltip-text::after {
content: " ";
position: absolute;
bottom: 100%; /* At the top of the tooltip */
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: transparent transparent black transparent;
}
.tooltip:hover .tooltip-text {
visibility: visible;
}
.z-search {
z-index: 1001;
}
.z-sidebar {
z-index: 1002;
}

View file

@ -1,956 +0,0 @@
Vue.createApp({
data() {
return {
isShowingAnnouncement: this.shouldShowAnnouncement(),
configNodesMaxAgeInSeconds: window.getConfigNodesMaxAgeInSeconds(),
configNodesOfflineAgeInSeconds: window.getConfigNodesOfflineAgeInSeconds(),
configWaypointsMaxAgeInSeconds: window.getConfigWaypointsMaxAgeInSeconds(),
configConnectionsMaxDistanceInMeters: window.getConfigConnectionsMaxDistanceInMeters(),
configZoomLevelGoToNode: window.getConfigZoomLevelGoToNode(),
configAutoUpdatePositionInUrl: window.getConfigAutoUpdatePositionInUrl(),
configEnableMapAnimations: window.getConfigEnableMapAnimations(),
configTemperatureFormat: window.getConfigTemperatureFormat(),
configConnectionsTimePeriodInSeconds: window.getConfigConnectionsTimePeriodInSeconds(),
configConnectionsColoredLines: window.getConfigConnectionsColoredLines(),
configConnectionsBidirectionalOnly: window.getConfigConnectionsBidirectionalOnly(),
configConnectionsMinSnrDb: window.getConfigConnectionsMinSnrDb(),
configConnectionsBidirectionalMinSnr: window.getConfigConnectionsBidirectionalMinSnr(),
isShowingHardwareModels: false,
hardwareModelStats: null,
isShowingInfoModal: this.shouldShowInfoModal(),
isShowingMobileSearch: false,
isShowingSettings: false,
nodes: [],
searchText: "",
selectedNode: null,
selectedNodeDeviceMetrics: [],
selectedNodeEnvironmentMetrics: [],
selectedNodePowerMetrics: [],
selectedNodeMqttMetrics: [],
selectedNodeTraceroutes: [],
deviceMetricsTimeRange: "7d",
environmentMetricsTimeRange: "7d",
powerMetricsTimeRange: "7d",
isPositionHistoryModalExpanded: true,
positionHistoryDateTimeFrom: null,
positionHistoryDateTimeTo: null,
selectedNodePositionHistory: [],
selectedNodeToShowPositionHistory: null,
selectedNodePositionHistoryMarkers: [],
selectedNodePositionHistoryPolyLines: [],
selectedTraceRoute: null,
tracerouteEdges: [],
selectedNodeToShowConnections: null,
moment: window.moment,
};
},
mounted: function() {
// load data
this.loadHardwareModelStats();
// handle map click callback from outside of vue
window._onMapClick = () => {
this.searchText = "";
this.isShowingMobileSearch = false;
};
// handle node callback from outside of vue
window._onNodeClick = (node) => {
this.selectedNode = node;
this.loadNodeDeviceMetrics(node.node_id);
this.loadNodeEnvironmentMetrics(node.node_id);
this.loadNodePowerMetrics(node.node_id);
this.loadNodeMqttMetrics(node.node_id);
this.loadNodeTraceroutes(node.node_id);
//this.loadNodePositionHistory(node.node_id);
};
// handle node callback from outside of vue
window._onShowNodeConnectionsClick = (node) => {
this.selectedNodeToShowConnections = node;
};
// handle nodes updated callback from outside of vue
window._onNodesUpdated = (nodes) => {
this.nodes = nodes;
};
},
methods: {
getAnnouncementId: function() {
// change this when making a new announcement
return "1";
},
shouldShowAnnouncement: function() {
const lastSeenAnnouncementId = window.localStorage.getItem("last-seen-announcement-id");
return lastSeenAnnouncementId?.toString() !== this.getAnnouncementId();
},
dismissAnnouncement: function() {
window.localStorage.setItem("last-seen-announcement-id", this.getAnnouncementId());
this.isShowingAnnouncement = false;
},
shouldShowInfoModal: function() {
return !window.getConfigHasSeenInfoModal()
&& !window.isMobile();
},
loadHardwareModelStats: function() {
window.axios.get('/api/v1/stats/hardware-models').then((response) => {
this.hardwareModelStats = response.data.hardware_model_stats;
}).catch((error) => {
// do nothing
});
},
loadNodeDeviceMetrics: function(nodeId) {
// calculate unix timestamps in milliseconds for supported time ranges
const oneDayAgoInMilliseconds = new Date().getTime() - (86400 * 1000);
const threeDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000);
const sevenDaysAgoInMilliseconds = new Date().getTime() - (604800 * 1000);
const thirtyDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000 * 10);
// determine how long back to load device metrics from
var timeFrom = threeDaysAgoInMilliseconds;
switch(this.deviceMetricsTimeRange){
case "1d": {
timeFrom = oneDayAgoInMilliseconds;
break;
}
case "3d": {
timeFrom = threeDaysAgoInMilliseconds;
break;
}
case "7d": {
timeFrom = sevenDaysAgoInMilliseconds;
break;
}
case "30d": {
timeFrom = thirtyDaysAgoInMilliseconds;
break;
}
}
window.axios.get(`/api/v1/nodes/${nodeId}/device-metrics`, {
params: {
time_from: timeFrom,
},
}).then((response) => {
// reverse response, as it's newest to oldest, but we want oldest to newest
this.selectedNodeDeviceMetrics = response.data.device_metrics.reverse();
this.renderDeviceMetricCharts();
}).catch(() => {
this.selectedNodeDeviceMetrics = [];
this.renderDeviceMetricCharts();
});
},
loadNodeEnvironmentMetrics: function(nodeId) {
// calculate unix timestamps in milliseconds for supported time ranges
const oneDayAgoInMilliseconds = new Date().getTime() - (86400 * 1000);
const threeDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000);
const sevenDaysAgoInMilliseconds = new Date().getTime() - (604800 * 1000);
const thirtyDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000 * 10);
// determine how long back to load environment metrics from
var timeFrom = threeDaysAgoInMilliseconds;
switch(this.environmentMetricsTimeRange){
case "1d": {
timeFrom = oneDayAgoInMilliseconds;
break;
}
case "3d": {
timeFrom = threeDaysAgoInMilliseconds;
break;
}
case "7d": {
timeFrom = sevenDaysAgoInMilliseconds;
break;
}
case "30d": {
timeFrom = thirtyDaysAgoInMilliseconds;
break;
}
}
window.axios.get(`/api/v1/nodes/${nodeId}/environment-metrics`, {
params: {
time_from: timeFrom,
},
}).then((response) => {
// reverse response, as it's newest to oldest, but we want oldest to newest
this.selectedNodeEnvironmentMetrics = response.data.environment_metrics.reverse();
this.renderEnvironmentMetricCharts();
}).catch(() => {
this.selectedNodeEnvironmentMetrics = [];
this.renderEnvironmentMetricCharts();
});
},
loadNodePowerMetrics: function(nodeId) {
// calculate unix timestamps in milliseconds for supported time ranges
const oneDayAgoInMilliseconds = new Date().getTime() - (86400 * 1000);
const threeDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000);
const sevenDaysAgoInMilliseconds = new Date().getTime() - (604800 * 1000);
const thirtyDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000 * 10);
// determine how long back to load power metrics from
var timeFrom = threeDaysAgoInMilliseconds;
switch(this.powerMetricsTimeRange){
case "1d": {
timeFrom = oneDayAgoInMilliseconds;
break;
}
case "3d": {
timeFrom = threeDaysAgoInMilliseconds;
break;
}
case "7d": {
timeFrom = sevenDaysAgoInMilliseconds;
break;
}
case "30d": {
timeFrom = thirtyDaysAgoInMilliseconds;
break;
}
}
window.axios.get(`/api/v1/nodes/${nodeId}/power-metrics`, {
params: {
time_from: timeFrom,
},
}).then((response) => {
// reverse response, as it's newest to oldest, but we want oldest to newest
this.selectedNodePowerMetrics = response.data.power_metrics.reverse();
this.renderPowerMetricCharts();
}).catch(() => {
this.selectedNodePowerMetrics = [];
this.renderPowerMetricCharts();
});
},
loadNodeMqttMetrics: function(nodeId) {
this.selectedNodeMqttMetrics = [];
window.axios.get(`/api/v1/nodes/${nodeId}/mqtt-metrics`).then((response) => {
this.selectedNodeMqttMetrics = response.data.mqtt_metrics;
}).catch(() => {
// do nothing
});
},
loadNodeTraceroutes: function(nodeId) {
this.selectedNodeTraceroutes = [];
window.axios.get(`/api/v1/nodes/${nodeId}/traceroutes`, {
params: {
count: 5,
},
}).then((response) => {
this.selectedNodeTraceroutes = response.data.traceroutes;
}).catch(() => {
// do nothing
});
},
loadNodePositionHistory: function(nodeId) {
this.selectedNodePositionHistory = [];
window.axios.get(`/api/v1/nodes/${nodeId}/position-history`, {
params: {
// parse from datetime-local format, and send as unix timestamp in milliseconds
time_from: moment(this.positionHistoryDateTimeFrom, "YYYY-MM-DDTHH:mm").format("x"),
time_to: moment(this.positionHistoryDateTimeTo, "YYYY-MM-DDTHH:mm").format("x"),
},
}).then((response) => {
this.selectedNodePositionHistory = response.data.position_history;
if(this.selectedNodeToShowPositionHistory != null){
clearAllPositionHistory();
onPositionHistoryUpdated(response.data.position_history);
}
}).catch(() => {
// do nothing
});
},
renderDeviceMetricCharts: function() {
try {
this.updateDeviceMetricsChart();
} catch(e) {
console.log(e);
}
},
updateDeviceMetricsChart: function() {
// destroy existing chart
const chartElementId = "deviceMetricsChart";
const existingChart = window.Chart.getChart(chartElementId);
if(existingChart != null){
existingChart.destroy();
}
// get chart element
const chartElement = window.document.getElementById(chartElementId);
if(!chartElement){
return;
}
// create chart data
const labels = [];
const batteryMetrics = [];
const channelUtilizationMetrics = [];
const airUtilTxMetrics = [];
for(const deviceMetric of this.selectedNodeDeviceMetrics){
labels.push(moment(deviceMetric.created_at));
batteryMetrics.push(deviceMetric.battery_level);
channelUtilizationMetrics.push(deviceMetric.channel_utilization);
airUtilTxMetrics.push(deviceMetric.air_util_tx);
}
// create chart
new window.Chart(chartElement, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Battery Level',
borderColor: '#3b82f6',
backgroundColor: '#3b82f6',
pointStyle: false, // no points
fill: false,
data: batteryMetrics,
},
{
label: 'Channel Util',
borderColor: '#22c55e',
backgroundColor: '#22c55e',
showLine: false, // no lines between points
fill: false,
data: channelUtilizationMetrics,
},
{
label: 'Air Util TX',
borderColor: '#f97316',
backgroundColor: '#f97316',
showLine: false, // no lines between points
fill: false,
data: airUtilTxMetrics,
},
],
},
options: {
responsive: true,
borderWidth: 2,
elements: {
point: {
radius: 2,
},
},
scales: {
x: {
position: 'top',
type: 'time',
time: {
unit: 'day',
displayFormats: {
day: 'MMM DD', // Jan 01
},
},
},
y: {
min: 0,
max: 101, // 101 is "Plugged In", need to include for tooltip to work
ticks: {
callback: (label) => `${label}%`,
},
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
mode: "index",
intersect: false,
callbacks: {
label: (item) => {
return `${item.dataset.label}: ${item.formattedValue}%`;
},
},
},
},
}
});
},
renderEnvironmentMetricCharts: function() {
try {
this.updateEnvironmentMetricsChart();
} catch(e) {
console.log(e);
}
},
updateEnvironmentMetricsChart: function() {
// destroy existing chart
const chartElementId = "environmentMetricsChart";
const existingChart = window.Chart.getChart(chartElementId);
if(existingChart != null){
existingChart.destroy();
}
// get chart element
const chartElement = window.document.getElementById(chartElementId);
if(!chartElement){
return;
}
// create chart data
const labels = [];
const temperatureMetrics = [];
const relativeHumidityMetrics = [];
const barometricPressureMetrics = [];
const iaqMetrics = [];
for(const deviceMetric of this.selectedNodeEnvironmentMetrics){
labels.push(moment(deviceMetric.created_at));
temperatureMetrics.push(deviceMetric.temperature);
relativeHumidityMetrics.push(deviceMetric.relative_humidity);
barometricPressureMetrics.push(deviceMetric.barometric_pressure);
iaqMetrics.push(deviceMetric.iaq);
}
// create chart
new window.Chart(chartElement, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Temperature',
suffix: '°C',
borderColor: '#3b82f6',
backgroundColor: '#3b82f6',
pointStyle: false, // no points
fill: false,
data: temperatureMetrics,
yAxisID: 'y',
},
{
label: 'Humidity',
suffix: '%',
borderColor: '#22c55e',
backgroundColor: '#22c55e',
pointStyle: false, // no points
fill: false,
data: relativeHumidityMetrics,
yAxisID: 'y',
},
{
label: 'Pressure',
suffix: 'hPa',
borderColor: '#f97316',
backgroundColor: '#f97316',
pointStyle: false, // no points
fill: false,
data: barometricPressureMetrics,
yAxisID: 'y1',
},
{
label: 'IAQ',
suffix: 'IAQ',
borderColor: '#f472b6',
backgroundColor: '#f472b6',
pointStyle: false, // no points
fill: false,
data: iaqMetrics,
yAxisID: 'yIAQ',
},
],
},
options: {
responsive: true,
borderWidth: 2,
spanGaps: 1000 * 60 * 60 * 24, // only show lines between metrics with a 24 hour or less gap
elements: {
point: {
radius: 2,
},
},
scales: {
x: {
position: 'top',
type: 'time',
time: {
unit: 'day',
displayFormats: {
day: 'MMM DD', // Jan 01
},
},
},
y: {
min: -20,
max: 100,
},
y1: {
min: 800,
max: 1100,
ticks: {
stepSize: 10,
callback: (label) => `${label} hPa`,
},
position: 'right',
grid: {
drawOnChartArea: false, // only want the grid lines for one axis to show up
},
},
yIAQ: {
type: 'linear',
display: false,
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
mode: "index",
intersect: false,
callbacks: {
label: (item) => {
return `${item.dataset.label}: ${item.formattedValue}${item.dataset.suffix}`;
},
},
},
},
}
});
},
renderPowerMetricCharts: function() {
try {
this.updatePowerMetricsChart();
} catch(e) {
console.log(e);
}
},
updatePowerMetricsChart: function() {
// destroy existing chart
const chartElementId = "powerMetricsChart";
const existingChart = window.Chart.getChart(chartElementId);
if(existingChart != null){
existingChart.destroy();
}
// get chart element
const chartElement = window.document.getElementById(chartElementId);
if(!chartElement){
return;
}
// create chart data
const labels = [];
const channel1VoltageReadings = [];
const channel2VoltageReadings = [];
const channel3VoltageReadings = [];
const channel1CurrentReadings = [];
const channel2CurrentReadings = [];
const channel3CurrentReadings = [];
for(const powerMetric of this.selectedNodePowerMetrics){
labels.push(moment(powerMetric.created_at));
channel1VoltageReadings.push(powerMetric.ch1_voltage);
channel2VoltageReadings.push(powerMetric.ch2_voltage);
channel3VoltageReadings.push(powerMetric.ch3_voltage);
channel1CurrentReadings.push(powerMetric.ch1_current);
channel2CurrentReadings.push(powerMetric.ch2_current);
channel3CurrentReadings.push(powerMetric.ch3_current);
}
// create chart
new window.Chart(chartElement, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Ch1 Voltage',
suffix: "V",
borderColor: '#3b82f6',
backgroundColor: '#3b82f6',
pointStyle: false, // no points
fill: false,
data: channel1VoltageReadings,
yAxisID: 'y',
},
{
label: 'Ch2 Voltage',
suffix: "V",
borderColor: '#22c55e',
backgroundColor: '#22c55e',
pointStyle: false, // no points
fill: false,
data: channel2VoltageReadings,
yAxisID: 'y',
},
{
label: 'Ch3 Voltage',
suffix: "V",
borderColor: '#f97316',
backgroundColor: '#f97316',
pointStyle: false, // no points
fill: false,
data: channel3VoltageReadings,
yAxisID: 'y',
},
{
label: 'Ch1 Current',
suffix: "mA",
borderColor: '#93c5fd',
backgroundColor: '#93c5fd',
pointStyle: false, // no points
fill: false,
data: channel1CurrentReadings,
yAxisID: 'y1',
},
{
label: 'Ch2 Current',
suffix: "mA",
borderColor: '#86efac',
backgroundColor: '#86efac',
pointStyle: false, // no points
fill: false,
data: channel2CurrentReadings,
yAxisID: 'y1',
},
{
label: 'Ch3 Current',
suffix: "mA",
borderColor: '#fdba74',
backgroundColor: '#fdba74',
pointStyle: false, // no points
fill: false,
data: channel3CurrentReadings,
yAxisID: 'y1',
},
],
},
options: {
responsive: true,
borderWidth: 2,
spanGaps: 1000 * 60 * 60 * 3, // only show lines between metrics with a 3 hour or less gap
elements: {
point: {
radius: 2,
},
},
scales: {
x: {
position: 'top',
type: 'time',
time: {
unit: 'day',
displayFormats: {
day: 'MMM DD', // Jan 01
},
},
},
y: {
min: 0,
suggestedMax: 6,
ticks: {
callback: (label) => `${label}V`,
},
},
y1: {
suggestedMin: -50,
suggestedMax: 50,
ticks: {
stepSize: 50,
callback: (label) => `${label}mA`,
},
position: 'right',
grid: {
drawOnChartArea: false, // only want the grid lines for one axis to show up
},
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
mode: "index",
intersect: false,
callbacks: {
label: (item) => {
return `${item.dataset.label}: ${item.formattedValue}${item.dataset.suffix}`;
},
},
},
},
}
});
},
showTraceRoute: function(traceroute) {
this.selectedTraceRoute = traceroute;
},
findNodeById: function(id) {
return window.findNodeById(id);
},
findNodeMarkerById: function(id) {
return window.findNodeMarkerById(id);
},
onSearchResultNodeClick: function(node) {
// clear search
this.searchText = "";
// hide search
this.isShowingMobileSearch = false;
// go to node
if(window.goToNode(node.node_id)){
return;
}
// fallback to showing node details since we can't go to the node
window.showNodeDetails(node.node_id);
},
dismissInfoModal: function() {
this.isShowingInfoModal = false;
window.setConfigHasSeenInfoModal(true);
},
getRegionFrequencyRange: function(regionName) {
return window.getRegionFrequencyRange(regionName);
},
showNodePositionHistory: function(nodeId) {
// find node
const node = findNodeById(nodeId);
if(!node){
return;
}
// update ui
this.selectedNode = null;
this.selectedNodeToShowPositionHistory = node;
this.isPositionHistoryModalExpanded = true;
// close node info tooltip as position history shows under it
window.closeAllTooltips();
// reset default time range when opening position history ui
// YYYY-MM-DDTHH:mm is the format expected by the datetime-local input type
this.positionHistoryDateTimeFrom = moment().subtract(1, "hours").format('YYYY-MM-DDTHH:mm');
this.positionHistoryDateTimeTo = moment().format('YYYY-MM-DDTHH:mm');
// load position history
this.loadNodePositionHistory(nodeId);
},
onPositionHistoryQuickRangeClick: function(range) {
// update position history time range
switch(range){
case "1h": {
this.positionHistoryDateTimeFrom = moment().subtract(1, "hours").format('YYYY-MM-DDTHH:mm');
this.positionHistoryDateTimeTo = moment().format('YYYY-MM-DDTHH:mm');
break;
}
case "24h": {
this.positionHistoryDateTimeFrom = moment().subtract(24, "hours").format('YYYY-MM-DDTHH:mm');
this.positionHistoryDateTimeTo = moment().format('YYYY-MM-DDTHH:mm');
break;
}
case "7d": {
this.positionHistoryDateTimeFrom = moment().subtract(7, "days").format('YYYY-MM-DDTHH:mm');
this.positionHistoryDateTimeTo = moment().format('YYYY-MM-DDTHH:mm');
break;
}
}
// reload position history
const node = this.selectedNodeToShowPositionHistory;
if(node){
this.loadNodePositionHistory(node.node_id);
}
},
getShareLinkForNode: function(nodeId) {
return window.location.origin + `/?node_id=${nodeId}`;
},
copyShareLinkForNode: function(nodeId) {
// make sure copy to clipboard is supported
if(!navigator.clipboard || !navigator.clipboard.writeText){
alert("Clipboard not supported. Site must be served via https on iOS.");
return;
}
// copy share link to clipboard
const url = this.getShareLinkForNode(nodeId);
navigator.clipboard.writeText(url);
// tell user we copied it
alert("Link copied to clipboard!");
},
dismissShowingNodeConnections: function() {
window._onHideNodeConnectionsClick();
this.selectedNodeToShowConnections = null;
},
dismissShowingNodePositionHistory: function() {
this.selectedNodePositionHistory = [];
this.selectedNodeToShowPositionHistory = null;
this.selectedNodePositionHistoryMarkers = [];
this.selectedNodePositionHistoryPolyLines = [];
cleanUpPositionHistory();
},
formatUptimeSeconds: function(secondsToFormat) {
secondsToFormat = Number(secondsToFormat);
var days = Math.floor(secondsToFormat / (3600 * 24));
var hours = Math.floor((secondsToFormat % (3600 * 24)) / 3600);
var minutes = Math.floor((secondsToFormat % 3600) / 60);
var seconds = Math.floor(secondsToFormat % 60);
var daysPlural = days === 1 ? 'day' : 'days';
return `${days} ${daysPlural} ${hours}h ${minutes}m ${seconds}s`;
},
formatTemperature: function(celsius) {
switch(this.configTemperatureFormat){
case "celsius": {
return `${Number(celsius).toFixed(0)}°C`;
}
case "fahrenheit": {
const fahrenheit = this.celsiusToFahrenheit(celsius);
return `${fahrenheit.toFixed(0)}°F`;
}
}
},
convertTemperature: function(celsius) {
switch(this.configTemperatureFormat){
case "celsius": {
return celsius;
}
case "fahrenheit": {
return this.celsiusToFahrenheit(celsius);
}
}
},
getTemperatureUnit: function() {
switch(this.configTemperatureFormat){
case "celsius": return "°C";
case "fahrenheit": return "°F";
}
},
celsiusToFahrenheit: function(celsius) {
return (celsius * 9/5) + 32;
},
getNodeColour(nodeId) {
// convert node id to a hex colour
return "#" + (nodeId & 0x00FFFFFF).toString(16).padStart(6, '0');
},
getNodeTextColour(nodeId) {
// extract rgb components
const r = (nodeId & 0xFF0000) >> 16;
const g = (nodeId & 0x00FF00) >> 8;
const b = nodeId & 0x0000FF;
// calculate brightness
const brightness = ((r * 0.299) + (g * 0.587) + (b * 0.114)) / 255;
// determine text color based on brightness
return brightness > 0.5 ? "#000000" : "#FFFFFF";
},
},
computed: {
searchedNodes() {
// search nodes
const nodes = this.nodes.filter((node) => {
const matchesId = node.node_id?.toLowerCase()?.includes(this.searchText.toLowerCase());
const matchesHexId = node.node_id_hex?.toLowerCase()?.includes(this.searchText.toLowerCase());
const matchesLongName = node.long_name?.toLowerCase()?.includes(this.searchText.toLowerCase());
const matchesShortName = node.short_name?.toLowerCase()?.includes(this.searchText.toLowerCase());
return matchesId || matchesHexId || matchesLongName || matchesShortName;
});
// order alphabetically by long name
nodes.sort((nodeA, nodeB) => {
const nodeALongName = nodeA.long_name || "";
const nodeBLongName = nodeB.long_name || "";
return nodeALongName.localeCompare(nodeBLongName);
});
// only return the first 500 results to avoid ui lag...
return nodes.slice(0, 500);
},
selectedNodeLatestPowerMetric() {
const [ latestPowerMetric ] = this.selectedNodePowerMetrics.slice(-1);
return latestPowerMetric;
},
},
watch: {
configNodesMaxAgeInSeconds() {
window.setConfigNodesMaxAgeInSeconds(this.configNodesMaxAgeInSeconds);
},
configNodesOfflineAgeInSeconds() {
window.setConfigNodesOfflineAgeInSeconds(this.configNodesOfflineAgeInSeconds);
},
configWaypointsMaxAgeInSeconds() {
window.setConfigWaypointsMaxAgeInSeconds(this.configWaypointsMaxAgeInSeconds);
},
configConnectionsMaxDistanceInMeters() {
window.setConfigConnectionsMaxDistanceInMeters(this.configConnectionsMaxDistanceInMeters);
},
configZoomLevelGoToNode() {
window.setConfigZoomLevelGoToNode(this.configZoomLevelGoToNode);
},
configAutoUpdatePositionInUrl() {
window.setConfigAutoUpdatePositionInUrl(this.configAutoUpdatePositionInUrl);
},
configEnableMapAnimations() {
window.setConfigEnableMapAnimations(this.configEnableMapAnimations);
},
configTemperatureFormat() {
window.setConfigTemperatureFormat(this.configTemperatureFormat);
},
configConnectionsTimePeriodInSeconds() {
window.setConfigConnectionsTimePeriodInSeconds(this.configConnectionsTimePeriodInSeconds);
},
configConnectionsColoredLines() {
window.setConfigConnectionsColoredLines(this.configConnectionsColoredLines);
},
configConnectionsBidirectionalOnly() {
window.setConfigConnectionsBidirectionalOnly(this.configConnectionsBidirectionalOnly);
},
configConnectionsMinSnrDb() {
window.setConfigConnectionsMinSnrDb(this.configConnectionsMinSnrDb);
},
configConnectionsBidirectionalMinSnr() {
window.setConfigConnectionsBidirectionalMinSnr(this.configConnectionsBidirectionalMinSnr);
},
deviceMetricsTimeRange() {
this.loadNodeDeviceMetrics(this.selectedNode.node_id);
},
environmentMetricsTimeRange() {
this.loadNodeEnvironmentMetrics(this.selectedNode.node_id);
},
powerMetricsTimeRange() {
this.loadNodePowerMetrics(this.selectedNode.node_id);
},
},
}).mount('#app');

View file

@ -1,199 +0,0 @@
function getConfigHasSeenInfoModal() {
return localStorage.getItem("config_has_seen_info_modal") === "true";
}
function setConfigHasSeenInfoModal(value) {
return localStorage.setItem("config_has_seen_info_modal", value);
}
function getConfigAutoUpdatePositionInUrl() {
// use user preference, or enable by default
const value = localStorage.getItem("config_auto_update_position_in_url");
return value === "true" || value == null;
}
function setConfigAutoUpdatePositionInUrl(value) {
return localStorage.setItem("config_auto_update_position_in_url", value);
}
function getConfigEnableMapAnimations() {
const value = localStorage.getItem("config_enable_map_animations");
// enable animations by default
if(value === null){
return true;
}
return value === "true";
}
function setConfigEnableMapAnimations(value) {
return localStorage.setItem("config_enable_map_animations", value);
}
function getConfigTemperatureFormat() {
return localStorage.getItem("config_temperature_format") || "celsius";
}
function setConfigTemperatureFormat(format) {
return localStorage.setItem("config_temperature_format", format);
}
function getConfigMapSelectedTileLayer() {
return localStorage.getItem("config_map_selected_tile_layer") || "Thunderforest Neighbourhood";
}
function setConfigMapSelectedTileLayer(layer) {
return localStorage.setItem("config_map_selected_tile_layer", layer);
}
function getConfigMapEnabledOverlayLayers() {
try {
const value = localStorage.getItem("config_map_enabled_overlay_layers");
if(value){
return JSON.parse(value);
}
} catch(e) {}
// overlays enabled by default
return ["Legend", "Position History", "Traceroutes"];
}
function setConfigMapEnabledOverlayLayers(layers) {
return localStorage.setItem("config_map_enabled_overlay_layers", JSON.stringify(layers));
}
function getConfigNodesMaxAgeInSeconds() {
const value = localStorage.getItem("config_nodes_max_age_in_seconds");
return value != null ? parseInt(value) : null;
}
function setConfigNodesMaxAgeInSeconds(value) {
if(value != null){
return localStorage.setItem("config_nodes_max_age_in_seconds", value);
} else {
return localStorage.removeItem("config_nodes_max_age_in_seconds");
}
}
function getConfigNodesOfflineAgeInSeconds() {
const value = localStorage.getItem("config_nodes_offline_age_in_seconds");
return value != null ? parseInt(value) : 10800;
}
function setConfigNodesOfflineAgeInSeconds(value) {
if(value != null){
return localStorage.setItem("config_nodes_offline_age_in_seconds", value);
} else {
return localStorage.removeItem("config_nodes_offline_age_in_seconds");
}
}
function getConfigWaypointsMaxAgeInSeconds() {
const value = localStorage.getItem("config_waypoints_max_age_in_seconds");
return value != null ? parseInt(value) : null;
}
function setConfigWaypointsMaxAgeInSeconds(value) {
if(value != null){
return localStorage.setItem("config_waypoints_max_age_in_seconds", value);
} else {
return localStorage.removeItem("config_waypoints_max_age_in_seconds");
}
}
function getConfigConnectionsMaxDistanceInMeters() {
const value = localStorage.getItem("config_connections_max_distance_in_meters");
// default to 70km (70,000 meters)
return value != null ? parseInt(value) : 70000;
}
function setConfigConnectionsMaxDistanceInMeters(value) {
return localStorage.setItem("config_connections_max_distance_in_meters", value);
}
function getConfigZoomLevelGoToNode() {
const value = localStorage.getItem("config_zoom_level_go_to_node");
const parsedValue = value != null ? parseInt(value) : null;
return parsedValue || 15;
}
function setConfigZoomLevelGoToNode(value) {
return localStorage.setItem("config_zoom_level_go_to_node", value);
}
function getConfigConnectionsTimePeriodInSeconds() {
const value = localStorage.getItem("config_connections_time_period_in_seconds");
// default to 7 days if unset
return value != null ? parseInt(value) : 604800;
}
function setConfigConnectionsTimePeriodInSeconds(value) {
return localStorage.setItem("config_connections_time_period_in_seconds", value);
}
function getConfigConnectionsColoredLines() {
const value = localStorage.getItem("config_connections_colored_lines");
// disable colored lines by default
if(value === null){
return false;
}
return value === "true";
}
function setConfigConnectionsColoredLines(value) {
return localStorage.setItem("config_connections_colored_lines", value);
}
function getConfigConnectionsBidirectionalOnly() {
const value = localStorage.getItem("config_connections_bidirectional_only");
// disable bidirectional filter by default
if(value === null){
return false;
}
return value === "true";
}
function setConfigConnectionsBidirectionalOnly(value) {
return localStorage.setItem("config_connections_bidirectional_only", value);
}
function getConfigConnectionsMinSnrDb() {
const value = localStorage.getItem("config_connections_min_snr_db");
// default to null (unset)
if(value === null || value === ""){
return null;
}
const parsed = parseFloat(value);
return isNaN(parsed) ? null : parsed;
}
function setConfigConnectionsMinSnrDb(value) {
if(value === null || value === "" || value === undefined){
return localStorage.removeItem("config_connections_min_snr_db");
}
// Convert to string for localStorage (handles both number and string inputs)
const stringValue = typeof value === "number" ? value.toString() : String(value);
return localStorage.setItem("config_connections_min_snr_db", stringValue);
}
function getConfigConnectionsBidirectionalMinSnr() {
const value = localStorage.getItem("config_connections_bidirectional_min_snr");
// disable bidirectional minimum SNR by default
if(value === null){
return false;
}
return value === "true";
}
function setConfigConnectionsBidirectionalMinSnr(value) {
return localStorage.setItem("config_connections_bidirectional_min_snr", value);
}
function isMobile() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 KiB

File diff suppressed because it is too large Load diff