/** * FarmBox Sync Agent * Синхронизация локального farm-cmms с облаком * Работает в фоне, не блокирует локальную работу */ const fs = require('fs'); const path = require('path'); const fetch = require('node-fetch'); const LOCAL_URL = process.env.LOCAL_CMMS_URL || 'http://localhost:3005'; const CLOUD_URL = process.env.CLOUD_CMMS_URL || 'https://to.zeroday.su'; const INTERVAL_SEC = parseInt(process.env.SYNC_INTERVAL_SEC || '30'); const FARM_ID = process.env.FARM_ID || 'farm_001'; const FARM_NAME = process.env.FARM_NAME || 'Ферма 1'; const SYNC_TOKEN = process.env.SYNC_TOKEN || 'changeme'; const QUEUE_DIR = path.join(__dirname, 'queue'); // Очередь изменений (персистентная — пережидает перезапуск) if (!fs.existsSync(QUEUE_DIR)) fs.mkdirSync(QUEUE_DIR, { recursive: true }); let isOnline = false; let lastSyncAt = null; let syncStats = { success: 0, failed: 0, queued: 0 }; // ─── Проверка интернета ──────────────────────────────── async function checkOnline() { try { const res = await fetch(`${CLOUD_URL}/api/health`, { timeout: 5000 }); return res.ok; } catch { return false; } } // ─── Получить изменения с локального сервера ────────── async function getLocalChanges(since) { try { const res = await fetch( `${LOCAL_URL}/api/sync/changes?since=${encodeURIComponent(since)}&farm_id=${FARM_ID}`, { headers: { 'X-Sync-Token': SYNC_TOKEN }, timeout: 10000 } ); if (!res.ok) return []; return res.json(); } catch (err) { console.error('[Sync] getLocalChanges:', err.message); return []; } } // ─── Отправить изменения в облако ───────────────────── async function pushToCloud(changes) { const res = await fetch(`${CLOUD_URL}/api/sync/receive`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Sync-Token': SYNC_TOKEN, 'X-Farm-Id': FARM_ID, 'X-Farm-Name': FARM_NAME }, body: JSON.stringify({ changes, farm_id: FARM_ID, farm_name: FARM_NAME }), timeout: 15000 }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); } // ─── Получить обновления из облака ──────────────────── async function pullFromCloud() { try { const res = await fetch( `${CLOUD_URL}/api/sync/push?farm_id=${FARM_ID}`, { headers: { 'X-Sync-Token': SYNC_TOKEN }, timeout: 10000 } ); if (!res.ok) return []; return res.json(); } catch { return []; } } // ─── Применить обновления из облака локально ────────── async function applyCloudUpdates(updates) { if (!updates?.length) return; try { await fetch(`${LOCAL_URL}/api/sync/apply`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Sync-Token': SYNC_TOKEN }, body: JSON.stringify({ updates }), timeout: 10000 }); } catch (err) { console.error('[Sync] applyCloudUpdates:', err.message); } } // ─── Сохранить в очередь если нет интернета ─────────── function saveToQueue(changes) { if (!changes?.length) return; const filename = path.join(QUEUE_DIR, `${Date.now()}.json`); fs.writeFileSync(filename, JSON.stringify(changes)); syncStats.queued += changes.length; console.log(`[Sync] 💾 Сохранено в очередь: ${changes.length} изменений`); } // ─── Отправить очередь когда появился интернет ──────── async function flushQueue() { const files = fs.readdirSync(QUEUE_DIR).filter(f => f.endsWith('.json')).sort(); if (!files.length) return; console.log(`[Sync] 📤 Отправка очереди: ${files.length} файлов`); for (const file of files) { try { const changes = JSON.parse(fs.readFileSync(path.join(QUEUE_DIR, file))); await pushToCloud(changes); fs.unlinkSync(path.join(QUEUE_DIR, file)); syncStats.success += changes.length; } catch (err) { console.error(`[Sync] Ошибка отправки ${file}:`, err.message); syncStats.failed++; break; // Пробуем в следующем цикле } } } // ─── Основной цикл синхронизации ───────────────────── let lastChangeTime = new Date(Date.now() - 60 * 60 * 1000).toISOString(); // -1 час при старте async function syncCycle() { const online = await checkOnline(); if (online && !isOnline) { console.log('[Sync] 🟢 Интернет появился — отправляем очередь'); await flushQueue(); } else if (!online && isOnline) { console.log('[Sync] 🔴 Интернет пропал — работаем локально'); } isOnline = online; try { // Получаем локальные изменения const changes = await getLocalChanges(lastChangeTime); if (changes.length > 0) { if (isOnline) { await pushToCloud(changes); lastSyncAt = new Date().toISOString(); syncStats.success += changes.length; console.log(`[Sync] ✅ Отправлено в облако: ${changes.length} изменений`); } else { saveToQueue(changes); } lastChangeTime = new Date().toISOString(); } // Получаем обновления из облака if (isOnline) { const updates = await pullFromCloud(); if (updates?.length > 0) { await applyCloudUpdates(updates); console.log(`[Sync] ⬇️ Получено из облака: ${updates.length} обновлений`); } } } catch (err) { console.error('[Sync] Ошибка цикла:', err.message); } } // ─── Запуск ─────────────────────────────────────────── console.log(`[Sync] 🚀 Агент синхронизации запущен`); console.log(`[Sync] Ферма: ${FARM_NAME} (${FARM_ID})`); console.log(`[Sync] Локальный: ${LOCAL_URL}`); console.log(`[Sync] Облако: ${CLOUD_URL}`); console.log(`[Sync] Интервал: ${INTERVAL_SEC}с`); syncCycle(); setInterval(syncCycle, INTERVAL_SEC * 1000);