(event)
| 5748 | const srcNote = d.captured_name |
| 5749 | ? `<p class="text-xs text-green-400 mb-2">🎥 Captured <span class="font-mono">${escapeHtml(d.captured_name)}</span> on ${escapeHtml(d.interface || '')} for ${d.seconds || ''}s${capMode} — saved to Ragnar (see “Open stored pcap”).</p>` |
| 5750 | : (d.source_name ? `<p class="text-xs text-gray-400 mb-2">📁 <span class="font-mono">${escapeHtml(d.source_name)}</span></p>` : ''); |
| 5751 | const note = d.note ? `<p class="text-xs text-amber-300 mb-2">${escapeHtml(d.note)}</p>` : ''; |
| 5752 | const monWarn = d.monitor_warning ? `<p class="text-xs text-amber-300 mb-2">📡 ${escapeHtml(d.monitor_warning)}</p>` : ''; |
| 5753 | |
| 5754 | const aiBtn = `<div class="mt-3 flex flex-wrap gap-2"><button onclick="aiAnalyzePcap()" class="bg-Ragnar-600 hover:bg-Ragnar-700 text-white px-3 py-1.5 rounded text-sm whitespace-nowrap">🧠 Explain with AI</button> |
| 5755 | <button onclick="pcapExportReport()" title="Open a printable capture report (Save as PDF)" class="bg-emerald-600 hover:bg-emerald-700 text-white px-3 py-1.5 rounded text-sm whitespace-nowrap">📄 Export as PDF</button> |
| 5756 | <div id="pcap-ai-results" class="hidden mt-2 w-full"></div></div>`; |
| 5757 | out.innerHTML = srcNote + note + monWarn + stats + `<div class="overflow-x-auto">${protoTable}${talkTable}</div>` + _pcapWifiHtml(d.wifi) + expert + aiBtn; |
| 5758 | _lastPcap = d; |
| 5759 | _lastPcapAI = null; // new capture — drop any AI summary from a previous one |
| 5760 | } |
| 5761 | |
| 5762 | // Build a printable one-page report from the last analyzed capture and hand it |
| 5763 | // to the browser's print dialog (→ "Save as PDF"). Same pop-up + window.print() |
| 5764 | // pattern as wifiExportReport(); no external libraries, works offline on a Pi. |
| 5765 | function pcapExportReport() { |
| 5766 | const d = _lastPcap; |
| 5767 | if (!d) { alert('Analyze a capture first.'); return; } |
| 5768 | const s = d.summary || {}; |
| 5769 | const now = new Date(); |
| 5770 | |
| 5771 | const summRows = [ |
| 5772 | ['Packets', s.packets != null ? s.packets : '—'], |
| 5773 | ['Size', _pcapBytes(s.file_size)], |
| 5774 | ['Duration', s.duration_s != null ? s.duration_s + ' s' : '—'], |
| 5775 | ['Avg packet', s.avg_packet_size != null ? Math.round(s.avg_packet_size) + ' B' : '—'], |
| 5776 | ['Data rate', s.data_byte_rate != null ? _pcapBytes(s.data_byte_rate) + '/s' : '—'], |
| 5777 | ['Encapsulation', s.encapsulation ? escapeHtml(String(s.encapsulation)) : '—'], |
| 5778 | ].map(r => `<tr><th>${r[0]}</th><td>${r[1]}</td></tr>`).join(''); |
| 5779 | const window_ = s.start_time |
| 5780 | ? `<p class="sub">${escapeHtml(String(s.start_time))} → ${escapeHtml(String(s.end_time || ''))}</p>` : ''; |
| 5781 | |
| 5782 | const protoRows = (d.protocols || []).map(p => |
| 5783 | `<tr><td class="mono">${' '.repeat(p.depth)}${escapeHtml(p.proto)}</td> |
| 5784 | <td class="num">${p.frames}</td><td class="num">${_pcapBytes(p.bytes)}</td></tr>`).join(''); |
| 5785 | const protoTable = protoRows ? `<h2>Protocol hierarchy</h2> |
| 5786 | <table><thead><tr><th>Protocol</th><th class="num">Frames</th><th class="num">Bytes</th></tr></thead> |
| 5787 | <tbody>${protoRows}</tbody></table>` : ''; |
| 5788 | |
| 5789 | const talkRows = (d.talkers || []).map(t => |
nothing calls this directly
no test coverage detected