{{-- Polling pauses while a config is on screen: every poll re-renders this page, and that would put the private key into an HTTP response every five seconds. Traffic figures can wait the minute someone needs to copy a key. --}}

{{ __('vpn.title') }}

{{ __('vpn.subtitle') }}

@if ($hubEndpoint === '' || $hubPublicKey === '')

{{ __('vpn.hub_incomplete_title') }}

{{ __('vpn.hub_incomplete_body') }}

@endif {{-- Shown once. The private key exists only in this response; a reload loses it. --}} @if ($newConfig)

{{ __('vpn.config_ready', ['name' => $newConfigName]) }}

{{ __('vpn.config_once') }}

{{ $newConfig }}
{{ __('vpn.download') }} {{ $showQr ? __('vpn.qr_hide') : __('vpn.qr_show') }} {{-- Only surfaces when copying is impossible (no clipboard API outside a secure context) — silence would look like a dead button. --}}

{{ __('vpn.copy_failed') }}

@if ($qrSvg)
{!! $qrSvg !!}

{{ __('vpn.qr_hint') }}

@endif
@endif
{{-- Peer list --}}

{{ __('vpn.peers') }}

{{ $lastSync ? __('vpn.last_sync', ['time' => \Illuminate\Support\Carbon::parse($lastSync)->diffForHumans()]) : __('vpn.never_synced') }}
@if ($peers->isEmpty())

{{ __('vpn.empty') }}

@else {{-- A register of accesses, not a spreadsheet. Seven columns in a fixed table left every one of them too narrow: names broke across two lines, "Letzter Kontakt" wrapped in its own heading, and the row still did not fit. These are attributes of ONE access, not quantities anyone compares down a column — so each access gets a row of its own with the identity on the first line and the measurements on the second, in mono where they line up by themselves. --}}
    @foreach ($peers as $peer) @php $status = $peer->status(); $tone = [ 'online' => 'border-success-border bg-success-bg text-success', 'idle' => 'border-line-strong bg-surface-2 text-muted', 'blocked' => 'border-danger-border bg-danger-bg text-danger', 'pending' => 'border-warning-border bg-warning-bg text-warning', ][$status]; // The rule carries the status too, so a blocked access is // visible while scanning without reading a single pill. $rule = [ 'online' => 'bg-success-bright', 'idle' => 'bg-line-strong', 'blocked' => 'bg-danger', 'pending' => 'bg-warning', ][$status]; @endphp
  • {{ $peer->name }} {{ $peer->host ? __('vpn.host_peer') : __('vpn.operator_peer') }} @if ($peer->owner) {{ $peer->owner->name }} @if ($peer->user_id === auth()->id()) {{ __('vpn.you') }} @endif @elseif (! $peer->host) {{ __('vpn.no_owner') }} @endif
    {{-- The measurements, each with its own label: without a header row they have to name themselves. --}}
    {{ $peer->allowed_ip }} @if ($peer->endpoint) {{ $peer->endpoint }} @endif ↓ {{ \App\Support\Bytes::human($peer->rx_bytes) }}  ↑ {{ \App\Support\Bytes::human($peer->tx_bytes) }} {{ __('vpn.handshake') }}: {{ $peer->last_handshake_at?->diffForHumans() ?? __('vpn.never') }}
    {{ __('vpn.status_'.$status) }}
    {{-- A stored config can be fetched again; without one there is nothing to hand out, and saying so beats an absent button. --}} @if (! $peer->hasStoredConfig() && $peer->kind === \App\Models\VpnPeer::KIND_STAFF && $peer->user_id === auth()->id()) @endif @can('downloadConfig', $peer) @endcan @can('block', $peer) @endcan @can('update', $peer) @if ($peer->kind === \App\Models\VpnPeer::KIND_STAFF) @endif @endcan @can('delete', $peer) @endcan
  • @endforeach
@endif
{{-- New access --}} @if ($canManage)

{{ __('vpn.add') }}

{{ __('vpn.owner_hint') }}

@error('ownerId')

{{ $message }}

@enderror
@error('storeConfig')

{{ $message }}

@enderror
{{ __('vpn.add') }} @endif {{-- Hub facts an operator needs when configuring a peer by hand --}}

{{ __('vpn.hub') }}

{{ __('vpn.endpoint') }}
{{ $hubEndpoint ?: '—' }}
{{ __('vpn.hub_key') }}
{{ $hubPublicKey ?: '—' }}
{{ __('vpn.subnet') }}
{{ config('provisioning.wireguard.subnet') }}