Merge branch 'develop'

This commit is contained in:
dadaloop82
2026-05-17 09:40:29 +00:00
11 changed files with 294 additions and 6 deletions
+7
View File
@@ -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.20] - 2026-05-20
### Added
- **Startup health check** — During the splash screen, the app now runs a comprehensive server-side diagnostic before loading: PHP version, required extensions (pdo_sqlite, curl, mbstring, json), `data/` directory writability, SQLite database connection and table integrity, `.env` file presence, Gemini AI key and Bring! token. Results are displayed as an animated checklist (✅ / ⚠️ / ❌). Critical failures (DB, extensions, data dir) block the app with a clear error message and a "Retry" button — the app never starts silently broken. Non-critical warnings (missing Gemini key, Bring! token) are shown as amber items but do not block startup.
- New `?action=health_check` PHP endpoint (early-exit, no rate-limit, no auth).
- New translation keys `startup.*` in all 5 languages (IT, EN, DE, FR, ES).
## [1.7.19] - 2026-05-19
### Added
+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.
+49
View File
@@ -116,6 +116,55 @@ body {
letter-spacing: 0.5px;
margin-top: -8px;
}
/* ── Startup health check list ─────────────────────────────────────── */
.preloader-checks {
display: flex;
flex-direction: column;
gap: 6px;
width: 240px;
max-width: 90vw;
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-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;
text-align: center;
max-width: 280px;
line-height: 1.4;
animation: zwFadeIn 0.3s ease;
}
.preloader-retry-btn {
background: #ef4444;
color: #fff;
border: none;
border-radius: 8px;
padding: 9px 22px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
animation: zwFadeIn 0.3s ease;
}
.preloader-retry-btn:active { opacity: 0.8; }
.header-logo-icon {
height: 28px;
width: auto;
+105
View File
@@ -14873,12 +14873,117 @@ function _heartbeatRetry() {
_runHeartbeat();
}
// ── Startup / Splash health check ────────────────────────────────────────────
/**
* Run a comprehensive server-side diagnostic during the splash screen.
* 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');
if (!checksEl) 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; }
};
// Show check list container
checksEl.innerHTML = '';
checksEl.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);
}
row.innerHTML = `<span class="pck-icon">${icon}</span><span class="pck-label">${text}</span>`;
row.dataset.state = state;
};
// 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'));
// Do the actual request
let result = null;
try {
const ctrl = new AbortController();
const tid = setTimeout(() => ctrl.abort(), 8000);
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;
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;
} 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;
}
}
/** Retry button handler in the startup error screen. */
function _startupRetry() {
location.reload();
}
/** Start the heartbeat loop (called once from _initApp). */
function startHeartbeat() {
_runHeartbeat(); // immediate first probe
}
async function _initApp() {
// ── Startup health check (runs during splash, blocks app if critical) ──────
const _startupOk = await _runStartupCheck();
if (!_startupOk) return; // preloader stays visible with error; app does not start
// Check for setup wizard resume (after language change)
const resumeStep = localStorage.getItem('evershelf_setup_step');
const resumeData = localStorage.getItem('evershelf_setup_data');
+8 -5
View File
@@ -11,7 +11,7 @@
<title>EverShelf</title>
<link rel="manifest" href="manifest.json">
<link rel="icon" type="image/png" href="assets/img/logo/logo_icon.png">
<link rel="stylesheet" href="assets/css/style.css?v=20260519c">
<link rel="stylesheet" href="assets/css/style.css?v=20260520a">
<!-- QuaggaJS for barcode scanning -->
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script>
<!-- @xenova/transformers: ES-module bootstrap that exposes a lazy category-classifier as window._categoryPipelinePromise -->
@@ -54,8 +54,11 @@
<div id="app-preloader" aria-hidden="true">
<div class="app-preloader-inner">
<img src="assets/img/logo/logo.png" alt="EverShelf" class="app-preloader-logo" />
<div class="app-preloader-spinner"></div>
<span class="app-preloader-version" id="preloader-version">v1.7.15</span>
<div class="app-preloader-spinner" id="preloader-spinner"></div>
<div id="preloader-checks" class="preloader-checks" style="display:none"></div>
<div id="preloader-error-msg" class="preloader-error-msg" style="display:none"></div>
<button id="preloader-retry-btn" class="preloader-retry-btn" style="display:none" onclick="_startupRetry()">🔄 <span data-i18n="startup.retry">Riprova</span></button>
<span class="app-preloader-version" id="preloader-version">v1.7.20</span>
</div>
</div>
@@ -68,7 +71,7 @@
<!-- Title — left-aligned; grows to fill space -->
<div class="header-title-wrap">
<h1 class="header-title" onclick="showPage('dashboard')">
<img src="assets/img/logo/logo_icon.png" alt="" class="header-logo-icon" aria-hidden="true" /><span data-i18n="nav.title">EverShelf</span><span class="header-version">v1.7.15</span>
<img src="assets/img/logo/logo_icon.png" alt="" class="header-logo-icon" aria-hidden="true" /><span data-i18n="nav.title">EverShelf</span><span class="header-version">v1.7.20</span>
</h1>
<!-- Update badge — shown alongside title, never replaces it -->
<span class="header-update-badge" id="header-update-badge" style="display:none"></span>
@@ -1604,6 +1607,6 @@
</div>
</div>
<script src="assets/js/app.js?v=20260519c"></script>
<script src="assets/js/app.js?v=20260520a"></script>
</body>
</html>
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "EverShelf",
"short_name": "EverShelf",
"description": "Gestione completa della dispensa di casa con scansione barcode",
"version": "1.7.19",
"version": "1.7.20",
"start_url": "/evershelf/",
"display": "standalone",
"background_color": "#f0f4e8",
+12
View File
@@ -1201,5 +1201,17 @@
"btn_csv": "CSV herunterladen",
"btn_pdf": "PDF / Drucken",
"btn_title": "Exportieren"
},
"startup": {
"check_php": "PHP",
"check_exts": "PHP-Erweiterungen",
"check_data_dir": "Datenverzeichnis",
"check_db": "Datenbank",
"check_env": "Konfiguration (.env)",
"check_gemini": "Gemini-AI-Schlüssel",
"check_bring": "Bring!-Token",
"critical_error": "Kritischer Fehler: Die App kann nicht gestartet werden. Prüfe die Serverlogs.",
"error_network": "Server nicht erreichbar. Bitte Verbindung prüfen.",
"retry": "Erneut versuchen"
}
}
+12
View File
@@ -1201,5 +1201,17 @@
"btn_csv": "Download CSV",
"btn_pdf": "PDF / Print",
"btn_title": "Export"
},
"startup": {
"check_php": "PHP",
"check_exts": "PHP extensions",
"check_data_dir": "Data directory",
"check_db": "Database",
"check_env": "Configuration (.env)",
"check_gemini": "Gemini AI key",
"check_bring": "Bring! token",
"critical_error": "Critical error: the app cannot start. Check your server logs.",
"error_network": "Cannot reach the server. Check your connection.",
"retry": "Retry"
}
}
+12
View File
@@ -1201,5 +1201,17 @@
"btn_csv": "Descargar CSV",
"btn_pdf": "PDF / Imprimir",
"btn_title": "Exportar"
},
"startup": {
"check_php": "PHP",
"check_exts": "Extensiones PHP",
"check_data_dir": "Carpeta de datos",
"check_db": "Base de datos",
"check_env": "Configuración (.env)",
"check_gemini": "Clave Gemini AI",
"check_bring": "Token de Bring!",
"critical_error": "Error crítico: la aplicación no puede iniciarse. Revisa los registros del servidor.",
"error_network": "No se puede contactar con el servidor. Comprueba tu conexión.",
"retry": "Reintentar"
}
}
+12
View File
@@ -1201,5 +1201,17 @@
"btn_csv": "Télécharger CSV",
"btn_pdf": "PDF / Imprimer",
"btn_title": "Exporter"
},
"startup": {
"check_php": "PHP",
"check_exts": "Extensions PHP",
"check_data_dir": "Dossier de données",
"check_db": "Base de données",
"check_env": "Configuration (.env)",
"check_gemini": "Clé Gemini AI",
"check_bring": "Token Bring!",
"critical_error": "Erreur critique : l'application ne peut pas démarrer. Vérifiez les logs du serveur.",
"error_network": "Impossible de contacter le serveur. Vérifiez votre connexion.",
"retry": "Réessayer"
}
}
+12
View File
@@ -1201,5 +1201,17 @@
"btn_csv": "Scarica CSV",
"btn_pdf": "PDF / Stampa",
"btn_title": "Esporta"
},
"startup": {
"check_php": "PHP",
"check_exts": "Estensioni PHP",
"check_data_dir": "Cartella dati",
"check_db": "Database",
"check_env": "Configurazione (.env)",
"check_gemini": "Chiave Gemini AI",
"check_bring": "Token Bring!",
"critical_error": "Errore critico: impossibile avviare l'app. Controlla i log del server.",
"error_network": "Impossibile contattare il server. Controlla la connessione.",
"retry": "Riprova"
}
}