feat: add 7 new features — exec shell, SSE events, compose import, docker resources, resource limits, email notifications, auto-updates
- Container exec/shell via WebSocket + xterm.js (subtle >_ button on cards) - Live dashboard updates via SSE (resource alerts, health changes, update notices) - Docker Compose import with YAML parsing, preview, and dependency-ordered deploy - Volume & network management modal with disk usage overview - CPU/memory resource limits on deploy and live update - Email SMTP notifications (nodemailer) alongside Discord/Telegram/ntfy - Scheduled auto-update scheduler with maintenance windows (daily/weekly/monthly) New deps: ws, js-yaml, nodemailer Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -27,19 +27,22 @@ class UpdateManager extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Start update checking
|
||||
* Start update checking and auto-update scheduler
|
||||
*/
|
||||
start() {
|
||||
if (this.checking) return;
|
||||
|
||||
|
||||
console.log('[UpdateManager] Starting update checks');
|
||||
this.checking = true;
|
||||
|
||||
|
||||
// Initial check
|
||||
this.checkForUpdates();
|
||||
|
||||
|
||||
// Schedule periodic checks
|
||||
this.checkInterval = setInterval(() => this.checkForUpdates(), CHECK_INTERVAL);
|
||||
|
||||
// Start auto-update scheduler (checks every hour)
|
||||
this.startAutoUpdateScheduler();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,14 +50,18 @@ class UpdateManager extends EventEmitter {
|
||||
*/
|
||||
stop() {
|
||||
if (!this.checking) return;
|
||||
|
||||
|
||||
console.log('[UpdateManager] Stopping update checks');
|
||||
this.checking = false;
|
||||
|
||||
|
||||
if (this.checkInterval) {
|
||||
clearInterval(this.checkInterval);
|
||||
this.checkInterval = null;
|
||||
}
|
||||
if (this.autoUpdateInterval) {
|
||||
clearInterval(this.autoUpdateInterval);
|
||||
this.autoUpdateInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -823,6 +830,92 @@ class UpdateManager extends EventEmitter {
|
||||
return lines.join('\n') || 'No changelog available';
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the auto-update scheduler — runs hourly, applies updates in maintenance windows
|
||||
*/
|
||||
startAutoUpdateScheduler() {
|
||||
const AUTO_CHECK_INTERVAL = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
// Delay first run by 10 minutes to let containers start
|
||||
setTimeout(() => this.runAutoUpdates(), 10 * 60 * 1000);
|
||||
this.autoUpdateInterval = setInterval(() => this.runAutoUpdates(), AUTO_CHECK_INTERVAL);
|
||||
|
||||
const count = Object.values(this.config.autoUpdate || {}).filter(c => c.enabled).length;
|
||||
if (count > 0) {
|
||||
console.log(`[UpdateManager] Auto-update scheduler started (${count} container(s) configured)`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute auto-updates for all configured containers
|
||||
*/
|
||||
async runAutoUpdates() {
|
||||
const autoConfig = this.config.autoUpdate || {};
|
||||
const now = new Date();
|
||||
const hour = now.getHours();
|
||||
const dayOfWeek = now.getDay(); // 0 = Sunday
|
||||
const dayOfMonth = now.getDate();
|
||||
|
||||
for (const [containerId, cfg] of Object.entries(autoConfig)) {
|
||||
if (!cfg.enabled) continue;
|
||||
|
||||
// Check maintenance window (e.g., "02:00-05:00")
|
||||
if (cfg.maintenanceWindow) {
|
||||
const [startStr, endStr] = cfg.maintenanceWindow.split('-').map(s => s.trim());
|
||||
const startHour = parseInt(startStr);
|
||||
const endHour = parseInt(endStr);
|
||||
if (startHour <= endHour) {
|
||||
if (hour < startHour || hour >= endHour) continue;
|
||||
} else {
|
||||
// Wraps midnight (e.g., "22:00-04:00")
|
||||
if (hour < startHour && hour >= endHour) continue;
|
||||
}
|
||||
} else {
|
||||
// Default: only run between 2AM and 4AM
|
||||
if (hour < 2 || hour >= 4) continue;
|
||||
}
|
||||
|
||||
// Check schedule
|
||||
const shouldRun =
|
||||
cfg.schedule === 'daily' ||
|
||||
(cfg.schedule === 'weekly' && dayOfWeek === 0) || // Sunday
|
||||
(cfg.schedule === 'monthly' && dayOfMonth === 1);
|
||||
|
||||
if (!shouldRun) continue;
|
||||
|
||||
// Check if already ran today
|
||||
const lastRun = cfg.lastAutoUpdate ? new Date(cfg.lastAutoUpdate) : null;
|
||||
if (lastRun && lastRun.toDateString() === now.toDateString()) continue;
|
||||
|
||||
// Check if this container has an available update
|
||||
const update = this.availableUpdates.get(containerId);
|
||||
if (!update) continue;
|
||||
|
||||
console.log(`[UpdateManager] Auto-updating ${update.containerName} (schedule: ${cfg.schedule})`);
|
||||
this.emit('auto-update-start', { containerId, containerName: update.containerName, schedule: cfg.schedule });
|
||||
|
||||
try {
|
||||
const result = await this.updateContainer(containerId, { autoRollback: cfg.autoRollback !== false });
|
||||
cfg.lastAutoUpdate = now.toISOString();
|
||||
this.saveConfig();
|
||||
console.log(`[UpdateManager] Auto-update completed for ${update.containerName}`);
|
||||
this.emit('auto-update-complete', { containerId, containerName: update.containerName, result });
|
||||
} catch (error) {
|
||||
console.error(`[UpdateManager] Auto-update failed for ${update.containerName}:`, error.message);
|
||||
cfg.lastAutoUpdate = now.toISOString(); // Don't retry same day
|
||||
this.saveConfig();
|
||||
this.emit('auto-update-failed', { containerId, containerName: update.containerName, error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get auto-update configuration for all containers
|
||||
*/
|
||||
getAutoUpdateConfig() {
|
||||
return this.config.autoUpdate || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure auto-update for a container
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user