diff --git a/VERSION b/VERSION index 7962dcf..085c0f2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.13 +1.3.14 diff --git a/lang/de/admin_settings.php b/lang/de/admin_settings.php index 4bc7c17..ac01dd3 100644 --- a/lang/de/admin_settings.php +++ b/lang/de/admin_settings.php @@ -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', + ], ]; diff --git a/lang/en/admin_settings.php b/lang/en/admin_settings.php index 96b2131..427d5ea 100644 --- a/lang/en/admin_settings.php +++ b/lang/en/admin_settings.php @@ -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', + ], ]; diff --git a/resources/views/livewire/admin/settings.blade.php b/resources/views/livewire/admin/settings.blade.php index 8172fb2..f4229f1 100644 --- a/resources/views/livewire/admin/settings.blade.php +++ b/resources/views/livewire/admin/settings.blade.php @@ -1,508 +1,552 @@ -
+

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

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

- {{-- 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. --}} -
-
-
-
-

{{ __('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 - - {{-- 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 - - {{-- 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 - - {{-- 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') -
- - {{-- 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 - - {{-- 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') }}

+{{-- ── 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. --}} +
+
+

{{ __('admin_settings.group.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. -
- {{ __('admin_settings.password_save') }} -
- - -
-

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

-
- - -
-
- {{ __('admin_settings.save') }} -
-
- - {{-- Staff & access (Owner-only) --}} - @if ($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 }}
-
+
+
{{ __('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 - {{-- Staff list --}} -
-
- - - - - - - - - - @foreach ($staff as $s) - - - - - + + {{-- 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 + + + + + + {{-- ── Das eigene Konto ────────────────────────────────────────────── --}} +
+
+

{{ __('admin_settings.group.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') +
+
+ + {{-- ── Das Team ─────────────────────────────────────────────────────── --}} + @if ($canManageStaff) +
+
+

{{ __('admin_settings.group.team') }}

+ +
+ +
+
+

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

+

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

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

{{ $message }}

@enderror +
+
+ + + @error('staffEmail')

{{ $message }}

@enderror +
+
+ +
-
{{ __('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 -
+ + @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