claude-mpm 4.2.40__py3-none-any.whl → 4.2.43__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_ENGINEER.md +114 -1
  3. claude_mpm/agents/BASE_OPS.md +156 -1
  4. claude_mpm/agents/INSTRUCTIONS.md +120 -11
  5. claude_mpm/agents/WORKFLOW.md +160 -10
  6. claude_mpm/agents/templates/agentic-coder-optimizer.json +17 -12
  7. claude_mpm/agents/templates/react_engineer.json +217 -0
  8. claude_mpm/agents/templates/web_qa.json +40 -4
  9. claude_mpm/commands/mpm-browser-monitor.md +370 -0
  10. claude_mpm/commands/mpm-monitor.md +177 -0
  11. claude_mpm/dashboard/static/built/components/code-viewer.js +1076 -2
  12. claude_mpm/dashboard/static/built/components/ui-state-manager.js +465 -2
  13. claude_mpm/dashboard/static/css/dashboard.css +2 -0
  14. claude_mpm/dashboard/static/js/browser-console-monitor.js +495 -0
  15. claude_mpm/dashboard/static/js/components/browser-log-viewer.js +763 -0
  16. claude_mpm/dashboard/static/js/components/code-viewer.js +931 -340
  17. claude_mpm/dashboard/static/js/components/diff-viewer.js +891 -0
  18. claude_mpm/dashboard/static/js/components/file-change-tracker.js +443 -0
  19. claude_mpm/dashboard/static/js/components/file-change-viewer.js +690 -0
  20. claude_mpm/dashboard/static/js/components/ui-state-manager.js +156 -19
  21. claude_mpm/dashboard/static/js/dashboard.js +16 -0
  22. claude_mpm/dashboard/static/js/socket-client.js +2 -2
  23. claude_mpm/dashboard/static/test-browser-monitor.html +470 -0
  24. claude_mpm/dashboard/templates/index.html +64 -99
  25. claude_mpm/services/monitor/handlers/browser.py +451 -0
  26. claude_mpm/services/monitor/server.py +267 -4
  27. {claude_mpm-4.2.40.dist-info → claude_mpm-4.2.43.dist-info}/METADATA +1 -1
  28. {claude_mpm-4.2.40.dist-info → claude_mpm-4.2.43.dist-info}/RECORD +32 -26
  29. claude_mpm/agents/templates/agentic-coder-optimizer.md +0 -44
  30. claude_mpm/agents/templates/agentic_coder_optimizer.json +0 -238
  31. claude_mpm/agents/templates/test-non-mpm.json +0 -20
  32. claude_mpm/dashboard/static/dist/components/code-viewer.js +0 -2
  33. {claude_mpm-4.2.40.dist-info → claude_mpm-4.2.43.dist-info}/WHEEL +0 -0
  34. {claude_mpm-4.2.40.dist-info → claude_mpm-4.2.43.dist-info}/entry_points.txt +0 -0
  35. {claude_mpm-4.2.40.dist-info → claude_mpm-4.2.43.dist-info}/licenses/LICENSE +0 -0
  36. {claude_mpm-4.2.40.dist-info → claude_mpm-4.2.43.dist-info}/top_level.txt +0 -0
@@ -18,6 +18,9 @@
18
18
 
19
19
  <!-- D3.js for Activity Tree Visualization -->
20
20
  <script src="https://d3js.org/d3.v7.min.js"></script>
21
+
22
+ <!-- Font Awesome for icons -->
23
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
21
24
 
22
25
  <!-- Syntax Highlighting - Prism.js -->
23
26
  <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
@@ -239,18 +242,19 @@
239
242
 
240
243
  <!-- Right: Tabbed Content -->
241
244
  <div class="events-container">
242
- <!-- Tab Navigation -->
245
+ <!-- Tab Navigation - Using hash-based navigation -->
243
246
  <div class="tab-nav">
