diff --git a/meshview/static/map-inspector.js b/meshview/static/map-inspector.js
index d27fbc8..899260d 100644
--- a/meshview/static/map-inspector.js
+++ b/meshview/static/map-inspector.js
@@ -3,6 +3,9 @@
const byId = id => document.getElementById(id);
const packetHistory = new Map();
const expandedPackets = new Set();
+ const displayedPackets = new Set();
+ const packetArrivals = new Map();
+ let trafficLoaded = false;
let nodeRequest = null;
let inspectedNodeId = null;
let recentPackets = [];
@@ -220,6 +223,11 @@
toggle.setAttribute('aria-expanded',String(!payloadRow.hidden)); toggle.textContent = payloadRow.hidden ? '▶' : '▼';
});
body.append(row,payloadRow);
+ if (!displayedPackets.has(id)) {
+ displayedPackets.add(id);
+ packetArrivals.set(id,Date.now());
+ }
+ if (packetArrivals.has(id)) markNewPacketArrival(row,packetArrivals.get(id));
}
list.scrollTop = scrollTop;
if (focusedId && focusedControl) {
@@ -243,6 +251,7 @@
for (const packet of event.detail) {
if (!packetHistory.has(String(packetId(packet)))) added = true;
packetHistory.set(String(packetId(packet)),packet);
+ if (!trafficLoaded) displayedPackets.add(String(packetId(packet)));
if (packet.channel && ![...byId('map-traffic-channel').options].some(o => o.value === packet.channel)) {
byId('map-traffic-channel').add(new Option(packet.channel,packet.channel));
}
@@ -253,10 +262,12 @@
if (String(packet.from_node_id) === inspectedNodeId && Number(packet.portnum) === 67) renderTelemetry(packet);
}
}
+ trafficLoaded = true;
if (packetHistory.size > 30) {
const sorted = [...packetHistory.values()].sort((a,b) => packetTime(b)-packetTime(a));
for (const packet of sorted.slice(30)) {
packetHistory.delete(String(packetId(packet))); expandedPackets.delete(String(packetId(packet)));
+ displayedPackets.delete(String(packetId(packet))); packetArrivals.delete(String(packetId(packet)));
}
}
if (!paused && added) renderTraffic();
diff --git a/meshview/static/packet-arrival.js b/meshview/static/packet-arrival.js
new file mode 100644
index 0000000..76d93b1
--- /dev/null
+++ b/meshview/static/packet-arrival.js
@@ -0,0 +1,12 @@
+/* A short, shared arrival cue for the two live packet tables. */
+window.markNewPacketArrival = function (row, startedAt = Date.now()) {
+ const elapsed = Math.max(0, Date.now() - startedAt);
+ const remaining = 1000 - elapsed;
+ if (remaining <= 0) return;
+ row.classList.add('packet-arrival-glow');
+ row.style.setProperty('--packet-glow-delay', `${-elapsed}ms`);
+ setTimeout(() => {
+ row.classList.remove('packet-arrival-glow');
+ row.style.removeProperty('--packet-glow-delay');
+ }, remaining);
+};
diff --git a/meshview/templates/base.html b/meshview/templates/base.html
index 99c1570..23ce482 100644
--- a/meshview/templates/base.html
+++ b/meshview/templates/base.html
@@ -15,6 +15,7 @@
+
{% block head %}{% endblock %}
@@ -90,6 +91,23 @@ input[type="checkbox"],input[type="radio"] { accent-color:var(--mesh-primary); }
.form-check-input:focus { border-color:#91bda1; box-shadow:0 0 0 .25rem rgba(var(--mesh-primary-rgb),.2); }
.page-link { --bs-pagination-color:var(--mesh-primary); --bs-pagination-hover-color:var(--mesh-primary-dark); --bs-pagination-active-bg:var(--mesh-primary); --bs-pagination-active-border-color:var(--mesh-primary); }
[data-bs-theme="light"] body { background:var(--mesh-page); color:#243247; font-family:Inter,system-ui,-apple-system,"Segoe UI",sans-serif; }
+/* Tint whole cells: an edge-only shadow is too subtle on wide packet tables. */
+.packet-arrival-glow > td {
+ animation:packet-arrival-beat 1s ease-in-out 1;
+ animation-delay:var(--packet-glow-delay,0ms);
+ box-shadow:inset 0 0 14px rgba(24,115,70,.12);
+}
+.packet-arrival-glow > td:first-child {
+ box-shadow:inset 4px 0 0 var(--mesh-primary),inset 0 0 14px rgba(24,115,70,.12);
+}
+@keyframes packet-arrival-beat {
+ 0%,100% { background-color:transparent; }
+ 45% { background-color:#ccebd7; }
+ 75% { background-color:#e2f3e8; }
+}
+@media(prefers-reduced-motion:reduce) {
+ .packet-arrival-glow > td { animation:none; background:#d8efdf; }
+}
.mesh-header { background:var(--mesh-primary); color:white; box-shadow:0 2px 8px #174b2d18; }
.mesh-header-inner { max-width:1600px; margin:auto; padding:0 28px; min-height:56px; display:flex; align-items:center; gap:32px; }
.mesh-brand { display:flex; align-items:center; gap:10px; font-size:19px; font-weight:700; color:white; text-decoration:none; flex-shrink:0; }
diff --git a/meshview/templates/firehose.html b/meshview/templates/firehose.html
index 5384229..30aa2a7 100644
--- a/meshview/templates/firehose.html
+++ b/meshview/templates/firehose.html
@@ -174,6 +174,7 @@ function logPacketTimes(packet) {
FIREHOSE FETCHING
====================================================== */
let lastImportTimeUs = null;
+let firehoseLoaded = false;
let updatesPaused = false;
let updateInterval = 3000;
let selectedChannel = "";
@@ -207,6 +208,7 @@ async function loadChannels() {
}
function resetFirehose() {
+ firehoseLoaded = false;
lastImportTimeUs = null;
document.getElementById("packet_list").innerHTML = "";
}
@@ -231,6 +233,8 @@ async function fetchUpdates() {
const data = await res.json();
const packets = data.packets || [];
+ const highlightNewPackets = firehoseLoaded;
+ firehoseLoaded = true;
if (!packets.length) return;
const list = document.getElementById("packet_list");
@@ -326,6 +330,7 @@ async function fetchUpdates() {
`;
list.insertAdjacentHTML("afterbegin", html);
+ if (highlightNewPackets) markNewPacketArrival(list.firstElementChild);
}
// Limit table size
diff --git a/meshview/templates/map-view.html b/meshview/templates/map-view.html
index 65607bd..3481512 100644
--- a/meshview/templates/map-view.html
+++ b/meshview/templates/map-view.html
@@ -157,7 +157,6 @@
.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 { background:white; }
-.mesh-traffic-row:nth-of-type(4n+3) { background:var(--mesh-stripe); }
.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; }