添加数据库修复脚本

This commit is contained in:
CN-JS-HuiBai
2026-04-04 19:11:40 +08:00
parent a686977da6
commit 3823eeede2
6 changed files with 549 additions and 70 deletions

View File

@@ -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;

View File

@@ -23,7 +23,8 @@
<header class="header" id="header">
<div class="header-left">
<div class="logo">
<svg class="logo-icon" viewBox="0 0 32 32" fill="none">
<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)"/>
@@ -35,7 +36,8 @@
</linearGradient>
</defs>
</svg>
<h1 class="logo-text">数据可视化展示大屏</h1>
</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,10 +270,15 @@
<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">&times;</button>
</div>
<div class="modal-body">
<!-- Prometheus Sources Tab -->
<div class="tab-content active" id="tab-prom">
<!-- Add Source Form -->
<div class="add-source-form" id="addSourceForm">
<h3>添加数据源</h3>
@@ -305,6 +313,37 @@
</div>
</div>
</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>
</div>
</div>
</div>

View File

@@ -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() {

View 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;

View File

@@ -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();
});

View File

@@ -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();
}