diff --git a/services/mam-api/src/routes/system.js b/services/mam-api/src/routes/system.js new file mode 100644 index 0000000..38b131f --- /dev/null +++ b/services/mam-api/src/routes/system.js @@ -0,0 +1,101 @@ +import express from 'express'; +import http from 'node:http'; +import { requireAuth } from '../middleware/auth.js'; + +const router = express.Router(); +router.use(requireAuth); + +const DOCKER_SOCKET = '/var/run/docker.sock'; +const COMPOSE_PROJECT = (process.env.DOCKER_NETWORK || 'wild-dragon_wild-dragon').split('_')[0]; + +function dockerGet(path) { + return new Promise((resolve, reject) => { + const req = http.request( + { socketPath: DOCKER_SOCKET, path, method: 'GET' }, + (res) => { + let data = ''; + res.on('data', chunk => { data += chunk; }); + res.on('end', () => { + try { resolve({ status: res.statusCode, body: JSON.parse(data) }); } + catch { resolve({ status: res.statusCode, body: data }); } + }); + } + ); + req.on('error', reject); + req.end(); + }); +} + +function dockerPost(path) { + return new Promise((resolve, reject) => { + const req = http.request( + { socketPath: DOCKER_SOCKET, path, method: 'POST', headers: { 'Content-Length': '0' } }, + (res) => { + res.resume(); + res.on('end', () => resolve({ status: res.statusCode })); + } + ); + req.on('error', reject); + req.end(); + }); +} + +// GET /containers – list containers for this compose project +router.get('/containers', async (req, res, next) => { + try { + const r = await dockerGet('/containers/json?all=1'); + if (r.status !== 200) return res.status(502).json({ error: 'Docker API error', code: r.status }); + + const containers = r.body + .filter(c => { + const labels = c.Labels || {}; + // Match by compose project label; fall back to network name + if (labels['com.docker.compose.project'] === COMPOSE_PROJECT) return true; + return Object.keys(c.NetworkSettings?.Networks || {}).some(n => n.includes(COMPOSE_PROJECT)); + }) + .map(c => ({ + id: c.Id.slice(0, 12), + name: (c.Names[0] || '').replace(/^\//, ''), + image: c.Image, + state: c.State, + status: c.Status, + created: c.Created, + service: (c.Labels || {})['com.docker.compose.service'] || '', + ports: (c.Ports || []) + .filter(p => p.PublicPort) + .map(p => `${p.PublicPort}:${p.PrivatePort}/${p.Type}`) + .join(', '), + })); + + res.json(containers); + } catch (err) { next(err); } +}); + +// POST /containers/:id/restart +router.post('/containers/:id/restart', async (req, res, next) => { + try { + const r = await dockerPost(`/containers/${req.params.id}/restart`); + if (r.status !== 204) return res.status(502).json({ error: 'Failed to restart', code: r.status }); + res.json({ ok: true }); + } catch (err) { next(err); } +}); + +// POST /containers/:id/stop +router.post('/containers/:id/stop', async (req, res, next) => { + try { + const r = await dockerPost(`/containers/${req.params.id}/stop`); + if (r.status !== 204 && r.status !== 304) return res.status(502).json({ error: 'Failed to stop', code: r.status }); + res.json({ ok: true }); + } catch (err) { next(err); } +}); + +// POST /containers/:id/start +router.post('/containers/:id/start', async (req, res, next) => { + try { + const r = await dockerPost(`/containers/${req.params.id}/start`); + if (r.status !== 204 && r.status !== 304) return res.status(502).json({ error: 'Failed to start', code: r.status }); + res.json({ ok: true }); + } catch (err) { next(err); } +}); + +export default router;