meccatronis commited on
Commit
8d4e0c1
·
verified ·
1 Parent(s): fb37e1c

Upload web_interface.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. web_interface.py +785 -0
web_interface.py ADDED
@@ -0,0 +1,785 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Web Interface for GPU Monitoring
4
+
5
+ Provides a web-based dashboard for remote GPU monitoring with real-time charts,
6
+ historical data, and fan control capabilities.
7
+ """
8
+
9
+ from flask import Flask, render_template, jsonify, request, redirect, url_for
10
+ from flask_cors import CORS
11
+ import json
12
+ import time
13
+ import logging
14
+ from datetime import datetime, timedelta
15
+ from typing import Dict, List, Any, Optional
16
+
17
+ from gpu_monitoring import GPUManager, GPUStatus
18
+ from gpu_fan_controller import FanController, FanMode, ProfileType
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ app = Flask(__name__)
23
+ CORS(app, origins="*") # Allow all origins for local development
24
+
25
+
26
+ class WebGPUManager:
27
+ """Web interface manager for GPU monitoring."""
28
+
29
+ def __init__(self):
30
+ self.gpu_manager = GPUManager()
31
+ self.fan_controller = FanController()
32
+ self.config = self.load_config()
33
+
34
+ # Initialize components
35
+ self.gpu_manager.initialize()
36
+ self.fan_controller.initialize()
37
+
38
+ def load_config(self) -> Dict:
39
+ """Load web interface configuration."""
40
+ try:
41
+ with open('config/monitoring.json', 'r') as f:
42
+ config = json.load(f)
43
+ return config.get('web', {})
44
+ except:
45
+ return {
46
+ 'enabled': True,
47
+ 'host': '0.0.0.0',
48
+ 'port': 5000,
49
+ 'debug': False
50
+ }
51
+
52
+ def get_current_status(self) -> Dict[str, Any]:
53
+ """Get current GPU status."""
54
+ status_dict = self.gpu_manager.get_status()
55
+ fan_status = self.fan_controller.get_status()
56
+
57
+ result = {
58
+ 'timestamp': time.time(),
59
+ 'gpus': {},
60
+ 'fan_control': {
61
+ 'mode': fan_status.mode.value if fan_status else 'unknown',
62
+ 'profile': fan_status.profile if fan_status else 'unknown',
63
+ 'current_pwm': fan_status.current_pwm if fan_status else 0,
64
+ 'temperature': fan_status.temperature if fan_status else 0.0
65
+ }
66
+ }
67
+
68
+ for gpu_name, gpu_status in status_dict.items():
69
+ if gpu_status:
70
+ result['gpus'][gpu_name] = {
71
+ 'temperature': gpu_status.temperature,
72
+ 'load': gpu_status.load,
73
+ 'fan_speed': gpu_status.fan_speed,
74
+ 'fan_pwm': gpu_status.fan_pwm,
75
+ 'power_draw': gpu_status.power_draw,
76
+ 'memory_used': gpu_status.memory_used,
77
+ 'memory_total': gpu_status.memory_total,
78
+ 'core_clock': gpu_status.core_clock,
79
+ 'memory_clock': gpu_status.memory_clock,
80
+ 'voltage': gpu_status.voltage,
81
+ 'efficiency': gpu_status.efficiency
82
+ }
83
+
84
+ return result
85
+
86
+ def get_historical_data(self, gpu_name: str, hours: int = 24) -> List[Dict[str, Any]]:
87
+ """Get historical data for a GPU."""
88
+ return self.gpu_manager.get_historical_data(gpu_name, hours)
89
+
90
+ def get_gpu_list(self) -> List[str]:
91
+ """Get list of available GPUs."""
92
+ return self.gpu_manager.get_gpu_list()
93
+
94
+ def get_fan_profiles(self) -> Dict[str, Any]:
95
+ """Get available fan profiles."""
96
+ profiles = self.fan_controller.get_profiles()
97
+ result = {}
98
+
99
+ for name, profile in profiles.items():
100
+ result[name] = {
101
+ 'name': profile.name,
102
+ 'type': profile.profile_type.value,
103
+ 'description': profile.description,
104
+ 'curve': profile.curve,
105
+ 'safety': profile.safety,
106
+ 'enabled': profile.enabled
107
+ }
108
+
109
+ return result
110
+
111
+ def set_fan_profile(self, profile_name: str) -> bool:
112
+ """Set fan profile."""
113
+ return self.fan_controller.set_profile(profile_name)
114
+
115
+ def set_fan_mode(self, mode: str) -> bool:
116
+ """Set fan mode."""
117
+ try:
118
+ fan_mode = FanMode(mode)
119
+ self.fan_controller.set_mode(fan_mode)
120
+ return True
121
+ except:
122
+ return False
123
+
124
+ def set_manual_pwm(self, pwm: int) -> bool:
125
+ """Set manual PWM."""
126
+ if 0 <= pwm <= 255:
127
+ self.fan_controller.set_manual_pwm(pwm)
128
+ return True
129
+ return False
130
+
131
+
132
+ # Initialize web manager
133
+ web_manager = WebGPUManager()
134
+
135
+
136
+ @app.route('/')
137
+ def index():
138
+ """Main dashboard page."""
139
+ return render_template('index.html')
140
+
141
+
142
+ @app.route('/api/status')
143
+ def api_status():
144
+ """API endpoint for current status."""
145
+ try:
146
+ status = web_manager.get_current_status()
147
+ return jsonify(status)
148
+ except Exception as e:
149
+ logger.error(f"Error getting status: {e}")
150
+ return jsonify({'error': str(e)}), 500
151
+
152
+
153
+ @app.route('/api/gpus')
154
+ def api_gpus():
155
+ """API endpoint for GPU list."""
156
+ try:
157
+ gpus = web_manager.get_gpu_list()
158
+ return jsonify({'gpus': gpus})
159
+ except Exception as e:
160
+ logger.error(f"Error getting GPU list: {e}")
161
+ return jsonify({'error': str(e)}), 500
162
+
163
+
164
+ @app.route('/api/history/<gpu_name>')
165
+ def api_history(gpu_name):
166
+ """API endpoint for historical data."""
167
+ try:
168
+ hours = request.args.get('hours', 24, type=int)
169
+ data = web_manager.get_historical_data(gpu_name, hours)
170
+ return jsonify({'data': data})
171
+ except Exception as e:
172
+ logger.error(f"Error getting historical data: {e}")
173
+ return jsonify({'error': str(e)}), 500
174
+
175
+
176
+ @app.route('/api/fan/profiles')
177
+ def api_fan_profiles():
178
+ """API endpoint for fan profiles."""
179
+ try:
180
+ profiles = web_manager.get_fan_profiles()
181
+ return jsonify({'profiles': profiles})
182
+ except Exception as e:
183
+ logger.error(f"Error getting fan profiles: {e}")
184
+ return jsonify({'error': str(e)}), 500
185
+
186
+
187
+ @app.route('/api/fan/profile', methods=['POST'])
188
+ def api_set_fan_profile():
189
+ """API endpoint to set fan profile."""
190
+ try:
191
+ data = request.get_json()
192
+ profile_name = data.get('profile')
193
+
194
+ if not profile_name:
195
+ return jsonify({'error': 'Profile name required'}), 400
196
+
197
+ success = web_manager.set_fan_profile(profile_name)
198
+
199
+ if success:
200
+ return jsonify({'success': True, 'message': f'Set profile to {profile_name}'})
201
+ else:
202
+ return jsonify({'error': f'Failed to set profile {profile_name}'}), 400
203
+
204
+ except Exception as e:
205
+ logger.error(f"Error setting fan profile: {e}")
206
+ return jsonify({'error': str(e)}), 500
207
+
208
+
209
+ @app.route('/api/fan/mode', methods=['POST'])
210
+ def api_set_fan_mode():
211
+ """API endpoint to set fan mode."""
212
+ try:
213
+ data = request.get_json()
214
+ mode = data.get('mode')
215
+
216
+ if not mode:
217
+ return jsonify({'error': 'Mode required'}), 400
218
+
219
+ success = web_manager.set_fan_mode(mode)
220
+
221
+ if success:
222
+ return jsonify({'success': True, 'message': f'Set mode to {mode}'})
223
+ else:
224
+ return jsonify({'error': f'Failed to set mode {mode}'}), 400
225
+
226
+ except Exception as e:
227
+ logger.error(f"Error setting fan mode: {e}")
228
+ return jsonify({'error': str(e)}), 500
229
+
230
+
231
+ @app.route('/api/fan/manual', methods=['POST'])
232
+ def api_set_manual_pwm():
233
+ """API endpoint to set manual PWM."""
234
+ try:
235
+ data = request.get_json()
236
+ pwm = data.get('pwm')
237
+
238
+ if pwm is None:
239
+ return jsonify({'error': 'PWM value required'}), 400
240
+
241
+ success = web_manager.set_manual_pwm(pwm)
242
+
243
+ if success:
244
+ return jsonify({'success': True, 'message': f'Set manual PWM to {pwm}'})
245
+ else:
246
+ return jsonify({'error': f'Invalid PWM value: {pwm}'}), 400
247
+
248
+ except Exception as e:
249
+ logger.error(f"Error setting manual PWM: {e}")
250
+ return jsonify({'error': str(e)}), 500
251
+
252
+
253
+ @app.route('/api/alerts')
254
+ def api_alerts():
255
+ """API endpoint for alerts."""
256
+ try:
257
+ # Get recent alerts from database
258
+ # This would need to be implemented in the GPUDataManager
259
+ alerts = [] # Placeholder
260
+ return jsonify({'alerts': alerts})
261
+ except Exception as e:
262
+ logger.error(f"Error getting alerts: {e}")
263
+ return jsonify({'error': str(e)}), 500
264
+
265
+
266
+ @app.route('/api/system')
267
+ def api_system():
268
+ """API endpoint for system information."""
269
+ try:
270
+ import psutil
271
+
272
+ system_info = {
273
+ 'cpu_count': psutil.cpu_count(),
274
+ 'cpu_percent': psutil.cpu_percent(interval=1),
275
+ 'memory': {
276
+ 'total': psutil.virtual_memory().total // (1024**3), # GB
277
+ 'available': psutil.virtual_memory().available // (1024**3), # GB
278
+ 'percent': psutil.virtual_memory().percent
279
+ },
280
+ 'disk': {
281
+ 'total': psutil.disk_usage('/').total // (1024**3), # GB
282
+ 'free': psutil.disk_usage('/').free // (1024**3), # GB
283
+ 'percent': (psutil.disk_usage('/').used / psutil.disk_usage('/').total) * 100
284
+ },
285
+ 'uptime': time.time() - psutil.boot_time()
286
+ }
287
+
288
+ return jsonify(system_info)
289
+ except Exception as e:
290
+ logger.error(f"Error getting system info: {e}")
291
+ return jsonify({'error': str(e)}), 500
292
+
293
+
294
+ @app.errorhandler(404)
295
+ def not_found(error):
296
+ """Handle 404 errors."""
297
+ return jsonify({'error': 'Not found'}), 404
298
+
299
+
300
+ @app.errorhandler(500)
301
+ def internal_error(error):
302
+ """Handle 500 errors."""
303
+ return jsonify({'error': 'Internal server error'}), 500
304
+
305
+
306
+ def create_templates():
307
+ """Create HTML templates directory and files."""
308
+ templates_dir = Path('templates')
309
+ static_dir = Path('static')
310
+
311
+ templates_dir.mkdir(exist_ok=True)
312
+ static_dir.mkdir(exist_ok=True)
313
+
314
+ # Create main HTML template
315
+ index_html = """
316
+ <!DOCTYPE html>
317
+ <html lang="en">
318
+ <head>
319
+ <meta charset="UTF-8">
320
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
321
+ <title>GPU Monitoring Dashboard</title>
322
+ <style>
323
+ :root {
324
+ --bg-color: #1a1a1a;
325
+ --card-bg: #2d2d2d;
326
+ --text-color: #ffffff;
327
+ --accent-color: #3498db;
328
+ --success-color: #2ecc71;
329
+ --warning-color: #f1c40f;
330
+ --danger-color: #e74c3c;
331
+ }
332
+
333
+ body {
334
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
335
+ background-color: var(--bg-color);
336
+ color: var(--text-color);
337
+ margin: 0;
338
+ padding: 20px;
339
+ }
340
+
341
+ .header {
342
+ display: flex;
343
+ justify-content: space-between;
344
+ align-items: center;
345
+ margin-bottom: 20px;
346
+ border-bottom: 2px solid var(--accent-color);
347
+ padding-bottom: 10px;
348
+ }
349
+
350
+ .header h1 {
351
+ margin: 0;
352
+ color: var(--accent-color);
353
+ }
354
+
355
+ .container {
356
+ display: grid;
357
+ grid-template-columns: 1fr 1fr;
358
+ gap: 20px;
359
+ }
360
+
361
+ .card {
362
+ background-color: var(--card-bg);
363
+ border-radius: 8px;
364
+ padding: 20px;
365
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
366
+ }
367
+
368
+ .card h3 {
369
+ margin-top: 0;
370
+ color: var(--accent-color);
371
+ }
372
+
373
+ .metric-grid {
374
+ display: grid;
375
+ grid-template-columns: repeat(2, 1fr);
376
+ gap: 15px;
377
+ }
378
+
379
+ .metric {
380
+ background-color: rgba(0, 0, 0, 0.3);
381
+ padding: 15px;
382
+ border-radius: 4px;
383
+ text-align: center;
384
+ }
385
+
386
+ .metric-value {
387
+ font-size: 24px;
388
+ font-weight: bold;
389
+ margin-bottom: 5px;
390
+ }
391
+
392
+ .metric-label {
393
+ font-size: 12px;
394
+ color: #888;
395
+ text-transform: uppercase;
396
+ }
397
+
398
+ .temp-good { color: var(--success-color); }
399
+ .temp-warn { color: var(--warning-color); }
400
+ .temp-danger { color: var(--danger-color); }
401
+
402
+ .chart-container {
403
+ width: 100%;
404
+ height: 300px;
405
+ margin-top: 20px;
406
+ }
407
+
408
+ .controls {
409
+ display: flex;
410
+ gap: 10px;
411
+ margin-bottom: 10px;
412
+ }
413
+
414
+ select, button {
415
+ padding: 8px 16px;
416
+ border: none;
417
+ border-radius: 4px;
418
+ background-color: var(--accent-color);
419
+ color: white;
420
+ cursor: pointer;
421
+ font-weight: bold;
422
+ }
423
+
424
+ button:hover {
425
+ background-color: #2980b9;
426
+ }
427
+
428
+ .status-indicator {
429
+ display: inline-block;
430
+ width: 10px;
431
+ height: 10px;
432
+ border-radius: 50%;
433
+ background-color: var(--success-color);
434
+ margin-right: 5px;
435
+ }
436
+
437
+ .status-offline {
438
+ background-color: var(--danger-color);
439
+ }
440
+
441
+ @media (max-width: 768px) {
442
+ .container {
443
+ grid-template-columns: 1fr;
444
+ }
445
+
446
+ .metric-grid {
447
+ grid-template-columns: repeat(2, 1fr);
448
+ }
449
+ }
450
+ </style>
451
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
452
+ </head>
453
+ <body>
454
+ <div class="header">
455
+ <h1>GPU Monitoring Dashboard</h1>
456
+ <div>
457
+ <span class="status-indicator" id="status-indicator"></span>
458
+ <span id="status-text">Connecting...</span>
459
+ </div>
460
+ </div>
461
+
462
+ <div class="container">
463
+ <div class="card">
464
+ <h3>Real-time Status</h3>
465
+ <div class="metric-grid" id="metrics-grid">
466
+ <!-- Metrics will be populated by JavaScript -->
467
+ </div>
468
+ </div>
469
+
470
+ <div class="card">
471
+ <h3>Fan Control</h3>
472
+ <div class="controls">
473
+ <select id="profile-select">
474
+ <option value="">Select Profile</option>
475
+ </select>
476
+ <button onclick="setProfile()">Apply Profile</button>
477
+ </div>
478
+ <div class="controls">
479
+ <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;">
480
+ <button onclick="setManualPWM()">Set Manual PWM</button>
481
+ </div>
482
+ <div style="margin-top: 15px;">
483
+ <div>Fan Mode: <span id="fan-mode">--</span></div>
484
+ <div>Current Profile: <span id="current-profile">--</span></div>
485
+ <div>Current PWM: <span id="current-pwm">--</span>%</div>
486
+ </div>
487
+ </div>
488
+
489
+ <div class="card" style="grid-column: 1 / -1;">
490
+ <h3>Temperature History</h3>
491
+ <div class="controls">
492
+ <select id="gpu-select">
493
+ <option value="">Select GPU</option>
494
+ </select>
495
+ <select id="hours-select">
496
+ <option value="1">1 Hour</option>
497
+ <option value="6">6 Hours</option>
498
+ <option value="24" selected>24 Hours</option>
499
+ <option value="168">7 Days</option>
500
+ </select>
501
+ <button onclick="loadHistory()">Load History</button>
502
+ </div>
503
+ <div class="chart-container">
504
+ <canvas id="temp-chart"></canvas>
505
+ </div>
506
+ </div>
507
+ </div>
508
+
509
+ <script>
510
+ let tempChart;
511
+ let updateInterval;
512
+
513
+ // Initialize chart
514
+ function initChart() {
515
+ const ctx = document.getElementById('temp-chart').getContext('2d');
516
+ tempChart = new Chart(ctx, {
517
+ type: 'line',
518
+ data: {
519
+ labels: [],
520
+ datasets: [{
521
+ label: 'Temperature (°C)',
522
+ data: [],
523
+ borderColor: '#3498db',
524
+ backgroundColor: 'rgba(52, 152, 219, 0.1)',
525
+ borderWidth: 2,
526
+ fill: true
527
+ }]
528
+ },
529
+ options: {
530
+ responsive: true,
531
+ maintainAspectRatio: false,
532
+ scales: {
533
+ y: {
534
+ beginAtZero: true,
535
+ max: 100
536
+ }
537
+ }
538
+ }
539
+ });
540
+ }
541
+
542
+ // Update real-time status
543
+ function updateStatus() {
544
+ fetch('/api/status')
545
+ .then(response => response.json())
546
+ .then(data => {
547
+ updateMetrics(data);
548
+ updateFanControl(data.fan_control);
549
+ updateStatusIndicator(true);
550
+ })
551
+ .catch(error => {
552
+ console.error('Error fetching status:', error);
553
+ updateStatusIndicator(false);
554
+ });
555
+ }
556
+
557
+ // Update metrics display
558
+ function updateMetrics(data) {
559
+ const grid = document.getElementById('metrics-grid');
560
+ grid.innerHTML = '';
561
+
562
+ for (const [gpuName, gpuData] of Object.entries(data.gpus)) {
563
+ const card = document.createElement('div');
564
+ card.className = 'card';
565
+ card.innerHTML = `
566
+ <h4>${gpuName}</h4>
567
+ <div class="metric-grid">
568
+ <div class="metric">
569
+ <div class="metric-value temp-${getTempClass(gpuData.temperature)}">${gpuData.temperature.toFixed(1)}°C</div>
570
+ <div class="metric-label">Temperature</div>
571
+ </div>
572
+ <div class="metric">
573
+ <div class="metric-value">${gpuData.load.toFixed(1)}%</div>
574
+ <div class="metric-label">Load</div>
575
+ </div>
576
+ <div class="metric">
577
+ <div class="metric-value">${gpuData.fan_speed} RPM</div>
578
+ <div class="metric-label">Fan Speed</div>
579
+ </div>
580
+ <div class="metric">
581
+ <div class="metric-value">${gpuData.power_draw.toFixed(1)} W</div>
582
+ <div class="metric-label">Power</div>
583
+ </div>
584
+ <div class="metric">
585
+ <div class="metric-value">${gpuData.memory_used}/${gpuData.memory_total} MB</div>
586
+ <div class="metric-label">VRAM</div>
587
+ </div>
588
+ <div class="metric">
589
+ <div class="metric-value">${gpuData.core_clock} MHz</div>
590
+ <div class="metric-label">Core Clock</div>
591
+ </div>
592
+ </div>
593
+ `;
594
+ grid.appendChild(card);
595
+ }
596
+ }
597
+
598
+ // Get temperature color class
599
+ function getTempClass(temp) {
600
+ if (temp < 60) return 'good';
601
+ if (temp < 75) return 'warn';
602
+ return 'danger';
603
+ }
604
+
605
+ // Update fan control display
606
+ function updateFanControl(fanData) {
607
+ document.getElementById('fan-mode').textContent = fanData.mode;
608
+ document.getElementById('current-profile').textContent = fanData.profile;
609
+ document.getElementById('current-pwm').textContent = fanData.current_pwm;
610
+ }
611
+
612
+ // Update status indicator
613
+ function updateStatusIndicator(online) {
614
+ const indicator = document.getElementById('status-indicator');
615
+ const text = document.getElementById('status-text');
616
+
617
+ if (online) {
618
+ indicator.className = 'status-indicator';
619
+ text.textContent = 'Online';
620
+ } else {
621
+ indicator.className = 'status-indicator status-offline';
622
+ text.textContent = 'Offline';
623
+ }
624
+ }
625
+
626
+ // Load fan profiles
627
+ function loadProfiles() {
628
+ fetch('/api/fan/profiles')
629
+ .then(response => response.json())
630
+ .then(data => {
631
+ const select = document.getElementById('profile-select');
632
+ select.innerHTML = '<option value="">Select Profile</option>';
633
+
634
+ for (const [name, profile] of Object.entries(data.profiles)) {
635
+ if (profile.enabled) {
636
+ const option = document.createElement('option');
637
+ option.value = name;
638
+ option.textContent = profile.name;
639
+ select.appendChild(option);
640
+ }
641
+ }
642
+ })
643
+ .catch(error => console.error('Error loading profiles:', error));
644
+ }
645
+
646
+ // Load GPU list
647
+ function loadGPUs() {
648
+ fetch('/api/gpus')
649
+ .then(response => response.json())
650
+ .then(data => {
651
+ const select = document.getElementById('gpu-select');
652
+ select.innerHTML = '<option value="">Select GPU</option>';
653
+
654
+ data.gpus.forEach(gpu => {
655
+ const option = document.createElement('option');
656
+ option.value = gpu;
657
+ option.textContent = gpu;
658
+ select.appendChild(option);
659
+ });
660
+ })
661
+ .catch(error => console.error('Error loading GPUs:', error));
662
+ }
663
+
664
+ // Set fan profile
665
+ function setProfile() {
666
+ const profile = document.getElementById('profile-select').value;
667
+ if (!profile) return;
668
+
669
+ fetch('/api/fan/profile', {
670
+ method: 'POST',
671
+ headers: {
672
+ 'Content-Type': 'application/json',
673
+ },
674
+ body: JSON.stringify({ profile: profile })
675
+ })
676
+ .then(response => response.json())
677
+ .then(data => {
678
+ if (data.success) {
679
+ alert('Profile updated successfully');
680
+ updateStatus();
681
+ } else {
682
+ alert('Error: ' + data.error);
683
+ }
684
+ })
685
+ .catch(error => {
686
+ console.error('Error:', error);
687
+ alert('Error updating profile');
688
+ });
689
+ }
690
+
691
+ // Set manual PWM
692
+ function setManualPWM() {
693
+ const pwm = parseInt(document.getElementById('manual-pwm').value);
694
+ if (isNaN(pwm) || pwm < 0 || pwm > 255) {
695
+ alert('Please enter a valid PWM value (0-255)');
696
+ return;
697
+ }
698
+
699
+ fetch('/api/fan/manual', {
700
+ method: 'POST',
701
+ headers: {
702
+ 'Content-Type': 'application/json',
703
+ },
704
+ body: JSON.stringify({ pwm: pwm })
705
+ })
706
+ .then(response => response.json())
707
+ .then(data => {
708
+ if (data.success) {
709
+ alert('Manual PWM set successfully');
710
+ updateStatus();
711
+ } else {
712
+ alert('Error: ' + data.error);
713
+ }
714
+ })
715
+ .catch(error => {
716
+ console.error('Error:', error);
717
+ alert('Error setting manual PWM');
718
+ });
719
+ }
720
+
721
+ // Load historical data
722
+ function loadHistory() {
723
+ const gpu = document.getElementById('gpu-select').value;
724
+ const hours = document.getElementById('hours-select').value;
725
+
726
+ if (!gpu) {
727
+ alert('Please select a GPU');
728
+ return;
729
+ }
730
+
731
+ fetch(`/api/history/${encodeURIComponent(gpu)}?hours=${hours}`)
732
+ .then(response => response.json())
733
+ .then(data => {
734
+ updateChart(data.data);
735
+ })
736
+ .catch(error => {
737
+ console.error('Error loading history:', error);
738
+ alert('Error loading historical data');
739
+ });
740
+ }
741
+
742
+ // Update chart with historical data
743
+ function updateChart(data) {
744
+ if (!data || data.length === 0) return;
745
+
746
+ const labels = data.map(d => new Date(d.timestamp * 1000).toLocaleTimeString());
747
+ const temps = data.map(d => d.temperature);
748
+
749
+ tempChart.data.labels = labels;
750
+ tempChart.data.datasets[0].data = temps;
751
+ tempChart.update();
752
+ }
753
+
754
+ // Initialize application
755
+ document.addEventListener('DOMContentLoaded', function() {
756
+ initChart();
757
+ loadProfiles();
758
+ loadGPUs();
759
+ updateStatus();
760
+
761
+ // Update status every 2 seconds
762
+ updateInterval = setInterval(updateStatus, 2000);
763
+ });
764
+ </script>
765
+ </body>
766
+ </html>
767
+ """
768
+
769
+ with open(templates_dir / 'index.html', 'w') as f:
770
+ f.write(index_html)
771
+
772
+
773
+ if __name__ == '__main__':
774
+ # Create templates if they don't exist
775
+ create_templates()
776
+
777
+ # Get configuration
778
+ config = web_manager.config
779
+
780
+ # Start Flask app
781
+ app.run(
782
+ host=config.get('host', '0.0.0.0'),
783
+ port=config.get('port', 5000),
784
+ debug=config.get('debug', False)
785
+ )