CluPilotCloud/resources/views/livewire/admin/readiness.blade.php

197 lines
12 KiB
PHP

<div class="space-y-5">
<div class="animate-rise">
<h1 class="text-2xl font-bold tracking-tight text-ink">{{ __('readiness.page_title') }}</h1>
<p class="mt-1 text-sm text-muted">{{ __('readiness.page_sub') }}</p>
</div>
{{-- The one line that matters. Everything below is detail. --}}
<div class="flex items-center gap-3 rounded-lg border p-5 shadow-xs animate-rise
{{ $ready ? 'border-success-border bg-success-bg' : 'border-warning-border bg-warning-bg' }}">
<x-ui.icon :name="$ready ? 'shield-check' : 'alert-triangle'"
class="{{ $ready ? 'text-success' : 'text-warning' }}" />
<p class="text-lg font-semibold {{ $ready ? 'text-success' : 'text-warning' }}">
@if ($ready)
{{ $mode->isTest() ? __('readiness.ready_test') : __('readiness.ready_live') }}
@else
{{ trans_choice('readiness.not_ready', $blockingCount, ['n' => $blockingCount]) }}
@endif
</p>
</div>
@foreach ($groups as $groupKey => $checks)
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise">
<h2 class="font-semibold text-ink">{{ __('readiness.group.'.$groupKey) }}</h2>
<ul class="mt-3 divide-y divide-line">
@foreach ($checks as $check)
@php
$result = $results[$check->key] ?? null;
// DIE MESSUNG GEWINNT.
//
// Der Haken kam bisher allein aus `satisfied` — der
// passiven Prüfung, die nur feststellt, dass etwas
// eingetragen IST. Lief daneben eine echte Messung und
// scheiterte, stand grünes „Erfüllt" über rotem „nicht
// in Ordnung", in derselben Zeile. Auf dem Live-Server
// gleich zweimal: beim DNS-Token und bei der
// VM-Vorlage.
//
// Das ist die Attrappe aus R19 in ihrer teuersten Form.
// Wer die Liste überfliegt, liest die Plakette, nicht
// die Kleinschrift — und geht mit „alles grün" weiter,
// während gemessen wurde, dass es nicht geht.
$erfuellt = $result !== null ? (bool) $result['ok'] : $check->satisfied;
$badgeStatus = $erfuellt ? 'active' : ($check->isBlocking() ? 'failed' : 'warning');
$statusLabel = $erfuellt
? __('readiness.status.satisfied')
: ($check->isBlocking() ? __('readiness.status.blocking') : __('readiness.status.warning'));
$iconName = $erfuellt ? 'check' : ($check->isBlocking() ? 'x' : 'alert-triangle');
$iconTone = $erfuellt ? 'text-success' : ($check->isBlocking() ? 'text-danger' : 'text-warning');
$heartbeat = $heartbeats[$check->key] ?? null;
@endphp
<li class="flex flex-wrap items-start gap-3 py-3">
<x-ui.icon :name="$iconName" class="mt-0.5 size-4 shrink-0 {{ $iconTone }}" />
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<span class="font-medium text-ink">{{ $check->label }}</span>
<x-ui.badge :status="$badgeStatus">{{ $statusLabel }}</x-ui.badge>
</div>
@unless ($erfuellt)
<p class="mt-1 text-sm text-muted">{{ $check->breaks }}</p>
@endunless
@if ($heartbeat !== null)
<p class="mt-1 font-mono text-xs text-faint">
{{ __('readiness.last_heartbeat', ['when' => $heartbeat]) }}
</p>
@endif
{{-- Die zwei Schlüssel, die den Tresor selbst
verschlüsseln. Ohne diesen Satz sieht ein
fehlendes Feld nach einem Versäumnis aus, und
jemand sucht danach. Es ist keins: ein
Schlüssel, den die Konsole setzen könnte,
schützt nichts mehr vor jemandem, der die
Konsole hat. Steht IM Textblock, nicht daneben
die Zeile ist ein Flex-Container aus Icon, Text
und Aktionen, ein vierter Nachbar zerlegt sie. --}}
@if (in_array($check->key, ['onboarding.secrets_key', 'onboarding.vpn_config_key'], true))
<p class="mt-1 max-w-[72ch] text-xs text-muted">{{ __('readiness.env_only') }}</p>
@endif
@if ($result !== null)
<p class="mt-1 text-xs {{ $result['ok'] ? 'text-success' : 'text-danger' }}">
{{ $result['ok'] ? __('readiness.check_ok') : __('readiness.check_failed') }}
@if (($result['reason'] ?? null) !== null)
<span class="font-mono">{{ $result['reason'] }}</span>
@endif
</p>
{{-- StripeCheck rechnet die Kontoart des Schlüssels längst aus,
und diese Seite hat sie bislang weggeworfen: wer „Prüfen"
drückte, las „Geprüft: in Ordnung" über einen Live-Schlüssel
im Testplatz. Die Prüfung selbst blockiert dafür jetzt
(BillingChecks); hier steht, was der Knopf gerade gesehen
hat, damit er ihr nicht widerspricht. --}}
{{-- Bei `zone_not_found` steht der Betreiber sonst vor einer
Sackgasse: er liest es als „Token falsch" und tauscht ihn
aus, obwohl der Token gerade eben die Zonenliste geholt hat.
Gesucht und gefunden nebeneinander beantwortet die Frage
selbst. --}}
@if (($result['reason'] ?? null) === 'zone_list_unreadable')
<p class="mt-1 text-xs text-muted">
{{ __('readiness.zone_list_unreadable', ['status' => $result['status'] ?? '?', 'body' => $result['body'] ?? '—']) }}
</p>
@endif
@if (($result['reason'] ?? null) === 'zone_list_failed')
<p class="mt-1 text-xs text-muted">
{{ __('readiness.zone_list_unclear', ['status' => $result['status'] ?? '?']) }}
</p>
@endif
{{-- Ein abgelehnter Schreibversuch, der NICHT 401/403 war,
sagt nichts über die Berechtigung. Ohne diese Zeile
stünde dort `read_only` und der Betreiber tauschte einen
funktionierenden Token aus. --}}
@if (($result['reason'] ?? null) === 'write_failed')
<p class="mt-1 text-xs text-muted">
{{ __('readiness.write_failed', ['status' => $result['status'] ?? '?', 'detail' => $result['detail'] ?? '—']) }}
</p>
@endif
@if (($result['reason'] ?? null) === 'zone_not_found')
<p class="mt-1 text-xs text-muted">
{{ __('readiness.zone_wanted', ['zone' => $result['zone'] ?? '—']) }}
@if (! empty($result['available']))
{{ __('readiness.zone_available', ['zones' => implode(', ', $result['available'])]) }}
@else
{{ __('readiness.zone_none') }}
@endif
</p>
@endif
@if (array_key_exists('live', $result))
@php $keyMode = $result['live'] ? 'live' : 'test'; @endphp
<p class="mt-1 text-xs {{ $keyMode === $mode->value ? 'text-muted' : 'text-danger' }}">
{{ __('readiness.check_key_mode', ['mode' => __('readiness.mode.'.$keyMode)]) }}
@unless ($keyMode === $mode->value)
{{ __('readiness.check_key_mode_conflict', ['mode' => __('readiness.mode.'.$mode->value)]) }}
@endunless
</p>
@endif
@endif
</div>
<div class="flex shrink-0 items-center gap-3">
{{-- mount() only requires hosts.manage OR secrets.manage, so an
Admin (hosts.manage alone) can reach this page — but runCheck()
itself demands secrets.manage and 403s otherwise. A button that
is visible and throws when pressed is worse than no button. --}}
@if (in_array($check->key, $runnable, true))
@can('secrets.manage')
<x-ui.button variant="secondary" size="sm"
wire:click="runCheck('{{ $check->key }}')"
wire:loading.attr="disabled"
wire:target="runCheck('{{ $check->key }}')">
<x-ui.icon name="refresh" class="size-4" />
{{ __('readiness.run_check') }}
</x-ui.button>
@endcan
@endif
{{-- Die zwei Herzschlag-Punkte haben kein Feld. Ihr
„Beheben" zeigte auf den Roheditor der .env, in
dem nichts steht, das hilft ein gestoppter
Arbeiterprozess ist kein Wert, sondern ein
Prozess. Hier steht deshalb die Aktion, die ihn
wirklich kuriert, statt eines Links. --}}
@if (in_array($check->key, $restartable, true) && $canRestart)
<x-ui.button variant="secondary" size="sm"
wire:click="restartWorkers"
wire:loading.attr="disabled"
wire:target="restartWorkers">
<x-ui.icon name="refresh" class="size-4" />
{{ __('readiness.restart_workers') }}
</x-ui.button>
@endif
@unless (in_array($check->key, $restartable, true))
<a href="{{ $this->checkUrl($check) }}" wire:navigate
class="inline-flex items-center gap-1 text-xs font-semibold text-accent-text hover:underline">
{{ __('readiness.fix_link') }}
<x-ui.icon name="external-link" class="size-4" />
</a>
@endunless
</div>
</li>
@endforeach
</ul>
</div>
@endforeach
</div>