Files
Virtnet.bond/extensions/Servers/Proxmox/views/partials/statistics/network-inner.blade.php
Joseph.Rawlings 9e899cb947 feat(proxmox): rebuild statistics UI and client runtime
New Blade layout, copy, and visual system for the monitoring tab; RRD hosts use data-statistics-chart hooks; tables use pxStatTableKit and data-statistics-data-row; network and comparison panels use new selectors; CSV helpers renamed (pxExport*); modals and telemetry refreshed.

Made-with: Cursor
2026-03-28 11:38:56 -04:00

1508 lines
144 KiB
PHP

<!-- Network Analysis Section -->
<div
class="relative mt-8 bg-background-secondary border border-neutral rounded-xl overflow-hidden"
data-statistics-panel="net-insights"
id="px-stat-net-insights"
style="width: 100%;"
wire:init="loadNetworkAnalysis"
>
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between p-4 border-b border-neutral bg-gradient-to-r from-primary/10 via-primary/5 to-transparent">
<div class="flex items-start gap-3 min-w-0 flex-1">
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/15 shrink-0">
<x-ri-router-line class="size-5 text-primary" />
</div>
<div class="min-w-0">
<div class="flex items-center gap-2 flex-wrap">
<h3 class="text-base font-semibold">Network Analysis</h3>
<div class="relative" x-data="{ open: false }" x-cloak>
<button type="button" @click="open = true" class="text-base/50 hover:text-base/70 transition-colors">
<x-ri-question-line class="size-4" />
</button>
<!-- Modal Backdrop -->
<div x-show="open"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
style="display: none;">
</div>
<!-- Modal -->
<div x-show="open"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
@click.self="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 flex items-center justify-center p-4"
style="display: none;">
<div class="relative w-full max-w-2xl max-h-[90vh] overflow-y-auto bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<div class="flex items-center gap-3 mb-4 pr-8">
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
<x-ri-router-line class="size-5 text-primary" />
</div>
<h6 class="font-sans text-lg font-semibold leading-tight">
Network Analysis
</h6>
</div>
<p class="block font-sans text-sm font-normal leading-relaxed text-base/90 mb-4">
Deep visibility into firewall sessions, security threats, and top consumers of bandwidth powered by the Core Router API. This section provides real-time monitoring of active network connections, traffic analysis, and security threat detection.
</p>
<div class="pt-4 mt-4 border-t border-neutral/30 space-y-2.5">
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Active Connections:</span> View all active network connections where the source or destination matches an IP assigned to your server. Connections are automatically filtered to show only relevant traffic. The table supports sorting by any column and filtering by search terms.
</p>
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Top Peers:</span> Identify the endpoints that have the most connections and highest traffic volume. This helps you understand which IPs or domains are consuming the most bandwidth.
</p>
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Top Ports:</span> See which network ports are most active, helping you identify the services and protocols generating the most traffic.
</p>
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Security:</span> Monitor security threats by detecting connections to known malicious IPs from blocklists (Spamhaus DROPv6 for IPv6, blocklist.de for IPv4). Security warnings display the threat type, malicious IP, port, and connection details.
</p>
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Export:</span> All connection data can be exported to CSV format for further analysis, reporting, or archiving.
</p>
</div>
</div>
</div>
</div>
</div>
<p class="text-xs text-base/50 mt-1">
Deep visibility into firewall sessions, security threats, and top consumers of bandwidth powered by the Core Router API.
</p>
</div>
</div>
<div class="flex items-center gap-2 shrink-0 flex-wrap">
<button
type="button"
wire:click="exportNetworkAnalysis"
class="bg-background border border-neutral px-3 py-2 rounded-md text-sm font-semibold hover:bg-background/80 flex items-center gap-2 h-[calc(0.5rem*2+1.25rem)]"
title="Export to CSV"
>
<x-ri-download-line class="size-4" />
Export
</button>
</div>
</div>
<div x-data="{ tab: 'connections' }" style="width: 100%; min-width: 100%;">
<div class="p-6 pb-4">
<div class="flex flex-wrap gap-2 border-b border-neutral/40 pb-4">
<button
type="button"
class="px-3 py-2 text-sm font-semibold rounded-md transition-colors"
:class="tab === 'connections' ? 'bg-primary/10 text-primary' : 'text-base/60 hover:text-base'"
@click="tab = 'connections'"
>
Active Connections
</button>
<button
type="button"
class="px-3 py-2 text-sm font-semibold rounded-md transition-colors"
:class="tab === 'top-connections' ? 'bg-primary/10 text-primary' : 'text-base/60 hover:text-base'"
@click="tab = 'top-connections'"
>
Top Peers
</button>
<button
type="button"
class="px-3 py-2 text-sm font-semibold rounded-md transition-colors"
:class="tab === 'ports' ? 'bg-primary/10 text-primary' : 'text-base/60 hover:text-base'"
@click="tab = 'ports'"
>
Top Ports
</button>
<button
type="button"
class="px-3 py-2 text-sm font-semibold rounded-md transition-colors"
:class="tab === 'security' ? 'bg-primary/10 text-primary' : 'text-base/60 hover:text-base'"
@click="tab = 'security'"
>
Security
</button>
</div>
</div>
@if($networkAnalysisLoading || ($networkAnalysis['lastUpdated'] === null && $networkAnalysis['error'] === null))
<div class="p-6 py-10 flex flex-col items-center gap-3 text-base/60" style="width: 100%; min-width: 100%;">
<svg fill="currentColor" stroke="currentColor" viewBox="0 0 128 128" class="size-6">
<g>
<path stroke-width="5" d="M31.6 8.98a4.02 4.02 0 0 0 1.7 5.3l6.12 3.22a52.56 52.56 0 1 0 63.13 10.73L96 33.97a43.88 43.88 0 1 1-53-8.5l-3.65 7.4a4.02 4.02 0 0 0 1.72 5.3 3.73 3.73 0 0 0 5.13-1.67l7.66-15.63a4.18 4.18 0 0 0-1.75-5.4L36.74 7.32a3.73 3.73 0 0 0-5.13 1.66z" fill="currentColor" stroke="currentColor"/>
<animateTransform attributeName="transform" type="rotate" from="0 64 64" to="360 64 64" dur="1000ms" repeatCount="indefinite"></animateTransform>
</g>
</svg>
<p class="text-sm font-semibold">
Fetching data from Core Router...
<a href="javascript:void(0)" onclick="window.location.reload()" class="text-primary hover:text-primary/80 underline">reload page</a>
</p>
</div>
@elseif($networkAnalysis['error'])
<div class="p-6" style="width: 100%; min-width: 100%;">
<div class="p-4 bg-warning/10 border border-warning/30 rounded-md">
<p class="text-sm font-semibold text-warning">{{ $networkAnalysis['error'] }}</p>
<p class="text-xs text-warning/70 mt-1">Ensure the Core Router API credentials are configured in your environment.</p>
</div>
</div>
@else
<div style="width: 100%; min-width: 100%; display: flex; flex-direction: column; box-sizing: border-box;">
<div x-show="tab === 'connections'" style="width: 100%; min-width: 100%; flex-shrink: 0; box-sizing: border-box;">
@if(($networkAnalysis['connectionCount'] ?? 0) == 0)
<div class="text-center py-6 text-sm text-base/60">No matching connections tracked for this server.</div>
@else
<div x-data="pxStatTableKit('px-stat-net-sessions')" style="width: 100%;">
<div class="p-4 border-b border-neutral bg-background/50">
<div class="flex items-center gap-3 flex-wrap">
<div class="flex-1 min-w-[200px]">
<input type="text"
x-model="search"
@input="filterTable()"
placeholder="Search connections..."
class="w-full px-3 py-2 text-sm bg-background border border-neutral rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50">
</div>
<button @click="search = ''; filterTable()"
x-show="search"
class="px-3 py-2 text-sm text-base/70 hover:text-base border border-neutral rounded-md hover:bg-background/80">
Clear
</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full" id="px-stat-net-sessions">
<thead>
<tr class="border-b border-neutral bg-background">
<th @click.stop="sortTable('direction')" data-sort="direction" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
<div class="flex items-center gap-2">
Direction
<x-ri-arrow-up-down-line class="size-3" />
</div>
</th>
<th @click.stop="sortTable('protocol')" data-sort="protocol" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
<div class="flex items-center gap-2">
Protocol
<x-ri-arrow-up-down-line class="size-3" />
</div>
</th>
<th @click.stop="sortTable('source')" data-sort="source" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
<div class="flex items-center gap-2">
Source
<x-ri-arrow-up-down-line class="size-3" />
</div>
</th>
<th @click.stop="sortTable('destination')" data-sort="destination" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
<div class="flex items-center gap-2">
Destination
<x-ri-arrow-up-down-line class="size-3" />
</div>
</th>
<th @click.stop="sortTable('state')" data-sort="state" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
<div class="flex items-center gap-2">
State
<x-ri-arrow-up-down-line class="size-3" />
</div>
</th>
<th @click.stop="sortTable('traffic')" data-sort="traffic" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
<div class="flex items-center gap-2">
Traffic
<x-ri-arrow-up-down-line class="size-3" />
</div>
</th>
</tr>
</thead>
<tbody class="divide-y divide-neutral/40">
@foreach($this->paginatedConnections as $index => $connection)
@php
// Use raw bytes value from backend for reliable numeric sorting
$bytesForSort = (int)($connection['bytes'] ?? 0);
@endphp
<tr wire:key="conn-{{ $connectionsCurrentPage }}-{{ $index }}"
data-statistics-data-row
class="hover:bg-background-secondary/40 transition-colors"
data-direction="{{ strtolower($connection['direction'] ?? '') }}"
data-protocol="{{ strtolower($connection['protocol'] ?? '') }}"
data-source="{{ strtolower($connection['source'] ?? '') }}"
data-destination="{{ strtolower($connection['destination'] ?? '') }}"
data-state="{{ strtolower($connection['state'] ?? '') }}"
data-traffic="{{ $bytesForSort }}">
<td class="py-3 px-4 text-sm whitespace-nowrap">
@php
$direction = $connection['direction'] ?? '—';
@endphp
@if($direction !== '—')
<div class="flex items-center gap-1">
<span class="font-semibold text-base/80">{{ $direction }}</span>
<div class="relative" x-data="{ open: false, directionDescription: window.getDirectionDescription('{{ $direction }}') }" x-cloak>
<button type="button" @click="open = true" class="text-base/50 hover:text-base/70 transition-colors">
<x-ri-information-line class="size-3" />
</button>
<!-- Modal Backdrop -->
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
style="display: none;">
</div>
<!-- Modal -->
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4"
style="display: none;">
<div @click.stop class="relative w-full max-w-4xl bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl max-h-[90vh] overflow-y-auto overflow-x-hidden">
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors z-10">
<x-ri-close-line class="size-5" />
</button>
<div class="p-6 break-words" style="width: 100%; max-width: 100%; box-sizing: border-box;">
<div class="flex items-center gap-2 mb-4">
<x-ri-information-line class="size-5 text-primary flex-shrink-0" />
<h3 class="text-lg font-semibold break-words" style="overflow-wrap: break-word; word-wrap: break-word;">Direction: <span class="font-mono break-all">{{ $direction }}</span></h3>
</div>
<div class="text-sm text-base/80 prose prose-sm max-w-none" style="overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; white-space: normal;" x-html="directionDescription"></div>
</div>
</div>
</div>
</div>
</div>
@else
—
@endif
</td>
<td class="py-3 px-4 text-sm whitespace-nowrap">
@php
$protocol = $connection['protocol'] ?? '—';
@endphp
@if($protocol !== '—')
<div class="flex items-center gap-1">
<span>{{ $protocol }}</span>
<div class="relative" x-data="{ open: false, protocolDescription: window.getProtocolDescription('{{ $protocol }}') }" x-cloak>
<button type="button" @click="open = true" class="text-base/50 hover:text-base/70 transition-colors">
<x-ri-information-line class="size-3" />
</button>
<!-- Modal Backdrop -->
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
style="display: none;">
</div>
<!-- Modal -->
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4"
style="display: none;">
<div @click.stop class="relative w-full max-w-4xl bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl max-h-[90vh] overflow-y-auto overflow-x-hidden">
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors z-10">
<x-ri-close-line class="size-5" />
</button>
<div class="p-6 break-words" style="width: 100%; max-width: 100%; box-sizing: border-box;">
<div class="flex items-center gap-2 mb-4">
<x-ri-information-line class="size-5 text-primary flex-shrink-0" />
<h3 class="text-lg font-semibold break-words" style="overflow-wrap: break-word; word-wrap: break-word;">Protocol: <span class="font-mono break-all">{{ $protocol }}</span></h3>
</div>
<div class="text-sm text-base/80 prose prose-sm max-w-none" style="overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; white-space: normal;" x-html="protocolDescription"></div>
</div>
</div>
</div>
</div>
</div>
@else
—
@endif
</td>
<td class="py-3 px-4 text-sm whitespace-nowrap">
@php
$sourceIP = $connection['source'] ?? '—';
$isSourcePublic = $sourceIP !== '—' && $this->checkIsPublicIP($sourceIP);
@endphp
<div class="flex items-center gap-1">
<span class="block font-mono text-xs">{{ $sourceIP }}</span>
@if($isSourcePublic)
<div class="relative" x-data="{ open: false, loading: false, ipData: null, ip: '{{ $sourceIP }}' }" x-cloak>
<button type="button" @click="
if (!ipData && !loading) {
loading = true;
open = true;
window.fetchIPLocation(ip).then(data => {
ipData = data;
loading = false;
}).catch(err => {
ipData = { status: 'error', message: 'Failed to load location data' };
loading = false;
});
} else {
open = true;
}
" class="text-base/50 hover:text-base/70 transition-colors" title="View IP Location">
<x-ri-map-pin-line class="size-3" />
</button>
<!-- Modal -->
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4"
style="display: none;">
<div @click.stop class="relative w-full max-w-4xl bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl max-h-[90vh] overflow-y-auto overflow-x-hidden">
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors z-10">
<x-ri-close-line class="size-5" />
</button>
<div class="p-6 break-words">
<div class="flex items-center gap-2 mb-4">
<x-ri-map-pin-line class="size-5 text-primary flex-shrink-0" />
<h3 class="text-lg font-semibold break-words">IP Address: <span class="font-mono break-all">{{ $sourceIP }}</span></h3>
</div>
<div x-show="loading" class="text-center py-8">
<svg fill="currentColor" stroke="currentColor" viewBox="0 0 128 128" class="size-6 mx-auto mb-2">
<g>
<path stroke-width="5" d="M31.6 8.98a4.02 4.02 0 0 0 1.7 5.3l6.12 3.22a52.56 52.56 0 1 0 63.13 10.73L96 33.97a43.88 43.88 0 1 1-53-8.5l-3.65 7.4a4.02 4.02 0 0 0 1.72 5.3 3.73 3.73 0 0 0 5.13-1.67l7.66-15.63a4.18 4.18 0 0 0-1.75-5.4L36.74 7.32a3.73 3.73 0 0 0-5.13 1.66z" fill="currentColor" stroke="currentColor"/>
<animateTransform attributeName="transform" type="rotate" from="0 64 64" to="360 64 64" dur="1000ms" repeatCount="indefinite"></animateTransform>
</g>
</svg>
<p class="text-sm text-base/60">Loading location data...</p>
</div>
<div x-show="ipData && !loading">
<template x-if="ipData && (ipData.status === 'success')">
<div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div class="space-y-2 break-words">
<div class="break-words"><span class="text-xs text-base/60">Country:</span> <span class="font-semibold text-sm break-words" x-text="ipData.country"></span></div>
<div class="break-words"><span class="text-xs text-base/60">Region:</span> <span class="font-semibold text-sm break-words" x-text="ipData.regionName + ' (' + ipData.region + ')'"></span></div>
<div class="break-words"><span class="text-xs text-base/60">City:</span> <span class="font-semibold text-sm break-words" x-text="ipData.city"></span></div>
<div class="break-words"><span class="text-xs text-base/60">ZIP Code:</span> <span class="font-semibold text-sm break-words" x-text="ipData.zip"></span></div>
<div class="break-words"><span class="text-xs text-base/60">Timezone:</span> <span class="font-semibold text-sm break-words" x-text="ipData.timezone"></span></div>
</div>
<div class="space-y-2 break-words">
<div class="break-words"><span class="text-xs text-base/60">ISP:</span> <span class="font-semibold text-sm break-words" x-text="ipData.isp"></span></div>
<div class="break-words"><span class="text-xs text-base/60">Organization:</span> <span class="font-semibold text-sm break-words" x-text="ipData.org"></span></div>
<div class="break-words"><span class="text-xs text-base/60">AS:</span> <span class="font-semibold text-sm break-all" x-text="ipData.as"></span></div>
<div class="break-words"><span class="text-xs text-base/60">Coordinates:</span> <span class="font-semibold text-sm font-mono break-all" x-text="ipData.lat + ', ' + ipData.lon"></span></div>
</div>
</div>
<div x-show="ipData.lat && ipData.lon" class="mt-4 w-full" style="max-width: 100%; box-sizing: border-box;">
<p class="text-xs text-base/60 mb-2">Location Map:</p>
<div class="w-full rounded-lg border border-neutral/20 overflow-hidden bg-background/50" style="position: relative; width: 100%; max-width: 100%; height: 256px; min-height: 256px; box-sizing: border-box;">
<iframe
x-bind:src="'https://www.openstreetmap.org/export/embed.html?bbox=' + encodeURIComponent((parseFloat(ipData.lon) - 0.01) + ',' + (parseFloat(ipData.lat) - 0.01) + ',' + (parseFloat(ipData.lon) + 0.01) + ',' + (parseFloat(ipData.lat) + 0.01)) + '&layer=mapnik&marker=' + encodeURIComponent(ipData.lat + ',' + ipData.lon)"
style="width: 100%; height: 100%; border: 0; display: block; margin: 0; padding: 0;"
frameborder="0"
loading="lazy"
allowfullscreen
title="Location Map"></iframe>
</div>
</div>
</div>
</template>
<template x-if="ipData && (ipData.status === 'fail' || ipData.status === 'error')">
<div class="text-center py-4">
<p class="text-sm text-warning" x-text="ipData.message || 'Failed to load location data'"></p>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
@endif
</div>
@if(isset($connection['source_port']))
<span class="text-xs text-base/60">{{ $connection['source_port'] }}</span>
@endif
</td>
<td class="py-3 px-4 text-sm whitespace-nowrap">
@php
$destIP = $connection['destination'] ?? '—';
$isDestPublic = $destIP !== '—' && $this->checkIsPublicIP($destIP);
@endphp
<div class="flex items-center gap-1">
<span class="block font-mono text-xs">{{ $destIP }}</span>
@if($isDestPublic)
<div class="relative" x-data="{ open: false, loading: false, ipData: null, ip: '{{ $destIP }}' }" x-cloak>
<button type="button" @click="
if (!ipData && !loading) {
loading = true;
open = true;
window.fetchIPLocation(ip).then(data => {
ipData = data;
loading = false;
}).catch(err => {
ipData = { status: 'error', message: 'Failed to load location data' };
loading = false;
});
} else {
open = true;
}
" class="text-base/50 hover:text-base/70 transition-colors" title="View IP Location">
<x-ri-map-pin-line class="size-3" />
</button>
<!-- Modal -->
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4"
style="display: none;">
<div @click.stop class="relative w-full max-w-4xl bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl max-h-[90vh] overflow-y-auto overflow-x-hidden">
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors z-10">
<x-ri-close-line class="size-5" />
</button>
<div class="p-6 break-words">
<div class="flex items-center gap-2 mb-4">
<x-ri-map-pin-line class="size-5 text-primary flex-shrink-0" />
<h3 class="text-lg font-semibold break-words">IP Address: <span class="font-mono break-all">{{ $destIP }}</span></h3>
</div>
<div x-show="loading" class="text-center py-8">
<svg fill="currentColor" stroke="currentColor" viewBox="0 0 128 128" class="size-6 mx-auto mb-2">
<g>
<path stroke-width="5" d="M31.6 8.98a4.02 4.02 0 0 0 1.7 5.3l6.12 3.22a52.56 52.56 0 1 0 63.13 10.73L96 33.97a43.88 43.88 0 1 1-53-8.5l-3.65 7.4a4.02 4.02 0 0 0 1.72 5.3 3.73 3.73 0 0 0 5.13-1.67l7.66-15.63a4.18 4.18 0 0 0-1.75-5.4L36.74 7.32a3.73 3.73 0 0 0-5.13 1.66z" fill="currentColor" stroke="currentColor"/>
<animateTransform attributeName="transform" type="rotate" from="0 64 64" to="360 64 64" dur="1000ms" repeatCount="indefinite"></animateTransform>
</g>
</svg>
<p class="text-sm text-base/60">Loading location data...</p>
</div>
<div x-show="ipData && !loading">
<template x-if="ipData && (ipData.status === 'success')">
<div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div class="space-y-2 break-words">
<div class="break-words"><span class="text-xs text-base/60">Country:</span> <span class="font-semibold text-sm break-words" x-text="ipData.country"></span></div>
<div class="break-words"><span class="text-xs text-base/60">Region:</span> <span class="font-semibold text-sm break-words" x-text="ipData.regionName + ' (' + ipData.region + ')'"></span></div>
<div class="break-words"><span class="text-xs text-base/60">City:</span> <span class="font-semibold text-sm break-words" x-text="ipData.city"></span></div>
<div class="break-words"><span class="text-xs text-base/60">ZIP Code:</span> <span class="font-semibold text-sm break-words" x-text="ipData.zip"></span></div>
<div class="break-words"><span class="text-xs text-base/60">Timezone:</span> <span class="font-semibold text-sm break-words" x-text="ipData.timezone"></span></div>
</div>
<div class="space-y-2 break-words">
<div class="break-words"><span class="text-xs text-base/60">ISP:</span> <span class="font-semibold text-sm break-words" x-text="ipData.isp"></span></div>
<div class="break-words"><span class="text-xs text-base/60">Organization:</span> <span class="font-semibold text-sm break-words" x-text="ipData.org"></span></div>
<div class="break-words"><span class="text-xs text-base/60">AS:</span> <span class="font-semibold text-sm break-all" x-text="ipData.as"></span></div>
<div class="break-words"><span class="text-xs text-base/60">Coordinates:</span> <span class="font-semibold text-sm font-mono break-all" x-text="ipData.lat + ', ' + ipData.lon"></span></div>
</div>
</div>
<div x-show="ipData.lat && ipData.lon" class="mt-4 w-full" style="max-width: 100%; box-sizing: border-box;">
<p class="text-xs text-base/60 mb-2">Location Map:</p>
<div class="w-full rounded-lg border border-neutral/20 overflow-hidden bg-background/50" style="position: relative; width: 100%; max-width: 100%; height: 256px; min-height: 256px; box-sizing: border-box;">
<iframe
x-bind:src="'https://www.openstreetmap.org/export/embed.html?bbox=' + encodeURIComponent((parseFloat(ipData.lon) - 0.01) + ',' + (parseFloat(ipData.lat) - 0.01) + ',' + (parseFloat(ipData.lon) + 0.01) + ',' + (parseFloat(ipData.lat) + 0.01)) + '&layer=mapnik&marker=' + encodeURIComponent(ipData.lat + ',' + ipData.lon)"
style="width: 100%; height: 100%; border: 0; display: block; margin: 0; padding: 0;"
frameborder="0"
loading="lazy"
allowfullscreen
title="Location Map"></iframe>
</div>
</div>
</div>
</template>
<template x-if="ipData && (ipData.status === 'fail' || ipData.status === 'error')">
<div class="text-center py-4">
<p class="text-sm text-warning" x-text="ipData.message || 'Failed to load location data'"></p>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
@endif
</div>
@if(isset($connection['destination_port']))
<span class="text-xs text-base/60">{{ $connection['destination_port'] }}</span>
@endif
</td>
<td class="py-3 px-4 text-sm whitespace-nowrap">
@php
$state = $connection['state'] ?? '—';
@endphp
@if($state !== '—')
<div class="flex items-center gap-1">
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-semibold bg-base/10 text-base/70 uppercase">
{{ $state }}
</span>
<div class="relative" x-data="{ open: false, stateDescription: window.getStateDescription('{{ $state }}') }" x-cloak>
<button type="button" @click="open = true" class="text-base/50 hover:text-base/70 transition-colors">
<x-ri-information-line class="size-3" />
</button>
<!-- Modal Backdrop -->
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
style="display: none;">
</div>
<!-- Modal -->
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4"
style="display: none;">
<div @click.stop class="relative w-full max-w-4xl bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl max-h-[90vh] overflow-y-auto overflow-x-hidden">
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors z-10">
<x-ri-close-line class="size-5" />
</button>
<div class="p-6 break-words" style="width: 100%; max-width: 100%; box-sizing: border-box;">
<div class="flex items-center gap-2 mb-4">
<x-ri-information-line class="size-5 text-primary flex-shrink-0" />
<h3 class="text-lg font-semibold break-words" style="overflow-wrap: break-word; word-wrap: break-word;">Connection State: <span class="font-mono break-all uppercase">{{ $state }}</span></h3>
</div>
<div class="text-sm text-base/80 prose prose-sm max-w-none" style="overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; white-space: normal;" x-html="stateDescription"></div>
</div>
</div>
</div>
</div>
</div>
@else
—
@endif
</td>
<td class="py-3 px-4 text-sm whitespace-nowrap">
<div class="flex flex-col">
<span class="font-semibold">{{ $connection['bytes_human'] ?? '—' }}</span>
@if(isset($connection['packets']))
<span class="text-xs text-base/60">{{ number_format($connection['packets']) }} packets</span>
@endif
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
{{-- Connections Pagination Controls --}}
@if(($networkAnalysis['connectionCount'] ?? 0) > 0)
<div class="p-4 border-t border-neutral flex flex-col sm:flex-row items-center justify-between gap-4">
{{-- Records per page selector --}}
<div class="flex items-center gap-3">
<label class="text-sm text-base/70">Show:</label>
<select wire:model.live="connectionsPerPage" class="px-3 py-1.5 border border-neutral rounded-lg bg-background text-sm">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="250">250</option>
</select>
<span class="text-sm text-base/60">per page</span>
</div>
{{-- Page info and navigation --}}
<div class="flex items-center gap-4">
<span class="text-sm text-base/60">
Showing {{ (($connectionsCurrentPage - 1) * $connectionsPerPage) + 1 }} - {{ min($connectionsCurrentPage * $connectionsPerPage, $networkAnalysis['connectionCount'] ?? 0) }} of {{ $networkAnalysis['connectionCount'] ?? 0 }} connections
</span>
<div class="flex items-center gap-1">
{{-- First page --}}
<button type="button"
wire:click="goToConnectionsPage(1)"
@if($connectionsCurrentPage === 1) disabled @endif
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="First page">
<x-ri-arrow-left-double-line class="size-4" />
</button>
{{-- Previous page --}}
<button type="button"
wire:click="previousConnectionsPage"
@if($connectionsCurrentPage === 1) disabled @endif
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Previous page">
<x-ri-arrow-left-s-line class="size-4" />
</button>
{{-- Page numbers --}}
@php
$connectionsTotalPages = $this->connectionsTotalPages;
$startPage = max(1, $connectionsCurrentPage - 2);
$endPage = min($connectionsTotalPages, $connectionsCurrentPage + 2);
@endphp
@if($startPage > 1)
<button type="button" wire:click="goToConnectionsPage(1)" class="px-3 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors text-sm">1</button>
@if($startPage > 2)
<span class="px-2 text-base/60">...</span>
@endif
@endif
@for($i = $startPage; $i <= $endPage; $i++)
<button type="button"
wire:click="goToConnectionsPage({{ $i }})"
class="px-3 py-1 border rounded text-sm transition-colors {{ $connectionsCurrentPage === $i ? 'bg-primary text-white border-primary' : 'border-neutral hover:bg-background-secondary' }}">
{{ $i }}
</button>
@endfor
@if($endPage < $connectionsTotalPages)
@if($endPage < $connectionsTotalPages - 1)
<span class="px-2 text-base/60">...</span>
@endif
<button type="button" wire:click="goToConnectionsPage({{ $connectionsTotalPages }})" class="px-3 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors text-sm">{{ $connectionsTotalPages }}</button>
@endif
{{-- Next page --}}
<button type="button"
wire:click="nextConnectionsPage"
@if($connectionsCurrentPage === $connectionsTotalPages) disabled @endif
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Next page">
<x-ri-arrow-right-s-line class="size-4" />
</button>
{{-- Last page --}}
<button type="button"
wire:click="goToConnectionsPage({{ $connectionsTotalPages }})"
@if($connectionsCurrentPage === $connectionsTotalPages) disabled @endif
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Last page">
<x-ri-arrow-right-double-line class="size-4" />
</button>
</div>
</div>
</div>
@endif
</div>
@endif
</div>
<div x-show="tab === 'top-connections'" style="width: 100%; min-width: 100%; flex-shrink: 0; box-sizing: border-box;">
@if(($networkAnalysis['topConnectionCount'] ?? 0) == 0)
<div class="text-center py-6 text-sm text-base/60">No connection data available.</div>
@else
<div x-data="pxStatTableKit('px-stat-net-peers')" style="width: 100%;">
<div class="p-4 border-b border-neutral bg-background/50">
<div class="flex items-center gap-3 flex-wrap">
<div class="flex-1 min-w-[200px]">
<input type="text"
x-model="search"
@input="filterTable()"
placeholder="Search peers..."
class="w-full px-3 py-2 text-sm bg-background border border-neutral rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50">
</div>
<button @click="search = ''; filterTable()"
x-show="search"
class="px-3 py-2 text-sm text-base/70 hover:text-base border border-neutral rounded-md hover:bg-background/80">
Clear
</button>
</div>
</div>
<div class="overflow-x-auto" style="width: 100%;">
<table class="w-full text-sm" id="px-stat-net-peers">
<thead>
<tr class="border-b border-neutral bg-background">
<th @click.stop="sortTable('endpoint')" data-sort="endpoint" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
<div class="flex items-center gap-2">
Endpoint
<x-ri-arrow-up-down-line class="size-3" />
</div>
</th>
<th @click.stop="sortTable('port')" data-sort="port" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
<div class="flex items-center gap-2">
Port
<x-ri-arrow-up-down-line class="size-3" />
</div>
</th>
<th @click.stop="sortTable('connections')" data-sort="connections" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
<div class="flex items-center gap-2">
Connections
<x-ri-arrow-up-down-line class="size-3" />
</div>
</th>
<th @click.stop="sortTable('traffic')" data-sort="traffic" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
<div class="flex items-center gap-2">
Traffic
<x-ri-arrow-up-down-line class="size-3" />
</div>
</th>
</tr>
</thead>
<tbody class="divide-y divide-neutral/30">
@foreach($this->paginatedTopConnections as $index => $row)
@php
$endpointIP = $row['endpoint'] ?? '—';
$port = $row['port'] ?? 0;
$connections = $row['count'] ?? 0;
$bytes = $row['bytes'] ?? 0;
$bytesForSort = $bytes;
@endphp
<tr wire:key="top-conn-{{ $topConnectionsCurrentPage }}-{{ $index }}" data-statistics-data-row class="hover:bg-background-secondary/50 transition-colors"
data-endpoint="{{ strtolower($endpointIP) }}"
data-port="{{ $port }}"
data-connections="{{ $connections }}"
data-traffic="{{ $bytesForSort }}">
<td class="py-2 px-3 font-mono text-xs">
@php
$isEndpointPublic = $endpointIP !== '—' && $this->checkIsPublicIP($endpointIP);
@endphp
<div class="flex items-center gap-1">
<span>{{ $endpointIP }}</span>
@if($isEndpointPublic)
<div class="relative" x-data="{ open: false, loading: false, ipData: null, ip: '{{ $endpointIP }}' }" x-cloak>
<button type="button" @click="
if (!ipData && !loading) {
loading = true;
open = true;
window.fetchIPLocation(ip).then(data => {
ipData = data;
loading = false;
}).catch(err => {
ipData = { status: 'error', message: 'Failed to load location data' };
loading = false;
});
} else {
open = true;
}
" class="text-base/50 hover:text-base/70 transition-colors" title="View IP Location">
<x-ri-map-pin-line class="size-3" />
</button>
<!-- Modal - Reusing same structure as Active Connections -->
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4"
style="display: none;">
<div @click.stop class="relative w-full max-w-2xl bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl max-h-[90vh] overflow-y-auto overflow-x-hidden">
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors z-10">
<x-ri-close-line class="size-5" />
</button>
<div class="p-6 break-words">
<div class="flex items-center gap-2 mb-4">
<x-ri-map-pin-line class="size-5 text-primary flex-shrink-0" />
<h3 class="text-lg font-semibold break-words">IP Address: <span class="font-mono break-all">{{ $endpointIP }}</span></h3>
</div>
<div x-show="loading" class="text-center py-8">
<svg fill="currentColor" stroke="currentColor" viewBox="0 0 128 128" class="size-6 mx-auto mb-2">
<g>
<path stroke-width="5" d="M31.6 8.98a4.02 4.02 0 0 0 1.7 5.3l6.12 3.22a52.56 52.56 0 1 0 63.13 10.73L96 33.97a43.88 43.88 0 1 1-53-8.5l-3.65 7.4a4.02 4.02 0 0 0 1.72 5.3 3.73 3.73 0 0 0 5.13-1.67l7.66-15.63a4.18 4.18 0 0 0-1.75-5.4L36.74 7.32a3.73 3.73 0 0 0-5.13 1.66z" fill="currentColor" stroke="currentColor"/>
<animateTransform attributeName="transform" type="rotate" from="0 64 64" to="360 64 64" dur="1000ms" repeatCount="indefinite"></animateTransform>
</g>
</svg>
<p class="text-sm text-base/60">Loading location data...</p>
</div>
<div x-show="ipData && !loading">
<template x-if="ipData && ipData.status === 'success'">
<div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div class="space-y-2 break-words">
<div class="break-words"><span class="text-xs text-base/60">Country:</span> <span class="font-semibold text-sm break-words" x-text="ipData.country"></span></div>
<div class="break-words"><span class="text-xs text-base/60">Region:</span> <span class="font-semibold text-sm break-words" x-text="ipData.regionName + ' (' + ipData.region + ')'"></span></div>
<div class="break-words"><span class="text-xs text-base/60">City:</span> <span class="font-semibold text-sm break-words" x-text="ipData.city"></span></div>
<div class="break-words"><span class="text-xs text-base/60">ZIP Code:</span> <span class="font-semibold text-sm break-words" x-text="ipData.zip"></span></div>
<div class="break-words"><span class="text-xs text-base/60">Timezone:</span> <span class="font-semibold text-sm break-words" x-text="ipData.timezone"></span></div>
</div>
<div class="space-y-2 break-words">
<div class="break-words"><span class="text-xs text-base/60">ISP:</span> <span class="font-semibold text-sm break-words" x-text="ipData.isp"></span></div>
<div class="break-words"><span class="text-xs text-base/60">Organization:</span> <span class="font-semibold text-sm break-words" x-text="ipData.org"></span></div>
<div class="break-words"><span class="text-xs text-base/60">AS:</span> <span class="font-semibold text-sm break-all" x-text="ipData.as"></span></div>
<div class="break-words"><span class="text-xs text-base/60">Coordinates:</span> <span class="font-semibold text-sm font-mono break-all" x-text="ipData.lat + ', ' + ipData.lon"></span></div>
</div>
</div>
<div x-show="ipData.lat && ipData.lon" class="mt-4 w-full">
<p class="text-xs text-base/60 mb-2">Location Map:</p>
<div class="w-full overflow-hidden rounded-lg border border-neutral/20" style="min-height: 256px;">
<iframe :src="'https://www.openstreetmap.org/export/embed.html?bbox=' + (parseFloat(ipData.lon) - 0.01) + ',' + (parseFloat(ipData.lat) - 0.01) + ',' + (parseFloat(ipData.lon) + 0.01) + ',' + (parseFloat(ipData.lat) + 0.01) + '&layer=mapnik&marker=' + ipData.lat + ',' + ipData.lon"
class="w-full h-64 rounded-lg border-0"
frameborder="0"
loading="lazy"
allowfullscreen></iframe>
</div>
</div>
</div>
</template>
<template x-if="ipData && ipData.status !== 'success'">
<div class="text-center py-4">
<p class="text-sm text-warning" x-text="ipData.message || 'Failed to load location data'"></p>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
@endif
</div>
</td>
<td class="py-2 px-3 text-xs">
@if(isset($row['port']) && $row['port'] !== '—')
<div class="flex items-center gap-1">
<span>{{ $row['port'] }}</span>
<div class="relative" x-data="{ open: false, portDescription: window.getPortDescription({{ (int)($row['port'] ?? 0) }}) }" x-cloak>
<button type="button" @click="open = true" class="text-base/50 hover:text-base/70 transition-colors">
<x-ri-information-line class="size-3" />
</button>
<!-- Modal Backdrop -->
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
style="display: none;">
</div>
<!-- Modal -->
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
@click.self="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 flex items-center justify-center p-4"
style="display: none;">
<div class="relative w-full max-w-md bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-4">
<button @click="open = false" class="absolute top-3 right-3 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-4" />
</button>
<div class="pr-6">
<div class="flex items-center gap-2 mb-2">
<x-ri-information-line class="size-5 text-primary" />
<h3 class="text-sm font-semibold">Port {{ $row['port'] }} Description</h3>
</div>
<p class="text-xs text-base/80" x-text="portDescription"></p>
</div>
</div>
</div>
</div>
</div>
@else
—
@endif
</td>
<td class="py-2 px-3 text-xs">{{ number_format($row['count']) }}</td>
<td class="py-2 px-3 text-xs">
<div class="flex flex-col">
<span class="font-semibold">{{ $row['bytes_human'] }}</span>
<span class="text-base/50">{{ number_format($row['packets']) }} packets</span>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
{{-- Top Connections Pagination Controls --}}
@if(($networkAnalysis['topConnectionCount'] ?? 0) > 0)
<div class="p-4 border-t border-neutral flex flex-col sm:flex-row items-center justify-between gap-4">
{{-- Records per page selector --}}
<div class="flex items-center gap-3">
<label class="text-sm text-base/70">Show:</label>
<select wire:model.live="topConnectionsPerPage" class="px-3 py-1.5 border border-neutral rounded-lg bg-background text-sm">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="250">250</option>
</select>
<span class="text-sm text-base/60">per page</span>
</div>
{{-- Page info and navigation --}}
<div class="flex items-center gap-4">
<span class="text-sm text-base/60">
Showing {{ (($topConnectionsCurrentPage - 1) * $topConnectionsPerPage) + 1 }} - {{ min($topConnectionsCurrentPage * $topConnectionsPerPage, $networkAnalysis['topConnectionCount'] ?? 0) }} of {{ $networkAnalysis['topConnectionCount'] ?? 0 }} peers
</span>
<div class="flex items-center gap-1">
{{-- First page --}}
<button type="button"
wire:click="goToTopConnectionsPage(1)"
@if($topConnectionsCurrentPage === 1) disabled @endif
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="First page">
<x-ri-arrow-left-double-line class="size-4" />
</button>
{{-- Previous page --}}
<button type="button"
wire:click="previousTopConnectionsPage"
@if($topConnectionsCurrentPage === 1) disabled @endif
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Previous page">
<x-ri-arrow-left-s-line class="size-4" />
</button>
{{-- Page numbers --}}
@php
$topConnectionsTotalPages = $this->topConnectionsTotalPages;
$startPage = max(1, $topConnectionsCurrentPage - 2);
$endPage = min($topConnectionsTotalPages, $topConnectionsCurrentPage + 2);
@endphp
@if($startPage > 1)
<button type="button" wire:click="goToTopConnectionsPage(1)" class="px-3 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors text-sm">1</button>
@if($startPage > 2)
<span class="px-2 text-base/60">...</span>
@endif
@endif
@for($i = $startPage; $i <= $endPage; $i++)
<button type="button"
wire:click="goToTopConnectionsPage({{ $i }})"
class="px-3 py-1 border rounded text-sm transition-colors {{ $topConnectionsCurrentPage === $i ? 'bg-primary text-white border-primary' : 'border-neutral hover:bg-background-secondary' }}">
{{ $i }}
</button>
@endfor
@if($endPage < $topConnectionsTotalPages)
@if($endPage < $topConnectionsTotalPages - 1)
<span class="px-2 text-base/60">...</span>
@endif
<button type="button" wire:click="goToTopConnectionsPage({{ $topConnectionsTotalPages }})" class="px-3 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors text-sm">{{ $topConnectionsTotalPages }}</button>
@endif
{{-- Next page --}}
<button type="button"
wire:click="nextTopConnectionsPage"
@if($topConnectionsCurrentPage === $topConnectionsTotalPages) disabled @endif
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Next page">
<x-ri-arrow-right-s-line class="size-4" />
</button>
{{-- Last page --}}
<button type="button"
wire:click="goToTopConnectionsPage({{ $topConnectionsTotalPages }})"
@if($topConnectionsCurrentPage === $topConnectionsTotalPages) disabled @endif
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Last page">
<x-ri-arrow-right-double-line class="size-4" />
</button>
</div>
</div>
</div>
@endif
</div>
@endif
</div>
<div x-show="tab === 'ports'" style="width: 100%; min-width: 100%; flex-shrink: 0; box-sizing: border-box;">
@if(($networkAnalysis['topPortCount'] ?? 0) == 0)
<div class="text-center py-6 text-sm text-base/60">No port statistics are available.</div>
@else
<div x-data="pxStatTableKit('px-stat-net-ports')" style="width: 100%;">
<div class="p-4 border-b border-neutral bg-background/50">
<div class="flex items-center gap-3 flex-wrap">
<div class="flex-1 min-w-[200px]">
<input type="text"
x-model="search"
@input="filterTable()"
placeholder="Search ports..."
class="w-full px-3 py-2 text-sm bg-background border border-neutral rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50">
</div>
<button @click="search = ''; filterTable()"
x-show="search"
class="px-3 py-2 text-sm text-base/70 hover:text-base border border-neutral rounded-md hover:bg-background/80">
Clear
</button>
</div>
</div>
<div class="overflow-x-auto" style="width: 100%;">
<table class="w-full text-sm" id="px-stat-net-ports">
<thead>
<tr class="border-b border-neutral bg-background">
<th @click.stop="sortTable('port')" data-sort="port" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
<div class="flex items-center gap-2">
Port
<x-ri-arrow-up-down-line class="size-3" />
</div>
</th>
<th @click.stop="sortTable('connections')" data-sort="connections" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
<div class="flex items-center gap-2">
Connections
<x-ri-arrow-up-down-line class="size-3" />
</div>
</th>
<th @click.stop="sortTable('traffic')" data-sort="traffic" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
<div class="flex items-center gap-2">
Traffic
<x-ri-arrow-up-down-line class="size-3" />
</div>
</th>
</tr>
</thead>
<tbody class="divide-y divide-neutral/30">
@foreach($this->paginatedTopPorts as $index => $row)
@php
$port = $row['port'] ?? 0;
$connections = $row['connections'] ?? 0;
$bytes = $row['bytes'] ?? 0;
$bytesForSort = $bytes;
@endphp
<tr wire:key="top-port-{{ $topPortsCurrentPage }}-{{ $index }}" data-statistics-data-row class="hover:bg-background-secondary/50 transition-colors"
data-port="{{ $port }}"
data-connections="{{ $connections }}"
data-traffic="{{ $bytesForSort }}">
<td class="py-2 px-3 font-semibold text-base/80">
<div class="flex items-center gap-1">
<span>{{ $row['port'] }}</span>
<div class="relative" x-data="{ open: false, portDescription: window.getPortDescription({{ (int)($row['port'] ?? 0) }}) }" x-cloak>
<button type="button" @click="open = true" class="text-base/50 hover:text-base/70 transition-colors">
<x-ri-information-line class="size-3" />
</button>
<!-- Modal Backdrop -->
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
style="display: none;">
</div>
<!-- Modal -->
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
@click.self="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 flex items-center justify-center p-4"
style="display: none;">
<div class="relative w-full max-w-md bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-4">
<button @click="open = false" class="absolute top-3 right-3 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-4" />
</button>
<div class="pr-6">
<div class="flex items-center gap-2 mb-2">
<x-ri-information-line class="size-5 text-primary" />
<h3 class="text-sm font-semibold">Port {{ $row['port'] }} Description</h3>
</div>
<p class="text-xs text-base/80" x-text="portDescription"></p>
</div>
</div>
</div>
</div>
</div>
</td>
<td class="py-2 px-3 text-xs">{{ number_format($row['connections']) }}</td>
<td class="py-2 px-3 text-xs">
<div class="flex flex-col">
<span class="font-semibold">{{ $row['bytes_human'] }}</span>
<span class="text-base/50">{{ number_format($row['packets']) }} packets</span>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
{{-- Top Ports Pagination Controls --}}
@if(($networkAnalysis['topPortCount'] ?? 0) > 0)
<div class="p-4 border-t border-neutral flex flex-col sm:flex-row items-center justify-between gap-4">
{{-- Records per page selector --}}
<div class="flex items-center gap-3">
<label class="text-sm text-base/70">Show:</label>
<select wire:model.live="topPortsPerPage" class="px-3 py-1.5 border border-neutral rounded-lg bg-background text-sm">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="250">250</option>
</select>
<span class="text-sm text-base/60">per page</span>
</div>
{{-- Page info and navigation --}}
<div class="flex items-center gap-4">
<span class="text-sm text-base/60">
Showing {{ (($topPortsCurrentPage - 1) * $topPortsPerPage) + 1 }} - {{ min($topPortsCurrentPage * $topPortsPerPage, $networkAnalysis['topPortCount'] ?? 0) }} of {{ $networkAnalysis['topPortCount'] ?? 0 }} ports
</span>
<div class="flex items-center gap-1">
{{-- First page --}}
<button type="button"
wire:click="goToTopPortsPage(1)"
@if($topPortsCurrentPage === 1) disabled @endif
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="First page">
<x-ri-arrow-left-double-line class="size-4" />
</button>
{{-- Previous page --}}
<button type="button"
wire:click="previousTopPortsPage"
@if($topPortsCurrentPage === 1) disabled @endif
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Previous page">
<x-ri-arrow-left-s-line class="size-4" />
</button>
{{-- Page numbers --}}
@php
$topPortsTotalPages = $this->topPortsTotalPages;
$startPage = max(1, $topPortsCurrentPage - 2);
$endPage = min($topPortsTotalPages, $topPortsCurrentPage + 2);
@endphp
@if($startPage > 1)
<button type="button" wire:click="goToTopPortsPage(1)" class="px-3 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors text-sm">1</button>
@if($startPage > 2)
<span class="px-2 text-base/60">...</span>
@endif
@endif
@for($i = $startPage; $i <= $endPage; $i++)
<button type="button"
wire:click="goToTopPortsPage({{ $i }})"
class="px-3 py-1 border rounded text-sm transition-colors {{ $topPortsCurrentPage === $i ? 'bg-primary text-white border-primary' : 'border-neutral hover:bg-background-secondary' }}">
{{ $i }}
</button>
@endfor
@if($endPage < $topPortsTotalPages)
@if($endPage < $topPortsTotalPages - 1)
<span class="px-2 text-base/60">...</span>
@endif
<button type="button" wire:click="goToTopPortsPage({{ $topPortsTotalPages }})" class="px-3 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors text-sm">{{ $topPortsTotalPages }}</button>
@endif
{{-- Next page --}}
<button type="button"
wire:click="nextTopPortsPage"
@if($topPortsCurrentPage === $topPortsTotalPages) disabled @endif
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Next page">
<x-ri-arrow-right-s-line class="size-4" />
</button>
{{-- Last page --}}
<button type="button"
wire:click="goToTopPortsPage({{ $topPortsTotalPages }})"
@if($topPortsCurrentPage === $topPortsTotalPages) disabled @endif
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Last page">
<x-ri-arrow-right-double-line class="size-4" />
</button>
</div>
</div>
</div>
@endif
</div>
@endif
</div>
<div x-show="tab === 'security'" style="width: 100%; min-width: 100%; flex-shrink: 0; box-sizing: border-box;">
@if(empty($networkAnalysis['security']))
<div class="text-center py-6 text-sm text-base/60">No security threats detected. All connections appear to be safe.</div>
@else
<div class="space-y-3" style="width: 100%;">
@foreach($networkAnalysis['security'] as $warning)
@php
$conn = $warning['connection'] ?? [];
@endphp
@php
$riskLevel = $warning['risk_level'] ?? 'Medium';
$attackType = $warning['attack_type'] ?? 'Unknown Threat';
$riskDescription = $warning['risk_description'] ?? 'Malicious IP detected from blocklist';
$riskBadgeClass = match($riskLevel) {
'Critical' => 'bg-danger/20 text-danger border-danger/30',
'High' => 'bg-warning/20 text-warning border-warning/30',
'Medium' => 'bg-warning/10 text-warning border-warning/20',
'Low' => 'bg-base/10 text-base/70 border-base/20',
default => 'bg-warning/10 text-warning border-warning/20',
};
@endphp
<div class="border border-warning/50 rounded-lg p-4 bg-warning/10" x-data="{ expanded: false }">
<div class="flex items-start gap-3">
<x-ri-alert-line class="size-5 text-warning flex-shrink-0 mt-0.5" />
<div class="flex-1">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2 flex-wrap">
<span class="text-sm font-semibold text-warning">Security Threat Detected</span>
<span class="text-xs px-2 py-1 rounded-full font-semibold bg-warning/20 text-warning uppercase">
{{ $warning['type'] }}
</span>
<span class="text-xs px-2 py-1 rounded-full font-semibold border {{ $riskBadgeClass }} uppercase">
{{ $riskLevel }} Risk
</span>
<span class="text-xs px-2 py-1 rounded-full font-semibold bg-background border border-neutral/30 text-base/90">
{{ $attackType }}
</span>
@if(isset($warning['quantity']) && $warning['quantity'] > 1)
<span class="text-xs px-2 py-1 rounded-full font-semibold bg-primary/20 text-primary border border-primary/30">
{{ $warning['quantity'] }}x Repeated
</span>
@endif
</div>
<button
@click="expanded = !expanded"
class="text-base/50 hover:text-base/70 transition-colors flex-shrink-0"
x-bind:title="expanded ? 'Collapse details' : 'Expand details'"
>
<x-ri-arrow-down-s-line
class="size-4 transition-transform duration-200"
x-bind:class="expanded ? 'rotate-180' : ''"
/>
</button>
</div>
<p class="text-sm text-base/90 mb-2">
{{ $warning['threat'] }}
@if(isset($warning['quantity']) && $warning['quantity'] > 1)
<span class="text-xs text-base/60 ml-2">({{ $warning['quantity'] }} similar connection{{ $warning['quantity'] > 1 ? 's' : '' }} detected)</span>
@endif
</p>
@php
$threatIP = $warning['ip'] ?? '—';
$isThreatPublic = $threatIP !== '—' && $this->checkIsPublicIP($threatIP);
@endphp
<div class="text-xs mb-2">
<span class="text-base/60">IP Address:</span>
<span class="font-mono font-semibold ml-1">{{ $threatIP }}</span>
@if($isThreatPublic)
<div class="relative inline-block ml-1" x-data="{ open: false, loading: false, ipData: null, ip: '{{ $threatIP }}' }" x-cloak>
<button type="button" @click="
if (!ipData && !loading) {
loading = true;
open = true;
window.fetchIPLocation(ip).then(data => {
ipData = data;
loading = false;
}).catch(err => {
ipData = { status: 'error', message: 'Failed to load location data' };
loading = false;
});
} else {
open = true;
}
" class="text-base/50 hover:text-base/70 transition-colors" title="View IP Location">
<x-ri-map-pin-line class="size-3" />
</button>
<!-- IP Location Modal (same as before) -->
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4"
style="display: none;">
<div @click.stop class="relative w-full max-w-4xl bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl max-h-[90vh] overflow-y-auto overflow-x-hidden">
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors z-10">
<x-ri-close-line class="size-5" />
</button>
<div class="p-6 break-words">
<div class="flex items-center gap-2 mb-4">
<x-ri-map-pin-line class="size-5 text-warning flex-shrink-0" />
<h3 class="text-lg font-semibold break-words">IP Address: <span class="font-mono break-all">{{ $threatIP }}</span></h3>
</div>
<div x-show="loading" class="text-center py-8">
<svg fill="currentColor" stroke="currentColor" viewBox="0 0 128 128" class="size-6 mx-auto mb-2">
<g>
<path stroke-width="5" d="M31.6 8.98a4.02 4.02 0 0 0 1.7 5.3l6.12 3.22a52.56 52.56 0 1 0 63.13 10.73L96 33.97a43.88 43.88 0 1 1-53-8.5l-3.65 7.4a4.02 4.02 0 0 0 1.72 5.3 3.73 3.73 0 0 0 5.13-1.67l7.66-15.63a4.18 4.18 0 0 0-1.75-5.4L36.74 7.32a3.73 3.73 0 0 0-5.13 1.66z" fill="currentColor" stroke="currentColor"/>
<animateTransform attributeName="transform" type="rotate" from="0 64 64" to="360 64 64" dur="1000ms" repeatCount="indefinite"></animateTransform>
</g>
</svg>
<p class="text-sm text-base/60">Loading location data...</p>
</div>
<div x-show="ipData && !loading">
<template x-if="ipData && (ipData.status === 'success')">
<div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div class="space-y-2 break-words">
<div class="break-words"><span class="text-xs text-base/60">Country:</span> <span class="font-semibold text-sm break-words" x-text="ipData.country"></span></div>
<div class="break-words"><span class="text-xs text-base/60">Region:</span> <span class="font-semibold text-sm break-words" x-text="ipData.regionName + ' (' + ipData.region + ')'"></span></div>
<div class="break-words"><span class="text-xs text-base/60">City:</span> <span class="font-semibold text-sm break-words" x-text="ipData.city"></span></div>
<div class="break-words"><span class="text-xs text-base/60">ZIP Code:</span> <span class="font-semibold text-sm break-words" x-text="ipData.zip"></span></div>
<div class="break-words"><span class="text-xs text-base/60">Timezone:</span> <span class="font-semibold text-sm break-words" x-text="ipData.timezone"></span></div>
</div>
<div class="space-y-2 break-words">
<div class="break-words"><span class="text-xs text-base/60">ISP:</span> <span class="font-semibold text-sm break-words" x-text="ipData.isp"></span></div>
<div class="break-words"><span class="text-xs text-base/60">Organization:</span> <span class="font-semibold text-sm break-words" x-text="ipData.org"></span></div>
<div class="break-words"><span class="text-xs text-base/60">AS:</span> <span class="font-semibold text-sm break-all" x-text="ipData.as"></span></div>
<div class="break-words"><span class="text-xs text-base/60">Coordinates:</span> <span class="font-semibold text-sm font-mono break-all" x-text="ipData.lat + ', ' + ipData.lon"></span></div>
</div>
</div>
<div x-show="ipData.lat && ipData.lon" class="mt-4 w-full" style="max-width: 100%; box-sizing: border-box;">
<p class="text-xs text-base/60 mb-2">Location Map:</p>
<div class="w-full rounded-lg border border-neutral/20 overflow-hidden bg-background/50" style="position: relative; width: 100%; max-width: 100%; height: 256px; min-height: 256px; box-sizing: border-box;">
<iframe
x-bind:src="'https://www.openstreetmap.org/export/embed.html?bbox=' + encodeURIComponent((parseFloat(ipData.lon) - 0.01) + ',' + (parseFloat(ipData.lat) - 0.01) + ',' + (parseFloat(ipData.lon) + 0.01) + ',' + (parseFloat(ipData.lat) + 0.01)) + '&layer=mapnik&marker=' + encodeURIComponent(ipData.lat + ',' + ipData.lon)"
style="width: 100%; height: 100%; border: 0; display: block; margin: 0; padding: 0;"
frameborder="0"
loading="lazy"
allowfullscreen
title="Location Map"></iframe>
</div>
</div>
</div>
</template>
<template x-if="ipData && (ipData.status === 'fail' || ipData.status === 'error')">
<div class="text-center py-4">
<p class="text-sm text-warning" x-text="ipData.message || 'Failed to load location data'"></p>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
@endif
</div>
<div x-show="expanded"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="space-y-3"
style="display: none;">
<div class="mb-3 p-3 rounded-md bg-background/50 border border-neutral/20">
<div class="text-xs font-semibold text-base/90 mb-1">Risk Analysis:</div>
<p class="text-xs text-base/75 leading-relaxed">{{ $riskDescription }}</p>
</div>
@if(isset($warning['mitigations']) && !empty($warning['mitigations']))
<div class="mb-3">
<div class="text-xs font-semibold text-base/90 mb-2">Suggested Mitigations:</div>
<div class="space-y-2">
@foreach(array_slice($warning['mitigations'], 0, 5) as $mitigation)
@php
$priorityColor = match($mitigation['priority'] ?? 'Medium') {
'Critical' => 'border-danger/30 bg-danger/5',
'High' => 'border-warning/30 bg-warning/5',
'Medium' => 'border-primary/30 bg-primary/5',
'Low' => 'border-base/20 bg-base/5',
default => 'border-base/20 bg-base/5',
};
$priorityText = match($mitigation['priority'] ?? 'Medium') {
'Critical' => 'text-danger',
'High' => 'text-warning',
'Medium' => 'text-primary',
'Low' => 'text-base/70',
default => 'text-base/70',
};
@endphp
<div class="p-2 rounded-md border {{ $priorityColor }}">
<div class="flex items-center gap-2 mb-1">
<span class="text-xs font-semibold {{ $priorityText }} uppercase">
{{ $mitigation['priority'] ?? 'Medium' }}
</span>
<span class="text-xs font-semibold text-base/90">
{{ $mitigation['title'] ?? 'Mitigation' }}
</span>
</div>
<p class="text-xs text-base/75 leading-relaxed mb-1">{{ $mitigation['description'] ?? '' }}</p>
<div class="mt-1 p-1.5 rounded bg-background/50 border border-neutral/10">
<p class="text-xs font-mono text-base/70 break-all">{{ $mitigation['action'] ?? '' }}</p>
</div>
</div>
@endforeach
@if(count($warning['mitigations']) > 5)
<p class="text-xs text-base/60 italic">+ {{ count($warning['mitigations']) - 5 }} more mitigation suggestion{{ count($warning['mitigations']) - 5 > 1 ? 's' : '' }} available</p>
@endif
</div>
</div>
@endif
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 text-xs">
@if(isset($conn['protocol']))
<div>
<span class="text-base/60">Protocol:</span>
<span class="font-semibold ml-1">{{ $conn['protocol'] }}</span>
</div>
@endif
@if(isset($conn['destination']))
<div>
<span class="text-base/60">{{ $warning['type'] === 'source' ? 'Destination' : 'Source' }}:</span>
<span class="font-mono font-semibold ml-1">{{ $warning['type'] === 'source' ? $conn['destination'] : $conn['source'] }}</span>
@if(isset($conn[$warning['type'] === 'source' ? 'destination_port' : 'source_port']))
<span class="font-mono text-base/70">:{{ $conn[$warning['type'] === 'source' ? 'destination_port' : 'source_port'] }}</span>
@endif
</div>
@endif
@if(isset($conn['bytes_human']))
<div>
<span class="text-base/60">Traffic:</span>
<span class="font-semibold ml-1">{{ $conn['bytes_human'] }}</span>
</div>
@endif
</div>
</div>
</div>
</div>
</div>
@endforeach
</div>
@endif
</div>
</div>
@endif
</div>
</div>