Files
Virtnet.bond/extensions/Servers/Proxmox/views/firewall.blade.php

2110 lines
140 KiB
PHP

<x-proxmox::layout :server="$server" view="firewall" :status="$status" :status-polling="true" :status-polling-interval="'2s'">
<style>
.firewall-toggle-switch {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
}
.firewall-toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.firewall-toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #e5e7eb;
transition: 0.3s;
border-radius: 24px;
}
.dark .firewall-toggle-slider {
background-color: #374151;
}
.firewall-toggle-slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
.firewall-toggle-switch input:checked + .firewall-toggle-slider {
background-color: #2563eb;
}
.firewall-toggle-switch input:checked + .firewall-toggle-slider:before {
transform: translateX(20px);
}
.firewall-toggle-switch input:disabled + .firewall-toggle-slider {
opacity: 0.5;
cursor: not-allowed;
}
.delete-rule-button {
background-color: #ef4444;
color: white;
padding: 0.625rem 1.25rem;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 10px 15px -3px rgba(239, 68, 68, 0.2);
display: flex;
align-items: center;
gap: 0.5rem;
white-space: nowrap;
}
.delete-rule-button:hover {
background-color: #dc2626;
}
.delete-rule-button:active {
transform: scale(0.95);
}
.delete-rule-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.firewall-log-search-input {
padding-left: 2.75rem !important;
}
/* Firewall Log Filter Buttons */
.firewall-filter-label {
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: rgba(var(--color-base), 0.5);
margin-right: 0.25rem;
}
.firewall-filter-btn {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
font-size: 0.75rem;
font-weight: 500;
border-radius: 0.5rem;
border: 1px solid rgba(var(--color-neutral), 1);
background-color: rgba(var(--color-background), 1);
color: rgba(var(--color-base), 0.7);
cursor: pointer;
transition: all 0.15s ease;
}
.firewall-filter-btn:hover {
border-color: rgba(var(--color-primary), 0.5);
color: rgba(var(--color-base), 1);
}
.firewall-filter-btn-count {
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-size: 0.625rem;
font-weight: 700;
background-color: rgba(var(--color-base), 0.1);
}
/* Active states for Policy filters */
.firewall-filter-btn.active-all {
background-color: #1e3a5f;
border-color: #1e3a5f;
color: #ffffff;
box-shadow: 0 2px 8px rgba(30, 58, 95, 0.4);
}
.firewall-filter-btn.active-all .firewall-filter-btn-count {
background-color: rgba(255, 255, 255, 0.25);
color: #ffffff;
}
/* DROP - Red */
.firewall-filter-btn.active-drop {
background-color: #dc2626;
border-color: #dc2626;
color: white;
box-shadow: 0 2px 8px rgba(220, 38, 38, 0.3);
}
.firewall-filter-btn.active-drop .firewall-filter-btn-count {
background-color: rgba(255, 255, 255, 0.25);
}
.firewall-filter-btn:not(.active-drop) .firewall-filter-btn-count.count-drop {
background-color: rgba(220, 38, 38, 0.1);
color: #dc2626;
}
/* REJECT - Orange-Red */
.firewall-filter-btn.active-reject {
background-color: #ea580c;
border-color: #ea580c;
color: white;
box-shadow: 0 2px 8px rgba(234, 88, 12, 0.3);
}
.firewall-filter-btn.active-reject .firewall-filter-btn-count {
background-color: rgba(255, 255, 255, 0.25);
}
.firewall-filter-btn:not(.active-reject) .firewall-filter-btn-count.count-reject {
background-color: rgba(234, 88, 12, 0.1);
color: #ea580c;
}
/* ACCEPT - Green */
.firewall-filter-btn.active-accept {
background-color: #16a34a;
border-color: #16a34a;
color: white;
box-shadow: 0 2px 8px rgba(22, 163, 74, 0.3);
}
.firewall-filter-btn.active-accept .firewall-filter-btn-count {
background-color: rgba(255, 255, 255, 0.25);
}
.firewall-filter-btn:not(.active-accept) .firewall-filter-btn-count.count-accept {
background-color: rgba(22, 163, 74, 0.1);
color: #16a34a;
}
/* LOG - Cyan */
.firewall-filter-btn.active-log {
background-color: #0891b2;
border-color: #0891b2;
color: white;
box-shadow: 0 2px 8px rgba(8, 145, 178, 0.3);
}
.firewall-filter-btn.active-log .firewall-filter-btn-count {
background-color: rgba(255, 255, 255, 0.25);
}
.firewall-filter-btn:not(.active-log) .firewall-filter-btn-count.count-log {
background-color: rgba(8, 145, 178, 0.1);
color: #0891b2;
}
/* Action Other - Gray */
.firewall-filter-btn.active-action-other {
background-color: #525252;
border-color: #525252;
color: white;
box-shadow: 0 2px 8px rgba(82, 82, 82, 0.3);
}
.firewall-filter-btn.active-action-other .firewall-filter-btn-count {
background-color: rgba(255, 255, 255, 0.25);
}
/* Active states for Protocol filters */
.firewall-filter-btn.active-proto-all {
background-color: #374151;
border-color: #374151;
color: #ffffff;
box-shadow: 0 2px 8px rgba(55, 65, 81, 0.4);
}
.firewall-filter-btn.active-proto-all .firewall-filter-btn-count {
background-color: rgba(255, 255, 255, 0.25);
color: #ffffff;
}
.firewall-filter-btn.active-tcp {
background-color: #3b82f6;
border-color: #3b82f6;
color: white;
}
.firewall-filter-btn.active-tcp .firewall-filter-btn-count {
background-color: rgba(255, 255, 255, 0.2);
}
.firewall-filter-btn:not(.active-tcp) .firewall-filter-btn-count.count-tcp {
background-color: rgba(59, 130, 246, 0.1);
color: #3b82f6;
}
.firewall-filter-btn.active-udp {
background-color: #f59e0b;
border-color: #f59e0b;
color: white;
}
.firewall-filter-btn.active-udp .firewall-filter-btn-count {
background-color: rgba(255, 255, 255, 0.2);
}
.firewall-filter-btn:not(.active-udp) .firewall-filter-btn-count.count-udp {
background-color: rgba(245, 158, 11, 0.1);
color: #f59e0b;
}
.firewall-filter-btn.active-icmp {
background-color: #a855f7;
border-color: #a855f7;
color: white;
}
.firewall-filter-btn.active-icmp .firewall-filter-btn-count {
background-color: rgba(255, 255, 255, 0.2);
}
.firewall-filter-btn:not(.active-icmp) .firewall-filter-btn-count.count-icmp {
background-color: rgba(168, 85, 247, 0.1);
color: #a855f7;
}
/* GRE - Teal */
.firewall-filter-btn.active-gre {
background-color: #0d9488;
border-color: #0d9488;
color: white;
box-shadow: 0 2px 8px rgba(13, 148, 136, 0.3);
}
.firewall-filter-btn.active-gre .firewall-filter-btn-count {
background-color: rgba(255, 255, 255, 0.25);
}
.firewall-filter-btn:not(.active-gre) .firewall-filter-btn-count.count-gre {
background-color: rgba(13, 148, 136, 0.1);
color: #0d9488;
}
/* ESP - Indigo */
.firewall-filter-btn.active-esp {
background-color: #4f46e5;
border-color: #4f46e5;
color: white;
box-shadow: 0 2px 8px rgba(79, 70, 229, 0.3);
}
.firewall-filter-btn.active-esp .firewall-filter-btn-count {
background-color: rgba(255, 255, 255, 0.25);
}
.firewall-filter-btn:not(.active-esp) .firewall-filter-btn-count.count-esp {
background-color: rgba(79, 70, 229, 0.1);
color: #4f46e5;
}
/* AH - Violet */
.firewall-filter-btn.active-ah {
background-color: #7c3aed;
border-color: #7c3aed;
color: white;
box-shadow: 0 2px 8px rgba(124, 58, 237, 0.3);
}
.firewall-filter-btn.active-ah .firewall-filter-btn-count {
background-color: rgba(255, 255, 255, 0.25);
}
.firewall-filter-btn:not(.active-ah) .firewall-filter-btn-count.count-ah {
background-color: rgba(124, 58, 237, 0.1);
color: #7c3aed;
}
/* SCTP - Pink */
.firewall-filter-btn.active-sctp {
background-color: #db2777;
border-color: #db2777;
color: white;
box-shadow: 0 2px 8px rgba(219, 39, 119, 0.3);
}
.firewall-filter-btn.active-sctp .firewall-filter-btn-count {
background-color: rgba(255, 255, 255, 0.25);
}
.firewall-filter-btn:not(.active-sctp) .firewall-filter-btn-count.count-sctp {
background-color: rgba(219, 39, 119, 0.1);
color: #db2777;
}
/* IGMP - Lime */
.firewall-filter-btn.active-igmp {
background-color: #65a30d;
border-color: #65a30d;
color: white;
box-shadow: 0 2px 8px rgba(101, 163, 13, 0.3);
}
.firewall-filter-btn.active-igmp .firewall-filter-btn-count {
background-color: rgba(255, 255, 255, 0.25);
}
.firewall-filter-btn:not(.active-igmp) .firewall-filter-btn-count.count-igmp {
background-color: rgba(101, 163, 13, 0.1);
color: #65a30d;
}
/* Protocol Other - Dark Gray */
.firewall-filter-btn.active-proto-other {
background-color: #525252;
border-color: #525252;
color: white;
box-shadow: 0 2px 8px rgba(82, 82, 82, 0.3);
}
.firewall-filter-btn.active-proto-other .firewall-filter-btn-count {
background-color: rgba(255, 255, 255, 0.25);
}
.firewall-filter-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
.firewall-filter-divider {
padding-top: 0.5rem;
border-top: 1px solid rgba(var(--color-neutral), 0.3);
}
/* State Details Button */
.firewall-state-btn {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.5rem;
font-size: 0.7rem;
font-weight: 500;
border-radius: 0.375rem;
background-color: rgba(var(--color-info), 0.1);
color: rgba(var(--color-info), 1);
border: 1px solid rgba(var(--color-info), 0.3);
cursor: pointer;
transition: all 0.15s ease;
white-space: nowrap;
}
.firewall-state-btn:hover {
background-color: rgba(var(--color-info), 0.2);
border-color: rgba(var(--color-info), 0.5);
color: rgba(var(--color-info), 1);
}
</style>
<div class="space-y-6">
<!-- Error Message - Centered Modal -->
@if($error && $showErrorModal)
<div class="fixed inset-0 flex items-center justify-center z-50 bg-black/50 backdrop-blur-sm"
wire:click="$set('error', ''); $set('showErrorModal', false)">
<div class="bg-error/20 border-2 border-error rounded-lg p-6 shadow-lg max-w-3xl w-full mx-4 max-h-[80vh] overflow-y-auto" wire:click.stop>
<div class="flex items-start gap-3">
<x-ri-error-warning-fill class="size-6 text-error flex-shrink-0 mt-0.5" />
<div class="flex-1">
<h3 class="text-lg font-semibold text-error mb-2 text-center">Firewall Error</h3>
<div class="bg-background/50 rounded p-3 border border-error/30">
<p class="text-sm text-error/90 leading-relaxed whitespace-pre-wrap break-words">{{ $error }}</p>
</div>
</div>
<button
wire:click="$set('error', ''); $set('showErrorModal', false)"
class="text-error/70 hover:text-error transition-colors flex-shrink-0">
<x-ri-close-line class="size-5" />
</button>
</div>
</div>
</div>
@endif
<!-- Firewall Status Card -->
<div class="bg-background border border-neutral rounded-lg p-6">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-3">
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
<x-ri-shield-line class="size-5 text-primary" />
</div>
<div>
<h2 class="text-xl font-semibold">Firewall Status</h2>
<p class="text-sm text-base/60 mt-1">Enable or disable the firewall for this virtual machine</p>
</div>
</div>
<div class="flex items-center gap-3">
<span class="text-sm font-medium text-base/70">Disabled</span>
<label class="firewall-toggle-switch" wire:loading.attr="disabled" wire:target="toggleFirewall">
<input
type="checkbox"
@if($firewallEnabled) checked @endif
wire:click="toggleFirewall"
wire:loading.attr="disabled">
<span class="firewall-toggle-slider"></span>
</label>
<span class="text-sm font-medium text-base/70">Enabled</span>
</div>
</div>
</div>
<!-- Network Adapters Card -->
<div class="bg-background border border-neutral rounded-lg p-6">
<div class="flex items-center gap-3 mb-4">
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
<x-ri-wifi-line class="size-5 text-primary" />
</div>
<div>
<h2 class="text-xl font-semibold">Network Adapters</h2>
<p class="text-sm text-base/60 mt-1">Enable or disable firewall for each network adapter</p>
</div>
</div>
@if(count($networkAdapters) > 0)
<div class="space-y-4">
@foreach($networkAdapters as $adapter)
<div class="bg-background-secondary border border-neutral/20 rounded-lg p-4">
<div class="flex items-center justify-between">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<h3 class="text-base font-semibold">{{ $adapter['name'] }}</h3>
<span class="px-2 py-1 rounded text-xs font-medium bg-primary/20 text-primary">
{{ ucfirst($adapter['model'] ?? 'Unknown') }}
</span>
</div>
<div class="text-sm text-base/60 space-y-1">
@if($adapter['mac'])
<div>MAC: <code class="text-xs">{{ $adapter['mac'] }}</code></div>
@endif
</div>
<p class="text-xs text-base/50 mt-2">Enables or disables the firewall ability for this network adapter</p>
</div>
<div class="flex flex-col items-center gap-2 ml-4">
<span class="text-xs font-medium text-base/60">Network Adapter Firewall</span>
<div class="flex items-center gap-3">
<span class="text-sm font-medium text-base/70">Disabled</span>
<label class="firewall-toggle-switch" wire:loading.attr="disabled">
<input
type="checkbox"
@if($adapterFirewallStates[$adapter['name']] ?? false) checked @endif
wire:click="toggleAdapterFirewall('{{ $adapter['name'] }}')"
wire:loading.attr="disabled">
<span class="firewall-toggle-slider"></span>
</label>
<span class="text-sm font-medium text-base/70">Enabled</span>
</div>
</div>
</div>
</div>
@endforeach
</div>
@else
<div class="text-center py-8 text-base/60">
<x-ri-wifi-line class="size-12 mx-auto mb-3 opacity-50" />
<p>No network adapters found</p>
</div>
@endif
</div>
@if($firewallEnabled)
<!-- Firewall Rules Card -->
<div class="bg-background border border-neutral rounded-lg p-6">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-3">
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
<x-ri-list-check class="size-5 text-primary" />
</div>
<div>
<h2 class="text-xl font-semibold">Firewall Rules</h2>
<p class="text-sm text-base/60 mt-1">Manage allow and block rules for network traffic</p>
</div>
</div>
<button
wire:click="startCreateRule"
class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium">
<x-ri-add-line class="size-4 inline mr-1" />
Add Rule
</button>
</div>
<!-- Rules Table -->
@if(count($rules) > 0)
<div class="overflow-x-auto">
<table class="w-full border-collapse">
<thead>
<tr class="border-b border-neutral">
<th class="text-left py-3 px-4 font-semibold text-sm">Position</th>
<th class="text-left py-3 px-4 font-semibold text-sm">Action</th>
<th class="text-left py-3 px-4 font-semibold text-sm">Source</th>
<th class="text-left py-3 px-4 font-semibold text-sm">Destination</th>
<th class="text-left py-3 px-4 font-semibold text-sm">Protocol</th>
<th class="text-left py-3 px-4 font-semibold text-sm">Port</th>
<th class="text-left py-3 px-4 font-semibold text-sm">Status</th>
<th class="text-left py-3 px-4 font-semibold text-sm">Actions</th>
</tr>
</thead>
<tbody>
@foreach($rules as $rule)
<tr class="border-b border-neutral/20 hover:bg-background-secondary/50">
<td class="py-3 px-4 text-sm">{{ $rule['pos'] ?? '-' }}</td>
<td class="py-3 px-4 text-sm">
<span class="px-2 py-1 rounded text-xs font-medium
{{ ($rule['action'] ?? '') === 'ACCEPT' ? 'bg-success/20 text-success' : '' }}
{{ ($rule['action'] ?? '') === 'REJECT' ? 'bg-warning/20 text-warning' : '' }}
{{ ($rule['action'] ?? '') === 'DROP' ? 'bg-error/20 text-error' : '' }}">
{{ $rule['action'] ?? '-' }}
</span>
</td>
<td class="py-3 px-4 text-sm">{{ $rule['source'] ?? '-' }}</td>
<td class="py-3 px-4 text-sm">{{ $rule['dest'] ?? '-' }}</td>
<td class="py-3 px-4 text-sm">{{ strtoupper($rule['proto'] ?? 'any') }}</td>
<td class="py-3 px-4 text-sm">{{ $rule['dport'] ?? ($rule['sport'] ?? '-') }}</td>
<td class="py-3 px-4 text-sm">
@if(isset($rule['enable']) && $rule['enable'] == 0)
<span class="px-2 py-1 rounded text-xs font-medium bg-base/20 text-base/60">Disabled</span>
@else
<span class="px-2 py-1 rounded text-xs font-medium bg-success/20 text-success">Enabled</span>
@endif
</td>
<td class="py-3 px-4 text-sm">
<div class="flex gap-2">
<button
wire:click="toggleRule({{ $rule['pos'] ?? 0 }})"
wire:loading.attr="disabled"
class="transition-colors
{{ (isset($rule['enable']) && $rule['enable'] == 0) ? 'text-warning hover:text-warning/80' : 'text-success hover:text-success/80' }}"
title="{{ (isset($rule['enable']) && $rule['enable'] == 0) ? 'Enable rule' : 'Disable rule' }}">
@if(isset($rule['enable']) && $rule['enable'] == 0)
<x-ri-toggle-line class="size-4" />
@else
<x-ri-toggle-fill class="size-4" />
@endif
</button>
<button
wire:click="startEditRule({{ $rule['pos'] ?? 0 }})"
class="text-primary hover:text-primary/80 transition-colors"
title="Edit rule">
<x-ri-edit-line class="size-4" />
</button>
<button
wire:click="confirmDeleteRule({{ $rule['pos'] ?? 0 }})"
class="text-error hover:text-error/80 transition-colors"
title="Delete rule">
<x-ri-delete-bin-line class="size-4" />
</button>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
<div class="text-center py-8 text-base/60">
<x-ri-list-check class="size-12 mx-auto mb-3 opacity-50" />
<p>No firewall rules configured</p>
</div>
@endif
</div>
<!-- Firewall Logs Card -->
<div class="bg-background border border-neutral rounded-lg p-6">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-3">
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
<x-ri-file-list-3-line class="size-5 text-primary" />
</div>
<div>
<h2 class="text-xl font-semibold">Firewall Logs</h2>
<p class="text-sm text-base/60 mt-1">Recent firewall activity and blocked/allowed connections ({{ count($firewallLogs) }} entries loaded)</p>
</div>
</div>
<button
wire:click="loadFirewallLogs"
wire:loading.attr="disabled"
class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium">
<x-ri-refresh-line class="size-4 inline mr-1" />
Refresh
</button>
</div>
@if(!empty($logError))
<div class="bg-warning/10 border border-warning/30 rounded-lg p-4 mb-4">
<div class="flex items-start gap-3">
<x-ri-error-warning-line class="size-5 text-warning flex-shrink-0 mt-0.5" />
<div class="flex-1">
<p class="text-sm text-warning/90">{{ $logError }}</p>
</div>
</div>
</div>
@endif
@if(count($firewallLogs) > 0)
<div
x-data="{
logs: [],
searchTerm: '',
sortField: 'timestamp_iso',
sortDirection: 'desc',
currentPage: 1,
perPage: 10,
actionFilters: [],
protocolFilters: [],
initialize(initialLogs) {
console.log('Initializing logs, received:', initialLogs ? initialLogs.length : 0, 'entries');
this.logs = (initialLogs || []).map((log, index) => ({
index,
...log,
}));
console.log('Logs after initialization:', this.logs.length);
},
sort(field) {
if (this.sortField === field) {
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
} else {
this.sortField = field;
this.sortDirection = 'asc';
}
this.currentPage = 1;
},
toggleActionFilter(filter) {
const idx = this.actionFilters.indexOf(filter);
if (idx === -1) {
this.actionFilters.push(filter);
} else {
this.actionFilters.splice(idx, 1);
}
this.currentPage = 1;
},
toggleProtocolFilter(filter) {
const idx = this.protocolFilters.indexOf(filter);
if (idx === -1) {
this.protocolFilters.push(filter);
} else {
this.protocolFilters.splice(idx, 1);
}
this.currentPage = 1;
},
clearActionFilters() {
this.actionFilters = [];
this.currentPage = 1;
},
clearProtocolFilters() {
this.protocolFilters = [];
this.currentPage = 1;
},
isActionActive(filter) {
return this.actionFilters.includes(filter);
},
isProtocolActive(filter) {
return this.protocolFilters.includes(filter);
},
get actionCounts() {
const counts = { all: this.logs.length, drop: 0, reject: 0, accept: 0, log: 0, other: 0 };
this.logs.forEach(log => {
const action = (log.action || '').toUpperCase();
if (action === 'DROP' || action.includes('DROP')) {
counts.drop++;
} else if (action === 'REJECT' || action.includes('REJECT')) {
counts.reject++;
} else if (action === 'ACCEPT' || action.includes('ACCEPT')) {
counts.accept++;
} else if (action === 'LOG' || action.includes('LOG')) {
counts.log++;
} else if (action) {
counts.other++;
}
});
return counts;
},
get protocolCounts() {
const counts = { all: this.logs.length, tcp: 0, udp: 0, icmp: 0, gre: 0, esp: 0, ah: 0, sctp: 0, igmp: 0, other: 0 };
this.logs.forEach(log => {
const proto = (log.protocol || '').toUpperCase();
if (proto === 'TCP') counts.tcp++;
else if (proto === 'UDP') counts.udp++;
else if (proto === 'ICMP' || proto === 'ICMPV6') counts.icmp++;
else if (proto === 'GRE') counts.gre++;
else if (proto === 'ESP') counts.esp++;
else if (proto === 'AH') counts.ah++;
else if (proto === 'SCTP') counts.sctp++;
else if (proto === 'IGMP') counts.igmp++;
else if (proto) counts.other++;
});
return counts;
},
get filteredLogs() {
const term = this.searchTerm.trim().toLowerCase();
let entries = this.logs;
// Apply action filters (multi-select)
if (this.actionFilters.length > 0) {
entries = entries.filter(log => {
const action = (log.action || '').toUpperCase();
for (const filter of this.actionFilters) {
if (filter === 'drop' && (action === 'DROP' || action.includes('DROP'))) return true;
if (filter === 'reject' && (action === 'REJECT' || action.includes('REJECT'))) return true;
if (filter === 'accept' && (action === 'ACCEPT' || action.includes('ACCEPT'))) return true;
if (filter === 'log' && (action === 'LOG' || action.includes('LOG'))) return true;
if (filter === 'other' && action && !['DROP', 'REJECT', 'ACCEPT', 'LOG'].some(a => action.includes(a))) return true;
}
return false;
});
}
// Apply protocol filters (multi-select)
if (this.protocolFilters.length > 0) {
entries = entries.filter(log => {
const proto = (log.protocol || '').toUpperCase();
for (const filter of this.protocolFilters) {
if (filter === 'tcp' && proto === 'TCP') return true;
if (filter === 'udp' && proto === 'UDP') return true;
if (filter === 'icmp' && (proto === 'ICMP' || proto === 'ICMPV6')) return true;
if (filter === 'gre' && proto === 'GRE') return true;
if (filter === 'esp' && proto === 'ESP') return true;
if (filter === 'ah' && proto === 'AH') return true;
if (filter === 'sctp' && proto === 'SCTP') return true;
if (filter === 'igmp' && proto === 'IGMP') return true;
if (filter === 'other' && proto && !['TCP', 'UDP', 'ICMP', 'ICMPV6', 'GRE', 'ESP', 'AH', 'SCTP', 'IGMP'].includes(proto)) return true;
}
return false;
});
}
// Apply search filter
if (term) {
entries = entries.filter((log) => {
return Object.values(log).some((value) => {
if (value === null || value === undefined) {
return false;
}
return String(value).toLowerCase().includes(term);
});
});
}
const dir = this.sortDirection === 'asc' ? 1 : -1;
const field = this.sortField;
return entries.slice().sort((a, b) => {
let aVal = a[field] ?? '';
let bVal = b[field] ?? '';
if (field === 'timestamp_iso') {
const aTime = aVal ? new Date(aVal).getTime() : 0;
const bTime = bVal ? new Date(bVal).getTime() : 0;
return (aTime - bTime) * dir;
}
if (!Number.isNaN(parseFloat(aVal)) && !Number.isNaN(parseFloat(bVal))) {
return (parseFloat(aVal) - parseFloat(bVal)) * dir;
}
return String(aVal).localeCompare(String(bVal)) * dir;
});
},
get paginatedLogs() {
const filtered = this.filteredLogs;
const start = (this.currentPage - 1) * this.perPage;
const end = start + this.perPage;
return filtered.slice(start, end);
},
get totalPages() {
return Math.ceil(this.filteredLogs.length / this.perPage);
},
get paginationRange() {
const total = this.totalPages;
const current = this.currentPage;
const delta = 2; // Show 2 pages on each side of current
if (total <= 0) return [];
if (total <= 7) {
// If 7 or fewer pages, show all pages
return Array.from({ length: total }, (_, i) => i + 1);
}
const range = [];
const left = Math.max(2, current - delta);
const right = Math.min(total - 1, current + delta);
// Always include first page
range.push(1);
// Add left ellipsis if there's a gap
if (left > 2) {
range.push('...');
}
// Add middle pages (avoiding duplicates with first/last)
for (let i = left; i <= right; i++) {
if (i !== 1 && i !== total) {
range.push(i);
}
}
// Add right ellipsis if there's a gap
if (right < total - 1) {
range.push('...');
}
// Always include last page (if different from first)
if (total > 1) {
range.push(total);
}
return range;
},
goToPage(page) {
if (page >= 1 && page <= this.totalPages) {
this.currentPage = page;
}
},
nextPage() {
if (this.currentPage < this.totalPages) {
this.currentPage++;
}
},
prevPage() {
if (this.currentPage > 1) {
this.currentPage--;
}
}
}"
x-init="initialize(@js($firewallLogs))"
x-effect="if (currentPage > totalPages && totalPages > 0) currentPage = totalPages"
class="space-y-4"
>
<!-- Filter Tabs -->
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 1rem;">
<!-- Action/Policy Filter (Multi-select) -->
<div class="firewall-filter-row">
<span class="firewall-filter-label">Policy:</span>
<button
type="button"
@click="clearActionFilters()"
:class="actionFilters.length === 0 ? 'active-all' : ''"
class="firewall-filter-btn">
<x-ri-list-check style="width: 14px; height: 14px;" />
All
<span class="firewall-filter-btn-count" x-text="actionCounts.all"></span>
</button>
<button
type="button"
@click="toggleActionFilter('drop')"
x-show="actionCounts.drop > 0"
:class="isActionActive('drop') ? 'active-drop' : ''"
class="firewall-filter-btn">
<x-ri-close-circle-line style="width: 14px; height: 14px;" />
DROP
<span class="firewall-filter-btn-count count-drop" x-text="actionCounts.drop"></span>
</button>
<button
type="button"
@click="toggleActionFilter('reject')"
x-show="actionCounts.reject > 0"
:class="isActionActive('reject') ? 'active-reject' : ''"
class="firewall-filter-btn">
<x-ri-forbid-line style="width: 14px; height: 14px;" />
REJECT
<span class="firewall-filter-btn-count count-reject" x-text="actionCounts.reject"></span>
</button>
<button
type="button"
@click="toggleActionFilter('accept')"
x-show="actionCounts.accept > 0"
:class="isActionActive('accept') ? 'active-accept' : ''"
class="firewall-filter-btn">
<x-ri-checkbox-circle-line style="width: 14px; height: 14px;" />
ACCEPT
<span class="firewall-filter-btn-count count-accept" x-text="actionCounts.accept"></span>
</button>
<button
type="button"
@click="toggleActionFilter('log')"
x-show="actionCounts.log > 0"
:class="isActionActive('log') ? 'active-log' : ''"
class="firewall-filter-btn">
<x-ri-file-list-line style="width: 14px; height: 14px;" />
LOG
<span class="firewall-filter-btn-count count-log" x-text="actionCounts.log"></span>
</button>
<button
type="button"
@click="toggleActionFilter('other')"
x-show="actionCounts.other > 0"
:class="isActionActive('other') ? 'active-action-other' : ''"
class="firewall-filter-btn">
Other
<span class="firewall-filter-btn-count" x-text="actionCounts.other"></span>
</button>
</div>
<!-- Protocol Filter (Multi-select) -->
<div class="firewall-filter-row">
<span class="firewall-filter-label">Protocol:</span>
<button
type="button"
@click="clearProtocolFilters()"
:class="protocolFilters.length === 0 ? 'active-proto-all' : ''"
class="firewall-filter-btn">
All
<span class="firewall-filter-btn-count" x-text="protocolCounts.all"></span>
</button>
<button
type="button"
@click="toggleProtocolFilter('tcp')"
x-show="protocolCounts.tcp > 0"
:class="isProtocolActive('tcp') ? 'active-tcp' : ''"
class="firewall-filter-btn">
TCP
<span class="firewall-filter-btn-count count-tcp" x-text="protocolCounts.tcp"></span>
</button>
<button
type="button"
@click="toggleProtocolFilter('udp')"
x-show="protocolCounts.udp > 0"
:class="isProtocolActive('udp') ? 'active-udp' : ''"
class="firewall-filter-btn">
UDP
<span class="firewall-filter-btn-count count-udp" x-text="protocolCounts.udp"></span>
</button>
<button
type="button"
@click="toggleProtocolFilter('icmp')"
x-show="protocolCounts.icmp > 0"
:class="isProtocolActive('icmp') ? 'active-icmp' : ''"
class="firewall-filter-btn">
ICMP
<span class="firewall-filter-btn-count count-icmp" x-text="protocolCounts.icmp"></span>
</button>
<button
type="button"
@click="toggleProtocolFilter('gre')"
x-show="protocolCounts.gre > 0"
:class="isProtocolActive('gre') ? 'active-gre' : ''"
class="firewall-filter-btn">
GRE
<span class="firewall-filter-btn-count count-gre" x-text="protocolCounts.gre"></span>
</button>
<button
type="button"
@click="toggleProtocolFilter('esp')"
x-show="protocolCounts.esp > 0"
:class="isProtocolActive('esp') ? 'active-esp' : ''"
class="firewall-filter-btn">
ESP
<span class="firewall-filter-btn-count count-esp" x-text="protocolCounts.esp"></span>
</button>
<button
type="button"
@click="toggleProtocolFilter('ah')"
x-show="protocolCounts.ah > 0"
:class="isProtocolActive('ah') ? 'active-ah' : ''"
class="firewall-filter-btn">
AH
<span class="firewall-filter-btn-count count-ah" x-text="protocolCounts.ah"></span>
</button>
<button
type="button"
@click="toggleProtocolFilter('sctp')"
x-show="protocolCounts.sctp > 0"
:class="isProtocolActive('sctp') ? 'active-sctp' : ''"
class="firewall-filter-btn">
SCTP
<span class="firewall-filter-btn-count count-sctp" x-text="protocolCounts.sctp"></span>
</button>
<button
type="button"
@click="toggleProtocolFilter('igmp')"
x-show="protocolCounts.igmp > 0"
:class="isProtocolActive('igmp') ? 'active-igmp' : ''"
class="firewall-filter-btn">
IGMP
<span class="firewall-filter-btn-count count-igmp" x-text="protocolCounts.igmp"></span>
</button>
<button
type="button"
@click="toggleProtocolFilter('other')"
x-show="protocolCounts.other > 0"
:class="isProtocolActive('other') ? 'active-proto-other' : ''"
class="firewall-filter-btn">
Other
<span class="firewall-filter-btn-count" x-text="protocolCounts.other"></span>
</button>
</div>
<!-- Search and Stats Row -->
<div class="firewall-filter-divider" style="display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.75rem;">
<div style="position: relative; width: 100%; max-width: 18rem;">
<x-ri-search-line style="width: 16px; height: 16px; position: absolute; left: 12px; top: 50%; transform: translateY(-50%); opacity: 0.4;" />
<input
type="text"
x-model="searchTerm"
@input="currentPage = 1"
placeholder="Search logs..."
class="firewall-log-search-input"
style="width: 100%; padding: 0.5rem 1rem; border: 1px solid rgba(var(--color-neutral), 1); border-radius: 0.5rem; background-color: rgba(var(--color-background), 1);"
/>
</div>
<div style="display: flex; align-items: center; gap: 1rem; font-size: 0.875rem;">
<div style="padding: 0.375rem 0.75rem; background-color: rgba(var(--color-background-secondary), 1); border: 1px solid rgba(var(--color-neutral), 0.3); border-radius: 0.5rem;">
<span style="opacity: 0.7;">Showing </span>
<span style="font-weight: 600; color: rgba(var(--color-primary), 1);" x-text="paginatedLogs.length"></span>
<span style="opacity: 0.7;"> of </span>
<span style="font-weight: 600;" x-text="filteredLogs.length"></span>
</div>
<div style="padding: 0.375rem 0.75rem; background-color: rgba(var(--color-background-secondary), 1); border: 1px solid rgba(var(--color-neutral), 0.3); border-radius: 0.5rem;">
<span style="opacity: 0.7;">Page </span>
<span style="font-weight: 600; color: rgba(var(--color-primary), 1);" x-text="currentPage"></span>
<span style="opacity: 0.7;"> / </span>
<span style="font-weight: 600;" x-text="totalPages || 1"></span>
</div>
</div>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full border-collapse">
<thead>
<tr class="border-b border-neutral">
<th class="text-left py-3 px-4 font-semibold text-sm">
<button type="button" class="flex items-center gap-1 hover:text-primary transition" @click="sort('timestamp_iso')">
Timestamp
<span x-show="sortField === 'timestamp_iso'" x-text="sortDirection === 'asc' ? '▲' : '▼'" class="text-[10px]"></span>
</button>
</th>
<th class="text-left py-3 px-4 font-semibold text-sm">
<button type="button" class="flex items-center gap-1 hover:text-primary transition" @click="sort('level_numeric')">
Level
<span x-show="sortField === 'level_numeric'" x-text="sortDirection === 'asc' ? '▲' : '▼'" class="text-[10px]"></span>
</button>
</th>
<th class="text-left py-3 px-4 font-semibold text-sm">
<button type="button" class="flex items-center gap-1 hover:text-primary transition" @click="sort('action')">
Action
<span x-show="sortField === 'action'" x-text="sortDirection === 'asc' ? '▲' : '▼'" class="text-[10px]"></span>
</button>
</th>
<th class="text-left py-3 px-4 font-semibold text-sm">
<button type="button" class="flex items-center gap-1 hover:text-primary transition" @click="sort('protocol')">
Protocol
<span x-show="sortField === 'protocol'" x-text="sortDirection === 'asc' ? '▲' : '▼'" class="text-[10px]"></span>
</button>
</th>
<th class="text-left py-3 px-4 font-semibold text-sm">
<button type="button" class="flex items-center gap-1 hover:text-primary transition" @click="sort('source')">
Source
<span x-show="sortField === 'source'" x-text="sortDirection === 'asc' ? '▲' : '▼'" class="text-[10px]"></span>
</button>
</th>
<th class="text-left py-3 px-4 font-semibold text-sm">
<button type="button" class="flex items-center gap-1 hover:text-primary transition" @click="sort('source_port')">
Source Port
<span x-show="sortField === 'source_port'" x-text="sortDirection === 'asc' ? '▲' : '▼'" class="text-[10px]"></span>
</button>
</th>
<th class="text-left py-3 px-4 font-semibold text-sm">
<button type="button" class="flex items-center gap-1 hover:text-primary transition" @click="sort('destination')">
Destination
<span x-show="sortField === 'destination'" x-text="sortDirection === 'asc' ? '▲' : '▼'" class="text-[10px]"></span>
</button>
</th>
<th class="text-left py-3 px-4 font-semibold text-sm">
<button type="button" class="flex items-center gap-1 hover:text-primary transition" @click="sort('destination_port')">
Dest Port
<span x-show="sortField === 'destination_port'" x-text="sortDirection === 'asc' ? '▲' : '▼'" class="text-[10px]"></span>
</button>
</th>
<th class="text-left py-3 px-4 font-semibold text-sm">
Actions
</th>
</tr>
</thead>
<tbody>
<template x-for="(log, index) in paginatedLogs" :key="log.index || index">
<tr class="border-b border-neutral/20 hover:bg-background-secondary/50">
<!-- Timestamp -->
<td class="py-3 px-4 text-sm" x-text="log.timestamp_display || '-' "></td>
<!-- Level -->
<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>
<!-- Action -->
<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>
<!-- Protocol with info modal -->
<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 }" x-cloak>
<button type="button" @click="open = true" class="text-base/50 hover:text-primary 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.away="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 bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6 max-h-[80vh] overflow-y-auto">
<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="pr-8">
<div class="flex items-center gap-2 mb-4">
<x-ri-information-line class="size-6 text-primary" />
<h3 class="text-lg font-semibold" x-text="`${log.protocol} Protocol`"></h3>
</div>
<div class="text-sm text-base/90" x-html="window.getProtocolDescription(log.protocol)"></div>
</div>
</div>
</div>
</div>
</template>
</div>
</td>
<!-- Source IP with location lookup -->
<td class="py-3 px-4 text-sm">
<div class="flex items-center gap-1">
<span class="font-mono text-xs" 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 }" x-cloak>
<button type="button" @click="
if (!ipData && !loading) {
loading = true;
open = true;
window.fetchIPLocation(log.source).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" 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>
</template>
</div>
</td>
<!-- Source Port with description -->
<td class="py-3 px-4 text-sm">
<div class="flex items-center gap-1">
<span x-text="log.source_port || '-' "></span>
<template x-if="log.source_port && log.source_port !== '-'">
<div class="relative" x-data="{ open: false }" x-cloak>
<button type="button" @click="open = true" class="text-base/50 hover:text-primary 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.away="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-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="pr-8">
<div class="flex items-center gap-2 mb-4">
<x-ri-information-line class="size-6 text-primary" />
<h3 class="text-lg font-semibold">Port <span x-text="log.source_port"></span></h3>
</div>
<p class="text-sm text-base/90" x-text="window.getPortDescription(parseInt(log.source_port) || 0)"></p>
</div>
</div>
</div>
</div>
</template>
</div>
</td>
<!-- Destination IP with location lookup -->
<td class="py-3 px-4 text-sm">
<div class="flex items-center gap-1">
<span class="font-mono text-xs" 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 }" x-cloak>
<button type="button" @click="
if (!ipData && !loading) {
loading = true;
open = true;
window.fetchIPLocation(log.destination).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" 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>
</template>
</div>
</td>
<!-- Destination Port with description -->
<td class="py-3 px-4 text-sm">
<div class="flex items-center gap-1">
<span x-text="log.destination_port || '-' "></span>
<template x-if="log.destination_port && log.destination_port !== '-'">
<div class="relative" x-data="{ open: false }" x-cloak>
<button type="button" @click="open = true" class="text-base/50 hover:text-primary 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.away="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-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="pr-8">
<div class="flex items-center gap-2 mb-4">
<x-ri-information-line class="size-6 text-primary" />
<h3 class="text-lg font-semibold">Port <span x-text="log.destination_port"></span></h3>
</div>
<p class="text-sm text-base/90" x-text="window.getPortDescription(parseInt(log.destination_port) || 0)"></p>
</div>
</div>
</div>
</div>
</template>
</div>
</td>
<!-- Actions -->
<td class="py-3 px-4 text-sm">
<div x-data="{ showStateModal: false }">
<button type="button"
@click="showStateModal = true"
class="firewall-state-btn"
title="View connection state details">
<x-ri-information-line style="width: 14px; height: 14px;" />
<span>Details</span>
</button>
<!-- State Details Modal -->
<div x-show="showStateModal"
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="showStateModal = false"
@keydown.escape.window="showStateModal = 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-lg bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl max-h-[90vh] overflow-y-auto">
<button @click="showStateModal = 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">
<!-- Header -->
<div class="flex items-center gap-3 mb-6 pb-4 border-b border-neutral/20">
<div class="p-2 rounded-lg" :class="log.action === 'DROP' || log.action === 'REJECT' ? 'bg-danger/20' : 'bg-success/20'">
<template x-if="log.action === 'DROP' || log.action === 'REJECT'">
<x-ri-shield-cross-line class="size-6 text-danger" />
</template>
<template x-if="log.action === 'ACCEPT'">
<x-ri-shield-check-line class="size-6 text-success" />
</template>
<template x-if="log.action !== 'DROP' && log.action !== 'REJECT' && log.action !== 'ACCEPT'">
<x-ri-shield-line class="size-6 text-base/70" />
</template>
</div>
<div>
<h3 class="text-lg font-semibold">Connection State</h3>
<p class="text-sm text-base/60" x-text="log.timestamp_display || 'Unknown time'"></p>
</div>
</div>
<!-- Action Badge -->
<div class="mb-4">
<span class="px-3 py-1.5 rounded-lg text-sm font-semibold" :class="log.action_badge_class" x-text="log.action || 'UNKNOWN'"></span>
</div>
<!-- Connection Details Grid -->
<div class="grid grid-cols-2 gap-4">
<!-- Source Section -->
<div class="col-span-2 sm:col-span-1 p-4 bg-background rounded-lg border border-neutral/20">
<div class="flex items-center gap-2 mb-3">
<x-ri-arrow-right-up-line class="size-4 text-info" />
<span class="text-xs font-semibold text-base/60 uppercase tracking-wide">Source</span>
</div>
<div class="space-y-2">
<div>
<span class="text-xs text-base/50">IP Address</span>
<p class="font-mono text-sm font-medium break-all" x-text="log.source || '-'"></p>
</div>
<div>
<span class="text-xs text-base/50">Port</span>
<p class="font-mono text-sm font-medium" x-text="log.source_port || '-'"></p>
</div>
</div>
</div>
<!-- Destination Section -->
<div class="col-span-2 sm:col-span-1 p-4 bg-background rounded-lg border border-neutral/20">
<div class="flex items-center gap-2 mb-3">
<x-ri-arrow-right-down-line class="size-4 text-warning" />
<span class="text-xs font-semibold text-base/60 uppercase tracking-wide">Destination</span>
</div>
<div class="space-y-2">
<div>
<span class="text-xs text-base/50">IP Address</span>
<p class="font-mono text-sm font-medium break-all" x-text="log.destination || '-'"></p>
</div>
<div>
<span class="text-xs text-base/50">Port</span>
<p class="font-mono text-sm font-medium" x-text="log.destination_port || '-'"></p>
</div>
</div>
</div>
<!-- Protocol & Level -->
<div class="col-span-2 p-4 bg-background rounded-lg border border-neutral/20">
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4">
<div>
<span class="text-xs text-base/50">Protocol</span>
<p class="text-sm font-semibold" x-text="log.protocol || '-'"></p>
</div>
<div>
<span class="text-xs text-base/50">Level</span>
<p class="text-sm">
<span class="px-2 py-0.5 rounded text-xs font-semibold uppercase" :class="log.level_badge_class" x-text="log.level_label || '-'"></span>
</p>
</div>
<div>
<span class="text-xs text-base/50">Interface</span>
<p class="font-mono text-sm font-medium" x-text="log.interface || '-'"></p>
</div>
<div>
<span class="text-xs text-base/50">MAC Address</span>
<p class="font-mono text-xs font-medium break-all" x-text="log.mac || '-'"></p>
</div>
</div>
</div>
<!-- Raw Log (if available) -->
<template x-if="log.raw">
<div class="col-span-2 p-4 bg-background rounded-lg border border-neutral/20">
<div class="flex items-center gap-2 mb-2">
<x-ri-code-line class="size-4 text-base/50" />
<span class="text-xs font-semibold text-base/60 uppercase tracking-wide">Raw Log Entry</span>
</div>
<pre class="text-xs font-mono text-base/70 whitespace-pre-wrap break-all bg-background-secondary p-3 rounded-lg border border-neutral/10 max-h-32 overflow-y-auto" x-text="log.raw"></pre>
</div>
</template>
</div>
<!-- Footer Actions -->
<div class="mt-6 pt-4 border-t border-neutral/20 flex justify-end gap-2">
<a :href="'{{ route('proxmox.server.statistics', ['server' => $server->id]) }}?network_search=' + encodeURIComponent(log.source || '') + '#network-analysis-card'"
class="px-4 py-2 text-sm font-medium bg-primary/10 text-primary border border-primary/30 rounded-lg hover:bg-primary/20 transition-colors inline-flex items-center gap-2">
<x-ri-line-chart-line class="size-4" />
View in Network Analysis
</a>
<button @click="showStateModal = false" class="px-4 py-2 text-sm font-medium bg-base/10 text-base/70 border border-neutral/30 rounded-lg hover:bg-base/20 transition-colors">
Close
</button>
</div>
</div>
</div>
</div>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<!-- Pagination Controls -->
<div x-show="totalPages > 1" class="flex items-center justify-center gap-2 mt-4 flex-wrap">
<button
type="button"
@click="prevPage()"
:disabled="currentPage === 1"
class="px-3 py-2 border border-neutral rounded-lg transition-all text-sm font-medium flex items-center justify-center"
:class="currentPage === 1 ? 'opacity-50 cursor-not-allowed bg-background-secondary' : 'hover:bg-primary/10 hover:border-primary/50'">
<x-ri-arrow-left-s-line class="size-4" />
</button>
<template x-for="(page, index) in paginationRange" :key="`page-${index}-${page}`">
<button
type="button"
@click="typeof page === 'number' ? goToPage(page) : null"
:disabled="typeof page !== 'number'"
class="px-4 py-2 border rounded-lg transition-all text-sm font-medium min-w-[44px] flex items-center justify-center"
:class="{
'bg-primary text-white border-primary hover:bg-primary/90': page === currentPage && typeof page === 'number',
'border-neutral hover:bg-primary/10 hover:border-primary/50': typeof page === 'number' && page !== currentPage,
'border-transparent cursor-default opacity-60': typeof page !== 'number'
}"
x-text="page">
</button>
</template>
<button
type="button"
@click="nextPage()"
:disabled="currentPage === totalPages"
class="px-3 py-2 border border-neutral rounded-lg transition-all text-sm font-medium flex items-center justify-center"
:class="currentPage === totalPages ? 'opacity-50 cursor-not-allowed bg-background-secondary' : 'hover:bg-primary/10 hover:border-primary/50'">
<x-ri-arrow-right-s-line class="size-4" />
</button>
</div>
<div x-show="filteredLogs.length === 0" x-cloak class="text-center py-8 text-base/60">
<x-ri-file-list-3-line class="size-12 mx-auto mb-3 opacity-50" />
<p>No logs matched your search.</p>
</div>
</div>
@elseif(empty($logError))
<div class="text-center py-8 text-base/60">
<x-ri-file-list-3-line class="size-12 mx-auto mb-3 opacity-50" />
<p>No firewall logs available</p>
<p class="text-xs mt-2">Ensure firewall logging is enabled in Proxmox (Datacenter > Firewall > Options)</p>
</div>
@endif
</div>
@else
<div class="bg-background-secondary border border-neutral/20 rounded-lg p-8 text-center">
<x-ri-shield-line class="size-16 mx-auto mb-4 opacity-50" />
<h3 class="text-lg font-semibold mb-2">Firewall is Disabled</h3>
<p class="text-base/60">Enable the firewall above to manage firewall rules.</p>
</div>
@endif
<!-- Add/Edit Rule Modal -->
@if($showRuleForm)
<div class="fixed inset-0 flex items-center justify-center z-50 bg-black/50 backdrop-blur-sm"
wire:click="$set('showRuleForm', false); $set('editingRulePos', null); resetRuleForm()">
<div class="bg-background border border-neutral rounded-lg p-6 shadow-lg max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto" wire:click.stop>
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold">{{ $editingRulePos !== null ? 'Edit Rule' : 'Create New Rule' }}</h3>
<button
wire:click="$set('showRuleForm', false); $set('editingRulePos', null); resetRuleForm()"
class="text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium mb-2">Action</label>
<select wire:model="ruleAction" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
<option value="ACCEPT">Accept</option>
<option value="REJECT">Reject</option>
<option value="DROP">Drop</option>
</select>
</div>
<div>
<label class="block text-sm font-medium mb-2">Protocol</label>
<select wire:model="ruleProto" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
<option value="tcp">TCP</option>
<option value="udp">UDP</option>
<option value="icmp">ICMP</option>
<option value="igmp">IGMP</option>
<option value="ggp">GGP</option>
<option value="ipencap">IPENCAP</option>
<option value="st">ST</option>
<option value="egp">EGP</option>
<option value="igp">IGP</option>
<option value="pip">PIP</option>
<option value="hmp">HMP</option>
<option value="xns-idp">XNS-IDP</option>
<option value="rdp">RDP</option>
<option value="iso-tp4">ISO-TP4</option>
<option value="dccp">DCCP</option>
<option value="xtp">XTP</option>
<option value="ddp">DDP</option>
<option value="idpr-cmtp">IDPR-CMTP</option>
<option value="ipv6">IPv6</option>
<option value="ipv6-route">IPv6-Route</option>
<option value="ipv6-frag">IPv6-Frag</option>
<option value="idrp">IDRP</option>
<option value="rsvp">RSVP</option>
<option value="gre">GRE</option>
<option value="esp">ESP</option>
<option value="ah">AH</option>
<option value="skip">SKIP</option>
<option value="ipv6-icmp">IPv6-ICMP</option>
<option value="ipv6-nonxt">IPv6-Nonxt</option>
<option value="ipv6-opts">IPv6-Opts</option>
<option value="vmtp">VMTP</option>
<option value="eigrp">EIGRP</option>
<option value="ospf">OSPF</option>
<option value="ax.25">AX.25</option>
<option value="ipip">IPIP</option>
<option value="etherip">EtherIP</option>
<option value="encap">ENCAP</option>
<option value="pim">PIM</option>
<option value="ipcomp">IPComp</option>
<option value="vrrp">VRRP</option>
<option value="l2tp">L2TP</option>
<option value="isis">ISIS</option>
<option value="sctp">SCTP</option>
<option value="fc">FC</option>
<option value="mobility-header">Mobility-Header</option>
<option value="udplite">UDPLite</option>
<option value="mpls-in-ip">MPLS-in-IP</option>
<option value="hip">HIP</option>
<option value="shim6">Shim6</option>
<option value="wesp">WESP</option>
<option value="rohc">ROHC</option>
</select>
</div>
<div>
<label class="block text-sm font-medium mb-2">Source</label>
<input type="text" wire:model="ruleSource" placeholder="e.g., 192.168.1.0/24" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
</div>
<div>
<label class="block text-sm font-medium mb-2">Destination</label>
<select wire:model="ruleDest" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
@foreach($availableIps as $ipData)
<option value="{{ $ipData['ip'] }}">{{ $ipData['ip'] }} ({{ $ipData['type'] }} - {{ $ipData['label'] }})</option>
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium mb-2">Destination Port</label>
<input type="text" wire:model="ruleDport" placeholder="e.g., 80,443 or 1000:2000" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
</div>
<div>
<label class="block text-sm font-medium mb-2">Source Port</label>
<input type="text" wire:model="ruleSport" placeholder="e.g., 8080" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
</div>
<div>
<label class="block text-sm font-medium mb-2">Interface</label>
<select wire:model="ruleIface" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
@foreach($networkAdapters as $adapter)
<option value="{{ $adapter['name'] }}">{{ $adapter['name'] }} ({{ ucfirst($adapter['model'] ?? 'Unknown') }})</option>
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium mb-2">Log Level</label>
<select wire:model="ruleLog" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
<option value="nolog">nolog</option>
<option value="emerg">emerg</option>
<option value="alert">alert</option>
<option value="crit">crit</option>
<option value="err">err</option>
<option value="warning">warning</option>
<option value="notice">notice</option>
<option value="info">info</option>
<option value="debug">debug</option>
</select>
</div>
<div class="md:col-span-2">
<label class="block text-sm font-medium mb-2">Comment</label>
<input type="text" wire:model="ruleComment" placeholder="Optional description" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
</div>
<div class="md:col-span-2 flex items-center gap-2">
<input type="checkbox" wire:model="ruleEnable" id="ruleEnable" class="rounded border-neutral">
<label for="ruleEnable" class="text-sm">Enable this rule</label>
</div>
</div>
<div class="flex gap-2 mt-6">
<button
wire:click="saveRule"
wire:loading.attr="disabled"
class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium">
<span wire:loading.remove wire:target="saveRule">Save Rule</span>
<span wire:loading wire:target="saveRule">Saving...</span>
</button>
<button
wire:click="$set('showRuleForm', false); $set('editingRulePos', null); resetRuleForm()"
class="px-4 py-2 bg-background-secondary border border-neutral rounded-lg hover:bg-background transition-colors text-sm font-medium">
Cancel
</button>
</div>
</div>
</div>
@endif
<!-- Delete Rule Confirmation Modal -->
@if($showDeleteModal && $deletingRulePos !== null)
@php
$ruleToDelete = collect($rules)->firstWhere('pos', $deletingRulePos);
@endphp
<div class="fixed inset-0 flex items-center justify-center z-50 bg-black/60 backdrop-blur-sm transition-opacity"
wire:click="cancelDeleteRule">
<div class="bg-background border border-neutral/30 rounded-xl p-6 shadow-2xl max-w-lg w-full mx-4 transform transition-all" wire:click.stop>
<div class="flex items-start gap-4 mb-6">
<div class="flex items-center justify-center w-12 h-12 rounded-full bg-error/10 flex-shrink-0">
<x-ri-delete-bin-fill class="size-6 text-error" />
</div>
<div class="flex-1">
<h3 class="text-xl font-bold mb-2">Delete Firewall Rule</h3>
<p class="text-sm text-base/70 mb-4 leading-relaxed">
Are you sure you want to delete this firewall rule? This action cannot be undone and may affect your network security.
</p>
@if($ruleToDelete)
<div class="bg-background-secondary/50 border border-neutral/20 rounded-lg p-4 mb-4">
<div class="text-sm space-y-2">
<div class="flex items-center gap-2">
<span class="font-semibold text-base/80 min-w-[80px]">Action:</span>
<span class="px-2 py-1 rounded text-xs font-medium
{{ ($ruleToDelete['action'] ?? '') === 'ACCEPT' ? 'bg-success/20 text-success' : '' }}
{{ ($ruleToDelete['action'] ?? '') === 'REJECT' ? 'bg-warning/20 text-warning' : '' }}
{{ ($ruleToDelete['action'] ?? '') === 'DROP' ? 'bg-error/20 text-error' : '' }}">
{{ $ruleToDelete['action'] ?? '-' }}
</span>
</div>
@if(!empty($ruleToDelete['source']))
<div class="flex items-center gap-2">
<span class="font-semibold text-base/80 min-w-[80px]">Source:</span>
<code class="text-xs bg-background px-2 py-1 rounded">{{ $ruleToDelete['source'] }}</code>
</div>
@endif
@if(!empty($ruleToDelete['dest']))
<div class="flex items-center gap-2">
<span class="font-semibold text-base/80 min-w-[80px]">Destination:</span>
<code class="text-xs bg-background px-2 py-1 rounded">{{ $ruleToDelete['dest'] }}</code>
</div>
@endif
@if(!empty($ruleToDelete['proto']))
<div class="flex items-center gap-2">
<span class="font-semibold text-base/80 min-w-[80px]">Protocol:</span>
<span class="text-base/90">{{ strtoupper($ruleToDelete['proto']) }}</span>
</div>
@endif
@if(!empty($ruleToDelete['dport']))
<div class="flex items-center gap-2">
<span class="font-semibold text-base/80 min-w-[80px]">Port:</span>
<span class="text-base/90">{{ $ruleToDelete['dport'] }}</span>
</div>
@endif
@if(!empty($ruleToDelete['comment']))
<div class="flex items-start gap-2 pt-1 border-t border-neutral/20">
<span class="font-semibold text-base/80 min-w-[80px]">Comment:</span>
<span class="text-base/90 flex-1">{{ $ruleToDelete['comment'] }}</span>
</div>
@endif
</div>
</div>
@endif
</div>
<button
wire:click="cancelDeleteRule"
class="text-base/40 hover:text-base transition-colors flex-shrink-0">
<x-ri-close-line class="size-5" />
</button>
</div>
<div class="flex gap-3 justify-end pt-4 border-t border-neutral/20">
<button
wire:click="cancelDeleteRule"
class="px-5 py-2.5 bg-background-secondary border border-neutral/30 rounded-lg hover:bg-background hover:border-neutral/50 transition-all text-sm font-medium text-base/80">
Cancel
</button>
<button
wire:click="deleteRule"
wire:loading.attr="disabled"
class="delete-rule-button">
<svg wire:loading.remove wire:target="deleteRule" class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M17 6h5v2h-2v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V8H2V6h5V3a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v3zm1 2H6v12h12V8zm-9 3h2v6H9v-6zm4 0h2v6h-2v-6zM9 4v2h6V4H9z"/>
</svg>
<svg wire:loading wire:target="deleteRule" class="animate-spin size-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span wire:loading.remove wire:target="deleteRule">Delete Rule</span>
<span wire:loading wire:target="deleteRule">Deleting...</span>
</button>
</div>
</div>
</div>
@endif
</div>
@script
<script>
// Check if an IP address is public (not private/localhost)
window.isPublicIP = function(ip) {
if (!ip || ip === '—' || ip === '-' || typeof ip !== 'string') return false;
ip = ip.trim();
// Check for localhost
if (ip === 'localhost' || ip === '127.0.0.1' || ip === '::1') return false;
// Check for IPv4 private ranges
const ipv4PrivateRanges = [
/^10\./, // 10.0.0.0/8
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // 172.16.0.0/12
/^192\.168\./, // 192.168.0.0/16
/^169\.254\./, // 169.254.0.0/16 (link-local)
/^127\./, // 127.0.0.0/8
/^0\./ // 0.0.0.0/8
];
// Check if it's IPv4
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (ipv4Regex.test(ip)) {
// Check against private ranges
if (ipv4PrivateRanges.some(range => range.test(ip))) {
return false;
}
return true;
}
// Check if it's IPv6
if (ip.includes(':')) {
// Check for IPv6 private ranges
// 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
window.fetchIPLocation = async function(ip) {
if (!window.isPublicIP(ip)) {
return { status: 'fail', message: 'Not a public IP address' };
}
ip = ip.trim();
try {
// Use Livewire to call server-side method
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 formatDescription = (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 formatDescription('Unknown protocol', '', '');
}
const protocolUpper = protocol.toUpperCase();
const protocolDescriptions = {
'TCP': formatDescription(
'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': formatDescription(
'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': formatDescription(
'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': formatDescription(
'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] || formatDescription(
`The protocol "${protocol}" is a network protocol identifier that defines the rules and format for data communication between network devices.`,
'Network protocols specify how data is formatted, transmitted, and received over a network.',
''
);
};
// Port descriptions mapping
window.getPortDescription = function(port) {
const portDescriptions = {
20: 'FTP Data Transfer - File Transfer Protocol data connection',
21: 'FTP Control - File Transfer Protocol control connection',
22: 'SSH - Secure Shell, used for secure remote login and command execution',
23: 'Telnet - Unencrypted remote terminal access (insecure)',
25: 'SMTP - Simple Mail Transfer Protocol for sending email',
53: 'DNS - Domain Name System for resolving domain names to IP addresses',
67: 'DHCP Server - Dynamic Host Configuration Protocol server',
68: 'DHCP Client - Dynamic Host Configuration Protocol client',
69: 'TFTP - Trivial File Transfer Protocol',
80: 'HTTP - Hypertext Transfer Protocol for web traffic',
88: 'Kerberos - Network authentication protocol',
110: 'POP3 - Post Office Protocol version 3 for receiving email',
119: 'NNTP - Network News Transfer Protocol',
123: 'NTP - Network Time Protocol for time synchronization',
135: 'MSRPC - Microsoft Remote Procedure Call',
139: 'NetBIOS - Network Basic Input/Output System',
143: 'IMAP - Internet Message Access Protocol for email',
161: 'SNMP - Simple Network Management Protocol',
162: 'SNMP Trap - Simple Network Management Protocol trap',
389: 'LDAP - Lightweight Directory Access Protocol',
443: 'HTTPS - Hypertext Transfer Protocol Secure (encrypted web traffic)',
445: 'SMB - Server Message Block for file sharing (Windows)',
465: 'SMTPS - SMTP over SSL/TLS',
514: 'Syslog - System logging protocol',
515: 'LPD/LPR - Line Printer Daemon/Line Printer Remote',
587: 'SMTP Submission - Mail submission port',
636: 'LDAPS - LDAP over SSL/TLS',
993: 'IMAPS - IMAP over SSL/TLS',
995: 'POP3S - POP3 over SSL/TLS',
1433: 'MSSQL - Microsoft SQL Server database',
1521: 'Oracle Database - Oracle database listener',
3306: 'MySQL - MySQL database server',
3389: 'RDP - Remote Desktop Protocol (Windows)',
5432: 'PostgreSQL - PostgreSQL database server',
5900: 'VNC - Virtual Network Computing remote desktop',
6379: 'Redis - Redis in-memory data store',
8080: 'HTTP Alternate - Alternative HTTP port, often used for web proxies',
8443: 'HTTPS Alternate - Alternative HTTPS port',
27017: 'MongoDB - MongoDB database server',
27018: 'MongoDB Shard - MongoDB sharding port',
27019: 'MongoDB Config - MongoDB configuration server',
};
// Check for common port ranges
if (port >= 1024 && port <= 49151) {
return 'Registered/User Port - Port in the registered range, commonly used by applications';
} else if (port >= 49152 && port <= 65535) {
return 'Dynamic/Private Port - Ephemeral port range used for temporary connections';
}
return portDescriptions[port] || 'Unknown Port - No standard service associated with this port number';
};
</script>
@endscript
</x-proxmox::layout>