| |
| """ |
| Web Interface for GPU Monitoring |
| |
| Provides a web-based dashboard for remote GPU monitoring with real-time charts, |
| historical data, and fan control capabilities. |
| """ |
|
|
| from flask import Flask, render_template, jsonify, request, redirect, url_for |
| from flask_cors import CORS |
| import json |
| import time |
| import logging |
| from datetime import datetime, timedelta |
| from typing import Dict, List, Any, Optional |
|
|
| from gpu_monitoring import GPUManager, GPUStatus |
| from gpu_fan_controller import FanController, FanMode, ProfileType |
|
|
| logger = logging.getLogger(__name__) |
|
|
| app = Flask(__name__) |
| CORS(app, origins="*") |
|
|
|
|
| class WebGPUManager: |
| """Web interface manager for GPU monitoring.""" |
| |
| def __init__(self): |
| self.gpu_manager = GPUManager() |
| self.fan_controller = FanController() |
| self.config = self.load_config() |
| |
| |
| self.gpu_manager.initialize() |
| self.fan_controller.initialize() |
| |
| def load_config(self) -> Dict: |
| """Load web interface configuration.""" |
| try: |
| with open('config/monitoring.json', 'r') as f: |
| config = json.load(f) |
| return config.get('web', {}) |
| except: |
| return { |
| 'enabled': True, |
| 'host': '0.0.0.0', |
| 'port': 5000, |
| 'debug': False |
| } |
| |
| def get_current_status(self) -> Dict[str, Any]: |
| """Get current GPU status.""" |
| status_dict = self.gpu_manager.get_status() |
| fan_status = self.fan_controller.get_status() |
| |
| result = { |
| 'timestamp': time.time(), |
| 'gpus': {}, |
| 'fan_control': { |
| 'mode': fan_status.mode.value if fan_status else 'unknown', |
| 'profile': fan_status.profile if fan_status else 'unknown', |
| 'current_pwm': fan_status.current_pwm if fan_status else 0, |
| 'temperature': fan_status.temperature if fan_status else 0.0 |
| } |
| } |
| |
| for gpu_name, gpu_status in status_dict.items(): |
| if gpu_status: |
| result['gpus'][gpu_name] = { |
| 'temperature': gpu_status.temperature, |
| 'load': gpu_status.load, |
| 'fan_speed': gpu_status.fan_speed, |
| 'fan_pwm': gpu_status.fan_pwm, |
| 'power_draw': gpu_status.power_draw, |
| 'memory_used': gpu_status.memory_used, |
| 'memory_total': gpu_status.memory_total, |
| 'core_clock': gpu_status.core_clock, |
| 'memory_clock': gpu_status.memory_clock, |
| 'voltage': gpu_status.voltage, |
| 'efficiency': gpu_status.efficiency |
| } |
| |
| return result |
| |
| def get_historical_data(self, gpu_name: str, hours: int = 24) -> List[Dict[str, Any]]: |
| """Get historical data for a GPU.""" |
| return self.gpu_manager.get_historical_data(gpu_name, hours) |
| |
| def get_gpu_list(self) -> List[str]: |
| """Get list of available GPUs.""" |
| return self.gpu_manager.get_gpu_list() |
| |
| def get_fan_profiles(self) -> Dict[str, Any]: |
| """Get available fan profiles.""" |
| profiles = self.fan_controller.get_profiles() |
| result = {} |
| |
| for name, profile in profiles.items(): |
| result[name] = { |
| 'name': profile.name, |
| 'type': profile.profile_type.value, |
| 'description': profile.description, |
| 'curve': profile.curve, |
| 'safety': profile.safety, |
| 'enabled': profile.enabled |
| } |
| |
| return result |
| |
| def set_fan_profile(self, profile_name: str) -> bool: |
| """Set fan profile.""" |
| return self.fan_controller.set_profile(profile_name) |
| |
| def set_fan_mode(self, mode: str) -> bool: |
| """Set fan mode.""" |
| try: |
| fan_mode = FanMode(mode) |
| self.fan_controller.set_mode(fan_mode) |
| return True |
| except: |
| return False |
| |
| def set_manual_pwm(self, pwm: int) -> bool: |
| """Set manual PWM.""" |
| if 0 <= pwm <= 255: |
| self.fan_controller.set_manual_pwm(pwm) |
| return True |
| return False |
|
|
|
|
| |
| web_manager = WebGPUManager() |
|
|
|
|
| @app.route('/') |
| def index(): |
| """Main dashboard page.""" |
| return render_template('index.html') |
|
|
|
|
| @app.route('/api/status') |
| def api_status(): |
| """API endpoint for current status.""" |
| try: |
| status = web_manager.get_current_status() |
| return jsonify(status) |
| except Exception as e: |
| logger.error(f"Error getting status: {e}") |
| return jsonify({'error': str(e)}), 500 |
|
|
|
|
| @app.route('/api/gpus') |
| def api_gpus(): |
| """API endpoint for GPU list.""" |
| try: |
| gpus = web_manager.get_gpu_list() |
| return jsonify({'gpus': gpus}) |
| except Exception as e: |
| logger.error(f"Error getting GPU list: {e}") |
| return jsonify({'error': str(e)}), 500 |
|
|
|
|
| @app.route('/api/history/<gpu_name>') |
| def api_history(gpu_name): |
| """API endpoint for historical data.""" |
| try: |
| hours = request.args.get('hours', 24, type=int) |
| data = web_manager.get_historical_data(gpu_name, hours) |
| return jsonify({'data': data}) |
| except Exception as e: |
| logger.error(f"Error getting historical data: {e}") |
| return jsonify({'error': str(e)}), 500 |
|
|
|
|
| @app.route('/api/fan/profiles') |
| def api_fan_profiles(): |
| """API endpoint for fan profiles.""" |
| try: |
| profiles = web_manager.get_fan_profiles() |
| return jsonify({'profiles': profiles}) |
| except Exception as e: |
| logger.error(f"Error getting fan profiles: {e}") |
| return jsonify({'error': str(e)}), 500 |
|
|
|
|
| @app.route('/api/fan/profile', methods=['POST']) |
| def api_set_fan_profile(): |
| """API endpoint to set fan profile.""" |
| try: |
| data = request.get_json() |
| profile_name = data.get('profile') |
| |
| if not profile_name: |
| return jsonify({'error': 'Profile name required'}), 400 |
| |
| success = web_manager.set_fan_profile(profile_name) |
| |
| if success: |
| return jsonify({'success': True, 'message': f'Set profile to {profile_name}'}) |
| else: |
| return jsonify({'error': f'Failed to set profile {profile_name}'}), 400 |
| |
| except Exception as e: |
| logger.error(f"Error setting fan profile: {e}") |
| return jsonify({'error': str(e)}), 500 |
|
|
|
|
| @app.route('/api/fan/mode', methods=['POST']) |
| def api_set_fan_mode(): |
| """API endpoint to set fan mode.""" |
| try: |
| data = request.get_json() |
| mode = data.get('mode') |
| |
| if not mode: |
| return jsonify({'error': 'Mode required'}), 400 |
| |
| success = web_manager.set_fan_mode(mode) |
| |
| if success: |
| return jsonify({'success': True, 'message': f'Set mode to {mode}'}) |
| else: |
| return jsonify({'error': f'Failed to set mode {mode}'}), 400 |
| |
| except Exception as e: |
| logger.error(f"Error setting fan mode: {e}") |
| return jsonify({'error': str(e)}), 500 |
|
|
|
|
| @app.route('/api/fan/manual', methods=['POST']) |
| def api_set_manual_pwm(): |
| """API endpoint to set manual PWM.""" |
| try: |
| data = request.get_json() |
| pwm = data.get('pwm') |
| |
| if pwm is None: |
| return jsonify({'error': 'PWM value required'}), 400 |
| |
| success = web_manager.set_manual_pwm(pwm) |
| |
| if success: |
| return jsonify({'success': True, 'message': f'Set manual PWM to {pwm}'}) |
| else: |
| return jsonify({'error': f'Invalid PWM value: {pwm}'}), 400 |
| |
| except Exception as e: |
| logger.error(f"Error setting manual PWM: {e}") |
| return jsonify({'error': str(e)}), 500 |
|
|
|
|
| @app.route('/api/alerts') |
| def api_alerts(): |
| """API endpoint for alerts.""" |
| try: |
| |
| |
| alerts = [] |
| return jsonify({'alerts': alerts}) |
| except Exception as e: |
| logger.error(f"Error getting alerts: {e}") |
| return jsonify({'error': str(e)}), 500 |
|
|
|
|
| @app.route('/api/system') |
| def api_system(): |
| """API endpoint for system information.""" |
| try: |
| import psutil |
| |
| system_info = { |
| 'cpu_count': psutil.cpu_count(), |
| 'cpu_percent': psutil.cpu_percent(interval=1), |
| 'memory': { |
| 'total': psutil.virtual_memory().total // (1024**3), |
| 'available': psutil.virtual_memory().available // (1024**3), |
| 'percent': psutil.virtual_memory().percent |
| }, |
| 'disk': { |
| 'total': psutil.disk_usage('/').total // (1024**3), |
| 'free': psutil.disk_usage('/').free // (1024**3), |
| 'percent': (psutil.disk_usage('/').used / psutil.disk_usage('/').total) * 100 |
| }, |
| 'uptime': time.time() - psutil.boot_time() |
| } |
| |
| return jsonify(system_info) |
| except Exception as e: |
| logger.error(f"Error getting system info: {e}") |
| return jsonify({'error': str(e)}), 500 |
|
|
|
|
| @app.errorhandler(404) |
| def not_found(error): |
| """Handle 404 errors.""" |
| return jsonify({'error': 'Not found'}), 404 |
|
|
|
|
| @app.errorhandler(500) |
| def internal_error(error): |
| """Handle 500 errors.""" |
| return jsonify({'error': 'Internal server error'}), 500 |
|
|
|
|
| def create_templates(): |
| """Create HTML templates directory and files.""" |
| templates_dir = Path('templates') |
| static_dir = Path('static') |
| |
| templates_dir.mkdir(exist_ok=True) |
| static_dir.mkdir(exist_ok=True) |
| |
| |
| index_html = """ |
| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>GPU Monitoring Dashboard</title> |
| <style> |
| :root { |
| --bg-color: #1a1a1a; |
| --card-bg: #2d2d2d; |
| --text-color: #ffffff; |
| --accent-color: #3498db; |
| --success-color: #2ecc71; |
| --warning-color: #f1c40f; |
| --danger-color: #e74c3c; |
| } |
| |
| body { |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; |
| background-color: var(--bg-color); |
| color: var(--text-color); |
| margin: 0; |
| padding: 20px; |
| } |
| |
| .header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 20px; |
| border-bottom: 2px solid var(--accent-color); |
| padding-bottom: 10px; |
| } |
| |
| .header h1 { |
| margin: 0; |
| color: var(--accent-color); |
| } |
| |
| .container { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 20px; |
| } |
| |
| .card { |
| background-color: var(--card-bg); |
| border-radius: 8px; |
| padding: 20px; |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); |
| } |
| |
| .card h3 { |
| margin-top: 0; |
| color: var(--accent-color); |
| } |
| |
| .metric-grid { |
| display: grid; |
| grid-template-columns: repeat(2, 1fr); |
| gap: 15px; |
| } |
| |
| .metric { |
| background-color: rgba(0, 0, 0, 0.3); |
| padding: 15px; |
| border-radius: 4px; |
| text-align: center; |
| } |
| |
| .metric-value { |
| font-size: 24px; |
| font-weight: bold; |
| margin-bottom: 5px; |
| } |
| |
| .metric-label { |
| font-size: 12px; |
| color: #888; |
| text-transform: uppercase; |
| } |
| |
| .temp-good { color: var(--success-color); } |
| .temp-warn { color: var(--warning-color); } |
| .temp-danger { color: var(--danger-color); } |
| |
| .chart-container { |
| width: 100%; |
| height: 300px; |
| margin-top: 20px; |
| } |
| |
| .controls { |
| display: flex; |
| gap: 10px; |
| margin-bottom: 10px; |
| } |
| |
| select, button { |
| padding: 8px 16px; |
| border: none; |
| border-radius: 4px; |
| background-color: var(--accent-color); |
| color: white; |
| cursor: pointer; |
| font-weight: bold; |
| } |
| |
| button:hover { |
| background-color: #2980b9; |
| } |
| |
| .status-indicator { |
| display: inline-block; |
| width: 10px; |
| height: 10px; |
| border-radius: 50%; |
| background-color: var(--success-color); |
| margin-right: 5px; |
| } |
| |
| .status-offline { |
| background-color: var(--danger-color); |
| } |
| |
| @media (max-width: 768px) { |
| .container { |
| grid-template-columns: 1fr; |
| } |
| |
| .metric-grid { |
| grid-template-columns: repeat(2, 1fr); |
| } |
| } |
| </style> |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> |
| </head> |
| <body> |
| <div class="header"> |
| <h1>GPU Monitoring Dashboard</h1> |
| <div> |
| <span class="status-indicator" id="status-indicator"></span> |
| <span id="status-text">Connecting...</span> |
| </div> |
| </div> |
| |
| <div class="container"> |
| <div class="card"> |
| <h3>Real-time Status</h3> |
| <div class="metric-grid" id="metrics-grid"> |
| <!-- Metrics will be populated by JavaScript --> |
| </div> |
| </div> |
| |
| <div class="card"> |
| <h3>Fan Control</h3> |
| <div class="controls"> |
| <select id="profile-select"> |
| <option value="">Select Profile</option> |
| </select> |
| <button onclick="setProfile()">Apply Profile</button> |
| </div> |
| <div class="controls"> |
| <input type="number" id="manual-pwm" min="0" max="255" value="0" style="padding: 8px; border-radius: 4px; border: 1px solid #555; background: #333; color: white;"> |
| <button onclick="setManualPWM()">Set Manual PWM</button> |
| </div> |
| <div style="margin-top: 15px;"> |
| <div>Fan Mode: <span id="fan-mode">--</span></div> |
| <div>Current Profile: <span id="current-profile">--</span></div> |
| <div>Current PWM: <span id="current-pwm">--</span>%</div> |
| </div> |
| </div> |
| |
| <div class="card" style="grid-column: 1 / -1;"> |
| <h3>Temperature History</h3> |
| <div class="controls"> |
| <select id="gpu-select"> |
| <option value="">Select GPU</option> |
| </select> |
| <select id="hours-select"> |
| <option value="1">1 Hour</option> |
| <option value="6">6 Hours</option> |
| <option value="24" selected>24 Hours</option> |
| <option value="168">7 Days</option> |
| </select> |
| <button onclick="loadHistory()">Load History</button> |
| </div> |
| <div class="chart-container"> |
| <canvas id="temp-chart"></canvas> |
| </div> |
| </div> |
| </div> |
| |
| <script> |
| let tempChart; |
| let updateInterval; |
| |
| // Initialize chart |
| function initChart() { |
| const ctx = document.getElementById('temp-chart').getContext('2d'); |
| tempChart = new Chart(ctx, { |
| type: 'line', |
| data: { |
| labels: [], |
| datasets: [{ |
| label: 'Temperature (°C)', |
| data: [], |
| borderColor: '#3498db', |
| backgroundColor: 'rgba(52, 152, 219, 0.1)', |
| borderWidth: 2, |
| fill: true |
| }] |
| }, |
| options: { |
| responsive: true, |
| maintainAspectRatio: false, |
| scales: { |
| y: { |
| beginAtZero: true, |
| max: 100 |
| } |
| } |
| } |
| }); |
| } |
| |
| // Update real-time status |
| function updateStatus() { |
| fetch('/api/status') |
| .then(response => response.json()) |
| .then(data => { |
| updateMetrics(data); |
| updateFanControl(data.fan_control); |
| updateStatusIndicator(true); |
| }) |
| .catch(error => { |
| console.error('Error fetching status:', error); |
| updateStatusIndicator(false); |
| }); |
| } |
| |
| // Update metrics display |
| function updateMetrics(data) { |
| const grid = document.getElementById('metrics-grid'); |
| grid.innerHTML = ''; |
| |
| for (const [gpuName, gpuData] of Object.entries(data.gpus)) { |
| const card = document.createElement('div'); |
| card.className = 'card'; |
| card.innerHTML = ` |
| <h4>${gpuName}</h4> |
| <div class="metric-grid"> |
| <div class="metric"> |
| <div class="metric-value temp-${getTempClass(gpuData.temperature)}">${gpuData.temperature.toFixed(1)}°C</div> |
| <div class="metric-label">Temperature</div> |
| </div> |
| <div class="metric"> |
| <div class="metric-value">${gpuData.load.toFixed(1)}%</div> |
| <div class="metric-label">Load</div> |
| </div> |
| <div class="metric"> |
| <div class="metric-value">${gpuData.fan_speed} RPM</div> |
| <div class="metric-label">Fan Speed</div> |
| </div> |
| <div class="metric"> |
| <div class="metric-value">${gpuData.power_draw.toFixed(1)} W</div> |
| <div class="metric-label">Power</div> |
| </div> |
| <div class="metric"> |
| <div class="metric-value">${gpuData.memory_used}/${gpuData.memory_total} MB</div> |
| <div class="metric-label">VRAM</div> |
| </div> |
| <div class="metric"> |
| <div class="metric-value">${gpuData.core_clock} MHz</div> |
| <div class="metric-label">Core Clock</div> |
| </div> |
| </div> |
| `; |
| grid.appendChild(card); |
| } |
| } |
| |
| // Get temperature color class |
| function getTempClass(temp) { |
| if (temp < 60) return 'good'; |
| if (temp < 75) return 'warn'; |
| return 'danger'; |
| } |
| |
| // Update fan control display |
| function updateFanControl(fanData) { |
| document.getElementById('fan-mode').textContent = fanData.mode; |
| document.getElementById('current-profile').textContent = fanData.profile; |
| document.getElementById('current-pwm').textContent = fanData.current_pwm; |
| } |
| |
| // Update status indicator |
| function updateStatusIndicator(online) { |
| const indicator = document.getElementById('status-indicator'); |
| const text = document.getElementById('status-text'); |
| |
| if (online) { |
| indicator.className = 'status-indicator'; |
| text.textContent = 'Online'; |
| } else { |
| indicator.className = 'status-indicator status-offline'; |
| text.textContent = 'Offline'; |
| } |
| } |
| |
| // Load fan profiles |
| function loadProfiles() { |
| fetch('/api/fan/profiles') |
| .then(response => response.json()) |
| .then(data => { |
| const select = document.getElementById('profile-select'); |
| select.innerHTML = '<option value="">Select Profile</option>'; |
| |
| for (const [name, profile] of Object.entries(data.profiles)) { |
| if (profile.enabled) { |
| const option = document.createElement('option'); |
| option.value = name; |
| option.textContent = profile.name; |
| select.appendChild(option); |
| } |
| } |
| }) |
| .catch(error => console.error('Error loading profiles:', error)); |
| } |
| |
| // Load GPU list |
| function loadGPUs() { |
| fetch('/api/gpus') |
| .then(response => response.json()) |
| .then(data => { |
| const select = document.getElementById('gpu-select'); |
| select.innerHTML = '<option value="">Select GPU</option>'; |
| |
| data.gpus.forEach(gpu => { |
| const option = document.createElement('option'); |
| option.value = gpu; |
| option.textContent = gpu; |
| select.appendChild(option); |
| }); |
| }) |
| .catch(error => console.error('Error loading GPUs:', error)); |
| } |
| |
| // Set fan profile |
| function setProfile() { |
| const profile = document.getElementById('profile-select').value; |
| if (!profile) return; |
| |
| fetch('/api/fan/profile', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify({ profile: profile }) |
| }) |
| .then(response => response.json()) |
| .then(data => { |
| if (data.success) { |
| alert('Profile updated successfully'); |
| updateStatus(); |
| } else { |
| alert('Error: ' + data.error); |
| } |
| }) |
| .catch(error => { |
| console.error('Error:', error); |
| alert('Error updating profile'); |
| }); |
| } |
| |
| // Set manual PWM |
| function setManualPWM() { |
| const pwm = parseInt(document.getElementById('manual-pwm').value); |
| if (isNaN(pwm) || pwm < 0 || pwm > 255) { |
| alert('Please enter a valid PWM value (0-255)'); |
| return; |
| } |
| |
| fetch('/api/fan/manual', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify({ pwm: pwm }) |
| }) |
| .then(response => response.json()) |
| .then(data => { |
| if (data.success) { |
| alert('Manual PWM set successfully'); |
| updateStatus(); |
| } else { |
| alert('Error: ' + data.error); |
| } |
| }) |
| .catch(error => { |
| console.error('Error:', error); |
| alert('Error setting manual PWM'); |
| }); |
| } |
| |
| // Load historical data |
| function loadHistory() { |
| const gpu = document.getElementById('gpu-select').value; |
| const hours = document.getElementById('hours-select').value; |
| |
| if (!gpu) { |
| alert('Please select a GPU'); |
| return; |
| } |
| |
| fetch(`/api/history/${encodeURIComponent(gpu)}?hours=${hours}`) |
| .then(response => response.json()) |
| .then(data => { |
| updateChart(data.data); |
| }) |
| .catch(error => { |
| console.error('Error loading history:', error); |
| alert('Error loading historical data'); |
| }); |
| } |
| |
| // Update chart with historical data |
| function updateChart(data) { |
| if (!data || data.length === 0) return; |
| |
| const labels = data.map(d => new Date(d.timestamp * 1000).toLocaleTimeString()); |
| const temps = data.map(d => d.temperature); |
| |
| tempChart.data.labels = labels; |
| tempChart.data.datasets[0].data = temps; |
| tempChart.update(); |
| } |
| |
| // Initialize application |
| document.addEventListener('DOMContentLoaded', function() { |
| initChart(); |
| loadProfiles(); |
| loadGPUs(); |
| updateStatus(); |
| |
| // Update status every 2 seconds |
| updateInterval = setInterval(updateStatus, 2000); |
| }); |
| </script> |
| </body> |
| </html> |
| """ |
| |
| with open(templates_dir / 'index.html', 'w') as f: |
| f.write(index_html) |
|
|
|
|
| if __name__ == '__main__': |
| |
| create_templates() |
| |
| |
| config = web_manager.config |
| |
| |
| app.run( |
| host=config.get('host', '0.0.0.0'), |
| port=config.get('port', 5000), |
| debug=config.get('debug', False) |
| ) |