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.
- claude_mpm/VERSION +1 -1
- claude_mpm/__init__.py +4 -0
- claude_mpm/agents/BASE_AGENT.md +164 -0
- claude_mpm/agents/{PM_INSTRUCTIONS_TEACH.md → CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md} +721 -41
- claude_mpm/agents/MEMORY.md +1 -1
- claude_mpm/agents/PM_INSTRUCTIONS.md +468 -468
- claude_mpm/agents/WORKFLOW.md +5 -254
- claude_mpm/agents/agent_loader.py +13 -44
- claude_mpm/agents/base_agent.json +1 -1
- claude_mpm/agents/frontmatter_validator.py +70 -2
- claude_mpm/agents/templates/circuit-breakers.md +431 -45
- claude_mpm/cli/__init__.py +0 -1
- claude_mpm/cli/__main__.py +4 -0
- claude_mpm/cli/chrome_devtools_installer.py +175 -0
- claude_mpm/cli/commands/agent_state_manager.py +18 -27
- claude_mpm/cli/commands/agents.py +175 -37
- claude_mpm/cli/commands/auto_configure.py +723 -236
- claude_mpm/cli/commands/config.py +88 -2
- claude_mpm/cli/commands/configure.py +1262 -157
- claude_mpm/cli/commands/configure_agent_display.py +25 -6
- claude_mpm/cli/commands/mpm_init/core.py +225 -46
- claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
- claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
- claude_mpm/cli/commands/postmortem.py +1 -1
- claude_mpm/cli/commands/profile.py +277 -0
- claude_mpm/cli/commands/skills.py +214 -189
- claude_mpm/cli/commands/summarize.py +413 -0
- claude_mpm/cli/executor.py +21 -3
- claude_mpm/cli/interactive/agent_wizard.py +85 -10
- claude_mpm/cli/parsers/agents_parser.py +54 -9
- claude_mpm/cli/parsers/auto_configure_parser.py +13 -138
- claude_mpm/cli/parsers/base_parser.py +12 -0
- claude_mpm/cli/parsers/config_parser.py +153 -83
- claude_mpm/cli/parsers/profile_parser.py +148 -0
- claude_mpm/cli/parsers/skills_parser.py +3 -2
- claude_mpm/cli/startup.py +879 -149
- claude_mpm/commands/mpm-config.md +28 -0
- claude_mpm/commands/mpm-doctor.md +9 -22
- claude_mpm/commands/mpm-help.md +5 -287
- claude_mpm/commands/mpm-init.md +81 -507
- claude_mpm/commands/mpm-monitor.md +15 -402
- claude_mpm/commands/mpm-organize.md +120 -0
- claude_mpm/commands/mpm-postmortem.md +6 -108
- claude_mpm/commands/mpm-session-resume.md +12 -363
- claude_mpm/commands/mpm-status.md +5 -69
- claude_mpm/commands/mpm-ticket-view.md +52 -495
- claude_mpm/commands/mpm-version.md +5 -107
- claude_mpm/config/agent_sources.py +27 -0
- claude_mpm/core/config.py +2 -4
- claude_mpm/core/framework/formatters/content_formatter.py +3 -13
- claude_mpm/core/framework/loaders/agent_loader.py +8 -5
- claude_mpm/core/framework/loaders/instruction_loader.py +52 -11
- claude_mpm/core/framework_loader.py +4 -2
- claude_mpm/core/logger.py +13 -0
- claude_mpm/core/optimized_startup.py +59 -0
- claude_mpm/core/output_style_manager.py +173 -43
- claude_mpm/core/shared/config_loader.py +1 -1
- claude_mpm/core/socketio_pool.py +3 -3
- claude_mpm/core/unified_agent_registry.py +134 -16
- claude_mpm/core/unified_config.py +22 -0
- claude_mpm/dashboard/static/svelte-build/_app/env.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.B_FtCwCQ.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.Cl_eSA4x.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BgChzWQ1.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIXEwuWe.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CWc5urbQ.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DMkZpdF2.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DjhvlsAc.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/N4qtv3Hx.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/uj46x2Wr.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.DTL5mJO-.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.DzuEhzqh.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/0.CAGBuiOw.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.DFLC8jdE.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.DPvEihJJ.js +10 -0
- claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -0
- claude_mpm/dashboard/static/svelte-build/favicon.svg +7 -0
- claude_mpm/dashboard/static/svelte-build/index.html +36 -0
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +211 -78
- claude_mpm/hooks/claude_hooks/hook_handler.py +155 -1
- claude_mpm/hooks/claude_hooks/installer.py +33 -10
- claude_mpm/hooks/claude_hooks/memory_integration.py +28 -0
- claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +30 -6
- claude_mpm/hooks/memory_integration_hook.py +46 -1
- claude_mpm/init.py +63 -19
- claude_mpm/models/agent_definition.py +7 -0
- claude_mpm/models/git_repository.py +3 -3
- claude_mpm/scripts/claude-hook-handler.sh +58 -18
- claude_mpm/scripts/launch_monitor.py +93 -13
- claude_mpm/scripts/start_activity_logging.py +0 -0
- claude_mpm/services/agents/agent_builder.py +3 -3
- claude_mpm/services/agents/agent_recommendation_service.py +278 -0
- claude_mpm/services/agents/agent_review_service.py +280 -0
- claude_mpm/services/agents/cache_git_manager.py +6 -6
- claude_mpm/services/agents/deployment/agent_deployment.py +29 -7
- claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -5
- claude_mpm/services/agents/deployment/agent_template_builder.py +5 -3
- claude_mpm/services/agents/deployment/agents_directory_resolver.py +2 -2
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +320 -29
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +546 -68
- claude_mpm/services/agents/git_source_manager.py +36 -2
- claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
- claude_mpm/services/agents/recommender.py +5 -3
- claude_mpm/services/agents/single_tier_deployment_service.py +2 -2
- claude_mpm/services/agents/sources/git_source_sync_service.py +13 -6
- claude_mpm/services/agents/startup_sync.py +22 -2
- claude_mpm/services/agents/toolchain_detector.py +10 -6
- claude_mpm/services/analysis/__init__.py +11 -1
- claude_mpm/services/analysis/clone_detector.py +1030 -0
- claude_mpm/services/command_deployment_service.py +81 -10
- claude_mpm/services/diagnostics/checks/agent_check.py +2 -2
- claude_mpm/services/diagnostics/checks/agent_sources_check.py +1 -1
- claude_mpm/services/event_bus/config.py +3 -1
- claude_mpm/services/git/git_operations_service.py +101 -16
- claude_mpm/services/monitor/daemon.py +9 -2
- claude_mpm/services/monitor/daemon_manager.py +39 -3
- claude_mpm/services/monitor/management/lifecycle.py +8 -1
- claude_mpm/services/monitor/server.py +698 -22
- claude_mpm/services/pm_skills_deployer.py +676 -0
- claude_mpm/services/profile_manager.py +331 -0
- claude_mpm/services/project/project_organizer.py +4 -0
- claude_mpm/services/self_upgrade_service.py +120 -12
- claude_mpm/services/skills/__init__.py +3 -0
- claude_mpm/services/skills/git_skill_source_manager.py +130 -2
- claude_mpm/services/skills/selective_skill_deployer.py +704 -0
- claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
- claude_mpm/services/skills_deployer.py +126 -9
- claude_mpm/services/socketio/dashboard_server.py +1 -0
- claude_mpm/services/socketio/event_normalizer.py +51 -6
- claude_mpm/services/socketio/server/core.py +386 -108
- claude_mpm/services/version_control/git_operations.py +103 -0
- claude_mpm/skills/skill_manager.py +92 -3
- claude_mpm/utils/agent_dependency_loader.py +14 -2
- claude_mpm/utils/agent_filters.py +17 -44
- claude_mpm/utils/gitignore.py +3 -0
- claude_mpm/utils/migration.py +4 -4
- claude_mpm/utils/robust_installer.py +47 -3
- {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/METADATA +57 -87
- {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/RECORD +160 -211
- claude_mpm-5.4.41.dist-info/entry_points.txt +5 -0
- claude_mpm-5.4.41.dist-info/licenses/LICENSE +94 -0
- claude_mpm-5.4.41.dist-info/licenses/LICENSE-FAQ.md +153 -0
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
- claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
- claude_mpm/agents/BASE_OPS.md +0 -219
- claude_mpm/agents/BASE_PM.md +0 -480
- claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
- claude_mpm/agents/BASE_QA.md +0 -167
- claude_mpm/agents/BASE_RESEARCH.md +0 -53
- claude_mpm/agents/base_agent_loader.py +0 -601
- claude_mpm/cli/commands/agents_detect.py +0 -380
- claude_mpm/cli/commands/agents_recommend.py +0 -309
- claude_mpm/cli/ticket_cli.py +0 -35
- claude_mpm/commands/mpm-agents-auto-configure.md +0 -278
- claude_mpm/commands/mpm-agents-detect.md +0 -177
- claude_mpm/commands/mpm-agents-list.md +0 -131
- claude_mpm/commands/mpm-agents-recommend.md +0 -223
- claude_mpm/commands/mpm-config-view.md +0 -150
- claude_mpm/commands/mpm-ticket-organize.md +0 -304
- claude_mpm/dashboard/analysis_runner.py +0 -455
- claude_mpm/dashboard/index.html +0 -13
- claude_mpm/dashboard/open_dashboard.py +0 -66
- claude_mpm/dashboard/static/css/activity.css +0 -1958
- claude_mpm/dashboard/static/css/connection-status.css +0 -370
- claude_mpm/dashboard/static/css/dashboard.css +0 -4701
- claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
- claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
- claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
- claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
- claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
- claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
- claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
- claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
- claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
- claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
- claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
- claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
- claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
- claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
- claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
- claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
- claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
- claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
- claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
- claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
- claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
- claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
- claude_mpm/dashboard/static/js/connection-manager.js +0 -536
- claude_mpm/dashboard/static/js/dashboard.js +0 -1914
- claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
- claude_mpm/dashboard/static/js/socket-client.js +0 -1474
- claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
- claude_mpm/dashboard/static/socket.io.min.js +0 -7
- claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
- claude_mpm/dashboard/templates/code_simple.html +0 -153
- claude_mpm/dashboard/templates/index.html +0 -606
- claude_mpm/dashboard/test_dashboard.html +0 -372
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
- claude_mpm/scripts/mcp_server.py +0 -75
- claude_mpm/scripts/mcp_wrapper.py +0 -39
- claude_mpm/services/mcp_gateway/__init__.py +0 -159
- claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
- claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
- claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
- claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
- claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
- claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
- claude_mpm/services/mcp_gateway/core/base.py +0 -312
- claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
- claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
- claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
- claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
- claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
- claude_mpm/services/mcp_gateway/main.py +0 -589
- claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
- claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
- claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
- claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
- claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
- claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
- claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
- claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
- claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
- claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
- claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
- claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
- claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
- claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
- claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
- claude_mpm-5.0.9.dist-info/entry_points.txt +0 -10
- claude_mpm-5.0.9.dist-info/licenses/LICENSE +0 -21
- /claude_mpm/agents/{OUTPUT_STYLE.md → CLAUDE_MPM_OUTPUT_STYLE.md} +0 -0
- {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/WHEEL +0 -0
- {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__(
|
|
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
|
-
|
|
316
|
-
if
|
|
317
|
-
with
|
|
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
|
-
|
|
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
|
|
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/
|
|
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
|
-
#
|
|
550
|
-
|
|
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
|
-
#
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
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
|