Files
meshview/meshview/templates/nodegraph.html
T
Joel Krauska e77428661c Version 3.0.0 Feature Release - Target Before Thanksgiving! (#96)
* Add alembic DB schema management (#86)

* Use alembic
* add creation helper
* example migration tool

* Store UTC int time in DB (#81)

* use UTC int time

* Remove old index notes script -- no longer needed

* modify alembic to support cleaner migrations

* add /version json endpoint

* move technical docs

* remove old migrate script

* add readme in docs:

* more doc tidy

* rm

* update api docs

* ignore other database files

* health endpoint

* alembic log format

* break out api calls in to their own file to reduce footprint

* ruff and docs

* vuln

* Improves arguments in mvrun.py

* Set dbcleanup.log location configurable

* mvrun work

* fallback if missing config

* remove unused loop

* improve migrations and fix logging problem with mqtt

* Container using slim/uv

* auto build containers

* symlink

* fix symlink

* checkout and containerfile

* make /app owned by ap0p

* Traceroute Return Path logged and displayed (#97)


* traceroute returns are now logged and /packetlist now graphs the correct data for a return route
* now using alembic to update schema
* HOWTO - Alembic

---------

Co-authored-by: Joel Krauska <jkrauska@gmail.com>

* DB Backups

* backups and cleanups are different

* ruff

* Docker Docs

* setup-dev

* graphviz for dot in Container

* Summary of 3.0.0 stuff

* Alembic was blocking mqtt logs

* Add us first/last timestamps to node table too

* Worked on /api/packet. Needed to modify
- Store.py to read the new time data
- api.py to present the new time data
- firehose.html chat.html and map.html now use the new apis and the time is the browser local time

* Worked on /api/packet. Needed to modify
- Store.py to read the new time data
- api.py to present the new time data
- firehose.html chat.html and map.html now use the new apis and the time is the browser local time

* Improves container build (#94)

* Worked on /api/packet. Needed to modify
- Store.py to read the new time data
- api.py to present the new time data
- firehose.html chat.html and map.html now use the new apis and the time is the browser local time

* Worked on /api/packet. Needed to modify
- Store.py to read the new time data
- api.py to present the new time data
- firehose.html chat.html and map.html now use the new apis and the time is the browser local time

* Worked on /api/packet. Needed to modify
- Added new api endpoint /api/packets_seen
- Modified web.py and store.py to support changes to APIs.
- Started to work on new_node.html and new_packet.html for presentation of data.

* Worked on /api/packet. Needed to modify
- Added new api endpoint /api/packets_seen
- Modified web.py and store.py to support changes to APIs.
- Started to work on new_node.html and new_packet.html for presentation of data.

* Finishing up all the pages for the 3.0 release.

Now all pages are functional.

* Finishing up all the pages for the 3.0 release.

Now all pages are functional.

* fix ruff format

* more ruff

* Finishing up all the pages for the 3.0 release.

Now all pages are functional.

* Finishing up all the pages for the 3.0 release.

Now all pages are functional.

* pyproject.toml requirements

* use sys.executable

* fix 0 epoch dates in /chat

* Make the robots do our bidding

* another compatibility fix when _us is empty and we need to sort by BOTH old and new

* Finishing up all the pages for the 3.0 release.

Now all pages are functional.

* Finishing up all the pages for the 3.0 release.

Now all pages are functional.

* Remamed new_node to node. shorter and descriptive.

* Remamed new_node to node. shorter and descriptive.

* Remamed new_node to node. shorter and descriptive.

* Remamed new_node to node. shorter and descriptive.

* Remamed new_node to node. shorter and descriptive.

* Remamed new_node to node. shorter and descriptive.

* More changes... almost ready for release.

Ranamed 2 pages for easy or reading.

* Fix the net page as it was not showing the date information

* Fix the net page as it was not showing the date information

* Fix the net page as it was not showing the date information

* Fix the net page as it was not showing the date information

* ruff

---------

Co-authored-by: Óscar García Amor <ogarcia@connectical.com>
Co-authored-by: Jim Schrempp <jschrempp@users.noreply.github.com>
Co-authored-by: Pablo Revilla <pablorevilla@gmail.com>
2025-11-28 11:17:20 -08:00

384 lines
11 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 UI */
.search-container {
position: absolute;
bottom: 100px;
left: 10px;
z-index: 10;
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 box */
#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 */
#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;
}
.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>
<!-- SEARCH + FILTER -->
<div class="search-container">
<label 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>
<!-- INFO BOX -->
<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>
<!-- LEGEND -->
<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>
// Initialize ECharts
const chart = echarts.init(document.getElementById("mynetwork"));
// -----------------------------------
// COLOR + ROLE HELPERS
// -----------------------------------
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) { return colors.role[role] || colors.role.other; }
function getSymbolSize(role) {
switch(role){
case "ROUTER":
case "ROUTER_LATE": return 30;
case "CLIENT_BASE": return 18;
case "CLIENT": return 15;
case "CLIENT_MUTE": return 7;
default: return 15;
}
}
function getLabel(role, shortName, longName) {
if (role === "ROUTER" || role === "ROUTER_LATE") return longName;
return shortName || "";
}
// -----------------------------------
// STATE
// -----------------------------------
let nodes = [];
let edges = [];
let filteredNodes = [];
let filteredEdges = [];
let lastSelectedNode = null;
let selectedChannel = null;
// -----------------------------------
// LOAD NODES + EDGES FROM API
// -----------------------------------
async function loadData() {
// 1. Load nodes
const n = await fetch("/api/nodes").then(r => r.json());
nodes = n.nodes.map(x => ({
name: String(x.node_id),
node_id: x.node_id,
long_name: x.long_name,
short_name: x.short_name,
hw_model: x.hw_model,
role: x.role,
channel: x.channel,
labelValue: getLabel(x.role, x.short_name, x.long_name),
symbolSize: getSymbolSize(x.role),
itemStyle: {
color: getRoleColor(x.role),
opacity: 1
},
label: {
show: true,
position: "right",
color: "#333",
fontSize: 12,
formatter: p => p.data.labelValue
}
}));
const allNodeIDs = new Set(nodes.map(n => n.name));
// 2. Load edges
const e = await fetch("/api/edges").then(r => r.json());
// Only keep edges that reference valid nodes
edges = e.edges
.filter(ed =>
allNodeIDs.has(String(ed.from)) &&
allNodeIDs.has(String(ed.to))
)
.map(ed => ({
source: String(ed.from),
target: String(ed.to),
edgeType: ed.type,
originalColor: colors.edge[ed.type] || "#ccc",
lineStyle: {
width: 2,
color: colors.edge[ed.type] || "#ccc",
opacity: 1
}
}));
// 3. Determine which nodes are actually used in edges
const usedNodeIDs = new Set();
edges.forEach(e => {
usedNodeIDs.add(e.source);
usedNodeIDs.add(e.target);
});
// 4. Remove unused (no-edge) nodes
nodes = nodes.filter(n => usedNodeIDs.has(n.name));
// 5. Double safety: remove any edges referencing removed nodes
edges = edges.filter(e =>
usedNodeIDs.has(e.source) && usedNodeIDs.has(e.target)
);
// 6. Now ready to build dropdown & render
populateChannelDropdown();
}
// -----------------------------------
// CHANNEL FILTER
// -----------------------------------
function populateChannelDropdown() {
const sel = document.getElementById("channel-select");
const chans = [...new Set(nodes.map(n => n.channel))].sort();
chans.forEach(ch => {
const opt = document.createElement("option");
opt.value = ch;
opt.text = ch;
sel.appendChild(opt);
});
selectedChannel = chans[0];
sel.value = selectedChannel;
filterByChannel();
}
function filterByChannel() {
selectedChannel = document.getElementById("channel-select").value;
filteredNodes = nodes.filter(n => n.channel === selectedChannel);
const allowed = new Set(filteredNodes.map(n => n.name));
filteredEdges = edges.filter(e => allowed.has(e.source) && allowed.has(e.target));
lastSelectedNode = null;
updateChart();
}
// -----------------------------------
// FORCE GRAPH UPDATE
// -----------------------------------
function updateChart() {
const updatedNodes = filteredNodes.map(n => {
let opacity = 1;
let borderColor = "transparent";
if (lastSelectedNode) {
const connected = filteredEdges.some(
e => (e.source === n.name && e.target === lastSelectedNode) ||
(e.target === n.name && e.source === lastSelectedNode)
);
if (n.name === lastSelectedNode) {
borderColor = colors.selection;
} else if (!connected) {
opacity = 0.3;
}
}
return {
...n,
itemStyle: { ...n.itemStyle, opacity, borderColor, borderWidth: 6 }
};
});
const updatedEdges = filteredEdges.map(e => {
const connected =
lastSelectedNode &&
(e.source === lastSelectedNode || e.target === lastSelectedNode);
return {
...e,
lineStyle: { ...e.lineStyle, opacity: connected ? 1 : 0.1 }
};
});
chart.setOption({
animation: false,
series: [{
type: "graph",
layout: "force",
roam: true,
data: updatedNodes,
links: updatedEdges,
force: { repulsion: 200, edgeLength: [80, 120] }
}]
});
}
// -----------------------------------
// CLICK EVENTS
// -----------------------------------
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(id) {
lastSelectedNode = id;
updateChart();
const n = filteredNodes.find(n => n.name === id);
if (!n) return;
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;
}
// -----------------------------------
// SEARCH
// -----------------------------------
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!");
}
// -----------------------------------
loadData();
window.addEventListener("resize", () => chart.resize());
</script>
{% endblock %}