dragonflight/services/mam-api/src/routes/cluster.js

108 lines
3.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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: Number(row.stale_seconds) < 120,
})));
} catch (err) { next(err); }
});
// POST /heartbeat upsert this node's registration (includes hardware capabilities)
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,
capabilities, 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, capabilities, metadata)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NOW(),$9,$10)
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(),
capabilities = EXCLUDED.capabilities,
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,
capabilities != null ? JSON.stringify(capabilities) : '{}',
metadata != null ? JSON.stringify(metadata) : null,
]
);
res.json(r.rows[0]);
} 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 {
const r = await pool.query(
'SELECT id, hostname, api_url FROM cluster_nodes WHERE id = $1',
[req.params.id]
);
if (r.rowCount === 0) return res.status(404).json({ error: 'Node not found' });
const node = r.rows[0];
if (!node.api_url) return res.json({ reachable: false, reason: 'no api_url registered' });
const start = Date.now();
try {
const upstream = await fetch(`${node.api_url}/health`, {
signal: AbortSignal.timeout(4000),
});
const latency_ms = Date.now() - start;
const body = await upstream.json().catch(() => ({}));
res.json({ reachable: upstream.ok, latency_ms, status: upstream.status, agent: body });
} catch (err) {
res.json({ reachable: false, latency_ms: Date.now() - start, reason: err.message });
}
} 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;