From 14796231f1c39bfddaf1500c21a07da2dabc0af9 Mon Sep 17 00:00:00 2001 From: CN-JS-HuiBai Date: Sat, 11 Apr 2026 00:10:36 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E8=87=AA=E6=A3=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db-schema-check.js | 216 +++++++++++++++++--------------------- server/index.js | 15 ++- 2 files changed, 109 insertions(+), 122 deletions(-) diff --git a/server/db-schema-check.js b/server/db-schema-check.js index 1c00d3b..3249d85 100644 --- a/server/db-schema-check.js +++ b/server/db-schema-check.js @@ -18,7 +18,11 @@ const SCHEMA = { created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `, - columns: [] + columns: [ + { name: 'username', sql: "ALTER TABLE users ADD COLUMN username VARCHAR(255) NOT NULL UNIQUE AFTER id" }, + { name: 'password', sql: "ALTER TABLE users ADD COLUMN password VARCHAR(255) NOT NULL AFTER username" }, + { name: 'salt', sql: "ALTER TABLE users ADD COLUMN salt VARCHAR(255) NOT NULL AFTER password" } + ] }, prometheus_sources: { createSql: ` @@ -34,23 +38,20 @@ const SCHEMA = { ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `, columns: [ - { - name: 'is_server_source', - sql: "ALTER TABLE prometheus_sources ADD COLUMN is_server_source TINYINT(1) DEFAULT 1 AFTER description" - }, - { - name: 'type', - sql: "ALTER TABLE prometheus_sources ADD COLUMN type VARCHAR(50) DEFAULT 'prometheus' AFTER is_server_source" - } + { name: 'name', sql: "ALTER TABLE prometheus_sources ADD COLUMN name VARCHAR(255) NOT NULL AFTER id" }, + { name: 'url', sql: "ALTER TABLE prometheus_sources ADD COLUMN url VARCHAR(500) NOT NULL AFTER name" }, + { name: 'description', sql: "ALTER TABLE prometheus_sources ADD COLUMN description TEXT AFTER url" }, + { name: 'is_server_source', sql: "ALTER TABLE prometheus_sources ADD COLUMN is_server_source TINYINT(1) DEFAULT 1 AFTER description" }, + { name: 'type', sql: "ALTER TABLE prometheus_sources ADD COLUMN type VARCHAR(50) DEFAULT 'prometheus' AFTER is_server_source" } ] }, site_settings: { createSql: ` CREATE TABLE IF NOT EXISTS site_settings ( id INT PRIMARY KEY DEFAULT 1, - page_name VARCHAR(255) DEFAULT 'Data Visualization Display Wall', + page_name VARCHAR(255) DEFAULT '数据可视化展示大屏', show_page_name TINYINT(1) DEFAULT 1, - title VARCHAR(255) DEFAULT 'Data Visualization Display Wall', + title VARCHAR(255) DEFAULT '数据可视化展示大屏', logo_url TEXT, logo_url_dark TEXT, favicon_url TEXT, @@ -76,78 +77,31 @@ const SCHEMA = { INSERT IGNORE INTO site_settings ( id, page_name, show_page_name, title, default_theme, show_95_bandwidth, p95_type, require_login_for_server_details ) VALUES ( - 1, 'Data Visualization Display Wall', 1, 'Data Visualization Display Wall', 'dark', 0, 'tx', 1 + 1, '数据可视化展示大屏', 1, '数据可视化展示大屏', 'dark', 0, 'tx', 1 ) `, columns: [ - { - name: 'show_page_name', - sql: "ALTER TABLE site_settings ADD COLUMN show_page_name TINYINT(1) DEFAULT 1 AFTER page_name" - }, - { - name: 'logo_url_dark', - sql: "ALTER TABLE site_settings ADD COLUMN logo_url_dark TEXT AFTER logo_url" - }, - { - name: 'favicon_url', - sql: "ALTER TABLE site_settings ADD COLUMN favicon_url TEXT AFTER logo_url_dark" - }, - { - name: 'show_95_bandwidth', - sql: "ALTER TABLE site_settings ADD COLUMN show_95_bandwidth TINYINT(1) DEFAULT 0 AFTER default_theme" - }, - { - name: 'p95_type', - sql: "ALTER TABLE site_settings ADD COLUMN p95_type VARCHAR(20) DEFAULT 'tx' AFTER show_95_bandwidth" - }, - { - name: 'require_login_for_server_details', - sql: "ALTER TABLE site_settings ADD COLUMN require_login_for_server_details TINYINT(1) DEFAULT 1 AFTER p95_type" - }, - { - name: 'blackbox_source_id', - sql: "ALTER TABLE site_settings ADD COLUMN blackbox_source_id INT AFTER require_login_for_server_details" - }, - { - name: 'latency_source', - sql: "ALTER TABLE site_settings ADD COLUMN latency_source VARCHAR(100) AFTER blackbox_source_id" - }, - { - name: 'latency_dest', - sql: "ALTER TABLE site_settings ADD COLUMN latency_dest VARCHAR(100) AFTER latency_source" - }, - { - name: 'latency_target', - sql: "ALTER TABLE site_settings ADD COLUMN latency_target VARCHAR(255) AFTER latency_dest" - }, - { - name: 'icp_filing', - sql: "ALTER TABLE site_settings ADD COLUMN icp_filing VARCHAR(255) AFTER latency_target" - }, - { - name: 'ps_filing', - sql: "ALTER TABLE site_settings ADD COLUMN ps_filing VARCHAR(255) AFTER icp_filing" - }, - { - name: 'network_data_sources', - sql: "ALTER TABLE site_settings ADD COLUMN network_data_sources TEXT AFTER ps_filing" - }, - { - name: 'show_server_ip', - sql: "ALTER TABLE site_settings ADD COLUMN show_server_ip TINYINT(1) DEFAULT 0 AFTER network_data_sources" - }, - { - name: 'ip_metric_name', - sql: "ALTER TABLE site_settings ADD COLUMN ip_metric_name VARCHAR(100) DEFAULT NULL AFTER show_server_ip" - }, - { - name: 'ip_label_name', - sql: "ALTER TABLE site_settings ADD COLUMN ip_label_name VARCHAR(100) DEFAULT 'address' AFTER ip_metric_name" - }, - { - name: 'custom_metrics', - sql: "ALTER TABLE site_settings ADD COLUMN custom_metrics JSON DEFAULT NULL AFTER ip_label_name" - } + { name: 'page_name', sql: "ALTER TABLE site_settings ADD COLUMN page_name VARCHAR(255) DEFAULT '数据可视化展示大屏' AFTER id" }, + { name: 'show_page_name', sql: "ALTER TABLE site_settings ADD COLUMN show_page_name TINYINT(1) DEFAULT 1 AFTER page_name" }, + { name: 'title', sql: "ALTER TABLE site_settings ADD COLUMN title VARCHAR(255) DEFAULT '数据可视化展示大屏' AFTER show_page_name" }, + { name: 'logo_url', sql: "ALTER TABLE site_settings ADD COLUMN logo_url TEXT AFTER title" }, + { name: 'logo_url_dark', sql: "ALTER TABLE site_settings ADD COLUMN logo_url_dark TEXT AFTER logo_url" }, + { name: 'favicon_url', sql: "ALTER TABLE site_settings ADD COLUMN favicon_url TEXT AFTER logo_url_dark" }, + { name: 'default_theme', sql: "ALTER TABLE site_settings ADD COLUMN default_theme VARCHAR(20) DEFAULT 'dark' AFTER favicon_url" }, + { name: 'show_95_bandwidth', sql: "ALTER TABLE site_settings ADD COLUMN show_95_bandwidth TINYINT(1) DEFAULT 0 AFTER default_theme" }, + { name: 'p95_type', sql: "ALTER TABLE site_settings ADD COLUMN p95_type VARCHAR(20) DEFAULT 'tx' AFTER show_95_bandwidth" }, + { name: 'require_login_for_server_details', sql: "ALTER TABLE site_settings ADD COLUMN require_login_for_server_details TINYINT(1) DEFAULT 1 AFTER p95_type" }, + { name: 'blackbox_source_id', sql: "ALTER TABLE site_settings ADD COLUMN blackbox_source_id INT AFTER require_login_for_server_details" }, + { name: 'latency_source', sql: "ALTER TABLE site_settings ADD COLUMN latency_source VARCHAR(100) AFTER blackbox_source_id" }, + { name: 'latency_dest', sql: "ALTER TABLE site_settings ADD COLUMN latency_dest VARCHAR(100) AFTER latency_source" }, + { name: 'latency_target', sql: "ALTER TABLE site_settings ADD COLUMN latency_target VARCHAR(255) AFTER latency_dest" }, + { name: 'icp_filing', sql: "ALTER TABLE site_settings ADD COLUMN icp_filing VARCHAR(255) AFTER latency_target" }, + { name: 'ps_filing', sql: "ALTER TABLE site_settings ADD COLUMN ps_filing VARCHAR(255) AFTER icp_filing" }, + { name: 'network_data_sources', sql: "ALTER TABLE site_settings ADD COLUMN network_data_sources TEXT AFTER ps_filing" }, + { name: 'show_server_ip', sql: "ALTER TABLE site_settings ADD COLUMN show_server_ip TINYINT(1) DEFAULT 0 AFTER network_data_sources" }, + { name: 'ip_metric_name', sql: "ALTER TABLE site_settings ADD COLUMN ip_metric_name VARCHAR(100) DEFAULT NULL AFTER show_server_ip" }, + { name: 'ip_label_name', sql: "ALTER TABLE site_settings ADD COLUMN ip_label_name VARCHAR(100) DEFAULT 'address' AFTER ip_metric_name" }, + { name: 'custom_metrics', sql: "ALTER TABLE site_settings ADD COLUMN custom_metrics JSON DEFAULT NULL AFTER ip_label_name" } ] }, traffic_stats: { @@ -162,7 +116,12 @@ const SCHEMA = { UNIQUE INDEX (timestamp) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `, - columns: [] + columns: [ + { name: 'rx_bytes', sql: "ALTER TABLE traffic_stats ADD COLUMN rx_bytes BIGINT UNSIGNED DEFAULT 0 AFTER id" }, + { name: 'tx_bytes', sql: "ALTER TABLE traffic_stats ADD COLUMN tx_bytes BIGINT UNSIGNED DEFAULT 0 AFTER rx_bytes" }, + { name: 'rx_bandwidth', sql: "ALTER TABLE traffic_stats ADD COLUMN rx_bandwidth DOUBLE DEFAULT 0 AFTER tx_bytes" }, + { name: 'tx_bandwidth', sql: "ALTER TABLE traffic_stats ADD COLUMN tx_bandwidth DOUBLE DEFAULT 0 AFTER rx_bandwidth" } + ] }, server_locations: { createSql: ` @@ -178,7 +137,15 @@ const SCHEMA = { last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `, - columns: [] + columns: [ + { name: 'ip', sql: "ALTER TABLE server_locations ADD COLUMN ip VARCHAR(255) NOT NULL UNIQUE AFTER id" }, + { name: 'country', sql: "ALTER TABLE server_locations ADD COLUMN country CHAR(2) AFTER ip" }, + { name: 'country_name', sql: "ALTER TABLE server_locations ADD COLUMN country_name VARCHAR(100) AFTER country" }, + { name: 'region', sql: "ALTER TABLE server_locations ADD COLUMN region VARCHAR(100) AFTER country_name" }, + { name: 'city', sql: "ALTER TABLE server_locations ADD COLUMN city VARCHAR(100) AFTER region" }, + { name: 'latitude', sql: "ALTER TABLE server_locations ADD COLUMN latitude DOUBLE AFTER city" }, + { name: 'longitude', sql: "ALTER TABLE server_locations ADD COLUMN longitude DOUBLE AFTER latitude" } + ] }, latency_routes: { createSql: ` @@ -191,64 +158,73 @@ const SCHEMA = { created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `, - columns: [] + columns: [ + { name: 'source_id', sql: "ALTER TABLE latency_routes ADD COLUMN source_id INT NOT NULL AFTER id" }, + { name: 'latency_source', sql: "ALTER TABLE latency_routes ADD COLUMN latency_source VARCHAR(100) NOT NULL AFTER source_id" }, + { name: 'latency_dest', sql: "ALTER TABLE latency_routes ADD COLUMN latency_dest VARCHAR(100) NOT NULL AFTER latency_source" }, + { name: 'latency_target', sql: "ALTER TABLE latency_routes ADD COLUMN latency_target VARCHAR(255) NOT NULL AFTER latency_dest" } + ] } }; -async function addColumnIfMissing(tableName, existingColumns, column) { - if (existingColumns.has(column.name)) { - return; - } - +async function ensureTable(tableName, tableSchema) { try { - console.log(`[Database Integrity] Missing column '${column.name}' in '${tableName}'. Adding it...`); - await db.query(column.sql); - console.log(`[Database Integrity] Column '${column.name}' added to '${tableName}'.`); - } catch (err) { - console.error(`[Database Integrity] Failed to add '${tableName}.${column.name}':`, err.message); + // 1. Ensure table exists + await db.query(tableSchema.createSql); + + // 2. Check columns + const [columns] = await db.query(`SHOW COLUMNS FROM \`${tableName}\``); + const existingColumns = new Set(columns.map((column) => column.Field)); + + console.log(`[Database Integrity] Table '${tableName}' verified (${columns.length} columns)`); - if (column.sql.includes(' AFTER ')) { - try { - const fallbackSql = column.sql.split(' AFTER ')[0]; - await db.query(fallbackSql); - console.log(`[Database Integrity] Column '${column.name}' added to '${tableName}' via fallback.`); - } catch (fallbackErr) { - console.error(`[Database Integrity] Fallback failed for '${tableName}.${column.name}':`, fallbackErr.message); + for (const column of tableSchema.columns || []) { + if (!existingColumns.has(column.name)) { + console.log(`[Database Integrity] Missing column '${column.name}' in '${tableName}'. Adding it...`); + await db.query(column.sql); + console.log(`[Database Integrity] Column '${column.name}' added to '${tableName}'.`); } } - } -} -async function ensureTable(tableName, tableSchema) { - await db.query(tableSchema.createSql); - - const [columns] = await db.query(`SHOW COLUMNS FROM \`${tableName}\``); - const existingColumns = new Set(columns.map((column) => column.Field)); - - for (const column of tableSchema.columns || []) { - await addColumnIfMissing(tableName, existingColumns, column); - } - - if (tableSchema.seedSql) { - await db.query(tableSchema.seedSql); + // 3. Seed data + if (tableSchema.seedSql) { + const [rows] = await db.query(`SELECT count(*) as count FROM \`${tableName}\``); + if (rows[0].count === 0) { + console.log(`[Database Integrity] Table '${tableName}' is empty. Seeding initial data...`); + await db.query(tableSchema.seedSql); + } + } + } catch (err) { + console.error(`[Database Integrity] Error ensuring table '${tableName}':`, err.message); + throw err; } } async function checkAndFixDatabase() { - // Check for DB host to see if we have configuration (could be in .env or environment) - if (!process.env.MYSQL_HOST && !fs.existsSync(path.join(__dirname, '..', '.env'))) { - console.log('[Database Integrity] No configuration found, skipping check.'); - return false; + console.log('[Database Integrity] Starting comprehensive database audit...'); + + // Try to check if we can even connect + try { + const health = await db.checkHealth(); + if (health.status !== 'up') { + console.warn(`[Database Integrity] initial health check failed: ${health.error}`); + // If we can't connect, maybe the DB itself doesn't exist? + // For now, we rely on the pool to handle connection retries/errors. + } + } catch (e) { + // Ignore health check errors, let ensureTable handle the primary queries } try { + let tablesChecked = 0; for (const [tableName, tableSchema] of Object.entries(SCHEMA)) { await ensureTable(tableName, tableSchema); + tablesChecked++; } + console.log(`[Database Integrity] Audit complete. ${tablesChecked} tables verified and healthy.`); return true; } catch (err) { - console.error('[Database Integrity] Startup schema check failed:', err.message); - // Rethrow to allow caller to decide if this is fatal + console.error('[Database Integrity] ❌ Audit failed:', err.message); throw err; } } diff --git a/server/index.js b/server/index.js index c5a9a71..9eac73a 100644 --- a/server/index.js +++ b/server/index.js @@ -305,15 +305,26 @@ function getCookie(req, name) { return matches ? decodeURIComponent(matches[1]) : undefined; } +/** + * Database Initialization Check + */ +let isDbInitialized = false; async function checkDb() { try { const fs = require('fs'); - if (!fs.existsSync(path.join(__dirname, '..', '.env'))) { + // In Docker or high-level environments, .env might not exist but process.env is set + const hasConfig = process.env.MYSQL_HOST || fs.existsSync(path.join(__dirname, '..', '.env')); + + if (!hasConfig) { isDbInitialized = false; return; } + + // Check for essential tables const [rows] = await db.query("SHOW TABLES LIKE 'prometheus_sources'"); - isDbInitialized = rows.length > 0; + const [rows2] = await db.query("SHOW TABLES LIKE 'site_settings'"); + + isDbInitialized = rows.length > 0 && rows2.length > 0; } catch (err) { isDbInitialized = false; }