Feature: Multi-email alerts and move resource between groups
- notificationService: Parse comma-separated emails from resource.email_to or global config; validate and send to each recipient - API: Add PATCH /api/resources/:id/group endpoint to move resource between groups with validation and cache invalidation - UI: Add group selector to edit modal; wire group change to PATCH endpoint; add help text for multi-email format
This commit is contained in:
1 parent
9c1c972045
commit
923c36e494
12 files changed
+115
-12
No files matched your search
+2
-1
@@ -113,8 +113,9 @@ db.exec(`
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_checks_resource_id ON checks(resource_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_checks_checked_at ON checks(checked_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_checks_resource_checked ON checks(resource_id, checked_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_checks_resource_checked ON checks(resource_id, checked_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_incidents_resource_id ON incidents(resource_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_incidents_resource_started ON incidents(resource_id, started_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_incidents_resolved ON incidents(resource_id, resolved_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_resources_group_id ON resources(group_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_maintenance_windows_resource ON maintenance_windows(resource_id);
|
||||
|
||||
@@ -219,6 +219,42 @@ app.put('/api/resources/:id', (req, res) => {
|
||||
res.json({ message: 'Resource updated' });
|
||||
});
|
||||
|
||||
// Move a resource to a different group
|
||||
app.patch('/api/resources/:id/group', (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
const { group_id } = req.body;
|
||||
if (!Number.isInteger(id)) {
|
||||
return res.status(400).json({ error: 'Invalid resource id' });
|
||||
}
|
||||
if (group_id === undefined || group_id === null || group_id === '') {
|
||||
return res.status(400).json({ error: 'group_id is required' });
|
||||
}
|
||||
const gid = parseInt(group_id, 10);
|
||||
if (!Number.isInteger(gid)) {
|
||||
return res.status(400).json({ error: 'Invalid group_id' });
|
||||
}
|
||||
|
||||
const stmt = db.prepare('UPDATE resources SET group_id = ? WHERE id = ?');
|
||||
const info = stmt.run(gid, id);
|
||||
if (info.changes === 0) {
|
||||
return res.status(404).json({ error: 'Resource not found' });
|
||||
}
|
||||
|
||||
// Invalidate caches and broadcast update
|
||||
cache.invalidatePattern('history:');
|
||||
cache.invalidatePattern('sla:');
|
||||
if (global.broadcastDashboardUpdate) {
|
||||
try { global.broadcastDashboardUpdate(); } catch {}
|
||||
}
|
||||
|
||||
const updated = db.prepare('SELECT * FROM resources WHERE id = ?').get(id);
|
||||
res.json(updated);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Failed to update group', details: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete resource
|
||||
app.delete('/api/resources/:id', (req, res) => {
|
||||
db.prepare('DELETE FROM resources WHERE id = ?').run(req.params.id);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
const axios = require('axios');
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const net = require('net');
|
||||
const tls = require('tls');
|
||||
const dns = require('dns').promises;
|
||||
@@ -26,6 +28,11 @@ function parseHostPort(urlString, fallbackPort) {
|
||||
}
|
||||
|
||||
class MonitorService {
|
||||
constructor() {
|
||||
// Keep-alive agents for pooled HTTP/S connections
|
||||
this.httpAgent = new http.Agent({ keepAlive: true, maxSockets: 50 });
|
||||
this.httpsAgent = new https.Agent({ keepAlive: true, maxSockets: 50 });
|
||||
}
|
||||
async checkResource(resource) {
|
||||
const startTime = Date.now();
|
||||
const base = {
|
||||
@@ -81,11 +88,14 @@ class MonitorService {
|
||||
}
|
||||
}
|
||||
|
||||
const isHttps = resource.url.startsWith('https');
|
||||
const response = await axios.get(resource.url, {
|
||||
timeout: resource.timeout,
|
||||
validateStatus: () => true,
|
||||
maxRedirects: 5,
|
||||
headers,
|
||||
httpAgent: this.httpAgent,
|
||||
httpsAgent: this.httpsAgent,
|
||||
});
|
||||
const responseTime = Date.now() - startTime;
|
||||
const isUp = response.status >= 200 && response.status < 400;
|
||||
|
||||
@@ -76,8 +76,17 @@ class NotificationService {
|
||||
const promises = [];
|
||||
|
||||
// Use resource-specific email if provided, otherwise fall back to global config
|
||||
if (this.emailEnabled && (resource.email_to || this.config.email_to)) {
|
||||
promises.push(this.sendEmail(resource, message, incident.type, stats));
|
||||
if (this.emailEnabled && (resource.email_to || this.config.email_to)) {
|
||||
const targetsCsv = resource.email_to || this.config.email_to;
|
||||
const targets = String(targetsCsv)
|
||||
.split(',')
|
||||
.map(t => t.trim())
|
||||
.filter(t => t && /.+@.+\..+/.test(t));
|
||||
if (targets.length > 0) {
|
||||
for (const email of targets) {
|
||||
promises.push(this.sendEmail(resource, message, incident.type, stats, email));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!this.emailEnabled) {
|
||||
// Email globally disabled
|
||||
@@ -158,7 +167,7 @@ class NotificationService {
|
||||
htmlContent += `</div>`;
|
||||
|
||||
// Use resource-specific email if provided, otherwise use global config
|
||||
const emailTo = resource.email_to || this.config.email_to;
|
||||
const emailTo = emailOverride || resource.email_to || this.config.email_to;
|
||||
|
||||
await this.transporter.sendMail({
|
||||
from: this.config.email_from,
|
||||
|
||||
@@ -92,6 +92,29 @@ class Scheduler {
|
||||
const result = await monitorService.checkResource(resource);
|
||||
monitorService.saveCheck(result);
|
||||
|
||||
// Rolling cap per resource (configurable via settings; default 10000)
|
||||
try {
|
||||
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 = ?
|
||||
)
|
||||
)
|
||||
`).run(resource.id, cap, resource.id);
|
||||
}
|
||||
} catch (e) {
|
||||
// Non-fatal
|
||||
}
|
||||
|
||||
const incident = monitorService.handleIncident(
|
||||
resource.id,
|
||||
result.status === 'down'
|
||||
|
||||
Reference in new issue
Block a user