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 %}
Follow packet traffic across your mesh network.
Follow packet traffic across your mesh network.
| Time | Packet ID | From | To | Channel | Port |
|---|---|---|---|---|---|
| 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); }); }