mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-03-04 23:27:46 +01:00
Fixed bug with nodegraph.html it was not able to handle some characters on the name_long
This commit is contained in:
@@ -31,7 +31,7 @@ The project serves as a real-time monitoring and diagnostic tool for the Meshtas
|
||||
|
||||
Samples of currently running instances:
|
||||
|
||||
- https://meshview.bayme.sh (SF Bay Area)
|
||||
- https://meshview.bayme.sh (SF Bay Area)
|
||||
- https://www.svme.sh/ (Sacramento Valley)
|
||||
- https://meshview.nyme.sh/ (New York)
|
||||
- https://map.wpamesh.net/ (Western Pennsylvania)
|
||||
@@ -42,7 +42,7 @@ Samples of currently running instances:
|
||||
- https://socalmesh.w4hac.com (Southern California)
|
||||
- https://meshview.lsinfra.de (Hessen - Germany)
|
||||
- https://map.nswmesh.au/ (Sydney - Australia)
|
||||
|
||||
- https://meshview.pvmesh.org/ (Pioneer Valley, Massachusetts)
|
||||
---
|
||||
|
||||
## Installing
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block head %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
@@ -11,9 +11,8 @@
|
||||
border: 1px solid #ddd;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.search-container {
|
||||
position: absolute;
|
||||
bottom: 100px;
|
||||
@@ -23,7 +22,6 @@
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.search-container input,
|
||||
.search-container select,
|
||||
.search-container button {
|
||||
@@ -31,45 +29,40 @@
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.search-container button {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-container button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
#node-info {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
background-color: rgba(255,255,255,0.95);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
width: 260px;
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#legend {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
background: rgba(255,255,255,0.9);
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.legend-box {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
@@ -83,7 +76,7 @@
|
||||
<div id="mynetwork"></div>
|
||||
|
||||
<div class="search-container">
|
||||
<label for="channel-select" style="color: #333;">Channel:</label>
|
||||
<label for="channel-select" style="color:#333;">Channel:</label>
|
||||
<select id="channel-select" onchange="filterByChannel()"></select>
|
||||
<input type="text" id="node-search" placeholder="Search node...">
|
||||
<button onclick="searchNode()">Search</button>
|
||||
@@ -97,182 +90,128 @@
|
||||
</div>
|
||||
|
||||
<div id="legend">
|
||||
<div><span class="legend-box" style="background-color: #ff5733;"></span> <span style="color: black;">Traceroute</span></div>
|
||||
<div><span class="legend-box" style="background-color: #3388ff;"></span> <span style="color: black;">Neighbor</span></div>
|
||||
<div><span class="legend-box" style="background-color:#ff5733;"></span> Traceroute</div>
|
||||
<div><span class="legend-box" style="background-color:#3388ff;"></span> Neighbor</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
const chart = echarts.init(document.getElementById('mynetwork'));
|
||||
<script>
|
||||
const chart = echarts.init(document.getElementById('mynetwork'));
|
||||
|
||||
const nodes = [
|
||||
{% for node in nodes %}
|
||||
{
|
||||
name: `{{ node.node_id }}`,
|
||||
value: `{{ node.long_name | tojson }}`,
|
||||
symbol: 'circle',
|
||||
symbolSize: 15,
|
||||
itemStyle: { color: '#007bff', opacity: 1},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'right',
|
||||
color: '#333',
|
||||
fontSize: 12,
|
||||
formatter: function(params) {
|
||||
return params.data.value.replace(/^\"(.*)\"$/, '$1');
|
||||
}
|
||||
},
|
||||
long_name: `{{ node.long_name }}`,
|
||||
short_name: `{{ node.short_name }}`,
|
||||
role: `{{ node.role }}`,
|
||||
hw_model: `{{ node.hw_model }}`,
|
||||
channel: `{{ node.channel }}`
|
||||
}{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
// --- Nodes ---
|
||||
const nodes = [
|
||||
{% for node in nodes %}
|
||||
{
|
||||
name: "{{ node.node_id }}", // node_id as string
|
||||
value: {{ node.long_name | tojson }}, // display label
|
||||
symbol: 'circle',
|
||||
symbolSize: 15,
|
||||
itemStyle: { color:'#007bff', opacity:1 },
|
||||
label: { show:true, position:'right', color:'#333', fontSize:12, formatter: (p)=>p.data.value },
|
||||
long_name: {{ node.long_name | tojson }},
|
||||
short_name: {{ node.short_name | tojson }},
|
||||
role: {{ node.role | tojson }},
|
||||
hw_model: {{ node.hw_model | tojson }},
|
||||
channel: {{ node.channel | tojson }}
|
||||
}{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
|
||||
const edges = [
|
||||
{% for edge in edges %}
|
||||
{
|
||||
source: `{{ edge.from }}`,
|
||||
target: `{{ edge.to }}`,
|
||||
originalColor: `{{ edge.originalColor }}`,
|
||||
lineStyle: {
|
||||
color: '#d3d3d3',
|
||||
width: 2,
|
||||
opacity: 1
|
||||
}
|
||||
}{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
// --- Edges ---
|
||||
const edges = [
|
||||
{% for edge in edges %}
|
||||
{
|
||||
source: "{{ edge.from }}", // edge source as string
|
||||
target: "{{ edge.to }}", // edge target as string
|
||||
originalColor: "{{ edge.originalColor }}"
|
||||
}{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
|
||||
let filteredNodes = [];
|
||||
let filteredEdges = [];
|
||||
let selectedChannel = 'LongFast';
|
||||
let lastSelectedNode = null;
|
||||
let filteredNodes = [];
|
||||
let filteredEdges = [];
|
||||
let selectedChannel = 'LongFast';
|
||||
let lastSelectedNode = null;
|
||||
|
||||
function populateChannelDropdown() {
|
||||
const channelSelect = document.getElementById('channel-select');
|
||||
const uniqueChannels = [...new Set(nodes.map(n => n.channel).filter(Boolean))].sort();
|
||||
function populateChannelDropdown() {
|
||||
const sel = document.getElementById('channel-select');
|
||||
const unique = [...new Set(nodes.map(n=>n.channel).filter(Boolean))].sort();
|
||||
unique.forEach(ch=>{
|
||||
const opt = document.createElement('option');
|
||||
opt.value=ch; opt.text=ch;
|
||||
if(ch==='LongFast') opt.selected=true;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
selectedChannel = sel.value;
|
||||
filterByChannel();
|
||||
}
|
||||
|
||||
for (const ch of uniqueChannels) {
|
||||
const option = document.createElement('option');
|
||||
option.value = ch;
|
||||
option.text = ch;
|
||||
if (ch === 'LongFast') option.selected = true;
|
||||
channelSelect.appendChild(option);
|
||||
}
|
||||
|
||||
selectedChannel = channelSelect.value;
|
||||
filterByChannel();
|
||||
}
|
||||
|
||||
function filterByChannel() {
|
||||
selectedChannel = document.getElementById('channel-select').value;
|
||||
filteredNodes = nodes.filter(n => n.channel === selectedChannel);
|
||||
const nodeSet = new Set(filteredNodes.map(n => n.name));
|
||||
filteredEdges = edges.filter(e => nodeSet.has(e.source) && nodeSet.has(e.target));
|
||||
lastSelectedNode = null;
|
||||
updateChart();
|
||||
}
|
||||
|
||||
function updateChart() {
|
||||
const updatedNodes = filteredNodes.map(node => {
|
||||
let opacity = 1;
|
||||
let color = '#007bff';
|
||||
if (lastSelectedNode) {
|
||||
const connected = filteredEdges.some(e =>
|
||||
(e.source === node.name && e.target === lastSelectedNode) ||
|
||||
(e.target === node.name && e.source === lastSelectedNode)
|
||||
);
|
||||
if (node.name === lastSelectedNode) {
|
||||
color = '#ff8c00'; // selected node orange
|
||||
opacity = 1;
|
||||
} else if (connected) {
|
||||
opacity = 1; // keep neighbors visible
|
||||
} else {
|
||||
opacity = 0.4; // dim unrelated
|
||||
}
|
||||
}
|
||||
return {
|
||||
...node,
|
||||
itemStyle: { color, opacity }
|
||||
};
|
||||
});
|
||||
|
||||
const updatedEdges = filteredEdges.map(edge => {
|
||||
let opacity = .1;
|
||||
let width = 2;
|
||||
if (lastSelectedNode) {
|
||||
const connected = edge.source === lastSelectedNode || edge.target === lastSelectedNode;
|
||||
opacity = connected ? 1 : 0.05;
|
||||
width = connected ? 2 : 1;
|
||||
}
|
||||
return {
|
||||
...edge,
|
||||
lineStyle: {
|
||||
color: edge.originalColor || '#d3d3d3',
|
||||
width,
|
||||
opacity
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
chart.setOption({
|
||||
series: [{
|
||||
type: 'graph',
|
||||
layout: 'force',
|
||||
data: updatedNodes,
|
||||
links: updatedEdges,
|
||||
roam: true,
|
||||
force: { repulsion: 200, edgeLength: [80, 120] }
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
chart.on('click', function (params) {
|
||||
if (params.dataType === 'node') {
|
||||
updateSelectedNode(params.data.name);
|
||||
} else {
|
||||
// background click -> reset
|
||||
lastSelectedNode = null;
|
||||
updateChart();
|
||||
document.getElementById('node-long-name').innerText = '';
|
||||
document.getElementById('node-short-name').innerText = '';
|
||||
document.getElementById('node-role').innerText = '';
|
||||
document.getElementById('node-hw-model').innerText = '';
|
||||
function filterByChannel() {
|
||||
selectedChannel = document.getElementById('channel-select').value;
|
||||
filteredNodes = nodes.filter(n=>n.channel===selectedChannel);
|
||||
const nodeSet = new Set(filteredNodes.map(n=>n.name));
|
||||
filteredEdges = edges.filter(e=>nodeSet.has(e.source) && nodeSet.has(e.target));
|
||||
lastSelectedNode=null;
|
||||
updateChart();
|
||||
}
|
||||
|
||||
function updateChart() {
|
||||
const updatedNodes = filteredNodes.map(node=>{
|
||||
let opacity=1, color='#007bff';
|
||||
if(lastSelectedNode){
|
||||
const connected = filteredEdges.some(e=>
|
||||
(e.source===node.name && e.target===lastSelectedNode) ||
|
||||
(e.target===node.name && e.source===lastSelectedNode)
|
||||
);
|
||||
if(node.name===lastSelectedNode){ color='#ff8c00'; opacity=1; }
|
||||
else if(connected) opacity=1;
|
||||
else opacity=0.4;
|
||||
}
|
||||
return {...node, itemStyle:{color,opacity}};
|
||||
});
|
||||
|
||||
function updateSelectedNode(selectedNode) {
|
||||
lastSelectedNode = selectedNode;
|
||||
updateChart();
|
||||
|
||||
const nodeData = filteredNodes.find(n => n.name === selectedNode);
|
||||
if (nodeData) {
|
||||
document.getElementById('node-long-name').innerText = nodeData.long_name;
|
||||
document.getElementById('node-short-name').innerText = nodeData.short_name;
|
||||
document.getElementById('node-role').innerText = nodeData.role;
|
||||
document.getElementById('node-hw-model').innerText = nodeData.hw_model;
|
||||
const updatedEdges = filteredEdges.map(edge=>{
|
||||
let opacity=0.1, width=2;
|
||||
if(lastSelectedNode){
|
||||
const connected = edge.source===lastSelectedNode || edge.target===lastSelectedNode;
|
||||
opacity=connected?1:0.05; width=connected?2:1;
|
||||
}
|
||||
return {...edge, lineStyle:{color:edge.originalColor||'#d3d3d3', width, opacity}};
|
||||
});
|
||||
|
||||
chart.setOption({series:[{type:'graph', layout:'force', data:updatedNodes, links:updatedEdges, roam:true, force:{repulsion:200, edgeLength:[80,120]}}]});
|
||||
}
|
||||
|
||||
chart.on('click', function(params){
|
||||
if(params.dataType==='node') updateSelectedNode(params.data.name);
|
||||
else{
|
||||
lastSelectedNode=null; updateChart();
|
||||
document.getElementById('node-long-name').innerText='';
|
||||
document.getElementById('node-short-name').innerText='';
|
||||
document.getElementById('node-role').innerText='';
|
||||
document.getElementById('node-hw-model').innerText='';
|
||||
}
|
||||
});
|
||||
|
||||
function searchNode() {
|
||||
const query = document.getElementById('node-search').value.toLowerCase().trim();
|
||||
if (!query) return;
|
||||
|
||||
const found = filteredNodes.find(node =>
|
||||
node.name.toLowerCase().includes(query) ||
|
||||
node.long_name.toLowerCase().includes(query) ||
|
||||
node.short_name.toLowerCase().includes(query)
|
||||
);
|
||||
|
||||
if (found) {
|
||||
updateSelectedNode(found.name);
|
||||
} else {
|
||||
alert("Node not found in current channel!");
|
||||
}
|
||||
function updateSelectedNode(selNode){
|
||||
lastSelectedNode=selNode; updateChart();
|
||||
const n = filteredNodes.find(x=>x.name===selNode);
|
||||
if(n){
|
||||
document.getElementById('node-long-name').innerText=n.long_name;
|
||||
document.getElementById('node-short-name').innerText=n.short_name;
|
||||
document.getElementById('node-role').innerText=n.role;
|
||||
document.getElementById('node-hw-model').innerText=n.hw_model;
|
||||
}
|
||||
}
|
||||
|
||||
populateChannelDropdown();
|
||||
function searchNode(){
|
||||
const q = document.getElementById('node-search').value.toLowerCase().trim();
|
||||
if(!q) return;
|
||||
const found = filteredNodes.find(n=>n.name.toLowerCase().includes(q) || n.long_name.toLowerCase().includes(q) || n.short_name.toLowerCase().includes(q));
|
||||
if(found) updateSelectedNode(found.name);
|
||||
else alert("Node not found in current channel!");
|
||||
}
|
||||
|
||||
populateChannelDropdown();
|
||||
window.addEventListener('resize', ()=>chart.resize());
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user