empathy-framework 4.7.1__py3-none-any.whl → 4.9.0__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.
- {empathy_framework-4.7.1.dist-info → empathy_framework-4.9.0.dist-info}/METADATA +65 -2
- {empathy_framework-4.7.1.dist-info → empathy_framework-4.9.0.dist-info}/RECORD +69 -59
- {empathy_framework-4.7.1.dist-info → empathy_framework-4.9.0.dist-info}/WHEEL +1 -1
- {empathy_framework-4.7.1.dist-info → empathy_framework-4.9.0.dist-info}/entry_points.txt +2 -1
- {empathy_framework-4.7.1.dist-info → empathy_framework-4.9.0.dist-info}/top_level.txt +0 -1
- empathy_os/__init__.py +2 -0
- empathy_os/cli/__init__.py +128 -238
- empathy_os/cli/__main__.py +5 -33
- empathy_os/cli/commands/__init__.py +1 -8
- empathy_os/cli/commands/help.py +331 -0
- empathy_os/cli/commands/info.py +140 -0
- empathy_os/cli/commands/inspect.py +437 -0
- empathy_os/cli/commands/metrics.py +92 -0
- empathy_os/cli/commands/orchestrate.py +184 -0
- empathy_os/cli/commands/patterns.py +207 -0
- empathy_os/cli/commands/provider.py +93 -81
- empathy_os/cli/commands/setup.py +96 -0
- empathy_os/cli/commands/status.py +235 -0
- empathy_os/cli/commands/sync.py +166 -0
- empathy_os/cli/commands/tier.py +121 -0
- empathy_os/cli/commands/workflow.py +574 -0
- empathy_os/cli/parsers/__init__.py +62 -0
- empathy_os/cli/parsers/help.py +41 -0
- empathy_os/cli/parsers/info.py +26 -0
- empathy_os/cli/parsers/inspect.py +66 -0
- empathy_os/cli/parsers/metrics.py +42 -0
- empathy_os/cli/parsers/orchestrate.py +61 -0
- empathy_os/cli/parsers/patterns.py +54 -0
- empathy_os/cli/parsers/provider.py +40 -0
- empathy_os/cli/parsers/setup.py +42 -0
- empathy_os/cli/parsers/status.py +47 -0
- empathy_os/cli/parsers/sync.py +31 -0
- empathy_os/cli/parsers/tier.py +33 -0
- empathy_os/cli/parsers/workflow.py +77 -0
- empathy_os/cli/utils/__init__.py +1 -0
- empathy_os/cli/utils/data.py +242 -0
- empathy_os/cli/utils/helpers.py +68 -0
- empathy_os/{cli.py → cli_legacy.py} +0 -26
- empathy_os/cli_minimal.py +662 -0
- empathy_os/cli_router.py +384 -0
- empathy_os/cli_unified.py +13 -2
- empathy_os/memory/short_term.py +146 -414
- empathy_os/memory/types.py +441 -0
- empathy_os/memory/unified.py +61 -48
- empathy_os/models/fallback.py +1 -1
- empathy_os/models/provider_config.py +59 -344
- empathy_os/models/registry.py +27 -176
- empathy_os/monitoring/alerts.py +14 -20
- empathy_os/monitoring/alerts_cli.py +24 -7
- empathy_os/project_index/__init__.py +2 -0
- empathy_os/project_index/index.py +210 -5
- empathy_os/project_index/scanner.py +48 -16
- empathy_os/project_index/scanner_parallel.py +291 -0
- empathy_os/workflow_commands.py +9 -9
- empathy_os/workflows/__init__.py +31 -2
- empathy_os/workflows/base.py +295 -317
- empathy_os/workflows/bug_predict.py +10 -2
- empathy_os/workflows/builder.py +273 -0
- empathy_os/workflows/caching.py +253 -0
- empathy_os/workflows/code_review_pipeline.py +1 -0
- empathy_os/workflows/history.py +512 -0
- empathy_os/workflows/perf_audit.py +129 -23
- empathy_os/workflows/routing.py +163 -0
- empathy_os/workflows/secure_release.py +1 -0
- empathy_os/workflows/security_audit.py +1 -0
- empathy_os/workflows/security_audit_phase3.py +352 -0
- empathy_os/workflows/telemetry_mixin.py +269 -0
- empathy_os/workflows/test_gen.py +7 -7
- empathy_os/dashboard/__init__.py +0 -15
- empathy_os/dashboard/server.py +0 -941
- empathy_os/vscode_bridge 2.py +0 -173
- empathy_os/workflows/progressive/README 2.md +0 -454
- empathy_os/workflows/progressive/__init__ 2.py +0 -92
- empathy_os/workflows/progressive/cli 2.py +0 -242
- empathy_os/workflows/progressive/core 2.py +0 -488
- empathy_os/workflows/progressive/orchestrator 2.py +0 -701
- empathy_os/workflows/progressive/reports 2.py +0 -528
- empathy_os/workflows/progressive/telemetry 2.py +0 -280
- empathy_os/workflows/progressive/test_gen 2.py +0 -514
- empathy_os/workflows/progressive/workflow 2.py +0 -628
- patterns/README.md +0 -119
- patterns/__init__.py +0 -95
- patterns/behavior.py +0 -298
- patterns/code_review_memory.json +0 -441
- patterns/core.py +0 -97
- patterns/debugging.json +0 -3763
- patterns/empathy.py +0 -268
- patterns/health_check_memory.json +0 -505
- patterns/input.py +0 -161
- patterns/memory_graph.json +0 -8
- patterns/refactoring_memory.json +0 -1113
- patterns/registry.py +0 -663
- patterns/security_memory.json +0 -8
- patterns/structural.py +0 -415
- patterns/validation.py +0 -194
- {empathy_framework-4.7.1.dist-info → empathy_framework-4.9.0.dist-info}/licenses/LICENSE +0 -0
empathy_os/dashboard/server.py
DELETED
|
@@ -1,941 +0,0 @@
|
|
|
1
|
-
"""Dashboard Server for Empathy Framework
|
|
2
|
-
|
|
3
|
-
Lightweight web server for viewing patterns, costs, and health.
|
|
4
|
-
Uses built-in http.server to avoid external dependencies.
|
|
5
|
-
|
|
6
|
-
Copyright 2025 Smart-AI-Memory
|
|
7
|
-
Licensed under Fair Source License 0.9
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import http.server
|
|
11
|
-
import json
|
|
12
|
-
import socketserver
|
|
13
|
-
import threading
|
|
14
|
-
import webbrowser
|
|
15
|
-
from datetime import datetime
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
from urllib.parse import urlparse
|
|
18
|
-
|
|
19
|
-
# Try to import optional dependencies
|
|
20
|
-
try:
|
|
21
|
-
from empathy_os.cost_tracker import CostTracker
|
|
22
|
-
|
|
23
|
-
HAS_COST_TRACKER = True
|
|
24
|
-
except ImportError:
|
|
25
|
-
CostTracker = None # type: ignore[misc, assignment]
|
|
26
|
-
HAS_COST_TRACKER = False
|
|
27
|
-
|
|
28
|
-
try:
|
|
29
|
-
from empathy_os.discovery import DiscoveryEngine
|
|
30
|
-
|
|
31
|
-
HAS_DISCOVERY = True
|
|
32
|
-
except ImportError:
|
|
33
|
-
DiscoveryEngine = None # type: ignore[misc, assignment]
|
|
34
|
-
HAS_DISCOVERY = False
|
|
35
|
-
|
|
36
|
-
try:
|
|
37
|
-
from empathy_os.workflows import get_workflow_stats, list_workflows
|
|
38
|
-
|
|
39
|
-
HAS_WORKFLOWS = True
|
|
40
|
-
except ImportError:
|
|
41
|
-
get_workflow_stats = None # type: ignore[assignment]
|
|
42
|
-
list_workflows = None # type: ignore[assignment]
|
|
43
|
-
HAS_WORKFLOWS = False
|
|
44
|
-
|
|
45
|
-
try:
|
|
46
|
-
from empathy_os.models.telemetry import TelemetryStore
|
|
47
|
-
|
|
48
|
-
HAS_TELEMETRY = True
|
|
49
|
-
except ImportError:
|
|
50
|
-
TelemetryStore = None # type: ignore[misc, assignment]
|
|
51
|
-
HAS_TELEMETRY = False
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
55
|
-
"""HTTP request handler for the dashboard."""
|
|
56
|
-
|
|
57
|
-
patterns_dir = "./patterns"
|
|
58
|
-
empathy_dir = ".empathy"
|
|
59
|
-
|
|
60
|
-
def log_message(self, format, *args):
|
|
61
|
-
"""Suppress default logging."""
|
|
62
|
-
|
|
63
|
-
def do_GET(self):
|
|
64
|
-
"""Handle GET requests."""
|
|
65
|
-
parsed = urlparse(self.path)
|
|
66
|
-
path = parsed.path
|
|
67
|
-
|
|
68
|
-
if path == "/" or path == "/index.html":
|
|
69
|
-
self._serve_dashboard()
|
|
70
|
-
elif path == "/api/patterns":
|
|
71
|
-
self._serve_patterns()
|
|
72
|
-
elif path == "/api/costs":
|
|
73
|
-
self._serve_costs()
|
|
74
|
-
elif path == "/api/stats":
|
|
75
|
-
self._serve_stats()
|
|
76
|
-
elif path == "/api/health":
|
|
77
|
-
self._serve_health()
|
|
78
|
-
elif path == "/api/workflows":
|
|
79
|
-
self._serve_workflows()
|
|
80
|
-
elif path == "/api/tests":
|
|
81
|
-
self._serve_tests()
|
|
82
|
-
else:
|
|
83
|
-
self.send_error(404, "Not Found")
|
|
84
|
-
|
|
85
|
-
def _serve_dashboard(self):
|
|
86
|
-
"""Serve the main dashboard HTML."""
|
|
87
|
-
html = self._generate_dashboard_html()
|
|
88
|
-
self.send_response(200)
|
|
89
|
-
self.send_header("Content-type", "text/html")
|
|
90
|
-
self.send_header("Content-Length", len(html))
|
|
91
|
-
self.end_headers()
|
|
92
|
-
self.wfile.write(html.encode())
|
|
93
|
-
|
|
94
|
-
def _serve_patterns(self):
|
|
95
|
-
"""Serve patterns as JSON."""
|
|
96
|
-
patterns = self._load_patterns()
|
|
97
|
-
self._send_json(patterns)
|
|
98
|
-
|
|
99
|
-
def _serve_costs(self):
|
|
100
|
-
"""Serve cost data as JSON."""
|
|
101
|
-
if HAS_COST_TRACKER and CostTracker is not None:
|
|
102
|
-
tracker = CostTracker(self.empathy_dir)
|
|
103
|
-
data = tracker.get_summary(30)
|
|
104
|
-
else:
|
|
105
|
-
data = {"error": "Cost tracking not available"}
|
|
106
|
-
self._send_json(data)
|
|
107
|
-
|
|
108
|
-
def _serve_stats(self):
|
|
109
|
-
"""Serve discovery stats as JSON."""
|
|
110
|
-
if HAS_DISCOVERY and DiscoveryEngine is not None:
|
|
111
|
-
engine = DiscoveryEngine(self.empathy_dir)
|
|
112
|
-
data = engine.get_stats()
|
|
113
|
-
else:
|
|
114
|
-
data = {"error": "Discovery not available"}
|
|
115
|
-
self._send_json(data)
|
|
116
|
-
|
|
117
|
-
def _serve_health(self):
|
|
118
|
-
"""Serve health check."""
|
|
119
|
-
self._send_json({"status": "healthy", "timestamp": datetime.now().isoformat()})
|
|
120
|
-
|
|
121
|
-
def _serve_workflows(self):
|
|
122
|
-
"""Serve workflow stats as JSON."""
|
|
123
|
-
if HAS_WORKFLOWS and get_workflow_stats is not None:
|
|
124
|
-
data = get_workflow_stats()
|
|
125
|
-
# Add available workflows
|
|
126
|
-
if list_workflows is not None:
|
|
127
|
-
data["available_workflows"] = list_workflows()
|
|
128
|
-
else:
|
|
129
|
-
data = {"error": "Workflows not available"}
|
|
130
|
-
self._send_json(data)
|
|
131
|
-
|
|
132
|
-
def _serve_tests(self):
|
|
133
|
-
"""Serve test tracking data as JSON."""
|
|
134
|
-
data = self._get_test_stats()
|
|
135
|
-
self._send_json(data)
|
|
136
|
-
|
|
137
|
-
def _get_test_stats(self) -> dict:
|
|
138
|
-
"""Get test tracking statistics.
|
|
139
|
-
|
|
140
|
-
Returns:
|
|
141
|
-
Dictionary with test tracking data including:
|
|
142
|
-
- total_files: Total files with test records
|
|
143
|
-
- passed_files: Files with passing tests
|
|
144
|
-
- failed_files: Files with failing tests
|
|
145
|
-
- coverage_avg: Average coverage percentage
|
|
146
|
-
- recent_tests: Recent test executions
|
|
147
|
-
- files_needing_tests: Files that need attention
|
|
148
|
-
"""
|
|
149
|
-
if not HAS_TELEMETRY or TelemetryStore is None:
|
|
150
|
-
return {"error": "Telemetry not available"}
|
|
151
|
-
|
|
152
|
-
try:
|
|
153
|
-
store = TelemetryStore(Path(self.empathy_dir))
|
|
154
|
-
|
|
155
|
-
# Get file test records and convert to dicts
|
|
156
|
-
file_tests_raw = store.get_file_tests(limit=100)
|
|
157
|
-
file_tests = [t.to_dict() if hasattr(t, "to_dict") else t for t in file_tests_raw]
|
|
158
|
-
|
|
159
|
-
# Calculate stats
|
|
160
|
-
total_files = len(file_tests)
|
|
161
|
-
passed_files = sum(1 for t in file_tests if t.get("last_test_result") == "passed")
|
|
162
|
-
failed_files = sum(1 for t in file_tests if t.get("last_test_result") == "failed")
|
|
163
|
-
|
|
164
|
-
# Coverage average (field is coverage_percent)
|
|
165
|
-
coverages = [
|
|
166
|
-
t.get("coverage_percent", 0) for t in file_tests if t.get("coverage_percent")
|
|
167
|
-
]
|
|
168
|
-
coverage_avg = sum(coverages) / len(coverages) if coverages else 0
|
|
169
|
-
|
|
170
|
-
# If no coverage data, use pass rate as a proxy
|
|
171
|
-
if coverage_avg == 0 and total_files > 0:
|
|
172
|
-
coverage_avg = (passed_files / total_files) * 100
|
|
173
|
-
|
|
174
|
-
# Get files needing attention (failed or stale) and convert to dicts
|
|
175
|
-
files_needing_raw = store.get_files_needing_tests(stale_only=False, failed_only=False)
|
|
176
|
-
files_needing_tests = [
|
|
177
|
-
t.to_dict() if hasattr(t, "to_dict") else t for t in files_needing_raw[:10]
|
|
178
|
-
]
|
|
179
|
-
|
|
180
|
-
# Get recent test executions and convert to dicts
|
|
181
|
-
recent_raw = store.get_test_executions(limit=10)
|
|
182
|
-
recent_executions = [t.to_dict() if hasattr(t, "to_dict") else t for t in recent_raw]
|
|
183
|
-
|
|
184
|
-
return {
|
|
185
|
-
"total_files": total_files,
|
|
186
|
-
"passed_files": passed_files,
|
|
187
|
-
"failed_files": failed_files,
|
|
188
|
-
"coverage_avg": round(coverage_avg, 1),
|
|
189
|
-
"files_needing_tests": files_needing_tests,
|
|
190
|
-
"recent_executions": recent_executions,
|
|
191
|
-
"file_tests": file_tests[:20], # Most recent 20
|
|
192
|
-
}
|
|
193
|
-
except Exception as e:
|
|
194
|
-
return {"error": str(e)}
|
|
195
|
-
|
|
196
|
-
def _send_json(self, data):
|
|
197
|
-
"""Send JSON response."""
|
|
198
|
-
content = json.dumps(data, indent=2, default=str)
|
|
199
|
-
self.send_response(200)
|
|
200
|
-
self.send_header("Content-type", "application/json")
|
|
201
|
-
self.send_header("Content-Length", len(content))
|
|
202
|
-
self.end_headers()
|
|
203
|
-
self.wfile.write(content.encode())
|
|
204
|
-
|
|
205
|
-
def _load_patterns(self) -> dict:
|
|
206
|
-
"""Load patterns from disk."""
|
|
207
|
-
patterns = {
|
|
208
|
-
"debugging": [],
|
|
209
|
-
"security": [],
|
|
210
|
-
"tech_debt": {"snapshots": []},
|
|
211
|
-
"inspection": [],
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
patterns_path = Path(self.patterns_dir)
|
|
215
|
-
if not patterns_path.exists():
|
|
216
|
-
return patterns
|
|
217
|
-
|
|
218
|
-
for name in ["debugging", "security", "tech_debt", "inspection"]:
|
|
219
|
-
file_path = patterns_path / f"{name}.json"
|
|
220
|
-
if file_path.exists():
|
|
221
|
-
try:
|
|
222
|
-
with open(file_path) as f:
|
|
223
|
-
data = json.load(f)
|
|
224
|
-
patterns[name] = data
|
|
225
|
-
except (OSError, json.JSONDecodeError):
|
|
226
|
-
pass
|
|
227
|
-
|
|
228
|
-
return patterns
|
|
229
|
-
|
|
230
|
-
def _generate_dashboard_html(self) -> str:
|
|
231
|
-
"""Generate the dashboard HTML page."""
|
|
232
|
-
patterns = self._load_patterns()
|
|
233
|
-
|
|
234
|
-
# Count patterns (handle both dict and list formats)
|
|
235
|
-
debugging_data = patterns.get("debugging", {})
|
|
236
|
-
if isinstance(debugging_data, dict):
|
|
237
|
-
bug_count = len(debugging_data.get("patterns", []))
|
|
238
|
-
else:
|
|
239
|
-
bug_count = len(debugging_data) if isinstance(debugging_data, list) else 0
|
|
240
|
-
|
|
241
|
-
security_data = patterns.get("security", {})
|
|
242
|
-
if isinstance(security_data, dict):
|
|
243
|
-
security_count = len(security_data.get("decisions", []))
|
|
244
|
-
else:
|
|
245
|
-
security_count = len(security_data) if isinstance(security_data, list) else 0
|
|
246
|
-
|
|
247
|
-
debt_items = 0
|
|
248
|
-
tech_debt_data = patterns.get("tech_debt", {})
|
|
249
|
-
if isinstance(tech_debt_data, dict):
|
|
250
|
-
snapshots = tech_debt_data.get("snapshots", [])
|
|
251
|
-
if snapshots:
|
|
252
|
-
debt_items = snapshots[-1].get("total_items", 0)
|
|
253
|
-
|
|
254
|
-
# Get cost summary
|
|
255
|
-
cost_summary = {"savings": 0, "savings_percent": 0, "requests": 0}
|
|
256
|
-
if CostTracker is not None:
|
|
257
|
-
try:
|
|
258
|
-
tracker = CostTracker(self.empathy_dir)
|
|
259
|
-
cost_summary = tracker.get_summary(30) # noqa: F841
|
|
260
|
-
except Exception: # noqa: BLE001
|
|
261
|
-
# INTENTIONAL: Dashboard should render even if cost tracking unavailable.
|
|
262
|
-
pass
|
|
263
|
-
|
|
264
|
-
# Get workflow stats
|
|
265
|
-
workflow_stats = {
|
|
266
|
-
"total_runs": 0,
|
|
267
|
-
"by_workflow": {},
|
|
268
|
-
"by_tier": {"cheap": 0, "capable": 0, "premium": 0},
|
|
269
|
-
"recent_runs": [],
|
|
270
|
-
"total_savings": 0.0,
|
|
271
|
-
"avg_savings_percent": 0.0,
|
|
272
|
-
}
|
|
273
|
-
if HAS_WORKFLOWS and get_workflow_stats is not None:
|
|
274
|
-
try:
|
|
275
|
-
workflow_stats = get_workflow_stats()
|
|
276
|
-
except Exception: # noqa: BLE001
|
|
277
|
-
# INTENTIONAL: Dashboard should render even if workflow stats unavailable.
|
|
278
|
-
pass
|
|
279
|
-
|
|
280
|
-
# Get test stats
|
|
281
|
-
test_stats = self._get_test_stats()
|
|
282
|
-
|
|
283
|
-
return f"""<!DOCTYPE html>
|
|
284
|
-
<html lang="en">
|
|
285
|
-
<head>
|
|
286
|
-
<meta charset="UTF-8">
|
|
287
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
288
|
-
<title>Empathy Framework Dashboard</title>
|
|
289
|
-
<style>
|
|
290
|
-
:root {{
|
|
291
|
-
--primary: #4f46e5;
|
|
292
|
-
--success: #10b981;
|
|
293
|
-
--warning: #f59e0b;
|
|
294
|
-
--danger: #ef4444;
|
|
295
|
-
--bg: #f3f4f6;
|
|
296
|
-
--card-bg: #ffffff;
|
|
297
|
-
--text: #1f2937;
|
|
298
|
-
--text-muted: #6b7280;
|
|
299
|
-
}}
|
|
300
|
-
|
|
301
|
-
* {{
|
|
302
|
-
margin: 0;
|
|
303
|
-
padding: 0;
|
|
304
|
-
box-sizing: border-box;
|
|
305
|
-
}}
|
|
306
|
-
|
|
307
|
-
body {{
|
|
308
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
309
|
-
background: var(--bg);
|
|
310
|
-
color: var(--text);
|
|
311
|
-
line-height: 1.6;
|
|
312
|
-
}}
|
|
313
|
-
|
|
314
|
-
.container {{
|
|
315
|
-
max-width: 1200px;
|
|
316
|
-
margin: 0 auto;
|
|
317
|
-
padding: 2rem;
|
|
318
|
-
}}
|
|
319
|
-
|
|
320
|
-
header {{
|
|
321
|
-
text-align: center;
|
|
322
|
-
margin-bottom: 2rem;
|
|
323
|
-
}}
|
|
324
|
-
|
|
325
|
-
h1 {{
|
|
326
|
-
color: var(--primary);
|
|
327
|
-
font-size: 2rem;
|
|
328
|
-
margin-bottom: 0.5rem;
|
|
329
|
-
}}
|
|
330
|
-
|
|
331
|
-
.subtitle {{
|
|
332
|
-
color: var(--text-muted);
|
|
333
|
-
}}
|
|
334
|
-
|
|
335
|
-
.grid {{
|
|
336
|
-
display: grid;
|
|
337
|
-
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
338
|
-
gap: 1.5rem;
|
|
339
|
-
margin-bottom: 2rem;
|
|
340
|
-
}}
|
|
341
|
-
|
|
342
|
-
.card {{
|
|
343
|
-
background: var(--card-bg);
|
|
344
|
-
border-radius: 12px;
|
|
345
|
-
padding: 1.5rem;
|
|
346
|
-
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
347
|
-
}}
|
|
348
|
-
|
|
349
|
-
.card h2 {{
|
|
350
|
-
font-size: 0.875rem;
|
|
351
|
-
color: var(--text-muted);
|
|
352
|
-
text-transform: uppercase;
|
|
353
|
-
letter-spacing: 0.05em;
|
|
354
|
-
margin-bottom: 0.5rem;
|
|
355
|
-
}}
|
|
356
|
-
|
|
357
|
-
.card .value {{
|
|
358
|
-
font-size: 2.5rem;
|
|
359
|
-
font-weight: 700;
|
|
360
|
-
color: var(--text);
|
|
361
|
-
}}
|
|
362
|
-
|
|
363
|
-
.card .label {{
|
|
364
|
-
color: var(--text-muted);
|
|
365
|
-
font-size: 0.875rem;
|
|
366
|
-
}}
|
|
367
|
-
|
|
368
|
-
.card.success .value {{
|
|
369
|
-
color: var(--success);
|
|
370
|
-
}}
|
|
371
|
-
|
|
372
|
-
.card.warning .value {{
|
|
373
|
-
color: var(--warning);
|
|
374
|
-
}}
|
|
375
|
-
|
|
376
|
-
.section {{
|
|
377
|
-
background: var(--card-bg);
|
|
378
|
-
border-radius: 12px;
|
|
379
|
-
padding: 1.5rem;
|
|
380
|
-
margin-bottom: 1.5rem;
|
|
381
|
-
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
382
|
-
}}
|
|
383
|
-
|
|
384
|
-
.section h2 {{
|
|
385
|
-
font-size: 1.25rem;
|
|
386
|
-
margin-bottom: 1rem;
|
|
387
|
-
color: var(--text);
|
|
388
|
-
}}
|
|
389
|
-
|
|
390
|
-
table {{
|
|
391
|
-
width: 100%;
|
|
392
|
-
border-collapse: collapse;
|
|
393
|
-
}}
|
|
394
|
-
|
|
395
|
-
th, td {{
|
|
396
|
-
text-align: left;
|
|
397
|
-
padding: 0.75rem;
|
|
398
|
-
border-bottom: 1px solid var(--bg);
|
|
399
|
-
}}
|
|
400
|
-
|
|
401
|
-
th {{
|
|
402
|
-
color: var(--text-muted);
|
|
403
|
-
font-weight: 500;
|
|
404
|
-
font-size: 0.875rem;
|
|
405
|
-
}}
|
|
406
|
-
|
|
407
|
-
.status {{
|
|
408
|
-
display: inline-block;
|
|
409
|
-
padding: 0.25rem 0.75rem;
|
|
410
|
-
border-radius: 9999px;
|
|
411
|
-
font-size: 0.75rem;
|
|
412
|
-
font-weight: 500;
|
|
413
|
-
}}
|
|
414
|
-
|
|
415
|
-
.status.resolved {{
|
|
416
|
-
background: #d1fae5;
|
|
417
|
-
color: #065f46;
|
|
418
|
-
}}
|
|
419
|
-
|
|
420
|
-
.status.investigating {{
|
|
421
|
-
background: #fef3c7;
|
|
422
|
-
color: #92400e;
|
|
423
|
-
}}
|
|
424
|
-
|
|
425
|
-
.commands {{
|
|
426
|
-
display: flex;
|
|
427
|
-
flex-wrap: wrap;
|
|
428
|
-
gap: 0.5rem;
|
|
429
|
-
margin-top: 1rem;
|
|
430
|
-
}}
|
|
431
|
-
|
|
432
|
-
.command {{
|
|
433
|
-
background: var(--bg);
|
|
434
|
-
padding: 0.5rem 1rem;
|
|
435
|
-
border-radius: 6px;
|
|
436
|
-
font-family: monospace;
|
|
437
|
-
font-size: 0.875rem;
|
|
438
|
-
}}
|
|
439
|
-
|
|
440
|
-
.workflow-grid {{
|
|
441
|
-
display: grid;
|
|
442
|
-
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
443
|
-
gap: 1rem;
|
|
444
|
-
margin-bottom: 1.5rem;
|
|
445
|
-
}}
|
|
446
|
-
|
|
447
|
-
.workflow-card {{
|
|
448
|
-
background: var(--bg);
|
|
449
|
-
border-radius: 8px;
|
|
450
|
-
padding: 1rem;
|
|
451
|
-
text-align: center;
|
|
452
|
-
}}
|
|
453
|
-
|
|
454
|
-
.workflow-card h3 {{
|
|
455
|
-
font-size: 0.875rem;
|
|
456
|
-
color: var(--text);
|
|
457
|
-
margin-bottom: 0.5rem;
|
|
458
|
-
}}
|
|
459
|
-
|
|
460
|
-
.workflow-card .runs {{
|
|
461
|
-
font-size: 1.5rem;
|
|
462
|
-
font-weight: 600;
|
|
463
|
-
color: var(--primary);
|
|
464
|
-
}}
|
|
465
|
-
|
|
466
|
-
.workflow-card .savings {{
|
|
467
|
-
font-size: 0.875rem;
|
|
468
|
-
color: var(--success);
|
|
469
|
-
}}
|
|
470
|
-
|
|
471
|
-
.tier-bar {{
|
|
472
|
-
display: flex;
|
|
473
|
-
height: 24px;
|
|
474
|
-
border-radius: 4px;
|
|
475
|
-
overflow: hidden;
|
|
476
|
-
margin: 1rem 0;
|
|
477
|
-
}}
|
|
478
|
-
|
|
479
|
-
.tier-bar .tier {{
|
|
480
|
-
display: flex;
|
|
481
|
-
align-items: center;
|
|
482
|
-
justify-content: center;
|
|
483
|
-
font-size: 0.75rem;
|
|
484
|
-
font-weight: 500;
|
|
485
|
-
color: white;
|
|
486
|
-
}}
|
|
487
|
-
|
|
488
|
-
.tier-bar .cheap {{ background: #10b981; }}
|
|
489
|
-
.tier-bar .capable {{ background: #3b82f6; }}
|
|
490
|
-
.tier-bar .premium {{ background: #8b5cf6; }}
|
|
491
|
-
|
|
492
|
-
.recent-run {{
|
|
493
|
-
display: flex;
|
|
494
|
-
align-items: center;
|
|
495
|
-
gap: 1rem;
|
|
496
|
-
padding: 0.75rem 0;
|
|
497
|
-
border-bottom: 1px solid var(--bg);
|
|
498
|
-
}}
|
|
499
|
-
|
|
500
|
-
.recent-run:last-child {{
|
|
501
|
-
border-bottom: none;
|
|
502
|
-
}}
|
|
503
|
-
|
|
504
|
-
.recent-run .name {{
|
|
505
|
-
font-weight: 500;
|
|
506
|
-
min-width: 100px;
|
|
507
|
-
}}
|
|
508
|
-
|
|
509
|
-
.recent-run .provider {{
|
|
510
|
-
font-size: 0.75rem;
|
|
511
|
-
color: var(--text-muted);
|
|
512
|
-
background: var(--bg);
|
|
513
|
-
padding: 0.125rem 0.5rem;
|
|
514
|
-
border-radius: 4px;
|
|
515
|
-
}}
|
|
516
|
-
|
|
517
|
-
.recent-run .result {{
|
|
518
|
-
margin-left: auto;
|
|
519
|
-
display: flex;
|
|
520
|
-
gap: 1rem;
|
|
521
|
-
align-items: center;
|
|
522
|
-
}}
|
|
523
|
-
|
|
524
|
-
.recent-run .savings-badge {{
|
|
525
|
-
font-size: 0.75rem;
|
|
526
|
-
color: var(--success);
|
|
527
|
-
font-weight: 500;
|
|
528
|
-
}}
|
|
529
|
-
|
|
530
|
-
.recent-run .time {{
|
|
531
|
-
font-size: 0.75rem;
|
|
532
|
-
color: var(--text-muted);
|
|
533
|
-
}}
|
|
534
|
-
|
|
535
|
-
footer {{
|
|
536
|
-
text-align: center;
|
|
537
|
-
color: var(--text-muted);
|
|
538
|
-
font-size: 0.875rem;
|
|
539
|
-
padding: 2rem 0;
|
|
540
|
-
}}
|
|
541
|
-
|
|
542
|
-
@media (prefers-color-scheme: dark) {{
|
|
543
|
-
:root {{
|
|
544
|
-
--bg: #111827;
|
|
545
|
-
--card-bg: #1f2937;
|
|
546
|
-
--text: #f9fafb;
|
|
547
|
-
--text-muted: #9ca3af;
|
|
548
|
-
}}
|
|
549
|
-
}}
|
|
550
|
-
</style>
|
|
551
|
-
</head>
|
|
552
|
-
<body>
|
|
553
|
-
<div class="container">
|
|
554
|
-
<header>
|
|
555
|
-
<h1>Empathy Framework Dashboard</h1>
|
|
556
|
-
<p class="subtitle">Pattern learning and cost optimization at a glance</p>
|
|
557
|
-
</header>
|
|
558
|
-
|
|
559
|
-
<div class="grid">
|
|
560
|
-
<div class="card">
|
|
561
|
-
<h2>Bug Patterns</h2>
|
|
562
|
-
<div class="value">{bug_count}</div>
|
|
563
|
-
<div class="label">patterns learned</div>
|
|
564
|
-
</div>
|
|
565
|
-
|
|
566
|
-
<div class="card">
|
|
567
|
-
<h2>Security Decisions</h2>
|
|
568
|
-
<div class="value">{security_count}</div>
|
|
569
|
-
<div class="label">documented</div>
|
|
570
|
-
</div>
|
|
571
|
-
|
|
572
|
-
<div class="card warning">
|
|
573
|
-
<h2>Tech Debt Items</h2>
|
|
574
|
-
<div class="value">{debt_items}</div>
|
|
575
|
-
<div class="label">tracked</div>
|
|
576
|
-
</div>
|
|
577
|
-
|
|
578
|
-
<div class="card">
|
|
579
|
-
<h2>Workflow Runs</h2>
|
|
580
|
-
<div class="value">{workflow_stats.get("total_runs", 0)}</div>
|
|
581
|
-
<div class="label">{workflow_stats.get("avg_savings_percent", 0):.0f}% savings</div>
|
|
582
|
-
</div>
|
|
583
|
-
|
|
584
|
-
<div class="card success">
|
|
585
|
-
<h2>Total Savings</h2>
|
|
586
|
-
<div class="value">${workflow_stats.get("total_savings", 0):.2f}</div>
|
|
587
|
-
<div class="label">workflows + API</div>
|
|
588
|
-
</div>
|
|
589
|
-
|
|
590
|
-
<div class="card {"success" if test_stats.get("failed_files", 0) == 0 else "warning"}">
|
|
591
|
-
<h2>Test Coverage</h2>
|
|
592
|
-
<div class="value">{test_stats.get("coverage_avg", 0):.0f}%</div>
|
|
593
|
-
<div class="label">{test_stats.get("total_files", 0)} files tracked</div>
|
|
594
|
-
</div>
|
|
595
|
-
</div>
|
|
596
|
-
|
|
597
|
-
<div class="section">
|
|
598
|
-
<h2>Recent Bug Patterns</h2>
|
|
599
|
-
<table>
|
|
600
|
-
<thead>
|
|
601
|
-
<tr>
|
|
602
|
-
<th>Type</th>
|
|
603
|
-
<th>Root Cause</th>
|
|
604
|
-
<th>Status</th>
|
|
605
|
-
<th>Resolved</th>
|
|
606
|
-
</tr>
|
|
607
|
-
</thead>
|
|
608
|
-
<tbody>
|
|
609
|
-
{self._render_bug_table(patterns)}
|
|
610
|
-
</tbody>
|
|
611
|
-
</table>
|
|
612
|
-
</div>
|
|
613
|
-
|
|
614
|
-
<div class="section">
|
|
615
|
-
<h2>Multi-Model Workflows</h2>
|
|
616
|
-
<div class="workflow-grid">
|
|
617
|
-
{self._render_workflow_cards(workflow_stats)}
|
|
618
|
-
</div>
|
|
619
|
-
|
|
620
|
-
<h3 style="margin-bottom: 0.5rem; font-size: 1rem;">Model Tier Usage</h3>
|
|
621
|
-
{self._render_tier_bar(workflow_stats)}
|
|
622
|
-
|
|
623
|
-
<h3 style="margin-top: 1.5rem; margin-bottom: 0.5rem; font-size: 1rem;">Recent Runs</h3>
|
|
624
|
-
{self._render_recent_runs(workflow_stats)}
|
|
625
|
-
</div>
|
|
626
|
-
|
|
627
|
-
<div class="section">
|
|
628
|
-
<h2>Test Tracking</h2>
|
|
629
|
-
{self._render_test_tracking(test_stats)}
|
|
630
|
-
</div>
|
|
631
|
-
|
|
632
|
-
<div class="section">
|
|
633
|
-
<h2>Quick Commands</h2>
|
|
634
|
-
<p>Run these commands for common tasks:</p>
|
|
635
|
-
<div class="commands">
|
|
636
|
-
<span class="command">empathy morning</span>
|
|
637
|
-
<span class="command">empathy ship</span>
|
|
638
|
-
<span class="command">empathy fix-all</span>
|
|
639
|
-
<span class="command">empathy learn</span>
|
|
640
|
-
<span class="command">empathy sync-claude</span>
|
|
641
|
-
<span class="command">empathy costs</span>
|
|
642
|
-
</div>
|
|
643
|
-
</div>
|
|
644
|
-
|
|
645
|
-
<footer>
|
|
646
|
-
<p>Empathy Framework Dashboard - Refresh to update data</p>
|
|
647
|
-
<p>Last updated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</p>
|
|
648
|
-
</footer>
|
|
649
|
-
</div>
|
|
650
|
-
|
|
651
|
-
<script>
|
|
652
|
-
// Auto-refresh every 30 seconds
|
|
653
|
-
setTimeout(() => location.reload(), 30000);
|
|
654
|
-
</script>
|
|
655
|
-
</body>
|
|
656
|
-
</html>"""
|
|
657
|
-
|
|
658
|
-
def _render_bug_table(self, patterns: dict) -> str:
|
|
659
|
-
"""Render bug patterns as table rows."""
|
|
660
|
-
debugging_data = patterns.get("debugging", {})
|
|
661
|
-
if isinstance(debugging_data, dict):
|
|
662
|
-
bugs = debugging_data.get("patterns", [])
|
|
663
|
-
elif isinstance(debugging_data, list):
|
|
664
|
-
bugs = debugging_data
|
|
665
|
-
else:
|
|
666
|
-
bugs = []
|
|
667
|
-
if not bugs:
|
|
668
|
-
return (
|
|
669
|
-
'<tr><td colspan="4">No patterns yet. Run "empathy learn" to get started.</td></tr>'
|
|
670
|
-
)
|
|
671
|
-
|
|
672
|
-
rows = []
|
|
673
|
-
for bug in bugs[-10:]: # Last 10
|
|
674
|
-
status_class = bug.get("status", "investigating")
|
|
675
|
-
root = bug.get("root_cause", "")
|
|
676
|
-
root_display = (root[:60] + "...") if len(root) > 60 else (root or "-")
|
|
677
|
-
resolved = bug.get("resolved_at") or bug.get("timestamp")
|
|
678
|
-
date_display = resolved[:10] if resolved else "-"
|
|
679
|
-
rows.append(
|
|
680
|
-
f"""
|
|
681
|
-
<tr>
|
|
682
|
-
<td>{bug.get("bug_type", "unknown")}</td>
|
|
683
|
-
<td>{root_display}</td>
|
|
684
|
-
<td><span class="status {status_class}">{status_class}</span></td>
|
|
685
|
-
<td>{date_display}</td>
|
|
686
|
-
</tr>
|
|
687
|
-
""",
|
|
688
|
-
)
|
|
689
|
-
|
|
690
|
-
return "".join(rows)
|
|
691
|
-
|
|
692
|
-
def _render_workflow_cards(self, workflow_stats: dict) -> str:
|
|
693
|
-
"""Render workflow stat cards."""
|
|
694
|
-
by_workflow = workflow_stats.get("by_workflow", {})
|
|
695
|
-
|
|
696
|
-
if not by_workflow:
|
|
697
|
-
return '<div class="workflow-card"><p>No workflow runs yet.</p></div>'
|
|
698
|
-
|
|
699
|
-
cards = []
|
|
700
|
-
for name, stats in by_workflow.items():
|
|
701
|
-
runs = stats.get("runs", 0)
|
|
702
|
-
savings = stats.get("savings", 0)
|
|
703
|
-
cards.append(
|
|
704
|
-
f"""
|
|
705
|
-
<div class="workflow-card">
|
|
706
|
-
<h3>{name}</h3>
|
|
707
|
-
<div class="runs">{runs}</div>
|
|
708
|
-
<div class="savings">${savings:.4f} saved</div>
|
|
709
|
-
</div>
|
|
710
|
-
""",
|
|
711
|
-
)
|
|
712
|
-
|
|
713
|
-
return "".join(cards)
|
|
714
|
-
|
|
715
|
-
def _render_tier_bar(self, workflow_stats: dict) -> str:
|
|
716
|
-
"""Render model tier usage bar."""
|
|
717
|
-
by_tier = workflow_stats.get("by_tier", {})
|
|
718
|
-
total = sum(by_tier.values())
|
|
719
|
-
|
|
720
|
-
if total == 0:
|
|
721
|
-
return '<div class="tier-bar"><div class="tier">No data yet</div></div>'
|
|
722
|
-
|
|
723
|
-
cheap_pct = (by_tier.get("cheap", 0) / total) * 100 if total > 0 else 0
|
|
724
|
-
capable_pct = (by_tier.get("capable", 0) / total) * 100 if total > 0 else 0
|
|
725
|
-
premium_pct = (by_tier.get("premium", 0) / total) * 100 if total > 0 else 0
|
|
726
|
-
|
|
727
|
-
return f"""
|
|
728
|
-
<div class="tier-bar">
|
|
729
|
-
<div class="tier cheap" style="width: {cheap_pct}%;">{cheap_pct:.0f}% cheap</div>
|
|
730
|
-
<div class="tier capable" style="width: {capable_pct}%;">{capable_pct:.0f}% capable</div>
|
|
731
|
-
<div class="tier premium" style="width: {premium_pct}%;">{premium_pct:.0f}% premium</div>
|
|
732
|
-
</div>
|
|
733
|
-
<div style="display: flex; gap: 1rem; font-size: 0.75rem; color: var(--text-muted);">
|
|
734
|
-
<span>Cheap (Haiku/GPT-mini): ${by_tier.get("cheap", 0):.4f}</span>
|
|
735
|
-
<span>Capable (Sonnet/GPT-4o): ${by_tier.get("capable", 0):.4f}</span>
|
|
736
|
-
<span>Premium (Opus/GPT-5): ${by_tier.get("premium", 0):.4f}</span>
|
|
737
|
-
</div>
|
|
738
|
-
"""
|
|
739
|
-
|
|
740
|
-
def _render_recent_runs(self, workflow_stats: dict) -> str:
|
|
741
|
-
"""Render recent workflow runs."""
|
|
742
|
-
recent_runs = workflow_stats.get("recent_runs", [])
|
|
743
|
-
|
|
744
|
-
if not recent_runs:
|
|
745
|
-
return '<p style="color: var(--text-muted);">No recent workflow runs.</p>'
|
|
746
|
-
|
|
747
|
-
runs_html = []
|
|
748
|
-
for run in recent_runs[:5]: # Show last 5
|
|
749
|
-
name = run.get("workflow", "unknown")
|
|
750
|
-
provider = run.get("provider", "unknown")
|
|
751
|
-
success = run.get("success", False)
|
|
752
|
-
savings_pct = run.get("savings_percent", 0)
|
|
753
|
-
started = run.get("started_at", "")
|
|
754
|
-
|
|
755
|
-
# Format time
|
|
756
|
-
time_str = started[:16].replace("T", " ") if started else "-"
|
|
757
|
-
|
|
758
|
-
status_icon = "✓" if success else "✗"
|
|
759
|
-
status_color = "var(--success)" if success else "var(--danger)"
|
|
760
|
-
|
|
761
|
-
runs_html.append(
|
|
762
|
-
f"""
|
|
763
|
-
<div class="recent-run">
|
|
764
|
-
<span style="color: {status_color};">{status_icon}</span>
|
|
765
|
-
<span class="name">{name}</span>
|
|
766
|
-
<span class="provider">{provider}</span>
|
|
767
|
-
<span class="result">
|
|
768
|
-
<span class="savings-badge">{savings_pct:.0f}% saved</span>
|
|
769
|
-
<span class="time">{time_str}</span>
|
|
770
|
-
</span>
|
|
771
|
-
</div>
|
|
772
|
-
""",
|
|
773
|
-
)
|
|
774
|
-
|
|
775
|
-
return "".join(runs_html)
|
|
776
|
-
|
|
777
|
-
def _render_test_tracking(self, test_stats: dict) -> str:
|
|
778
|
-
"""Render test tracking section."""
|
|
779
|
-
if "error" in test_stats:
|
|
780
|
-
return f'<p style="color: var(--text-muted);">{test_stats["error"]}</p>'
|
|
781
|
-
|
|
782
|
-
total = test_stats.get("total_files", 0)
|
|
783
|
-
passed = test_stats.get("passed_files", 0)
|
|
784
|
-
failed = test_stats.get("failed_files", 0)
|
|
785
|
-
coverage = test_stats.get("coverage_avg", 0)
|
|
786
|
-
|
|
787
|
-
if total == 0:
|
|
788
|
-
return '<p style="color: var(--text-muted);">No test tracking data yet. Run tests with empathy to start tracking.</p>'
|
|
789
|
-
|
|
790
|
-
# Calculate pass rate
|
|
791
|
-
pass_rate = (passed / total * 100) if total > 0 else 0
|
|
792
|
-
|
|
793
|
-
# Stats grid
|
|
794
|
-
html = f"""
|
|
795
|
-
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 1.5rem;">
|
|
796
|
-
<div style="text-align: center; padding: 1rem; background: var(--bg); border-radius: 8px;">
|
|
797
|
-
<div style="font-size: 1.5rem; font-weight: 600; color: var(--success);">{passed}</div>
|
|
798
|
-
<div style="font-size: 0.75rem; color: var(--text-muted);">Passing</div>
|
|
799
|
-
</div>
|
|
800
|
-
<div style="text-align: center; padding: 1rem; background: var(--bg); border-radius: 8px;">
|
|
801
|
-
<div style="font-size: 1.5rem; font-weight: 600; color: var(--danger);">{failed}</div>
|
|
802
|
-
<div style="font-size: 0.75rem; color: var(--text-muted);">Failing</div>
|
|
803
|
-
</div>
|
|
804
|
-
<div style="text-align: center; padding: 1rem; background: var(--bg); border-radius: 8px;">
|
|
805
|
-
<div style="font-size: 1.5rem; font-weight: 600; color: var(--primary);">{pass_rate:.0f}%</div>
|
|
806
|
-
<div style="font-size: 0.75rem; color: var(--text-muted);">Pass Rate</div>
|
|
807
|
-
</div>
|
|
808
|
-
<div style="text-align: center; padding: 1rem; background: var(--bg); border-radius: 8px;">
|
|
809
|
-
<div style="font-size: 1.5rem; font-weight: 600; color: var(--warning);">{coverage:.0f}%</div>
|
|
810
|
-
<div style="font-size: 0.75rem; color: var(--text-muted);">Coverage</div>
|
|
811
|
-
</div>
|
|
812
|
-
</div>
|
|
813
|
-
"""
|
|
814
|
-
|
|
815
|
-
# Files needing attention
|
|
816
|
-
files_needing = test_stats.get("files_needing_tests", [])
|
|
817
|
-
if files_needing:
|
|
818
|
-
html += """
|
|
819
|
-
<h3 style="margin-bottom: 0.5rem; font-size: 1rem;">Files Needing Attention</h3>
|
|
820
|
-
<table>
|
|
821
|
-
<thead>
|
|
822
|
-
<tr>
|
|
823
|
-
<th>File</th>
|
|
824
|
-
<th>Status</th>
|
|
825
|
-
<th>Last Run</th>
|
|
826
|
-
</tr>
|
|
827
|
-
</thead>
|
|
828
|
-
<tbody>
|
|
829
|
-
"""
|
|
830
|
-
for file_record in files_needing[:5]:
|
|
831
|
-
file_path = file_record.get("file_path", "unknown")
|
|
832
|
-
# Truncate long paths
|
|
833
|
-
display_path = ("..." + file_path[-40:]) if len(file_path) > 40 else file_path
|
|
834
|
-
result = file_record.get("last_test_result", "unknown")
|
|
835
|
-
timestamp = (
|
|
836
|
-
file_record.get("timestamp", "")[:10] if file_record.get("timestamp") else "-"
|
|
837
|
-
)
|
|
838
|
-
status_class = "resolved" if result == "passed" else "investigating"
|
|
839
|
-
html += f"""
|
|
840
|
-
<tr>
|
|
841
|
-
<td title="{file_path}">{display_path}</td>
|
|
842
|
-
<td><span class="status {status_class}">{result}</span></td>
|
|
843
|
-
<td>{timestamp}</td>
|
|
844
|
-
</tr>
|
|
845
|
-
"""
|
|
846
|
-
html += "</tbody></table>"
|
|
847
|
-
|
|
848
|
-
# Recent test executions
|
|
849
|
-
recent = test_stats.get("recent_executions", [])
|
|
850
|
-
if recent:
|
|
851
|
-
html += """
|
|
852
|
-
<h3 style="margin-top: 1.5rem; margin-bottom: 0.5rem; font-size: 1rem;">Recent Test Runs</h3>
|
|
853
|
-
"""
|
|
854
|
-
for execution in recent[:5]:
|
|
855
|
-
suite = execution.get("test_suite", "unknown")
|
|
856
|
-
total_tests = execution.get("total_tests", 0)
|
|
857
|
-
exec_passed = execution.get("passed", 0)
|
|
858
|
-
exec_failed = execution.get("failed", 0)
|
|
859
|
-
duration = execution.get("duration_seconds", 0)
|
|
860
|
-
timestamp = (
|
|
861
|
-
execution.get("timestamp", "")[:16].replace("T", " ")
|
|
862
|
-
if execution.get("timestamp")
|
|
863
|
-
else "-"
|
|
864
|
-
)
|
|
865
|
-
success = execution.get("success", False)
|
|
866
|
-
|
|
867
|
-
status_icon = "✓" if success else "✗"
|
|
868
|
-
status_color = "var(--success)" if success else "var(--danger)"
|
|
869
|
-
|
|
870
|
-
html += f"""
|
|
871
|
-
<div class="recent-run">
|
|
872
|
-
<span style="color: {status_color};">{status_icon}</span>
|
|
873
|
-
<span class="name">{suite}</span>
|
|
874
|
-
<span class="provider">{total_tests} tests</span>
|
|
875
|
-
<span class="result">
|
|
876
|
-
<span style="color: var(--success);">{exec_passed} passed</span>
|
|
877
|
-
{f'<span style="color: var(--danger);">{exec_failed} failed</span>' if exec_failed > 0 else ""}
|
|
878
|
-
<span class="time">{duration:.1f}s | {timestamp}</span>
|
|
879
|
-
</span>
|
|
880
|
-
</div>
|
|
881
|
-
"""
|
|
882
|
-
|
|
883
|
-
return html
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
class ThreadedHTTPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
|
887
|
-
"""Threaded HTTP server."""
|
|
888
|
-
|
|
889
|
-
allow_reuse_address = True
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
def run_dashboard(
|
|
893
|
-
port: int = 8765,
|
|
894
|
-
patterns_dir: str = "./patterns",
|
|
895
|
-
empathy_dir: str = ".empathy",
|
|
896
|
-
open_browser: bool = True,
|
|
897
|
-
) -> None:
|
|
898
|
-
"""Run the dashboard server.
|
|
899
|
-
|
|
900
|
-
Args:
|
|
901
|
-
port: Port to run on (default: 8765)
|
|
902
|
-
patterns_dir: Path to patterns directory
|
|
903
|
-
empathy_dir: Path to empathy data directory
|
|
904
|
-
open_browser: Open browser automatically
|
|
905
|
-
|
|
906
|
-
"""
|
|
907
|
-
# Configure handler
|
|
908
|
-
DashboardHandler.patterns_dir = patterns_dir
|
|
909
|
-
DashboardHandler.empathy_dir = empathy_dir
|
|
910
|
-
|
|
911
|
-
url = f"http://localhost:{port}"
|
|
912
|
-
|
|
913
|
-
print("\n Empathy Dashboard")
|
|
914
|
-
print(f" Running at: {url}")
|
|
915
|
-
print(" Press Ctrl+C to stop\n")
|
|
916
|
-
|
|
917
|
-
if open_browser:
|
|
918
|
-
# Open browser in a separate thread to not block server start
|
|
919
|
-
threading.Timer(0.5, lambda: webbrowser.open(url)).start()
|
|
920
|
-
|
|
921
|
-
try:
|
|
922
|
-
with ThreadedHTTPServer(("", port), DashboardHandler) as httpd:
|
|
923
|
-
httpd.serve_forever()
|
|
924
|
-
except KeyboardInterrupt:
|
|
925
|
-
print("\n Dashboard stopped.")
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
def cmd_dashboard(args):
|
|
929
|
-
"""CLI command handler for dashboard."""
|
|
930
|
-
port = getattr(args, "port", 8765)
|
|
931
|
-
patterns_dir = getattr(args, "patterns_dir", "./patterns")
|
|
932
|
-
empathy_dir = getattr(args, "empathy_dir", ".empathy")
|
|
933
|
-
no_browser = getattr(args, "no_browser", False)
|
|
934
|
-
|
|
935
|
-
run_dashboard(
|
|
936
|
-
port=port,
|
|
937
|
-
patterns_dir=patterns_dir,
|
|
938
|
-
empathy_dir=empathy_dir,
|
|
939
|
-
open_browser=not no_browser,
|
|
940
|
-
)
|
|
941
|
-
return 0
|