diff --git a/backend/routes/schedulerRoutes.js b/backend/routes/schedulerRoutes.js index ef07750..9aa95e1 100644 --- a/backend/routes/schedulerRoutes.js +++ b/backend/routes/schedulerRoutes.js @@ -6,6 +6,7 @@ const { sendError, sendOk } = require('../middleware/errorHandler'); const { readS3TextObject, writeS3JsonObject } = require('../services/s3Service'); const networkMapScheduler = require('../services/networkMapScheduler'); const pingServicesScheduler = require('../services/pingServicesScheduler'); +const uptimeMonitorScheduler = require('../services/uptimeMonitorScheduler'); const UI_SETTINGS_KEY = 'bgp_data/rt_ui_settings.json'; const NETWORK_MAP_CACHE_KEY = 'network-map-cache/latest.json'; @@ -23,6 +24,11 @@ const PING_SERVICES_SCHEDULER_KEYS = [ 'pingServicesSchedulerIntervalMinutes', ]; +const UPTIME_MONITOR_SCHEDULER_KEYS = [ + 'uptimeMonitorSchedulerEnabled', + 'uptimeMonitorSchedulerIntervalMinutes', +]; + async function readUiSettings() { try { const data = await readS3TextObject(UI_SETTINGS_KEY).catch(() => ({ body: '{}' })); @@ -178,6 +184,66 @@ async function runPingServicesSchedulerNow(req, res) { } } +// === Uptime monitor scheduler === + +/** GET /api/scheduler/uptime-monitor/settings */ +async function getUptimeMonitorSchedulerSettings(req, res) { + try { + const ui = await readUiSettings(); + const settings = uptimeMonitorScheduler.getSettingsFromUiSettings(ui); + const nextRunAt = uptimeMonitorScheduler.getNextRunAt(settings); + return res.json({ ...settings, nextRunAt: nextRunAt || undefined }); + } catch (e) { + console.error('[scheduler] getUptimeMonitorSettings', e); + return sendError(res, 500, 'Не удалось прочитать настройки', 'E_SCHEDULER'); + } +} + +/** PATCH /api/scheduler/uptime-monitor/settings */ +async function patchUptimeMonitorSchedulerSettings(req, res) { + try { + const body = req.body || {}; + const current = await readUiSettings(); + for (const key of UPTIME_MONITOR_SCHEDULER_KEYS) { + if (Object.prototype.hasOwnProperty.call(body, key)) { + current[key] = body[key]; + } + } + await writeS3JsonObject(UI_SETTINGS_KEY, current); + const settings = uptimeMonitorScheduler.getSettingsFromUiSettings(current); + return res.json(settings); + } catch (e) { + console.error('[scheduler] patchUptimeMonitorSettings', e); + return sendError(res, 500, 'Не удалось сохранить настройки', 'E_SCHEDULER'); + } +} + +/** POST /api/scheduler/uptime-monitor/run-now */ +async function runUptimeMonitorSchedulerNow(req, res) { + try { + const port = Number(process.env.PORT) || 3001; + const baseUrl = `http://127.0.0.1:${port}`; + const resServers = await fetch(`${baseUrl}/api/servers`).catch(() => null); + const data = resServers?.ok ? await resServers.json() : []; + const servers = Array.isArray(data) ? data : []; + const jumphosts = servers.filter( + (s) => s && ['jumphost', 'home'].includes(String(s.type || '').toLowerCase()) + ); + const serverIds = jumphosts.map((s) => s.id || s.dns || s.ip).filter(Boolean); + for (const serverId of serverIds) { + fetch(`${baseUrl}/api/uptime/check`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ serverId }), + }).catch((err) => console.warn('[scheduler] uptime check', serverId, err?.message)); + } + return sendOk(res, { message: 'Проверки Uptime Monitor запущены в фоне' }); + } catch (e) { + console.error('[scheduler] uptime-monitor run-now', e); + return sendError(res, 500, 'Не удалось запустить', 'E_SCHEDULER'); + } +} + module.exports = { getNetworkMapSchedulerSettings, patchNetworkMapSchedulerSettings, @@ -187,4 +253,7 @@ module.exports = { getPingServicesSchedulerSettings, patchPingServicesSchedulerSettings, runPingServicesSchedulerNow, + getUptimeMonitorSchedulerSettings, + patchUptimeMonitorSchedulerSettings, + runUptimeMonitorSchedulerNow, }; diff --git a/backend/server.js b/backend/server.js index 5e2c3ae..a9f5c14 100644 --- a/backend/server.js +++ b/backend/server.js @@ -33,6 +33,7 @@ const trafficRoutes = require('./routes/trafficRoutes'); const { initMikrotikBackupScheduler } = require('./services/mikrotikBackupScheduler'); const { initNetworkMapScheduler } = require('./services/networkMapScheduler'); const { initPingServicesScheduler } = require('./services/pingServicesScheduler'); +const { initUptimeMonitorScheduler } = require('./services/uptimeMonitorScheduler'); const schedulerRoutes = require('./routes/schedulerRoutes'); const app = express(); @@ -499,6 +500,11 @@ app.get('/api/scheduler/ping-services/settings', schedulerRoutes.getPingServices app.patch('/api/scheduler/ping-services/settings', writeLimiter, schedulerRoutes.patchPingServicesSchedulerSettings); app.post('/api/scheduler/ping-services/run-now', writeLimiter, schedulerRoutes.runPingServicesSchedulerNow); +// === SCHEDULER (Uptime Monitor: проверки по расписанию, по умолчанию каждые 2 мин) === +app.get('/api/scheduler/uptime-monitor/settings', schedulerRoutes.getUptimeMonitorSchedulerSettings); +app.patch('/api/scheduler/uptime-monitor/settings', writeLimiter, schedulerRoutes.patchUptimeMonitorSchedulerSettings); +app.post('/api/scheduler/uptime-monitor/run-now', writeLimiter, schedulerRoutes.runUptimeMonitorSchedulerNow); + // === MIKROTIK VALIDATION === app.post('/api/mikrotik/validate', async (req, res) => { const { config } = req.body; @@ -541,5 +547,10 @@ app.listen(port, () => { } catch (e) { logger.error({ component: 'ping-services-scheduler', err: e && e.message }, 'Failed to start ping services scheduler'); } + try { + initUptimeMonitorScheduler(logger, port); + } catch (e) { + logger.error({ component: 'uptime-monitor-scheduler', err: e && e.message }, 'Failed to start uptime monitor scheduler'); + } }); diff --git a/backend/services/uptimeMonitorScheduler.js b/backend/services/uptimeMonitorScheduler.js new file mode 100644 index 0000000..0b99f7d --- /dev/null +++ b/backend/services/uptimeMonitorScheduler.js @@ -0,0 +1,142 @@ +/** + * Планировщик проверок Uptime Monitor. + * Периодически вызывает POST /api/uptime/check для каждого jumphost/home и обновляет кеш в S3. + * После перезапуска контейнера кеш обновляется по расписанию без необходимости открывать страницу. + */ + +const { readS3TextObject } = require('./s3Service'); + +const UI_SETTINGS_KEY = 'bgp_data/rt_ui_settings.json'; + +const DEFAULT_INTERVAL_MINUTES = 2; +const PARALLEL_CHECKS = 2; + +let schedulerTimer = null; +let lastScheduledRunAt = null; + +function getSettingsFromUiSettings(uiSettings) { + return { + enabled: uiSettings.uptimeMonitorSchedulerEnabled !== false, + intervalMinutes: Math.max(1, Math.min(1440, parseInt(uiSettings.uptimeMonitorSchedulerIntervalMinutes, 10) || DEFAULT_INTERVAL_MINUTES)), + }; +} + +function getNextRunAt(settings) { + if (!settings || !settings.enabled) return null; + const intervalMs = (settings.intervalMinutes || DEFAULT_INTERVAL_MINUTES) * 60 * 1000; + const from = lastScheduledRunAt || Date.now(); + return from + intervalMs; +} + +/** + * Запустить планировщик Uptime Monitor. + * @param {object} logger - pino logger + * @param {number} port - порт приложения для вызова API + */ +function initUptimeMonitorScheduler(logger, port) { + const log = logger || console; + const baseUrl = `http://127.0.0.1:${port}`; + + async function runChecks(serverIds) { + const runOne = async (serverId) => { + try { + const res = await fetch(`${baseUrl}/api/uptime/check`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ serverId }), + }); + if (!res.ok) { + log.warn({ component: 'uptime-monitor-scheduler', serverId, status: res.status }, 'Uptime check failed'); + } + } catch (err) { + log.warn({ component: 'uptime-monitor-scheduler', serverId, err: err?.message }, 'Uptime check error'); + } + }; + + for (let i = 0; i < serverIds.length; i += PARALLEL_CHECKS) { + const chunk = serverIds.slice(i, i + PARALLEL_CHECKS); + await Promise.all(chunk.map(runOne)); + } + } + + async function tick() { + lastScheduledRunAt = Date.now(); + let uiSettings; + try { + const raw = await readS3TextObject(UI_SETTINGS_KEY).catch(() => ({ body: '{}' })); + uiSettings = JSON.parse(raw?.body || '{}') || {}; + } catch (_) { + return; + } + const settings = getSettingsFromUiSettings(uiSettings); + if (!settings.enabled) return; + + let servers = []; + try { + const res = await fetch(`${baseUrl}/api/servers`); + const data = res.ok ? await res.json() : []; + servers = Array.isArray(data) ? data : []; + } catch (err) { + log.error({ component: 'uptime-monitor-scheduler', err: err?.message }, 'Failed to fetch servers'); + return; + } + + const jumphosts = servers.filter( + (s) => s && ['jumphost', 'home'].includes(String(s.type || '').toLowerCase()) + ); + const serverIds = jumphosts + .map((s) => s.id || s.dns || s.ip) + .filter(Boolean); + + if (serverIds.length === 0) { + log.info({ component: 'uptime-monitor-scheduler' }, 'No jumphost/home servers to check'); + return; + } + + log.info( + { component: 'uptime-monitor-scheduler', count: serverIds.length }, + 'Running uptime monitor checks for %d server(s)', + serverIds.length + ); + await runChecks(serverIds); + } + + async function schedule() { + let uiSettings; + try { + const raw = await readS3TextObject(UI_SETTINGS_KEY).catch(() => ({ body: '{}' })); + uiSettings = JSON.parse(raw?.body || '{}') || {}; + } catch (_) { + return; + } + const settings = getSettingsFromUiSettings(uiSettings); + if (!settings.enabled) { + log.info({ component: 'uptime-monitor-scheduler' }, 'Uptime monitor scheduler disabled in settings'); + return; + } + const intervalMs = settings.intervalMinutes * 60 * 1000; + log.info( + { component: 'uptime-monitor-scheduler', intervalMinutes: settings.intervalMinutes }, + 'Uptime monitor scheduler started (first run in 20 s, then every %d min)', + settings.intervalMinutes + ); + schedulerTimer = setInterval(tick, intervalMs); + setTimeout(() => tick(), 20 * 1000); + } + + schedule(); +} + +function stopUptimeMonitorScheduler() { + if (schedulerTimer) { + clearInterval(schedulerTimer); + schedulerTimer = null; + } +} + +module.exports = { + initUptimeMonitorScheduler, + stopUptimeMonitorScheduler, + getSettingsFromUiSettings, + getNextRunAt, +}; diff --git a/frontend/src/SettingsPage.jsx b/frontend/src/SettingsPage.jsx index 59f3d66..3e49f15 100644 --- a/frontend/src/SettingsPage.jsx +++ b/frontend/src/SettingsPage.jsx @@ -89,6 +89,8 @@ export default function SettingsPage() { const [uptimeMonitorIntervalSeconds, setUptimeMonitorIntervalSeconds] = useState('120'); const [uptimeMonitorCheckType, setUptimeMonitorCheckType] = useState('http'); const [uptimeMonitorCacheSeconds, setUptimeMonitorCacheSeconds] = useState('120'); + const [uptimeMonitorSchedulerEnabled, setUptimeMonitorSchedulerEnabled] = useState(true); + const [uptimeMonitorSchedulerIntervalMinutes, setUptimeMonitorSchedulerIntervalMinutes] = useState('2'); const [serversList, setServersList] = useState([]); const [sidebarSearch, setSidebarSearch] = useState(''); const [activeSection, setActiveSection] = useState(() => { @@ -272,6 +274,12 @@ export default function SettingsPage() { setUptimeMonitorCacheSeconds( data?.uptimeMonitorCacheSeconds != null ? String(data.uptimeMonitorCacheSeconds) : '120' ); + setUptimeMonitorSchedulerEnabled(data?.uptimeMonitorSchedulerEnabled !== false); + setUptimeMonitorSchedulerIntervalMinutes( + data?.uptimeMonitorSchedulerIntervalMinutes != null + ? String(data.uptimeMonitorSchedulerIntervalMinutes) + : '2' + ); const raw = data?.trafficInterfaces; setTrafficInterfacesSelected( Array.isArray(raw) @@ -391,6 +399,11 @@ export default function SettingsPage() { 0, parseInt(uptimeMonitorCacheSeconds, 10) || 120 ), + uptimeMonitorSchedulerEnabled: Boolean(uptimeMonitorSchedulerEnabled), + uptimeMonitorSchedulerIntervalMinutes: Math.max( + 1, + Math.min(1440, parseInt(uptimeMonitorSchedulerIntervalMinutes, 10) || 2) + ), trafficInterfaces: Array.isArray(trafficInterfacesSelected) ? trafficInterfacesSelected.map((p) => ({ serverKey: p.serverKey, @@ -825,6 +838,33 @@ export default function SettingsPage() { /> +
+
+ setUptimeMonitorSchedulerEnabled(e.target.checked)} + disabled={saving} + /> + +
+ +
)} diff --git a/frontend/src/UptimeMonitorPage.jsx b/frontend/src/UptimeMonitorPage.jsx index b4184dc..cf3da16 100644 --- a/frontend/src/UptimeMonitorPage.jsx +++ b/frontend/src/UptimeMonitorPage.jsx @@ -187,7 +187,14 @@ export default function UptimeMonitorPage() { setChecking(false); }, [jumphosts]); - // Автопроверка по интервалу в фоне (при открытии страницы данные берём из кеша, без немедленного запуска проверки) + // Первый запуск проверки через несколько секунд после загрузки (после перезапуска контейнера кеш может быть пустым или устаревшим) + useEffect(() => { + if (jumphosts.length === 0) return; + const t = setTimeout(runChecks, 4000); + return () => clearTimeout(t); + }, [jumphosts.length, runChecks]); + + // Автопроверка по интервалу в фоне useEffect(() => { if (jumphosts.length === 0) return; intervalRef.current = setInterval(runChecks, checkIntervalMs);