Compare commits

..

2 Commits

Author SHA1 Message Date
dadaloop82 e858b3cc85 Merge branch 'develop' 2026-05-17 09:50:51 +00:00
dadaloop82 78f499205c feat: progress bar startup check with 29 diagnostics (v1.7.21)
- Replace banner checklist with real-time progress bar + per-check label
  Bar fills smoothly (0→100%) as each check runs; label shows current check.
  On success: bar stays green briefly then fades. On warnings: amber badges
  shown for 2.2s. On critical error: bar turns red + error block + Retry.
- Extend health_check to 29 comprehensive checks:
  PHP 8.0+ version, 4 critical extensions (pdo_sqlite/curl/json/mbstring),
  4 optional extensions (openssl/fileinfo/zip/intl), PHP memory/timeout/upload,
  data/ writable, rate_limits/ dir, backups/ dir, actual file-write test,
  free disk space, SQLite connect, required tables, PRAGMA quick_check integrity,
  WAL mode, DB file size, inventory row count, .env file, Gemini AI key,
  Bring! credentials + token, cURL SSL version, internet reachability (Gemini API)
- Fresh-install detection: if dispensa.db not found + data/ writable → OK (auto-create)
- Translations: startup.* expanded to 28 keys in IT, EN, DE, FR, ES
- CSS: new .preloader-progress-wrap, .preloader-bar-track, .preloader-bar,
  .preloader-check-label, .preloader-warn-badge; removed old .preloader-checks
