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');