'use strict'; (async () => { let me; try { me = await api('api/me'); } catch (e) { return showError(e); } topNav(me); if (!me.admin) return showError(new Error('Administrator access required.')); const main = settingsShell(me, 'admin/health').content; async function load() { try { const [health, scraper] = await Promise.all([api('api/admin/health'), api('api/admin/scraper')]); main.textContent = ''; main.append(h('section', { class: 'card' }, h('p', {}, `Reader: ${health.reader.ok ? 'healthy' : 'scan error'} · Sentinel: ${health.sentinel.status} · Scraper: ${scraper.status || 'unavailable'}`), h('p', { class: 'muted small' }, health.sentinel.checkedAt ? `Last independent check: ${new Date(health.sentinel.checkedAt).toLocaleString()}` : 'Sentinel has not reported yet.'), h('a', { class: 'btn', href: 'settings.html#admin/scraper' }, 'Queue & hoster settings'))); for (const entry of scraper.active || []) main.append(h('section', { class: 'card' }, h('h2', {}, entry.source), h('p', {}, `${entry.current.title} · ${entry.job?.completed || 0}/${entry.job?.total || '?'} chapters`), h('p', { class: 'muted' }, `Checkpoint: ${entry.job?.updated || 'catalog discovery'}`))); const failures = (scraper.queue || []).filter(item => ['error', 'quarantined', 'unavailable', 'retry_wait'].includes(item.status)); main.append(h('section', { class: 'card' }, h('h2', {}, 'Jobs needing attention'), failures.length ? failures.map(item => h('div', { class: 'collection-editor' }, h('p', {}, `${item.title} · ${item.status} · ${item.failure?.kind || 'unknown'}`), h('p', { class: 'muted' }, item.error || ''), item.nextAttemptAt ? h('p', {}, `Retry scheduled: ${new Date(item.nextAttemptAt).toLocaleString()}`) : null, h('div', { class: 'row' }, h('button', { class: 'btn', onclick: async () => { if (item.failure?.kind === 'access' && !confirm('Has the source access problem been resolved? This manually authorizes one retry.')) return; await api('api/admin/scraper', { command: 'control', action: item.status === 'unavailable' ? 'inspect' : 'retry', id: item.id }); load(); } }, item.status === 'unavailable' ? 'Inspect source' : 'Retry'), h('button', { class: 'btn', onclick: async () => { await api('api/admin/scraper', { command: 'control', action: 'skip', id: item.id }); load(); } }, 'Skip job')))) : h('p', { class: 'muted' }, 'No failed jobs.'))); const incidents = [...(health.sentinel.incidents || []), ...(scraper.incidents || [])]; main.append(h('details', { class: 'card', open: incidents.some(i => i.state === 'open') }, h('summary', {}, 'Incident & recovery history'), incidents.slice(-50).map(i => h('p', {}, `${i.at} · ${i.state} · ${i.message || i.failure?.message || ''}${i.resolution ? ' · ' + i.resolution : ''}`)))); } catch (e) { main.textContent = ''; main.append(h('p', { class: 'error', role: 'alert' }, e.message)); } setTimeout(load, document.hidden ? 30000 : 10000); } load(); })();