mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-03-04 23:27:46 +01:00
e77428661c
* 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>
384 lines
11 KiB
HTML
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 %}
|