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:
1 parent
7968c9231e
commit
db85573986
37 files changed
+3521
-207
No files matched your search
+78
-40
@@ -7,6 +7,8 @@ const dns = require('dns').promises;
|
||||
const WebSocket = require('ws');
|
||||
const ping = require('ping');
|
||||
const db = require('./database');
|
||||
const RetryPolicy = require('./retryPolicy');
|
||||
const CircuitBreaker = require('./circuitBreaker');
|
||||
|
||||
const DEFAULT_PORTS = {
|
||||
http: 80,
|
||||
@@ -32,6 +34,33 @@ class MonitorService {
|
||||
// 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 });
|
||||
|
||||
// Circuit breaker for HTTP checks (5 failures before opening, 60s reset)
|
||||
this.httpCircuitBreaker = new CircuitBreaker(5, 60000, this.isTransientError);
|
||||
|
||||
// Retry policy: 3 retries with exponential backoff
|
||||
this.retryPolicy = new RetryPolicy(3, 1000, 10000);
|
||||
|
||||
// Circuit breakers per resource for fine-grained control
|
||||
this.resourceCircuitBreakers = new Map();
|
||||
}
|
||||
|
||||
getResourceCircuitBreaker(resourceId) {
|
||||
if (!this.resourceCircuitBreakers.has(resourceId)) {
|
||||
this.resourceCircuitBreakers.set(resourceId, new CircuitBreaker(3, 120000, this.isTransientError));
|
||||
}
|
||||
return this.resourceCircuitBreakers.get(resourceId);
|
||||
}
|
||||
|
||||
isTransientError(error) {
|
||||
// Only retry on transient errors (network, timeout, 5xx)
|
||||
if (error.code === 'ECONNREFUSED' || error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT') {
|
||||
return true;
|
||||
}
|
||||
if (error.response?.status >= 500) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
async checkResource(resource) {
|
||||
const startTime = Date.now();
|
||||
@@ -77,49 +106,59 @@ class MonitorService {
|
||||
|
||||
async checkHttp(resource, startTime, base) {
|
||||
let result = { ...base };
|
||||
const circuitBreaker = this.getResourceCircuitBreaker(resource.id);
|
||||
|
||||
try {
|
||||
const headers = {};
|
||||
if (resource.http_headers) {
|
||||
try {
|
||||
const customHeaders = JSON.parse(resource.http_headers);
|
||||
Object.assign(headers, customHeaders);
|
||||
} catch (e) {
|
||||
// Invalid JSON, skip custom headers
|
||||
}
|
||||
}
|
||||
await circuitBreaker.execute(async () => {
|
||||
return await this.retryPolicy.execute(async () => {
|
||||
const headers = {};
|
||||
if (resource.http_headers) {
|
||||
try {
|
||||
const customHeaders = JSON.parse(resource.http_headers);
|
||||
Object.assign(headers, customHeaders);
|
||||
} catch (e) {
|
||||
// Invalid JSON, skip custom headers
|
||||
}
|
||||
}
|
||||
|
||||
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 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;
|
||||
|
||||
// Check for keyword if specified
|
||||
let keywordMatch = true;
|
||||
if (resource.http_keyword && response.data) {
|
||||
const dataStr = typeof response.data === 'string' ? response.data : JSON.stringify(response.data);
|
||||
keywordMatch = dataStr.includes(resource.http_keyword);
|
||||
if (!keywordMatch) {
|
||||
result.error_message = `Keyword "${resource.http_keyword}" not found in response`;
|
||||
}
|
||||
}
|
||||
|
||||
result = {
|
||||
...result,
|
||||
status: (isUp && keywordMatch) ? 'up' : 'down',
|
||||
response_time: responseTime,
|
||||
status_code: response.status,
|
||||
error_message: isUp && keywordMatch ? null : (result.error_message || `HTTP ${response.status}`),
|
||||
};
|
||||
|
||||
return result;
|
||||
}, `HTTP check for ${resource.name}`);
|
||||
});
|
||||
const responseTime = Date.now() - startTime;
|
||||
const isUp = response.status >= 200 && response.status < 400;
|
||||
|
||||
// Check for keyword if specified
|
||||
let keywordMatch = true;
|
||||
if (resource.http_keyword && response.data) {
|
||||
const dataStr = typeof response.data === 'string' ? response.data : JSON.stringify(response.data);
|
||||
keywordMatch = dataStr.includes(resource.http_keyword);
|
||||
if (!keywordMatch) {
|
||||
result.error_message = `Keyword "${resource.http_keyword}" not found in response`;
|
||||
}
|
||||
}
|
||||
|
||||
result = {
|
||||
...result,
|
||||
status: (isUp && keywordMatch) ? 'up' : 'down',
|
||||
response_time: responseTime,
|
||||
status_code: response.status,
|
||||
error_message: isUp && keywordMatch ? null : (result.error_message || `HTTP ${response.status}`),
|
||||
};
|
||||
} catch (error) {
|
||||
result.error_message = error.message;
|
||||
result.response_time = Date.now() - startTime;
|
||||
const responseTime = Date.now() - startTime;
|
||||
result.error_message = error.originalError?.message || error.message;
|
||||
result.response_time = responseTime;
|
||||
result.status = 'down';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -706,7 +745,6 @@ class MonitorService {
|
||||
|
||||
return currentResponseTime > threshold;
|
||||
} catch (error) {
|
||||
console.error('Error checking response time anomaly:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user