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.

Files changed (158) hide show
  1. crackerjack/CLAUDE.md +1005 -0
  2. crackerjack/RULES.md +380 -0
  3. crackerjack/__init__.py +42 -13
  4. crackerjack/__main__.py +225 -253
  5. crackerjack/agents/__init__.py +41 -0
  6. crackerjack/agents/architect_agent.py +281 -0
  7. crackerjack/agents/base.py +169 -0
  8. crackerjack/agents/coordinator.py +512 -0
  9. crackerjack/agents/documentation_agent.py +498 -0
  10. crackerjack/agents/dry_agent.py +388 -0
  11. crackerjack/agents/formatting_agent.py +245 -0
  12. crackerjack/agents/import_optimization_agent.py +281 -0
  13. crackerjack/agents/performance_agent.py +669 -0
  14. crackerjack/agents/proactive_agent.py +104 -0
  15. crackerjack/agents/refactoring_agent.py +788 -0
  16. crackerjack/agents/security_agent.py +529 -0
  17. crackerjack/agents/test_creation_agent.py +652 -0
  18. crackerjack/agents/test_specialist_agent.py +486 -0
  19. crackerjack/agents/tracker.py +212 -0
  20. crackerjack/api.py +560 -0
  21. crackerjack/cli/__init__.py +24 -0
  22. crackerjack/cli/facade.py +104 -0
  23. crackerjack/cli/handlers.py +267 -0
  24. crackerjack/cli/interactive.py +471 -0
  25. crackerjack/cli/options.py +401 -0
  26. crackerjack/cli/utils.py +18 -0
  27. crackerjack/code_cleaner.py +670 -0
  28. crackerjack/config/__init__.py +19 -0
  29. crackerjack/config/hooks.py +218 -0
  30. crackerjack/core/__init__.py +0 -0
  31. crackerjack/core/async_workflow_orchestrator.py +406 -0
  32. crackerjack/core/autofix_coordinator.py +200 -0
  33. crackerjack/core/container.py +104 -0
  34. crackerjack/core/enhanced_container.py +542 -0
  35. crackerjack/core/performance.py +243 -0
  36. crackerjack/core/phase_coordinator.py +561 -0
  37. crackerjack/core/proactive_workflow.py +316 -0
  38. crackerjack/core/session_coordinator.py +289 -0
  39. crackerjack/core/workflow_orchestrator.py +640 -0
  40. crackerjack/dynamic_config.py +577 -0
  41. crackerjack/errors.py +263 -41
  42. crackerjack/executors/__init__.py +11 -0
  43. crackerjack/executors/async_hook_executor.py +431 -0
  44. crackerjack/executors/cached_hook_executor.py +242 -0
  45. crackerjack/executors/hook_executor.py +345 -0
  46. crackerjack/executors/individual_hook_executor.py +669 -0
  47. crackerjack/intelligence/__init__.py +44 -0
  48. crackerjack/intelligence/adaptive_learning.py +751 -0
  49. crackerjack/intelligence/agent_orchestrator.py +551 -0
  50. crackerjack/intelligence/agent_registry.py +414 -0
  51. crackerjack/intelligence/agent_selector.py +502 -0
  52. crackerjack/intelligence/integration.py +290 -0
  53. crackerjack/interactive.py +576 -315
  54. crackerjack/managers/__init__.py +11 -0
  55. crackerjack/managers/async_hook_manager.py +135 -0
  56. crackerjack/managers/hook_manager.py +137 -0
  57. crackerjack/managers/publish_manager.py +411 -0
  58. crackerjack/managers/test_command_builder.py +151 -0
  59. crackerjack/managers/test_executor.py +435 -0
  60. crackerjack/managers/test_manager.py +258 -0
  61. crackerjack/managers/test_manager_backup.py +1124 -0
  62. crackerjack/managers/test_progress.py +144 -0
  63. crackerjack/mcp/__init__.py +0 -0
  64. crackerjack/mcp/cache.py +336 -0
  65. crackerjack/mcp/client_runner.py +104 -0
  66. crackerjack/mcp/context.py +615 -0
  67. crackerjack/mcp/dashboard.py +636 -0
  68. crackerjack/mcp/enhanced_progress_monitor.py +479 -0
  69. crackerjack/mcp/file_monitor.py +336 -0
  70. crackerjack/mcp/progress_components.py +569 -0
  71. crackerjack/mcp/progress_monitor.py +949 -0
  72. crackerjack/mcp/rate_limiter.py +332 -0
  73. crackerjack/mcp/server.py +22 -0
  74. crackerjack/mcp/server_core.py +244 -0
  75. crackerjack/mcp/service_watchdog.py +501 -0
  76. crackerjack/mcp/state.py +395 -0
  77. crackerjack/mcp/task_manager.py +257 -0
  78. crackerjack/mcp/tools/__init__.py +17 -0
  79. crackerjack/mcp/tools/core_tools.py +249 -0
  80. crackerjack/mcp/tools/error_analyzer.py +308 -0
  81. crackerjack/mcp/tools/execution_tools.py +370 -0
  82. crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
  83. crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
  84. crackerjack/mcp/tools/intelligence_tools.py +314 -0
  85. crackerjack/mcp/tools/monitoring_tools.py +502 -0
  86. crackerjack/mcp/tools/proactive_tools.py +384 -0
  87. crackerjack/mcp/tools/progress_tools.py +141 -0
  88. crackerjack/mcp/tools/utility_tools.py +341 -0
  89. crackerjack/mcp/tools/workflow_executor.py +360 -0
  90. crackerjack/mcp/websocket/__init__.py +14 -0
  91. crackerjack/mcp/websocket/app.py +39 -0
  92. crackerjack/mcp/websocket/endpoints.py +559 -0
  93. crackerjack/mcp/websocket/jobs.py +253 -0
  94. crackerjack/mcp/websocket/server.py +116 -0
  95. crackerjack/mcp/websocket/websocket_handler.py +78 -0
  96. crackerjack/mcp/websocket_server.py +10 -0
  97. crackerjack/models/__init__.py +31 -0
  98. crackerjack/models/config.py +93 -0
  99. crackerjack/models/config_adapter.py +230 -0
  100. crackerjack/models/protocols.py +118 -0
  101. crackerjack/models/task.py +154 -0
  102. crackerjack/monitoring/ai_agent_watchdog.py +450 -0
  103. crackerjack/monitoring/regression_prevention.py +638 -0
  104. crackerjack/orchestration/__init__.py +0 -0
  105. crackerjack/orchestration/advanced_orchestrator.py +970 -0
  106. crackerjack/orchestration/execution_strategies.py +341 -0
  107. crackerjack/orchestration/test_progress_streamer.py +636 -0
  108. crackerjack/plugins/__init__.py +15 -0
  109. crackerjack/plugins/base.py +200 -0
  110. crackerjack/plugins/hooks.py +246 -0
  111. crackerjack/plugins/loader.py +335 -0
  112. crackerjack/plugins/managers.py +259 -0
  113. crackerjack/py313.py +8 -3
  114. crackerjack/services/__init__.py +22 -0
  115. crackerjack/services/cache.py +314 -0
  116. crackerjack/services/config.py +347 -0
  117. crackerjack/services/config_integrity.py +99 -0
  118. crackerjack/services/contextual_ai_assistant.py +516 -0
  119. crackerjack/services/coverage_ratchet.py +347 -0
  120. crackerjack/services/debug.py +736 -0
  121. crackerjack/services/dependency_monitor.py +617 -0
  122. crackerjack/services/enhanced_filesystem.py +439 -0
  123. crackerjack/services/file_hasher.py +151 -0
  124. crackerjack/services/filesystem.py +395 -0
  125. crackerjack/services/git.py +165 -0
  126. crackerjack/services/health_metrics.py +611 -0
  127. crackerjack/services/initialization.py +847 -0
  128. crackerjack/services/log_manager.py +286 -0
  129. crackerjack/services/logging.py +174 -0
  130. crackerjack/services/metrics.py +578 -0
  131. crackerjack/services/pattern_cache.py +362 -0
  132. crackerjack/services/pattern_detector.py +515 -0
  133. crackerjack/services/performance_benchmarks.py +653 -0
  134. crackerjack/services/security.py +163 -0
  135. crackerjack/services/server_manager.py +234 -0
  136. crackerjack/services/smart_scheduling.py +144 -0
  137. crackerjack/services/tool_version_service.py +61 -0
  138. crackerjack/services/unified_config.py +437 -0
  139. crackerjack/services/version_checker.py +248 -0
  140. crackerjack/slash_commands/__init__.py +14 -0
  141. crackerjack/slash_commands/init.md +122 -0
  142. crackerjack/slash_commands/run.md +163 -0
  143. crackerjack/slash_commands/status.md +127 -0
  144. crackerjack-0.31.4.dist-info/METADATA +742 -0
  145. crackerjack-0.31.4.dist-info/RECORD +148 -0
  146. crackerjack-0.31.4.dist-info/entry_points.txt +2 -0
  147. crackerjack/.gitignore +0 -34
  148. crackerjack/.libcst.codemod.yaml +0 -18
  149. crackerjack/.pdm.toml +0 -1
  150. crackerjack/.pre-commit-config-ai.yaml +0 -149
  151. crackerjack/.pre-commit-config-fast.yaml +0 -69
  152. crackerjack/.pre-commit-config.yaml +0 -114
  153. crackerjack/crackerjack.py +0 -4140
  154. crackerjack/pyproject.toml +0 -285
  155. crackerjack-0.29.0.dist-info/METADATA +0 -1289
  156. crackerjack-0.29.0.dist-info/RECORD +0 -17
  157. {crackerjack-0.29.0.dist-info → crackerjack-0.31.4.dist-info}/WHEEL +0 -0
  158. {crackerjack-0.29.0.dist-info → crackerjack-0.31.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,14 @@
1
+ from .app import create_websocket_app
2
+ from .endpoints import register_endpoints
3
+ from .jobs import JobManager
4
+ from .server import WebSocketServer, main
5
+ from .websocket_handler import WebSocketHandler
6
+
7
+ __all__ = [
8
+ "JobManager",
9
+ "WebSocketHandler",
10
+ "WebSocketServer",
11
+ "create_websocket_app",
12
+ "main",
13
+ "register_endpoints",
14
+ ]
@@ -0,0 +1,39 @@
1
+ import asyncio
2
+ from pathlib import Path
3
+
4
+ from fastapi import FastAPI
5
+
6
+ from .endpoints import register_endpoints
7
+ from .jobs import JobManager
8
+ from .websocket_handler import register_websocket_routes
9
+
10
+
11
+ def create_websocket_app(job_manager: JobManager, progress_dir: Path) -> FastAPI:
12
+ app = FastAPI(
13
+ title="Crackerjack WebSocket Server",
14
+ description="Real-time progress monitoring for Crackerjack workflows",
15
+ version="1.0.0",
16
+ )
17
+
18
+ # Store job_manager in app state for startup/shutdown events
19
+ app.state.job_manager = job_manager
20
+
21
+ @app.on_event("startup")
22
+ async def startup_event() -> None:
23
+ """Start background tasks."""
24
+ if job_manager:
25
+ asyncio.create_task(job_manager.monitor_progress_files())
26
+ asyncio.create_task(job_manager.cleanup_old_jobs())
27
+ asyncio.create_task(job_manager.timeout_stuck_jobs())
28
+
29
+ @app.on_event("shutdown")
30
+ async def shutdown_event() -> None:
31
+ """Cleanup on shutdown."""
32
+ if job_manager:
33
+ job_manager.cleanup()
34
+
35
+ register_endpoints(app, job_manager, progress_dir)
36
+
37
+ register_websocket_routes(app, job_manager, progress_dir)
38
+
39
+ return app
@@ -0,0 +1,559 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from fastapi import FastAPI
5
+ from fastapi.responses import HTMLResponse
6
+
7
+ from .jobs import JobManager
8
+
9
+
10
+ def _build_job_list(job_manager: JobManager, progress_dir: Path) -> list[dict]:
11
+ """Build list of jobs from progress files."""
12
+ jobs = []
13
+ if not progress_dir.exists():
14
+ return jobs
15
+ for progress_file in progress_dir.glob("job-*.json"):
16
+ job_id = job_manager.extract_job_id_from_file(progress_file)
17
+ if job_id and job_manager.validate_job_id(job_id):
18
+ try:
19
+ progress_data = json.loads(progress_file.read_text())
20
+ jobs.append(
21
+ {
22
+ "job_id": job_id,
23
+ "status": progress_data.get("status", "unknown"),
24
+ "message": progress_data.get("message", ""),
25
+ "progress": progress_data.get("overall_progress", 0),
26
+ },
27
+ )
28
+ except (json.JSONDecodeError, OSError):
29
+ continue
30
+ jobs.sort(key=lambda j: j.get("job_id", ""), reverse=True)
31
+ return jobs
32
+
33
+
34
+ def _build_status_response(job_manager: JobManager, jobs: list[dict]) -> dict:
35
+ """Build the status response dictionary."""
36
+ return {
37
+ "status": "running",
38
+ "message": "Crackerjack WebSocket Server",
39
+ "active_connections": len(job_manager.active_connections),
40
+ "jobs": jobs[:10],
41
+ "websocket_url": "ws://localhost:8675/ws/progress/{job_id}",
42
+ "endpoints": {
43
+ "status": "/",
44
+ "latest_job": "/latest",
45
+ "job_monitor": "/monitor/{job_id}",
46
+ "test": "/test",
47
+ "websocket": "/ws/progress/{job_id}",
48
+ },
49
+ }
50
+
51
+
52
+ def _get_test_html() -> str:
53
+ """Generate HTML content for test page."""
54
+ return """
55
+ <!DOCTYPE html>
56
+ <html>
57
+ <head>
58
+ <title>WebSocket Test Page</title>
59
+ <meta charset="UTF-8">
60
+ <style>
61
+ body {
62
+ font-family: Arial, sans-serif;
63
+ max-width: 800px;
64
+ margin: 50px auto;
65
+ padding: 20px;
66
+ background-color: #f5f5f5;
67
+ }
68
+ .container {
69
+ background: white;
70
+ padding: 30px;
71
+ border-radius: 10px;
72
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
73
+ }
74
+ .test-section {
75
+ margin: 20px 0;
76
+ padding: 15px;
77
+ border: 1px solid #ddd;
78
+ border-radius: 5px;
79
+ }
80
+ button {
81
+ background: #007acc;
82
+ color: white;
83
+ border: none;
84
+ padding: 10px 20px;
85
+ border-radius: 5px;
86
+ cursor: pointer;
87
+ margin: 5px;
88
+ }
89
+ button:hover { background: #005999; }
90
+ input[type="text"] {
91
+ padding: 8px;
92
+ border: 1px solid #ddd;
93
+ border-radius: 3px;
94
+ margin: 5px;
95
+ width: 200px;
96
+ }
97
+ .status {
98
+ margin: 10px 0;
99
+ padding: 10px;
100
+ border-radius: 5px;
101
+ font-weight: bold;
102
+ }
103
+ .success { background: #d4edda; color: #155724; }
104
+ .error { background: #f8d7da; color: #721c24; }
105
+ .info { background: #d1ecf1; color: #0c5460; }
106
+ #log {
107
+ background: #1e1e1e;
108
+ color: #ffffff;
109
+ padding: 15px;
110
+ border-radius: 5px;
111
+ font-family: 'Courier New', monospace;
112
+ max-height: 300px;
113
+ overflow-y: auto;
114
+ margin-top: 20px;
115
+ }
116
+ </style>
117
+ </head>
118
+ <body>
119
+ <div class="container">
120
+ <h1>🧪 WebSocket Test Page</h1>
121
+ <p>Test WebSocket connectivity and server functionality</p>
122
+
123
+ <div class="test-section">
124
+ <h3>1. Server Status</h3>
125
+ <button onclick="checkServerStatus()">Check Status</button>
126
+ <div id="serverStatus"></div>
127
+ </div>
128
+
129
+ <div class="test-section">
130
+ <h3>2. Latest Job</h3>
131
+ <button onclick="checkLatestJob()">Get Latest Job</button>
132
+ <div id="latestJob"></div>
133
+ </div>
134
+
135
+ <div class="test-section">
136
+ <h3>3. WebSocket Test</h3>
137
+ <input type="text" id="testJobId" placeholder="Enter job ID (e.g., test-123)" value="test-123">
138
+ <button onclick="testWebSocket()">Test WebSocket</button>
139
+ <button onclick="disconnectWebSocket()">Disconnect</button>
140
+ <div id="wsStatus"></div>
141
+ </div>
142
+
143
+ <div id="log">
144
+ <div>Ready to test...</div>
145
+ </div>
146
+ </div>
147
+
148
+ <script>
149
+ let testWs = null;
150
+ const log = document.getElementById('log');
151
+
152
+ function addLog(message, type = 'info') {
153
+ const timestamp = new Date().toLocaleTimeString();
154
+ const div = document.createElement('div');
155
+ div.textContent = `[${timestamp}] ${message}`;
156
+ if (type === 'error') div.style.color = '#ff6b6b';
157
+ if (type === 'success') div.style.color = '#51cf66';
158
+ log.appendChild(div);
159
+ log.scrollTop = log.scrollHeight;
160
+ }
161
+
162
+ function showStatus(elementId, message, type = 'info') {
163
+ const element = document.getElementById(elementId);
164
+ element.innerHTML = `<div class="status ${type}">${message}</div>`;
165
+ }
166
+
167
+ async function checkServerStatus() {
168
+ try {
169
+ addLog('Checking server status...');
170
+ const response = await fetch('/');
171
+ const data = await response.json();
172
+
173
+ showStatus('serverStatus',
174
+ `Status: ${data.status}<br>` +
175
+ `Active connections: ${data.active_connections}<br>` +
176
+ `Jobs: ${data.jobs?.length || 0}`, 'success');
177
+ addLog('Server status retrieved successfully', 'success');
178
+ } catch (error) {
179
+ showStatus('serverStatus', `Error: ${error.message}`, 'error');
180
+ addLog(`Server status error: ${error.message}`, 'error');
181
+ }
182
+ }
183
+
184
+ async function checkLatestJob() {
185
+ try {
186
+ addLog('Getting latest job...');
187
+ const response = await fetch('/latest');
188
+ const data = await response.json();
189
+
190
+ if (data.job_id) {
191
+ showStatus('latestJob',
192
+ `Job ID: ${data.job_id}<br>` +
193
+ `Status: ${data.progress?.status || 'unknown'}<br>` +
194
+ `Progress: ${data.progress?.overall_progress || 0}%`, 'success');
195
+ addLog(`Latest job: ${data.job_id}`, 'success');
196
+ } else {
197
+ showStatus('latestJob', data.message, 'info');
198
+ addLog(data.message);
199
+ }
200
+ } catch (error) {
201
+ showStatus('latestJob', `Error: ${error.message}`, 'error');
202
+ addLog(`Latest job error: ${error.message}`, 'error');
203
+ }
204
+ }
205
+
206
+ function testWebSocket() {
207
+ const jobId = document.getElementById('testJobId').value;
208
+ if (!jobId) {
209
+ showStatus('wsStatus', 'Please enter a job ID', 'error');
210
+ return;
211
+ }
212
+
213
+ disconnectWebSocket();
214
+
215
+ addLog(`Connecting to WebSocket for job: ${jobId}`);
216
+ const wsUrl = `ws://localhost:8675/ws/progress/${jobId}`;
217
+ testWs = new WebSocket(wsUrl);
218
+
219
+ testWs.onopen = function() {
220
+ showStatus('wsStatus', `Connected to ${jobId}`, 'success');
221
+ addLog('WebSocket connected successfully', 'success');
222
+ };
223
+
224
+ testWs.onmessage = function(event) {
225
+ try {
226
+ const data = JSON.parse(event.data);
227
+ addLog(`Received: ${data.message || 'Progress update'}`);
228
+ } catch (e) {
229
+ addLog(`Raw message: ${event.data}`);
230
+ }
231
+ };
232
+
233
+ testWs.onclose = function() {
234
+ showStatus('wsStatus', 'WebSocket closed', 'info');
235
+ addLog('WebSocket connection closed');
236
+ };
237
+
238
+ testWs.onerror = function(error) {
239
+ showStatus('wsStatus', `WebSocket error: ${error.message}`, 'error');
240
+ addLog(`WebSocket error: ${error.message}`, 'error');
241
+ };
242
+ }
243
+
244
+ function disconnectWebSocket() {
245
+ if (testWs) {
246
+ testWs.close();
247
+ testWs = null;
248
+ showStatus('wsStatus', 'Disconnected', 'info');
249
+ addLog('WebSocket disconnected');
250
+ }
251
+ }
252
+
253
+ // Auto-check server status on load
254
+ window.onload = function() {
255
+ checkServerStatus();
256
+ };
257
+ </script>
258
+ </body>
259
+ </html>
260
+ """
261
+
262
+
263
+ def _get_monitor_html(job_id: str) -> str:
264
+ """Generate HTML content for job monitor page."""
265
+ return f"""
266
+ <!DOCTYPE html>
267
+ <html>
268
+ <head>
269
+ <title>Job Monitor - {job_id}</title>
270
+ <meta charset="UTF-8">
271
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
272
+ <style>
273
+ body {{
274
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
275
+ margin: 0;
276
+ padding: 20px;
277
+ background-color: #f5f5f5;
278
+ }}
279
+ .container {{
280
+ max-width: 800px;
281
+ margin: 0 auto;
282
+ background: white;
283
+ padding: 30px;
284
+ border-radius: 10px;
285
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
286
+ }}
287
+ .header {{
288
+ text-align: center;
289
+ margin-bottom: 30px;
290
+ padding-bottom: 20px;
291
+ border-bottom: 2px solid #007acc;
292
+ }}
293
+ .job-id {{
294
+ font-family: 'Courier New', monospace;
295
+ background: #f0f8ff;
296
+ padding: 5px 10px;
297
+ border-radius: 5px;
298
+ color: #007acc;
299
+ }}
300
+ .status {{
301
+ margin: 20px 0;
302
+ padding: 15px;
303
+ border-radius: 5px;
304
+ font-weight: bold;
305
+ }}
306
+ .status.running {{ background-color: #fff3cd; color: #856404; }}
307
+ .status.completed {{ background-color: #d4edda; color: #155724; }}
308
+ .status.failed {{ background-color: #f8d7da; color: #721c24; }}
309
+ .status.connecting {{ background-color: #cce7ff; color: #004085; }}
310
+ .progress-bar {{
311
+ width: 100%;
312
+ height: 20px;
313
+ background-color: #e9ecef;
314
+ border-radius: 10px;
315
+ overflow: hidden;
316
+ margin: 10px 0;
317
+ }}
318
+ .progress-fill {{
319
+ height: 100%;
320
+ background-color: #007acc;
321
+ width: 0%;
322
+ transition: width 0.3s ease;
323
+ }}
324
+ .details {{
325
+ margin: 20px 0;
326
+ padding: 15px;
327
+ background: #f8f9fa;
328
+ border-radius: 5px;
329
+ border-left: 4px solid #007acc;
330
+ }}
331
+ .log {{
332
+ margin: 20px 0;
333
+ padding: 15px;
334
+ background: #1e1e1e;
335
+ color: #ffffff;
336
+ font-family: 'Courier New', monospace;
337
+ border-radius: 5px;
338
+ max-height: 300px;
339
+ overflow-y: auto;
340
+ font-size: 12px;
341
+ line-height: 1.4;
342
+ }}
343
+ .connection-status {{
344
+ position: fixed;
345
+ top: 10px;
346
+ right: 10px;
347
+ padding: 5px 10px;
348
+ border-radius: 15px;
349
+ font-size: 12px;
350
+ font-weight: bold;
351
+ }}
352
+ .connected {{ background: #d4edda; color: #155724; }}
353
+ .disconnected {{ background: #f8d7da; color: #721c24; }}
354
+ .reconnecting {{ background: #fff3cd; color: #856404; }}
355
+ </style>
356
+ </head>
357
+ <body>
358
+ <div class="connection-status" id="connectionStatus">Connecting...</div>
359
+
360
+ <div class="container">
361
+ <div class="header">
362
+ <h1>🚀 Crackerjack Job Monitor</h1>
363
+ <p>Job ID: <span class="job-id">{job_id}</span></p>
364
+ </div>
365
+
366
+ <div class="status connecting" id="status">
367
+ Status: Connecting to job...
368
+ </div>
369
+
370
+ <div class="progress-bar">
371
+ <div class="progress-fill" id="progressFill"></div>
372
+ </div>
373
+ <div id="progressText">Progress: 0%</div>
374
+
375
+ <div class="details" id="details">
376
+ <strong>Current Stage:</strong> <span id="currentStage">Initializing</span><br>
377
+ <strong>Iteration:</strong> <span id="iteration">0</span> / <span id="maxIterations">10</span><br>
378
+ <strong>Message:</strong> <span id="message">Connecting...</span>
379
+ </div>
380
+
381
+ <div class="log" id="log">
382
+ <div>Connecting to WebSocket...</div>
383
+ </div>
384
+ </div>
385
+
386
+ <script>
387
+ const jobId = '{job_id}';
388
+ const wsUrl = `ws://localhost:8675/ws/progress/${{jobId}}`;
389
+ let ws = null;
390
+ let reconnectAttempts = 0;
391
+ const maxReconnectAttempts = 10;
392
+
393
+ const elements = {{
394
+ status: document.getElementById('status'),
395
+ progressFill: document.getElementById('progressFill'),
396
+ progressText: document.getElementById('progressText'),
397
+ currentStage: document.getElementById('currentStage'),
398
+ iteration: document.getElementById('iteration'),
399
+ maxIterations: document.getElementById('maxIterations'),
400
+ message: document.getElementById('message'),
401
+ log: document.getElementById('log'),
402
+ connectionStatus: document.getElementById('connectionStatus')
403
+ }};
404
+
405
+ function updateConnectionStatus(status) {{
406
+ elements.connectionStatus.className = 'connection-status ' + status;
407
+ elements.connectionStatus.textContent = {{
408
+ 'connected': '🟢 Connected',
409
+ 'disconnected': '🔴 Disconnected',
410
+ 'reconnecting': '🟡 Reconnecting...'
411
+ }}[status] || '⚪ Unknown';
412
+ }}
413
+
414
+ function addLogEntry(message, type = 'info') {{
415
+ const timestamp = new Date().toLocaleTimeString();
416
+ const logEntry = document.createElement('div');
417
+ logEntry.textContent = `[${{timestamp}}] ${{message}}`;
418
+ if (type === 'error') logEntry.style.color = '#ff6b6b';
419
+ if (type === 'success') logEntry.style.color = '#51cf66';
420
+ elements.log.appendChild(logEntry);
421
+ elements.log.scrollTop = elements.log.scrollHeight;
422
+ }}
423
+
424
+ function updateProgress(data) {{
425
+ const progress = data.overall_progress || 0;
426
+ const status = data.status || 'unknown';
427
+
428
+ elements.progressFill.style.width = progress + '%';
429
+ elements.progressText.textContent = `Progress: ${{progress}}%`;
430
+
431
+ elements.status.textContent = `Status: ${{status}}`;
432
+ elements.status.className = 'status ' + status.toLowerCase();
433
+
434
+ elements.currentStage.textContent = data.current_stage || 'Unknown';
435
+ elements.iteration.textContent = data.iteration || 0;
436
+ elements.maxIterations.textContent = data.max_iterations || 10;
437
+ elements.message.textContent = data.message || 'No message';
438
+
439
+ addLogEntry(`${{data.current_stage || 'Unknown stage'}}: ${{data.message || 'No message'}}`,
440
+ status === 'failed' ? 'error' : status === 'completed' ? 'success' : 'info');
441
+ }}
442
+
443
+ function connect() {{
444
+ if (ws && ws.readyState === WebSocket.OPEN) return;
445
+
446
+ updateConnectionStatus('reconnecting');
447
+ addLogEntry('Connecting to WebSocket...');
448
+
449
+ ws = new WebSocket(wsUrl);
450
+
451
+ ws.onopen = function() {{
452
+ updateConnectionStatus('connected');
453
+ addLogEntry('Connected to WebSocket', 'success');
454
+ reconnectAttempts = 0;
455
+ }};
456
+
457
+ ws.onmessage = function(event) {{
458
+ try {{
459
+ const data = JSON.parse(event.data);
460
+ updateProgress(data);
461
+ }} catch (e) {{
462
+ addLogEntry('Error parsing message: ' + e.message, 'error');
463
+ }}
464
+ }};
465
+
466
+ ws.onclose = function() {{
467
+ updateConnectionStatus('disconnected');
468
+ addLogEntry('WebSocket connection closed');
469
+ if (reconnectAttempts < maxReconnectAttempts) {{
470
+ setTimeout(() => {{
471
+ reconnectAttempts++;
472
+ addLogEntry(`Attempting to reconnect (${{reconnectAttempts}}/${{maxReconnectAttempts}})...`);
473
+ connect();
474
+ }}, 2000);
475
+ }} else {{
476
+ addLogEntry('Maximum reconnection attempts reached', 'error');
477
+ }}
478
+ }};
479
+
480
+ ws.onerror = function(error) {{
481
+ addLogEntry('WebSocket error occurred', 'error');
482
+ updateConnectionStatus('disconnected');
483
+ }};
484
+ }}
485
+
486
+ // Start connection
487
+ connect();
488
+
489
+ // Periodically check connection health
490
+ setInterval(() => {{
491
+ if (!ws || ws.readyState !== WebSocket.OPEN) {{
492
+ if (reconnectAttempts < maxReconnectAttempts) {{
493
+ connect();
494
+ }}
495
+ }}
496
+ }}, 5000);
497
+ </script>
498
+ </body>
499
+ </html>
500
+ """
501
+
502
+
503
+ def register_endpoints(
504
+ app: FastAPI,
505
+ job_manager: JobManager,
506
+ progress_dir: Path,
507
+ ) -> None:
508
+ @app.get("/")
509
+ async def get_status():
510
+ try:
511
+ jobs = _build_job_list(job_manager, progress_dir)
512
+ return _build_status_response(job_manager, jobs)
513
+ except Exception as e:
514
+ return {"status": "error", "message": str(e)}
515
+
516
+ @app.get("/latest")
517
+ async def get_latest_job():
518
+ try:
519
+ latest_job_id = job_manager.get_latest_job_id()
520
+
521
+ if not latest_job_id:
522
+ return {
523
+ "status": "no_jobs",
524
+ "message": "No jobs found",
525
+ "job_id": None,
526
+ "progress": None,
527
+ }
528
+
529
+ progress_data = job_manager.get_job_progress(latest_job_id)
530
+
531
+ return {
532
+ "status": "success",
533
+ "message": f"Latest job: {latest_job_id}",
534
+ "job_id": latest_job_id,
535
+ "progress": progress_data,
536
+ "websocket_url": f"ws://localhost:8675/ws/progress/{latest_job_id}",
537
+ "monitor_url": f"http://localhost:8675/monitor/{latest_job_id}",
538
+ }
539
+
540
+ except Exception as e:
541
+ return {
542
+ "status": "error",
543
+ "message": f"Failed to get latest job: {e}",
544
+ "job_id": None,
545
+ "progress": None,
546
+ }
547
+
548
+ @app.get("/monitor/{job_id}")
549
+ async def get_job_monitor_page(job_id: str):
550
+ if not job_manager.validate_job_id(job_id):
551
+ return HTMLResponse(
552
+ content="<h1>Error</h1><p>Invalid job ID</p>",
553
+ status_code=400,
554
+ )
555
+ return HTMLResponse(content=_get_monitor_html(job_id))
556
+
557
+ @app.get("/test")
558
+ async def get_test_page():
559
+ return HTMLResponse(content=_get_test_html())