Add heartbeat monitoring, alert rules, cert badges, webhook templates, and status page visibility
- Heartbeat type: cron jobs POST to /api/heartbeat/:token to stay alive; scheduler marks
them down if no ping is received within the configured timeout window
- Alert rules: per-resource consecutive_failures_threshold now respected in handleIncident,
overriding the global setting; GET /api/resources/:id/alert-rules endpoint added
- SSL cert expiry badge: dashboard rows surface days_remaining for TLS monitors, turning
orange at ≤30 days and red at ≤7 days
- Webhook payload templates: {{name}}/{{status}}/{{url}}/{{type}}/{{message}}/{{timestamp}}
variables interpolated before POST; Slack-compatible text wrapping as fallback
- Status page visibility: is_public column per resource; GET /api/status-page returns only
public resources; Status.js updated to use the new endpoint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
34986b5760
commit
a6cf6a9762
16 files changed
+554
-422
No files matched your search
+45
-40
@@ -7,6 +7,8 @@ class Scheduler {
|
||||
constructor() {
|
||||
this.jobs = new Map();
|
||||
this.isRunning = false;
|
||||
this.lastCheckedAt = new Map();
|
||||
this.slowAlertCooldown = new Map();
|
||||
}
|
||||
|
||||
start() {
|
||||
@@ -47,40 +49,21 @@ class Scheduler {
|
||||
SELECT id, retention_days FROM resources
|
||||
`).all();
|
||||
|
||||
const insertArchived = db.prepare(`
|
||||
const bulkArchive = db.prepare(`
|
||||
INSERT INTO archived_checks (resource_id, status, response_time, status_code, error_message, details, checked_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const deleteOld = db.prepare(`
|
||||
DELETE FROM checks WHERE id = ?
|
||||
SELECT resource_id, status, response_time, status_code, error_message, details, checked_at
|
||||
FROM checks WHERE resource_id = ? AND checked_at < ?
|
||||
`);
|
||||
const bulkDelete = db.prepare(`DELETE FROM checks WHERE resource_id = ? AND checked_at < ?`);
|
||||
|
||||
const transaction = db.transaction(() => {
|
||||
resources.forEach(resource => {
|
||||
// Use per-resource retention if set, otherwise use global
|
||||
const retentionDays = resource.retention_days || globalRetentionDays;
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - retentionDays);
|
||||
const cutoffString = cutoffDate.toISOString().replace('T', ' ').split('.')[0];
|
||||
|
||||
// Get old checks for this resource
|
||||
const oldChecks = db.prepare(`
|
||||
SELECT * FROM checks WHERE resource_id = ? AND checked_at < ?
|
||||
`).all(resource.id, cutoffString);
|
||||
|
||||
oldChecks.forEach(check => {
|
||||
insertArchived.run(
|
||||
check.resource_id,
|
||||
check.status,
|
||||
check.response_time,
|
||||
check.status_code,
|
||||
check.error_message,
|
||||
check.details,
|
||||
check.checked_at
|
||||
);
|
||||
deleteOld.run(check.id);
|
||||
});
|
||||
bulkArchive.run(resource.id, cutoffString);
|
||||
bulkDelete.run(resource.id, cutoffString);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -101,6 +84,13 @@ class Scheduler {
|
||||
|
||||
const baseSpacingMs = 250; // spread starts in 250ms increments
|
||||
const tasks = resources.map((resource, index) => (async () => {
|
||||
// Honour per-resource check_interval — skip if not yet due
|
||||
const now = Date.now();
|
||||
const lastChecked = this.lastCheckedAt.get(resource.id) || 0;
|
||||
const interval = resource.check_interval || 60000;
|
||||
if (now - lastChecked < interval) return;
|
||||
this.lastCheckedAt.set(resource.id, now);
|
||||
|
||||
await sleep(index * baseSpacingMs + jitter());
|
||||
try {
|
||||
const result = await monitorService.checkResource(resource);
|
||||
@@ -111,19 +101,15 @@ class Scheduler {
|
||||
const capSetting = db.prepare(`SELECT value FROM settings WHERE key = 'checks_cap_per_resource'`).get();
|
||||
const cap = capSetting ? parseInt(capSetting.value) : 10000;
|
||||
if (Number.isFinite(cap) && cap > 0) {
|
||||
// Delete oldest rows beyond cap, using id ordering per resource
|
||||
db.prepare(`
|
||||
DELETE FROM checks
|
||||
WHERE id IN (
|
||||
SELECT id FROM checks
|
||||
WHERE resource_id = ?
|
||||
ORDER BY id ASC
|
||||
LIMIT (
|
||||
SELECT MAX(COUNT(*) - ?, 0)
|
||||
FROM checks WHERE resource_id = ?
|
||||
const rowCount = db.prepare('SELECT COUNT(*) as cnt FROM checks WHERE resource_id = ?').get(resource.id).cnt;
|
||||
const excess = rowCount - cap;
|
||||
if (excess > 0) {
|
||||
db.prepare(`
|
||||
DELETE FROM checks WHERE id IN (
|
||||
SELECT id FROM checks WHERE resource_id = ? ORDER BY id ASC LIMIT ?
|
||||
)
|
||||
)
|
||||
`).run(resource.id, cap, resource.id);
|
||||
`).run(resource.id, excess);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Non-fatal
|
||||
@@ -137,9 +123,9 @@ class Scheduler {
|
||||
if (incident.type !== 'none') {
|
||||
const stats = monitorService.getResourceStats(resource.id, 24);
|
||||
const recentChecks = db.prepare(`
|
||||
SELECT response_time, status FROM checks
|
||||
WHERE resource_id = ?
|
||||
ORDER BY checked_at DESC
|
||||
SELECT response_time, status FROM checks
|
||||
WHERE resource_id = ?
|
||||
ORDER BY checked_at DESC
|
||||
LIMIT 12
|
||||
`).all(resource.id);
|
||||
stats.recentChecks = recentChecks.reverse();
|
||||
@@ -147,6 +133,25 @@ class Scheduler {
|
||||
await notificationService.sendAlert(resource, incident, stats);
|
||||
}
|
||||
}
|
||||
|
||||
// Response time threshold alert (only when up, with 1-hour cooldown)
|
||||
if (
|
||||
result.status === 'up' &&
|
||||
resource.response_time_threshold &&
|
||||
result.response_time > resource.response_time_threshold &&
|
||||
!resource.maintenance_mode
|
||||
) {
|
||||
const lastSlowAlert = this.slowAlertCooldown.get(resource.id) || 0;
|
||||
if (now - lastSlowAlert > 60 * 60 * 1000) {
|
||||
this.slowAlertCooldown.set(resource.id, now);
|
||||
await notificationService.sendAlert(resource, {
|
||||
type: 'slow',
|
||||
responseTime: result.response_time,
|
||||
threshold: resource.response_time_threshold,
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Check error handled silently
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user