crackerjack 0.29.0__py3-none-any.whl → 0.31.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.
Potentially problematic release.
This version of crackerjack might be problematic. Click here for more details.
- crackerjack/CLAUDE.md +1005 -0
- crackerjack/RULES.md +380 -0
- crackerjack/__init__.py +42 -13
- crackerjack/__main__.py +225 -253
- crackerjack/agents/__init__.py +41 -0
- crackerjack/agents/architect_agent.py +281 -0
- crackerjack/agents/base.py +169 -0
- crackerjack/agents/coordinator.py +512 -0
- crackerjack/agents/documentation_agent.py +498 -0
- crackerjack/agents/dry_agent.py +388 -0
- crackerjack/agents/formatting_agent.py +245 -0
- crackerjack/agents/import_optimization_agent.py +281 -0
- crackerjack/agents/performance_agent.py +669 -0
- crackerjack/agents/proactive_agent.py +104 -0
- crackerjack/agents/refactoring_agent.py +788 -0
- crackerjack/agents/security_agent.py +529 -0
- crackerjack/agents/test_creation_agent.py +652 -0
- crackerjack/agents/test_specialist_agent.py +486 -0
- crackerjack/agents/tracker.py +212 -0
- crackerjack/api.py +560 -0
- crackerjack/cli/__init__.py +24 -0
- crackerjack/cli/facade.py +104 -0
- crackerjack/cli/handlers.py +267 -0
- crackerjack/cli/interactive.py +471 -0
- crackerjack/cli/options.py +401 -0
- crackerjack/cli/utils.py +18 -0
- crackerjack/code_cleaner.py +670 -0
- crackerjack/config/__init__.py +19 -0
- crackerjack/config/hooks.py +218 -0
- crackerjack/core/__init__.py +0 -0
- crackerjack/core/async_workflow_orchestrator.py +406 -0
- crackerjack/core/autofix_coordinator.py +200 -0
- crackerjack/core/container.py +104 -0
- crackerjack/core/enhanced_container.py +542 -0
- crackerjack/core/performance.py +243 -0
- crackerjack/core/phase_coordinator.py +561 -0
- crackerjack/core/proactive_workflow.py +316 -0
- crackerjack/core/session_coordinator.py +289 -0
- crackerjack/core/workflow_orchestrator.py +640 -0
- crackerjack/dynamic_config.py +577 -0
- crackerjack/errors.py +263 -41
- crackerjack/executors/__init__.py +11 -0
- crackerjack/executors/async_hook_executor.py +431 -0
- crackerjack/executors/cached_hook_executor.py +242 -0
- crackerjack/executors/hook_executor.py +345 -0
- crackerjack/executors/individual_hook_executor.py +669 -0
- crackerjack/intelligence/__init__.py +44 -0
- crackerjack/intelligence/adaptive_learning.py +751 -0
- crackerjack/intelligence/agent_orchestrator.py +551 -0
- crackerjack/intelligence/agent_registry.py +414 -0
- crackerjack/intelligence/agent_selector.py +502 -0
- crackerjack/intelligence/integration.py +290 -0
- crackerjack/interactive.py +576 -315
- crackerjack/managers/__init__.py +11 -0
- crackerjack/managers/async_hook_manager.py +135 -0
- crackerjack/managers/hook_manager.py +137 -0
- crackerjack/managers/publish_manager.py +411 -0
- crackerjack/managers/test_command_builder.py +151 -0
- crackerjack/managers/test_executor.py +435 -0
- crackerjack/managers/test_manager.py +258 -0
- crackerjack/managers/test_manager_backup.py +1124 -0
- crackerjack/managers/test_progress.py +144 -0
- crackerjack/mcp/__init__.py +0 -0
- crackerjack/mcp/cache.py +336 -0
- crackerjack/mcp/client_runner.py +104 -0
- crackerjack/mcp/context.py +615 -0
- crackerjack/mcp/dashboard.py +636 -0
- crackerjack/mcp/enhanced_progress_monitor.py +479 -0
- crackerjack/mcp/file_monitor.py +336 -0
- crackerjack/mcp/progress_components.py +569 -0
- crackerjack/mcp/progress_monitor.py +949 -0
- crackerjack/mcp/rate_limiter.py +332 -0
- crackerjack/mcp/server.py +22 -0
- crackerjack/mcp/server_core.py +244 -0
- crackerjack/mcp/service_watchdog.py +501 -0
- crackerjack/mcp/state.py +395 -0
- crackerjack/mcp/task_manager.py +257 -0
- crackerjack/mcp/tools/__init__.py +17 -0
- crackerjack/mcp/tools/core_tools.py +249 -0
- crackerjack/mcp/tools/error_analyzer.py +308 -0
- crackerjack/mcp/tools/execution_tools.py +370 -0
- crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
- crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
- crackerjack/mcp/tools/intelligence_tools.py +314 -0
- crackerjack/mcp/tools/monitoring_tools.py +502 -0
- crackerjack/mcp/tools/proactive_tools.py +384 -0
- crackerjack/mcp/tools/progress_tools.py +141 -0
- crackerjack/mcp/tools/utility_tools.py +341 -0
- crackerjack/mcp/tools/workflow_executor.py +360 -0
- crackerjack/mcp/websocket/__init__.py +14 -0
- crackerjack/mcp/websocket/app.py +39 -0
- crackerjack/mcp/websocket/endpoints.py +559 -0
- crackerjack/mcp/websocket/jobs.py +253 -0
- crackerjack/mcp/websocket/server.py +116 -0
- crackerjack/mcp/websocket/websocket_handler.py +78 -0
- crackerjack/mcp/websocket_server.py +10 -0
- crackerjack/models/__init__.py +31 -0
- crackerjack/models/config.py +93 -0
- crackerjack/models/config_adapter.py +230 -0
- crackerjack/models/protocols.py +118 -0
- crackerjack/models/task.py +154 -0
- crackerjack/monitoring/ai_agent_watchdog.py +450 -0
- crackerjack/monitoring/regression_prevention.py +638 -0
- crackerjack/orchestration/__init__.py +0 -0
- crackerjack/orchestration/advanced_orchestrator.py +970 -0
- crackerjack/orchestration/execution_strategies.py +341 -0
- crackerjack/orchestration/test_progress_streamer.py +636 -0
- crackerjack/plugins/__init__.py +15 -0
- crackerjack/plugins/base.py +200 -0
- crackerjack/plugins/hooks.py +246 -0
- crackerjack/plugins/loader.py +335 -0
- crackerjack/plugins/managers.py +259 -0
- crackerjack/py313.py +8 -3
- crackerjack/services/__init__.py +22 -0
- crackerjack/services/cache.py +314 -0
- crackerjack/services/config.py +347 -0
- crackerjack/services/config_integrity.py +99 -0
- crackerjack/services/contextual_ai_assistant.py +516 -0
- crackerjack/services/coverage_ratchet.py +347 -0
- crackerjack/services/debug.py +736 -0
- crackerjack/services/dependency_monitor.py +617 -0
- crackerjack/services/enhanced_filesystem.py +439 -0
- crackerjack/services/file_hasher.py +151 -0
- crackerjack/services/filesystem.py +395 -0
- crackerjack/services/git.py +165 -0
- crackerjack/services/health_metrics.py +611 -0
- crackerjack/services/initialization.py +847 -0
- crackerjack/services/log_manager.py +286 -0
- crackerjack/services/logging.py +174 -0
- crackerjack/services/metrics.py +578 -0
- crackerjack/services/pattern_cache.py +362 -0
- crackerjack/services/pattern_detector.py +515 -0
- crackerjack/services/performance_benchmarks.py +653 -0
- crackerjack/services/security.py +163 -0
- crackerjack/services/server_manager.py +234 -0
- crackerjack/services/smart_scheduling.py +144 -0
- crackerjack/services/tool_version_service.py +61 -0
- crackerjack/services/unified_config.py +437 -0
- crackerjack/services/version_checker.py +248 -0
- crackerjack/slash_commands/__init__.py +14 -0
- crackerjack/slash_commands/init.md +122 -0
- crackerjack/slash_commands/run.md +163 -0
- crackerjack/slash_commands/status.md +127 -0
- crackerjack-0.31.4.dist-info/METADATA +742 -0
- crackerjack-0.31.4.dist-info/RECORD +148 -0
- crackerjack-0.31.4.dist-info/entry_points.txt +2 -0
- crackerjack/.gitignore +0 -34
- crackerjack/.libcst.codemod.yaml +0 -18
- crackerjack/.pdm.toml +0 -1
- crackerjack/.pre-commit-config-ai.yaml +0 -149
- crackerjack/.pre-commit-config-fast.yaml +0 -69
- crackerjack/.pre-commit-config.yaml +0 -114
- crackerjack/crackerjack.py +0 -4140
- crackerjack/pyproject.toml +0 -285
- crackerjack-0.29.0.dist-info/METADATA +0 -1289
- crackerjack-0.29.0.dist-info/RECORD +0 -17
- {crackerjack-0.29.0.dist-info → crackerjack-0.31.4.dist-info}/WHEEL +0 -0
- {crackerjack-0.29.0.dist-info → crackerjack-0.31.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,949 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import atexit
|
|
3
|
+
import signal
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
import time
|
|
8
|
+
from contextlib import suppress
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from textual.app import App, ComposeResult
|
|
13
|
+
from textual.containers import Container
|
|
14
|
+
from textual.widget import Widget
|
|
15
|
+
from textual.widgets import DataTable, Footer, Label, ProgressBar
|
|
16
|
+
|
|
17
|
+
from .progress_components import (
|
|
18
|
+
ErrorCollector,
|
|
19
|
+
JobDataCollector,
|
|
20
|
+
ServiceHealthChecker,
|
|
21
|
+
ServiceManager,
|
|
22
|
+
TerminalRestorer,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AgentStatusPanel(Widget):
|
|
27
|
+
def __init__(self, **kwargs) -> None:
|
|
28
|
+
super().__init__(**kwargs)
|
|
29
|
+
self.border_title = "🤖 AI Agents"
|
|
30
|
+
self.border_title_align = "left"
|
|
31
|
+
|
|
32
|
+
def compose(self) -> ComposeResult:
|
|
33
|
+
yield DataTable(id="agents-table")
|
|
34
|
+
yield Label("Coordinator: Loading...", id="coordinator-status")
|
|
35
|
+
yield Label("Stats: Loading...", id="agent-stats")
|
|
36
|
+
|
|
37
|
+
def on_mount(self) -> None:
|
|
38
|
+
with suppress(Exception):
|
|
39
|
+
agents_table = self.query_one("#agents-table", DataTable)
|
|
40
|
+
agents_table.add_columns(
|
|
41
|
+
"Agent",
|
|
42
|
+
"Status",
|
|
43
|
+
"Issue Type",
|
|
44
|
+
"Confidence",
|
|
45
|
+
"Time",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
agents_table.styles.max_height = "8"
|
|
49
|
+
|
|
50
|
+
def update_agent_data(self, agent_data: dict) -> None:
|
|
51
|
+
with suppress(Exception):
|
|
52
|
+
self._update_coordinator_status(agent_data)
|
|
53
|
+
self._update_agents_table(agent_data)
|
|
54
|
+
self._update_stats(agent_data)
|
|
55
|
+
|
|
56
|
+
def _update_coordinator_status(self, data: dict) -> None:
|
|
57
|
+
with suppress(Exception):
|
|
58
|
+
activity = data.get("agent_activity", {})
|
|
59
|
+
registry = activity.get("agent_registry", {})
|
|
60
|
+
coordinator_status = activity.get("coordinator_status", "idle")
|
|
61
|
+
total_agents = registry.get("total_agents", 6)
|
|
62
|
+
|
|
63
|
+
status_emoji = "✅" if coordinator_status == "active" else "⏸️"
|
|
64
|
+
status_label = self.query_one("#coordinator-status", Label)
|
|
65
|
+
status_label.update(
|
|
66
|
+
f"Coordinator: {status_emoji} {coordinator_status.title()} ({total_agents} agents)",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def _update_agents_table(self, data: dict) -> None:
|
|
70
|
+
with suppress(Exception):
|
|
71
|
+
agents_table = self.query_one("#agents-table", DataTable)
|
|
72
|
+
agents_table.clear()
|
|
73
|
+
|
|
74
|
+
activity = data.get("agent_activity", {})
|
|
75
|
+
active_agents = activity.get("active_agents", [])
|
|
76
|
+
|
|
77
|
+
if not active_agents:
|
|
78
|
+
agents_table.add_row("No active agents", " - ", " - ", " - ", " - ")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
for agent in active_agents:
|
|
82
|
+
agent_type = agent.get("agent_type", "Unknown")
|
|
83
|
+
status = agent.get("status", "unknown")
|
|
84
|
+
confidence = agent.get("confidence", 0)
|
|
85
|
+
processing_time = agent.get("processing_time", 0)
|
|
86
|
+
|
|
87
|
+
emoji = self._get_agent_emoji(agent_type)
|
|
88
|
+
|
|
89
|
+
current_issue = agent.get("current_issue", {})
|
|
90
|
+
issue_type = (
|
|
91
|
+
current_issue.get("type", " - ")
|
|
92
|
+
if current_issue
|
|
93
|
+
else agent.get("issue_type", " - ")
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
status_display = f"{self._get_status_emoji(status)} {status.title()}"
|
|
97
|
+
|
|
98
|
+
agents_table.add_row(
|
|
99
|
+
f"{emoji} {agent_type}",
|
|
100
|
+
status_display,
|
|
101
|
+
issue_type,
|
|
102
|
+
f"{confidence: .0 % }" if confidence > 0 else " - ",
|
|
103
|
+
f"{processing_time: .1f}s" if processing_time > 0 else " - ",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def _update_stats(self, data: dict) -> None:
|
|
107
|
+
with suppress(Exception):
|
|
108
|
+
performance = data.get("agent_performance", {})
|
|
109
|
+
total_issues = performance.get("total_issues_processed", 0)
|
|
110
|
+
success_rate = performance.get("success_rate", 0)
|
|
111
|
+
avg_time = performance.get("average_processing_time", 0)
|
|
112
|
+
cache_hits = performance.get("cache_hits", 0)
|
|
113
|
+
|
|
114
|
+
stats_label = self.query_one("#agent-stats", Label)
|
|
115
|
+
stats_text = f"Stats: {total_issues} issues | {success_rate: .0 % } success"
|
|
116
|
+
if avg_time > 0:
|
|
117
|
+
stats_text += f" | {avg_time: .1f}s avg"
|
|
118
|
+
if cache_hits > 0:
|
|
119
|
+
stats_text += f" | {cache_hits} cached"
|
|
120
|
+
|
|
121
|
+
if success_rate >= 80:
|
|
122
|
+
stats_text += " ↑🟢"
|
|
123
|
+
elif success_rate >= 60:
|
|
124
|
+
stats_text += " 🟡"
|
|
125
|
+
elif total_issues > 0:
|
|
126
|
+
stats_text += " ↓🔴"
|
|
127
|
+
|
|
128
|
+
stats_label.update(stats_text)
|
|
129
|
+
|
|
130
|
+
def _get_agent_emoji(self, agent_type: str) -> str:
|
|
131
|
+
emojis = {
|
|
132
|
+
"FormattingAgent": "🎨",
|
|
133
|
+
"SecurityAgent": "🔒",
|
|
134
|
+
"TestSpecialistAgent": "🧪",
|
|
135
|
+
"TestCreationAgent": "➕",
|
|
136
|
+
"RefactoringAgent": "🔧",
|
|
137
|
+
"ImportOptimizationAgent": "📦",
|
|
138
|
+
}
|
|
139
|
+
return emojis.get(agent_type) or "🤖"
|
|
140
|
+
|
|
141
|
+
def _get_status_emoji(self, status: str) -> str:
|
|
142
|
+
emojis = {
|
|
143
|
+
"evaluating": "🔍",
|
|
144
|
+
"processing": "⏳",
|
|
145
|
+
"completed": "✅",
|
|
146
|
+
"failed": "❌",
|
|
147
|
+
"idle": "⏸️",
|
|
148
|
+
}
|
|
149
|
+
return emojis.get(status.lower()) or "❓"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class JobPanel(Widget):
|
|
153
|
+
def __init__(self, job_data: dict, **kwargs) -> None:
|
|
154
|
+
super().__init__(**kwargs)
|
|
155
|
+
self.job_data = job_data
|
|
156
|
+
self.completion_time = None
|
|
157
|
+
self.iteration_count = job_data.get("iteration", 0)
|
|
158
|
+
self.max_iterations = job_data.get("max_iterations", 10)
|
|
159
|
+
self.fade_timer = None
|
|
160
|
+
self.remove_timer = None
|
|
161
|
+
self.fade_level = 0
|
|
162
|
+
self.border_style = self._calculate_border_style()
|
|
163
|
+
|
|
164
|
+
def _calculate_border_style(self) -> str:
|
|
165
|
+
status = self.job_data.get("status", "unknown").lower()
|
|
166
|
+
|
|
167
|
+
if status == "completed":
|
|
168
|
+
errors = self.job_data.get("errors", [])
|
|
169
|
+
hook_failures = self.job_data.get("hook_failures", [])
|
|
170
|
+
test_failures = self.job_data.get("test_failures", [])
|
|
171
|
+
total_failures = (
|
|
172
|
+
len(hook_failures)
|
|
173
|
+
+ len(test_failures)
|
|
174
|
+
+ len([e for e in errors if "failed" in str(e).lower()])
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if total_failures == 0:
|
|
178
|
+
return "round green"
|
|
179
|
+
return "round red"
|
|
180
|
+
if status == "failed" or self.iteration_count >= 10:
|
|
181
|
+
return "round red"
|
|
182
|
+
if status == "running":
|
|
183
|
+
return "round blue"
|
|
184
|
+
return "round white"
|
|
185
|
+
|
|
186
|
+
def on_mount(self) -> None:
|
|
187
|
+
project_name = self.job_data.get("project", "crackerjack")
|
|
188
|
+
status = self.job_data.get("status", "").lower()
|
|
189
|
+
if status == "running":
|
|
190
|
+
self.border_title = f"📁 {project_name}"
|
|
191
|
+
self.border_subtitle = "💓"
|
|
192
|
+
self.border_subtitle_align = "right"
|
|
193
|
+
else:
|
|
194
|
+
self.border_title = f"📁 {project_name}"
|
|
195
|
+
self.border_title_align = "left"
|
|
196
|
+
|
|
197
|
+
self._setup_errors_table()
|
|
198
|
+
|
|
199
|
+
self._update_progress_bar()
|
|
200
|
+
|
|
201
|
+
status = self.job_data.get("status", "").lower()
|
|
202
|
+
if status in ("completed", "failed") and self.completion_time is None:
|
|
203
|
+
self.completion_time = time.time()
|
|
204
|
+
|
|
205
|
+
self.fade_timer = self.set_timer(300.0, self._start_fade)
|
|
206
|
+
|
|
207
|
+
self.remove_timer = self.set_timer(1200.0, self._remove_panel)
|
|
208
|
+
|
|
209
|
+
def _setup_errors_table(self) -> None:
|
|
210
|
+
with suppress(Exception):
|
|
211
|
+
errors_container = self.query_one(".job-errors")
|
|
212
|
+
errors_container.border_title = "❌ Errors"
|
|
213
|
+
|
|
214
|
+
errors_table = self.query_one(
|
|
215
|
+
f"#job-errors-{self.job_data.get('job_id', 'unknown')}",
|
|
216
|
+
DataTable,
|
|
217
|
+
)
|
|
218
|
+
errors_table.add_columns("", "", "", "")
|
|
219
|
+
|
|
220
|
+
self._update_errors_table()
|
|
221
|
+
|
|
222
|
+
def _update_errors_table(self) -> None:
|
|
223
|
+
with suppress(Exception):
|
|
224
|
+
errors_table = self.query_one(
|
|
225
|
+
f"#job-errors-{self.job_data.get('job_id', 'unknown')}",
|
|
226
|
+
DataTable,
|
|
227
|
+
)
|
|
228
|
+
errors_table.clear()
|
|
229
|
+
|
|
230
|
+
total_errors = self.job_data.get("total_issues", 0)
|
|
231
|
+
fixed_errors = self.job_data.get("errors_fixed", 0)
|
|
232
|
+
|
|
233
|
+
remaining_errors = max(0, total_errors - fixed_errors)
|
|
234
|
+
|
|
235
|
+
progress_pct = 0
|
|
236
|
+
if total_errors > 0:
|
|
237
|
+
progress_pct = int((fixed_errors / total_errors) * 100)
|
|
238
|
+
|
|
239
|
+
if total_errors == 0 and "errors" in self.job_data:
|
|
240
|
+
errors = self.job_data.get("errors", [])
|
|
241
|
+
hook_failures = self.job_data.get("hook_failures", [])
|
|
242
|
+
test_failures = self.job_data.get("test_failures", [])
|
|
243
|
+
total_errors = len(errors) + len(hook_failures) + len(test_failures)
|
|
244
|
+
failed_errors = (
|
|
245
|
+
len(hook_failures)
|
|
246
|
+
+ len(test_failures)
|
|
247
|
+
+ len([e for e in errors if "failed" in str(e).lower()])
|
|
248
|
+
)
|
|
249
|
+
fixed_errors = max(0, total_errors - failed_errors)
|
|
250
|
+
remaining_errors = failed_errors
|
|
251
|
+
if total_errors > 0:
|
|
252
|
+
progress_pct = int((fixed_errors / total_errors) * 100)
|
|
253
|
+
|
|
254
|
+
discovered_label = "🔍 Found"
|
|
255
|
+
discovered_value = f"{total_errors: > 15}"
|
|
256
|
+
resolved_label = "✅ Fixed"
|
|
257
|
+
resolved_value = f"{fixed_errors: > 15}"
|
|
258
|
+
|
|
259
|
+
remaining_label = "❌ Left"
|
|
260
|
+
remaining_value = f"{remaining_errors: > 15}"
|
|
261
|
+
progress_label = "📈 Done"
|
|
262
|
+
progress_value = f"{progress_pct} % ".rjust(15)
|
|
263
|
+
|
|
264
|
+
errors_table.add_rows(
|
|
265
|
+
[
|
|
266
|
+
(
|
|
267
|
+
discovered_label,
|
|
268
|
+
discovered_value,
|
|
269
|
+
resolved_label,
|
|
270
|
+
resolved_value,
|
|
271
|
+
),
|
|
272
|
+
(remaining_label, remaining_value, progress_label, progress_value),
|
|
273
|
+
],
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
def _update_progress_bar(self) -> None:
|
|
277
|
+
with suppress(Exception):
|
|
278
|
+
progress_bar = self.query_one(
|
|
279
|
+
f"#job-progress-{self.job_data.get('job_id', 'unknown')}",
|
|
280
|
+
ProgressBar,
|
|
281
|
+
)
|
|
282
|
+
progress_value = self.iteration_count / max(self.max_iterations, 1) * 100
|
|
283
|
+
progress_bar.update(progress=progress_value)
|
|
284
|
+
|
|
285
|
+
def _start_fade(self) -> None:
|
|
286
|
+
self.fade_level += 1
|
|
287
|
+
|
|
288
|
+
if self.fade_level == 1:
|
|
289
|
+
self.add_class("fade-1")
|
|
290
|
+
elif self.fade_level == 2:
|
|
291
|
+
self.add_class("fade-2")
|
|
292
|
+
elif self.fade_level == 3:
|
|
293
|
+
self.add_class("fade-3")
|
|
294
|
+
elif self.fade_level >= 4:
|
|
295
|
+
self.add_class("fade-4")
|
|
296
|
+
|
|
297
|
+
if self.fade_level < 4:
|
|
298
|
+
self.fade_timer = self.set_timer(300.0, self._start_fade)
|
|
299
|
+
|
|
300
|
+
def _remove_panel(self) -> None:
|
|
301
|
+
if hasattr(self.app, "completed_jobs_stats"):
|
|
302
|
+
job_id = self.job_data.get("job_id")
|
|
303
|
+
|
|
304
|
+
total_errors = self.job_data.get("total_issues", 0)
|
|
305
|
+
fixed_errors = self.job_data.get("errors_fixed", 0)
|
|
306
|
+
remaining_errors = max(0, total_errors - fixed_errors)
|
|
307
|
+
|
|
308
|
+
if total_errors == 0 and "errors" in self.job_data:
|
|
309
|
+
errors = self.job_data.get("errors", [])
|
|
310
|
+
hook_failures = self.job_data.get("hook_failures", [])
|
|
311
|
+
test_failures = self.job_data.get("test_failures", [])
|
|
312
|
+
|
|
313
|
+
total_errors = len(errors) + len(hook_failures) + len(test_failures)
|
|
314
|
+
failed_errors = (
|
|
315
|
+
len(hook_failures)
|
|
316
|
+
+ len(test_failures)
|
|
317
|
+
+ len([e for e in errors if "failed" in str(e).lower()])
|
|
318
|
+
)
|
|
319
|
+
fixed_errors = max(0, total_errors - failed_errors)
|
|
320
|
+
remaining_errors = failed_errors
|
|
321
|
+
|
|
322
|
+
self.app.completed_jobs_stats[job_id] = {
|
|
323
|
+
"status": self.job_data.get("status", "unknown"),
|
|
324
|
+
"total_errors": total_errors,
|
|
325
|
+
"fixed_errors": fixed_errors,
|
|
326
|
+
"remaining_errors": remaining_errors,
|
|
327
|
+
"completion_time": self.completion_time,
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if hasattr(self.app, "active_jobs"):
|
|
331
|
+
job_id = self.job_data.get("job_id")
|
|
332
|
+
if job_id in self.app.active_jobs:
|
|
333
|
+
del self.app.active_jobs[job_id]
|
|
334
|
+
self.remove()
|
|
335
|
+
|
|
336
|
+
def compose(self) -> ComposeResult:
|
|
337
|
+
with Container(classes="job-panel"):
|
|
338
|
+
yield from self._compose_status_column()
|
|
339
|
+
yield from self._compose_errors_column()
|
|
340
|
+
yield from self._compose_mcp_message()
|
|
341
|
+
|
|
342
|
+
def _compose_mcp_message(self) -> ComposeResult:
|
|
343
|
+
mcp_message = self.job_data.get("message", "Processing...")
|
|
344
|
+
yield Label(f"💬 {mcp_message}", classes="mcp-message")
|
|
345
|
+
|
|
346
|
+
def _compose_status_column(self) -> ComposeResult:
|
|
347
|
+
with Container(classes="job-status"):
|
|
348
|
+
yield from self._compose_job_identifiers()
|
|
349
|
+
yield from self._compose_progress_info()
|
|
350
|
+
yield from self._compose_stage_and_status()
|
|
351
|
+
yield from self._compose_agent_info()
|
|
352
|
+
yield from self._compose_warning_messages()
|
|
353
|
+
|
|
354
|
+
def _compose_job_identifiers(self) -> ComposeResult:
|
|
355
|
+
job_id = self.job_data.get(
|
|
356
|
+
"full_job_id",
|
|
357
|
+
self.job_data.get("job_id", "Unknown"),
|
|
358
|
+
)
|
|
359
|
+
yield Label(f"🆔 UUID: {job_id}")
|
|
360
|
+
|
|
361
|
+
def _compose_progress_info(self) -> ComposeResult:
|
|
362
|
+
progress_stage = f"{self.iteration_count} / {self.max_iterations}"
|
|
363
|
+
yield Label(f"📊 Progress: {progress_stage}")
|
|
364
|
+
yield ProgressBar(
|
|
365
|
+
total=100,
|
|
366
|
+
show_eta=False,
|
|
367
|
+
show_percentage=False,
|
|
368
|
+
id=f"job-progress-{self.job_data.get('job_id', 'unknown')}",
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
def _compose_stage_and_status(self) -> ComposeResult:
|
|
372
|
+
yield Label(f"🎯 Stage: {self.job_data.get('stage', 'Unknown')}")
|
|
373
|
+
yield Label(f"📝 Status: {self.job_data.get('status', 'Unknown')}")
|
|
374
|
+
|
|
375
|
+
def _compose_agent_info(self) -> ComposeResult:
|
|
376
|
+
agent_summary = self.job_data.get("agent_summary", {})
|
|
377
|
+
if not agent_summary:
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
active_count = agent_summary.get("active_count", 0)
|
|
381
|
+
cached_fixes = agent_summary.get("cached_fixes", 0)
|
|
382
|
+
|
|
383
|
+
if active_count > 0 or cached_fixes > 0:
|
|
384
|
+
agent_text = f"🤖 Agents: {active_count} active"
|
|
385
|
+
if cached_fixes > 0:
|
|
386
|
+
agent_text += f", {cached_fixes} cached"
|
|
387
|
+
|
|
388
|
+
agents_data = agent_summary.get("agents", [])
|
|
389
|
+
if agents_data:
|
|
390
|
+
avg_confidence = sum(
|
|
391
|
+
agent.get("confidence", 0) for agent in agents_data
|
|
392
|
+
) / max(len(agents_data), 1)
|
|
393
|
+
if avg_confidence > 0:
|
|
394
|
+
agent_text += f", {avg_confidence: .0 % } conf"
|
|
395
|
+
yield Label(agent_text)
|
|
396
|
+
|
|
397
|
+
def _compose_warning_messages(self) -> ComposeResult:
|
|
398
|
+
if self.iteration_count >= 10:
|
|
399
|
+
yield Label("⚠️ Max iterations reached")
|
|
400
|
+
|
|
401
|
+
def _compose_errors_column(self) -> ComposeResult:
|
|
402
|
+
with Container(classes="job-errors"):
|
|
403
|
+
yield DataTable(
|
|
404
|
+
id=f"job-errors-{self.job_data.get('job_id', 'unknown')}",
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class CrackerjackDashboard(App):
|
|
409
|
+
ENABLE_COMMAND_PALETTE = False
|
|
410
|
+
CSS_PATH = Path(__file__).parent / "progress_monitor.tcss"
|
|
411
|
+
|
|
412
|
+
BINDINGS = [
|
|
413
|
+
("q", "quit", "Quit"),
|
|
414
|
+
]
|
|
415
|
+
|
|
416
|
+
def __init__(self) -> None:
|
|
417
|
+
super().__init__()
|
|
418
|
+
self.progress_dir = Path(tempfile.gettempdir()) / "crackerjack-mcp-progress"
|
|
419
|
+
self.websocket_url = "ws://localhost:8675"
|
|
420
|
+
self.refresh_timer = None
|
|
421
|
+
self.active_jobs = {}
|
|
422
|
+
self.completed_jobs_stats = {}
|
|
423
|
+
self.current_polling_method = "File"
|
|
424
|
+
|
|
425
|
+
self.job_collector = JobDataCollector(self.progress_dir, self.websocket_url)
|
|
426
|
+
self.service_checker = ServiceHealthChecker()
|
|
427
|
+
self.error_collector = ErrorCollector()
|
|
428
|
+
self.service_manager = ServiceManager()
|
|
429
|
+
self.terminal_restorer = TerminalRestorer()
|
|
430
|
+
|
|
431
|
+
def compose(self) -> ComposeResult:
|
|
432
|
+
with Container(id="main-container"):
|
|
433
|
+
yield from self._compose_top_section()
|
|
434
|
+
yield from self._compose_discovery_section()
|
|
435
|
+
yield Footer()
|
|
436
|
+
|
|
437
|
+
def _compose_top_section(self) -> ComposeResult:
|
|
438
|
+
with Container(id="top-section"):
|
|
439
|
+
yield from self._compose_left_column()
|
|
440
|
+
yield from self._compose_right_column()
|
|
441
|
+
|
|
442
|
+
def _compose_left_column(self) -> ComposeResult:
|
|
443
|
+
with Container(id="left-column"):
|
|
444
|
+
yield from self._compose_jobs_panel()
|
|
445
|
+
yield AgentStatusPanel(id="agent-status-panel")
|
|
446
|
+
|
|
447
|
+
def _compose_right_column(self) -> ComposeResult:
|
|
448
|
+
with Container(id="right-column"):
|
|
449
|
+
yield from self._compose_errors_panel()
|
|
450
|
+
yield from self._compose_services_panel()
|
|
451
|
+
|
|
452
|
+
def _compose_jobs_panel(self) -> ComposeResult:
|
|
453
|
+
with Container(id="jobs-panel"):
|
|
454
|
+
yield DataTable(
|
|
455
|
+
id="jobs-table",
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
def _compose_errors_panel(self) -> ComposeResult:
|
|
459
|
+
with Container(id="errors-panel"):
|
|
460
|
+
yield DataTable(
|
|
461
|
+
id="errors-table",
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
def _compose_services_panel(self) -> ComposeResult:
|
|
465
|
+
with Container(id="services-panel"):
|
|
466
|
+
yield DataTable(
|
|
467
|
+
id="services-table",
|
|
468
|
+
zebra_stripes=True,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
def _compose_discovery_section(self) -> ComposeResult:
|
|
472
|
+
with Container(id="discovery-section"):
|
|
473
|
+
yield Container(id="job-discovery-container")
|
|
474
|
+
|
|
475
|
+
def on_mount(self) -> None:
|
|
476
|
+
self._setup_border_titles()
|
|
477
|
+
self._setup_datatables()
|
|
478
|
+
|
|
479
|
+
asyncio.create_task(self._ensure_services_running())
|
|
480
|
+
self._start_refresh_timer()
|
|
481
|
+
|
|
482
|
+
atexit.register(self._restore_terminal_fallback)
|
|
483
|
+
|
|
484
|
+
signal.signal(signal.SIGINT, self._signal_handler)
|
|
485
|
+
signal.signal(signal.SIGTERM, self._signal_handler)
|
|
486
|
+
|
|
487
|
+
def on_unmount(self) -> None:
|
|
488
|
+
with suppress(Exception):
|
|
489
|
+
self._cleanup_started_services()
|
|
490
|
+
|
|
491
|
+
def _setup_border_titles(self) -> None:
|
|
492
|
+
self.query_one("#top-section").border_title = "🚀 Crackerjack Dashboard"
|
|
493
|
+
self.query_one("#jobs-panel").border_title = "📊 Issue Metrics"
|
|
494
|
+
self.query_one("#errors-panel").border_title = "❌ Error Tracking"
|
|
495
|
+
self.query_one("#services-panel").border_title = "🔧 Service Health"
|
|
496
|
+
self.query_one("#discovery-section").border_title = "🔍 Active Jobs"
|
|
497
|
+
|
|
498
|
+
def _setup_datatables(self) -> None:
|
|
499
|
+
jobs_table = self.query_one("#jobs-table", DataTable)
|
|
500
|
+
jobs_table.add_columns("", "", "", "")
|
|
501
|
+
|
|
502
|
+
services_table = self.query_one("#services-table", DataTable)
|
|
503
|
+
services_table.add_columns("Service", "Status", "Restarts")
|
|
504
|
+
|
|
505
|
+
errors_table = self.query_one("#errors-table", DataTable)
|
|
506
|
+
errors_table.add_columns("", "", "", "")
|
|
507
|
+
|
|
508
|
+
self._show_default_values()
|
|
509
|
+
|
|
510
|
+
asyncio.create_task(self._refresh_data())
|
|
511
|
+
|
|
512
|
+
def _show_default_values(self) -> None:
|
|
513
|
+
default_jobs_data = {
|
|
514
|
+
"active": 0,
|
|
515
|
+
"completed": 0,
|
|
516
|
+
"failed": 0,
|
|
517
|
+
"total": 0,
|
|
518
|
+
"individual_jobs": [],
|
|
519
|
+
}
|
|
520
|
+
self._update_jobs_table(default_jobs_data)
|
|
521
|
+
|
|
522
|
+
self._update_errors_table([])
|
|
523
|
+
|
|
524
|
+
async def _ensure_services_running(self) -> None:
|
|
525
|
+
await self.service_manager.ensure_services_running()
|
|
526
|
+
|
|
527
|
+
def _start_refresh_timer(self) -> None:
|
|
528
|
+
self.refresh_timer = self.set_interval(0.5, self._refresh_data)
|
|
529
|
+
|
|
530
|
+
async def _refresh_data(self) -> None:
|
|
531
|
+
with suppress(Exception):
|
|
532
|
+
if hasattr(self, "_refresh_counter"):
|
|
533
|
+
self._refresh_counter += 1
|
|
534
|
+
else:
|
|
535
|
+
self._refresh_counter = 0
|
|
536
|
+
|
|
537
|
+
if self._refresh_counter % 10 == 0:
|
|
538
|
+
await self._ensure_services_running()
|
|
539
|
+
|
|
540
|
+
jobs_data = await self._discover_jobs()
|
|
541
|
+
|
|
542
|
+
services_data = await self._collect_services_data()
|
|
543
|
+
|
|
544
|
+
errors_data = await self._collect_recent_errors()
|
|
545
|
+
|
|
546
|
+
if jobs_data["individual_jobs"]:
|
|
547
|
+
jobs_data["individual_jobs"][0].get("project", "crackerjack")
|
|
548
|
+
|
|
549
|
+
self.query_one("#services-panel").border_title = "🔧 Services"
|
|
550
|
+
|
|
551
|
+
self._update_jobs_table(jobs_data)
|
|
552
|
+
self._update_services_table(services_data)
|
|
553
|
+
self._update_errors_table(errors_data)
|
|
554
|
+
self._update_job_panels(jobs_data)
|
|
555
|
+
self._update_agent_panel(jobs_data)
|
|
556
|
+
self._update_status_bars(jobs_data)
|
|
557
|
+
|
|
558
|
+
async def _discover_jobs(self) -> dict:
|
|
559
|
+
result = await self.job_collector.discover_jobs()
|
|
560
|
+
self.current_polling_method = result["method"]
|
|
561
|
+
return result["data"]
|
|
562
|
+
|
|
563
|
+
async def _collect_services_data(self) -> list:
|
|
564
|
+
return await self.service_checker.collect_services_data()
|
|
565
|
+
|
|
566
|
+
async def _collect_recent_errors(self) -> list:
|
|
567
|
+
return await self.error_collector.collect_recent_errors()
|
|
568
|
+
|
|
569
|
+
def _update_jobs_table(self, jobs_data: dict) -> None:
|
|
570
|
+
with suppress(Exception):
|
|
571
|
+
jobs_table = self.query_one("#jobs-table", DataTable)
|
|
572
|
+
jobs_table.clear()
|
|
573
|
+
|
|
574
|
+
total_issues = jobs_data.get("total_issues", 0)
|
|
575
|
+
errors_fixed = jobs_data.get("errors_fixed", 0)
|
|
576
|
+
errors_failed = jobs_data.get("errors_failed", 0)
|
|
577
|
+
jobs_data.get("current_errors", 0)
|
|
578
|
+
|
|
579
|
+
for job_stats in self.completed_jobs_stats.values():
|
|
580
|
+
total_issues += job_stats.get("total_issues", 0)
|
|
581
|
+
errors_fixed += job_stats.get("errors_fixed", 0)
|
|
582
|
+
errors_failed += job_stats.get("errors_failed", 0)
|
|
583
|
+
|
|
584
|
+
remaining_errors = max(0, total_issues - errors_fixed)
|
|
585
|
+
|
|
586
|
+
discovered_label = "🔍 Found"
|
|
587
|
+
discovered_value = f"{total_issues: > 12}"
|
|
588
|
+
resolved_label = "✅ Fixed"
|
|
589
|
+
resolved_value = f"{errors_fixed: > 12}"
|
|
590
|
+
|
|
591
|
+
remaining_label = "❌ Left"
|
|
592
|
+
remaining_value = f"{remaining_errors: > 12}"
|
|
593
|
+
progress_label = "📈 Done"
|
|
594
|
+
progress_pct = (
|
|
595
|
+
int(errors_fixed / total_issues * 100) if total_issues > 0 else 0
|
|
596
|
+
)
|
|
597
|
+
progress_value = f"{progress_pct} % ".rjust(12)
|
|
598
|
+
|
|
599
|
+
jobs_table.add_rows(
|
|
600
|
+
[
|
|
601
|
+
(
|
|
602
|
+
discovered_label,
|
|
603
|
+
discovered_value,
|
|
604
|
+
resolved_label,
|
|
605
|
+
resolved_value,
|
|
606
|
+
),
|
|
607
|
+
(remaining_label, remaining_value, progress_label, progress_value),
|
|
608
|
+
],
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
def _update_services_table(self, services_data: list) -> None:
|
|
612
|
+
with suppress(Exception):
|
|
613
|
+
services_table = self.query_one("#services-table", DataTable)
|
|
614
|
+
services_table.clear()
|
|
615
|
+
|
|
616
|
+
for service in services_data:
|
|
617
|
+
service_name = (
|
|
618
|
+
service[0]
|
|
619
|
+
.replace("WebSocket Server", "WebSocket")
|
|
620
|
+
.replace(" Server", "")
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
status_text = service[1] or "❓ Unknown"
|
|
624
|
+
restart_count = service[2] if len(service) > 2 else "0"
|
|
625
|
+
restart_value = f"{restart_count: ^ 8}"
|
|
626
|
+
services_table.add_row(service_name, status_text, restart_value)
|
|
627
|
+
|
|
628
|
+
method_emoji = "🌐" if self.current_polling_method == "WebSocket" else "📁"
|
|
629
|
+
polling_status = f"{method_emoji} {self.current_polling_method}"
|
|
630
|
+
|
|
631
|
+
if self.current_polling_method == "WebSocket":
|
|
632
|
+
polling_status += " 🟢"
|
|
633
|
+
services_table.add_row("Polling", polling_status, "")
|
|
634
|
+
|
|
635
|
+
def _update_errors_table(self, errors_data: list) -> None:
|
|
636
|
+
with suppress(Exception):
|
|
637
|
+
errors_table = self.query_one("#errors-table", DataTable)
|
|
638
|
+
errors_table.clear()
|
|
639
|
+
|
|
640
|
+
job_errors = (
|
|
641
|
+
[
|
|
642
|
+
e
|
|
643
|
+
for e in errors_data
|
|
644
|
+
if "crackerjack" in str(e).lower() and "job" in str(e).lower()
|
|
645
|
+
]
|
|
646
|
+
if errors_data
|
|
647
|
+
else []
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
active_errors = 0
|
|
651
|
+
fixed_errors = 0
|
|
652
|
+
total_errors = 0
|
|
653
|
+
|
|
654
|
+
if job_errors:
|
|
655
|
+
total_errors = len(job_errors)
|
|
656
|
+
active_errors = sum(
|
|
657
|
+
1
|
|
658
|
+
for e in job_errors
|
|
659
|
+
if "running" in str(e).lower() or "active" in str(e).lower()
|
|
660
|
+
)
|
|
661
|
+
sum(
|
|
662
|
+
1
|
|
663
|
+
for e in job_errors
|
|
664
|
+
if "failed" in str(e).lower() or "error" in str(e).lower()
|
|
665
|
+
)
|
|
666
|
+
fixed_errors = sum(
|
|
667
|
+
1
|
|
668
|
+
for e in job_errors
|
|
669
|
+
if "fixed" in str(e).lower() or "resolved" in str(e).lower()
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
discovered_label = "🔍 Found"
|
|
673
|
+
discovered_value = f"{total_errors: > 12}"
|
|
674
|
+
resolved_label = "✅ Fixed"
|
|
675
|
+
resolved_value = f"{fixed_errors: > 12}"
|
|
676
|
+
|
|
677
|
+
remaining_label = "❌ Left"
|
|
678
|
+
remaining_value = f"{active_errors: > 12}"
|
|
679
|
+
progress_label = "📈 Done"
|
|
680
|
+
progress_pct = (
|
|
681
|
+
int(fixed_errors / total_errors * 100) if total_errors > 0 else 0
|
|
682
|
+
)
|
|
683
|
+
progress_value = f"{progress_pct} % ".rjust(12)
|
|
684
|
+
|
|
685
|
+
errors_table.add_rows(
|
|
686
|
+
[
|
|
687
|
+
(
|
|
688
|
+
discovered_label,
|
|
689
|
+
discovered_value,
|
|
690
|
+
resolved_label,
|
|
691
|
+
resolved_value,
|
|
692
|
+
),
|
|
693
|
+
(remaining_label, remaining_value, progress_label, progress_value),
|
|
694
|
+
],
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
def _update_agent_panel(self, jobs_data: dict) -> None:
|
|
698
|
+
with suppress(Exception):
|
|
699
|
+
agent_panel = self.query_one("#agent-status-panel", AgentStatusPanel)
|
|
700
|
+
|
|
701
|
+
agent_data = {}
|
|
702
|
+
for job in jobs_data.get("individual_jobs", []):
|
|
703
|
+
if "agent_activity" in job or "agent_performance" in job:
|
|
704
|
+
agent_data = job
|
|
705
|
+
break
|
|
706
|
+
|
|
707
|
+
if agent_data:
|
|
708
|
+
agent_panel.update_agent_data(agent_data)
|
|
709
|
+
|
|
710
|
+
def _update_job_panels(self, jobs_data: dict) -> None:
|
|
711
|
+
with suppress(Exception):
|
|
712
|
+
container = self.query_one("#job-discovery-container")
|
|
713
|
+
current_job_ids = self._get_current_job_ids(jobs_data)
|
|
714
|
+
|
|
715
|
+
self._remove_obsolete_panels(current_job_ids)
|
|
716
|
+
self._update_or_create_panels(jobs_data, container)
|
|
717
|
+
self._handle_placeholder_visibility(container)
|
|
718
|
+
|
|
719
|
+
def _get_current_job_ids(self, jobs_data: dict) -> set:
|
|
720
|
+
return (
|
|
721
|
+
{job["job_id"] for job in jobs_data["individual_jobs"]}
|
|
722
|
+
if jobs_data["individual_jobs"]
|
|
723
|
+
else set()
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
def _remove_obsolete_panels(self, current_job_ids: set) -> None:
|
|
727
|
+
jobs_to_remove = []
|
|
728
|
+
for job_id, panel in self.active_jobs.items():
|
|
729
|
+
panel_status = panel.job_data.get("status", "").lower()
|
|
730
|
+
if (
|
|
731
|
+
job_id not in current_job_ids
|
|
732
|
+
and panel_status not in ("completed", "failed")
|
|
733
|
+
and panel.completion_time is None
|
|
734
|
+
):
|
|
735
|
+
jobs_to_remove.append(job_id)
|
|
736
|
+
|
|
737
|
+
for job_id in jobs_to_remove:
|
|
738
|
+
panel = self.active_jobs.pop(job_id)
|
|
739
|
+
panel.remove()
|
|
740
|
+
|
|
741
|
+
def _update_or_create_panels(self, jobs_data: dict, container) -> None:
|
|
742
|
+
if not jobs_data["individual_jobs"]:
|
|
743
|
+
return
|
|
744
|
+
|
|
745
|
+
for job in jobs_data["individual_jobs"]:
|
|
746
|
+
job_id = job["job_id"]
|
|
747
|
+
if job_id in self.active_jobs:
|
|
748
|
+
self._update_existing_panel(job)
|
|
749
|
+
else:
|
|
750
|
+
self._create_new_panel(job, container)
|
|
751
|
+
|
|
752
|
+
def _update_existing_panel(self, job: dict) -> None:
|
|
753
|
+
existing_panel = self.active_jobs[job["job_id"]]
|
|
754
|
+
existing_panel.job_data = job
|
|
755
|
+
existing_panel.iteration_count = job.get("iteration", 0)
|
|
756
|
+
|
|
757
|
+
self._update_panel_title(existing_panel, job)
|
|
758
|
+
existing_panel._update_errors_table()
|
|
759
|
+
existing_panel._update_progress_bar()
|
|
760
|
+
self._handle_job_completion(existing_panel, job)
|
|
761
|
+
self._update_panel_border(existing_panel)
|
|
762
|
+
|
|
763
|
+
def _update_panel_title(self, panel, job: dict) -> None:
|
|
764
|
+
project_name = job.get("project", "crackerjack")
|
|
765
|
+
status = job.get("status", "").lower()
|
|
766
|
+
|
|
767
|
+
panel.border_title = f"📁 {project_name}"
|
|
768
|
+
panel.border_title_align = "left"
|
|
769
|
+
|
|
770
|
+
if status == "running":
|
|
771
|
+
panel.border_subtitle = "💓"
|
|
772
|
+
panel.border_subtitle_align = "right"
|
|
773
|
+
else:
|
|
774
|
+
panel.border_subtitle = ""
|
|
775
|
+
|
|
776
|
+
def _handle_job_completion(self, panel, job: dict) -> None:
|
|
777
|
+
job_status = job.get("status", "").lower()
|
|
778
|
+
if job_status in ("completed", "failed") and panel.completion_time is None:
|
|
779
|
+
panel.completion_time = time.time()
|
|
780
|
+
panel.fade_timer = panel.set_timer(300.0, panel._start_fade)
|
|
781
|
+
panel.remove_timer = panel.set_timer(1200.0, panel._remove_panel)
|
|
782
|
+
|
|
783
|
+
def _update_panel_border(self, panel) -> None:
|
|
784
|
+
new_border = panel._calculate_border_style()
|
|
785
|
+
if new_border != panel.border_style:
|
|
786
|
+
panel.border_style = new_border
|
|
787
|
+
panel.refresh()
|
|
788
|
+
|
|
789
|
+
def _create_new_panel(self, job: dict, container) -> None:
|
|
790
|
+
job_panel = JobPanel(job)
|
|
791
|
+
self.active_jobs[job["job_id"]] = job_panel
|
|
792
|
+
container.mount(job_panel)
|
|
793
|
+
|
|
794
|
+
def _handle_placeholder_visibility(self, container) -> None:
|
|
795
|
+
has_placeholder = bool(container.query("#no-jobs-label"))
|
|
796
|
+
|
|
797
|
+
if not self.active_jobs and not has_placeholder:
|
|
798
|
+
container.mount(
|
|
799
|
+
Label(
|
|
800
|
+
"No active jobs detected. Start a Crackerjack job to see progress here.",
|
|
801
|
+
id="no-jobs-label",
|
|
802
|
+
),
|
|
803
|
+
)
|
|
804
|
+
elif self.active_jobs and has_placeholder:
|
|
805
|
+
container.query("#no-jobs-label").remove()
|
|
806
|
+
|
|
807
|
+
def _update_status_bars(self, jobs_data: dict) -> None:
|
|
808
|
+
pass
|
|
809
|
+
|
|
810
|
+
def action_refresh(self) -> None:
|
|
811
|
+
asyncio.create_task(self._refresh_data())
|
|
812
|
+
|
|
813
|
+
def action_clear(self) -> None:
|
|
814
|
+
with suppress(Exception):
|
|
815
|
+
for table_id in ("#jobs-table", "#services-table", "#errors-table"):
|
|
816
|
+
table = self.query_one(table_id, DataTable)
|
|
817
|
+
table.clear()
|
|
818
|
+
|
|
819
|
+
container = self.query_one("#job-discovery-container")
|
|
820
|
+
container.query("JobPanel").remove()
|
|
821
|
+
container.query("Label").remove()
|
|
822
|
+
self.active_jobs.clear()
|
|
823
|
+
|
|
824
|
+
def action_quit(self) -> None:
|
|
825
|
+
with suppress(Exception):
|
|
826
|
+
if self.refresh_timer:
|
|
827
|
+
self.refresh_timer.stop()
|
|
828
|
+
self._cleanup_started_services()
|
|
829
|
+
self._restore_terminal()
|
|
830
|
+
self.exit()
|
|
831
|
+
|
|
832
|
+
def _restore_terminal(self) -> None:
|
|
833
|
+
self.terminal_restorer.restore_terminal()
|
|
834
|
+
|
|
835
|
+
def _restore_terminal_fallback(self) -> None:
|
|
836
|
+
self.terminal_restorer.restore_terminal()
|
|
837
|
+
|
|
838
|
+
def _signal_handler(self, _signum, _frame) -> None:
|
|
839
|
+
with suppress(Exception):
|
|
840
|
+
self._restore_terminal()
|
|
841
|
+
self._cleanup_started_services()
|
|
842
|
+
sys.exit(0)
|
|
843
|
+
|
|
844
|
+
def _cleanup_started_services(self) -> None:
|
|
845
|
+
self.service_manager.cleanup_services()
|
|
846
|
+
|
|
847
|
+
def _format_time_metric(self, seconds: float) -> str:
|
|
848
|
+
if seconds < 60:
|
|
849
|
+
return f"{seconds: .0f}s"
|
|
850
|
+
if seconds < 3600:
|
|
851
|
+
return f"{seconds / 60: .0f}m {seconds % 60: .0f}s"
|
|
852
|
+
return f"{seconds / 3600: .0f}h {(seconds % 3600) / 60: .0f}m"
|
|
853
|
+
|
|
854
|
+
def _format_metric_with_trend(self, value: int, trend: str = "") -> str:
|
|
855
|
+
formatted = f"{value: , }"
|
|
856
|
+
if trend:
|
|
857
|
+
formatted += f" {trend}"
|
|
858
|
+
return formatted
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
class JobMetrics:
|
|
862
|
+
def __init__(self, job_id: str, project_path: str = "") -> None:
|
|
863
|
+
self.job_id = job_id
|
|
864
|
+
self.project_path = project_path
|
|
865
|
+
self.project_name = Path(project_path).name if project_path else "crackerjack"
|
|
866
|
+
self.start_time = time.time()
|
|
867
|
+
self.last_update = time.time()
|
|
868
|
+
self.completion_time: float | None = None
|
|
869
|
+
|
|
870
|
+
self.iteration = 0
|
|
871
|
+
self.max_iterations = 10
|
|
872
|
+
self.current_stage = "Initializing"
|
|
873
|
+
self.status = "running"
|
|
874
|
+
self.message = ""
|
|
875
|
+
|
|
876
|
+
self.stages_completed = set()
|
|
877
|
+
self.stages_failed = set()
|
|
878
|
+
|
|
879
|
+
self.errors = []
|
|
880
|
+
self.warnings = []
|
|
881
|
+
self.hook_failures = []
|
|
882
|
+
self.test_failures = []
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
async def run_progress_monitor(
|
|
886
|
+
enable_watchdog: bool = True,
|
|
887
|
+
dev_mode: bool = False,
|
|
888
|
+
) -> None:
|
|
889
|
+
with suppress(Exception):
|
|
890
|
+
console = Console()
|
|
891
|
+
console.print(
|
|
892
|
+
"[bold green]🚀 Starting Crackerjack Progress Monitor[/bold green]",
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
if enable_watchdog:
|
|
896
|
+
console.print("[bold yellow]🐕 Service Watchdog: Enabled[/bold yellow]")
|
|
897
|
+
|
|
898
|
+
if dev_mode:
|
|
899
|
+
console.print("[bold cyan]🛠️ Development Mode: Enabled[/bold cyan]")
|
|
900
|
+
|
|
901
|
+
app = CrackerjackDashboard()
|
|
902
|
+
|
|
903
|
+
if dev_mode:
|
|
904
|
+
app.dev = True
|
|
905
|
+
|
|
906
|
+
await app.run_async()
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
async def run_crackerjack_with_progress(
|
|
910
|
+
command: str = " / crackerjack: run",
|
|
911
|
+
) -> None:
|
|
912
|
+
with suppress(Exception):
|
|
913
|
+
console = Console()
|
|
914
|
+
console.print(
|
|
915
|
+
"[bold green]🚀 Starting Crackerjack Progress Monitor[/bold green]",
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
app = CrackerjackDashboard()
|
|
919
|
+
await app.run_async()
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
def main() -> None:
|
|
923
|
+
try:
|
|
924
|
+
app = CrackerjackDashboard()
|
|
925
|
+
app.run()
|
|
926
|
+
except KeyboardInterrupt:
|
|
927
|
+
with suppress(Exception):
|
|
928
|
+
sys.stdout.write("\033[?25h\033[0m")
|
|
929
|
+
sys.stdout.flush()
|
|
930
|
+
subprocess.run(
|
|
931
|
+
["stty", "sane"],
|
|
932
|
+
check=False,
|
|
933
|
+
capture_output=True,
|
|
934
|
+
timeout=1,
|
|
935
|
+
)
|
|
936
|
+
except Exception:
|
|
937
|
+
with suppress(Exception):
|
|
938
|
+
sys.stdout.write("\033[?25h\033[0m")
|
|
939
|
+
sys.stdout.flush()
|
|
940
|
+
subprocess.run(
|
|
941
|
+
["stty", "sane"],
|
|
942
|
+
check=False,
|
|
943
|
+
capture_output=True,
|
|
944
|
+
timeout=1,
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
if __name__ == "__main__":
|
|
949
|
+
main()
|