Give the settings page an outline
tests / pest (push) Failing after 8m0s Details
tests / assets (push) Successful in 20s Details
tests / release (push) Has been skipped Details

Reported as cramped boxes with empty space either side, and it was: one
column half the window wide, eight cards of identical weight stacked in an
order nobody chose — the operator's own password sitting between two
platform-wide switches — and every card taking a full row to hold three lines
of text.

The page is wider now and has three groups, each with a heading: what belongs
to the installation, what belongs to the signed-in operator, and the team.
Short cards sit side by side instead of one per row. The two-factor policy
moved into the installation group, where it belongs: it is a rule for
everybody who signs in, set by the owner, not a setting on one account — and
it fills the space the short website card was leaving beside the long
allow-list.

Fields are sized to what goes in them. The three password boxes were laid out
three across inside a narrow card; they stack in a half-width one. The window
times are four characters and were as wide as their column.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feature/betriebsmodus v1.3.14
nexxo 2026-07-29 15:29:07 +02:00
parent 6c92aa5dd7
commit da76a260fb
4 changed files with 551 additions and 491 deletions

View File

@ -1 +1 @@
1.3.13
1.3.14

View File

@ -190,4 +190,12 @@ return [
'weekday' => [
1 => 'Mo', 2 => 'Di', 3 => 'Mi', 4 => 'Do', 5 => 'Fr', 6 => 'Sa', 7 => 'So',
],
// The page's outline. It had none — eight cards of equal weight in one
// column, with the operator's own password between two platform switches.
'group' => [
'installation' => 'Installation',
'account' => 'Mein Konto',
'team' => 'Team & Zugriff',
],
];

View File

@ -189,4 +189,12 @@ return [
'weekday' => [
1 => 'Mon', 2 => 'Tue', 3 => 'Wed', 4 => 'Thu', 5 => 'Fri', 6 => 'Sat', 7 => 'Sun',
],
// The page's outline. It had none — eight cards of equal weight in one
// column, with the operator's own password between two platform switches.
'group' => [
'installation' => 'Installation',
'account' => 'My account',
'team' => 'Team & access',
],
];

View File