244
- <button class="tab-button active" data-tab="events">📊 Events</button>
245
- <button class="tab-button" data-tab="agents">🤖 Agents</button>
246
- <button class="tab-button" data-tab="tools">🔧 Tools</button>
247
- <button class="tab-button" data-tab="files">📁 Files</button>
248
- <button class="tab-button" data-tab="activity">🌳 Activity</button>
249
- <button class="tab-button" data-tab="code">🧬 Code</button>
247
+ <a href="#events" class="tab-button" data-tab="events">📊 Events</a>
248
+ <a href="#agents" class="tab-button" data-tab="agents">🤖 Agents</a>
249
+ <a href="#tools" class="tab-button" data-tab="tools">🔧 Tools</a>
250
+ <a href="#files" class="tab-button" data-tab="files">📁 Files</a>
251
+ <a href="#activity" class="tab-button" data-tab="activity">🌳 Activity</a>
252
+ <a href="#file_tree" class="tab-button active" data-tab="claude-tree">📝 File Tree</a>
253
+ <a href="#browser_logs" class="tab-button" data-tab="browser-logs">🌐 Browser Logs</a>
250
254
  </div>
251
255
 
252
256
  <!-- Events Tab -->
253
- <div class="tab-content active" id="events-tab">
257
+ <div class="tab-content" id="events-tab">
254
258
  <div class="tab-filters">
255
259
  <input type="text" id="events-search-input" placeholder="Search events...">
256
260
  <select id="events-type-filter">
@@ -385,76 +389,18 @@
385
389
  </div>
386
390
  </div>
387
391
 
388
- <!-- Code Tab -->
389
- <div class="tab-content" id="code-tab">
390
- <div class="code-container">
391
- <div class="code-split-container">
392
- <div id="code-tree-container" class="code-tree-container">
393
- <!-- Top-left corner: Language selector -->
394
- <div class="tree-corner-controls top-left">
395
- <div class="control-group">
396
- <label class="control-label">Languages:</label>
397
- <div class="checkbox-group">
398
- <label class="checkbox-label"><input type="checkbox" class="language-checkbox" value="python" checked> Python</label>
399
- <label class="checkbox-label"><input type="checkbox" class="language-checkbox" value="javascript" checked> JS</label>
400
- <label class="checkbox-label"><input type="checkbox" class="language-checkbox" value="typescript" checked> TS</label>
401
- </div>
402
- </div>
403
- </div>
404
-
405
- <!-- Top-right corner: Layout and search -->
406
- <div class="tree-corner-controls top-right">
407
- <div class="control-group">
408
- <select id="code-layout" class="select-compact">
409
- <option value="tree">Tree</option>
410
- <option value="radial">Radial</option>
411
- </select>
412
- <input type="text" id="code-search" placeholder="Search..." class="search-compact">
413
- </div>
414
- </div>
415
-
416
- <!-- Bottom-left corner: Stats and Status -->
417
- <div class="tree-corner-controls bottom-left">
418
- <div class="stats-display" id="code-stats">
419
- <span id="stats-files">0 files</span> •
420
- <span id="stats-classes">0 classes</span> •
421
- <span id="stats-functions">0 functions</span> •
422
- <span id="stats-methods">0 methods</span>
423
- </div>
424
- <div class="status-display" id="code-breadcrumb">
425
- <div class="breadcrumb-ticker" id="breadcrumb-ticker">
426
- <span id="breadcrumb-content">Ready to analyze...</span>
427
- </div>
428
- </div>
429
- </div>
430
-
431
- <!-- Bottom-right corner: Ignore patterns -->
432
- <div class="tree-corner-controls bottom-right">
433
- <div class="control-group">
434
- <label class="control-label">Ignore:</label>
435
- <input type="text" id="ignore-patterns" placeholder="test*, *.spec.js, node_modules" class="input-compact">
436
- </div>
437
- </div>
438
- <div id="code-tree"></div>
439
- <!-- Collapsible legend -->
440
- <div class="tree-legend collapsed" id="tree-legend" style="display: none;">
441
- <button class="legend-close" onclick="document.getElementById('tree-legend').style.display='none'">✕</button>
442
- <div class="legend-content">
443
- <div class="legend-column">
444
- <div class="legend-item"><span class="legend-icon">📦</span> Module</div>
445
- <div class="legend-item"><span class="legend-icon">🏛️</span> Class</div>
446
- <div class="legend-item"><span class="legend-icon">⚡</span> Function</div>
447
- <div class="legend-item"><span class="legend-icon">🔧</span> Method</div>
448
- </div>
449
- <div class="legend-column">
450
- <div class="legend-item"><span class="legend-icon complexity-low">●</span> Low</div>
451
- <div class="legend-item"><span class="legend-icon complexity-medium">●</span> Med</div>
452
- <div class="legend-item"><span class="legend-icon complexity-high">●</span> High</div>
453
- </div>
454
- </div>
455
- </div>
456
- </div>
392
+ <!-- File Tree Tab -->
393
+ <div class="tab-content active" id="claude-tree-tab">
394
+ <div id="claude-tree-container" style="width: 100%; height: 100%;">
395
+ <!-- File activity tree will be rendered here by code-viewer.js -->
457
396
  </div>
