1<?php
2/**
3 * Title: System Health
4 * Icon: fa-heartbeat
5 * Group: Operations
6 * Order: 10
7 * Description: PHP, database, cache, disk and migrations at a glance.
8 */
9
10if (!defined('ACP_ROOT')) {
11 http_response_code(403);
12 die('Direct access denied.');
13}
14
15function acp_health_bytes(int $bytes): string {
16 if ($bytes >= 1073741824) return number_format($bytes / 1073741824, 2) . ' GB';
17 if ($bytes >= 1048576) return number_format($bytes / 1048576, 1) . ' MB';
18 if ($bytes >= 1024) return number_format($bytes / 1024, 1) . ' KB';
19 return $bytes . ' B';
20}
21
22// --- PHP ---------------------------------------------------------------
23$phpChecks = array(
24 array('PHP version', PHP_VERSION_ID >= 80100, PHP_VERSION),
25);
26foreach (array('mysqli' => true, 'curl' => false, 'openssl' => false, 'gd' => false, 'zip' => false, 'mbstring' => false) as $ext => $required) {
27 $phpChecks[] = array(
28 'Extension: ' . $ext,
29 extension_loaded($ext),
30 extension_loaded($ext) ? 'Loaded' : ($required ? 'Missing (required)' : 'Missing (optional)'),
31 );
32}
33
34// --- Database ------------------------------------------------------------
35$dbSize = db()->fetchOne("
36 SELECT
37 ROUND(SUM(data_length + index_length)) AS `bytes`,
38 COUNT(*) AS `tables`
39 FROM `information_schema`.`tables`
40 WHERE `table_schema` = DATABASE();
41");
42$dbBytes = (int)($dbSize['bytes'] ?? 0);
43$dbTables = (int)($dbSize['tables'] ?? 0);
44
45// --- Cache -----------------------------------------------------------
46$cacheStats = function_exists('znote_cache_stats') ? znote_cache_stats() : array('files' => 0, 'bytes' => 0, 'apcu_available' => false);
47
48// --- Migrations --------------------------------------------------------
49$migrationStatus = function_exists('znote_migrations_status') ? znote_migrations_status() : array();
50$pendingMigrations = 0;
51foreach ($migrationStatus as $m) {
52 if (($m['state'] ?? '') === 'pending') $pendingMigrations++;
53}
54
55// --- Disk --------------------------------------------------------------
56$diskFree = @disk_free_space(dirname(__DIR__, 2));
57$diskTotal = @disk_total_space(dirname(__DIR__, 2));
58
59// --- Error log -----------------------------------------------------------
60$errorLogPath = trim((string)(setting('error_log:path', '') ?: (string)@ini_get('error_log')));
61$errorLogSize = ($errorLogPath !== '' && is_file($errorLogPath)) ? (int)filesize($errorLogPath) : null;
62$errorLogModified = ($errorLogPath !== '' && is_file($errorLogPath)) ? (int)filemtime($errorLogPath) : null;
63
64// --- Backups -------------------------------------------------------------
65$backups = function_exists('znote_backups_list') ? znote_backups_list() : array();
66$lastBackup = $backups[0] ?? null;
67?>
68
69<div class="acp-stats">
70 <?php
71 acp_stat(t_default('acp.health.stat_db_size', 'Database size'), acp_health_bytes($dbBytes), 'fa-database', null, 'blue');
72 acp_stat(t_default('acp.health.stat_cache', 'Cache entries'), number_format((int)($cacheStats['files'] ?? 0)), 'fa-bolt', acp_url('settings'), 'purple');
73 acp_stat(t_default('acp.health.stat_migrations', 'Pending migrations'), $pendingMigrations, 'fa-database', acp_url('migrations'), $pendingMigrations > 0 ? 'amber' : 'green');
74 acp_stat(t_default('acp.health.stat_backup', 'Last backup'), $lastBackup ? h(getClock((int)$lastBackup['time'], true)) : t_default('acp.health.no_backup', 'None yet'), 'fa-archive', acp_url('backups'), $lastBackup ? 'green' : 'amber');
75 ?>
76</div>
77
78<div class="acp-grid acp-grid--2">
79
80 <section class="acp-card">
81 <header class="acp-card-head">
82 <h2><?= t_default('acp.health.php_title', 'PHP & extensions') ?></h2>
83 </header>
84 <div class="acp-card-body is-flush">
85 <div class="acp-table-wrap">
86 <table class="acp-table">
87 <tbody>
88 <?php foreach ($phpChecks as $check): ?>
89 <tr>
90 <td><?= h($check[0]) ?></td>
91 <td class="is-num">
92 <span class="acp-pill acp-pill--<?= $check[1] ? 'green' : 'amber' ?>"><?= h($check[2]) ?></span>
93 </td>
94 </tr>
95 <?php endforeach; ?>
96 </tbody>
97 </table>
98 </div>
99 </div>
100 </section>
101
102 <section class="acp-card">
103 <header class="acp-card-head">
104 <h2><?= t_default('acp.health.storage_title', 'Storage') ?></h2>
105 </header>
106 <div class="acp-card-body is-flush">
107 <div class="acp-table-wrap">
108 <table class="acp-table">
109 <tbody>
110 <tr>
111 <td><?= t_default('acp.health.db_tables', 'Database tables') ?></td>
112 <td class="is-num"><?= number_format($dbTables) ?></td>
113 </tr>
114 <tr>
115 <td><?= t_default('acp.health.cache_size', 'Cache on disk') ?></td>
116 <td class="is-num"><?= h(acp_health_bytes((int)($cacheStats['bytes'] ?? 0))) ?></td>
117 </tr>
118 <tr>
119 <td><?= t_default('acp.health.apcu', 'APCu memory cache') ?></td>
120 <td class="is-num">
121 <span class="acp-pill acp-pill--<?= !empty($cacheStats['apcu_available']) ? 'green' : 'grey' ?>">
122 <?= !empty($cacheStats['apcu_available']) ? t_default('acp.health.available', 'Available') : t_default('acp.health.unavailable', 'Unavailable') ?>
123 </span>
124 </td>
125 </tr>
126 <?php if ($diskFree !== false && $diskTotal !== false && $diskTotal > 0): ?>
127 <tr>
128 <td><?= t_default('acp.health.disk_free', 'Disk free') ?></td>
129 <td class="is-num"><?= h(acp_health_bytes((int)$diskFree)) ?> / <?= h(acp_health_bytes((int)$diskTotal)) ?></td>
130 </tr>
131 <?php endif; ?>
132 </tbody>
133 </table>
134 </div>
135 </div>
136 </section>
137
138</div>
139
140<section class="acp-card">
141 <header class="acp-card-head">
142 <h2><?= t_default('acp.health.errorlog_title', 'Error log') ?></h2>
143 <a class="acp-btn acp-btn--ghost acp-btn--sm" href="<?= h(acp_url('error_log')) ?>" style="margin-left:auto;">
144 <?= t_default('acp.health.errorlog_open', 'Open') ?>
145 </a>
146 </header>
147 <div class="acp-card-body">
148 <?php if ($errorLogSize !== null): ?>
149 <p>
150 <?= t_default('acp.health.errorlog_summary', '{path} - {size}, last written {when}.', [
151 'path' => '<code>' . h($errorLogPath) . '</code>',
152 'size' => h(acp_health_bytes($errorLogSize)),
153 'when' => h(getClock((int)$errorLogModified, true)),
154 ]) ?>
155 </p>
156 <?php else: ?>
157 <p class="is-muted"><?= t_default('acp.health.errorlog_none', 'No log file found at the configured or auto-detected path.') ?></p>
158 <?php endif; ?>
159 </div>
160</section>
161