diff --git a/CHANGELOG.md b/CHANGELOG.md index e2dc4de..f71ea35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Recipe scraps tips** — During cooking steps, detect "waste" generated (peels, cores, bones, eggshells, coffee grounds, citrus zest, etc.) and surface AI-powered tips on how to reuse them (compost, natural cleaner, broth, candied peel, etc.). Could be shown as an optional collapsible hint card below the step that generates the scrap. +## [1.7.21] - 2026-05-20 + +### Changed +- **Startup health check** — Complete redesign from a banner checklist to a **real-time progress bar**. The bar fills smoothly as each of 29 diagnostic checks runs, with the current check name shown below in real time. Warnings (⚠️) are displayed as amber badges that remain visible for 2 seconds before the app proceeds. Critical failures turn the bar red and show a detailed error block with a Retry button. +- **29 comprehensive checks**: PHP version, 8 PHP extensions (pdo_sqlite, curl, json, mbstring, openssl, fileinfo, zip, intl), PHP memory/timeout/upload config, data directory, rate_limits dir, backups dir, disk write test, free disk space, SQLite connection, required tables, integrity (PRAGMA quick_check), WAL mode, DB size, inventory row count, .env file, Gemini AI key, Bring! credentials, Bring! token, cURL SSL, internet reachability. +- Warnings now clearly visible: each non-critical failure shows as a named amber badge (e.g. "⚠️ Bring! token") that cannot be missed. + ## [1.7.20] - 2026-05-20 ### Added diff --git a/api/index.php b/api/index.php index ebb8a86..fa22711 100644 --- a/api/index.php +++ b/api/index.php @@ -110,63 +110,202 @@ if (($_GET['action'] ?? '') === 'ping') { if (($_GET['action'] ?? '') === 'health_check') { $checks = []; - // 1. PHP version - $phpOk = version_compare(PHP_VERSION, '8.0.0', '>='); - $checks['php'] = ['ok' => $phpOk, 'value' => PHP_VERSION]; + // ── 1. PHP version ──────────────────────────────────────────────────────── + $checks['php_version'] = [ + 'ok' => version_compare(PHP_VERSION, '8.0.0', '>='), + '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)]; + // ── 2. Critical PHP extensions ──────────────────────────────────────────── + foreach (['pdo_sqlite', 'curl', 'json', 'mbstring'] as $ext) { + $checks['ext_' . $ext] = ['ok' => extension_loaded($ext)]; + } - // 3. data/ directory writable + // ── 3. Optional PHP extensions ──────────────────────────────────────────── + foreach (['openssl', 'fileinfo', 'zip', 'intl'] as $ext) { + $checks['ext_' . $ext] = ['ok' => extension_loaded($ext), 'optional' => true]; + } + + // ── 4. PHP runtime configuration ───────────────────────────────────────── + // Memory limit + $memRaw = ini_get('memory_limit'); + $memBytes = (function ($v) { + $v = trim($v); + if ($v === '-1') return PHP_INT_MAX; + $unit = strtolower(substr($v, -1)); + $num = (int) $v; + return match ($unit) { 'g' => $num * 1073741824, 'm' => $num * 1048576, 'k' => $num * 1024, default => $num }; + })($memRaw); + $checks['php_memory'] = ['ok' => $memBytes >= 64 * 1048576, 'value' => $memRaw, 'optional' => true]; + + // Max execution time + $maxExec = (int) ini_get('max_execution_time'); + $checks['php_max_exec'] = [ + 'ok' => $maxExec === 0 || $maxExec >= 30, + 'value' => $maxExec === 0 ? '∞' : $maxExec . 's', + 'optional' => true, + ]; + + // Upload size + $uploadRaw = ini_get('upload_max_filesize'); + $checks['php_upload'] = ['ok' => true, 'value' => $uploadRaw, 'optional' => true]; + + // ── 5. data/ directory ──────────────────────────────────────────────────── $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]; + if (!is_dir($dataDir)) @mkdir($dataDir, 0775, true); + $dataDirOk = is_dir($dataDir) && is_writable($dataDir); + $checks['data_dir'] = ['ok' => $dataDirOk, '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, + // data/rate_limits/ + $rlDir = $dataDir . '/rate_limits'; + if (!is_dir($rlDir)) @mkdir($rlDir, 0775, true); + $checks['data_rate_limits'] = ['ok' => is_dir($rlDir) && is_writable($rlDir), 'optional' => true]; + + // data/backups/ + $bkDir = $dataDir . '/backups'; + if (!is_dir($bkDir)) @mkdir($bkDir, 0775, true); + $checks['data_backups'] = ['ok' => is_dir($bkDir) && is_writable($bkDir), 'optional' => true]; + + // ── 6. Actual file-write test ───────────────────────────────────────────── + $testFile = $dataDir . '/_hc_' . getmypid() . '.tmp'; + $writeOk = $dataDirOk && (@file_put_contents($testFile, 'hc') !== false); + if ($writeOk) @unlink($testFile); + $checks['data_write_test'] = ['ok' => $writeOk]; + + // ── 7. Free disk space ──────────────────────────────────────────────────── + $freeBytes = $dataDirOk ? @disk_free_space($dataDir) : false; + $freeMB = $freeBytes !== false ? round($freeBytes / 1048576) : null; + $checks['disk_space'] = [ + 'ok' => $freeBytes === false || $freeBytes > 50 * 1048576, + 'value' => $freeMB !== null ? $freeMB . ' MB liberi' : null, + 'optional' => true, + ]; + + // ── 8. SQLite database ──────────────────────────────────────────────────── + $dbPath = $dataDir . '/dispensa.db'; + $isFresh = !file_exists($dbPath) && $dataDirOk; + + if ($isFresh) { + // Fresh install: DB will be created automatically on first real API call + $checks['db_connect'] = ['ok' => true, 'fresh' => true, 'value' => 'nuovo impianto']; + $checks['db_tables'] = ['ok' => true, 'fresh' => true]; + $checks['db_integrity'] = ['ok' => true, 'fresh' => true]; + $checks['db_wal'] = ['ok' => true, 'fresh' => true, 'optional' => true]; + $checks['db_size'] = ['ok' => true, 'value' => '0 KB', 'optional' => true]; + $checks['db_row_count'] = ['ok' => true, 'value' => '0 prodotti', 'optional' => true]; + } else { + $pdo = null; $dbConnOk = false; + try { + $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'); + $dbConnOk = true; + $checks['db_connect'] = ['ok' => true]; + } catch (\Throwable $e) { + $checks['db_connect'] = ['ok' => false, 'error' => $e->getMessage()]; + } + + if ($dbConnOk && $pdo) { + // Required tables + try { + $tables = $pdo->query("SELECT name FROM sqlite_master WHERE type='table'")->fetchAll(PDO::FETCH_COLUMN); + $required = ['inventory', 'products', 'transactions']; + $missing = array_values(array_diff($required, $tables)); + $checks['db_tables'] = ['ok' => empty($missing), 'missing' => $missing]; + } catch (\Throwable $e) { + $checks['db_tables'] = ['ok' => false, 'error' => $e->getMessage()]; + } + + // Integrity (fast) + try { + $integ = $pdo->query("PRAGMA quick_check")->fetchColumn(); + $checks['db_integrity'] = ['ok' => $integ === 'ok', 'value' => $integ !== 'ok' ? $integ : null]; + } catch (\Throwable $e) { + $checks['db_integrity'] = ['ok' => false, 'error' => $e->getMessage()]; + } + + // WAL mode + try { + $wal = $pdo->query("PRAGMA journal_mode")->fetchColumn(); + $checks['db_wal'] = ['ok' => $wal === 'wal', 'value' => $wal, 'optional' => true]; + } catch (\Throwable $e) { + $checks['db_wal'] = ['ok' => false, 'optional' => true]; + } + + // DB file size + $dbSizeKB = round(filesize($dbPath) / 1024); + $checks['db_size'] = ['ok' => true, 'value' => $dbSizeKB . ' KB', 'optional' => true]; + + // Row count + try { + $cnt = $pdo->query("SELECT COUNT(*) FROM inventory WHERE quantity > 0")->fetchColumn(); + $checks['db_row_count'] = ['ok' => true, 'value' => $cnt . ' prodotti in inventario', 'optional' => true]; + } catch (\Throwable $e) { + $checks['db_row_count'] = ['ok' => true, 'value' => '?', 'optional' => true]; + } + } else { + foreach (['db_tables', 'db_integrity'] as $k) $checks[$k] = ['ok' => false]; + foreach (['db_wal', 'db_size', 'db_row_count'] as $k) $checks[$k] = ['ok' => false, 'optional' => true]; + } + } + + // ── 9. .env file ────────────────────────────────────────────────────────── + $checks['env_file'] = ['ok' => file_exists(__DIR__ . '/../.env'), 'optional' => true]; + + // ── 10. Gemini AI key ───────────────────────────────────────────────────── + $checks['gemini_key'] = ['ok' => !empty(env('GEMINI_API_KEY')), 'optional' => true]; + + // ── 11. Bring! credentials & token ──────────────────────────────────────── + $checks['bring_credentials'] = [ + 'ok' => !empty(env('BRING_EMAIL')) && !empty(env('BRING_PASSWORD')), + 'optional' => true, + ]; + $checks['bring_token'] = ['ok' => !empty(env('BRING_ACCESS_TOKEN')), 'optional' => true]; + + // ── 12. cURL SSL support ────────────────────────────────────────────────── + if (function_exists('curl_version')) { + $cv = curl_version(); + $checks['curl_ssl'] = [ + 'ok' => !empty($cv['ssl_version']), + 'value' => $cv['ssl_version'] ?? null, + 'optional' => true, + ]; + } else { + $checks['curl_ssl'] = ['ok' => false, 'optional' => true]; + } + + // ── 13. Internet / Gemini API reachability ──────────────────────────────── + $internetOk = false; + if (extension_loaded('curl')) { + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => 'https://generativelanguage.googleapis.com/', + CURLOPT_NOBODY => true, + CURLOPT_FOLLOWLOCATION => false, + CURLOPT_TIMEOUT => 3, + CURLOPT_CONNECTTIMEOUT => 2, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_SSL_VERIFYPEER => false, ]); - $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(); + curl_exec($ch); + $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlErr = curl_errno($ch); + curl_close($ch); + $internetOk = ($httpCode > 0) || ($curlErr === 0); } - $checks['database'] = ['ok' => $dbOk, 'error' => $dbError ?: null]; + $checks['internet'] = ['ok' => $internetOk, 'optional' => true]; - // 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); + // ── Compute overall result ──────────────────────────────────────────────── + $criticalKeys = [ + 'php_version', 'ext_pdo_sqlite', 'ext_curl', 'ext_json', 'ext_mbstring', + 'data_dir', 'data_write_test', 'db_connect', 'db_tables', 'db_integrity', + ]; + $allOk = array_reduce($criticalKeys, fn($c, $k) => $c && ($checks[$k]['ok'] ?? false), true); header('Content-Type: application/json'); - echo json_encode(['ok' => $allOk, 'checks' => $checks], JSON_UNESCAPED_UNICODE); + echo json_encode(['ok' => $allOk, 'checks' => $checks, 'fresh' => $isFresh ?? false], JSON_UNESCAPED_UNICODE); exit; } diff --git a/assets/css/style.css b/assets/css/style.css index 830dcf0..2922b42 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -116,41 +116,72 @@ body { letter-spacing: 0.5px; margin-top: -8px; } -/* ── Startup health check list ─────────────────────────────────────── */ -.preloader-checks { +/* ── Startup progress bar ───────────────────────────────────────────── */ +.preloader-progress-wrap { display: flex; flex-direction: column; - gap: 6px; - width: 240px; - max-width: 90vw; + align-items: center; + gap: 10px; + width: 250px; + max-width: 88vw; + animation: zwFadeIn 0.2s ease; +} +.preloader-bar-track { + width: 100%; + height: 6px; + background: rgba(255,255,255,0.12); + border-radius: 99px; + overflow: hidden; +} +.preloader-bar { + height: 100%; + width: 0%; + background: linear-gradient(90deg, #4ade80, #22c55e); + border-radius: 99px; + transition: width 0.18s ease, background 0.3s ease; +} +.preloader-bar.bar-error { background: linear-gradient(90deg, #f87171, #ef4444); } +.preloader-bar.bar-warn { background: linear-gradient(90deg, #fbbf24, #f59e0b); } +.preloader-check-label { + color: rgba(255,255,255,0.60); + font-size: 0.74rem; + font-family: monospace; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; + min-height: 1.1em; + letter-spacing: 0.01em; +} +.preloader-warnings { + display: flex; + flex-wrap: wrap; + gap: 5px; + justify-content: center; + max-width: 270px; animation: zwFadeIn 0.25s ease; } -.preloader-check-row { - display: flex; - align-items: center; - gap: 8px; - font-size: 0.82rem; - color: rgba(255,255,255,0.80); - background: rgba(255,255,255,0.07); - border-radius: 8px; - padding: 5px 10px; - transition: background 0.2s; +.preloader-warn-badge { + background: rgba(251,191,36,0.15); + color: #fcd34d; + border: 1px solid rgba(251,191,36,0.35); + border-radius: 99px; + padding: 3px 10px; + font-size: 0.71rem; + white-space: nowrap; } -.preloader-check-row[data-state="ok"] { background: rgba(74,222,128,0.10); color: rgba(255,255,255,0.92); } -.preloader-check-row[data-state="warn"] { background: rgba(251,191,36,0.12); color: rgba(255,255,255,0.92); } -.preloader-check-row[data-state="error"] { background: rgba(239,68,68,0.15); color: #fca5a5; } -.pck-icon { font-size: 1rem; line-height: 1; flex-shrink: 0; } -.pck-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .preloader-error-msg { color: #fca5a5; background: rgba(239,68,68,0.18); border: 1px solid rgba(239,68,68,0.4); border-radius: 10px; - padding: 10px 16px; - font-size: 0.88rem; + padding: 12px 18px; + font-size: 0.84rem; text-align: center; max-width: 280px; - line-height: 1.4; + line-height: 1.5; + white-space: pre-line; animation: zwFadeIn 0.3s ease; } .preloader-retry-btn { diff --git a/assets/js/app.js b/assets/js/app.js index 27c4008..aaebbb8 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -14876,97 +14876,162 @@ function _heartbeatRetry() { // ── Startup / Splash health check ──────────────────────────────────────────── /** * Run a comprehensive server-side diagnostic during the splash screen. + * Shows a real-time progress bar + current check label. * Returns true if the app can proceed, false if a critical check failed. */ async function _runStartupCheck() { - const checksEl = document.getElementById('preloader-checks'); - const errorEl = document.getElementById('preloader-error-msg'); - const retryBtn = document.getElementById('preloader-retry-btn'); - const spinnerEl = document.getElementById('preloader-spinner'); + const spinnerEl = document.getElementById('preloader-spinner'); + const wrapEl = document.getElementById('preloader-progress-wrap'); + const barEl = document.getElementById('preloader-bar'); + const labelEl = document.getElementById('preloader-check-label'); + const warningsEl = document.getElementById('preloader-warnings'); + const errorEl = document.getElementById('preloader-error-msg'); + const retryBtn = document.getElementById('preloader-retry-btn'); - if (!checksEl) return true; // preloader already removed + if (!wrapEl) return true; // preloader already removed - // Label map (populated again after translations are available) - const label = (key, fallback) => { - try { return t('startup.' + key); } catch(e) { return fallback; } - }; + const tl = (key, fallback) => { try { return t('startup.' + key); } catch(e) { return fallback; } }; - // Show check list container - checksEl.innerHTML = ''; - checksEl.style.display = ''; + // Switch from spinner to progress bar + if (spinnerEl) spinnerEl.style.display = 'none'; + wrapEl.style.display = ''; - // Helper: add / update a row - const addRow = (id, text, state) => { - const icon = state === 'ok' ? '✅' : state === 'warn' ? '⚠️' : state === 'loading' ? '⏳' : '❌'; - let row = document.getElementById('startup-row-' + id); - if (!row) { - row = document.createElement('div'); - row.id = 'startup-row-' + id; - row.className = 'preloader-check-row'; - checksEl.appendChild(row); + // Helper: set progress bar + label + let _curPct = 0; + const setProgress = (pct, label, state) => { + _curPct = pct; + if (barEl) { + barEl.style.width = pct + '%'; + barEl.className = 'preloader-bar' + (state === 'error' ? ' bar-error' : state === 'warn' ? ' bar-warn' : ''); } - row.innerHTML = `${text}`; - row.dataset.state = state; + if (labelEl) labelEl.textContent = label || ''; }; - // Show loading rows immediately so the splash looks active - const checkDefs = [ - { id: 'php', key: 'check_php', fallback: 'PHP' }, - { id: 'exts', key: 'check_exts', fallback: 'Estensioni PHP' }, - { id: 'data', key: 'check_data_dir', fallback: 'Cartella dati' }, - { id: 'db', key: 'check_db', fallback: 'Database' }, - { id: 'env', key: 'check_env', fallback: 'Configurazione' }, - { id: 'gemini', key: 'check_gemini', fallback: 'Chiave Gemini AI' }, - { id: 'bring', key: 'check_bring', fallback: 'Bring! token' }, - ]; - checkDefs.forEach(c => addRow(c.id, label(c.key, c.fallback), 'loading')); + // Phase 1: animate 0→15% while fetching (so it never looks stuck) + setProgress(0, tl('connecting', 'Connessione al server...')); + let _fetchDone = false; + const slowAnim = setInterval(() => { + if (!_fetchDone && _curPct < 13) setProgress(_curPct + 1, labelEl?.textContent); + }, 100); - // Do the actual request + // Make the request let result = null; try { const ctrl = new AbortController(); - const tid = setTimeout(() => ctrl.abort(), 8000); + const tid = setTimeout(() => ctrl.abort(), 12000); const resp = await fetch('api/index.php?action=health_check', { signal: ctrl.signal }); clearTimeout(tid); result = await resp.json(); } catch(e) { - // Network or timeout error — cannot reach server at all - if (spinnerEl) spinnerEl.style.display = 'none'; - checksEl.style.display = 'none'; - const msg = label('error_network', 'Impossibile contattare il server. Controlla la connessione.'); - errorEl.textContent = msg; + clearInterval(slowAnim); + setProgress(100, tl('error_network', 'Impossibile contattare il server'), 'error'); + errorEl.textContent = tl('error_network', 'Impossibile contattare il server. Controlla la connessione di rete.'); + errorEl.style.display = ''; + retryBtn.style.display = ''; + return false; + } + clearInterval(slowAnim); + _fetchDone = true; + + // ── Ordered check definitions (must match PHP keys) ─────────────────────── + // { key, label, critical } + const CHECKS = [ + // PHP runtime + { key: 'php_version', label: 'PHP', critical: true }, + { key: 'ext_pdo_sqlite', label: 'PDO SQLite', critical: true }, + { key: 'ext_curl', label: 'cURL', critical: true }, + { key: 'ext_json', label: 'JSON', critical: true }, + { key: 'ext_mbstring', label: 'mbstring', critical: true }, + { key: 'ext_openssl', label: 'OpenSSL', critical: false }, + { key: 'ext_fileinfo', label: 'Fileinfo', critical: false }, + { key: 'ext_zip', label: 'ZIP', critical: false }, + { key: 'ext_intl', label: 'Intl', critical: false }, + { key: 'php_memory', label: tl('check_php_memory', 'Memoria PHP'), critical: false }, + { key: 'php_max_exec', label: tl('check_php_timeout', 'Timeout PHP'), critical: false }, + { key: 'php_upload', label: tl('check_php_upload', 'Upload PHP'), critical: false }, + // Filesystem + { key: 'data_dir', label: tl('check_data_dir', 'Cartella dati'), critical: true }, + { key: 'data_rate_limits', label: tl('check_rate_limits', 'Rate limits dir'),critical: false }, + { key: 'data_backups', label: tl('check_backups', 'Backup dir'), critical: false }, + { key: 'data_write_test', label: tl('check_write_test', 'Test scrittura'), critical: true }, + { key: 'disk_space', label: tl('check_disk_space', 'Spazio disco'), critical: false }, + // Database + { key: 'db_connect', label: tl('check_db_connect', 'Connessione DB'), critical: true }, + { key: 'db_tables', label: tl('check_db_tables', 'Tabelle DB'), critical: true }, + { key: 'db_integrity', label: tl('check_db_integrity','Integrità DB'), critical: true }, + { key: 'db_wal', label: tl('check_db_wal', 'WAL mode'), critical: false }, + { key: 'db_size', label: tl('check_db_size', 'Dimensione DB'), critical: false }, + { key: 'db_row_count', label: tl('check_db_rows', 'Dati inventario'),critical: false }, + // Config + { key: 'env_file', label: tl('check_env', 'File .env'), critical: false }, + { key: 'gemini_key', label: tl('check_gemini', 'Gemini AI key'), critical: false }, + { key: 'bring_credentials', label: tl('check_bring_creds', 'Bring! credenziali'), critical: false }, + { key: 'bring_token', label: tl('check_bring_token', 'Bring! token'), critical: false }, + // Network + { key: 'curl_ssl', label: tl('check_curl_ssl', 'cURL SSL'), critical: false }, + { key: 'internet', label: tl('check_internet', 'Internet'), critical: false }, + ]; + + const checks = result.checks || {}; + const warnings = []; + const errors = []; + const total = CHECKS.filter(d => checks[d.key] !== undefined).length; + let done = 0; + + // Phase 2: step through each check with real-time label + for (const def of CHECKS) { + const c = checks[def.key]; + if (c === undefined) continue; // not returned by server + + done++; + const pct = 15 + Math.round((done / total) * 83); // 15→98% + const isOk = c.ok === true; + const isOpt = c.optional === true || !def.critical; + const isFresh = c.fresh === true; + + // Build label: "check name (extra value)" + let lbl = def.label; + if (c.value) lbl += ` (${c.value})`; + if (isFresh) lbl += ` — ${tl('fresh_install', 'nuovo impianto')}`; + if (!isOk && c.error) lbl += ` — ${c.error}`; + if (!isOk && c.missing?.length) lbl += ` — mancanti: ${c.missing.join(', ')}`; + + const icon = isOk ? '✅' : isOpt ? '⚠️' : '❌'; + setProgress(pct, `${icon} ${lbl}`); + + if (!isOk && !isFresh) { + (isOpt ? warnings : errors).push({ def, c }); + } + + await new Promise(r => setTimeout(r, 45)); // ~45ms per step → ~1.3s total + } + + // ── Completed ───────────────────────────────────────────────────────────── + if (errors.length > 0) { + setProgress(100, `❌ ${tl('critical_error_short', 'Errore critico')}`, 'error'); + const errDetail = errors.map(e => e.def.label + (e.c.error ? `: ${e.c.error}` : '')).join('\n'); + errorEl.textContent = `${tl('critical_error', 'Errore critico: l\'app non può avviarsi.')}${errDetail ? '\n' + errDetail : ''}`; errorEl.style.display = ''; retryBtn.style.display = ''; return false; } - const c = result.checks || {}; - - // Update each row - addRow('php', label('check_php', 'PHP') + (c.php?.value ? ` ${c.php.value}` : ''), c.php?.ok ? 'ok' : 'error'); - addRow('exts', label('check_exts', 'Estensioni PHP') + (c.php_extensions?.missing?.length ? ` (mancanti: ${c.php_extensions.missing.join(', ')})` : ''), c.php_extensions?.ok ? 'ok' : 'error'); - addRow('data', label('check_data_dir', 'Cartella dati'), c.data_dir?.ok ? 'ok' : 'error'); - addRow('db', label('check_db', 'Database') + (c.database?.error ? ` (${c.database.error})` : ''), c.database?.ok ? 'ok' : 'error'); - addRow('env', label('check_env', 'Configurazione'), c.env_file?.ok ? 'ok' : 'warn'); - addRow('gemini', label('check_gemini', 'Chiave Gemini AI'), c.gemini_key?.ok ? 'ok' : 'warn'); - addRow('bring', label('check_bring', 'Bring! token'), c.bring_token?.ok ? 'ok' : 'warn'); - - const allOk = result.ok === true; - - if (allOk) { - // Brief pause so the user sees the green checkmarks, then hide checks - await new Promise(r => setTimeout(r, 1200)); - checksEl.style.display = 'none'; - return true; + // Warnings only + if (warnings.length > 0) { + setProgress(100, `⚠️ ${warnings.length} ${tl('warnings_found', 'avvisi rilevati')}`, 'warn'); + warningsEl.innerHTML = warnings + .map(w => `⚠️ ${w.def.label}`) + .join(''); + warningsEl.style.display = ''; + await new Promise(r => setTimeout(r, 2200)); // show warnings for 2.2s + warningsEl.style.display = 'none'; } else { - // Critical failure — keep preloader visible, hide spinner, show error - if (spinnerEl) spinnerEl.style.display = 'none'; - const errMsg = label('critical_error', 'Errore critico: l\'app non può avviarsi. Controlla i log del server.'); - errorEl.textContent = errMsg; - errorEl.style.display = ''; - retryBtn.style.display = ''; - return false; + setProgress(100, `✅ ${tl('all_ok', 'Sistema OK')}`); + await new Promise(r => setTimeout(r, 700)); } + + wrapEl.style.display = 'none'; + return true; } /** Retry button handler in the startup error screen. */ diff --git a/index.html b/index.html index 30d5f01..21e5fb9 100644 --- a/index.html +++ b/index.html @@ -11,7 +11,7 @@
-
+
+
- v1.7.20
+ v1.7.21