3 * @module MonitoringRoutes
4 * @description Agent monitoring and device assignment routes. Retrieves paginated lists of agents with connection status, search, and tenant filtering. Supports device assignment to tenants and customers, with optional MeshCentral mesh group relocation. Database is the source of truth, kept in sync by auto-sync worker.
5 * @see {@link ../lib/meshcentral-api} for MeshCentral API integration
6 * @see {@link ../services/db} for database connection
7 * @see {@link ../middleware/auth} for authentication middleware
8 * @see {@link ../middleware/tenant} for tenant context utilities
9 * @apiDefine MonitoringGroup Monitoring
10 * @apiGroup Monitoring
11 * @apiHeader {string} Authorization Bearer token required.
12 * @apiHeader {string} [X-Tenant-ID] Tenant context header (optional for root/MSP users).
13 * @apiError (Error 401) Unauthorized Missing or invalid token.
14 * @apiError (Error 403) Forbidden Only administrators can assign devices.
15 * @apiError (Error 404) NotFound Device or tenant not found.
16 * @apiError (Error 500) ServerError Internal server error.
19const express = require('express');
20const router = express.Router();
21const authenticateToken = require('../middleware/auth');
23const { getTenantFilter, setTenantContext } = require('../middleware/tenant');
24const MeshCentralAPI = require('../lib/meshcentral-api');
27 * @api {get} /monitoring Get monitoring data (agent list with stats)
28 * @apiName GetMonitoring
29 * @apiGroup Monitoring
30 * @apiDescription Retrieve paginated list of agents with connection status, tenant/customer info, and global stats (total, online, offline). Database is the source of truth (kept in sync by auto-sync worker). Supports search filtering and MSP tenant impersonation.
31 * @apiParam {number} [page=1] Page number for pagination.
32 * @apiParam {number} [limit=20] Number of agents per page.
33 * @apiParam {string} [search] Search term for hostname, name, or MeshCentral node ID.
34 * @apiParam {string} [tenantId] Tenant ID for MSP impersonation (root/MSP users only).
35 * @apiSuccess {object[]} agents List of agents.
36 * @apiSuccess {string} agents.agent_id Agent ID.
37 * @apiSuccess {string} agents.agent_uuid Agent UUID.
38 * @apiSuccess {string} agents.hostname Agent hostname.
39 * @apiSuccess {string} agents.platform Platform (windows, linux, darwin).
40 * @apiSuccess {string} agents.os Operating system.
41 * @apiSuccess {string} agents.os_version OS version.
42 * @apiSuccess {string} agents.tenant_id Tenant ID.
43 * @apiSuccess {string} agents.tenant_name Tenant name.
44 * @apiSuccess {string} agents.customer_id Customer ID.
45 * @apiSuccess {string} agents.customer_name Customer name.
46 * @apiSuccess {boolean} agents.meshcentral_connected MeshCentral connection status.
47 * @apiSuccess {string} agents.status Status (online/offline).
48 * @apiSuccess {Date} agents.last_seen Last seen timestamp.
49 * @apiSuccess {boolean} agents.is_untagged Whether agent is untagged.
50 * @apiSuccess {Number} total Total agent count.
51 * @apiSuccess {Object} overview Global stats.
52 * @apiSuccess {Number} overview.totalAgents Total agents.
53 * @apiSuccess {Number} overview.agentsOnline Online agents.
54 * @apiSuccess {Number} overview.agentsOffline Offline agents.
55 * @apiSuccess {Number} page Current page.
56 * @apiSuccess {Number} perPage Agents per page.
58 * @apiExample {curl} Example usage:
59 * curl -H "Authorization: Bearer <token>" -H "X-Tenant-ID: <id>" https://api.example.com/monitoring?page=1&limit=20&search=server
61router.get('/', authenticateToken, setTenantContext, async (req, res) => {
62 const pool = require('../services/db');
64 const page = parseInt(req.query.page, 10) || 1;
65 const limit = parseInt(req.query.limit, 10) || 20;
66 const offset = (page - 1) * limit;
67 const search = req.query.search || '';
69 console.log('[Monitoring] Request received:', {
75 // For root/MSP users, allow filtering by selected tenant (impersonation)
76 let effectiveTenant = req.tenant;
77 if (req.tenant && req.tenant.isMsp) {
78 const selectedTenantId = req.query.tenantId || req.query.tenant_id;
79 if (selectedTenantId) {
80 effectiveTenant = { ...req.tenant, id: selectedTenantId, isMsp: false };
84 const isRootOrMsp = req.tenant.isMsp || effectiveTenant.id === '00000000-0000-0000-0000-000000000001';
86 console.log('[Monitoring] Query context:', {
87 tenantId: effectiveTenant.id,
92 // Build tenant filter for query
98 whereClause = `WHERE a.tenant_id = $${paramIndex}`;
99 params.push(effectiveTenant.id);
102 // Root/MSP users see all agents
103 whereClause = 'WHERE 1=1';
108 whereClause += ` AND (
109 LOWER(a.hostname) LIKE LOWER($${paramIndex}) OR
110 LOWER(a.name) LIKE LOWER($${paramIndex}) OR
111 LOWER(a.meshcentral_nodeid) LIKE LOWER($${paramIndex})
113 params.push(`%${search}%`);
118 const countResult = await pool.query(
119 `SELECT COUNT(*) as total FROM agents a ${whereClause}`,
122 const total = parseInt(countResult.rows[0].total, 10);
124 // Get paginated agents from our database
125 const agentsResult = await pool.query(
136 a.meshcentral_nodeid,
137 a.meshcentral_connected,
138 a.meshcentral_last_seen,
142 t.name as tenant_name,
143 t.meshcentral_group_id,
144 c.name as customer_name
146 LEFT JOIN tenants t ON a.tenant_id = t.tenant_id
147 LEFT JOIN customers c ON a.customer_id = c.customer_id
149 ORDER BY a.meshcentral_connected DESC, a.last_seen DESC NULLS LAST
150 LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
151 [...params, limit, offset]
154 const agents = agentsResult.rows;
156 // Calculate stats (from ALL agents, not just paginated)
157 const statsResult = await pool.query(
160 SUM(CASE WHEN meshcentral_connected = true THEN 1 ELSE 0 END) as online,
161 SUM(CASE WHEN meshcentral_connected = false OR meshcentral_connected IS NULL THEN 1 ELSE 0 END) as offline
167 const stats = statsResult.rows[0];
168 const agentsOnline = parseInt(stats.online, 10) || 0;
169 const agentsOffline = parseInt(stats.offline, 10) || 0;
170 const totalAgents = parseInt(stats.total, 10) || 0;
172 console.log('[Monitoring] Stats:', {
174 online: agentsOnline,
175 offline: agentsOffline,
176 pageSize: agents.length
179 // Map agents to frontend format
180 const mappedAgents = agents.map(a => ({
181 agent_id: a.agent_id,
182 agent_uuid: a.agent_uuid,
183 hostname: a.hostname || a.name || 'Unknown',
184 name: a.name || a.hostname,
185 platform: a.platform,
187 os_version: a.os_version,
188 tenant_id: a.tenant_id,
189 tenant_name: a.tenant_name,
190 customer_id: a.customer_id,
191 customer_name: a.customer_name,
192 meshcentral_nodeid: a.meshcentral_nodeid,
193 meshcentral_connected: a.meshcentral_connected || false,
194 mesh_id: a.meshcentral_group_id,
195 status: a.meshcentral_connected ? 'online' : 'offline',
196 last_seen: a.meshcentral_last_seen || a.last_seen,
197 created_at: a.created_at,
198 is_untagged: !a.tenant_id || !a.meshcentral_nodeid
202 agents: mappedAgents,
205 totalAgents: totalAgents,
206 agentsOnline: agentsOnline,
207 agentsOffline: agentsOffline
213 console.error('[Monitoring] Error fetching monitoring data:', err);
214 res.status(500).json({ error: 'Server error', message: err.message });
219 * @api {post} /monitoring/assign-device Assign untagged device to tenant
220 * @apiName AssignDevice
221 * @apiGroup Monitoring
222 * @apiDescription Assign an untagged MeshCentral device to a tenant and optionally to a customer. Updates device tags in MeshCentral and creates/updates agent in the database. Optionally moves device to tenant's MeshCentral mesh group. Only root/MSP users can assign devices.
223 * @apiParam {string} nodeId MeshCentral node ID.
224 * @apiParam {string} tenantId Tenant ID to assign to.
225 * @apiParam {string} [customerId] Customer ID to assign to (optional).
226 * @apiParam {boolean} [moveMesh=false] Whether to move device to tenant's mesh group.
227 * @apiSuccess {boolean} success Operation success status.
228 * @apiSuccess {object} device Assigned device details.
229 * @apiSuccess {string} device.nodeId MeshCentral node ID.
230 * @apiSuccess {string} device.hostname Device hostname.
231 * @apiSuccess {string} device.tenantId Assigned tenant ID.
232 * @apiSuccess {string} device.tenantName Assigned tenant name.
233 * @apiSuccess {string} device.customerId Assigned customer ID.
234 * @apiSuccess {boolean} device.moved Whether device was moved to tenant's mesh group.
235 * @apiError (Error 400) BadRequest nodeId and tenantId are required.
236 * @apiError (Error 403) Forbidden Only administrators can assign devices.
237 * @apiError (Error 404) NotFound Device or tenant not found.
238 * @apiExample {curl} Example usage:
239 * curl -X POST -H "Authorization: Bearer <token>" -d '{"nodeId":"node//abc123","tenantId":"tenant-id","customerId":"customer-id","moveMesh":true}' https://api.example.com/monitoring/assign-device
241router.post('/assign-device', authenticateToken, setTenantContext, async (req, res) => {
242 const pool = require('../services/db');
244 const { nodeId, tenantId, customerId, moveMesh = false } = req.body;
246 // Only root/MSP users can assign devices
247 if (!req.tenant.isMsp) {
248 return res.status(403).json({ error: 'Only administrators can assign devices' });
251 if (!nodeId || !tenantId) {
252 return res.status(400).json({ error: 'nodeId and tenantId are required' });
256 const tenantResult = await pool.query(
257 'SELECT tenant_id, name, meshcentral_group_id FROM tenants WHERE tenant_id = $1',
261 if (tenantResult.rows.length === 0) {
262 return res.status(404).json({ error: 'Tenant not found' });
265 const tenant = tenantResult.rows[0];
267 // Connect to MeshCentral
268 const meshcentral = new MeshCentralAPI({
269 url: process.env.MESHCENTRAL_URL,
270 username: process.env.MESHCENTRAL_ADMIN_USER || process.env.MESHCENTRAL_USERNAME,
271 password: process.env.MESHCENTRAL_ADMIN_PASS || process.env.MESHCENTRAL_PASSWORD
274 await meshcentral.login();
275 const node = await meshcentral.getNode(nodeId);
278 return res.status(404).json({ error: 'Device not found in MeshCentral' });
281 const parsed = MeshCentralAPI.parseNodeData(node);
282 const tagData = MeshCentralAPI.parseDeviceTags(node.tags);
284 // Update device tags in MeshCentral
285 await meshcentral.updateDeviceMetadata(nodeId, {
287 customerId: customerId,
288 tags: tagData.customTags
291 console.log(`✅ Tagged device ${parsed.hostname} with tenant ${tenantId}`);
293 // Create or update agent in database
294 const agentCheck = await pool.query(
295 'SELECT agent_id FROM agents WHERE meshcentral_nodeid = $1',
299 if (agentCheck.rows.length > 0) {
300 // Update existing agent
303 SET tenant_id = $1, customer_id = $2, meshcentral_connected = $3, last_seen = $4
304 WHERE meshcentral_nodeid = $5`,
305 [tenantId, customerId, parsed.connected, parsed.lastSeen, nodeId]
307 console.log(`✅ Updated agent ${agentCheck.rows[0].agent_id} in database`);
310 const { v4: uuidv4 } = require('uuid');
312 `INSERT INTO agents (
313 tenant_id, customer_id, hostname, platform, os, os_version,
314 agent_uuid, meshcentral_nodeid, meshcentral_connected,
315 last_seen, status, created_at
316 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW())`,
328 parsed.connected ? 'online' : 'offline'
331 console.log(`✅ Created new agent for ${parsed.hostname} in database`);
334 // Optionally move device to tenant's mesh group
336 if (moveMesh && tenant.meshcentral_group_id && node.meshid !== tenant.meshcentral_group_id) {
338 await meshcentral._sendWebSocketMessage({
339 action: 'changeDeviceMesh',
341 meshid: tenant.meshcentral_group_id
344 console.log(`✅ Moved device to ${tenant.name} mesh group`);
346 console.error('Failed to move device to tenant mesh:', moveErr);
354 hostname: parsed.hostname,
356 tenantName: tenant.name,
357 customerId: customerId,
362 console.error('Error assigning device:', err);
363 res.status(500).json({ error: 'Failed to assign device', message: err.message });
367module.exports = router;