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:
2026-04-05 16:15:14 -07:00
parent b60e7e40d0
commit bdf3f247b1
30 changed files with 2423 additions and 313 deletions

View File

@@ -190,6 +190,36 @@ module.exports = function({ docker, log, asyncHandler }) {
success(res, { logs: logs.toString() });
}, 'container-logs'));
// Update resource limits on a running container
router.put('/:id/resources', asyncHandler(async (req, res) => {
const container = await getVerifiedContainer(req.params.id);
const { memory, cpus } = req.body;
const updateConfig = {};
if (memory !== undefined) {
updateConfig.Memory = memory > 0 ? Math.round(memory * 1024 * 1024) : 0; // MB to bytes, 0 = unlimited
updateConfig.MemoryReservation = memory > 0 ? Math.round(memory * 1024 * 1024 * 0.5) : 0;
}
if (cpus !== undefined) {
updateConfig.NanoCpus = cpus > 0 ? Math.round(cpus * 1e9) : 0; // 0 = unlimited
}
await container.update(updateConfig);
success(res, { message: 'Resource limits updated' });
}, 'container-resources'));
// Get resource limits for a container
router.get('/:id/resources', asyncHandler(async (req, res) => {
const container = await getVerifiedContainer(req.params.id);
const info = await container.inspect();
const hc = info.HostConfig;
success(res, {
memory: hc.Memory ? Math.round(hc.Memory / 1024 / 1024) : 0, // bytes to MB
memoryReservation: hc.MemoryReservation ? Math.round(hc.MemoryReservation / 1024 / 1024) : 0,
cpus: hc.NanoCpus ? hc.NanoCpus / 1e9 : 0,
});
}, 'container-resources-get'));
// Delete container
router.delete('/:id', asyncHandler(async (req, res) => {
const container = await getVerifiedContainer(req.params.id);