false, 'detail' => 'erro no check: ' . $e->getMessage()]; } }; $root = $repo_root; $checks = []; // ─── Plataformas estáticas ─────────────────────────────────────────────────── // // O apex (andmore.pt) não tem base de dados — ver a nota no hub_bootstrap.php. // Com ela caem quatro checks: base de dados, SMTP, fila e migrações. Nenhum se // pode medir, e o contrato diz que um check em falta aparece como «não // reportado», que é exactamente a verdade. // // O que não se faz é deixá-los cair calados: sem uma palavra, o cartão no hub // fica com quatro linhas em branco e ninguém sabe se aquilo é uma montra // estática ou uma app com o arranque partido. O check `storage` diz qual é. if (!$hub_has_db_layer) { $checks['storage'] = ['ok' => true, 'detail' => 'montra estática — sem base de dados']; } if ($pdo instanceof PDO) $checks['database'] = $health_check(function () use ($pdo) { $version = (string) $pdo->query('SELECT VERSION()')->fetchColumn(); $tables = (int) $pdo->query('SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE()')->fetchColumn(); return ['ok' => true, 'detail' => $version . ', ' . $tables . ' tabelas']; }); // SMTP: abre a ligação e fecha. NÃO envia nada — isto corre de poucos em poucos // minutos e um envio por sondagem seria spam a si próprio. if ($pdo instanceof PDO) $checks['smtp'] = $health_check(function () use ($pdo) { // O SMTP da frota é configurado no backoffice, não no .env (as chaves do // .env são placeholders em branco de propósito). Ler só o .env dava «sem // SMTP configurado» em sites que o têm a funcionar há meses — daí o // platform_smtp_config(), que é o resolvedor canónico da frota. $cfg = platform_smtp_config($pdo); $host = trim((string) $cfg['host']); if ($host === '') return ['ok' => true, 'detail' => 'sem SMTP configurado']; $port = (int) ($cfg['port'] ?: 587); $e1 = 0; $e2 = ''; $sock = @fsockopen($host, $port, $e1, $e2, 5); if (!$sock) return ['ok' => false, 'detail' => "$host:$port — $e2"]; $greeting = trim((string) fgets($sock, 512)); fclose($sock); return ['ok' => str_starts_with($greeting, '220'), 'detail' => "$host:$port — $greeting"]; }); // Fila de envios. Só as plataformas que a têm reportam: o contrato diz que um // check ausente aparece como «não reportado», que é a verdade, em vez de um // `ok` que não mediu nada. $queue = !($pdo instanceof PDO) ? [] : $health_check(function () use ($pdo) { if (!$pdo->query("SHOW TABLES LIKE 'message_queue'")->fetch()) return []; $rows = $pdo->query("SELECT status, COUNT(*) c FROM message_queue GROUP BY status")->fetchAll(PDO::FETCH_KEY_PAIR); $failed = (int) ($rows['failed'] ?? 0); $pending = (int) ($rows['pending'] ?? 0); return ['ok' => $failed === 0, 'detail' => $pending . ' por enviar, ' . $failed . ' falhadas', 'pending' => $pending, 'failed' => $failed]; }); if ($queue) $checks['queue'] = $queue; // Migrações por aplicar — o sinal de que um `git pull` ficou a meio e a app // está a correr contra um esquema antigo. if ($pdo instanceof PDO) $checks['migrations'] = $health_check(function () use ($pdo) { hub_load_migration_helpers(); // Não medir não é estar bem: sem runner isto diz-se por medir, e o hub // mostra-o como aviso em vez de um «tudo ok» que esconde migrações à espera. if (!function_exists('migrations_status')) { return ['ok' => false, 'detail' => 'runner indisponível — por medir', 'measured' => false]; } $pending = 0; foreach (migrations_status($pdo) as $m) if (empty($m['applied_at'])) $pending++; return ['ok' => $pending === 0, 'detail' => $pending . ' por aplicar', 'pending' => $pending]; }); // Espaço em disco: uma cópia de segurança que não cabe é uma cópia que não há. $checks['disk'] = $health_check(function () use ($root) { $free = @disk_free_space($root); $total = @disk_total_space($root); if ($free === false) return ['ok' => true, 'detail' => 'indisponível']; $used = ($total && $total > 0) ? (int) round(100 * ($total - $free) / $total) : null; return ['ok' => $free > 512 * 1024 * 1024, 'detail' => round($free / 1073741824, 2) . ' GB livres', 'used_percent' => $used, 'free_bytes' => (int) $free]; }); // Git e cópias entram como checks — não como chaves de topo. Ambos devolvem [] // quando não se aplicam (num cPanel sem repositório, por exemplo), e o contrato // diz que um check ausente é «não reportado», não uma falha. $git = $health_check(fn() => hub_git_check($root)); if ($git) $checks['git'] = $git; // Numa montra estática não se pergunta por cópias: não há base de dados a // despejar e o código está todo no git, que o check acima já reporta. Sem esta // guarda o backup_list() -- que existe sempre, porque vem do platform-tools -- // respondia «nenhuma cópia encontrada» e punha o cartão a amarelo para sempre, // por uma cópia que não faria sentido nenhum existir. $backup = $hub_has_db_layer ? $health_check(fn() => hub_backup_check()) : []; if ($backup) $checks['backup'] = $backup; hub_json([ // Só a base de dados: ver a nota no topo sobre o que significa `ok`. Numa // plataforma estática não há base de dados que decida nada -- o que torna a // plataforma inutilizável é não responder, e nesse caso não se chega aqui. 'ok' => $hub_has_db_layer ? !empty($checks['database']['ok']) : true, 'app' => hub_app_name(), 'version' => hub_version($root, $git), 'env' => hub_env('APP_ENV', 'prod'), 'php_version' => PHP_VERSION, 'generated_at' => date('c'), 'checks' => $checks, // Extra fora do contrato — o hub guarda tudo em payload_json. Diz onde // ficam as ações e quais são, para não ter de manter uma lista por // plataforma. Repara que exigem OUTRA chave. // // Com as operações desligadas não se anuncia endpoint nem lista de ações. // O apex nem sequer tem o stub do hub_ops.php — anunciar seis ações e um // caminho que devolve 404 é pior do que não anunciar nada: quem lê a // resposta fica a achar que a plataforma sabe fazer coisas que não sabe. 'ops' => hub_ops_key() === '' ? [ 'endpoint' => null, 'actions' => [], 'enabled' => false, 'auth' => 'X-Hub-Key: ', ] : [ 'endpoint' => hub_base_url('api/hub_ops.php'), 'actions' => hub_ops_actions(), 'enabled' => true, 'auth' => 'X-Hub-Key: ', ], ]);