397
+ </div>
398
+
399
+ <!-- Browser Logs Tab -->
400
+ <div class="tab-content" id="browser-logs-tab">
401
+ <div id="browser-logs-container" style="width: 100%; height: 100%;" data-component="browser-logs">
402
+ <!-- Browser logs will be rendered here by browser-log-viewer.js -->
403
+ <!-- This container is EXCLUSIVELY for browser console logs -->
458
404
  </div>
459
405
  </div>
460
406
 
@@ -463,26 +409,6 @@
463
409
  </div>
464
410
  </div>
465
411
 
466
- <!-- Code Viewer Modal -->
467
- <div id="code-viewer-modal" class="modal" style="display: none;">
468
- <div class="modal-content">
469
- <div class="modal-header">
470
- <h2 id="code-viewer-title">Code Viewer</h2>
471
- <button class="close" onclick="closeCodeViewer()">&times;</button>
472
- </div>
473
- <div class="modal-body">
474
- <pre><code id="code-viewer-content"></code></pre>
475
- </div>
476
- <div class="modal-footer">
477
- <div class="code-viewer-info">
478
- <span id="code-viewer-path"></span>
479
- <span id="code-viewer-lines"></span>
480
- <span id="code-viewer-language"></span>
481
- </div>
482
- <button onclick="closeCodeViewer()">Close</button>
483
- </div>
484
- </div>
485
- </div>
486
412
 
487
413
  <!-- Footer -->
488
414
  <div class="footer">
@@ -571,19 +497,58 @@
571
497
  .then(() => loadModule('/static/js/components/code-tree/tree-constants.js'))
572
498
  .then(() => loadModule('/static/js/components/code-tree/tree-search.js'))
573
499
  .then(() => loadModule('/static/js/components/code-tree/tree-breadcrumb.js'))
500
+ .then(() => {
501
+ // Load UI State Manager v2 from source with nuclear browser log handling
502
+ return loadModule('/static/js/components/ui-state-manager.js?v=2.0-NUCLEAR');
503
+ })
574
504
  .then(() => {
575
505
  // Now load main components in parallel
576
506
  return Promise.all([
577
507
  loadModule('/static/dist/dashboard.js'),
578
508
  loadModule('/static/dist/components/activity-tree.js'),
579
509
  loadModule('/static/js/components/code-tree.js'), // TEMPORARY: Direct source for debugging
580
- loadModule('/static/dist/components/code-viewer.js'),
581
- loadModule('/static/dist/components/file-viewer.js') // File viewer for viewing file contents
510
+ loadModule('/static/js/components/code-viewer.js'), // Code viewer now includes file change tracking
511
+ loadModule('/static/dist/components/file-viewer.js'), // File viewer for viewing file contents
512
+ // TEMPORARILY DISABLED for simplest implementation
513
+ // loadModule('/static/js/components/browser-log-viewer.js?v=2.0-NUCLEAR') // Browser console log viewer v2.0 NUCLEAR
514
+ // TEMPORARILY DISABLED - BrowserLogViewer initialization
515
+ // .then(() => {
516
+ // // Initialize BrowserLogViewer v2.0 immediately after loading
517
+ // if (typeof BrowserLogViewer !== 'undefined') {
518
+ // const container = document.getElementById('browser-logs-container');
519
+ // if (container && !window.browserLogViewer) {
520
+ // console.error('[Main] Initializing BrowserLogViewer v2.0 NUCLEAR...');
521
+ // window.browserLogViewer = new BrowserLogViewer(container);
522
+ // console.error('[Main] BrowserLogViewer v2.0 NUCLEAR initialized successfully');
523
+ // } else {
524
+ // console.error('[Main] BrowserLogViewer v2.0 already initialized or container not found');
525
+ // }
526
+ // } else {
527
+ // console.error('[Main] BrowserLogViewer class not found - script may not have loaded');
528
+ // }
529
+ // })
582
530
  ]);
583
531
  })
