feat(versions): professional update phase labels, responsive update card, changelog accordion
Also update UpdateProgressTest assertions to match the new phase label strings. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>feat/v1-foundation
parent
542ad1a4d8
commit
a1b37751a0
|
|
@ -7,10 +7,10 @@ return [
|
||||||
'subtitle' => 'Der Stack wird neu gebaut. Diese Seite erkennt automatisch, wann er wieder erreichbar ist.',
|
'subtitle' => 'Der Stack wird neu gebaut. Diese Seite erkennt automatisch, wann er wieder erreichbar ist.',
|
||||||
|
|
||||||
// Phase labels (displayed in the checklist)
|
// Phase labels (displayed in the checklist)
|
||||||
'phase_fetch' => 'Holen',
|
'phase_fetch' => 'Update wird abgerufen',
|
||||||
'phase_build' => 'Bauen',
|
'phase_build' => 'Image wird erstellt',
|
||||||
'phase_migrate' => 'Migrieren',
|
'phase_migrate' => 'Datenbank wird migriert',
|
||||||
'phase_restart' => 'Neustart',
|
'phase_restart' => 'Dienste werden neu gestartet',
|
||||||
|
|
||||||
// Status messages
|
// Status messages
|
||||||
'elapsed' => 'Vergangene Zeit',
|
'elapsed' => 'Vergangene Zeit',
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@ return [
|
||||||
'subtitle' => 'The stack is rebuilding. This page will automatically detect when it is back up.',
|
'subtitle' => 'The stack is rebuilding. This page will automatically detect when it is back up.',
|
||||||
|
|
||||||
// Phase labels (displayed in the checklist)
|
// Phase labels (displayed in the checklist)
|
||||||
'phase_fetch' => 'Fetch',
|
'phase_fetch' => 'Fetching update',
|
||||||
'phase_build' => 'Build',
|
'phase_build' => 'Building image',
|
||||||
'phase_migrate' => 'Migrate',
|
'phase_migrate' => 'Migrating database',
|
||||||
'phase_restart' => 'Restart',
|
'phase_restart' => 'Restarting services',
|
||||||
|
|
||||||
// Status messages
|
// Status messages
|
||||||
'elapsed' => 'Elapsed time',
|
'elapsed' => 'Elapsed time',
|
||||||
|
|
|
||||||
|
|
@ -40,32 +40,35 @@
|
||||||
|
|
||||||
{{-- Honest status banner — installed version vs. newest release tag in the channel --}}
|
{{-- Honest status banner — installed version vs. newest release tag in the channel --}}
|
||||||
<div @class([
|
<div @class([
|
||||||
'flex flex-wrap items-center gap-4 rounded-xl border bg-surface p-5 shadow-panel',
|
'flex flex-col gap-4 rounded-xl border bg-surface p-5 shadow-panel sm:flex-row sm:items-center',
|
||||||
'border-warning/30' => $isUpdate,
|
'border-warning/30' => $isUpdate,
|
||||||
'border-line' => ! $isUpdate,
|
'border-line' => ! $isUpdate,
|
||||||
])>
|
])>
|
||||||
<span @class([
|
{{-- Icon + version/badges block --}}
|
||||||
'grid h-12 w-12 shrink-0 place-items-center rounded-xl border',
|
<div class="flex items-start gap-4 sm:contents">
|
||||||
'border-warning/30 bg-warning/10 text-warning' => $isUpdate,
|
<span @class([
|
||||||
'border-accent/25 bg-accent/10 text-accent-text' => ! $isUpdate,
|
'grid h-12 w-12 shrink-0 place-items-center rounded-xl border',
|
||||||
])>
|
'border-warning/30 bg-warning/10 text-warning' => $isUpdate,
|
||||||
<x-icon :name="$isUpdate ? 'alert' : 'tag'" class="h-6 w-6" />
|
'border-accent/25 bg-accent/10 text-accent-text' => ! $isUpdate,
|
||||||
</span>
|
])>
|
||||||
<div class="min-w-0 flex-1">
|
<x-icon :name="$isUpdate ? 'alert' : 'tag'" class="h-6 w-6" />
|
||||||
<p class="flex flex-wrap items-center gap-2 font-display text-base font-semibold text-ink">
|
</span>
|
||||||
Clusev <span class="font-mono text-accent-text">v{{ $version }}</span>
|
<div class="min-w-0 flex-1">
|
||||||
<x-badge tone="accent">{{ __('versions.channel_badge', ['channel' => $channel]) }}</x-badge>
|
<p class="flex flex-wrap items-center gap-2 font-display text-base font-semibold text-ink">
|
||||||
@if ($latestTag)
|
Clusev <span class="font-mono text-accent-text">v{{ $version }}</span>
|
||||||
<x-badge tone="neutral">{{ __('versions.latest_release_badge', ['tag' => $latestTag]) }}</x-badge>
|
<x-badge tone="accent">{{ __('versions.channel_badge', ['channel' => $channel]) }}</x-badge>
|
||||||
@endif
|
@if ($latestTag)
|
||||||
</p>
|
<x-badge tone="neutral">{{ __('versions.latest_release_badge', ['tag' => $latestTag]) }}</x-badge>
|
||||||
<p class="mt-0.5 font-mono text-[11px] leading-relaxed text-ink-3">
|
@endif
|
||||||
@if ($updateStatus)
|
</p>
|
||||||
{{ $updateStatus }}
|
<p class="mt-0.5 font-mono text-[11px] leading-relaxed text-ink-3">
|
||||||
@else
|
@if ($updateStatus)
|
||||||
{{ __('versions.banner_default') }}
|
{{ $updateStatus }}
|
||||||
@endif
|
@else
|
||||||
</p>
|
{{ __('versions.banner_default') }}
|
||||||
|
@endif
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{-- One-click update: writes the update sentinel; a host watcher runs update.sh. Mirrors
|
{{-- One-click update: writes the update sentinel; a host watcher runs update.sh. Mirrors
|
||||||
the System restart button (direct click → "running" notice via a flag — not
|
the System restart button (direct click → "running" notice via a flag — not
|
||||||
|
|
@ -73,34 +76,30 @@
|
||||||
@if ($updateRequested)
|
@if ($updateRequested)
|
||||||
{{-- Poll until the rebuilt stack reports a newer version (pollUpdate flips the state +
|
{{-- Poll until the rebuilt stack reports a newer version (pollUpdate flips the state +
|
||||||
fires a success toast). Survives the rebuild downtime: failed polls just retry. --}}
|
fires a success toast). Survives the rebuild downtime: failed polls just retry. --}}
|
||||||
<p class="flex shrink-0 items-center gap-1.5 font-mono text-[11px] text-warning" wire:poll.5s="pollUpdate">
|
<p class="flex w-full items-center gap-1.5 font-mono text-[11px] text-warning sm:w-auto sm:shrink-0" wire:poll.5s="pollUpdate">
|
||||||
<x-icon name="rotate" class="h-3.5 w-3.5 shrink-0 animate-spin" />{{ __('versions.update_running') }}
|
<x-icon name="rotate" class="h-3.5 w-3.5 shrink-0 animate-spin" />{{ __('versions.update_running') }}
|
||||||
</p>
|
</p>
|
||||||
@elseif ($isUpdate)
|
@elseif ($isUpdate)
|
||||||
<x-btn variant="primary" wire:click="requestUpdate" wire:target="requestUpdate" wire:loading.attr="disabled" class="shrink-0">
|
<x-btn variant="primary" wire:click="requestUpdate" wire:target="requestUpdate" wire:loading.attr="disabled" class="w-full sm:w-auto sm:shrink-0">
|
||||||
<svg wire:loading wire:target="requestUpdate" class="h-3.5 w-3.5 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M21 12a9 9 0 1 1-6.219-8.56" stroke-linecap="round" /></svg>
|
<svg wire:loading wire:target="requestUpdate" class="h-3.5 w-3.5 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M21 12a9 9 0 1 1-6.219-8.56" stroke-linecap="round" /></svg>
|
||||||
<x-icon name="rotate" class="h-3.5 w-3.5" wire:loading.remove wire:target="requestUpdate" />
|
<x-icon name="rotate" class="h-3.5 w-3.5" wire:loading.remove wire:target="requestUpdate" />
|
||||||
{{ __('versions.update_now') }}
|
{{ __('versions.update_now') }}
|
||||||
</x-btn>
|
</x-btn>
|
||||||
@endif
|
@endif
|
||||||
@if ($lastChecked)
|
@if ($lastChecked)
|
||||||
<span class="shrink-0 font-mono text-[11px] text-ink-4">{{ __('versions.checked_at', ['time' => $lastChecked]) }}</span>
|
<span class="font-mono text-[11px] text-ink-4 sm:shrink-0">{{ __('versions.checked_at', ['time' => $lastChecked]) }}</span>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,1fr)_300px]">
|
<div class="grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,1fr)_300px]">
|
||||||
{{-- Release history BY VERSION --}}
|
{{-- Release history BY VERSION --}}
|
||||||
<x-panel :title="__('versions.changelog_title')" :subtitle="__('versions.changelog_subtitle', ['count' => count($releases)])" :padded="false">
|
<x-panel :title="__('versions.changelog_title')" :subtitle="__('versions.changelog_subtitle', ['count' => count($releases)])" :padded="false">
|
||||||
<div class="px-4 py-4 sm:px-5">
|
<div class="divide-y divide-line px-4 py-2 sm:px-5">
|
||||||
@forelse ($releases as $rel)
|
@forelse ($releases as $rel)
|
||||||
<div class="flex gap-4 pb-6 last:pb-0">
|
@if ($loop->first)
|
||||||
<div class="flex flex-col items-center">
|
{{-- Latest version: always open, not collapsible --}}
|
||||||
<span @class(['mt-1 h-2.5 w-2.5 shrink-0 rounded-full', 'bg-accent' => $loop->first, 'bg-ink-4' => ! $loop->first])></span>
|
<div class="py-4">
|
||||||
@unless ($loop->last)<span class="mt-1 w-px flex-1 bg-line"></span>@endunless
|
<div class="mb-2.5 flex flex-wrap items-center gap-2">
|
||||||
</div>
|
|
||||||
<div class="-mt-0.5 min-w-0 flex-1">
|
|
||||||
{{-- Version + date header --}}
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
@if ($rel['unreleased'])
|
@if ($rel['unreleased'])
|
||||||
<span class="font-display text-sm font-semibold text-ink">{{ __('versions.unreleased') }}</span>
|
<span class="font-display text-sm font-semibold text-ink">{{ __('versions.unreleased') }}</span>
|
||||||
<x-badge tone="cyan">{{ __('versions.in_progress') }}</x-badge>
|
<x-badge tone="cyan">{{ __('versions.in_progress') }}</x-badge>
|
||||||
|
|
@ -111,9 +110,7 @@
|
||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
<div class="space-y-3">
|
||||||
{{-- Grouped changes --}}
|
|
||||||
<div class="mt-2.5 space-y-3">
|
|
||||||
@foreach ($rel['groups'] as $group => $items)
|
@foreach ($rel['groups'] as $group => $items)
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-1.5 flex items-center gap-2">
|
<div class="mb-1.5 flex items-center gap-2">
|
||||||
|
|
@ -131,9 +128,42 @@
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
@else
|
||||||
|
{{-- Older versions: collapsible <details> accordion --}}
|
||||||
|
<details class="group py-1">
|
||||||
|
<summary class="flex cursor-pointer list-none items-center gap-2 py-3 [&::-webkit-details-marker]:hidden">
|
||||||
|
<x-icon name="chevron-right" class="h-4 w-4 shrink-0 text-ink-4 transition-transform duration-200 [details[open]_&]:rotate-90" />
|
||||||
|
@if ($rel['unreleased'])
|
||||||
|
<span class="font-display text-sm font-semibold text-ink">{{ __('versions.unreleased') }}</span>
|
||||||
|
<x-badge tone="cyan">{{ __('versions.in_progress') }}</x-badge>
|
||||||
|
@else
|
||||||
|
<span class="font-mono text-sm font-semibold text-accent-text">v{{ $rel['version'] }}</span>
|
||||||
|
@if ($rel['date'])
|
||||||
|
<span class="font-mono text-[11px] text-ink-4">{{ $rel['date'] }}</span>
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
</summary>
|
||||||
|
<div class="pb-4 pt-1 space-y-3">
|
||||||
|
@foreach ($rel['groups'] as $group => $items)
|
||||||
|
<div>
|
||||||
|
<div class="mb-1.5 flex items-center gap-2">
|
||||||
|
<x-badge :tone="$tone($group)" class="shrink-0">{{ $sectionLabel($group) }}</x-badge>
|
||||||
|
</div>
|
||||||
|
<ul class="space-y-1.5 pl-0.5">
|
||||||
|
@foreach ($items as $text)
|
||||||
|
<li class="flex items-start gap-2.5">
|
||||||
|
<span class="mt-1.5 h-1 w-1 shrink-0 rounded-full bg-ink-4"></span>
|
||||||
|
<span class="text-sm leading-relaxed text-ink-2">{{ $text }}</span>
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
@endif
|
||||||
@empty
|
@empty
|
||||||
<p class="font-mono text-[11px] text-ink-3">{{ __('versions.changelog_empty') }}</p>
|
<p class="py-4 font-mono text-[11px] text-ink-3">{{ __('versions.changelog_empty') }}</p>
|
||||||
@endforelse
|
@endforelse
|
||||||
</div>
|
</div>
|
||||||
</x-panel>
|
</x-panel>
|
||||||
|
|
|
||||||
|
|
@ -188,9 +188,9 @@ class UpdateProgressTest extends TestCase
|
||||||
$body = $this->get(route('update.progress'))->content();
|
$body = $this->get(route('update.progress'))->content();
|
||||||
|
|
||||||
// German labels (default locale in tests)
|
// German labels (default locale in tests)
|
||||||
$this->assertStringContainsString('Holen', $body);
|
$this->assertStringContainsString('Update wird abgerufen', $body);
|
||||||
$this->assertStringContainsString('Bauen', $body);
|
$this->assertStringContainsString('Image wird erstellt', $body);
|
||||||
$this->assertStringContainsString('Migrieren', $body);
|
$this->assertStringContainsString('Datenbank wird migriert', $body);
|
||||||
$this->assertStringContainsString('Neustart', $body);
|
$this->assertStringContainsString('Dienste werden neu gestartet', $body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue