diff --git a/meshview/__version__.py b/meshview/__version__.py index de9f5a7..6f7f517 100644 --- a/meshview/__version__.py +++ b/meshview/__version__.py @@ -3,8 +3,8 @@ import subprocess from pathlib import Path -__version__ = "3.0.5" -__release_date__ = "2026-2-6" +__version__ = "3.0.6" +__release_date__ = "2026-3-6" def get_git_revision(): diff --git a/meshview/templates/base.html b/meshview/templates/base.html index 44f3d0d..80447ab 100644 --- a/meshview/templates/base.html +++ b/meshview/templates/base.html @@ -176,6 +176,7 @@ async function initializePage() { items.push(`${dict[key] || key}`); } } + items.push('MeshviewWorld'); menu.innerHTML = items.join(" - "); } diff --git a/meshview/templates/map.html b/meshview/templates/map.html index f9832af..1caf633 100644 --- a/meshview/templates/map.html +++ b/meshview/templates/map.html @@ -23,7 +23,25 @@ #reset-filters-button:hover { background-color:#da190b; } #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: 2px solid #111; + border-radius: 6px; + padding: 6px 10px; + font-size: 14px; + font-weight: 600; + 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 { position: relative; @@ -274,7 +292,7 @@ function fetchNewPackets(){ const marker = markerById[pkt.from_node_id]; const nodeData = nodeMap.get(pkt.from_node_id); if(marker && nodeData) { - blinkNode(marker,nodeData.long_name,pkt.portnum); + blinkNode(marker, nodeData.long_name, pkt.portnum, pkt.payload); } else { addUnmappedPacket(pkt, nodeData); } @@ -568,7 +586,20 @@ map.on('click', e=>{ BLINKING ====================================================== */ -function blinkNode(marker,longName,portnum){ +function escapeHtml(value){ + return String(value) + .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(activeBlinks.has(marker)){ @@ -578,13 +609,24 @@ function blinkNode(marker,longName,portnum){ } 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 ? `
${escapeHtml(shortPayload)}` : ""; + const tooltip = L.tooltip({ permanent:true, direction:'top', 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()) .addTo(map); @@ -599,7 +641,7 @@ function blinkNode(marker,longName,portnum){ } blinkCount++; - if(blinkCount>7){ + if(Date.now() - blinkStart > blinkDurationMs){ clearInterval(interval); marker.setStyle({ fillColor: marker.originalColor }); map.removeLayer(tooltip); diff --git a/meshview/templates/node.html b/meshview/templates/node.html index 334c492..d3c8a7c 100644 --- a/meshview/templates/node.html +++ b/meshview/templates/node.html @@ -18,21 +18,23 @@ /* --- Node Info --- */ .node-info { - background-color: #1f2226; - border: 1px solid #3a3f44; color: #ddd; font-size: 0.88rem; - padding: 12px 14px; margin-bottom: 14px; - border-radius: 8px; - display: grid; grid-template-columns: repeat(3, minmax(120px, 1fr)); - grid-column-gap: 14px; - grid-row-gap: 6px; + grid-column-gap: 12px; + 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 { color: #9fd4ff; font-weight: 600; @@ -357,31 +359,32 @@
-
Node ID:
-
Hex ID:
-
Long Name:
-
Short Name:
- -
Hardware Model:
-
Firmware:
-
Role:
- -
MQTT Gateway:
-
Channel:
-
Latitude:
-
Longitude:
- -
First Update:
-
Last Update:
-
- Statistics: - +
+
Node ID:
+
Hex ID:
+
Short Name:
+
Long Name:
+
Channel:
+
+
+
Latitude:
+
Longitude:
+
Role:
+
Hardware Model:
+
MQTT Gateway:
+
+
+
First Update:
+
Last Update:
+
Firmware:
+
+ Statistics: + +
- -
@@ -1439,13 +1442,20 @@ async function loadPacketHistogram() { const DAYS = 7; 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 dayLabels = []; for (let i = DAYS - 1; i >= 0; i--) { const d = new Date(now); d.setDate(d.getDate() - i); - dayKeys.push(d.toISOString().slice(0, 10)); + dayKeys.push(dayKeyFromDate(d)); dayLabels.push( d.toLocaleDateString([], { month: "short", day: "numeric" }) ); @@ -1473,9 +1483,7 @@ async function loadPacketHistogram() { for (const pkt of packets) { if (!pkt.import_time_us) continue; - const day = new Date(pkt.import_time_us / 1000) - .toISOString() - .slice(0, 10); + const day = dayKeyFromDate(new Date(pkt.import_time_us / 1000)); if (!dayKeys.includes(day)) continue;