Files
peardock/ui/stats-settings.js
T
Raven Scott c5015a66e8
Release rolling / release (push) Successful in 8m4s
Alert System Changes: Fixes applied (feature kept, load bounded)
1. Removed docker.df() entirely
2. Poll only what rules need
   • docker_daemon → cheap ping
   • container_health / stack_health → listContainers
   • resource → host statfs only (no Docker)
   • Event-only rules → no poll timer
3. Poll floor 60s, default 120s (was 15s min / 60s default)
4. No overlapping polls (pollInFlight)
5. Event path: drop noisy actions (exec_*, attach, …), match only relevant rules, serialize work (bounded queue)
6. UI status shows whether poll is active
2026-08-04 22:13:20 -04:00

231 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Settings → Performance
* Server-side Docker stats collection (interval, concurrency).
*/
import { Methods } from '../shared/protocol.js'
import { manager } from '../client/manager.js'
import { presentError } from '../client/errors.js'
/** @type {object|null} */
let statsState = null
/** Skip auto-apply while hydrating form from server */
let suppressLiveApply = false
/** @type {ReturnType<typeof setTimeout>|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 collect = document.getElementById('stats-collect-sec')
if (collect) {
collect.value = String(
config.collectIntervalSec ?? Math.round((config.collectIntervalMs || 5000) / 1000)
)
}
const broadcast = document.getElementById('stats-broadcast-sec')
if (broadcast) {
broadcast.value = String(
config.broadcastIntervalSec ?? Math.round((config.broadcastIntervalMs || 5000) / 1000)
)
}
const samples = document.getElementById('stats-samples-per-tick')
if (samples) samples.value = String(config.samplesPerTick ?? 4)
const conc = document.getElementById('stats-concurrency')
if (conc) conc.value = String(config.concurrency ?? 2)
const listSec = document.getElementById('stats-list-sec')
if (listSec) {
listSec.value = String(
config.listIntervalSec ?? Math.round((config.listIntervalMs || 15000) / 1000)
)
}
const cache = document.getElementById('stats-cache-ttl-ms')
if (cache) cache.value = String(config.cacheTtlMs ?? 2000)
const warn = document.getElementById('stats-warn-containers')
if (warn) warn.value = String(config.warnContainers ?? 80)
const active = config.active ? 'collecting' : 'idle (no peers or paused)'
setEngineStatus(
`${active} · gap ${config.collectIntervalSec ?? '?'}s · ${config.samplesPerTick ?? '?'} samples · ×${config.concurrency ?? '?'}`
)
const meta = document.getElementById('stats-runtime-meta')
if (meta) {
const parts = [
`peers=${config.peers ?? 0}`,
`running=${config.runningTracked ?? '?'}`,
`gap=${config.collectIntervalMs}ms`,
`samples/tick=${config.samplesPerTick}`,
`concurrency=${config.concurrency}`,
`list=${config.listIntervalMs}ms`,
`broadcast=${config.broadcastIntervalMs}ms`,
]
if (config.path) parts.push(`file=${config.path}`)
meta.textContent = parts.join(' · ')
}
} finally {
setTimeout(() => {
suppressLiveApply = false
}, 0)
}
}
function readFormPartial() {
const collectSec = Number(document.getElementById('stats-collect-sec')?.value)
const broadcastSec = Number(document.getElementById('stats-broadcast-sec')?.value)
const samplesPerTick = Number(document.getElementById('stats-samples-per-tick')?.value)
const concurrency = Number(document.getElementById('stats-concurrency')?.value)
const listSec = Number(document.getElementById('stats-list-sec')?.value)
const cacheTtlMs = Number(document.getElementById('stats-cache-ttl-ms')?.value)
const warnContainers = Number(document.getElementById('stats-warn-containers')?.value)
return {
collectIntervalSec: Number.isFinite(collectSec) ? collectSec : undefined,
broadcastIntervalSec: Number.isFinite(broadcastSec) ? broadcastSec : undefined,
samplesPerTick: Number.isFinite(samplesPerTick) ? samplesPerTick : undefined,
concurrency: Number.isFinite(concurrency) ? concurrency : undefined,
listIntervalSec: Number.isFinite(listSec) ? listSec : undefined,
cacheTtlMs: Number.isFinite(cacheTtlMs) ? cacheTtlMs : 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) => {
el.addEventListener('change', () => scheduleApply())
el.addEventListener('input', () => {
// Debounce number typing
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,
}