mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-03-04 23:27:46 +01:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f87831c3a5 | ||
|
|
9c40ce2d59 |
@@ -8,7 +8,6 @@ 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.
|
- **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`
|
- Ubuntu install (inside the venv): `./env/bin/pip install pyitm`
|
||||||
- Coverage: predicted coverage overlay (Longley‑Rice area mode) with perimeter rendering and documentation.
|
- 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.
|
- 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.
|
- Map UX: deterministic jitter for overlapping nodes; edges follow jittered positions.
|
||||||
- Tooling: Meshtastic protobuf updater script with `--check` and `UPSTREAM_REV.txt` tracking.
|
- Tooling: Meshtastic protobuf updater script with `--check` and `UPSTREAM_REV.txt` tracking.
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
__version__ = "3.0.5"
|
__version__ = "3.0.6"
|
||||||
__release_date__ = "2026-2-6"
|
__release_date__ = "2026-3-6"
|
||||||
|
|
||||||
|
|
||||||
def get_git_revision():
|
def get_git_revision():
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ async function initializePage() {
|
|||||||
items.push(`<a href="${urls[i]}">${dict[key] || key}</a>`);
|
items.push(`<a href="${urls[i]}">${dict[key] || key}</a>`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
items.push('<a href="https://meshview.world" target="_blank" rel="noopener noreferrer">MeshviewWorld</a>');
|
||||||
|
|
||||||
menu.innerHTML = items.join(" - ");
|
menu.innerHTML = items.join(" - ");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,25 @@
|
|||||||
#reset-filters-button:hover { background-color:#da190b; }
|
#reset-filters-button:hover { background-color:#da190b; }
|
||||||
#reset-filters-button:active { background-color:#c41e0d; }
|
#reset-filters-button:active { background-color:#c41e0d; }
|
||||||
|
|
||||||
.blinking-tooltip { background:white;color:black;border:1px solid black;border-radius:4px;padding:2px 5px; }
|
.blinking-tooltip {
|
||||||
|
background: white;
|
||||||
|
color: black;
|
||||||
|
border: 1px solid #111;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.25);
|
||||||
|
}
|
||||||
|
.blinking-tooltip.text-packet {
|
||||||
|
animation: textPulse 1.1s ease-in-out 6;
|
||||||
|
border-color: #ff8c00;
|
||||||
|
}
|
||||||
|
@keyframes textPulse {
|
||||||
|
0% { box-shadow: 0 2px 8px rgba(0,0,0,0.25); }
|
||||||
|
50% { box-shadow: 0 4px 14px rgba(255,140,0,0.45); }
|
||||||
|
100% { box-shadow: 0 2px 8px rgba(0,0,0,0.25); }
|
||||||
|
}
|
||||||
|
|
||||||
#map-wrapper {
|
#map-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -210,44 +228,6 @@ function hashToColor(str){
|
|||||||
return c;
|
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){
|
function hashToUnit(str){
|
||||||
let h = 2166136261;
|
let h = 2166136261;
|
||||||
for(let i=0;i<str.length;i++){
|
for(let i=0;i<str.length;i++){
|
||||||
@@ -312,7 +292,7 @@ function fetchNewPackets(){
|
|||||||
const marker = markerById[pkt.from_node_id];
|
const marker = markerById[pkt.from_node_id];
|
||||||
const nodeData = nodeMap.get(pkt.from_node_id);
|
const nodeData = nodeMap.get(pkt.from_node_id);
|
||||||
if(marker && nodeData) {
|
if(marker && nodeData) {
|
||||||
blinkNode(marker,nodeData.long_name,pkt.portnum);
|
blinkNode(marker, nodeData.long_name, pkt.portnum, pkt.payload);
|
||||||
} else {
|
} else {
|
||||||
addUnmappedPacket(pkt, nodeData);
|
addUnmappedPacket(pkt, nodeData);
|
||||||
}
|
}
|
||||||
@@ -451,11 +431,32 @@ function renderNodesOnMap(){
|
|||||||
marker.originalColor = color;
|
marker.originalColor = color;
|
||||||
markerById[node.key] = marker;
|
markerById[node.key] = marker;
|
||||||
|
|
||||||
marker.bindPopup(buildNodePopup(node));
|
const popup = `
|
||||||
|
<a href="/node/${node.node_id}">${node.long_name}</a> (${node.short_name})<br>
|
||||||
|
|
||||||
|
<span data-translate-lang="channel_label"></span> ${node.channel}<br>
|
||||||
|
<span data-translate-lang="model_label"></span> ${node.hw_model}<br>
|
||||||
|
<span data-translate-lang="role_label"></span> ${node.role}<br>
|
||||||
|
<span data-translate-lang="mqtt_gateway"></span> ${
|
||||||
|
node.is_mqtt_gateway ? (mapTranslations.yes || "Yes") : (mapTranslations.no || "No")
|
||||||
|
}<br>
|
||||||
|
|
||||||
|
${
|
||||||
|
node.last_seen_us
|
||||||
|
? `<span data-translate-lang="last_seen"></span> ${timeAgoFromUs(node.last_seen_us)}<br>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
${
|
||||||
|
node.firmware
|
||||||
|
? `<span data-translate-lang="firmware"></span> ${node.firmware}<br>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
marker.on('click', () => {
|
marker.on('click', () => {
|
||||||
onNodeClick(node);
|
onNodeClick(node);
|
||||||
marker.setPopupContent(buildNodePopup(node));
|
marker.bindPopup(popup).openPopup();
|
||||||
marker.openPopup();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -585,7 +586,20 @@ map.on('click', e=>{
|
|||||||
BLINKING
|
BLINKING
|
||||||
====================================================== */
|
====================================================== */
|
||||||
|
|
||||||
function blinkNode(marker,longName,portnum){
|
function escapeHtml(value){
|
||||||
|
return String(value)
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTextPort(portnum){
|
||||||
|
return portnum === 1 || portnum === 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
function blinkNode(marker,longName,portnum,payload){
|
||||||
if(!map.hasLayer(marker)) return;
|
if(!map.hasLayer(marker)) return;
|
||||||
|
|
||||||
if(activeBlinks.has(marker)){
|
if(activeBlinks.has(marker)){
|
||||||
@@ -595,13 +609,24 @@ function blinkNode(marker,longName,portnum){
|
|||||||
}
|
}
|
||||||
|
|
||||||
let blinkCount = 0;
|
let blinkCount = 0;
|
||||||
|
const blinkStart = Date.now();
|
||||||
|
const blinkDurationMs = 7000;
|
||||||
|
const safeName = escapeHtml(longName);
|
||||||
|
const portLabel = portMap[portnum] || `Port ${portnum ?? "?"}`;
|
||||||
|
const payloadText = (payload || "").trim();
|
||||||
|
const showPayload = isTextPort(portnum) && payloadText && payloadText !== "Did not decode";
|
||||||
|
const shortPayload = showPayload && payloadText.length > 80
|
||||||
|
? `${payloadText.slice(0, 77)}...`
|
||||||
|
: payloadText;
|
||||||
|
const payloadLine = showPayload ? `<br><span>${escapeHtml(shortPayload)}</span>` : "";
|
||||||
|
|
||||||
const tooltip = L.tooltip({
|
const tooltip = L.tooltip({
|
||||||
permanent:true,
|
permanent:true,
|
||||||
direction:'top',
|
direction:'top',
|
||||||
offset:[0,-marker.options.radius-5],
|
offset:[0,-marker.options.radius-5],
|
||||||
className:'blinking-tooltip'
|
className: isTextPort(portnum) ? 'blinking-tooltip text-packet' : 'blinking-tooltip'
|
||||||
})
|
})
|
||||||
.setContent(`${longName} (${portMap[portnum] || "Port "+portnum})`)
|
.setContent(`${safeName} (${escapeHtml(portLabel)})${payloadLine}`)
|
||||||
.setLatLng(marker.getLatLng())
|
.setLatLng(marker.getLatLng())
|
||||||
.addTo(map);
|
.addTo(map);
|
||||||
|
|
||||||
@@ -616,7 +641,7 @@ function blinkNode(marker,longName,portnum){
|
|||||||
}
|
}
|
||||||
blinkCount++;
|
blinkCount++;
|
||||||
|
|
||||||
if(blinkCount>7){
|
if(Date.now() - blinkStart > blinkDurationMs){
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
marker.setStyle({ fillColor: marker.originalColor });
|
marker.setStyle({ fillColor: marker.originalColor });
|
||||||
map.removeLayer(tooltip);
|
map.removeLayer(tooltip);
|
||||||
|
|||||||
@@ -18,21 +18,23 @@
|
|||||||
|
|
||||||
/* --- Node Info --- */
|
/* --- Node Info --- */
|
||||||
.node-info {
|
.node-info {
|
||||||
background-color: #1f2226;
|
|
||||||
border: 1px solid #3a3f44;
|
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
padding: 12px 14px;
|
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(120px, 1fr));
|
grid-template-columns: repeat(3, minmax(120px, 1fr));
|
||||||
grid-column-gap: 14px;
|
grid-column-gap: 12px;
|
||||||
grid-row-gap: 6px;
|
grid-row-gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-info div { padding: 2px 0; }
|
.node-info-col {
|
||||||
|
background-color: #1f2226;
|
||||||
|
border: 1px solid #3a3f44;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-info-col div { padding: 2px 0; }
|
||||||
.node-info strong {
|
.node-info strong {
|
||||||
color: #9fd4ff;
|
color: #9fd4ff;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -357,31 +359,32 @@
|
|||||||
|
|
||||||
<!-- Node Info -->
|
<!-- Node Info -->
|
||||||
<div id="node-info" class="node-info">
|
<div id="node-info" class="node-info">
|
||||||
<div><strong data-translate-lang="node_id">Node ID</strong><strong>: </strong><span id="info-node-id">—</span></div>
|
<div class="node-info-col">
|
||||||
<div><strong data-translate-lang="id">Hex ID</strong><strong>: </strong><span id="info-id">—</span></div>
|
<div><strong data-translate-lang="node_id">Node ID</strong><strong>: </strong><span id="info-node-id">—</span></div>
|
||||||
<div><strong data-translate-lang="long_name">Long Name</strong><strong>: </strong> <span id="info-long-name">—</span></div>
|
<div><strong data-translate-lang="id">Hex ID</strong><strong>: </strong><span id="info-id">—</span></div>
|
||||||
<div><strong data-translate-lang="short_name">Short Name</strong><strong>: </strong> <span id="info-short-name">—</span></div>
|
<div><strong data-translate-lang="short_name">Short Name</strong><strong>: </strong> <span id="info-short-name">—</span></div>
|
||||||
|
<div><strong data-translate-lang="long_name">Long Name</strong><strong>: </strong> <span id="info-long-name">—</span></div>
|
||||||
<div><strong data-translate-lang="hw_model">Hardware Model</strong><strong>: </strong> <span id="info-hw-model">—</span></div>
|
<div><strong data-translate-lang="channel">Channel</strong><strong>: </strong> <span id="info-channel">—</span></div>
|
||||||
<div><strong data-translate-lang="firmware">Firmware</strong><strong>: </strong> <span id="info-firmware">—</span></div>
|
</div>
|
||||||
<div><strong data-translate-lang="role">Role</strong><strong>: </strong> <span id="info-role">—</span></div>
|
<div class="node-info-col">
|
||||||
|
<div><strong data-translate-lang="latitude">Latitude</strong><strong>: </strong> <span id="info-lat">—</span></div>
|
||||||
<div><strong data-translate-lang="mqtt_gateway">MQTT Gateway</strong><strong>: </strong> <span id="info-mqtt-gateway">—</span></div>
|
<div><strong data-translate-lang="longitude">Longitude</strong><strong>: </strong> <span id="info-lon">—</span></div>
|
||||||
<div><strong data-translate-lang="channel">Channel</strong><strong>: </strong> <span id="info-channel">—</span></div>
|
<div><strong data-translate-lang="role">Role</strong><strong>: </strong> <span id="info-role">—</span></div>
|
||||||
<div><strong data-translate-lang="latitude">Latitude</strong><strong>: </strong> <span id="info-lat">—</span></div>
|
<div><strong data-translate-lang="hw_model">Hardware Model</strong><strong>: </strong> <span id="info-hw-model">—</span></div>
|
||||||
<div><strong data-translate-lang="longitude">Longitude</strong><strong>: </strong> <span id="info-lon">—</span></div>
|
<div><strong data-translate-lang="mqtt_gateway">MQTT Gateway</strong><strong>: </strong> <span id="info-mqtt-gateway">—</span></div>
|
||||||
|
</div>
|
||||||
<div><strong data-translate-lang="first_update">First Update</strong><strong>: </strong> <span id="info-first-update">—</span></div>
|
<div class="node-info-col">
|
||||||
<div><strong data-translate-lang="last_update">Last Update</strong><strong>: </strong> <span id="info-last-update">—</span></div>
|
<div><strong data-translate-lang="first_update">First Update</strong><strong>: </strong> <span id="info-first-update">—</span></div>
|
||||||
<div>
|
<div><strong data-translate-lang="last_update">Last Update</strong><strong>: </strong> <span id="info-last-update">—</span></div>
|
||||||
<strong data-translate-lang="statistics">Statistics</strong><strong>: </strong>
|
<div><strong data-translate-lang="firmware">Firmware</strong><strong>: </strong> <span id="info-firmware">—</span></div>
|
||||||
<span id="info-stats"
|
<div>
|
||||||
data-label-24h="24h"
|
<strong data-translate-lang="statistics">Statistics</strong><strong>: </strong>
|
||||||
data-label-sent="Packets sent"
|
<span id="info-stats"
|
||||||
data-label-seen="Times seen">—</span>
|
data-label-24h="24h"
|
||||||
|
data-label-sent="Packets sent"
|
||||||
|
data-label-seen="Times seen">—</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Map. -->
|
<!-- Map. -->
|
||||||
@@ -895,21 +898,13 @@ function addMarker(id, lat, lon, color = "red", node = null) {
|
|||||||
async function drawNeighbors(src, nids) {
|
async function drawNeighbors(src, nids) {
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
|
|
||||||
// Prefer the currently displayed source position (e.g. latest track point),
|
// Ensure source node position exists
|
||||||
// then fall back to the node API location.
|
const srcNode = await fetchNodeFromApi(src);
|
||||||
let srcLat;
|
if (!srcNode || !srcNode.last_lat || !srcNode.last_long) return;
|
||||||
let srcLon;
|
|
||||||
let srcNode = currentNode || nodeCache[src] || null;
|
|
||||||
|
|
||||||
if (nodePositions[src]) {
|
const srcLat = srcNode.last_lat / 1e7;
|
||||||
[srcLat, srcLon] = nodePositions[src];
|
const srcLon = srcNode.last_long / 1e7;
|
||||||
} else {
|
nodePositions[src] = [srcLat, srcLon];
|
||||||
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) {
|
for (const nid of nids) {
|
||||||
const neighbor = await fetchNodeFromApi(nid);
|
const neighbor = await fetchNodeFromApi(nid);
|
||||||
@@ -1447,13 +1442,20 @@ async function loadPacketHistogram() {
|
|||||||
const DAYS = 7;
|
const DAYS = 7;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
|
const dayKeyFromDate = (d) => {
|
||||||
|
const y = d.getFullYear();
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(d.getDate()).padStart(2, "0");
|
||||||
|
return `${y}-${m}-${day}`;
|
||||||
|
};
|
||||||
|
|
||||||
const dayKeys = [];
|
const dayKeys = [];
|
||||||
const dayLabels = [];
|
const dayLabels = [];
|
||||||
|
|
||||||
for (let i = DAYS - 1; i >= 0; i--) {
|
for (let i = DAYS - 1; i >= 0; i--) {
|
||||||
const d = new Date(now);
|
const d = new Date(now);
|
||||||
d.setDate(d.getDate() - i);
|
d.setDate(d.getDate() - i);
|
||||||
dayKeys.push(d.toISOString().slice(0, 10));
|
dayKeys.push(dayKeyFromDate(d));
|
||||||
dayLabels.push(
|
dayLabels.push(
|
||||||
d.toLocaleDateString([], { month: "short", day: "numeric" })
|
d.toLocaleDateString([], { month: "short", day: "numeric" })
|
||||||
);
|
);
|
||||||
@@ -1481,9 +1483,7 @@ async function loadPacketHistogram() {
|
|||||||
for (const pkt of packets) {
|
for (const pkt of packets) {
|
||||||
if (!pkt.import_time_us) continue;
|
if (!pkt.import_time_us) continue;
|
||||||
|
|
||||||
const day = new Date(pkt.import_time_us / 1000)
|
const day = dayKeyFromDate(new Date(pkt.import_time_us / 1000));
|
||||||
.toISOString()
|
|
||||||
.slice(0, 10);
|
|
||||||
|
|
||||||
if (!dayKeys.includes(day)) continue;
|
if (!dayKeys.includes(day)) continue;
|
||||||
|
|
||||||
@@ -1627,16 +1627,15 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
// ✅ MAP MUST EXIST FIRST
|
// ✅ MAP MUST EXIST FIRST
|
||||||
if (!map) initMap();
|
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
|
// ✅ DRAW LATEST NEIGHBORS ONCE
|
||||||
const neighborIds = await loadLatestNeighborIds();
|
const neighborIds = await loadLatestNeighborIds();
|
||||||
if (neighborIds.length) {
|
if (neighborIds.length) {
|
||||||
await drawNeighbors(fromNodeId, neighborIds);
|
await drawNeighbors(fromNodeId, neighborIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ⚠️ Track may add to map, but must not hide it
|
||||||
|
await loadTrack();
|
||||||
|
|
||||||
await loadPackets();
|
await loadPackets();
|
||||||
initPacketPortFilter();
|
initPacketPortFilter();
|
||||||
await loadTelemetryCharts();
|
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_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="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="channel">Channel <span class="sort-icon"></span></th>
|
||||||
<th data-translate-lang="mqtt_gateway">MQTT <span class="sort-icon"></span></th>
|
<th data-translate-lang="mqtt_gateway">MQTT</th>
|
||||||
<th data-translate-lang="last_seen">Last Seen <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>
|
<th data-translate-lang="favorite"></th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -331,7 +331,7 @@ const minStatusMs = 300;
|
|||||||
const headers = document.querySelectorAll("thead th");
|
const headers = document.querySelectorAll("thead th");
|
||||||
const keyMap = [
|
const keyMap = [
|
||||||
"short_name","long_name","hw_model","firmware","role",
|
"short_name","long_name","hw_model","firmware","role",
|
||||||
"last_lat","last_long","channel","is_mqtt_gateway","last_seen_us"
|
"last_lat","last_long","channel","last_seen_us"
|
||||||
];
|
];
|
||||||
|
|
||||||
function debounce(fn, delay = 250) {
|
function debounce(fn, delay = 250) {
|
||||||
@@ -717,11 +717,6 @@ document.addEventListener("DOMContentLoaded", async function() {
|
|||||||
B = B || 0;
|
B = B || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === "is_mqtt_gateway") {
|
|
||||||
A = A ? 1 : 0;
|
|
||||||
B = B ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize strings for stable sorting
|
// Normalize strings for stable sorting
|
||||||
if (typeof A === "string") A = A.toLowerCase();
|
if (typeof A === "string") A = A.toLowerCase();
|
||||||
if (typeof B === "string") B = B.toLowerCase();
|
if (typeof B === "string") B = B.toLowerCase();
|
||||||
|
|||||||
@@ -408,11 +408,6 @@ seenSorted.forEach(s => {
|
|||||||
hopGroups[hopValue].push(s);
|
hopGroups[hopValue].push(s);
|
||||||
});
|
});
|
||||||
|
|
||||||
function formatHopDisplay(hopKey, hopStart){
|
|
||||||
const startVal = hopStart ?? "—";
|
|
||||||
return `${hopKey}/${startVal}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------------------------------------------
|
/* ---------------------------------------------
|
||||||
Render grouped gateway table + map markers
|
Render grouped gateway table + map markers
|
||||||
----------------------------------------------*/
|
----------------------------------------------*/
|
||||||
@@ -429,7 +424,6 @@ seenTableBody.innerHTML = Object.keys(hopGroups)
|
|||||||
const node = nodeLookup[s.node_id];
|
const node = nodeLookup[s.node_id];
|
||||||
const label = node?.long_name || s.node_id;
|
const label = node?.long_name || s.node_id;
|
||||||
|
|
||||||
const hopDisplay = formatHopDisplay(hopKey, s.hop_start);
|
|
||||||
const timeStr = s.import_time_us
|
const timeStr = s.import_time_us
|
||||||
? new Date(s.import_time_us/1000).toLocaleTimeString()
|
? new Date(s.import_time_us/1000).toLocaleTimeString()
|
||||||
: "—";
|
: "—";
|
||||||
@@ -488,7 +482,7 @@ seenTableBody.innerHTML = Object.keys(hopGroups)
|
|||||||
RSSI: ${s.rx_rssi ?? "—"}<br>
|
RSSI: ${s.rx_rssi ?? "—"}<br>
|
||||||
SNR: ${s.rx_snr ?? "—"}<br><br>
|
SNR: ${s.rx_snr ?? "—"}<br><br>
|
||||||
|
|
||||||
<b data-translate-lang="hops">Hops</b>: ${hopDisplay}
|
<b data-translate-lang="hops">Hops</b>: ${hopKey}
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@@ -499,7 +493,7 @@ seenTableBody.innerHTML = Object.keys(hopGroups)
|
|||||||
<td><a href="/node/${s.node_id}">${label}</a></td>
|
<td><a href="/node/${s.node_id}">${label}</a></td>
|
||||||
<td>${s.rx_rssi ?? "—"}</td>
|
<td>${s.rx_rssi ?? "—"}</td>
|
||||||
<td>${s.rx_snr ?? "—"}</td>
|
<td>${s.rx_snr ?? "—"}</td>
|
||||||
<td>${hopDisplay}</td>
|
<td>${hopKey}</td>
|
||||||
<td>${s.channel ?? "—"}</td>
|
<td>${s.channel ?? "—"}</td>
|
||||||
<td>${timeStr}</td>
|
<td>${timeStr}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
Reference in New Issue
Block a user