161 lines
8.7 KiB
PHP
161 lines
8.7 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'">
|
|
@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;
|
|
// Only once a run has actually started, not while merely queued —
|
|
// otherwise this is the PREVIOUS run's leftovers, read on every page
|
|
// load whether or not anything is running at all.
|
|
$updateRunningSince = $updateState['started_at'] !== null
|
|
? __('admin_settings.update_since', ['when' => $updateState['started_at']->diffForHumans()])
|
|
: null;
|
|
$updateLog = $updateState['started_at'] !== null ? $updateChannel->lastLog() : null;
|
|
@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)]))">
|
|
@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>
|
|
|
|
{{-- 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),
|
|
nextCheckAt: @js($updateState['next_check_at']?->toIso8601String()),
|
|
runningSince: @js($updateRunningSince),
|
|
log: @js($updateLog),
|
|
})"
|
|
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] flex items-center justify-center bg-bg px-4"
|
|
role="alert"
|
|
aria-live="assertive"
|
|
>
|
|
<div class="w-full max-w-lg rounded-lg border border-line bg-surface p-8 text-center shadow-md">
|
|
<div class="mx-auto flex size-14 items-center justify-center rounded-pill bg-accent-subtle text-accent-text">
|
|
<x-ui.icon name="refresh" class="size-7 animate-spin" />
|
|
</div>
|
|
|
|
<h1 class="mt-5 text-2xl font-bold tracking-tight text-ink">{{ __('admin_settings.update_overlay_title') }}</h1>
|
|
<p class="mt-2 text-sm text-muted">{{ __('admin_settings.update_offline_hint') }}</p>
|
|
|
|
{{-- Queued: the agent has not picked the request up yet, so
|
|
there is no step. A live countdown instead of silence —
|
|
"Startet in 0:47" reads as something happening; several
|
|
minutes of nothing reads as broken. --}}
|
|
<p class="mt-4 rounded-lg border border-line bg-surface-2 px-4 py-3 text-sm font-medium text-body"
|
|
x-show="!step">
|
|
{{ __('admin_settings.update_starts_in') }} <span x-text="countdownLabel" class="font-mono"></span>
|
|
</p>
|
|
|
|
{{-- Running: the step, how long it has been going, and the tail
|
|
of the log — a run that IS stuck is then visibly stuck AT a
|
|
step, not just a spinner nobody can read anything from. --}}
|
|
<div x-show="step" class="mt-4 space-y-2 text-left">
|
|
<p class="rounded-lg border border-line bg-surface-2 px-4 py-3 text-sm font-medium text-body" x-text="step"></p>
|
|
<p class="text-xs text-muted" x-show="runningSince" x-text="runningSince"></p>
|
|
<pre
|
|
x-show="log"
|
|
x-text="log"
|
|
class="max-h-40 overflow-auto rounded-lg border border-line bg-surface-2 p-3 text-left font-mono text-xs leading-relaxed text-body"
|
|
></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@livewire('wire-elements-modal')
|
|
@livewireScripts
|
|
</body>
|
|
</html>
|