2 * MeshCentral Auto-Sync Worker (Backup/Safety Net)
5 * 1. PRIMARY: meshcentral-device-sync.js (Direct DB queries, runs every 5 minutes)
6 * 2. BACKUP: This worker (API-based, runs daily at 3 AM as safety net)
7 * 3. DEPRECATED: POST /api/meshcentral/sync (Manual API endpoint, use /api/sync/meshcentral instead)
9 * This worker runs daily as a backup to catch devices missed by the primary sync.
10 * The primary sync method (meshcentral-device-sync.js) queries the MeshCentral
11 * PostgreSQL database directly every 5 minutes, which is much faster and more reliable.
13 * This worker serves as a safety net to:
14 * - Catch devices missed by primary sync due to transient errors
15 * - Verify sync accuracy once daily
16 * - Update connection status for all devices comprehensive check
18 * Uses hardware identifiers (hostname, MAC, motherboard UUID) for matching
20 * TODO: When primary sync is proven stable for 30+ days, this worker can be
21 * deprecated or converted to a weekly health check instead of daily.
24const { Worker, Queue } = require('bullmq');
25const Redis = require('ioredis');
26const db = require('../db');
27const MeshCentralAPI = require('../lib/meshcentral-api');
29const redisConfig = require('../config/redis');
30const connection = new Redis({
32 maxRetriesPerRequest: null // BullMQ requires this
35// Create queue for MeshCentral sync jobs
36const meshSyncQueue = new Queue('meshcentral-sync', { connection });
39 * Get MeshCentral API instance.
40 * Creates authenticated MeshCentral API client with WebSocket connection.
41 * @function getMeshAPI
42 * @returns {Promise<MeshCentralAPI>} Authenticated API instance
44async function getMeshAPI() {
45 const api = new MeshCentralAPI({
46 url: process.env.MESHCENTRAL_URL || 'https://rmm-psa-meshcentral-aq48h.ondigitalocean.app',
47 username: process.env.MESHCENTRAL_ADMIN_USER || 'admin',
48 password: process.env.MESHCENTRAL_ADMIN_PASS || 'admin'
52 await api.connectWebSocket();
57 * Perform MeshCentral device sync.
58 * Fetches devices from MeshCentral and matches with database agents.
59 * @function syncMeshCentralDevices
60 * @returns {Promise<void>}
62async function syncMeshCentralDevices() {
63 console.log('\nš [MeshSync] Starting automatic sync...');
66 const api = await getMeshAPI();
67 const nodes = await api.getNodes();
69 console.log(`š [MeshSync] Found ${nodes.length} MeshCentral devices`);
75 const matchDetails = [];
77 for (const node of nodes) {
79 const parsed = MeshCentralAPI.parseNodeData(node);
81 if (!parsed.hostname) {
82 console.log(`ā ļø [MeshSync] Skipping device without hostname: ${parsed.nodeId}`);
87 // Check if device already linked
88 const existingLink = await db.query(
89 'SELECT agent_id, hostname, hardware_data FROM agents WHERE meshcentral_nodeid = $1',
93 if (existingLink.rows.length > 0) {
94 // Update connection status
97 SET meshcentral_connected = $1,
98 meshcentral_last_seen = $2,
99 last_seen = GREATEST(last_seen, $2)
100 WHERE meshcentral_nodeid = $3
110 // Try to find matching agent by hostname
111 const hostnameMatch = await db.query(
112 'SELECT agent_id, agent_uuid, hostname FROM agents WHERE LOWER(hostname) = LOWER($1) AND meshcentral_nodeid IS NULL LIMIT 1',
116 if (hostnameMatch.rows.length > 0) {
117 const agent = hostnameMatch.rows[0];
119 // Link agent to MeshCentral device
122 SET meshcentral_nodeid = $1,
123 meshcentral_connected = $2,
124 meshcentral_last_seen = $3
125 WHERE agent_id = $4`,
126 [parsed.nodeId, parsed.connected, parsed.lastSeen, agent.agent_id]
131 agentId: agent.agent_id,
132 agentUuid: agent.agent_uuid,
133 hostname: agent.hostname,
134 nodeId: parsed.nodeId,
138 console.log(`ā
[MeshSync] Linked agent ${agent.agent_id} (${agent.hostname}) to MeshCentral node ${parsed.nodeId.substring(0, 20)}...`);
140 // Try MAC address matching if available
141 let macMatched = false;
143 if (parsed.hardware && parsed.hardware.macAddresses && parsed.hardware.macAddresses.length > 0) {
144 for (const mac of parsed.hardware.macAddresses) {
145 const macMatch = await db.query(`
146 SELECT agent_id, agent_uuid, hostname, hardware_data
148 WHERE meshcentral_nodeid IS NULL
149 AND hardware_data IS NOT NULL
150 AND hardware_data::text ILIKE $1
154 if (macMatch.rows.length > 0) {
155 const agent = macMatch.rows[0];
159 SET meshcentral_nodeid = $1,
160 meshcentral_connected = $2,
161 meshcentral_last_seen = $3
162 WHERE agent_id = $4`,
163 [parsed.nodeId, parsed.connected, parsed.lastSeen, agent.agent_id]
169 agentId: agent.agent_id,
170 agentUuid: agent.agent_uuid,
171 hostname: agent.hostname,
172 nodeId: parsed.nodeId,
173 method: 'mac_address',
177 console.log(`ā
[MeshSync] Linked agent ${agent.agent_id} (${agent.hostname}) to MeshCentral node via MAC ${mac}`);
184 // No match found - could create new agent or skip
185 console.log(`ā¹ļø [MeshSync] No matching agent for device: ${parsed.hostname} (${parsed.nodeId.substring(0, 20)}...)`);
190 console.error(`ā [MeshSync] Error processing node:`, err.message);
195 timestamp: new Date().toISOString(),
198 updated: updatedCount,
200 skipped: skippedCount,
201 matchDetails: matchDetails
204 console.log(`\nš [MeshSync] Sync complete:`);
205 console.log(` Total devices: ${summary.total}`);
206 console.log(` Newly linked: ${summary.linked}`);
207 console.log(` Updated: ${summary.updated}`);
208 console.log(` Skipped: ${summary.skipped}`);
214 console.error('ā [MeshSync] Sync failed:', error.message);
220 * BullMQ Worker - Processes sync jobs
222const worker = new Worker('meshcentral-sync', async (job) => {
223 console.log(`\nš [MeshSync Worker] Processing job ${job.id}...`);
226 const result = await syncMeshCentralDevices();
229 console.error(`ā [MeshSync Worker] Job ${job.id} failed:`, error.message);
234 concurrency: 1, // Only run one sync job at a time
237 duration: 60000 // Max 1 job per minute
241worker.on('completed', (job, result) => {
242 console.log(`ā
[MeshSync Worker] Job ${job.id} completed: ${result.linked} linked, ${result.updated} updated`);
245worker.on('failed', (job, err) => {
246 console.error(`ā [MeshSync Worker] Job ${job?.id} failed:`, err.message);
250 * Schedule recurring sync job (every 10 minutes)
252async function scheduleRecurringSync() {
253 // Remove any existing repeatable jobs
254 const repeatableJobs = await meshSyncQueue.getRepeatableJobs();
255 for (const job of repeatableJobs) {
256 await meshSyncQueue.removeRepeatableByKey(job.key);
259 // Add new repeatable job - daily at 3 AM (backup to webhook system)
260 await meshSyncQueue.add(
265 pattern: '0 3 * * *', // Daily at 3:00 AM
266 immediately: true // Run immediately on startup
269 count: 10 // Keep last 10 completed jobs
272 count: 20 // Keep last 20 failed jobs
277 console.log('š [MeshSync] Scheduled to run daily at 3:00 AM (backup sync)');
278 console.log('ā¹ļø [MeshSync] Primary sync via webhook: /api/webhooks/meshcentral');
281// Initialize on startup
282scheduleRecurringSync().catch(err => {
283 console.error('ā [MeshSync] Failed to schedule sync:', err);
286// Manual trigger function (can be called from API)
288 * Trigger manual MeshCentral device sync.
289 * Adds high-priority sync job to queue.
290 * @function triggerManualSync
291 * @returns {Promise<string>} Job ID of queued sync task
293async function triggerManualSync() {
294 const job = await meshSyncQueue.add('manual-sync', {}, { priority: 1 });
295 console.log(`š [MeshSync] Manual sync triggered - Job ID: ${job.id}`);
299console.log('ā
[MeshSync] Worker initialized and ready');
304 syncMeshCentralDevices