feat: startup health check during splash screen (v1.7.20)
- Add ?action=health_check PHP endpoint (early-exit, before rate-limiter) Checks: PHP version, required extensions, data/ writability, SQLite DB connection + table integrity, .env file, Gemini AI key, Bring! token - Display animated checklist in splash screen with per-item icons (ok/warn/error); critical failures block app launch with clear error message and Retry button; optional warnings shown but don't block - New JS: _runStartupCheck(), _startupRetry(); called first in _initApp() - New HTML elements in #app-preloader: #preloader-checks, #preloader-error-msg, #preloader-retry-btn (hidden until startup check completes) - New CSS: .preloader-checks, .preloader-check-row, .preloader-error-msg, .preloader-retry-btn with state colors (ok=green, warn=amber, error=red) - Translations: startup.* keys (10 per language) in IT, EN, DE, FR, ES - Asset version bump: v=20260520a
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user