feat(PingServices): implement history tracking for ping results and update API responses to include historical data
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m46s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m46s
This commit is contained in:
@@ -367,6 +367,35 @@ async function loadUiSettingsSync() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PING_SERVICES_CACHE_KEY_PREFIX = 'ping-services/cache_';
|
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.
|
* Обновить кеш пинг-сервисов: выполнить пинги по списку из настроек и записать результат в S3.
|
||||||
@@ -394,9 +423,8 @@ async function refreshPingServicesCache() {
|
|||||||
gatewayIp = (uiSettings.pingServicesGatewayIp && String(uiSettings.pingServicesGatewayIp).trim()) || null;
|
gatewayIp = (uiSettings.pingServicesGatewayIp && String(uiSettings.pingServicesGatewayIp).trim()) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = cacheSeconds > 0
|
const suffix = pingServicesStorageSuffix(viaRouter, serverId, gatewayIp);
|
||||||
? PING_SERVICES_CACHE_KEY_PREFIX + (viaRouter ? 'router' : 'web') + '_' + String(serverId || '').replace(/[^a-zA-Z0-9.-]/g, '_') + '_' + String(gatewayIp || '').replace(/[^a-zA-Z0-9.]/g, '_')
|
const cacheKey = cacheSeconds > 0 ? PING_SERVICES_CACHE_KEY_PREFIX + suffix : null;
|
||||||
: null;
|
|
||||||
|
|
||||||
if (viaRouter) {
|
if (viaRouter) {
|
||||||
const { runPingViaRouter } = require('./mikrotikConfigRoutes');
|
const { runPingViaRouter } = require('./mikrotikConfigRoutes');
|
||||||
@@ -433,6 +461,14 @@ async function refreshPingServicesCache() {
|
|||||||
if (cacheKey) {
|
if (cacheKey) {
|
||||||
writeS3JsonObject(cacheKey, { byId, cachedAt: Date.now() }).catch((err) => console.warn('[ping-services] cache write failed:', err?.message));
|
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;
|
return byId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,6 +483,14 @@ async function refreshPingServicesCache() {
|
|||||||
if (cacheKey) {
|
if (cacheKey) {
|
||||||
writeS3JsonObject(cacheKey, { byId, cachedAt: Date.now() }).catch((err) => console.warn('[ping-services] cache write failed:', err?.message));
|
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;
|
return byId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,9 +529,8 @@ async function getPingServices(req, res) {
|
|||||||
gatewayIp = (uiSettings.pingServicesGatewayIp && String(uiSettings.pingServicesGatewayIp).trim()) || null;
|
gatewayIp = (uiSettings.pingServicesGatewayIp && String(uiSettings.pingServicesGatewayIp).trim()) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = cacheSeconds > 0
|
const suffix = pingServicesStorageSuffix(viaRouter, serverId, gatewayIp);
|
||||||
? PING_SERVICES_CACHE_KEY_PREFIX + (viaRouter ? 'router' : 'web') + '_' + String(serverId || '').replace(/[^a-zA-Z0-9.-]/g, '_') + '_' + String(gatewayIp || '').replace(/[^a-zA-Z0-9.]/g, '_')
|
const cacheKey = cacheSeconds > 0 ? PING_SERVICES_CACHE_KEY_PREFIX + suffix : null;
|
||||||
: null;
|
|
||||||
const skipCache = ['1', 'true', 'yes'].includes(String(req.query?.refresh || req.query?.nocache || '').toLowerCase());
|
const skipCache = ['1', 'true', 'yes'].includes(String(req.query?.refresh || req.query?.nocache || '').toLowerCase());
|
||||||
if (cacheKey && !skipCache) {
|
if (cacheKey && !skipCache) {
|
||||||
try {
|
try {
|
||||||
@@ -497,19 +540,21 @@ async function getPingServices(req, res) {
|
|||||||
const cachedAt = typeof cached.cachedAt === 'number' ? cached.cachedAt : 0;
|
const cachedAt = typeof cached.cachedAt === 'number' ? cached.cachedAt : 0;
|
||||||
const ttlMs = cacheSeconds * 1000;
|
const ttlMs = cacheSeconds * 1000;
|
||||||
if (cachedAt && Date.now() - cachedAt < ttlMs && cached.byId && typeof cached.byId === 'object') {
|
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 (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const byId = await refreshPingServicesCache();
|
const byId = await refreshPingServicesCache();
|
||||||
return res.json(byId);
|
const history = await readPingServicesHistory(suffix);
|
||||||
|
return res.json({ byId, history });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('ping-services error', e);
|
console.error('ping-services error', e);
|
||||||
const fallback = {};
|
const fallback = {};
|
||||||
PING_SERVICES_DEFAULT.forEach((s) => { fallback[s.id] = { id: s.id, name: s.name, host: s.host, ms: null }; });
|
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: {} });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,8 +78,6 @@ function MetricCard({ title, value, icon: Icon, color, description }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const PING_HISTORY_MAX = 30;
|
|
||||||
|
|
||||||
/** Обёртка для Sparkline по ширине контейнера */
|
/** Обёртка для Sparkline по ширине контейнера */
|
||||||
function PingSparklineWrap({ history, color }) {
|
function PingSparklineWrap({ history, color }) {
|
||||||
const wrapRef = useRef(null);
|
const wrapRef = useRef(null);
|
||||||
@@ -191,7 +189,6 @@ function Dashboard() {
|
|||||||
const [pingServices, setPingServices] = useState(null);
|
const [pingServices, setPingServices] = useState(null);
|
||||||
const [pingServicesConfig, setPingServicesConfig] = useState([]);
|
const [pingServicesConfig, setPingServicesConfig] = useState([]);
|
||||||
const [pingLoading, setPingLoading] = useState(true);
|
const [pingLoading, setPingLoading] = useState(true);
|
||||||
const [previousPingServices, setPreviousPingServices] = useState(null);
|
|
||||||
const [pingHistory, setPingHistory] = useState(() => ({}));
|
const [pingHistory, setPingHistory] = useState(() => ({}));
|
||||||
const [expandedPingId, setExpandedPingId] = useState(null);
|
const [expandedPingId, setExpandedPingId] = useState(null);
|
||||||
|
|
||||||
@@ -308,26 +305,11 @@ function Dashboard() {
|
|||||||
setPingLoading(true);
|
setPingLoading(true);
|
||||||
api.get('/ping-services', { params: forceRefresh ? { refresh: 1 } : {} })
|
api.get('/ping-services', { params: forceRefresh ? { refresh: 1 } : {} })
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
const next = data && typeof data === 'object' ? data : null;
|
if (!data || typeof data !== 'object') return;
|
||||||
setPingServices((current) => {
|
const byId = data.byId ?? data;
|
||||||
if (current && typeof current === 'object' && Object.keys(current).length > 0) {
|
const history = data.history ?? {};
|
||||||
setPreviousPingServices(current);
|
setPingServices(byId);
|
||||||
}
|
setPingHistory(typeof history === 'object' ? history : {});
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(() => setPingServices(null))
|
.catch(() => setPingServices(null))
|
||||||
.finally(() => setPingLoading(false));
|
.finally(() => setPingLoading(false));
|
||||||
@@ -391,7 +373,10 @@ function Dashboard() {
|
|||||||
<PingServiceCard
|
<PingServiceCard
|
||||||
config={config}
|
config={config}
|
||||||
ms={pingServices?.[config.id]?.ms ?? null}
|
ms={pingServices?.[config.id]?.ms ?? null}
|
||||||
previousMs={previousPingServices?.[config.id]?.ms ?? null}
|
previousMs={(() => {
|
||||||
|
const hist = pingHistory[config.id] || [];
|
||||||
|
return hist.length >= 2 ? hist[hist.length - 2] : null;
|
||||||
|
})()}
|
||||||
history={pingHistory[config.id] || []}
|
history={pingHistory[config.id] || []}
|
||||||
loading={pingLoading}
|
loading={pingLoading}
|
||||||
isExpanded={expandedPingId === config.id}
|
isExpanded={expandedPingId === config.id}
|
||||||
|
|||||||
Reference in New Issue
Block a user