claude-mpm 4.2.2__py3-none-any.whl → 4.2.4__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.
@@ -248,7 +248,7 @@ class SocketClient {
248
248
  * @private
249
249
  * @const
250
250
  */
251
- this.pingTimeout = 90000; // 90 seconds for health check (more lenient than Socket.IO timeout)
251
+ this.pingTimeout = 120000; // 120 seconds for health check (more lenient for stability)
252
252
 
253
253
  /**
254
254
  * Health check interval timer.
@@ -346,13 +346,13 @@ class SocketClient {
346
346
  autoConnect: true,
347
347
  reconnection: true,
348
348
  reconnectionDelay: 1000,
349
- reconnectionDelayMax: 5000,
350
- reconnectionAttempts: 5, // Try 5 times then stop (was Infinity which can cause issues)
351
- timeout: 20000, // Connection timeout
349
+ reconnectionDelayMax: 10000, // Increased max delay for stability
350
+ reconnectionAttempts: 10, // Increased attempts for better resilience
351
+ timeout: 30000, // Increased connection timeout to 30 seconds
352
352
  forceNew: true,
353
353
  transports: ['websocket', 'polling'],
354
- pingInterval: 25000, // CRITICAL: Must match server's 25 seconds
355
- pingTimeout: 20000 // CRITICAL: Must match server's 20 seconds
354
+ pingInterval: 30000, // Increased ping interval for stability
355
+ pingTimeout: 60000 // Much longer timeout for better stability
356
356
  });
357
357
 
358
358
  this.setupSocketHandlers();
@@ -1218,8 +1218,11 @@ class SocketClient {
1218
1218
  transformedEvent[key] = eventData.data[key];
1219
1219
  }
1220
1220
  } else {
1221
- // Log warning if data field would overwrite a protected field
1222
- console.warn(`Protected field '${key}' in data object was not copied to top level to preserve event structure`);
1221
+ // Log debug info if data field would overwrite a protected field
1222
+ // Only log for non-timestamp fields to reduce noise
1223
+ if (key !== 'timestamp') {
1224
+ console.debug(`Protected field '${key}' in data object was not copied to top level to preserve event structure`);
1225
+ }
1223
1226
  }
1224
1227
  });
1225
1228
 
@@ -247,7 +247,6 @@
247
247
  <button class="tab-button" data-tab="files">📁 Files</button>
248
248
  <button class="tab-button" data-tab="activity">🌳 Activity</button>
249
249
  <button class="tab-button" data-tab="code">🧬 Code</button>
250
- <a href="/code-simple" class="tab-button" style="background: #f7fafc; color: #4a5568; text-decoration: none; border-left: 2px solid #e2e8f0;">📁 Simple View</a>
251
250
  </div>
252
251
 
253
252
  <!-- Events Tab -->
@@ -389,45 +388,52 @@
389
388
  <!-- Code Tab -->
390
389
  <div class="tab-content" id="code-tab">
391
390
  <div class="code-container">
392
- <!-- Simplified header with controls -->
393
- <div class="code-header-compact">
394
- <div class="header-left">
395
- <button id="code-expand-all" class="btn-compact" title="Expand All">⊕</button>
396
- <button id="code-collapse-all" class="btn-compact" title="Collapse All">⊖</button>
397
- <button id="code-reset-zoom" class="btn-compact" title="Reset Zoom">⟲</button>
398
- <button id="code-toggle-legend" class="btn-compact" title="Toggle Legend">ℹ️</button>
399
- </div>
400
- <div class="header-center">
401
- <span class="stat-compact">📁 <span id="file-count">0</span></span>
402
- <span class="stat-compact">🏛️ <span id="class-count">0</span></span>
403
- <span class="stat-compact">⚡ <span id="function-count">0</span></span>
404
- <span class="stat-compact">📝 <span id="line-count">0</span></span>
391
+ <div id="code-tree-container" class="code-tree-container">
392
+ <!-- Top-left corner: Language selector -->
393
+ <div class="tree-corner-controls top-left">
394
+ <div class="control-group">
395
+ <label class="control-label">Languages:</label>
396
+ <div class="checkbox-group">
397
+ <label class="checkbox-label"><input type="checkbox" class="language-checkbox" value="python" checked> Python</label>
398
+ <label class="checkbox-label"><input type="checkbox" class="language-checkbox" value="javascript" checked> JS</label>
399
+ <label class="checkbox-label"><input type="checkbox" class="language-checkbox" value="typescript" checked> TS</label>
400
+ </div>
401
+ </div>
405
402
  </div>
406
- <div class="header-right">
407
- <select id="language-filter" class="select-compact">
408
- <option value="all">All</option>
409
- <option value="python">Python</option>
410
- <option value="javascript">JS</option>
411
- <option value="typescript">TS</option>
412
- </select>
413
- <input type="text" id="code-search" placeholder="Search..." class="search-compact">
403
+
404
+ <!-- Top-right corner: Layout and search -->
405
+ <div class="tree-corner-controls top-right">
406
+ <div class="control-group">
407
+ <select id="code-layout" class="select-compact">
408
+ <option value="tree">Tree</option>
409
+ <option value="radial">Radial</option>
410
+ </select>
411
+ <input type="text" id="code-search" placeholder="Search..." class="search-compact">
412
+ </div>
414
413
  </div>
415
- </div>
416
- <!-- Advanced options - visible by default -->
417
- <div class="code-advanced-options-visible">
418
- <div class="advanced-content">
419
- <div class="option-group">
420
- <label>Languages:</label>
421
- <label><input type="checkbox" class="language-checkbox" value="python" checked> Python</label>
422
- <label><input type="checkbox" class="language-checkbox" value="javascript" checked> JS</label>
423
- <label><input type="checkbox" class="language-checkbox" value="typescript" checked> TS</label>
414
+
415
+ <!-- Bottom-left corner: Stats and Status -->
416
+ <div class="tree-corner-controls bottom-left">
417
+ <div class="stats-display" id="code-stats">
418
+ <span id="stats-files">0 files</span> •
419
+ <span id="stats-classes">0 classes</span>
420
+ <span id="stats-functions">0 functions</span>
421
+ <span id="stats-methods">0 methods</span>
424
422
  </div>
425
- <div class="option-group">
426
- <label>Ignore: <input type="text" id="ignore-patterns" placeholder="test*, *.spec.js, node_modules" class="input-compact" style="width: 200px;"></label>
423
+ <div class="status-display" id="code-breadcrumb">
424
+ <div class="breadcrumb-ticker" id="breadcrumb-ticker">
425
+ <span id="breadcrumb-content">Ready to analyze...</span>
426
+ </div>
427
+ </div>
428
+ </div>
429
+
430
+ <!-- Bottom-right corner: Ignore patterns -->
431
+ <div class="tree-corner-controls bottom-right">
432
+ <div class="control-group">
433
+ <label class="control-label">Ignore:</label>
434
+ <input type="text" id="ignore-patterns" placeholder="test*, *.spec.js, node_modules" class="input-compact">
427
435
  </div>
428
436
  </div>
429
- </div>
430
- <div id="code-tree-container" class="code-tree-container">
431
437
  <div id="code-tree"></div>
432
438
  <!-- Collapsible legend -->
433
439
  <div class="tree-legend collapsed" id="tree-legend" style="display: none;">
@@ -447,11 +453,6 @@
447
453
  </div>
448
454
  </div>
449
455
  </div>
450
- <div class="code-breadcrumb" id="code-breadcrumb">
451
- <div class="breadcrumb-ticker" id="breadcrumb-ticker">
452
- <span id="breadcrumb-content">Ready to analyze...</span>
453
- </div>
454
- </div>
455
456
  </div>
456
457
  </div>
457
458
 
@@ -704,16 +704,29 @@ tools:
704
704
  if triggers and not examples:
705
705
  # Convert first trigger to example with commentary
706
706
  trigger = triggers[0]
707
+
708
+ # Handle both string and dict trigger formats
709
+ if isinstance(trigger, dict):
710
+ # New format with pattern and confidence
711
+ trigger_text = trigger.get("pattern", "")
712
+ else:
713
+ # Old format with simple string
714
+ trigger_text = str(trigger)
715
+
716
+ # Skip if we don't have valid trigger text
717
+ if not trigger_text:
718
+ return examples
719
+
707
720
  agent_type = template_data.get("agent_type", "general")
708
721
 
709
722
  examples.extend(
710
723
  [
711
724
  "<example>",
712
- f"Context: When user needs {trigger}",
713
- f'user: "{trigger}"',
714
- f'assistant: "I\'ll use the {agent_name} agent for {trigger}."',
725
+ f"Context: When user needs {trigger_text}",
726
+ f'user: "{trigger_text}"',
727
+ f'assistant: "I\'ll use the {agent_name} agent for {trigger_text}."',
715
728
  "<commentary>",
716
- f"This {agent_type} agent is appropriate because it has specialized capabilities for {trigger.lower()} tasks.",
729
+ f"This {agent_type} agent is appropriate because it has specialized capabilities for {trigger_text.lower()} tasks.",
717
730
  "</commentary>",
718
731
  "</example>",
719
732
  ]
@@ -0,0 +1,482 @@
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 glob
16
+ import os
17
+ import sys
18
+ from pathlib import Path
19
+ from typing import Any, Dict, Optional
20
+
21
+ try:
22
+ import aiohttp
23
+ import socketio
24
+ from aiohttp import web
25
+
26
+ DEPENDENCIES_AVAILABLE = True
27
+ except ImportError:
28
+ DEPENDENCIES_AVAILABLE = False
29
+ socketio = None
30
+ aiohttp = None
31
+ web = None
32
+
33
+
34
+ def find_dashboard_files() -> Optional[Path]:
35
+ """Find dashboard files across different installation methods."""
36
+ # Try different possible locations
37
+ possible_locations = [
38
+ # Development/direct install
39
+ Path(__file__).parent.parent.parent / "dashboard",
40
+ # Current working directory (for development)
41
+ Path.cwd() / "src" / "claude_mpm" / "dashboard",
42
+ # Pip install in current Python environment
43
+ Path(sys.prefix)
44
+ / "lib"
45
+ / f"python{sys.version_info.major}.{sys.version_info.minor}"
46
+ / "site-packages"
47
+ / "claude_mpm"
48
+ / "dashboard",
49
+ # User site-packages
50
+ Path.home()
51
+ / ".local"
52
+ / "lib"
53
+ / f"python{sys.version_info.major}.{sys.version_info.minor}"
54
+ / "site-packages"
55
+ / "claude_mpm"
56
+ / "dashboard",
57
+ ]
58
+
59
+ # Add glob patterns for different Python versions
60
+ python_patterns = [
61
+ f"/opt/homebrew/lib/python{sys.version_info.major}.{sys.version_info.minor}/site-packages/claude_mpm/dashboard",
62
+ f"/usr/local/lib/python{sys.version_info.major}.{sys.version_info.minor}/site-packages/claude_mpm/dashboard",
63
+ ]
64
+
65
+ # Check direct paths first
66
+ for location in possible_locations:
67
+ if location.exists() and (location / "templates" / "index.html").exists():
68
+ return location
69
+
70
+ # Check pattern-based paths
71
+ for pattern in python_patterns:
72
+ matches = glob.glob(pattern)
73
+ for match in matches:
74
+ path = Path(match)
75
+ if path.exists() and (path / "templates" / "index.html").exists():
76
+ return path
77
+
78
+ # Fallback: try to find via module import
79
+ try:
80
+ import claude_mpm.dashboard
81
+
82
+ module_path = Path(claude_mpm.dashboard.__file__).parent
83
+ if (module_path / "templates" / "index.html").exists():
84
+ return module_path
85
+ except ImportError:
86
+ pass
87
+
88
+ return None
89
+
90
+
91
+ def create_mock_ast_data(file_path: str, file_name: str) -> Dict[str, Any]:
92
+ """Create mock AST analysis data."""
93
+ ext = file_name.split(".")[-1].lower() if "." in file_name else ""
94
+
95
+ elements = []
96
+ if ext == "py":
97
+ elements = [
98
+ {
99
+ "name": "MockClass",
100
+ "type": "class",
101
+ "line": 10,
102
+ "complexity": 2,
103
+ "docstring": "Mock class for demonstration",
104
+ "methods": [
105
+ {"name": "__init__", "type": "method", "line": 11, "complexity": 1},
106
+ {
107
+ "name": "mock_method",
108
+ "type": "method",
109
+ "line": 15,
110
+ "complexity": 1,
111
+ },
112
+ ],
113
+ },
114
+ {
115
+ "name": "mock_function",
116
+ "type": "function",
117
+ "line": 20,
118
+ "complexity": 1,
119
+ "docstring": "Mock function for demonstration",
120
+ },
121
+ ]
122
+ elif ext in ["js", "ts", "jsx", "tsx"]:
123
+ elements = [
124
+ {
125
+ "name": "MockClass",
126
+ "type": "class",
127
+ "line": 5,
128
+ "complexity": 2,
129
+ "methods": [
130
+ {
131
+ "name": "constructor",
132
+ "type": "method",
133
+ "line": 6,
134
+ "complexity": 1,
135
+ },
136
+ {
137
+ "name": "mockMethod",
138
+ "type": "method",
139
+ "line": 10,
140
+ "complexity": 1,
141
+ },
142
+ ],
143
+ },
144
+ {"name": "mockFunction", "type": "function", "line": 15, "complexity": 1},
145
+ ]
146
+
147
+ return {
148
+ "path": file_path,
149
+ "elements": elements,
150
+ "complexity": sum(e.get("complexity", 1) for e in elements),
151
+ "lines": 50,
152
+ "stats": {
153
+ "classes": len([e for e in elements if e["type"] == "class"]),
154
+ "functions": len([e for e in elements if e["type"] == "function"]),
155
+ "methods": sum(len(e.get("methods", [])) for e in elements),
156
+ "lines": 50,
157
+ },
158
+ }
159
+
160
+
161
+ class StableDashboardServer:
162
+ """Stable dashboard server that works across all installation methods."""
163
+
164
+ def __init__(self, host: str = "localhost", port: int = 8765, debug: bool = False):
165
+ self.host = host
166
+ self.port = port
167
+ self.debug = debug
168
+ self.dashboard_path = None
169
+ self.app = None
170
+ self.sio = None
171
+
172
+ def setup(self) -> bool:
173
+ """Set up the server components."""
174
+ if not DEPENDENCIES_AVAILABLE:
175
+ print(
176
+ "❌ Error: Missing dependencies. Install with: pip install aiohttp python-socketio"
177
+ )
178
+ return False
179
+
180
+ # Find dashboard files
181
+ self.dashboard_path = find_dashboard_files()
182
+ if not self.dashboard_path:
183
+ print("❌ Error: Could not find dashboard files")
184
+ print("Please ensure Claude MPM is properly installed")
185
+ return False
186
+
187
+ print(f"📁 Using dashboard files from: {self.dashboard_path}")
188
+
189
+ # Create SocketIO server with improved timeout settings
190
+ self.sio = socketio.AsyncServer(
191
+ cors_allowed_origins="*",
192
+ logger=True,
193
+ engineio_logger=True,
194
+ ping_interval=30, # Match client's 30 second ping interval
195
+ ping_timeout=60, # Match client's 60 second timeout
196
+ max_http_buffer_size=1e8, # Allow larger messages
197
+ )
198
+ self.app = web.Application()
199
+ self.sio.attach(self.app)
200
+ print("✅ SocketIO server created and attached")
201
+
202
+ # Set up routes
203
+ self._setup_routes()
204
+ self._setup_socketio_events()
205
+
206
+ print("✅ Server setup complete!")
207
+
208
+ return True
209
+
210
+ def _setup_routes(self):
211
+ """Set up HTTP routes."""
212
+ self.app.router.add_get("/", self._serve_dashboard)
213
+ self.app.router.add_get("/static/{path:.*}", self._serve_static)
214
+ self.app.router.add_get("/api/directory/list", self._list_directory)
215
+ self.app.router.add_get("/api/file/read", self._read_file)
216
+ self.app.router.add_get("/version.json", self._serve_version)
217
+
218
+ def _setup_socketio_events(self):
219
+ """Set up SocketIO event handlers."""
220
+
221
+ @self.sio.event
222
+ async def connect(sid, environ):
223
+ print(f"✅ SocketIO client connected: {sid}")
224
+ print(f" Client info: {environ.get('HTTP_USER_AGENT', 'Unknown')}")
225
+ # Send a test message to confirm connection
226
+ await self.sio.emit(
227
+ "connection_test", {"status": "connected", "server": "stable"}, room=sid
228
+ )
229
+
230
+ @self.sio.event
231
+ async def disconnect(sid):
232
+ print(f"❌ SocketIO client disconnected: {sid}")
233
+
234
+ @self.sio.event
235
+ async def code_analyze_file(sid, data):
236
+ print(
237
+ f"📡 Received file analysis request from {sid}: {data.get('path', 'unknown')}"
238
+ )
239
+
240
+ file_path = data.get("path", "")
241
+ file_name = file_path.split("/")[-1] if file_path else "unknown"
242
+
243
+ # Create mock response
244
+ response = create_mock_ast_data(file_path, file_name)
245
+
246
+ print(f"📤 Sending analysis response: {len(response['elements'])} elements")
247
+ await self.sio.emit("code:file:analyzed", response, room=sid)
248
+
249
+ # CRITICAL: Handle the actual event name with colons that the client sends
250
+ @self.sio.on("code:analyze:file")
251
+ async def handle_code_analyze_file(sid, data):
252
+ print(
253
+ f"📡 Received code:analyze:file from {sid}: {data.get('path', 'unknown')}"
254
+ )
255
+
256
+ file_path = data.get("path", "")
257
+ file_name = file_path.split("/")[-1] if file_path else "unknown"
258
+
259
+ # Create mock response
260
+ response = create_mock_ast_data(file_path, file_name)
261
+
262
+ print(f"📤 Sending analysis response: {len(response['elements'])} elements")
263
+ await self.sio.emit("code:file:analyzed", response, room=sid)
264
+
265
+ # Handle other events the dashboard sends
266
+ @self.sio.event
267
+ async def get_git_branch(sid, data):
268
+ print(f"📡 Received git branch request from {sid}: {data}")
269
+ await self.sio.emit(
270
+ "git_branch_response", {"branch": "main", "path": data}, room=sid
271
+ )
272
+
273
+ @self.sio.event
274
+ async def request_status(sid, data):
275
+ print(f"📡 Received status request from {sid}")
276
+ await self.sio.emit(
277
+ "status_response", {"status": "running", "server": "stable"}, room=sid
278
+ )
279
+
280
+ # Handle the event with dots (SocketIO converts colons to dots sometimes)
281
+ @self.sio.event
282
+ async def request_dot_status(sid, data):
283
+ print(f"📡 Received request.status from {sid}")
284
+ await self.sio.emit(
285
+ "status_response", {"status": "running", "server": "stable"}, room=sid
286
+ )
287
+
288
+ @self.sio.event
289
+ async def code_discover_top_level(sid, data):
290
+ print(f"📡 Received top-level discovery request from {sid}")
291
+ await self.sio.emit("code:top_level:discovered", {"status": "ok"}, room=sid)
292
+
293
+ async def _serve_dashboard(self, request):
294
+ """Serve the main dashboard HTML."""
295
+ dashboard_file = self.dashboard_path / "templates" / "index.html"
296
+ if dashboard_file.exists():
297
+ with open(dashboard_file) as f:
298
+ content = f.read()
299
+ return web.Response(text=content, content_type="text/html")
300
+ return web.Response(text="Dashboard not found", status=404)
301
+
302
+ async def _serve_static(self, request):
303
+ """Serve static files."""
304
+ file_path = request.match_info["path"]
305
+ static_file = self.dashboard_path / "static" / file_path
306
+
307
+ if static_file.exists() and static_file.is_file():
308
+ content_type = (
309
+ "text/javascript"
310
+ if file_path.endswith(".js")
311
+ else "text/css" if file_path.endswith(".css") else "text/plain"
312
+ )
313
+ with open(static_file) as f:
314
+ content = f.read()
315
+ return web.Response(text=content, content_type=content_type)
316
+ return web.Response(text="File not found", status=404)
317
+
318
+ async def _list_directory(self, request):
319
+ """List directory contents."""
320
+ path = request.query.get("path", ".")
321
+ abs_path = os.path.abspath(os.path.expanduser(path))
322
+
323
+ result = {"path": abs_path, "exists": os.path.exists(abs_path), "contents": []}
324
+
325
+ if os.path.exists(abs_path) and os.path.isdir(abs_path):
326
+ try:
327
+ for item in sorted(os.listdir(abs_path)):
328
+ item_path = os.path.join(abs_path, item)
329
+ result["contents"].append(
330
+ {
331
+ "name": item,
332
+ "path": item_path,
333
+ "is_directory": os.path.isdir(item_path),
334
+ "is_file": os.path.isfile(item_path),
335
+ "is_code_file": item.endswith(
336
+ (".py", ".js", ".ts", ".jsx", ".tsx")
337
+ ),
338
+ }
339
+ )
340
+ except PermissionError:
341
+ result["error"] = "Permission denied"
342
+
343
+ return web.json_response(result)
344
+
345
+ async def _read_file(self, request):
346
+ """Read file content for source viewer."""
347
+ file_path = request.query.get("path", "")
348
+
349
+ if not file_path:
350
+ return web.json_response({"error": "No path provided"}, status=400)
351
+
352
+ abs_path = os.path.abspath(os.path.expanduser(file_path))
353
+
354
+ # Security check - ensure file is within the project
355
+ try:
356
+ # Get the project root (current working directory)
357
+ project_root = os.getcwd()
358
+ # Ensure the path is within the project
359
+ if not abs_path.startswith(project_root):
360
+ return web.json_response({"error": "Access denied"}, status=403)
361
+ except Exception:
362
+ pass # Allow read if we can't determine project root
363
+
364
+ if not os.path.exists(abs_path):
365
+ return web.json_response({"error": "File not found"}, status=404)
366
+
367
+ if not os.path.isfile(abs_path):
368
+ return web.json_response({"error": "Not a file"}, status=400)
369
+
370
+ try:
371
+ # Read file with appropriate encoding
372
+ encodings = ["utf-8", "latin-1", "cp1252"]
373
+ content = None
374
+
375
+ for encoding in encodings:
376
+ try:
377
+ with open(abs_path, encoding=encoding) as f:
378
+ content = f.read()
379
+ break
380
+ except UnicodeDecodeError:
381
+ continue
382
+
383
+ if content is None:
384
+ return web.json_response({"error": "Could not decode file"}, status=400)
385
+
386
+ return web.json_response(
387
+ {
388
+ "path": abs_path,
389
+ "name": os.path.basename(abs_path),
390
+ "content": content,
391
+ "lines": len(content.splitlines()),
392
+ "size": os.path.getsize(abs_path),
393
+ }
394
+ )
395
+
396
+ except PermissionError:
397
+ return web.json_response({"error": "Permission denied"}, status=403)
398
+ except Exception as e:
399
+ return web.json_response({"error": str(e)}, status=500)
400
+
401
+ async def _serve_version(self, request):
402
+ """Serve version information."""
403
+ version_info = {
404
+ "version": "4.2.2",
405
+ "server": "stable",
406
+ "features": ["http", "socketio", "mock_ast"],
407
+ "status": "running",
408
+ }
409
+ return web.json_response(version_info)
410
+
411
+ def run(self):
412
+ """Run the server with automatic port conflict resolution."""
413
+ print("🔧 Setting up server...")
414
+ if not self.setup():
415
+ print("❌ Server setup failed")
416
+ return False
417
+
418
+ print(f"🚀 Starting stable dashboard server at http://{self.host}:{self.port}")
419
+ print("✅ Server ready: HTTP + SocketIO on same port")
420
+ print("📡 SocketIO events registered:")
421
+ print(" - connect/disconnect")
422
+ print(" - code_analyze_file (from 'code:analyze:file')")
423
+ print("🌐 HTTP endpoints available:")
424
+ print(" - GET / (dashboard)")
425
+ print(" - GET /static/* (static files)")
426
+ print(" - GET /api/directory/list (directory API)")
427
+ print(f"🔗 Open in browser: http://{self.host}:{self.port}")
428
+
429
+ # Try to start server with port conflict handling
430
+ max_port_attempts = 10
431
+ original_port = self.port
432
+
433
+ for attempt in range(max_port_attempts):
434
+ try:
435
+ web.run_app(self.app, host=self.host, port=self.port, access_log=None)
436
+ break # Server started successfully
437
+ except KeyboardInterrupt:
438
+ print("\n🛑 Server stopped by user")
439
+ break
440
+ except OSError as e:
441
+ if "[Errno 48]" in str(e) or "Address already in use" in str(e):
442
+ # Port is already in use, try next port
443
+ if attempt < max_port_attempts - 1:
444
+ self.port += 1
445
+ print(
446
+ f"⚠️ Port {self.port - 1} in use, trying port {self.port}..."
447
+ )
448
+ # Recreate the app with new port
449
+ self.setup()
450
+ else:
451
+ print(
452
+ f"❌ Could not find available port after {max_port_attempts} attempts"
453
+ )
454
+ print(f" Ports {original_port} to {self.port} are all in use")
455
+ return False
456
+ else:
457
+ # Other OS error
458
+ print(f"❌ Server error: {e}")
459
+ if self.debug:
460
+ import traceback
461
+
462
+ traceback.print_exc()
463
+ return False
464
+ except Exception as e:
465
+ print(f"❌ Server error: {e}")
466
+ if self.debug:
467
+ import traceback
468
+
469
+ traceback.print_exc()
470
+ return False
471
+
472
+ return True
473
+
474
+
475
+ def create_stable_server(
476
+ dashboard_path: Optional[Path] = None, **kwargs
477
+ ) -> StableDashboardServer:
478
+ """Create a stable dashboard server instance."""
479
+ server = StableDashboardServer(**kwargs)
480
+ if dashboard_path:
481
+ server.dashboard_path = dashboard_path
482
+ return server