From 44843475c8b3a90e979b83f60e62f1ff546d7edd Mon Sep 17 00:00:00 2001 From: CN-JS-HuiBai Date: Tue, 14 Apr 2026 16:56:46 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AE=89=E5=85=A8=E5=92=8C?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E7=AD=96=E7=95=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/js/app.js | 9 ++++- server/geo-service.js | 36 +++++++++++++++++- server/index.js | 74 +++++++++++++++++++++++++----------- server/prometheus-service.js | 21 +++++++--- 4 files changed, 110 insertions(+), 30 deletions(-) diff --git a/public/js/app.js b/public/js/app.js index 8ad01a8..9ef7b33 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -248,9 +248,16 @@ function init() { try { console.log('[Init] Start...'); + + // Clear existing intervals to prevent duplication on re-init + if (backgroundIntervals && backgroundIntervals.length > 0) { + backgroundIntervals.forEach(clearInterval); + } + backgroundIntervals = []; + // Resource Gauges Time updateGaugesTime(); - setInterval(updateGaugesTime, 1000); + backgroundIntervals.push(setInterval(updateGaugesTime, 1000)); // Initial footer year if (dom.copyrightYear) { diff --git a/server/geo-service.js b/server/geo-service.js index f991f25..900b6fd 100644 --- a/server/geo-service.js +++ b/server/geo-service.js @@ -85,7 +85,18 @@ async function getLocation(target) { // Secondary DB check with resolved IP const [rows] = await db.query('SELECT * FROM server_locations WHERE ip = ?', [cleanIp]); if (rows.length > 0) { - return normalizeGeo(rows[0]); + const data = rows[0]; + // Cache the domain mapping to avoid future DNS lookups + if (cleanTarget !== cleanIp) { + try { + await db.query(` + INSERT INTO server_locations (ip, country, country_name, region, city, latitude, longitude) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE last_updated = CURRENT_TIMESTAMP + `, [cleanTarget, data.country, data.country_name, data.region, data.city, data.latitude, data.longitude]); + } catch(e) {} + } + return normalizeGeo(data); } } catch (err) { // Quiet DNS failure for tokens (legacy bug mitigation) @@ -145,6 +156,29 @@ async function getLocation(target) { locationData.longitude ]); + // Cache the domain target as well if it differs from the resolved IP + if (cleanTarget !== cleanIp) { + await db.query(` + INSERT INTO server_locations (ip, country, country_name, region, city, latitude, longitude) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + country = VALUES(country), + country_name = VALUES(country_name), + region = VALUES(region), + city = VALUES(city), + latitude = VALUES(latitude), + longitude = VALUES(longitude) + `, [ + cleanTarget, + locationData.country, + locationData.country_name, + locationData.region, + locationData.city, + locationData.latitude, + locationData.longitude + ]); + } + return locationData; } } catch (err) { diff --git a/server/index.js b/server/index.js index 5624b65..33675c8 100644 --- a/server/index.js +++ b/server/index.js @@ -29,6 +29,7 @@ const PASSWORD_ITERATIONS = parseInt(process.env.PASSWORD_ITERATIONS, 10) || 210 const ALLOW_REMOTE_SETUP = process.env.ALLOW_REMOTE_SETUP === 'true'; const COOKIE_SECURE = process.env.COOKIE_SECURE === 'true'; const APP_SECRET = process.env.APP_SECRET || crypto.randomBytes(32).toString('hex'); +process.env.APP_SECRET = APP_SECRET; const RATE_LIMITS = { login: { windowMs: 15 * 60 * 1000, max: 8 }, setup: { windowMs: 10 * 60 * 1000, max: 20 } @@ -1063,34 +1064,61 @@ async function getOverview(force = false) { const validMetrics = allMetrics.filter(m => m !== null); - // Aggregate across all sources - let totalServers = 0; + // Use Maps to deduplicate servers across multiple Prometheus sources + const uniqueOverviewServers = new Map(); + const uniqueDetailServers = new Map(); + + for (const m of validMetrics) { + if (m.isOverview) { + for (const s of m.servers) { + // originalInstance is the true IP/host before token masking + const key = `${s.originalInstance}::${s.job}`; + if (!uniqueOverviewServers.has(key)) { + uniqueOverviewServers.set(key, s); + } else if (s.up && !uniqueOverviewServers.get(key).up) { + // Prefer 'up' status if duplicate + uniqueOverviewServers.set(key, s); + } + } + } + + if (m.isDetail) { + for (const s of m.servers) { + const key = `${s.originalInstance}::${s.job}`; + if (!uniqueDetailServers.has(key)) { + uniqueDetailServers.set(key, s); + } else if (s.up && !uniqueDetailServers.get(key).up) { + uniqueDetailServers.set(key, s); + } + } + } + } + + const allOverviewServers = Array.from(uniqueOverviewServers.values()); + const allDetailServers = Array.from(uniqueDetailServers.values()); + + // Aggregate across unique deduplicated servers + let totalServers = allOverviewServers.length; let activeServers = 0; let cpuUsed = 0, cpuTotal = 0; let memUsed = 0, memTotal = 0; let diskUsed = 0, diskTotal = 0; let netRx = 0, netTx = 0; let traffic24hRx = 0, traffic24hTx = 0; - let allServers = []; - for (const m of validMetrics) { - if (m.isOverview) { - totalServers += m.totalServers; - activeServers += (m.activeServers !== undefined ? m.activeServers : m.totalServers); - cpuUsed += m.cpu.used; - cpuTotal += m.cpu.total; - memUsed += m.memory.used; - memTotal += m.memory.total; - diskUsed += m.disk.used; - diskTotal += m.disk.total; - netRx += m.network.rx; - netTx += m.network.tx; - traffic24hRx += m.traffic24h.rx; - traffic24hTx += m.traffic24h.tx; - } - - if (m.isDetail) { - allServers = allServers.concat(m.servers); + for (const inst of allOverviewServers) { + if (inst.up) { + activeServers++; + cpuUsed += (inst.cpuPercent / 100) * inst.cpuCores; + cpuTotal += inst.cpuCores; + memUsed += inst.memUsed; + memTotal += inst.memTotal; + diskUsed += inst.diskUsed; + diskTotal += inst.diskTotal; + netRx += inst.netRx || 0; + netTx += inst.netTx || 0; + traffic24hRx += inst.traffic24hRx || 0; + traffic24hTx += inst.traffic24hTx || 0; } } @@ -1122,12 +1150,12 @@ async function getOverview(force = false) { tx: traffic24hTx, total: traffic24hRx + traffic24hTx }, - servers: allServers + servers: allDetailServers }; // --- Add Geo Information to Servers --- const geoServers = await Promise.all(overview.servers.map(async (server) => { - const realInstance = server.originalInstance || prometheusService.resolveToken(server.instance); + const realInstance = server.originalInstance || await prometheusService.resolveToken(server.instance); // Helper to get host from instance (handles IPv6 with brackets, IPv4:port, etc.) let cleanIp = realInstance; if (cleanIp.startsWith('[')) { diff --git a/server/prometheus-service.js b/server/prometheus-service.js index 91d7141..e324a65 100644 --- a/server/prometheus-service.js +++ b/server/prometheus-service.js @@ -1,6 +1,7 @@ const axios = require('axios'); const http = require('http'); const https = require('https'); +const cache = require('./cache'); // <-- ADD const QUERY_TIMEOUT = 10000; @@ -10,7 +11,11 @@ const httpAgent = new http.Agent({ keepAlive: true }); const httpsAgent = new https.Agent({ keepAlive: true }); const serverIdMap = new Map(); // token -> { instance, job, source, lastSeen } -const SECRET = process.env.APP_SECRET || crypto.randomBytes(32).toString('hex'); + +function getSecret() { + // Use the env variable populated by index.js initialization + return process.env.APP_SECRET || 'fallback-secret-for-safety'; +} // Periodic cleanup of serverIdMap to prevent infinite growth setInterval(() => { @@ -24,7 +29,7 @@ setInterval(() => { }, 3600000); // Once per hour function getServerToken(instance, job, source) { - const hash = crypto.createHmac('sha256', SECRET) + const hash = crypto.createHmac('sha256', getSecret()) .update(`${instance}:${job}:${source}`) .digest('hex') .substring(0, 16); @@ -245,6 +250,9 @@ async function getOverviewMetrics(url, sourceName) { // Store mapping for detail queries serverIdMap.set(token, { instance: originalInstance, source: sourceName, job, lastSeen: Date.now() }); + + // Also store in Valkey for resilience across restarts + cache.set(`server_token:${token}`, originalInstance, 86400).catch(()=>{}); if (!instances.has(token)) { instances.set(token, { @@ -559,10 +567,13 @@ function mergeCpuHistories(histories) { } -function resolveToken(token) { +async function resolveToken(token) { if (serverIdMap.has(token)) { return serverIdMap.get(token).instance; } + const cachedInstance = await cache.get(`server_token:${token}`); + if (cachedInstance) return cachedInstance; + return token; } @@ -571,7 +582,7 @@ function resolveToken(token) { */ async function getServerDetails(baseUrl, instance, job, settings = {}) { const url = normalizeUrl(baseUrl); - const node = resolveToken(instance); + const node = await resolveToken(instance); // Queries based on the requested dashboard structure const queries = { @@ -735,7 +746,7 @@ async function getServerDetails(baseUrl, instance, job, settings = {}) { */ async function getServerHistory(baseUrl, instance, job, metric, range = '1h', start = null, end = null, p95Type = 'tx') { const url = normalizeUrl(baseUrl); - const node = resolveToken(instance); + const node = await resolveToken(instance); // CPU Busy history: 100 - idle if (metric === 'cpuBusy') {