Show one design while updating, not two swapping mid-run
Reported from live: pressing update showed a blue panel first, which then vanished and was replaced by a completely different "we are updating" screen. Both were mine. The console drew its own overlay in the app's light tokens; a moment later the containers went down and Laravel's 503 page took over with its own standalone styling. One event, two faces, swapping while an operator watches. There is now one panel — resources/views/partials/updating-panel.blade.php — rendered by both. It carries its own inline CSS and cpu- prefixed class names because the 503 page is served with the application down: no compiled stylesheet, no Tailwind, nothing but what is in that file. Dark mode comes from prefers-color-scheme for the same reason. The operator detail (step, running since, log tail) rides inside the panel behind a flag, since the same file is shown to customers on the 503 page. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>feat/granted-plans
parent
15b536f393
commit
0671e8e119
|
|
@ -13,33 +13,8 @@
|
|||
mid-deploy or freshly installed, which is exactly when the Vite
|
||||
manifest may not exist. Pulling it in through @vite would throw an
|
||||
exception instead of rendering the reassurance the visitor came for. --}}
|
||||
<style>
|
||||
:root { color-scheme: light dark; --bg: #f6f7f9; --fg: #0f172a; --muted: #64748b; --accent: #f97316; --card: #ffffff; --line: #e2e8f0; }
|
||||
@media (prefers-color-scheme: dark) { :root { --bg: #0b0f19; --fg: #e2e8f0; --muted: #94a3b8; --card: #111827; --line: #1f2937; } }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 4rem 1.5rem; text-align: center;
|
||||
background: var(--bg); color: var(--fg);
|
||||
font-family: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
main { max-width: 30rem; }
|
||||
.mark { width: 2.5rem; height: 2.5rem; margin: 0 auto; border-radius: 0.75rem; background: var(--accent); }
|
||||
h1 { margin: 1.75rem 0 0; font-size: 1.5rem; line-height: 1.25; letter-spacing: -0.01em; }
|
||||
p { margin: 0.75rem 0 0; font-size: 0.9rem; line-height: 1.65; color: var(--muted); }
|
||||
.badge { display: inline-flex; align-items: center; gap: 0.5rem; margin-top: 1.25rem; padding: 0.25rem 0.75rem;
|
||||
border: 1px solid var(--line); border-radius: 9999px; background: var(--card);
|
||||
font-size: 0.72rem; font-weight: 600; color: var(--accent); }
|
||||
.dot { width: 0.5rem; height: 0.5rem; border-radius: 9999px; background: var(--accent); animation: pulse 1.6s ease-in-out infinite; }
|
||||
@keyframes pulse { 50% { opacity: 0.35; } }
|
||||
@media (prefers-reduced-motion: reduce) { .dot { animation: none; } }
|
||||
.foot { margin-top: 2rem; font-size: 0.75rem; color: var(--muted); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="mark"></div>
|
||||
<div class="badge"><span class="dot"></span>{{ __('updating.badge') }}</div>
|
||||
<h1>{{ __('updating.title') }}</h1>
|
||||
<p>{{ __('updating.body') }}</p>
|
||||
<p class="foot">{{ __('updating.auto') }}</p>
|
||||
</main>
|
||||
<body style="margin:0;min-height:100vh">
|
||||
@include('partials.updating-panel')
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -119,47 +119,23 @@
|
|||
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"
|
||||
class="fixed inset-0 z-[100] overflow-auto"
|
||||
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>
|
||||
{{-- 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,
|
||||
'starting' => $updateState['instant']
|
||||
? __('admin_settings.update_starting')
|
||||
: __('admin_settings.update_starting_queued'),
|
||||
])
|
||||
</div>
|
||||
|
||||
@livewire('wire-elements-modal')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
{{--
|
||||
"We are updating" — one panel, used in both places it can appear.
|
||||
|
||||
It showed up twice with two different faces: the console's own overlay in the
|
||||
app's light tokens while the application was still up, and then this, the
|
||||
503 page, once the containers went down. Two designs for one event, one
|
||||
replacing the other mid-update. Reported as "the first screen must not be
|
||||
there", and rightly.
|
||||
|
||||
The styles are inline and self-contained rather than Tailwind classes,
|
||||
because the other place this renders is the 503 page — shown while the
|
||||
application is mid-deploy or freshly installed, exactly when the Vite
|
||||
manifest may not exist. A stylesheet reference there would throw instead of
|
||||
rendering the reassurance the visitor came for.
|
||||
|
||||
Prefixed class names: this is injected into the console's own document,
|
||||
which has a stylesheet of its own, and `.badge` is not a name to claim
|
||||
globally.
|
||||
--}}
|
||||
<style>
|
||||
.cpu-wrap { --cpu-bg: #f6f7f9; --cpu-fg: #0f172a; --cpu-muted: #64748b; --cpu-accent: #f97316; --cpu-card: #ffffff; --cpu-line: #e2e8f0;
|
||||
display: grid; place-items: center; text-align: center; padding: 4rem 1.5rem; min-height: 100%;
|
||||
background: var(--cpu-bg); color: var(--cpu-fg);
|
||||
font-family: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.cpu-wrap { --cpu-bg: #0b0f19; --cpu-fg: #e2e8f0; --cpu-muted: #94a3b8; --cpu-card: #111827; --cpu-line: #1f2937; }
|
||||
}
|
||||
.cpu-main { max-width: 34rem; width: 100%; }
|
||||
.cpu-mark { width: 2.5rem; height: 2.5rem; margin: 0 auto; border-radius: 0.75rem; background: var(--cpu-accent); }
|
||||
.cpu-badge { display: inline-flex; align-items: center; gap: 0.5rem; margin-top: 1.25rem; padding: 0.25rem 0.75rem;
|
||||
border: 1px solid var(--cpu-line); border-radius: 9999px; background: var(--cpu-card);
|
||||
font-size: 0.72rem; font-weight: 600; color: var(--cpu-accent); }
|
||||
.cpu-dot { width: 0.5rem; height: 0.5rem; border-radius: 9999px; background: var(--cpu-accent); animation: cpu-pulse 1.6s ease-in-out infinite; }
|
||||
@keyframes cpu-pulse { 50% { opacity: 0.35; } }
|
||||
@media (prefers-reduced-motion: reduce) { .cpu-dot { animation: none; } }
|
||||
.cpu-title { margin: 1.75rem 0 0; font-size: 1.5rem; line-height: 1.25; letter-spacing: -0.01em; }
|
||||
.cpu-body { margin: 0.75rem 0 0; font-size: 0.9rem; line-height: 1.65; color: var(--cpu-muted); }
|
||||
.cpu-foot { margin-top: 2rem; font-size: 0.75rem; color: var(--cpu-muted); }
|
||||
.cpu-detail { margin-top: 1.5rem; text-align: left; }
|
||||
.cpu-step { margin: 0; padding: 0.7rem 0.9rem; border: 1px solid var(--cpu-line); border-radius: 0.5rem;
|
||||
background: var(--cpu-card); font-size: 0.85rem; font-weight: 500; }
|
||||
.cpu-since { margin: 0.5rem 0 0; font-size: 0.75rem; color: var(--cpu-muted); }
|
||||
.cpu-log { margin: 0.5rem 0 0; max-height: 10rem; overflow: auto; padding: 0.7rem 0.9rem;
|
||||
border: 1px solid var(--cpu-line); border-radius: 0.5rem; background: var(--cpu-card);
|
||||
font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 0.72rem; line-height: 1.6; }
|
||||
</style>
|
||||
|
||||
<div class="cpu-wrap">
|
||||
<main class="cpu-main">
|
||||
<div class="cpu-mark"></div>
|
||||
<div class="cpu-badge"><span class="cpu-dot"></span>{{ __('updating.badge') }}</div>
|
||||
<h1 class="cpu-title">{{ __('updating.title') }}</h1>
|
||||
<p class="cpu-body">{{ $body ?? __('updating.body') }}</p>
|
||||
|
||||
{{-- Only in the console. A customer looking at the 503 has no use for a
|
||||
deployment step, and an operator watching a run that is stuck has
|
||||
no use for anything else: "läuft gerade" alone is what sends
|
||||
somebody to the shell. The markup is Alpine-driven and inert
|
||||
wherever Alpine is not — which is why it is behind a flag rather
|
||||
than always present. --}}
|
||||
@if ($detail ?? false)
|
||||
<div class="cpu-detail">
|
||||
<p class="cpu-step" x-show="!step">{{ $starting ?? '' }}</p>
|
||||
<p class="cpu-step" x-show="step" x-text="step" x-cloak></p>
|
||||
<p class="cpu-since" x-show="runningSince" x-text="runningSince" x-cloak></p>
|
||||
<pre class="cpu-log" x-show="log" x-text="log" x-cloak></pre>
|
||||
</div>
|
||||
@endif
|
||||
<p class="cpu-foot">{{ __('updating.auto') }}</p>
|
||||
</main>
|
||||
</div>
|
||||
|
|
@ -761,3 +761,32 @@ it('does not answer the watcher for somebody who is not an operator', function (
|
|||
->get(route('admin.update.state'))
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('shows one design while updating, not two swapping mid-run', function () {
|
||||
// Reported as "the first screen must not be there". It was the console's
|
||||
// own overlay in the app's light tokens; then the containers went down and
|
||||
// the 503 page took over with a completely different face. Two designs for
|
||||
// one event, replacing each other mid-update.
|
||||
//
|
||||
// Both render the same partial now, so there is nothing left that can
|
||||
// disagree.
|
||||
$console = Illuminate\Support\Facades\File::get(resource_path('views/layouts/admin.blade.php'));
|
||||
$maintenance = Illuminate\Support\Facades\File::get(resource_path('views/errors/503.blade.php'));
|
||||
|
||||
expect($console)->toContain("partials.updating-panel")
|
||||
->and($maintenance)->toContain("partials.updating-panel")
|
||||
// And nothing left behind that would render a second look.
|
||||
->and($console)->not->toContain('update_overlay_title')
|
||||
->and($maintenance)->not->toContain('<style>');
|
||||
});
|
||||
|
||||
it('keeps the deployment step in the overlay, where an operator needs it', function () {
|
||||
// The panel is shared with a page customers see, so the operator detail is
|
||||
// behind a flag rather than always present — but it has to still be there,
|
||||
// because "läuft gerade" on its own is what sends somebody to the shell.
|
||||
$panel = Illuminate\Support\Facades\File::get(resource_path('views/partials/updating-panel.blade.php'));
|
||||
|
||||
expect($panel)->toContain('x-text="step"')
|
||||
->toContain('x-text="runningSince"')
|
||||
->toContain('x-text="log"');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue