claude-mpm 5.0.9__py3-none-any.whl → 5.4.41__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.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

Files changed (263) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__init__.py +4 -0
  3. claude_mpm/agents/BASE_AGENT.md +164 -0
  4. claude_mpm/agents/{PM_INSTRUCTIONS_TEACH.md → CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md} +721 -41
  5. claude_mpm/agents/MEMORY.md +1 -1
  6. claude_mpm/agents/PM_INSTRUCTIONS.md +468 -468
  7. claude_mpm/agents/WORKFLOW.md +5 -254
  8. claude_mpm/agents/agent_loader.py +13 -44
  9. claude_mpm/agents/base_agent.json +1 -1
  10. claude_mpm/agents/frontmatter_validator.py +70 -2
  11. claude_mpm/agents/templates/circuit-breakers.md +431 -45
  12. claude_mpm/cli/__init__.py +0 -1
  13. claude_mpm/cli/__main__.py +4 -0
  14. claude_mpm/cli/chrome_devtools_installer.py +175 -0
  15. claude_mpm/cli/commands/agent_state_manager.py +18 -27
  16. claude_mpm/cli/commands/agents.py +175 -37
  17. claude_mpm/cli/commands/auto_configure.py +723 -236
  18. claude_mpm/cli/commands/config.py +88 -2
  19. claude_mpm/cli/commands/configure.py +1262 -157
  20. claude_mpm/cli/commands/configure_agent_display.py +25 -6
  21. claude_mpm/cli/commands/mpm_init/core.py +225 -46
  22. claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
  23. claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
  24. claude_mpm/cli/commands/postmortem.py +1 -1
  25. claude_mpm/cli/commands/profile.py +277 -0
  26. claude_mpm/cli/commands/skills.py +214 -189
  27. claude_mpm/cli/commands/summarize.py +413 -0
  28. claude_mpm/cli/executor.py +21 -3
  29. claude_mpm/cli/interactive/agent_wizard.py +85 -10
  30. claude_mpm/cli/parsers/agents_parser.py +54 -9
  31. claude_mpm/cli/parsers/auto_configure_parser.py +13 -138
  32. claude_mpm/cli/parsers/base_parser.py +12 -0
  33. claude_mpm/cli/parsers/config_parser.py +153 -83
  34. claude_mpm/cli/parsers/profile_parser.py +148 -0
  35. claude_mpm/cli/parsers/skills_parser.py +3 -2
  36. claude_mpm/cli/startup.py +879 -149
  37. claude_mpm/commands/mpm-config.md +28 -0
  38. claude_mpm/commands/mpm-doctor.md +9 -22
  39. claude_mpm/commands/mpm-help.md +5 -287
  40. claude_mpm/commands/mpm-init.md +81 -507
  41. claude_mpm/commands/mpm-monitor.md +15 -402
  42. claude_mpm/commands/mpm-organize.md +120 -0
  43. claude_mpm/commands/mpm-postmortem.md +6 -108
  44. claude_mpm/commands/mpm-session-resume.md +12 -363
  45. claude_mpm/commands/mpm-status.md +5 -69
  46. claude_mpm/commands/mpm-ticket-view.md +52 -495
  47. claude_mpm/commands/mpm-version.md +5 -107
  48. claude_mpm/config/agent_sources.py +27 -0
  49. claude_mpm/core/config.py +2 -4
  50. claude_mpm/core/framework/formatters/content_formatter.py +3 -13
  51. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  52. claude_mpm/core/framework/loaders/instruction_loader.py +52 -11
  53. claude_mpm/core/framework_loader.py +4 -2
  54. claude_mpm/core/logger.py +13 -0
  55. claude_mpm/core/optimized_startup.py +59 -0
  56. claude_mpm/core/output_style_manager.py +173 -43
  57. claude_mpm/core/shared/config_loader.py +1 -1
  58. claude_mpm/core/socketio_pool.py +3 -3
  59. claude_mpm/core/unified_agent_registry.py +134 -16
  60. claude_mpm/core/unified_config.py +22 -0
  61. claude_mpm/dashboard/static/svelte-build/_app/env.js +1 -0
  62. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.B_FtCwCQ.css +1 -0
  63. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.Cl_eSA4x.css +1 -0
  64. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BgChzWQ1.js +1 -0
  65. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIXEwuWe.js +1 -0
  66. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CWc5urbQ.js +1 -0
  67. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DMkZpdF2.js +2 -0
  68. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DjhvlsAc.js +1 -0
  69. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/N4qtv3Hx.js +2 -0
  70. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/uj46x2Wr.js +1 -0
  71. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.DTL5mJO-.js +2 -0
  72. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.DzuEhzqh.js +1 -0
  73. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/0.CAGBuiOw.js +1 -0
  74. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.DFLC8jdE.js +1 -0
  75. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.DPvEihJJ.js +10 -0
  76. claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -0
  77. claude_mpm/dashboard/static/svelte-build/favicon.svg +7 -0
  78. claude_mpm/dashboard/static/svelte-build/index.html +36 -0
  79. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
  80. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
  81. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  82. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  83. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  84. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  85. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  86. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
  87. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  88. claude_mpm/hooks/claude_hooks/event_handlers.py +211 -78
  89. claude_mpm/hooks/claude_hooks/hook_handler.py +155 -1
  90. claude_mpm/hooks/claude_hooks/installer.py +33 -10
  91. claude_mpm/hooks/claude_hooks/memory_integration.py +28 -0
  92. claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
  93. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  94. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  95. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  96. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
  97. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  98. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  99. claude_mpm/hooks/claude_hooks/services/connection_manager.py +30 -6
  100. claude_mpm/hooks/memory_integration_hook.py +46 -1
  101. claude_mpm/init.py +63 -19
  102. claude_mpm/models/agent_definition.py +7 -0
  103. claude_mpm/models/git_repository.py +3 -3
  104. claude_mpm/scripts/claude-hook-handler.sh +58 -18
  105. claude_mpm/scripts/launch_monitor.py +93 -13
  106. claude_mpm/scripts/start_activity_logging.py +0 -0
  107. claude_mpm/services/agents/agent_builder.py +3 -3
  108. claude_mpm/services/agents/agent_recommendation_service.py +278 -0
  109. claude_mpm/services/agents/agent_review_service.py +280 -0
  110. claude_mpm/services/agents/cache_git_manager.py +6 -6
  111. claude_mpm/services/agents/deployment/agent_deployment.py +29 -7
  112. claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -5
  113. claude_mpm/services/agents/deployment/agent_template_builder.py +5 -3
  114. claude_mpm/services/agents/deployment/agents_directory_resolver.py +2 -2
  115. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +320 -29
  116. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +546 -68
  117. claude_mpm/services/agents/git_source_manager.py +36 -2
  118. claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
  119. claude_mpm/services/agents/recommender.py +5 -3
  120. claude_mpm/services/agents/single_tier_deployment_service.py +2 -2
  121. claude_mpm/services/agents/sources/git_source_sync_service.py +13 -6
  122. claude_mpm/services/agents/startup_sync.py +22 -2
  123. claude_mpm/services/agents/toolchain_detector.py +10 -6
  124. claude_mpm/services/analysis/__init__.py +11 -1
  125. claude_mpm/services/analysis/clone_detector.py +1030 -0
  126. claude_mpm/services/command_deployment_service.py +81 -10
  127. claude_mpm/services/diagnostics/checks/agent_check.py +2 -2
  128. claude_mpm/services/diagnostics/checks/agent_sources_check.py +1 -1
  129. claude_mpm/services/event_bus/config.py +3 -1
  130. claude_mpm/services/git/git_operations_service.py +101 -16
  131. claude_mpm/services/monitor/daemon.py +9 -2
  132. claude_mpm/services/monitor/daemon_manager.py +39 -3
  133. claude_mpm/services/monitor/management/lifecycle.py +8 -1
  134. claude_mpm/services/monitor/server.py +698 -22
  135. claude_mpm/services/pm_skills_deployer.py +676 -0
  136. claude_mpm/services/profile_manager.py +331 -0
  137. claude_mpm/services/project/project_organizer.py +4 -0
  138. claude_mpm/services/self_upgrade_service.py +120 -12
  139. claude_mpm/services/skills/__init__.py +3 -0
  140. claude_mpm/services/skills/git_skill_source_manager.py +130 -2
  141. claude_mpm/services/skills/selective_skill_deployer.py +704 -0
  142. claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
  143. claude_mpm/services/skills_deployer.py +126 -9
  144. claude_mpm/services/socketio/dashboard_server.py +1 -0
  145. claude_mpm/services/socketio/event_normalizer.py +51 -6
  146. claude_mpm/services/socketio/server/core.py +386 -108
  147. claude_mpm/services/version_control/git_operations.py +103 -0
  148. claude_mpm/skills/skill_manager.py +92 -3
  149. claude_mpm/utils/agent_dependency_loader.py +14 -2
  150. claude_mpm/utils/agent_filters.py +17 -44
  151. claude_mpm/utils/gitignore.py +3 -0
  152. claude_mpm/utils/migration.py +4 -4
  153. claude_mpm/utils/robust_installer.py +47 -3
  154. {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/METADATA +57 -87
  155. {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/RECORD +160 -211
  156. claude_mpm-5.4.41.dist-info/entry_points.txt +5 -0
  157. claude_mpm-5.4.41.dist-info/licenses/LICENSE +94 -0
  158. claude_mpm-5.4.41.dist-info/licenses/LICENSE-FAQ.md +153 -0
  159. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
  160. claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
  161. claude_mpm/agents/BASE_OPS.md +0 -219
  162. claude_mpm/agents/BASE_PM.md +0 -480
  163. claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
  164. claude_mpm/agents/BASE_QA.md +0 -167
  165. claude_mpm/agents/BASE_RESEARCH.md +0 -53
  166. claude_mpm/agents/base_agent_loader.py +0 -601
  167. claude_mpm/cli/commands/agents_detect.py +0 -380
  168. claude_mpm/cli/commands/agents_recommend.py +0 -309
  169. claude_mpm/cli/ticket_cli.py +0 -35
  170. claude_mpm/commands/mpm-agents-auto-configure.md +0 -278
  171. claude_mpm/commands/mpm-agents-detect.md +0 -177
  172. claude_mpm/commands/mpm-agents-list.md +0 -131
  173. claude_mpm/commands/mpm-agents-recommend.md +0 -223
  174. claude_mpm/commands/mpm-config-view.md +0 -150
  175. claude_mpm/commands/mpm-ticket-organize.md +0 -304
  176. claude_mpm/dashboard/analysis_runner.py +0 -455
  177. claude_mpm/dashboard/index.html +0 -13
  178. claude_mpm/dashboard/open_dashboard.py +0 -66
  179. claude_mpm/dashboard/static/css/activity.css +0 -1958
  180. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  181. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  182. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  183. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  184. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  185. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  186. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  187. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  188. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  189. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  190. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  191. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  192. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  193. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  194. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  195. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  196. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  197. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  198. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  199. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  200. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  201. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  202. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  203. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  204. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  205. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  206. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  207. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  208. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  209. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  210. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  211. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  212. claude_mpm/dashboard/templates/code_simple.html +0 -153
  213. claude_mpm/dashboard/templates/index.html +0 -606
  214. claude_mpm/dashboard/test_dashboard.html +0 -372
  215. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  216. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
  217. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
  218. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
  219. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
  220. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
  221. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
  222. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
  223. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
  224. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
  225. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
  226. claude_mpm/scripts/mcp_server.py +0 -75
  227. claude_mpm/scripts/mcp_wrapper.py +0 -39
  228. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  229. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  230. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  231. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  232. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  233. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  234. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  235. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  236. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  237. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  238. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
  239. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  240. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  241. claude_mpm/services/mcp_gateway/main.py +0 -589
  242. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  243. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  244. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  245. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  246. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  247. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  248. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  249. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  250. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  251. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  252. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  253. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  254. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  255. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  256. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  257. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  258. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  259. claude_mpm-5.0.9.dist-info/entry_points.txt +0 -10
  260. claude_mpm-5.0.9.dist-info/licenses/LICENSE +0 -21
  261. /claude_mpm/agents/{OUTPUT_STYLE.md → CLAUDE_MPM_OUTPUT_STYLE.md} +0 -0
  262. {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/WHEEL +0 -0
  263. {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/top_level.txt +0 -0
@@ -15,7 +15,6 @@ DESIGN DECISIONS:
15
15
  """
16
16
 
17
17
  import asyncio
18
- import contextlib
19
18
  import os
20
19
  import threading
21
20
  import time
@@ -25,10 +24,11 @@ from typing import Dict, Optional
25
24
 
26
25
  import socketio
27
26
  from aiohttp import web
27
+ from watchdog.events import FileSystemEventHandler
28
+ from watchdog.observers import Observer
28
29
 
29
30
  from ...core.enums import ServiceState
30
31
  from ...core.logging_config import get_logger
31
- from ...dashboard.api.simple_directory import list_directory
32
32
  from .event_emitter import get_event_emitter
33
33
  from .handlers.code_analysis import CodeAnalysisHandler
34
34
  from .handlers.dashboard import DashboardHandler
@@ -45,6 +45,91 @@ except ImportError:
45
45
  EVENTBUS_AVAILABLE = False
46
46
 
47
47
 
48
+ class SvelteBuildWatcher(FileSystemEventHandler):
49
+ """File watcher for Svelte build directory changes.
50
+
51
+ Watches for file changes in svelte-build directory and triggers
52
+ hot reload via Socket.IO event emission.
53
+
54
+ STABILITY FIX: Added thread lock and stop() method to prevent timer leaks.
55
+ """
56
+
57
+ def __init__(
58
+ self, sio: socketio.AsyncServer, loop: asyncio.AbstractEventLoop, logger
59
+ ):
60
+ """Initialize the file watcher.
61
+
62
+ Args:
63
+ sio: Socket.IO server instance for emitting events
64
+ loop: Event loop for async operations
65
+ logger: Logger instance
66
+ """
67
+ super().__init__()
68
+ self.sio = sio
69
+ self.loop = loop
70
+ self.logger = logger
71
+ self.debounce_timer = None
72
+ self.debounce_delay = 0.5 # Wait 500ms after last change
73
+ self._timer_lock = threading.Lock() # STABILITY FIX: Prevent race condition
74
+
75
+ def stop(self):
76
+ """Stop the watcher and cancel any pending timers.
77
+
78
+ STABILITY FIX: Ensures timer is cancelled on shutdown.
79
+ """
80
+ with self._timer_lock:
81
+ if self.debounce_timer:
82
+ self.debounce_timer.cancel()
83
+ self.debounce_timer = None
84
+
85
+ def on_any_event(self, event):
86
+ """Handle any file system event.
87
+
88
+ Args:
89
+ event: File system event from watchdog
90
+ """
91
+ # Ignore directory events and temporary files
92
+ if event.is_directory or event.src_path.endswith((".tmp", ".swp", "~")):
93
+ return
94
+
95
+ self.logger.debug(
96
+ f"File change detected: {event.event_type} - {event.src_path}"
97
+ )
98
+
99
+ # STABILITY FIX: Use lock to prevent timer race condition
100
+ with self._timer_lock:
101
+ # Cancel existing timer
102
+ if self.debounce_timer:
103
+ self.debounce_timer.cancel()
104
+
105
+ # Schedule reload after debounce delay
106
+ self.debounce_timer = threading.Timer(
107
+ self.debounce_delay, self._trigger_reload
108
+ )
109
+ self.debounce_timer.start()
110
+
111
+ def _trigger_reload(self):
112
+ """Trigger hot reload by emitting Socket.IO event."""
113
+ try:
114
+ # Schedule the async emit in the event loop
115
+ asyncio.run_coroutine_threadsafe(self._emit_reload_event(), self.loop)
116
+ self.logger.info("Hot reload triggered - Svelte build changed")
117
+ except Exception as e:
118
+ self.logger.error(f"Error triggering reload: {e}")
119
+
120
+ async def _emit_reload_event(self):
121
+ """Emit the reload event to all connected clients."""
122
+ if self.sio:
123
+ await self.sio.emit(
124
+ "reload",
125
+ {
126
+ "type": "reload",
127
+ "timestamp": datetime.now(timezone.utc).isoformat() + "Z",
128
+ "reason": "svelte-build-updated",
129
+ },
130
+ )
131
+
132
+
48
133
  class UnifiedMonitorServer:
49
134
  """Unified server that combines HTTP dashboard and Socket.IO functionality.
50
135
 
@@ -52,15 +137,19 @@ class UnifiedMonitorServer:
52
137
  Replaces multiple competing server implementations with one stable solution.
53
138
  """
54
139
 
55
- def __init__(self, host: str = "localhost", port: int = 8765):
140
+ def __init__(
141
+ self, host: str = "localhost", port: int = 8765, enable_hot_reload: bool = False
142
+ ):
56
143
  """Initialize the unified monitor server.
57
144
 
58
145
  Args:
59
146
  host: Host to bind to
60
147
  port: Port to bind to
148
+ enable_hot_reload: Enable file watching and hot reload for development
61
149
  """
62
150
  self.host = host
63
151
  self.port = port
152
+ self.enable_hot_reload = enable_hot_reload
64
153
  self.logger = get_logger(__name__)
65
154
 
66
155
  # Core components
@@ -78,6 +167,10 @@ class UnifiedMonitorServer:
78
167
  # High-performance event emitter
79
168
  self.event_emitter = None
80
169
 
170
+ # File watching (optional for dev mode)
171
+ self.file_observer: Optional[Observer] = None
172
+ self.file_watcher: Optional[SvelteBuildWatcher] = None
173
+
81
174
  # State
82
175
  self.running = False
83
176
  self.loop = None
@@ -184,6 +277,9 @@ class UnifiedMonitorServer:
184
277
 
185
278
  time.sleep(0.1)
186
279
 
280
+ # STABILITY FIX: Give tasks more time to clean up before closing
281
+ time.sleep(0.5)
282
+
187
283
  # Clear the event loop from the thread BEFORE closing
188
284
  # This prevents other code from accidentally using it
189
285
  asyncio.set_event_loop(None)
@@ -229,6 +325,10 @@ class UnifiedMonitorServer:
229
325
  self.heartbeat_task = asyncio.create_task(self._heartbeat_loop())
230
326
  self.logger.info("Heartbeat task started (3-minute interval)")
231
327
 
328
+ # Setup file watching for hot reload (if enabled)
329
+ if self.enable_hot_reload:
330
+ self._setup_file_watcher()
331
+
232
332
  # Setup HTTP routes
233
333
  self._setup_http_routes()
234
334
 
@@ -304,20 +404,64 @@ class UnifiedMonitorServer:
304
404
  self.logger.error(f"Error setting up event emitter: {e}")
305
405
  raise
306
406
 
407
+ def _setup_file_watcher(self):
408
+ """Setup file watcher for Svelte build directory.
409
+
410
+ Watches for changes in svelte-build and triggers hot reload.
411
+ Only enabled when enable_hot_reload is True.
412
+ """
413
+ try:
414
+ dashboard_dir = Path(__file__).resolve().parent.parent.parent / "dashboard"
415
+ svelte_build_dir = dashboard_dir / "static" / "svelte-build"
416
+
417
+ if not svelte_build_dir.exists():
418
+ self.logger.warning(
419
+ f"Svelte build directory not found: {svelte_build_dir}. "
420
+ "Hot reload disabled."
421
+ )
422
+ return
423
+
424
+ # Create file watcher with Socket.IO reference
425
+ self.file_watcher = SvelteBuildWatcher(
426
+ sio=self.sio, loop=self.loop, logger=self.logger
427
+ )
428
+
429
+ # Create observer and schedule watching
430
+ self.file_observer = Observer()
431
+ self.file_observer.schedule(
432
+ self.file_watcher, str(svelte_build_dir), recursive=True
433
+ )
434
+ self.file_observer.start()
435
+
436
+ self.logger.info(f"🔥 Hot reload enabled - watching {svelte_build_dir}")
437
+
438
+ except Exception as e:
439
+ self.logger.error(f"Error setting up file watcher: {e}")
440
+ # Don't raise - hot reload is optional
441
+
307
442
  def _setup_http_routes(self):
308
443
  """Setup HTTP routes for the dashboard."""
309
444
  try:
310
- # Dashboard static files
311
- dashboard_dir = Path(__file__).parent.parent.parent / "dashboard"
445
+ # Dashboard static files - use .resolve() for absolute path
446
+ dashboard_dir = Path(__file__).resolve().parent.parent.parent / "dashboard"
447
+ static_dir = dashboard_dir / "static"
312
448
 
313
- # Main dashboard route
449
+ # Main dashboard route - serve Svelte dashboard
314
450
  async def dashboard_index(request):
315
- template_path = dashboard_dir / "templates" / "index.html"
316
- if template_path.exists():
317
- with template_path.open() as f:
451
+ svelte_index = static_dir / "svelte-build" / "index.html"
452
+ if svelte_index.exists():
453
+ with svelte_index.open(encoding="utf-8") as f:
318
454
  content = f.read()
319
455
  return web.Response(text=content, content_type="text/html")
320
- return web.Response(text="Dashboard not found", status=404)
456
+
457
+ # Log error with path details for debugging
458
+ self.logger.error(
459
+ f"Dashboard index.html not found at: {svelte_index.resolve()}"
460
+ )
461
+ return web.Response(
462
+ text=f"Dashboard not found. Expected location: {svelte_index.resolve()}",
463
+ status=404,
464
+ )
321
465
 
322
466
  # Health check
323
467
  async def health_check(request):
@@ -325,7 +469,8 @@ class UnifiedMonitorServer:
325
469
  version = "1.0.0"
326
470
  try:
327
471
  version_file = (
328
- Path(__file__).parent.parent.parent.parent.parent / "VERSION"
472
+ Path(__file__).resolve().parent.parent.parent.parent.parent
473
+ / "VERSION"
329
474
  )
330
475
  if version_file.exists():
331
476
  version = version_file.read_text().strip()
@@ -442,6 +587,243 @@ class UnifiedMonitorServer:
442
587
  {"success": False, "error": str(e)}, status=500
443
588
  )
444
589
 
590
+ # File listing endpoint for file browser
591
+ async def api_files_handler(request):
592
+ """List files in a directory for the file browser."""
593
+ try:
594
+ # Get path from query param, default to working directory
595
+ path = request.query.get("path", str(Path.cwd()))
596
+ dir_path = Path(path)
597
+
598
+ if not dir_path.exists():
599
+ return web.json_response(
600
+ {"success": False, "error": "Directory not found"},
601
+ status=404,
602
+ )
603
+
604
+ if not dir_path.is_dir():
605
+ return web.json_response(
606
+ {"success": False, "error": "Path is not a directory"},
607
+ status=400,
608
+ )
609
+
610
+ # Patterns to exclude
611
+ exclude_patterns = {
612
+ ".git",
613
+ "node_modules",
614
+ "__pycache__",
615
+ ".svelte-kit",
616
+ "venv",
617
+ ".venv",
618
+ "dist",
619
+ "build",
620
+ ".next",
621
+ ".cache",
622
+ ".pytest_cache",
623
+ ".mypy_cache",
624
+ ".ruff_cache",
625
+ "eggs",
626
+ "*.egg-info",
627
+ ".tox",
628
+ ".nox",
629
+ "htmlcov",
630
+ ".coverage",
631
+ }
632
+
633
+ entries = []
634
+ try:
635
+ for entry in sorted(
636
+ dir_path.iterdir(),
637
+ key=lambda x: (not x.is_dir(), x.name.lower()),
638
+ ):
639
+ # Skip hidden files and excluded patterns
640
+ if entry.name.startswith(".") and entry.name not in {
641
+ ".env",
642
+ ".gitignore",
643
+ }:
644
+ if entry.name in {".git", ".svelte-kit", ".cache"}:
645
+ continue
646
+ if entry.name in exclude_patterns:
647
+ continue
648
+ if any(
649
+ entry.name.endswith(p.replace("*", ""))
650
+ for p in exclude_patterns
651
+ if "*" in p
652
+ ):
653
+ continue
654
+
655
+ try:
656
+ stat = entry.stat()
657
+ entries.append(
658
+ {
659
+ "name": entry.name,
660
+ "path": str(entry),
661
+ "type": "directory"
662
+ if entry.is_dir()
663
+ else "file",
664
+ "size": stat.st_size if entry.is_file() else 0,
665
+ "modified": stat.st_mtime,
666
+ "extension": entry.suffix.lstrip(".")
667
+ if entry.is_file()
668
+ else None,
669
+ }
670
+ )
671
+ except (PermissionError, OSError):
672
+ continue
673
+
674
+ except PermissionError:
675
+ return web.json_response(
676
+ {"success": False, "error": "Permission denied"},
677
+ status=403,
678
+ )
679
+
680
+ # Separate directories and files
681
+ directories = [e for e in entries if e["type"] == "directory"]
682
+ files = [e for e in entries if e["type"] == "file"]
683
+
684
+ return web.json_response(
685
+ {
686
+ "success": True,
687
+ "path": str(dir_path),
688
+ "directories": directories,
689
+ "files": files,
690
+ "total_directories": len(directories),
691
+ "total_files": len(files),
692
+ }
693
+ )
694
+
695
+ except Exception as e:
696
+ self.logger.error(f"Error listing directory: {e}")
697
+ return web.json_response(
698
+ {"success": False, "error": str(e)}, status=500
699
+ )
700
+
701
+ # File read endpoint (GET) for file browser
702
+ async def api_file_read_handler(request):
703
+ """Read file content via GET request."""
704
+ import base64
705
+
706
+ try:
707
+ file_path = request.query.get("path", "")
708
+
709
+ if not file_path:
710
+ return web.json_response(
711
+ {"success": False, "error": "Path parameter required"},
712
+ status=400,
713
+ )
714
+
715
+ path = Path(file_path)
716
+
717
+ if not path.exists():
718
+ return web.json_response(
719
+ {"success": False, "error": "File not found"},
720
+ status=404,
721
+ )
722
+
723
+ if not path.is_file():
724
+ return web.json_response(
725
+ {"success": False, "error": "Path is not a file"},
726
+ status=400,
727
+ )
728
+
729
+ # Get file info
730
+ file_size = path.stat().st_size
731
+ file_ext = path.suffix.lstrip(".").lower()
732
+
733
+ # Define image extensions
734
+ image_extensions = {
735
+ "png",
736
+ "jpg",
737
+ "jpeg",
738
+ "gif",
739
+ "svg",
740
+ "webp",
741
+ "ico",
742
+ "bmp",
743
+ }
744
+
745
+ # Check if file is an image
746
+ if file_ext in image_extensions:
747
+ # Read as binary and encode to base64
748
+ try:
749
+ binary_content = path.read_bytes()
750
+ base64_content = base64.b64encode(binary_content).decode(
751
+ "utf-8"
752
+ )
753
+
754
+ # Map extension to MIME type
755
+ mime_types = {
756
+ "png": "image/png",
757
+ "jpg": "image/jpeg",
758
+ "jpeg": "image/jpeg",
759
+ "gif": "image/gif",
760
+ "svg": "image/svg+xml",
761
+ "webp": "image/webp",
762
+ "ico": "image/x-icon",
763
+ "bmp": "image/bmp",
764
+ }
765
+ mime_type = mime_types.get(file_ext, "image/png")
766
+
767
+ return web.json_response(
768
+ {
769
+ "success": True,
770
+ "path": str(path),
771
+ "content": base64_content,
772
+ "size": file_size,
773
+ "type": "image",
774
+ "mime": mime_type,
775
+ "extension": file_ext,
776
+ }
777
+ )
778
+ except Exception as e:
779
+ self.logger.error(f"Error reading image file: {e}")
780
+ return web.json_response(
781
+ {
782
+ "success": False,
783
+ "error": f"Failed to read image: {e!s}",
784
+ },
785
+ status=500,
786
+ )
787
+
788
+ # Read text file content
789
+ try:
790
+ content = path.read_text(encoding="utf-8")
791
+ lines = content.count("\n") + 1
792
+ except UnicodeDecodeError:
793
+ return web.json_response(
794
+ {"success": False, "error": "File is not a text file"},
795
+ status=415,
796
+ )
797
+
798
+ return web.json_response(
799
+ {
800
+ "success": True,
801
+ "path": str(path),
802
+ "content": content,
803
+ "lines": lines,
804
+ "size": file_size,
805
+ "type": file_ext or "text",
806
+ }
807
+ )
808
+
809
+ except Exception as e:
810
+ self.logger.error(f"Error reading file: {e}")
811
+ return web.json_response(
812
+ {"success": False, "error": str(e)}, status=500
813
+ )
814
+
815
+ # Favicon handler
816
+ async def favicon_handler(request):
817
+ """Serve favicon.svg from static directory."""
818
+ from aiohttp.web_fileresponse import FileResponse
819
+
820
+ favicon_path = static_dir / "svelte-build" / "favicon.svg"
821
+ if favicon_path.exists():
822
+ return FileResponse(
823
+ favicon_path, headers={"Content-Type": "image/svg+xml"}
824
+ )
825
+ raise web.HTTPNotFound()
826
+
445
827
  # Version endpoint for dashboard build tracker
446
828
  async def version_handler(request):
447
829
  """Serve version information for dashboard build tracker."""
@@ -507,7 +889,7 @@ class UnifiedMonitorServer:
507
889
  async def working_directory_handler(request):
508
890
  """Return the current working directory."""
509
891
  return web.json_response(
510
- {"working_directory": Path.cwd(), "success": True}
892
+ {"working_directory": str(Path.cwd()), "success": True}
511
893
  )
512
894
 
513
895
  # Monitor page routes
@@ -525,15 +907,249 @@ class UnifiedMonitorServer:
525
907
  return web.Response(text=content, content_type="text/html")
526
908
  return web.Response(text="Page not found", status=404)
527
909
 
910
+ # Git history handler
911
+ async def git_history_handler(request: web.Request) -> web.Response:
912
+ """Get git history for a file."""
913
+ import subprocess
914
+
915
+ try:
916
+ data = await request.json()
917
+ file_path = data.get("path", "")
918
+ limit = data.get("limit", 10)
919
+
920
+ if not file_path:
921
+ return web.json_response(
922
+ {
923
+ "success": False,
924
+ "error": "No path provided",
925
+ "commits": [],
926
+ },
927
+ status=400,
928
+ )
929
+
930
+ path = Path(file_path)
931
+ if not path.exists():
932
+ return web.json_response(
933
+ {
934
+ "success": False,
935
+ "error": "File not found",
936
+ "commits": [],
937
+ },
938
+ status=404,
939
+ )
940
+
941
+ # Get git log for file
942
+ result = subprocess.run(
943
+ [
944
+ "git",
945
+ "log",
946
+ f"-{limit}",
947
+ "--pretty=format:%H|%an|%ar|%s",
948
+ "--",
949
+ str(path),
950
+ ],
951
+ check=False,
952
+ capture_output=True,
953
+ text=True,
954
+ cwd=str(path.parent),
955
+ )
956
+
957
+ commits = []
958
+ if result.returncode == 0 and result.stdout:
959
+ for line in result.stdout.strip().split("\n"):
960
+ if line:
961
+ parts = line.split("|", 3)
962
+ if len(parts) == 4:
963
+ commits.append(
964
+ {
965
+ "hash": parts[0][:7],
966
+ "author": parts[1],
967
+ "date": parts[2],
968
+ "message": parts[3],
969
+ }
970
+ )
971
+
972
+ return web.json_response({"success": True, "commits": commits})
973
+ except Exception as e:
974
+ return web.json_response(
975
+ {"success": False, "error": str(e), "commits": []}, status=500
976
+ )
977
+
978
+ # Git diff handler
979
+ async def git_diff_handler(request: web.Request) -> web.Response:
980
+ """Get git diff for a file with optional commit selection."""
981
+ import subprocess
982
+
983
+ try:
984
+ file_path = request.query.get("path", "")
985
+ commit_hash = request.query.get(
986
+ "commit", ""
987
+ ) # Optional commit hash
988
+
989
+ if not file_path:
990
+ return web.json_response(
991
+ {
992
+ "success": False,
993
+ "error": "No path provided",
994
+ "diff": "",
995
+ "has_changes": False,
996
+ },
997
+ status=400,
998
+ )
999
+
1000
+ path = Path(file_path)
1001
+ if not path.exists():
1002
+ return web.json_response(
1003
+ {
1004
+ "success": False,
1005
+ "error": "File not found",
1006
+ "diff": "",
1007
+ "has_changes": False,
1008
+ },
1009
+ status=404,
1010
+ )
1011
+
1012
+ # Find git repository root
1013
+ git_root_result = subprocess.run(
1014
+ ["git", "rev-parse", "--show-toplevel"],
1015
+ check=False,
1016
+ capture_output=True,
1017
+ text=True,
1018
+ cwd=str(path.parent),
1019
+ )
1020
+
1021
+ if git_root_result.returncode != 0:
1022
+ # Not in a git repository
1023
+ return web.json_response(
1024
+ {
1025
+ "success": True,
1026
+ "diff": "",
1027
+ "has_changes": False,
1028
+ "tracked": False,
1029
+ "history": [],
1030
+ "has_uncommitted": False,
1031
+ }
1032
+ )
1033
+
1034
+ git_root = Path(git_root_result.stdout.strip())
1035
+
1036
+ # Check if file is tracked by git
1037
+ ls_files_result = subprocess.run(
1038
+ ["git", "ls-files", "--error-unmatch", str(path)],
1039
+ check=False,
1040
+ capture_output=True,
1041
+ text=True,
1042
+ cwd=str(git_root),
1043
+ )
1044
+
1045
+ if ls_files_result.returncode != 0:
1046
+ # File is not tracked by git
1047
+ return web.json_response(
1048
+ {
1049
+ "success": True,
1050
+ "diff": "",
1051
+ "has_changes": False,
1052
+ "tracked": False,
1053
+ "history": [],
1054
+ "has_uncommitted": False,
1055
+ }
1056
+ )
1057
+
1058
+ # Get commit history for this file (last 5 commits)
1059
+ history_result = subprocess.run(
1060
+ [
1061
+ "git",
1062
+ "log",
1063
+ "-5",
1064
+ "--pretty=format:%H|%s|%ar",
1065
+ "--",
1066
+ str(path),
1067
+ ],
1068
+ check=False,
1069
+ capture_output=True,
1070
+ text=True,
1071
+ cwd=str(git_root),
1072
+ )
1073
+
1074
+ history = []
1075
+ if history_result.returncode == 0 and history_result.stdout:
1076
+ for line in history_result.stdout.strip().split("\n"):
1077
+ if line:
1078
+ parts = line.split("|", 2)
1079
+ if len(parts) == 3:
1080
+ history.append(
1081
+ {
1082
+ "hash": parts[0][:7], # Short hash
1083
+ "full_hash": parts[0], # Full hash for API
1084
+ "message": parts[1],
1085
+ "time_ago": parts[2],
1086
+ }
1087
+ )
1088
+
1089
+ # Check for uncommitted changes
1090
+ uncommitted_result = subprocess.run(
1091
+ ["git", "diff", "HEAD", str(path)],
1092
+ check=False,
1093
+ capture_output=True,
1094
+ text=True,
1095
+ cwd=str(git_root),
1096
+ )
1097
+
1098
+ has_uncommitted = bool(uncommitted_result.stdout.strip())
1099
+
1100
+ # Get diff based on commit parameter
1101
+ if commit_hash:
1102
+ # Get diff for specific commit
1103
+ result = subprocess.run(
1104
+ ["git", "show", commit_hash, "--", str(path)],
1105
+ check=False,
1106
+ capture_output=True,
1107
+ text=True,
1108
+ cwd=str(git_root),
1109
+ )
1110
+ diff_output = result.stdout if result.returncode == 0 else ""
1111
+ has_changes = bool(diff_output.strip())
1112
+ else:
1113
+ # Get uncommitted diff (default behavior)
1114
+ diff_output = uncommitted_result.stdout
1115
+ has_changes = has_uncommitted
1116
+
1117
+ return web.json_response(
1118
+ {
1119
+ "success": True,
1120
+ "diff": diff_output,
1121
+ "has_changes": has_changes,
1122
+ "tracked": True,
1123
+ "history": history,
1124
+ "has_uncommitted": has_uncommitted,
1125
+ }
1126
+ )
1127
+ except Exception as e:
1128
+ return web.json_response(
1129
+ {
1130
+ "success": False,
1131
+ "error": str(e),
1132
+ "diff": "",
1133
+ "has_changes": False,
1134
+ "history": [],
1135
+ "has_uncommitted": False,
1136
+ },
1137
+ status=500,
1138
+ )
1139
+
528
1140
  # Register routes
529
1141
  self.app.router.add_get("/", dashboard_index)
1142
+ self.app.router.add_get("/favicon.svg", favicon_handler)
530
1143
  self.app.router.add_get("/health", health_check)
531
1144
  self.app.router.add_get("/version.json", version_handler)
532
1145
  self.app.router.add_get("/api/config", config_handler)
533
1146
  self.app.router.add_get("/api/working-directory", working_directory_handler)
534
- self.app.router.add_get("/api/directory", list_directory)
1147
+ self.app.router.add_get("/api/files", api_files_handler)
1148
+ self.app.router.add_get("/api/file/read", api_file_read_handler)
1149
+ self.app.router.add_get("/api/file/diff", git_diff_handler)
535
1150
  self.app.router.add_post("/api/events", api_events_handler)
536
1151
  self.app.router.add_post("/api/file", api_file_handler)
1152
+ self.app.router.add_post("/api/git-history", git_history_handler)
537
1153
 
538
1154
  # Monitor page routes
539
1155
  self.app.router.add_get("/monitor", lambda r: monitor_page_handler(r))
@@ -546,12 +1162,43 @@ class UnifiedMonitorServer:
546
1162
  "/monitor/events", lambda r: monitor_page_handler(r)
547
1163
  )
548
1164
 
549
- # Static files with cache busting headers for development
550
- static_dir = dashboard_dir / "static"
1165
+ # Serve Svelte _app assets (compiled JS/CSS)
1166
+ svelte_build_dir = static_dir / "svelte-build"
1167
+ if svelte_build_dir.exists():
1168
+ svelte_app_dir = svelte_build_dir / "_app"
1169
+ if svelte_app_dir.exists():
1170
+ # Serve _app assets with proper caching
1171
+ async def app_assets_handler(request):
1172
+ """Serve Svelte _app assets."""
1173
+ from aiohttp.web_fileresponse import FileResponse
1174
+
1175
+ rel_path = request.match_info["filepath"]
1176
+ file_path = svelte_app_dir / rel_path
1177
+
1178
+ if not file_path.exists() or not file_path.is_file():
1179
+ raise web.HTTPNotFound()
1180
+
1181
+ response = FileResponse(file_path)
1182
+
1183
+ # Add cache headers for immutable assets
1184
+ if "/immutable/" in str(rel_path):
1185
+ response.headers["Cache-Control"] = (
1186
+ "public, max-age=31536000, immutable"
1187
+ )
1188
+ else:
1189
+ response.headers["Cache-Control"] = (
1190
+ "no-cache, no-store, must-revalidate"
1191
+ )
1192
+
1193
+ return response
1194
+
1195
+ self.app.router.add_get("/_app/{filepath:.*}", app_assets_handler)
1196
+
1197
+ # Legacy static files (for backward compatibility)
551
1198
  if static_dir.exists():
552
1199
 
553
1200
  async def static_handler(request):
554
- """Serve static files with cache-control headers for development."""
1201
+ """Serve legacy static files with cache-control headers for development."""
555
1202
 
556
1203
  from aiohttp.web_fileresponse import FileResponse
557
1204
 
@@ -576,10 +1223,13 @@ class UnifiedMonitorServer:
576
1223
 
577
1224
  self.app.router.add_get("/static/{filepath:.*}", static_handler)
578
1225
 
579
- # Templates
580
- templates_dir = dashboard_dir / "templates"
581
- if templates_dir.exists():
582
- self.app.router.add_static("/templates/", templates_dir)
1226
+ # Log dashboard availability
1227
+ if svelte_build_dir.exists():
1228
+ self.logger.info(
1229
+ f"✅ Svelte dashboard available at / (root) (build: {svelte_build_dir})"
1230
+ )
1231
+ else:
1232
+ self.logger.warning(f"Svelte build not found at: {svelte_build_dir}")
583
1233
 
584
1234
  self.logger.info("HTTP routes registered successfully")
585
1235
 
@@ -691,11 +1341,37 @@ class UnifiedMonitorServer:
691
1341
  async def _cleanup_async(self):
692
1342
  """Cleanup async resources."""
693
1343
  try:
1344
+ # Stop file observer if running
1345
+ # STABILITY FIX: Ensure watcher is stopped and verify observer termination
1346
+ if self.file_observer:
1347
+ try:
1348
+ # Stop the watcher first to cancel pending timers
1349
+ if self.file_watcher:
1350
+ self.file_watcher.stop()
1351
+
1352
+ # Stop the observer
1353
+ self.file_observer.stop()
1354
+ self.file_observer.join(timeout=2)
1355
+
1356
+ # Verify observer actually stopped
1357
+ if self.file_observer.is_alive():
1358
+ self.logger.warning("File observer did not stop cleanly")
1359
+
1360
+ self.logger.debug("File observer stopped")
1361
+ except Exception as e:
1362
+ self.logger.debug(f"Error stopping file observer: {e}")
1363
+ finally:
1364
+ self.file_observer = None
1365
+ self.file_watcher = None
1366
+
694
1367
  # Cancel heartbeat task if running
1368
+ # STABILITY FIX: Add timeout to prevent infinite wait on cancellation
695
1369
  if self.heartbeat_task and not self.heartbeat_task.done():
696
1370
  self.heartbeat_task.cancel()
697
- with contextlib.suppress(asyncio.CancelledError):
698
- await self.heartbeat_task
1371
+ try:
1372
+ await asyncio.wait_for(self.heartbeat_task, timeout=2.0)
1373
+ except (asyncio.CancelledError, asyncio.TimeoutError):
1374
+ pass
699
1375
  self.logger.debug("Heartbeat task cancelled")
700
1376
 
701
1377
  # Close the Socket.IO server first to stop accepting new connections