diff --git a/package-lock.json b/package-lock.json index fb548e6..33edc1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,8 @@ "dotenv": "^16.4.0", "express": "^4.21.0", "ioredis": "^5.10.1", - "mysql2": "^3.11.0" + "mysql2": "^3.11.0", + "ws": "^8.20.0" } }, "node_modules/@ioredis/commands": { @@ -1215,6 +1216,27 @@ "engines": { "node": ">= 0.8" } + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 5d0d058..3752419 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "dotenv": "^16.4.0", "express": "^4.21.0", "ioredis": "^5.10.1", - "mysql2": "^3.11.0" + "mysql2": "^3.11.0", + "ws": "^8.20.0" } } diff --git a/public/css/style.css b/public/css/style.css index c6d4dac..bd12695 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -127,7 +127,9 @@ html { body { font-family: var(--font-sans); - background: var(--bg-primary); + background: radial-gradient(circle at 0% 0%, #1a1e2e 0%, #0a0e1a 45%), + radial-gradient(circle at 100% 100%, #151a2e 0%, #0a0e1a 45%); + background-attachment: fixed; color: var(--text-primary); min-height: 100vh; overflow-x: hidden; @@ -146,65 +148,7 @@ body { pointer-events: none; } -.bg-glow { - position: fixed; - border-radius: 50%; - filter: blur(120px); - opacity: 0.4; - z-index: 0; - pointer-events: none; - animation: glowFloat 20s ease-in-out infinite; - will-change: transform, opacity; - transform: translateZ(0); -} - -.bg-glow-1 { - width: 600px; - height: 600px; - background: radial-gradient(circle, rgba(99, 102, 241, 0.15), transparent 70%); - top: -200px; - left: -100px; - animation-delay: 0s; -} - -.bg-glow-2 { - width: 500px; - height: 500px; - background: radial-gradient(circle, rgba(6, 182, 212, 0.12), transparent 70%); - bottom: -150px; - right: -100px; - animation-delay: -7s; -} - -.bg-glow-3 { - width: 400px; - height: 400px; - background: radial-gradient(circle, rgba(168, 85, 247, 0.1), transparent 70%); - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - animation-delay: -14s; -} - -@keyframes glowFloat { - - 0%, - 100% { - transform: translate3d(0, 0, 0) scale(1); - } - - 25% { - transform: translate3d(30px, -30px, 0) scale(1.05); - } - - 50% { - transform: translate3d(-20px, 20px, 0) scale(0.95); - } - - 75% { - transform: translate3d(25px, 15px, 0) scale(1.02); - } -} +/* ---- Animated Background Classes Removed for Performance ---- */ /* ---- App Container ---- */ #app { @@ -223,9 +167,7 @@ body { align-items: center; justify-content: space-between; padding: 0 28px; - background: rgba(10, 14, 26, 0.85); - backdrop-filter: blur(12px) saturate(150%); - -webkit-backdrop-filter: blur(12px) saturate(150%); + background: rgba(10, 14, 26, 0.95); /* More opaque, no filter */ border-bottom: 1px solid var(--border-color); } @@ -455,8 +397,9 @@ input:checked+.slider:before { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius-lg); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); + /* backdrop-filter: blur(8px); */ + /* -webkit-backdrop-filter: blur(8px); */ + background: rgba(15, 22, 50, 0.9); /* More solid background for readability without blur */ transform: translateZ(0); transition: all 0.3s ease; overflow: hidden; @@ -624,8 +567,9 @@ input:checked+.slider:before { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius-lg); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); + /* backdrop-filter: blur(8px); */ + /* -webkit-backdrop-filter: blur(8px); */ + background: rgba(15, 22, 50, 0.9); /* More solid background for readability without blur */ transform: translateZ(0); overflow: hidden; transition: all 0.3s ease; diff --git a/public/index.html b/public/index.html index 5831b62..678c50b 100644 --- a/public/index.html +++ b/public/index.html @@ -38,11 +38,8 @@ - +
-
-
-
diff --git a/public/js/app.js b/public/js/app.js index 6be2819..46d0ca4 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -305,10 +305,40 @@ loadSiteSettings(); - setInterval(fetchMetrics, REFRESH_INTERVAL); + // setInterval(fetchMetrics, REFRESH_INTERVAL); - Now using WebSockets + initWebSocket(); setInterval(fetchNetworkHistory, NETWORK_HISTORY_INTERVAL); } + // ---- Real-time WebSocket ---- + function initWebSocket() { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${protocol}//${window.location.host}`; + const ws = new WebSocket(wsUrl); + + ws.onmessage = (event) => { + try { + const msg = JSON.parse(event.data); + if (msg.type === 'overview') { + allServersData = msg.data.servers || []; + updateDashboard(msg.data); + } + } catch (err) { + console.error('WS Message Error:', err); + } + }; + + ws.onclose = () => { + console.log('WS connection closed. Reconnecting in 5s...'); + setTimeout(initWebSocket, 5000); + }; + + ws.onerror = (err) => { + // Small silent error here to not alert the user constantly if server is down during maintenance + ws.close(); + }; + } + // ---- Theme Switching ---- function toggleTheme() { const theme = dom.themeToggle.checked ? 'light' : 'dark'; @@ -507,11 +537,9 @@ } }, series: [{ - type: 'effectScatter', + type: 'scatter', coordinateSystem: 'geo', geoIndex: 0, - showEffectOn: 'render', - rippleEffect: { brushType: 'stroke', scale: 3, period: 6 }, symbolSize: 5, itemStyle: { color: '#06b6d4', diff --git a/server/index.js b/server/index.js index 3bf62a0..9472088 100644 --- a/server/index.js +++ b/server/index.js @@ -7,6 +7,8 @@ const prometheusService = require('./prometheus-service'); const cache = require('./cache'); const geoService = require('./geo-service'); const checkAndFixDatabase = require('./db-integrity-check'); +const http = require('http'); +const WebSocket = require('ws'); const app = express(); const PORT = process.env.PORT || 3000; @@ -570,136 +572,135 @@ app.post('/api/settings', requireAuth, async (req, res) => { // ==================== Metrics Aggregation ==================== +// Reusable function to get overview metrics +async function getOverview() { + const [sources] = await db.query('SELECT * FROM prometheus_sources'); + if (sources.length === 0) { + return { + totalServers: 0, + activeServers: 0, + cpu: { used: 0, total: 0, percent: 0 }, + memory: { used: 0, total: 0, percent: 0 }, + disk: { used: 0, total: 0, percent: 0 }, + network: { total: 0, rx: 0, tx: 0 }, + traffic24h: { rx: 0, tx: 0, total: 0 }, + servers: [] + }; + } + + const allMetrics = await Promise.all(sources.map(async (source) => { + const cacheKey = `source_metrics:${source.url}:${source.name}`; + const cached = await cache.get(cacheKey); + if (cached) return cached; + + try { + const metrics = await prometheusService.getOverviewMetrics(source.url, source.name); + // Don't set cache here if we want real-time WS push to be fresh, + // but keeping it for REST API performance is fine. + await cache.set(cacheKey, metrics, 15); // Cache for 15s + return metrics; + } catch (err) { + console.error(`Error fetching metrics from ${source.name}:`, err.message); + return null; + } + })); + + const validMetrics = allMetrics.filter(m => m !== null); + + // Aggregate across all sources + let totalServers = 0; + let activeServers = 0; + let cpuUsed = 0, cpuTotal = 0; + let memUsed = 0, memTotal = 0; + let diskUsed = 0, diskTotal = 0; + let netRx = 0, netTx = 0; + let traffic24hRx = 0, traffic24hTx = 0; + let allServers = []; + + for (const m of validMetrics) { + totalServers += m.totalServers; + activeServers += (m.activeServers !== undefined ? m.activeServers : m.totalServers); + cpuUsed += m.cpu.used; + cpuTotal += m.cpu.total; + memUsed += m.memory.used; + memTotal += m.memory.total; + diskUsed += m.disk.used; + diskTotal += m.disk.total; + netRx += m.network.rx; + netTx += m.network.tx; + traffic24hRx += m.traffic24h.rx; + traffic24hTx += m.traffic24h.tx; + allServers = allServers.concat(m.servers); + } + + const overview = { + totalServers, + activeServers, + cpu: { + used: cpuUsed, + total: cpuTotal, + percent: cpuTotal > 0 ? (cpuUsed / cpuTotal * 100) : 0 + }, + memory: { + used: memUsed, + total: memTotal, + percent: memTotal > 0 ? (memUsed / memTotal * 100) : 0 + }, + disk: { + used: diskUsed, + total: diskTotal, + percent: diskTotal > 0 ? (diskUsed / diskTotal * 100) : 0 + }, + network: { + total: netRx + netTx, + rx: netRx, + tx: netTx + }, + traffic24h: { + rx: traffic24hRx, + tx: traffic24hTx, + total: traffic24hRx + traffic24hTx + }, + servers: allServers + }; + + // --- Add Geo Information to Servers --- + const geoServers = await Promise.all(overview.servers.map(async (server) => { + const realInstance = server.originalInstance || prometheusService.resolveToken(server.instance); + const cleanIp = realInstance.split(':')[0]; + + let geoData = null; + try { + const [rows] = await db.query('SELECT * FROM server_locations WHERE ip = ?', [cleanIp]); + if (rows.length > 0) { + geoData = rows[0]; + } else { + geoService.getLocation(cleanIp).catch(() => {}); + } + } catch (e) {} + + const { originalInstance, ...safeServer } = server; + if (geoData) { + return { + ...safeServer, + country: geoData.country, + countryName: geoData.country_name, + city: geoData.city, + lat: geoData.latitude, + lng: geoData.longitude + }; + } + return safeServer; + })); + + overview.servers = geoServers; + return overview; +} + // Get all aggregated metrics from all Prometheus sources app.get('/api/metrics/overview', async (req, res) => { try { - const [sources] = await db.query('SELECT * FROM prometheus_sources'); - if (sources.length === 0) { - return res.json({ - totalServers: 0, - cpu: { used: 0, total: 0, percent: 0 }, - memory: { used: 0, total: 0, percent: 0 }, - disk: { used: 0, total: 0, percent: 0 }, - network: { total: 0, rx: 0, tx: 0 }, - traffic24h: { rx: 0, tx: 0, total: 0 }, - servers: [] - }); - } - - const allMetrics = await Promise.all(sources.map(async (source) => { - const cacheKey = `source_metrics:${source.url}:${source.name}`; - const cached = await cache.get(cacheKey); - if (cached) return cached; - - try { - const metrics = await prometheusService.getOverviewMetrics(source.url, source.name); - await cache.set(cacheKey, metrics, 15); // Cache for 15s - return metrics; - } catch (err) { - console.error(`Error fetching metrics from ${source.name}:`, err.message); - return null; - } - })); - - const validMetrics = allMetrics.filter(m => m !== null); - - // Aggregate across all sources - let totalServers = 0; - let activeServers = 0; - let cpuUsed = 0, cpuTotal = 0; - let memUsed = 0, memTotal = 0; - let diskUsed = 0, diskTotal = 0; - let netRx = 0, netTx = 0; - let traffic24hRx = 0, traffic24hTx = 0; - let allServers = []; - - for (const m of validMetrics) { - totalServers += m.totalServers; - activeServers += m.activeServers || m.totalServers; // Default if missing - cpuUsed += m.cpu.used; - cpuTotal += m.cpu.total; - memUsed += m.memory.used; - memTotal += m.memory.total; - diskUsed += m.disk.used; - diskTotal += m.disk.total; - netRx += m.network.rx; - netTx += m.network.tx; - traffic24hRx += m.traffic24h.rx; - traffic24hTx += m.traffic24h.tx; - allServers = allServers.concat(m.servers); - } - - - - const overview = { - totalServers, - activeServers, - cpu: { - used: cpuUsed, - total: cpuTotal, - percent: cpuTotal > 0 ? (cpuUsed / cpuTotal * 100) : 0 - }, - memory: { - used: memUsed, - total: memTotal, - percent: memTotal > 0 ? (memUsed / memTotal * 100) : 0 - }, - disk: { - used: diskUsed, - total: diskTotal, - percent: diskTotal > 0 ? (diskUsed / diskTotal * 100) : 0 - }, - network: { - total: netRx + netTx, - rx: netRx, - tx: netTx - }, - traffic24h: { - rx: traffic24hRx, - tx: traffic24hTx, - total: traffic24hRx + traffic24hTx - }, - servers: allServers - }; - - // --- Add Geo Information to Servers --- - const geoServers = await Promise.all(overview.servers.map(async (server) => { - // Use originalInstance if available for correct location lookup - const realInstance = server.originalInstance || prometheusService.resolveToken(server.instance); - const cleanIp = realInstance.split(':')[0]; - - let geoData = null; - - // 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) { - geoData = rows[0]; - } else { - // Trigger background resolution for future requests - geoService.getLocation(cleanIp).catch(() => {}); - } - } catch (e) { - // DB error, skip geo for now - } - - // Prepare the server object without sensitive originalInstance - const { originalInstance, ...safeServer } = server; - - if (geoData) { - return { - ...safeServer, - country: geoData.country, - countryName: geoData.country_name, - city: geoData.city, - lat: geoData.latitude, - lng: geoData.longitude - }; - } - return safeServer; - })); - - overview.servers = geoServers; + const overview = await getOverview(); res.json(overview); } catch (err) { console.error('Error fetching overview metrics:', err); @@ -820,11 +821,38 @@ app.get('*', (req, res, next) => { }); +// ==================== WebSocket Server ==================== + +const server = http.createServer(app); +const wss = new WebSocket.Server({ server }); + +function broadcast(data) { + const message = JSON.stringify(data); + wss.clients.forEach(client => { + if (client.readyState === WebSocket.OPEN) { + client.send(message); + } + }); +} + +// Broadcast loop +async function broadcastMetrics() { + try { + const overview = await getOverview(); + broadcast({ type: 'overview', data: overview }); + } catch (err) { + // console.error('WS Broadcast error:', err.message); + } +} + // Check and fix database integrity on startup checkAndFixDatabase(); -app.listen(PORT, HOST, () => { - console.log(`\n 🚀 Data Visualization Display Wall`); +const REFRESH_INT = parseInt(process.env.REFRESH_INTERVAL) || 5000; +setInterval(broadcastMetrics, REFRESH_INT); + +server.listen(PORT, HOST, () => { + console.log(`\n 🚀 Data Visualization Display Wall (WebSocket Enabled)`); console.log(` 📊 Server running at http://${HOST === '0.0.0.0' ? 'localhost' : HOST}:${PORT}`); console.log(` ⚙️ Configure Prometheus sources at http://${HOST === '0.0.0.0' ? 'localhost' : HOST}:${PORT}/settings\n`); });