From ec672544728cc5c4a057fe2bc31932a938bccdca Mon Sep 17 00:00:00 2001 From: pablorevilla-meshtastic Date: Mon, 26 Oct 2026 09:21:10 -0700 Subject: [PATCH] quick map fixes --- meshview/lang/en.json | 4 +- meshview/lang/es.json | 4 +- meshview/lang/ru.json | 4 +- meshview/static/map-inspector.js | 99 ++------------------------------ meshview/templates/base.html | 3 +- meshview/templates/firehose.html | 4 +- meshview/templates/map-view.html | 58 +++++++++---------- 7 files changed, 40 insertions(+), 136 deletions(-) diff --git a/meshview/lang/en.json b/meshview/lang/en.json index 1f8c103..ab694cc 100644 --- a/meshview/lang/en.json +++ b/meshview/lang/en.json @@ -2,7 +2,7 @@ "base": { "chat": "Chat", "nodes": "Nodes", - "everything": "See Everything", + "everything": "Live Traffic", "graphs": "Mesh Graphs", "net": "Weekly Net", "map": "Live Map", @@ -176,7 +176,7 @@ }, "firehose": { - "live_feed": "📡 Live Feed", + "live_feed": "📡 Live Traffic", "pause": "Pause", "resume": "Resume", "time": "Time", diff --git a/meshview/lang/es.json b/meshview/lang/es.json index c362ce5..779147f 100644 --- a/meshview/lang/es.json +++ b/meshview/lang/es.json @@ -2,7 +2,7 @@ "base": { "chat": "Conversaciones", "nodes": "Nodos", - "everything": "Mostrar todo", + "everything": "Tráfico en vivo", "graphs": "Gráficos de la Malla", "net": "Red Semanal", "map": "Mapa en Vivo", @@ -175,7 +175,7 @@ }, "firehose": { - "live_feed": "📡 Flujo en vivo", + "live_feed": "📡 Tráfico en vivo", "pause": "Pausar", "resume": "Reanudar", "time": "Hora", diff --git a/meshview/lang/ru.json b/meshview/lang/ru.json index 34c2f13..9ebd63d 100644 --- a/meshview/lang/ru.json +++ b/meshview/lang/ru.json @@ -2,7 +2,7 @@ "base": { "chat": "Чат", "nodes": "Ноды", - "everything": "Показать все", + "everything": "Трафик в реальном времени", "graphs": "Mesh графики", "net": "Недельная сеть", "map": "Живая карта", @@ -159,7 +159,7 @@ "node_not_found": "Узел не найден в текущем канале!" }, "firehose": { - "live_feed": "📡 Прямой эфир", + "live_feed": "📡 Трафик в реальном времени", "pause": "Пауза", "resume": "Продолжить", "time": "Время", diff --git a/meshview/static/map-inspector.js b/meshview/static/map-inspector.js index 9b9c0b7..d27fbc8 100644 --- a/meshview/static/map-inspector.js +++ b/meshview/static/map-inspector.js @@ -3,11 +3,8 @@ const byId = id => document.getElementById(id); const packetHistory = new Map(); const expandedPackets = new Set(); - const packetHighlight = L.layerGroup().addTo(map); let nodeRequest = null; - let packetRequest = null; let inspectedNodeId = null; - let selectedPacketId = null; let recentPackets = []; let telemetryTime = -1; let paused = false; @@ -23,8 +20,7 @@ const portName = packet => window.PORT_LABEL_MAP[packet.portnum] || `Port ${packet.portnum ?? '?'}`; const dateText = us => us ? new Date(Number(us) / 1000).toLocaleString() : 'Unknown time'; const timeText = us => us ? new Date(Number(us) / 1000).toLocaleTimeString([], {hour:'2-digit', minute:'2-digit', hour12:false}) : '—'; - const destination = packet => Number(packet.to_node_id) === 4294967295 ? 'Broadcast' : - Number(packet.to_node_id) === 1 ? 'MQTT' : nodeName(packet.to_node_id, packet.to_long_name); + async function fetchJson(url, signal) { const response = await fetch(url, {signal}); @@ -94,14 +90,14 @@ if (!recentPackets.length) { note(container, 'No recent packets found.'); return; } container.replaceChildren(); for (const packet of recentPackets) { - const button = document.createElement('button'); button.type = 'button'; + const button = document.createElement('a'); + button.href = `/packet/${encodeURIComponent(packetId(packet))}`; button.className = 'mesh-inspector-packet'; const heading = document.createElement('strong'); heading.textContent = `${portName(packet)} · ${timeText(packet.import_time_us)}`; const preview = document.createElement('small'); preview.textContent = packet.payload || `From ${nodeName(packet.from_node_id, packet.long_name)}`; button.append(heading, preview); - button.addEventListener('click', () => selectMapPacket(packet, false)); container.append(button); } } @@ -160,85 +156,6 @@ byId('node-search').focus({preventScroll:true}); }); - function locationFor(id) { - const node = findNode(id); - return node && !isInvalidCoord(node) ? getNodeLatLng(node) : null; - } - function drawPacketObservation(packet, seen) { - packetHighlight.clearLayers(); - const source = locationFor(packet.from_node_id); - const color = getComputedStyle(document.documentElement).getPropertyValue('--mesh-primary').trim() || '#187346'; - const bounds = []; - if (source) { - L.circleMarker(source, {radius:15, color, weight:3, fillOpacity:0.15, interactive:false}).addTo(packetHighlight); - bounds.push(source); - } - let mapped = 0; - for (const row of seen) { - const position = locationFor(row.node_id); - if (!position) continue; - mapped++; bounds.push(position); - L.circleMarker(position, {radius:12, color, weight:2, fillOpacity:0.2, interactive:false}).addTo(packetHighlight); - } - if (bounds.length) map.fitBounds(L.latLngBounds(bounds), {padding:[35,35],maxZoom:13}); - return mapped; - } - async function selectMapPacket(packet, inspectSender = true) { - packetRequest?.abort(); - const request = new AbortController(); packetRequest = request; - const id = packetId(packet); selectedPacketId = String(id); - const panel = byId('mesh-packet-inspector'); panel.hidden = false; - byId('packet-inspector-title').textContent = `Packet #${id}`; - byId('packet-inspector-summary').textContent = `${nodeName(packet.from_node_id, packet.long_name)} → ${destination(packet)}`; - byId('packet-inspector-meta').textContent = `${portName(packet)} · ${packet.channel || 'Unknown channel'} · ${dateText(packet.import_time_us)}`; - byId('packet-inspector-payload').textContent = packet.payload || '(No decoded payload)'; - byId('packet-inspector-link').href = `/packet/${encodeURIComponent(id)}`; - byId('packet-inspector-observers').replaceChildren(); - byId('packet-inspector-status').textContent = 'Loading gateway observations…'; - document.querySelectorAll('.mesh-traffic-row').forEach(row => { - const selected = row.dataset.packetId === selectedPacketId; - row.classList.toggle('selected', selected); - row.querySelector('.mesh-traffic-select').setAttribute('aria-pressed', String(selected)); - }); - drawPacketObservation(packet, []); - if (inspectSender) openMapNodeInspector(findNode(packet.from_node_id) || {key:packet.from_node_id,long_name:packet.long_name}); - if (matchMedia('(max-width: 700px)').matches) panel.scrollIntoView({block:'nearest'}); - try { - const data = await fetchJson(`/api/packets_seen/${encodeURIComponent(id)}`, request.signal); - if (request.signal.aborted) return; - if (!Array.isArray(data.seen)) throw new Error('Invalid observations response'); - const byGateway = new Map(); - for (const row of data.seen) { - if (Number(row.node_id) === Number(packet.from_node_id)) continue; - const previous = byGateway.get(String(row.node_id)); - if (!previous || Number(row.import_time_us || 0) >= Number(previous.import_time_us || 0)) byGateway.set(String(row.node_id), row); - } - const seen = [...byGateway.values()]; - for (const row of seen) { - const button = document.createElement('button'); button.type = 'button'; button.className = 'mesh-observer'; - button.append(nodeName(row.node_id)); - const signal = document.createElement('small'); - signal.textContent = `RSSI ${row.rx_rssi ?? '—'} dBm · SNR ${row.rx_snr ?? '—'} dB${locationFor(row.node_id) ? '' : ' · No mapped position'}`; - button.append(signal); - button.addEventListener('click', () => openMapNodeInspector(findNode(row.node_id) || {key:row.node_id})); - byId('packet-inspector-observers').append(button); - } - const mapped = drawPacketObservation(packet, seen); - byId('packet-inspector-status').textContent = seen.length ? - `${seen.length} gateway${seen.length === 1 ? '' : 's'} observed · ${mapped} mapped${locationFor(packet.from_node_id) ? '' : ' · Sender has no mapped position'}` : - 'No receiving gateway observations recorded for this packet.'; - } catch (error) { - if (error.name !== 'AbortError') byId('packet-inspector-status').textContent = 'Gateway observations unavailable. Select the packet again to retry.'; - } - } - byId('packet-inspector-close').addEventListener('click', () => { - packetRequest?.abort(); packetHighlight.clearLayers(); selectedPacketId = null; - byId('mesh-packet-inspector').hidden = true; - document.querySelectorAll('.mesh-traffic-row.selected').forEach(row => { - row.classList.remove('selected'); row.querySelector('.mesh-traffic-select').setAttribute('aria-pressed','false'); - }); - }); - function renderTraffic() { const list = byId('map-traffic-list'); const body = byId('map-traffic-body'); @@ -271,7 +188,6 @@ for (const packet of packets) { const id = String(packetId(packet)); const row = document.createElement('tr'); row.className = 'mesh-traffic-row'; row.dataset.packetId = id; - const selected = id === selectedPacketId; row.classList.toggle('selected',selected); const time = document.createElement('td'); time.className = 'mesh-traffic-time'; time.textContent = packet.import_time_us ? new Date(Number(packet.import_time_us)/1000).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit',hour12:false}) : '—'; time.title = dateText(packet.import_time_us); @@ -291,11 +207,7 @@ tag.style.backgroundColor = window.PORT_COLOR_MAP[packet.portnum] || '#6c757d'; tag.title = packet.payload || ''; const number = document.createElement('span'); number.className = 'mesh-traffic-port-number'; number.textContent = `(${packet.portnum ?? '?'})`; - const inspect = document.createElement('button'); inspect.type = 'button'; inspect.className = 'mesh-traffic-select'; - inspect.dataset.trafficFocus = 'map'; inspect.setAttribute('aria-pressed',String(selected)); - inspect.textContent = 'Map'; inspect.title = 'Inspect this packet on the map'; - inspect.addEventListener('click', () => selectMapPacket(packet)); - port.append(tag,number,inspect); + port.append(tag,number); row.append(time,packetCell,nodeCell(packet.from_node_id,packet.long_name),nodeCell(packet.to_node_id,packet.to_long_name,true),channelCell,port); const payloadRow = document.createElement('tr'); payloadRow.className = 'mesh-traffic-payload'; payloadRow.hidden = !expandedPackets.has(id); @@ -307,9 +219,6 @@ payloadRow.hidden = !expandedPackets.has(id); toggle.setAttribute('aria-expanded',String(!payloadRow.hidden)); toggle.textContent = payloadRow.hidden ? '▶' : '▼'; }); - row.addEventListener('click', event => { - if (!event.target.closest('a,button')) selectMapPacket(packet); - }); body.append(row,payloadRow); } list.scrollTop = scrollTop; diff --git a/meshview/templates/base.html b/meshview/templates/base.html index e2c2f87..99c1570 100644 --- a/meshview/templates/base.html +++ b/meshview/templates/base.html @@ -138,6 +138,7 @@ input[type="checkbox"],input[type="radio"] { accent-color:var(--mesh-primary); } .mesh-map-panel #filter-container { display:flex; align-items:center; justify-content:flex-start; flex-wrap:wrap; gap:5px; margin:0; padding:12px; font-size:12px; border-top:1px solid var(--mesh-border); } .mesh-map-panel .filter-checkbox { margin:0 2px 0 10px; accent-color:var(--mesh-primary); } .mesh-map-panel #share-button,.mesh-map-panel #reset-filters-button { background:var(--mesh-soft); color:var(--mesh-primary); margin:5px; padding:8px 12px; font-size:12px; } +.mesh-map-panel #share-button.is-copied { background:var(--mesh-primary-dark); color:white; } .mesh-map-panel #reset-filters-button { color:#546174; background:#f0f3f7; } .mesh-map-panel #map-legend { bottom:80px!important; left:12px; right:auto!important; box-shadow:0 2px 8px #0001; font-size:11px; } .mesh-map-panel #unmapped-packets { bottom:25px; right:10px; width:190px; } @@ -279,7 +280,7 @@ async function initializePage() { for (let i = 0; i < keys.length; i++) { const key = keys[i]; if (site[key] === "true" || site[key] === true) { - items.push(`${escapeHtml(dict[key] || key)}`); + items.push(`${escapeHtml(dict[key] || (key === "everything" ? "Live Traffic" : key))}`); } } items.push('API'); diff --git a/meshview/templates/firehose.html b/meshview/templates/firehose.html index 9b62f2f..5384229 100644 --- a/meshview/templates/firehose.html +++ b/meshview/templates/firehose.html @@ -15,10 +15,10 @@ {% block body %}
-
NETWORK ACTIVITY

Live packets

Follow packet traffic across your mesh network.

View map ↗
+
NETWORK ACTIVITY

Live Traffic

Follow packet traffic across your mesh network.

View map ↗
-

📡 Live Feed

+

📡 Live Traffic

diff --git a/meshview/templates/map-view.html b/meshview/templates/map-view.html index 9f662e0..65607bd 100644 --- a/meshview/templates/map-view.html +++ b/meshview/templates/map-view.html @@ -119,10 +119,17 @@ /* Standalone map uses the full content width. */ .mesh-map-only .mesh-map-panel #map-wrapper { height:calc(100dvh - 290px); min-height:420px; } @media(max-width:700px) { .mesh-map-only .mesh-map-panel #map-wrapper { height:calc(100dvh - 300px); min-height:380px; } } +/* Let the map determine the desktop row height; scroll longer node panels. */ +@media(min-width:701px) { + .mesh-dashboard .mesh-node-panel { contain:size; } + .mesh-dashboard .mesh-node-browser:not([hidden]) { height:100%; display:flex; flex-direction:column; } + .mesh-dashboard .mesh-node-list { flex:1; min-height:0; } + .mesh-dashboard .mesh-node-inspector { max-height:100%; } +} /* Map-side inspection and shared live packet stream. */ -.mesh-node-browser[hidden],.mesh-node-inspector[hidden],.mesh-packet-inspector[hidden] { display:none; } +.mesh-node-browser[hidden],.mesh-node-inspector[hidden] { display:none; } .mesh-node-inspector { max-height:690px; overflow:auto; } -.mesh-inspector-back,.mesh-inspector-close,.mesh-traffic-control { padding:6px 10px; background:var(--mesh-soft); color:var(--mesh-primary); border:1px solid var(--mesh-border); border-radius:5px; font-size:12px; } +.mesh-inspector-back,.mesh-traffic-control { padding:6px 10px; background:var(--mesh-soft); color:var(--mesh-primary); border:1px solid var(--mesh-border); border-radius:5px; font-size:12px; } .mesh-inspector-body { padding:16px; } .mesh-inspector-name { font-size:17px; font-weight:650; overflow-wrap:anywhere; margin:0 0 5px; } .mesh-inspector-meta { color:var(--mesh-muted); font-size:11px; margin:0 0 16px; overflow-wrap:anywhere; } @@ -132,7 +139,7 @@ .mesh-inspector-section { margin:18px 0 10px; font-size:12px; font-weight:650; padding-top:14px; border-top:1px solid var(--mesh-border); } .mesh-inspector-actions { display:flex; flex-wrap:wrap; gap:8px; margin-top:14px; } .mesh-inspector-actions a { font-size:12px; } -.mesh-inspector-packet { display:block; width:100%; text-align:left; padding:10px 0; border:0; border-bottom:1px solid #edf1f6; background:white; color:#334155; font-size:12px; } +.mesh-inspector-packet { display:block; width:100%; text-align:left; padding:10px 0; border:0; border-bottom:1px solid #edf1f6; background:white; color:#334155; font-size:12px; text-decoration:none; } .mesh-inspector-packet:hover { background:var(--mesh-soft); } .mesh-inspector-packet strong,.mesh-inspector-packet small { display:block; } .mesh-inspector-packet small { color:var(--mesh-muted); margin-top:4px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } @@ -141,12 +148,6 @@ .mesh-inspector-telemetry div { padding:10px; background:var(--mesh-soft); border-radius:6px; } .mesh-inspector-telemetry strong { display:block; font-size:18px; color:var(--mesh-primary); } .mesh-inspector-telemetry span { font-size:10px; color:var(--mesh-muted); } -.mesh-packet-inspector { border-top:1px solid var(--mesh-border); } -.mesh-packet-heading { margin:0; font-size:14px; font-weight:600; } -.mesh-packet-payload { max-height:100px; overflow:auto; white-space:pre-wrap; overflow-wrap:anywhere; padding:12px; background:var(--mesh-stripe); border:1px solid var(--mesh-border); border-radius:6px; font:12px/1.6 ui-monospace,monospace; } -.mesh-observer-list { display:flex; gap:8px; flex-wrap:wrap; margin:12px 0; } -.mesh-observer { text-align:left; background:var(--mesh-soft); border:1px solid var(--mesh-border); color:var(--mesh-primary); border-radius:6px; padding:8px 10px; font-size:12px; max-width:100%; overflow-wrap:anywhere; } -.mesh-observer small { display:block; font-size:10px; color:var(--mesh-muted); margin-top:3px; } .mesh-traffic { margin-top:20px; background:white; border:1px solid var(--mesh-border); border-radius:9px; overflow:hidden; } .mesh-traffic-tools { display:flex; gap:8px; align-items:center; flex-wrap:wrap; } .mesh-traffic-status { font-size:11px; color:var(--mesh-primary); } @@ -155,21 +156,21 @@ .mesh-traffic-table { width:100%; min-width:780px; border-collapse:collapse; font-size:13px; color:#334155; } .mesh-traffic-table th { position:sticky; top:0; z-index:1; padding:12px 14px; background:var(--mesh-primary); color:white; font-weight:500; font-size:12px; white-space:nowrap; text-align:left; } .mesh-traffic-table td { padding:11px 14px; border-bottom:1px solid #edf1f6; text-align:left; } -.mesh-traffic-row { cursor:pointer; background:white; } +.mesh-traffic-row { background:white; } .mesh-traffic-row:nth-of-type(4n+3) { background:var(--mesh-stripe); } -.mesh-traffic-row:hover,.mesh-traffic-row.selected { background:var(--mesh-soft); } +.mesh-traffic-row:hover { background:var(--mesh-soft); } .mesh-traffic-table a { color:var(--mesh-primary); text-decoration:none; } .mesh-traffic-table a:hover { text-decoration:underline; } .mesh-traffic-time { white-space:nowrap; font-size:12px; } .mesh-traffic-type { display:inline-block; color:white; padding:3px 7px; border-radius:5px; font-size:10px; font-weight:500; white-space:nowrap; } .mesh-traffic-port-number { color:var(--mesh-muted); font-size:11px; margin:0 6px; } -.mesh-traffic-select,.mesh-traffic-toggle { border:1px solid var(--mesh-border); background:var(--mesh-soft); color:var(--mesh-primary); border-radius:5px; font-size:11px; padding:4px 7px; white-space:nowrap; } +.mesh-traffic-toggle { border:1px solid var(--mesh-border); background:var(--mesh-soft); color:var(--mesh-primary); border-radius:5px; font-size:11px; padding:4px 7px; white-space:nowrap; } .mesh-traffic-toggle { margin-right:7px; } .mesh-traffic-payload[hidden] { display:none; } .mesh-traffic-payload td { background:var(--mesh-soft); } .mesh-traffic-payload pre { margin:0; max-height:180px; overflow:auto; white-space:pre-wrap; overflow-wrap:anywhere; font:12px/1.7 ui-monospace,monospace; } #map-traffic-channel { border:1px solid var(--mesh-border); border-radius:5px; color:#334155; background:white; font-size:12px; padding:6px 8px; max-width:180px; } -:is(.mesh-traffic-select,.mesh-traffic-toggle,.mesh-traffic-table a,#map-traffic-channel,.mesh-observer,.mesh-inspector-packet,.mesh-inspector-back,.mesh-inspector-close,.mesh-traffic-control):focus-visible { outline:2px solid var(--mesh-primary); outline-offset:2px; } +:is(.mesh-traffic-toggle,.mesh-traffic-table a,#map-traffic-channel,.mesh-inspector-packet,.mesh-inspector-back,.mesh-traffic-control):focus-visible { outline:2px solid var(--mesh-primary); outline-offset:2px; } .mesh-traffic-note { padding:8px 18px; border-top:1px solid var(--mesh-border); color:var(--mesh-muted); font-size:11px; margin:0; } @media(max-width:700px) { .mesh-node-inspector { max-height:none; } .mesh-traffic .mesh-panel-title { align-items:flex-start; flex-direction:column; } } @@ -257,6 +258,7 @@ Show MQTT Gateways Only
+{% if not dashboard %}
- -{% if dashboard %} - {% endif %} + + {% if dashboard %}
@@ -289,7 +280,7 @@
TimePacket IDFromToChannelPort
Loading recent network traffic…
-

Latest 30 packets · expand the arrow to view payloads · select a row or Map to inspect gateway observations.

+

Latest 30 packets · expand the arrow to view payloads · follow packet IDs or node names for details.

{% endif %} @@ -1185,19 +1176,22 @@ function updateNodeVisibility(){ SHARE / RESET ====================================================== */ +let shareViewResetTimer = null; function shareCurrentView() { const c = map.getCenter(); const url = `${window.location.origin}/map?lat=${c.lat.toFixed(6)}&lng=${c.lng.toFixed(6)}&zoom=${map.getZoom()}`; navigator.clipboard.writeText(url).then(()=>{ const btn = document.getElementById('share-button'); - const old = btn.textContent; + if (!btn.classList.contains('is-copied')) btn.dataset.shareLabel = btn.textContent; + clearTimeout(shareViewResetTimer); btn.textContent = '✓ ' + (mapTranslations.link_copied || 'Link Copied!'); - btn.style.backgroundColor = 'var(--mesh-primary-dark)'; + btn.classList.add('is-copied'); - setTimeout(()=>{ - btn.textContent = old; - btn.style.backgroundColor = 'var(--mesh-primary)'; + shareViewResetTimer = setTimeout(()=>{ + btn.textContent = btn.dataset.shareLabel; + btn.classList.remove('is-copied'); + delete btn.dataset.shareLabel; }, 2000); }); }