feat(cluster): node registry API — heartbeat, list, deregister
This commit is contained in:
parent
bd8b492ff6
commit
66844b93d3
1 changed files with 81 additions and 0 deletions
81
services/mam-api/src/routes/cluster.js
Normal file
81
services/mam-api/src/routes/cluster.js
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import express from 'express';
|
||||
import pool from '../db/pool.js';
|
||||
import { requireAuth } from '../middleware/auth.js';
|
||||
|
||||
const router = express.Router();
|
||||
router.use(requireAuth);
|
||||
|
||||
// GET / – list all registered cluster nodes with online status
|
||||
router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
const r = await pool.query(
|
||||
`SELECT *,
|
||||
EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds
|
||||
FROM cluster_nodes
|
||||
ORDER BY registered_at ASC`
|
||||
);
|
||||
res.json(r.rows.map(row => ({
|
||||
...row,
|
||||
// online = last heartbeat within 2 minutes
|
||||
online: Number(row.stale_seconds) < 120,
|
||||
})));
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
// POST /heartbeat – upsert this node's registration
|
||||
router.post('/heartbeat', async (req, res, next) => {
|
||||
try {
|
||||
const {
|
||||
hostname, ip_address,
|
||||
role = 'worker', version, api_url,
|
||||
cpu_usage, mem_used_mb, mem_total_mb,
|
||||
metadata,
|
||||
} = req.body;
|
||||
|
||||
if (!hostname) return res.status(400).json({ error: 'hostname is required' });
|
||||
|
||||
const r = await pool.query(
|
||||
`INSERT INTO cluster_nodes
|
||||
(hostname, ip_address, role, version, api_url,
|
||||
cpu_usage, mem_used_mb, mem_total_mb, last_seen, metadata)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NOW(),$9)
|
||||
ON CONFLICT (hostname) DO UPDATE SET
|
||||
ip_address = EXCLUDED.ip_address,
|
||||
role = EXCLUDED.role,
|
||||
version = EXCLUDED.version,
|
||||
api_url = EXCLUDED.api_url,
|
||||
cpu_usage = EXCLUDED.cpu_usage,
|
||||
mem_used_mb = EXCLUDED.mem_used_mb,
|
||||
mem_total_mb = EXCLUDED.mem_total_mb,
|
||||
last_seen = NOW(),
|
||||
metadata = EXCLUDED.metadata
|
||||
RETURNING *`,
|
||||
[
|
||||
hostname,
|
||||
ip_address || null,
|
||||
role,
|
||||
version || null,
|
||||
api_url || null,
|
||||
cpu_usage != null ? cpu_usage : null,
|
||||
mem_used_mb != null ? mem_used_mb : null,
|
||||
mem_total_mb != null ? mem_total_mb : null,
|
||||
metadata != null ? JSON.stringify(metadata) : null,
|
||||
]
|
||||
);
|
||||
res.json(r.rows[0]);
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
// DELETE /:id – deregister a node
|
||||
router.delete('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const r = await pool.query(
|
||||
'DELETE FROM cluster_nodes WHERE id = $1 RETURNING id',
|
||||
[req.params.id]
|
||||
);
|
||||
if (r.rowCount === 0) return res.status(404).json({ error: 'Node not found' });
|
||||
res.json({ ok: true });
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
export default router;
|
||||
Loading…
Reference in a new issue