@ -1,9 +1,22 @@
<div class="mx-auto max-w-3xl space-y-6">
<div class="mx-auto max-w-[1120px] space-y-10">
<div class="animate-rise">
<h1 class="text-2xl font-bold tracking-tight text-ink">{{ __('admin_settings.title') }}</h1>
<p class="mt-1 text-sm text-muted">{{ __('admin_settings.subtitle') }}</p>
</div>
{{-- ── 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. --}}
<section class="space-y-5">
<div class="flex items-center gap-4">
<h2 class="lbl">{{ __('admin_settings.group.installation') }}</h2>
<span class="h-px flex-1 bg-line"></span>
</div>
{{-- Version & update --}}
@if ($canManageSite)
{{-- Polls itself: an operator watching an update run should not have to
@ -194,13 +207,13 @@
<div>
<label for="auto-from" class="mb-1.5 block text-xs font-semibold text-muted">{{ __('admin_settings.auto_from') }}</label>
<input id="auto-from" type="time" wire:model="autoFrom" @disabled(! $autoUpdate)
class="min-h-10 rounded border border-line bg-surface px-3 font-mono text-sm text-ink">
class="min-h-10 w-32 rounded border border-line bg-surface px-3 font-mono text-sm text-ink">
@error('autoFrom') <p class="mt-1 text-xs text-danger">{{ $message }}</p> @enderror
</div>
<div>
<label for="auto-to" class="mb-1.5 block text-xs font-semibold text-muted">{{ __('admin_settings.auto_to') }}</label>
<input id="auto-to" type="time" wire:model="autoTo" @disabled(! $autoUpdate)
class="min-h-10 rounded border border-line bg-surface px-3 font-mono text-sm text-ink">
class="min-h-10 w-32 rounded border border-line bg-surface px-3 font-mono text-sm text-ink">
@error('autoTo') <p class="mt-1 text-xs text-danger">{{ $message }}</p> @enderror
</div>
<x-ui.button wire:click="saveUpdateWindow" variant="secondary" size="sm">{{ __('admin_settings.save') }}</x-ui.button>
@ -248,6 +261,12 @@
</div>
@endif
<div class="grid gap-5 lg:grid-cols-2 lg:items-start">
{{-- 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. --}}
<div class="space-y-5">
{{-- My account --}}
@if ($canManageSite)
<div class="rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise">
@ -279,6 +298,30 @@
</div>
@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)
<div class="rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:45ms]">
<h2 class="font-semibold text-ink">{{ __('admin_settings.two_factor_title') }}</h2>
<p class="mt-1 max-w-xl text-sm text-muted">{{ __('admin_settings.two_factor_body') }}</p>
<form wire:submit="saveTwoFactorPolicy" class="mt-4 flex flex-wrap items-center justify-between gap-3">
<label class="flex items-center gap-2 text-sm text-body">
<input type="checkbox" wire:model="requireTwoFactor" class="size-4 rounded border-line-strong text-accent" />
{{ __('admin_settings.two_factor_require') }}
</label>
<x-ui.button type="submit" variant="primary" wire:loading.attr="disabled" wire:target="saveTwoFactorPolicy">
{{ __('admin_settings.save') }}
</x-ui.button>
</form>
@error('requireTwoFactor')<p class="mt-1.5 text-xs text-danger">{{ $message }}</p>@enderror
</div>
@endif
</div>
{{-- Who may reach this console --}}
@if ($canManageSite)
<div class="rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:30ms]">
@ -342,35 +385,29 @@
</div>
</div>
@endif
{{-- 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. --}}
<div class="animate-rise [animation-delay:40ms]">
@livewire('admin.sessions')
</div>
{{-- 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)
<div class="rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:45ms]">
<h2 class="font-semibold text-ink">{{ __('admin_settings.two_factor_title') }}</h2>
<p class="mt-1 max-w-xl text-sm text-muted">{{ __('admin_settings.two_factor_body') }}</p>
<form wire:submit="saveTwoFactorPolicy" class="mt-4 flex flex-wrap items-center justify-between gap-3">
<label class="flex items-center gap-2 text-sm text-body">
<input type="checkbox" wire:model="requireTwoFactor" class="size-4 rounded border-line-strong text-accent" />
{{ __('admin_settings.two_factor_require') }}
</label>
<x-ui.button type="submit" variant="primary" wire:loading.attr="disabled" wire:target="saveTwoFactorPolicy">
{{ __('admin_settings.save') }}
</x-ui.button>
</section>
{{-- ── Das eigene Konto ────────────────────────────────────────────── --}}
<section class="space-y-5">
<div class="flex items-center gap-4">
<h2 class="lbl">{{ __('admin_settings.group.account') }}</h2>
<span class="h-px flex-1 bg-line"></span>
</div>
<div class="grid gap-5 lg:grid-cols-2 lg:items-start">
<form wire:submit="saveAccount" class="space-y-4 rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:60ms]">
<h2 class="font-semibold text-ink">{{ __('admin_settings.account_title') }}</h2>
<div class="grid gap-4">
<x-ui.input name="name" wire:model="name" :label="__('admin_settings.name')" />
<x-ui.input name="email" wire:model="email" :label="__('admin_settings.email')" type="email" />
</div>
<div class="flex justify-end">
<x-ui.button variant="primary" type="submit" wire:loading.attr="disabled" wire:target="saveAccount">{{ __('admin_settings.save') }}</x-ui.button>
</div>
</form>
@error('requireTwoFactor')<p class="mt-1.5 text-xs text-danger">{{ $message }}</p>@enderror
</div>
@endif
{{-- 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. --}}
@ -380,7 +417,7 @@
<p class="mt-1 text-sm text-muted">{{ __('admin_settings.password_sub') }}</p>
</div>
<div class="grid gap-4 sm:grid-cols-3">
<div class="grid gap-4">
<x-ui.input name="currentPassword" type="password" autocomplete="current-password"
:label="__('admin_settings.password_current')" wire:model="currentPassword" />
<x-ui.input name="newPassword" type="password" autocomplete="new-password"
@ -393,20 +430,26 @@
<x-ui.button type="submit" variant="primary" wire:loading.attr="disabled" wire:target="updateOwnPassword">{{ __('admin_settings.password_save') }}</x-ui.button>
</div>
</form>
<form wire:submit="saveAccount" class="space-y-4 rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:60ms]">
<h2 class="font-semibold text-ink">{{ __('admin_settings.account_title') }}</h2>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<x-ui.input name="name" wire:model="name" :label="__('admin_settings.name')" />
<x-ui.input name="email" wire:model="email" :label="__('admin_settings.email')" type="email" />
</div>
<div class="flex justify-end">
<x-ui.button variant="primary" type="submit" wire:loading.attr="disabled" wire:target="saveAccount">{{ __('admin_settings.save') }}</x-ui.button>
</div>
</form>
{{-- Staff & access (Owner-only) --}}
{{-- 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. --}}
<div class="animate-rise [animation-delay:40ms]">
@livewire('admin.sessions')
</div>
</section>
{{-- ── Das Team ─────────────────────────────────────────────────────── --}}
@if ($canManageStaff)
<section class="space-y-5">
<div class="flex items-center gap-4">
<h2 class="lbl">{{ __('admin_settings.group.team') }}</h2>
<span class="h-px flex-1 bg-line"></span>
</div>
<div class="space-y-4 rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:120ms]">
<div>
<h2 class="font-semibold text-ink">{{ __('admin_settings.staff_title') }}</h2>
@ -504,5 +547,6 @@
</div>
</div>
</div>
</section>
@endif
</div>