mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-03-04 23:27:46 +01:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdf70da803 | ||
|
|
2cc53dc3b7 | ||
|
|
bf5f23a0ab | ||
|
|
37851cd7f8 | ||
|
|
e0ca9900d3 |
@@ -8,6 +8,7 @@ The project serves as a real-time monitoring and diagnostic tool for the Meshtas
|
||||
- **IMPORTANT:** the predicted coverage feature requires the extra `pyitm` dependency. If it is not installed, the coverage API will return 503.
|
||||
- Ubuntu install (inside the venv): `./env/bin/pip install pyitm`
|
||||
- Coverage: predicted coverage overlay (Longley‑Rice area mode) with perimeter rendering and documentation.
|
||||
- UI: added QR code display for quick node/app access.
|
||||
- Gateways: persistent gateway tracking (`is_mqtt_gateway`) and UI indicators in nodes, map popups, and stats.
|
||||
- Map UX: deterministic jitter for overlapping nodes; edges follow jittered positions.
|
||||
- Tooling: Meshtastic protobuf updater script with `--check` and `UPSTREAM_REV.txt` tracking.
|
||||
|
||||
@@ -210,6 +210,44 @@ function hashToColor(str){
|
||||
return c;
|
||||
}
|
||||
|
||||
function labelFor(key, fallback){
|
||||
return mapTranslations[key] || fallback;
|
||||
}
|
||||
|
||||
function buildNodePopup(node){
|
||||
const labels = {
|
||||
channel: labelFor("channel_label", "Channel"),
|
||||
model: labelFor("model_label", "Model"),
|
||||
role: labelFor("role_label", "Role"),
|
||||
mqtt: labelFor("mqtt_gateway", "MQTT Gateway"),
|
||||
lastSeen: labelFor("last_seen", "Last Seen"),
|
||||
firmware: labelFor("firmware", "Firmware"),
|
||||
yes: mapTranslations.yes || "Yes",
|
||||
no: mapTranslations.no || "No"
|
||||
};
|
||||
|
||||
return `
|
||||
<b><a href="/node/${node.node_id}">${node.long_name}</a> (${node.short_name})</b><br>
|
||||
|
||||
<b>${labels.channel}</b> ${node.channel}<br>
|
||||
<b>${labels.model}</b> ${node.hw_model}<br>
|
||||
<b>${labels.role}</b> ${node.role}<br>
|
||||
<b>${labels.mqtt}</b> ${node.is_mqtt_gateway ? labels.yes : labels.no}<br>
|
||||
|
||||
${
|
||||
node.last_seen_us
|
||||
? `<b>${labels.lastSeen}</b> ${timeAgoFromUs(node.last_seen_us)}<br>`
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
node.firmware
|
||||
? `<b>${labels.firmware}</b> ${node.firmware}<br>`
|
||||
: ""
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function hashToUnit(str){
|
||||
let h = 2166136261;
|
||||
for(let i=0;i<str.length;i++){
|
||||
@@ -413,32 +451,11 @@ function renderNodesOnMap(){
|
||||
marker.originalColor = color;
|
||||
markerById[node.key] = marker;
|
||||
|
||||
const popup = `
|
||||
<b><a href="/node/${node.node_id}">${node.long_name}</a> (${node.short_name})</b><br>
|
||||
|
||||
<b data-translate-lang="channel_label"></b> ${node.channel}<br>
|
||||
<b data-translate-lang="model_label"></b> ${node.hw_model}<br>
|
||||
<b data-translate-lang="role_label"></b> ${node.role}<br>
|
||||
<b data-translate-lang="mqtt_gateway"></b> ${
|
||||
node.is_mqtt_gateway ? (mapTranslations.yes || "Yes") : (mapTranslations.no || "No")
|
||||
}<br>
|
||||
|
||||
${
|
||||
node.last_seen_us
|
||||
? `<b data-translate-lang="last_seen"></b> ${timeAgoFromUs(node.last_seen_us)}<br>`
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
node.firmware
|
||||
? `<b data-translate-lang="firmware"></b> ${node.firmware}<br>`
|
||||
: ""
|
||||
}
|
||||
`;
|
||||
|
||||
marker.bindPopup(buildNodePopup(node));
|
||||
marker.on('click', () => {
|
||||
onNodeClick(node);
|
||||
marker.bindPopup(popup).openPopup();
|
||||
marker.setPopupContent(buildNodePopup(node));
|
||||
marker.openPopup();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -895,13 +895,21 @@ function addMarker(id, lat, lon, color = "red", node = null) {
|
||||
async function drawNeighbors(src, nids) {
|
||||
if (!map) return;
|
||||
|
||||
// Ensure source node position exists
|
||||
const srcNode = await fetchNodeFromApi(src);
|
||||
if (!srcNode || !srcNode.last_lat || !srcNode.last_long) return;
|
||||
// Prefer the currently displayed source position (e.g. latest track point),
|
||||
// then fall back to the node API location.
|
||||
let srcLat;
|
||||
let srcLon;
|
||||
let srcNode = currentNode || nodeCache[src] || null;
|
||||
|
||||
const srcLat = srcNode.last_lat / 1e7;
|
||||
const srcLon = srcNode.last_long / 1e7;
|
||||
nodePositions[src] = [srcLat, srcLon];
|
||||
if (nodePositions[src]) {
|
||||
[srcLat, srcLon] = nodePositions[src];
|
||||
} else {
|
||||
srcNode = srcNode || await fetchNodeFromApi(src);
|
||||
if (!srcNode || !srcNode.last_lat || !srcNode.last_long) return;
|
||||
srcLat = srcNode.last_lat / 1e7;
|
||||
srcLon = srcNode.last_long / 1e7;
|
||||
nodePositions[src] = [srcLat, srcLon];
|
||||
}
|
||||
|
||||
for (const nid of nids) {
|
||||
const neighbor = await fetchNodeFromApi(nid);
|
||||
@@ -1619,15 +1627,16 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
// ✅ MAP MUST EXIST FIRST
|
||||
if (!map) initMap();
|
||||
|
||||
// Load the track first so neighbor links anchor to the same
|
||||
// visible current-node position shown on this page.
|
||||
await loadTrack();
|
||||
|
||||
// ✅ DRAW LATEST NEIGHBORS ONCE
|
||||
const neighborIds = await loadLatestNeighborIds();
|
||||
if (neighborIds.length) {
|
||||
await drawNeighbors(fromNodeId, neighborIds);
|
||||
}
|
||||
|
||||
// ⚠️ Track may add to map, but must not hide it
|
||||
await loadTrack();
|
||||
|
||||
await loadPackets();
|
||||
initPacketPortFilter();
|
||||
await loadTelemetryCharts();
|
||||
|
||||
@@ -266,7 +266,7 @@ select, .export-btn, .search-box, .clear-btn {
|
||||
<th data-translate-lang="last_lat">Last Latitude <span class="sort-icon"></span></th>
|
||||
<th data-translate-lang="last_long">Last Longitude <span class="sort-icon"></span></th>
|
||||
<th data-translate-lang="channel">Channel <span class="sort-icon"></span></th>
|
||||
<th data-translate-lang="mqtt_gateway">MQTT</th>
|
||||
<th data-translate-lang="mqtt_gateway">MQTT <span class="sort-icon"></span></th>
|
||||
<th data-translate-lang="last_seen">Last Seen <span class="sort-icon"></span></th>
|
||||
<th data-translate-lang="favorite"></th>
|
||||
</tr>
|
||||
@@ -331,7 +331,7 @@ const minStatusMs = 300;
|
||||
const headers = document.querySelectorAll("thead th");
|
||||
const keyMap = [
|
||||
"short_name","long_name","hw_model","firmware","role",
|
||||
"last_lat","last_long","channel","last_seen_us"
|
||||
"last_lat","last_long","channel","is_mqtt_gateway","last_seen_us"
|
||||
];
|
||||
|
||||
function debounce(fn, delay = 250) {
|
||||
@@ -717,6 +717,11 @@ document.addEventListener("DOMContentLoaded", async function() {
|
||||
B = B || 0;
|
||||
}
|
||||
|
||||
if (key === "is_mqtt_gateway") {
|
||||
A = A ? 1 : 0;
|
||||
B = B ? 1 : 0;
|
||||
}
|
||||
|
||||
// Normalize strings for stable sorting
|
||||
if (typeof A === "string") A = A.toLowerCase();
|
||||
if (typeof B === "string") B = B.toLowerCase();
|
||||
|
||||
@@ -408,6 +408,11 @@ seenSorted.forEach(s => {
|
||||
hopGroups[hopValue].push(s);
|
||||
});
|
||||
|
||||
function formatHopDisplay(hopKey, hopStart){
|
||||
const startVal = hopStart ?? "—";
|
||||
return `${hopKey}/${startVal}`;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------
|
||||
Render grouped gateway table + map markers
|
||||
----------------------------------------------*/
|
||||
@@ -424,6 +429,7 @@ seenTableBody.innerHTML = Object.keys(hopGroups)
|
||||
const node = nodeLookup[s.node_id];
|
||||
const label = node?.long_name || s.node_id;
|
||||
|
||||
const hopDisplay = formatHopDisplay(hopKey, s.hop_start);
|
||||
const timeStr = s.import_time_us
|
||||
? new Date(s.import_time_us/1000).toLocaleTimeString()
|
||||
: "—";
|
||||
@@ -482,7 +488,7 @@ seenTableBody.innerHTML = Object.keys(hopGroups)
|
||||
RSSI: ${s.rx_rssi ?? "—"}<br>
|
||||
SNR: ${s.rx_snr ?? "—"}<br><br>
|
||||
|
||||
<b data-translate-lang="hops">Hops</b>: ${hopKey}
|
||||
<b data-translate-lang="hops">Hops</b>: ${hopDisplay}
|
||||
</div>
|
||||
`);
|
||||
|
||||
@@ -493,7 +499,7 @@ seenTableBody.innerHTML = Object.keys(hopGroups)
|
||||
<td><a href="/node/${s.node_id}">${label}</a></td>
|
||||
<td>${s.rx_rssi ?? "—"}</td>
|
||||
<td>${s.rx_snr ?? "—"}</td>
|
||||
<td>${hopKey}</td>
|
||||
<td>${hopDisplay}</td>
|
||||
<td>${s.channel ?? "—"}</td>
|
||||
<td>${timeStr}</td>
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user