Files
Virtnet.bond/extensions/Servers/Proxmox/views/firewall.blade.php.old
Joseph.Rawlings 87e0157693 Refactor styling in help and network views
- Updated background classes for various elements in help.blade.php to remove opacity for better visibility.
- Adjusted background classes in network.blade.php to enhance error and info message visibility.
- Ensured consistent styling for icons and sections across both views.
2025-12-18 07:16:59 -05:00

2636 lines
204 KiB
PHP

<x-proxmox::layout :server="$server" view="firewall" :status="$status" :status-polling="true" :status-polling-interval="'2s'">
{{-- Global Loader Styles --}}
<style>
.fw-loader {
width: 24px;
aspect-ratio: 1;
border-radius: 50%;
padding: 3px;
background:
conic-gradient(from 135deg at top, currentColor 90deg, #0000 0) 0 calc(50% - 2px)/10px 5px,
radial-gradient(farthest-side at bottom left, #0000 calc(100% - 3px), currentColor calc(100% - 2px) 99%, #0000) top right/50% 50% content-box content-box,
radial-gradient(farthest-side at top, #0000 calc(100% - 3px), currentColor calc(100% - 2px) 99%, #0000) bottom/100% 50% content-box content-box;
background-repeat: no-repeat;
animation: fw-loader-spin 1s infinite linear;
}
.fw-loader-sm {
width: 16px;
aspect-ratio: 1;
border-radius: 50%;
padding: 2px;
background:
conic-gradient(from 135deg at top, currentColor 90deg, #0000 0) 0 calc(50% - 1.5px)/6px 3px,
radial-gradient(farthest-side at bottom left, #0000 calc(100% - 2px), currentColor calc(100% - 1.5px) 99%, #0000) top right/50% 50% content-box content-box,
radial-gradient(farthest-side at top, #0000 calc(100% - 2px), currentColor calc(100% - 1.5px) 99%, #0000) bottom/100% 50% content-box content-box;
background-repeat: no-repeat;
animation: fw-loader-spin 1s infinite linear;
}
.fw-loader-lg {
width: 32px;
aspect-ratio: 1;
border-radius: 50%;
padding: 4px;
background:
conic-gradient(from 135deg at top, currentColor 90deg, #0000 0) 0 calc(50% - 3px)/12px 6px,
radial-gradient(farthest-side at bottom left, #0000 calc(100% - 4px), currentColor calc(100% - 3px) 99%, #0000) top right/50% 50% content-box content-box,
radial-gradient(farthest-side at top, #0000 calc(100% - 4px), currentColor calc(100% - 3px) 99%, #0000) bottom/100% 50% content-box content-box;
background-repeat: no-repeat;
animation: fw-loader-spin 1s infinite linear;
}
.fw-loader-xl {
width: 50px;
aspect-ratio: 1;
border-radius: 50%;
padding: 6px;
background:
conic-gradient(from 135deg at top, currentColor 90deg, #0000 0) 0 calc(50% - 4px)/17px 8.5px,
radial-gradient(farthest-side at bottom left, #0000 calc(100% - 6px), currentColor calc(100% - 5px) 99%, #0000) top right/50% 50% content-box content-box,
radial-gradient(farthest-side at top, #0000 calc(100% - 6px), currentColor calc(100% - 5px) 99%, #0000) bottom/100% 50% content-box content-box;
background-repeat: no-repeat;
animation: fw-loader-spin 1s infinite linear;
}
@keyframes fw-loader-spin {
100% { transform: rotate(1turn); }
}
/* Firewall Toggle Switch */
.fw-toggle {
position: relative;
width: 64px;
height: 32px;
cursor: pointer;
flex-shrink: 0;
}
.fw-toggle input {
opacity: 0;
width: 0;
height: 0;
}
.fw-toggle-slider {
position: absolute;
inset: 0;
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
border-radius: 32px;
transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
box-shadow: inset 0 2px 4px rgba(0,0,0,0.2), 0 2px 8px rgba(220, 38, 38, 0.3);
}
.fw-toggle-slider::before {
content: '';
position: absolute;
width: 24px;
height: 24px;
left: 4px;
bottom: 4px;
background: linear-gradient(135deg, #ffffff 0%, #f3f4f6 100%);
border-radius: 50%;
transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
.fw-toggle input:checked + .fw-toggle-slider {
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
box-shadow: inset 0 2px 4px rgba(0,0,0,0.2), 0 2px 8px rgba(34, 197, 94, 0.3);
}
.fw-toggle input:checked + .fw-toggle-slider::before {
transform: translateX(32px);
}
.fw-toggle:hover .fw-toggle-slider {
filter: brightness(1.1);
}
.fw-toggle input:disabled + .fw-toggle-slider {
opacity: 0.6;
cursor: not-allowed;
}
.fw-toggle-label {
font-weight: 600;
font-size: 0.875rem;
transition: color 0.3s ease;
}
.fw-toggle-label-enabled {
color: #22c55e;
}
.fw-toggle-label-disabled {
color: #dc2626;
}
@keyframes fw-toggle-pulse {
0%, 100% { box-shadow: inset 0 2px 4px rgba(0,0,0,0.2), 0 2px 8px rgba(34, 197, 94, 0.3); }
50% { box-shadow: inset 0 2px 4px rgba(0,0,0,0.2), 0 2px 16px rgba(34, 197, 94, 0.5); }
}
.fw-toggle input:checked + .fw-toggle-slider {
animation: fw-toggle-pulse 2s infinite ease-in-out;
}
</style>
<div class="flex flex-col gap-6" x-data="{ pageLoaded: false }" x-init="setTimeout(() => pageLoaded = true, 50)">
{{-- Page Header --}}
<div :class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-2'"
style="transition: all 0.4s ease-out;">
<x-proxmox::ui.section-header
title="Firewall Management"
subtitle="Configure firewall rules, policies, aliases, and IPSets for your virtual machine"
icon="ri-shield-line"
/>
</div>
{{-- Error Modal --}}
@if($error && $showErrorModal)
<x-proxmox::ui.modal title="Error" icon="ri-error-warning-line" icon-class="text-error" :show="true" wire:key="error-modal">
<x-slot name="header">
<button wire:click="$set('showErrorModal', false)" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
</x-slot>
<p class="text-sm text-base/70">{{ $error }}</p>
<x-slot name="footer">
<x-proxmox::ui.button wire:click="$set('showErrorModal', false)" variant="secondary">
Close
</x-proxmox::ui.button>
</x-slot>
</x-proxmox::ui.modal>
@endif
{{-- Main Firewall Toggle & Status --}}
<div class="relative overflow-hidden rounded-xl border border-primary/20 bg-gradient-to-br from-primary/10 via-primary/5 to-transparent p-6 mb-6 transition-all duration-300 hover:shadow-lg"
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'"
style="transition: all 0.4s ease-out 0.1s;">
{{-- Decorative elements --}}
<div class="absolute top-0 right-0 w-32 h-32 bg-primary/10 rounded-full -translate-y-1/2 translate-x-1/2 blur-2xl transition-transform duration-500 hover:scale-110"></div>
<div class="absolute bottom-0 left-0 w-24 h-24 bg-primary/5 rounded-full translate-y-1/2 -translate-x-1/2 blur-xl transition-transform duration-500 hover:scale-110"></div>
<div class="relative flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-xl bg-primary/20 flex items-center justify-center ring-4 ring-primary/10 transition-transform duration-300 hover:scale-110">
<x-ri-shield-line class="size-6 text-primary" />
</div>
<div>
<h2 class="text-xl font-bold">Master Firewall Control</h2>
<p class="text-sm mt-1">
@if($firewallEnabled)
<span class="inline-flex items-center gap-1.5 text-success font-medium">
<span class="relative flex h-2 w-2">
<span class="absolute inline-flex h-full w-full rounded-full bg-success opacity-75 animate-ping"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-success"></span>
</span>
Active - Your VM is protected
</span>
@else
<span class="inline-flex items-center gap-1.5 text-warning font-medium">
<x-ri-error-warning-line class="size-4" />
Disabled - No firewall protection
</span>
@endif
</p>
</div>
</div>
<div class="flex items-center gap-4">
<div class="flex items-center gap-3">
<span class="fw-toggle-label {{ $firewallEnabled ? 'fw-toggle-label-enabled' : 'fw-toggle-label-disabled' }}">
{{ $firewallEnabled ? 'Enabled' : 'Disabled' }}
</span>
<label class="fw-toggle" wire:loading.class="pointer-events-none">
<input
type="checkbox"
wire:click="toggleFirewall"
wire:loading.attr="disabled"
{{ $firewallEnabled ? 'checked' : '' }}
>
<span class="fw-toggle-slider"></span>
</label>
</div>
<span wire:loading wire:target="toggleFirewall" class="text-xs text-base/50 flex items-center gap-1.5">
<span class="fw-loader-sm text-primary"></span>
{{ $firewallEnabled ? 'Disabling...' : 'Enabling...' }}
</span>
</div>
</div>
</div>
{{-- Firewall Policies & Network Adapters Grid --}}
@if($firewallEnabled)
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
{{-- Default Policies Card --}}
<x-proxmox::ui.card>
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-warning/10 flex items-center justify-center">
<x-ri-filter-3-line class="size-5 text-warning" />
</div>
<div>
<h3 class="text-lg font-semibold">Default Policies</h3>
<p class="text-sm text-base/60">Control default traffic behavior</p>
</div>
</div>
<button type="button" wire:click="openPolicyInfoModal" class="text-info hover:text-info/80 transition-colors" title="Learn about policies">
<x-ri-information-line class="size-5" />
</button>
</div>
<div class="space-y-4">
<x-proxmox::ui.select
wire:model="policyIn"
label="Incoming Traffic (Policy In)"
help="Default action for incoming traffic not matching any rule">
<option value="ACCEPT">Accept</option>
<option value="REJECT">Reject</option>
<option value="DROP">Drop</option>
</x-proxmox::ui.select>
<x-proxmox::ui.select
wire:model="policyOut"
label="Outgoing Traffic (Policy Out)"
help="Default action for outgoing traffic not matching any rule">
<option value="ACCEPT">Accept</option>
<option value="REJECT">Reject</option>
<option value="DROP">Drop</option>
</x-proxmox::ui.select>
<x-proxmox::ui.button
wire:click="updatePolicies"
wire:loading.attr="disabled"
class="w-full mt-4">
<span wire:loading.remove wire:target="updatePolicies">Update Policies</span>
<span wire:loading wire:target="updatePolicies">Updating...</span>
</x-proxmox::ui.button>
</div>
</x-proxmox::ui.card>
{{-- Network Adapters --}}
<x-proxmox::ui.card>
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-lg bg-success/10 flex items-center justify-center">
<x-ri-router-line class="size-5 text-success" />
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold">Network Adapters</h3>
<p class="text-sm text-base/60">Per-device firewall control</p>
</div>
</div>
@if(count($networkAdapters) > 0)
<div class="space-y-3">
@foreach($networkAdapters as $adapter)
<div class="p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-gray-700/20 rounded-xl hover:border-gray-300 dark:hover:border-gray-700/40 transition-all">
<div class="flex items-center justify-between">
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<span class="font-semibold text-sm">{{ $adapter['name'] }}</span>
<x-proxmox::ui.badge :variant="($adapterFirewallStates[$adapter['name']] ?? false) ? 'success' : 'secondary'">
{{ ($adapterFirewallStates[$adapter['name']] ?? false) ? 'Active' : 'Off' }}
</x-proxmox::ui.badge>
</div>
<div class="text-xs text-base/60">
{{ ucfirst($adapter['model'] ?? 'Unknown') }}
@if($adapter['mac'])
<span class="ml-2">{{ $adapter['mac'] }}</span>
@endif
</div>
</div>
<div class="flex items-center gap-2">
@if($adapterFirewallStates[$adapter['name']] ?? false)
<x-proxmox::ui.button
wire:click="toggleAdapterFirewall('{{ $adapter['name'] }}')"
wire:loading.attr="disabled"
wire:key="adapter-{{ $adapter['name'] }}"
variant="danger"
size="sm"
icon="ri-close-line">
<span wire:loading.remove wire:target="toggleAdapterFirewall('{{ $adapter['name'] }}')">Disable</span>
<span wire:loading wire:target="toggleAdapterFirewall('{{ $adapter['name'] }}')">...</span>
</x-proxmox::ui.button>
@else
<x-proxmox::ui.button
wire:click="toggleAdapterFirewall('{{ $adapter['name'] }}')"
wire:loading.attr="disabled"
wire:key="adapter-{{ $adapter['name'] }}"
variant="success"
size="sm"
icon="ri-shield-line">
<span wire:loading.remove wire:target="toggleAdapterFirewall('{{ $adapter['name'] }}')">Enable</span>
<span wire:loading wire:target="toggleAdapterFirewall('{{ $adapter['name'] }}')">...</span>
</x-proxmox::ui.button>
@endif
</div>
</div>
</div>
@endforeach
</div>
@else
<x-proxmox::ui.empty-state
icon="ri-router-line"
title="No Network Adapters"
description="No network adapters found on this VM" />
@endif
</x-proxmox::ui.card>
</div>
{{-- Policy Info Modal --}}
@if($showPolicyInfoModal)
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="relative w-full max-w-3xl bg-white dark:bg-gray-900 rounded-2xl border border-gray-200 dark:border-gray-800 shadow-2xl max-h-[85vh] overflow-y-auto">
<button wire:click="closePolicyInfoModal" 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">
<div class="flex items-center gap-3 mb-6">
<div class="w-12 h-12 rounded-xl bg-info/20 flex items-center justify-center">
<x-ri-information-line class="size-6 text-info" />
</div>
<div>
<h3 class="text-xl font-bold">Understanding Firewall Policies</h3>
<p class="text-sm text-base/60">How default policies control your network traffic</p>
</div>
</div>
<div class="space-y-6 prose prose-sm max-w-none">
<div class="p-4 bg-background border border-neutral/20 rounded-lg">
<h4 class="font-semibold text-base mb-2 flex items-center gap-2">
<x-ri-arrow-down-line class="size-4 text-warning" />
Policy In (Incoming Traffic)
</h4>
<p class="text-sm text-base/80 mb-3">Controls what happens to traffic coming INTO your VM when it doesn't match any explicit firewall rule.</p>
<ul class="space-y-2 text-sm">
<li><strong class="text-success">ACCEPT:</strong> Allow all incoming traffic by default (permissive, requires explicit DENY rules)</li>
<li><strong class="text-warning">REJECT:</strong> Block incoming traffic and send rejection notification to sender</li>
<li><strong class="text-error">DROP:</strong> Silently discard incoming traffic without notifying sender (recommended)</li>
</ul>
</div>
<div class="p-4 bg-background border border-neutral/20 rounded-lg">
<h4 class="font-semibold text-base mb-2 flex items-center gap-2">
<x-ri-arrow-up-line class="size-4 text-info" />
Policy Out (Outgoing Traffic)
</h4>
<p class="text-sm text-base/80 mb-3">Controls what happens to traffic going OUT of your VM when it doesn't match any explicit firewall rule.</p>
<ul class="space-y-2 text-sm">
<li><strong class="text-success">ACCEPT:</strong> Allow all outgoing traffic by default (recommended for most VMs)</li>
<li><strong class="text-warning">REJECT:</strong> Block outgoing traffic and notify your VM</li>
<li><strong class="text-error">DROP:</strong> Silently discard outgoing traffic</li>
</ul>
</div>
<div class="p-4 bg-warning/10 border border-warning/30 rounded-lg">
<h4 class="font-semibold text-base mb-2 flex items-center gap-2">
<x-ri-lock-line class="size-4 text-warning" />
Creating a Whitelist Configuration
</h4>
<p class="text-sm text-base/80 mb-2">To create a strict whitelist (only allow specific traffic):</p>
<ol class="list-decimal list-inside space-y-1 text-sm text-base/80">
<li>Set <strong>Policy In</strong> to <strong>DROP</strong></li>
<li>Set <strong>Policy Out</strong> to <strong>DROP</strong> (if restricting outbound)</li>
<li>Create explicit <strong>ACCEPT</strong> rules for allowed traffic</li>
<li>Everything not explicitly allowed will be blocked</li>
</ol>
</div>
<div class="p-4 bg-info/10 border border-info/30 rounded-lg">
<h4 class="font-semibold text-base mb-2">Additional Filters</h4>
<p class="text-sm text-base/80 mb-2"><strong>IP Filter:</strong> Validates that source/destination IP addresses in packets match the VM's assigned IPs. Helps prevent IP spoofing.</p>
<p class="text-sm text-base/80"><strong>MAC Filter:</strong> Validates that MAC addresses match the VM's network adapters. Provides additional layer of security.</p>
</div>
</div>
<div class="flex justify-end mt-6">
<button wire:click="closePolicyInfoModal" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium">
Got it
</button>
</div>
</div>
</div>
</div>
@endif
{{-- Tabs Navigation --}}
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl mb-6 shadow-theme-sm" x-data="{ activeTab: 'rules' }">
<div class="flex border-b border-neutral/20 overflow-x-auto">
<button @click="activeTab = 'rules'"
class="px-6 py-4 text-sm font-semibold transition-all relative whitespace-nowrap"
:class="activeTab === 'rules' ? 'text-primary' : 'text-base/60 hover:text-base'">
<div class="flex items-center gap-2">
<x-ri-list-check class="size-4" />
<span>Firewall Rules</span>
</div>
<div x-show="activeTab === 'rules'" class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"></div>
</button>
<button @click="activeTab = 'aliases'"
class="px-6 py-4 text-sm font-semibold transition-all relative whitespace-nowrap"
:class="activeTab === 'aliases' ? 'text-primary' : 'text-base/60 hover:text-base'">
<div class="flex items-center gap-2">
<x-ri-bookmark-line class="size-4" />
<span>IP Aliases</span>
</div>
<div x-show="activeTab === 'aliases'" class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"></div>
</button>
<button @click="activeTab = 'ipsets'"
class="px-6 py-4 text-sm font-semibold transition-all relative whitespace-nowrap"
:class="activeTab === 'ipsets' ? 'text-primary' : 'text-base/60 hover:text-base'">
<div class="flex items-center gap-2">
<x-ri-folder-3-line class="size-4" />
<span>IP Sets</span>
</div>
<div x-show="activeTab === 'ipsets'" class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"></div>
</button>
<button @click="activeTab = 'logs'"
class="px-6 py-4 text-sm font-semibold transition-all relative whitespace-nowrap"
:class="activeTab === 'logs' ? 'text-primary' : 'text-base/60 hover:text-base'">
<div class="flex items-center gap-2">
<x-ri-file-list-3-line class="size-4" />
<span>Firewall Logs</span>
<span class="px-2 py-0.5 bg-primary/10 text-primary rounded-full text-xs font-bold">{{ count($firewallLogs) }}</span>
</div>
<div x-show="activeTab === 'logs'" class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"></div>
</button>
<button @click="activeTab = 'saved'"
class="px-6 py-4 text-sm font-semibold transition-all relative whitespace-nowrap"
:class="activeTab === 'saved' ? 'text-primary' : 'text-base/60 hover:text-base'">
<div class="flex items-center gap-2">
<x-ri-save-3-line class="size-4" />
<span>Saved Configurations</span>
<span class="px-2 py-0.5 bg-primary/10 text-primary rounded-full text-xs font-bold">{{ count($savedConfigurations) }}</span>
</div>
<div x-show="activeTab === 'saved'" class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"></div>
</button>
</div>
{{-- Tab Contents --}}
<div class="p-6">
{{-- RULES TAB --}}
<div x-show="activeTab === 'rules'" x-transition>
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold">Firewall Rules</h3>
<button type="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 flex items-center gap-2">
<x-ri-add-line class="size-4" />
Add Rule
</button>
</div>
@if(count($rules) > 0)
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b border-neutral/20">
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Order</th>
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Pos</th>
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Action</th>
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Direction</th>
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Source</th>
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Destination</th>
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Protocol</th>
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Ports</th>
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Status</th>
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Actions</th>
</tr>
</thead>
<tbody>
@foreach($rules as $index => $rule)
<tr class="border-b border-neutral/10 hover:bg-background-secondary/30 transition-colors">
<td class="py-3 px-4 text-sm">
<div class="flex flex-col gap-1">
@if($index > 0)
<button type="button"
wire:click="reorderRules({{ $rule['pos'] }}, {{ $rules[$index - 1]['pos'] }})"
wire:key="move-up-{{ $index }}"
class="text-base/50 hover:text-primary transition-colors p-0.5 hover:bg-primary/10 rounded"
title="Move up">
<x-ri-arrow-up-s-line class="size-4" />
</button>
@else
<div class="size-4 p-0.5"></div>
@endif
@if($index < count($rules) - 1)
<button type="button"
wire:click="reorderRules({{ $rule['pos'] }}, {{ $rules[$index + 1]['pos'] }})"
wire:key="move-down-{{ $index }}"
class="text-base/50 hover:text-primary transition-colors p-0.5 hover:bg-primary/10 rounded"
title="Move down">
<x-ri-arrow-down-s-line class="size-4" />
</button>
@else
<div class="size-4 p-0.5"></div>
@endif
</div>
</td>
<td class="py-3 px-4 text-sm font-mono">{{ $rule['pos'] ?? '-' }}</td>
<td class="py-3 px-4 text-sm">
@php
$actionClass = match($rule['action'] ?? '') {
'ACCEPT' => 'bg-success/20 text-success',
'REJECT' => 'bg-warning/20 text-warning',
'DROP' => 'bg-error/20 text-error',
default => 'bg-base/20 text-base/60'
};
@endphp
<span class="px-2 py-1 rounded text-xs font-medium {{ $actionClass }}">
{{ $rule['action'] ?? '-' }}
</span>
</td>
<td class="py-3 px-4 text-sm">
<span class="px-2 py-1 rounded text-xs font-medium bg-info/20 text-info">
{{ strtoupper($rule['type'] ?? 'IN') }}
</span>
</td>
<td class="py-3 px-4 text-sm font-mono text-xs">
<div class="flex items-center gap-1">
<span>{{ $rule['source'] ?? '-' }}</span>
@php $ruleSourceIP = $rule['source'] ?? ''; @endphp
@if($ruleSourceIP && $ruleSourceIP !== '-' && $this->checkIsPublicIP($ruleSourceIP))
<div class="relative" x-data="{ open: false, loading: false, ipData: null }" x-cloak>
<button type="button" @click.stop="
if (!ipData && !loading) {
loading = true;
open = true;
window.fetchIPLocation('{{ $ruleSourceIP }}').then(data => {
ipData = data;
loading = false;
}).catch(err => {
ipData = { status: 'error', message: 'Failed to load' };
loading = false;
});
} else {
open = true;
}
" class="text-base/50 hover:text-base/70 transition-colors" title="View IP Location">
<x-ri-map-pin-line class="size-3" />
</button>
{{-- IP Location Modal (same as logs) --}}
<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-[60] 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">
<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">
<div class="flex items-center gap-2 mb-4">
<x-ri-map-pin-line class="size-5 text-primary" />
<h3 class="text-lg font-semibold">IP Address: <span class="font-mono">{{ $ruleSourceIP }}</span></h3>
</div>
<div x-show="loading" class="text-center py-8">
<div class="fw-loader size-6 mx-auto mb-2 text-primary"></div>
<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">
<div><span class="text-xs text-base/60">Country:</span> <span class="font-semibold text-sm" x-text="ipData.country"></span></div>
<div><span class="text-xs text-base/60">Region:</span> <span class="font-semibold text-sm" x-text="ipData.regionName + ' (' + ipData.region + ')'"></span></div>
<div><span class="text-xs text-base/60">City:</span> <span class="font-semibold text-sm" x-text="ipData.city"></span></div>
<div><span class="text-xs text-base/60">ZIP:</span> <span class="font-semibold text-sm" x-text="ipData.zip"></span></div>
<div><span class="text-xs text-base/60">Timezone:</span> <span class="font-semibold text-sm" x-text="ipData.timezone"></span></div>
</div>
<div class="space-y-2">
<div><span class="text-xs text-base/60">ISP:</span> <span class="font-semibold text-sm" x-text="ipData.isp"></span></div>
<div><span class="text-xs text-base/60">Organization:</span> <span class="font-semibold text-sm" x-text="ipData.org"></span></div>
<div><span class="text-xs text-base/60">AS:</span> <span class="font-semibold text-sm" x-text="ipData.as"></span></div>
<div><span class="text-xs text-base/60">Coordinates:</span> <span class="font-semibold text-sm font-mono" x-text="ipData.lat + ', ' + ipData.lon"></span></div>
</div>
</div>
<div x-show="ipData.lat && ipData.lon" class="mt-4">
<p class="text-xs text-base/60 mb-2">Location Map:</p>
<div class="rounded-lg border border-neutral/20 overflow-hidden" style="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>
@endif
</div>
</td>
<td class="py-3 px-4 text-sm font-mono text-xs">
<div class="flex items-center gap-1">
<span>{{ $rule['dest'] ?? '-' }}</span>
@php $ruleDestIP = $rule['dest'] ?? ''; @endphp
@if($ruleDestIP && $ruleDestIP !== '-' && $this->checkIsPublicIP($ruleDestIP))
<div class="relative" x-data="{ open: false, loading: false, ipData: null }" x-cloak>
<button type="button" @click.stop="
if (!ipData && !loading) {
loading = true;
open = true;
window.fetchIPLocation('{{ $ruleDestIP }}').then(data => {
ipData = data;
loading = false;
}).catch(err => {
ipData = { status: 'error', message: 'Failed to load' };
loading = false;
});
} else {
open = true;
}
" class="text-base/50 hover:text-base/70 transition-colors" title="View IP Location">
<x-ri-map-pin-line class="size-3" />
</button>
{{-- IP Location Modal (same structure) --}}
<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-[60] 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">
<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">
<div class="flex items-center gap-2 mb-4">
<x-ri-map-pin-line class="size-5 text-primary" />
<h3 class="text-lg font-semibold">IP Address: <span class="font-mono">{{ $ruleDestIP }}</span></h3>
</div>
<div x-show="loading" class="text-center py-8">
<div class="fw-loader size-6 mx-auto mb-2 text-primary"></div>
<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">
<div><span class="text-xs text-base/60">Country:</span> <span class="font-semibold text-sm" x-text="ipData.country"></span></div>
<div><span class="text-xs text-base/60">Region:</span> <span class="font-semibold text-sm" x-text="ipData.regionName + ' (' + ipData.region + ')'"></span></div>
<div><span class="text-xs text-base/60">City:</span> <span class="font-semibold text-sm" x-text="ipData.city"></span></div>
<div><span class="text-xs text-base/60">ZIP:</span> <span class="font-semibold text-sm" x-text="ipData.zip"></span></div>
<div><span class="text-xs text-base/60">Timezone:</span> <span class="font-semibold text-sm" x-text="ipData.timezone"></span></div>
</div>
<div class="space-y-2">
<div><span class="text-xs text-base/60">ISP:</span> <span class="font-semibold text-sm" x-text="ipData.isp"></span></div>
<div><span class="text-xs text-base/60">Organization:</span> <span class="font-semibold text-sm" x-text="ipData.org"></span></div>
<div><span class="text-xs text-base/60">AS:</span> <span class="font-semibold text-sm" x-text="ipData.as"></span></div>
<div><span class="text-xs text-base/60">Coordinates:</span> <span class="font-semibold text-sm font-mono" x-text="ipData.lat + ', ' + ipData.lon"></span></div>
</div>
</div>
<div x-show="ipData.lat && ipData.lon" class="mt-4">
<p class="text-xs text-base/60 mb-2">Location Map:</p>
<div class="rounded-lg border border-neutral/20 overflow-hidden" style="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>
@endif
</div>
</td>
<td class="py-3 px-4 text-sm">{{ strtoupper($rule['proto'] ?? ($rule['macro'] ?? 'ANY')) }}</td>
<td class="py-3 px-4 text-sm font-mono text-xs">{{ $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 type="button" wire:click="toggleRule({{ $index }})" wire:key="toggle-{{ $index }}" class="text-warning hover:text-warning/80 transition-colors" title="Toggle 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 type="button" wire:click="startEditRule({{ $index }})" wire:key="edit-{{ $index }}" class="text-primary hover:text-primary/80 transition-colors" title="Edit rule">
<x-ri-edit-line class="size-4" />
</button>
<button type="button" wire:click="confirmDeleteRule({{ $index }})" wire:key="delete-{{ $index }}" 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-12 text-base/60">
<x-ri-shield-line class="size-16 mx-auto mb-4 opacity-30" />
<h3 class="text-lg font-semibold mb-2">No Firewall Rules</h3>
<p class="text-sm mb-4">Create your first rule to control network traffic</p>
<button type="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" />
Create Rule
</button>
</div>
@endif
</div>
{{-- ALIASES TAB --}}
<div x-show="activeTab === 'aliases'" x-transition>
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-lg font-semibold">IP Aliases</h3>
<p class="text-sm text-base/60">Create named references to IP addresses or CIDR ranges</p>
</div>
<button type="button" wire:click="startCreateAlias" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium flex items-center gap-2">
<x-ri-add-line class="size-4" />
Add Alias
</button>
</div>
@if(count($aliases) > 0)
<div class="grid gap-3">
@foreach($aliases as $alias)
<div class="p-4 bg-background-secondary/50 border border-neutral/20 rounded-lg hover:border-neutral/40 transition-colors">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="font-semibold text-sm mb-1">{{ $alias['name'] }}</div>
<div class="text-xs font-mono text-base/70 mb-2">{{ $alias['cidr'] }}</div>
@if(!empty($alias['comment']))
<div class="text-xs text-base/60">{{ $alias['comment'] }}</div>
@endif
</div>
<div class="flex gap-2">
<button type="button" wire:click="startEditAlias('{{ $alias['name'] }}')" class="text-primary hover:text-primary/80 transition-colors" title="Edit alias">
<x-ri-edit-line class="size-4" />
</button>
<button type="button" wire:click="confirmDeleteAlias('{{ $alias['name'] }}')" class="text-error hover:text-error/80 transition-colors" title="Delete alias">
<x-ri-delete-bin-line class="size-4" />
</button>
</div>
</div>
</div>
@endforeach
</div>
@else
<div class="text-center py-12 text-base/60">
<x-ri-bookmark-line class="size-16 mx-auto mb-4 opacity-30" />
<h3 class="text-lg font-semibold mb-2">No IP Aliases</h3>
<p class="text-sm mb-4">Create aliases to simplify rule management</p>
<button type="button" wire:click="startCreateAlias" 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" />
Create Alias
</button>
</div>
@endif
</div>
{{-- IPSETS TAB --}}
<div x-show="activeTab === 'ipsets'" x-transition>
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-lg font-semibold">IP Sets</h3>
<p class="text-sm text-base/60">Group multiple IPs/CIDRs for easier rule management</p>
</div>
<button type="button" wire:click="startCreateIPSet" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium flex items-center gap-2">
<x-ri-add-line class="size-4" />
Create IP Set
</button>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
{{-- IPSets List --}}
<div>
<h4 class="text-sm font-semibold text-base/70 uppercase tracking-wide mb-3">Available IP Sets</h4>
@if(count($ipsets) > 0)
<div class="space-y-2">
@foreach($ipsets as $ipset)
<div class="p-3 bg-background-secondary/50 border border-neutral/20 rounded-lg hover:border-primary/40 transition-colors cursor-pointer"
wire:click="selectIPSet('{{ $ipset['name'] }}')"
class="{{ $selectedIPSetName === $ipset['name'] ? 'border-primary/60 bg-primary/5' : '' }}">
<div class="flex items-center justify-between">
<div class="flex-1">
<div class="font-semibold text-sm">{{ $ipset['name'] }}</div>
@if(!empty($ipset['comment']))
<div class="text-xs text-base/60 mt-1">{{ $ipset['comment'] }}</div>
@endif
</div>
<div class="flex gap-2">
<button type="button" wire:click.stop="confirmDeleteIPSet('{{ $ipset['name'] }}')" class="text-error hover:text-error/80 transition-colors" title="Delete IPSet">
<x-ri-delete-bin-line class="size-4" />
</button>
</div>
</div>
</div>
@endforeach
</div>
@else
<div class="text-center py-8 text-base/60 bg-background-secondary/30 rounded-lg border border-dashed border-neutral/30">
<x-ri-folder-3-line class="size-12 mx-auto mb-3 opacity-30" />
<p class="text-sm">No IP Sets created yet</p>
</div>
@endif
</div>
{{-- IPSet Members --}}
<div>
@if($selectedIPSetName)
<div class="flex items-center justify-between mb-3">
<h4 class="text-sm font-semibold text-base/70 uppercase tracking-wide">Members of: <span class="text-primary">{{ $selectedIPSetName }}</span></h4>
<button type="button" wire:click="startAddIPSetMember" class="px-3 py-1.5 bg-success text-white rounded-lg hover:bg-success/90 transition-colors text-xs font-medium flex items-center gap-1">
<x-ri-add-line class="size-3" />
Add IP/CIDR
</button>
</div>
@if(count($ipsetMembers) > 0)
<div class="space-y-2 max-h-96 overflow-y-auto">
@foreach($ipsetMembers as $member)
<div class="p-3 bg-background border border-neutral/20 rounded-lg">
<div class="flex items-center justify-between">
<div class="flex-1">
<div class="font-mono text-sm font-semibold">{{ $member['cidr'] }}</div>
@if(!empty($member['comment']))
<div class="text-xs text-base/60 mt-1">{{ $member['comment'] }}</div>
@endif
@if(isset($member['nomatch']) && $member['nomatch'])
<span class="inline-block mt-1 px-2 py-0.5 bg-warning/20 text-warning text-xs font-medium rounded">No Match</span>
@endif
</div>
<button type="button" wire:click="confirmDeleteIPSetMember('{{ $member['cidr'] }}')" class="text-error hover:text-error/80 transition-colors" title="Remove">
<x-ri-close-circle-line class="size-4" />
</button>
</div>
</div>
@endforeach
</div>
@else
<div class="text-center py-8 text-base/60 bg-background-secondary/30 rounded-lg border border-dashed border-neutral/30">
<x-ri-inbox-line class="size-10 mx-auto mb-2 opacity-30" />
<p class="text-sm">No members in this IPSet</p>
<button type="button" wire:click="startAddIPSetMember" class="mt-3 px-3 py-1.5 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-xs font-medium">
Add First Member
</button>
</div>
@endif
@else
<div class="text-center py-12 text-base/60">
<x-ri-arrow-left-line class="size-12 mx-auto mb-3 opacity-30" />
<p class="text-sm">Select an IP Set to view its members</p>
</div>
@endif
</div>
</div>
</div>
{{-- LOGS TAB --}}
<div x-show="activeTab === 'logs'"
x-transition
wire:poll.30s="loadFirewallLogs"
x-init="$watch('activeTab', value => { if (value === 'logs' && !@json($logsLoaded)) { $wire.loadFirewallLogs(); } })">
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-lg font-semibold">Firewall Logs</h3>
<div class="flex items-center gap-3 mt-1">
<p class="text-xs text-base/60">Showing the latest logs first (most recent at top)</p>
@if($lastLogRefresh)
<span class="text-xs text-success/80">• Last updated: {{ $lastLogRefresh }}</span>
@endif
<span class="text-xs text-info/80">• Auto-refreshes every 30s</span>
</div>
</div>
<div class="flex items-center gap-3">
<button type="button"
x-data
@click="
const logs = @js($firewallLogs);
if (!logs || logs.length === 0) {
alert('No logs to export');
return;
}
const headers = ['Timestamp', 'Action', 'Source', 'Destination', 'Protocol', 'Source Port', 'Destination Port', 'Chain', 'Interface'];
const rows = logs.map(log => [
log.timestamp_display || log.timestamp || '',
log.action || '',
log.source || '',
log.destination || '',
log.protocol || '',
log.source_port || '',
log.destination_port || '',
log.chain || '',
log.interface || ''
]);
let csvContent = headers.join(',') + '\n';
const dq = String.fromCharCode(34);
rows.forEach(row => {
csvContent += row.map(cell => {
const str = String(cell);
if (str.indexOf(',') !== -1 || str.indexOf(dq) !== -1 || str.indexOf('\n') !== -1) {
return dq + str.replace(new RegExp(dq, 'g'), dq + dq) + dq;
}
return str;
}).join(',') + '\n';
});
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const timestamp = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-');
link.href = URL.createObjectURL(blob);
link.download = 'firewall-logs-' + timestamp + '.csv';
link.click();
URL.revokeObjectURL(link.href);
"
class="px-4 py-2 bg-secondary text-white rounded-lg hover:bg-secondary/90 transition-colors text-sm font-medium flex items-center gap-2">
<x-ri-download-line class="size-4" />
Export CSV
</button>
<button type="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 flex items-center gap-2">
<span wire:loading.remove wire:target="loadFirewallLogs">
<x-ri-refresh-line class="size-4" />
</span>
<div wire:loading wire:target="loadFirewallLogs" class="fw-loader-sm"></div>
<span wire:loading.remove wire:target="loadFirewallLogs">Refresh</span>
<span wire:loading wire:target="loadFirewallLogs">Loading...</span>
</button>
</div>
</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" />
<p class="text-sm text-warning/90">{{ $logError }}</p>
</div>
</div>
@endif
@if(!$logsLoaded && count($firewallLogs) === 0)
<div class="text-center py-12 text-base/60">
<div class="fw-loader-xl text-primary mx-auto mb-4"></div>
<p class="text-sm">Loading firewall logs...</p>
</div>
@elseif(count($firewallLogs) > 0)
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b border-neutral/20">
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Time</th>
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Action</th>
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Source</th>
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Destination</th>
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Protocol</th>
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Src Port</th>
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Dest Port</th>
<th class="text-center py-3 px-4 text-xs font-semibold text-base/70 uppercase">Analysis</th>
</tr>
</thead>
<tbody>
@foreach($this->paginatedLogs as $log)
<tr class="border-b border-neutral/10 hover:bg-background-secondary/30 transition-colors">
<td class="py-3 px-4 text-xs text-base/70">
@if(isset($log['timestamp_iso']) && $log['timestamp_iso'] !== '-')
<div x-data="{
timestamp: '{{ $log['timestamp_iso'] }}',
formatTime() {
try {
const date = new Date(this.timestamp);
if (isNaN(date.getTime())) return '{{ $log['timestamp'] }}';
return date.toLocaleString('en-US', {
month: 'short',
day: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
} catch(e) {
return '{{ $log['timestamp'] }}';
}
},
getRelativeTime() {
try {
const date = new Date(this.timestamp);
if (isNaN(date.getTime())) return '';
const now = new Date();
const diffMs = now - date;
const diffSec = Math.floor(diffMs / 1000);
const diffMin = Math.floor(diffSec / 60);
const diffHour = Math.floor(diffMin / 60);
const diffDay = Math.floor(diffHour / 24);
if (diffSec < 60) return diffSec + ' seconds ago';
if (diffMin < 60) return diffMin + ' minute' + (diffMin !== 1 ? 's' : '') + ' ago';
if (diffHour < 24) return diffHour + ' hour' + (diffHour !== 1 ? 's' : '') + ' ago';
return diffDay + ' day' + (diffDay !== 1 ? 's' : '') + ' ago';
} catch(e) {
return '';
}
}
}"
class="cursor-help"
:title="getRelativeTime()"
x-text="formatTime()"></div>
@else
{{ $log['timestamp_display'] ?? $log['timestamp'] ?? '-' }}
@endif
</td>
<td class="py-3 px-4 text-sm">
<span class="px-2 py-1 rounded text-xs font-medium {{ $log['level_badge_class'] ?? '' }}">
{{ $log['action'] ?? '-' }}
</span>
</td>
<td class="py-3 px-4 text-xs font-mono">
<div class="flex items-center gap-1">
<span>{{ $log['source'] ?? '-' }}</span>
@php $sourceIP = $log['source'] ?? ''; @endphp
@if($sourceIP && $sourceIP !== '-' && $this->checkIsPublicIP($sourceIP))
<div class="relative" x-data="{ open: false, loading: false, ipData: null }" x-cloak>
<button type="button" @click.stop="
if (!ipData && !loading) {
loading = true;
open = true;
window.fetchIPLocation('{{ $sourceIP }}').then(data => {
ipData = data;
loading = false;
}).catch(err => {
ipData = { status: 'error', message: 'Failed to load' };
loading = false;
});
} else {
open = true;
}
" class="text-base/50 hover:text-base/70 transition-colors" title="View IP Location">
<x-ri-map-pin-line class="size-3" />
</button>
{{-- IP Location Modal --}}
<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-[60] 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">
<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">
<div class="flex items-center gap-2 mb-4">
<x-ri-map-pin-line class="size-5 text-primary" />
<h3 class="text-lg font-semibold">IP Address: <span class="font-mono">{{ $sourceIP }}</span></h3>
</div>
<div x-show="loading" class="text-center py-8">
<div class="fw-loader text-primary mx-auto mb-2"></div>
<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">
<div><span class="text-xs text-base/60">Country:</span> <span class="font-semibold text-sm" x-text="ipData.country"></span></div>
<div><span class="text-xs text-base/60">Region:</span> <span class="font-semibold text-sm" x-text="ipData.regionName + ' (' + ipData.region + ')'"></span></div>
<div><span class="text-xs text-base/60">City:</span> <span class="font-semibold text-sm" x-text="ipData.city"></span></div>
<div><span class="text-xs text-base/60">ZIP:</span> <span class="font-semibold text-sm" x-text="ipData.zip"></span></div>
<div><span class="text-xs text-base/60">Timezone:</span> <span class="font-semibold text-sm" x-text="ipData.timezone"></span></div>
</div>
<div class="space-y-2">
<div><span class="text-xs text-base/60">ISP:</span> <span class="font-semibold text-sm" x-text="ipData.isp"></span></div>
<div><span class="text-xs text-base/60">Organization:</span> <span class="font-semibold text-sm" x-text="ipData.org"></span></div>
<div><span class="text-xs text-base/60">AS:</span> <span class="font-semibold text-sm" x-text="ipData.as"></span></div>
<div><span class="text-xs text-base/60">Coordinates:</span> <span class="font-semibold text-sm font-mono" x-text="ipData.lat + ', ' + ipData.lon"></span></div>
</div>
</div>
<div x-show="ipData.lat && ipData.lon" class="mt-4">
<p class="text-xs text-base/60 mb-2">Location Map:</p>
<div class="rounded-lg border border-neutral/20 overflow-hidden" style="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>
@endif
</div>
</td>
<td class="py-3 px-4 text-xs font-mono">
<div class="flex items-center gap-1">
<span>{{ $log['destination'] ?? '-' }}</span>
@php $destIP = $log['destination'] ?? ''; @endphp
@if($destIP && $destIP !== '-' && $this->checkIsPublicIP($destIP))
<div class="relative" x-data="{ open: false, loading: false, ipData: null }" x-cloak>
<button type="button" @click.stop="
if (!ipData && !loading) {
loading = true;
open = true;
window.fetchIPLocation('{{ $destIP }}').then(data => {
ipData = data;
loading = false;
}).catch(err => {
ipData = { status: 'error', message: 'Failed to load' };
loading = false;
});
} else {
open = true;
}
" class="text-base/50 hover:text-base/70 transition-colors" title="View IP Location">
<x-ri-map-pin-line class="size-3" />
</button>
{{-- IP Location Modal --}}
<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-[60] 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">
<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">
<div class="flex items-center gap-2 mb-4">
<x-ri-map-pin-line class="size-5 text-primary" />
<h3 class="text-lg font-semibold">IP Address: <span class="font-mono">{{ $destIP }}</span></h3>
</div>
<div x-show="loading" class="text-center py-8">
<div class="fw-loader text-primary mx-auto mb-2"></div>
<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">
<div><span class="text-xs text-base/60">Country:</span> <span class="font-semibold text-sm" x-text="ipData.country"></span></div>
<div><span class="text-xs text-base/60">Region:</span> <span class="font-semibold text-sm" x-text="ipData.regionName + ' (' + ipData.region + ')'"></span></div>
<div><span class="text-xs text-base/60">City:</span> <span class="font-semibold text-sm" x-text="ipData.city"></span></div>
<div><span class="text-xs text-base/60">ZIP:</span> <span class="font-semibold text-sm" x-text="ipData.zip"></span></div>
<div><span class="text-xs text-base/60">Timezone:</span> <span class="font-semibold text-sm" x-text="ipData.timezone"></span></div>
</div>
<div class="space-y-2">
<div><span class="text-xs text-base/60">ISP:</span> <span class="font-semibold text-sm" x-text="ipData.isp"></span></div>
<div><span class="text-xs text-base/60">Organization:</span> <span class="font-semibold text-sm" x-text="ipData.org"></span></div>
<div><span class="text-xs text-base/60">AS:</span> <span class="font-semibold text-sm" x-text="ipData.as"></span></div>
<div><span class="text-xs text-base/60">Coordinates:</span> <span class="font-semibold text-sm font-mono" x-text="ipData.lat + ', ' + ipData.lon"></span></div>
</div>
</div>
<div x-show="ipData.lat && ipData.lon" class="mt-4">
<p class="text-xs text-base/60 mb-2">Location Map:</p>
<div class="rounded-lg border border-neutral/20 overflow-hidden" style="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>
@endif
</div>
</td>
<td class="py-3 px-4 text-sm">{{ $log['protocol'] ?? '-' }}</td>
<td class="py-3 px-4 text-xs font-mono">{{ $log['source_port'] ?? '-' }}</td>
<td class="py-3 px-4 text-xs font-mono">{{ $log['destination_port'] ?? '-' }}</td>
<td class="py-3 px-4 text-center">
@php
$sourceIP = $log['source'] ?? '';
$destIP = $log['destination'] ?? '';
$sourcePort = $log['source_port'] ?? '';
$destPort = $log['destination_port'] ?? '';
$protocol = $log['protocol'] ?? '';
@endphp
@if($coreRouterConfigured && $sourceIP && $sourceIP !== '-')
<div class="relative"
x-data="{
open: false,
loading: false,
connectionData: null,
error: null,
sourceIP: '{{ addslashes($sourceIP) }}',
destIP: '{{ addslashes($destIP) }}',
sourcePort: '{{ addslashes($sourcePort) }}',
destPort: '{{ addslashes($destPort) }}',
protocol: '{{ addslashes($protocol) }}',
loadConnectionData() {
this.loading = true;
this.error = null;
console.log('Loading connection data for:', this.sourceIP, '->', this.destIP, 'Port:', this.destPort, 'Protocol:', this.protocol);
$wire.getNetworkAnalysisForLog(this.sourceIP, this.destIP, this.sourcePort, this.destPort, this.protocol)
.then(data => {
console.log('Received connection data:', data);
this.connectionData = data;
this.loading = false;
})
.catch(err => {
console.error('Failed to load connection data:', err);
this.error = 'Failed to load connection data';
this.loading = false;
});
},
exportToCsv() {
if (!this.connectionData || !this.connectionData.connections || this.connectionData.connections.length === 0) {
return;
}
const headers = ['Protocol', 'Source IP', 'Source Port', 'Destination IP', 'Destination Port', 'Direction', 'State', 'Interface', 'Bytes', 'Packets'];
const rows = this.connectionData.connections.map(conn => [
conn.protocol || '',
conn.source || '',
conn.source_port || '',
conn.destination || '',
conn.destination_port || '',
conn.direction || '',
conn.state || '',
conn.interface || '',
conn.bytes || 0,
conn.packets || 0
]);
let csvContent = headers.join(',') + '\n';
rows.forEach(row => {
csvContent += row.map(cell => {
const str = String(cell);
const dq = String.fromCharCode(34);
if (str.indexOf(',') !== -1 || str.indexOf(dq) !== -1 || str.indexOf('\n') !== -1) {
return dq + str.replace(new RegExp(dq, 'g'), dq + dq) + dq;
}
return str;
}).join(',') + '\n';
});
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const timestamp = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-');
link.href = URL.createObjectURL(blob);
link.download = 'network-analysis-' + this.sourceIP + '-' + timestamp + '.csv';
link.click();
URL.revokeObjectURL(link.href);
}
}"
x-cloak>
<button type="button"
@click="open = true; if (!connectionData && !loading) { loadConnectionData(); }"
class="text-primary hover:text-primary/80 transition-colors p-1 rounded hover:bg-primary/10"
title="View Network Analysis">
<x-ri-line-chart-line class="size-4" />
</button>
{{-- Network Analysis 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-[60] 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-5xl bg-gradient-to-br from-background via-background-secondary to-background rounded-2xl border border-primary/20 shadow-2xl max-h-[90vh] overflow-hidden">
<div class="absolute top-4 right-4 flex items-center gap-2 z-10">
<button @click="loadConnectionData()"
:disabled="loading"
class="w-9 h-9 flex items-center justify-center text-base/50 hover:text-primary transition-colors bg-background/80 backdrop-blur-sm rounded-full hover:bg-background disabled:opacity-50 disabled:cursor-not-allowed"
title="Refresh data">
<x-ri-refresh-line x-show="!loading" class="size-5" />
<div x-show="loading" x-cloak class="fw-loader-sm text-primary"></div>
</button>
<button @click="open = false"
class="w-9 h-9 flex items-center justify-center text-base/50 hover:text-base transition-colors bg-background/80 backdrop-blur-sm rounded-full hover:bg-background">
<x-ri-close-line class="size-5" />
</button>
</div>
{{-- Modal Header --}}
<div class="bg-gradient-to-r from-primary/10 to-secondary/10 border-b border-primary/20 p-6">
<div class="flex items-center gap-3">
<div class="w-12 h-12 rounded-xl bg-primary/20 flex items-center justify-center">
<x-ri-line-chart-line class="size-6 text-primary" />
</div>
<div>
<h3 class="text-xl font-bold text-base">Network Connection Analysis</h3>
<p class="text-sm text-base/60 mt-1">Detailed connection information from firewall state</p>
</div>
</div>
</div>
{{-- Modal Content --}}
<div class="p-6 overflow-y-auto" style="max-height: calc(90vh - 180px);">
{{-- Loading State --}}
<div x-show="loading && !connectionData" class="text-center py-12">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 mb-4">
<div class="fw-loader-lg text-primary"></div>
</div>
<p class="text-sm text-base/60">Fetching connection data...</p>
</div>
{{-- Error State --}}
<div x-show="error && !loading && !connectionData" class="text-center py-12">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-warning/10 mb-4">
<x-ri-error-warning-line class="size-8 text-warning" />
</div>
<p class="text-sm text-warning" x-text="error"></p>
</div>
{{-- Connection Data Display --}}
<div x-show="connectionData && !loading">
<template x-if="connectionData && connectionData.connections && connectionData.connections.length > 0">
<div>
<template x-for="(conn, index) in connectionData.connections" :key="index">
<div class="mb-6 last:mb-0 bg-background/50 border border-neutral/40 rounded-xl p-5 hover:border-primary/30 transition-colors">
{{-- Connection Header --}}
<div class="flex items-center justify-between mb-4 pb-3 border-b border-neutral/20">
<div class="flex items-center gap-2">
<span class="px-3 py-1 rounded-lg text-xs font-semibold bg-primary/20 text-primary border border-primary/30" x-text="'Connection #' + (index + 1)"></span>
<span class="px-2 py-1 rounded text-xs font-medium bg-secondary/20 text-secondary" x-text="conn.protocol || 'N/A'"></span>
</div>
<div class="flex items-center gap-2">
<span :class="{
'bg-success/20 text-success border-success/30': conn.direction === 'Inbound',
'bg-info/20 text-info border-info/30': conn.direction === 'Outbound'
}" class="px-2 py-1 rounded text-xs font-medium border" x-text="conn.direction || 'N/A'"></span>
</div>
</div>
{{-- Connection Details Grid --}}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{{-- Source Information --}}
<div class="space-y-3">
<h4 class="text-sm font-semibold text-success flex items-center gap-2">
<x-ri-arrow-right-line class="size-4" />
Source
</h4>
<div class="pl-6 space-y-2">
<div class="flex items-center justify-between py-1.5 px-3 bg-background rounded-lg">
<span class="text-xs text-base/60">IP Address</span>
<span class="text-sm font-mono font-semibold" x-text="conn.source || 'N/A'"></span>
</div>
<div class="flex items-center justify-between py-1.5 px-3 bg-background rounded-lg">
<span class="text-xs text-base/60">Port</span>
<span class="text-sm font-mono font-semibold" x-text="conn.source_port || 'N/A'"></span>
</div>
</div>
</div>
{{-- Destination Information --}}
<div class="space-y-3">
<h4 class="text-sm font-semibold text-error flex items-center gap-2">
<x-ri-arrow-left-line class="size-4" />
Destination
</h4>
<div class="pl-6 space-y-2">
<div class="flex items-center justify-between py-1.5 px-3 bg-background rounded-lg">
<span class="text-xs text-base/60">IP Address</span>
<span class="text-sm font-mono font-semibold" x-text="conn.destination || 'N/A'"></span>
</div>
<div class="flex items-center justify-between py-1.5 px-3 bg-background rounded-lg">
<span class="text-xs text-base/60">Port</span>
<span class="text-sm font-mono font-semibold" x-text="conn.destination_port || 'N/A'"></span>
</div>
</div>
</div>
</div>
{{-- Connection Statistics --}}
<div class="mt-4 pt-4 border-t border-neutral/20">
<h4 class="text-sm font-semibold mb-3 flex items-center gap-2">
<x-ri-bar-chart-line class="size-4 text-primary" />
Traffic Statistics
</h4>
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div class="bg-gradient-to-br from-primary/10 to-primary/5 border border-primary/20 rounded-lg p-3">
<p class="text-xs text-base/60 mb-1">Data Transferred</p>
<p class="text-lg font-bold text-primary" x-text="conn.bytes_human || '0 B'"></p>
</div>
<div class="bg-gradient-to-br from-secondary/10 to-secondary/5 border border-secondary/20 rounded-lg p-3">
<p class="text-xs text-base/60 mb-1">Packets</p>
<p class="text-lg font-bold text-secondary" x-text="conn.packets || '0'"></p>
</div>
<div class="bg-gradient-to-br from-info/10 to-info/5 border border-info/20 rounded-lg p-3">
<p class="text-xs text-base/60 mb-1">State</p>
<p class="text-sm font-bold text-info" x-text="conn.state || 'UNKNOWN'"></p>
</div>
<div class="bg-gradient-to-br from-success/10 to-success/5 border border-success/20 rounded-lg p-3">
<p class="text-xs text-base/60 mb-1">Interface</p>
<p class="text-sm font-bold text-success" x-text="conn.interface || 'N/A'"></p>
</div>
</div>
</div>
</div>
</template>
</div>
</template>
{{-- No Connections Found --}}
<template x-if="connectionData && (!connectionData.connections || connectionData.connections.length === 0)">
<div class="text-center py-12">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-base/10 mb-4">
<x-ri-information-line class="size-8 text-base/60" />
</div>
<p class="text-sm text-base/60">No matching network analysis data found for this log entry.</p>
<p class="text-xs text-base/50 mt-2">The connection may have already closed or network analysis data may not be available.</p>
</div>
</template>
</div>
</div>
{{-- Modal Footer --}}
<div class="border-t border-neutral/20 bg-background/50 p-4">
<div class="flex items-center justify-between">
<p class="text-xs text-base/50">Data from Core Router network state tracking</p>
<div class="flex items-center gap-2">
<button @click="exportToCsv()"
x-show="connectionData && connectionData.connections && connectionData.connections.length > 0"
class="px-4 py-2 bg-secondary text-white rounded-lg hover:bg-secondary/90 transition-colors text-sm font-medium flex items-center gap-2">
<x-ri-download-line class="size-4" />
Export CSV
</button>
<button @click="open = false"
class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium">
Close
</button>
</div>
</div>
</div>
</div>
</div>
</div>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
{{-- Pagination Controls --}}
<div class="mt-6 flex items-center justify-between border-t border-neutral/20 pt-4">
{{-- Records per page selector --}}
<div class="flex items-center gap-3">
<label class="text-sm text-base/70">Show:</label>
<select wire:model.live="perPage" class="px-3 py-1.5 border border-neutral rounded-lg bg-background text-sm">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="250">250</option>
</select>
<span class="text-sm text-base/60">per page</span>
</div>
{{-- Page info and navigation --}}
<div class="flex items-center gap-4">
<span class="text-sm text-base/60">
Showing {{ (($currentPage - 1) * $perPage) + 1 }} - {{ min($currentPage * $perPage, count($firewallLogs)) }} of {{ count($firewallLogs) }} logs
</span>
<div class="flex items-center gap-1">
{{-- First page --}}
<button type="button"
wire:click="goToPage(1)"
@if($currentPage === 1) disabled @endif
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="First page">
<x-ri-arrow-left-double-line class="size-4" />
</button>
{{-- Previous page --}}
<button type="button"
wire:click="previousPage"
@if($currentPage === 1) disabled @endif
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Previous page">
<x-ri-arrow-left-s-line class="size-4" />
</button>
{{-- Page numbers --}}
@php
$totalPages = $this->totalPages;
$startPage = max(1, $currentPage - 2);
$endPage = min($totalPages, $currentPage + 2);
@endphp
@if($startPage > 1)
<button type="button" wire:click="goToPage(1)" class="px-3 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors text-sm">1</button>
@if($startPage > 2)
<span class="px-2 text-base/60">...</span>
@endif
@endif
@for($i = $startPage; $i <= $endPage; $i++)
<button type="button"
wire:click="goToPage({{ $i }})"
class="px-3 py-1 border rounded text-sm transition-colors {{ $currentPage === $i ? 'bg-primary text-white border-primary' : 'border-neutral hover:bg-background-secondary' }}">
{{ $i }}
</button>
@endfor
@if($endPage < $totalPages)
@if($endPage < $totalPages - 1)
<span class="px-2 text-base/60">...</span>
@endif
<button type="button" wire:click="goToPage({{ $totalPages }})" class="px-3 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors text-sm">{{ $totalPages }}</button>
@endif
{{-- Next page --}}
<button type="button"
wire:click="nextPage"
@if($currentPage === $totalPages) disabled @endif
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Next page">
<x-ri-arrow-right-s-line class="size-4" />
</button>
{{-- Last page --}}
<button type="button"
wire:click="goToPage({{ $totalPages }})"
@if($currentPage === $totalPages) disabled @endif
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Last page">
<x-ri-arrow-right-double-line class="size-4" />
</button>
</div>
</div>
</div>
@else
<div class="text-center py-12 text-base/60">
<x-ri-file-list-3-line class="size-16 mx-auto mb-4 opacity-30" />
<h3 class="text-lg font-semibold mb-2">No Firewall Logs</h3>
<p class="text-sm">Logs will appear here when the firewall blocks or allows traffic</p>
</div>
@endif
</div>
{{-- SAVED CONFIGURATIONS TAB --}}
<div x-show="activeTab === 'saved'" x-transition>
<div class="flex items-center justify-between mb-6">
<div>
<h3 class="text-lg font-semibold">Saved Configurations</h3>
<p class="text-sm text-base/60 mt-1">Save and reuse firewall configurations across your VPS instances</p>
</div>
<div class="flex items-center gap-2">
{{-- Apply to All Button --}}
<button type="button"
wire:click="openApplyToAllModal"
@if(!$this->canApplyToAll()) disabled @endif
class="px-4 py-2 bg-success text-white rounded-lg transition-colors text-sm font-medium flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed {{ $this->canApplyToAll() ? 'hover:bg-success/90' : '' }}"
title="{{ !$this->currentConfigMatchesSaved() ? 'Save the current configuration first' : (count($this->getUserOtherServers()) === 0 ? 'No other servers to apply to' : 'Apply to all your other servers') }}">
<x-ri-global-line class="size-4" />
Apply to All
</button>
{{-- Save Current Configuration Button --}}
<button type="button"
wire:click="openSaveConfigModal"
@if($this->currentConfigMatchesSaved()) disabled @endif
class="px-4 py-2 bg-primary text-white rounded-lg transition-colors text-sm font-medium flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed {{ !$this->currentConfigMatchesSaved() ? 'hover:bg-primary/90' : '' }}"
title="{{ $this->currentConfigMatchesSaved() ? 'This configuration is already saved' : 'Save the current configuration' }}">
<x-ri-save-3-line class="size-4" />
Save Current Configuration
</button>
</div>
</div>
@if(count($savedConfigurations) > 0)
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b border-neutral/20">
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Configuration Name</th>
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Rules</th>
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Policies</th>
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Created</th>
<th class="text-center py-3 px-4 text-xs font-semibold text-base/70 uppercase">Actions</th>
</tr>
</thead>
<tbody>
@foreach($savedConfigurations as $config)
<tr class="border-b border-neutral/10 hover:bg-background-secondary/30 transition-colors">
<td class="py-3 px-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center">
<x-ri-shield-star-line class="size-4 text-primary" />
</div>
<span class="font-medium">{{ $config['configuration_name'] }}</span>
</div>
</td>
<td class="py-3 px-4">
<span class="px-2 py-1 bg-info/10 text-info rounded-full text-xs font-medium">
{{ is_array($config['firewall_rules']) ? count($config['firewall_rules']) : 0 }} rules
</span>
</td>
<td class="py-3 px-4">
<div class="flex items-center gap-2">
<span class="px-2 py-0.5 text-xs rounded {{ $config['incoming_policy'] === 'DROP' ? 'bg-error/10 text-error' : 'bg-success/10 text-success' }}">
IN: {{ $config['incoming_policy'] }}
</span>
<span class="px-2 py-0.5 text-xs rounded {{ $config['outgoing_policy'] === 'DROP' ? 'bg-error/10 text-error' : 'bg-success/10 text-success' }}">
OUT: {{ $config['outgoing_policy'] }}
</span>
</div>
</td>
<td class="py-3 px-4 text-sm text-base/70">
{{ \Carbon\Carbon::parse($config['created_at'])->format('M d, Y H:i') }}
</td>
<td class="py-3 px-4">
<div class="flex items-center justify-center gap-2">
<button type="button" wire:click="confirmApplyConfiguration({{ $config['id'] }})" class="p-2 text-success hover:bg-success/10 rounded-lg transition-colors" title="Apply Configuration">
<x-ri-play-circle-line class="size-5" />
</button>
<button type="button" wire:click="viewConfiguration({{ $config['id'] }})" class="p-2 text-info hover:bg-info/10 rounded-lg transition-colors" title="View Details">
<x-ri-eye-line class="size-5" />
</button>
<button type="button" wire:click="openRenameConfigModal({{ $config['id'] }})" class="p-2 text-warning hover:bg-warning/10 rounded-lg transition-colors" title="Rename Configuration">
<x-ri-edit-line class="size-5" />
</button>
<button type="button" wire:click="confirmDeleteConfiguration({{ $config['id'] }})" class="p-2 text-error hover:bg-error/10 rounded-lg transition-colors" title="Delete Configuration">
<x-ri-delete-bin-line class="size-5" />
</button>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
<div class="text-center py-12 text-base/60">
<x-ri-save-3-line class="size-16 mx-auto mb-4 opacity-30" />
<h3 class="text-lg font-semibold mb-2">No Saved Configurations</h3>
<p class="text-sm">Save your current firewall configuration to reuse it later or apply it to other VPS instances</p>
</div>
@endif
</div>
</div>
</div>
@else
<div class="bg-background border border-neutral rounded-xl p-8">
<div class="max-w-2xl mx-auto text-center">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-warning/10 border-2 border-warning/20 mb-4">
<x-ri-alert-line class="size-8 text-warning" />
</div>
<h3 class="text-xl font-semibold mb-2">Firewall Protection Disabled</h3>
<p class="text-base/60 mb-6 max-w-md mx-auto">Your virtual machine is currently unprotected. Enable the firewall above to secure your network traffic with customizable rules.</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-left">
<div class="p-4 bg-background-secondary/50 rounded-lg border border-neutral/20">
<div class="flex items-center gap-2 mb-2">
<x-ri-shield-check-line class="size-5 text-primary" />
<h4 class="font-semibold text-sm">Traffic Control</h4>
</div>
<p class="text-xs text-base/60">Define rules to allow or block specific connections</p>
</div>
<div class="p-4 bg-background-secondary/50 rounded-lg border border-neutral/20">
<div class="flex items-center gap-2 mb-2">
<x-ri-eye-line class="size-5 text-primary" />
<h4 class="font-semibold text-sm">Monitor Activity</h4>
</div>
<p class="text-xs text-base/60">Track and log all firewall events in real-time</p>
</div>
<div class="p-4 bg-background-secondary/50 rounded-lg border border-neutral/20">
<div class="flex items-center gap-2 mb-2">
<x-ri-settings-3-line class="size-5 text-primary" />
<h4 class="font-semibold text-sm">Flexible Policies</h4>
</div>
<p class="text-xs text-base/60">Configure default behavior for incoming and outgoing traffic</p>
</div>
</div>
</div>
</div>
@endif
{{-- Rule Form Modal --}}
@if($showRuleForm)
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4"
x-data="{
macroDefinitions: @js($this->macroDefinitions),
currentMacro: @entangle('ruleMacro'),
handleMacroChange() {
if (this.currentMacro && this.currentMacro !== '' && this.macroDefinitions[this.currentMacro]) {
const macroDef = this.macroDefinitions[this.currentMacro];
// Auto-populate protocol
if (macroDef.proto) {
$wire.set('ruleProto', macroDef.proto);
}
// Auto-populate destination port
if (macroDef.dport) {
$wire.set('ruleDport', macroDef.dport);
}
}
}
}"
x-init="$watch('currentMacro', () => handleMacroChange())">
<div class="relative w-full max-w-4xl bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl max-h-[90vh] overflow-y-auto">
<button wire:click="cancelRuleForm" 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">
<h3 class="text-xl font-semibold mb-6">{{ $editingRuleIndex !== null ? 'Edit' : 'Create' }} Firewall Rule</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{{-- Action --}}
<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 text-sm">
<option value="ACCEPT">Accept</option>
<option value="REJECT">Reject</option>
<option value="DROP">Drop</option>
</select>
</div>
{{-- Type --}}
<div>
<label class="block text-sm font-medium mb-2">Direction</label>
<select wire:model="ruleType" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background text-sm">
<option value="in">Incoming</option>
<option value="out">Outgoing</option>
</select>
</div>
{{-- Macro --}}
<div>
<label class="block text-sm font-medium mb-2">Macro (Pre-defined Service)</label>
<select wire:model="ruleMacro" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background text-sm">
@foreach($availableMacros as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</select>
<p class="text-xs text-base/60 mt-1">
<x-ri-lightbulb-line class="size-3 inline mr-0.5" />
Selecting a macro will automatically set the protocol and port(s)
</p>
</div>
{{-- Interface --}}
<div>
<label class="block text-sm font-medium mb-2">Network Interface</label>
<select wire:model="ruleIface" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background text-sm">
<option value="">Any</option>
@foreach($networkAdapters as $adapter)
<option value="{{ $adapter['name'] }}">{{ $adapter['name'] }} ({{ ucfirst($adapter['model'] ?? 'Unknown') }})</option>
@endforeach
</select>
</div>
{{-- Source --}}
<div>
<label class="block text-sm font-medium mb-2">Source IP/CIDR</label>
<input type="text" wire:model="ruleSource" placeholder="e.g., 192.168.1.0/24 or leave empty for any" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background text-sm">
<p class="text-xs text-base/60 mt-1">Can use alias names or leave empty for any source</p>
</div>
{{-- Destination --}}
<div>
<label class="block text-sm font-medium mb-2">Destination IP/CIDR</label>
<select wire:model="ruleDest" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background text-sm">
<option value="">Any</option>
@foreach($availableIps as $ipData)
<option value="{{ $ipData['ip'] }}">{{ $ipData['ip'] }} ({{ $ipData['label'] }})</option>
@endforeach
@foreach($aliases as $alias)
<option value="{{ $alias['name'] }}">{{ $alias['name'] }} (Alias)</option>
@endforeach
</select>
</div>
{{-- Protocol --}}
<div>
<label class="block text-sm font-medium mb-2">
Protocol
<span class="text-xs font-normal text-base/50">(auto-filled by macro)</span>
</label>
<select wire:model="ruleProto" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background text-sm">
<option value="">Any</option>
<option value="tcp">TCP</option>
<option value="udp">UDP</option>
<option value="icmp">ICMP</option>
<option value="igmp">IGMP</option>
<option value="ah">AH (IPsec Authentication Header)</option>
<option value="esp">ESP (IPsec Encapsulating Security Payload)</option>
<option value="ospf">OSPF</option>
</select>
</div>
{{-- Destination Port --}}
<div>
<label class="block text-sm font-medium mb-2">
Destination Port
<span class="text-xs font-normal text-base/50">(auto-filled by macro)</span>
</label>
<input type="text" wire:model="ruleDport" placeholder="e.g., 80, 443, 1000:2000" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background text-sm">
</div>
{{-- Source Port --}}
<div>
<label class="block text-sm font-medium mb-2">Source Port (Optional)</label>
<input type="text" wire:model="ruleSport" placeholder="e.g., 80, 443" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background text-sm">
</div>
{{-- Log Level --}}
<div>
<label class="block text-sm font-medium mb-2">Logging</label>
<select wire:model="ruleLog" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background text-sm">
<option value="nolog">No Logging</option>
<option value="emerg">Emergency</option>
<option value="alert">Alert</option>
<option value="crit">Critical</option>
<option value="err">Error</option>
<option value="warning">Warning</option>
<option value="notice">Notice</option>
<option value="info">Info</option>
<option value="debug">Debug</option>
</select>
</div>
{{-- Comment --}}
<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 text-sm">
</div>
{{-- Enable --}}
<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 font-medium">Enable this rule immediately</label>
</div>
</div>
<div class="flex gap-3 justify-end mt-6 pt-4 border-t border-neutral/20">
<button type="button" wire:click="cancelRuleForm" class="px-4 py-2 bg-background border border-neutral rounded-lg hover:bg-background-secondary transition-colors text-sm font-medium">
Cancel
</button>
<button type="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>
</div>
</div>
</div>
</div>
@endif
{{-- Delete Rule Modal --}}
@if($showDeleteRuleModal && $deletingRuleIndex !== null && isset($rules[$deletingRuleIndex]))
@php $ruleToDelete = $rules[$deletingRuleIndex]; @endphp
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="relative w-full max-w-lg bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
<button wire:click="cancelDeleteRule" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<div class="flex items-start gap-4 mb-4">
<div class="w-12 h-12 rounded-full bg-error/10 flex items-center justify-center 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">Are you sure you want to delete this rule? This action cannot be undone.</p>
<div class="p-3 bg-background border border-neutral/20 rounded-lg text-sm space-y-1">
<div><span class="text-base/60">Action:</span> <span class="font-semibold">{{ $ruleToDelete['action'] ?? '-' }}</span></div>
<div><span class="text-base/60">Source:</span> <span class="font-mono text-xs">{{ $ruleToDelete['source'] ?? 'Any' }}</span></div>
<div><span class="text-base/60">Destination:</span> <span class="font-mono text-xs">{{ $ruleToDelete['dest'] ?? 'Any' }}</span></div>
</div>
</div>
</div>
<div class="flex gap-3 justify-end">
<x-proxmox::ui.button wire:click="cancelDeleteRule" variant="secondary">
Cancel
</x-proxmox::ui.button>
<x-proxmox::ui.button wire:click="deleteRule" wire:loading.attr="disabled" variant="danger">
<span wire:loading.remove wire:target="deleteRule">Delete Rule</span>
<span wire:loading wire:target="deleteRule">Deleting...</span>
</x-proxmox::ui.button>
</div>
</div>
</div>
@endif
{{-- Alias Form Modal --}}
@if($showAliasForm)
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="relative w-full max-w-lg bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
<button wire:click="cancelAliasForm" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<h3 class="text-xl font-semibold mb-6">{{ $editingAliasName ? 'Edit' : 'Create' }} IP Alias</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-2">Alias Name</label>
<input type="text" wire:model="aliasName" placeholder="e.g., web-servers" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background text-sm" {{ $editingAliasName ? 'readonly' : '' }}>
@error('aliasName') <span class="text-error text-xs mt-1">{{ $message }}</span> @enderror
</div>
<div>
<label class="block text-sm font-medium mb-2">IP Address / CIDR</label>
<input type="text" wire:model="aliasCidr" placeholder="e.g., 192.168.1.100 or 10.0.0.0/24" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background text-sm">
@error('aliasCidr') <span class="text-error text-xs mt-1">{{ $message }}</span> @enderror
</div>
<div>
<label class="block text-sm font-medium mb-2">Comment (Optional)</label>
<input type="text" wire:model="aliasComment" placeholder="Description of this alias" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background text-sm">
</div>
</div>
<div class="flex gap-3 justify-end mt-6 pt-4 border-t border-neutral/20">
<button wire:click="cancelAliasForm" class="px-4 py-2 bg-background border border-neutral rounded-lg hover:bg-background-secondary transition-colors text-sm font-medium">
Cancel
</button>
<button wire:click="saveAlias" 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="saveAlias">Save Alias</span>
<span wire:loading wire:target="saveAlias">Saving...</span>
</button>
</div>
</div>
</div>
@endif
{{-- Delete Alias Modal --}}
@if($showDeleteAliasModal && $deletingAliasName)
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="relative w-full max-w-md bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
<button wire:click="cancelDeleteAlias" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<div class="flex items-start gap-4 mb-4">
<div class="w-12 h-12 rounded-full bg-error/10 flex items-center justify-center flex-shrink-0">
<x-ri-delete-bin-fill class="size-6 text-error" />
</div>
<div>
<h3 class="text-xl font-bold mb-2">Delete IP Alias</h3>
<p class="text-sm text-base/70 mb-2">Delete alias "<span class="font-semibold">{{ $deletingAliasName }}</span>"?</p>
<p class="text-xs text-base/60">This may affect firewall rules using this alias.</p>
</div>
</div>
<div class="flex gap-3 justify-end">
<x-proxmox::ui.button wire:click="cancelDeleteAlias" variant="secondary">
Cancel
</x-proxmox::ui.button>
<x-proxmox::ui.button wire:click="deleteAlias" wire:loading.attr="disabled" variant="danger">
<span wire:loading.remove wire:target="deleteAlias">Delete</span>
<span wire:loading wire:target="deleteAlias">Deleting...</span>
</x-proxmox::ui.button>
</div>
</div>
</div>
@endif
{{-- IPSet Form Modal --}}
@if($showIPSetForm)
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="relative w-full max-w-lg bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
<button wire:click="cancelIPSetForm" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<h3 class="text-xl font-semibold mb-6">Create IP Set</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-2">IPSet Name</label>
<input type="text" wire:model="ipsetName" placeholder="e.g., trusted-networks" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background text-sm">
@error('ipsetName') <span class="text-error text-xs mt-1">{{ $message }}</span> @enderror
</div>
<div>
<label class="block text-sm font-medium mb-2">Comment (Optional)</label>
<input type="text" wire:model="ipsetComment" placeholder="Description of this IPSet" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background text-sm">
</div>
<div class="p-3 bg-info/10 border border-info/30 rounded-lg">
<p class="text-xs text-info/90">After creating the IPSet, you can add IP addresses and CIDR ranges to it.</p>
</div>
</div>
<div class="flex gap-3 justify-end mt-6 pt-4 border-t border-neutral/20">
<button wire:click="cancelIPSetForm" class="px-4 py-2 bg-background border border-neutral rounded-lg hover:bg-background-secondary transition-colors text-sm font-medium">
Cancel
</button>
<button wire:click="saveIPSet" 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="saveIPSet">Create IPSet</span>
<span wire:loading wire:target="saveIPSet">Creating...</span>
</button>
</div>
</div>
</div>
@endif
{{-- Delete IPSet Modal --}}
@if($showDeleteIPSetModal && $deletingIPSetName)
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="relative w-full max-w-md bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
<button wire:click="cancelDeleteIPSet" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<div class="flex items-start gap-4 mb-4">
<div class="w-12 h-12 rounded-full bg-error/10 flex items-center justify-center flex-shrink-0">
<x-ri-delete-bin-fill class="size-6 text-error" />
</div>
<div>
<h3 class="text-xl font-bold mb-2">Delete IP Set</h3>
<p class="text-sm text-base/70 mb-2">Delete IPSet "<span class="font-semibold">{{ $deletingIPSetName }}</span>"?</p>
<p class="text-xs text-base/60">All members will be removed and rules using this IPSet may be affected.</p>
</div>
</div>
<div class="flex gap-3 justify-end">
<x-proxmox::ui.button wire:click="cancelDeleteIPSet" variant="secondary">
Cancel
</x-proxmox::ui.button>
<x-proxmox::ui.button wire:click="deleteIPSet" wire:loading.attr="disabled" variant="danger">
<span wire:loading.remove wire:target="deleteIPSet">Delete</span>
<span wire:loading wire:target="deleteIPSet">Deleting...</span>
</x-proxmox::ui.button>
</div>
</div>
</div>
@endif
{{-- IPSet Member Form Modal --}}
@if($showIPSetMemberForm && $selectedIPSetName)
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="relative w-full max-w-lg bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
<button wire:click="cancelIPSetMemberForm" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<h3 class="text-xl font-semibold mb-6">Add IP/CIDR to {{ $selectedIPSetName }}</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-2">IP Address / CIDR</label>
<input type="text" wire:model="memberCidr" placeholder="e.g., 192.168.1.100 or 10.0.0.0/16" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background text-sm">
@error('memberCidr') <span class="text-error text-xs mt-1">{{ $message }}</span> @enderror
</div>
<div>
<label class="block text-sm font-medium mb-2">Comment (Optional)</label>
<input type="text" wire:model="memberComment" placeholder="Description" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background text-sm">
</div>
<div class="flex items-center gap-2">
<input type="checkbox" wire:model="memberNomatch" id="memberNomatch" class="rounded border-neutral">
<label for="memberNomatch" class="text-sm">
<span class="font-medium">No Match</span>
<span class="text-base/60">(Exclude this IP from the set)</span>
</label>
</div>
</div>
<div class="flex gap-3 justify-end mt-6 pt-4 border-t border-neutral/20">
<button wire:click="cancelIPSetMemberForm" class="px-4 py-2 bg-background border border-neutral rounded-lg hover:bg-background-secondary transition-colors text-sm font-medium">
Cancel
</button>
<button wire:click="saveIPSetMember" 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="saveIPSetMember">Add Member</span>
<span wire:loading wire:target="saveIPSetMember">Adding...</span>
</button>
</div>
</div>
</div>
@endif
{{-- Delete IPSet Member Modal --}}
@if($showDeleteIPSetMemberModal && $deletingMemberCidr)
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="relative w-full max-w-md bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
<button wire:click="cancelDeleteIPSetMember" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<div class="flex items-start gap-4 mb-4">
<div class="w-12 h-12 rounded-full bg-error/10 flex items-center justify-center flex-shrink-0">
<x-ri-close-circle-fill class="size-6 text-error" />
</div>
<div>
<h3 class="text-xl font-bold mb-2">Remove Member</h3>
<p class="text-sm text-base/70 mb-2">Remove "<span class="font-mono font-semibold">{{ $deletingMemberCidr }}</span>" from {{ $selectedIPSetName }}?</p>
</div>
</div>
<div class="flex gap-3 justify-end">
<x-proxmox::ui.button wire:click="cancelDeleteIPSetMember" variant="secondary">
Cancel
</x-proxmox::ui.button>
<x-proxmox::ui.button wire:click="deleteIPSetMember" wire:loading.attr="disabled" variant="danger">
<span wire:loading.remove wire:target="deleteIPSetMember">Remove</span>
<span wire:loading wire:target="deleteIPSetMember">Removing...</span>
</x-proxmox::ui.button>
</div>
</div>
</div>
@endif
</div>
{{-- Save Configuration Modal --}}
@if($showSaveConfigModal)
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="relative w-full max-w-lg bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
<button wire:click="closeSaveConfigModal" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<div class="flex items-start gap-4 mb-6">
<div class="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
<x-ri-save-3-line class="size-6 text-primary" />
</div>
<div>
<h3 class="text-xl font-bold mb-1">Save Current Configuration</h3>
<p class="text-sm text-base/70">Save your current firewall rules and policies as a reusable configuration</p>
</div>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-2">Configuration Name</label>
<input type="text" wire:model="newConfigName" placeholder="e.g., Web Server Rules"
class="w-full px-4 py-2 bg-background border border-neutral rounded-lg focus:border-primary focus:outline-none transition-colors">
@error('newConfigName')
<p class="text-error text-xs mt-1">{{ $message }}</p>
@enderror
</div>
<div class="bg-background/50 border border-neutral/30 rounded-lg p-4">
<h4 class="text-sm font-semibold mb-3">Configuration Summary</h4>
<div class="grid grid-cols-2 gap-3 text-sm">
<div class="flex items-center justify-between p-2 bg-background rounded">
<span class="text-base/60">Rules</span>
<span class="font-semibold">{{ count($rules) }}</span>
</div>
<div class="flex items-center justify-between p-2 bg-background rounded">
<span class="text-base/60">Incoming Policy</span>
<span class="font-semibold {{ $policyIn === 'DROP' ? 'text-error' : 'text-success' }}">{{ $policyIn }}</span>
</div>
<div class="flex items-center justify-between p-2 bg-background rounded">
<span class="text-base/60">Outgoing Policy</span>
<span class="font-semibold {{ $policyOut === 'DROP' ? 'text-error' : 'text-success' }}">{{ $policyOut }}</span>
</div>
</div>
</div>
</div>
<div class="flex gap-3 justify-end mt-6 pt-4 border-t border-neutral/20">
<button wire:click="closeSaveConfigModal" class="px-4 py-2 bg-background border border-neutral rounded-lg hover:bg-background-secondary transition-colors text-sm font-medium">
Cancel
</button>
<button wire:click="saveConfiguration" 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="saveConfiguration">Save Configuration</span>
<span wire:loading wire:target="saveConfiguration">Saving...</span>
</button>
</div>
</div>
</div>
@endif
{{-- View Configuration Modal --}}
@if($showViewConfigModal && $viewingConfiguration)
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="relative w-full max-w-4xl bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl max-h-[90vh] overflow-hidden flex flex-col">
<div class="p-6 border-b border-neutral/20">
<button wire:click="closeViewConfigModal" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<div class="flex items-start gap-4">
<div class="w-12 h-12 rounded-full bg-info/10 flex items-center justify-center flex-shrink-0">
<x-ri-eye-line class="size-6 text-info" />
</div>
<div>
<h3 class="text-xl font-bold mb-1">{{ $viewingConfiguration->configuration_name }}</h3>
<p class="text-sm text-base/70">Created on {{ $viewingConfiguration->created_at->format('M d, Y H:i') }}</p>
</div>
</div>
</div>
<div class="p-6 overflow-y-auto flex-1">
{{-- Policies --}}
<div class="mb-6">
<h4 class="text-sm font-semibold mb-3">Traffic Policies</h4>
<div class="grid grid-cols-2 gap-4">
<div class="p-4 bg-background rounded-lg border border-neutral/20">
<div class="flex items-center justify-between">
<span class="text-sm text-base/60">Incoming Traffic</span>
<span class="px-3 py-1 rounded-full text-sm font-semibold {{ $viewingConfiguration->incoming_policy === 'DROP' ? 'bg-error/10 text-error' : 'bg-success/10 text-success' }}">
{{ $viewingConfiguration->incoming_policy }}
</span>
</div>
</div>
<div class="p-4 bg-background rounded-lg border border-neutral/20">
<div class="flex items-center justify-between">
<span class="text-sm text-base/60">Outgoing Traffic</span>
<span class="px-3 py-1 rounded-full text-sm font-semibold {{ $viewingConfiguration->outgoing_policy === 'DROP' ? 'bg-error/10 text-error' : 'bg-success/10 text-success' }}">
{{ $viewingConfiguration->outgoing_policy }}
</span>
</div>
</div>
</div>
</div>
{{-- Compatibility Warnings --}}
@if(count($configCompatibilityIssues) > 0)
<div class="mb-6 p-4 bg-warning/10 border border-warning/30 rounded-lg">
<div class="flex items-start gap-3">
<x-ri-error-warning-line class="size-5 text-warning flex-shrink-0 mt-0.5" />
<div>
<h4 class="text-sm font-semibold text-warning mb-2">Compatibility Issues</h4>
<p class="text-xs text-warning/80 mb-2">The following rules may not be compatible with this VPS:</p>
<ul class="text-xs text-warning/80 space-y-1">
@foreach($configCompatibilityIssues as $issue)
<li>• Rule #{{ $issue['rule_index'] + 1 }}: {{ implode(', ', $issue['issues']) }}</li>
@endforeach
</ul>
</div>
</div>
</div>
@endif
{{-- Rules --}}
<div>
<h4 class="text-sm font-semibold mb-3">Firewall Rules ({{ count($viewingConfiguration->firewall_rules) }})</h4>
@if(count($viewingConfiguration->firewall_rules) > 0)
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-neutral/20">
<th class="text-left py-2 px-3 text-xs font-semibold text-base/70 uppercase">#</th>
<th class="text-left py-2 px-3 text-xs font-semibold text-base/70 uppercase">Action</th>
<th class="text-left py-2 px-3 text-xs font-semibold text-base/70 uppercase">Direction</th>
<th class="text-left py-2 px-3 text-xs font-semibold text-base/70 uppercase">Source</th>
<th class="text-left py-2 px-3 text-xs font-semibold text-base/70 uppercase">Destination</th>
<th class="text-left py-2 px-3 text-xs font-semibold text-base/70 uppercase">Protocol</th>
<th class="text-left py-2 px-3 text-xs font-semibold text-base/70 uppercase">Port</th>
<th class="text-left py-2 px-3 text-xs font-semibold text-base/70 uppercase">Status</th>
</tr>
</thead>
<tbody>
@foreach($viewingConfiguration->firewall_rules as $index => $rule)
@php
$hasIssue = collect($configCompatibilityIssues)->contains('rule_index', $index);
@endphp
<tr class="border-b border-neutral/10 {{ $hasIssue ? 'bg-warning/5' : '' }}">
<td class="py-2 px-3 text-base/60">{{ $index + 1 }}</td>
<td class="py-2 px-3">
<span class="px-2 py-0.5 rounded text-xs font-medium {{ ($rule['action'] ?? 'ACCEPT') === 'ACCEPT' ? 'bg-success/10 text-success' : 'bg-error/10 text-error' }}">
{{ $rule['action'] ?? 'ACCEPT' }}
</span>
</td>
<td class="py-2 px-3">{{ strtoupper($rule['type'] ?? '-') }}</td>
<td class="py-2 px-3 font-mono text-xs">{{ $rule['source'] ?? 'any' }}</td>
<td class="py-2 px-3 font-mono text-xs">{{ $rule['dest'] ?? 'any' }}</td>
<td class="py-2 px-3">{{ $rule['proto'] ?? '-' }}</td>
<td class="py-2 px-3 font-mono text-xs">{{ $rule['dport'] ?? '-' }}</td>
<td class="py-2 px-3">
@if($hasIssue)
<span class="px-2 py-0.5 rounded text-xs font-medium bg-warning/10 text-warning">Incompatible</span>
@elseif(isset($rule['enable']) && $rule['enable'] == 0)
<span class="px-2 py-0.5 rounded text-xs font-medium bg-base/10 text-base/60">Disabled</span>
@else
<span class="px-2 py-0.5 rounded text-xs font-medium bg-success/10 text-success">Enabled</span>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
<p class="text-sm text-base/60 text-center py-4">No rules in this configuration</p>
@endif
</div>
</div>
<div class="p-4 border-t border-neutral/20 bg-background/50">
<div class="flex gap-3 justify-end">
<button wire:click="closeViewConfigModal" class="px-4 py-2 bg-background border border-neutral rounded-lg hover:bg-background-secondary transition-colors text-sm font-medium">
Close
</button>
<button wire:click="confirmApplyConfiguration({{ $viewingConfiguration->id }})" class="px-4 py-2 bg-success text-white rounded-lg hover:bg-success/90 transition-colors text-sm font-medium flex items-center gap-2">
<x-ri-play-circle-line class="size-4" />
Apply Configuration
</button>
</div>
</div>
</div>
</div>
@endif
{{-- Apply Configuration Modal --}}
@if($showApplyConfigModal && $applyingConfiguration)
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="relative w-full max-w-lg bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
<button wire:click="closeApplyConfigModal" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<div class="flex items-start gap-4 mb-6">
<div class="w-12 h-12 rounded-full bg-success/10 flex items-center justify-center flex-shrink-0">
<x-ri-play-circle-line class="size-6 text-success" />
</div>
<div>
<h3 class="text-xl font-bold mb-1">Apply Configuration</h3>
<p class="text-sm text-base/70">Apply "{{ $applyingConfiguration->configuration_name }}" to this VPS</p>
</div>
</div>
<div class="bg-warning/10 border border-warning/30 rounded-lg p-4 mb-6">
<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="text-sm">
<p class="font-semibold text-warning mb-1">Warning</p>
<p class="text-warning/80">This will replace all existing firewall rules with the rules from this configuration. This action cannot be undone.</p>
</div>
</div>
</div>
@if(count($applyCompatibilityWarnings) > 0)
<div class="bg-info/10 border border-info/30 rounded-lg p-4 mb-6">
<div class="flex items-start gap-3">
<x-ri-information-line class="size-5 text-info flex-shrink-0 mt-0.5" />
<div class="text-sm">
<p class="font-semibold text-info mb-1">Compatibility Notice</p>
<p class="text-info/80">{{ count($applyCompatibilityWarnings) }} rule(s) in this configuration are not fully compatible with this VPS and will be disabled.</p>
</div>
</div>
</div>
@endif
<div class="bg-background/50 border border-neutral/30 rounded-lg p-4 mb-6">
<h4 class="text-sm font-semibold mb-3">Configuration Details</h4>
<div class="grid grid-cols-3 gap-3 text-sm">
<div class="text-center p-2 bg-background rounded">
<p class="text-xs text-base/60 mb-1">Rules</p>
<p class="font-semibold">{{ count($applyingConfiguration->firewall_rules) }}</p>
</div>
<div class="text-center p-2 bg-background rounded">
<p class="text-xs text-base/60 mb-1">IN Policy</p>
<p class="font-semibold {{ $applyingConfiguration->incoming_policy === 'DROP' ? 'text-error' : 'text-success' }}">{{ $applyingConfiguration->incoming_policy }}</p>
</div>
<div class="text-center p-2 bg-background rounded">
<p class="text-xs text-base/60 mb-1">OUT Policy</p>
<p class="font-semibold {{ $applyingConfiguration->outgoing_policy === 'DROP' ? 'text-error' : 'text-success' }}">{{ $applyingConfiguration->outgoing_policy }}</p>
</div>
</div>
</div>
<div class="flex gap-3 justify-end">
<button wire:click="closeApplyConfigModal" class="px-4 py-2 bg-background border border-neutral rounded-lg hover:bg-background-secondary transition-colors text-sm font-medium">
Cancel
</button>
<button wire:click="applyConfiguration" wire:loading.attr="disabled" class="px-4 py-2 bg-success text-white rounded-lg hover:bg-success/90 transition-colors text-sm font-medium">
<span wire:loading.remove wire:target="applyConfiguration">Apply Configuration</span>
<span wire:loading wire:target="applyConfiguration">Applying...</span>
</button>
</div>
</div>
</div>
@endif
{{-- Delete Configuration Modal --}}
@if($showDeleteConfigModal && $deletingConfigId)
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="relative w-full max-w-md bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
<button wire:click="cancelDeleteConfiguration" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<div class="flex items-start gap-4 mb-4">
<div class="w-12 h-12 rounded-full bg-error/10 flex items-center justify-center flex-shrink-0">
<x-ri-delete-bin-line class="size-6 text-error" />
</div>
<div>
<h3 class="text-xl font-bold mb-2">Delete Configuration</h3>
<p class="text-sm text-base/70">Are you sure you want to delete this saved configuration? This action cannot be undone.</p>
</div>
</div>
<div class="flex gap-3 justify-end">
<x-proxmox::ui.button wire:click="cancelDeleteConfiguration" variant="secondary">
Cancel
</x-proxmox::ui.button>
<x-proxmox::ui.button wire:click="deleteConfiguration" wire:loading.attr="disabled" variant="danger">
<span wire:loading.remove wire:target="deleteConfiguration">Delete</span>
<span wire:loading wire:target="deleteConfiguration">Deleting...</span>
</x-proxmox::ui.button>
</div>
</div>
</div>
@endif
{{-- Apply to All Servers Modal --}}
@if($showApplyToAllModal)
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="relative w-full max-w-2xl bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
<button wire:click="closeApplyToAllModal" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<div class="flex items-start gap-4 mb-6">
<div class="w-12 h-12 rounded-full bg-success/10 flex items-center justify-center flex-shrink-0">
<x-ri-global-line class="size-6 text-success" />
</div>
<div>
<h3 class="text-xl font-bold mb-2">Apply to All Servers</h3>
<p class="text-sm text-base/70">Apply the current firewall configuration to all your other VPS instances.</p>
</div>
</div>
@php $otherServers = $this->getUserOtherServers(); @endphp
@if(count($applyToAllResults) > 0)
{{-- Show Results --}}
<div class="mb-6">
<h4 class="text-sm font-semibold mb-3">Results</h4>
<div class="space-y-2 max-h-64 overflow-y-auto">
@foreach($applyToAllResults as $result)
<div class="flex items-center justify-between p-3 rounded-lg {{ $result['success'] ? 'bg-success/10 border border-success/20' : 'bg-error/10 border border-error/20' }}">
<div class="flex items-center gap-3">
@if($result['success'])
<x-ri-checkbox-circle-fill class="size-5 text-success" />
@else
<x-ri-close-circle-fill class="size-5 text-error" />
@endif
<span class="font-medium">{{ $result['hostname'] }}</span>
</div>
<span class="text-xs {{ $result['success'] ? 'text-success' : 'text-error' }}">{{ $result['message'] }}</span>
</div>
@endforeach
</div>
</div>
<div class="flex gap-3 justify-end">
<button wire:click="closeApplyToAllModal" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium">
Done
</button>
</div>
@else
{{-- Show Confirmation --}}
<div class="bg-warning/10 border border-warning/30 rounded-lg p-4 mb-6">
<div class="flex items-start gap-3">
<x-ri-alert-line class="size-5 text-warning flex-shrink-0 mt-0.5" />
<div>
<h4 class="font-semibold text-warning mb-1">Warning</h4>
<p class="text-sm text-base/70">This will replace the firewall configuration on all your other servers. Existing rules will be deleted and replaced with the current configuration.</p>
</div>
</div>
</div>
@if(count($otherServers) > 0)
<div class="mb-6">
<h4 class="text-sm font-semibold mb-3">Servers to Update ({{ count($otherServers) }})</h4>
<div class="space-y-2 max-h-48 overflow-y-auto">
@foreach($otherServers as $otherServer)
<div class="flex items-center justify-between p-3 bg-background/50 border border-neutral/20 rounded-lg">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center">
<x-ri-server-line class="size-4 text-primary" />
</div>
<div>
<span class="font-medium">{{ $otherServer['hostname'] ?? 'VPS ' . $otherServer['vm_id'] }}</span>
<span class="text-xs text-base/50 ml-2">VM {{ $otherServer['vm_id'] }}</span>
</div>
</div>
</div>
@endforeach
</div>
</div>
<div class="bg-background/50 border border-neutral/30 rounded-lg p-4 mb-6">
<h4 class="text-sm font-semibold mb-2">Configuration to Apply</h4>
<div class="grid grid-cols-3 gap-3 text-sm">
<div>
<span class="text-base/60">Rules:</span>
<span class="font-medium ml-1">{{ count($rules) }}</span>
</div>
<div>
<span class="text-base/60">Policy In:</span>
<span class="font-medium ml-1 {{ $policyIn === 'DROP' ? 'text-error' : 'text-success' }}">{{ $policyIn }}</span>
</div>
<div>
<span class="text-base/60">Policy Out:</span>
<span class="font-medium ml-1 {{ $policyOut === 'DROP' ? 'text-error' : 'text-success' }}">{{ $policyOut }}</span>
</div>
</div>
</div>
@else
<div class="text-center py-8 text-base/60">
<x-ri-server-line class="size-12 mx-auto mb-3 opacity-30" />
<p>No other servers found to apply the configuration to.</p>
</div>
@endif
<div class="flex gap-3 justify-end">
<button wire:click="closeApplyToAllModal" class="px-4 py-2 bg-background border border-neutral rounded-lg hover:bg-background-secondary transition-colors text-sm font-medium">
Cancel
</button>
@if(count($otherServers) > 0)
<button wire:click="applyToAllServers" wire:loading.attr="disabled" class="px-4 py-2 bg-success text-white rounded-lg hover:bg-success/90 transition-colors text-sm font-medium disabled:opacity-50">
<span wire:loading.remove wire:target="applyToAllServers">Apply to {{ count($otherServers) }} Server{{ count($otherServers) > 1 ? 's' : '' }}</span>
<span wire:loading wire:target="applyToAllServers">Applying...</span>
</button>
@endif
</div>
@endif
</div>
</div>
@endif
{{-- Rename Configuration Modal --}}
@if($showRenameConfigModal && $renamingConfigId)
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="relative w-full max-w-md bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
<button wire:click="closeRenameConfigModal" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<div class="flex items-start gap-4 mb-6">
<div class="w-12 h-12 rounded-full bg-warning/10 flex items-center justify-center flex-shrink-0">
<x-ri-edit-line class="size-6 text-warning" />
</div>
<div>
<h3 class="text-xl font-bold mb-1">Rename Configuration</h3>
<p class="text-sm text-base/70">Enter a new name for this configuration</p>
</div>
</div>
<div class="mb-6">
<label for="renameConfigName" class="block text-sm font-medium mb-2">Configuration Name</label>
<input type="text"
id="renameConfigName"
wire:model="renameConfigName"
class="w-full px-4 py-2 bg-background border border-neutral rounded-lg focus:border-primary focus:ring-1 focus:ring-primary transition-colors"
placeholder="Enter configuration name"
maxlength="255">
@error('renameConfigName')
<p class="mt-1 text-sm text-error">{{ $message }}</p>
@enderror
</div>
<div class="flex gap-3 justify-end">
<button wire:click="closeRenameConfigModal" class="px-4 py-2 bg-background border border-neutral rounded-lg hover:bg-background-secondary transition-colors text-sm font-medium">
Cancel
</button>
<button wire:click="renameConfiguration" wire:loading.attr="disabled" class="px-4 py-2 bg-warning text-white rounded-lg hover:bg-warning/90 transition-colors text-sm font-medium disabled:opacity-50">
<span wire:loading.remove wire:target="renameConfiguration">Rename</span>
<span wire:loading wire:target="renameConfiguration">Renaming...</span>
</button>
</div>
</div>
</div>
@endif
@script
<script>
// IP location helper functions
window.extractIP = function(ip) {
if (!ip || typeof ip !== 'string') return ip;
ip = ip.trim();
const ipv4WithPort = /^(\d+\.\d+\.\d+\.\d+):(\d+)$/.exec(ip);
if (ipv4WithPort) return ipv4WithPort[1];
const ipv6WithPort = /^\[([^\]]+)\]:(\d+)$/.exec(ip);
if (ipv6WithPort) return ipv6WithPort[1];
if (ip.includes(':') && (ip.match(/:/g) || []).length >= 2) {
return ip;
}
return ip;
};
window.isPublicIP = function(ip) {
if (!ip || ip === '—' || ip === '-' || typeof ip !== 'string') return false;
ip = window.extractIP(ip).trim();
if (ip === 'localhost' || ip === '127.0.0.1' || ip === '::1') return false;
const ipv4PrivateRanges = [
/^10\./, /^172\.(1[6-9]|2[0-9]|3[0-1])\./, /^192\.168\./,
/^169\.254\./, /^127\./, /^0\./
];
for (const range of ipv4PrivateRanges) {
if (range.test(ip)) return false;
}
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (ipv4Regex.test(ip)) return true;
if (ip.includes(':')) {
if (/^fe[89ab][0-9a-fA-F]:/i.test(ip)) return false;
if (/^f[cd][0-9a-fA-F]/i.test(ip)) return false;
if (ip.startsWith('::ffff:')) {
return window.isPublicIP(ip.substring(7));
}
const ipv6Regex = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/;
return ipv6Regex.test(ip);
}
return false;
};
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') };
}
};
</script>
@endscript
</x-proxmox::layout>