ui(servers): full-width lone firewall/fail2ban panel with internal 2-col layout

On the Server-Details security section, a single installed tool was capped at
lg:max-w-2xl and left-aligned, leaving a large void to its right. Now a lone
panel spans the full content width and splits its own body into two columns on
lg ($lone flag): firewall = defaults/state | rules list; fail2ban = status
summary + view-bans | whitelist (ignoreip). Two tools keep the side-by-side
lg:grid-cols-2 grid. All wire:click / $dispatch bindings, buttons, and __() keys
preserved; no new lang keys.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat/v1-foundation
boban 2026-06-15 03:06:59 +02:00
parent 7e1d342844
commit 02d74bb147
1 changed files with 81 additions and 56 deletions

View File

@ -276,14 +276,20 @@
$showFw = $fwSupported && ($fwInstalled || $fwReadError);
$showF2b = $f2bSupported && ($f2bInstalled || $f2bReadError);
$panelCount = ($showFw ? 1 : 0) + ($showF2b ? 1 : 0);
// A single installed tool would otherwise sit in a capped, left-aligned box
// beside a large void. Instead the lone panel spans the full content width and
// splits its OWN body into two columns on lg — a rich, deliberate card rather
// than a fragment. Two tools keep the side-by-side grid.
$lone = $panelCount === 1;
@endphp
@if ($panelCount > 0)
{{-- Two tools side-by-side; a lone tool is capped so it doesn't stretch edge-to-edge. --}}
{{-- Two tools side-by-side; a lone tool spans full width and lays its body out
internally in two columns (see $lone), so neither case leaves dead space. --}}
<div @class([
'grid grid-cols-1 gap-3',
'lg:grid-cols-2 lg:items-start' => $panelCount === 2,
'lg:max-w-2xl' => $panelCount === 1,
])>
@if ($showFw)
{{-- Firewall-Regeln --}}
@ -313,26 +319,31 @@
</x-btn>
</div>
@else
<div class="flex flex-wrap items-center gap-2 border-b border-line px-4 py-2.5 sm:px-5">
@if ($fwTool === 'ufw')
<span class="font-mono text-[10px] uppercase tracking-wider text-ink-4">{{ __('servers.firewall_default') }}</span>
<x-badge tone="neutral">{{ __('servers.firewall_incoming', ['value' => $fwDefaults['incoming'] ?? '—']) }}</x-badge>
<x-badge tone="neutral">{{ __('servers.firewall_outgoing', ['value' => $fwDefaults['outgoing'] ?? '—']) }}</x-badge>
@else
<span class="font-mono text-[10px] uppercase tracking-wider text-ink-4">{{ __('servers.firewall_zone') }}</span>
<x-badge tone="neutral">{{ $fwDefaults['zone'] ?? '—' }}</x-badge>
@endif
</div>
{{-- Lone firewall: defaults/state in the left column, rules list in the
right. Two-tool case keeps the single stacked column. --}}
<div @class(['lg:grid lg:grid-cols-2 lg:divide-x lg:divide-line' => $lone])>
<div>
<div class="flex flex-wrap items-center gap-2 border-b border-line px-4 py-2.5 sm:px-5">
@if ($fwTool === 'ufw')
<span class="font-mono text-[10px] uppercase tracking-wider text-ink-4">{{ __('servers.firewall_default') }}</span>
<x-badge tone="neutral">{{ __('servers.firewall_incoming', ['value' => $fwDefaults['incoming'] ?? '—']) }}</x-badge>
<x-badge tone="neutral">{{ __('servers.firewall_outgoing', ['value' => $fwDefaults['outgoing'] ?? '—']) }}</x-badge>
@else
<span class="font-mono text-[10px] uppercase tracking-wider text-ink-4">{{ __('servers.firewall_zone') }}</span>
<x-badge tone="neutral">{{ $fwDefaults['zone'] ?? '—' }}</x-badge>
@endif
</div>
@if ($fwReadOnly)
<div class="flex items-start gap-2.5 border-b border-line bg-inset px-4 py-2.5 sm:px-5">
<x-icon name="lock" class="mt-0.5 h-3.5 w-3.5 shrink-0 text-ink-4" />
<p class="font-mono text-[11px] leading-relaxed text-ink-3">{{ __('servers.firewall_readonly') }}</p>
@if ($fwReadOnly)
<div class="flex items-start gap-2.5 border-b border-line bg-inset px-4 py-2.5 sm:px-5">
<x-icon name="lock" class="mt-0.5 h-3.5 w-3.5 shrink-0 text-ink-4" />
<p class="font-mono text-[11px] leading-relaxed text-ink-3">{{ __('servers.firewall_readonly') }}</p>
</div>
@endif
</div>
@endif
<div class="divide-y divide-line">
@forelse ($fwRules as $rule)
<div class="divide-y divide-line">
@forelse ($fwRules as $rule)
{{-- $ruleTone, NOT $tone: the page-level $tone is a closure used by the
gauges/Volumes panel; reusing the name here would clobber it. --}}
@php $ruleTone = $actTone[$rule['action'] ?? 'ALLOW'] ?? 'online'; @endphp
@ -362,11 +373,12 @@
</x-btn>
@endunless
</div>
@empty
<div class="px-4 py-4 sm:px-5">
<p class="font-mono text-[11px] text-ink-3">{{ __('servers.firewall_no_rules') }}</p>
</div>
@endforelse
@empty
<div class="px-4 py-4 sm:px-5">
<p class="font-mono text-[11px] text-ink-3">{{ __('servers.firewall_no_rules') }}</p>
</div>
@endforelse
</div>
</div>
@endif
</x-panel>
@ -404,39 +416,52 @@
</x-btn>
</div>
@else
{{-- Compact summary; the full jail/banned-IP list (which can grow very
long) lives in a scrollable modal, opened via the button below. --}}
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-line px-4 py-3 sm:px-5">
<p class="font-mono text-[11px] text-ink-3">
<span class="text-online">{{ __('common.active') }}</span>
· <span @class(['text-offline' => $f2bBanned > 0])>{{ __('servers.fail2ban_banned', ['count' => $f2bBanned]) }}</span>
· {{ __('servers.fail2ban_jails_count', ['count' => count($f2bJails)]) }}
</p>
<x-btn variant="secondary" size="sm" class="shrink-0"
wire:click="$dispatch('openModal', { component: 'modals.fail2ban-bans', arguments: { serverId: {{ $server->id }} } })">
<x-icon name="lock" class="h-3.5 w-3.5" /> {{ __('servers.fail2ban_view_bans') }}
</x-btn>
</div>
{{-- Whitelist (ignoreip) --}}
<div class="border-t border-line px-4 py-3 sm:px-5">
<p class="mb-2 font-mono text-[10px] uppercase tracking-wider text-ink-4">{{ __('servers.whitelist_label') }}</p>
<div class="flex flex-wrap gap-1.5">
@foreach ($f2bIgnore as $ip)
<span class="inline-flex items-center gap-1.5 rounded-sm border border-line bg-inset px-2 py-1 font-mono text-[11px] text-ink-2">
{{ $ip }}
@unless (in_array($ip, ['127.0.0.1/8', '::1'], true))
<button type="button" wire:click="removeIgnore({{ \Illuminate\Support\Js::from($ip) }})" class="text-ink-4 hover:text-offline" title="{{ __('common.remove') }}">
<x-icon name="x" class="h-3 w-3" />
</button>
@endunless
</span>
@endforeach
{{-- Lone fail2ban: status summary + "Gesperrte IPs ansehen" in the left
column, the whitelist (ignoreip) management in the right. Two-tool
case keeps the single stacked column. --}}
<div @class(['lg:grid lg:grid-cols-2 lg:divide-x lg:divide-line' => $lone])>
{{-- Compact summary; the full jail/banned-IP list (which can grow very
long) lives in a scrollable modal, opened via the button below. --}}
<div @class([
'flex flex-wrap items-center justify-between gap-3 px-4 py-3 sm:px-5',
'border-b border-line lg:border-b-0' => $lone,
'border-b border-line' => ! $lone,
])>
<p class="font-mono text-[11px] text-ink-3">
<span class="text-online">{{ __('common.active') }}</span>
· <span @class(['text-offline' => $f2bBanned > 0])>{{ __('servers.fail2ban_banned', ['count' => $f2bBanned]) }}</span>
· {{ __('servers.fail2ban_jails_count', ['count' => count($f2bJails)]) }}
</p>
<x-btn variant="secondary" size="sm" class="shrink-0"
wire:click="$dispatch('openModal', { component: 'modals.fail2ban-bans', arguments: { serverId: {{ $server->id }} } })">
<x-icon name="lock" class="h-3.5 w-3.5" /> {{ __('servers.fail2ban_view_bans') }}
</x-btn>
</div>
<div class="mt-2.5 flex items-center gap-2">
<input wire:model="newIgnoreIp" wire:keydown.enter="addIgnore" type="text" placeholder="{{ __('servers.whitelist_placeholder') }}"
class="h-8 w-full max-w-xs rounded-md border border-line bg-inset px-2.5 font-mono text-[11px] text-ink placeholder:text-ink-4 focus:border-accent/40 focus:outline-none" />
<x-btn variant="secondary" size="sm" wire:click="addIgnore">{{ __('common.add') }}</x-btn>
{{-- Whitelist (ignoreip) --}}
<div @class([
'px-4 py-3 sm:px-5',
'border-t border-line lg:border-t-0' => $lone,
'border-t border-line' => ! $lone,
])>
<p class="mb-2 font-mono text-[10px] uppercase tracking-wider text-ink-4">{{ __('servers.whitelist_label') }}</p>
<div class="flex flex-wrap gap-1.5">
@foreach ($f2bIgnore as $ip)
<span class="inline-flex items-center gap-1.5 rounded-sm border border-line bg-inset px-2 py-1 font-mono text-[11px] text-ink-2">
{{ $ip }}
@unless (in_array($ip, ['127.0.0.1/8', '::1'], true))
<button type="button" wire:click="removeIgnore({{ \Illuminate\Support\Js::from($ip) }})" class="text-ink-4 hover:text-offline" title="{{ __('common.remove') }}">
<x-icon name="x" class="h-3 w-3" />
</button>
@endunless
</span>
@endforeach
</div>
<div class="mt-2.5 flex items-center gap-2">
<input wire:model="newIgnoreIp" wire:keydown.enter="addIgnore" type="text" placeholder="{{ __('servers.whitelist_placeholder') }}"
class="h-8 w-full max-w-xs rounded-md border border-line bg-inset px-2.5 font-mono text-[11px] text-ink placeholder:text-ink-4 focus:border-accent/40 focus:outline-none" />
<x-btn variant="secondary" size="sm" wire:click="addIgnore">{{ __('common.add') }}</x-btn>
</div>
</div>
</div>
@endif