- shared/platform-tools: ea3616a → 12f26fd (27 commits). Traz o módulo de audit (f761d99, o pin da frota), «manter sessão iniciada», o registo de segurança da frota (o gancho que o login já tinha passa a funcionar) e a correção do IP do cliente encontrada nesta auditoria (último X-Forwarded-For / X-Real-IP), promovida ao partilhado. client_ip() da app delega nele. - config/functions_audit.php: audit() sobre platform_audit(); tabela audit_log criada no primeiro uso, segredos ocultados, nunca parte a ação. - Registado: login, falha de login, bloqueio por excesso de tentativas, logout, pedidos de recuperação, mudança/definição de palavra-passe, registo e criação de utilizadores, convites, perfil (antes/depois), todas as definições (db_update_setting, com valor anterior), cada passo da lixeira (apagar, restaurar, eliminar de vez) e ações nas mensagens. - Sistema → Auditoria (views/sysadmin/audit.php), só sysadmin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
181 lines
9.3 KiB
PHP
181 lines
9.3 KiB
PHP
<?php
|
|
// Estado desta plataforma, para o monitor do hub. Só leitura: nenhum parâmetro
|
|
// muda comportamento e nenhum dado de negócio sai daqui. As ações vivem noutro
|
|
// endpoint (api/hub_ops.php) e com outra chave — ver ../functions_hub.php.
|
|
//
|
|
// Formato: docs/CONTRATO_AGENTE_SAUDE.md do hub. `git` e `backup` são entradas
|
|
// DENTRO de `checks`; um check em falta aparece como «não reportado» e um check
|
|
// a `false` pinta o cartão de amarelo.
|
|
//
|
|
// **`ok` de topo não é o AND dos checks.** Segundo o contrato, `ok:false`
|
|
// significa «responde, mas está inutilizável» — e é isso que o hub mostra como
|
|
// «Em baixo». Uma mensagem falhada na fila ou uma cópia antiga não põem um site
|
|
// em baixo: são checks a false, que já dão o aviso amarelo. Aqui `ok` segue a
|
|
// base de dados, que é o que decide se a plataforma consegue sequer servir.
|
|
//
|
|
// Cada verificação corre isolada: uma que rebente reporta-se como falha, em vez
|
|
// de derrubar a resposta inteira — que apareceria no hub como app inacessível,
|
|
// que é uma coisa diferente e mais alarmante.
|
|
//
|
|
// O stub de cada site (public/api/health.php) é uma linha a incluir isto.
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/../hub_bootstrap.php';
|
|
|
|
hub_key_verify_or_die();
|
|
|
|
$health_check = static function (callable $fn): array {
|
|
try {
|
|
return $fn();
|
|
} catch (Throwable $e) {
|
|
return ['ok' => 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;
|
|
|
|
// Envio de eventos de segurança ao hub. [] numa plataforma por emparelhar ou
|
|
// sem base de dados — não reportado, como os outros.
|
|
$security = !($pdo instanceof PDO) ? [] : $health_check(function () use ($pdo) {
|
|
require_once __DIR__ . '/../functions_security_events.php';
|
|
return hub_security_check($pdo);
|
|
});
|
|
if ($security) $checks['security'] = $security;
|
|
|
|
// A base de dados tem a forma que o código partilhado espera? Reporta as avarias
|
|
// silenciosas (uma coluna em falta que desliga o throttle sem erro nenhum) —
|
|
// ver functions_connector.php. Só leitura, uma consulta ao information_schema.
|
|
if ($pdo instanceof PDO) $checks['conformance'] = $health_check(function () use ($pdo) {
|
|
require_once __DIR__ . '/../functions_connector.php';
|
|
return platform_conformance_check($pdo);
|
|
});
|
|
|
|
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: <chave de operações>',
|
|
] : [
|
|
'endpoint' => hub_base_url('api/hub_ops.php'),
|
|
'actions' => hub_ops_actions(),
|
|
'enabled' => true,
|
|
'auth' => 'X-Hub-Key: <chave de operações>',
|
|
],
|
|
// As páginas de sistema deste site, para o hub ligar para a página local
|
|
// (o recurso quando o hub não chega). Só com router: ver functions_routes.php.
|
|
'tools' => function_exists('platform_routes_announced') ? platform_routes_announced() : [],
|
|
// E os endpoints pelo router da API — o caminho para endpoints novos.
|
|
'api' => function_exists('platform_api_routes_announced') ? platform_api_routes_announced() : [],
|
|
]);
|