$exception->getMessage(), ]); } try { $pendingMigrations = count(manageUpdatePendingMigrations()); } catch (Throwable $exception) { manageClientLog('WARNING', 'Heartbeat could not read migrations', [ 'error' => $exception->getMessage(), ]); } $diskFree = 0; $free = @disk_free_space(manageClientAppRoot()); if (is_float($free) || is_int($free)) { $diskFree = (int) $free; } return [ 'version' => manageClientVersion(), 'php_version' => PHP_VERSION, 'disk_free' => $diskFree, 'pending_migrations' => $pendingMigrations, 'last_backup_at' => $lastBackupAt, ]; } /** * Sends the heartbeat. * * @return array{success: bool, latest: string, update_available: bool, server_time: string} */ function manageHeartbeatSend(): array { $decoded = manageClientRequestJson( 'POST', 'heartbeat.php', manageHeartbeatPayload(), (int) MANAGE_HTTP_TIMEOUT ); manageWriteJson((string) MANAGE_HEARTBEAT_STATE, [ 'sent_at' => date(DATE_ATOM), 'latest' => (string) ($decoded['latest'] ?? ''), 'update_available' => !empty($decoded['update_available']), ]); return [ 'success' => !empty($decoded['success']), 'latest' => (string) ($decoded['latest'] ?? ''), 'update_available' => !empty($decoded['update_available']), 'server_time' => (string) ($decoded['server_time'] ?? ''), ]; } /** * Heartbeat that never throws. For call sites inside a page render, where an * unreachable server must not surface as an error. */ function manageHeartbeatSendQuietly(): ?array { if (!manageClientConfigured()) { return null; } try { return manageHeartbeatSend(); } catch (Throwable $exception) { manageClientLog('WARNING', 'Heartbeat failed', ['error' => $exception->getMessage()]); return null; } } function manageHeartbeatLastSentAt(): int { $timestamp = strtotime((string) (manageReadJson((string) MANAGE_HEARTBEAT_STATE)['sent_at'] ?? '')); return $timestamp === false ? 0 : $timestamp; } /** * Sends a heartbeat at most once per hour, so opening the settings page * repeatedly does not mean one HTTP round trip per render. Never throws. */ function manageHeartbeatSendIfDue(int $intervalSeconds = 3600): ?array { if (time() - manageHeartbeatLastSentAt() < max(1, $intervalSeconds)) { return null; } return manageHeartbeatSendQuietly(); }