Compare commits
14 commits
dependabot
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 8dc7cdb811 | |||
| b9ed0914d4 | |||
| fc4fff532a | |||
| e22b835114 | |||
| d452e8b3ad | |||
| 720f3b1529 | |||
| 8c54e71efc | |||
| 6e6bb22a9c | |||
| c090352c32 | |||
| 7a57d252dd | |||
| 8bb4d9d9dd | |||
| 33c24d9fe6 | |||
| a0181b8e0f | |||
| 360694842c |
|
|
@ -65,6 +65,11 @@ git clone https://github.com/liamcottle/meshtastic-map
|
||||||
cd meshtastic-map
|
cd meshtastic-map
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Install Meshtastic protobufs definitions
|
||||||
|
```
|
||||||
|
git clone https://github.com/meshtastic/protobufs src/protobufs
|
||||||
|
```
|
||||||
|
|
||||||
Install NodeJS dependencies
|
Install NodeJS dependencies
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
BIN
screenshot.png
|
Before Width: | Height: | Size: 270 KiB After Width: | Height: | Size: 5.4 MiB |
115
src/public/assets/css/styles.css
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
956
src/public/assets/js/app.js
Normal file
|
|
@ -0,0 +1,956 @@
|
||||||
|
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');
|
||||||
199
src/public/assets/js/config.js
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
1692
src/public/assets/js/map.js
Normal file
BIN
src/public/images/devices/HELTEC_MESH_POCKET.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
src/public/images/devices/HELTEC_MESH_SOLAR.png
Normal file
|
After Width: | Height: | Size: 364 KiB |
BIN
src/public/images/devices/HELTEC_V4.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
src/public/images/devices/PORTDUINO.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
src/public/images/devices/SEEED_SOLAR_NODE.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
src/public/images/devices/SEEED_WIO_TRACKER_L1.png
Normal file
|
After Width: | Height: | Size: 309 KiB |
BIN
src/public/images/devices/THINKNODE_M1.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
src/public/images/devices/T_ETH_ELITE.png
Normal file
|
After Width: | Height: | Size: 346 KiB |
BIN
src/public/images/devices/XIAO_NRF52_KIT.png
Normal file
|
After Width: | Height: | Size: 495 KiB |