feat: startup health check during splash screen (v1.7.20)

- Add ?action=health_check PHP endpoint (early-exit, before rate-limiter)
  Checks: PHP version, required extensions, data/ writability, SQLite DB
  connection + table integrity, .env file, Gemini AI key, Bring! token
- Display animated checklist in splash screen with per-item icons
  (ok/warn/error); critical failures block app launch with clear error
  message and Retry button; optional warnings shown but don't block
- New JS: _runStartupCheck(), _startupRetry(); called first in _initApp()
- New HTML elements in #app-preloader: #preloader-checks, #preloader-error-msg,
  #preloader-retry-btn (hidden until startup check completes)
- New CSS: .preloader-checks, .preloader-check-row, .preloader-error-msg,
  .preloader-retry-btn with state colors (ok=green, warn=amber, error=red)
- Translations: startup.* keys (10 per language) in IT, EN, DE, FR, ES
- Asset version bump: v=20260520a
This commit is contained in:
dadaloop82
2026-05-17 09:40:11 +00:00
parent 8750e44687
commit d3b119c7fe
11 changed files with 294 additions and 6 deletions
+64
View File
@@ -106,6 +106,70 @@ if (($_GET['action'] ?? '') === 'ping') {
exit;
}
// ── Health check — startup diagnostic (no rate-limit, no auth required) ──────
if (($_GET['action'] ?? '') === 'health_check') {
$checks = [];
// 1. PHP version
$phpOk = version_compare(PHP_VERSION, '8.0.0', '>=');
$checks['php'] = ['ok' => $phpOk, 'value' => PHP_VERSION];
// 2. Required PHP extensions
$requiredExts = ['pdo_sqlite', 'curl', 'mbstring', 'json'];
$missingExts = array_filter($requiredExts, fn($e) => !extension_loaded($e));
$checks['php_extensions'] = ['ok' => empty($missingExts), 'missing' => array_values($missingExts)];
// 3. data/ directory writable
$dataDir = __DIR__ . '/../data';
$dataWritable = is_dir($dataDir) && is_writable($dataDir);
if (!$dataWritable && !is_dir($dataDir)) {
@mkdir($dataDir, 0775, true);
$dataWritable = is_dir($dataDir) && is_writable($dataDir);
}
$checks['data_dir'] = ['ok' => $dataWritable, 'path' => realpath($dataDir) ?: $dataDir];
// 4. SQLite DB accessible
$dbOk = false; $dbError = '';
try {
$dbPath = $dataDir . '/dispensa.db';
$pdo = new PDO('sqlite:' . $dbPath, null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$pdo->query('SELECT 1');
// Check at least inventory table exists
$tables = $pdo->query("SELECT name FROM sqlite_master WHERE type='table'")->fetchAll(PDO::FETCH_COLUMN);
$dbOk = in_array('inventory', $tables);
if (!$dbOk) $dbError = 'Missing tables (fresh install?)';
} catch (\Throwable $e) {
$dbError = $e->getMessage();
}
$checks['database'] = ['ok' => $dbOk, 'error' => $dbError ?: null];
// 5. .env loaded + Gemini key present
$envPath = __DIR__ . '/../.env';
$envLoaded = file_exists($envPath);
$geminiKey = env('GEMINI_API_KEY');
$checks['env_file'] = ['ok' => $envLoaded];
$checks['gemini_key'] = ['ok' => !empty($geminiKey)];
// 6. Bring! token (optional — warning only)
$bringToken = env('BRING_ACCESS_TOKEN');
$checks['bring_token'] = ['ok' => !empty($bringToken), 'optional' => true];
// 7. cURL available + internet reachable (light check, no actual call)
$curlOk = function_exists('curl_init');
$checks['curl'] = ['ok' => $curlOk];
// Overall: critical = php, php_extensions, data_dir, database
$critical = ['php', 'php_extensions', 'data_dir', 'database'];
$allOk = array_reduce($critical, fn($c, $k) => $c && ($checks[$k]['ok'] ?? false), true);
header('Content-Type: application/json');
echo json_encode(['ok' => $allOk, 'checks' => $checks], JSON_UNESCAPED_UNICODE);
exit;
}
// ===== RATE LIMITING =====
/**
* Simple file-based rate limiter.