(data)
| 12290 | if (!select) return; |
| 12291 | const current = select.value; |
| 12292 | // Rebuild options preserving selection |
| 12293 | select.innerHTML = '<option value="all">All Networks</option>'; |
| 12294 | (networks || []).forEach(n => { |
| 12295 | const opt = document.createElement('option'); |
| 12296 | opt.value = n; |
| 12297 | opt.textContent = n === 'unknown' ? 'Unknown Network' : n; |
| 12298 | if (n === current) opt.selected = true; |
| 12299 | select.appendChild(opt); |
| 12300 | }); |
| 12301 | } |
| 12302 | |
| 12303 | function _buildAttackHostBlock(ip, hostLogs) { |
| 12304 | const successCount = hostLogs.filter(l => l.status === 'success').length; |
| 12305 | const failedCount = hostLogs.filter(l => l.status === 'failed').length; |
| 12306 | const timeoutCount = hostLogs.filter(l => l.status === 'timeout').length; |
| 12307 | const safeId = ip.replace(/[^a-zA-Z0-9]/g, '-'); |
| 12308 | |
| 12309 | let html = ` |
| 12310 | <div class="bg-slate-800 bg-opacity-50 rounded-lg border border-slate-700 overflow-hidden"> |
| 12311 | <div class="px-4 py-3 bg-slate-900 bg-opacity-50 flex items-center justify-between cursor-pointer hover:bg-opacity-70 transition-colors" onclick="toggleAttackHost('${safeId}')"> |
| 12312 | <div class="flex items-center space-x-3"> |
| 12313 | <svg class="w-5 h-5 text-Ragnar-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 12314 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"></path> |
| 12315 | </svg> |
| 12316 | <span class="font-semibold text-lg">${ip}</span> |
| 12317 | <span class="text-sm text-gray-400">(${hostLogs.length} attacks)</span> |
| 12318 | </div> |
| 12319 | <div class="flex items-center space-x-4"> |
| 12320 | <span class="text-sm text-green-400">✓ ${successCount}</span> |
| 12321 | <span class="text-sm text-red-400">✗ ${failedCount}</span> |
| 12322 | <span class="text-sm text-yellow-400">⏱ ${timeoutCount}</span> |
| 12323 | <svg id="attack-chevron-${safeId}" class="w-5 h-5 text-gray-400 transform transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 12324 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> |
| 12325 | </svg> |
| 12326 | </div> |
| 12327 | </div> |
| 12328 | <div id="attack-host-${safeId}" class="hidden px-4 py-3 space-y-2"> |
| 12329 | `; |
| 12330 | |
| 12331 | hostLogs.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); |
| 12332 | |
| 12333 | hostLogs.forEach(log => { |
| 12334 | const statusColors = { |
| 12335 | 'success': 'bg-green-900 bg-opacity-30 border-green-500', |
| 12336 | 'failed': 'bg-red-900 bg-opacity-30 border-red-500', |
| 12337 | 'timeout': 'bg-yellow-900 bg-opacity-30 border-yellow-500' |
| 12338 | }; |
| 12339 | const statusIcons = { 'success': '✓', 'failed': '✗', 'timeout': '⏱' }; |
| 12340 | const statusTextColors = { 'success': 'text-green-400', 'failed': 'text-red-400', 'timeout': 'text-yellow-400' }; |
| 12341 | |
| 12342 | const colorClass = statusColors[log.status] || 'bg-gray-900 bg-opacity-30 border-gray-500'; |
| 12343 | const icon = statusIcons[log.status] || '•'; |
| 12344 | const textColor = statusTextColors[log.status] || 'text-gray-400'; |
| 12345 | |
| 12346 | html += ` |
| 12347 | <div class="border-l-4 ${colorClass} p-3 rounded-r-lg"> |
| 12348 | <div class="flex items-start justify-between"> |
| 12349 | <div class="flex-1"> |
no test coverage detected