diff --git a/meshview/lang/en.json b/meshview/lang/en.json
index 2936f62..b063000 100644
--- a/meshview/lang/en.json
+++ b/meshview/lang/en.json
@@ -64,10 +64,9 @@
},
"net": {
- "number_of_checkins": "Number of Check-ins:",
- "view_packet_details": "View packet details",
- "view_all_packets_from_node": "View all packets from this node",
- "no_packets_found": "No packets found."
+ "net_title": "Weekly Net:",
+ "total_messages": "Number of messages:",
+ "view_packet_details": "More details"
},
"map": {
@@ -157,17 +156,17 @@
"graph": "Graph"
},
"node": {
- "specifications": "Specifications:",
- "node_id": "Node ID:",
- "long_name": "Long Name:",
- "short_name": "Short Name:",
- "hw_model": "Hardware Model:",
- "firmware": "Firmware:",
- "role": "Role:",
- "channel": "Channel:",
- "latitude": "Latitude:",
- "longitude": "Longitude:",
- "last_update": "Last Update:",
+ "specifications": "Specifications",
+ "node_id": "Node ID",
+ "long_name": "Long Name",
+ "short_name": "Short Name",
+ "hw_model": "Hardware Model",
+ "firmware": "Firmware",
+ "role": "Role",
+ "channel": "Channel",
+ "latitude": "Latitude",
+ "longitude": "Longitude",
+ "last_update": "Last Update",
"battery_voltage": "Battery & Voltage",
"air_channel": "Air & Channel Utilization",
"environment": "Environment Metrics",
diff --git a/meshview/store.py b/meshview/store.py
index db85028..5392b45 100644
--- a/meshview/store.py
+++ b/meshview/store.py
@@ -260,11 +260,12 @@ async def get_node_traffic(node_id: int):
return []
-async def get_nodes(role=None, channel=None, hw_model=None, days_active=None):
+async def get_nodes(node_id=None, role=None, channel=None, hw_model=None, days_active=None):
"""
Fetches nodes from the database based on optional filtering criteria.
Parameters:
+ node_id
role (str, optional): The role of the node (converted to uppercase for consistency).
channel (str, optional): The communication channel associated with the node.
hw_model (str, optional): The hardware model of the node.
@@ -280,6 +281,8 @@ async def get_nodes(role=None, channel=None, hw_model=None, days_active=None):
query = select(Node)
# Apply filters based on provided parameters
+ if node_id is not None:
+ query = query.where(Node.node_id == node_id)
if role is not None:
query = query.where(Node.role == role.upper()) # Ensure role is uppercase
if channel is not None:
diff --git a/meshview/templates/node.html b/meshview/templates/node.html
index 97809f3..7ed1434 100644
--- a/meshview/templates/node.html
+++ b/meshview/templates/node.html
@@ -137,25 +137,25 @@
- 📡 Specifications:
+ 📡 Specifications::
-
Node ID: —
-
Long Name: —
-
Short Name: —
+
Node ID:—
+
Long Name: —
+
Short Name: —
-
Hardware Model: —
-
Firmware: —
-
Role: —
+
Hardware Model: —
+
Firmware: —
+
Role: —
-
Channel: —
-
Latitude: —
-
Longitude: —
+
Channel: —
+
Latitude: —
+
Longitude: —
-
Last Update: —
+
Last Update: —
@@ -253,6 +253,9 @@ async function loadTranslationsNode() {
nodeTranslations = await res.json();
applyTranslationsNode(nodeTranslations);
+
+ // Broadcast label can be set here since translations are now loaded
+ nodeMap[4294967295] = nodeTranslations.all_broadcast || "All";
} catch (err) {
console.error("Node translation load failed", err);
}
@@ -273,57 +276,134 @@ function applyTranslationsNode(dict, root=document) {
}
/* ======================================================
- NODE LOGIC
+ POPUP + TIME HELPERS
====================================================== */
-let nodeMap = {}, nodePositions = {}, map, markers = {};
+function makeNodePopup(node) {
+ return `
+
+
+ ${node.long_name || node.short_name || node.node_id}
+
+ ${node.short_name ? ` (${node.short_name})` : ""}
+
+
+ ${nodeTranslations.node_id || "Node ID"}:
+ ${node.node_id}
+
+
+ ${nodeTranslations.hw_model || "HW Model"}:
+ ${node.hw_model ?? "—"}
+
+
+ ${nodeTranslations.channel || "Channel"}:
+ ${node.channel ?? "—"}
+
+
+ ${nodeTranslations.role || "Role"}:
+ ${node.role ?? "—"}
+
+
+ ${nodeTranslations.firmware || "Firmware"}:
+ ${node.firmware ?? "—"}
+
+
+ ${nodeTranslations.last_update || "Last Update"}:
+ ${formatLastSeen(node.last_seen_us)}
+
+ `;
+}
+
+function formatLastSeen(us) {
+ if (!us) return "—";
+ const d = new Date(us / 1000);
+ return d.toLocaleString([], {
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit"
+ });
+}
+
+function formatLocalTime(us){
+ return new Date(us / 1000).toLocaleString([], {
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit"
+ });
+}
+
+/* ======================================================
+ GLOBALS
+ ====================================================== */
+
+let nodeMap = {}; // node_id -> label
+let nodePositions = {}; // node_id -> [lat, lon]
+let nodeCache = {}; // node_id -> full node object
+let currentNode = null;
+
+let map, markers = {};
let chartData = {}, neighborData = { ids:[], names:[], snrs:[] };
-let allNodes = [];
+
let fromNodeId = new URLSearchParams(window.location.search).get("from_node_id");
if (!fromNodeId) {
const parts = window.location.pathname.split("/");
fromNodeId = parts[parts.length - 1];
}
-// --- Load nodes ---
-async function loadNodes(){
+/* ======================================================
+ API HELPERS (USE /api/nodes?node_id=...)
+ ====================================================== */
+
+async function fetchNodeFromApi(nodeId) {
+ if (nodeCache[nodeId]) return nodeCache[nodeId];
+
try {
- const res = await fetch("/api/nodes");
+ const res = await fetch(`/api/nodes?node_id=${encodeURIComponent(nodeId)}`);
if (!res.ok) {
- console.error("Failed /api/nodes", res.status);
- return;
+ console.error("Failed /api/nodes?node_id=", nodeId, res.status);
+ return null;
}
const data = await res.json();
- allNodes = data.nodes || [];
+ const node = (data.nodes || [])[0];
+ if (!node) return null;
- for (const n of allNodes) {
- const name = n.long_name || n.short_name || n.id || n.node_id;
- nodeMap[n.node_id] = name;
+ nodeCache[nodeId] = node;
- if (n.last_lat && n.last_long)
- nodePositions[n.node_id] = [n.last_lat / 1e7, n.last_long / 1e7];
+ const label = node.long_name || node.short_name || node.id || node.node_id;
+ nodeMap[node.node_id] = label;
+
+ if (node.last_lat && node.last_long) {
+ nodePositions[node.node_id] = [node.last_lat / 1e7, node.last_long / 1e7];
}
- nodeMap[4294967295] = nodeTranslations.all_broadcast || "All";
-
- document.getElementById("nodeLabel").textContent = nodeMap[fromNodeId] || fromNodeId;
-
+ return node;
} catch (err) {
- console.error("Error loading nodes:", err);
+ console.error("Error fetching node", nodeId, err);
+ return null;
}
}
-// --- Load Node Info ---
+/* ======================================================
+ LOAD NODE INFO (SINGLE NODE)
+ ====================================================== */
+
async function loadNodeInfo(){
try {
- if (!allNodes.length) await loadNodes();
+ const node = await fetchNodeFromApi(fromNodeId);
+ currentNode = node;
- const node = allNodes.find(n => String(n.node_id) === String(fromNodeId));
if (!node) {
document.getElementById("node-info").style.display = "none";
return;
}
+ // Label in title
+ document.getElementById("nodeLabel").textContent =
+ nodeMap[fromNodeId] || fromNodeId;
+
+ // Info card
document.getElementById("info-node-id").textContent = node.node_id ?? "—";
document.getElementById("info-long-name").textContent = node.long_name ?? "—";
document.getElementById("info-short-name").textContent = node.short_name ?? "—";
@@ -332,51 +412,54 @@ async function loadNodeInfo(){
document.getElementById("info-role").textContent = node.role ?? "—";
document.getElementById("info-channel").textContent = node.channel ?? "—";
- document.getElementById("info-lat").textContent = node.last_lat ? (node.last_lat / 1e7).toFixed(6) : "—";
- document.getElementById("info-lon").textContent = node.last_long ? (node.last_long / 1e7).toFixed(6) : "—";
+ document.getElementById("info-lat").textContent =
+ node.last_lat ? (node.last_lat / 1e7).toFixed(6) : "—";
+ document.getElementById("info-lon").textContent =
+ node.last_long ? (node.last_long / 1e7).toFixed(6) : "—";
let lastSeen = "—";
if (node.last_seen_us) {
- lastSeen = new Date(node.last_seen_us / 1000).toLocaleString([], {
- month: "2-digit",
- day: "2-digit",
- hour: "2-digit",
- minute: "2-digit"
- });
+ lastSeen = formatLastSeen(node.last_seen_us);
}
-
document.getElementById("info-last-update").textContent = lastSeen;
-
-
} catch (err) {
console.error("Failed to load node info:", err);
document.getElementById("node-info").style.display = "none";
}
}
-function nodeLink(id) {
+/* ======================================================
+ NODE LINK RENDERING
+ ====================================================== */
- // Broadcast to everyone (ALL)
+function nodeLink(id, labelOverride = null) {
+ // Broadcast
if (id === 4294967295) {
return `
${nodeTranslations.all_broadcast || "All"}
`;
}
- // Direct to MQTT broker
+ // Direct to MQTT
if (id === 1) {
return `
${nodeTranslations.direct_to_mqtt || "Direct to MQTT"}
`;
}
- // Normal node → clickable link
- return `
- ${nodeMap[id] || id}
+ // Normal node
+ const label = labelOverride || nodeMap[id] || id;
+
+ return `
+ ${label}
`;
}
+
+/* ======================================================
+ PORT LABELS
+ ====================================================== */
+
function portLabel(p) {
const PORT_COLOR_MAP = {
0: "#6c757d",
@@ -426,16 +509,10 @@ function portLabel(p) {
`;
}
-function formatLocalTime(us){
- return new Date(us / 1000).toLocaleString([], {
- month: "2-digit",
- day: "2-digit",
- hour: "2-digit",
- minute: "2-digit"
- });
-}
+/* ======================================================
+ MAP SETUP
+ ====================================================== */
-// --- Map ---
function initMap(){
map = L.map('map', { preferCanvas:true }).setView([37.7749, -122.4194], 8);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
@@ -450,30 +527,45 @@ function hideMap(){
}
}
-function addMarker(id, lat, lon, label, color = "red"){
+function addMarker(id, lat, lon, color = "red", node = null) {
if (!map) return;
if (isNaN(lat) || isNaN(lon)) return;
+
nodePositions[id] = [lat, lon];
+
+ if (!node) {
+ node = nodeCache[id] || null;
+ }
+
+ const popupHtml = node ? makeNodePopup(node) : `
${id}`;
+
const m = L.circleMarker([lat, lon], {
- radius: 5,
+ radius: 6,
color,
fillColor: color,
fillOpacity: 1
- }).addTo(map).bindPopup(label);
+ }).addTo(map).bindPopup(popupHtml);
+
markers[id] = m;
m.bringToFront();
}
-function drawNeighbors(src, nids){
+async function drawNeighbors(src, nids){
if (!map) return;
- const s = nodePositions[src];
- if (!s) return;
+ const srcPos = nodePositions[src];
+ if (!srcPos) return;
+
for (const nid of nids) {
- const pos = nodePositions[nid];
- if (pos) {
- addMarker(nid, pos[0], pos[1], nodeMap[nid] || nid, "blue");
- L.polyline([s, pos], { color:'gray', weight:1 }).addTo(map);
- }
+ const neighbor = await fetchNodeFromApi(nid);
+ if (!neighbor || !neighbor.last_lat || !neighbor.last_long) continue;
+
+ const lat = neighbor.last_lat / 1e7;
+ const lon = neighbor.last_long / 1e7;
+
+ addMarker(nid, lat, lon, "blue", neighbor);
+
+ const dstPos = [lat, lon];
+ L.polyline([srcPos, dstPos], { color:'gray', weight:1 }).addTo(map);
}
}
@@ -482,15 +574,19 @@ function ensureMapVisible(){
requestAnimationFrame(() => {
map.invalidateSize();
const group = L.featureGroup(Object.values(markers));
- if (group.getLayers().length > 0)
- map.fitBounds(group.getBounds(), {
- padding: [20, 20],
- maxZoom: 11 //
- });
+ if (group.getLayers().length > 0) {
+ map.fitBounds(group.getBounds(), {
+ padding: [20, 20],
+ maxZoom: 11
+ });
+ }
});
}
-// --- Position Track (last 50 position packets) ---
+/* ======================================================
+ POSITION TRACK (portnum=3)
+ ====================================================== */
+
async function loadTrack(){
try {
const url = new URL("/api/packets", window.location.origin);
@@ -526,7 +622,6 @@ async function loadTrack(){
}
if (!points.length) {
- // No position packets -> hide map entirely
hideMap();
return;
}
@@ -538,7 +633,6 @@ async function loadTrack(){
const latest = points[points.length - 1];
nodePositions[fromNodeId] = [latest.lat, latest.lon];
- // Ensure map exists
if (!map) {
initMap();
}
@@ -549,57 +643,28 @@ async function loadTrack(){
weight: 2
}).addTo(map);
- // First + last markers only
const first = points[0];
const last = points[points.length - 1];
+ const node = currentNode || nodeCache[fromNodeId] || null;
+
const startMarker = L.circleMarker([first.lat, first.lon], {
radius: 6,
color: 'green',
fillColor: 'green',
fillOpacity: 1
- }).addTo(map).bindPopup("Start");
+ }).addTo(map).bindPopup(node ? makeNodePopup(node) : "Start");
+
const endMarker = L.circleMarker([last.lat, last.lon], {
radius: 6,
color: 'red',
fillColor: 'red',
fillOpacity: 1
- }).addTo(map).bindPopup("Latest");
+ }).addTo(map).bindPopup(node ? makeNodePopup(node) : "Latest");
markers["__track_start"] = startMarker;
markers["__track_end"] = endMarker;
- // Hover tooltip on the track: nearest point to cursor
- trackLine.on('mousemove', function(e){
- const { lat, lng } = e.latlng;
- let bestIdx = 0;
- let bestDist = Infinity;
- for (let i = 0; i < latlngs.length; i++) {
- const dLat = latlngs[i][0] - lat;
- const dLng = latlngs[i][1] - lng;
- const dist = dLat * dLat + dLng * dLng;
- if (dist < bestDist) {
- bestDist = dist;
- bestIdx = i;
- }
- }
- const p = points[bestIdx];
- const dt = new Date(p.time / 1000);
- const ts = dt.toLocaleString([], {
- month:"2-digit",
- day:"2-digit",
- hour:"2-digit",
- minute:"2-digit"
- });
- const tooltipHtml =
- `${ts}
` +
- `Lat: ${p.lat.toFixed(6)}
` +
- `Lon: ${p.lon.toFixed(6)}`;
-
- trackLine.bindTooltip(tooltipHtml, { sticky:true }).openTooltip(e.latlng);
- });
-
- // Fit map to full track
map.fitBounds(trackLine.getBounds(), { padding:[20,20] });
} catch (err) {
@@ -608,7 +673,10 @@ async function loadTrack(){
}
}
-// --- Packets (for table + neighbor map overlay) ---
+/* ======================================================
+ PACKETS TABLE + NEIGHBOR OVERLAY
+ ====================================================== */
+
async function loadPackets(){
const url = new URL("/api/packets", window.location.origin);
url.searchParams.set("from_node_id", fromNodeId);
@@ -621,40 +689,45 @@ async function loadPackets(){
const list = document.getElementById("packet_list");
for (const pkt of (data.packets || []).reverse()) {
- const safePayload = (pkt.payload || "").replace(/[<>]/g, m => m == "<" ? "<" : ">");
+ const safePayload = (pkt.payload || "").replace(/[<>]/g, m => m === "<" ? "<" : ">");
const localTime = formatLocalTime(pkt.import_time_us);
const fromCell = nodeLink(pkt.from_node_id);
- const toCell = nodeLink(pkt.to_node_id);
+ const toCell = nodeLink(pkt.to_node_id, pkt.to_long_name);
- // Neighbor packets still update map overlay if map exists
+ // Neighbor packets (port 71) → draw neighbors on map
if (pkt.portnum === 71 && pkt.payload) {
const nids = [];
const re = /neighbors\s*\{\s*node_id:\s*(\d+)/g;
let m;
- while ((m = re.exec(pkt.payload)) !== null) nids.push(parseInt(m[1]));
- drawNeighbors(pkt.from_node_id, nids);
+ while ((m = re.exec(pkt.payload)) !== null) {
+ nids.push(parseInt(m[1]));
+ }
+ if (nids.length && map) {
+ await drawNeighbors(pkt.from_node_id, nids);
+ }
}
let inlineLinks = "";
- if (pkt.portnum === 3 && pkt.payload) {
- const latMatch = pkt.payload.match(/latitude_i:\s*(-?\d+)/);
- const lonMatch = pkt.payload.match(/longitude_i:\s*(-?\d+)/);
+ // Position link (Google Maps)
+ if (pkt.portnum === 3 && pkt.payload) {
+ const latMatch = pkt.payload.match(/latitude_i:\s*(-?\d+)/);
+ const lonMatch = pkt.payload.match(/longitude_i:\s*(-?\d+)/);
- if (latMatch && lonMatch) {
- const lat = parseFloat(latMatch[1]) / 1e7;
- const lon = parseFloat(lonMatch[1]) / 1e7;
- inlineLinks += `
📍`;
+ if (latMatch && lonMatch) {
+ const lat = parseFloat(latMatch[1]) / 1e7;
+ const lon = parseFloat(lonMatch[1]) / 1e7;
+ inlineLinks += `
📍`;
+ }
}
- }
-
- if (pkt.portnum === 70) {
- let traceId = pkt.id;
- const match = pkt.payload?.match(/ID:\s*(\d+)/i);
- if (match) traceId = match[1];
- inlineLinks += `
⮕`;
- }
+ // Traceroute link
+ if (pkt.portnum === 70) {
+ let traceId = pkt.id;
+ const match = pkt.payload?.match(/ID:\s*(\d+)/i);
+ if (match) traceId = match[1];
+ inlineLinks += `
⮕`;
+ }
list.insertAdjacentHTML("afterbegin", `
@@ -670,11 +743,15 @@ async function loadPackets(){
}
}
-// --- Telemetry Charts (battery / air / env) ---
+/* ======================================================
+ TELEMETRY CHARTS (portnum=67)
+ ====================================================== */
+
async function loadTelemetryCharts(){
const url = `/api/packets?portnum=67&from_node_id=${fromNodeId}`;
const res = await fetch(url);
if (!res.ok) return;
+
const data = await res.json();
const packets = data.packets || [];
chartData = {
@@ -690,13 +767,29 @@ async function loadTelemetryCharts(){
chartData.times.push(
t.toLocaleString([], { month:"2-digit", day:"2-digit", hour:"2-digit", minute:"2-digit" })
);
- chartData.battery.push(parseFloat(pl.match(/battery_level:\s*([\d.]+)/)?.[1] || NaN));
- chartData.voltage.push(parseFloat(pl.match(/voltage:\s*([\d.]+)/)?.[1] || NaN));
- chartData.airUtil.push(parseFloat(pl.match(/air_util_tx:\s*([\d.]+)/)?.[1] || NaN));
- chartData.chanUtil.push(parseFloat(pl.match(/channel_utilization:\s*([\d.]+)/)?.[1] || NaN));
- chartData.temperature.push(parseFloat(pl.match(/temperature:\s*([\d.]+)/)?.[1] || NaN));
- chartData.humidity.push(parseFloat(pl.match(/relative_humidity:\s*([\d.]+)/)?.[1] || NaN));
- chartData.pressure.push(parseFloat(pl.match(/barometric_pressure:\s*([\d.]+)/)?.[1] || NaN));
+
+ // Matches your payload exactly (inside device_metrics {})
+ chartData.battery.push(
+ parseFloat(pl.match(/battery_level:\s*([\d.]+)/)?.[1] || NaN)
+ );
+ chartData.voltage.push(
+ parseFloat(pl.match(/voltage:\s*([\d.]+)/)?.[1] || NaN)
+ );
+ chartData.airUtil.push(
+ parseFloat(pl.match(/air_util_tx:\s*([\d.]+)/)?.[1] || NaN)
+ );
+ chartData.chanUtil.push(
+ parseFloat(pl.match(/channel_utilization:\s*([\d.]+)/)?.[1] || NaN)
+ );
+ chartData.temperature.push(
+ parseFloat(pl.match(/temperature:\s*([\d.]+)/)?.[1] || NaN)
+ );
+ chartData.humidity.push(
+ parseFloat(pl.match(/relative_humidity:\s*([\d.]+)/)?.[1] || NaN)
+ );
+ chartData.pressure.push(
+ parseFloat(pl.match(/barometric_pressure:\s*([\d.]+)/)?.[1] || NaN)
+ );
}
const hasBattery = chartData.battery.some(v => !isNaN(v));
@@ -812,7 +905,10 @@ async function loadTelemetryCharts(){
});
}
-// --- Neighbor chart (latest portnum=71) ---
+/* ======================================================
+ NEIGHBOR CHART (portnum=71, latest)
+ ====================================================== */
+
async function loadNeighborChart(){
const url = `/api/packets?portnum=71&from_node_id=${fromNodeId}&limit=1`;
const res = await fetch(url);
@@ -831,6 +927,7 @@ async function loadNeighborChart(){
const re = /neighbors\s*\{\s*([^}]+)\}/g;
let m;
const ids = [], names = [], snrs = [];
+
while ((m = re.exec(payload)) !== null) {
const block = m[1];
const idMatch = block.match(/node_id:\s*(\d+)/);
@@ -840,6 +937,11 @@ async function loadNeighborChart(){
const nid = parseInt(idMatch[1], 10);
const snr = parseFloat(snrMatch[1]);
+ // Try to enrich with node name from API (lazy)
+ if (!nodeMap[nid]) {
+ await fetchNodeFromApi(nid);
+ }
+
ids.push(nid);
names.push(nodeMap[nid] || nid);
snrs.push(snr);
@@ -884,7 +986,10 @@ async function loadNeighborChart(){
});
}
-// --- Expand / Export ---
+/* ======================================================
+ EXPAND / EXPORT BUTTONS
+ ====================================================== */
+
function expandChart(type){
const srcEl = document.getElementById(`chart_${type}`);
if (!srcEl) return;
@@ -943,7 +1048,10 @@ function exportCSV(type){
link.click();
}
-// --- Expand payload rows ---
+/* ======================================================
+ EXPAND PAYLOAD ROWS
+ ====================================================== */
+
document.addEventListener("click", e => {
const btn = e.target.closest(".toggle-btn");
if (!btn) return;
@@ -952,13 +1060,16 @@ document.addEventListener("click", e => {
btn.textContent = row.classList.contains("expanded") ? "▼" : "▶";
});
-// --- Init ---
+/* ======================================================
+ INIT
+ ====================================================== */
+
document.addEventListener("DOMContentLoaded", async () => {
- await loadTranslationsNode(); // <-- REQUIRED
+ await loadTranslationsNode(); // translations first
requestAnimationFrame(async () => {
- await loadNodes();
- await loadNodeInfo();
+ await loadNodeInfo(); // single-node fetch
+ if (!map) initMap(); // init map early so neighbors can draw
await loadTrack();
await loadPackets();
await loadTelemetryCharts();
@@ -971,4 +1082,4 @@ document.addEventListener("DOMContentLoaded", async () => {
});
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/meshview/web_api/api.py b/meshview/web_api/api.py
index 923c518..f57bc59 100644
--- a/meshview/web_api/api.py
+++ b/meshview/web_api/api.py
@@ -48,6 +48,7 @@ async def api_channels(request: web.Request):
async def api_nodes(request):
try:
# Optional query parameters
+ node_id = request.query.get("node_id")
role = request.query.get("role")
channel = request.query.get("channel")
hw_model = request.query.get("hw_model")
@@ -61,7 +62,7 @@ async def api_nodes(request):
# Fetch nodes from database
nodes = await store.get_nodes(
- role=role, channel=channel, hw_model=hw_model, days_active=days_active
+ node_id=node_id, role=role, channel=channel, hw_model=hw_model, days_active=days_active
)
# Prepare the JSON response
@@ -214,6 +215,7 @@ async def api_packets(request):
"portnum": int(p.portnum),
"long_name": getattr(p.from_node, "long_name", ""),
"payload": (p.payload or "").strip(),
+ "to_long_name": getattr(p.to_node, "long_name", ""),
}
reply_id = getattr(