(data)
| 13739 | const lng = Number(net.gps.longitude).toFixed(5); |
| 13740 | return `<span class="font-mono text-emerald-300" title="Lat: ${lat}, Lng: ${lng}">${lat}, ${lng}</span>`; |
| 13741 | } |
| 13742 | return '<span class="text-gray-500">-</span>'; |
| 13743 | } |
| 13744 | |
| 13745 | function _pwnFileSizeLabel(bytes) { |
| 13746 | if (!bytes || bytes <= 0) return ''; |
| 13747 | if (bytes < 1024) return bytes + ' B'; |
| 13748 | if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; |
| 13749 | return (bytes / 1048576).toFixed(1) + ' MB'; |
| 13750 | } |
| 13751 | |
| 13752 | function _pwnFilesHTML(files) { |
| 13753 | if (!Array.isArray(files) || files.length === 0) return ''; |
| 13754 | return '<div class="flex flex-wrap gap-2 mt-2">' + files.map(f => { |
| 13755 | const name = escapeHtml(f.name || ''); |
| 13756 | const size = _pwnFileSizeLabel(f.size); |
| 13757 | const sizeLabel = size ? ` (${size})` : ''; |
| 13758 | const encodedName = encodeURIComponent(f.name || ''); |
| 13759 | return `<a href="/api/pwnagotchi/download?file=${encodedName}" download |
| 13760 | class="inline-flex items-center gap-1 px-2 py-1 rounded text-xs bg-slate-700 hover:bg-slate-600 text-slate-200 hover:text-white transition-colors" |
| 13761 | title="Download ${name}${sizeLabel}"> |
| 13762 | <svg class="w-3 h-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg> |
| 13763 | <span class="truncate max-w-[10rem]">${name}</span></a>`; |
| 13764 | }).join('') + '</div>'; |
| 13765 | } |
| 13766 | |
| 13767 | function displayPwnNetworksTable(networks) { |
| 13768 | const container = document.getElementById('pwn-networks-table'); |
| 13769 | if (!container) return; |
| 13770 | |
| 13771 | if (!Array.isArray(networks) || networks.length === 0) { |
| 13772 | container.innerHTML = '<p class="text-gray-400">No Pwnagotchi captures found yet.</p>'; |
| 13773 | return; |
| 13774 | } |
| 13775 | |
| 13776 | const previewCount = 10; |
| 13777 | const hasMore = networks.length > previewCount; |
| 13778 | const previewItems = networks.slice(0, previewCount); |
| 13779 | const hiddenItems = hasMore ? networks.slice(previewCount) : []; |
| 13780 | |
| 13781 | /* --- Mobile card layout (shown < md) --- */ |
| 13782 | function networkCardHTML(net) { |
| 13783 | const ssid = escapeHtml(net.ssid || 'Unknown'); |
| 13784 | const bssid = escapeHtml(net.bssid || 'Unknown'); |
| 13785 | const lastSeen = net.last_seen ? formatTimestamp(net.last_seen) : 'Unknown'; |
| 13786 | return ` |
| 13787 | <div class="bg-slate-800 bg-opacity-60 rounded-lg p-4"> |
| 13788 | <div class="flex items-start justify-between gap-2 mb-2"> |
| 13789 | <span class="text-white font-semibold text-sm break-all">${ssid}</span> |
| 13790 | <span class="text-xs text-gray-400 whitespace-nowrap">${lastSeen}</span> |
| 13791 | </div> |
| 13792 | <div class="text-xs text-gray-400 font-mono mb-2">${bssid}</div> |
| 13793 | <div class="flex flex-wrap gap-1 mb-1">${_pwnBadgesHTML(net)}</div> |
| 13794 | <div class="text-xs mt-1">${_pwnGpsHTML(net)}</div> |
| 13795 | ${_pwnFilesHTML(net.files)} |
| 13796 | </div>`; |
| 13797 | } |
| 13798 |
no test coverage detected