CluPilotCloud/resources/views/layouts/admin.blade.php

153 lines
8.2 KiB
PHP

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="h-full">
<head>
<x-shell.head :title="$title ?? __('admin.console')" />
</head>
{{--
theme-admin no longer swaps the palette. It used to load a complete dark set
its own surfaces, its own orange, its own status triad which made the
console a second product and forced every shared component to work in two
worlds. It now carries density only: smaller type, tighter rows, tighter
corners. Colour comes from one place.
--}}
<body class="theme-admin min-h-full bg-bg text-body antialiased" x-data="{ nav: false }" :class="nav && 'overflow-hidden lg:overflow-auto'" data-login-url="{{ route('admin.login') }}">
@php
$release = App\Services\Deployment\Release::current();
// Seeds the page-wide update watcher below. Computed here, once, for
// every console page — not only Settings — because the thing being
// watched restarts the containers serving whichever page the
// operator happens to have open, so the watcher (and its overlay)
// must already exist before that happens, wherever they are.
$updateChannel = app(App\Services\Deployment\UpdateChannel::class);
$updateState = $updateChannel->state();
$updateStep = $updateState['phase'] !== null
? __('admin_settings.update_step', ['step' => $updateState['phase']])
: null;
// The elapsed time and the deployment's raw output used to be read here
// and handed to the overlay. They are gone from it: neither is the
// question somebody staring at a maintenance screen has, and reading
// the log meant a file read on EVERY console page load, whether or not
// anything was running. The settings page still shows the log, where
// somebody is actually looking for it.
@endphp
<div class="flex min-h-screen lg:h-screen lg:overflow-hidden">
<x-shell.nav
prefix="admin.nav"
console
:footer="$release->label()"
:groups="\App\Support\Navigation::console()"
/>
<div class="flex min-w-0 flex-1 flex-col lg:h-screen lg:overflow-hidden">
<x-shell.topbar :crumbs="array_values(array_filter([__('admin.console'), $title ?? \App\Support\Navigation::currentLabel(console: true)]))">
{{-- "Bin ich im Test?" darf keine Frage sein, die man erst
durch Navigieren zur Integrationsseite beantwortet die
Plakette steht im Kopf jeder Konsolenseite, solange der
Modus test ist, und verschwindet von selbst im Livebetrieb. --}}
@if (\App\Support\OperatingMode::current()->isTest())
<span class="inline-flex items-center gap-1.5 rounded-full bg-amber-100 px-2.5 py-1 text-xs font-medium text-amber-900 dark:bg-amber-900/40 dark:text-amber-200">
<x-ui.icon name="beaker" class="size-4" />
{{ __('readiness.mode.test') }}
</span>
@endif
@auth
<div class="relative" x-data="{ open: false }" @keydown.escape="open = false">
<button type="button" class="flex min-h-11 items-center gap-2.5 rounded px-2 hover:bg-surface-hover" @click="open = !open" :aria-expanded="open" aria-haspopup="menu">
<span class="grid size-8 place-items-center rounded-pill bg-accent-subtle text-sm font-bold text-accent-text">{{ \Illuminate\Support\Str::upper(\Illuminate\Support\Str::substr(auth()->user()->name, 0, 1)) }}</span>
<span class="hidden text-sm font-medium text-ink sm:block">{{ auth()->user()->name }}</span>
<x-ui.icon name="chevron-down" class="size-4 text-muted" />
</button>
<div x-show="open" x-cloak @click.outside="open = false" class="absolute right-0 mt-1 w-48 overflow-hidden rounded-lg border border-line bg-surface py-1 shadow-md">
<form method="POST" action="{{ route('admin.logout') }}">
@csrf
<button type="submit" class="flex min-h-11 w-full items-center gap-2.5 px-3.5 text-left text-sm text-body hover:bg-surface-hover">
<x-ui.icon name="log-out" class="size-4" />{{ __('dashboard.logout') }}
</button>
</form>
</div>
</div>
@endauth
</x-shell.topbar>
<main class="mx-auto w-full max-w-[1320px] flex-1 px-4 py-6 lg:overflow-y-auto lg:px-7 lg:py-7">
{{ $slot }}
</main>
</div>
</div>
<div
x-data="{ show: false, msg: '', timer: null }"
@notify.window="msg = $event.detail.message; show = true; clearTimeout(timer); timer = setTimeout(() => show = false, 3200)"
x-show="show" x-cloak
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-12"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 translate-y-12"
role="status" aria-live="polite"
class="fixed bottom-6 left-1/2 z-[80] -translate-x-1/2 rounded-pill bg-plate px-5 py-3 text-sm font-semibold text-white shadow-md"
>
<span x-text="msg"></span>
</div>
<x-shell.connection-banner />
{{-- Maintenance overlay: the console during a deployment.
Bound HERE, once, rather than on the settings page's own update
card — this is the only binding of updateWatcher (resources/js/app.js)
in the app, so it is already in the browser, and stays in the
browser, no matter which console page the deploy catches the
operator on. Reported bug: the settings card flickered and vanished
mid-run, and every OTHER console page showed nothing at all, because
nothing was watching there.
Client-side is the only way this can work: the containers go down
mid-run, which is exactly when the view has to stay visible — a
server-rendered maintenance route cannot answer while it is itself
offline.
No close affordance: the run does not stop because an operator
closed a dialog, so offering one would be a lie. It clears itself —
window.location.reload() from inside updateWatcher — the moment the
build commit changes or `running` goes false, landing the operator
back on the very page they were on. --}}
<div
x-data="updateWatcher({
url: '{{ route('admin.update.state') }}',
commit: @js($updateState['commit']),
running: @js($updateState['running']),
step: @js($updateStep),
})"
{{-- The button's own click, not the next poll three seconds later.
Those three seconds were long enough to show a second, smaller
"läuft gerade" panel inside the settings card first. --}}
@update-started.window="wasRunning = true"
x-show="wasRunning"
x-cloak
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
class="fixed inset-0 z-[100] overflow-auto"
role="alert"
aria-live="assertive"
>
{{-- The SAME panel the 503 page renders. It used to be a light card in
the app's own tokens, which meant an operator saw one design while
the application was still up and a completely different one the
moment the containers went down — two faces for one event, swapping
mid-update. The operator detail rides along inside it rather than
beside it. --}}
@include('partials.updating-panel', [
'body' => __('admin_settings.update_offline_hint'),
'detail' => true,
])
</div>
@livewire('wire-elements-modal')
@livewireScripts
</body>
</html>