feat: premium tier features — auto-backup scheduling, resource alerting, bundled workflows, one-click revert, notification manager

This commit is contained in:
Hermes
2026-05-27 23:39:46 -07:00
parent 6ce0a18f98
commit 11823a1466
19 changed files with 3686 additions and 407 deletions
+152 -3
View File
@@ -16,6 +16,7 @@ const STATS_FILE = process.env.STATS_FILE || path.join(__dirname, 'container-sta
const STATS_HOURLY_FILE = process.env.STATS_HOURLY_FILE || path.join(__dirname, 'container-stats-hourly.json');
const STATS_DAILY_FILE = process.env.STATS_DAILY_FILE || path.join(__dirname, 'container-stats-daily.json');
const ALERT_CONFIG_FILE = process.env.ALERT_CONFIG_FILE || path.join(__dirname, 'alert-config.json');
const ALERT_HISTORY_FILE = process.env.ALERT_HISTORY_FILE || path.join(__dirname, 'alert-history.json');
const STATS_RETENTION_HOURS = parseInt(process.env.STATS_RETENTION_HOURS || '168', 10); // 7 days raw
const STATS_HOURLY_RETENTION_DAYS = parseInt(process.env.STATS_HOURLY_RETENTION_DAYS || '30', 10); // 30 days hourly
const STATS_DAILY_RETENTION_DAYS = parseInt(process.env.STATS_DAILY_RETENTION_DAYS || '365', 10); // 365 days daily
@@ -35,11 +36,21 @@ class ResourceMonitor extends EventEmitter {
this.dailyHistory = new Map(); // containerId -> { name, samples: [...] } (daily avg, 365d)
this.alerts = new Map(); // containerId -> alert config
this.lastAlerts = new Map(); // containerId -> last alert timestamp
this.alertHistory = []; // alert history entries
this.notificationManager = null;
this.loadStats();
this.loadHourlyStats();
this.loadDailyStats();
this.loadAlertConfig();
this.loadAlertHistory();
}
/**
* Set the notification manager for sending alerts
*/
setNotificationManager(nm) {
this.notificationManager = nm;
}
/**
@@ -285,20 +296,58 @@ class ResourceMonitor extends EventEmitter {
if (alerts.length > 0) {
this.lastAlerts.set(containerId, now);
this.emit('alert', {
// Add alert history entries
for (const alert of alerts) {
this.addAlertHistoryEntry({
id: `${containerId}-${Date.now()}-${alert.type}`,
timestamp: new Date().toISOString(),
containerId,
containerName,
type: alert.type,
metric: alert.type,
value: alert.value,
threshold: alert.threshold,
severity: alert.severity,
notified: !!this.notificationManager,
autoRestartTriggered: !!alertConfig.autoRestart
});
}
const alertPayload = {
containerId,
containerName,
timestamp: new Date().toISOString(),
alerts,
stats,
config: alertConfig
});
};
this.emit('alert', alertPayload);
// Send notification if manager is configured
if (this.notificationManager) {
this.notificationManager.sendAlert(alertPayload).catch(err => {
console.error('[ResourceMonitor] Failed to send alert notification:', err.message);
});
}
// Auto-restart if configured
if (alertConfig.autoRestart) {
this.restartContainer(containerId, containerName, alerts);
}
// Trigger bundled workflows for resource-alert
this.triggerWorkflows('resource-alert', {
containerId,
containerName,
alerts,
stats,
diskPercent: (stats.disk?.readBytes + stats.disk?.writeBytes) > 0
? Math.round((stats.disk.readBytes / (stats.disk.readBytes + stats.disk.writeBytes)) * 100)
: 0,
host: require('os').hostname()
});
}
}
@@ -318,11 +367,55 @@ class ResourceMonitor extends EventEmitter {
timestamp: new Date().toISOString(),
reason: alerts
});
// Send notification if manager is configured
if (this.notificationManager) {
this.notificationManager.send('auto-restart', {
containerId,
containerName,
timestamp: new Date().toISOString(),
reason: alerts
}).catch(err => {
console.error('[ResourceMonitor] Failed to send auto-restart notification:', err.message);
});
}
} catch (error) {
console.error(`[ResourceMonitor] Failed to restart ${containerName}:`, error.message);
}
}
/**
* Trigger bundled workflows for an event
*/
triggerWorkflows(eventType, eventData) {
if (!this.workflowEngine) {
console.log('[ResourceMonitor] Workflow engine not set, skipping workflow trigger');
return;
}
try {
this.workflowEngine.triggerForEvent(eventType, eventData)
.then(results => {
if (results && results.length > 0) {
console.log(`[ResourceMonitor] Triggered ${results.length} workflow(s) for ${eventType}`);
}
})
.catch(err => {
console.error('[ResourceMonitor] Workflow trigger error:', err.message);
});
} catch (error) {
console.error('[ResourceMonitor] Error triggering workflows:', error.message);
}
}
/**
* Set the workflow engine for triggering workflows
*/
setWorkflowEngine(workflowEngine) {
this.workflowEngine = workflowEngine;
console.log('[ResourceMonitor] Workflow engine configured');
}
/**
* Get current stats for a container
*/
@@ -430,6 +523,62 @@ class ResourceMonitor extends EventEmitter {
this.saveAlertConfig();
}
/**
* Get all alert configurations
*/
getAllAlertConfigs() {
const configs = {};
for (const [containerId, config] of this.alerts.entries()) {
configs[containerId] = config;
}
return configs;
}
/**
* Add entry to alert history
*/
addAlertHistoryEntry(entry) {
this.alertHistory.unshift(entry);
// Keep only last 1000 entries
if (this.alertHistory.length > 1000) {
this.alertHistory = this.alertHistory.slice(0, 1000);
}
this.saveAlertHistory();
}
/**
* Get alert history
*/
getAlertHistory(limit = 50) {
return this.alertHistory.slice(0, limit);
}
/**
* Load alert history from disk
*/
loadAlertHistory() {
try {
if (fs.existsSync(ALERT_HISTORY_FILE)) {
const data = JSON.parse(fs.readFileSync(ALERT_HISTORY_FILE, 'utf8'));
this.alertHistory = Array.isArray(data) ? data : [];
console.log(`[ResourceMonitor] Loaded ${this.alertHistory.length} alert history entries`);
}
} catch (error) {
console.error('[ResourceMonitor] Error loading alert history:', error.message);
}
}
/**
* Save alert history to disk
*/
saveAlertHistory() {
try {
fs.writeFileSync(ALERT_HISTORY_FILE, JSON.stringify(this.alertHistory, null, 2));
} catch (error) {
console.error('[ResourceMonitor] Error saving alert history:', error.message);
}
}
/**
* Cleanup old stats beyond retention period
*/