claude-mpm 4.2.7__py3-none-any.whl → 4.2.9__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 +24 -15
- claude_mpm/cli/parser.py +79 -2
- claude_mpm/cli/parsers/__init__.py +29 -0
- claude_mpm/dashboard/static/css/code-tree.css +22 -2
- claude_mpm/dashboard/static/css/dashboard.css +15 -1
- claude_mpm/dashboard/static/js/components/code-tree.js +90 -35
- claude_mpm/dashboard/templates/index.html +9 -1
- claude_mpm/services/agents/deployment/agent_format_converter.py +3 -3
- claude_mpm/services/agents/deployment/agent_template_builder.py +3 -4
- claude_mpm/services/dashboard/stable_server.py +175 -117
- claude_mpm/services/socketio/client_proxy.py +20 -12
- claude_mpm/services/socketio/dashboard_server.py +4 -4
- claude_mpm/services/socketio/monitor_client.py +4 -6
- claude_mpm/services/socketio/monitor_server.py +2 -2
- {claude_mpm-4.2.7.dist-info → claude_mpm-4.2.9.dist-info}/METADATA +1 -1
- {claude_mpm-4.2.7.dist-info → claude_mpm-4.2.9.dist-info}/RECORD +21 -21
- {claude_mpm-4.2.7.dist-info → claude_mpm-4.2.9.dist-info}/WHEEL +0 -0
- {claude_mpm-4.2.7.dist-info → claude_mpm-4.2.9.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.2.7.dist-info → claude_mpm-4.2.9.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.2.7.dist-info → claude_mpm-4.2.9.dist-info}/top_level.txt +0 -0
|
@@ -39,8 +39,7 @@ except ImportError:
|
|
|
39
39
|
|
|
40
40
|
# Set up logging
|
|
41
41
|
logging.basicConfig(
|
|
42
|
-
level=logging.INFO,
|
|
43
|
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
42
|
+
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
44
43
|
)
|
|
45
44
|
logger = logging.getLogger(__name__)
|
|
46
45
|
|
|
@@ -184,23 +183,25 @@ class StableDashboardServer:
|
|
|
184
183
|
self.sio = None
|
|
185
184
|
self.server_runner = None
|
|
186
185
|
self.server_site = None
|
|
187
|
-
|
|
186
|
+
|
|
188
187
|
# Event storage with circular buffer (keep last 500 events)
|
|
189
188
|
self.event_history: Deque[Dict[str, Any]] = deque(maxlen=500)
|
|
190
189
|
self.event_count = 0
|
|
191
190
|
self.server_start_time = time.time()
|
|
192
191
|
self.last_event_time = None
|
|
193
192
|
self.connected_clients = set()
|
|
194
|
-
|
|
193
|
+
|
|
195
194
|
# Resilience features
|
|
196
195
|
self.retry_count = 0
|
|
197
196
|
self.max_retries = 3
|
|
198
197
|
self.health_check_failures = 0
|
|
199
198
|
self.is_healthy = True
|
|
200
|
-
|
|
199
|
+
|
|
201
200
|
# Persistent event storage (optional)
|
|
202
|
-
self.persist_events =
|
|
203
|
-
|
|
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"
|
|
204
205
|
if self.persist_events:
|
|
205
206
|
self.event_log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
206
207
|
|
|
@@ -219,16 +220,16 @@ class StableDashboardServer:
|
|
|
219
220
|
print("❌ Error: Could not find dashboard files")
|
|
220
221
|
print("Please ensure Claude MPM is properly installed")
|
|
221
222
|
return False
|
|
222
|
-
|
|
223
|
+
|
|
223
224
|
# Validate that the dashboard path has the required files
|
|
224
225
|
template_path = self.dashboard_path / "templates" / "index.html"
|
|
225
226
|
static_path = self.dashboard_path / "static"
|
|
226
|
-
|
|
227
|
+
|
|
227
228
|
if not template_path.exists():
|
|
228
229
|
print(f"❌ Error: Dashboard template not found at {template_path}")
|
|
229
230
|
print("Please ensure Claude MPM dashboard files are properly installed")
|
|
230
231
|
return False
|
|
231
|
-
|
|
232
|
+
|
|
232
233
|
if not static_path.exists():
|
|
233
234
|
print(f"❌ Error: Dashboard static files not found at {static_path}")
|
|
234
235
|
print("Please ensure Claude MPM dashboard files are properly installed")
|
|
@@ -236,8 +237,10 @@ class StableDashboardServer:
|
|
|
236
237
|
|
|
237
238
|
if self.debug:
|
|
238
239
|
print(f"🔍 Debug: Dashboard path resolved to: {self.dashboard_path}")
|
|
239
|
-
print(
|
|
240
|
-
template_exists = (
|
|
240
|
+
print("🔍 Debug: Checking for required files...")
|
|
241
|
+
template_exists = (
|
|
242
|
+
self.dashboard_path / "templates" / "index.html"
|
|
243
|
+
).exists()
|
|
241
244
|
static_exists = (self.dashboard_path / "static").exists()
|
|
242
245
|
print(f" - templates/index.html: {template_exists}")
|
|
243
246
|
print(f" - static directory: {static_exists}")
|
|
@@ -273,17 +276,19 @@ class StableDashboardServer:
|
|
|
273
276
|
# IMPORTANT: Only add explicit routes, never add static file serving for root
|
|
274
277
|
# This prevents aiohttp from serving directory listings
|
|
275
278
|
self.app.router.add_get("/", self._serve_dashboard)
|
|
276
|
-
self.app.router.add_get(
|
|
279
|
+
self.app.router.add_get(
|
|
280
|
+
"/index.html", self._serve_dashboard
|
|
281
|
+
) # Also handle /index.html
|
|
277
282
|
self.app.router.add_get("/static/{path:.*}", self._serve_static)
|
|
278
283
|
self.app.router.add_get("/api/directory/list", self._list_directory)
|
|
279
284
|
self.app.router.add_get("/api/file/read", self._read_file)
|
|
280
285
|
self.app.router.add_get("/version.json", self._serve_version)
|
|
281
|
-
|
|
286
|
+
|
|
282
287
|
# New resilience endpoints
|
|
283
288
|
self.app.router.add_get("/health", self._health_check)
|
|
284
289
|
self.app.router.add_get("/api/status", self._serve_status)
|
|
285
290
|
self.app.router.add_get("/api/events/history", self._serve_event_history)
|
|
286
|
-
|
|
291
|
+
|
|
287
292
|
# CRITICAL: Add the missing /api/events endpoint for receiving events
|
|
288
293
|
self.app.router.add_post("/api/events", self._receive_event)
|
|
289
294
|
|
|
@@ -295,17 +300,17 @@ class StableDashboardServer:
|
|
|
295
300
|
self.connected_clients.add(sid)
|
|
296
301
|
if self.debug:
|
|
297
302
|
print(f"✅ SocketIO client connected: {sid}")
|
|
298
|
-
user_agent = environ.get(
|
|
303
|
+
user_agent = environ.get("HTTP_USER_AGENT", "Unknown")
|
|
299
304
|
# Truncate long user agents for readability
|
|
300
305
|
if len(user_agent) > 80:
|
|
301
306
|
user_agent = user_agent[:77] + "..."
|
|
302
307
|
print(f" Client info: {user_agent}")
|
|
303
|
-
|
|
308
|
+
|
|
304
309
|
# Send connection confirmation
|
|
305
310
|
await self.sio.emit(
|
|
306
311
|
"connection_test", {"status": "connected", "server": "stable"}, room=sid
|
|
307
312
|
)
|
|
308
|
-
|
|
313
|
+
|
|
309
314
|
# Send recent event history to new client
|
|
310
315
|
if self.event_history:
|
|
311
316
|
# Send last 20 events to catch up new client
|
|
@@ -333,7 +338,9 @@ class StableDashboardServer:
|
|
|
333
338
|
response = create_mock_ast_data(file_path, file_name)
|
|
334
339
|
|
|
335
340
|
if self.debug:
|
|
336
|
-
print(
|
|
341
|
+
print(
|
|
342
|
+
f"📤 Sending analysis response: {len(response['elements'])} elements"
|
|
343
|
+
)
|
|
337
344
|
await self.sio.emit("code:file:analyzed", response, room=sid)
|
|
338
345
|
|
|
339
346
|
# CRITICAL: Handle the actual event name with colons that the client sends
|
|
@@ -351,7 +358,9 @@ class StableDashboardServer:
|
|
|
351
358
|
response = create_mock_ast_data(file_path, file_name)
|
|
352
359
|
|
|
353
360
|
if self.debug:
|
|
354
|
-
print(
|
|
361
|
+
print(
|
|
362
|
+
f"📤 Sending analysis response: {len(response['elements'])} elements"
|
|
363
|
+
)
|
|
355
364
|
await self.sio.emit("code:file:analyzed", response, room=sid)
|
|
356
365
|
|
|
357
366
|
# Handle other events the dashboard sends
|
|
@@ -385,28 +394,28 @@ class StableDashboardServer:
|
|
|
385
394
|
if self.debug:
|
|
386
395
|
print(f"📡 Received top-level discovery request from {sid}")
|
|
387
396
|
await self.sio.emit("code:top_level:discovered", {"status": "ok"}, room=sid)
|
|
388
|
-
|
|
397
|
+
|
|
389
398
|
# Mock event generator when no real events
|
|
390
399
|
@self.sio.event
|
|
391
400
|
async def request_mock_event(sid, data):
|
|
392
401
|
"""Generate a mock event for testing."""
|
|
393
402
|
if self.debug:
|
|
394
403
|
print(f"📡 Mock event requested by {sid}")
|
|
395
|
-
|
|
404
|
+
|
|
396
405
|
mock_event = self._create_mock_event()
|
|
397
406
|
# Store and broadcast like a real event
|
|
398
407
|
self.event_count += 1
|
|
399
408
|
self.last_event_time = datetime.now()
|
|
400
409
|
self.event_history.append(mock_event)
|
|
401
410
|
await self.sio.emit("claude_event", mock_event)
|
|
402
|
-
|
|
411
|
+
|
|
403
412
|
def _create_mock_event(self) -> Dict[str, Any]:
|
|
404
413
|
"""Create a mock event for testing/demo purposes."""
|
|
405
414
|
import random
|
|
406
|
-
|
|
415
|
+
|
|
407
416
|
event_types = ["file", "command", "test", "build", "deploy"]
|
|
408
417
|
event_subtypes = ["start", "progress", "complete", "error", "warning"]
|
|
409
|
-
|
|
418
|
+
|
|
410
419
|
return {
|
|
411
420
|
"type": random.choice(event_types),
|
|
412
421
|
"subtype": random.choice(event_subtypes),
|
|
@@ -416,31 +425,31 @@ class StableDashboardServer:
|
|
|
416
425
|
"message": f"Mock {random.choice(['operation', 'task', 'process'])} {random.choice(['started', 'completed', 'in progress'])}",
|
|
417
426
|
"file": f"/path/to/file_{random.randint(1, 100)}.py",
|
|
418
427
|
"line": random.randint(1, 500),
|
|
419
|
-
"progress": random.randint(0, 100)
|
|
428
|
+
"progress": random.randint(0, 100),
|
|
420
429
|
},
|
|
421
430
|
"session_id": "mock-session",
|
|
422
|
-
"server_event_id": self.event_count + 1
|
|
431
|
+
"server_event_id": self.event_count + 1,
|
|
423
432
|
}
|
|
424
|
-
|
|
433
|
+
|
|
425
434
|
async def _start_mock_event_generator(self):
|
|
426
435
|
"""Start generating mock events if no real events for a while."""
|
|
427
436
|
try:
|
|
428
437
|
while True:
|
|
429
438
|
await asyncio.sleep(30) # Check every 30 seconds
|
|
430
|
-
|
|
439
|
+
|
|
431
440
|
# If no events in last 60 seconds and clients connected, generate mock
|
|
432
441
|
if self.connected_clients and (
|
|
433
|
-
not self.last_event_time
|
|
434
|
-
(datetime.now() - self.last_event_time).total_seconds() > 60
|
|
442
|
+
not self.last_event_time
|
|
443
|
+
or (datetime.now() - self.last_event_time).total_seconds() > 60
|
|
435
444
|
):
|
|
436
445
|
if self.debug:
|
|
437
446
|
print("⏰ No recent events, generating mock event")
|
|
438
|
-
|
|
447
|
+
|
|
439
448
|
mock_event = self._create_mock_event()
|
|
440
449
|
self.event_count += 1
|
|
441
450
|
self.last_event_time = datetime.now()
|
|
442
451
|
self.event_history.append(mock_event)
|
|
443
|
-
|
|
452
|
+
|
|
444
453
|
await self.sio.emit("claude_event", mock_event)
|
|
445
454
|
except asyncio.CancelledError:
|
|
446
455
|
pass
|
|
@@ -449,20 +458,24 @@ class StableDashboardServer:
|
|
|
449
458
|
|
|
450
459
|
async def _serve_dashboard(self, request):
|
|
451
460
|
"""Serve the main dashboard HTML with fallback."""
|
|
452
|
-
dashboard_file =
|
|
453
|
-
|
|
461
|
+
dashboard_file = (
|
|
462
|
+
self.dashboard_path / "templates" / "index.html"
|
|
463
|
+
if self.dashboard_path
|
|
464
|
+
else None
|
|
465
|
+
)
|
|
466
|
+
|
|
454
467
|
# Try to serve actual dashboard
|
|
455
468
|
if dashboard_file and dashboard_file.exists():
|
|
456
469
|
try:
|
|
457
|
-
with open(dashboard_file,
|
|
470
|
+
with open(dashboard_file, encoding="utf-8") as f:
|
|
458
471
|
content = f.read()
|
|
459
472
|
return web.Response(text=content, content_type="text/html")
|
|
460
473
|
except Exception as e:
|
|
461
474
|
logger.error(f"Error reading dashboard template: {e}")
|
|
462
475
|
# Fall through to fallback HTML
|
|
463
|
-
|
|
476
|
+
|
|
464
477
|
# Fallback HTML if template missing or error
|
|
465
|
-
fallback_html =
|
|
478
|
+
fallback_html = """
|
|
466
479
|
<!DOCTYPE html>
|
|
467
480
|
<html lang="en">
|
|
468
481
|
<head>
|
|
@@ -491,7 +504,7 @@ class StableDashboardServer:
|
|
|
491
504
|
<h1>Claude MPM Dashboard</h1>
|
|
492
505
|
<div class="subtitle">Fallback Mode - Template not found</div>
|
|
493
506
|
</div>
|
|
494
|
-
|
|
507
|
+
|
|
495
508
|
<div id="status" class="status healthy">
|
|
496
509
|
<h3>Server Status</h3>
|
|
497
510
|
<div class="metric">
|
|
@@ -507,7 +520,7 @@ class StableDashboardServer:
|
|
|
507
520
|
<div class="metric-value" id="events">Loading...</div>
|
|
508
521
|
</div>
|
|
509
522
|
</div>
|
|
510
|
-
|
|
523
|
+
|
|
511
524
|
<div class="events">
|
|
512
525
|
<h3>Recent Events</h3>
|
|
513
526
|
<div id="event-list">
|
|
@@ -515,29 +528,29 @@ class StableDashboardServer:
|
|
|
515
528
|
</div>
|
|
516
529
|
</div>
|
|
517
530
|
</div>
|
|
518
|
-
|
|
531
|
+
|
|
519
532
|
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
|
|
520
533
|
<script>
|
|
521
534
|
// Fallback dashboard JavaScript
|
|
522
535
|
const socket = io();
|
|
523
|
-
|
|
536
|
+
|
|
524
537
|
// Update status periodically
|
|
525
538
|
async function updateStatus() {
|
|
526
539
|
try {
|
|
527
540
|
const response = await fetch('/api/status');
|
|
528
541
|
const data = await response.json();
|
|
529
|
-
|
|
542
|
+
|
|
530
543
|
document.getElementById('health').textContent = data.status;
|
|
531
544
|
document.getElementById('uptime').textContent = data.uptime.human;
|
|
532
545
|
document.getElementById('events').textContent = data.events.total;
|
|
533
|
-
|
|
546
|
+
|
|
534
547
|
const statusDiv = document.getElementById('status');
|
|
535
548
|
statusDiv.className = data.status === 'running' ? 'status healthy' : 'status degraded';
|
|
536
549
|
} catch (e) {
|
|
537
550
|
console.error('Failed to fetch status:', e);
|
|
538
551
|
}
|
|
539
552
|
}
|
|
540
|
-
|
|
553
|
+
|
|
541
554
|
// Listen for events
|
|
542
555
|
socket.on('claude_event', (event) => {
|
|
543
556
|
const eventList = document.getElementById('event-list');
|
|
@@ -545,25 +558,25 @@ class StableDashboardServer:
|
|
|
545
558
|
eventDiv.className = 'event';
|
|
546
559
|
eventDiv.textContent = JSON.stringify(event, null, 2);
|
|
547
560
|
eventList.insertBefore(eventDiv, eventList.firstChild);
|
|
548
|
-
|
|
561
|
+
|
|
549
562
|
// Keep only last 10 events
|
|
550
563
|
while (eventList.children.length > 10) {
|
|
551
564
|
eventList.removeChild(eventList.lastChild);
|
|
552
565
|
}
|
|
553
566
|
});
|
|
554
|
-
|
|
567
|
+
|
|
555
568
|
socket.on('connect', () => {
|
|
556
569
|
console.log('Connected to dashboard server');
|
|
557
570
|
});
|
|
558
|
-
|
|
571
|
+
|
|
559
572
|
// Initial load and periodic updates
|
|
560
573
|
updateStatus();
|
|
561
574
|
setInterval(updateStatus, 5000);
|
|
562
575
|
</script>
|
|
563
576
|
</body>
|
|
564
577
|
</html>
|
|
565
|
-
|
|
566
|
-
|
|
578
|
+
"""
|
|
579
|
+
|
|
567
580
|
logger.warning("Serving fallback dashboard HTML")
|
|
568
581
|
return web.Response(text=fallback_html, content_type="text/html")
|
|
569
582
|
|
|
@@ -636,6 +649,10 @@ class StableDashboardServer:
|
|
|
636
649
|
return web.json_response({"error": "Not a file"}, status=400)
|
|
637
650
|
|
|
638
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
|
+
|
|
639
656
|
# Read file with appropriate encoding
|
|
640
657
|
encodings = ["utf-8", "latin-1", "cp1252"]
|
|
641
658
|
content = None
|
|
@@ -651,13 +668,29 @@ class StableDashboardServer:
|
|
|
651
668
|
if content is None:
|
|
652
669
|
return web.json_response({"error": "Could not decode file"}, status=400)
|
|
653
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
|
+
|
|
654
685
|
return web.json_response(
|
|
655
686
|
{
|
|
656
687
|
"path": abs_path,
|
|
657
688
|
"name": os.path.basename(abs_path),
|
|
658
|
-
"content":
|
|
659
|
-
"lines": len(
|
|
689
|
+
"content": formatted_content,
|
|
690
|
+
"lines": len(formatted_content.splitlines()),
|
|
660
691
|
"size": os.path.getsize(abs_path),
|
|
692
|
+
"type": "json" if is_json else "text",
|
|
693
|
+
"is_valid_json": is_valid_json,
|
|
661
694
|
}
|
|
662
695
|
)
|
|
663
696
|
|
|
@@ -670,121 +703,130 @@ class StableDashboardServer:
|
|
|
670
703
|
"""Health check endpoint for monitoring."""
|
|
671
704
|
uptime = time.time() - self.server_start_time
|
|
672
705
|
status = "healthy" if self.is_healthy else "degraded"
|
|
673
|
-
|
|
706
|
+
|
|
674
707
|
health_info = {
|
|
675
708
|
"status": status,
|
|
676
709
|
"uptime_seconds": round(uptime, 2),
|
|
677
710
|
"connected_clients": len(self.connected_clients),
|
|
678
711
|
"event_count": self.event_count,
|
|
679
|
-
"last_event":
|
|
712
|
+
"last_event": (
|
|
713
|
+
self.last_event_time.isoformat() if self.last_event_time else None
|
|
714
|
+
),
|
|
680
715
|
"retry_count": self.retry_count,
|
|
681
716
|
"health_check_failures": self.health_check_failures,
|
|
682
|
-
"event_history_size": len(self.event_history)
|
|
717
|
+
"event_history_size": len(self.event_history),
|
|
683
718
|
}
|
|
684
|
-
|
|
719
|
+
|
|
685
720
|
status_code = 200 if self.is_healthy else 503
|
|
686
721
|
return web.json_response(health_info, status=status_code)
|
|
687
|
-
|
|
722
|
+
|
|
688
723
|
async def _serve_status(self, request):
|
|
689
724
|
"""Detailed server status endpoint."""
|
|
690
725
|
uptime = time.time() - self.server_start_time
|
|
691
|
-
|
|
726
|
+
|
|
692
727
|
status_info = {
|
|
693
728
|
"server": "stable",
|
|
694
729
|
"version": "4.2.3",
|
|
695
730
|
"status": "running" if self.is_healthy else "degraded",
|
|
696
731
|
"uptime": {
|
|
697
732
|
"seconds": round(uptime, 2),
|
|
698
|
-
"human": self._format_uptime(uptime)
|
|
733
|
+
"human": self._format_uptime(uptime),
|
|
699
734
|
},
|
|
700
735
|
"connections": {
|
|
701
736
|
"active": len(self.connected_clients),
|
|
702
|
-
"clients": list(self.connected_clients)
|
|
737
|
+
"clients": list(self.connected_clients),
|
|
703
738
|
},
|
|
704
739
|
"events": {
|
|
705
740
|
"total": self.event_count,
|
|
706
741
|
"buffered": len(self.event_history),
|
|
707
|
-
"last_received":
|
|
742
|
+
"last_received": (
|
|
743
|
+
self.last_event_time.isoformat() if self.last_event_time else None
|
|
744
|
+
),
|
|
708
745
|
},
|
|
709
746
|
"features": [
|
|
710
|
-
"http",
|
|
711
|
-
"
|
|
747
|
+
"http",
|
|
748
|
+
"socketio",
|
|
749
|
+
"event_bridge",
|
|
750
|
+
"health_monitoring",
|
|
751
|
+
"auto_retry",
|
|
752
|
+
"event_history",
|
|
753
|
+
"graceful_degradation",
|
|
712
754
|
],
|
|
713
755
|
"resilience": {
|
|
714
756
|
"retry_count": self.retry_count,
|
|
715
757
|
"max_retries": self.max_retries,
|
|
716
758
|
"health_failures": self.health_check_failures,
|
|
717
|
-
"persist_events": self.persist_events
|
|
718
|
-
}
|
|
759
|
+
"persist_events": self.persist_events,
|
|
760
|
+
},
|
|
719
761
|
}
|
|
720
762
|
return web.json_response(status_info)
|
|
721
|
-
|
|
763
|
+
|
|
722
764
|
async def _serve_event_history(self, request):
|
|
723
765
|
"""Serve recent event history."""
|
|
724
|
-
limit = int(request.query.get(
|
|
766
|
+
limit = int(request.query.get("limit", "100"))
|
|
725
767
|
events = list(self.event_history)[-limit:]
|
|
726
|
-
return web.json_response(
|
|
727
|
-
"events": events,
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
})
|
|
731
|
-
|
|
768
|
+
return web.json_response(
|
|
769
|
+
{"events": events, "count": len(events), "total_events": self.event_count}
|
|
770
|
+
)
|
|
771
|
+
|
|
732
772
|
async def _receive_event(self, request):
|
|
733
773
|
"""Receive events from hook system via HTTP POST."""
|
|
734
774
|
try:
|
|
735
775
|
# Parse event data
|
|
736
776
|
data = await request.json()
|
|
737
|
-
|
|
777
|
+
|
|
738
778
|
# Add server metadata
|
|
739
779
|
event = {
|
|
740
780
|
**data,
|
|
741
781
|
"received_at": datetime.now().isoformat(),
|
|
742
|
-
"server_event_id": self.event_count + 1
|
|
782
|
+
"server_event_id": self.event_count + 1,
|
|
743
783
|
}
|
|
744
|
-
|
|
784
|
+
|
|
745
785
|
# Update tracking
|
|
746
786
|
self.event_count += 1
|
|
747
787
|
self.last_event_time = datetime.now()
|
|
748
|
-
|
|
788
|
+
|
|
749
789
|
# Store in circular buffer
|
|
750
790
|
self.event_history.append(event)
|
|
751
|
-
|
|
791
|
+
|
|
752
792
|
# Persist to disk if enabled
|
|
753
793
|
if self.persist_events:
|
|
754
794
|
try:
|
|
755
|
-
with open(self.event_log_path,
|
|
756
|
-
f.write(json.dumps(event) +
|
|
795
|
+
with open(self.event_log_path, "a") as f:
|
|
796
|
+
f.write(json.dumps(event) + "\n")
|
|
757
797
|
except Exception as e:
|
|
758
798
|
logger.error(f"Failed to persist event: {e}")
|
|
759
|
-
|
|
799
|
+
|
|
760
800
|
# Emit to all connected SocketIO clients
|
|
761
801
|
if self.sio and self.connected_clients:
|
|
762
802
|
await self.sio.emit("claude_event", event)
|
|
763
803
|
if self.debug:
|
|
764
|
-
print(
|
|
765
|
-
|
|
804
|
+
print(
|
|
805
|
+
f"📡 Forwarded event to {len(self.connected_clients)} clients"
|
|
806
|
+
)
|
|
807
|
+
|
|
766
808
|
# Return success response
|
|
767
|
-
return web.json_response(
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
+
|
|
773
817
|
except json.JSONDecodeError as e:
|
|
774
818
|
logger.error(f"Invalid JSON in event request: {e}")
|
|
775
819
|
return web.json_response(
|
|
776
|
-
{"error": "Invalid JSON", "details": str(e)},
|
|
777
|
-
status=400
|
|
820
|
+
{"error": "Invalid JSON", "details": str(e)}, status=400
|
|
778
821
|
)
|
|
779
822
|
except Exception as e:
|
|
780
823
|
logger.error(f"Error processing event: {e}")
|
|
781
824
|
if self.debug:
|
|
782
825
|
traceback.print_exc()
|
|
783
826
|
return web.json_response(
|
|
784
|
-
{"error": "Failed to process event", "details": str(e)},
|
|
785
|
-
status=500
|
|
827
|
+
{"error": "Failed to process event", "details": str(e)}, status=500
|
|
786
828
|
)
|
|
787
|
-
|
|
829
|
+
|
|
788
830
|
async def _serve_version(self, request):
|
|
789
831
|
"""Serve version information."""
|
|
790
832
|
version_info = {
|
|
@@ -794,14 +836,14 @@ class StableDashboardServer:
|
|
|
794
836
|
"status": "running" if self.is_healthy else "degraded",
|
|
795
837
|
}
|
|
796
838
|
return web.json_response(version_info)
|
|
797
|
-
|
|
839
|
+
|
|
798
840
|
def _format_uptime(self, seconds: float) -> str:
|
|
799
841
|
"""Format uptime in human-readable format."""
|
|
800
842
|
days = int(seconds // 86400)
|
|
801
843
|
hours = int((seconds % 86400) // 3600)
|
|
802
844
|
minutes = int((seconds % 3600) // 60)
|
|
803
845
|
secs = int(seconds % 60)
|
|
804
|
-
|
|
846
|
+
|
|
805
847
|
parts = []
|
|
806
848
|
if days > 0:
|
|
807
849
|
parts.append(f"{days}d")
|
|
@@ -810,31 +852,35 @@ class StableDashboardServer:
|
|
|
810
852
|
if minutes > 0:
|
|
811
853
|
parts.append(f"{minutes}m")
|
|
812
854
|
parts.append(f"{secs}s")
|
|
813
|
-
|
|
855
|
+
|
|
814
856
|
return " ".join(parts)
|
|
815
857
|
|
|
816
858
|
def run(self):
|
|
817
859
|
"""Run the server with automatic restart on crash."""
|
|
818
860
|
restart_attempts = 0
|
|
819
861
|
max_restart_attempts = 5
|
|
820
|
-
|
|
862
|
+
|
|
821
863
|
while restart_attempts < max_restart_attempts:
|
|
822
864
|
try:
|
|
823
|
-
print(
|
|
824
|
-
|
|
865
|
+
print(
|
|
866
|
+
f"🔧 Setting up server... (attempt {restart_attempts + 1}/{max_restart_attempts})"
|
|
867
|
+
)
|
|
868
|
+
|
|
825
869
|
# Reset health status on restart
|
|
826
870
|
self.is_healthy = True
|
|
827
871
|
self.health_check_failures = 0
|
|
828
|
-
|
|
872
|
+
|
|
829
873
|
if not self.setup():
|
|
830
874
|
if not DEPENDENCIES_AVAILABLE:
|
|
831
875
|
print("❌ Missing required dependencies")
|
|
832
876
|
return False
|
|
833
|
-
|
|
877
|
+
|
|
834
878
|
# Continue with fallback mode even if dashboard files not found
|
|
835
879
|
print("⚠️ Dashboard files not found - running in fallback mode")
|
|
836
|
-
print(
|
|
837
|
-
|
|
880
|
+
print(
|
|
881
|
+
" Server will provide basic functionality and receive events"
|
|
882
|
+
)
|
|
883
|
+
|
|
838
884
|
# Set up minimal server without dashboard files
|
|
839
885
|
self.sio = socketio.AsyncServer(
|
|
840
886
|
cors_allowed_origins="*",
|
|
@@ -848,25 +894,29 @@ class StableDashboardServer:
|
|
|
848
894
|
self.sio.attach(self.app)
|
|
849
895
|
self._setup_routes()
|
|
850
896
|
self._setup_socketio_events()
|
|
851
|
-
|
|
897
|
+
|
|
852
898
|
return self._run_with_resilience()
|
|
853
|
-
|
|
899
|
+
|
|
854
900
|
except Exception as e:
|
|
855
901
|
restart_attempts += 1
|
|
856
902
|
logger.error(f"Server crashed: {e}")
|
|
857
903
|
if self.debug:
|
|
858
904
|
traceback.print_exc()
|
|
859
|
-
|
|
905
|
+
|
|
860
906
|
if restart_attempts < max_restart_attempts:
|
|
861
|
-
wait_time = min(
|
|
907
|
+
wait_time = min(
|
|
908
|
+
2**restart_attempts, 30
|
|
909
|
+
) # Exponential backoff, max 30s
|
|
862
910
|
print(f"🔄 Restarting server in {wait_time} seconds...")
|
|
863
911
|
time.sleep(wait_time)
|
|
864
912
|
else:
|
|
865
|
-
print(
|
|
913
|
+
print(
|
|
914
|
+
f"❌ Server failed after {max_restart_attempts} restart attempts"
|
|
915
|
+
)
|
|
866
916
|
return False
|
|
867
|
-
|
|
917
|
+
|
|
868
918
|
return False
|
|
869
|
-
|
|
919
|
+
|
|
870
920
|
def _run_with_resilience(self):
|
|
871
921
|
"""Run server with port conflict resolution and error handling."""
|
|
872
922
|
|
|
@@ -904,11 +954,11 @@ class StableDashboardServer:
|
|
|
904
954
|
web.run_app(self.app, host=self.host, port=self.port)
|
|
905
955
|
else:
|
|
906
956
|
web.run_app(
|
|
907
|
-
self.app,
|
|
908
|
-
host=self.host,
|
|
909
|
-
port=self.port,
|
|
957
|
+
self.app,
|
|
958
|
+
host=self.host,
|
|
959
|
+
port=self.port,
|
|
910
960
|
access_log=None,
|
|
911
|
-
print=lambda *args: None # Suppress startup messages in non-debug mode
|
|
961
|
+
print=lambda *args: None, # Suppress startup messages in non-debug mode
|
|
912
962
|
)
|
|
913
963
|
return True # Server started successfully
|
|
914
964
|
except KeyboardInterrupt:
|
|
@@ -916,7 +966,11 @@ class StableDashboardServer:
|
|
|
916
966
|
return True
|
|
917
967
|
except OSError as e:
|
|
918
968
|
error_str = str(e)
|
|
919
|
-
if
|
|
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
|
+
):
|
|
920
974
|
# Port is already in use
|
|
921
975
|
if attempt < max_port_attempts - 1:
|
|
922
976
|
self.port += 1
|
|
@@ -930,7 +984,9 @@ class StableDashboardServer:
|
|
|
930
984
|
f"❌ Could not find available port after {max_port_attempts} attempts"
|
|
931
985
|
)
|
|
932
986
|
print(f" Ports {original_port} to {self.port} are all in use")
|
|
933
|
-
print(
|
|
987
|
+
print(
|
|
988
|
+
"\n💡 Tip: Check if another dashboard instance is running"
|
|
989
|
+
)
|
|
934
990
|
print(" You can stop it with: claude-mpm dashboard stop")
|
|
935
991
|
return False
|
|
936
992
|
else:
|
|
@@ -938,12 +994,14 @@ class StableDashboardServer:
|
|
|
938
994
|
print(f"❌ Server error: {e}")
|
|
939
995
|
if self.debug:
|
|
940
996
|
import traceback
|
|
997
|
+
|
|
941
998
|
traceback.print_exc()
|
|
942
999
|
return False
|
|
943
1000
|
except Exception as e:
|
|
944
1001
|
print(f"❌ Unexpected server error: {e}")
|
|
945
1002
|
if self.debug:
|
|
946
1003
|
import traceback
|
|
1004
|
+
|
|
947
1005
|
traceback.print_exc()
|
|
948
1006
|
else:
|
|
949
1007
|
print("\n💡 Run with --debug flag for more details")
|