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

169 lines
9.1 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;
// 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>
<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),
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 yet either.
This used to count down to the agent's next tick. It was wrong
twice over. Wrong in fact: the countdown target was recomputed
on every poll, and where the reported interval was shorter than
the timer actually installed it landed in the past every time
and reset to a full minute so it ran backwards four seconds
and jumped, forever. And wrong in kind: with the agent woken
the moment the request lands there is nothing left to wait for.
A sentence, not a clock. --}}
<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">
{{ $updateState['instant'] ? __('admin_settings.update_starting') : __('admin_settings.update_starting_queued') }}
</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>