*/ public static function all(): array { return [ new Check( // Named after the vault entry it checks (ssh.private_key), not // a shorthand: ReadinessPageTest's guard walks every // SecretVault::REGISTRY key and demands a Check key that // CONTAINS it (dots turned to underscores) — 'onboarding. // ssh_key' does not contain 'ssh_private_key' and slipped // through unnoticed until that guard existed (Task 12). key: 'onboarding.ssh_private_key', group: self::GROUP, severity: Check::SEVERITY_BLOCKING, label: __('readiness.onboarding.ssh_key'), breaks: __('readiness.onboarding.ssh_key_breaks'), // The SSH identity card lives on the 'platform' tab // (integrations.blade.php: "own machines"), beside the // public-key setting it pairs with — not 'integrations', // which is not a member of Integrations::TABS at all and // sent an operator clicking this exact blocking check to the // wrong tab (Fix-Runde, Befund 1). tab: 'platform', // EstablishSshTrust only deploys the PUBLIC half. Every step // after it (HostStep's own shell helper), and later every // Traefik write on a host already onboarded (SshTraefikWriter), // connects with THIS private key. satisfied: filled(app(SecretVault::class)->get('ssh.private_key')), ), new Check( key: 'onboarding.secrets_key', group: self::GROUP, severity: Check::SEVERITY_BLOCKING, label: __('readiness.onboarding.secrets_key'), breaks: __('readiness.onboarding.secrets_key_breaks'), // SECRETS_KEY has no field anywhere on Integrations' 'services' // or 'platform' tabs — the only place an operator can actually // set it is the raw .env editor on the 'env' tab. tab: 'env', // isUsable() rather than a bare filled() on the config value — // SecretCipher's own docblock warns that a second, shorter // rule that only checks emptiness lets a malformed-but-non- // empty key sail through, exactly the gap that once let two // other call sites reach an uncaught RuntimeException. satisfied: app(SecretVault::class)->isUsable(), ), new Check( key: 'onboarding.vpn_config_key', group: self::GROUP, severity: Check::SEVERITY_BLOCKING, label: __('readiness.onboarding.vpn_config_key'), breaks: __('readiness.onboarding.vpn_config_key_breaks'), // Same reasoning as onboarding.secrets_key just above: // VPN_CONFIG_KEY has no console field either, only the raw // .env editor. tab: 'env', // Same reuse-not-reimplement reasoning as isUsable() above: // ConfigVault::available() is the one place that already knows // what counts as a usable key. satisfied: ConfigVault::available(), ), new Check( key: 'onboarding.wg_hub', group: self::GROUP, severity: Check::SEVERITY_BLOCKING, label: __('readiness.onboarding.wg_hub'), breaks: __('readiness.onboarding.wg_hub_breaks'), // Two of the three values this check reads (wgHubPubkey, // wgEndpoint) are 'platform'-tab fields; only the third // (subnet) is .env-only and cannot be changed from the // console at all (see the docblock on satisfied: below). The // tab an operator can actually act on is 'platform'. tab: 'platform', // Two of three is no half a tunnel, it is none: ConfigureWireguard // writes the peer config from all three at once. Subnet is read // straight from config(), never through Settings/ProvisioningSettings // — that class's own docblock explains why: the compose file // (vpn-dns, vpn-gateway) reads the same CLUPILOT_WG_SUBNET at // container boot, so a console-only override would let the // console show one value while the running containers use another. satisfied: filled(ProvisioningSettings::wgHubPublicKey()) && filled(ProvisioningSettings::wgEndpoint()) && filled((string) config('provisioning.wireguard.subnet')), ), new Check( key: 'onboarding.datacenter', group: self::GROUP, severity: Check::SEVERITY_BLOCKING, label: __('readiness.onboarding.datacenter'), breaks: __('readiness.onboarding.datacenter_breaks'), tab: 'datacenters', satisfied: Datacenter::query()->exists(), ), ]; } }