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 @@
-
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`, {