feat: cluster heartbeat stores capabilities (GPU/BMD hardware detection)

This commit is contained in:
Zac Gaetano 2026-05-20 14:18:22 -04:00
parent a941f609f0
commit dd3c2894f6

View file

@ -16,20 +16,19 @@ router.get('/', async (req, res, next) => {
); );
res.json(r.rows.map(row => ({ res.json(r.rows.map(row => ({
...row, ...row,
// online = last heartbeat within 2 minutes
online: Number(row.stale_seconds) < 120, online: Number(row.stale_seconds) < 120,
}))); })));
} catch (err) { next(err); } } catch (err) { next(err); }
}); });
// POST /heartbeat upsert this node's registration // POST /heartbeat upsert this node's registration (includes hardware capabilities)
router.post('/heartbeat', async (req, res, next) => { router.post('/heartbeat', async (req, res, next) => {
try { try {
const { const {
hostname, ip_address, hostname, ip_address,
role = 'worker', version, api_url, role = 'worker', version, api_url,
cpu_usage, mem_used_mb, mem_total_mb, cpu_usage, mem_used_mb, mem_total_mb,
metadata, capabilities, metadata,
} = req.body; } = req.body;
if (!hostname) return res.status(400).json({ error: 'hostname is required' }); if (!hostname) return res.status(400).json({ error: 'hostname is required' });
@ -37,8 +36,8 @@ router.post('/heartbeat', async (req, res, next) => {
const r = await pool.query( const r = await pool.query(
`INSERT INTO cluster_nodes `INSERT INTO cluster_nodes
(hostname, ip_address, role, version, api_url, (hostname, ip_address, role, version, api_url,
cpu_usage, mem_used_mb, mem_total_mb, last_seen, metadata) cpu_usage, mem_used_mb, mem_total_mb, last_seen, capabilities, metadata)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NOW(),$9) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NOW(),$9,$10)
ON CONFLICT (hostname) DO UPDATE SET ON CONFLICT (hostname) DO UPDATE SET
ip_address = EXCLUDED.ip_address, ip_address = EXCLUDED.ip_address,
role = EXCLUDED.role, role = EXCLUDED.role,
@ -48,6 +47,7 @@ router.post('/heartbeat', async (req, res, next) => {
mem_used_mb = EXCLUDED.mem_used_mb, mem_used_mb = EXCLUDED.mem_used_mb,
mem_total_mb = EXCLUDED.mem_total_mb, mem_total_mb = EXCLUDED.mem_total_mb,
last_seen = NOW(), last_seen = NOW(),
capabilities = EXCLUDED.capabilities,
metadata = EXCLUDED.metadata metadata = EXCLUDED.metadata
RETURNING *`, RETURNING *`,
[ [
@ -59,6 +59,7 @@ router.post('/heartbeat', async (req, res, next) => {
cpu_usage != null ? cpu_usage : null, cpu_usage != null ? cpu_usage : null,
mem_used_mb != null ? mem_used_mb : null, mem_used_mb != null ? mem_used_mb : null,
mem_total_mb != null ? mem_total_mb : null, mem_total_mb != null ? mem_total_mb : null,
capabilities != null ? JSON.stringify(capabilities) : '{}',
metadata != null ? JSON.stringify(metadata) : null, metadata != null ? JSON.stringify(metadata) : null,
] ]
); );
@ -99,7 +100,7 @@ router.delete('/:id', async (req, res, next) => {
'DELETE FROM cluster_nodes WHERE id = $1 RETURNING id', 'DELETE FROM cluster_nodes WHERE id = $1 RETURNING id',
[req.params.id] [req.params.id]
); );
if (r.rowCount === 0) return res.status(404).json({ error: 'Node not found' });; if (r.rowCount === 0) return res.status(404).json({ error: 'Node not found' });
res.json({ ok: true }); res.json({ ok: true });
} catch (err) { next(err); } } catch (err) { next(err); }
}); });