Compare commits
1 commit
master
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff8ee2e01d |
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
|||
BIN
screenshot.png
|
Before Width: | Height: | Size: 5.4 MiB After Width: | Height: | Size: 270 KiB |
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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');
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 364 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 309 KiB |
|
Before Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 346 KiB |
|
Before Width: | Height: | Size: 495 KiB |