cluster route: fallback IP from request + /devices/blackmagic endpoint

Heartbeat handler now overrides 172.x docker bridge IPs with the
request's source address when the request itself came from a real LAN.
Adds GET /devices/blackmagic that flattens every node's capabilities so
the recorder UI can show a card picker spanning the whole cluster.
This commit is contained in:
Zac Gaetano 2026-05-21 00:16:36 -04:00
parent 485af25d4a
commit 0efef0d81b

View file

@ -5,6 +5,18 @@ import { requireAuth } from '../middleware/auth.js';
const router = express.Router();
router.use(requireAuth);
// If the agent reported a Docker-bridge IP (172.16/12) but the request
// itself came from a real LAN address, prefer the request's source — the
// agent likely runs in bridge mode without NODE_IP set.
function pickIp(reportedIp, reqIp) {
const clean = (s) => (s || '').replace(/^::ffff:/, '');
const isBridge = (ip) => /^172\.(1[6-9]|2\d|3[01])\./.test(ip || '');
const r = clean(reqIp);
if (!reportedIp) return r || null;
if (isBridge(reportedIp) && r && !isBridge(r)) return r;
return reportedIp;
}
// GET / list all registered cluster nodes with online status
router.get('/', async (req, res, next) => {
try {
@ -33,6 +45,8 @@ router.post('/heartbeat', async (req, res, next) => {
if (!hostname) return res.status(400).json({ error: 'hostname is required' });
const effectiveIp = pickIp(ip_address, req.ip || req.socket?.remoteAddress);
const r = await pool.query(
`INSERT INTO cluster_nodes
(hostname, ip_address, role, version, api_url,
@ -52,7 +66,7 @@ router.post('/heartbeat', async (req, res, next) => {
RETURNING *`,
[
hostname,
ip_address || null,
effectiveIp,
role,
version || null,
api_url || null,
@ -67,6 +81,38 @@ router.post('/heartbeat', async (req, res, next) => {
} catch (err) { next(err); }
});
// GET /devices/blackmagic flatten every node's DeckLink cards for the
// recorder picker. Returns one entry per device with the host node info.
router.get('/devices/blackmagic', async (req, res, next) => {
try {
const r = await pool.query(
`SELECT id, hostname, ip_address, role, capabilities,
EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds
FROM cluster_nodes
WHERE capabilities IS NOT NULL`
);
const out = [];
for (const row of r.rows) {
const online = Number(row.stale_seconds) < 120;
const bm = (row.capabilities && row.capabilities.blackmagic) || [];
const model = (row.capabilities && row.capabilities.blackmagic_model) || null;
bm.forEach((d, idx) => {
out.push({
node_id: row.id,
hostname: row.hostname,
ip_address: row.ip_address,
role: row.role,
online,
model,
index: d.index !== undefined ? d.index : idx,
device: d.device,
});
});
}
res.json(out);
} catch (err) { next(err); }
});
// GET /:id/ping probe the node's api_url/health endpoint directly
router.get('/:id/ping', async (req, res, next) => {
try {