From 1725ec1de9816c2fe0e3036e12d6be8466db4f46 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Wed, 20 May 2026 14:18:43 -0400 Subject: [PATCH] feat: settings routes for hardware inventory, GPU transcoding, capture service URL --- services/mam-api/src/routes/settings.js | 132 +++++++++++++++++++----- 1 file changed, 104 insertions(+), 28 deletions(-) diff --git a/services/mam-api/src/routes/settings.js b/services/mam-api/src/routes/settings.js index 09b2253..d370941 100644 --- a/services/mam-api/src/routes/settings.js +++ b/services/mam-api/src/routes/settings.js @@ -6,7 +6,8 @@ import { getAmppConfig } from '../ampp/client.js'; const router = express.Router(); router.use(requireAuth); -// GET /api/v1/settings/ampp — Return current AMPP config (token value masked) +// ── AMPP integration ─────────────────────────────────────────────────────── + router.get('/ampp', async (req, res, next) => { try { const result = await pool.query( @@ -15,7 +16,7 @@ router.get('/ampp', async (req, res, next) => { const out = {}; for (const row of result.rows) { if (row.key === 'ampp_token') { - out.ampp_token_exists = true; // Never return the raw token + out.ampp_token_exists = true; } else { out[row.key] = row.value; } @@ -26,62 +27,137 @@ router.get('/ampp', async (req, res, next) => { } }); -// PUT /api/v1/settings/ampp — Save AMPP credentials router.put('/ampp', async (req, res, next) => { try { const { ampp_base_url, ampp_token } = req.body; - if (!ampp_base_url) { - return res.status(400).json({ error: 'ampp_base_url is required' }); - } - + if (!ampp_base_url) return res.status(400).json({ error: 'ampp_base_url is required' }); const baseUrl = ampp_base_url.trim().replace(/\/$/, ''); - await pool.query( - `INSERT INTO settings (key, value, updated_at) - VALUES ('ampp_base_url', $1, NOW()) + `INSERT INTO settings (key, value, updated_at) VALUES ('ampp_base_url', $1, NOW()) ON CONFLICT (key) DO UPDATE SET value = $1, updated_at = NOW()`, [baseUrl] ); - if (ampp_token) { await pool.query( - `INSERT INTO settings (key, value, updated_at) - VALUES ('ampp_token', $1, NOW()) + `INSERT INTO settings (key, value, updated_at) VALUES ('ampp_token', $1, NOW()) ON CONFLICT (key) DO UPDATE SET value = $1, updated_at = NOW()`, [ampp_token.trim()] ); } - res.json({ message: 'AMPP settings saved' }); } catch (err) { next(err); } }); -// POST /api/v1/settings/ampp/test — Verify AMPP connectivity router.post('/ampp/test', async (req, res, next) => { try { const config = await getAmppConfig(); - if (!config) { - return res.status(400).json({ error: 'AMPP credentials not configured' }); - } - + if (!config) return res.status(400).json({ error: 'AMPP credentials not configured' }); const testUrl = `${config.ampp_base_url}/api/v1/store/folder/folders?limit=1`; const testRes = await fetch(testUrl, { - headers: { - Authorization: `Bearer ${config.ampp_token}`, - 'Content-Type': 'application/json', - }, + headers: { Authorization: `Bearer ${config.ampp_token}`, 'Content-Type': 'application/json' }, }); - - if (!testRes.ok) { - return res.status(400).json({ error: `AMPP returned HTTP ${testRes.status}` }); - } - + if (!testRes.ok) return res.status(400).json({ error: `AMPP returned HTTP ${testRes.status}` }); res.json({ message: 'AMPP connection successful' }); } catch (err) { res.status(400).json({ error: `Connection failed: ${err.message}` }); } }); +// ── Hardware inventory ───────────────────────────────────────────────────── +// GET /api/v1/settings/hardware — aggregate GPU/BMD capabilities from all cluster nodes + +router.get('/hardware', async (req, res, next) => { + try { + const result = await pool.query( + `SELECT id, hostname, role, ip_address, capabilities, + EXTRACT(EPOCH FROM (NOW() - last_seen))::int AS stale_seconds + FROM cluster_nodes + ORDER BY role, hostname` + ); + const nodes = result.rows.map(n => ({ + id: n.id, + hostname: n.hostname, + role: n.role, + ip_address: n.ip_address, + online: n.stale_seconds < 120, + capabilities: n.capabilities || {}, + })); + res.json({ nodes }); + } catch (err) { + next(err); + } +}); + +// ── GPU/Transcoding settings ─────────────────────────────────────────────── +// Keys: gpu_transcode_enabled, gpu_codec, gpu_preset, gpu_bitrate_mbps, gpu_node + +router.get('/transcoding', async (req, res, next) => { + try { + const result = await pool.query( + `SELECT key, value FROM settings + WHERE key IN ('gpu_transcode_enabled','gpu_codec','gpu_preset','gpu_bitrate_mbps','gpu_node')` + ); + const out = { + gpu_transcode_enabled: 'false', + gpu_codec: 'h264_nvenc', + gpu_preset: 'p4', + gpu_bitrate_mbps: '8', + gpu_node: '', + }; + for (const { key, value } of result.rows) out[key] = value; + res.json(out); + } catch (err) { + next(err); + } +}); + +router.put('/transcoding', async (req, res, next) => { + try { + const allowed = ['gpu_transcode_enabled', 'gpu_codec', 'gpu_preset', 'gpu_bitrate_mbps', 'gpu_node']; + for (const key of allowed) { + if (req.body[key] !== undefined) { + await pool.query( + `INSERT INTO settings (key, value, updated_at) VALUES ($1, $2, NOW()) + ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = NOW()`, + [key, String(req.body[key])] + ); + } + } + res.json({ message: 'Transcoding settings saved' }); + } catch (err) { + next(err); + } +}); + +// ── Capture service routing ──────────────────────────────────────────────── +// capture_service_url — points to the remote capture node's API +// When set, the local capture proxy routes all requests to this URL instead of the sidecar + +router.get('/capture-service', async (req, res, next) => { + try { + const result = await pool.query( + "SELECT value FROM settings WHERE key = 'capture_service_url'" + ); + res.json({ capture_service_url: result.rows[0]?.value || '' }); + } catch (err) { + next(err); + } +}); + +router.put('/capture-service', async (req, res, next) => { + try { + const url = (req.body.capture_service_url || '').trim().replace(/\/$/, ''); + await pool.query( + `INSERT INTO settings (key, value, updated_at) VALUES ('capture_service_url', $1, NOW()) + ON CONFLICT (key) DO UPDATE SET value = $1, updated_at = NOW()`, + [url] + ); + res.json({ message: 'Capture service URL saved' }); + } catch (err) { + next(err); + } +}); + export default router;