crackerjack 0.30.3__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 -299
- 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 +618 -928
- 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 +94 -103
- 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/crackerjack.py +0 -3805
- crackerjack/pyproject.toml +0 -286
- crackerjack-0.30.3.dist-info/METADATA +0 -1290
- crackerjack-0.30.3.dist-info/RECORD +0 -16
- {crackerjack-0.30.3.dist-info → crackerjack-0.31.4.dist-info}/WHEEL +0 -0
- {crackerjack-0.30.3.dist-info → crackerjack-0.31.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from collections import deque
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
import aiohttp
|
|
8
|
+
from rich.text import Text
|
|
9
|
+
from textual import work
|
|
10
|
+
from textual.app import App, ComposeResult
|
|
11
|
+
from textual.containers import (
|
|
12
|
+
Container,
|
|
13
|
+
Horizontal,
|
|
14
|
+
)
|
|
15
|
+
from textual.reactive import reactive
|
|
16
|
+
from textual.widgets import (
|
|
17
|
+
Button,
|
|
18
|
+
DataTable,
|
|
19
|
+
Footer,
|
|
20
|
+
Header,
|
|
21
|
+
Log,
|
|
22
|
+
Static,
|
|
23
|
+
TabbedContent,
|
|
24
|
+
TabPane,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from crackerjack.services import server_manager
|
|
28
|
+
|
|
29
|
+
from .progress_components import JobDataCollector, TerminalRestorer
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from textual.timer import Timer
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MetricCard(Static):
|
|
36
|
+
DEFAULT_CSS = """
|
|
37
|
+
MetricCard {
|
|
38
|
+
width: 1fr;
|
|
39
|
+
height: 4;
|
|
40
|
+
border: solid $primary;
|
|
41
|
+
padding: 0 1;
|
|
42
|
+
margin: 0 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
MetricCard.critical {
|
|
46
|
+
border: solid $error;
|
|
47
|
+
color: $error;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
MetricCard.warning {
|
|
51
|
+
border: solid $warning;
|
|
52
|
+
color: $warning;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
MetricCard.success {
|
|
56
|
+
border: solid $success;
|
|
57
|
+
color: $success;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
MetricCard.info {
|
|
61
|
+
border: solid $info;
|
|
62
|
+
color: $info;
|
|
63
|
+
}
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
value = reactive(" -- ")
|
|
67
|
+
label = reactive("Metric")
|
|
68
|
+
trend = reactive("")
|
|
69
|
+
status = reactive("")
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
label: str,
|
|
74
|
+
value: str = " -- ",
|
|
75
|
+
trend: str = "",
|
|
76
|
+
status: str = "",
|
|
77
|
+
**kwargs,
|
|
78
|
+
) -> None:
|
|
79
|
+
super().__init__(**kwargs)
|
|
80
|
+
self.label = label
|
|
81
|
+
self.value = value
|
|
82
|
+
self.trend = trend
|
|
83
|
+
self.status = status
|
|
84
|
+
|
|
85
|
+
def render(self) -> Text:
|
|
86
|
+
text = Text()
|
|
87
|
+
text.append(f"{self.label}\n", style="dim")
|
|
88
|
+
text.append(str(self.value), style="bold")
|
|
89
|
+
if self.trend:
|
|
90
|
+
text.append(f" {self.trend}", style="green" if "↑" in self.trend else "red")
|
|
91
|
+
return text
|
|
92
|
+
|
|
93
|
+
def update_metric(self, value: str, trend: str = "", status: str = "") -> None:
|
|
94
|
+
self.value = value
|
|
95
|
+
self.trend = trend
|
|
96
|
+
self.status = status
|
|
97
|
+
|
|
98
|
+
self.remove_class("critical", "warning", "success", "info")
|
|
99
|
+
if status:
|
|
100
|
+
self.add_class(status)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class SystemOverviewWidget(Static):
|
|
104
|
+
DEFAULT_CSS = """
|
|
105
|
+
SystemOverviewWidget {
|
|
106
|
+
height: 8;
|
|
107
|
+
border: solid $primary;
|
|
108
|
+
margin: 1;
|
|
109
|
+
}
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def compose(self) -> ComposeResult:
|
|
113
|
+
with Container(id="system_overview"):
|
|
114
|
+
with Horizontal(classes="metrics_row"):
|
|
115
|
+
yield MetricCard("CPU", "0 % ", id="cpu_metric")
|
|
116
|
+
yield MetricCard("Memory", "0MB", id="memory_metric")
|
|
117
|
+
yield MetricCard("Active Jobs", "0", id="jobs_metric")
|
|
118
|
+
yield MetricCard("Queue Depth", "0", id="queue_metric")
|
|
119
|
+
|
|
120
|
+
with Horizontal(classes="status_row"):
|
|
121
|
+
yield MetricCard("MCP Server", "Stopped", id="mcp_status")
|
|
122
|
+
yield MetricCard("WebSocket", "Stopped", id="websocket_status")
|
|
123
|
+
yield MetricCard("AI Agent", "Idle", id="agent_status")
|
|
124
|
+
yield MetricCard("Last Run", "Never", id="last_run")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class JobsTableWidget(Static):
|
|
128
|
+
DEFAULT_CSS = """
|
|
129
|
+
JobsTableWidget {
|
|
130
|
+
height: 1fr;
|
|
131
|
+
border: solid $primary;
|
|
132
|
+
margin: 1;
|
|
133
|
+
}
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def compose(self) -> ComposeResult:
|
|
137
|
+
with Container():
|
|
138
|
+
yield DataTable(id="jobs_table")
|
|
139
|
+
|
|
140
|
+
def on_mount(self) -> None:
|
|
141
|
+
table = self.query_one("#jobs_table", DataTable)
|
|
142
|
+
table.add_columns(
|
|
143
|
+
"Job ID",
|
|
144
|
+
"Status",
|
|
145
|
+
"Stage",
|
|
146
|
+
"Progress",
|
|
147
|
+
"Started",
|
|
148
|
+
"Duration",
|
|
149
|
+
"Issues",
|
|
150
|
+
)
|
|
151
|
+
table.zebra_stripes = True
|
|
152
|
+
table.show_header = True
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class LogViewWidget(Static):
|
|
156
|
+
DEFAULT_CSS = """
|
|
157
|
+
LogViewWidget {
|
|
158
|
+
height: 1fr;
|
|
159
|
+
border: solid $primary;
|
|
160
|
+
margin: 1;
|
|
161
|
+
}
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
def compose(self) -> ComposeResult:
|
|
165
|
+
with Container():
|
|
166
|
+
with Horizontal(id="log_controls"):
|
|
167
|
+
yield Button("Clear", id="clear_logs")
|
|
168
|
+
yield Button("Pause", id="pause_logs")
|
|
169
|
+
yield Button("Export", id="export_logs")
|
|
170
|
+
yield Log(id="log_display")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class AIAgentWidget(Static):
|
|
174
|
+
DEFAULT_CSS = """
|
|
175
|
+
AIAgentWidget {
|
|
176
|
+
height: 12;
|
|
177
|
+
border: solid $primary;
|
|
178
|
+
margin: 1;
|
|
179
|
+
}
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
def compose(self) -> ComposeResult:
|
|
183
|
+
with Container():
|
|
184
|
+
with Horizontal(classes="agent_metrics"):
|
|
185
|
+
yield MetricCard("Active Agents", "0", id="active_agents")
|
|
186
|
+
yield MetricCard("Issues Fixed", "0", "↑", id="issues_fixed")
|
|
187
|
+
yield MetricCard("Avg Confidence", "0 % ", id="avg_confidence")
|
|
188
|
+
yield MetricCard("Cache Hits", "0", id="cache_hits")
|
|
189
|
+
|
|
190
|
+
yield DataTable(id="agents_table")
|
|
191
|
+
|
|
192
|
+
def on_mount(self) -> None:
|
|
193
|
+
table = self.query_one("#agents_table", DataTable)
|
|
194
|
+
table.add_columns("Agent", "Type", "Status", "Confidence", "Fixed", "Runtime")
|
|
195
|
+
table.zebra_stripes = True
|
|
196
|
+
table.show_header = True
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class PerformanceWidget(Static):
|
|
200
|
+
DEFAULT_CSS = """
|
|
201
|
+
PerformanceWidget {
|
|
202
|
+
height: 10;
|
|
203
|
+
border: solid $primary;
|
|
204
|
+
margin: 1;
|
|
205
|
+
}
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
def __init__(self, **kwargs) -> None:
|
|
209
|
+
super().__init__(**kwargs)
|
|
210
|
+
self.cpu_history: deque[float] = deque(maxlen=50)
|
|
211
|
+
self.memory_history: deque[float] = deque(maxlen=50)
|
|
212
|
+
self.job_history: deque[int] = deque(maxlen=50)
|
|
213
|
+
|
|
214
|
+
def compose(self) -> ComposeResult:
|
|
215
|
+
with Container():
|
|
216
|
+
with Horizontal():
|
|
217
|
+
yield MetricCard("Avg Job Time", "0s", id="avg_job_time")
|
|
218
|
+
yield MetricCard("Success Rate", "100 % ", id="success_rate")
|
|
219
|
+
yield MetricCard("Error Rate", "0 % ", id="error_rate")
|
|
220
|
+
yield MetricCard("Throughput", "0 j / h", id="throughput")
|
|
221
|
+
|
|
222
|
+
yield Static(id="performance_chart")
|
|
223
|
+
|
|
224
|
+
def update_performance_data(self, cpu: float, memory: float, jobs: int) -> None:
|
|
225
|
+
self.cpu_history.append(cpu)
|
|
226
|
+
self.memory_history.append(memory)
|
|
227
|
+
self.job_history.append(jobs)
|
|
228
|
+
|
|
229
|
+
self._render_performance_chart()
|
|
230
|
+
|
|
231
|
+
def _render_performance_chart(self) -> None:
|
|
232
|
+
if not self.cpu_history:
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
max_cpu = max(self.cpu_history) if self.cpu_history else 1
|
|
236
|
+
max_mem = max(self.memory_history) if self.memory_history else 1
|
|
237
|
+
|
|
238
|
+
cpu_line = "".join(self._get_bar_char(val, max_cpu) for val in self.cpu_history)
|
|
239
|
+
mem_line = "".join(
|
|
240
|
+
self._get_bar_char(val, max_mem) for val in self.memory_history
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
chart_text = f"CPU: {cpu_line}\nMEM: {mem_line}"
|
|
244
|
+
|
|
245
|
+
chart = self.query_one("#performance_chart", Static)
|
|
246
|
+
chart.update(chart_text)
|
|
247
|
+
|
|
248
|
+
def _get_bar_char(self, value: float, max_value: float) -> str:
|
|
249
|
+
if max_value == 0:
|
|
250
|
+
return "▁"
|
|
251
|
+
ratio = value / max_value
|
|
252
|
+
if ratio >= 0.875:
|
|
253
|
+
return "█"
|
|
254
|
+
if ratio >= 0.75:
|
|
255
|
+
return "▇"
|
|
256
|
+
if ratio >= 0.625:
|
|
257
|
+
return "▆"
|
|
258
|
+
if ratio >= 0.5:
|
|
259
|
+
return "▅"
|
|
260
|
+
if ratio >= 0.375:
|
|
261
|
+
return "▄"
|
|
262
|
+
if ratio >= 0.25:
|
|
263
|
+
return "▃"
|
|
264
|
+
if ratio >= 0.125:
|
|
265
|
+
return "▂"
|
|
266
|
+
return "▁"
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class CrackerjackDashboard(App):
|
|
270
|
+
TITLE = "Crackerjack Dashboard"
|
|
271
|
+
SUB_TITLE = "Comprehensive Project Monitoring"
|
|
272
|
+
|
|
273
|
+
CSS_PATH = None
|
|
274
|
+
|
|
275
|
+
BINDINGS = [
|
|
276
|
+
("q", "quit", "Quit"),
|
|
277
|
+
("r", "refresh", "Refresh"),
|
|
278
|
+
("c", "clear_logs", "Clear Logs"),
|
|
279
|
+
("p", "pause_logs", "Pause / Resume Logs"),
|
|
280
|
+
("s", "toggle_servers", "Start / Stop Servers"),
|
|
281
|
+
("d", "toggle_debug", "Debug Mode"),
|
|
282
|
+
]
|
|
283
|
+
|
|
284
|
+
DEFAULT_CSS = """
|
|
285
|
+
Screen {
|
|
286
|
+
layout: vertical;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
Header {
|
|
290
|
+
dock: top;
|
|
291
|
+
height: 3;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
Footer {
|
|
295
|
+
dock: bottom;
|
|
296
|
+
height: 1;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.metrics_row {
|
|
300
|
+
height: 4;
|
|
301
|
+
margin: 0 1;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.status_row {
|
|
305
|
+
height: 4;
|
|
306
|
+
margin: 0 1;
|
|
307
|
+
}
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
def __init__(self, **kwargs) -> None:
|
|
311
|
+
super().__init__(**kwargs)
|
|
312
|
+
|
|
313
|
+
self.job_collector = JobDataCollector()
|
|
314
|
+
self.terminal_restorer = TerminalRestorer()
|
|
315
|
+
|
|
316
|
+
self.is_paused = False
|
|
317
|
+
self.debug_mode = False
|
|
318
|
+
self.last_refresh = time.time()
|
|
319
|
+
|
|
320
|
+
self.jobs_data: dict[str, Any] = {}
|
|
321
|
+
self.logs_buffer: list[str] = []
|
|
322
|
+
self.performance_data = {
|
|
323
|
+
"cpu": 0.0,
|
|
324
|
+
"memory": 0.0,
|
|
325
|
+
"jobs": 0,
|
|
326
|
+
"success_rate": 100.0,
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
self.update_timer: Timer | None = None
|
|
330
|
+
|
|
331
|
+
def compose(self) -> ComposeResult:
|
|
332
|
+
yield Header()
|
|
333
|
+
|
|
334
|
+
with TabbedContent(id="main_tabs"):
|
|
335
|
+
with TabPane("Overview", id="overview_tab"):
|
|
336
|
+
yield SystemOverviewWidget(id="system_overview")
|
|
337
|
+
yield JobsTableWidget(id="jobs_widget")
|
|
338
|
+
|
|
339
|
+
with TabPane("AI Agents", id="agents_tab"):
|
|
340
|
+
yield AIAgentWidget(id="ai_agent_widget")
|
|
341
|
+
|
|
342
|
+
with TabPane("Performance", id="performance_tab"):
|
|
343
|
+
yield PerformanceWidget(id="performance_widget")
|
|
344
|
+
|
|
345
|
+
with TabPane("Logs", id="logs_tab"):
|
|
346
|
+
yield LogViewWidget(id="log_widget")
|
|
347
|
+
|
|
348
|
+
yield Footer()
|
|
349
|
+
|
|
350
|
+
def on_mount(self) -> None:
|
|
351
|
+
self.log("Crackerjack Dashboard starting...")
|
|
352
|
+
|
|
353
|
+
self.update_timer = self.set_interval(2.0, self.update_dashboard)
|
|
354
|
+
|
|
355
|
+
self.call_later(self.initial_setup)
|
|
356
|
+
|
|
357
|
+
async def initial_setup(self) -> None:
|
|
358
|
+
try:
|
|
359
|
+
await self._check_server_status()
|
|
360
|
+
|
|
361
|
+
await self._load_jobs_data()
|
|
362
|
+
|
|
363
|
+
self.call_later(self.update_dashboard)
|
|
364
|
+
|
|
365
|
+
self.log("Dashboard initialized successfully")
|
|
366
|
+
|
|
367
|
+
except Exception as e:
|
|
368
|
+
self.log(f"Error during initial setup: {e}")
|
|
369
|
+
|
|
370
|
+
@work(exclusive=True)
|
|
371
|
+
async def update_dashboard(self) -> None:
|
|
372
|
+
if self.is_paused:
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
try:
|
|
376
|
+
current_time = time.time()
|
|
377
|
+
|
|
378
|
+
await self._update_system_metrics()
|
|
379
|
+
|
|
380
|
+
await self._update_jobs_data()
|
|
381
|
+
|
|
382
|
+
await self._update_agent_status()
|
|
383
|
+
|
|
384
|
+
await self._update_performance_metrics()
|
|
385
|
+
|
|
386
|
+
await self._update_logs()
|
|
387
|
+
|
|
388
|
+
self.last_refresh = current_time
|
|
389
|
+
|
|
390
|
+
except Exception as e:
|
|
391
|
+
self.log(f"Error updating dashboard: {e}")
|
|
392
|
+
|
|
393
|
+
async def _check_server_status(self) -> None:
|
|
394
|
+
try:
|
|
395
|
+
mcp_processes = server_manager.find_mcp_server_processes()
|
|
396
|
+
mcp_running = len(mcp_processes) > 0
|
|
397
|
+
mcp_metric = self.query_one("#mcp_status", MetricCard)
|
|
398
|
+
mcp_metric.update_metric(
|
|
399
|
+
"Running" if mcp_running else "Stopped",
|
|
400
|
+
status="success" if mcp_running else "critical",
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
ws_running = await self._check_websocket_server()
|
|
404
|
+
ws_metric = self.query_one("#websocket_status", MetricCard)
|
|
405
|
+
ws_metric.update_metric(
|
|
406
|
+
"Running" if ws_running else "Stopped",
|
|
407
|
+
status="success" if ws_running else "critical",
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
except Exception as e:
|
|
411
|
+
self.log(f"Error checking server status: {e}")
|
|
412
|
+
|
|
413
|
+
async def _check_websocket_server(self) -> bool:
|
|
414
|
+
try:
|
|
415
|
+
timeout = aiohttp.ClientTimeout(total=5.0)
|
|
416
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
417
|
+
async with session.get("http://localhost:8675/") as response:
|
|
418
|
+
return response.status == 200
|
|
419
|
+
except Exception:
|
|
420
|
+
return False
|
|
421
|
+
|
|
422
|
+
async def _update_system_metrics(self) -> None:
|
|
423
|
+
try:
|
|
424
|
+
import psutil
|
|
425
|
+
|
|
426
|
+
cpu_percent = psutil.cpu_percent(interval=None)
|
|
427
|
+
memory = psutil.virtual_memory()
|
|
428
|
+
memory_mb = memory.used // (1024 * 1024)
|
|
429
|
+
|
|
430
|
+
cpu_metric = self.query_one("#cpu_metric", MetricCard)
|
|
431
|
+
memory_metric = self.query_one("#memory_metric", MetricCard)
|
|
432
|
+
|
|
433
|
+
cpu_metric.update_metric(f"{cpu_percent: .1f} % ")
|
|
434
|
+
memory_metric.update_metric(f"{memory_mb}MB")
|
|
435
|
+
|
|
436
|
+
self.performance_data["cpu"] = cpu_percent
|
|
437
|
+
self.performance_data["memory"] = memory_mb
|
|
438
|
+
|
|
439
|
+
except ImportError:
|
|
440
|
+
pass
|
|
441
|
+
except Exception as e:
|
|
442
|
+
self.log(f"Error updating system metrics: {e}")
|
|
443
|
+
|
|
444
|
+
async def _load_jobs_data(self) -> None:
|
|
445
|
+
try:
|
|
446
|
+
if await self._check_websocket_server():
|
|
447
|
+
jobs_data = await self._fetch_jobs_from_websocket()
|
|
448
|
+
if jobs_data:
|
|
449
|
+
self.jobs_data.update(jobs_data)
|
|
450
|
+
|
|
451
|
+
file_jobs = await self._collect_jobs_from_filesystem()
|
|
452
|
+
if file_jobs:
|
|
453
|
+
self.jobs_data.update(file_jobs)
|
|
454
|
+
|
|
455
|
+
except Exception as e:
|
|
456
|
+
self.log(f"Error loading jobs data: {e}")
|
|
457
|
+
|
|
458
|
+
async def _fetch_jobs_from_websocket(self) -> dict[str, Any]:
|
|
459
|
+
try:
|
|
460
|
+
timeout = aiohttp.ClientTimeout(total=10.0)
|
|
461
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
462
|
+
async with session.get("http://localhost:8675/api/jobs") as response:
|
|
463
|
+
if response.status == 200:
|
|
464
|
+
return await response.json()
|
|
465
|
+
return {}
|
|
466
|
+
except Exception as e:
|
|
467
|
+
self.log(f"Error fetching WebSocket jobs: {e}")
|
|
468
|
+
return {}
|
|
469
|
+
|
|
470
|
+
async def _collect_jobs_from_filesystem(self) -> dict[str, Any]:
|
|
471
|
+
try:
|
|
472
|
+
jobs = {}
|
|
473
|
+
|
|
474
|
+
import tempfile
|
|
475
|
+
|
|
476
|
+
temp_dir = Path(tempfile.gettempdir())
|
|
477
|
+
debug_files = temp_dir.glob("crackerjack-debug-*.log")
|
|
478
|
+
for debug_file in debug_files:
|
|
479
|
+
try:
|
|
480
|
+
if debug_file.stat().st_mtime > (time.time() - 3600):
|
|
481
|
+
debug_file.read_text()
|
|
482
|
+
job_id = debug_file.stem.replace("crackerjack-debug-", "")
|
|
483
|
+
jobs[job_id] = {
|
|
484
|
+
"id": job_id,
|
|
485
|
+
"status": "completed",
|
|
486
|
+
"log_file": str(debug_file),
|
|
487
|
+
"timestamp": debug_file.stat().st_mtime,
|
|
488
|
+
}
|
|
489
|
+
except Exception:
|
|
490
|
+
continue
|
|
491
|
+
|
|
492
|
+
return jobs
|
|
493
|
+
|
|
494
|
+
except Exception as e:
|
|
495
|
+
self.log(f"Error collecting filesystem jobs: {e}")
|
|
496
|
+
return {}
|
|
497
|
+
|
|
498
|
+
async def _update_jobs_data(self) -> None:
|
|
499
|
+
try:
|
|
500
|
+
table = self.query_one("#jobs_table", DataTable)
|
|
501
|
+
table.clear()
|
|
502
|
+
|
|
503
|
+
active_count = len(
|
|
504
|
+
[j for j in self.jobs_data.values() if j.get("status") != "completed"],
|
|
505
|
+
)
|
|
506
|
+
jobs_metric = self.query_one("#jobs_metric", MetricCard)
|
|
507
|
+
jobs_metric.update_metric(str(active_count))
|
|
508
|
+
|
|
509
|
+
for job_id, job_data in sorted(
|
|
510
|
+
self.jobs_data.items(),
|
|
511
|
+
key=lambda x: x[1].get("timestamp", 0),
|
|
512
|
+
reverse=True,
|
|
513
|
+
):
|
|
514
|
+
status = job_data.get("status", "unknown")
|
|
515
|
+
stage = job_data.get("current_stage", "N/A")
|
|
516
|
+
progress = job_data.get("progress", 0)
|
|
517
|
+
started = datetime.fromtimestamp(job_data.get("timestamp", 0)).strftime(
|
|
518
|
+
"%H:%M:%S",
|
|
519
|
+
)
|
|
520
|
+
duration = self._format_duration(job_data.get("duration", 0))
|
|
521
|
+
issues = job_data.get("issues_found", 0)
|
|
522
|
+
|
|
523
|
+
table.add_row(
|
|
524
|
+
job_id[:8],
|
|
525
|
+
status.title(),
|
|
526
|
+
stage,
|
|
527
|
+
f"{progress} % ",
|
|
528
|
+
started,
|
|
529
|
+
duration,
|
|
530
|
+
str(issues),
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
except Exception as e:
|
|
534
|
+
self.log(f"Error updating jobs data: {e}")
|
|
535
|
+
|
|
536
|
+
async def _update_agent_status(self) -> None:
|
|
537
|
+
try:
|
|
538
|
+
agents_table = self.query_one("#agents_table", DataTable)
|
|
539
|
+
agents_table.clear()
|
|
540
|
+
|
|
541
|
+
active_agents_metric = self.query_one("#active_agents", MetricCard)
|
|
542
|
+
issues_fixed_metric = self.query_one("#issues_fixed", MetricCard)
|
|
543
|
+
avg_confidence_metric = self.query_one("#avg_confidence", MetricCard)
|
|
544
|
+
cache_hits_metric = self.query_one("#cache_hits", MetricCard)
|
|
545
|
+
|
|
546
|
+
active_agents_metric.update_metric("0")
|
|
547
|
+
issues_fixed_metric.update_metric("0")
|
|
548
|
+
avg_confidence_metric.update_metric("0 % ")
|
|
549
|
+
cache_hits_metric.update_metric("0")
|
|
550
|
+
|
|
551
|
+
except Exception as e:
|
|
552
|
+
self.log(f"Error updating agent status: {e}")
|
|
553
|
+
|
|
554
|
+
async def _update_performance_metrics(self) -> None:
|
|
555
|
+
try:
|
|
556
|
+
performance_widget = self.query_one(
|
|
557
|
+
"#performance_widget",
|
|
558
|
+
PerformanceWidget,
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
cpu = self.performance_data.get("cpu", 0)
|
|
562
|
+
memory = self.performance_data.get("memory", 0)
|
|
563
|
+
jobs = len(self.jobs_data)
|
|
564
|
+
|
|
565
|
+
performance_widget.update_performance_data(cpu, memory, jobs)
|
|
566
|
+
|
|
567
|
+
avg_job_time = self.query_one("#avg_job_time", MetricCard)
|
|
568
|
+
success_rate = self.query_one("#success_rate", MetricCard)
|
|
569
|
+
|
|
570
|
+
avg_job_time.update_metric("0s")
|
|
571
|
+
success_rate.update_metric("100 % ")
|
|
572
|
+
|
|
573
|
+
except Exception as e:
|
|
574
|
+
self.log(f"Error updating performance metrics: {e}")
|
|
575
|
+
|
|
576
|
+
async def _update_logs(self) -> None:
|
|
577
|
+
try:
|
|
578
|
+
log_display = self.query_one("#log_display", Log)
|
|
579
|
+
|
|
580
|
+
current_time = datetime.now().strftime("%H:%M:%S")
|
|
581
|
+
if len(self.logs_buffer) % 10 == 0:
|
|
582
|
+
log_display.write_line(f"[{current_time}] Dashboard refresh completed")
|
|
583
|
+
|
|
584
|
+
except Exception as e:
|
|
585
|
+
self.log(f"Error updating logs: {e}")
|
|
586
|
+
|
|
587
|
+
def _format_duration(self, seconds: float) -> str:
|
|
588
|
+
if seconds < 60:
|
|
589
|
+
return f"{seconds: .1f}s"
|
|
590
|
+
if seconds < 3600:
|
|
591
|
+
minutes = seconds / 60
|
|
592
|
+
return f"{minutes: .1f}m"
|
|
593
|
+
hours = seconds / 3600
|
|
594
|
+
return f"{hours: .1f}h"
|
|
595
|
+
|
|
596
|
+
def action_refresh(self) -> None:
|
|
597
|
+
self.call_later(self.update_dashboard)
|
|
598
|
+
self.log("Manual refresh triggered")
|
|
599
|
+
|
|
600
|
+
def action_clear_logs(self) -> None:
|
|
601
|
+
try:
|
|
602
|
+
log_display = self.query_one("#log_display", Log)
|
|
603
|
+
log_display.clear()
|
|
604
|
+
self.logs_buffer.clear()
|
|
605
|
+
self.log("Logs cleared")
|
|
606
|
+
except Exception as e:
|
|
607
|
+
self.log(f"Error clearing logs: {e}")
|
|
608
|
+
|
|
609
|
+
def action_pause_logs(self) -> None:
|
|
610
|
+
self.is_paused = not self.is_paused
|
|
611
|
+
status = "paused" if self.is_paused else "resumed"
|
|
612
|
+
self.log(f"Dashboard updates {status}")
|
|
613
|
+
|
|
614
|
+
def action_toggle_servers(self) -> None:
|
|
615
|
+
self.log("Server toggle not implemented yet")
|
|
616
|
+
|
|
617
|
+
def action_toggle_debug(self) -> None:
|
|
618
|
+
self.debug_mode = not self.debug_mode
|
|
619
|
+
self.log(f"Debug mode {'enabled' if self.debug_mode else 'disabled'}")
|
|
620
|
+
|
|
621
|
+
async def on_exit(self) -> None:
|
|
622
|
+
if self.update_timer:
|
|
623
|
+
self.update_timer.stop()
|
|
624
|
+
|
|
625
|
+
self.terminal_restorer.restore_terminal()
|
|
626
|
+
|
|
627
|
+
self.log("Dashboard shutting down...")
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def run_dashboard() -> None:
|
|
631
|
+
app = CrackerjackDashboard()
|
|
632
|
+
app.run()
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
if __name__ == "__main__":
|
|
636
|
+
run_dashboard()
|