diff --git a/public/js/app.js b/public/js/app.js
index a4b4111..ef57932 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -164,6 +164,8 @@
let myMap2D = null;
let editingRouteId = null;
+ let allStoredSources = [];
+ let allStoredLatencyRoutes = [];
async function fetchJsonWithFallback(urls) {
let lastError = null;
@@ -330,6 +332,31 @@
dom.btnChangePassword.addEventListener('click', saveChangePassword);
}
+ // Settings management event delegation
+ if (dom.sourceItems) {
+ dom.sourceItems.addEventListener('click', (e) => {
+ const btnEdit = e.target.closest('.btn-edit-source');
+ const btnDelete = e.target.closest('.btn-delete-source');
+ if (btnEdit) {
+ window.editSource(parseInt(btnEdit.getAttribute('data-id'), 10));
+ } else if (btnDelete) {
+ window.deleteSource(parseInt(btnDelete.getAttribute('data-id'), 10));
+ }
+ });
+ }
+
+ if (dom.latencyRoutesList) {
+ dom.latencyRoutesList.addEventListener('click', (e) => {
+ const btnEdit = e.target.closest('.btn-edit-route');
+ const btnDelete = e.target.closest('.btn-delete-route');
+ if (btnEdit) {
+ window.editRoute(parseInt(btnEdit.getAttribute('data-id'), 10));
+ } else if (btnDelete) {
+ window.deleteLatencyRoute(parseInt(btnDelete.getAttribute('data-id'), 10));
+ }
+ });
+ }
+
// Globe expansion (FLIP animation via Web Animations API)
let savedGlobeRect = null;
let globeAnimating = false;
@@ -1811,8 +1838,8 @@
card.className = 'detail-metric-card';
card.style.flex = '1 1 calc(50% - 10px)';
card.innerHTML = `
- ${item.name}
- ${item.value || '-'}
+ ${escapeHtml(item.name)}
+ ${escapeHtml(item.value || '-')}
`;
customDataContainer.appendChild(card);
});
@@ -1830,6 +1857,19 @@
diskMetricItem.after(dom.detailPartitionsContainer);
}
+ dom.detailPartitionsList.innerHTML = data.partitions.map(p => `
+
+
+ ${escapeHtml(p.mountpoint || p.device)}
+ ${escapeHtml(formatBytes(p.used))} / ${escapeHtml(formatBytes(p.total))}
+
+
+
${p.percent.toFixed(1)}%
+
+ `).join('');
+
dom.partitionHeader.onclick = (e) => {
e.stopPropagation();
dom.detailPartitionsContainer.classList.toggle('active');
@@ -2309,6 +2349,7 @@
return;
}
const routes = await response.json();
+ allStoredLatencyRoutes = routes;
renderLatencyRoutes(routes);
} catch (err) {
console.error('Error loading latency routes:', err);
@@ -2322,7 +2363,7 @@
}
dom.latencyRoutesList.innerHTML = routes.map(route => `
-
+
${escapeHtml(route.latency_source)} ↔ ${escapeHtml(route.latency_dest)}
@@ -2333,8 +2374,8 @@
-
-
+
+
`).join('');
@@ -2555,6 +2596,7 @@
}
const sources = await response.json();
const sourcesArray = Array.isArray(sources) ? sources : [];
+ allStoredSources = sourcesArray;
const promSources = sourcesArray.filter(s => s.type !== 'blackbox');
if (dom.totalServersLabel) dom.totalServersLabel.textContent = `服务器总数 (${promSources.length} 数据源)`;
updateSourceFilterOptions(sourcesArray);
@@ -2587,8 +2629,8 @@
${source.description ? `
${escapeHtml(source.description)}
` : ''}
-
-
+
+
`).join('');
diff --git a/server/index.js b/server/index.js
index 3068e24..bb161bc 100644
--- a/server/index.js
+++ b/server/index.js
@@ -28,6 +28,7 @@ const SESSION_TTL_SECONDS = parseInt(process.env.SESSION_TTL_SECONDS, 10) || 864
const PASSWORD_ITERATIONS = parseInt(process.env.PASSWORD_ITERATIONS, 10) || 210000;
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');
const RATE_LIMITS = {
login: { windowMs: 15 * 60 * 1000, max: 8 },
setup: { windowMs: 10 * 60 * 1000, max: 20 }
@@ -619,6 +620,7 @@ COOKIE_SECURE=${process.env.COOKIE_SECURE || 'false'}
SESSION_TTL_SECONDS=${process.env.SESSION_TTL_SECONDS || SESSION_TTL_SECONDS}
PASSWORD_ITERATIONS=${process.env.PASSWORD_ITERATIONS || PASSWORD_ITERATIONS}
ENABLE_EXTERNAL_GEO_LOOKUP=${process.env.ENABLE_EXTERNAL_GEO_LOOKUP || 'false'}
+APP_SECRET=${process.env.APP_SECRET || APP_SECRET}
`;
fs.writeFileSync(path.join(__dirname, '..', '.env'), envContent);
@@ -1384,6 +1386,8 @@ app.get('/api/metrics/latency', async (req, res) => {
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
let isBroadcastRunning = false;
+let cachedLatencyRoutes = null;
+let lastRoutesUpdate = 0;
function broadcast(data) {
const message = JSON.stringify(data);
@@ -1394,21 +1398,25 @@ function broadcast(data) {
});
}
-// Broadcast loop
async function broadcastMetrics() {
if (isBroadcastRunning) return;
isBroadcastRunning = true;
try {
const overview = await getOverview();
- // Also include latencies in the broadcast to make map lines real-time
- const [routes] = await db.query(`
- SELECT r.*, s.url, s.type as source_type
- FROM latency_routes r
- JOIN prometheus_sources s ON r.source_id = s.id
- `);
+ // Refresh routes list every 60 seconds or if it hasn't been fetched yet
+ const now = Date.now();
+ if (!cachedLatencyRoutes || now - lastRoutesUpdate > 60000) {
+ const [routes] = await db.query(`
+ SELECT r.*, s.url, s.type as source_type
+ FROM latency_routes r
+ JOIN prometheus_sources s ON r.source_id = s.id
+ `);
+ cachedLatencyRoutes = routes;
+ lastRoutesUpdate = now;
+ }
- const latencyResults = await Promise.all(routes.map(async (route) => {
+ const latencyResults = await Promise.all(cachedLatencyRoutes.map(async (route) => {
let latency = await cache.get(`latency:route:${route.id}`);
if (latency === null && route.source_type === 'prometheus') {
latency = await prometheusService.getLatency(route.url, route.latency_target);
@@ -1478,6 +1486,16 @@ async function start() {
}
}, 3600000); // Once per hour
+ // Periodic cleanup of sessions Map to prevent memory growth
+ setInterval(() => {
+ const now = Date.now();
+ for (const [sessionId, session] of sessions.entries()) {
+ if (session.expiresAt && session.expiresAt <= now) {
+ sessions.delete(sessionId);
+ }
+ }
+ }, 300000); // Once every 5 minutes
+
server.listen(PORT, HOST, () => {
console.log(`\n 🚀 Data Visualization Display Wall (WebSocket Enabled)`);
console.log(` 📊 Server running at http://${HOST === '0.0.0.0' ? 'localhost' : HOST}:${PORT}`);
diff --git a/server/prometheus-service.js b/server/prometheus-service.js
index 0d263fd..6c1b117 100644
--- a/server/prometheus-service.js
+++ b/server/prometheus-service.js
@@ -876,10 +876,8 @@ module.exports = {
getLatency: async (blackboxUrl, target) => {
if (!blackboxUrl || !target) return null;
try {
- const normalized = blackboxUrl.trim().replace(/\/+$/, '');
+ const normalized = normalizeUrl(blackboxUrl);
- // Construct a single optimized query searching for priority metrics and common labels
- // Prioritize probe_icmp_duration_seconds OVER probe_duration_seconds
const queryExpr = `(
probe_icmp_duration_seconds{phase="rtt", instance="${target}"} or
probe_icmp_duration_seconds{phase="rtt", target="${target}"} or
@@ -891,14 +889,9 @@ module.exports = {
probe_duration_seconds{target="${target}"}
)`;
- const params = new URLSearchParams({ query: queryExpr });
- const res = await fetch(`${normalized}/api/v1/query?${params.toString()}`);
-
- if (res.ok) {
- const data = await res.json();
- if (data.status === 'success' && data.data.result.length > 0) {
- return parseFloat(data.data.result[0].value[1]) * 1000;
- }
+ const result = await query(normalized, queryExpr);
+ if (result && result.length > 0) {
+ return parseFloat(result[0].value[1]) * 1000;
}
return null;
} catch (err) {