添加数据库修复脚本
This commit is contained in:
@@ -71,6 +71,41 @@
|
||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
:root.light-theme .bg-grid {
|
||||
background-image:
|
||||
linear-gradient(rgba(99, 102, 241, 0.06) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(99, 102, 241, 0.06) 1px, transparent 1px);
|
||||
}
|
||||
|
||||
:root.light-theme .header {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
:root.light-theme .stat-card {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
:root.light-theme .chart-card {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
:root.light-theme .chart-footer {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
:root.light-theme .server-table th {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
:root.light-theme .modal {
|
||||
background: #ffffff;
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
:root.light-theme .modal-overlay {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
/* ---- Reset & Base ---- */
|
||||
*, *::before, *::after {
|
||||
margin: 0;
|
||||
@@ -721,6 +756,68 @@ body {
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* ---- Modal Tabs ---- */
|
||||
.modal-tabs {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.modal-tab {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
padding: 8px 4px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.modal-tab:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.modal-tab.active {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.modal-tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -21px; /* Align with header border */
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: var(--gradient-primary);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ---- Logo Styling ---- */
|
||||
#logoIconContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.logo-icon-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
@@ -23,19 +23,21 @@
|
||||
<header class="header" id="header">
|
||||
<div class="header-left">
|
||||
<div class="logo">
|
||||
<svg class="logo-icon" 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>
|
||||
<h1 class="logo-text">数据可视化展示大屏</h1>
|
||||
<div id="logoIconContainer">
|
||||
<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>
|
||||
</div>
|
||||
<h1 class="logo-text" id="logoText">数据可视化展示大屏</h1>
|
||||
</div>
|
||||
<div class="header-meta">
|
||||
<span class="server-count" id="serverCount">
|
||||
@@ -48,7 +50,8 @@
|
||||
<div class="header-right">
|
||||
<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>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="theme-icon 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>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="theme-icon moon-icon" style="display: none;"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
|
||||
</button>
|
||||
<div id="userSection">
|
||||
<button class="btn btn-login" id="btnLogin">登录</button>
|
||||
@@ -267,41 +270,77 @@
|
||||
<div class="modal-overlay" id="settingsModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h2>Prometheus 数据源管理</h2>
|
||||
<div class="modal-tabs">
|
||||
<button class="modal-tab active" data-tab="prom">数据源管理</button>
|
||||
<button class="modal-tab" data-tab="site">大屏设置</button>
|
||||
</div>
|
||||
<button class="modal-close" id="modalClose">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Add Source Form -->
|
||||
<div class="add-source-form" id="addSourceForm">
|
||||
<h3>添加数据源</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="sourceName">名称</label>
|
||||
<input type="text" id="sourceName" placeholder="例:生产环境" autocomplete="off">
|
||||
<!-- Prometheus Sources Tab -->
|
||||
<div class="tab-content active" id="tab-prom">
|
||||
<!-- Add Source Form -->
|
||||
<div class="add-source-form" id="addSourceForm">
|
||||
<h3>添加数据源</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="sourceName">名称</label>
|
||||
<input type="text" id="sourceName" placeholder="例:生产环境" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group form-group-wide">
|
||||
<label for="sourceUrl">Prometheus URL</label>
|
||||
<input type="url" id="sourceUrl" placeholder="http://prometheus.example.com:9090" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-group-wide">
|
||||
<label for="sourceUrl">Prometheus URL</label>
|
||||
<input type="url" id="sourceUrl" placeholder="http://prometheus.example.com:9090" autocomplete="off">
|
||||
<div class="form-row">
|
||||
<div class="form-group form-group-wide">
|
||||
<label for="sourceDesc">描述 (可选)</label>
|
||||
<input type="text" id="sourceDesc" placeholder="数据源描述" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-test" id="btnTest">测试连接</button>
|
||||
<button class="btn btn-add" id="btnAdd">添加</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-message" id="formMessage"></div>
|
||||
</div>
|
||||
|
||||
<!-- Source List -->
|
||||
<div class="source-list" id="sourceList">
|
||||
<h3>已配置数据源</h3>
|
||||
<div class="source-items" id="sourceItems">
|
||||
<div class="source-empty">暂无数据源</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group form-group-wide">
|
||||
<label for="sourceDesc">描述 (可选)</label>
|
||||
<input type="text" id="sourceDesc" placeholder="数据源描述" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-test" id="btnTest">测试连接</button>
|
||||
<button class="btn btn-add" id="btnAdd">添加</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-message" id="formMessage"></div>
|
||||
</div>
|
||||
|
||||
<!-- Source List -->
|
||||
<div class="source-list" id="sourceList">
|
||||
<h3>已配置数据源</h3>
|
||||
<div class="source-items" id="sourceItems">
|
||||
<div class="source-empty">暂无数据源</div>
|
||||
<!-- Site Settings Tab -->
|
||||
<div class="tab-content" id="tab-site">
|
||||
<div class="site-settings-form">
|
||||
<h3>自定义大屏展示</h3>
|
||||
<div class="form-group">
|
||||
<label for="pageNameInput">页面名称 (浏览器标签页标题)</label>
|
||||
<input type="text" id="pageNameInput" placeholder="例:运维监控大屏">
|
||||
</div>
|
||||
<div class="form-group" style="margin-top: 15px;">
|
||||
<label for="siteTitleInput">标题 (大屏左上角显示名称)</label>
|
||||
<input type="text" id="siteTitleInput" placeholder="例:数据可视化展示大屏">
|
||||
</div>
|
||||
<div class="form-group" style="margin-top: 15px;">
|
||||
<label for="logoUrlInput">Logo URL (图片链接,为空则显示默认图标)</label>
|
||||
<input type="url" id="logoUrlInput" placeholder="https://example.com/logo.png">
|
||||
</div>
|
||||
<div class="form-group" style="margin-top: 15px;">
|
||||
<label for="defaultThemeInput">默认主题</label>
|
||||
<select id="defaultThemeInput" style="padding: 10px 14px; background: var(--bg-input); border: 1px solid var(--border-color); border-radius: var(--radius-sm); color: var(--text-primary);">
|
||||
<option value="dark">默认夜间模式</option>
|
||||
<option value="light">默认白天模式</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-actions" style="margin-top: 25px; display: flex; justify-content: flex-end;">
|
||||
<button class="btn btn-add" id="btnSaveSiteSettings">保存设置</button>
|
||||
</div>
|
||||
<div class="form-message" id="siteSettingsMessage"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
171
public/js/app.js
171
public/js/app.js
@@ -43,8 +43,21 @@
|
||||
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'),
|
||||
@@ -67,11 +80,7 @@
|
||||
updateClock();
|
||||
setInterval(updateClock, 1000);
|
||||
|
||||
// Theme initialization
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme === 'light') {
|
||||
document.documentElement.classList.add('light-theme');
|
||||
}
|
||||
// Initial theme check (localStorage handled after site settings load to ensure priority)
|
||||
|
||||
// Network chart
|
||||
networkChart = new AreaChart(dom.networkCanvas);
|
||||
@@ -94,6 +103,17 @@
|
||||
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') {
|
||||
@@ -108,6 +128,7 @@
|
||||
// Start data fetching
|
||||
fetchMetrics();
|
||||
fetchNetworkHistory();
|
||||
loadSiteSettings();
|
||||
setInterval(fetchMetrics, REFRESH_INTERVAL);
|
||||
setInterval(fetchNetworkHistory, NETWORK_HISTORY_INTERVAL);
|
||||
}
|
||||
@@ -115,7 +136,25 @@
|
||||
// ---- Theme Switching ----
|
||||
function toggleTheme() {
|
||||
const isLight = document.documentElement.classList.toggle('light-theme');
|
||||
localStorage.setItem('theme', isLight ? 'light' : 'dark');
|
||||
const theme = isLight ? 'light' : 'dark';
|
||||
localStorage.setItem('theme', theme);
|
||||
updateThemeIcons(theme);
|
||||
}
|
||||
|
||||
function applyTheme(theme) {
|
||||
const isLight = theme === 'light';
|
||||
document.documentElement.classList.toggle('light-theme', isLight);
|
||||
updateThemeIcons(theme);
|
||||
}
|
||||
|
||||
function updateThemeIcons(theme) {
|
||||
if (theme === 'light') {
|
||||
dom.sunIcon.style.display = 'none';
|
||||
dom.moonIcon.style.display = 'block';
|
||||
} else {
|
||||
dom.sunIcon.style.display = 'block';
|
||||
dom.moonIcon.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Auth Logic ----
|
||||
@@ -381,6 +420,126 @@
|
||||
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();
|
||||
|
||||
// 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');
|
||||
if (savedTheme) {
|
||||
applyTheme(savedTheme);
|
||||
} else if (settings.default_theme) {
|
||||
applyTheme(settings.default_theme);
|
||||
} else {
|
||||
applyTheme('dark'); // Fallback
|
||||
}
|
||||
} 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() {
|
||||
|
||||
130
server/db-integrity-check.js
Normal file
130
server/db-integrity-check.js
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Database Integrity Check
|
||||
* Runs at startup to ensure all required tables exist.
|
||||
* Recreates the database if any tables are missing.
|
||||
*/
|
||||
require('dotenv').config();
|
||||
const mysql = require('mysql2/promise');
|
||||
const db = require('./db');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const REQUIRED_TABLES = [
|
||||
'users',
|
||||
'prometheus_sources',
|
||||
'site_settings',
|
||||
'traffic_stats'
|
||||
];
|
||||
|
||||
async function checkAndFixDatabase() {
|
||||
// Only run if .env is already configured
|
||||
const envPath = path.join(__dirname, '..', '.env');
|
||||
if (!fs.existsSync(envPath)) return;
|
||||
|
||||
const dbHost = process.env.MYSQL_HOST || 'localhost';
|
||||
const dbUser = process.env.MYSQL_USER || 'root';
|
||||
const dbPass = process.env.MYSQL_PASSWORD || '';
|
||||
const dbPort = parseInt(process.env.MYSQL_PORT) || 3306;
|
||||
const dbName = process.env.MYSQL_DATABASE || 'display_wall';
|
||||
|
||||
try {
|
||||
// Check tables
|
||||
const [rows] = await db.query("SHOW TABLES");
|
||||
const existingTables = rows.map(r => Object.values(r)[0]);
|
||||
|
||||
const missingTables = REQUIRED_TABLES.filter(t => !existingTables.includes(t));
|
||||
|
||||
if (missingTables.length > 0) {
|
||||
console.log(`[Database Integrity] ⚠️ Missing tables: ${missingTables.join(', ')}`);
|
||||
await recreateDatabase(dbHost, dbPort, dbUser, dbPass, dbName);
|
||||
} else {
|
||||
// console.log(`[Database Integrity] ✅ All tables accounted for.`);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code === 'ER_BAD_DB_ERROR') {
|
||||
console.log(`[Database Integrity] ⚠️ Database "${dbName}" does not exist.`);
|
||||
await recreateDatabase(dbHost, dbPort, dbUser, dbPass, dbName);
|
||||
} else {
|
||||
console.error('[Database Integrity] ❌ Error checking integrity:', err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function recreateDatabase(host, port, user, password, dbName) {
|
||||
console.log(`[Database Integrity] 🔄 Re-initializing database "${dbName}"...`);
|
||||
|
||||
let connection;
|
||||
try {
|
||||
connection = await mysql.createConnection({ host, port, user, password });
|
||||
|
||||
// Drop and create database
|
||||
await connection.query(`DROP DATABASE IF EXISTS \`${dbName}\``);
|
||||
await connection.query(`CREATE DATABASE \`${dbName}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`);
|
||||
await connection.query(`USE \`${dbName}\``);
|
||||
|
||||
// Recreate all tables
|
||||
console.log(' - Creating table "users"...');
|
||||
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(' - Creating table "prometheus_sources"...');
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS prometheus_sources (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
url VARCHAR(500) NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
|
||||
console.log(' - Creating table "site_settings"...');
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS site_settings (
|
||||
id INT PRIMARY KEY DEFAULT 1,
|
||||
page_name VARCHAR(255) DEFAULT '数据可视化展示大屏',
|
||||
title VARCHAR(255) DEFAULT '数据可视化展示大屏',
|
||||
logo_url TEXT,
|
||||
default_theme VARCHAR(20) DEFAULT 'dark',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
await connection.query(`
|
||||
INSERT INTO site_settings (id, page_name, title, default_theme)
|
||||
VALUES (1, '数据可视化展示大屏', '数据可视化展示大屏', 'dark')
|
||||
`);
|
||||
|
||||
console.log(' - Creating table "traffic_stats"...');
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS traffic_stats (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
rx_bytes BIGINT UNSIGNED DEFAULT 0,
|
||||
tx_bytes BIGINT UNSIGNED DEFAULT 0,
|
||||
rx_bandwidth DOUBLE DEFAULT 0,
|
||||
tx_bandwidth DOUBLE DEFAULT 0,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE INDEX (timestamp)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
|
||||
console.log(`[Database Integrity] ✅ Re-initialization complete.`);
|
||||
|
||||
// Refresh db pool in the main app context
|
||||
db.initPool();
|
||||
|
||||
} catch (err) {
|
||||
console.error('[Database Integrity] ❌ Critical failure during re-initialization:', err.message);
|
||||
} finally {
|
||||
if (connection) await connection.end();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = checkAndFixDatabase;
|
||||
@@ -4,6 +4,7 @@ const cors = require('cors');
|
||||
const path = require('path');
|
||||
const db = require('./db');
|
||||
const prometheusService = require('./prometheus-service');
|
||||
const checkAndFixDatabase = require('./db-integrity-check');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
@@ -157,6 +158,21 @@ app.post('/api/setup/init', async (req, res) => {
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS site_settings (
|
||||
id INT PRIMARY KEY DEFAULT 1,
|
||||
page_name VARCHAR(255) DEFAULT '数据可视化展示大屏',
|
||||
title VARCHAR(255) DEFAULT '数据可视化展示大屏',
|
||||
logo_url TEXT,
|
||||
default_theme VARCHAR(20) DEFAULT 'dark',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
await connection.query(`
|
||||
INSERT IGNORE INTO site_settings (id, page_name, title, default_theme)
|
||||
VALUES (1, '数据可视化展示大屏', '数据可视化展示大屏', 'dark')
|
||||
`);
|
||||
|
||||
await connection.end();
|
||||
|
||||
// Save to .env
|
||||
@@ -348,6 +364,47 @@ app.post('/api/sources/test', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== Site Settings ====================
|
||||
|
||||
// Get site settings
|
||||
app.get('/api/settings', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await db.query('SELECT * FROM site_settings WHERE id = 1');
|
||||
if (rows.length === 0) {
|
||||
return res.json({
|
||||
page_name: '数据可视化展示大屏',
|
||||
title: '数据可视化展示大屏',
|
||||
logo_url: null
|
||||
});
|
||||
}
|
||||
res.json(rows[0]);
|
||||
} catch (err) {
|
||||
console.error('Error fetching settings:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch settings' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update site settings
|
||||
app.post('/api/settings', requireAuth, async (req, res) => {
|
||||
const { page_name, title, logo_url, default_theme } = req.body;
|
||||
try {
|
||||
await db.query(
|
||||
`INSERT INTO site_settings (id, page_name, title, logo_url, default_theme)
|
||||
VALUES (1, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
page_name = VALUES(page_name),
|
||||
title = VALUES(title),
|
||||
logo_url = VALUES(logo_url),
|
||||
default_theme = VALUES(default_theme)`,
|
||||
[page_name, title, logo_url, default_theme]
|
||||
);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error('Error updating settings:', err);
|
||||
res.status(500).json({ error: 'Failed to update settings' });
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== Metrics Aggregation ====================
|
||||
|
||||
// Get all aggregated metrics from all Prometheus sources
|
||||
@@ -597,29 +654,8 @@ async function recordTrafficStats() {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if need to create traffic_stats table if db already initialized
|
||||
async function ensureTrafficTable() {
|
||||
if (!isDbInitialized) return;
|
||||
try {
|
||||
await db.query(`
|
||||
CREATE TABLE IF NOT EXISTS traffic_stats (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
rx_bytes BIGINT UNSIGNED DEFAULT 0,
|
||||
tx_bytes BIGINT UNSIGNED DEFAULT 0,
|
||||
rx_bandwidth DOUBLE DEFAULT 0,
|
||||
tx_bandwidth DOUBLE DEFAULT 0,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE INDEX (timestamp)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
// Add columns if missing for existing tables
|
||||
try { await db.query('ALTER TABLE traffic_stats ADD COLUMN rx_bandwidth DOUBLE DEFAULT 0'); } catch(e) {}
|
||||
try { await db.query('ALTER TABLE traffic_stats ADD COLUMN tx_bandwidth DOUBLE DEFAULT 0'); } catch(e) {}
|
||||
try { await db.query('ALTER TABLE traffic_stats ADD UNIQUE INDEX (timestamp)'); } catch(e) {}
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
ensureTrafficTable().then(() => {
|
||||
// Check and fix database integrity on startup
|
||||
checkAndFixDatabase().then(() => {
|
||||
initialPreload();
|
||||
});
|
||||
|
||||
|
||||
@@ -49,6 +49,24 @@ async function initDatabase() {
|
||||
`);
|
||||
console.log(' ✅ Table "prometheus_sources" ready');
|
||||
|
||||
// Create site_settings table
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS site_settings (
|
||||
id INT PRIMARY KEY DEFAULT 1,
|
||||
page_name VARCHAR(255) DEFAULT '数据可视化展示大屏',
|
||||
title VARCHAR(255) DEFAULT '数据可视化展示大屏',
|
||||
logo_url TEXT,
|
||||
default_theme VARCHAR(20) DEFAULT 'dark',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
// Insert default settings if not exists
|
||||
await connection.query(`
|
||||
INSERT IGNORE INTO site_settings (id, page_name, title, default_theme)
|
||||
VALUES (1, '数据可视化展示大屏', '数据可视化展示大屏', 'dark')
|
||||
`);
|
||||
console.log(' ✅ Table "site_settings" ready');
|
||||
|
||||
console.log('\n🎉 Database initialization complete!\n');
|
||||
await connection.end();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user