Release rolling / release (push) Successful in 8m4s
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
231 lines
7.6 KiB
JavaScript
231 lines
7.6 KiB
JavaScript
/**
|
||
* 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,
|
||
}
|