diff --git a/meshview/templates/node.html b/meshview/templates/node.html
index e3df6a3..b0246e3 100644
--- a/meshview/templates/node.html
+++ b/meshview/templates/node.html
@@ -239,6 +239,9 @@
+
+
+
@@ -246,13 +249,13 @@
-
+
+
+
@@ -422,6 +425,7 @@ let nodeMap = {}; // node_id -> label
let nodePositions = {}; // node_id -> [lat, lon]
let nodeCache = {}; // node_id -> full node object
let currentNode = null;
+let currentPacketRows = [];
let map, markers = {};
let chartData = {}, neighborData = { ids:[], names:[], snrs:[] };
@@ -760,6 +764,9 @@ async function loadPackets(filters = {}) {
if (!res.ok) return;
const data = await res.json();
+ const packets = data.packets || [];
+ currentPacketRows = packets;
+
for (const pkt of (data.packets || []).reverse()) {
const safePayload = (pkt.payload || "")
@@ -1318,6 +1325,49 @@ async function loadNodeStats(nodeId) {
loadPackets(filters);
}
+ function exportPacketsCSV() {
+ if (!currentPacketRows.length) {
+ alert("No packets to export.");
+ return;
+ }
+
+ const rows = [
+ ["Time", "Packet ID", "From Node", "To Node", "Port", "Port Name", "Payload"]
+ ];
+
+ for (const pkt of currentPacketRows) {
+ const time = pkt.import_time_us
+ ? new Date(pkt.import_time_us / 1000).toISOString()
+ : "";
+
+ const portName = PORT_LABEL_MAP[pkt.portnum] || `Port ${pkt.portnum}`;
+
+ // Escape quotes + line breaks for CSV safety
+ const payload = (pkt.payload || "")
+ .replace(/"/g, '""')
+ .replace(/\r?\n/g, " ");
+
+ rows.push([
+ time,
+ pkt.id,
+ pkt.from_node_id,
+ pkt.to_node_id,
+ pkt.portnum,
+ portName,
+ `"${payload}"`
+ ]);
+ }
+
+ const csv = rows.map(r => r.join(",")).join("\n");
+ const blob = new Blob([csv], { type: "text/csv" });
+
+ const link = document.createElement("a");
+ link.href = URL.createObjectURL(blob);
+ link.download = `packets_${fromNodeId}_${Date.now()}.csv`;
+ link.click();
+}
+
+
{% endblock %}