DnsTokenCheck::class, 'onboarding.wg_hub' => WireguardEndpointCheck::class, 'provisioning.vm_template' => VmTemplateCheck::class, 'billing.stripe_secret' => StripeCheck::class, ]; /** * Check::key → the App\Support\Settings row it is written under. * * Display only, and carries no threshold of its own: whether a heartbeat * counts as fresh is decided exactly once, by * App\Support\Readiness\OperationChecks::isFresh(), and shows up here * only as the check's own `satisfied`/severity — this map exists solely * so heartbeatDisplay() knows which stored value to format for which * check. (A previous version of this comment claimed a staleness * threshold was kept in sync with OperationChecks::STALE_AFTER_MINUTES — * there is no such value here to keep in sync. CLAUDE.md's R19 names this * exact shape of mistake — a call that reads as an assurance and is not * one — as worse than no comment at all, because it stops the next * reader from checking for themselves.) */ private const HEARTBEAT_KEYS = [ 'operation.scheduler' => 'heartbeat.scheduler', 'operation.queue_provisioning' => 'heartbeat.queue_provisioning', ]; /** * The on-demand checks' own answers, keyed by Check::key. * * Never pre-filled in mount(): DnsTokenCheck writes a real TXT record to * the production DNS zone the moment it runs, so nothing here may run * except in direct response to an operator's own click. * * @var array> */ public array $results = []; public function mount(): void { abort_unless(Gate::any(['hosts.manage', 'secrets.manage']), 403); } public function runCheck(string $key): void { $this->authorize('secrets.manage'); // Der Schlüssel kommt aus dem Browser. Ohne diese Zeile wäre das ein // Weg, beliebige Klassen aufzurufen. $class = self::RUNNABLE[$key] ?? null; if ($class === null) { return; } $this->results[$key] = app($class)->run(); } /** * Where an operator goes to actually fix ONE entry. * * Not always Admin\Integrations: six of the nine `tab` values used across * the five check groups name a DIFFERENT admin page entirely — 'hosts', * 'plans', 'datacenters', 'mail', 'templates' and 'company' are pages of * their own, not Integrations tabs, and forwarding them as `?tab=` would * either 404 or (since Integrations::mount() falls back silently to its * own first tab for an unrecognised value) land quietly on the wrong * section. Every OTHER `tab` value that exists in the five check group * files today (checked at the source, Fix-Runde) really IS one of * Integrations::TABS — nine of them were not, until that same Fix-Runde * corrected them there rather than papering over it here (see * BillingChecks/OnboardingChecks/ProvisioningChecks/DeliveryChecks). The * `default` arm below is therefore a pure safety net for a value nobody * currently produces, not a route any check relies on — kept so a future * typo degrades to the Integrations landing tab instead of an * UnhandledMatchError taking the whole page down. */ public function checkUrl(Check $check): string { if (in_array($check->tab, Integrations::TABS, true)) { return route('admin.integrations', ['tab' => $check->tab]); } return match ($check->tab) { 'hosts' => route('admin.hosts'), 'plans' => route('admin.plans'), 'datacenters' => route('admin.datacenters'), 'mail' => route('admin.mail'), 'templates' => route('admin.templates'), // Company details AND the invoice series both live on // Admin\Finance — there is no dedicated "company" page. 'company' => route('admin.finance'), default => route('admin.integrations'), }; } public function render() { return view('livewire.admin.readiness', [ 'groups' => ReadinessRegistry::byGroup(), 'blockingCount' => count(ReadinessRegistry::blocking()), 'ready' => ReadinessRegistry::isReady(), 'mode' => OperatingMode::current(), 'runnable' => array_keys(self::RUNNABLE), 'heartbeats' => collect(self::HEARTBEAT_KEYS) ->map(fn (string $settingsKey) => $this->heartbeatDisplay($settingsKey)) ->all(), ]); } /** * The heartbeat's own stored moment, in the operator's zone (R19) — or * null for "nothing stored" AND for "unreadable", the same forgiving * either-way OperationChecks::isFresh() already uses. A readiness page * whose only job is to calmly report what is missing must not itself go * down over a garbled timestamp (see that method's own docblock for the * incident this already caused once, inside Readiness::all() itself). */ private function heartbeatDisplay(string $settingsKey): ?string { $raw = Settings::get($settingsKey); if (blank($raw)) { return null; } try { return Carbon::parse($raw)->local()->isoFormat('DD.MM.YYYY HH:mm:ss'); } catch (\Throwable) { return null; } } }