WebSockets & Real-Time Apps
Building a Real-Time Dashboard Project
Building a Real-Time Dashboard Project
Real-time dashboards provide instant visibility into system metrics, user activity, and business data. In this comprehensive lesson, we'll build a production-ready dashboard that displays live data using WebSockets, implementing charts, metrics, and monitoring capabilities.
Dashboard Architecture Overview
A well-designed real-time dashboard consists of several key components:
Architecture Components:
- Data Collection Layer: Gathers metrics from various sources
- WebSocket Server: Broadcasts updates to connected clients
- Frontend Dashboard: Displays data with visualizations
- Data Aggregation: Processes and summarizes raw data
- Alert System: Notifies users of critical events
Backend Dashboard Server
Let's build a Node.js server that collects and broadcasts dashboard metrics:
// dashboard-server.js\nconst WebSocket = require('ws');\nconst express = require('express');\nconst os = require('os');\n\nconst app = express();\nconst server = require('http').createServer(app);\nconst wss = new WebSocket.Server({ server });\n\n// Store active connections\nconst clients = new Set();\n\n// Dashboard metrics storage\nconst metrics = {\n activeUsers: 0,\n systemHealth: {\n cpu: 0,\n memory: 0,\n uptime: 0\n },\n realtimeStats: {\n requestsPerMinute: 0,\n errorsPerMinute: 0,\n avgResponseTime: 0\n },\n userActivity: [],\n alerts: []\n};\n\n// Simulate activity tracking\nconst activityLog = [];\nconst MAX_ACTIVITY_ENTRIES = 100;\n\nwss.on('connection', (ws) => {\n console.log('Dashboard client connected');\n clients.add(ws);\n metrics.activeUsers = clients.size;\n\n // Send initial data\n ws.send(JSON.stringify({\n type: 'initial',\n data: metrics\n }));\n\n // Broadcast user count update\n broadcastMetric('activeUsers', metrics.activeUsers);\n\n ws.on('close', () => {\n clients.delete(ws);\n metrics.activeUsers = clients.size;\n broadcastMetric('activeUsers', metrics.activeUsers);\n });\n\n ws.on('error', console.error);\n});\n\n// Broadcast helper function\nfunction broadcastMetric(type, data) {\n const message = JSON.stringify({ type, data, timestamp: Date.now() });\n clients.forEach(client => {\n if (client.readyState === WebSocket.OPEN) {\n client.send(message);\n }\n });\n}\n\n// Collect system health metrics\nfunction collectSystemHealth() {\n const cpuUsage = os.loadavg()[0] / os.cpus().length * 100;\n const totalMem = os.totalmem();\n const freeMem = os.freemem();\n const memUsage = ((totalMem - freeMem) / totalMem) * 100;\n\n metrics.systemHealth = {\n cpu: Math.round(cpuUsage * 100) / 100,\n memory: Math.round(memUsage * 100) / 100,\n uptime: Math.round(os.uptime())\n };\n\n broadcastMetric('systemHealth', metrics.systemHealth);\n\n // Check for alerts\n if (cpuUsage > 80) {\n addAlert('warning', 'High CPU usage detected: ' + cpuUsage.toFixed(1) + '%');\n }\n if (memUsage > 85) {\n addAlert('danger', 'High memory usage detected: ' + memUsage.toFixed(1) + '%');\n }\n}\n\n// Simulate real-time statistics\nfunction updateRealtimeStats() {\n metrics.realtimeStats = {\n requestsPerMinute: Math.floor(Math.random() * 1000) + 500,\n errorsPerMinute: Math.floor(Math.random() * 10),\n avgResponseTime: Math.floor(Math.random() * 200) + 50\n };\n\n broadcastMetric('realtimeStats', metrics.realtimeStats);\n}\n\n// Track user activity\nfunction logUserActivity(action, details) {\n const activity = {\n id: Date.now(),\n action,\n details,\n timestamp: new Date().toISOString(),\n user: 'User' + Math.floor(Math.random() * 1000)\n };\n\n activityLog.unshift(activity);\n if (activityLog.length > MAX_ACTIVITY_ENTRIES) {\n activityLog.pop();\n }\n\n metrics.userActivity = activityLog.slice(0, 10); // Send last 10\n broadcastMetric('userActivity', metrics.userActivity);\n}\n\n// Alert system\nfunction addAlert(level, message) {\n const alert = {\n id: Date.now(),\n level, // info, warning, danger\n message,\n timestamp: new Date().toISOString()\n };\n\n metrics.alerts.unshift(alert);\n if (metrics.alerts.length > 50) {\n metrics.alerts.pop();\n }\n\n broadcastMetric('alert', alert);\n}\n\n// Simulate user activity\nfunction simulateUserActivity() {\n const actions = [\n { action: 'login', details: 'User logged in' },\n { action: 'page_view', details: 'Viewed dashboard' },\n { action: 'data_export', details: 'Exported report' },\n { action: 'settings_change', details: 'Updated preferences' },\n { action: 'logout', details: 'User logged out' }\n ];\n\n const randomAction = actions[Math.floor(Math.random() * actions.length)];\n logUserActivity(randomAction.action, randomAction.details);\n}\n\n// Start metric collection intervals\nsetInterval(collectSystemHealth, 2000); // Every 2 seconds\nsetInterval(updateRealtimeStats, 3000); // Every 3 seconds\nsetInterval(simulateUserActivity, 5000); // Every 5 seconds\n\n// Initial alert\naddAlert('info', 'Dashboard server started successfully');\n\nserver.listen(3000, () => {\n console.log('Dashboard server running on port 3000');\n});Frontend Dashboard HTML
Create a comprehensive dashboard interface with multiple widgets:
<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n <meta name="viewport" content="width=device-width, initial-scale=1.0">\n <title>Real-Time Dashboard</title>\n <style>\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n background: #f5f7fa;\n color: #2d3748;\n }\n\n .header {\n background: #2c3e50;\n color: white;\n padding: 1rem 2rem;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n\n .header h1 {\n font-size: 1.5rem;\n font-weight: 600;\n }\n\n .connection-status {\n display: inline-block;\n padding: 0.25rem 0.75rem;\n border-radius: 12px;\n font-size: 0.875rem;\n margin-left: 1rem;\n }\n\n .status-connected {\n background: #10b981;\n color: white;\n }\n\n .status-disconnected {\n background: #ef4444;\n color: white;\n }\n\n .dashboard {\n padding: 2rem;\n max-width: 1400px;\n margin: 0 auto;\n }\n\n .metrics-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));\n gap: 1.5rem;\n margin-bottom: 2rem;\n }\n\n .metric-card {\n background: white;\n padding: 1.5rem;\n border-radius: 8px;\n box-shadow: 0 1px 3px rgba(0,0,0,0.1);\n }\n\n .metric-label {\n font-size: 0.875rem;\n color: #6b7280;\n margin-bottom: 0.5rem;\n }\n\n .metric-value {\n font-size: 2rem;\n font-weight: 700;\n color: #1f2937;\n }\n\n .metric-unit {\n font-size: 1rem;\n color: #9ca3af;\n }\n\n .chart-container {\n background: white;\n padding: 1.5rem;\n border-radius: 8px;\n box-shadow: 0 1px 3px rgba(0,0,0,0.1);\n margin-bottom: 2rem;\n }\n\n .chart-title {\n font-size: 1.125rem;\n font-weight: 600;\n margin-bottom: 1rem;\n }\n\n .activity-feed {\n background: white;\n padding: 1.5rem;\n border-radius: 8px;\n box-shadow: 0 1px 3px rgba(0,0,0,0.1);\n max-height: 400px;\n overflow-y: auto;\n }\n\n .activity-item {\n padding: 0.75rem;\n border-left: 3px solid #3b82f6;\n margin-bottom: 0.75rem;\n background: #f9fafb;\n border-radius: 4px;\n }\n\n .activity-action {\n font-weight: 600;\n color: #1f2937;\n }\n\n .activity-time {\n font-size: 0.75rem;\n color: #6b7280;\n margin-top: 0.25rem;\n }\n\n .alerts-container {\n background: white;\n padding: 1.5rem;\n border-radius: 8px;\n box-shadow: 0 1px 3px rgba(0,0,0,0.1);\n }\n\n .alert {\n padding: 1rem;\n border-radius: 6px;\n margin-bottom: 0.75rem;\n border-left: 4px solid;\n }\n\n .alert-info {\n background: #dbeafe;\n border-color: #3b82f6;\n color: #1e40af;\n }\n\n .alert-warning {\n background: #fef3c7;\n border-color: #f59e0b;\n color: #92400e;\n }\n\n .alert-danger {\n background: #fee2e2;\n border-color: #ef4444;\n color: #991b1b;\n }\n\n .progress-bar {\n width: 100%;\n height: 8px;\n background: #e5e7eb;\n border-radius: 4px;\n overflow: hidden;\n margin-top: 0.5rem;\n }\n\n .progress-fill {\n height: 100%;\n background: #3b82f6;\n transition: width 0.3s ease;\n }\n\n .progress-fill.warning {\n background: #f59e0b;\n }\n\n .progress-fill.danger {\n background: #ef4444;\n }\n </style>\n</head>\n<body>\n <div class="header">\n <h1>\n Real-Time Dashboard\n <span id="connectionStatus" class="connection-status status-disconnected">Disconnected</span>\n </h1>\n </div>\n\n <div class="dashboard">\n <!-- Key Metrics -->\n <div class="metrics-grid">\n <div class="metric-card">\n <div class="metric-label">Active Users</div>\n <div class="metric-value"><span id="activeUsers">0</span></div>\n </div>\n <div class="metric-card">\n <div class="metric-label">Requests/Min</div>\n <div class="metric-value"><span id="requestsPerMin">0</span></div>\n </div>\n <div class="metric-card">\n <div class="metric-label">Avg Response Time</div>\n <div class="metric-value"><span id="avgResponse">0</span> <span class="metric-unit">ms</span></div>\n </div>\n <div class="metric-card">\n <div class="metric-label">Error Rate</div>\n <div class="metric-value"><span id="errorRate">0</span> <span class="metric-unit">/min</span></div>\n </div>\n </div>\n\n <!-- System Health -->\n <div class="chart-container">\n <div class="chart-title">System Health</div>\n <div style="margin-bottom: 1rem;">\n <div class="metric-label">CPU Usage</div>\n <div><span id="cpuValue">0</span>%</div>\n <div class="progress-bar">\n <div id="cpuProgress" class="progress-fill" style="width: 0%"></div>\n </div>\n </div>\n <div>\n <div class="metric-label">Memory Usage</div>\n <div><span id="memValue">0</span>%</div>\n <div class="progress-bar">\n <div id="memProgress" class="progress-fill" style="width: 0%"></div>\n </div>\n </div>\n </div>\n\n <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem;">\n <!-- User Activity Feed -->\n <div class="activity-feed">\n <div class="chart-title">Recent Activity</div>\n <div id="activityFeed"></div>\n </div>\n\n <!-- Alerts -->\n <div class="alerts-container">\n <div class="chart-title">System Alerts</div>\n <div id="alertsContainer"></div>\n </div>\n </div>\n </div>\n\n <script src="dashboard.js"></script>\n</body>\n</html>Dashboard JavaScript Logic
Implement the WebSocket client with data visualization:
// dashboard.js\nclass DashboardClient {\n constructor() {\n this.ws = null;\n this.reconnectAttempts = 0;\n this.maxReconnectAttempts = 5;\n this.reconnectDelay = 3000;\n this.connect();\n }\n\n connect() {\n this.ws = new WebSocket('ws://localhost:3000');\n\n this.ws.onopen = () => {\n console.log('Connected to dashboard server');\n this.reconnectAttempts = 0;\n this.updateConnectionStatus(true);\n };\n\n this.ws.onmessage = (event) => {\n const message = JSON.parse(event.data);\n this.handleMessage(message);\n };\n\n this.ws.onerror = (error) => {\n console.error('WebSocket error:', error);\n };\n\n this.ws.onclose = () => {\n console.log('Disconnected from dashboard server');\n this.updateConnectionStatus(false);\n this.attemptReconnect();\n };\n }\n\n handleMessage(message) {\n switch(message.type) {\n case 'initial':\n this.updateAllMetrics(message.data);\n break;\n case 'activeUsers':\n this.updateActiveUsers(message.data);\n break;\n case 'systemHealth':\n this.updateSystemHealth(message.data);\n break;\n case 'realtimeStats':\n this.updateRealtimeStats(message.data);\n break;\n case 'userActivity':\n this.updateActivityFeed(message.data);\n break;\n case 'alert':\n this.addAlert(message.data);\n break;\n }\n }\n\n updateConnectionStatus(connected) {\n const statusEl = document.getElementById('connectionStatus');\n if (connected) {\n statusEl.textContent = 'Connected';\n statusEl.className = 'connection-status status-connected';\n } else {\n statusEl.textContent = 'Disconnected';\n statusEl.className = 'connection-status status-disconnected';\n }\n }\n\n updateAllMetrics(data) {\n this.updateActiveUsers(data.activeUsers);\n this.updateSystemHealth(data.systemHealth);\n this.updateRealtimeStats(data.realtimeStats);\n this.updateActivityFeed(data.userActivity);\n data.alerts.forEach(alert => this.addAlert(alert));\n }\n\n updateActiveUsers(count) {\n document.getElementById('activeUsers').textContent = count;\n this.animateValue('activeUsers', count);\n }\n\n updateSystemHealth(health) {\n // Update CPU\n document.getElementById('cpuValue').textContent = health.cpu.toFixed(1);\n const cpuProgress = document.getElementById('cpuProgress');\n cpuProgress.style.width = health.cpu + '%';\n cpuProgress.className = 'progress-fill ' + this.getHealthClass(health.cpu);\n\n // Update Memory\n document.getElementById('memValue').textContent = health.memory.toFixed(1);\n const memProgress = document.getElementById('memProgress');\n memProgress.style.width = health.memory + '%';\n memProgress.className = 'progress-fill ' + this.getHealthClass(health.memory);\n }\n\n getHealthClass(value) {\n if (value > 85) return 'danger';\n if (value > 70) return 'warning';\n return '';\n }\n\n updateRealtimeStats(stats) {\n document.getElementById('requestsPerMin').textContent = stats.requestsPerMinute;\n document.getElementById('avgResponse').textContent = stats.avgResponseTime;\n document.getElementById('errorRate').textContent = stats.errorsPerMinute;\n }\n\n updateActivityFeed(activities) {\n const feed = document.getElementById('activityFeed');\n feed.innerHTML = activities.map(activity => `\n <div class="activity-item">\n <div class="activity-action">${activity.user}: ${activity.action}</div>\n <div>${activity.details}</div>\n <div class="activity-time">${this.formatTime(activity.timestamp)}</div>\n </div>\n `).join('');\n }\n\n addAlert(alert) {\n const container = document.getElementById('alertsContainer');\n const alertEl = document.createElement('div');\n alertEl.className = `alert alert-${alert.level}`;\n alertEl.innerHTML = `\n <strong>${alert.level.toUpperCase()}</strong>\n <div>${alert.message}</div>\n <div style="font-size: 0.75rem; margin-top: 0.25rem;">${this.formatTime(alert.timestamp)}</div>\n `;\n container.insertBefore(alertEl, container.firstChild);\n\n // Keep only last 10 alerts\n while (container.children.length > 10) {\n container.removeChild(container.lastChild);\n }\n }\n\n formatTime(timestamp) {\n const date = new Date(timestamp);\n return date.toLocaleTimeString();\n }\n\n animateValue(elementId, newValue) {\n const element = document.getElementById(elementId);\n element.style.transition = 'transform 0.3s ease';\n element.style.transform = 'scale(1.2)';\n setTimeout(() => {\n element.style.transform = 'scale(1)';\n }, 300);\n }\n\n attemptReconnect() {\n if (this.reconnectAttempts < this.maxReconnectAttempts) {\n this.reconnectAttempts++;\n console.log(`Reconnecting... attempt ${this.reconnectAttempts}`);\n setTimeout(() => this.connect(), this.reconnectDelay);\n } else {\n console.error('Max reconnection attempts reached');\n }\n }\n}\n\n// Initialize dashboard\nconst dashboard = new DashboardClient();Best Practices: Implement auto-refresh patterns with exponential backoff, use efficient data structures for time-series data, implement data aggregation to reduce bandwidth, add client-side caching for historical data, and provide manual refresh capabilities.
Advanced Dashboard Features
Enhance the dashboard with additional capabilities:
// Advanced features for dashboard.js\n\n// Time-series data storage\nclass TimeSeriesStore {\n constructor(maxPoints = 60) {\n this.maxPoints = maxPoints;\n this.data = [];\n }\n\n add(value, timestamp = Date.now()) {\n this.data.push({ value, timestamp });\n if (this.data.length > this.maxPoints) {\n this.data.shift();\n }\n }\n\n getLatest(count = 10) {\n return this.data.slice(-count);\n }\n\n getAverage() {\n if (this.data.length === 0) return 0;\n const sum = this.data.reduce((acc, item) => acc + item.value, 0);\n return sum / this.data.length;\n }\n\n getMax() {\n if (this.data.length === 0) return 0;\n return Math.max(...this.data.map(item => item.value));\n }\n}\n\n// Chart visualization using Canvas\nclass SimpleLineChart {\n constructor(canvasId, options = {}) {\n this.canvas = document.getElementById(canvasId);\n this.ctx = this.canvas.getContext('2d');\n this.data = [];\n this.maxPoints = options.maxPoints || 60;\n this.color = options.color || '#3b82f6';\n this.label = options.label || 'Value';\n }\n\n addDataPoint(value) {\n this.data.push(value);\n if (this.data.length > this.maxPoints) {\n this.data.shift();\n }\n this.render();\n }\n\n render() {\n const { width, height } = this.canvas;\n this.ctx.clearRect(0, 0, width, height);\n\n if (this.data.length < 2) return;\n\n const max = Math.max(...this.data, 1);\n const step = width / (this.maxPoints - 1);\n\n this.ctx.beginPath();\n this.ctx.strokeStyle = this.color;\n this.ctx.lineWidth = 2;\n\n this.data.forEach((value, index) => {\n const x = index * step;\n const y = height - (value / max) * height * 0.9;\n if (index === 0) {\n this.ctx.moveTo(x, y);\n } else {\n this.ctx.lineTo(x, y);\n }\n });\n\n this.ctx.stroke();\n }\n}\n\n// Data export functionality\nclass DataExporter {\n static exportToCSV(data, filename) {\n const csv = this.convertToCSV(data);\n const blob = new Blob([csv], { type: 'text/csv' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = filename;\n a.click();\n URL.revokeObjectURL(url);\n }\n\n static convertToCSV(data) {\n if (data.length === 0) return '';\n const headers = Object.keys(data[0]).join(',');\n const rows = data.map(row => \n Object.values(row).map(val => \n typeof val === 'string' ? `"${val}"` : val\n ).join(',')\n );\n return [headers, ...rows].join('\n');\n }\n}\n\n// Date range filtering\nclass DateRangeFilter {\n constructor() {\n this.startDate = null;\n this.endDate = null;\n }\n\n setRange(start, end) {\n this.startDate = new Date(start);\n this.endDate = new Date(end);\n }\n\n filter(data, timestampField = 'timestamp') {\n if (!this.startDate || !this.endDate) return data;\n return data.filter(item => {\n const timestamp = new Date(item[timestampField]);\n return timestamp >= this.startDate && timestamp <= this.endDate;\n });\n }\n}\n\n// Usage example\nconst cpuHistory = new TimeSeriesStore(120);\nconst memoryHistory = new TimeSeriesStore(120);\n\n// Add to dashboard message handler\nfunction enhancedSystemHealthUpdate(health) {\n cpuHistory.add(health.cpu);\n memoryHistory.add(health.memory);\n \n console.log('Average CPU:', cpuHistory.getAverage().toFixed(2));\n console.log('Max Memory:', memoryHistory.getMax().toFixed(2));\n}Performance Considerations: Be mindful of memory usage when storing time-series data in the browser. Limit the number of data points retained, use efficient data structures, implement data aggregation for older data, and consider offloading historical data analysis to the server.
Dashboard Auto-Refresh Patterns
Implement intelligent auto-refresh strategies:
// Auto-refresh manager\nclass AutoRefreshManager {\n constructor() {\n this.refreshIntervals = new Map();\n this.isPageVisible = true;\n this.setupVisibilityHandling();\n }\n\n setupVisibilityHandling() {\n document.addEventListener('visibilitychange', () => {\n this.isPageVisible = !document.hidden;\n if (this.isPageVisible) {\n this.resumeAllRefreshes();\n } else {\n this.pauseAllRefreshes();\n }\n });\n }\n\n register(name, callback, interval, runImmediately = false) {\n if (this.refreshIntervals.has(name)) {\n this.unregister(name);\n }\n\n if (runImmediately) {\n callback();\n }\n\n const intervalId = setInterval(() => {\n if (this.isPageVisible) {\n callback();\n }\n }, interval);\n\n this.refreshIntervals.set(name, {\n callback,\n interval,\n intervalId,\n isPaused: false\n });\n }\n\n unregister(name) {\n const refresh = this.refreshIntervals.get(name);\n if (refresh) {\n clearInterval(refresh.intervalId);\n this.refreshIntervals.delete(name);\n }\n }\n\n pauseAllRefreshes() {\n this.refreshIntervals.forEach((refresh, name) => {\n if (!refresh.isPaused) {\n clearInterval(refresh.intervalId);\n refresh.isPaused = true;\n }\n });\n }\n\n resumeAllRefreshes() {\n this.refreshIntervals.forEach((refresh, name) => {\n if (refresh.isPaused) {\n refresh.intervalId = setInterval(() => {\n if (this.isPageVisible) {\n refresh.callback();\n }\n }, refresh.interval);\n refresh.isPaused = false;\n }\n });\n }\n}\n\n// Usage\nconst refreshManager = new AutoRefreshManager();\n\n// Register different refresh intervals\nrefreshManager.register('systemHealth', () => {\n console.log('Refreshing system health...');\n}, 5000, true);\n\nrefreshManager.register('userActivity', () => {\n console.log('Refreshing user activity...');\n}, 10000, true);Project Challenge: Extend this dashboard to include: 1) A mini line chart for CPU/memory trends over time, 2) Customizable metric thresholds with visual indicators, 3) Dashboard widget drag-and-drop rearrangement, 4) Export functionality for metrics data to CSV, 5) User preferences persistence using localStorage, 6) Dark mode toggle with theme persistence.
Testing the Dashboard
Start the server and test all features:
# Terminal 1: Start dashboard server\nnode dashboard-server.js\n\n# Terminal 2: Serve dashboard HTML (or open directly in browser)\npython -m http.server 8000\n\n# Open browser to http://localhost:8000\n# Observe:\n# - Connection status updates\n# - Live metric updates every 2-3 seconds\n# - System health progress bars\n# - User activity stream\n# - Alert notifications\n# - Smooth animations and transitions
Key Takeaways: Real-time dashboards require careful architecture planning, efficient data broadcasting strategies, responsive UI design, intelligent auto-refresh patterns, and performance optimization. Use WebSockets for instant updates, implement client-side data aggregation, provide visual feedback for all state changes, and ensure graceful degradation when connections fail.