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:
113
dashcaddy-api/routes/docker-resources.js
Normal file
113
dashcaddy-api/routes/docker-resources.js
Normal file
@@ -0,0 +1,113 @@
|
||||
const express = require('express');
|
||||
const { success } = require('../response-helpers');
|
||||
const { ValidationError } = require('../errors');
|
||||
|
||||
/**
|
||||
* Docker resources route factory (volumes, networks, disk usage)
|
||||
* @param {Object} deps
|
||||
* @param {Object} deps.docker - Docker client wrapper
|
||||
* @param {Function} deps.asyncHandler - Async route handler wrapper
|
||||
* @returns {express.Router}
|
||||
*/
|
||||
module.exports = function({ docker, asyncHandler }) {
|
||||
const router = express.Router();
|
||||
|
||||
// ===== VOLUMES =====
|
||||
|
||||
router.get('/volumes', asyncHandler(async (req, res) => {
|
||||
const result = await docker.client.listVolumes();
|
||||
const volumes = (result.Volumes || []).map(v => ({
|
||||
name: v.Name,
|
||||
driver: v.Driver,
|
||||
mountpoint: v.Mountpoint,
|
||||
scope: v.Scope,
|
||||
created: v.CreatedAt,
|
||||
labels: v.Labels || {},
|
||||
}));
|
||||
success(res, { volumes, count: volumes.length });
|
||||
}, 'docker-volumes-list'));
|
||||
|
||||
router.post('/volumes', asyncHandler(async (req, res) => {
|
||||
const { name, driver } = req.body;
|
||||
if (!name || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/.test(name)) {
|
||||
throw new ValidationError('Invalid volume name');
|
||||
}
|
||||
const volume = await docker.client.createVolume({
|
||||
Name: name,
|
||||
Driver: driver || 'local',
|
||||
});
|
||||
success(res, { message: `Volume "${name}" created`, volume: { name: volume.name } });
|
||||
}, 'docker-volumes-create'));
|
||||
|
||||
router.delete('/volumes/:name', asyncHandler(async (req, res) => {
|
||||
const volume = docker.client.getVolume(req.params.name);
|
||||
await volume.remove({ force: req.query.force === 'true' });
|
||||
success(res, { message: `Volume "${req.params.name}" removed` });
|
||||
}, 'docker-volumes-delete'));
|
||||
|
||||
// ===== NETWORKS =====
|
||||
|
||||
router.get('/networks', asyncHandler(async (req, res) => {
|
||||
const networkList = await docker.client.listNetworks();
|
||||
const networks = networkList.map(n => ({
|
||||
id: n.Id.substring(0, 12),
|
||||
name: n.Name,
|
||||
driver: n.Driver,
|
||||
scope: n.Scope,
|
||||
internal: n.Internal,
|
||||
containers: Object.keys(n.Containers || {}).length,
|
||||
created: n.Created,
|
||||
}));
|
||||
success(res, { networks, count: networks.length });
|
||||
}, 'docker-networks-list'));
|
||||
|
||||
router.post('/networks', asyncHandler(async (req, res) => {
|
||||
const { name, driver } = req.body;
|
||||
if (!name || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$/.test(name)) {
|
||||
throw new ValidationError('Invalid network name');
|
||||
}
|
||||
const network = await docker.client.createNetwork({
|
||||
Name: name,
|
||||
Driver: driver || 'bridge',
|
||||
});
|
||||
success(res, { message: `Network "${name}" created`, id: network.id });
|
||||
}, 'docker-networks-create'));
|
||||
|
||||
router.delete('/networks/:id', asyncHandler(async (req, res) => {
|
||||
const network = docker.client.getNetwork(req.params.id);
|
||||
await network.remove();
|
||||
success(res, { message: 'Network removed' });
|
||||
}, 'docker-networks-delete'));
|
||||
|
||||
// ===== DISK USAGE =====
|
||||
|
||||
router.get('/disk-usage', asyncHandler(async (req, res) => {
|
||||
const df = await docker.client.df();
|
||||
const summary = {
|
||||
images: {
|
||||
count: (df.Images || []).length,
|
||||
size: (df.Images || []).reduce((sum, i) => sum + (i.Size || 0), 0),
|
||||
reclaimable: (df.Images || []).filter(i => i.Containers === 0).reduce((sum, i) => sum + (i.Size || 0), 0),
|
||||
},
|
||||
containers: {
|
||||
count: (df.Containers || []).length,
|
||||
running: (df.Containers || []).filter(c => c.State === 'running').length,
|
||||
size: (df.Containers || []).reduce((sum, c) => sum + (c.SizeRw || 0), 0),
|
||||
},
|
||||
volumes: {
|
||||
count: (df.Volumes || []).length,
|
||||
size: (df.Volumes || []).reduce((sum, v) => sum + (v.UsageData?.Size || 0), 0),
|
||||
reclaimable: (df.Volumes || []).filter(v => v.UsageData?.RefCount === 0).reduce((sum, v) => sum + (v.UsageData?.Size || 0), 0),
|
||||
},
|
||||
buildCache: {
|
||||
count: (df.BuildCache || []).length,
|
||||
size: (df.BuildCache || []).reduce((sum, b) => sum + (b.Size || 0), 0),
|
||||
reclaimable: (df.BuildCache || []).filter(b => !b.InUse).reduce((sum, b) => sum + (b.Size || 0), 0),
|
||||
},
|
||||
};
|
||||
summary.totalSize = summary.images.size + summary.containers.size + summary.volumes.size + summary.buildCache.size;
|
||||
success(res, summary);
|
||||
}, 'docker-disk-usage'));
|
||||
|
||||
return router;
|
||||
};
|
||||
Reference in New Issue
Block a user