添加鉴权逻辑
This commit is contained in:
@@ -55,6 +55,22 @@
|
|||||||
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root.light-theme {
|
||||||
|
--bg-primary: #f5f7fb;
|
||||||
|
--bg-secondary: #ffffff;
|
||||||
|
--bg-card: rgba(255, 255, 255, 0.9);
|
||||||
|
--bg-card-hover: rgba(255, 255, 255, 1);
|
||||||
|
--bg-input: #ffffff;
|
||||||
|
--border-color: rgba(99, 102, 241, 0.1);
|
||||||
|
--border-hover: rgba(99, 102, 241, 0.25);
|
||||||
|
--text-primary: #1e293b;
|
||||||
|
--text-secondary: #64748b;
|
||||||
|
--text-muted: #94a3b8;
|
||||||
|
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
|
||||||
|
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- Reset & Base ---- */
|
/* ---- Reset & Base ---- */
|
||||||
*, *::before, *::after {
|
*, *::before, *::after {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -503,8 +519,8 @@ body {
|
|||||||
|
|
||||||
.gauge {
|
.gauge {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 120px;
|
width: 100px;
|
||||||
height: 120px;
|
height: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gauge svg {
|
.gauge svg {
|
||||||
@@ -549,7 +565,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.gauge-label {
|
.gauge-label {
|
||||||
font-size: 0.7rem;
|
font-size: 0.65rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|||||||
@@ -47,6 +47,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<div class="clock" id="clock"></div>
|
<div class="clock" id="clock"></div>
|
||||||
|
<button class="btn-icon theme-toggle" id="themeToggle" title="切换主题">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="sun-icon"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
|
||||||
|
</button>
|
||||||
|
<div id="userSection">
|
||||||
|
<button class="btn btn-login" id="btnLogin">登录</button>
|
||||||
|
</div>
|
||||||
<button class="btn-settings" id="btnSettings" title="配置管理">
|
<button class="btn-settings" id="btnSettings" title="配置管理">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<circle cx="12" cy="12" r="3"></circle>
|
<circle cx="12" cy="12" r="3"></circle>
|
||||||
@@ -237,7 +243,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>状态</th>
|
<th>状态</th>
|
||||||
<th>服务器</th>
|
<th>Job / 实例</th>
|
||||||
<th>数据源</th>
|
<th>数据源</th>
|
||||||
<th>CPU</th>
|
<th>CPU</th>
|
||||||
<th>内存</th>
|
<th>内存</th>
|
||||||
@@ -303,6 +309,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Login Modal -->
|
||||||
|
<div class="modal-overlay" id="loginModalOverlay">
|
||||||
|
<div class="modal" style="max-width: 400px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>用户登录</h2>
|
||||||
|
<button class="modal-close" id="closeLoginModal">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="loginForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">用户名</label>
|
||||||
|
<input type="text" id="username" placeholder="请输入用户名" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-top: 16px;">
|
||||||
|
<label for="password">密码</label>
|
||||||
|
<input type="password" id="password" placeholder="请输入密码" required>
|
||||||
|
</div>
|
||||||
|
<div id="loginError" style="color: var(--accent-rose); font-size: 0.8rem; margin-top: 10px; display: none;"></div>
|
||||||
|
<button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 24px; background: var(--gradient-primary); color: white; border: none; padding: 12px; border-radius: 8px; cursor: pointer;">登 录</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="/js/utils.js"></script>
|
<script src="/js/utils.js"></script>
|
||||||
<script src="/js/chart.js"></script>
|
<script src="/js/chart.js"></script>
|
||||||
<script src="/js/app.js"></script>
|
<script src="/js/app.js"></script>
|
||||||
|
|||||||
@@ -125,10 +125,47 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="init-form" id="promForm" style="display: none;">
|
<div class="init-form" id="adminForm" style="display: none;">
|
||||||
|
<div class="init-header" style="margin-bottom: 20px;">
|
||||||
|
<h2 style="font-size: 20px;">创建管理员账户</h2>
|
||||||
|
<p>请设置系统的第一个管理员账号和密码</p>
|
||||||
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group form-group-wide">
|
<div class="form-group form-group-wide">
|
||||||
<label for="promName">数据源名称 (Name)</label>
|
<label for="adminUsername">管理员用户名</label>
|
||||||
|
<input type="text" id="adminUsername" placeholder="请输入用户名" value="admin" autocomplete="username">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group form-group-wide">
|
||||||
|
<label for="adminPassword">登录密码</label>
|
||||||
|
<input type="password" id="adminPassword" placeholder="请输入密码" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group form-group-wide">
|
||||||
|
<label for="adminPasswordConfirm">确认密码</label>
|
||||||
|
<input type="password" id="adminPasswordConfirm" placeholder="请再次输入密码" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-message" id="adminMessageBox"></div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn btn-add" id="btnAdminCreate">创建账户</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="init-form" id="promForm" style="display: none;">
|
||||||
|
<div class="init-header" style="margin-bottom: 20px;">
|
||||||
|
<h2 style="font-size: 20px;">设置数据源 (可选)</h2>
|
||||||
|
<p>您可以现在添加一个 Prometheus 数据源,或者稍后在系统中配置</p>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group form-group-wide">
|
||||||
|
<label for="promName">数据源名称</label>
|
||||||
<input type="text" id="promName" placeholder="例如:生产环境" autocomplete="off">
|
<input type="text" id="promName" placeholder="例如:生产环境" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
157
public/js/app.js
157
public/js/app.js
@@ -42,12 +42,21 @@
|
|||||||
btnTest: document.getElementById('btnTest'),
|
btnTest: document.getElementById('btnTest'),
|
||||||
btnAdd: document.getElementById('btnAdd'),
|
btnAdd: document.getElementById('btnAdd'),
|
||||||
formMessage: document.getElementById('formMessage'),
|
formMessage: document.getElementById('formMessage'),
|
||||||
sourceItems: document.getElementById('sourceItems')
|
sourceItems: document.getElementById('sourceItems'),
|
||||||
|
// Auth & Theme elements
|
||||||
|
themeToggle: document.getElementById('themeToggle'),
|
||||||
|
userSection: document.getElementById('userSection'),
|
||||||
|
btnLogin: document.getElementById('btnLogin'),
|
||||||
|
loginModal: document.getElementById('loginModalOverlay'),
|
||||||
|
closeLoginModal: document.getElementById('closeLoginModal'),
|
||||||
|
loginForm: document.getElementById('loginForm'),
|
||||||
|
loginError: document.getElementById('loginError')
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- State ----
|
// ---- State ----
|
||||||
let previousMetrics = null;
|
let previousMetrics = null;
|
||||||
let networkChart = null;
|
let networkChart = null;
|
||||||
|
let user = null; // Currently logged in user
|
||||||
|
|
||||||
// ---- Initialize ----
|
// ---- Initialize ----
|
||||||
function init() {
|
function init() {
|
||||||
@@ -58,6 +67,12 @@
|
|||||||
updateClock();
|
updateClock();
|
||||||
setInterval(updateClock, 1000);
|
setInterval(updateClock, 1000);
|
||||||
|
|
||||||
|
// Theme initialization
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme === 'light') {
|
||||||
|
document.documentElement.classList.add('light-theme');
|
||||||
|
}
|
||||||
|
|
||||||
// Network chart
|
// Network chart
|
||||||
networkChart = new AreaChart(dom.networkCanvas);
|
networkChart = new AreaChart(dom.networkCanvas);
|
||||||
|
|
||||||
@@ -70,11 +85,26 @@
|
|||||||
dom.btnTest.addEventListener('click', testConnection);
|
dom.btnTest.addEventListener('click', testConnection);
|
||||||
dom.btnAdd.addEventListener('click', addSource);
|
dom.btnAdd.addEventListener('click', addSource);
|
||||||
|
|
||||||
|
// Auth & Theme listeners
|
||||||
|
dom.themeToggle.addEventListener('click', 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();
|
||||||
|
});
|
||||||
|
|
||||||
// Keyboard shortcut
|
// Keyboard shortcut
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape') closeSettings();
|
if (e.key === 'Escape') {
|
||||||
|
closeSettings();
|
||||||
|
closeLoginModal();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check auth status
|
||||||
|
checkAuthStatus();
|
||||||
|
|
||||||
// Start data fetching
|
// Start data fetching
|
||||||
fetchMetrics();
|
fetchMetrics();
|
||||||
fetchNetworkHistory();
|
fetchNetworkHistory();
|
||||||
@@ -82,6 +112,88 @@
|
|||||||
setInterval(fetchNetworkHistory, NETWORK_HISTORY_INTERVAL);
|
setInterval(fetchNetworkHistory, NETWORK_HISTORY_INTERVAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Theme Switching ----
|
||||||
|
function toggleTheme() {
|
||||||
|
const isLight = document.documentElement.classList.toggle('light-theme');
|
||||||
|
localStorage.setItem('theme', isLight ? 'light' : 'dark');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 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.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.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Add SVG Gradient Defs ----
|
// ---- Add SVG Gradient Defs ----
|
||||||
function addGaugeSvgDefs() {
|
function addGaugeSvgDefs() {
|
||||||
const svgs = document.querySelectorAll('.gauge svg');
|
const svgs = document.querySelectorAll('.gauge svg');
|
||||||
@@ -97,7 +209,7 @@
|
|||||||
grad.setAttribute('id', gradients[i].id);
|
grad.setAttribute('id', gradients[i].id);
|
||||||
grad.setAttribute('x1', '0%');
|
grad.setAttribute('x1', '0%');
|
||||||
grad.setAttribute('y1', '0%');
|
grad.setAttribute('y1', '0%');
|
||||||
grad.setAttribute('x2', '100%');
|
grad.setAttribute('x1', '100%');
|
||||||
grad.setAttribute('y2', '100%');
|
grad.setAttribute('y2', '100%');
|
||||||
|
|
||||||
gradients[i].colors.forEach((color, ci) => {
|
gradients[i].colors.forEach((color, ci) => {
|
||||||
@@ -150,13 +262,13 @@
|
|||||||
dom.diskDetail.textContent = `${formatBytes(data.disk.used)} / ${formatBytes(data.disk.total)}`;
|
dom.diskDetail.textContent = `${formatBytes(data.disk.used)} / ${formatBytes(data.disk.total)}`;
|
||||||
|
|
||||||
// Bandwidth
|
// Bandwidth
|
||||||
dom.totalBandwidth.textContent = formatBandwidth(data.network.totalBandwidth);
|
dom.totalBandwidth.textContent = formatBandwidth(data.network.total || 0);
|
||||||
dom.bandwidthDetail.textContent = `↓ ${formatBandwidth(data.network.rx)} ↑ ${formatBandwidth(data.network.tx)}`;
|
dom.bandwidthDetail.textContent = `↓ ${formatBandwidth(data.network.rx)} ↑ ${formatBandwidth(data.network.tx)}`;
|
||||||
|
|
||||||
// 24h traffic
|
// 24h traffic
|
||||||
dom.traffic24hRx.textContent = formatBytes(data.traffic24h.rx);
|
dom.traffic24hRx.textContent = formatBytes(data.traffic24h.rx);
|
||||||
dom.traffic24hTx.textContent = formatBytes(data.traffic24h.tx);
|
dom.traffic24hTx.textContent = formatBytes(data.traffic24h.tx);
|
||||||
dom.traffic24hTotal.textContent = formatBytes(data.traffic24h.total);
|
dom.traffic24hTotal.textContent = formatBytes(data.traffic24h.total || (data.traffic24h.rx + data.traffic24h.tx));
|
||||||
|
|
||||||
// Update gauges
|
// Update gauges
|
||||||
updateGauge(dom.gaugeCpuFill, dom.gaugeCpuValue, cpuPct);
|
updateGauge(dom.gaugeCpuFill, dom.gaugeCpuValue, cpuPct);
|
||||||
@@ -186,10 +298,6 @@
|
|||||||
const offset = CIRCUMFERENCE - (clamped / 100) * CIRCUMFERENCE;
|
const offset = CIRCUMFERENCE - (clamped / 100) * CIRCUMFERENCE;
|
||||||
fillEl.style.strokeDashoffset = offset;
|
fillEl.style.strokeDashoffset = offset;
|
||||||
valueEl.textContent = formatPercent(clamped);
|
valueEl.textContent = formatPercent(clamped);
|
||||||
|
|
||||||
// Change color based on usage
|
|
||||||
const color = getUsageColor(clamped);
|
|
||||||
// We keep gradient but could override for critical
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Server Table ----
|
// ---- Server Table ----
|
||||||
@@ -218,7 +326,10 @@
|
|||||||
<td>
|
<td>
|
||||||
<span class="status-dot ${server.up ? 'status-dot-online' : 'status-dot-offline'}"></span>
|
<span class="status-dot ${server.up ? 'status-dot-online' : 'status-dot-offline'}"></span>
|
||||||
</td>
|
</td>
|
||||||
<td style="color: var(--text-primary); font-weight: 500;">${escapeHtml(server.instance)}</td>
|
<td>
|
||||||
|
<div style="color: var(--text-primary); font-weight: 600; font-family: var(--font-sans);">${escapeHtml(server.job)}</div>
|
||||||
|
<div style="font-size: 0.65rem; color: var(--text-muted); opacity: 0.8;">${escapeHtml(server.instance)}</div>
|
||||||
|
</td>
|
||||||
<td>${escapeHtml(server.source)}</td>
|
<td>${escapeHtml(server.source)}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="usage-bar">
|
<div class="usage-bar">
|
||||||
@@ -330,7 +441,12 @@
|
|||||||
if (data.status === 'ok') {
|
if (data.status === 'ok') {
|
||||||
showMessage(`连接成功!Prometheus 版本: ${data.version}`, 'success');
|
showMessage(`连接成功!Prometheus 版本: ${data.version}`, 'success');
|
||||||
} else {
|
} else {
|
||||||
showMessage(`连接失败: ${data.message}`, 'error');
|
if (response.status === 401) {
|
||||||
|
showMessage('请先登录', 'error');
|
||||||
|
openLoginModal();
|
||||||
|
} else {
|
||||||
|
showMessage(`连接失败: ${data.message || data.error}`, 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showMessage(`连接失败: ${err.message}`, 'error');
|
showMessage(`连接失败: ${err.message}`, 'error');
|
||||||
@@ -342,6 +458,12 @@
|
|||||||
|
|
||||||
// ---- Add Source ----
|
// ---- Add Source ----
|
||||||
async function addSource() {
|
async function addSource() {
|
||||||
|
if (!user) {
|
||||||
|
showMessage('请先登录后操作', 'error');
|
||||||
|
openLoginModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const name = dom.sourceName.value.trim();
|
const name = dom.sourceName.value.trim();
|
||||||
const url = dom.sourceUrl.value.trim();
|
const url = dom.sourceUrl.value.trim();
|
||||||
const description = dom.sourceDesc.value.trim();
|
const description = dom.sourceDesc.value.trim();
|
||||||
@@ -367,12 +489,12 @@
|
|||||||
dom.sourceUrl.value = '';
|
dom.sourceUrl.value = '';
|
||||||
dom.sourceDesc.value = '';
|
dom.sourceDesc.value = '';
|
||||||
loadSources();
|
loadSources();
|
||||||
// Refresh metrics immediately
|
|
||||||
fetchMetrics();
|
fetchMetrics();
|
||||||
fetchNetworkHistory();
|
fetchNetworkHistory();
|
||||||
} else {
|
} else {
|
||||||
const err = await response.json();
|
const err = await response.json();
|
||||||
showMessage(`添加失败: ${err.error}`, 'error');
|
showMessage(`添加失败: ${err.error}`, 'error');
|
||||||
|
if (response.status === 401) openLoginModal();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showMessage(`添加失败: ${err.message}`, 'error');
|
showMessage(`添加失败: ${err.message}`, 'error');
|
||||||
@@ -384,6 +506,11 @@
|
|||||||
|
|
||||||
// ---- Delete Source ----
|
// ---- Delete Source ----
|
||||||
window.deleteSource = async function (id) {
|
window.deleteSource = async function (id) {
|
||||||
|
if (!user) {
|
||||||
|
showMessage('请先登录后操作', 'error');
|
||||||
|
openLoginModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!confirm('确定要删除这个数据源吗?')) return;
|
if (!confirm('确定要删除这个数据源吗?')) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -392,6 +519,8 @@
|
|||||||
loadSources();
|
loadSources();
|
||||||
fetchMetrics();
|
fetchMetrics();
|
||||||
fetchNetworkHistory();
|
fetchNetworkHistory();
|
||||||
|
} else {
|
||||||
|
if (response.status === 401) openLoginModal();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error deleting source:', err);
|
console.error('Error deleting source:', err);
|
||||||
@@ -412,7 +541,7 @@
|
|||||||
// ---- Escape HTML ----
|
// ---- Escape HTML ----
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.textContent = text;
|
div.textContent = text || '';
|
||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,7 +550,7 @@
|
|||||||
try {
|
try {
|
||||||
const response = await fetch('/api/sources');
|
const response = await fetch('/api/sources');
|
||||||
const sources = await response.json();
|
const sources = await response.json();
|
||||||
dom.sourceCount.textContent = `${sources.length} 个数据源`;
|
dom.sourceCount.textContent = `${Array.isArray(sources) ? sources.length : 0} 个数据源`;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,16 +21,58 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const btnPromAdd = document.getElementById('btnPromAdd');
|
const btnPromAdd = document.getElementById('btnPromAdd');
|
||||||
const promMessageBox = document.getElementById('promMessageBox');
|
const promMessageBox = document.getElementById('promMessageBox');
|
||||||
|
|
||||||
|
const adminForm = document.getElementById('adminForm');
|
||||||
|
const adminUsername = document.getElementById('adminUsername');
|
||||||
|
const adminPassword = document.getElementById('adminPassword');
|
||||||
|
const adminPasswordConfirm = document.getElementById('adminPasswordConfirm');
|
||||||
|
const btnAdminCreate = document.getElementById('btnAdminCreate');
|
||||||
|
const adminMessageBox = document.getElementById('adminMessageBox');
|
||||||
|
|
||||||
function showMessage(msg, isError = false) {
|
function showMessage(msg, isError = false) {
|
||||||
messageBox.textContent = msg;
|
messageBox.textContent = msg;
|
||||||
messageBox.className = 'form-message ' + (isError ? 'error' : 'success');
|
messageBox.className = 'form-message ' + (isError ? 'error' : 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showAdminMessage(msg, isError = false) {
|
||||||
|
adminMessageBox.textContent = msg;
|
||||||
|
adminMessageBox.className = 'form-message ' + (isError ? 'error' : 'success');
|
||||||
|
}
|
||||||
|
|
||||||
function showPromMessage(msg, isError = false) {
|
function showPromMessage(msg, isError = false) {
|
||||||
promMessageBox.textContent = msg;
|
promMessageBox.textContent = msg;
|
||||||
promMessageBox.className = 'form-message ' + (isError ? 'error' : 'success');
|
promMessageBox.className = 'form-message ' + (isError ? 'error' : 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Step Controller ---
|
||||||
|
async function checkStatus() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/setup/status');
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
initForm.style.display = 'none';
|
||||||
|
adminForm.style.display = 'none';
|
||||||
|
promForm.style.display = 'none';
|
||||||
|
|
||||||
|
if (!data.initialized) {
|
||||||
|
initForm.style.display = 'block';
|
||||||
|
initHeaderTitle.textContent = '数据库初始化';
|
||||||
|
initHeaderDesc.textContent = '请配置您的 MySQL 数据库连接信息以完成首次设置';
|
||||||
|
} else if (data.needsAdmin) {
|
||||||
|
adminForm.style.display = 'block';
|
||||||
|
initHeaderTitle.textContent = '创建管理员账户';
|
||||||
|
initHeaderDesc.textContent = '请设置系统的第一个管理员账号和密码';
|
||||||
|
} else {
|
||||||
|
promForm.style.display = 'block';
|
||||||
|
initHeaderTitle.textContent = '配置 Prometheus';
|
||||||
|
initHeaderDesc.textContent = '配置您的第一个 Prometheus 数据源监控连接';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
initForm.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkStatus();
|
||||||
|
|
||||||
btnTest.addEventListener('click', async () => {
|
btnTest.addEventListener('click', async () => {
|
||||||
btnTest.disabled = true;
|
btnTest.disabled = true;
|
||||||
const oldText = btnTest.textContent;
|
const oldText = btnTest.textContent;
|
||||||
@@ -79,12 +121,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showMessage('数据库初始化成功!进入下一步...');
|
showMessage('数据库初始化成功!进入下一步...');
|
||||||
setTimeout(() => {
|
setTimeout(checkStatus, 1000);
|
||||||
initForm.style.display = 'none';
|
|
||||||
promForm.style.display = 'block';
|
|
||||||
initHeaderTitle.textContent = '配置 Prometheus';
|
|
||||||
initHeaderDesc.textContent = '配置您的第一个 Prometheus 数据源监控连接';
|
|
||||||
}, 1000);
|
|
||||||
} else {
|
} else {
|
||||||
showMessage('初始化失败: ' + (data.error || '未知错误'), true);
|
showMessage('初始化失败: ' + (data.error || '未知错误'), true);
|
||||||
btnInit.disabled = false;
|
btnInit.disabled = false;
|
||||||
@@ -97,6 +134,39 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
btnAdminCreate.addEventListener('click', async () => {
|
||||||
|
const username = adminUsername.value.trim();
|
||||||
|
const password = adminPassword.value;
|
||||||
|
const confirm = adminPasswordConfirm.value;
|
||||||
|
|
||||||
|
if (!username || !password) return showAdminMessage('请填写用户名和密码', true);
|
||||||
|
if (password !== confirm) return showAdminMessage('两次输入的密码不一致', true);
|
||||||
|
|
||||||
|
btnAdminCreate.disabled = true;
|
||||||
|
btnAdminCreate.textContent = '创建中...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/setup/admin', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, password })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (res.ok) {
|
||||||
|
showAdminMessage('管理员账户创建成功!');
|
||||||
|
setTimeout(checkStatus, 1000);
|
||||||
|
} else {
|
||||||
|
showAdminMessage('创建失败: ' + (data.error || '未知错误'), true);
|
||||||
|
btnAdminCreate.disabled = false;
|
||||||
|
btnAdminCreate.textContent = '创建账户';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showAdminMessage('请求失败: ' + err.message, true);
|
||||||
|
btnAdminCreate.disabled = false;
|
||||||
|
btnAdminCreate.textContent = '创建账户';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
btnPromTest.addEventListener('click', async () => {
|
btnPromTest.addEventListener('click', async () => {
|
||||||
const url = promUrl.value.trim();
|
const url = promUrl.value.trim();
|
||||||
if (!url) return showPromMessage('请输入 Prometheus URL', true);
|
if (!url) return showPromMessage('请输入 Prometheus URL', true);
|
||||||
@@ -144,6 +214,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
setTimeout(() => window.location.href = '/', 1500);
|
setTimeout(() => window.location.href = '/', 1500);
|
||||||
} else {
|
} else {
|
||||||
const err = await res.json();
|
const err = await res.json();
|
||||||
|
// If 401, it means we somehow bypassed auth on this step, but it might be okay during init
|
||||||
showPromMessage(`添加失败: ${err.error || '未知错误'}`, true);
|
showPromMessage(`添加失败: ${err.error || '未知错误'}`, true);
|
||||||
btnPromAdd.disabled = false;
|
btnPromAdd.disabled = false;
|
||||||
btnPromAdd.textContent = oldText;
|
btnPromAdd.textContent = oldText;
|
||||||
|
|||||||
145
server/index.js
145
server/index.js
@@ -11,24 +11,80 @@ const HOST = process.env.HOST || '0.0.0.0';
|
|||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
const fs = require('fs');
|
const crypto = require('crypto');
|
||||||
|
|
||||||
let isDbInitialized = false;
|
let isDbInitialized = false;
|
||||||
|
const sessions = new Map(); // Simple session store: sessionId -> {userId, username}
|
||||||
|
|
||||||
|
// Middleware: Check Auth
|
||||||
|
function requireAuth(req, res, next) {
|
||||||
|
const sessionId = getCookie(req, 'session_id');
|
||||||
|
if (sessionId && sessions.has(sessionId)) {
|
||||||
|
req.user = sessions.get(sessionId);
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
res.status(401).json({ error: 'Auth required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Get Cookie
|
||||||
|
function getCookie(req, name) {
|
||||||
|
const matches = req.headers.cookie && req.headers.cookie.match(new RegExp('(?:^|; )' + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + '=([^;]*)'));
|
||||||
|
return matches ? decodeURIComponent(matches[1]) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
async function checkDb() {
|
async function checkDb() {
|
||||||
try {
|
try {
|
||||||
const [rows] = await db.query("SHOW TABLES LIKE 'prometheus_sources'");
|
const fs = require('fs');
|
||||||
if (rows.length > 0) {
|
if (!fs.existsSync(path.join(__dirname, '..', '.env'))) {
|
||||||
isDbInitialized = true;
|
|
||||||
} else {
|
|
||||||
isDbInitialized = false;
|
isDbInitialized = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
const [rows] = await db.query("SHOW TABLES LIKE 'prometheus_sources'");
|
||||||
|
isDbInitialized = rows.length > 0;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
isDbInitialized = false;
|
isDbInitialized = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkDb();
|
checkDb();
|
||||||
|
// --- Auth API ---
|
||||||
|
app.post('/api/auth/login', async (req, res) => {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
try {
|
||||||
|
const [rows] = await db.query('SELECT * FROM users WHERE username = ?', [username]);
|
||||||
|
if (rows.length === 0) return res.status(401).json({ error: 'Invalid credentials' });
|
||||||
|
|
||||||
|
const user = rows[0];
|
||||||
|
const hash = crypto.pbkdf2Sync(password, user.salt, 1000, 64, 'sha512').toString('hex');
|
||||||
|
|
||||||
|
if (hash === user.password) {
|
||||||
|
const sessionId = crypto.randomBytes(32).toString('hex');
|
||||||
|
sessions.set(sessionId, { id: user.id, username: user.username });
|
||||||
|
res.setHeader('Set-Cookie', `session_id=${sessionId}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400`);
|
||||||
|
res.json({ success: true, username: user.username });
|
||||||
|
} else {
|
||||||
|
res.status(401).json({ error: 'Invalid credentials' });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: 'Login failed' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/auth/logout', (req, res) => {
|
||||||
|
const sessionId = getCookie(req, 'session_id');
|
||||||
|
if (sessionId) sessions.delete(sessionId);
|
||||||
|
res.setHeader('Set-Cookie', 'session_id=; Path=/; HttpOnly; Max-Age=0');
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/auth/status', (req, res) => {
|
||||||
|
const sessionId = getCookie(req, 'session_id');
|
||||||
|
if (sessionId && sessions.has(sessionId)) {
|
||||||
|
res.json({ authenticated: true, username: sessions.get(sessionId).username });
|
||||||
|
} else {
|
||||||
|
res.json({ authenticated: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Setup API Routes
|
// Setup API Routes
|
||||||
app.post('/api/setup/test', async (req, res) => {
|
app.post('/api/setup/test', async (req, res) => {
|
||||||
@@ -78,6 +134,16 @@ app.post('/api/setup/init', async (req, res) => {
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
await connection.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
salt VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||||
|
`);
|
||||||
|
|
||||||
await connection.end();
|
await connection.end();
|
||||||
|
|
||||||
// Save to .env
|
// Save to .env
|
||||||
@@ -110,18 +176,71 @@ REFRESH_INTERVAL=${process.env.REFRESH_INTERVAL || 5000}
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Middleware to protect routes
|
// Setup Status Check
|
||||||
app.use((req, res, next) => {
|
app.get('/api/setup/status', async (req, res) => {
|
||||||
if (!isDbInitialized) {
|
try {
|
||||||
if (req.path.startsWith('/api/setup') || req.path === '/init.html' || req.path.startsWith('/css/') || req.path.startsWith('/js/') || req.path.startsWith('/fonts/')) {
|
if (!isDbInitialized) {
|
||||||
return next();
|
return res.json({ initialized: false, step: 'db' });
|
||||||
}
|
}
|
||||||
|
const [rows] = await db.query('SELECT COUNT(*) as count FROM users');
|
||||||
|
if (rows[0].count === 0) {
|
||||||
|
return res.json({ initialized: true, needsAdmin: true, step: 'admin' });
|
||||||
|
}
|
||||||
|
res.json({ initialized: true, needsAdmin: false, step: 'prom' });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Status check error:', err);
|
||||||
|
res.json({ initialized: false, step: 'db' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create First Admin
|
||||||
|
app.post('/api/setup/admin', async (req, res) => {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
if (!username || !password) return res.status(400).json({ error: 'Username and password are required' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [rows] = await db.query('SELECT COUNT(*) as count FROM users');
|
||||||
|
if (rows[0].count > 0) return res.status(403).json({ error: 'Admin already exists' });
|
||||||
|
|
||||||
|
const salt = crypto.randomBytes(16).toString('hex');
|
||||||
|
const hash = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512').toString('hex');
|
||||||
|
|
||||||
|
await db.query('INSERT INTO users (username, password, salt) VALUES (?, ?, ?)', [username, hash, salt]);
|
||||||
|
res.json({ success: true, message: 'Admin account created' });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Admin creation error:', err);
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Middleware to protect routes & enforce setup
|
||||||
|
app.use(async (req, res, next) => {
|
||||||
|
// Allow system files and setup APIs
|
||||||
|
if (req.path.startsWith('/api/setup') || req.path === '/init.html' || req.path.startsWith('/css/') || req.path.startsWith('/js/') || req.path.startsWith('/fonts/')) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce DB setup
|
||||||
|
if (!isDbInitialized) {
|
||||||
if (req.path.startsWith('/api/')) {
|
if (req.path.startsWith('/api/')) {
|
||||||
return res.status(503).json({ error: 'Database not initialized', needSetup: true });
|
return res.status(503).json({ error: 'Database not initialized', needSetup: true });
|
||||||
}
|
}
|
||||||
return res.redirect('/init.html');
|
return res.redirect('/init.html');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enforce User setup
|
||||||
|
try {
|
||||||
|
const [rows] = await db.query('SELECT COUNT(*) as count FROM users');
|
||||||
|
if (rows[0].count === 0) {
|
||||||
|
if (req.path.startsWith('/api/')) {
|
||||||
|
return res.status(503).json({ error: 'Admin not configured', needAdmin: true });
|
||||||
|
}
|
||||||
|
return res.redirect('/init.html?step=admin');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// If table doesn't exist, it's a DB initialization issue
|
||||||
|
}
|
||||||
|
|
||||||
if (req.path === '/init.html') {
|
if (req.path === '/init.html') {
|
||||||
return res.redirect('/');
|
return res.redirect('/');
|
||||||
}
|
}
|
||||||
@@ -153,7 +272,7 @@ app.get('/api/sources', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add a new Prometheus source
|
// Add a new Prometheus source
|
||||||
app.post('/api/sources', async (req, res) => {
|
app.post('/api/sources', requireAuth, async (req, res) => {
|
||||||
let { name, url, description } = req.body;
|
let { name, url, description } = req.body;
|
||||||
if (!name || !url) {
|
if (!name || !url) {
|
||||||
return res.status(400).json({ error: 'Name and URL are required' });
|
return res.status(400).json({ error: 'Name and URL are required' });
|
||||||
@@ -173,7 +292,7 @@ app.post('/api/sources', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Update a Prometheus source
|
// Update a Prometheus source
|
||||||
app.put('/api/sources/:id', async (req, res) => {
|
app.put('/api/sources/:id', requireAuth, async (req, res) => {
|
||||||
let { name, url, description } = req.body;
|
let { name, url, description } = req.body;
|
||||||
if (url && !/^https?:\/\//i.test(url)) url = 'http://' + url;
|
if (url && !/^https?:\/\//i.test(url)) url = 'http://' + url;
|
||||||
try {
|
try {
|
||||||
@@ -190,7 +309,7 @@ app.put('/api/sources/:id', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Delete a Prometheus source
|
// Delete a Prometheus source
|
||||||
app.delete('/api/sources/:id', async (req, res) => {
|
app.delete('/api/sources/:id', requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await db.query('DELETE FROM prometheus_sources WHERE id = ?', [req.params.id]);
|
await db.query('DELETE FROM prometheus_sources WHERE id = ?', [req.params.id]);
|
||||||
res.json({ message: 'Source deleted' });
|
res.json({ message: 'Source deleted' });
|
||||||
|
|||||||
@@ -24,6 +24,18 @@ async function initDatabase() {
|
|||||||
|
|
||||||
await connection.query(`USE \`${dbName}\``);
|
await connection.query(`USE \`${dbName}\``);
|
||||||
|
|
||||||
|
// Create users table
|
||||||
|
await connection.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
salt VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||||
|
`);
|
||||||
|
console.log(' ✅ Table "users" ready');
|
||||||
|
|
||||||
// Create prometheus_sources table
|
// Create prometheus_sources table
|
||||||
await connection.query(`
|
await connection.query(`
|
||||||
CREATE TABLE IF NOT EXISTS prometheus_sources (
|
CREATE TABLE IF NOT EXISTS prometheus_sources (
|
||||||
@@ -32,8 +44,8 @@ async function initDatabase() {
|
|||||||
url VARCHAR(500) NOT NULL,
|
url VARCHAR(500) NOT NULL,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||||
`);
|
`);
|
||||||
console.log(' ✅ Table "prometheus_sources" ready');
|
console.log(' ✅ Table "prometheus_sources" ready');
|
||||||
|
|
||||||
|
|||||||
@@ -178,10 +178,12 @@ async function getOverviewMetrics(url, sourceName) {
|
|||||||
// Build per-instance data map
|
// Build per-instance data map
|
||||||
const instances = new Map();
|
const instances = new Map();
|
||||||
|
|
||||||
const getOrCreate = (instance) => {
|
const getOrCreate = (metric) => {
|
||||||
if (!instances.has(instance)) {
|
const key = metric.instance;
|
||||||
instances.set(instance, {
|
if (!instances.has(key)) {
|
||||||
instance,
|
instances.set(key, {
|
||||||
|
instance: key,
|
||||||
|
job: metric.job || 'Unknown',
|
||||||
source: sourceName,
|
source: sourceName,
|
||||||
cpuPercent: 0,
|
cpuPercent: 0,
|
||||||
cpuCores: 0,
|
cpuCores: 0,
|
||||||
@@ -194,54 +196,54 @@ async function getOverviewMetrics(url, sourceName) {
|
|||||||
up: false
|
up: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return instances.get(instance);
|
return instances.get(key);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse UP status
|
// Parse UP status
|
||||||
for (const r of upResult) {
|
for (const r of upResult) {
|
||||||
const inst = getOrCreate(r.metric.instance);
|
const inst = getOrCreate(r.metric);
|
||||||
inst.up = parseFloat(r.value[1]) === 1;
|
inst.up = parseFloat(r.value[1]) === 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse CPU usage
|
// Parse CPU usage
|
||||||
for (const r of cpuResult) {
|
for (const r of cpuResult) {
|
||||||
const inst = getOrCreate(r.metric.instance);
|
const inst = getOrCreate(r.metric);
|
||||||
inst.cpuPercent = parseFloat(r.value[1]) || 0;
|
inst.cpuPercent = parseFloat(r.value[1]) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse CPU count
|
// Parse CPU count
|
||||||
for (const r of cpuCountResult) {
|
for (const r of cpuCountResult) {
|
||||||
const inst = getOrCreate(r.metric.instance);
|
const inst = getOrCreate(r.metric);
|
||||||
inst.cpuCores = parseFloat(r.value[1]) || 0;
|
inst.cpuCores = parseFloat(r.value[1]) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse memory
|
// Parse memory
|
||||||
for (const r of memTotalResult) {
|
for (const r of memTotalResult) {
|
||||||
const inst = getOrCreate(r.metric.instance);
|
const inst = getOrCreate(r.metric);
|
||||||
inst.memTotal = parseFloat(r.value[1]) || 0;
|
inst.memTotal = parseFloat(r.value[1]) || 0;
|
||||||
}
|
}
|
||||||
for (const r of memAvailResult) {
|
for (const r of memAvailResult) {
|
||||||
const inst = getOrCreate(r.metric.instance);
|
const inst = getOrCreate(r.metric);
|
||||||
inst.memUsed = inst.memTotal - (parseFloat(r.value[1]) || 0);
|
inst.memUsed = inst.memTotal - (parseFloat(r.value[1]) || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse disk
|
// Parse disk
|
||||||
for (const r of diskTotalResult) {
|
for (const r of diskTotalResult) {
|
||||||
const inst = getOrCreate(r.metric.instance);
|
const inst = getOrCreate(r.metric);
|
||||||
inst.diskTotal = parseFloat(r.value[1]) || 0;
|
inst.diskTotal = parseFloat(r.value[1]) || 0;
|
||||||
}
|
}
|
||||||
for (const r of diskFreeResult) {
|
for (const r of diskFreeResult) {
|
||||||
const inst = getOrCreate(r.metric.instance);
|
const inst = getOrCreate(r.metric);
|
||||||
inst.diskUsed = inst.diskTotal - (parseFloat(r.value[1]) || 0);
|
inst.diskUsed = inst.diskTotal - (parseFloat(r.value[1]) || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse network rates
|
// Parse network rates
|
||||||
for (const r of netRxResult) {
|
for (const r of netRxResult) {
|
||||||
const inst = getOrCreate(r.metric.instance);
|
const inst = getOrCreate(r.metric);
|
||||||
inst.netRx = parseFloat(r.value[1]) || 0;
|
inst.netRx = parseFloat(r.value[1]) || 0;
|
||||||
}
|
}
|
||||||
for (const r of netTxResult) {
|
for (const r of netTxResult) {
|
||||||
const inst = getOrCreate(r.metric.instance);
|
const inst = getOrCreate(r.metric);
|
||||||
inst.netTx = parseFloat(r.value[1]) || 0;
|
inst.netTx = parseFloat(r.value[1]) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +292,8 @@ async function getOverviewMetrics(url, sourceName) {
|
|||||||
},
|
},
|
||||||
network: {
|
network: {
|
||||||
rx: totalNetRx,
|
rx: totalNetRx,
|
||||||
tx: totalNetTx
|
tx: totalNetTx,
|
||||||
|
total: totalNetRx + totalNetTx
|
||||||
},
|
},
|
||||||
traffic24h: {
|
traffic24h: {
|
||||||
rx: totalTraffic24hRx,
|
rx: totalTraffic24hRx,
|
||||||
|
|||||||
Reference in New Issue
Block a user