Fix per-resource settings: tags saving, is_public bug, global setting inheritance

Bug fixes:
- tags field was missing from INSERT/UPDATE SQL — tags never persisted
- is_public: `0 !== false` is true in JS, so private resources (is_public=0)
  were silently flipped to public on every edit; fixed with explicit 0/false check

Global setting inheritance:
- One-time DB migration nulls out check_interval and timeout on all existing
  resources so they inherit from global monitoring settings going forward
- Scheduler reads global check_interval and timeout from DB each cycle and
  uses them as the fallback when per-resource values are null
- Create and edit modals: empty field = use global; placeholder shows the
  current global value so it's clear what will be used

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-08-06 17:01:03 -04:00
1 parent 3a3d32ecac
commit 59078362da
4 files changed
+49 -18

No files matched your search

+10
View File
@@ -333,4 +333,14 @@ db.exec(`
CREATE INDEX IF NOT EXISTS idx_resources_heartbeat_token ON resources(heartbeat_token);
`);
// One-time migration: null out per-resource check_interval and timeout so all resources
// inherit from the global monitoring settings. Runs once, tracked by a settings flag.
try {
const alreadyMigrated = db.prepare("SELECT value FROM settings WHERE key = 'migrated_global_defaults_v1'").get();
if (!alreadyMigrated) {
db.prepare("UPDATE resources SET check_interval = NULL, timeout = NULL").run();
db.prepare("INSERT OR IGNORE INTO settings (key, value) VALUES ('migrated_global_defaults_v1', 'true')").run();
}
} catch (e) {}
module.exports = db;
+10 -8
View File
@@ -343,7 +343,7 @@ app.get('/api/resources/:id', (req, res) => {
// Create resource
app.post('/api/resources', (req, res) => {
const { name, url, type, check_interval, timeout, group_id, http_keyword, http_headers, quiet_hours_start, quiet_hours_end, cert_expiry_days, sla_target, email_to, maintenance_mode, retention_days, is_public, heartbeat_timeout, consecutive_failures_threshold, response_time_threshold, http_method, http_body } = req.body;
const { name, url, type, check_interval, timeout, group_id, http_keyword, http_headers, quiet_hours_start, quiet_hours_end, cert_expiry_days, sla_target, email_to, maintenance_mode, retention_days, is_public, heartbeat_timeout, consecutive_failures_threshold, response_time_threshold, http_method, http_body, tags } = req.body;
const validationError = validateResourceBody(req.body);
if (validationError) return res.status(400).json({ error: validationError });
@@ -354,8 +354,8 @@ app.post('/api/resources', (req, res) => {
const effectiveUrl = isHeartbeat ? `heartbeat://${name}` : url;
const stmt = db.prepare(`
INSERT INTO resources (name, url, type, check_interval, timeout, group_id, http_keyword, http_headers, quiet_hours_start, quiet_hours_end, cert_expiry_days, sla_target, email_to, maintenance_mode, retention_days, is_public, heartbeat_token, heartbeat_timeout, consecutive_failures_threshold, response_time_threshold, http_method, http_body)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO resources (name, url, type, check_interval, timeout, group_id, http_keyword, http_headers, quiet_hours_start, quiet_hours_end, cert_expiry_days, sla_target, email_to, maintenance_mode, retention_days, is_public, heartbeat_token, heartbeat_timeout, consecutive_failures_threshold, response_time_threshold, http_method, http_body, tags)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(
@@ -374,13 +374,14 @@ app.post('/api/resources', (req, res) => {
email_to || null,
maintenance_mode ? 1 : 0,
retention_days || null,
is_public !== false ? 1 : 0,
(is_public === false || is_public === 0) ? 0 : 1,
token,
heartbeat_timeout || 300000,
consecutive_failures_threshold || 1,
response_time_threshold || null,
http_method || 'GET',
http_body || null
http_body || null,
tags || null
);
// Invalidate related cache entries
@@ -401,7 +402,7 @@ app.post('/api/resources', (req, res) => {
// Update resource
app.put('/api/resources/:id', (req, res) => {
const { name, url, type, check_interval, timeout, enabled, group_id, http_keyword, http_headers, quiet_hours_start, quiet_hours_end, cert_expiry_days, sla_target, email_to, maintenance_mode, retention_days, is_public, heartbeat_timeout, consecutive_failures_threshold, response_time_threshold, http_method, http_body } = req.body;
const { name, url, type, check_interval, timeout, enabled, group_id, http_keyword, http_headers, quiet_hours_start, quiet_hours_end, cert_expiry_days, sla_target, email_to, maintenance_mode, retention_days, is_public, heartbeat_timeout, consecutive_failures_threshold, response_time_threshold, http_method, http_body, tags } = req.body;
const validationError = validateResourceBody(req.body);
if (validationError) return res.status(400).json({ error: validationError });
@@ -412,7 +413,7 @@ app.put('/api/resources/:id', (req, res) => {
const stmt = db.prepare(`
UPDATE resources
SET name = ?, url = ?, type = ?, check_interval = ?, timeout = ?, enabled = ?, group_id = ?, http_keyword = ?, http_headers = ?, quiet_hours_start = ?, quiet_hours_end = ?, cert_expiry_days = ?, sla_target = ?, email_to = ?, maintenance_mode = ?, retention_days = ?, is_public = ?, heartbeat_timeout = ?, consecutive_failures_threshold = ?, response_time_threshold = ?, http_method = ?, http_body = ?
SET name = ?, url = ?, type = ?, check_interval = ?, timeout = ?, enabled = ?, group_id = ?, http_keyword = ?, http_headers = ?, quiet_hours_start = ?, quiet_hours_end = ?, cert_expiry_days = ?, sla_target = ?, email_to = ?, maintenance_mode = ?, retention_days = ?, is_public = ?, heartbeat_timeout = ?, consecutive_failures_threshold = ?, response_time_threshold = ?, http_method = ?, http_body = ?, tags = ?
WHERE id = ?
`);
@@ -433,12 +434,13 @@ app.put('/api/resources/:id', (req, res) => {
email_to || null,
maintenance_mode ? 1 : 0,
retention_days || null,
is_public !== false ? 1 : 0,
(is_public === false || is_public === 0) ? 0 : 1,
heartbeat_timeout || 300000,
consecutive_failures_threshold || 1,
response_time_threshold || null,
http_method || 'GET',
http_body || null,
tags || null,
req.params.id
);
+14 -3
View File
@@ -121,6 +121,12 @@ class Scheduler {
async runChecks() {
const resources = db.prepare(`SELECT * FROM resources WHERE enabled = 1`).all();
// Read global defaults once per cycle so per-resource nulls inherit correctly
const globalIntervalRow = db.prepare(`SELECT value FROM settings WHERE key = 'check_interval'`).get();
const globalTimeoutRow = db.prepare(`SELECT value FROM settings WHERE key = 'timeout'`).get();
const globalCheckInterval = parseInt(globalIntervalRow?.value) || 60000;
const globalTimeout = parseInt(globalTimeoutRow?.value) || 5000;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const jitter = (maxMs = 1500) => Math.floor(Math.random() * maxMs);
const baseSpacingMs = 250;
@@ -129,16 +135,21 @@ class Scheduler {
const pendingAlerts = [];
const tasks = resources.map((resource, index) => (async () => {
// Honour per-resource check_interval — skip if not yet due
// Honour per-resource check_interval, falling back to global setting
const now = Date.now();
const lastChecked = this.lastCheckedAt.get(resource.id) || 0;
const interval = resource.check_interval || 60000;
const interval = resource.check_interval || globalCheckInterval;
if (now - lastChecked < interval) return;
this.lastCheckedAt.set(resource.id, now);
// Resolve effective timeout: per-resource override or global default
const effectiveResource = resource.timeout
? resource
: { ...resource, timeout: globalTimeout };
await sleep(index * baseSpacingMs + jitter());
try {
const result = await monitorService.checkResource(resource);
const result = await monitorService.checkResource(effectiveResource);
monitorService.saveCheck(result);
// Rolling cap per resource (configurable via settings; default 10000)