优化数据库稳定性
This commit is contained in:
@@ -164,6 +164,8 @@
|
|||||||
|
|
||||||
let myMap2D = null;
|
let myMap2D = null;
|
||||||
let editingRouteId = null;
|
let editingRouteId = null;
|
||||||
|
let allStoredSources = [];
|
||||||
|
let allStoredLatencyRoutes = [];
|
||||||
|
|
||||||
async function fetchJsonWithFallback(urls) {
|
async function fetchJsonWithFallback(urls) {
|
||||||
let lastError = null;
|
let lastError = null;
|
||||||
@@ -330,6 +332,31 @@
|
|||||||
dom.btnChangePassword.addEventListener('click', saveChangePassword);
|
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)
|
// Globe expansion (FLIP animation via Web Animations API)
|
||||||
let savedGlobeRect = null;
|
let savedGlobeRect = null;
|
||||||
let globeAnimating = false;
|
let globeAnimating = false;
|
||||||
@@ -1811,8 +1838,8 @@
|
|||||||
card.className = 'detail-metric-card';
|
card.className = 'detail-metric-card';
|
||||||
card.style.flex = '1 1 calc(50% - 10px)';
|
card.style.flex = '1 1 calc(50% - 10px)';
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<span class="detail-metric-label">${item.name}</span>
|
<span class="detail-metric-label">${escapeHtml(item.name)}</span>
|
||||||
<span class="detail-metric-value">${item.value || '-'}</span>
|
<span class="detail-metric-value">${escapeHtml(item.value || '-')}</span>
|
||||||
`;
|
`;
|
||||||
customDataContainer.appendChild(card);
|
customDataContainer.appendChild(card);
|
||||||
});
|
});
|
||||||
@@ -1830,6 +1857,19 @@
|
|||||||
diskMetricItem.after(dom.detailPartitionsContainer);
|
diskMetricItem.after(dom.detailPartitionsContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dom.detailPartitionsList.innerHTML = data.partitions.map(p => `
|
||||||
|
<div class="partition-row">
|
||||||
|
<div class="partition-info">
|
||||||
|
<span class="partition-name">${escapeHtml(p.mountpoint || p.device)}</span>
|
||||||
|
<span class="partition-usage-text">${escapeHtml(formatBytes(p.used))} / ${escapeHtml(formatBytes(p.total))}</span>
|
||||||
|
</div>
|
||||||
|
<div class="partition-bar-bg">
|
||||||
|
<div class="partition-bar-fill ${getUsageClass(p.percent)}" style="width: ${p.percent}%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="partition-percent">${p.percent.toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
dom.partitionHeader.onclick = (e) => {
|
dom.partitionHeader.onclick = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
dom.detailPartitionsContainer.classList.toggle('active');
|
dom.detailPartitionsContainer.classList.toggle('active');
|
||||||
@@ -2309,6 +2349,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const routes = await response.json();
|
const routes = await response.json();
|
||||||
|
allStoredLatencyRoutes = routes;
|
||||||
renderLatencyRoutes(routes);
|
renderLatencyRoutes(routes);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading latency routes:', err);
|
console.error('Error loading latency routes:', err);
|
||||||
@@ -2322,7 +2363,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
dom.latencyRoutesList.innerHTML = routes.map(route => `
|
dom.latencyRoutesList.innerHTML = routes.map(route => `
|
||||||
<div class="latency-route-item" style="display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: rgba(255,255,255,0.03); border: 1px solid var(--border-color); border-radius: 8px;">
|
<div class="latency-route-item" data-id="${route.id}" style="display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: rgba(255,255,255,0.03); border: 1px solid var(--border-color); border-radius: 8px;">
|
||||||
<div class="route-info" style="display: flex; flex-direction: column; gap: 4px;">
|
<div class="route-info" style="display: flex; flex-direction: column; gap: 4px;">
|
||||||
<div style="font-weight: 600; font-size: 0.88rem; color: var(--text-primary);">
|
<div style="font-weight: 600; font-size: 0.88rem; color: var(--text-primary);">
|
||||||
${escapeHtml(route.latency_source)} ↔ ${escapeHtml(route.latency_dest)}
|
${escapeHtml(route.latency_source)} ↔ ${escapeHtml(route.latency_dest)}
|
||||||
@@ -2333,8 +2374,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="route-actions" style="display: flex; gap: 8px;">
|
<div class="route-actions" style="display: flex; gap: 8px;">
|
||||||
<button class="btn btn-test" onclick="editRoute(${route.id}, ${route.source_id}, '${escapeHtml(route.latency_source)}', '${escapeHtml(route.latency_dest)}', '${escapeHtml(route.latency_target)}')" style="padding: 4px 10px; font-size: 0.72rem;">编辑</button>
|
<button class="btn btn-test btn-edit-route" data-id="${route.id}" style="padding: 4px 10px; font-size: 0.72rem;">编辑</button>
|
||||||
<button class="btn btn-delete" onclick="deleteLatencyRoute(${route.id})" style="padding: 4px 10px; font-size: 0.72rem;">删除</button>
|
<button class="btn btn-delete btn-delete-route" data-id="${route.id}" style="padding: 4px 10px; font-size: 0.72rem;">删除</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
@@ -2555,6 +2596,7 @@
|
|||||||
}
|
}
|
||||||
const sources = await response.json();
|
const sources = await response.json();
|
||||||
const sourcesArray = Array.isArray(sources) ? sources : [];
|
const sourcesArray = Array.isArray(sources) ? sources : [];
|
||||||
|
allStoredSources = sourcesArray;
|
||||||
const promSources = sourcesArray.filter(s => s.type !== 'blackbox');
|
const promSources = sourcesArray.filter(s => s.type !== 'blackbox');
|
||||||
if (dom.totalServersLabel) dom.totalServersLabel.textContent = `服务器总数 (${promSources.length} 数据源)`;
|
if (dom.totalServersLabel) dom.totalServersLabel.textContent = `服务器总数 (${promSources.length} 数据源)`;
|
||||||
updateSourceFilterOptions(sourcesArray);
|
updateSourceFilterOptions(sourcesArray);
|
||||||
@@ -2587,8 +2629,8 @@
|
|||||||
${source.description ? `<div class="source-item-desc">${escapeHtml(source.description)}</div>` : ''}
|
${source.description ? `<div class="source-item-desc">${escapeHtml(source.description)}</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="source-item-actions">
|
<div class="source-item-actions">
|
||||||
<button class="btn btn-secondary btn-sm" onclick="editSource(${JSON.stringify(source).replace(/"/g, '"')})">编辑</button>
|
<button class="btn btn-secondary btn-sm btn-edit-source" data-id="${source.id}">编辑</button>
|
||||||
<button class="btn btn-delete btn-sm" onclick="deleteSource(${source.id})">删除</button>
|
<button class="btn btn-delete btn-sm btn-delete-source" data-id="${source.id}">删除</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
|
|||||||
@@ -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 PASSWORD_ITERATIONS = parseInt(process.env.PASSWORD_ITERATIONS, 10) || 210000;
|
||||||
const ALLOW_REMOTE_SETUP = process.env.ALLOW_REMOTE_SETUP === 'true';
|
const ALLOW_REMOTE_SETUP = process.env.ALLOW_REMOTE_SETUP === 'true';
|
||||||
const COOKIE_SECURE = process.env.COOKIE_SECURE === 'true';
|
const COOKIE_SECURE = process.env.COOKIE_SECURE === 'true';
|
||||||
|
const APP_SECRET = process.env.APP_SECRET || crypto.randomBytes(32).toString('hex');
|
||||||
const RATE_LIMITS = {
|
const RATE_LIMITS = {
|
||||||
login: { windowMs: 15 * 60 * 1000, max: 8 },
|
login: { windowMs: 15 * 60 * 1000, max: 8 },
|
||||||
setup: { windowMs: 10 * 60 * 1000, max: 20 }
|
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}
|
SESSION_TTL_SECONDS=${process.env.SESSION_TTL_SECONDS || SESSION_TTL_SECONDS}
|
||||||
PASSWORD_ITERATIONS=${process.env.PASSWORD_ITERATIONS || PASSWORD_ITERATIONS}
|
PASSWORD_ITERATIONS=${process.env.PASSWORD_ITERATIONS || PASSWORD_ITERATIONS}
|
||||||
ENABLE_EXTERNAL_GEO_LOOKUP=${process.env.ENABLE_EXTERNAL_GEO_LOOKUP || 'false'}
|
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);
|
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 server = http.createServer(app);
|
||||||
const wss = new WebSocket.Server({ server });
|
const wss = new WebSocket.Server({ server });
|
||||||
let isBroadcastRunning = false;
|
let isBroadcastRunning = false;
|
||||||
|
let cachedLatencyRoutes = null;
|
||||||
|
let lastRoutesUpdate = 0;
|
||||||
|
|
||||||
function broadcast(data) {
|
function broadcast(data) {
|
||||||
const message = JSON.stringify(data);
|
const message = JSON.stringify(data);
|
||||||
@@ -1394,21 +1398,25 @@ function broadcast(data) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Broadcast loop
|
|
||||||
async function broadcastMetrics() {
|
async function broadcastMetrics() {
|
||||||
if (isBroadcastRunning) return;
|
if (isBroadcastRunning) return;
|
||||||
isBroadcastRunning = true;
|
isBroadcastRunning = true;
|
||||||
try {
|
try {
|
||||||
const overview = await getOverview();
|
const overview = await getOverview();
|
||||||
|
|
||||||
// Also include latencies in the broadcast to make map lines real-time
|
// 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(`
|
const [routes] = await db.query(`
|
||||||
SELECT r.*, s.url, s.type as source_type
|
SELECT r.*, s.url, s.type as source_type
|
||||||
FROM latency_routes r
|
FROM latency_routes r
|
||||||
JOIN prometheus_sources s ON r.source_id = s.id
|
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}`);
|
let latency = await cache.get(`latency:route:${route.id}`);
|
||||||
if (latency === null && route.source_type === 'prometheus') {
|
if (latency === null && route.source_type === 'prometheus') {
|
||||||
latency = await prometheusService.getLatency(route.url, route.latency_target);
|
latency = await prometheusService.getLatency(route.url, route.latency_target);
|
||||||
@@ -1478,6 +1486,16 @@ async function start() {
|
|||||||
}
|
}
|
||||||
}, 3600000); // Once per hour
|
}, 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, () => {
|
server.listen(PORT, HOST, () => {
|
||||||
console.log(`\n 🚀 Data Visualization Display Wall (WebSocket Enabled)`);
|
console.log(`\n 🚀 Data Visualization Display Wall (WebSocket Enabled)`);
|
||||||
console.log(` 📊 Server running at http://${HOST === '0.0.0.0' ? 'localhost' : HOST}:${PORT}`);
|
console.log(` 📊 Server running at http://${HOST === '0.0.0.0' ? 'localhost' : HOST}:${PORT}`);
|
||||||
|
|||||||
@@ -876,10 +876,8 @@ module.exports = {
|
|||||||
getLatency: async (blackboxUrl, target) => {
|
getLatency: async (blackboxUrl, target) => {
|
||||||
if (!blackboxUrl || !target) return null;
|
if (!blackboxUrl || !target) return null;
|
||||||
try {
|
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 = `(
|
const queryExpr = `(
|
||||||
probe_icmp_duration_seconds{phase="rtt", instance="${target}"} or
|
probe_icmp_duration_seconds{phase="rtt", instance="${target}"} or
|
||||||
probe_icmp_duration_seconds{phase="rtt", target="${target}"} or
|
probe_icmp_duration_seconds{phase="rtt", target="${target}"} or
|
||||||
@@ -891,14 +889,9 @@ module.exports = {
|
|||||||
probe_duration_seconds{target="${target}"}
|
probe_duration_seconds{target="${target}"}
|
||||||
)`;
|
)`;
|
||||||
|
|
||||||
const params = new URLSearchParams({ query: queryExpr });
|
const result = await query(normalized, queryExpr);
|
||||||
const res = await fetch(`${normalized}/api/v1/query?${params.toString()}`);
|
if (result && result.length > 0) {
|
||||||
|
return parseFloat(result[0].value[1]) * 1000;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user