584
532
  .then(() => {
585
533
  console.log('All dashboard modules loaded successfully');
586
534
 
535
+ // CodeViewer will auto-initialize and handle tab switching internally
536
+
537
+ // Browser Log Viewer initialization is now handled by UIStateManager
538
+ // This prevents duplicate event handlers and tab selection conflicts
539
+
540
+ // Hash navigation will handle default tab based on URL
541
+ // If no hash, default will be 'events' as per hashToTab mapping
542
+ // To start with File Tree, we can set hash if not present
543
+ setTimeout(() => {
544
+ if (!window.location.hash) {
545
+ console.log('No hash present, setting default to File Tree tab...');
546
+ window.location.hash = '#file_tree';
547
+ } else {
548
+ console.log('Hash present:', window.location.hash);
549
+ }
550
+ }, 500);
551
+
587
552
  // Give modules time to execute and initialize dashboard
588
553
  setTimeout(() => {
589
554
  console.log('Checking dashboard initialization...');
@@ -0,0 +1,451 @@
1
+ """
2
+ Browser Console Handler for Unified Monitor
3
+ ===========================================
4
+
5
+ WHY: This handler manages browser console events from web pages that have
6
+ the browser console monitor script injected. It provides centralized logging
7
+ and debugging capabilities for client-side applications.
8
+
9
+ DESIGN DECISIONS:
10
+ - Creates separate log files per browser session for easy tracking
11
+ - Stores logs in .claude-mpm/logs/client/ directory
12
+ - Handles multiple concurrent browser connections
13
+ - Tracks active browser sessions with metadata
14
+ - Provides structured logging with timestamps and context
15
+ - Integrates with the unified monitor architecture
16
+ """
17
+
18
+ import json
19
+ from datetime import datetime
20
+ from pathlib import Path
21
+ from typing import Dict, Set
22
+
23
+ import socketio
24
+
25
+ from ....core.logging_config import get_logger
26
+
27
+
28
+ class BrowserHandler:
29
+ """Event handler for browser console monitoring functionality.
30
+
31
+ WHY: Manages browser console events from injected monitoring scripts
32
+ and provides centralized logging for client-side debugging.
33
+ """
34
+
35
+ def __init__(self, sio: socketio.AsyncServer):
36
+ """Initialize the browser handler.
37
+
38
+ Args:
39
+ sio: Socket.IO server instance
40
+ """
41
+ self.sio = sio
42
+ self.logger = get_logger(__name__)
43
+
44
+ # Browser session management
45
+ self.active_browsers: Set[str] = set()
46
+ self.browser_info: Dict[str, Dict] = {}
47
+
48
+ # Logging configuration
49
+ self.log_dir = Path.cwd() / ".claude-mpm" / "logs" / "client"
50
+ self.log_files: Dict[str, Path] = {}
51
+
52
+ # Ensure log directory exists
53
+ self._ensure_log_directory()
54
+
55
+ def register(self):
56
+ """Register Socket.IO event handlers."""
57
+ try:
58
+ # Browser connection events
59
+ self.sio.on("browser:connect", self.handle_browser_connect)
60
+ self.sio.on("browser:disconnect", self.handle_browser_disconnect)
61
+ self.sio.on("browser:hide", self.handle_browser_hide)
62
+
63
+ # Console events
64
+ self.sio.on("browser:console", self.handle_console_event)
65
+
66
+ # Browser management events
67
+ self.sio.on("browser:list", self.handle_browser_list)
68
+ self.sio.on("browser:info", self.handle_browser_info)
69
+
70
+ self.logger.info("Browser event handlers registered")
71
+
72
+ except Exception as e:
73
+ self.logger.error(f"Error registering browser handlers: {e}")
74
+ raise
75
+
76
+ def _ensure_log_directory(self):
77
+ """Ensure the client logs directory exists."""
78
+ try:
79
+ self.log_dir.mkdir(parents=True, exist_ok=True)
80
+ self.logger.debug(f"Client logs directory ensured: {self.log_dir}")
81
+ except Exception as e:
82
+ self.logger.error(f"Error creating client logs directory: {e}")
83
+ raise
84
+
85
+ def _get_log_file_path(self, browser_id: str) -> Path:
86
+ """Get log file path for a browser session.
87
+
88
+ Args:
89
+ browser_id: Unique browser identifier
90
+
91
+ Returns:
92
+ Path to the log file
93
+ """
94
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
95
+ log_filename = f"{browser_id}_{timestamp}.log"
96
+ return self.log_dir / log_filename
97
+
98
+ def _write_log_entry(
99
+ self,
100
+ browser_id: str,
101
+ level: str,
102
+ message: str,
103
+ timestamp: str = None,
104
+ extra_data: Dict = None,
105
+ ):
106
+ """Write a log entry to the browser's log file.
107
+
108
+ Args:
109
+ browser_id: Browser identifier
110
+ level: Log level (INFO, ERROR, etc.)
111
+ message: Log message
112
+ timestamp: Event timestamp (ISO format)
113
+ extra_data: Additional event data
114
+ """
115
+ try:
116
+ # Get or create log file for this browser
117
+ if browser_id not in self.log_files:
118
+ self.log_files[browser_id] = self._get_log_file_path(browser_id)
119
+
120
+ log_file = self.log_files[browser_id]
121
+
122
+ # Format timestamp
123
+ if not timestamp:
124
+ timestamp = datetime.now().isoformat()
125
+
126
+ # Format log entry
127
+ log_entry = f"[{timestamp}] [{level}] [{browser_id}] {message}"
128
+
129
+ # Add extra data if provided
130
+ if extra_data:
131
+ filtered_data = {
132
+ k: v
133
+ for k, v in extra_data.items()
134
+ if k not in ["browser_id", "level", "timestamp", "message"]
135
+ }
136
+ if filtered_data:
137
+ log_entry += f"\n Data: {json.dumps(filtered_data, indent=2)}"
138
+
139
+ # Write to file
140
+ with open(log_file, "a", encoding="utf-8") as f:
141
+ f.write(log_entry + "\n")
142
+
143
+ self.logger.debug(f"Log entry written for {browser_id}: {level}")
144
+
145
+ except Exception as e:
146
+ self.logger.error(f"Error writing log entry for {browser_id}: {e}")
147
+
148
+ async def handle_browser_connect(self, sid: str, data: Dict):
149
+ """Handle browser connection event.
150
+
151
+ Args:
152
+ sid: Socket.IO session ID
153
+ data: Browser connection data
154
+ """
155
+ try:
156
+ browser_id = data.get("browser_id")
157
+ if not browser_id:
158
+ self.logger.warning(f"Browser connect without ID from {sid}")
159
+ return
160
+
161
+ # Track browser session
162
+ self.active_browsers.add(browser_id)
163
+
164
+ # Store browser info
165
+ browser_info = {
166
+ "browser_id": browser_id,
167
+ "socket_id": sid,
168
+ "connected_at": datetime.now().isoformat(),
169
+ "user_agent": data.get("user_agent", "Unknown"),
170
+ "url": data.get("url", "Unknown"),
171
+ "last_activity": datetime.now().isoformat(),
172
+ }
173
+ self.browser_info[browser_id] = browser_info
174
+
175
+ # Log connection
176
+ self._write_log_entry(
177
+ browser_id,
178
+ "INFO",
179
+ f'Browser connected from {data.get("url", "unknown URL")}',
180
+ data.get("timestamp"),
181
+ browser_info,
182
+ )
183
+
184
+ self.logger.info(f"Browser connected: {browser_id} from {data.get('url')}")
185
+
186
+ # Send acknowledgment
187
+ await self.sio.emit(
188
+ "browser:connected",
189
+ {
190
+ "browser_id": browser_id,
191
+ "status": "connected",
192
+ "message": "Console monitoring active",
193
+ },
194
+ room=sid,
195
+ )
196
+
197
+ # Broadcast browser count update to dashboard
198
+ await self._broadcast_browser_stats()
199
+
200
+ except Exception as e:
201
+ self.logger.error(f"Error handling browser connect: {e}")
202
+
203
+ async def handle_browser_disconnect(self, sid: str, data: Dict):
204
+ """Handle browser disconnection event.
205
+
206
+ Args:
207
+ sid: Socket.IO session ID
208
+ data: Browser disconnection data
209
+ """
210
+ try:
211
+ browser_id = data.get("browser_id")
212
+ if not browser_id:
213
+ return
214
+
215
+ # Log disconnection
216
+ self._write_log_entry(
217
+ browser_id, "INFO", "Browser disconnected", data.get("timestamp")
218
+ )
219
+
220
+ # Update browser info
221
+ if browser_id in self.browser_info:
222
+ self.browser_info[browser_id][
223
+ "disconnected_at"
224
+ ] = datetime.now().isoformat()
225
+ self.browser_info[browser_id]["status"] = "disconnected"
226
+
227
+ self.logger.info(f"Browser disconnected: {browser_id}")
228
+
229
+ # Broadcast browser count update
230
+ await self._broadcast_browser_stats()
231
+
232
+ except Exception as e:
233
+ self.logger.error(f"Error handling browser disconnect: {e}")
234
+
235
+ async def handle_browser_hide(self, sid: str, data: Dict):
236
+ """Handle browser hide event (tab hidden/mobile app backgrounded).
237
+
238
+ Args:
239
+ sid: Socket.IO session ID
240
+ data: Browser hide data
241
+ """
242
+ try:
243
+ browser_id = data.get("browser_id")
244
+ if not browser_id:
245
+ return
246
+
247
+ # Log hide event
248
+ self._write_log_entry(
249
+ browser_id,
250
+ "INFO",
251
+ "Browser tab hidden/backgrounded",
252
+ data.get("timestamp"),
253
+ )
254
+
255
+ # Update browser info
256
+ if browser_id in self.browser_info:
257
+ self.browser_info[browser_id]["hidden_at"] = datetime.now().isoformat()
258
+ self.browser_info[browser_id]["status"] = "hidden"
259
+
260
+ self.logger.debug(f"Browser hidden: {browser_id}")
261
+
262
+ except Exception as e:
263
+ self.logger.error(f"Error handling browser hide: {e}")
264
+
265
+ async def handle_console_event(self, sid: str, data: Dict):
266
+ """Handle console event from browser.
267
+
268
+ Args:
269
+ sid: Socket.IO session ID
270
+ data: Console event data
271
+ """
272
+ try:
273
+ browser_id = data.get("browser_id")
274
+ level = data.get("level", "LOG")
275
+ message = data.get("message", "")
276
+ timestamp = data.get("timestamp")
277
+
278
+ if not browser_id:
279
+ self.logger.warning(f"Console event without browser ID from {sid}")
280
+ return
281
+
282
+ # Update last activity
283
+ if browser_id in self.browser_info:
284
+ self.browser_info[browser_id][
285
+ "last_activity"
286
+ ] = datetime.now().isoformat()
287
+
288
+ # Log console event
289
+ self._write_log_entry(browser_id, level, message, timestamp, data)
290
+
291
+ # Log to main logger based on level
292
+ log_message = f"[{browser_id}] {message}"
293
+ if level == "ERROR":
294
+ self.logger.error(log_message)
295
+ elif level == "WARN":
296
+ self.logger.warning(log_message)
297
+ else:
298
+ self.logger.debug(log_message)
299
+
300
+ # Format log entry for dashboard
301
+ log_entry = {
302
+ "browser_id": browser_id,
303
+ "level": level,
304
+ "message": message,
305
+ "timestamp": timestamp,
306
+ "url": data.get("url"),
307
+ "line_info": data.get("line_info"),
308
+ }
309
+
310
+ # Forward event to dashboard clients using both event names for compatibility
311
+ await self.sio.emit("dashboard:browser:console", log_entry)
312
+ await self.sio.emit("browser_log", log_entry)
313
+
314
+ except Exception as e:
315
+ self.logger.error(f"Error handling console event: {e}")
316
+
317
+ async def handle_browser_list(self, sid: str, data: Dict):
318
+ """Handle browser list request.
319
+
320
+ Args:
321
+ sid: Socket.IO session ID
322
+ data: Request data
323
+ """
324
+ try:
325
+ browser_list = []
326
+ for browser_id, info in self.browser_info.items():
327
+ browser_list.append(
328
+ {
329
+ "browser_id": browser_id,
330
+ "url": info.get("url", "Unknown"),
331
+ "user_agent": info.get("user_agent", "Unknown"),
332
+ "connected_at": info.get("connected_at"),
333
+ "last_activity": info.get("last_activity"),
334
+ "status": info.get("status", "active"),
335
+ }
336
+ )
337
+
338
+ await self.sio.emit(
339
+ "browser:list:response",
340
+ {
341
+ "browsers": browser_list,
342
+ "total": len(browser_list),
343
+ "active": len(self.active_browsers),
344
+ },
345
+ room=sid,
346
+ )
347
+
348
+ except Exception as e:
349
+ self.logger.error(f"Error getting browser list: {e}")
350
+ await self.sio.emit(
351
+ "browser:error", {"error": f"Browser list error: {e!s}"}, room=sid
352
+ )
353
+
354
+ async def handle_browser_info(self, sid: str, data: Dict):
355
+ """Handle browser info request.
356
+
357
+ Args:
358
+ sid: Socket.IO session ID
359
+ data: Request data containing browser_id
360
+ """
361
+ try:
362
+ browser_id = data.get("browser_id")
363
+ if not browser_id or browser_id not in self.browser_info:
364
+ await self.sio.emit(
365
+ "browser:error", {"error": "Browser not found"}, room=sid
366
+ )
367
+ return
368
+
369
+ info = self.browser_info[browser_id]
370
+
371
+ # Add log file info
372
+ log_file_path = self.log_files.get(browser_id)
373
+ if log_file_path and log_file_path.exists():
374
+ info["log_file"] = str(log_file_path)
375
+ info["log_size"] = log_file_path.stat().st_size
376
+
377
+ await self.sio.emit("browser:info:response", info, room=sid)
378
+
379
+ except Exception as e:
380
+ self.logger.error(f"Error getting browser info: {e}")
381
+ await self.sio.emit(
382
+ "browser:error", {"error": f"Browser info error: {e!s}"}, room=sid
383
+ )
384
+
385
+ async def _broadcast_browser_stats(self):
386
+ """Broadcast browser statistics to all dashboard clients."""
387
+ try:
388
+ stats = {
389
+ "total_browsers": len(self.browser_info),
390
+ "active_browsers": len(self.active_browsers),
391
+ "connected_browsers": len(
392
+ [
393
+ info
394
+ for info in self.browser_info.values()
395
+ if info.get("status") != "disconnected"
396
+ ]
397
+ ),
398
+ }
399
+
400
+ await self.sio.emit("dashboard:browser:stats", stats)
401
+
402
+ except Exception as e:
403
+ self.logger.error(f"Error broadcasting browser stats: {e}")
404
+
405
+ def cleanup_old_sessions(self, max_age_hours: int = 24):
406
+ """Clean up old browser sessions and log files.
407
+
408
+ Args:
409
+ max_age_hours: Maximum age in hours before cleanup
410
+ """
411
+ try:
412
+ from datetime import timedelta
413
+
414
+ cutoff_time = datetime.now() - timedelta(hours=max_age_hours)
415
+
416
+ # Remove old browser info
417
+ to_remove = []
418
+ for browser_id, info in self.browser_info.items():
419
+ try:
420
+ last_activity = datetime.fromisoformat(
421
+ info.get("last_activity", "")
422
+ )
423
+ if last_activity < cutoff_time:
424
+ to_remove.append(browser_id)
425
+ except (ValueError, TypeError):
426
+ # Invalid timestamp, mark for removal
427
+ to_remove.append(browser_id)
428
+
429
+ for browser_id in to_remove:
430
+ self.browser_info.pop(browser_id, None)
431
+ self.active_browsers.discard(browser_id)
432
+ self.log_files.pop(browser_id, None)
433
+
434
+ if to_remove:
435
+ self.logger.info(f"Cleaned up {len(to_remove)} old browser sessions")
436
+
437
+ except Exception as e:
438
+ self.logger.error(f"Error cleaning up browser sessions: {e}")
439
+
440
+ def get_stats(self) -> Dict:
441
+ """Get handler statistics.
442
+
443
+ Returns:
444
+ Dictionary with handler stats
445
+ """
446
+ return {
447
+ "total_browsers": len(self.browser_info),
448
+ "active_browsers": len(self.active_browsers),
449
+ "log_files": len(self.log_files),
450
+ "log_directory": str(self.log_dir),
451
+ }