const PING_MAX = 30; let pingResults = []; let manifest = null; const logStreams = new Map(); let statusPollTimer = null; /** Fallback when manifest API fails and static file is an older v1 without widgets. */ const DEFAULT_WIDGETS = [ { id: 'authoring-queue', type: 'queue', title: 'Authoring builds', integration: 'authoring' }, { id: 'authoring-detail', type: 'authoringDetail', title: 'Authoring detail', integration: 'authoring' }, { id: 'watcher', type: 'badge', title: 'Lima watcher', integration: 'watcher' }, { id: 'timesheet-summary', type: 'timesheet', title: 'Hours this week', integration: 'timesheet' }, { id: 'scripts-health', type: 'log', title: 'Script logs', integration: 'scripts', logKey: 'orgWebBuild' }, { id: 'calibre-sync', type: 'log', title: 'Calibre export', integration: 'scripts', logKey: 'calibreSync' }, { id: 'org-web-build', type: 'buildStatus', title: 'Website build', integration: 'builds', lastRunKey: 'org_web' }, ]; function manifestWidgets(data) { return Array.isArray(data?.widgets) && data.widgets.length > 0 ? data.widgets : DEFAULT_WIDGETS; } function apiFetch(url, options = {}) { return fetch(url, { cache: 'no-store', ...options }); } export async function initDashboard() { manifest = await loadManifest(); applyDashboardMeta(manifest); renderLinks(manifest.links); renderWidgets(manifestWidgets(manifest)); renderBuilds(manifest.builds); await refreshIntegrationWidgets(); startStatusPolling(manifest.integrations); updateClock(); setInterval(updateClock, 1000); setDailyMessage(); startCountdown(); setInterval(fetchHealth, 10000); fetchHealth(); } async function loadManifest() { try { const apiRes = await apiFetch('/api/zone/manifest'); if (apiRes.ok) { const data = await apiRes.json(); validateManifest(data); return data; } } catch { /* fallback to static file for local dev */ } const res = await apiFetch('data/manifest.json'); if (!res.ok) throw new Error('Failed to load manifest'); const data = await res.json(); validateManifest(data); return data; } function validateManifest(data) { const required = ['dashboard', 'links', 'builds', 'status']; for (const key of required) { if (!(key in data)) throw new Error(`manifest missing ${key}`); } const ids = new Set(); for (const link of data.links) { if (ids.has(link.id)) throw new Error(`duplicate link id ${link.id}`); ids.add(link.id); } const buildIds = new Set(); for (const build of data.builds) { if (buildIds.has(build.id)) throw new Error(`duplicate build id ${build.id}`); buildIds.add(build.id); } } function applyDashboardMeta({ dashboard }) { document.title = dashboard.title; document.querySelector('.topbar h1').textContent = dashboard.title; document.querySelector('.tagline').textContent = dashboard.tagline; } function renderLinks(links) { const nav = document.getElementById('quickLinks'); nav.innerHTML = ''; const order = ['platform', 'infra']; for (const group of order) { for (const link of links.filter(l => l.group === group)) { const a = document.createElement('a'); a.href = link.href; a.className = 'link-card'; a.dataset.linkId = link.id; if (link.external) { a.target = '_blank'; a.rel = 'noreferrer'; } const span = document.createElement('span'); span.textContent = link.label; a.appendChild(span); nav.appendChild(a); } } } function renderWidgets(widgets) { const grid = document.getElementById('widgetGrid'); if (!grid) return; grid.innerHTML = ''; for (const widget of widgets) { grid.appendChild(createWidgetCard(widget)); } } function createWidgetCard(widget) { const card = document.createElement('article'); card.className = 'widget-card'; card.dataset.widgetId = widget.id; card.dataset.integration = widget.integration || ''; card.innerHTML = `
`; return card; } async function refreshIntegrationWidgets() { try { const res = await apiFetch('/api/zone/status'); if (!res.ok) { setIntegrationPollMessage(`Status poll failed (${res.status})`); return false; } const status = await res.json(); updateWidgets(status); setIntegrationPollMessage(`Integrations updated: ${new Date().toLocaleTimeString()}`); return true; } catch (err) { setIntegrationPollMessage('Status poll offline'); console.warn('zone status poll failed', err); return false; } } function setIntegrationPollMessage(text) { const el = document.getElementById('lastUpdated'); if (el) el.textContent = text; } function startStatusPolling(integrations) { if (statusPollTimer) clearInterval(statusPollTimer); const pollValues = Object.values(integrations || {}).map(i => i.pollMs || 10000); const pollMs = pollValues.length > 0 ? Math.min(...pollValues, 10000) : 10000; statusPollTimer = setInterval(() => refreshIntegrationWidgets(), pollMs); } function widgetElements(widgetId) { const card = document.querySelector(`[data-widget-id="${widgetId}"]`); return { body: card?.querySelector('.widget-body'), badge: card?.querySelector('.widget-badge'), }; } function updateWidgets(status) { const widgets = manifestWidgets(manifest); for (const widget of widgets) { try { updateWidgetCard(widget, status); } catch (err) { const { body } = widgetElements(widget.id); if (body) body.textContent = 'Update error'; console.warn(`widget ${widget.id} update failed`, err); } } } function updateWidgetCard(widget, status) { const { body, badge } = widgetElements(widget.id); if (!body) return; const type = widget.type || widget.integration; switch (type) { case 'queue': { const a = status.authoring || {}; const running = a.running ? 'Running' : 'Idle'; setBadge(badge, running, a.running ? 'warn' : a.error ? 'error' : 'ok'); const current = a.current?.title || a.message || 'No builds yet'; const queued = a.queued ?? a.pendingCount ?? (a.pending?.length || 0); body.textContent = `${current} · queued: ${queued}`; break; } case 'authoringDetail': { const a = status.authoring || {}; const pending = a.pendingCount ?? (a.pending?.length || 0); setBadge(badge, String(pending), pending > 0 ? 'warn' : 'ok'); const failed = a.lastFailedTitle ? `Last failed: ${a.lastFailedTitle}` : 'No recent failures'; body.textContent = `Pending: ${pending} · ${failed}`; break; } case 'badge': { const w = status.watcher || {}; const active = w.ActiveState || w.active || 'unknown'; const isActive = active === 'active'; setBadge(badge, active, isActive ? 'ok' : 'warn'); body.textContent = w.SubState ? `SubState: ${w.SubState}` : (w.message || 'Watcher'); break; } case 'timesheet': { const t = status.timesheet || {}; setBadge(badge, t.available ? 'OK' : '—', t.available ? 'ok' : 'neutral'); body.textContent = t.available ? `Week ${t.weekStart}: ${t.hoursWorked}h worked (${t.delta >= 0 ? '+' : ''}${t.delta}h vs contracted)` : (t.message || 'No data'); break; } case 'log': { const info = (status.scripts || {})[widget.logKey] || {}; const label = info.label || widget.logKey; const stale = info.stale; setBadge(badge, stale ? 'Stale' : (info.exists === false ? 'Missing' : 'OK'), stale || info.exists === false ? 'error' : 'ok'); const mtime = info.mtime ? new Date(info.mtime).toLocaleString() : 'never'; const line = typeof info.lastLine === 'string' ? info.lastLine.slice(0, 80) : ''; body.textContent = `${label}: ${mtime}${info.staleReason ? ` (${info.staleReason})` : ''}${line ? ` · ${line}` : ''}`; break; } case 'buildStatus': { const run = (status.lastRuns || {})[widget.lastRunKey] || {}; const exit = run.exitCode; const ok = exit === 0 || exit === '0'; setBadge(badge, run.running ? 'Running' : (exit === '' || exit == null ? '—' : `exit ${exit}`), run.running ? 'warn' : ok ? 'ok' : 'error'); const when = run.lastRun && run.lastRun !== 'never' ? new Date(run.lastRun).toLocaleString() : 'never'; body.textContent = `Last run: ${when}`; break; } default: body.textContent = `Unknown widget type: ${type}`; setBadge(badge, '?', 'neutral'); } } function setBadge(el, text, tone) { if (!el) return; el.textContent = text; el.className = `badge ${tone} widget-badge`; } function renderBuilds(builds) { const grid = document.getElementById('buildGrid'); grid.innerHTML = ''; for (const build of builds) { grid.appendChild(createBuildCard(build)); } document.querySelectorAll('.toggleLogs').forEach(btn => { btn.addEventListener('click', () => { const target = document.getElementById(btn.dataset.target); target.classList.toggle('hidden'); btn.textContent = target.classList.contains('hidden') ? 'Show logs' : 'Hide logs'; }); }); } function createBuildCard(build) { const article = document.createElement('article'); article.className = 'panel run-card'; article.dataset.buildId = build.id; article.setAttribute('data-build-id', build.id); const btnId = build.id === 'org_web' ? 'buildBtn' : 'emacsBtn'; const killId = build.id === 'org_web' ? 'killWebBtn' : 'killEmacsBtn'; const resultId = build.id === 'org_web' ? 'buildResult' : 'emacsResult'; const logId = build.id === 'org_web' ? 'logOutput' : 'emacsLogs'; const lastRunId = build.id === 'org_web' ? 'webLastRunMeta' : 'emacsLastRunMeta'; article.innerHTML = `Waiting for action
Eid is now
'; return; } document.getElementById('cd-days').textContent = String(Math.floor(diff / 86400000)).padStart(3, '0'); document.getElementById('cd-hours').textContent = String(Math.floor((diff / 3600000) % 24)).padStart(2, '0'); document.getElementById('cd-mins').textContent = String(Math.floor((diff / 60000) % 60)).padStart(2, '0'); document.getElementById('cd-secs').textContent = String(Math.floor((diff / 1000) % 60)).padStart(2, '0'); } update(); setInterval(update, 1000); } initDashboard().catch(err => { console.error(err); document.getElementById('globalStatus').className = 'global-error'; document.getElementById('globalStatus').innerHTML = 'Failed to load dashboard manifest'; });