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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/BASE_ENGINEER.md +114 -1
- claude_mpm/agents/BASE_OPS.md +156 -1
- claude_mpm/agents/INSTRUCTIONS.md +120 -11
- claude_mpm/agents/WORKFLOW.md +160 -10
- claude_mpm/agents/templates/agentic-coder-optimizer.json +17 -12
- claude_mpm/agents/templates/react_engineer.json +217 -0
- claude_mpm/agents/templates/web_qa.json +40 -4
- claude_mpm/commands/mpm-browser-monitor.md +370 -0
- claude_mpm/commands/mpm-monitor.md +177 -0
- claude_mpm/dashboard/static/built/components/code-viewer.js +1076 -2
- claude_mpm/dashboard/static/built/components/ui-state-manager.js +465 -2
- claude_mpm/dashboard/static/css/dashboard.css +2 -0
- claude_mpm/dashboard/static/js/browser-console-monitor.js +495 -0
- claude_mpm/dashboard/static/js/components/browser-log-viewer.js +763 -0
- claude_mpm/dashboard/static/js/components/code-viewer.js +931 -340
- claude_mpm/dashboard/static/js/components/diff-viewer.js +891 -0
- claude_mpm/dashboard/static/js/components/file-change-tracker.js +443 -0
- claude_mpm/dashboard/static/js/components/file-change-viewer.js +690 -0
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +156 -19
- claude_mpm/dashboard/static/js/dashboard.js +16 -0
- claude_mpm/dashboard/static/js/socket-client.js +2 -2
- claude_mpm/dashboard/static/test-browser-monitor.html +470 -0
- claude_mpm/dashboard/templates/index.html +64 -99
- claude_mpm/services/monitor/handlers/browser.py +451 -0
- claude_mpm/services/monitor/server.py +267 -4
- {claude_mpm-4.2.40.dist-info → claude_mpm-4.2.43.dist-info}/METADATA +1 -1
- {claude_mpm-4.2.40.dist-info → claude_mpm-4.2.43.dist-info}/RECORD +32 -26
- claude_mpm/agents/templates/agentic-coder-optimizer.md +0 -44
- claude_mpm/agents/templates/agentic_coder_optimizer.json +0 -238
- claude_mpm/agents/templates/test-non-mpm.json +0 -20
- claude_mpm/dashboard/static/dist/components/code-viewer.js +0 -2
- {claude_mpm-4.2.40.dist-info → claude_mpm-4.2.43.dist-info}/WHEEL +0 -0
- {claude_mpm-4.2.40.dist-info → claude_mpm-4.2.43.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.2.40.dist-info → claude_mpm-4.2.43.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
<
|
245
|
-
<
|
246
|
-
<
|
247
|
-
<
|
248
|
-
<
|
249
|
-
<
|
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
|
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
|
-
<!--
|
389
|
-
<div class="tab-content" id="
|
390
|
-
<div
|
391
|
-
|
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()">×</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/
|
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
|
+
}
|