mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-10-31 03:01:58 +01:00
quick map fixes
This commit is contained in:
1 parent
ef926798fe
commit
ec67254472
7 files changed
+40
-136
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Время",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(`<a href="${urls[i]}"${(location.pathname === urls[i] || (key === "map" && location.pathname === "/map.html")) ? ' class="active" aria-current="page"' : ""}>${escapeHtml(dict[key] || key)}</a>`);
|
||||
items.push(`<a href="${urls[i]}"${(location.pathname === urls[i] || (key === "map" && location.pathname === "/map.html")) ? ' class="active" aria-current="page"' : ""}>${escapeHtml(dict[key] || (key === "everything" ? "Live Traffic" : key))}</a>`);
|
||||
}
|
||||
}
|
||||
items.push('<a href="/api.html">API</a>');
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
|
||||
{% block body %}
|
||||
<div class="container">
|
||||
<header class="data-heading"><div><span class="mesh-eyebrow">NETWORK ACTIVITY</span><h1>Live packets</h1><p>Follow packet traffic across your mesh network.</p></div><a class="mesh-secondary-button" href="/map">View map ↗</a></header>
|
||||
<header class="data-heading"><div><span class="mesh-eyebrow">NETWORK ACTIVITY</span><h1>Live Traffic</h1><p>Follow packet traffic across your mesh network.</p></div><a class="mesh-secondary-button" href="/map">View map ↗</a></header>
|
||||
|
||||
<form class="data-toolbar firehose-controls">
|
||||
<h2 class="mb-0" data-translate-lang="live_feed">📡 Live Feed</h2>
|
||||
<h2 class="mb-0" data-translate-lang="live_feed">📡 Live Traffic</h2>
|
||||
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<label for="channel-filter" class="mb-0" data-translate-lang="channel">Channel</label>
|
||||
|
||||
@@ -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; } }
|
||||
</style>
|
||||
@@ -257,6 +258,7 @@
|
||||
<span data-translate-lang="show_mqtt_only">Show MQTT Gateways Only</span>
|
||||
</div>
|
||||
|
||||
{% if not dashboard %}
|
||||
<div style="text-align:center;margin-top:5px;">
|
||||
<button id="share-button" onclick="shareCurrentView()" data-translate-lang="share_view">
|
||||
🔗 Share This View
|
||||
@@ -265,21 +267,10 @@
|
||||
↺ Reset Filters To Defaults
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{% if dashboard %}
|
||||
<section id="mesh-packet-inspector" class="mesh-packet-inspector" aria-label="Selected packet" hidden>
|
||||
<div class="mesh-panel-title"><h2 id="packet-inspector-title">Packet observations</h2><button id="packet-inspector-close" class="mesh-inspector-close" type="button">Clear</button></div>
|
||||
<div class="mesh-inspector-body">
|
||||
<p id="packet-inspector-summary" class="mesh-packet-heading"></p>
|
||||
<p id="packet-inspector-meta" class="mesh-inspector-note"></p>
|
||||
<pre id="packet-inspector-payload" class="mesh-packet-payload"></pre>
|
||||
<div id="packet-inspector-observers" class="mesh-observer-list" aria-live="polite"></div>
|
||||
<p id="packet-inspector-status" class="mesh-inspector-note" role="status"></p>
|
||||
<a id="packet-inspector-link" href="/firehose">Full packet details ↗</a>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
</section>
|
||||
{% if dashboard %}
|
||||
</div>
|
||||
@@ -289,7 +280,7 @@
|
||||
<div id="map-traffic-list" class="mesh-traffic-list" role="region" aria-label="Live packet table" tabindex="0">
|
||||
<table class="mesh-traffic-table"><thead><tr><th>Time</th><th>Packet ID</th><th>From</th><th>To</th><th>Channel</th><th>Port</th></tr></thead><tbody id="map-traffic-body"><tr><td colspan="6" class="mesh-empty">Loading recent network traffic…</td></tr></tbody></table>
|
||||
</div>
|
||||
<p class="mesh-traffic-note">Latest 30 packets · expand the arrow to view payloads · select a row or Map to inspect gateway observations.</p>
|
||||
<p class="mesh-traffic-note">Latest 30 packets · expand the arrow to view payloads · follow packet IDs or node names for details.</p>
|
||||
</section>
|
||||
|
||||
{% 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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user