diff --git a/server/cache.js b/server/cache.js new file mode 100644 index 0000000..0132b22 --- /dev/null +++ b/server/cache.js @@ -0,0 +1,54 @@ +// Simple in-memory cache with TTL support +class CacheManager { + constructor() { + this.cache = new Map(); + } + + set(key, value, ttlSeconds = 300) { + const expiresAt = Date.now() + (ttlSeconds * 1000); + this.cache.set(key, { value, expiresAt }); + } + + get(key) { + const item = this.cache.get(key); + if (!item) return null; + + if (Date.now() > item.expiresAt) { + this.cache.delete(key); + return null; + } + + return item.value; + } + + has(key) { + return this.get(key) !== null; + } + + delete(key) { + this.cache.delete(key); + } + + clear() { + this.cache.clear(); + } + + // Invalidate all cache keys matching a pattern + invalidatePattern(pattern) { + for (const key of this.cache.keys()) { + if (key.includes(pattern)) { + this.cache.delete(key); + } + } + } + + // Get cache statistics + getStats() { + return { + size: this.cache.size, + keys: Array.from(this.cache.keys()), + }; + } +} + +module.exports = new CacheManager(); diff --git a/server/database.js b/server/database.js index 5b73cc2..942dc39 100644 --- a/server/database.js +++ b/server/database.js @@ -100,6 +100,10 @@ try { db.prepare("ALTER TABLE resources ADD COLUMN sla_target REAL DEFAULT 99.9").run(); } catch (err) {} +try { + db.prepare("ALTER TABLE resources ADD COLUMN email_to TEXT").run(); +} catch (err) {} + try { db.prepare("ALTER TABLE incidents ADD COLUMN acknowledged INTEGER DEFAULT 0").run(); } catch (err) {} diff --git a/server/index.js b/server/index.js index e8fe307..ddde1fb 100644 --- a/server/index.js +++ b/server/index.js @@ -7,6 +7,7 @@ const db = require('./database'); const scheduler = require('./scheduler'); const monitorService = require('./monitorService'); const notificationService = require('./notificationService'); +const cache = require('./cache'); const app = express(); const PORT = process.env.PORT || 3001; @@ -99,6 +100,10 @@ app.post('/api/resources', (req, res) => { email_to || null ); + // Invalidate related cache entries + cache.invalidatePattern('history:'); + cache.invalidatePattern('sla:'); + res.json({ id: result.lastInsertRowid, message: 'Resource created' }); }); @@ -129,12 +134,22 @@ app.put('/api/resources/:id', (req, res) => { email_to || null, req.params.id ); + + // Invalidate related cache entries + cache.invalidatePattern('history:'); + cache.invalidatePattern('sla:'); + res.json({ message: 'Resource updated' }); }); // Delete resource app.delete('/api/resources/:id', (req, res) => { db.prepare('DELETE FROM resources WHERE id = ?').run(req.params.id); + + // Invalidate related cache entries + cache.invalidatePattern('history:'); + cache.invalidatePattern('sla:'); + res.json({ message: 'Resource deleted' }); }); @@ -331,6 +346,15 @@ app.get('/api/history/overview', (req, res) => { const currentPage = Math.max(1, parseInt(pageParam || page || 1)); const offset = (currentPage - 1) * pageLimit; + // Create cache key based on query parameters + const cacheKey = `history:days=${days}:page=${currentPage}:limit=${pageLimit}`; + + // Check cache first + const cachedResult = cache.get(cacheKey); + if (cachedResult) { + return res.json(cachedResult); + } + // Get total count of enabled resources const totalCount = db.prepare('SELECT COUNT(*) as count FROM resources WHERE enabled = 1').get(); const total = totalCount.count; @@ -377,7 +401,12 @@ app.get('/api/history/overview', (req, res) => { }; }); - res.json({ resources: overview, total, page: currentPage, limit: pageLimit }); + const result = { resources: overview, total, page: currentPage, limit: pageLimit }; + + // Cache result for 2 minutes (120 seconds) + cache.set(cacheKey, result, 120); + + res.json(result); }); // Acknowledge incident @@ -407,6 +436,15 @@ app.get('/api/sla', (req, res) => { const currentPage = Math.max(1, parseInt(page)); const offset = (currentPage - 1) * pageLimit; + // Create cache key + const cacheKey = `sla:days=${days}:page=${currentPage}:limit=${pageLimit}`; + + // Check cache first + const cachedResult = cache.get(cacheKey); + if (cachedResult) { + return res.json(cachedResult); + } + // Get total count const totalCount = db.prepare('SELECT COUNT(*) as count FROM resources WHERE enabled = 1').get(); const total = totalCount.count; @@ -448,7 +486,12 @@ app.get('/api/sla', (req, res) => { }; }); - res.json({ resources: slaData, total, page: currentPage, limit: pageLimit }); + const result = { resources: slaData, total, page: currentPage, limit: pageLimit }; + + // Cache result for 3 minutes (180 seconds) + cache.set(cacheKey, result, 180); + + res.json(result); }); // Test email diff --git a/server/notificationService.js b/server/notificationService.js index cdee9ac..1aec8b4 100644 --- a/server/notificationService.js +++ b/server/notificationService.js @@ -69,7 +69,8 @@ class NotificationService { const promises = []; - if (this.emailEnabled) { + // 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)); } @@ -141,14 +142,17 @@ class NotificationService { htmlContent += ``; + // Use resource-specific email if provided, otherwise use global config + const emailTo = resource.email_to || this.config.email_to; + await this.transporter.sendMail({ from: this.config.email_from, - to: this.config.email_to, + to: emailTo, subject: `Alert: ${resource.name} is ${type === 'started' ? 'DOWN' : 'UP'}`, text: message, html: htmlContent, }); - console.log(`Email sent for ${resource.name}`); + console.log(`Email sent for ${resource.name} to ${emailTo}`); } catch (error) { console.error('Email error:', error.message); }