diff --git a/backend/routes/miscRoutes.js b/backend/routes/miscRoutes.js index d9a9d79..9c46110 100644 --- a/backend/routes/miscRoutes.js +++ b/backend/routes/miscRoutes.js @@ -367,6 +367,35 @@ async function loadUiSettingsSync() { } const PING_SERVICES_CACHE_KEY_PREFIX = 'ping-services/cache_'; +const PING_SERVICES_HISTORY_KEY_PREFIX = 'ping-services/history_'; +const PING_HISTORY_MAX = 30; + +function pingServicesStorageSuffix(viaRouter, serverId, gatewayIp) { + return (viaRouter ? 'router' : 'web') + '_' + String(serverId || '').replace(/[^a-zA-Z0-9.-]/g, '_') + '_' + String(gatewayIp || '').replace(/[^a-zA-Z0-9.]/g, '_'); +} + +/** + * Прочитать историю замеров пингов из S3. Формат: { byId: { [serviceId]: number[] } }. + */ +async function readPingServicesHistory(suffix) { + const key = PING_SERVICES_HISTORY_KEY_PREFIX + suffix; + try { + const raw = await readS3TextObject(key).catch(() => null); + if (!raw?.body) return {}; + const parsed = JSON.parse(raw.body); + return parsed?.byId && typeof parsed.byId === 'object' ? parsed.byId : {}; + } catch { + return {}; + } +} + +/** + * Записать историю замеров пингов в S3. + */ +async function writePingServicesHistory(suffix, byId) { + const key = PING_SERVICES_HISTORY_KEY_PREFIX + suffix; + await writeS3JsonObject(key, { byId }).catch((err) => console.warn('[ping-services] history write failed:', err?.message)); +} /** * Обновить кеш пинг-сервисов: выполнить пинги по списку из настроек и записать результат в S3. @@ -394,9 +423,8 @@ async function refreshPingServicesCache() { gatewayIp = (uiSettings.pingServicesGatewayIp && String(uiSettings.pingServicesGatewayIp).trim()) || null; } - const cacheKey = cacheSeconds > 0 - ? PING_SERVICES_CACHE_KEY_PREFIX + (viaRouter ? 'router' : 'web') + '_' + String(serverId || '').replace(/[^a-zA-Z0-9.-]/g, '_') + '_' + String(gatewayIp || '').replace(/[^a-zA-Z0-9.]/g, '_') - : null; + const suffix = pingServicesStorageSuffix(viaRouter, serverId, gatewayIp); + const cacheKey = cacheSeconds > 0 ? PING_SERVICES_CACHE_KEY_PREFIX + suffix : null; if (viaRouter) { const { runPingViaRouter } = require('./mikrotikConfigRoutes'); @@ -433,6 +461,14 @@ async function refreshPingServicesCache() { if (cacheKey) { writeS3JsonObject(cacheKey, { byId, cachedAt: Date.now() }).catch((err) => console.warn('[ping-services] cache write failed:', err?.message)); } + const history = await readPingServicesHistory(suffix); + Object.keys(byId).forEach((id) => { + const ms = byId[id]?.ms; + if (typeof ms !== 'number') return; + const list = Array.isArray(history[id]) ? history[id] : []; + history[id] = [...list, ms].slice(-PING_HISTORY_MAX); + }); + await writePingServicesHistory(suffix, history); return byId; } @@ -447,6 +483,14 @@ async function refreshPingServicesCache() { if (cacheKey) { writeS3JsonObject(cacheKey, { byId, cachedAt: Date.now() }).catch((err) => console.warn('[ping-services] cache write failed:', err?.message)); } + const history = await readPingServicesHistory(suffix); + Object.keys(byId).forEach((id) => { + const ms = byId[id]?.ms; + if (typeof ms !== 'number') return; + const list = Array.isArray(history[id]) ? history[id] : []; + history[id] = [...list, ms].slice(-PING_HISTORY_MAX); + }); + await writePingServicesHistory(suffix, history); return byId; } @@ -485,9 +529,8 @@ async function getPingServices(req, res) { gatewayIp = (uiSettings.pingServicesGatewayIp && String(uiSettings.pingServicesGatewayIp).trim()) || null; } - const cacheKey = cacheSeconds > 0 - ? PING_SERVICES_CACHE_KEY_PREFIX + (viaRouter ? 'router' : 'web') + '_' + String(serverId || '').replace(/[^a-zA-Z0-9.-]/g, '_') + '_' + String(gatewayIp || '').replace(/[^a-zA-Z0-9.]/g, '_') - : null; + const suffix = pingServicesStorageSuffix(viaRouter, serverId, gatewayIp); + const cacheKey = cacheSeconds > 0 ? PING_SERVICES_CACHE_KEY_PREFIX + suffix : null; const skipCache = ['1', 'true', 'yes'].includes(String(req.query?.refresh || req.query?.nocache || '').toLowerCase()); if (cacheKey && !skipCache) { try { @@ -497,19 +540,21 @@ async function getPingServices(req, res) { const cachedAt = typeof cached.cachedAt === 'number' ? cached.cachedAt : 0; const ttlMs = cacheSeconds * 1000; if (cachedAt && Date.now() - cachedAt < ttlMs && cached.byId && typeof cached.byId === 'object') { - return res.json(cached.byId); + const history = await readPingServicesHistory(suffix); + return res.json({ byId: cached.byId, history }); } } } catch (_) {} } const byId = await refreshPingServicesCache(); - return res.json(byId); + const history = await readPingServicesHistory(suffix); + return res.json({ byId, history }); } catch (e) { console.error('ping-services error', e); const fallback = {}; PING_SERVICES_DEFAULT.forEach((s) => { fallback[s.id] = { id: s.id, name: s.name, host: s.host, ms: null }; }); - res.status(500).json(fallback); + res.status(500).json({ byId: fallback, history: {} }); } } diff --git a/frontend/src/Dashboard.jsx b/frontend/src/Dashboard.jsx index 4941b1d..8135843 100644 --- a/frontend/src/Dashboard.jsx +++ b/frontend/src/Dashboard.jsx @@ -78,8 +78,6 @@ function MetricCard({ title, value, icon: Icon, color, description }) { ); } -const PING_HISTORY_MAX = 30; - /** Обёртка для Sparkline по ширине контейнера */ function PingSparklineWrap({ history, color }) { const wrapRef = useRef(null); @@ -191,7 +189,6 @@ function Dashboard() { const [pingServices, setPingServices] = useState(null); const [pingServicesConfig, setPingServicesConfig] = useState([]); const [pingLoading, setPingLoading] = useState(true); - const [previousPingServices, setPreviousPingServices] = useState(null); const [pingHistory, setPingHistory] = useState(() => ({})); const [expandedPingId, setExpandedPingId] = useState(null); @@ -308,26 +305,11 @@ function Dashboard() { setPingLoading(true); api.get('/ping-services', { params: forceRefresh ? { refresh: 1 } : {} }) .then(({ data }) => { - const next = data && typeof data === 'object' ? data : null; - setPingServices((current) => { - if (current && typeof current === 'object' && Object.keys(current).length > 0) { - setPreviousPingServices(current); - } - return next; - }); - if (next) { - setPingHistory((h) => { - const out = { ...h }; - Object.keys(next).forEach((id) => { - const ms = next[id]?.ms; - if (typeof ms !== 'number') return; - const list = Array.isArray(out[id]) ? out[id] : []; - const nextList = [...list, ms].slice(-PING_HISTORY_MAX); - out[id] = nextList; - }); - return out; - }); - } + if (!data || typeof data !== 'object') return; + const byId = data.byId ?? data; + const history = data.history ?? {}; + setPingServices(byId); + setPingHistory(typeof history === 'object' ? history : {}); }) .catch(() => setPingServices(null)) .finally(() => setPingLoading(false)); @@ -391,7 +373,10 @@ function Dashboard() { { + const hist = pingHistory[config.id] || []; + return hist.length >= 2 ? hist[hist.length - 2] : null; + })()} history={pingHistory[config.id] || []} loading={pingLoading} isExpanded={expandedPingId === config.id}