From 090452969cc1f86d5c0d2773c3b6fd10e344c149 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Tue, 19 May 2026 23:50:19 -0400 Subject: [PATCH] feat(api): register system + cluster routes; add self-heartbeat on startup --- services/mam-api/src/index.js | 47 +++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/services/mam-api/src/index.js b/services/mam-api/src/index.js index 664cdb7..36fbd10 100644 --- a/services/mam-api/src/index.js +++ b/services/mam-api/src/index.js @@ -3,6 +3,7 @@ import express from 'express'; import cors from 'cors'; import session from 'express-session'; import ConnectPgSimple from 'connect-pg-simple'; +import os from 'node:os'; import pool from './db/pool.js'; import { errorHandler } from './middleware/errors.js'; @@ -21,6 +22,8 @@ import usersRouter from './routes/users.js'; import groupsRouter from './routes/groups.js'; import tokensRouter from './routes/tokens.js'; import sequencesRouter from './routes/sequences.js'; +import systemRouter from './routes/system.js'; +import clusterRouter from './routes/cluster.js'; const app = express(); const PORT = process.env.PORT || 3000; @@ -71,6 +74,8 @@ app.use('/api/v1/users', usersRouter); app.use('/api/v1/groups', groupsRouter); app.use('/api/v1/tokens', tokensRouter); app.use('/api/v1/sequences', sequencesRouter); +app.use('/api/v1/system', systemRouter); +app.use('/api/v1/cluster', clusterRouter); // ── Error handler ───────────────────────────────────────────────────────────── app.use(errorHandler); @@ -97,6 +102,48 @@ async function runMigrations() { } await runMigrations(); +// ── Cluster self-heartbeat ──────────────────────────────────────────────────── +// Registers this node in cluster_nodes every 30 s so the Cluster page shows it. +function getLocalIp() { + const ifaces = os.networkInterfaces(); + for (const name of Object.keys(ifaces)) { + for (const iface of (ifaces[name] || [])) { + if (iface.family === 'IPv4' && !iface.internal) return iface.address; + } + } + return '127.0.0.1'; +} + +function selfHeartbeat() { + const load = os.loadavg()[0]; + const total = os.totalmem(); + const used = total - os.freemem(); + pool.query( + `INSERT INTO cluster_nodes + (hostname, ip_address, role, version, api_url, + cpu_usage, mem_used_mb, mem_total_mb, last_seen) + VALUES ($1,$2,'primary',$3,$4,$5,$6,$7,NOW()) + ON CONFLICT (hostname) DO UPDATE SET + ip_address = EXCLUDED.ip_address, + cpu_usage = EXCLUDED.cpu_usage, + mem_used_mb = EXCLUDED.mem_used_mb, + mem_total_mb = EXCLUDED.mem_total_mb, + last_seen = NOW()`, + [ + os.hostname(), + getLocalIp(), + process.env.npm_package_version || null, + `http://${getLocalIp()}:${PORT}`, + parseFloat(load.toFixed(2)), + Math.round(used / 1024 / 1024), + Math.round(total / 1024 / 1024), + ] + ).catch(err => console.error('[cluster] heartbeat failed:', err.message)); +} + +setInterval(selfHeartbeat, 30_000); +selfHeartbeat(); + app.listen(PORT, () => { const authMode = process.env.AUTH_ENABLED === 'true' ? 'ENABLED' : 'DISABLED (set AUTH_ENABLED=true for production)'; console.log(`MAM API listening on port ${PORT}`);