feat: Add real-time notification center with WebSocket integration

- Add in-app notification system with database storage
- Implement bell icon component with unread count badge in dashboard header
- Add full Notifications page tab with pagination and filtering (All/Unread/Read)
- Implement real-time WebSocket push notifications to all connected clients
- Add notification API endpoints: GET, PUT /read, DELETE, POST /clear
- Fix critical bug in notificationService where 'status' variable was undefined
- Add Notifications route to App.js routing
- Enhanced UI with notification popup in header and dedicated full-page view
- Auto-reconnect WebSocket with 5s timeout on disconnect
- Support rich notification content with resource details and timestamps
This commit is contained in:
nmemmert committed 2026-01-14 14:51:29 -05:00
1 parent 7968c9231e
commit db85573986
37 files changed
+3521 -207

No files matched your search

+44 -27
View File
@@ -6,12 +6,20 @@ const notificationService = require('./notificationService');
class Scheduler {
constructor() {
this.jobs = new Map();
this.isRunning = false;
}
start() {
// Run checks every minute
cron.schedule('* * * * *', async () => {
await this.runChecks();
if (!this.isRunning) {
this.isRunning = true;
try {
await this.runChecks();
} finally {
this.isRunning = false;
}
}
});
// Archive old checks daily at 2 AM
@@ -23,37 +31,44 @@ class Scheduler {
setTimeout(() => {
this.archiveOldChecks();
}, 5000);
console.log('Scheduler started');
}
archiveOldChecks() {
try {
// Get global retention setting
const retentionSetting = db.prepare(`
SELECT value FROM settings WHERE key = 'retention_days'
`).get();
const retentionDays = retentionSetting ? parseInt(retentionSetting.value) : 30;
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - retentionDays);
const cutoffString = cutoffDate.toISOString().replace('T', ' ').split('.')[0];
const globalRetentionDays = retentionSetting ? parseInt(retentionSetting.value) : 30;
// Move old checks to archived_checks
const oldChecks = db.prepare(`
SELECT * FROM checks WHERE checked_at < ?
`).all(cutoffString);
// Get all resources with their retention settings
const resources = db.prepare(`
SELECT id, retention_days FROM resources
`).all();
if (oldChecks.length > 0) {
const insertArchived = db.prepare(`
INSERT INTO archived_checks (resource_id, status, response_time, status_code, error_message, details, checked_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
`);
const insertArchived = 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 = ?
`);
const deleteOld = db.prepare(`
DELETE FROM checks WHERE id = ?
`);
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);
const transaction = db.transaction(() => {
oldChecks.forEach(check => {
insertArchived.run(
check.resource_id,
@@ -67,12 +82,11 @@ class Scheduler {
deleteOld.run(check.id);
});
});
});
transaction();
console.log(`[Archival] Archived ${oldChecks.length} checks older than ${retentionDays} days`);
}
transaction();
} catch (error) {
console.error('[Archival] Error archiving checks:', error.message);
// Archive error handled silently
}
}
@@ -133,10 +147,8 @@ class Scheduler {
await notificationService.sendAlert(resource, incident, stats);
}
}
console.log(`Checked ${resource.name}: ${result.status} (${result.response_time}ms)`);
} catch (error) {
console.error(`Error checking ${resource.name}:`, error.message);
// Check error handled silently
}
})());
@@ -147,6 +159,11 @@ class Scheduler {
global.broadcastDashboardUpdate();
}
}
stop() {
// Stop the scheduler (cron tasks will continue running but we flag isRunning)
this.isRunning = false;
}
}
module.exports = new Scheduler();