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

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

{{-- ── The tabs ────────────────────────────────────────────────────── The page was three headed sections in one column, two thousand pixels of scroll, with the operator's own password somewhere in the middle of it. Which part somebody wants is a property of what they came to do, not of how far they scroll. The choice is in the query string (see the #[Url] attribute), so a reload, a bookmark and the back button all land where the operator was. That matters here more than on most pages: the installation tab refreshes itself while an update runs, and a tab state kept only in the component would drop them back to the first tab mid-deployment. --}}
@foreach ($tabs as $name) @endforeach
{{-- ── Die Installation ───────────────────────────────────────────────── Grouped and given headings because the page was one undifferentiated stack: eight cards of the same weight in a column half the window wide, with the operator's own password sitting between two platform-wide switches. What each card belongs to is now said out loud, and the short ones sit side by side instead of each taking a full row to hold three lines of text. --}} @if ($tab === 'installation')
{{-- Version & update --}} @if ($canManageSite) {{-- Polls itself: an operator watching an update run should not have to reload the page to find out whether it finished. Fast while something is happening, slow when nothing is. wire:poll alone is not enough, because the thing being watched restarts the thing doing the watching. Mid-run the requests fail, and what answers afterwards is a NEW build being questioned by the old page's JavaScript — so the card sat on "läuft" until somebody reloaded. That gap is now covered by the full-screen overlay in layouts/admin.blade.php, which binds the same watcher once for every console page rather than here — this card no longer binds it itself, so the two do not double-poll while this page is open. wire:poll below is unrelated and stays: it is Livewire's own refresh of this card's own details (log tail, elapsed time) for as long as a Livewire connection exists. --}} {{-- The poll stops while a run is in flight. The overlay's own watcher is what follows a deployment — a plain fetch with no component state behind it, which is why it survives the restart. This one cannot, and every attempt during maintenance mode was a Livewire request answering 503. --}}

{{ __('admin_settings.update_title') }}

@if ($update['running']) {{ __('admin_settings.update_running') }} @elseif ($update['checking']) {{ __('admin_settings.update_checking') }} @elseif ($update['available']) {{-- The version, when it is known: "Version 1.1.0 verfügbar" is a thing an operator can decide about; "1 Aktualisierung verfügbar" is not. --}} @if ($update['target_release']) {{ __('admin_settings.update_available_release', ['version' => ltrim($update['target_release'], 'v')]) }} @else {{ __('admin_settings.update_available', ['n' => $update['behind']]) }} @endif @elseif ($update['behind'] === 0) {{ __('admin_settings.update_current') }} @else {{-- Never shown as "up to date": not knowing and being current are different answers. --}} {{ __('admin_settings.update_unknown') }} @endif
{{ __('admin_settings.update_version') }}:
{{ $update['version'] }}@if ($update['commit']) · {{ substr($update['commit'], 0, 7) }}@endif
@if ($update['source'])
{{ __('admin_settings.update_source') }}:
{{ $update['source'] }}
@endif @if ($update['deployed_at'])
{{ __('admin_settings.update_deployed') }}:
{{ $update['deployed_at']->diffForHumans() }}
@endif @if ($update['checked_at'])
{{ __('admin_settings.update_checked') }}:
{{ $update['checked_at']->diffForHumans() }}
@endif
{{-- Bound, not @disabled(): the directive compiles to inline PHP inside the component tag, and Blade then stops seeing a component. Two separate actions, always offered together: looking and applying are different intentions, and an operator who only wants to know whether anything is new must not be routed through a real deployment to find out. "Jetzt aktualisieren" now requires a released version to install. It used to be offered unconditionally, because `behind` was a reading taken up to a minute ago and a console that had not noticed the last commit yet would refuse to act on it. That reasoning belonged to counting commits on a branch, where the answer changes several times an hour. An update is a TAG now: it appears when somebody releases one, not when somebody pushes — and pressing the check button answers within a second, so a stale reading is no longer something to work around. Disabled for the cases where it would go nowhere: nothing released above the installed version, a run already in flight, no agent, or a check already pending (so a second click cannot collide with the first and be refused). --}}
@if ($update['running']) {{ __('admin_settings.update_running') }} @elseif (! $update['agent_seen']) {{ __('admin_settings.update_unknown') }} @else {{ $update['checking'] ? __('admin_settings.update_checking') : __('admin_settings.update_check') }} {{ __('admin_settings.update_now') }} @endif
@if (! $update['agent_seen']) {{ __('admin_settings.update_no_agent') }} @elseif ($update['last_error']) {{ $update['last_error'] }} @elseif (! $update['available'] && ! $update['running'] && ! $update['checking']) {{-- Why the button is grey. Without this the console looks broken to whoever just pushed to main and expects to see something: it is not broken, it is waiting for a release. --}}

{{ __('admin_settings.update_only_releases') }}

@endif {{-- Where it is. "Läuft gerade" alone is what sends an operator to the shell: a queued update does nothing visible until the agent picks it up, and while it runs the site is in maintenance mode and this page is unreachable. Both look identical to "wedged" without a time — hence the live countdown, replacing what used to be a static "spätestens um 17:19" that read as a promise nothing then visibly kept. There is no countdown any more, for either. It was wrong in fact — the target was recomputed on every poll, and where the interval the agent reported was shorter than the timer actually installed on that host it landed in the past every time and reset to a full minute, so it ran backwards a few seconds and jumped, forever. And it was wrong in kind: a check starts nothing, and an update now starts at once. A queued run says so in a sentence; a queued check says nothing beyond its badge. --}} {{-- And the step is NOT shown here. It used to be, in a small bordered block inside this card, and the moment a run began an operator saw it appear and then, up to three seconds later, saw the full-page overlay cover it and say the same thing in a different size. Two windows for one event. The overlay in layouts/admin carries the step, the running time and the log tail — it is the one that survives the restart, so it is the one that keeps them. --}} {{-- The window. Deliberately inside the update card rather than a section of its own: it is the same decision as the button next to it, only taken in advance. --}}
{{ __('admin_settings.auto_days') }}
@foreach ([1, 2, 3, 4, 5, 6, 7] as $day) @endforeach
@error('autoDays')

{{ $message }}

@enderror
@error('autoFrom')

{{ $message }}

@enderror
@error('autoTo')

{{ $message }}

@enderror
{{ __('admin_settings.save') }}

@if ($autoUpdate) {{ __('admin_settings.auto_next', [ 'days' => collect($autoDays)->sort()->map(fn ($d) => __('admin_settings.weekday.'.$d))->join(', '), 'from' => $autoFrom, 'to' => $autoTo, ]) }} @else {{ __('admin_settings.auto_off') }} @endif

@if ($update['last_state'] !== null && ! $update['running'])

{{ __('admin_settings.update_last_run', [ 'state' => __('admin_settings.update_state.'.$update['last_state']), 'when' => $update['last_finished_at']?->diffForHumans() ?? '—', ]) }} @if ($update['last_started_at'] && $update['last_finished_at']) · {{ __('admin_settings.update_took', ['duration' => $update['last_started_at']->diffForHumans($update['last_finished_at'], true)]) }} @endif {{-- Only on a failure: on a run that worked the last step is "left maintenance mode", which says nothing. --}} @if ($update['last_state'] === 'failed' && $update['last_phase']) · {{ __('admin_settings.update_failed_at', ['step' => $update['last_phase']]) }} @endif

@endif {{-- Open while it runs: "läuft gerade" on its own tells an operator nothing about whether it is progressing or wedged. --}} @if ($updateLog)
{{ __('admin_settings.update_log') }}
{{ $updateLog }}
@endif
@endif
{{-- Website visibility and two-factor policy stack in the left column: both are short, and the console's allow-list beside them is long. Three cards of one row each left a column of white space down the middle of the page. --}}
{{-- My account --}} @if ($canManageSite)

{{ __('admin_settings.site_title') }}

{{ $sitePublic ? __('admin_settings.site_public') : __('admin_settings.site_hidden') }}

{{ $sitePublic ? __('admin_settings.site_public_body') : __('admin_settings.site_hidden_body') }}

{{ __('admin_settings.site_your_view', ['ip' => request()->ip()]) }} {{ $viewerOnVpn ? __('admin_settings.site_via_vpn') : __('admin_settings.site_not_vpn') }}

{{ $sitePublic ? __('admin_settings.site_hide') : __('admin_settings.site_show') }}
@endif {{-- Two-factor policy — voluntary by default, the Owner can make it compulsory. The switch refuses to turn on while the operator flipping it has no confirmed two-factor themselves: the page that would turn it back off sits behind the switch. --}} @if ($canManageSite)

{{ __('admin_settings.two_factor_title') }}

{{ __('admin_settings.two_factor_body') }}

{{ __('admin_settings.save') }}
@error('requireTwoFactor')

{{ $message }}

@enderror
@endif
{{-- Who may reach this console --}} @if ($canManageSite)

{{ __('admin_settings.console_title') }}

{{ $consoleRestricted ? __('admin_settings.console_locked_badge') : __('admin_settings.console_open_badge') }}

{{ $consoleRestricted ? __('admin_settings.console_locked_body') : __('admin_settings.console_open_body') }}

{{ __('admin_settings.console_your_ip', ['ip' => $viewerIp]) }}

{{ $consoleRestricted ? __('admin_settings.console_unlock') : __('admin_settings.console_lock') }}

{{ __('admin_settings.console_always') }}

@foreach ($consoleVpnRanges as $range)
{{ $range }} {{ __('admin_settings.console_vpn_note') }}
@endforeach

{{ __('admin_settings.console_extra') }}

@forelse ($consoleIps as $ip)
{{ $ip }} {{ __('admin_settings.console_remove') }}
@empty

{{ __('admin_settings.console_none') }}

@endforelse {{-- The hint sits under the row, not inside it: as a sibling of the field it made the flex row as tall as field-plus-hint, and the button stretched to match. --}}
{{ __('admin_settings.console_add') }}

{{ __('admin_settings.console_ip_hint') }}

@endif
@endif {{-- ── Das eigene Konto ────────────────────────────────────────────── --}} @if ($tab === 'account')

{{ __('admin_settings.account_title') }}

{{ __('admin_settings.save') }}
{{-- Own password. There was no way to change one at all: an account created with a generated password kept it until someone opened a shell. --}}

{{ __('admin_settings.password_title') }}

{{ __('admin_settings.password_sub') }}

{{ __('admin_settings.password_save') }}
{{-- Every operator sees their own, with no capability required: this is the account you are signed in as, and being unable to see where else that is true is the gap the feature exists to close. --}}
@livewire('admin.sessions')
@endif {{-- ── Zwei-Faktor ────────────────────────────────────────────────────── The unchanged Admin\TwoFactorSetup component, rendered here instead of behind a navigation entry of its own. It is a setting, and it was the one thing an operator had to leave the settings page to change about their own access. It still answers at its own route as well, because RequireOperatorTwoFactor sends an operator there who may not open anything else yet — this page included. Same component either way, so there is no second copy of the enrolment logic to keep in step. --}} @if ($tab === 'two-factor')
@livewire('admin.two-factor-setup', ['embedded' => true])
@endif {{-- ── Das Team ─────────────────────────────────────────────────────── --}} @if ($tab === 'team' && $canManageStaff)

{{ __('admin_settings.staff_title') }}

{{ __('admin_settings.staff_sub') }}

{{-- Invite --}}
@error('staffName')

{{ $message }}

@enderror
@error('staffEmail')

{{ $message }}

@enderror
@error('staffRole')

{{ $message }}

@enderror
{{ __('admin_settings.invite') }}

{{ __('admin_settings.invite_hint') }}

@if ($invitedPassword)

{{ __('admin_settings.temp_title') }}

{{ __('admin_settings.temp_hint') }}

{{ __('admin_settings.email') }}:
{{ $invitedEmail }}
{{ __('admin_settings.temp_password') }}:
{{ $invitedPassword }}
@endif {{-- Staff list --}}
@foreach ($staff as $s) @endforeach
{{ __('admin_settings.col_person') }} {{ __('admin_settings.role') }} {{ __('admin_settings.col_actions') }}

{{ $s['name'] }} @if ($s['self'])({{ __('admin_settings.you') }})@endif

{{ $s['email'] }}

@if ($s['self']) {{ __('admin_settings.role_'.\Illuminate\Support\Str::slug($s['role'])) }} @else @endif {{-- Your own row has no actions — you cannot revoke yourself — and an empty cell reads as a bug rather than as a rule. --}} @if ($s['self']) @endif @unless ($s['self']) @endunless
@endif