5 Commits

Author SHA1 Message Date
pablorevilla-meshtastic
bdf70da803 fix location of node in node.html 2026-02-24 12:00:08 -08:00
pablorevilla-meshtastic
2cc53dc3b7 Added documentation 2026-02-13 22:50:31 -08:00
pablorevilla-meshtastic
bf5f23a0ab fixed sorting error with nodelist 2026-02-13 22:35:32 -08:00
pablorevilla-meshtastic
37851cd7f8 packet.html fix 2026-02-13 20:34:16 -08:00
pablorevilla-meshtastic
e0ca9900d3 fix map error 2026-02-13 20:18:04 -08:00
5 changed files with 75 additions and 37 deletions

View File

@@ -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 (LongleyRice 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.

View File

@@ -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();
});
});

View File

@@ -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();

View File

@@ -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();

View File

@@ -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>