// Copyright (c) 2026 Gordon Bolton. MIT License. import { state, STATUS_REFRESH_MS } from '../state.js'; import { refreshStatusPanel as apiRefreshStatusPanel } from '../api.js'; import { fetchWebhookStatus } from './webhooks.js'; export async function toggleStatusPanel() { const panel = document.getElementById('status-panel'); const webhooksSection = document.getElementById('webhooks-section'); if (!panel.classList.contains('hidden')) { // Close both panels (webhooks is a sibling, hide it too) panel.classList.add('hidden'); if (webhooksSection) webhooksSection.classList.add('hidden'); if (state.statusRefreshHandle) { clearInterval(state.statusRefreshHandle); state.statusRefreshHandle = null; } return; } // Open status panel and webhooks section (siblings) panel.classList.remove('hidden'); // Show webhooks section for admin users (collapsed by default) if (webhooksSection && state.isAdmin) { webhooksSection.classList.remove('hidden'); state.webhookSectionExpanded = false; document.getElementById('webhooks-content').classList.add('hidden'); document.getElementById('webhooks-toggle').classList.remove('expanded'); await fetchWebhookStatus(); } else if (webhooksSection) { webhooksSection.classList.add('hidden'); } refreshStatusPanel(); if (state.statusRefreshHandle) clearInterval(state.statusRefreshHandle); state.statusRefreshHandle = setInterval(refreshStatusPanel, STATUS_REFRESH_MS); } export function closeStatusPanel() { document.getElementById('status-panel').classList.add('hidden'); const webhooksSection = document.getElementById('webhooks-section'); if (webhooksSection) webhooksSection.classList.add('hidden'); if (state.statusRefreshHandle) { clearInterval(state.statusRefreshHandle); state.statusRefreshHandle = null; } } export async function refreshStatusPanel() { const panel = document.getElementById('status-panel'); const contentDiv = document.getElementById('status-content'); console.log('[Status] panel found:', !!panel, 'contentDiv found:', !!contentDiv, 'panel display:', panel?.style?.display); if (!panel || panel.classList.contains('hidden')) return; console.log('[Status] Refreshing status panel...'); try { const result = await apiRefreshStatusPanel(); if (result.success) { console.log('[Status] Got status data, rendering...'); renderStatusPanel(result.data, panel); } else { console.error('[Status] API returned error:', result.error); if (contentDiv && (!contentDiv.innerHTML || contentDiv.innerHTML.includes('status-loading'))) { contentDiv.innerHTML = '

Failed to load status: ' + result.error + '

'; } } } catch (err) { console.error('[Status] Error fetching status:', err); // Don't overwrite panel on transient error during auto-refresh if (contentDiv && (!contentDiv.innerHTML || contentDiv.innerHTML.includes('status-loading'))) { contentDiv.innerHTML = '

Failed to load status: ' + err.message + '

'; } } } export function renderStatusPanel(data, panel) { console.log('[Status] renderStatusPanel called with data:', data ? 'yes' : 'no', 'keys:', data ? Object.keys(data) : 'none'); const s = data.server; const hrs = Math.floor(s.uptimeSeconds / 3600); const mins = Math.floor((s.uptimeSeconds % 3600) / 60); const secs = s.uptimeSeconds % 60; const uptime = `${hrs}h ${mins}m ${secs}s`; const totalKB = (data.cache.totalSizeBytes / 1024).toFixed(1); let html = `

Server Status

Server
Uptime${uptime}
Node${escapeHtml(s.nodeVersion)}
Memory (RSS)${s.memoryUsageMB} MB
Heap${s.heapUsedMB} / ${s.heapTotalMB} MB
Data Refresh
`; const pollIntervalMs = data.polling.intervalMs; const clients = data.clients || []; const sseClients = clients.filter(c => c.type === 'sse'); if (data.polling.enabled) { html += `
Background poll${pollIntervalMs / 1000}s
`; } else { html += `
Background pollDisabled
`; } const mode = sseClients.length > 0 ? `SSE push` : (data.polling.enabled ? 'Background' : 'On-demand (idle)'); html += `
Delivery mode${mode}
`; html += `
SSE clients${sseClients.length}
`; for (const c of sseClients) { const age = Math.round((Date.now() - c.connectedAt) / 1000); html += `
${escapeHtml(c.user)}connected ${age}s ago
`; } html += `
`; // Webhook metrics card (admin only) if (state.isAdmin && data.webhooks) { const wh = data.webhooks; const sonarrEnabled = wh.sonarr?.enabled ? '●' : '○'; const radarrEnabled = wh.radarr?.enabled ? '●' : '○'; const sonarrEvents = wh.sonarr?.eventsReceived || 0; const radarrEvents = wh.radarr?.eventsReceived || 0; const sonarrPolls = wh.sonarr?.pollsSkipped || 0; const radarrPolls = wh.radarr?.pollsSkipped || 0; html += `
Webhooks
Sonarr${sonarrEnabled} ${wh.sonarr?.enabled ? 'Enabled' : 'Disabled'}
Radarr${radarrEnabled} ${wh.radarr?.enabled ? 'Enabled' : 'Disabled'}
EventsS:${sonarrEvents} R:${radarrEvents}
Polls skippedS:${sonarrPolls} R:${radarrPolls}
`; } // Poll timings card const lp = data.polling.lastPoll; if (lp) { const pollAge = Math.round((Date.now() - new Date(lp.timestamp).getTime()) / 1000); html += `
Last Poll (${lp.totalMs}ms total, ${pollAge}s ago)
`; const maxTaskMs = lp.tasks.reduce((max, t) => Math.max(max, t.ms), 1); for (const t of lp.tasks) { const barWidth = Math.max(2, (t.ms / maxTaskMs) * 100); html += `
${escapeHtml(t.label)}
${t.ms}ms
`; } html += `
`; } // Cache table html += `
Cache (${data.cache.entryCount} entries, ${totalKB} KB)
`; for (const e of data.cache.entries) { const sizeStr = e.sizeBytes > 1024 ? (e.sizeBytes / 1024).toFixed(1) + ' KB' : e.sizeBytes + ' B'; const ttlStr = e.expired ? 'expired' : (e.ttlRemainingMs / 1000).toFixed(0) + 's'; const items = e.itemCount !== null ? e.itemCount : '—'; html += ``; } html += `
KeyItemsSizeTTL
${escapeHtml(e.key)}${items}${sizeStr}${ttlStr}
`; // Render into status-content div, not the whole panel (preserves webhooks section) const contentDiv = document.getElementById('status-content'); const panelCheck = document.getElementById('status-panel'); console.log('[Status] contentDiv found:', !!contentDiv, 'panel children:', panelCheck?.children?.length, 'HTML length:', html.length); if (panelCheck) { console.log('[Status] panel innerHTML preview:', panelCheck.innerHTML.substring(0, 200)); } if (contentDiv) { contentDiv.innerHTML = html; console.log('[Status] HTML rendered, contentDiv innerHTML length:', contentDiv.innerHTML.length); } else { console.error('[Status] contentDiv not found!'); } // Wire close button — addEventListener avoids CSP inline handler restrictions const closeBtn = document.getElementById('status-close-btn'); if (closeBtn) closeBtn.addEventListener('click', closeStatusPanel); // Set bar widths via JS DOM assignment — immune to CSP style-src restrictions panel.querySelectorAll('.timing-bar[data-w]').forEach(el => { el.style.width = el.dataset.w + '%'; }); } function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; }