/** * Settings → Performance * Server-side Docker stats (view-interest streams + push cadence). */ import { Methods } from '../shared/protocol.js' import { manager } from '../client/manager.js' import { presentError } from '../client/errors.js' /** @type {object|null} */ let statsState = null let suppressLiveApply = false /** @type {ReturnType|null} */ let applyTimer = null let applyInFlight = false function showAlert(type, msg) { if (typeof window.showAlert === 'function') window.showAlert(type, msg) else if (typeof window.peardockOps?.showAlert === 'function') { window.peardockOps.showAlert(type, msg) } } async function rpc(method, args = {}) { if (!manager.active?.connected) { throw new Error('Connect to a PearDock server to manage stats settings') } return manager.request(method, args) } function setStatus(msg, tone = 'muted') { const el = document.getElementById('stats-status') if (!el) return el.className = `small mt-2 text-${tone === 'danger' ? 'danger' : tone === 'success' ? 'success' : 'muted'}` el.textContent = msg || '' } function setEngineStatus(text) { const el = document.getElementById('stats-engine-status') if (el) el.textContent = text || '—' } /** * Load config from the active server into the form. */ export async function loadStatsPanel() { const offline = document.getElementById('stats-offline-hint') if (!manager.active?.connected) { if (offline) offline.classList.remove('hidden') setEngineStatus('Not connected') const meta = document.getElementById('stats-runtime-meta') if (meta) meta.textContent = '' return } if (offline) offline.classList.add('hidden') try { const res = await rpc(Methods.getStatsConfig || 'getStatsConfig') statsState = res?.config || null renderStatsForm(statsState) setStatus('') } catch (err) { setStatus(err.message || 'Failed to load stats config', 'danger') setEngineStatus('Error loading') presentError?.(err, 'getStatsConfig', { showAlert }) } } /** * @param {object|null} config */ function renderStatsForm(config) { if (!config) return suppressLiveApply = true try { const broadcast = document.getElementById('stats-broadcast-sec') if (broadcast) { broadcast.value = String( config.broadcastIntervalSec ?? Math.round((config.broadcastIntervalMs || 1000) / 1000) ) } const listSec = document.getElementById('stats-list-sec') if (listSec) { listSec.value = String( config.listIntervalSec ?? Math.round((config.listIntervalMs || 10000) / 1000) ) } const warn = document.getElementById('stats-warn-containers') if (warn) warn.value = String(config.warnContainers ?? 80) // Keep hidden legacy inputs in sync if present const collect = document.getElementById('stats-collect-sec') if (collect) { collect.value = String( config.broadcastIntervalSec ?? Math.round((config.broadcastIntervalMs || 1000) / 1000) ) } const active = config.active ? `streaming · ${config.streams ?? '?'} streams · ${config.watchers ?? 0} watcher(s)` : 'idle (no client on Containers/Dashboard)' setEngineStatus(active) const meta = document.getElementById('stats-runtime-meta') if (meta) { const parts = [ `mode=${config.mode || 'interest-stream'}`, `peers=${config.peers ?? 0}`, `watchers=${config.watchers ?? 0}`, `streams=${config.streams ?? 0}`, `broadcast=${config.broadcastIntervalMs}ms`, `list=${config.listIntervalMs}ms`, ] if (config.path) parts.push(`file=${config.path}`) meta.textContent = parts.join(' · ') } } finally { setTimeout(() => { suppressLiveApply = false }, 0) } } function readFormPartial() { const broadcastSec = Number(document.getElementById('stats-broadcast-sec')?.value) const listSec = Number(document.getElementById('stats-list-sec')?.value) const warnContainers = Number(document.getElementById('stats-warn-containers')?.value) return { broadcastIntervalSec: Number.isFinite(broadcastSec) ? broadcastSec : undefined, listIntervalSec: Number.isFinite(listSec) ? listSec : undefined, warnContainers: Number.isFinite(warnContainers) ? warnContainers : undefined, } } function scheduleApply() { if (suppressLiveApply) return if (!manager.active?.connected) { setStatus('Connect to a server to apply changes', 'danger') return } if (applyTimer) clearTimeout(applyTimer) setStatus('Applying…', 'muted') applyTimer = setTimeout(() => { applyTimer = null applyLive().catch((err) => { setStatus(err.message || 'Apply failed', 'danger') presentError?.(err, 'updateStatsConfig', { showAlert }) }) }, 400) } async function applyLive() { if (applyInFlight) { scheduleApply() return } applyInFlight = true try { const partial = readFormPartial() const res = await rpc(Methods.updateStatsConfig || 'updateStatsConfig', { config: partial, }) statsState = res?.config || statsState renderStatsForm(statsState) setStatus('Applied on server', 'success') } finally { applyInFlight = false } } async function resetDefaults() { if (!manager.active?.connected) { setStatus('Connect to a server to reset', 'danger') return } try { setStatus('Resetting…', 'muted') const res = await rpc(Methods.updateStatsConfig || 'updateStatsConfig', { replace: true, config: {}, }) statsState = res?.config || null renderStatsForm(statsState) setStatus('Reset to server defaults', 'success') showAlert?.('success', 'Stats settings reset to defaults') } catch (err) { setStatus(err.message || 'Reset failed', 'danger') presentError?.(err, 'updateStatsConfig', { showAlert }) } } /** * Wire change listeners once. */ export function initStatsSettings() { const panel = document.getElementById('settings-panel-performance') if (!panel || panel.dataset.statsWired === '1') return panel.dataset.statsWired = '1' panel.querySelectorAll('.stats-live-field').forEach((el) => { // Skip hidden legacy fields from auto-apply noise if (el.type === 'hidden') return el.addEventListener('change', () => scheduleApply()) el.addEventListener('input', () => { if (el.tagName === 'INPUT') scheduleApply() }) }) document.getElementById('stats-refresh')?.addEventListener('click', () => { loadStatsPanel().catch(() => {}) }) document.getElementById('stats-reset-defaults')?.addEventListener('click', () => { resetDefaults().catch(() => {}) }) } export default { initStatsSettings, loadStatsPanel, }