Harden live server apply for alerts and peer roles
Release rolling / release (push) Successful in 7m37s

Flush alert config immediately, re-arm runtime without restart, and push
role updates to connected clients so ACL changes apply live.
This commit is contained in:
Raven Scott
2026-07-17 19:51:30 -04:00
parent e2fa2ff85a
commit 30f283394e
5 changed files with 68 additions and 10 deletions
+16 -3
View File
@@ -5,7 +5,7 @@
import * as peerPolicy from '../core/peer-policy.js'
import * as connectionInvites from '../core/connection-invites.js'
import { peers } from '../core/peer-registry.js'
import { Roles } from '../../shared/protocol.js'
import { Roles, Pushes } from '../../shared/protocol.js'
import * as validation from '../utils/validation.js'
import { decodePeardockInvite } from '../../shared/crypto-auth.js'
@@ -263,7 +263,20 @@ export function registerPeerHandlers(session) {
if (!peerId || !role) throw new Error('peerId and role required')
const entry = peerPolicy.setPeerRole(peerId, role)
const live = peers.get(peerId)
if (live) live.role = role
return { success: true, data: entry }
if (live) {
// Apply ACL live — no reconnect / server restart
live.role = role
try {
live.push(Pushes.session, {
type: 'roleUpdate',
role,
peerId,
updatedAt: new Date().toISOString(),
})
} catch {
// ignore push failures
}
}
return { success: true, data: entry, appliedLive: Boolean(live) }
})
}
+20 -7
View File
@@ -334,15 +334,28 @@ function normalizeConfig(raw) {
}
}
/**
* Persist config promptly. Coalesce bursts so rapid UI edits don't thrash disk,
* but never wait more than ~120ms so process death can't strand live config.
*/
function scheduleSave() {
if (saveTimer) return
saveTimer = setTimeout(() => {
saveTimer = null
saveDisk()
}, 400)
}, 120)
if (typeof saveTimer.unref === 'function') saveTimer.unref()
}
/** Flush any pending debounced save immediately (e.g. before critical ops). */
function flushSave() {
if (saveTimer) {
clearTimeout(saveTimer)
saveTimer = null
}
saveDisk()
}
function saveDisk() {
try {
const payload = {
@@ -1260,7 +1273,7 @@ export function updateAlertsConfig(partial, opts = {}) {
config = normalizeConfig(merged)
}
config.updatedAt = new Date().toISOString()
saveDisk()
flushSave()
// Live apply: poll interval, enable/disable — no process restart
applyRuntimeConfig({
kickPoll: config.enabled && (!wasEnabled || partial.pollIntervalMs != null),
@@ -1284,8 +1297,8 @@ export function upsertAlertChannel(input) {
} else {
config.channels.push(ch)
}
scheduleSave()
// Channel list is read on each fire — already live; log for ops clarity
// In-memory config is used on next fire; flush disk promptly (no restart)
flushSave()
logger.debug('alerts: channel upserted live', {
id: ch.id,
enabled: ch.enabled,
@@ -1303,7 +1316,7 @@ export function deleteAlertChannel(id) {
rule.channelIds = rule.channelIds.filter((x) => x !== id)
}
}
scheduleSave()
flushSave()
return before !== config.channels.length
}
@@ -1312,7 +1325,7 @@ export function upsertAlertRule(input) {
const idx = config.rules.findIndex((r) => r.id === rule.id)
if (idx >= 0) config.rules[idx] = rule
else config.rules.push(rule)
scheduleSave()
flushSave()
logger.debug('alerts: rule upserted live', {
id: rule.id,
enabled: rule.enabled,
@@ -1324,7 +1337,7 @@ export function upsertAlertRule(input) {
export function deleteAlertRule(id) {
const before = config.rules.length
config.rules = config.rules.filter((r) => r.id !== id)
scheduleSave()
flushSave()
return before !== config.rules.length
}
+1
View File
@@ -88,6 +88,7 @@ function arm(job) {
job.timer = null
}
if (!job.enabled || job.intervalMs < 60_000) return
// Live: re-arm whenever upserted — no server restart required
job.timer = setInterval(() => {
runJob(job)
}, job.intervalMs)