From e60fa2b982dc158daab148130d5a29631b4b129e Mon Sep 17 00:00:00 2001 From: CN-JS-HuiBai Date: Sun, 12 Apr 2026 17:48:43 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/css/style.css | 98 ++++++++++++++++++++++++++++++++++++ public/index.html | 43 +++++++--------- public/js/app.js | 51 ++++--------------- server/index.js | 34 +++---------- server/prometheus-service.js | 4 +- 5 files changed, 136 insertions(+), 94 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index a015eb3..39ac413 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -2803,3 +2803,101 @@ input:checked+.slider:before { justify-content: center; } } + +/* ---- Source Settings Toggles ---- */ +.source-options-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 16px; + background: rgba(255, 255, 255, 0.03); + padding: 16px; + border-radius: var(--radius-sm); + border: 1px solid var(--border-color); + margin-top: 8px; +} + +.source-option-item { + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + user-select: none; + transition: all 0.2s ease; + padding: 4px 8px; + border-radius: var(--radius-sm); +} + +.source-option-item:hover { + background: rgba(255, 255, 255, 0.03); +} + +.source-option-item:hover .source-option-label { + color: var(--text-primary); +} + +.source-option-label { + font-size: 0.88rem; + color: var(--text-secondary); + font-weight: 500; +} + +.switch-wrapper { + position: relative; + width: 38px; + height: 20px; + flex-shrink: 0; +} + +.switch-input { + opacity: 0; + width: 0; + height: 0; + position: absolute; +} + +.switch-label { + position: absolute; + cursor: pointer; + inset: 0; + background-color: var(--bg-input); + transition: .35s cubic-bezier(0.4, 0, 0.2, 1); + border: 1px solid var(--border-color); + border-radius: 34px; +} + +.switch-label:before { + position: absolute; + content: ''; + height: 14px; + width: 14px; + left: 2px; + bottom: 2px; + background-color: var(--text-muted); + transition: .35s cubic-bezier(0.4, 0, 0.2, 1); + border-radius: 50%; +} + +.switch-input:checked + .switch-label { + background-color: rgba(99, 102, 241, 0.15); + border-color: var(--accent-indigo); +} + +.switch-input:checked + .switch-label:before { + transform: translateX(18px); + background-color: var(--accent-indigo); + box-shadow: 0 0 10px rgba(99, 102, 241, 0.3); +} + +/* Light theme support */ +:root.light-theme .source-options-grid { + background: rgba(0, 0, 0, 0.02); +} + +:root.light-theme .source-option-item:hover { + background: rgba(0, 0, 0, 0.03); +} + +:root.light-theme .switch-label:before { + background-color: #94a3b8; +} + diff --git a/public/index.html b/public/index.html index c376a32..757060a 100644 --- a/public/index.html +++ b/public/index.html @@ -476,23 +476,25 @@ -
- - - +
+ +
+ + + +
@@ -582,13 +584,6 @@
-
- -
-
加载数据源中...
-
- 选择参与 24 小时网络流量统计的 Prometheus 数据源。如果不勾选任何项,则统计所有数据源。 -
diff --git a/public/js/app.js b/public/js/app.js index ee1853f..90162b5 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -103,7 +103,6 @@ oldPasswordInput: document.getElementById('oldPassword'), newPasswordInput: document.getElementById('newPassword'), confirmNewPasswordInput: document.getElementById('confirmNewPassword'), - networkSourceSelector: document.getElementById('network-source-selector'), btnChangePassword: document.getElementById('btnChangePassword'), changePasswordMessage: document.getElementById('changePasswordMessage'), globeContainer: document.getElementById('globeContainer'), @@ -276,7 +275,7 @@ dom.serverSourceOption.style.display = 'none'; dom.isServerSource.checked = false; } else { - dom.serverSourceOption.style.display = 'flex'; + dom.serverSourceOption.style.display = ''; dom.isServerSource.checked = true; } }); @@ -618,6 +617,7 @@ checkAuthStatus(); // Start data fetching + loadSources(); fetchMetrics(); fetchNetworkHistory(); fetchLatency(); @@ -2104,21 +2104,6 @@ if (dom.showPageNameInput) dom.showPageNameInput.value = settings.show_page_name !== undefined ? settings.show_page_name.toString() : "1"; if (dom.requireLoginForServerDetailsInput) dom.requireLoginForServerDetailsInput.value = settings.require_login_for_server_details ? "1" : "0"; - // Handle network data sources checkboxes - if (settings.network_data_sources) { - const selected = settings.network_data_sources.split(',').map(s => s.trim()); - const checkboxes = dom.networkSourceSelector.querySelectorAll('input[type="checkbox"]'); - checkboxes.forEach(cb => { - cb.checked = selected.includes(cb.value); - }); - // We'll also store this in a temporary place because loadSources might run later - dom.networkSourceSelector.dataset.pendingSelected = settings.network_data_sources; - } else { - const checkboxes = dom.networkSourceSelector.querySelectorAll('input[type="checkbox"]'); - checkboxes.forEach(cb => cb.checked = false); - dom.networkSourceSelector.dataset.pendingSelected = ""; - } - // Handle Theme Priority: localStorage > Site Default const savedTheme = localStorage.getItem('theme'); const themeToApply = savedTheme || settings.default_theme || 'dark'; @@ -2278,7 +2263,6 @@ p95_type: dom.p95TypeSelect ? dom.p95TypeSelect.value : 'tx', ps_filing: dom.psFilingInput ? dom.psFilingInput.value.trim() : '', icp_filing: dom.icpFilingInput ? dom.icpFilingInput.value.trim() : '', - network_data_sources: Array.from(dom.networkSourceSelector.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.value).join(','), show_server_ip: dom.showServerIpInput ? (dom.showServerIpInput.value === "1") : false, ip_metric_name: dom.ipMetricNameInput ? dom.ipMetricNameInput.value.trim() : null, ip_label_name: dom.ipLabelNameInput ? dom.ipLabelNameInput.value.trim() : 'address', @@ -2596,8 +2580,14 @@ async function loadSources() { try { + if (dom.sourceItems) { + dom.sourceItems.innerHTML = '
正在加载数据源...
'; + } const response = await fetch('/api/sources'); if (response.status === 401) { + if (dom.sourceItems) { + dom.sourceItems.innerHTML = '
请登录后管理数据源
'; + } promptLogin('登录后可查看和管理数据源'); return; } @@ -2608,7 +2598,6 @@ if (dom.totalServersLabel) dom.totalServersLabel.textContent = `服务器总数 (${promSources.length} 数据源)`; updateSourceFilterOptions(sourcesArray); renderSources(sourcesArray); - renderNetworkSourceSelector(sourcesArray); } catch (err) { console.error('Error loading sources:', err); } @@ -2645,26 +2634,6 @@ `).join(''); } - function renderNetworkSourceSelector(sources) { - if (!dom.networkSourceSelector) return; - - // Only show Prometheus sources for filtering - const promSources = sources.filter(s => s.type !== 'blackbox'); - - if (promSources.length === 0) { - dom.networkSourceSelector.innerHTML = '
暂无可用数据源
'; - return; - } - - const pendingSelected = dom.networkSourceSelector.dataset.pendingSelected ? dom.networkSourceSelector.dataset.pendingSelected.split(',').map(s => s.trim()) : []; - - dom.networkSourceSelector.innerHTML = promSources.map(source => ` - - `).join(''); - } // ---- Test Connection ---- async function testConnection() { @@ -2726,7 +2695,7 @@ if (source.type === 'blackbox') { dom.serverSourceOption.style.display = 'none'; } else { - dom.serverSourceOption.style.display = 'flex'; + dom.serverSourceOption.style.display = ''; } dom.btnAdd.textContent = '保存修改'; @@ -2754,7 +2723,7 @@ dom.sourceDesc.value = ''; if (dom.isOverviewSource) dom.isOverviewSource.checked = true; if (dom.isDetailSource) dom.isDetailSource.checked = true; - dom.serverSourceOption.style.display = 'flex'; + dom.serverSourceOption.style.display = ''; dom.btnAdd.textContent = '添加'; const cancelBtn = document.getElementById('btnCancelEditSource'); diff --git a/server/index.js b/server/index.js index d1de74c..acef9c3 100644 --- a/server/index.js +++ b/server/index.js @@ -797,7 +797,8 @@ app.get('/api/sources', requireAuth, async (req, res) => { const res = await fetch(`${source.url.replace(/\/+$/, '')}/metrics`, { timeout: 3000 }).catch(() => null); response = (res && res.ok) ? 'Blackbox Exporter Ready' : 'Connection Error'; } else { - response = await prometheusService.testConnection(source.url); + // Use a shorter timeout for list view to prevent blocking UI + response = await prometheusService.testConnection(source.url, 2500); } return { ...source, status: 'online', version: response }; } catch (e) { @@ -954,7 +955,6 @@ app.post('/api/settings', requireAuth, async (req, res) => { latency_target: current.latency_target || null, icp_filing: icp_filing !== undefined ? icp_filing : (current.icp_filing || null), ps_filing: ps_filing !== undefined ? ps_filing : (current.ps_filing || null), - network_data_sources: network_data_sources !== undefined ? network_data_sources : (current.network_data_sources || null), show_server_ip: show_server_ip !== undefined ? (show_server_ip ? 1 : 0) : (current.show_server_ip || 0), ip_metric_name: ip_metric_name !== undefined ? ip_metric_name : (current.ip_metric_name || null), ip_label_name: ip_label_name !== undefined ? ip_label_name : (current.ip_label_name || 'address'), @@ -995,7 +995,7 @@ app.post('/api/settings', requireAuth, async (req, res) => { settings.page_name, settings.show_page_name, settings.title, settings.logo_url, settings.logo_url_dark, settings.favicon_url, settings.default_theme, settings.show_95_bandwidth, settings.p95_type, settings.require_login_for_server_details, settings.blackbox_source_id, settings.latency_source, settings.latency_dest, settings.latency_target, - settings.icp_filing, settings.ps_filing, settings.network_data_sources, settings.show_server_ip, + settings.icp_filing, settings.ps_filing, settings.show_server_ip, settings.ip_metric_name, settings.ip_label_name, settings.custom_metrics ] ); @@ -1011,7 +1011,9 @@ app.post('/api/settings', requireAuth, async (req, res) => { // Reusable function to get overview metrics async function getOverview(force = false) { + // Fetch sources: overview OR detail const [sources] = await db.query('SELECT * FROM prometheus_sources WHERE (is_overview_source = 1 OR is_detail_source = 1) AND type != "blackbox"'); + if (sources.length === 0) { return { totalServers: 0, @@ -1024,11 +1026,6 @@ async function getOverview(force = false) { servers: [] }; } - - const [settingsRows] = await db.query('SELECT network_data_sources FROM site_settings WHERE id = 1'); - const selectedSourcesStr = settingsRows.length > 0 ? settingsRows[0].network_data_sources : null; - const selectedSourceNames = selectedSourcesStr ? selectedSourcesStr.split(',').map(s => s.trim()).filter(s => s) : []; - const allMetrics = await Promise.all(sources.map(async (source) => { const cacheKey = `source_metrics:${source.url}:${source.name}`; if (force) { @@ -1077,10 +1074,6 @@ async function getOverview(force = false) { memTotal += m.memory.total; diskUsed += m.disk.used; diskTotal += m.disk.total; - } - - // Aggregates ONLY for selected network sources - if (selectedSourceNames.length === 0 || selectedSourceNames.includes(m.sourceName)) { netRx += m.network.rx; netTx += m.network.tx; traffic24hRx += m.traffic24h.rx; @@ -1195,21 +1188,8 @@ app.get('/api/metrics/network-history', async (req, res) => { if (cached) return res.json(cached); } - const [settingsRows] = await db.query('SELECT network_data_sources FROM site_settings WHERE id = 1'); - const selectedSourcesStr = settingsRows.length > 0 ? settingsRows[0].network_data_sources : null; - - let query = 'SELECT * FROM prometheus_sources WHERE is_overview_source = 1 AND type != "blackbox"'; - let params = []; - - if (selectedSourcesStr) { - const selectedSourceNames = selectedSourcesStr.split(',').map(s => s.trim()).filter(s => s); - if (selectedSourceNames.length > 0) { - query += ' AND name IN (?)'; - params.push(selectedSourceNames); - } - } - - const [sources] = await db.query(query, params); + const query = 'SELECT * FROM prometheus_sources WHERE is_overview_source = 1 AND type != "blackbox"'; + const [sources] = await db.query(query); if (sources.length === 0) { return res.json({ timestamps: [], rx: [], tx: [] }); } diff --git a/server/prometheus-service.js b/server/prometheus-service.js index 6c1b117..f8e25b0 100644 --- a/server/prometheus-service.js +++ b/server/prometheus-service.js @@ -64,12 +64,12 @@ function createClient(baseUrl) { /** * Test Prometheus connection */ -async function testConnection(url) { +async function testConnection(url, customTimeout = null) { const normalized = normalizeUrl(url); try { // Using native fetch to avoid follow-redirects/axios "protocol mismatch" issues in some Node environments const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), QUERY_TIMEOUT); + const timer = setTimeout(() => controller.abort(), customTimeout || QUERY_TIMEOUT); // Node native fetch - handles http/https automatically const res = await fetch(`${normalized}/api/v1/status/buildinfo`, {