- Version: v1.7.21, assets v=20260520b
2026-05-17 09:50:42 +00:00
10 changed files with 512 additions and 174 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.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
+187 -48
View File
@@ -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;
}
+54 -23
View File
@@ -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 {
+131 -66
View File
@@ -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 = `<span class="pck-icon">${icon}</span><span class="pck-label">${text}</span>`;
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 => `<span class="preloader-warn-badge">⚠️ ${w.def.label}</span>`)
.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. */
+11 -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=20260520a">
<link rel="stylesheet" href="assets/css/style.css?v=20260520b">
<!-- 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 -->
@@ -55,10 +55,16 @@
<div class="app-preloader-inner">
<img src="assets/img/logo/logo.png" alt="EverShelf" class="app-preloader-logo" />
<div class="app-preloader-spinner" id="preloader-spinner"></div>
<div id="preloader-checks" class="preloader-checks" style="display:none"></div>
<div id="preloader-progress-wrap" class="preloader-progress-wrap" style="display:none">
<div class="preloader-bar-track">
<div id="preloader-bar" class="preloader-bar"></div>
</div>
<div id="preloader-check-label" class="preloader-check-label">&nbsp;</div>
</div>
<div id="preloader-warnings" class="preloader-warnings" 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>
<span class="app-preloader-version" id="preloader-version">v1.7.21</span>
</div>
</div>
@@ -71,7 +77,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.20</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.21</span>
</h1>
<!-- Update badge — shown alongside title, never replaces it -->
<span class="header-update-badge" id="header-update-badge" style="display:none"></span>
@@ -1607,6 +1613,6 @@
</div>
</div>
<script src="assets/js/app.js?v=20260520a"></script>
<script src="assets/js/app.js?v=20260520b"></script>
</body>
</html>
+24 -6
View File
@@ -1203,15 +1203,33 @@
"btn_title": "Exportieren"
},
"startup": {
"check_php": "PHP",
"check_exts": "PHP-Erweiterungen",
"connecting": "Serververbindung wird hergestellt...",
"check_php_memory": "PHP-Speicher",
"check_php_timeout": "PHP-Timeout",
"check_php_upload": "PHP-Upload",
"check_data_dir": "Datenverzeichnis",
"check_db": "Datenbank",
"check_env": "Konfiguration (.env)",
"check_rate_limits": "Rate-Limits-Verzeichnis",
"check_backups": "Backup-Verzeichnis",
"check_write_test": "Schreibtest",
"check_disk_space": "Speicherplatz",
"check_db_connect": "Datenbankverbindung",
"check_db_tables": "Datenbanktabellen",
"check_db_integrity": "Datenbankintegrität",
"check_db_wal": "WAL-Modus",
"check_db_size": "Datenbankgröße",
"check_db_rows": "Inventardaten",
"check_env": ".env-Datei",
"check_gemini": "Gemini-AI-Schlüssel",
"check_bring": "Bring!-Token",
"check_bring_creds": "Bring!-Anmeldedaten",
"check_bring_token": "Bring!-Token",
"check_curl_ssl": "cURL-SSL",
"check_internet": "Internetverbindung",
"fresh_install": "Neuinstallation",
"warnings_found": "Warnungen",
"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 Verbindung prüfen.",
"error_network": "Server nicht erreichbar. Bitte Netzwerkverbindung prüfen.",
"retry": "Erneut versuchen"
}
}
+24 -6
View File
@@ -1203,15 +1203,33 @@
"btn_title": "Export"
},
"startup": {
"check_php": "PHP",
"check_exts": "PHP extensions",
"connecting": "Connecting to server...",
"check_php_memory": "PHP memory",
"check_php_timeout": "PHP timeout",
"check_php_upload": "PHP upload",
"check_data_dir": "Data directory",
"check_db": "Database",
"check_env": "Configuration (.env)",
"check_rate_limits": "Rate limits dir",
"check_backups": "Backup dir",
"check_write_test": "Disk write test",
"check_disk_space": "Disk space",
"check_db_connect": "Database connection",
"check_db_tables": "Database tables",
"check_db_integrity": "Database integrity",
"check_db_wal": "WAL mode",
"check_db_size": "Database size",
"check_db_rows": "Inventory data",
"check_env": ".env file",
"check_gemini": "Gemini AI key",
"check_bring": "Bring! token",
"check_bring_creds": "Bring! credentials",
"check_bring_token": "Bring! token",
"check_curl_ssl": "cURL SSL",
"check_internet": "Internet connection",
"fresh_install": "fresh install",
"warnings_found": "warnings found",
"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 connection.",
"error_network": "Cannot reach the server. Check your network connection.",
"retry": "Retry"
}
}
+24 -6
View File
@@ -1203,15 +1203,33 @@
"btn_title": "Exportar"
},
"startup": {
"check_php": "PHP",
"check_exts": "Extensiones PHP",
"connecting": "Conectando al servidor...",
"check_php_memory": "Memoria PHP",
"check_php_timeout": "Tiempo de espera PHP",
"check_php_upload": "Upload PHP",
"check_data_dir": "Carpeta de datos",
"check_db": "Base de datos",
"check_env": "Configuración (.env)",
"check_rate_limits": "Dir. rate limits",
"check_backups": "Dir. copias de seguridad",
"check_write_test": "Prueba escritura disco",
"check_disk_space": "Espacio en disco",
"check_db_connect": "Conexión base de datos",
"check_db_tables": "Tablas de la BD",
"check_db_integrity": "Integridad BD",
"check_db_wal": "Modo WAL",
"check_db_size": "Tamaño de la BD",
"check_db_rows": "Datos del inventario",
"check_env": "Archivo .env",
"check_gemini": "Clave Gemini AI",
"check_bring": "Token de Bring!",
"check_bring_creds": "Credenciales Bring!",
"check_bring_token": "Token de Bring!",
"check_curl_ssl": "cURL SSL",
"check_internet": "Conexión a internet",
"fresh_install": "instalación nueva",
"warnings_found": "avisos detectados",
"all_ok": "Sistema OK",
"critical_error_short": "Error crítico",
"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.",
"error_network": "No se puede contactar con el servidor. Comprueba tu conexión de red.",
"retry": "Reintentar"
}
}
+25 -7
View File
@@ -1203,15 +1203,33 @@
"btn_title": "Exporter"
},
"startup": {
"check_php": "PHP",
"check_exts": "Extensions PHP",
"connecting": "Connexion au serveur...",
"check_php_memory": "Mémoire PHP",
"check_php_timeout": "Délai PHP",
"check_php_upload": "Upload PHP",
"check_data_dir": "Dossier de données",
"check_db": "Base de données",
"check_env": "Configuration (.env)",
"check_rate_limits": "Dossier rate limits",
"check_backups": "Dossier sauvegardes",
"check_write_test": "Test d'écriture disque",
"check_disk_space": "Espace disque",
"check_db_connect": "Connexion base de données",
"check_db_tables": "Tables de la BDD",
"check_db_integrity": "Intégrité BDD",
"check_db_wal": "Mode WAL",
"check_db_size": "Taille de la BDD",
"check_db_rows": "Données inventaire",
"check_env": "Fichier .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.",
"check_bring_creds": "Identifiants Bring!",
"check_bring_token": "Token Bring!",
"check_curl_ssl": "cURL SSL",
"check_internet": "Connexion internet",
"fresh_install": "nouvelle installation",
"warnings_found": "avertissements détectés",
"all_ok": "Système OK",
"critical_error_short": "Erreur critique",
"critical_error": "Erreur critique : l'application ne peut pas démarrer. Vérifiez les logs.",
"error_network": "Impossible de contacter le serveur. Vérifiez votre connexion réseau.",
"retry": "Réessayer"
}
}
+25 -7
View File
@@ -1203,15 +1203,33 @@
"btn_title": "Esporta"
},
"startup": {
"check_php": "PHP",
"check_exts": "Estensioni PHP",
"connecting": "Connessione al server...",
"check_php_memory": "Memoria PHP",
"check_php_timeout": "Timeout PHP",
"check_php_upload": "Upload PHP",
"check_data_dir": "Cartella dati",
"check_db": "Database",
"check_env": "Configurazione (.env)",
"check_rate_limits": "Dir rate limits",
"check_backups": "Dir backup",
"check_write_test": "Test scrittura disco",
"check_disk_space": "Spazio disco",
"check_db_connect": "Connessione database",
"check_db_tables": "Tabelle database",
"check_db_integrity": "Integrità database",
"check_db_wal": "WAL mode",
"check_db_size": "Dimensione database",
"check_db_rows": "Dati inventario",
"check_env": "File .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.",
"check_bring_creds": "Credenziali Bring!",
"check_bring_token": "Token Bring!",
"check_curl_ssl": "cURL SSL",
"check_internet": "Connessione internet",
"fresh_install": "nuovo impianto",
"warnings_found": "avvisi rilevati",
"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.",
"retry": "Riprova"
}
}