design(release): rebuild the Release page in the Tactical Terminal system

Replace the bland version + four plain buttons with a composed layout: a current-
version hero (mono version, channel + source), tactile bump tiles showing the
current → target transition with a Beta-tag badge (continue-beta highlighted
accent), styled running/last-pushed states, and a vertical pipeline rail
(Gitea → GitHub-privat → CI → Staging). Tokens-only, responsive 375/768/1280,
German, no emoji; the R5 confirm flow + component contract are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/v1-foundation
boban 2026-06-23 00:32:23 +02:00
parent 25516ad323
commit 6a284caa44
3 changed files with 209 additions and 56 deletions

View File

@ -3,21 +3,45 @@
return [
'title' => 'Release',
'nav' => 'Release',
'eyebrow' => 'Release-Pipeline',
'heading' => 'Release-Steuerung',
'subtitle' => 'Beta auf Staging schneiden — nur auf der Dev-Instanz.',
'dev_instance' => 'Dev-Instanz',
'current' => 'Aktuelle Version',
'continue_beta' => 'Nächste Beta (:v)',
'bump_patch' => 'Patch-Beta (:v)',
'bump_minor' => 'Minor-Beta (:v)',
'bump_major' => 'Major-Beta (:v)',
'channel' => 'Kanal',
'source' => 'Quelle',
// option tiles
'choose' => 'Beta schneiden',
'kind_continue' => 'Nächste Beta',
'kind_patch' => 'Patch',
'kind_minor' => 'Minor',
'kind_major' => 'Major',
'tile_beta' => 'Beta-Tag',
'hint' => 'Schneidet einen Beta-Tag und pusht ihn nach Gitea → Mirror → CI → Staging.',
'confirm_title' => 'Beta schneiden?',
'confirm_body' => 'Beta :target wird geschnitten und nach Gitea → Mirror → CI → Staging gepusht. Das stößt einen Deploy auf der Staging-Instanz an.',
'confirm_action' => 'Schneiden & pushen',
'staged_label' => 'Zuletzt geschnitten:',
// running / result
'running_title' => 'Release läuft',
'running' => 'Beta :target wird geschnitten und gepusht …',
'in_progress' => 'läuft',
'staged' => 'Beta :tag gepusht → Mirror → CI → Staging.',
'staged_label' => 'Zuletzt gepusht:',
'failed' => 'Release fehlgeschlagen: :reason',
// pipeline rail
'pipeline' => 'Pipeline',
'pipe_sub_gitea' => 'Quelle · Tag wird hier erzeugt',
'pipe_sub_mirror' => 'Automatischer Push-Mirror',
'pipe_sub_ci' => 'Tests laufen',
'pipe_sub_staging' => 'Beta wird ausgerollt',
// confirm modal (R5)
'confirm_title' => 'Beta auf Staging schneiden?',
'confirm_body' => 'Schneidet eine Beta von :target, taggt sie und pusht nach Gitea → Mirror → CI → Staging. Der Tag ist unwiderruflich.',
'confirm_action' => 'Beta schneiden',
// errors
'invalid_target' => 'Ungültige Ziel-Version.',
'throttled' => 'Zu viele Versuche — in :seconds s erneut.',
'write_failed' => 'Anfrage konnte nicht geschrieben werden (Signal-Ordner nicht beschreibbar).',

View File

@ -3,21 +3,45 @@
return [
'title' => 'Release',
'nav' => 'Release',
'eyebrow' => 'Release pipeline',
'heading' => 'Release control',
'subtitle' => 'Cut a staging beta — dev instance only.',
'dev_instance' => 'Dev instance',
'current' => 'Current version',
'continue_beta' => 'Next beta (:v)',
'bump_patch' => 'Patch beta (:v)',
'bump_minor' => 'Minor beta (:v)',
'bump_major' => 'Major beta (:v)',
'channel' => 'Channel',
'source' => 'Source',
// option tiles
'choose' => 'Cut a beta',
'kind_continue' => 'Next beta',
'kind_patch' => 'Patch',
'kind_minor' => 'Minor',
'kind_major' => 'Major',
'tile_beta' => 'Beta tag',
'hint' => 'Cuts a beta tag and pushes it to Gitea → mirror → CI → staging.',
'confirm_title' => 'Cut beta?',
'confirm_body' => 'Beta :target will be cut and pushed to Gitea → mirror → CI → staging. This triggers a deploy on the staging instance.',
'confirm_action' => 'Cut & push',
'staged_label' => 'Last cut:',
// running / result
'running_title' => 'Release running',
'running' => 'Cutting and pushing beta :target …',
'in_progress' => 'running',
'staged' => 'Beta :tag pushed → mirror → CI → staging.',
'staged_label' => 'Last pushed:',
'failed' => 'Release failed: :reason',
// pipeline rail
'pipeline' => 'Pipeline',
'pipe_sub_gitea' => 'Source · tag is cut here',
'pipe_sub_mirror' => 'Automatic push mirror',
'pipe_sub_ci' => 'Tests run',
'pipe_sub_staging' => 'Beta is deployed',
// confirm modal (R5)
'confirm_title' => 'Cut a staging beta?',
'confirm_body' => 'Cuts a beta of :target, tags it and pushes to Gitea → mirror → CI → staging. The tag is irreversible.',
'confirm_action' => 'Cut beta',
// errors
'invalid_target' => 'Invalid target version.',
'throttled' => 'Too many attempts — retry in :seconds s.',
'write_failed' => 'Could not write the request (signal directory not writable).',

View File

@ -1,45 +1,150 @@
<div class="space-y-6">
<div class="space-y-1.5">
<h1 class="font-display text-2xl font-bold tracking-tight text-ink">{{ __('release.heading') }}</h1>
<p class="font-mono text-xs text-ink-3">{{ __('release.subtitle') }}</p>
@php
$isBeta = str_contains($current, '-beta');
$tiles = [];
if (isset($targets['continueBeta'])) {
$tiles[] = ['target' => $targets['continueBeta'], 'label' => __('release.kind_continue'), 'icon' => 'rotate', 'primary' => true];
}
$tiles[] = ['target' => $targets['patch'], 'label' => __('release.kind_patch'), 'icon' => 'tag', 'primary' => false];
$tiles[] = ['target' => $targets['minor'], 'label' => __('release.kind_minor'), 'icon' => 'tag', 'primary' => false];
$tiles[] = ['target' => $targets['major'], 'label' => __('release.kind_major'), 'icon' => 'git-branch', 'primary' => false];
$steps = [
['name' => 'Gitea', 'sub' => __('release.pipe_sub_gitea'), 'active' => $pendingId !== null],
['name' => 'GitHub-privat', 'sub' => __('release.pipe_sub_mirror'), 'active' => false],
['name' => 'CI', 'sub' => __('release.pipe_sub_ci'), 'active' => false],
['name' => 'Staging', 'sub' => __('release.pipe_sub_staging'), 'active' => false],
];
@endphp
<div class="space-y-5">
{{-- Header --}}
<div class="flex flex-wrap items-end justify-between gap-3">
<div>
<p class="font-mono text-[11px] uppercase tracking-[0.2em] text-accent-text">{{ __('release.eyebrow') }}</p>
<h2 class="mt-0.5 font-display text-xl font-semibold text-ink sm:text-2xl">{{ __('release.heading') }}</h2>
<p class="mt-1 font-mono text-xs text-ink-3">{{ __('release.subtitle') }}</p>
</div>
<x-badge tone="cyan">{{ __('release.dev_instance') }}</x-badge>
</div>
<x-panel>
<div class="flex flex-col gap-1.5">
<span class="font-mono text-[11px] uppercase tracking-wider text-ink-3">{{ __('release.current') }}</span>
<span class="font-mono text-lg text-ink">{{ $current }}</span>
<div class="grid gap-5 xl:grid-cols-[minmax(0,1fr)_22rem]">
{{-- Left: current version + action --}}
<div class="space-y-5">
{{-- Current version hero --}}
<x-panel :padded="false">
<div class="flex flex-col gap-5 p-5 sm:flex-row sm:items-center sm:justify-between">
<div class="flex items-center gap-4">
<div class="grid h-12 w-12 shrink-0 place-items-center rounded-lg border border-accent/25 bg-accent/10">
<x-icon name="tag" class="h-5 w-5 text-accent" />
</div>
<div>
<p class="font-mono text-[11px] uppercase tracking-wider text-ink-3">{{ __('release.current') }}</p>
<p class="font-mono text-2xl font-semibold leading-tight text-ink">{{ $current }}</p>
</div>
</div>
<dl class="flex gap-7 font-mono text-[11px]">
<div>
<dt class="uppercase tracking-wider text-ink-4">{{ __('release.channel') }}</dt>
<dd class="mt-1 {{ $isBeta ? 'text-warning' : 'text-online' }}">{{ $isBeta ? 'beta' : 'stable' }}</dd>
</div>
<div>
<dt class="uppercase tracking-wider text-ink-4">{{ __('release.source') }}</dt>
<dd class="mt-1 text-ink-2">Gitea</dd>
</div>
</dl>
</div>
</x-panel>
@if ($pendingId !== null)
{{-- Running --}}
<div class="flex items-center gap-4 rounded-lg border border-warning/30 bg-warning/5 px-5 py-4" wire:poll.5s="pollResult">
<div class="grid h-10 w-10 shrink-0 place-items-center rounded-lg border border-warning/30 bg-warning/10">
<x-icon name="rotate" class="h-5 w-5 animate-spin text-warning" />
</div>
<div class="min-w-0">
<p class="font-display text-sm font-semibold text-ink">{{ __('release.running_title') }}</p>
<p class="mt-0.5 truncate font-mono text-[11px] text-ink-3">{{ __('release.running', ['target' => $pendingTarget]) }}</p>
</div>
<x-status-pill status="pending" class="ml-auto shrink-0">{{ __('release.in_progress') }}</x-status-pill>
</div>
@else
@if ($lastTag !== null)
{{-- Last successful push --}}
<div class="flex flex-wrap items-center gap-2.5 rounded-lg border border-online/25 bg-online/10 px-5 py-3.5">
<x-status-dot status="online" />
<span class="font-mono text-[11px] text-ink-3">{{ __('release.staged_label') }}</span>
<span class="font-mono text-xs font-semibold text-online">{{ $lastTag }}</span>
<span class="font-mono text-[11px] text-ink-4"> Mirror CI Staging</span>
</div>
@endif
{{-- Option tiles --}}
<div>
<p class="mb-2.5 font-mono text-[11px] uppercase tracking-wider text-ink-3">{{ __('release.choose') }}</p>
<div class="grid gap-3 sm:grid-cols-2">
@foreach ($tiles as $t)
<button type="button"
wire:key="tile-{{ $t['target'] }}"
wire:click="confirmDeployStaging('{{ $t['target'] }}')"
wire:loading.attr="disabled"
@class([
'group flex flex-col gap-3 rounded-lg border p-4 text-left transition-colors focus:outline-none focus-visible:border-accent/50',
'border-accent/25 bg-accent/10 hover:bg-accent/15' => $t['primary'],
'border-line bg-inset hover:border-line-strong hover:bg-raised' => ! $t['primary'],
])>
<div class="flex items-center justify-between gap-2">
<span @class([
'font-display text-sm font-semibold',
'text-accent-text' => $t['primary'],
'text-ink' => ! $t['primary'],
])>{{ $t['label'] }}</span>
<x-icon :name="$t['icon']" @class([
'h-4 w-4 shrink-0',
'text-accent' => $t['primary'],
'text-ink-4 transition-colors group-hover:text-ink-2' => ! $t['primary'],
]) />
</div>
<div class="flex items-center gap-2 font-mono text-xs">
<span class="text-ink-4">{{ $current }}</span>
<x-icon name="chevron-right" class="h-3.5 w-3.5 shrink-0 text-ink-4" />
<span @class([
'font-semibold',
'text-accent-text' => $t['primary'],
'text-ink' => ! $t['primary'],
])>{{ $t['target'] }}</span>
<x-badge tone="{{ $t['primary'] ? 'accent' : 'neutral' }}" class="ml-auto">{{ __('release.tile_beta') }}</x-badge>
</div>
</button>
@endforeach
</div>
<p class="mt-3 font-mono text-[11px] leading-relaxed text-ink-4">{{ __('release.hint') }}</p>
</div>
@endif
</div>
@if ($lastTag !== null)
<p class="mt-4 flex items-center gap-1.5 font-mono text-[11px] text-online">
<x-icon name="tag" class="h-3.5 w-3.5 shrink-0" />
{{ __('release.staged_label') }} <span class="font-medium">{{ $lastTag }}</span>
</p>
@endif
@if ($pendingId !== null)
<p class="mt-5 flex items-center gap-1.5 font-mono text-[11px] text-warning" wire:poll.5s="pollResult">
<x-icon name="rotate" class="h-3.5 w-3.5 shrink-0 animate-spin" />
{{ __('release.running', ['target' => $pendingTarget]) }}
</p>
@else
<div class="mt-5 grid gap-2.5 sm:grid-cols-2">
@if (isset($targets['continueBeta']))
<x-btn variant="primary" wire:click="confirmDeployStaging('{{ $targets['continueBeta'] }}')" wire:loading.attr="disabled">
{{ __('release.continue_beta', ['v' => $targets['continueBeta']]) }}
</x-btn>
@endif
<x-btn variant="secondary" wire:click="confirmDeployStaging('{{ $targets['patch'] }}')" wire:loading.attr="disabled">
{{ __('release.bump_patch', ['v' => $targets['patch']]) }}
</x-btn>
<x-btn variant="secondary" wire:click="confirmDeployStaging('{{ $targets['minor'] }}')" wire:loading.attr="disabled">
{{ __('release.bump_minor', ['v' => $targets['minor']]) }}
</x-btn>
<x-btn variant="secondary" wire:click="confirmDeployStaging('{{ $targets['major'] }}')" wire:loading.attr="disabled">
{{ __('release.bump_major', ['v' => $targets['major']]) }}
</x-btn>
</div>
<p class="mt-3 font-mono text-[11px] text-ink-4">{{ __('release.hint') }}</p>
@endif
</x-panel>
{{-- Right: pipeline rail --}}
<x-panel :title="__('release.pipeline')">
<ol>
@foreach ($steps as $s)
<li class="flex gap-3" wire:key="step-{{ $loop->index }}">
<div class="flex flex-col items-center">
<span @class([
'grid h-6 w-6 shrink-0 place-items-center rounded-full border font-mono text-[10px]',
'border-accent/40 bg-accent/15 text-accent-text' => $s['active'],
'border-line bg-inset text-ink-3' => ! $s['active'],
])>{{ $loop->iteration }}</span>
@unless ($loop->last)
<span class="my-1 w-px flex-1 bg-line" aria-hidden="true"></span>
@endunless
</div>
<div @class(['min-w-0', 'pb-5' => ! $loop->last])>
<p class="font-mono text-xs {{ $s['active'] ? 'text-accent-text' : 'text-ink' }}">{{ $s['name'] }}</p>
<p class="mt-0.5 font-mono text-[11px] text-ink-4">{{ $s['sub'] }}</p>
</div>
</li>
@endforeach
</ol>
</x-panel>
</div>
</div>