refactor: simplify Proxmox Firewall log handling by removing unnecessary log grouping and optimizing log fetching logic

This commit is contained in:
2025-12-02 16:54:50 -05:00
parent 16be121e8c
commit 3f28a92ece
2 changed files with 25 additions and 975 deletions

View File

@@ -21,7 +21,7 @@ class Firewall extends Component
public array $networkAdapters = [];
public array $adapterFirewallStates = [];
public array $firewallLogs = [];
public int $logLimit = 500;
public int $logLimit = 100;
public array $availableIps = [];
public bool $showDeleteModal = false;
public ?int $deletingRulePos = null;
@@ -148,11 +148,6 @@ class Firewall extends Component
$newState = $currentState ? 0 : 1;
$payload = ['enable' => $newState];
// If enabling firewall, also disable DHCP
if ($newState === 1) {
$payload['dhcp'] = 0;
}
$response = $this->firewallRequest(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/options',
'put',
@@ -807,10 +802,7 @@ class Firewall extends Component
$response = $this->firewallRequest(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/log',
'get',
[
'limit' => $this->logLimit,
'start' => 0
]
['limit' => $this->logLimit]
);
if (! $response->successful()) {
@@ -832,50 +824,17 @@ class Firewall extends Component
return;
}
// Proxmox firewall log endpoint returns text/plain format
// Each log entry is on a new line
$body = $response->body();
$rawLogs = [];
$contentType = strtolower($response->header('Content-Type', ''));
if (! empty($body)) {
if (str_contains($contentType, 'application/json')) {
// When Accept header allows JSON, Proxmox returns structured data
$jsonData = $response->json();
$entries = [];
if (is_array($jsonData)) {
$entries = $jsonData['data'] ?? $jsonData;
}
if (is_array($entries)) {
foreach ($entries as $entry) {
$logText = null;
if (is_array($entry)) {
$logText = $entry['log']
?? $entry['text']
?? $entry['t']
?? $entry['n']
?? null;
} elseif (is_string($entry)) {
$logText = $entry;
}
if (is_string($logText) && trim($logText) !== '') {
$rawLogs[] = trim($logText);
}
}
}
} else {
// Proxmox firewall log endpoint may also return text/plain
// Each log entry is on a new line (handle both \n and \r\n)
$lines = preg_split('/\r\n|\r|\n/', $body);
foreach ($lines as $line) {
$line = trim((string) $line);
if ($line !== '') {
$rawLogs[] = $line;
}
}
}
// Split by newlines and filter empty lines
$lines = explode("\n", trim($body));
$rawLogs = array_filter($lines, static function ($line) {
return ! empty(trim($line));
});
}
\Log::debug('Firewall logs fetched from Proxmox API', [
@@ -883,12 +842,11 @@ class Firewall extends Component
'body_length' => strlen($body),
'content_type' => $response->header('Content-Type'),
'sample' => ! empty($rawLogs) ? substr($rawLogs[array_key_first($rawLogs)], 0, 200) : null,
'first_5_lines' => array_slice($rawLogs, 0, 5),
]);
$this->firewallLogs = [];
foreach ($rawLogs as $index => $logEntry) {
foreach ($rawLogs as $logEntry) {
$logText = '';
if (is_string($logEntry)) {
@@ -910,36 +868,14 @@ class Firewall extends Component
continue;
}
$parsedLog = $this->parseFirewallLog($logText);
$this->firewallLogs[] = $parsedLog;
// Debug first few parsed logs
if ($index < 3) {
\Log::debug('Parsed firewall log entry', [
'index' => $index,
'raw' => substr($logText, 0, 200),
'parsed' => $parsedLog,
]);
}
$this->firewallLogs[] = $this->parseFirewallLog($logText);
}
// Group similar logs
$this->firewallLogs = $this->groupSimilarLogs($this->firewallLogs);
\Log::debug('Firewall logs processing complete', [
'raw_logs_count' => count($rawLogs),
'parsed_logs_count' => count($this->firewallLogs),
'sample_parsed_logs' => array_slice($this->firewallLogs, 0, 3),
]);
if (empty($this->firewallLogs) && ! empty($rawLogs)) {
$this->logError = 'Logs received but could not be processed.';
} elseif (empty($this->firewallLogs)) {
$this->logError = 'No firewall logs available. Ensure firewall logging is enabled in Proxmox (Datacenter > Firewall > Options).';
}
// Dispatch event to update frontend without full re-render
$this->dispatch('logs-updated', logs: $this->firewallLogs);
} catch (\Exception $e) {
$errorMessage = $e->getMessage();
if (method_exists($e, 'response') && $e->response()) {
@@ -963,76 +899,6 @@ class Firewall extends Component
}
}
/**
* Group similar log entries together
*/
protected function groupSimilarLogs(array $logs): array
{
if (empty($logs)) {
return [];
}
$grouped = [];
$groups = [];
foreach ($logs as $log) {
// Create a signature for grouping: action + protocol + source + destination + dest_port
$signature = sprintf(
'%s|%s|%s|%s|%s',
$log['action'] ?? '',
$log['protocol'] ?? '',
$log['source'] ?? '',
$log['destination'] ?? '',
$log['destination_port'] ?? ''
);
if (!isset($groups[$signature])) {
$groups[$signature] = [
'signature' => $signature,
'count' => 0,
'first_log' => $log,
'last_timestamp' => $log['timestamp_iso'] ?? null,
'first_timestamp' => $log['timestamp_iso'] ?? null,
'logs' => [],
];
}
$groups[$signature]['count']++;
$groups[$signature]['logs'][] = $log;
// Update timestamps
if ($log['timestamp_iso']) {
if (!$groups[$signature]['first_timestamp'] || $log['timestamp_iso'] < $groups[$signature]['first_timestamp']) {
$groups[$signature]['first_timestamp'] = $log['timestamp_iso'];
}
if (!$groups[$signature]['last_timestamp'] || $log['timestamp_iso'] > $groups[$signature]['last_timestamp']) {
$groups[$signature]['last_timestamp'] = $log['timestamp_iso'];
}
}
}
// Convert groups to array and add group metadata
foreach ($groups as $group) {
$firstLog = $group['first_log'];
$firstLog['is_group'] = $group['count'] > 1;
$firstLog['group_count'] = $group['count'];
$firstLog['grouped_logs'] = $group['logs'];
$firstLog['group_first_timestamp'] = $group['first_timestamp'];
$firstLog['group_last_timestamp'] = $group['last_timestamp'];
$grouped[] = $firstLog;
}
// Sort by most recent timestamp
usort($grouped, function($a, $b) {
$aTime = $a['group_last_timestamp'] ?? $a['timestamp_iso'] ?? '';
$bTime = $b['group_last_timestamp'] ?? $b['timestamp_iso'] ?? '';
return strcmp($bTime, $aTime); // Descending order (most recent first)
});
return $grouped;
}
protected function parseFirewallLog(string $logText): array
{
$result = [
@@ -1135,101 +1001,6 @@ class Firewall extends Component
return $result;
}
/**
* Check if an IP address is public (callable from frontend)
*/
public function checkIsPublicIP(string $ip): bool
{
return $this->isPublicIP($ip);
}
/**
* Check if an IP address is public (not private/localhost)
*/
protected function isPublicIP(string $ip): bool
{
$ip = $this->extractIPFromString($ip);
if (empty($ip)) {
return false;
}
// Check for localhost
if (in_array(strtolower($ip), ['localhost', '127.0.0.1', '::1'])) {
return false;
}
// Validate IP format
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
return false;
}
// Check if it's a private IP
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return true;
}
return false;
}
/**
* Extract IP address from a string (removes port if present)
*/
protected function extractIPFromString(string $input): string
{
$input = trim($input);
// Handle IPv6 with port: [2001:db8::1]:8080
if (preg_match('/^\[([^\]]+)\](?::\d+)?$/', $input, $matches)) {
return $matches[1];
}
// Handle IPv4 with port: 192.168.1.1:8080
if (preg_match('/^([^:]+):\d+$/', $input, $matches)) {
// Make sure it's not IPv6 without brackets
if (!str_contains($matches[1], ':')) {
return $matches[1];
}
}
// Return as-is if no port detected
return $input;
}
/**
* Fetch IP geolocation data
*/
public function fetchIPLocation($ip): array
{
if (empty($ip)) {
return ['status' => 'fail', 'message' => 'IP address is required'];
}
// Extract and validate IP address
$cleanIP = $this->extractIPFromString((string)$ip);
if (!$this->isPublicIP($cleanIP)) {
return ['status' => 'fail', 'message' => 'Not a valid public IP address'];
}
try {
// Use HTTP protocol to fetch geolocation data
$response = \Illuminate\Support\Facades\Http::timeout(5)->get("http://ip-api.com/json/{$cleanIP}");
if ($response->successful()) {
return $response->json();
}
return ['status' => 'fail', 'message' => 'Failed to fetch location data'];
} catch (\Exception $e) {
\Log::error('Error fetching IP location', [
'ip' => $cleanIP,
'error' => $e->getMessage(),
]);
return ['status' => 'fail', 'message' => 'Error: ' . $e->getMessage()];
}
}
public function render()
{
return view('proxmox::firewall')->layoutData([

View File

@@ -74,9 +74,6 @@
opacity: 0.5;
cursor: not-allowed;
}
.firewall-log-search-input {
padding-left: 2.75rem;
}
</style>
<div class="space-y-6">
@@ -328,41 +325,11 @@
searchTerm: '',
sortField: 'timestamp_iso',
sortDirection: 'desc',
initialized: false,
seenRaws: new Set(),
initialize(initialLogs) {
if (this.initialized) return;
if (initialLogs && initialLogs.length > 0) {
this.logs = initialLogs.map((log, index) => ({
index,
...log,
}));
// Track which logs we've seen
this.logs.forEach(log => {
if (log.raw) this.seenRaws.add(log.raw);
});
this.initialized = true;
}
},
updateLogs(newLogs) {
if (!newLogs || newLogs.length === 0) return;
// Filter out logs we've already seen
const logsToAdd = newLogs.filter(log => {
if (!log.raw) return false;
if (this.seenRaws.has(log.raw)) return false;
this.seenRaws.add(log.raw);
return true;
});
if (logsToAdd.length > 0) {
// Add new logs to the beginning (most recent first)
this.logs = [...logsToAdd, ...this.logs].map((log, index) => ({
index,
...log,
}));
}
this.logs = (initialLogs || []).map((log, index) => ({
index,
...log,
}));
},
sort(field) {
if (this.sortField === field) {
@@ -409,14 +376,6 @@
},
}"
x-init="initialize(@js($firewallLogs))"
@logs-updated.window="updateLogs($event.detail.logs || $event.detail[0]?.logs || $event.detail[0] || [])"
x-effect="
// Watch for changes in the backend data and merge new logs
const backendLogs = @js($firewallLogs);
if (initialized && backendLogs && backendLogs.length > 0) {
updateLogs(backendLogs);
}
"
class="space-y-4"
>
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3">
@@ -426,7 +385,7 @@
type="text"
x-model="searchTerm"
placeholder="Search logs..."
class="firewall-log-search-input w-full pr-4 py-2 border border-neutral rounded-lg bg-background focus:outline-none focus:ring-2 focus:ring-primary/40"
class="w-full pl-10 pr-4 py-2 border border-neutral rounded-lg bg-background focus:outline-none focus:ring-2 focus:ring-primary/40"
/>
</div>
<div class="text-xs text-base/50">
@@ -490,543 +449,20 @@
</thead>
<tbody>
<template x-for="(log, index) in filteredLogs" :key="index">
<template x-if="true">
<template x-if="log.is_group">
<!-- Grouped log row -->
<template x-data="{ expanded: false }">
<tr class="border-b border-neutral/20 hover:bg-background-secondary/50 cursor-pointer" @click="expanded = !expanded">
<td class="py-3 px-4 text-sm">
<div class="flex items-center gap-2">
<svg class="size-3 text-base/50 transition-transform flex-shrink-0" :class="{ 'rotate-90': expanded }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
<span x-text="log.timestamp_display || '-'"></span>
</div>
</td>
<td class="py-3 px-4 text-sm">
<span class="px-2 py-1 rounded text-xs font-semibold uppercase" :class="log.level_badge_class" x-text="log.level_label || '-'"></span>
</td>
<td class="py-3 px-4 text-sm">
<span class="px-2 py-1 rounded text-xs font-semibold" :class="log.action_badge_class" x-text="log.action || '-'"></span>
</td>
<td class="py-3 px-4 text-sm">
<div class="flex items-center gap-1">
<span x-text="log.protocol || '-'"></span>
<template x-if="log.protocol && log.protocol !== '-'">
<div class="relative" x-data="{ open: false, protocolDescription: window.getProtocolDescription(log.protocol) }" x-cloak>
<button type="button" @click.stop="open = true" class="text-base/50 hover:text-base/70 transition-colors">
<x-ri-information-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.stop="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-information-line class="size-5 text-primary flex-shrink-0" />
<h3 class="text-lg font-semibold break-words">Protocol: <span class="font-mono break-all" x-text="log.protocol"></span></h3>
</div>
<div class="text-sm text-base/80 prose prose-sm max-w-none" x-html="protocolDescription"></div>
</div>
</div>
</div>
</div>
</template>
</div>
</td>
<td class="py-3 px-4 text-sm">
<div class="flex items-center gap-1">
<span class="font-mono" x-text="log.source || '-'"></span>
<template x-if="log.source && log.source !== '-' && window.isPublicIP(log.source)">
<div class="relative" x-data="{ open: false, loading: false, ipData: null, ip: log.source }" x-cloak>
<button type="button" @click.stop="
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.stop="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" x-text="log.source"></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 rounded-lg border border-neutral/20 overflow-hidden bg-background/50" style="position: relative; width: 100%; max-width: 100%; height: 256px; min-height: 256px;">
<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;"
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>
</template>
</div>
</td>
<td class="py-3 px-4 text-sm" x-text="log.source_port || '-'"></td>
<td class="py-3 px-4 text-sm">
<div class="flex items-center gap-1">
<span class="font-mono" x-text="log.destination || '-'"></span>
<template x-if="log.destination && log.destination !== '-' && window.isPublicIP(log.destination)">
<div class="relative" x-data="{ open: false, loading: false, ipData: null, ip: log.destination }" x-cloak>
<button type="button" @click.stop="
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.stop="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" x-text="log.destination"></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 rounded-lg border border-neutral/20 overflow-hidden bg-background/50" style="position: relative; width: 100%; max-width: 100%; height: 256px; min-height: 256px;">
<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;"
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>
</template>
</div>
</td>
<td class="py-3 px-4 text-sm">
<div class="flex items-center gap-2">
<span x-text="log.destination_port || '-'"></span>
<span class="px-2 py-1 bg-primary/10 text-primary rounded-full text-xs font-semibold" x-text="log.group_count"></span>
</div>
</td>
</tr>
<!-- Expanded details row -->
<tr x-show="expanded" x-collapse class="border-b border-neutral/20">
<td colspan="8" class="p-0">
<div class="bg-background/30 px-4 py-3">
<div class="text-xs text-base/60 mb-3 font-semibold">
<span x-text="log.group_count"></span> similar events
</div>
<div class="space-y-2 max-h-96 overflow-y-auto">
<template x-for="(subLog, subIndex) in log.grouped_logs" :key="subIndex">
<div class="bg-background-secondary border border-neutral/10 rounded-lg p-3">
<div class="flex items-center gap-3 mb-2">
<span class="text-xs text-base/70" x-text="subLog.timestamp_display"></span>
<span class="px-2 py-0.5 rounded text-xs font-semibold uppercase" :class="subLog.level_badge_class" x-text="subLog.level_label"></span>
<span class="px-2 py-0.5 rounded text-xs font-semibold" :class="subLog.action_badge_class" x-text="subLog.action"></span>
</div>
<div class="grid grid-cols-2 gap-x-4 gap-y-1 text-xs mb-2">
<div><span class="text-base/50">Protocol:</span> <span class="font-semibold" x-text="subLog.protocol || '-'"></span></div>
<div><span class="text-base/50">Interface:</span> <span class="font-semibold" x-text="subLog.iface || '-'"></span></div>
<div><span class="text-base/50">Source:</span> <span class="font-mono font-semibold" x-text="subLog.source || '-'"></span><span x-show="subLog.source_port" class="text-base/50">:<span class="font-semibold" x-text="subLog.source_port"></span></span></div>
<div><span class="text-base/50">Destination:</span> <span class="font-mono font-semibold" x-text="subLog.destination || '-'"></span><span x-show="subLog.destination_port" class="text-base/50">:<span class="font-semibold" x-text="subLog.destination_port"></span></span></div>
</div>
<details class="text-xs">
<summary class="cursor-pointer text-base/50 hover:text-base/70 transition-colors">View raw log</summary>
<div class="mt-2 p-2 bg-background/50 rounded border border-neutral/10 font-mono text-[10px] text-base/60 break-all" x-text="subLog.raw"></div>
</details>
</div>
</template>
</div>
</div>
</td>
</tr>
</template>
</template>
<template x-if="!log.is_group">
<!-- Single log row -->
<tr class="border-b border-neutral/20 hover:bg-background-secondary/50">
<td class="py-3 px-4 text-sm" x-text="log.timestamp_display || '-' "></td>
<td class="py-3 px-4 text-sm">
<span class="px-2 py-1 rounded text-xs font-semibold uppercase" :class="log.level_badge_class" x-text="log.level_label || '-' "></span>
</td>
<td class="py-3 px-4 text-sm">
<span class="px-2 py-1 rounded text-xs font-semibold" :class="log.action_badge_class" x-text="log.action || '-' "></span>
</td>
<tr class="border-b border-neutral/20 hover:bg-background-secondary/50">
<td class="py-3 px-4 text-sm" x-text="log.timestamp_display || '-' "></td>
<td class="py-3 px-4 text-sm">
<div class="flex items-center gap-1">
<span x-text="log.protocol || '-'"></span>
<template x-if="log.protocol && log.protocol !== '-'">
<div class="relative" x-data="{ open: false, protocolDescription: window.getProtocolDescription(log.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 -->
<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-information-line class="size-5 text-primary flex-shrink-0" />
<h3 class="text-lg font-semibold break-words">Protocol: <span class="font-mono break-all" x-text="log.protocol"></span></h3>
</div>
<div class="text-sm text-base/80 prose prose-sm max-w-none" x-html="protocolDescription"></div>
</div>
</div>
</div>
</div>
</template>
</div>
<span class="px-2 py-1 rounded text-xs font-semibold uppercase" :class="log.level_badge_class" x-text="log.level_label || '-' "></span>
</td>
<td class="py-3 px-4 text-sm">
<div class="flex items-center gap-1">
<span class="font-mono" x-text="log.source || '-'"></span>
<template x-if="log.source && log.source !== '-' && window.isPublicIP(log.source)">
<div class="relative" x-data="{ open: false, loading: false, ipData: null, ip: log.source }" 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" x-text="log.source"></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 rounded-lg border border-neutral/20 overflow-hidden bg-background/50" style="position: relative; width: 100%; max-width: 100%; height: 256px; min-height: 256px;">
<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;"
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>
</template>
</div>
<span class="px-2 py-1 rounded text-xs font-semibold" :class="log.action_badge_class" x-text="log.action || '-' "></span>
</td>
<td class="py-3 px-4 text-sm" x-text="log.protocol || '-' "></td>
<td class="py-3 px-4 text-sm font-mono" x-text="log.source || '-' "></td>
<td class="py-3 px-4 text-sm" x-text="log.source_port || '-' "></td>
<td class="py-3 px-4 text-sm">
<div class="flex items-center gap-1">
<span class="font-mono" x-text="log.destination || '-'"></span>
<template x-if="log.destination && log.destination !== '-' && window.isPublicIP(log.destination)">
<div class="relative" x-data="{ open: false, loading: false, ipData: null, ip: log.destination }" 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" x-text="log.destination"></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 rounded-lg border border-neutral/20 overflow-hidden bg-background/50" style="position: relative; width: 100%; max-width: 100%; height: 256px; min-height: 256px;">
<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;"
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>
</template>
</div>
</td>
<td class="py-3 px-4 text-sm" x-text="log.destination_port || '-' "></td>
</tr>
</template>
</template>
<td class="py-3 px-4 text-sm font-mono" x-text="log.destination || '-' "></td>
<td class="py-3 px-4 text-sm" x-text="log.destination_port || '-' "></td>
</tr>
</template>
</tbody>
</table>
@@ -1295,161 +731,4 @@
@endif
</div>
<script>
// Helper function to extract IP from string (removes port)
window.extractIP = function(input) {
if (!input) return '';
input = String(input).trim();
// Handle IPv6 with port: [2001:db8::1]:8080
const ipv6WithPortMatch = input.match(/^\[([^\]]+)\](?::\d+)?$/);
if (ipv6WithPortMatch) {
return ipv6WithPortMatch[1];
}
// Handle IPv4 with port: 192.168.1.1:8080
const ipv4WithPortMatch = input.match(/^([^:]+):\d+$/);
if (ipv4WithPortMatch && !ipv4WithPortMatch[1].includes(':')) {
return ipv4WithPortMatch[1];
}
return input;
};
// Check if IP is public
window.isPublicIP = function(ip) {
if (!ip || typeof ip !== 'string') {
return false;
}
ip = window.extractIP(ip);
// Check for localhost
if (['localhost', '127.0.0.1', '::1'].includes(ip.toLowerCase())) {
return false;
}
// Check if it's IPv4
if (/^(\d{1,3}\.){3}\d{1,3}$/.test(ip)) {
const parts = ip.split('.').map(Number);
// Check for valid IPv4 range
if (parts.some(part => part > 255)) {
return false;
}
// Check for private ranges
if (parts[0] === 10) return false;
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return false;
if (parts[0] === 192 && parts[1] === 168) return false;
if (parts[0] === 127) return false;
if (parts[0] === 169 && parts[1] === 254) return false;
return true;
}
// Check if it's IPv6
if (ip.includes(':')) {
// Link-local (fe80::/10)
if (/^fe[89ab][0-9a-fA-F]:/i.test(ip)) {
return false;
}
// Unique Local Address - ULA (fc00::/7)
if (/^f[cd][0-9a-fA-F]/i.test(ip)) {
return false;
}
// IPv4-mapped IPv6 (::ffff:x.x.x.x)
if (ip.startsWith('::ffff:')) {
const ipv4Part = ip.substring(7);
return window.isPublicIP(ipv4Part);
}
// If it's a valid-looking IPv6 and not in private ranges, consider it public
const ipv6Regex = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/;
return ipv6Regex.test(ip);
}
return false;
};
// Fetch IP geolocation data via server-side proxy
window.fetchIPLocation = async function(ip) {
if (!window.isPublicIP(ip)) {
return { status: 'fail', message: 'Not a public IP address' };
}
ip = window.extractIP(ip);
try {
const data = await @this.call('fetchIPLocation', ip);
return data || { status: 'fail', message: 'No data returned' };
} catch (error) {
console.error('Error fetching IP location:', error);
return { status: 'fail', message: 'Failed to fetch location data: ' + (error.message || 'Unknown error') };
}
};
// Helper function to format descriptions with sections
const formatStateDescription = (overview, meaning, scenarios, notes = null) => {
let html = '<div class="space-y-4">';
if (overview) {
html += `<div><h4 class="font-semibold text-base mb-2">Overview</h4><p>${overview}</p></div>`;
}
if (meaning) {
html += `<div><h4 class="font-semibold text-base mb-2">What This Means</h4><p>${meaning}</p></div>`;
}
if (scenarios) {
html += `<div><h4 class="font-semibold text-base mb-2">Common Scenarios</h4>${scenarios}</div>`;
}
if (notes) {
html += `<div><h4 class="font-semibold text-base mb-2">Additional Notes</h4><p>${notes}</p></div>`;
}
html += '</div>';
return html;
};
// Protocol descriptions mapping
window.getProtocolDescription = function(protocol) {
if (!protocol || typeof protocol !== 'string') {
return formatStateDescription('Unknown protocol', '', '');
}
const protocolUpper = protocol.toUpperCase();
const protocolDescriptions = {
'TCP': formatStateDescription(
'TCP (Transmission Control Protocol) is a connection-oriented protocol that provides reliable, ordered, and error-checked delivery of data between applications.',
'TCP establishes a connection before data transfer and ensures data integrity through acknowledgments and retransmissions. It guarantees that data is delivered in order and without errors.',
'<p>Common uses include:</p><ul class="list-disc list-inside space-y-1 ml-2 mt-2"><li>Web browsing (HTTP/HTTPS)</li><li>Email (SMTP, IMAP, POP3)</li><li>File transfers (FTP)</li><li>Remote access (SSH, RDP)</li><li>Database connections</li><li>API communications</li></ul>',
'TCP is the most commonly used transport protocol on the Internet and provides reliable communication at the cost of slightly higher overhead compared to UDP.'
),
'UDP': formatStateDescription(
'UDP (User Datagram Protocol) is a connectionless protocol that provides fast, low-overhead data transmission without guarantees of delivery, ordering, or duplicate protection.',
'UDP does not establish connections before sending data and does not provide reliability features like acknowledgments or retransmissions. It prioritizes speed over reliability.',
'<p>Common uses include:</p><ul class="list-disc list-inside space-y-1 ml-2 mt-2"><li>DNS queries</li><li>Video streaming</li><li>Online gaming</li><li>VoIP (Voice over IP)</li><li>Real-time applications</li><li>DHCP (Dynamic Host Configuration Protocol)</li></ul>',
'UDP is ideal for applications where speed is more important than reliability, or where occasional packet loss is acceptable.'
),
'ICMP': formatStateDescription(
'ICMP (Internet Control Message Protocol) is a network layer protocol used for diagnostic and control purposes.',
'ICMP is used by network devices to send error messages and operational information. It is essential for network troubleshooting and does not carry application data.',
'<p>Common uses include:</p><ul class="list-disc list-inside space-y-1 ml-2 mt-2"><li>Ping (echo request/reply) for connectivity testing</li><li>Traceroute for path discovery</li><li>Error reporting (destination unreachable, time exceeded)</li><li>Network diagnostics</li></ul>',
'ICMP is a stateless protocol and does not use connection states like TCP or UDP. It operates at the network layer alongside IP.'
),
'ICMPV6': formatStateDescription(
'ICMPv6 (Internet Control Message Protocol version 6) is the IPv6 equivalent of ICMP, used for error reporting and diagnostic functions in IPv6 networks.',
'ICMPv6 includes additional features beyond what ICMP provides for IPv4, including integrated support for address configuration and network discovery.',
'<p>Key features include:</p><ul class="list-disc list-inside space-y-1 ml-2 mt-2"><li>Neighbor Discovery Protocol (NDP) for IPv6</li><li>Path MTU Discovery for IPv6</li><li>Router discovery and configuration</li><li>Error reporting and diagnostics</li></ul>',
'ICMPv6 is essential for IPv6 network operation and provides functions that in IPv4 are handled by multiple protocols (ARP, ICMP, etc.).'
),
};
return protocolDescriptions[protocolUpper] || formatStateDescription(
`${protocol} is a network protocol used for communication between devices.`,
'This protocol handles specific types of network traffic and follows particular rules for data transmission.',
''
);
};
</script>
</x-proxmox::layout>