Files
PromdataPanel/public/js/app.js
2026-04-04 22:05:53 +08:00

808 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Main Application - Data Visualization Display Wall
*/
(function () {
'use strict';
// ---- Config ----
const REFRESH_INTERVAL = 5000; // 5 seconds
const NETWORK_HISTORY_INTERVAL = 60000; // 1 minute
// ---- DOM Elements ----
const dom = {
clock: document.getElementById('clock'), // May be null if removed from UI
serverCountText: document.getElementById('serverCountText'),
sourceCount: document.getElementById('sourceCount'),
totalServers: document.getElementById('totalServers'),
cpuPercent: document.getElementById('cpuPercent'),
cpuDetail: document.getElementById('cpuDetail'),
memPercent: document.getElementById('memPercent'),
memDetail: document.getElementById('memDetail'),
diskPercent: document.getElementById('diskPercent'),
diskDetail: document.getElementById('diskDetail'),
totalBandwidth: document.getElementById('totalBandwidth'),
bandwidthDetail: document.getElementById('bandwidthDetail'),
traffic24hRx: document.getElementById('traffic24hRx'),
traffic24hTx: document.getElementById('traffic24hTx'),
traffic24hTotal: document.getElementById('traffic24hTotal'),
trafficP95: document.getElementById('trafficP95'),
networkCanvas: document.getElementById('networkCanvas'),
serverTableBody: document.getElementById('serverTableBody'),
btnSettings: document.getElementById('btnSettings'),
settingsModal: document.getElementById('settingsModal'),
modalClose: document.getElementById('modalClose'),
sourceName: document.getElementById('sourceName'),
sourceUrl: document.getElementById('sourceUrl'),
sourceDesc: document.getElementById('sourceDesc'),
btnTest: document.getElementById('btnTest'),
btnAdd: document.getElementById('btnAdd'),
formMessage: document.getElementById('formMessage'),
sourceItems: document.getElementById('sourceItems'),
// Site Settings
modalTabs: document.querySelectorAll('.modal-tab'),
tabContents: document.querySelectorAll('.tab-content'),
pageNameInput: document.getElementById('pageNameInput'),
siteTitleInput: document.getElementById('siteTitleInput'),
logoUrlInput: document.getElementById('logoUrlInput'),
btnSaveSiteSettings: document.getElementById('btnSaveSiteSettings'),
siteSettingsMessage: document.getElementById('siteSettingsMessage'),
logoText: document.getElementById('logoText'),
logoIconContainer: document.getElementById('logoIconContainer'),
defaultThemeInput: document.getElementById('defaultThemeInput'),
// Auth & Theme elements
themeToggle: document.getElementById('themeToggle'),
sunIcon: document.querySelector('.sun-icon'),
moonIcon: document.querySelector('.moon-icon'),
userSection: document.getElementById('userSection'),
btnLogin: document.getElementById('btnLogin'),
loginModal: document.getElementById('loginModalOverlay'),
closeLoginModal: document.getElementById('closeLoginModal'),
loginForm: document.getElementById('loginForm'),
loginError: document.getElementById('loginError'),
footerTime: document.getElementById('footerTime'),
legendP95: document.getElementById('legendP95'),
// Server Details Modal
serverDetailModal: document.getElementById('serverDetailModal'),
serverDetailClose: document.getElementById('serverDetailClose'),
serverDetailTitle: document.getElementById('serverDetailTitle'),
detailGrid: document.getElementById('detailGrid'),
detailLoading: document.getElementById('detailLoading'),
detailCpuBusy: document.getElementById('detailCpuBusy'),
detailSysLoad: document.getElementById('detailSysLoad'),
detailMemUsedPct: document.getElementById('detailMemUsedPct'),
detailSwapUsedPct: document.getElementById('detailSwapUsedPct'),
detailRootFsUsedPct: document.getElementById('detailRootFsUsedPct'),
detailCpuCores: document.getElementById('detailCpuCores'),
detailMemTotal: document.getElementById('detailMemTotal'),
detailUptime: document.getElementById('detailUptime'),
detailNetRx: document.getElementById('detailNetRx'),
detailNetTx: document.getElementById('detailNetTx')
};
// ---- State ----
let previousMetrics = null;
let networkChart = null;
let user = null; // Currently logged in user
// ---- Initialize ----
function init() {
// Resource Gauges Time
updateGaugesTime();
setInterval(updateGaugesTime, 1000);
// Initial theme check (localStorage handled after site settings load to ensure priority)
// Network chart
networkChart = new AreaChart(dom.networkCanvas);
// Event listeners
dom.btnSettings.addEventListener('click', openSettings);
dom.modalClose.addEventListener('click', closeSettings);
dom.settingsModal.addEventListener('click', (e) => {
if (e.target === dom.settingsModal) closeSettings();
});
dom.btnTest.addEventListener('click', testConnection);
dom.btnAdd.addEventListener('click', addSource);
// Auth & Theme listeners
dom.themeToggle.addEventListener('change', toggleTheme);
dom.btnLogin.addEventListener('click', openLoginModal);
dom.closeLoginModal.addEventListener('click', closeLoginModal);
dom.loginForm.addEventListener('submit', handleLogin);
dom.loginModal.addEventListener('click', (e) => {
if (e.target === dom.loginModal) closeLoginModal();
});
// Tab switching
dom.modalTabs.forEach(tab => {
tab.addEventListener('click', () => {
const targetTab = tab.getAttribute('data-tab');
switchTab(targetTab);
});
});
// Site settings
dom.btnSaveSiteSettings.addEventListener('click', saveSiteSettings);
// Keyboard shortcut
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeSettings();
closeLoginModal();
closeServerDetail();
}
});
// Server detail modal listeners
dom.serverDetailClose.addEventListener('click', closeServerDetail);
dom.serverDetailModal.addEventListener('click', (e) => {
if (e.target === dom.serverDetailModal) closeServerDetail();
});
// Server table row click delegator
dom.serverTableBody.addEventListener('click', (e) => {
const row = e.target.closest('tr');
if (row && !row.classList.contains('empty-row')) {
const instance = row.getAttribute('data-instance');
const job = row.getAttribute('data-job');
const source = row.getAttribute('data-source');
if (instance && job && source) {
openServerDetail(instance, job, source);
}
}
});
// P95 Toggle
if (dom.legendP95) {
dom.legendP95.addEventListener('click', () => {
networkChart.showP95 = !networkChart.showP95;
dom.legendP95.classList.toggle('disabled', !networkChart.showP95);
networkChart.draw();
});
}
// Check auth status
checkAuthStatus();
// Start data fetching
fetchMetrics();
fetchNetworkHistory();
// Site settings
if (window.SITE_SETTINGS) {
applySiteSettings(window.SITE_SETTINGS);
// Actual theme class already applied in head, just update icons and inputs
const savedTheme = localStorage.getItem('theme');
const currentTheme = savedTheme || window.SITE_SETTINGS.default_theme || 'dark';
updateThemeIcons(currentTheme);
// Still populate inputs
dom.pageNameInput.value = window.SITE_SETTINGS.page_name || '';
dom.siteTitleInput.value = window.SITE_SETTINGS.title || '';
dom.logoUrlInput.value = window.SITE_SETTINGS.logo_url || '';
dom.defaultThemeInput.value = window.SITE_SETTINGS.default_theme || 'dark';
}
loadSiteSettings();
setInterval(fetchMetrics, REFRESH_INTERVAL);
setInterval(fetchNetworkHistory, NETWORK_HISTORY_INTERVAL);
}
// ---- Theme Switching ----
function toggleTheme() {
const theme = dom.themeToggle.checked ? 'light' : 'dark';
document.documentElement.classList.toggle('light-theme', theme === 'light');
localStorage.setItem('theme', theme);
updateThemeIcons(theme);
}
function applyTheme(theme) {
let actualTheme = theme;
if (theme === 'auto') {
actualTheme = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
}
const isLight = actualTheme === 'light';
dom.themeToggle.checked = isLight;
document.documentElement.classList.toggle('light-theme', isLight);
updateThemeIcons(actualTheme);
}
function updateThemeIcons(theme) {
// Icons are in the slider, we adjust their opacity to show which one is active
if (theme === 'light') {
dom.sunIcon.style.opacity = '0.3';
dom.moonIcon.style.opacity = '1';
} else {
dom.sunIcon.style.opacity = '1';
dom.moonIcon.style.opacity = '0.3';
}
}
// ---- Auth Logic ----
async function checkAuthStatus() {
try {
const resp = await fetch('/api/auth/status');
const data = await resp.json();
if (data.authenticated) {
updateUserUI(data.username);
} else {
updateUserUI(null);
}
} catch (err) {
updateUserUI(null);
}
}
function updateUserUI(username) {
if (username) {
user = username;
dom.btnSettings.style.display = 'flex';
dom.userSection.innerHTML = `
<div class="user-info">
<span class="username">${escapeHtml(username)}</span>
<button class="btn btn-logout" id="btnLogout">退出</button>
</div>
`;
document.getElementById('btnLogout').addEventListener('click', handleLogout);
} else {
user = null;
dom.btnSettings.style.display = 'none';
dom.userSection.innerHTML = `<button class="btn btn-login" id="btnLogin">登录</button>`;
document.getElementById('btnLogin').addEventListener('click', openLoginModal);
}
}
function openLoginModal() {
dom.loginModal.classList.add('active');
dom.loginError.style.display = 'none';
}
function closeLoginModal() {
dom.loginModal.classList.remove('active');
dom.loginForm.reset();
}
async function handleLogin(e) {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
try {
const resp = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await resp.json();
if (resp.ok) {
updateUserUI(data.username);
closeLoginModal();
} else {
dom.loginError.textContent = data.error || '登录失败';
dom.loginError.style.display = 'block';
}
} catch (err) {
dom.loginError.textContent = '服务器错误';
dom.loginError.style.display = 'block';
}
}
async function handleLogout() {
try {
await fetch('/api/auth/logout', { method: 'POST' });
updateUserUI(null);
} catch (err) {
console.error('Logout failed:', err);
}
}
// ---- Clock ----
function updateClock() {
if (dom.clock) {
dom.clock.textContent = formatClock();
}
}
function updateGaugesTime() {
const clockStr = formatClock();
if (dom.footerTime) {
dom.footerTime.textContent = clockStr;
}
}
// ---- Fetch Metrics ----
async function fetchMetrics() {
try {
const response = await fetch('/api/metrics/overview');
const data = await response.json();
updateDashboard(data);
} catch (err) {
console.error('Error fetching metrics:', err);
}
}
// ---- Update Dashboard ----
function updateDashboard(data) {
// Server count
dom.totalServers.textContent = data.totalServers;
dom.serverCountText.textContent = `${data.totalServers} 台服务器`;
// CPU
const cpuPct = data.cpu.percent;
dom.cpuPercent.textContent = formatPercent(cpuPct);
dom.cpuDetail.textContent = `${data.cpu.used.toFixed(1)} / ${data.cpu.total.toFixed(0)} 核心`;
// Memory
const memPct = data.memory.percent;
dom.memPercent.textContent = formatPercent(memPct);
dom.memDetail.textContent = `${formatBytes(data.memory.used)} / ${formatBytes(data.memory.total)}`;
// Disk
const diskPct = data.disk.percent;
dom.diskPercent.textContent = formatPercent(diskPct);
dom.diskDetail.textContent = `${formatBytes(data.disk.used)} / ${formatBytes(data.disk.total)}`;
// Bandwidth
dom.totalBandwidth.textContent = formatBandwidth(data.network.total || 0);
dom.bandwidthDetail.textContent = `${formatBandwidth(data.network.rx)}${formatBandwidth(data.network.tx)}`;
// 24h traffic
dom.traffic24hRx.textContent = formatBytes(data.traffic24h.rx);
dom.traffic24hTx.textContent = formatBytes(data.traffic24h.tx);
dom.traffic24hTotal.textContent = formatBytes(data.traffic24h.total || (data.traffic24h.rx + data.traffic24h.tx));
// Update server table
updateServerTable(data.servers);
// Flash animation
if (previousMetrics) {
[dom.cpuPercent, dom.memPercent, dom.diskPercent, dom.totalBandwidth].forEach(el => {
el.classList.remove('value-update');
void el.offsetWidth; // Force reflow
el.classList.add('value-update');
});
}
previousMetrics = data;
}
// ---- Server Table ----
function updateServerTable(servers) {
if (!servers || servers.length === 0) {
dom.serverTableBody.innerHTML = `
<tr class="empty-row">
<td colspan="8">暂无数据 - 请先配置 Prometheus 数据源</td>
</tr>
`;
return;
}
// Sort servers: online first, then by cpu usage
servers.sort((a, b) => {
if (a.up !== b.up) return b.up ? 1 : -1;
return b.cpuPercent - a.cpuPercent;
});
dom.serverTableBody.innerHTML = servers.map(server => {
const memPct = server.memTotal > 0 ? (server.memUsed / server.memTotal * 100) : 0;
const diskPct = server.diskTotal > 0 ? (server.diskUsed / server.diskTotal * 100) : 0;
return `
<tr data-instance="${escapeHtml(server.instance)}" data-job="${escapeHtml(server.job)}" data-source="${escapeHtml(server.source)}" style="cursor: pointer;">
<td>
<span class="status-dot ${server.up ? 'status-dot-online' : 'status-dot-offline'}"></span>
</td>
<td>
<div style="color: var(--text-primary); font-weight: 600; font-family: var(--font-sans);">${escapeHtml(server.job)}</div>
</td>
<td>${escapeHtml(server.source)}</td>
<td>
<div class="usage-bar">
<div class="usage-bar-track">
<div class="usage-bar-fill usage-bar-fill-cpu" style="width: ${Math.min(server.cpuPercent, 100)}%"></div>
</div>
<span>${formatPercent(server.cpuPercent)}</span>
</div>
</td>
<td>
<div class="usage-bar">
<div class="usage-bar-track">
<div class="usage-bar-fill usage-bar-fill-mem" style="width: ${Math.min(memPct, 100)}%"></div>
</div>
<span>${formatPercent(memPct)}</span>
</div>
</td>
<td>
<div class="usage-bar">
<div class="usage-bar-track">
<div class="usage-bar-fill usage-bar-fill-disk" style="width: ${Math.min(diskPct, 100)}%"></div>
</div>
<span>${formatPercent(diskPct)}</span>
</div>
</td>
<td>${formatBandwidth(server.netRx)}</td>
<td>${formatBandwidth(server.netTx)}</td>
</tr>
`;
}).join('');
}
// ---- Server Detail ----
async function openServerDetail(instance, job, source) {
dom.serverDetailTitle.textContent = `${job} - 服务器详情`;
dom.serverDetailModal.classList.add('active');
// Show loading
dom.detailGrid.style.opacity = '0.3';
dom.detailLoading.style.display = 'block';
try {
const url = `/api/metrics/server-details?instance=${encodeURIComponent(instance)}&job=${encodeURIComponent(job)}&source=${encodeURIComponent(source)}`;
const response = await fetch(url);
if (!response.ok) throw new Error('Fetch failed');
const data = await response.json();
renderServerDetail(data);
} catch (err) {
console.error('Error fetching server details:', err);
} finally {
dom.detailGrid.style.opacity = '1';
dom.detailLoading.style.display = 'none';
}
}
function closeServerDetail() {
dom.serverDetailModal.classList.remove('active');
}
function renderServerDetail(data) {
dom.detailCpuBusy.textContent = formatPercent(data.cpuBusy);
dom.detailSysLoad.textContent = data.sysLoad.toFixed(1) + '%';
dom.detailMemUsedPct.textContent = formatPercent(data.memUsedPct);
dom.detailSwapUsedPct.textContent = formatPercent(data.swapUsedPct);
dom.detailRootFsUsedPct.textContent = formatPercent(data.rootFsUsedPct);
dom.detailCpuCores.textContent = data.cpuCores + ' 核心';
dom.detailMemTotal.textContent = formatBytes(data.memTotal);
// Uptime formatting
const uptimeSec = data.uptime;
const days = Math.floor(uptimeSec / 86400);
const hours = Math.floor((uptimeSec % 86400) / 3600);
const mins = Math.floor((uptimeSec % 3600) / 60);
dom.detailUptime.textContent = `${days}${hours}小时 ${mins}`;
dom.detailNetRx.textContent = formatBandwidth(data.netRx);
dom.detailNetTx.textContent = formatBandwidth(data.netTx);
}
// ---- Network History ----
async function fetchNetworkHistory() {
try {
const response = await fetch('/api/metrics/network-history');
const data = await response.json();
networkChart.setData(data);
if (dom.trafficP95 && networkChart.p95) {
dom.trafficP95.textContent = formatBandwidth(networkChart.p95);
}
} catch (err) {
console.error('Error fetching network history:', err);
}
}
// ---- Settings Modal ----
function openSettings() {
dom.settingsModal.classList.add('active');
loadSources();
}
function closeSettings() {
dom.settingsModal.classList.remove('active');
hideMessage();
hideSiteMessage();
}
// ---- Tab Switching ----
function switchTab(tabId) {
dom.modalTabs.forEach(tab => {
tab.classList.toggle('active', tab.getAttribute('data-tab') === tabId);
});
dom.tabContents.forEach(content => {
content.classList.toggle('active', content.id === `tab-${tabId}`);
});
}
// ---- Site Settings ----
async function loadSiteSettings() {
try {
const response = await fetch('/api/settings');
const settings = await response.json();
window.SITE_SETTINGS = settings; // Cache it globally
// Update inputs
dom.pageNameInput.value = settings.page_name || '';
dom.siteTitleInput.value = settings.title || '';
dom.logoUrlInput.value = settings.logo_url || '';
dom.defaultThemeInput.value = settings.default_theme || 'dark';
// Apply to UI
applySiteSettings(settings);
// Handle Theme Priority: localStorage > Site Default
const savedTheme = localStorage.getItem('theme');
const themeToApply = savedTheme || settings.default_theme || 'dark';
applyTheme(themeToApply);
// Listen for system theme changes if set to auto
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', () => {
const currentSavedTheme = localStorage.getItem('theme');
const defaultTheme = window.SITE_SETTINGS ? window.SITE_SETTINGS.default_theme : 'dark';
const activeTheme = currentSavedTheme || defaultTheme;
if (activeTheme === 'auto') {
applyTheme('auto');
}
});
} catch (err) {
console.error('Error loading site settings:', err);
}
}
function applySiteSettings(settings) {
if (settings.page_name) {
document.title = settings.page_name;
}
if (settings.title) {
dom.logoText.textContent = settings.title;
}
// Logo Icon
if (settings.logo_url) {
dom.logoIconContainer.innerHTML = `<img src="${escapeHtml(settings.logo_url)}" alt="Logo" class="logo-icon-img">`;
} else {
// Restore default SVG
dom.logoIconContainer.innerHTML = `
<svg class="logo-icon" id="logoSvg" viewBox="0 0 32 32" fill="none">
<rect x="2" y="2" width="28" height="28" rx="8" stroke="url(#logoGrad)" stroke-width="2.5"/>
<path d="M8 22 L12 14 L16 18 L20 10 L24 16" stroke="url(#logoGrad)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<circle cx="12" cy="14" r="2" fill="url(#logoGrad)"/>
<circle cx="20" cy="10" r="2" fill="url(#logoGrad)"/>
<defs>
<linearGradient id="logoGrad" x1="0" y1="0" x2="32" y2="32">
<stop offset="0%" stop-color="#6366f1"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
</defs>
</svg>
`;
}
}
async function saveSiteSettings() {
if (!user) {
showSiteMessage('请先登录后操作', 'error');
openLoginModal();
return;
}
const settings = {
page_name: dom.pageNameInput.value.trim(),
title: dom.siteTitleInput.value.trim(),
logo_url: dom.logoUrlInput.value.trim(),
default_theme: dom.defaultThemeInput.value
};
dom.btnSaveSiteSettings.disabled = true;
dom.btnSaveSiteSettings.textContent = '保存中...';
try {
const response = await fetch('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings)
});
if (response.ok) {
showSiteMessage('设置保存成功', 'success');
applySiteSettings(settings);
} else {
const err = await response.json();
showSiteMessage(`保存失败: ${err.error}`, 'error');
if (response.status === 401) openLoginModal();
}
} catch (err) {
showSiteMessage(`保存失败: ${err.message}`, 'error');
} finally {
dom.btnSaveSiteSettings.disabled = false;
dom.btnSaveSiteSettings.textContent = '保存设置';
}
}
function showSiteMessage(text, type) {
dom.siteSettingsMessage.textContent = text;
dom.siteSettingsMessage.className = `form-message ${type}`;
setTimeout(hideSiteMessage, 5000);
}
function hideSiteMessage() {
dom.siteSettingsMessage.className = 'form-message';
}
async function loadSources() {
try {
const response = await fetch('/api/sources');
const sources = await response.json();
dom.sourceCount.textContent = `${sources.length} 个数据源`;
renderSources(sources);
} catch (err) {
console.error('Error loading sources:', err);
}
}
function renderSources(sources) {
if (sources.length === 0) {
dom.sourceItems.innerHTML = '<div class="source-empty">暂无数据源</div>';
return;
}
dom.sourceItems.innerHTML = sources.map(source => `
<div class="source-item" data-id="${source.id}">
<div class="source-item-info">
<div class="source-item-name">
${escapeHtml(source.name)}
<span class="source-status ${source.status === 'online' ? 'source-status-online' : 'source-status-offline'}">
${source.status === 'online' ? '在线' : '离线'}
</span>
</div>
<div class="source-item-url">${escapeHtml(source.url)}</div>
${source.description ? `<div class="source-item-desc">${escapeHtml(source.description)}</div>` : ''}
</div>
<div class="source-item-actions">
<button class="btn btn-delete" onclick="deleteSource(${source.id})">删除</button>
</div>
</div>
`).join('');
}
// ---- Test Connection ----
async function testConnection() {
const url = dom.sourceUrl.value.trim();
if (!url) {
showMessage('请输入 Prometheus URL', 'error');
return;
}
dom.btnTest.textContent = '测试中...';
dom.btnTest.disabled = true;
try {
const response = await fetch('/api/sources/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url })
});
const data = await response.json();
if (data.status === 'ok') {
showMessage(`连接成功Prometheus 版本: ${data.version}`, 'success');
} else {
if (response.status === 401) {
showMessage('请先登录', 'error');
openLoginModal();
} else {
showMessage(`连接失败: ${data.message || data.error}`, 'error');
}
}
} catch (err) {
showMessage(`连接失败: ${err.message}`, 'error');
} finally {
dom.btnTest.textContent = '测试连接';
dom.btnTest.disabled = false;
}
}
// ---- Add Source ----
async function addSource() {
if (!user) {
showMessage('请先登录后操作', 'error');
openLoginModal();
return;
}
const name = dom.sourceName.value.trim();
const url = dom.sourceUrl.value.trim();
const description = dom.sourceDesc.value.trim();
if (!name || !url) {
showMessage('请填写名称和URL', 'error');
return;
}
dom.btnAdd.textContent = '添加中...';
dom.btnAdd.disabled = true;
try {
const response = await fetch('/api/sources', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, url, description })
});
if (response.ok) {
showMessage('数据源添加成功', 'success');
dom.sourceName.value = '';
dom.sourceUrl.value = '';
dom.sourceDesc.value = '';
loadSources();
fetchMetrics();
fetchNetworkHistory();
} else {
const err = await response.json();
showMessage(`添加失败: ${err.error}`, 'error');
if (response.status === 401) openLoginModal();
}
} catch (err) {
showMessage(`添加失败: ${err.message}`, 'error');
} finally {
dom.btnAdd.textContent = '添加';
dom.btnAdd.disabled = false;
}
}
// ---- Delete Source ----
window.deleteSource = async function (id) {
if (!user) {
showMessage('请先登录后操作', 'error');
openLoginModal();
return;
}
if (!confirm('确定要删除这个数据源吗?')) return;
try {
const response = await fetch(`/api/sources/${id}`, { method: 'DELETE' });
if (response.ok) {
loadSources();
fetchMetrics();
fetchNetworkHistory();
} else {
if (response.status === 401) openLoginModal();
}
} catch (err) {
console.error('Error deleting source:', err);
}
};
// ---- Messages ----
function showMessage(text, type) {
dom.formMessage.textContent = text;
dom.formMessage.className = `form-message ${type}`;
setTimeout(hideMessage, 5000);
}
function hideMessage() {
dom.formMessage.className = 'form-message';
}
// ---- Escape HTML ----
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text || '';
return div.innerHTML;
}
// ---- Load source count on page load ----
async function loadSourceCount() {
try {
const response = await fetch('/api/sources');
const sources = await response.json();
dom.sourceCount.textContent = `${Array.isArray(sources) ? sources.length : 0} 个数据源`;
} catch (err) {
// ignore
}
}
// ---- Start ----
loadSourceCount();
init();
})();