diff --git a/public/js/app.js b/public/js/app.js
index d9acdf7..8e9c457 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -362,43 +362,57 @@
function initGlobe() {
if (!dom.globeContainer) return;
- myGlobe = Globe()
- (dom.globeContainer)
- .globeImageUrl('//unpkg.com/three-globe/example/img/earth-dark.jpg')
- .bumpImageUrl('//unpkg.com/three-globe/example/img/earth-topology.png')
- .backgroundColor('rgba(0,0,0,0)')
- .showAtmosphere(true)
- .atmosphereColor('#6366f1')
- .atmosphereDaylightAlpha(0.1)
- .pointsData([])
- .pointColor(() => '#06b6d4')
- .pointAltitude(0.05)
- .pointRadius(0.8)
- .pointsMerge(true)
- .pointLabel(d => `
-
-
${escapeHtml(d.job)}
-
${escapeHtml(d.city || '')}, ${escapeHtml(d.countryName || d.country || '')}
-
-
BW: ↓${formatBandwidth(d.netRx)} ↑${formatBandwidth(d.netTx)}
+ if (typeof Globe !== 'function') {
+ console.warn('[Globe] Globe.gl library not loaded. 3D visualization will be disabled.');
+ dom.globeContainer.innerHTML = `
+
+ Globe.gl 库加载失败
请检查网络连接或刷新页面
-
- `);
+ `;
+ return;
+ }
- // Resizing
- const resizeObserver = new ResizeObserver(() => {
- if (myGlobe) {
- const width = dom.globeContainer.clientWidth;
- const height = dom.globeContainer.clientHeight;
- myGlobe.width(width).height(height);
- }
- });
- resizeObserver.observe(dom.globeContainer);
+ try {
+ myGlobe = Globe()
+ (dom.globeContainer)
+ .globeImageUrl('//unpkg.com/three-globe/example/img/earth-dark.jpg')
+ .bumpImageUrl('//unpkg.com/three-globe/example/img/earth-topology.png')
+ .backgroundColor('rgba(0,0,0,0)')
+ .showAtmosphere(true)
+ .atmosphereColor('#6366f1')
+ .atmosphereDaylightAlpha(0.1)
+ .pointsData([])
+ .pointColor(() => '#06b6d4')
+ .pointAltitude(0.05)
+ .pointRadius(0.8)
+ .pointsMerge(true)
+ .pointLabel(d => `
+
+
${escapeHtml(d.job)}
+
${escapeHtml(d.city || '')}, ${escapeHtml(d.countryName || d.country || '')}
+
+ BW: ↓${formatBandwidth(d.netRx)} ↑${formatBandwidth(d.netTx)}
+
+
+ `);
- // Initial view
- myGlobe.controls().autoRotate = true;
- myGlobe.controls().autoRotateSpeed = 0.5;
- myGlobe.controls().enableZoom = false; // Disable zoom to maintain dashboard feel
+ // Resizing
+ const resizeObserver = new ResizeObserver(() => {
+ if (myGlobe) {
+ const width = dom.globeContainer.clientWidth;
+ const height = dom.globeContainer.clientHeight;
+ myGlobe.width(width).height(height);
+ }
+ });
+ resizeObserver.observe(dom.globeContainer);
+
+ // Initial view
+ myGlobe.controls().autoRotate = true;
+ myGlobe.controls().autoRotateSpeed = 0.5;
+ myGlobe.controls().enableZoom = false; // Disable zoom to maintain dashboard feel
+ } catch (err) {
+ console.error('[Globe] Initialization failed:', err);
+ }
}
function updateGlobe(servers) {
diff --git a/server/db-integrity-check.js b/server/db-integrity-check.js
index 6d021d1..a30f0c8 100644
--- a/server/db-integrity-check.js
+++ b/server/db-integrity-check.js
@@ -18,16 +18,9 @@ const REQUIRED_TABLES = [
];
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");
@@ -36,110 +29,88 @@ async function checkAndFixDatabase() {
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.`);
+ console.log(`[Database Integrity] ⚠️ Missing tables: ${missingTables.join(', ')}. Creating them...`);
+
+ for (const table of missingTables) {
+ await createTable(table);
+ }
+ console.log(`[Database Integrity] ✅ Missing tables created.`);
}
} 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);
- }
+ 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(' - Creating table "server_locations"...');
- await connection.query(`
- CREATE TABLE IF NOT EXISTS server_locations (
- id INT AUTO_INCREMENT PRIMARY KEY,
- ip VARCHAR(255) NOT NULL UNIQUE,
- country CHAR(2),
- country_name VARCHAR(100),
- region VARCHAR(100),
- city VARCHAR(100),
- latitude DOUBLE,
- longitude DOUBLE,
- last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_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();
+async function createTable(tableName) {
+ console.log(` - Creating table "${tableName}"...`);
+ switch (tableName) {
+ case 'users':
+ await db.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
+ `);
+ break;
+ case 'prometheus_sources':
+ await db.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
+ `);
+ break;
+ case 'site_settings':
+ await db.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 db.query(`
+ INSERT IGNORE INTO site_settings (id, page_name, title, default_theme)
+ VALUES (1, '数据可视化展示大屏', '数据可视化展示大屏', 'dark')
+ `);
+ break;
+ case 'traffic_stats':
+ 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
+ `);
+ break;
+ case 'server_locations':
+ await db.query(`
+ CREATE TABLE IF NOT EXISTS server_locations (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ ip VARCHAR(255) NOT NULL UNIQUE,
+ country CHAR(2),
+ country_name VARCHAR(100),
+ region VARCHAR(100),
+ city VARCHAR(100),
+ latitude DOUBLE,
+ longitude DOUBLE,
+ last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ `);
+ break;
}
}
diff --git a/server/index.js b/server/index.js
index 50745f2..d744c25 100644
--- a/server/index.js
+++ b/server/index.js
@@ -582,25 +582,28 @@ app.get('/api/metrics/overview', async (req, res) => {
// --- Add Geo Information to Servers ---
const geoServers = await Promise.all(overview.servers.map(async (server) => {
- // The original IP is masked in the response from prometheusService.getOverviewMetrics
- // But we can get it back from serverIdMap in prometheusService if we are on the same process
- // Actually, prometheusService needs to provide a way to get the real IP back.
- // Or we can just modify getOverviewMetrics to return the real IP for internal use.
-
- // Let's look at prometheusService.js's getServerIdMap logic
const realInstance = prometheusService.resolveToken(server.instance);
const cleanIp = realInstance.split(':')[0];
- // Attempt to get location
- const location = await geoService.getLocation(cleanIp);
- if (location) {
- return {
- ...server,
- country: location.country,
- countryName: location.country_name,
- lat: location.latitude,
- lng: location.longitude
- };
+ // Try to get from DB cache only (fast)
+ try {
+ const [rows] = await db.query('SELECT * FROM server_locations WHERE ip = ?', [cleanIp]);
+ if (rows.length > 0) {
+ const location = rows[0];
+ return {
+ ...server,
+ country: location.country,
+ countryName: location.country_name,
+ city: location.city,
+ lat: location.latitude,
+ lng: location.longitude
+ };
+ } else {
+ // Trigger background resolution for future requests
+ geoService.getLocation(cleanIp).catch(() => {});
+ }
+ } catch (e) {
+ // DB error, skip geo for now
}
return server;
}));