claude-mpm 4.2.9__py3-none-any.whl → 4.2.11__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/cli/commands/dashboard.py +59 -126
- claude_mpm/cli/commands/monitor.py +71 -212
- claude_mpm/cli/commands/run.py +33 -33
- claude_mpm/dashboard/static/css/code-tree.css +8 -16
- claude_mpm/dashboard/static/dist/components/code-tree.js +1 -1
- claude_mpm/dashboard/static/dist/components/file-viewer.js +2 -0
- claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/components/unified-data-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/dashboard.js +1 -1
- claude_mpm/dashboard/static/dist/socket-client.js +1 -1
- claude_mpm/dashboard/static/js/components/code-tree.js +692 -114
- claude_mpm/dashboard/static/js/components/file-viewer.js +538 -0
- claude_mpm/dashboard/static/js/components/module-viewer.js +26 -0
- claude_mpm/dashboard/static/js/components/unified-data-viewer.js +166 -14
- claude_mpm/dashboard/static/js/dashboard.js +108 -91
- claude_mpm/dashboard/static/js/socket-client.js +9 -7
- claude_mpm/dashboard/templates/index.html +2 -7
- claude_mpm/hooks/claude_hooks/hook_handler.py +1 -11
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +54 -59
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +112 -72
- claude_mpm/services/agents/deployment/agent_template_builder.py +0 -1
- claude_mpm/services/cli/unified_dashboard_manager.py +354 -0
- claude_mpm/services/monitor/__init__.py +20 -0
- claude_mpm/services/monitor/daemon.py +256 -0
- claude_mpm/services/monitor/event_emitter.py +279 -0
- claude_mpm/services/monitor/handlers/__init__.py +20 -0
- claude_mpm/services/monitor/handlers/code_analysis.py +334 -0
- claude_mpm/services/monitor/handlers/dashboard.py +298 -0
- claude_mpm/services/monitor/handlers/hooks.py +491 -0
- claude_mpm/services/monitor/management/__init__.py +18 -0
- claude_mpm/services/monitor/management/health.py +124 -0
- claude_mpm/services/monitor/management/lifecycle.py +298 -0
- claude_mpm/services/monitor/server.py +442 -0
- claude_mpm/tools/code_tree_analyzer.py +33 -17
- {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.11.dist-info}/METADATA +1 -1
- {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.11.dist-info}/RECORD +41 -36
- claude_mpm/cli/commands/socketio_monitor.py +0 -233
- claude_mpm/scripts/socketio_daemon.py +0 -571
- claude_mpm/scripts/socketio_daemon_hardened.py +0 -937
- claude_mpm/scripts/socketio_daemon_wrapper.py +0 -78
- claude_mpm/scripts/socketio_server_manager.py +0 -349
- claude_mpm/services/cli/dashboard_launcher.py +0 -423
- claude_mpm/services/cli/socketio_manager.py +0 -595
- claude_mpm/services/dashboard/stable_server.py +0 -1020
- claude_mpm/services/socketio/monitor_server.py +0 -505
- {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.11.dist-info}/WHEEL +0 -0
- {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.11.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.11.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.11.dist-info}/top_level.txt +0 -0
|
@@ -1,1020 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Stable Dashboard Server for claude-mpm.
|
|
3
|
-
|
|
4
|
-
WHY: This module provides a simple, stable HTTP + SocketIO server that works
|
|
5
|
-
across all installation methods (direct, pip, pipx, homebrew, npm).
|
|
6
|
-
|
|
7
|
-
DESIGN DECISIONS:
|
|
8
|
-
- Uses proven python-socketio + aiohttp combination
|
|
9
|
-
- Automatically finds dashboard files across installation methods
|
|
10
|
-
- Provides both HTTP endpoints and SocketIO real-time features
|
|
11
|
-
- Simple mock AST analysis to avoid complex backend dependencies
|
|
12
|
-
- Graceful fallbacks for missing dependencies
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
import asyncio
|
|
16
|
-
import glob
|
|
17
|
-
import json
|
|
18
|
-
import logging
|
|
19
|
-
import os
|
|
20
|
-
import sys
|
|
21
|
-
import time
|
|
22
|
-
import traceback
|
|
23
|
-
from collections import deque
|
|
24
|
-
from datetime import datetime
|
|
25
|
-
from pathlib import Path
|
|
26
|
-
from typing import Any, Deque, Dict, Optional
|
|
27
|
-
|
|
28
|
-
try:
|
|
29
|
-
import aiohttp
|
|
30
|
-
import socketio
|
|
31
|
-
from aiohttp import web
|
|
32
|
-
|
|
33
|
-
DEPENDENCIES_AVAILABLE = True
|
|
34
|
-
except ImportError:
|
|
35
|
-
DEPENDENCIES_AVAILABLE = False
|
|
36
|
-
socketio = None
|
|
37
|
-
aiohttp = None
|
|
38
|
-
web = None
|
|
39
|
-
|
|
40
|
-
# Set up logging
|
|
41
|
-
logging.basicConfig(
|
|
42
|
-
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
43
|
-
)
|
|
44
|
-
logger = logging.getLogger(__name__)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def find_dashboard_files() -> Optional[Path]:
|
|
48
|
-
"""Find dashboard files across different installation methods."""
|
|
49
|
-
# Try different possible locations
|
|
50
|
-
possible_locations = [
|
|
51
|
-
# Development/direct install
|
|
52
|
-
Path(__file__).parent.parent.parent / "dashboard",
|
|
53
|
-
# Current working directory (for development)
|
|
54
|
-
Path.cwd() / "src" / "claude_mpm" / "dashboard",
|
|
55
|
-
# Pip install in current Python environment
|
|
56
|
-
Path(sys.prefix)
|
|
57
|
-
/ "lib"
|
|
58
|
-
/ f"python{sys.version_info.major}.{sys.version_info.minor}"
|
|
59
|
-
/ "site-packages"
|
|
60
|
-
/ "claude_mpm"
|
|
61
|
-
/ "dashboard",
|
|
62
|
-
# User site-packages
|
|
63
|
-
Path.home()
|
|
64
|
-
/ ".local"
|
|
65
|
-
/ "lib"
|
|
66
|
-
/ f"python{sys.version_info.major}.{sys.version_info.minor}"
|
|
67
|
-
/ "site-packages"
|
|
68
|
-
/ "claude_mpm"
|
|
69
|
-
/ "dashboard",
|
|
70
|
-
]
|
|
71
|
-
|
|
72
|
-
# Add glob patterns for different Python versions
|
|
73
|
-
python_patterns = [
|
|
74
|
-
f"/opt/homebrew/lib/python{sys.version_info.major}.{sys.version_info.minor}/site-packages/claude_mpm/dashboard",
|
|
75
|
-
f"/usr/local/lib/python{sys.version_info.major}.{sys.version_info.minor}/site-packages/claude_mpm/dashboard",
|
|
76
|
-
]
|
|
77
|
-
|
|
78
|
-
# Check direct paths first
|
|
79
|
-
for location in possible_locations:
|
|
80
|
-
if location.exists() and (location / "templates" / "index.html").exists():
|
|
81
|
-
return location
|
|
82
|
-
|
|
83
|
-
# Check pattern-based paths
|
|
84
|
-
for pattern in python_patterns:
|
|
85
|
-
matches = glob.glob(pattern)
|
|
86
|
-
for match in matches:
|
|
87
|
-
path = Path(match)
|
|
88
|
-
if path.exists() and (path / "templates" / "index.html").exists():
|
|
89
|
-
return path
|
|
90
|
-
|
|
91
|
-
# Fallback: try to find via module import
|
|
92
|
-
try:
|
|
93
|
-
import claude_mpm.dashboard
|
|
94
|
-
|
|
95
|
-
module_path = Path(claude_mpm.dashboard.__file__).parent
|
|
96
|
-
if (module_path / "templates" / "index.html").exists():
|
|
97
|
-
return module_path
|
|
98
|
-
except ImportError:
|
|
99
|
-
pass
|
|
100
|
-
|
|
101
|
-
return None
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def create_mock_ast_data(file_path: str, file_name: str) -> Dict[str, Any]:
|
|
105
|
-
"""Create mock AST analysis data."""
|
|
106
|
-
ext = file_name.split(".")[-1].lower() if "." in file_name else ""
|
|
107
|
-
|
|
108
|
-
elements = []
|
|
109
|
-
if ext == "py":
|
|
110
|
-
elements = [
|
|
111
|
-
{
|
|
112
|
-
"name": "MockClass",
|
|
113
|
-
"type": "class",
|
|
114
|
-
"line": 10,
|
|
115
|
-
"complexity": 2,
|
|
116
|
-
"docstring": "Mock class for demonstration",
|
|
117
|
-
"methods": [
|
|
118
|
-
{"name": "__init__", "type": "method", "line": 11, "complexity": 1},
|
|
119
|
-
{
|
|
120
|
-
"name": "mock_method",
|
|
121
|
-
"type": "method",
|
|
122
|
-
"line": 15,
|
|
123
|
-
"complexity": 1,
|
|
124
|
-
},
|
|
125
|
-
],
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
"name": "mock_function",
|
|
129
|
-
"type": "function",
|
|
130
|
-
"line": 20,
|
|
131
|
-
"complexity": 1,
|
|
132
|
-
"docstring": "Mock function for demonstration",
|
|
133
|
-
},
|
|
134
|
-
]
|
|
135
|
-
elif ext in ["js", "ts", "jsx", "tsx"]:
|
|
136
|
-
elements = [
|
|
137
|
-
{
|
|
138
|
-
"name": "MockClass",
|
|
139
|
-
"type": "class",
|
|
140
|
-
"line": 5,
|
|
141
|
-
"complexity": 2,
|
|
142
|
-
"methods": [
|
|
143
|
-
{
|
|
144
|
-
"name": "constructor",
|
|
145
|
-
"type": "method",
|
|
146
|
-
"line": 6,
|
|
147
|
-
"complexity": 1,
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
"name": "mockMethod",
|
|
151
|
-
"type": "method",
|
|
152
|
-
"line": 10,
|
|
153
|
-
"complexity": 1,
|
|
154
|
-
},
|
|
155
|
-
],
|
|
156
|
-
},
|
|
157
|
-
{"name": "mockFunction", "type": "function", "line": 15, "complexity": 1},
|
|
158
|
-
]
|
|
159
|
-
|
|
160
|
-
return {
|
|
161
|
-
"path": file_path,
|
|
162
|
-
"elements": elements,
|
|
163
|
-
"complexity": sum(e.get("complexity", 1) for e in elements),
|
|
164
|
-
"lines": 50,
|
|
165
|
-
"stats": {
|
|
166
|
-
"classes": len([e for e in elements if e["type"] == "class"]),
|
|
167
|
-
"functions": len([e for e in elements if e["type"] == "function"]),
|
|
168
|
-
"methods": sum(len(e.get("methods", [])) for e in elements),
|
|
169
|
-
"lines": 50,
|
|
170
|
-
},
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
class StableDashboardServer:
|
|
175
|
-
"""Stable dashboard server that works across all installation methods."""
|
|
176
|
-
|
|
177
|
-
def __init__(self, host: str = "localhost", port: int = 8765, debug: bool = False):
|
|
178
|
-
self.host = host
|
|
179
|
-
self.port = port
|
|
180
|
-
self.debug = debug
|
|
181
|
-
self.dashboard_path = None
|
|
182
|
-
self.app = None
|
|
183
|
-
self.sio = None
|
|
184
|
-
self.server_runner = None
|
|
185
|
-
self.server_site = None
|
|
186
|
-
|
|
187
|
-
# Event storage with circular buffer (keep last 500 events)
|
|
188
|
-
self.event_history: Deque[Dict[str, Any]] = deque(maxlen=500)
|
|
189
|
-
self.event_count = 0
|
|
190
|
-
self.server_start_time = time.time()
|
|
191
|
-
self.last_event_time = None
|
|
192
|
-
self.connected_clients = set()
|
|
193
|
-
|
|
194
|
-
# Resilience features
|
|
195
|
-
self.retry_count = 0
|
|
196
|
-
self.max_retries = 3
|
|
197
|
-
self.health_check_failures = 0
|
|
198
|
-
self.is_healthy = True
|
|
199
|
-
|
|
200
|
-
# Persistent event storage (optional)
|
|
201
|
-
self.persist_events = (
|
|
202
|
-
os.environ.get("CLAUDE_MPM_PERSIST_EVENTS", "false").lower() == "true"
|
|
203
|
-
)
|
|
204
|
-
self.event_log_path = Path.home() / ".claude" / "dashboard_events.jsonl"
|
|
205
|
-
if self.persist_events:
|
|
206
|
-
self.event_log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
207
|
-
|
|
208
|
-
def setup(self) -> bool:
|
|
209
|
-
"""Set up the server components."""
|
|
210
|
-
if not DEPENDENCIES_AVAILABLE:
|
|
211
|
-
print(
|
|
212
|
-
"❌ Error: Missing dependencies. Install with: pip install aiohttp python-socketio"
|
|
213
|
-
)
|
|
214
|
-
return False
|
|
215
|
-
|
|
216
|
-
# Find dashboard files only if not already set (for testing)
|
|
217
|
-
if not self.dashboard_path:
|
|
218
|
-
self.dashboard_path = find_dashboard_files()
|
|
219
|
-
if not self.dashboard_path:
|
|
220
|
-
print("❌ Error: Could not find dashboard files")
|
|
221
|
-
print("Please ensure Claude MPM is properly installed")
|
|
222
|
-
return False
|
|
223
|
-
|
|
224
|
-
# Validate that the dashboard path has the required files
|
|
225
|
-
template_path = self.dashboard_path / "templates" / "index.html"
|
|
226
|
-
static_path = self.dashboard_path / "static"
|
|
227
|
-
|
|
228
|
-
if not template_path.exists():
|
|
229
|
-
print(f"❌ Error: Dashboard template not found at {template_path}")
|
|
230
|
-
print("Please ensure Claude MPM dashboard files are properly installed")
|
|
231
|
-
return False
|
|
232
|
-
|
|
233
|
-
if not static_path.exists():
|
|
234
|
-
print(f"❌ Error: Dashboard static files not found at {static_path}")
|
|
235
|
-
print("Please ensure Claude MPM dashboard files are properly installed")
|
|
236
|
-
return False
|
|
237
|
-
|
|
238
|
-
if self.debug:
|
|
239
|
-
print(f"🔍 Debug: Dashboard path resolved to: {self.dashboard_path}")
|
|
240
|
-
print("🔍 Debug: Checking for required files...")
|
|
241
|
-
template_exists = (
|
|
242
|
-
self.dashboard_path / "templates" / "index.html"
|
|
243
|
-
).exists()
|
|
244
|
-
static_exists = (self.dashboard_path / "static").exists()
|
|
245
|
-
print(f" - templates/index.html: {template_exists}")
|
|
246
|
-
print(f" - static directory: {static_exists}")
|
|
247
|
-
|
|
248
|
-
print(f"📁 Using dashboard files from: {self.dashboard_path}")
|
|
249
|
-
|
|
250
|
-
# Create SocketIO server with improved timeout settings
|
|
251
|
-
logger_enabled = self.debug # Only enable verbose logging in debug mode
|
|
252
|
-
self.sio = socketio.AsyncServer(
|
|
253
|
-
cors_allowed_origins="*",
|
|
254
|
-
logger=logger_enabled,
|
|
255
|
-
engineio_logger=logger_enabled,
|
|
256
|
-
ping_interval=30, # Match client's 30 second ping interval
|
|
257
|
-
ping_timeout=60, # Match client's 60 second timeout
|
|
258
|
-
max_http_buffer_size=1e8, # Allow larger messages
|
|
259
|
-
)
|
|
260
|
-
# Create app WITHOUT any static file handlers to prevent directory listing
|
|
261
|
-
# This is critical - we only want explicit routes we define
|
|
262
|
-
self.app = web.Application()
|
|
263
|
-
self.sio.attach(self.app)
|
|
264
|
-
print("✅ SocketIO server created and attached")
|
|
265
|
-
|
|
266
|
-
# Set up routes
|
|
267
|
-
self._setup_routes()
|
|
268
|
-
self._setup_socketio_events()
|
|
269
|
-
|
|
270
|
-
print("✅ Server setup complete!")
|
|
271
|
-
|
|
272
|
-
return True
|
|
273
|
-
|
|
274
|
-
def _setup_routes(self):
|
|
275
|
-
"""Set up HTTP routes."""
|
|
276
|
-
# IMPORTANT: Only add explicit routes, never add static file serving for root
|
|
277
|
-
# This prevents aiohttp from serving directory listings
|
|
278
|
-
self.app.router.add_get("/", self._serve_dashboard)
|
|
279
|
-
self.app.router.add_get(
|
|
280
|
-
"/index.html", self._serve_dashboard
|
|
281
|
-
) # Also handle /index.html
|
|
282
|
-
self.app.router.add_get("/static/{path:.*}", self._serve_static)
|
|
283
|
-
self.app.router.add_get("/api/directory/list", self._list_directory)
|
|
284
|
-
self.app.router.add_get("/api/file/read", self._read_file)
|
|
285
|
-
self.app.router.add_get("/version.json", self._serve_version)
|
|
286
|
-
|
|
287
|
-
# New resilience endpoints
|
|
288
|
-
self.app.router.add_get("/health", self._health_check)
|
|
289
|
-
self.app.router.add_get("/api/status", self._serve_status)
|
|
290
|
-
self.app.router.add_get("/api/events/history", self._serve_event_history)
|
|
291
|
-
|
|
292
|
-
# CRITICAL: Add the missing /api/events endpoint for receiving events
|
|
293
|
-
self.app.router.add_post("/api/events", self._receive_event)
|
|
294
|
-
|
|
295
|
-
def _setup_socketio_events(self):
|
|
296
|
-
"""Set up SocketIO event handlers."""
|
|
297
|
-
|
|
298
|
-
@self.sio.event
|
|
299
|
-
async def connect(sid, environ):
|
|
300
|
-
self.connected_clients.add(sid)
|
|
301
|
-
if self.debug:
|
|
302
|
-
print(f"✅ SocketIO client connected: {sid}")
|
|
303
|
-
user_agent = environ.get("HTTP_USER_AGENT", "Unknown")
|
|
304
|
-
# Truncate long user agents for readability
|
|
305
|
-
if len(user_agent) > 80:
|
|
306
|
-
user_agent = user_agent[:77] + "..."
|
|
307
|
-
print(f" Client info: {user_agent}")
|
|
308
|
-
|
|
309
|
-
# Send connection confirmation
|
|
310
|
-
await self.sio.emit(
|
|
311
|
-
"connection_test", {"status": "connected", "server": "stable"}, room=sid
|
|
312
|
-
)
|
|
313
|
-
|
|
314
|
-
# Send recent event history to new client
|
|
315
|
-
if self.event_history:
|
|
316
|
-
# Send last 20 events to catch up new client
|
|
317
|
-
recent_events = list(self.event_history)[-20:]
|
|
318
|
-
for event in recent_events:
|
|
319
|
-
await self.sio.emit("claude_event", event, room=sid)
|
|
320
|
-
|
|
321
|
-
@self.sio.event
|
|
322
|
-
async def disconnect(sid):
|
|
323
|
-
self.connected_clients.discard(sid)
|
|
324
|
-
if self.debug:
|
|
325
|
-
print(f"📤 SocketIO client disconnected: {sid}")
|
|
326
|
-
|
|
327
|
-
@self.sio.event
|
|
328
|
-
async def code_analyze_file(sid, data):
|
|
329
|
-
if self.debug:
|
|
330
|
-
print(
|
|
331
|
-
f"📡 Received file analysis request from {sid}: {data.get('path', 'unknown')}"
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
file_path = data.get("path", "")
|
|
335
|
-
file_name = file_path.split("/")[-1] if file_path else "unknown"
|
|
336
|
-
|
|
337
|
-
# Create mock response
|
|
338
|
-
response = create_mock_ast_data(file_path, file_name)
|
|
339
|
-
|
|
340
|
-
if self.debug:
|
|
341
|
-
print(
|
|
342
|
-
f"📤 Sending analysis response: {len(response['elements'])} elements"
|
|
343
|
-
)
|
|
344
|
-
await self.sio.emit("code:file:analyzed", response, room=sid)
|
|
345
|
-
|
|
346
|
-
# CRITICAL: Handle the actual event name with colons that the client sends
|
|
347
|
-
@self.sio.on("code:analyze:file")
|
|
348
|
-
async def handle_code_analyze_file(sid, data):
|
|
349
|
-
if self.debug:
|
|
350
|
-
print(
|
|
351
|
-
f"📡 Received code:analyze:file from {sid}: {data.get('path', 'unknown')}"
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
file_path = data.get("path", "")
|
|
355
|
-
file_name = file_path.split("/")[-1] if file_path else "unknown"
|
|
356
|
-
|
|
357
|
-
# Create mock response
|
|
358
|
-
response = create_mock_ast_data(file_path, file_name)
|
|
359
|
-
|
|
360
|
-
if self.debug:
|
|
361
|
-
print(
|
|
362
|
-
f"📤 Sending analysis response: {len(response['elements'])} elements"
|
|
363
|
-
)
|
|
364
|
-
await self.sio.emit("code:file:analyzed", response, room=sid)
|
|
365
|
-
|
|
366
|
-
# Handle other events the dashboard sends
|
|
367
|
-
@self.sio.event
|
|
368
|
-
async def get_git_branch(sid, data):
|
|
369
|
-
if self.debug:
|
|
370
|
-
print(f"📡 Received git branch request from {sid}: {data}")
|
|
371
|
-
await self.sio.emit(
|
|
372
|
-
"git_branch_response", {"branch": "main", "path": data}, room=sid
|
|
373
|
-
)
|
|
374
|
-
|
|
375
|
-
@self.sio.event
|
|
376
|
-
async def request_status(sid, data):
|
|
377
|
-
if self.debug:
|
|
378
|
-
print(f"📡 Received status request from {sid}")
|
|
379
|
-
await self.sio.emit(
|
|
380
|
-
"status_response", {"status": "running", "server": "stable"}, room=sid
|
|
381
|
-
)
|
|
382
|
-
|
|
383
|
-
# Handle the event with dots (SocketIO converts colons to dots sometimes)
|
|
384
|
-
@self.sio.event
|
|
385
|
-
async def request_dot_status(sid, data):
|
|
386
|
-
if self.debug:
|
|
387
|
-
print(f"📡 Received request.status from {sid}")
|
|
388
|
-
await self.sio.emit(
|
|
389
|
-
"status_response", {"status": "running", "server": "stable"}, room=sid
|
|
390
|
-
)
|
|
391
|
-
|
|
392
|
-
@self.sio.event
|
|
393
|
-
async def code_discover_top_level(sid, data):
|
|
394
|
-
if self.debug:
|
|
395
|
-
print(f"📡 Received top-level discovery request from {sid}")
|
|
396
|
-
await self.sio.emit("code:top_level:discovered", {"status": "ok"}, room=sid)
|
|
397
|
-
|
|
398
|
-
# Mock event generator when no real events
|
|
399
|
-
@self.sio.event
|
|
400
|
-
async def request_mock_event(sid, data):
|
|
401
|
-
"""Generate a mock event for testing."""
|
|
402
|
-
if self.debug:
|
|
403
|
-
print(f"📡 Mock event requested by {sid}")
|
|
404
|
-
|
|
405
|
-
mock_event = self._create_mock_event()
|
|
406
|
-
# Store and broadcast like a real event
|
|
407
|
-
self.event_count += 1
|
|
408
|
-
self.last_event_time = datetime.now()
|
|
409
|
-
self.event_history.append(mock_event)
|
|
410
|
-
await self.sio.emit("claude_event", mock_event)
|
|
411
|
-
|
|
412
|
-
def _create_mock_event(self) -> Dict[str, Any]:
|
|
413
|
-
"""Create a mock event for testing/demo purposes."""
|
|
414
|
-
import random
|
|
415
|
-
|
|
416
|
-
event_types = ["file", "command", "test", "build", "deploy"]
|
|
417
|
-
event_subtypes = ["start", "progress", "complete", "error", "warning"]
|
|
418
|
-
|
|
419
|
-
return {
|
|
420
|
-
"type": random.choice(event_types),
|
|
421
|
-
"subtype": random.choice(event_subtypes),
|
|
422
|
-
"timestamp": datetime.now().isoformat(),
|
|
423
|
-
"source": "mock",
|
|
424
|
-
"data": {
|
|
425
|
-
"message": f"Mock {random.choice(['operation', 'task', 'process'])} {random.choice(['started', 'completed', 'in progress'])}",
|
|
426
|
-
"file": f"/path/to/file_{random.randint(1, 100)}.py",
|
|
427
|
-
"line": random.randint(1, 500),
|
|
428
|
-
"progress": random.randint(0, 100),
|
|
429
|
-
},
|
|
430
|
-
"session_id": "mock-session",
|
|
431
|
-
"server_event_id": self.event_count + 1,
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
async def _start_mock_event_generator(self):
|
|
435
|
-
"""Start generating mock events if no real events for a while."""
|
|
436
|
-
try:
|
|
437
|
-
while True:
|
|
438
|
-
await asyncio.sleep(30) # Check every 30 seconds
|
|
439
|
-
|
|
440
|
-
# If no events in last 60 seconds and clients connected, generate mock
|
|
441
|
-
if self.connected_clients and (
|
|
442
|
-
not self.last_event_time
|
|
443
|
-
or (datetime.now() - self.last_event_time).total_seconds() > 60
|
|
444
|
-
):
|
|
445
|
-
if self.debug:
|
|
446
|
-
print("⏰ No recent events, generating mock event")
|
|
447
|
-
|
|
448
|
-
mock_event = self._create_mock_event()
|
|
449
|
-
self.event_count += 1
|
|
450
|
-
self.last_event_time = datetime.now()
|
|
451
|
-
self.event_history.append(mock_event)
|
|
452
|
-
|
|
453
|
-
await self.sio.emit("claude_event", mock_event)
|
|
454
|
-
except asyncio.CancelledError:
|
|
455
|
-
pass
|
|
456
|
-
except Exception as e:
|
|
457
|
-
logger.error(f"Mock event generator error: {e}")
|
|
458
|
-
|
|
459
|
-
async def _serve_dashboard(self, request):
|
|
460
|
-
"""Serve the main dashboard HTML with fallback."""
|
|
461
|
-
dashboard_file = (
|
|
462
|
-
self.dashboard_path / "templates" / "index.html"
|
|
463
|
-
if self.dashboard_path
|
|
464
|
-
else None
|
|
465
|
-
)
|
|
466
|
-
|
|
467
|
-
# Try to serve actual dashboard
|
|
468
|
-
if dashboard_file and dashboard_file.exists():
|
|
469
|
-
try:
|
|
470
|
-
with open(dashboard_file, encoding="utf-8") as f:
|
|
471
|
-
content = f.read()
|
|
472
|
-
return web.Response(text=content, content_type="text/html")
|
|
473
|
-
except Exception as e:
|
|
474
|
-
logger.error(f"Error reading dashboard template: {e}")
|
|
475
|
-
# Fall through to fallback HTML
|
|
476
|
-
|
|
477
|
-
# Fallback HTML if template missing or error
|
|
478
|
-
fallback_html = """
|
|
479
|
-
<!DOCTYPE html>
|
|
480
|
-
<html lang="en">
|
|
481
|
-
<head>
|
|
482
|
-
<meta charset="UTF-8">
|
|
483
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
484
|
-
<title>Claude MPM Dashboard - Fallback Mode</title>
|
|
485
|
-
<style>
|
|
486
|
-
body { font-family: system-ui, -apple-system, sans-serif; margin: 0; padding: 20px; background: #1e1e1e; color: #e0e0e0; }
|
|
487
|
-
.container { max-width: 1200px; margin: 0 auto; }
|
|
488
|
-
.header { background: #2d2d2d; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
|
|
489
|
-
.status { background: #2d2d2d; padding: 15px; border-radius: 8px; margin-bottom: 20px; }
|
|
490
|
-
.status.healthy { border-left: 4px solid #4caf50; }
|
|
491
|
-
.status.degraded { border-left: 4px solid #ff9800; }
|
|
492
|
-
.events { background: #2d2d2d; padding: 20px; border-radius: 8px; }
|
|
493
|
-
.event { background: #1e1e1e; padding: 10px; margin: 10px 0; border-radius: 4px; }
|
|
494
|
-
h1 { color: #fff; margin: 0; }
|
|
495
|
-
.subtitle { color: #999; margin-top: 5px; }
|
|
496
|
-
.metric { display: inline-block; margin-right: 20px; }
|
|
497
|
-
.metric-label { color: #999; font-size: 12px; }
|
|
498
|
-
.metric-value { color: #fff; font-size: 20px; font-weight: bold; }
|
|
499
|
-
</style>
|
|
500
|
-
</head>
|
|
501
|
-
<body>
|
|
502
|
-
<div class="container">
|
|
503
|
-
<div class="header">
|
|
504
|
-
<h1>Claude MPM Dashboard</h1>
|
|
505
|
-
<div class="subtitle">Fallback Mode - Template not found</div>
|
|
506
|
-
</div>
|
|
507
|
-
|
|
508
|
-
<div id="status" class="status healthy">
|
|
509
|
-
<h3>Server Status</h3>
|
|
510
|
-
<div class="metric">
|
|
511
|
-
<div class="metric-label">Health</div>
|
|
512
|
-
<div class="metric-value" id="health">Loading...</div>
|
|
513
|
-
</div>
|
|
514
|
-
<div class="metric">
|
|
515
|
-
<div class="metric-label">Uptime</div>
|
|
516
|
-
<div class="metric-value" id="uptime">Loading...</div>
|
|
517
|
-
</div>
|
|
518
|
-
<div class="metric">
|
|
519
|
-
<div class="metric-label">Events</div>
|
|
520
|
-
<div class="metric-value" id="events">Loading...</div>
|
|
521
|
-
</div>
|
|
522
|
-
</div>
|
|
523
|
-
|
|
524
|
-
<div class="events">
|
|
525
|
-
<h3>Recent Events</h3>
|
|
526
|
-
<div id="event-list">
|
|
527
|
-
<div class="event">Waiting for events...</div>
|
|
528
|
-
</div>
|
|
529
|
-
</div>
|
|
530
|
-
</div>
|
|
531
|
-
|
|
532
|
-
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
|
|
533
|
-
<script>
|
|
534
|
-
// Fallback dashboard JavaScript
|
|
535
|
-
const socket = io();
|
|
536
|
-
|
|
537
|
-
// Update status periodically
|
|
538
|
-
async function updateStatus() {
|
|
539
|
-
try {
|
|
540
|
-
const response = await fetch('/api/status');
|
|
541
|
-
const data = await response.json();
|
|
542
|
-
|
|
543
|
-
document.getElementById('health').textContent = data.status;
|
|
544
|
-
document.getElementById('uptime').textContent = data.uptime.human;
|
|
545
|
-
document.getElementById('events').textContent = data.events.total;
|
|
546
|
-
|
|
547
|
-
const statusDiv = document.getElementById('status');
|
|
548
|
-
statusDiv.className = data.status === 'running' ? 'status healthy' : 'status degraded';
|
|
549
|
-
} catch (e) {
|
|
550
|
-
console.error('Failed to fetch status:', e);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// Listen for events
|
|
555
|
-
socket.on('claude_event', (event) => {
|
|
556
|
-
const eventList = document.getElementById('event-list');
|
|
557
|
-
const eventDiv = document.createElement('div');
|
|
558
|
-
eventDiv.className = 'event';
|
|
559
|
-
eventDiv.textContent = JSON.stringify(event, null, 2);
|
|
560
|
-
eventList.insertBefore(eventDiv, eventList.firstChild);
|
|
561
|
-
|
|
562
|
-
// Keep only last 10 events
|
|
563
|
-
while (eventList.children.length > 10) {
|
|
564
|
-
eventList.removeChild(eventList.lastChild);
|
|
565
|
-
}
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
socket.on('connect', () => {
|
|
569
|
-
console.log('Connected to dashboard server');
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
// Initial load and periodic updates
|
|
573
|
-
updateStatus();
|
|
574
|
-
setInterval(updateStatus, 5000);
|
|
575
|
-
</script>
|
|
576
|
-
</body>
|
|
577
|
-
</html>
|
|
578
|
-
"""
|
|
579
|
-
|
|
580
|
-
logger.warning("Serving fallback dashboard HTML")
|
|
581
|
-
return web.Response(text=fallback_html, content_type="text/html")
|
|
582
|
-
|
|
583
|
-
async def _serve_static(self, request):
|
|
584
|
-
"""Serve static files."""
|
|
585
|
-
file_path = request.match_info["path"]
|
|
586
|
-
static_file = self.dashboard_path / "static" / file_path
|
|
587
|
-
|
|
588
|
-
if static_file.exists() and static_file.is_file():
|
|
589
|
-
content_type = (
|
|
590
|
-
"text/javascript"
|
|
591
|
-
if file_path.endswith(".js")
|
|
592
|
-
else "text/css" if file_path.endswith(".css") else "text/plain"
|
|
593
|
-
)
|
|
594
|
-
with open(static_file) as f:
|
|
595
|
-
content = f.read()
|
|
596
|
-
return web.Response(text=content, content_type=content_type)
|
|
597
|
-
return web.Response(text="File not found", status=404)
|
|
598
|
-
|
|
599
|
-
async def _list_directory(self, request):
|
|
600
|
-
"""List directory contents."""
|
|
601
|
-
path = request.query.get("path", ".")
|
|
602
|
-
abs_path = os.path.abspath(os.path.expanduser(path))
|
|
603
|
-
|
|
604
|
-
result = {"path": abs_path, "exists": os.path.exists(abs_path), "contents": []}
|
|
605
|
-
|
|
606
|
-
if os.path.exists(abs_path) and os.path.isdir(abs_path):
|
|
607
|
-
try:
|
|
608
|
-
for item in sorted(os.listdir(abs_path)):
|
|
609
|
-
item_path = os.path.join(abs_path, item)
|
|
610
|
-
result["contents"].append(
|
|
611
|
-
{
|
|
612
|
-
"name": item,
|
|
613
|
-
"path": item_path,
|
|
614
|
-
"is_directory": os.path.isdir(item_path),
|
|
615
|
-
"is_file": os.path.isfile(item_path),
|
|
616
|
-
"is_code_file": item.endswith(
|
|
617
|
-
(".py", ".js", ".ts", ".jsx", ".tsx")
|
|
618
|
-
),
|
|
619
|
-
}
|
|
620
|
-
)
|
|
621
|
-
except PermissionError:
|
|
622
|
-
result["error"] = "Permission denied"
|
|
623
|
-
|
|
624
|
-
return web.json_response(result)
|
|
625
|
-
|
|
626
|
-
async def _read_file(self, request):
|
|
627
|
-
"""Read file content for source viewer."""
|
|
628
|
-
file_path = request.query.get("path", "")
|
|
629
|
-
|
|
630
|
-
if not file_path:
|
|
631
|
-
return web.json_response({"error": "No path provided"}, status=400)
|
|
632
|
-
|
|
633
|
-
abs_path = os.path.abspath(os.path.expanduser(file_path))
|
|
634
|
-
|
|
635
|
-
# Security check - ensure file is within the project
|
|
636
|
-
try:
|
|
637
|
-
# Get the project root (current working directory)
|
|
638
|
-
project_root = os.getcwd()
|
|
639
|
-
# Ensure the path is within the project
|
|
640
|
-
if not abs_path.startswith(project_root):
|
|
641
|
-
return web.json_response({"error": "Access denied"}, status=403)
|
|
642
|
-
except Exception:
|
|
643
|
-
pass # Allow read if we can't determine project root
|
|
644
|
-
|
|
645
|
-
if not os.path.exists(abs_path):
|
|
646
|
-
return web.json_response({"error": "File not found"}, status=404)
|
|
647
|
-
|
|
648
|
-
if not os.path.isfile(abs_path):
|
|
649
|
-
return web.json_response({"error": "Not a file"}, status=400)
|
|
650
|
-
|
|
651
|
-
try:
|
|
652
|
-
# Determine file type
|
|
653
|
-
file_ext = os.path.splitext(abs_path)[1].lower()
|
|
654
|
-
is_json = file_ext in [".json", ".jsonl", ".geojson"]
|
|
655
|
-
|
|
656
|
-
# Read file with appropriate encoding
|
|
657
|
-
encodings = ["utf-8", "latin-1", "cp1252"]
|
|
658
|
-
content = None
|
|
659
|
-
|
|
660
|
-
for encoding in encodings:
|
|
661
|
-
try:
|
|
662
|
-
with open(abs_path, encoding=encoding) as f:
|
|
663
|
-
content = f.read()
|
|
664
|
-
break
|
|
665
|
-
except UnicodeDecodeError:
|
|
666
|
-
continue
|
|
667
|
-
|
|
668
|
-
if content is None:
|
|
669
|
-
return web.json_response({"error": "Could not decode file"}, status=400)
|
|
670
|
-
|
|
671
|
-
# Format JSON files for better readability
|
|
672
|
-
formatted_content = content
|
|
673
|
-
is_valid_json = False
|
|
674
|
-
if is_json:
|
|
675
|
-
try:
|
|
676
|
-
import json
|
|
677
|
-
|
|
678
|
-
parsed = json.loads(content)
|
|
679
|
-
formatted_content = json.dumps(parsed, indent=2, sort_keys=False)
|
|
680
|
-
is_valid_json = True
|
|
681
|
-
except json.JSONDecodeError:
|
|
682
|
-
# Not valid JSON, return as-is
|
|
683
|
-
is_valid_json = False
|
|
684
|
-
|
|
685
|
-
return web.json_response(
|
|
686
|
-
{
|
|
687
|
-
"path": abs_path,
|
|
688
|
-
"name": os.path.basename(abs_path),
|
|
689
|
-
"content": formatted_content,
|
|
690
|
-
"lines": len(formatted_content.splitlines()),
|
|
691
|
-
"size": os.path.getsize(abs_path),
|
|
692
|
-
"type": "json" if is_json else "text",
|
|
693
|
-
"is_valid_json": is_valid_json,
|
|
694
|
-
}
|
|
695
|
-
)
|
|
696
|
-
|
|
697
|
-
except PermissionError:
|
|
698
|
-
return web.json_response({"error": "Permission denied"}, status=403)
|
|
699
|
-
except Exception as e:
|
|
700
|
-
return web.json_response({"error": str(e)}, status=500)
|
|
701
|
-
|
|
702
|
-
async def _health_check(self, request):
|
|
703
|
-
"""Health check endpoint for monitoring."""
|
|
704
|
-
uptime = time.time() - self.server_start_time
|
|
705
|
-
status = "healthy" if self.is_healthy else "degraded"
|
|
706
|
-
|
|
707
|
-
health_info = {
|
|
708
|
-
"status": status,
|
|
709
|
-
"uptime_seconds": round(uptime, 2),
|
|
710
|
-
"connected_clients": len(self.connected_clients),
|
|
711
|
-
"event_count": self.event_count,
|
|
712
|
-
"last_event": (
|
|
713
|
-
self.last_event_time.isoformat() if self.last_event_time else None
|
|
714
|
-
),
|
|
715
|
-
"retry_count": self.retry_count,
|
|
716
|
-
"health_check_failures": self.health_check_failures,
|
|
717
|
-
"event_history_size": len(self.event_history),
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
status_code = 200 if self.is_healthy else 503
|
|
721
|
-
return web.json_response(health_info, status=status_code)
|
|
722
|
-
|
|
723
|
-
async def _serve_status(self, request):
|
|
724
|
-
"""Detailed server status endpoint."""
|
|
725
|
-
uptime = time.time() - self.server_start_time
|
|
726
|
-
|
|
727
|
-
status_info = {
|
|
728
|
-
"server": "stable",
|
|
729
|
-
"version": "4.2.3",
|
|
730
|
-
"status": "running" if self.is_healthy else "degraded",
|
|
731
|
-
"uptime": {
|
|
732
|
-
"seconds": round(uptime, 2),
|
|
733
|
-
"human": self._format_uptime(uptime),
|
|
734
|
-
},
|
|
735
|
-
"connections": {
|
|
736
|
-
"active": len(self.connected_clients),
|
|
737
|
-
"clients": list(self.connected_clients),
|
|
738
|
-
},
|
|
739
|
-
"events": {
|
|
740
|
-
"total": self.event_count,
|
|
741
|
-
"buffered": len(self.event_history),
|
|
742
|
-
"last_received": (
|
|
743
|
-
self.last_event_time.isoformat() if self.last_event_time else None
|
|
744
|
-
),
|
|
745
|
-
},
|
|
746
|
-
"features": [
|
|
747
|
-
"http",
|
|
748
|
-
"socketio",
|
|
749
|
-
"event_bridge",
|
|
750
|
-
"health_monitoring",
|
|
751
|
-
"auto_retry",
|
|
752
|
-
"event_history",
|
|
753
|
-
"graceful_degradation",
|
|
754
|
-
],
|
|
755
|
-
"resilience": {
|
|
756
|
-
"retry_count": self.retry_count,
|
|
757
|
-
"max_retries": self.max_retries,
|
|
758
|
-
"health_failures": self.health_check_failures,
|
|
759
|
-
"persist_events": self.persist_events,
|
|
760
|
-
},
|
|
761
|
-
}
|
|
762
|
-
return web.json_response(status_info)
|
|
763
|
-
|
|
764
|
-
async def _serve_event_history(self, request):
|
|
765
|
-
"""Serve recent event history."""
|
|
766
|
-
limit = int(request.query.get("limit", "100"))
|
|
767
|
-
events = list(self.event_history)[-limit:]
|
|
768
|
-
return web.json_response(
|
|
769
|
-
{"events": events, "count": len(events), "total_events": self.event_count}
|
|
770
|
-
)
|
|
771
|
-
|
|
772
|
-
async def _receive_event(self, request):
|
|
773
|
-
"""Receive events from hook system via HTTP POST."""
|
|
774
|
-
try:
|
|
775
|
-
# Parse event data
|
|
776
|
-
data = await request.json()
|
|
777
|
-
|
|
778
|
-
# Add server metadata
|
|
779
|
-
event = {
|
|
780
|
-
**data,
|
|
781
|
-
"received_at": datetime.now().isoformat(),
|
|
782
|
-
"server_event_id": self.event_count + 1,
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
# Update tracking
|
|
786
|
-
self.event_count += 1
|
|
787
|
-
self.last_event_time = datetime.now()
|
|
788
|
-
|
|
789
|
-
# Store in circular buffer
|
|
790
|
-
self.event_history.append(event)
|
|
791
|
-
|
|
792
|
-
# Persist to disk if enabled
|
|
793
|
-
if self.persist_events:
|
|
794
|
-
try:
|
|
795
|
-
with open(self.event_log_path, "a") as f:
|
|
796
|
-
f.write(json.dumps(event) + "\n")
|
|
797
|
-
except Exception as e:
|
|
798
|
-
logger.error(f"Failed to persist event: {e}")
|
|
799
|
-
|
|
800
|
-
# Emit to all connected SocketIO clients
|
|
801
|
-
if self.sio and self.connected_clients:
|
|
802
|
-
await self.sio.emit("claude_event", event)
|
|
803
|
-
if self.debug:
|
|
804
|
-
print(
|
|
805
|
-
f"📡 Forwarded event to {len(self.connected_clients)} clients"
|
|
806
|
-
)
|
|
807
|
-
|
|
808
|
-
# Return success response
|
|
809
|
-
return web.json_response(
|
|
810
|
-
{
|
|
811
|
-
"status": "received",
|
|
812
|
-
"event_id": event["server_event_id"],
|
|
813
|
-
"clients_notified": len(self.connected_clients),
|
|
814
|
-
}
|
|
815
|
-
)
|
|
816
|
-
|
|
817
|
-
except json.JSONDecodeError as e:
|
|
818
|
-
logger.error(f"Invalid JSON in event request: {e}")
|
|
819
|
-
return web.json_response(
|
|
820
|
-
{"error": "Invalid JSON", "details": str(e)}, status=400
|
|
821
|
-
)
|
|
822
|
-
except Exception as e:
|
|
823
|
-
logger.error(f"Error processing event: {e}")
|
|
824
|
-
if self.debug:
|
|
825
|
-
traceback.print_exc()
|
|
826
|
-
return web.json_response(
|
|
827
|
-
{"error": "Failed to process event", "details": str(e)}, status=500
|
|
828
|
-
)
|
|
829
|
-
|
|
830
|
-
async def _serve_version(self, request):
|
|
831
|
-
"""Serve version information."""
|
|
832
|
-
version_info = {
|
|
833
|
-
"version": "4.2.3",
|
|
834
|
-
"server": "stable",
|
|
835
|
-
"features": ["http", "socketio", "event_bridge", "resilience"],
|
|
836
|
-
"status": "running" if self.is_healthy else "degraded",
|
|
837
|
-
}
|
|
838
|
-
return web.json_response(version_info)
|
|
839
|
-
|
|
840
|
-
def _format_uptime(self, seconds: float) -> str:
|
|
841
|
-
"""Format uptime in human-readable format."""
|
|
842
|
-
days = int(seconds // 86400)
|
|
843
|
-
hours = int((seconds % 86400) // 3600)
|
|
844
|
-
minutes = int((seconds % 3600) // 60)
|
|
845
|
-
secs = int(seconds % 60)
|
|
846
|
-
|
|
847
|
-
parts = []
|
|
848
|
-
if days > 0:
|
|
849
|
-
parts.append(f"{days}d")
|
|
850
|
-
if hours > 0:
|
|
851
|
-
parts.append(f"{hours}h")
|
|
852
|
-
if minutes > 0:
|
|
853
|
-
parts.append(f"{minutes}m")
|
|
854
|
-
parts.append(f"{secs}s")
|
|
855
|
-
|
|
856
|
-
return " ".join(parts)
|
|
857
|
-
|
|
858
|
-
def run(self):
|
|
859
|
-
"""Run the server with automatic restart on crash."""
|
|
860
|
-
restart_attempts = 0
|
|
861
|
-
max_restart_attempts = 5
|
|
862
|
-
|
|
863
|
-
while restart_attempts < max_restart_attempts:
|
|
864
|
-
try:
|
|
865
|
-
print(
|
|
866
|
-
f"🔧 Setting up server... (attempt {restart_attempts + 1}/{max_restart_attempts})"
|
|
867
|
-
)
|
|
868
|
-
|
|
869
|
-
# Reset health status on restart
|
|
870
|
-
self.is_healthy = True
|
|
871
|
-
self.health_check_failures = 0
|
|
872
|
-
|
|
873
|
-
if not self.setup():
|
|
874
|
-
if not DEPENDENCIES_AVAILABLE:
|
|
875
|
-
print("❌ Missing required dependencies")
|
|
876
|
-
return False
|
|
877
|
-
|
|
878
|
-
# Continue with fallback mode even if dashboard files not found
|
|
879
|
-
print("⚠️ Dashboard files not found - running in fallback mode")
|
|
880
|
-
print(
|
|
881
|
-
" Server will provide basic functionality and receive events"
|
|
882
|
-
)
|
|
883
|
-
|
|
884
|
-
# Set up minimal server without dashboard files
|
|
885
|
-
self.sio = socketio.AsyncServer(
|
|
886
|
-
cors_allowed_origins="*",
|
|
887
|
-
logger=self.debug,
|
|
888
|
-
engineio_logger=self.debug,
|
|
889
|
-
ping_interval=30,
|
|
890
|
-
ping_timeout=60,
|
|
891
|
-
max_http_buffer_size=1e8,
|
|
892
|
-
)
|
|
893
|
-
self.app = web.Application()
|
|
894
|
-
self.sio.attach(self.app)
|
|
895
|
-
self._setup_routes()
|
|
896
|
-
self._setup_socketio_events()
|
|
897
|
-
|
|
898
|
-
return self._run_with_resilience()
|
|
899
|
-
|
|
900
|
-
except Exception as e:
|
|
901
|
-
restart_attempts += 1
|
|
902
|
-
logger.error(f"Server crashed: {e}")
|
|
903
|
-
if self.debug:
|
|
904
|
-
traceback.print_exc()
|
|
905
|
-
|
|
906
|
-
if restart_attempts < max_restart_attempts:
|
|
907
|
-
wait_time = min(
|
|
908
|
-
2**restart_attempts, 30
|
|
909
|
-
) # Exponential backoff, max 30s
|
|
910
|
-
print(f"🔄 Restarting server in {wait_time} seconds...")
|
|
911
|
-
time.sleep(wait_time)
|
|
912
|
-
else:
|
|
913
|
-
print(
|
|
914
|
-
f"❌ Server failed after {max_restart_attempts} restart attempts"
|
|
915
|
-
)
|
|
916
|
-
return False
|
|
917
|
-
|
|
918
|
-
return False
|
|
919
|
-
|
|
920
|
-
def _run_with_resilience(self):
|
|
921
|
-
"""Run server with port conflict resolution and error handling."""
|
|
922
|
-
|
|
923
|
-
print(f"🚀 Starting stable dashboard server at http://{self.host}:{self.port}")
|
|
924
|
-
print("✅ Server ready: HTTP + SocketIO with resilience features")
|
|
925
|
-
print("🛡️ Resilience features enabled:")
|
|
926
|
-
print(" - Automatic restart on crash")
|
|
927
|
-
print(" - Health monitoring endpoint (/health)")
|
|
928
|
-
print(" - Event history buffer (500 events)")
|
|
929
|
-
print(" - Graceful degradation")
|
|
930
|
-
print(" - Connection retry logic")
|
|
931
|
-
print("📡 SocketIO events:")
|
|
932
|
-
print(" - claude_event (real-time events from hooks)")
|
|
933
|
-
print(" - code:analyze:file (code analysis)")
|
|
934
|
-
print(" - connection management")
|
|
935
|
-
print("🌐 HTTP endpoints:")
|
|
936
|
-
print(" - GET / (dashboard)")
|
|
937
|
-
print(" - GET /health (health check)")
|
|
938
|
-
print(" - POST /api/events (receive hook events)")
|
|
939
|
-
print(" - GET /api/status (detailed status)")
|
|
940
|
-
print(" - GET /api/events/history (event history)")
|
|
941
|
-
print(" - GET /api/directory/list")
|
|
942
|
-
print(" - GET /api/file/read")
|
|
943
|
-
print(f"\n🔗 Open in browser: http://{self.host}:{self.port}")
|
|
944
|
-
print("\n Press Ctrl+C to stop the server\n")
|
|
945
|
-
|
|
946
|
-
# Try to start server with port conflict handling
|
|
947
|
-
max_port_attempts = 10
|
|
948
|
-
original_port = self.port
|
|
949
|
-
|
|
950
|
-
for attempt in range(max_port_attempts):
|
|
951
|
-
try:
|
|
952
|
-
# Use the print_func parameter to control access log output
|
|
953
|
-
if self.debug:
|
|
954
|
-
web.run_app(self.app, host=self.host, port=self.port)
|
|
955
|
-
else:
|
|
956
|
-
web.run_app(
|
|
957
|
-
self.app,
|
|
958
|
-
host=self.host,
|
|
959
|
-
port=self.port,
|
|
960
|
-
access_log=None,
|
|
961
|
-
print=lambda *args: None, # Suppress startup messages in non-debug mode
|
|
962
|
-
)
|
|
963
|
-
return True # Server started successfully
|
|
964
|
-
except KeyboardInterrupt:
|
|
965
|
-
print("\n🛑 Server stopped by user")
|
|
966
|
-
return True
|
|
967
|
-
except OSError as e:
|
|
968
|
-
error_str = str(e)
|
|
969
|
-
if (
|
|
970
|
-
"[Errno 48]" in error_str
|
|
971
|
-
or "Address already in use" in error_str
|
|
972
|
-
or "address already in use" in error_str.lower()
|
|
973
|
-
):
|
|
974
|
-
# Port is already in use
|
|
975
|
-
if attempt < max_port_attempts - 1:
|
|
976
|
-
self.port += 1
|
|
977
|
-
print(
|
|
978
|
-
f"⚠️ Port {self.port - 1} is in use, trying port {self.port}..."
|
|
979
|
-
)
|
|
980
|
-
# Recreate the app with new port
|
|
981
|
-
self.setup()
|
|
982
|
-
else:
|
|
983
|
-
print(
|
|
984
|
-
f"❌ Could not find available port after {max_port_attempts} attempts"
|
|
985
|
-
)
|
|
986
|
-
print(f" Ports {original_port} to {self.port} are all in use")
|
|
987
|
-
print(
|
|
988
|
-
"\n💡 Tip: Check if another dashboard instance is running"
|
|
989
|
-
)
|
|
990
|
-
print(" You can stop it with: claude-mpm dashboard stop")
|
|
991
|
-
return False
|
|
992
|
-
else:
|
|
993
|
-
# Other OS error
|
|
994
|
-
print(f"❌ Server error: {e}")
|
|
995
|
-
if self.debug:
|
|
996
|
-
import traceback
|
|
997
|
-
|
|
998
|
-
traceback.print_exc()
|
|
999
|
-
return False
|
|
1000
|
-
except Exception as e:
|
|
1001
|
-
print(f"❌ Unexpected server error: {e}")
|
|
1002
|
-
if self.debug:
|
|
1003
|
-
import traceback
|
|
1004
|
-
|
|
1005
|
-
traceback.print_exc()
|
|
1006
|
-
else:
|
|
1007
|
-
print("\n💡 Run with --debug flag for more details")
|
|
1008
|
-
return False
|
|
1009
|
-
|
|
1010
|
-
return True
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
def create_stable_server(
|
|
1014
|
-
dashboard_path: Optional[Path] = None, **kwargs
|
|
1015
|
-
) -> StableDashboardServer:
|
|
1016
|
-
"""Create a stable dashboard server instance."""
|
|
1017
|
-
server = StableDashboardServer(**kwargs)
|
|
1018
|
-
if dashboard_path:
|
|
1019
|
-
server.dashboard_path = dashboard_path
|
|
1020
|
-
return server
|