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:
1 parent
3a3d32ecac
commit
59078362da
4 files changed
+49
-18
No files matched your search
+15
-7
@@ -22,8 +22,8 @@ function Dashboard() {
|
|||||||
name: '',
|
name: '',
|
||||||
url: '',
|
url: '',
|
||||||
type: 'http',
|
type: 'http',
|
||||||
check_interval: 60000,
|
check_interval: null,
|
||||||
timeout: 5000,
|
timeout: null,
|
||||||
group_id: null,
|
group_id: null,
|
||||||
http_keyword: '',
|
http_keyword: '',
|
||||||
http_headers: '',
|
http_headers: '',
|
||||||
@@ -48,6 +48,8 @@ function Dashboard() {
|
|||||||
const [sortKey, setSortKey] = useState('severity');
|
const [sortKey, setSortKey] = useState('severity');
|
||||||
const [renderLimit, setRenderLimit] = useState(120);
|
const [renderLimit, setRenderLimit] = useState(120);
|
||||||
const [refreshInterval, setRefreshInterval] = useState(15000);
|
const [refreshInterval, setRefreshInterval] = useState(15000);
|
||||||
|
const [globalCheckInterval, setGlobalCheckInterval] = useState(60000);
|
||||||
|
const [globalTimeout, setGlobalTimeout] = useState(5000);
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
const [pendingDeleteId, setPendingDeleteId] = useState(null);
|
const [pendingDeleteId, setPendingDeleteId] = useState(null);
|
||||||
const [pendingBulkDelete, setPendingBulkDelete] = useState(false);
|
const [pendingBulkDelete, setPendingBulkDelete] = useState(false);
|
||||||
@@ -144,6 +146,8 @@ function Dashboard() {
|
|||||||
if (s.items_per_page) setRenderLimit(parseInt(s.items_per_page));
|
if (s.items_per_page) setRenderLimit(parseInt(s.items_per_page));
|
||||||
if (s.default_sort) setSortKey(s.default_sort);
|
if (s.default_sort) setSortKey(s.default_sort);
|
||||||
if (s.refresh_interval) setRefreshInterval(parseInt(s.refresh_interval));
|
if (s.refresh_interval) setRefreshInterval(parseInt(s.refresh_interval));
|
||||||
|
if (s.check_interval) setGlobalCheckInterval(parseInt(s.check_interval));
|
||||||
|
if (s.timeout) setGlobalTimeout(parseInt(s.timeout));
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -159,7 +163,7 @@ function Dashboard() {
|
|||||||
}, [connectWebSocket, loadResources, refreshInterval]);
|
}, [connectWebSocket, loadResources, refreshInterval]);
|
||||||
|
|
||||||
const emptyForm = {
|
const emptyForm = {
|
||||||
name: '', url: '', type: 'http', check_interval: 60000, timeout: 5000,
|
name: '', url: '', type: 'http', check_interval: null, timeout: null,
|
||||||
group_id: null, http_keyword: '', http_headers: '', quiet_hours_start: '',
|
group_id: null, http_keyword: '', http_headers: '', quiet_hours_start: '',
|
||||||
quiet_hours_end: '', cert_expiry_days: 30, sla_target: 99.9, email_to: '',
|
quiet_hours_end: '', cert_expiry_days: 30, sla_target: 99.9, email_to: '',
|
||||||
maintenance_mode: false, tags: '', consecutive_failures_threshold: 1,
|
maintenance_mode: false, tags: '', consecutive_failures_threshold: 1,
|
||||||
@@ -825,13 +829,15 @@ function Dashboard() {
|
|||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>Check Interval (ms)</label>
|
<label>Check Interval (ms)</label>
|
||||||
<input type="number" value={formData.check_interval} onChange={(e) => setFormData({ ...formData, check_interval: parseInt(e.target.value) })} min="10000" />
|
<input type="number" value={formData.check_interval ?? ''} placeholder={`${globalCheckInterval} (global default)`} onChange={(e) => setFormData({ ...formData, check_interval: e.target.value ? parseInt(e.target.value) : null })} min="10000" />
|
||||||
|
<small>Leave empty to use global setting ({globalCheckInterval}ms)</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{formData.type !== 'heartbeat' && (
|
{formData.type !== 'heartbeat' && (
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>Timeout (ms)</label>
|
<label>Timeout (ms)</label>
|
||||||
<input type="number" value={formData.timeout} onChange={(e) => setFormData({ ...formData, timeout: parseInt(e.target.value) })} min="1000" />
|
<input type="number" value={formData.timeout ?? ''} placeholder={`${globalTimeout} (global default)`} onChange={(e) => setFormData({ ...formData, timeout: e.target.value ? parseInt(e.target.value) : null })} min="1000" />
|
||||||
|
<small>Leave empty to use global setting ({globalTimeout}ms)</small>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -965,12 +971,14 @@ function Dashboard() {
|
|||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>Check Interval (ms)</label>
|
<label>Check Interval (ms)</label>
|
||||||
<input type="number" value={editData.check_interval || 60000} onChange={(e) => setEditData({ ...editData, check_interval: parseInt(e.target.value) })} min="10000" />
|
<input type="number" value={editData.check_interval ?? ''} placeholder={`${globalCheckInterval} (global default)`} onChange={(e) => setEditData({ ...editData, check_interval: e.target.value ? parseInt(e.target.value) : null })} min="10000" />
|
||||||
|
<small>Leave empty to use global setting ({globalCheckInterval}ms)</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>Timeout (ms)</label>
|
<label>Timeout (ms)</label>
|
||||||
<input type="number" value={editData.timeout || 5000} onChange={(e) => setEditData({ ...editData, timeout: parseInt(e.target.value) })} min="1000" />
|
<input type="number" value={editData.timeout ?? ''} placeholder={`${globalTimeout} (global default)`} onChange={(e) => setEditData({ ...editData, timeout: e.target.value ? parseInt(e.target.value) : null })} min="1000" />
|
||||||
|
<small>Leave empty to use global setting ({globalTimeout}ms)</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(editData.type === 'http' || editData.type === 'https' || editData.type === 'health') && (
|
{(editData.type === 'http' || editData.type === 'https' || editData.type === 'health') && (
|
||||||
|
|||||||
@@ -333,4 +333,14 @@ db.exec(`
|
|||||||
CREATE INDEX IF NOT EXISTS idx_resources_heartbeat_token ON resources(heartbeat_token);
|
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;
|
module.exports = db;
|
||||||
+10
-8
@@ -343,7 +343,7 @@ app.get('/api/resources/:id', (req, res) => {
|
|||||||
|
|
||||||
// Create resource
|
// Create resource
|
||||||
app.post('/api/resources', (req, res) => {
|
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);
|
const validationError = validateResourceBody(req.body);
|
||||||
if (validationError) return res.status(400).json({ error: validationError });
|
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 effectiveUrl = isHeartbeat ? `heartbeat://${name}` : url;
|
||||||
|
|
||||||
const stmt = db.prepare(`
|
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)
|
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const result = stmt.run(
|
const result = stmt.run(
|
||||||
@@ -374,13 +374,14 @@ app.post('/api/resources', (req, res) => {
|
|||||||
email_to || null,
|
email_to || null,
|
||||||
maintenance_mode ? 1 : 0,
|
maintenance_mode ? 1 : 0,
|
||||||
retention_days || null,
|
retention_days || null,
|
||||||
is_public !== false ? 1 : 0,
|
(is_public === false || is_public === 0) ? 0 : 1,
|
||||||
token,
|
token,
|
||||||
heartbeat_timeout || 300000,
|
heartbeat_timeout || 300000,
|
||||||
consecutive_failures_threshold || 1,
|
consecutive_failures_threshold || 1,
|
||||||
response_time_threshold || null,
|
response_time_threshold || null,
|
||||||
http_method || 'GET',
|
http_method || 'GET',
|
||||||
http_body || null
|
http_body || null,
|
||||||
|
tags || null
|
||||||
);
|
);
|
||||||
|
|
||||||
// Invalidate related cache entries
|
// Invalidate related cache entries
|
||||||
@@ -401,7 +402,7 @@ app.post('/api/resources', (req, res) => {
|
|||||||
|
|
||||||
// Update resource
|
// Update resource
|
||||||
app.put('/api/resources/:id', (req, res) => {
|
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);
|
const validationError = validateResourceBody(req.body);
|
||||||
if (validationError) return res.status(400).json({ error: validationError });
|
if (validationError) return res.status(400).json({ error: validationError });
|
||||||
@@ -412,7 +413,7 @@ app.put('/api/resources/:id', (req, res) => {
|
|||||||
|
|
||||||
const stmt = db.prepare(`
|
const stmt = db.prepare(`
|
||||||
UPDATE resources
|
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 = ?
|
WHERE id = ?
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@@ -433,12 +434,13 @@ app.put('/api/resources/:id', (req, res) => {
|
|||||||
email_to || null,
|
email_to || null,
|
||||||
maintenance_mode ? 1 : 0,
|
maintenance_mode ? 1 : 0,
|
||||||
retention_days || null,
|
retention_days || null,
|
||||||
is_public !== false ? 1 : 0,
|
(is_public === false || is_public === 0) ? 0 : 1,
|
||||||
heartbeat_timeout || 300000,
|
heartbeat_timeout || 300000,
|
||||||
consecutive_failures_threshold || 1,
|
consecutive_failures_threshold || 1,
|
||||||
response_time_threshold || null,
|
response_time_threshold || null,
|
||||||
http_method || 'GET',
|
http_method || 'GET',
|
||||||
http_body || null,
|
http_body || null,
|
||||||
|
tags || null,
|
||||||
req.params.id
|
req.params.id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+14
-3
@@ -121,6 +121,12 @@ class Scheduler {
|
|||||||
async runChecks() {
|
async runChecks() {
|
||||||
const resources = db.prepare(`SELECT * FROM resources WHERE enabled = 1`).all();
|
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 sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
const jitter = (maxMs = 1500) => Math.floor(Math.random() * maxMs);
|
const jitter = (maxMs = 1500) => Math.floor(Math.random() * maxMs);
|
||||||
const baseSpacingMs = 250;
|
const baseSpacingMs = 250;
|
||||||
@@ -129,16 +135,21 @@ class Scheduler {
|
|||||||
const pendingAlerts = [];
|
const pendingAlerts = [];
|
||||||
|
|
||||||
const tasks = resources.map((resource, index) => (async () => {
|
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 now = Date.now();
|
||||||
const lastChecked = this.lastCheckedAt.get(resource.id) || 0;
|
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;
|
if (now - lastChecked < interval) return;
|
||||||
this.lastCheckedAt.set(resource.id, now);
|
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());
|
await sleep(index * baseSpacingMs + jitter());
|
||||||
try {
|
try {
|
||||||
const result = await monitorService.checkResource(resource);
|
const result = await monitorService.checkResource(effectiveResource);
|
||||||
monitorService.saveCheck(result);
|
monitorService.saveCheck(result);
|
||||||
|
|
||||||
// Rolling cap per resource (configurable via settings; default 10000)
|
// Rolling cap per resource (configurable via settings; default 10000)
|
||||||
|
|||||||
Reference in new issue
Block a user