diff --git a/CHANGELOG.md b/CHANGELOG.md index f71ea35..7b5b5d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,22 @@ 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.22] - 2026-05-17 + +### Fixed +- **DB name corrected** — `health_check` now looks for `evershelf.db` (was wrongly looking for `dispensa.db`). Auto-migration included: if `evershelf.db` is missing but `dispensa.db` exists, it is renamed automatically on startup. +- **Removed legacy `data/dispensa.db`** — the old database file has been deleted; only `evershelf.db` is used. +- **Conditional checks** — Bring!, TTS, Scale and Internet checks only run when the respective feature is enabled in `.env` (no more false ❌/⚠️ for unconfigured features). +- **Backups check** — no longer checks if `data/backups/` is writable by www-data (cron writes as root). Now checks that backup files actually exist and the most recent one is recent. +- **Bring! token check** — reads `data/bring_token.json` file instead of looking for a non-existent `BRING_ACCESS_TOKEN` env var. + +### Changed +- **Warning popup with 5s countdown** — when non-critical checks fail at startup, a styled popup appears showing each warning with its label and a plain-language hint explaining the problem. A countdown bar auto-closes the popup after 5 seconds, then the app starts normally. +- **Error blocking popup** — when critical checks fail, a clear blocking panel shows with title "Errore critico", each failed check listed with its explanation hint, and a Retry button. The app does not start. +- **`db_legacy` check added** — warns (optional) if the old `dispensa.db` file is still present alongside `evershelf.db`. +- **32 total checks** — added `db_legacy`, `tts_url`, `scale_gateway` to the check set (conditional). +- **Hint messages** — every check now has an Italian-language `hint` field explaining what is wrong and how to fix it. + ## [1.7.21] - 2026-05-20 ### Changed diff --git a/api/index.php b/api/index.php index fa22711..668987f 100644 --- a/api/index.php +++ b/api/index.php @@ -110,6 +110,10 @@ if (($_GET['action'] ?? '') === 'ping') { if (($_GET['action'] ?? '') === 'health_check') { $checks = []; + // ── Helper: read .env values without triggering app init ───────────────── + $envVals = loadEnv(); // already cached by loadEnv() + $envGet = fn($k) => $envVals[$k] ?? ''; + // ── 1. PHP version ──────────────────────────────────────────────────────── $checks['php_version'] = [ 'ok' => version_compare(PHP_VERSION, '8.0.0', '>='), @@ -127,28 +131,16 @@ if (($_GET['action'] ?? '') === 'health_check') { } // ── 4. PHP runtime configuration ───────────────────────────────────────── - // Memory limit - $memRaw = ini_get('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 }; + $v = trim($v); if ($v === '-1') return PHP_INT_MAX; + $u = strtolower(substr($v, -1)); $n = (int)$v; + return match($u) { 'g' => $n*1073741824, 'm' => $n*1048576, 'k' => $n*1024, default => $n }; })($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]; + $checks['php_memory'] = ['ok' => $memBytes >= 64*1048576, 'value' => $memRaw, 'optional' => true]; + $maxExec = (int) ini_get('max_execution_time'); + $checks['php_max_exec'] = ['ok' => $maxExec === 0 || $maxExec >= 30, 'value' => $maxExec === 0 ? '∞' : $maxExec.'s', 'optional' => true]; + $checks['php_upload'] = ['ok' => true, 'value' => ini_get('upload_max_filesize'), 'optional' => true]; // ── 5. data/ directory ──────────────────────────────────────────────────── $dataDir = __DIR__ . '/../data'; @@ -158,13 +150,24 @@ if (($_GET['action'] ?? '') === 'health_check') { // data/rate_limits/ $rlDir = $dataDir . '/rate_limits'; - if (!is_dir($rlDir)) @mkdir($rlDir, 0775, true); + if (!is_dir($rlDir) && $dataDirOk) @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]; + // data/backups/ — written by cron as root; just verify dir exists and has recent files + $bkDir = $dataDir . '/backups'; + $bkDirExists = is_dir($bkDir); + $bkFiles = $bkDirExists ? array_filter(scandir($bkDir), fn($f) => str_ends_with($f, '.db')) : []; + $lastBkTime = $bkDirExists && $bkFiles + ? max(array_map(fn($f) => filemtime($bkDir.'/'.$f), $bkFiles)) + : 0; + $bkRecent = $lastBkTime > 0 && (time() - $lastBkTime) < 86400*2; // within 2 days + $bkCount = count($bkFiles); + $checks['data_backups'] = [ + 'ok' => $bkDirExists && $bkCount > 0, + 'optional' => true, + 'value' => $bkDirExists ? ($bkCount . ' backup' . ($bkRecent ? ', ultimo recente' : ', ultimo vecchio')) : null, + 'hint' => $bkDirExists ? ($bkCount === 0 ? 'Nessun backup trovato — cron configurato?' : (!$bkRecent ? 'Ultimo backup datato — cron in esecuzione?' : null)) : 'Cartella backup mancante', + ]; // ── 6. Actual file-write test ───────────────────────────────────────────── $testFile = $dataDir . '/_hc_' . getmypid() . '.tmp'; @@ -174,19 +177,37 @@ if (($_GET['action'] ?? '') === 'health_check') { // ── 7. Free disk space ──────────────────────────────────────────────────── $freeBytes = $dataDirOk ? @disk_free_space($dataDir) : false; - $freeMB = $freeBytes !== false ? round($freeBytes / 1048576) : null; + $freeMB = $freeBytes !== false ? round($freeBytes/1048576) : null; $checks['disk_space'] = [ - 'ok' => $freeBytes === false || $freeBytes > 50 * 1048576, - 'value' => $freeMB !== null ? $freeMB . ' MB liberi' : null, + 'ok' => $freeBytes === false || $freeBytes > 50*1048576, + 'value' => $freeMB !== null ? $freeMB.' MB liberi' : null, 'optional' => true, + 'hint' => $freeBytes !== false && $freeBytes <= 50*1048576 ? 'Meno di 50 MB liberi — libera spazio sul disco' : null, ]; // ── 8. SQLite database ──────────────────────────────────────────────────── - $dbPath = $dataDir . '/dispensa.db'; - $isFresh = !file_exists($dbPath) && $dataDirOk; + // Correct DB name is evershelf.db; detect legacy dispensa.db and suggest migration + $dbPath = $dataDir . '/evershelf.db'; + $legacyDb = $dataDir . '/dispensa.db'; + $hasLegacy = file_exists($legacyDb); + $isFresh = !file_exists($dbPath) && $dataDirOk; + + // Auto-migrate: if evershelf.db missing but dispensa.db exists, rename it + if ($isFresh && $hasLegacy && is_writable($legacyDb)) { + if (@rename($legacyDb, $dbPath)) { + $hasLegacy = false; + $isFresh = false; + } + } + + // Legacy DB still present alongside evershelf.db → warn + $checks['db_legacy'] = [ + 'ok' => !$hasLegacy, + 'optional' => true, + 'hint' => $hasLegacy ? 'Trovato vecchio dispensa.db — il file è ormai obsoleto, puoi eliminarlo manualmente' : null, + ]; 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]; @@ -202,110 +223,138 @@ if (($_GET['action'] ?? '') === 'health_check') { ]); $pdo->query('SELECT 1'); $dbConnOk = true; - $checks['db_connect'] = ['ok' => true]; + $checks['db_connect'] = ['ok' => true, 'value' => basename($dbPath)]; } catch (\Throwable $e) { - $checks['db_connect'] = ['ok' => false, 'error' => $e->getMessage()]; + $checks['db_connect'] = ['ok' => false, 'error' => $e->getMessage(), + 'hint' => 'Impossibile aprire il database — verifica permessi su data/evershelf.db']; } 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()]; - } + $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, + 'hint' => !empty($missing) ? 'Tabelle mancanti: ' . implode(', ', $missing) . ' — esegui una chiamata API per auto-inizializzare il DB' : null, + ]; - // 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()]; - } + // Integrity + $integ = $pdo->query("PRAGMA quick_check")->fetchColumn(); + $checks['db_integrity'] = [ + 'ok' => $integ === 'ok', + 'value' => $integ !== 'ok' ? $integ : null, + 'hint' => $integ !== 'ok' ? 'Database corrotto: ' . $integ . ' — ripristina da un backup in data/backups/' : null, + ]; - // 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]; - } + // WAL + $wal = $pdo->query("PRAGMA journal_mode")->fetchColumn(); + $checks['db_wal'] = ['ok' => $wal === 'wal', 'value' => $wal, 'optional' => true, + 'hint' => $wal !== 'wal' ? 'Modalità journal non ottimale — sarà corretta automaticamente al primo avvio' : null]; - // 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]; - } + // Size & rows + $checks['db_size'] = ['ok' => true, 'value' => round(filesize($dbPath)/1024).' KB', 'optional' => true]; + $cnt = $pdo->query("SELECT COUNT(*) FROM inventory WHERE quantity > 0")->fetchColumn(); + $checks['db_row_count'] = ['ok' => true, 'value' => $cnt.' prodotti in inventario', '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]; + foreach (['db_tables', 'db_integrity'] as $k) + $checks[$k] = ['ok' => false, 'hint' => 'Impossibile verificare — connessione DB fallita']; + 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')), + $envExists = file_exists(__DIR__ . '/../.env'); + $checks['env_file'] = [ + 'ok' => $envExists, 'optional' => true, + 'hint' => !$envExists ? 'File .env mancante — copia .env.example in .env e configura i valori' : null, ]; - $checks['bring_token'] = ['ok' => !empty(env('BRING_ACCESS_TOKEN')), 'optional' => true]; - // ── 12. cURL SSL support ────────────────────────────────────────────────── + // ── 10. Gemini AI — solo se GEMINI_API_KEY è impostata ─────────────────── + $geminiKey = $envGet('GEMINI_API_KEY'); + if (!empty($geminiKey)) { + $checks['gemini_key'] = ['ok' => strlen($geminiKey) > 20, 'optional' => true, + 'hint' => strlen($geminiKey) <= 20 ? 'Chiave Gemini AI sembra troppo corta — verifica il valore in .env' : null]; + } else { + $checks['gemini_key'] = ['ok' => false, 'optional' => true, + 'hint' => 'GEMINI_API_KEY non configurata — le funzioni AI non saranno disponibili']; + } + + // ── 11. Bring! — solo se EMAIL+PASSWORD sono impostate ─────────────────── + $bringEmail = $envGet('BRING_EMAIL'); + $bringPassword = $envGet('BRING_PASSWORD'); + $bringEnabled = !empty($bringEmail) && !empty($bringPassword); + if ($bringEnabled) { + $checks['bring_credentials'] = ['ok' => true, 'optional' => true]; + // Token: stored in data/bring_token.json (not in .env) + $bringTokenFile = $dataDir . '/bring_token.json'; + $bringTokenOk = false; + $bringTokenHint = null; + if (file_exists($bringTokenFile)) { + $bringData = @json_decode(@file_get_contents($bringTokenFile), true); + $bringTokenOk = !empty($bringData['access_token'] ?? ($bringData['accessToken'] ?? '')); + if (!$bringTokenOk) $bringTokenHint = 'Token Bring! presente ma non valido — verrà rinnovato automaticamente al prossimo accesso'; + } else { + $bringTokenHint = 'Token Bring! non ancora generato — verrà creato al primo accesso alla lista spesa'; + } + $checks['bring_token'] = ['ok' => $bringTokenOk, 'optional' => true, 'hint' => $bringTokenHint]; + } + // If Bring! not configured, skip entirely (no check at all) + + // ── 12. TTS — solo se TTS_ENABLED ──────────────────────────────────────── + if ($envGet('TTS_ENABLED') === 'true') { + $ttsUrl = $envGet('TTS_URL'); + $checks['tts_url'] = [ + 'ok' => !empty($ttsUrl), + 'optional' => true, + 'hint' => empty($ttsUrl) ? 'TTS_ENABLED=true ma TTS_URL non configurata' : null, + ]; + } + + // ── 13. Scale gateway — solo se SCALE_ENABLED ──────────────────────────── + if ($envGet('SCALE_ENABLED') === 'true') { + $scaleUrl = $envGet('SCALE_GATEWAY_URL'); + $checks['scale_gateway'] = [ + 'ok' => !empty($scaleUrl), + 'optional' => true, + 'hint' => empty($scaleUrl) ? 'SCALE_ENABLED=true ma SCALE_GATEWAY_URL non configurata' : null, + ]; + } + + // ── 14. cURL SSL ────────────────────────────────────────────────────────── if (function_exists('curl_version')) { $cv = curl_version(); - $checks['curl_ssl'] = [ - 'ok' => !empty($cv['ssl_version']), - 'value' => $cv['ssl_version'] ?? null, - 'optional' => true, - ]; + $checks['curl_ssl'] = ['ok' => !empty($cv['ssl_version']), 'value' => $cv['ssl_version'] ?? null, 'optional' => true, + 'hint' => empty($cv['ssl_version']) ? 'cURL senza supporto SSL — le chiamate HTTPS potrebbero fallire' : null]; } else { - $checks['curl_ssl'] = ['ok' => false, 'optional' => true]; + $checks['curl_ssl'] = ['ok' => false, 'optional' => true, 'hint' => 'cURL non disponibile']; } - // ── 13. Internet / Gemini API reachability ──────────────────────────────── - $internetOk = false; - if (extension_loaded('curl')) { + // ── 15. Internet — raggiungibilità API Gemini (solo se Gemini configurato) ─ + if (!empty($geminiKey) && 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, - ]); + curl_setopt_array($ch, [CURLOPT_URL => 'https://generativelanguage.googleapis.com/', CURLOPT_NOBODY => true, + CURLOPT_FOLLOWLOCATION => false, CURLOPT_TIMEOUT => 4, CURLOPT_CONNECTTIMEOUT => 3, + CURLOPT_RETURNTRANSFER => true, CURLOPT_SSL_VERIFYPEER => false]); curl_exec($ch); $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); - $curlErr = curl_errno($ch); + $curlErrNo = curl_errno($ch); curl_close($ch); - $internetOk = ($httpCode > 0) || ($curlErr === 0); + $internetOk = $httpCode > 0 || $curlErrNo === 0; + $checks['internet'] = ['ok' => $internetOk, 'optional' => true, + 'hint' => !$internetOk ? 'Impossibile raggiungere i server Gemini — le funzioni AI non funzioneranno senza connessione internet' : null]; } - $checks['internet'] = ['ok' => $internetOk, 'optional' => 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', - ]; + $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, 'fresh' => $isFresh ?? false], JSON_UNESCAPED_UNICODE); + echo json_encode(['ok' => $allOk, 'checks' => $checks, 'fresh' => $isFresh], JSON_UNESCAPED_UNICODE); exit; } diff --git a/assets/css/style.css b/assets/css/style.css index 2922b42..00e7df7 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -155,13 +155,88 @@ body { letter-spacing: 0.01em; } .preloader-warnings { - display: flex; - flex-wrap: wrap; - gap: 5px; - justify-content: center; - max-width: 270px; - animation: zwFadeIn 0.25s ease; + max-width: 310px; + width: 100%; + animation: zwFadeIn 0.3s ease; } + +/* ── Warning popup (auto-close 5s) ─────────────────────────── */ +.startup-popup { + border-radius: 12px; + overflow: hidden; + width: 100%; +} +.startup-popup-warn { + background: rgba(30,20,0,0.85); + border: 1px solid rgba(251,191,36,0.45); +} +.startup-popup-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 9px 14px 7px; + font-size: 0.82rem; + font-weight: 600; + color: #fcd34d; + gap: 8px; +} +.startup-popup-countdown { + background: rgba(251,191,36,0.2); + color: #fcd34d; + border-radius: 50%; + width: 22px; + height: 22px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: 700; + flex-shrink: 0; +} +.startup-popup-body { + padding: 2px 14px 10px; + display: flex; + flex-direction: column; + gap: 8px; +} +.startup-warn-item { + display: flex; + align-items: flex-start; + gap: 8px; +} +.startup-warn-icon { + font-size: 1rem; + flex-shrink: 0; + margin-top: 1px; +} +.startup-warn-body { + font-size: 0.78rem; + color: #d4c08a; + line-height: 1.4; +} +.startup-warn-body strong { + display: block; + color: #fcd34d; + font-size: 0.82rem; + margin-bottom: 2px; +} +.startup-warn-body p { + margin: 0; + color: #c8a954; +} +.startup-popup-bar-wrap { + height: 3px; + background: rgba(251,191,36,0.15); +} +.startup-popup-bar { + height: 3px; + background: #fbbf24; + width: 100%; + will-change: width; +} + +/* ── Error popup (blocking) ─────────────────────────────────── */ +/* Keep .preloader-warn-badge for backward compat */ .preloader-warn-badge { background: rgba(251,191,36,0.15); color: #fcd34d; @@ -175,15 +250,22 @@ body { color: #fca5a5; background: rgba(239,68,68,0.18); border: 1px solid rgba(239,68,68,0.4); - border-radius: 10px; - padding: 12px 18px; - font-size: 0.84rem; - text-align: center; - max-width: 280px; - line-height: 1.5; + border-radius: 12px; + padding: 14px 18px; + font-size: 0.80rem; + text-align: left; + max-width: 300px; + line-height: 1.6; white-space: pre-line; animation: zwFadeIn 0.3s ease; } +.preloader-error-msg strong { + display: block; + font-size: 0.9rem; + color: #f87171; + margin-bottom: 8px; + text-align: center; +} .preloader-retry-btn { background: #ef4444; color: #fff; diff --git a/assets/js/app.js b/assets/js/app.js index aaebbb8..c00ea18 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -14924,52 +14924,56 @@ async function _runStartupCheck() { result = await resp.json(); } catch(e) { 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 = ''; + _showStartupErrorPopup( + tl('error_network', 'Impossibile contattare il server'), + tl('error_network_detail', 'Il browser non riesce a raggiungere il server PHP.\n\nPossibili cause:\n• Il server Apache/PHP non è in esecuzione\n• Problema di rete o firewall\n• URL dell\'app non corretta\n\nControlla che il server sia avviato e riprova.'), + errorEl, retryBtn + ); + setProgress(100, `❌ ${tl('error_network', 'Server non raggiungibile')}`, 'error'); 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 }, + { 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 }, + { 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 }, + { key: 'db_legacy', label: tl('check_db_legacy', 'DB legacy'), critical: false }, + { 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 & optional features + { 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 }, + { key: 'tts_url', label: tl('check_tts', 'TTS URL'), critical: false }, + { key: 'scale_gateway', label: tl('check_scale', 'Scale gateway'), critical: false }, // Network - { key: 'curl_ssl', label: tl('check_curl_ssl', 'cURL SSL'), critical: false }, - { key: 'internet', label: tl('check_internet', 'Internet'), critical: false }, + { 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 || {}; @@ -14978,10 +14982,10 @@ async function _runStartupCheck() { const total = CHECKS.filter(d => checks[d.key] !== undefined).length; let done = 0; - // Phase 2: step through each check with real-time label + // Phase 2: step through each check with animated label for (const def of CHECKS) { const c = checks[def.key]; - if (c === undefined) continue; // not returned by server + if (c === undefined) continue; // not returned by server (feature not enabled) done++; const pct = 15 + Math.round((done / total) * 83); // 15→98% @@ -14989,10 +14993,10 @@ async function _runStartupCheck() { const isOpt = c.optional === true || !def.critical; const isFresh = c.fresh === true; - // Build label: "check name (extra value)" + // Build label with value let lbl = def.label; - if (c.value) lbl += ` (${c.value})`; - if (isFresh) lbl += ` — ${tl('fresh_install', 'nuovo impianto')}`; + 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(', ')}`; @@ -15003,37 +15007,100 @@ async function _runStartupCheck() { (isOpt ? warnings : errors).push({ def, c }); } - await new Promise(r => setTimeout(r, 45)); // ~45ms per step → ~1.3s total + await new Promise(r => setTimeout(r, 40)); } - // ── Completed ───────────────────────────────────────────────────────────── + // ── Errors → red bar + blocking popup ──────────────────────────────────── 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 = ''; + await new Promise(r => setTimeout(r, 300)); + const errLines = errors.map(e => { + const hint = e.c.hint || (e.c.error ? e.c.error : null); + return `❌ ${e.def.label}${hint ? '\n → ' + hint : ''}`; + }).join('\n\n'); + _showStartupErrorPopup( + tl('critical_error_short', 'Errore critico'), + tl('critical_error_intro', 'L\'app non può avviarsi a causa dei seguenti problemi:') + '\n\n' + errLines, + errorEl, retryBtn + ); return false; } - // Warnings only + // ── Warnings → amber bar + warning popup auto-close 5s ─────────────────── 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 + setProgress(100, `⚠️ ${warnings.length} ${tl('warnings_found', 'avvisi')}`, 'warn'); + await new Promise(r => setTimeout(r, 200)); + + // Build warning popup (auto-close 5s) + _showStartupWarningPopup(warnings, warningsEl, tl); + + // Wait for user to read (5s) then proceed + await new Promise(r => setTimeout(r, 5200)); + + // Hide warning popup warningsEl.style.display = 'none'; } else { setProgress(100, `✅ ${tl('all_ok', 'Sistema OK')}`); - await new Promise(r => setTimeout(r, 700)); + await new Promise(r => setTimeout(r, 600)); } wrapEl.style.display = 'none'; return true; } +/** Builds and shows the warning popup with countdown (auto-closes after 5s). */ +function _showStartupWarningPopup(warnings, container, tl) { + const lines = warnings.map(w => { + const hint = w.c.hint || null; + return `
+ ⚠️ +
+ ${w.def.label} + ${hint ? `

${hint}

` : ''} +
+
`; + }).join(''); + + container.innerHTML = ` +
+
+ ⚠️ ${warnings.length} ${tl('warnings_found', 'avviso/i rilevato/i')} + 5 +
+
${lines}
+
+
`; + container.style.display = ''; + + // Animate countdown bar + const barEl = document.getElementById('startup-popup-bar'); + const cntEl = document.getElementById('startup-countdown'); + if (barEl) { + barEl.style.transition = 'none'; + barEl.style.width = '100%'; + requestAnimationFrame(() => { + requestAnimationFrame(() => { + barEl.style.transition = 'width 5s linear'; + barEl.style.width = '0%'; + }); + }); + } + let secs = 4; + const t = setInterval(() => { + if (cntEl) cntEl.textContent = secs; + secs--; + if (secs < 0) clearInterval(t); + }, 1000); +} + +/** Shows a blocking error in the preloader (no auto-close). */ +function _showStartupErrorPopup(title, detail, errorEl, retryBtn) { + if (!errorEl) return; + errorEl.innerHTML = `${title}\n${detail}`; + errorEl.style.display = ''; + if (retryBtn) retryBtn.style.display = ''; +} + /** Retry button handler in the startup error screen. */ function _startupRetry() { location.reload(); diff --git a/index.html b/index.html index 21e5fb9..eafc0ec 100644 --- a/index.html +++ b/index.html @@ -11,7 +11,7 @@ EverShelf - + @@ -64,7 +64,7 @@ - v1.7.21 + v1.7.22 @@ -77,7 +77,7 @@

- EverShelfv1.7.21 + EverShelfv1.7.22

@@ -1613,6 +1613,6 @@
- + diff --git a/manifest.json b/manifest.json index 86ba629..fc36dd8 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "name": "EverShelf", "short_name": "EverShelf", "description": "Gestione completa della dispensa di casa con scansione barcode", - "version": "1.7.20", + "version": "1.7.22", "start_url": "/evershelf/", "display": "standalone", "background_color": "#f0f4e8", diff --git a/translations/de.json b/translations/de.json index 2780b0a..6ba2db4 100644 --- a/translations/de.json +++ b/translations/de.json @@ -1212,6 +1212,7 @@ "check_backups": "Backup-Verzeichnis", "check_write_test": "Schreibtest", "check_disk_space": "Speicherplatz", + "check_db_legacy": "Legacy-DB (dispensa.db)", "check_db_connect": "Datenbankverbindung", "check_db_tables": "Datenbanktabellen", "check_db_integrity": "Datenbankintegrität", @@ -1222,6 +1223,8 @@ "check_gemini": "Gemini-AI-Schlüssel", "check_bring_creds": "Bring!-Anmeldedaten", "check_bring_token": "Bring!-Token", + "check_tts": "Text-to-Speech-URL", + "check_scale": "Waagen-Gateway", "check_curl_ssl": "cURL-SSL", "check_internet": "Internetverbindung", "fresh_install": "Neuinstallation", @@ -1229,7 +1232,9 @@ "all_ok": "System OK", "critical_error_short": "Kritischer Fehler", "critical_error": "Kritischer Fehler: Die App kann nicht gestartet werden. Prüfe die Serverlogs.", - "error_network": "Server nicht erreichbar. Bitte Netzwerkverbindung prüfen.", + "critical_error_intro": "Die App kann aufgrund folgender Probleme nicht gestartet werden:", + "error_network": "Server nicht erreichbar.", + "error_network_detail": "Der Browser kann den PHP-Server nicht erreichen.\n\nMögliche Ursachen:\n• Apache/PHP-Server läuft nicht\n• Netzwerk- oder Firewall-Problem\n• Falsche App-URL\n\nBitte Server starten und erneut versuchen.", "retry": "Erneut versuchen" } } \ No newline at end of file diff --git a/translations/en.json b/translations/en.json index 86c6675..f79ce0e 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1212,6 +1212,7 @@ "check_backups": "Backup dir", "check_write_test": "Disk write test", "check_disk_space": "Disk space", + "check_db_legacy": "Legacy DB (dispensa.db)", "check_db_connect": "Database connection", "check_db_tables": "Database tables", "check_db_integrity": "Database integrity", @@ -1222,6 +1223,8 @@ "check_gemini": "Gemini AI key", "check_bring_creds": "Bring! credentials", "check_bring_token": "Bring! token", + "check_tts": "Text-to-Speech URL", + "check_scale": "Scale gateway", "check_curl_ssl": "cURL SSL", "check_internet": "Internet connection", "fresh_install": "fresh install", @@ -1229,7 +1232,9 @@ "all_ok": "System OK", "critical_error_short": "Critical error", "critical_error": "Critical error: the app cannot start. Check your server logs.", - "error_network": "Cannot reach the server. Check your network connection.", + "critical_error_intro": "The app cannot start due to the following issues:", + "error_network": "Cannot reach the server.", + "error_network_detail": "The browser cannot reach the PHP server.\n\nPossible causes:\n• Apache/PHP server is not running\n• Network or firewall issue\n• Incorrect app URL\n\nMake sure the server is started and try again.", "retry": "Retry" } } \ No newline at end of file diff --git a/translations/it.json b/translations/it.json index f3dbea5..dc71620 100644 --- a/translations/it.json +++ b/translations/it.json @@ -1212,6 +1212,7 @@ "check_backups": "Dir backup", "check_write_test": "Test scrittura disco", "check_disk_space": "Spazio disco", + "check_db_legacy": "DB legacy (dispensa.db)", "check_db_connect": "Connessione database", "check_db_tables": "Tabelle database", "check_db_integrity": "Integrità database", @@ -1222,6 +1223,8 @@ "check_gemini": "Chiave Gemini AI", "check_bring_creds": "Credenziali Bring!", "check_bring_token": "Token Bring!", + "check_tts": "URL Text-to-Speech", + "check_scale": "Gateway bilancia", "check_curl_ssl": "cURL SSL", "check_internet": "Connessione internet", "fresh_install": "nuovo impianto", @@ -1229,7 +1232,9 @@ "all_ok": "Sistema OK", "critical_error_short": "Errore critico", "critical_error": "Errore critico: l'app non può avviarsi. Controlla i log del server.", - "error_network": "Impossibile contattare il server. Controlla la connessione di rete.", + "critical_error_intro": "L'app non può avviarsi a causa dei seguenti problemi:", + "error_network": "Impossibile contattare il server.", + "error_network_detail": "Il browser non riesce a raggiungere il server PHP.\n\nPossibili cause:\n• Il server Apache/PHP non è in esecuzione\n• Problema di rete o firewall\n• URL dell'app non corretta\n\nControlla che il server sia avviato e riprova.", "retry": "Riprova" } } \ No newline at end of file