mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-03-04 23:27:46 +01:00
296 lines
9.2 KiB
HTML
296 lines
9.2 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block head %}
|
|
<script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>
|
|
{% endblock %}
|
|
|
|
{% block css %}
|
|
#mynetwork {
|
|
width: 100%;
|
|
height: 100vh;
|
|
border: 1px solid #ddd;
|
|
background-color: #f8f9fa;
|
|
border-radius: 10px;
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
}
|
|
.search-container {
|
|
position: absolute;
|
|
bottom: 100px;
|
|
left: 10px;
|
|
z-index: 10;1
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
}
|
|
.search-container input,
|
|
.search-container select,
|
|
.search-container button {
|
|
padding: 8px;
|
|
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);
|
|
padding: 12px;
|
|
border-radius: 8px;
|
|
border: 1px solid #ccc;
|
|
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);
|
|
padding: 10px;
|
|
border-radius: 5px;
|
|
border: 1px solid #ccc;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
font-size: 14px;
|
|
display: flex;
|
|
color: #333;
|
|
}
|
|
.legend-category {
|
|
margin-right: 10px;
|
|
code {
|
|
color: inherit;
|
|
}
|
|
}
|
|
.legend-box {
|
|
display: inline-block;
|
|
width: 12px;
|
|
height: 12px;
|
|
margin-right: 5px;
|
|
border-radius: 3px;
|
|
&.circle {
|
|
border-radius: 6px;
|
|
}
|
|
}
|
|
{% endblock %}
|
|
|
|
{% block body %}
|
|
<div id="mynetwork"></div>
|
|
|
|
<div class="search-container">
|
|
<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>
|
|
</div>
|
|
|
|
<div id="node-info">
|
|
<b>Long Name:</b> <span id="node-long-name"></span><br>
|
|
<b>Short Name:</b> <span id="node-short-name"></span><br>
|
|
<b>Role:</b> <span id="node-role"></span><br>
|
|
<b>Hardware Model:</b> <span id="node-hw-model"></span>
|
|
</div>
|
|
|
|
<div id="legend">
|
|
<div class="legend-category">
|
|
<div><span class="legend-box" style="background-color: #ff5733"></span> Traceroute</div>
|
|
<div><span class="legend-box" style="background-color: #049acd"></span> Neighbor</div>
|
|
</div>
|
|
<div class="legend-category">
|
|
<div><span class="legend-box circle" style="background-color: #ff5733"></span> <code>ROUTER</code></div>
|
|
<div><span class="legend-box circle" style="background-color: #b65224"></span> <code>ROUTER_LATE</code></div>
|
|
</div>
|
|
<div class="legend-category">
|
|
<div><span class="legend-box circle" style="background-color: #007bff"></span> <code>CLIENT</code></div>
|
|
<div><span class="legend-box circle" style="background-color: #00c3ff"></span> <code>CLIENT_MUTE</code></div>
|
|
</div>
|
|
<div class="legend-category">
|
|
<div><span class="legend-box circle" style="background-color: #049acd"></span> <code>CLIENT_BASE</code></div>
|
|
<div><span class="legend-box circle" style="background-color: #ffbf00"></span> Other</div>
|
|
</div>
|
|
<div class="legend-category">
|
|
<div><span class="legend-box circle" style="background-color: #6c757d"></span> Unknown</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const chart = echarts.init(document.getElementById('mynetwork'));
|
|
|
|
const colors = {
|
|
edge: {
|
|
traceroute: '#ff5733',
|
|
neighbor: '#049acd',
|
|
},
|
|
role: {
|
|
ROUTER: '#ff5733',
|
|
ROUTER_LATE: '#b65224',
|
|
CLIENT: '#007bff',
|
|
CLIENT_MUTE: '#00c3ff',
|
|
CLIENT_BASE: '#049acd',
|
|
other: '#ffbf00',
|
|
unknown: '#6c757d',
|
|
},
|
|
selection: '#ff8c00',
|
|
};
|
|
|
|
function getRoleColor(role) {
|
|
if (!role) return colors.role.unknown;
|
|
return colors.role[role] || colors.role.other;
|
|
}
|
|
|
|
|
|
function getSymbolSize (role) {
|
|
switch (role) {
|
|
case 'ROUTER': return 30;
|
|
case 'ROUTER_LATE': return 30;
|
|
case 'CLIENT_BASE': return 18;
|
|
case 'CLIENT': return 15;
|
|
case 'CLIENT_MUTE': return 7;
|
|
default: return 15; // Unknown or other roles
|
|
}
|
|
}
|
|
|
|
function getLabel (role, short_name, long_name) {
|
|
if (role === 'ROUTER') return long_name;
|
|
if (role === 'ROUTER_LATE') return long_name;
|
|
if (role === 'CLIENT_BASE') return short_name;
|
|
if (role === 'CLIENT') return short_name;
|
|
if (role === 'CLIENT_MUTE') return short_name;
|
|
return short_name || '';
|
|
}
|
|
|
|
// --- Nodes ---
|
|
const nodes = [
|
|
{% for node in nodes %}
|
|
{
|
|
name: "{{ node.node_id }}", // node_id as string
|
|
value: getLabel({{node.role | tojson}}, {{node.short_name | tojson }}, {{node.long_name | tojson}}), // display label
|
|
symbol: 'circle',
|
|
symbolSize: getSymbolSize({{node.role | tojson}}),
|
|
itemStyle: { color: getRoleColor({{node.role | tojson}}), 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 }}
|
|
},
|
|
{% endfor %}
|
|
];
|
|
|
|
// --- Edges ---
|
|
const edges = [
|
|
{% for edge in edges %}
|
|
{
|
|
source: "{{ edge.from }}", // edge source as string
|
|
target: "{{ edge.to }}", // edge target as string
|
|
originalColor: colors.edge[{{edge.type | tojson}}],
|
|
lineStyle: {
|
|
color: colors.edge[{{edge.type | tojson}}],
|
|
width: {{edge.weight | tojson}},
|
|
},
|
|
},
|
|
{% endfor %}
|
|
];
|
|
|
|
let filteredNodes = [];
|
|
let filteredEdges = [];
|
|
let selectedChannel = 'LongFast';
|
|
let lastSelectedNode = null;
|
|
|
|
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();
|
|
}
|
|
|
|
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=getRoleColor(node.role), borderColor='transparent', borderWidth=6;
|
|
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) {
|
|
opacity=1;
|
|
borderColor=colors.selection;
|
|
}
|
|
else if(connected) opacity=1;
|
|
else opacity=0.4;
|
|
}
|
|
return {...node, itemStyle:{...node.itemStyle, color,opacity, borderColor, borderWidth}};
|
|
});
|
|
|
|
const updatedEdges = filteredEdges.map(edge=>{
|
|
let opacity=0.1, width=edge.lineStyle.width;
|
|
if(lastSelectedNode){
|
|
const connected = edge.source===lastSelectedNode || edge.target===lastSelectedNode;
|
|
opacity=connected?1:0.05; width=edge.lineStyle.width;
|
|
}
|
|
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 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;
|
|
}
|
|
}
|
|
|
|
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 %}
|