empathy-framework 5.1.1__py3-none-any.whl → 5.2.1__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-5.1.1.dist-info → empathy_framework-5.2.1.dist-info}/METADATA +52 -3
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.2.1.dist-info}/RECORD +69 -28
- empathy_os/cli_router.py +9 -0
- empathy_os/core_modules/__init__.py +15 -0
- empathy_os/mcp/__init__.py +10 -0
- empathy_os/mcp/server.py +506 -0
- empathy_os/memory/control_panel.py +1 -131
- empathy_os/memory/control_panel_support.py +145 -0
- empathy_os/memory/encryption.py +159 -0
- empathy_os/memory/long_term.py +41 -626
- empathy_os/memory/long_term_types.py +99 -0
- empathy_os/memory/mixins/__init__.py +25 -0
- empathy_os/memory/mixins/backend_init_mixin.py +244 -0
- empathy_os/memory/mixins/capabilities_mixin.py +199 -0
- empathy_os/memory/mixins/handoff_mixin.py +208 -0
- empathy_os/memory/mixins/lifecycle_mixin.py +49 -0
- empathy_os/memory/mixins/long_term_mixin.py +352 -0
- empathy_os/memory/mixins/promotion_mixin.py +109 -0
- empathy_os/memory/mixins/short_term_mixin.py +182 -0
- empathy_os/memory/short_term.py +7 -0
- empathy_os/memory/simple_storage.py +302 -0
- empathy_os/memory/storage_backend.py +167 -0
- empathy_os/memory/unified.py +21 -1120
- empathy_os/meta_workflows/cli_commands/__init__.py +56 -0
- empathy_os/meta_workflows/cli_commands/agent_commands.py +321 -0
- empathy_os/meta_workflows/cli_commands/analytics_commands.py +442 -0
- empathy_os/meta_workflows/cli_commands/config_commands.py +232 -0
- empathy_os/meta_workflows/cli_commands/memory_commands.py +182 -0
- empathy_os/meta_workflows/cli_commands/template_commands.py +354 -0
- empathy_os/meta_workflows/cli_commands/workflow_commands.py +382 -0
- empathy_os/meta_workflows/cli_meta_workflows.py +52 -1802
- empathy_os/models/telemetry/__init__.py +71 -0
- empathy_os/models/telemetry/analytics.py +594 -0
- empathy_os/models/telemetry/backend.py +196 -0
- empathy_os/models/telemetry/data_models.py +431 -0
- empathy_os/models/telemetry/storage.py +489 -0
- empathy_os/orchestration/__init__.py +35 -0
- empathy_os/orchestration/execution_strategies.py +481 -0
- empathy_os/orchestration/meta_orchestrator.py +488 -1
- empathy_os/routing/workflow_registry.py +36 -0
- empathy_os/telemetry/cli.py +19 -724
- empathy_os/telemetry/commands/__init__.py +14 -0
- empathy_os/telemetry/commands/dashboard_commands.py +696 -0
- empathy_os/tools.py +183 -0
- empathy_os/workflows/__init__.py +5 -0
- empathy_os/workflows/autonomous_test_gen.py +860 -161
- empathy_os/workflows/base.py +6 -2
- empathy_os/workflows/code_review.py +4 -1
- empathy_os/workflows/document_gen/__init__.py +25 -0
- empathy_os/workflows/document_gen/config.py +30 -0
- empathy_os/workflows/document_gen/report_formatter.py +162 -0
- empathy_os/workflows/document_gen/workflow.py +1426 -0
- empathy_os/workflows/document_gen.py +22 -1598
- empathy_os/workflows/security_audit.py +2 -2
- empathy_os/workflows/security_audit_phase3.py +7 -4
- empathy_os/workflows/seo_optimization.py +633 -0
- empathy_os/workflows/test_gen/__init__.py +52 -0
- empathy_os/workflows/test_gen/ast_analyzer.py +249 -0
- empathy_os/workflows/test_gen/config.py +88 -0
- empathy_os/workflows/test_gen/data_models.py +38 -0
- empathy_os/workflows/test_gen/report_formatter.py +289 -0
- empathy_os/workflows/test_gen/test_templates.py +381 -0
- empathy_os/workflows/test_gen/workflow.py +655 -0
- empathy_os/workflows/test_gen.py +42 -1905
- empathy_os/memory/types 2.py +0 -441
- empathy_os/models/telemetry.py +0 -1660
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.2.1.dist-info}/WHEEL +0 -0
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.2.1.dist-info}/entry_points.txt +0 -0
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.2.1.dist-info}/licenses/LICENSE +0 -0
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.2.1.dist-info}/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +0 -0
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.2.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
"""Dashboard command implementations for telemetry CLI.
|
|
2
|
+
|
|
3
|
+
Provides interactive HTML dashboard generation for telemetry data.
|
|
4
|
+
|
|
5
|
+
Copyright 2025 Smart-AI-Memory
|
|
6
|
+
Licensed under Fair Source License 0.9
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import http.server
|
|
10
|
+
import socketserver
|
|
11
|
+
import tempfile
|
|
12
|
+
import webbrowser
|
|
13
|
+
from collections import Counter
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from ..usage_tracker import UsageTracker
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def cmd_telemetry_dashboard(args: Any) -> int:
|
|
21
|
+
"""Open interactive telemetry dashboard in browser.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
args: Parsed command-line arguments
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Exit code (0 for success)
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
tracker = UsageTracker.get_instance()
|
|
31
|
+
entries = tracker.export_to_dict(days=getattr(args, "days", 30))
|
|
32
|
+
|
|
33
|
+
if not entries:
|
|
34
|
+
print("No telemetry data available.")
|
|
35
|
+
return 0
|
|
36
|
+
|
|
37
|
+
# Calculate statistics
|
|
38
|
+
total_cost = sum(e.get("cost", 0) for e in entries)
|
|
39
|
+
total_calls = len(entries)
|
|
40
|
+
avg_duration = (
|
|
41
|
+
sum(e.get("duration_ms", 0) for e in entries) / total_calls if total_calls > 0 else 0
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Tier distribution
|
|
45
|
+
tiers = [e.get("tier", "UNKNOWN") for e in entries]
|
|
46
|
+
tier_counts = Counter(tiers)
|
|
47
|
+
tier_distribution = {tier: (count / total_calls) * 100 for tier, count in tier_counts.items()}
|
|
48
|
+
|
|
49
|
+
# Calculate savings (baseline: all PREMIUM tier)
|
|
50
|
+
premium_input_cost = 0.015 / 1000 # per token
|
|
51
|
+
premium_output_cost = 0.075 / 1000 # per token
|
|
52
|
+
|
|
53
|
+
baseline_cost = sum(
|
|
54
|
+
(e.get("tokens", {}).get("input", 0) * premium_input_cost)
|
|
55
|
+
+ (e.get("tokens", {}).get("output", 0) * premium_output_cost)
|
|
56
|
+
for e in entries
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
saved = baseline_cost - total_cost
|
|
60
|
+
savings_pct = (saved / baseline_cost * 100) if baseline_cost > 0 else 0
|
|
61
|
+
|
|
62
|
+
# Generate HTML
|
|
63
|
+
html_content = f"""<!DOCTYPE html>
|
|
64
|
+
<html lang="en">
|
|
65
|
+
<head>
|
|
66
|
+
<meta charset="UTF-8">
|
|
67
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
68
|
+
<title>Empathy Telemetry Dashboard</title>
|
|
69
|
+
<style>
|
|
70
|
+
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
71
|
+
body {{
|
|
72
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
73
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
74
|
+
padding: 20px;
|
|
75
|
+
min-height: 100vh;
|
|
76
|
+
}}
|
|
77
|
+
.container {{
|
|
78
|
+
max-width: 1400px;
|
|
79
|
+
margin: 0 auto;
|
|
80
|
+
}}
|
|
81
|
+
.header {{
|
|
82
|
+
color: white;
|
|
83
|
+
text-align: center;
|
|
84
|
+
margin-bottom: 40px;
|
|
85
|
+
}}
|
|
86
|
+
.header h1 {{
|
|
87
|
+
font-size: 48px;
|
|
88
|
+
font-weight: 700;
|
|
89
|
+
margin-bottom: 10px;
|
|
90
|
+
}}
|
|
91
|
+
.header p {{
|
|
92
|
+
font-size: 18px;
|
|
93
|
+
opacity: 0.9;
|
|
94
|
+
}}
|
|
95
|
+
.stats-grid {{
|
|
96
|
+
display: grid;
|
|
97
|
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
98
|
+
gap: 20px;
|
|
99
|
+
margin-bottom: 30px;
|
|
100
|
+
}}
|
|
101
|
+
.stat-card {{
|
|
102
|
+
background: white;
|
|
103
|
+
border-radius: 12px;
|
|
104
|
+
padding: 30px;
|
|
105
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
106
|
+
}}
|
|
107
|
+
.savings-card {{
|
|
108
|
+
grid-column: span 2;
|
|
109
|
+
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
|
|
110
|
+
color: white;
|
|
111
|
+
}}
|
|
112
|
+
.stat-label {{
|
|
113
|
+
font-size: 14px;
|
|
114
|
+
text-transform: uppercase;
|
|
115
|
+
letter-spacing: 1px;
|
|
116
|
+
margin-bottom: 10px;
|
|
117
|
+
opacity: 0.8;
|
|
118
|
+
}}
|
|
119
|
+
.stat-value {{
|
|
120
|
+
font-size: 56px;
|
|
121
|
+
font-weight: 700;
|
|
122
|
+
margin-bottom: 5px;
|
|
123
|
+
}}
|
|
124
|
+
.stat-sublabel {{
|
|
125
|
+
font-size: 16px;
|
|
126
|
+
opacity: 0.7;
|
|
127
|
+
}}
|
|
128
|
+
.tier-distribution {{
|
|
129
|
+
display: flex;
|
|
130
|
+
gap: 10px;
|
|
131
|
+
margin-top: 15px;
|
|
132
|
+
height: 50px;
|
|
133
|
+
}}
|
|
134
|
+
.tier-bar {{
|
|
135
|
+
flex: 1;
|
|
136
|
+
display: flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
justify-content: center;
|
|
139
|
+
border-radius: 8px;
|
|
140
|
+
font-weight: 600;
|
|
141
|
+
color: white;
|
|
142
|
+
font-size: 14px;
|
|
143
|
+
}}
|
|
144
|
+
.tier-premium {{ background: linear-gradient(135deg, #9c27b0, #7b1fa2); }}
|
|
145
|
+
.tier-capable {{ background: linear-gradient(135deg, #2196f3, #1976d2); }}
|
|
146
|
+
.tier-cheap {{ background: linear-gradient(135deg, #4caf50, #388e3c); }}
|
|
147
|
+
table {{
|
|
148
|
+
width: 100%;
|
|
149
|
+
background: white;
|
|
150
|
+
border-radius: 12px;
|
|
151
|
+
overflow: hidden;
|
|
152
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
153
|
+
}}
|
|
154
|
+
th, td {{
|
|
155
|
+
padding: 16px;
|
|
156
|
+
text-align: left;
|
|
157
|
+
}}
|
|
158
|
+
th {{
|
|
159
|
+
background: #f5f5f5;
|
|
160
|
+
font-weight: 600;
|
|
161
|
+
font-size: 13px;
|
|
162
|
+
text-transform: uppercase;
|
|
163
|
+
letter-spacing: 0.5px;
|
|
164
|
+
color: #666;
|
|
165
|
+
}}
|
|
166
|
+
tr:hover {{
|
|
167
|
+
background: #f9f9f9;
|
|
168
|
+
}}
|
|
169
|
+
.tier-badge {{
|
|
170
|
+
display: inline-block;
|
|
171
|
+
padding: 4px 10px;
|
|
172
|
+
border-radius: 4px;
|
|
173
|
+
font-size: 11px;
|
|
174
|
+
font-weight: 600;
|
|
175
|
+
color: white;
|
|
176
|
+
}}
|
|
177
|
+
.badge-premium {{ background: #9c27b0; }}
|
|
178
|
+
.badge-capable {{ background: #2196f3; }}
|
|
179
|
+
.badge-cheap {{ background: #4caf50; }}
|
|
180
|
+
.cache-hit {{ color: #4caf50; font-weight: 600; }}
|
|
181
|
+
.cache-miss {{ color: #999; }}
|
|
182
|
+
</style>
|
|
183
|
+
</head>
|
|
184
|
+
<body>
|
|
185
|
+
<div class="container">
|
|
186
|
+
<div class="header">
|
|
187
|
+
<h1>📊 Empathy Telemetry Dashboard</h1>
|
|
188
|
+
<p>Last {len(entries)} LLM API calls • Real-time cost tracking</p>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<div class="stats-grid">
|
|
192
|
+
<div class="stat-card savings-card">
|
|
193
|
+
<div class="stat-label">Cost Savings (Tier Routing)</div>
|
|
194
|
+
<div class="stat-value">${saved:.2f}</div>
|
|
195
|
+
<div class="stat-sublabel">
|
|
196
|
+
{savings_pct:.1f}% saved • Baseline: ${baseline_cost:.2f} • Actual: ${
|
|
197
|
+
total_cost:.2f}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<div class="stat-card">
|
|
202
|
+
<div class="stat-label">Total Cost</div>
|
|
203
|
+
<div class="stat-value">${total_cost:.2f}</div>
|
|
204
|
+
<div class="stat-sublabel">{total_calls} API calls</div>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<div class="stat-card">
|
|
208
|
+
<div class="stat-label">Avg Duration</div>
|
|
209
|
+
<div class="stat-value">{avg_duration / 1000:.1f}s</div>
|
|
210
|
+
<div class="stat-sublabel">Per API call</div>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<div class="stat-card">
|
|
215
|
+
<div class="stat-label">Tier Distribution</div>
|
|
216
|
+
<div class="tier-distribution">
|
|
217
|
+
{
|
|
218
|
+
"".join(
|
|
219
|
+
f'<div class="tier-bar tier-{tier.lower()}">{tier}: {pct:.1f}%</div>'
|
|
220
|
+
for tier, pct in tier_distribution.items()
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<h2 style="color: white; margin: 40px 0 20px 0; font-size: 28px;">Recent LLM Calls</h2>
|
|
227
|
+
<table>
|
|
228
|
+
<thead>
|
|
229
|
+
<tr>
|
|
230
|
+
<th>Time</th>
|
|
231
|
+
<th>Workflow</th>
|
|
232
|
+
<th>Stage</th>
|
|
233
|
+
<th>Tier</th>
|
|
234
|
+
<th>Cost</th>
|
|
235
|
+
<th>Tokens</th>
|
|
236
|
+
<th>Cache</th>
|
|
237
|
+
<th>Duration</th>
|
|
238
|
+
</tr>
|
|
239
|
+
</thead>
|
|
240
|
+
<tbody>
|
|
241
|
+
{
|
|
242
|
+
"".join(
|
|
243
|
+
f'''<tr>
|
|
244
|
+
<td>{datetime.fromisoformat(e.get("ts", "").replace("Z", "+00:00")).strftime("%H:%M:%S")}</td>
|
|
245
|
+
<td>{e.get("workflow", "")}</td>
|
|
246
|
+
<td>{e.get("stage", "")}</td>
|
|
247
|
+
<td><span class="tier-badge badge-{e.get("tier", "").lower()}">{e.get("tier", "")}</span></td>
|
|
248
|
+
<td>${e.get("cost", 0):.4f}</td>
|
|
249
|
+
<td>{e.get("tokens", {}).get("input", 0)}/{e.get("tokens", {}).get("output", 0)}</td>
|
|
250
|
+
<td class="cache-{"hit" if e.get("cache", {}).get("hit") else "miss"}">
|
|
251
|
+
{"HIT" if e.get("cache", {}).get("hit") else "MISS"}
|
|
252
|
+
</td>
|
|
253
|
+
<td>{e.get("duration_ms", 0) / 1000:.1f}s</td>
|
|
254
|
+
</tr>'''
|
|
255
|
+
for e in list(reversed(entries))[:20]
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
</tbody>
|
|
259
|
+
</table>
|
|
260
|
+
</div>
|
|
261
|
+
</body>
|
|
262
|
+
</html>"""
|
|
263
|
+
|
|
264
|
+
# Write to temp file
|
|
265
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
|
|
266
|
+
f.write(html_content)
|
|
267
|
+
temp_path = f.name
|
|
268
|
+
|
|
269
|
+
print(f"📊 Opening dashboard in browser: {temp_path}")
|
|
270
|
+
webbrowser.open(f"file://{temp_path}")
|
|
271
|
+
|
|
272
|
+
return 0
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def cmd_file_test_dashboard(args: Any) -> int:
|
|
276
|
+
"""Open interactive file test status dashboard in browser.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
args: Parsed command-line arguments
|
|
280
|
+
- port: Port to serve on (default: 8765)
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Exit code (0 for success)
|
|
284
|
+
"""
|
|
285
|
+
from empathy_os.models.telemetry import get_telemetry_store
|
|
286
|
+
|
|
287
|
+
port = getattr(args, "port", 8765)
|
|
288
|
+
|
|
289
|
+
def generate_dashboard_html() -> str:
|
|
290
|
+
"""Generate the dashboard HTML with current data."""
|
|
291
|
+
store = get_telemetry_store()
|
|
292
|
+
all_records = store.get_file_tests(limit=100000)
|
|
293
|
+
|
|
294
|
+
if not all_records:
|
|
295
|
+
return _generate_empty_dashboard()
|
|
296
|
+
|
|
297
|
+
# Get latest record per file
|
|
298
|
+
latest_by_file: dict[str, Any] = {}
|
|
299
|
+
for record in all_records:
|
|
300
|
+
existing = latest_by_file.get(record.file_path)
|
|
301
|
+
if existing is None or record.timestamp > existing.timestamp:
|
|
302
|
+
latest_by_file[record.file_path] = record
|
|
303
|
+
|
|
304
|
+
records = list(latest_by_file.values())
|
|
305
|
+
|
|
306
|
+
# Calculate stats
|
|
307
|
+
total = len(records)
|
|
308
|
+
passed = sum(1 for r in records if r.last_test_result == "passed")
|
|
309
|
+
failed = sum(1 for r in records if r.last_test_result in ("failed", "error"))
|
|
310
|
+
no_tests = sum(1 for r in records if r.last_test_result == "no_tests")
|
|
311
|
+
stale = sum(1 for r in records if r.is_stale)
|
|
312
|
+
|
|
313
|
+
# Sort by status priority: failed > stale > no_tests > passed
|
|
314
|
+
def sort_key(r):
|
|
315
|
+
if r.last_test_result in ("failed", "error"):
|
|
316
|
+
return (0, r.file_path)
|
|
317
|
+
if r.is_stale:
|
|
318
|
+
return (1, r.file_path)
|
|
319
|
+
if r.last_test_result == "no_tests":
|
|
320
|
+
return (2, r.file_path)
|
|
321
|
+
return (3, r.file_path)
|
|
322
|
+
|
|
323
|
+
records.sort(key=sort_key)
|
|
324
|
+
|
|
325
|
+
# Generate table rows
|
|
326
|
+
rows_html = ""
|
|
327
|
+
for record in records:
|
|
328
|
+
result = record.last_test_result
|
|
329
|
+
if result == "passed":
|
|
330
|
+
status_class = "passed"
|
|
331
|
+
status_icon = "✅"
|
|
332
|
+
elif result in ("failed", "error"):
|
|
333
|
+
status_class = "failed"
|
|
334
|
+
status_icon = "❌"
|
|
335
|
+
elif result == "no_tests":
|
|
336
|
+
status_class = "no-tests"
|
|
337
|
+
status_icon = "⚠️"
|
|
338
|
+
else:
|
|
339
|
+
status_class = "skipped"
|
|
340
|
+
status_icon = "⏭️"
|
|
341
|
+
|
|
342
|
+
stale_badge = '<span class="badge stale">STALE</span>' if record.is_stale else ""
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
dt = datetime.fromisoformat(record.timestamp.rstrip("Z"))
|
|
346
|
+
ts_display = dt.strftime("%Y-%m-%d %H:%M")
|
|
347
|
+
except (ValueError, AttributeError):
|
|
348
|
+
ts_display = record.timestamp[:16] if record.timestamp else "-"
|
|
349
|
+
|
|
350
|
+
rows_html += f"""
|
|
351
|
+
<tr class="{status_class}">
|
|
352
|
+
<td class="file-path">{record.file_path}</td>
|
|
353
|
+
<td class="status">{status_icon} {result.upper()} {stale_badge}</td>
|
|
354
|
+
<td class="numeric">{record.test_count}</td>
|
|
355
|
+
<td class="numeric passed-count">{record.passed}</td>
|
|
356
|
+
<td class="numeric failed-count">{record.failed + record.errors}</td>
|
|
357
|
+
<td class="numeric">{record.duration_seconds:.1f}s</td>
|
|
358
|
+
<td class="timestamp">{ts_display}</td>
|
|
359
|
+
</tr>
|
|
360
|
+
"""
|
|
361
|
+
|
|
362
|
+
return """<!DOCTYPE html>
|
|
363
|
+
<html lang="en">
|
|
364
|
+
<head>
|
|
365
|
+
<meta charset="UTF-8">
|
|
366
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
367
|
+
<title>File Test Status Dashboard</title>
|
|
368
|
+
<style>
|
|
369
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
370
|
+
body {
|
|
371
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
372
|
+
background: #ffffff;
|
|
373
|
+
color: #333;
|
|
374
|
+
padding: 20px;
|
|
375
|
+
min-height: 100vh;
|
|
376
|
+
}
|
|
377
|
+
.container { max-width: 1600px; margin: 0 auto; }
|
|
378
|
+
.header {
|
|
379
|
+
display: flex;
|
|
380
|
+
justify-content: space-between;
|
|
381
|
+
align-items: center;
|
|
382
|
+
margin-bottom: 30px;
|
|
383
|
+
padding-bottom: 20px;
|
|
384
|
+
border-bottom: 1px solid #e0e0e0;
|
|
385
|
+
}
|
|
386
|
+
.header h1 { font-size: 28px; color: #333; }
|
|
387
|
+
.refresh-btn {
|
|
388
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
389
|
+
color: white;
|
|
390
|
+
border: none;
|
|
391
|
+
padding: 12px 24px;
|
|
392
|
+
border-radius: 8px;
|
|
393
|
+
font-size: 16px;
|
|
394
|
+
cursor: pointer;
|
|
395
|
+
display: flex;
|
|
396
|
+
align-items: center;
|
|
397
|
+
gap: 8px;
|
|
398
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
399
|
+
}
|
|
400
|
+
.refresh-btn:hover {
|
|
401
|
+
transform: translateY(-2px);
|
|
402
|
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
403
|
+
}
|
|
404
|
+
.refresh-btn:active { transform: translateY(0); }
|
|
405
|
+
.refresh-btn.spinning .icon { animation: spin 1s linear infinite; }
|
|
406
|
+
@keyframes spin { 100% { transform: rotate(360deg); } }
|
|
407
|
+
.stats {
|
|
408
|
+
display: grid;
|
|
409
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
410
|
+
gap: 20px;
|
|
411
|
+
margin-bottom: 30px;
|
|
412
|
+
}
|
|
413
|
+
.stat-card {
|
|
414
|
+
background: #f8f9fa;
|
|
415
|
+
border-radius: 12px;
|
|
416
|
+
padding: 20px;
|
|
417
|
+
text-align: center;
|
|
418
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
|
419
|
+
}
|
|
420
|
+
.stat-card.passed { border-left: 4px solid #22c55e; }
|
|
421
|
+
.stat-card.failed { border-left: 4px solid #ef4444; }
|
|
422
|
+
.stat-card.no-tests { border-left: 4px solid #f59e0b; }
|
|
423
|
+
.stat-card.stale { border-left: 4px solid #8b5cf6; }
|
|
424
|
+
.stat-card.total { border-left: 4px solid #3b82f6; }
|
|
425
|
+
.stat-value { font-size: 36px; font-weight: bold; }
|
|
426
|
+
.stat-label { font-size: 14px; color: #666; margin-top: 5px; }
|
|
427
|
+
.stat-card.passed .stat-value { color: #22c55e; }
|
|
428
|
+
.stat-card.failed .stat-value { color: #ef4444; }
|
|
429
|
+
.stat-card.no-tests .stat-value { color: #f59e0b; }
|
|
430
|
+
.stat-card.stale .stat-value { color: #8b5cf6; }
|
|
431
|
+
.stat-card.total .stat-value { color: #3b82f6; }
|
|
432
|
+
.filter-bar {
|
|
433
|
+
display: flex;
|
|
434
|
+
gap: 10px;
|
|
435
|
+
margin-bottom: 20px;
|
|
436
|
+
flex-wrap: wrap;
|
|
437
|
+
}
|
|
438
|
+
.filter-btn {
|
|
439
|
+
background: #f8f9fa;
|
|
440
|
+
color: #666;
|
|
441
|
+
border: 1px solid #e0e0e0;
|
|
442
|
+
padding: 8px 16px;
|
|
443
|
+
border-radius: 6px;
|
|
444
|
+
cursor: pointer;
|
|
445
|
+
transition: all 0.2s;
|
|
446
|
+
}
|
|
447
|
+
.filter-btn:hover, .filter-btn.active {
|
|
448
|
+
background: #667eea;
|
|
449
|
+
color: #fff;
|
|
450
|
+
border-color: #667eea;
|
|
451
|
+
}
|
|
452
|
+
.search-input {
|
|
453
|
+
flex: 1;
|
|
454
|
+
min-width: 200px;
|
|
455
|
+
background: #fff;
|
|
456
|
+
border: 1px solid #e0e0e0;
|
|
457
|
+
color: #333;
|
|
458
|
+
padding: 8px 16px;
|
|
459
|
+
border-radius: 6px;
|
|
460
|
+
font-size: 14px;
|
|
461
|
+
}
|
|
462
|
+
.search-input:focus {
|
|
463
|
+
outline: none;
|
|
464
|
+
border-color: #667eea;
|
|
465
|
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
466
|
+
}
|
|
467
|
+
table {
|
|
468
|
+
width: 100%;
|
|
469
|
+
border-collapse: collapse;
|
|
470
|
+
background: #fff;
|
|
471
|
+
border-radius: 12px;
|
|
472
|
+
overflow: hidden;
|
|
473
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
474
|
+
}
|
|
475
|
+
th, td { padding: 12px 16px; text-align: left; }
|
|
476
|
+
th {
|
|
477
|
+
background: #f8f9fa;
|
|
478
|
+
font-weight: 600;
|
|
479
|
+
color: #333;
|
|
480
|
+
position: sticky;
|
|
481
|
+
top: 0;
|
|
482
|
+
border-bottom: 2px solid #e0e0e0;
|
|
483
|
+
}
|
|
484
|
+
tr { border-bottom: 1px solid #f0f0f0; }
|
|
485
|
+
tr:hover { background: #f8f9fa; }
|
|
486
|
+
tr.failed { background: rgba(239, 68, 68, 0.08); }
|
|
487
|
+
tr.no-tests { background: rgba(245, 158, 11, 0.05); }
|
|
488
|
+
.file-path { font-family: 'Monaco', 'Menlo', monospace; font-size: 13px; color: #333; }
|
|
489
|
+
.numeric { text-align: right; font-family: monospace; }
|
|
490
|
+
.passed-count { color: #22c55e; }
|
|
491
|
+
.failed-count { color: #ef4444; }
|
|
492
|
+
.timestamp { color: #888; font-size: 12px; }
|
|
493
|
+
.badge {
|
|
494
|
+
display: inline-block;
|
|
495
|
+
padding: 2px 8px;
|
|
496
|
+
border-radius: 4px;
|
|
497
|
+
font-size: 10px;
|
|
498
|
+
font-weight: bold;
|
|
499
|
+
margin-left: 8px;
|
|
500
|
+
}
|
|
501
|
+
.badge.stale { background: #8b5cf6; color: #fff; }
|
|
502
|
+
.hidden { display: none; }
|
|
503
|
+
.last-updated { color: #888; font-size: 12px; margin-top: 20px; text-align: center; }
|
|
504
|
+
</style>
|
|
505
|
+
</head>
|
|
506
|
+
<body>
|
|
507
|
+
<div class="container">
|
|
508
|
+
<div class="header">
|
|
509
|
+
<h1>📊 File Test Status Dashboard</h1>
|
|
510
|
+
<button class="refresh-btn" onclick="refreshData()">
|
|
511
|
+
<span class="icon">🔄</span>
|
|
512
|
+
<span>Refresh</span>
|
|
513
|
+
</button>
|
|
514
|
+
</div>
|
|
515
|
+
|
|
516
|
+
<div class="stats">
|
|
517
|
+
<div class="stat-card total">
|
|
518
|
+
<div class="stat-value">""" + str(total) + """</div>
|
|
519
|
+
<div class="stat-label">Total Files</div>
|
|
520
|
+
</div>
|
|
521
|
+
<div class="stat-card passed">
|
|
522
|
+
<div class="stat-value">""" + str(passed) + """</div>
|
|
523
|
+
<div class="stat-label">Passed</div>
|
|
524
|
+
</div>
|
|
525
|
+
<div class="stat-card failed">
|
|
526
|
+
<div class="stat-value">""" + str(failed) + """</div>
|
|
527
|
+
<div class="stat-label">Failed</div>
|
|
528
|
+
</div>
|
|
529
|
+
<div class="stat-card no-tests">
|
|
530
|
+
<div class="stat-value">""" + str(no_tests) + """</div>
|
|
531
|
+
<div class="stat-label">No Tests</div>
|
|
532
|
+
</div>
|
|
533
|
+
<div class="stat-card stale">
|
|
534
|
+
<div class="stat-value">""" + str(stale) + """</div>
|
|
535
|
+
<div class="stat-label">Stale</div>
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
|
|
539
|
+
<div class="filter-bar">
|
|
540
|
+
<button class="filter-btn active" data-filter="all">All</button>
|
|
541
|
+
<button class="filter-btn" data-filter="passed">✅ Passed</button>
|
|
542
|
+
<button class="filter-btn" data-filter="failed">❌ Failed</button>
|
|
543
|
+
<button class="filter-btn" data-filter="no-tests">⚠️ No Tests</button>
|
|
544
|
+
<button class="filter-btn" data-filter="stale">🔄 Stale</button>
|
|
545
|
+
<input type="text" class="search-input" placeholder="Search files..." id="searchInput">
|
|
546
|
+
</div>
|
|
547
|
+
|
|
548
|
+
<table id="fileTable">
|
|
549
|
+
<thead>
|
|
550
|
+
<tr>
|
|
551
|
+
<th>File Path</th>
|
|
552
|
+
<th>Status</th>
|
|
553
|
+
<th>Tests</th>
|
|
554
|
+
<th>Passed</th>
|
|
555
|
+
<th>Failed</th>
|
|
556
|
+
<th>Duration</th>
|
|
557
|
+
<th>Last Run</th>
|
|
558
|
+
</tr>
|
|
559
|
+
</thead>
|
|
560
|
+
<tbody>
|
|
561
|
+
""" + rows_html + """
|
|
562
|
+
</tbody>
|
|
563
|
+
</table>
|
|
564
|
+
|
|
565
|
+
<div class="last-updated">
|
|
566
|
+
Last updated: """ + datetime.now().strftime("%Y-%m-%d %H:%M:%S") + """
|
|
567
|
+
</div>
|
|
568
|
+
</div>
|
|
569
|
+
|
|
570
|
+
<script>
|
|
571
|
+
// Filter functionality
|
|
572
|
+
const filterBtns = document.querySelectorAll('.filter-btn');
|
|
573
|
+
const rows = document.querySelectorAll('#fileTable tbody tr');
|
|
574
|
+
const searchInput = document.getElementById('searchInput');
|
|
575
|
+
|
|
576
|
+
let currentFilter = 'all';
|
|
577
|
+
|
|
578
|
+
filterBtns.forEach(btn => {
|
|
579
|
+
btn.addEventListener('click', () => {
|
|
580
|
+
filterBtns.forEach(b => b.classList.remove('active'));
|
|
581
|
+
btn.classList.add('active');
|
|
582
|
+
currentFilter = btn.dataset.filter;
|
|
583
|
+
applyFilters();
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
searchInput.addEventListener('input', applyFilters);
|
|
588
|
+
|
|
589
|
+
function applyFilters() {
|
|
590
|
+
const searchTerm = searchInput.value.toLowerCase();
|
|
591
|
+
rows.forEach(row => {
|
|
592
|
+
const filePath = row.querySelector('.file-path').textContent.toLowerCase();
|
|
593
|
+
const matchesSearch = filePath.includes(searchTerm);
|
|
594
|
+
const matchesFilter = currentFilter === 'all' ||
|
|
595
|
+
(currentFilter === 'passed' && row.classList.contains('passed')) ||
|
|
596
|
+
(currentFilter === 'failed' && row.classList.contains('failed')) ||
|
|
597
|
+
(currentFilter === 'no-tests' && row.classList.contains('no-tests')) ||
|
|
598
|
+
(currentFilter === 'stale' && row.innerHTML.includes('STALE'));
|
|
599
|
+
|
|
600
|
+
row.classList.toggle('hidden', !(matchesSearch && matchesFilter));
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Refresh functionality
|
|
605
|
+
function refreshData() {
|
|
606
|
+
const btn = document.querySelector('.refresh-btn');
|
|
607
|
+
btn.classList.add('spinning');
|
|
608
|
+
btn.disabled = true;
|
|
609
|
+
|
|
610
|
+
// Reload the page to get fresh data
|
|
611
|
+
setTimeout(() => {
|
|
612
|
+
window.location.reload();
|
|
613
|
+
}, 500);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Auto-refresh every 60 seconds (optional)
|
|
617
|
+
// setInterval(refreshData, 60000);
|
|
618
|
+
</script>
|
|
619
|
+
</body>
|
|
620
|
+
</html>"""
|
|
621
|
+
|
|
622
|
+
def _generate_empty_dashboard() -> str:
|
|
623
|
+
"""Generate dashboard HTML when no data available."""
|
|
624
|
+
return """<!DOCTYPE html>
|
|
625
|
+
<html lang="en">
|
|
626
|
+
<head>
|
|
627
|
+
<meta charset="UTF-8">
|
|
628
|
+
<title>File Test Status Dashboard</title>
|
|
629
|
+
<style>
|
|
630
|
+
body {
|
|
631
|
+
font-family: -apple-system, sans-serif;
|
|
632
|
+
background: #ffffff;
|
|
633
|
+
color: #333;
|
|
634
|
+
display: flex;
|
|
635
|
+
justify-content: center;
|
|
636
|
+
align-items: center;
|
|
637
|
+
height: 100vh;
|
|
638
|
+
text-align: center;
|
|
639
|
+
}
|
|
640
|
+
.message { max-width: 500px; }
|
|
641
|
+
h1 { margin-bottom: 20px; color: #333; }
|
|
642
|
+
code {
|
|
643
|
+
background: #f8f9fa;
|
|
644
|
+
color: #333;
|
|
645
|
+
padding: 10px 20px;
|
|
646
|
+
border-radius: 6px;
|
|
647
|
+
display: block;
|
|
648
|
+
margin-top: 20px;
|
|
649
|
+
border: 1px solid #e0e0e0;
|
|
650
|
+
}
|
|
651
|
+
</style>
|
|
652
|
+
</head>
|
|
653
|
+
<body>
|
|
654
|
+
<div class="message">
|
|
655
|
+
<h1>📊 No Test Data Available</h1>
|
|
656
|
+
<p>Run the file test tracker to populate data:</p>
|
|
657
|
+
<code>empathy file-tests --scan</code>
|
|
658
|
+
<p style="margin-top: 20px; color: #888;">Or track individual files:</p>
|
|
659
|
+
<code>python -c "from empathy_os.workflows.test_runner import track_file_tests; track_file_tests('src/your_file.py')"</code>
|
|
660
|
+
</div>
|
|
661
|
+
</body>
|
|
662
|
+
</html>"""
|
|
663
|
+
|
|
664
|
+
class DashboardHandler(http.server.SimpleHTTPRequestHandler):
|
|
665
|
+
"""Custom handler for the dashboard."""
|
|
666
|
+
|
|
667
|
+
def do_GET(self):
|
|
668
|
+
"""Handle GET requests."""
|
|
669
|
+
if self.path == "/" or self.path == "/index.html":
|
|
670
|
+
self.send_response(200)
|
|
671
|
+
self.send_header("Content-type", "text/html")
|
|
672
|
+
self.end_headers()
|
|
673
|
+
html = generate_dashboard_html()
|
|
674
|
+
self.wfile.write(html.encode())
|
|
675
|
+
else:
|
|
676
|
+
self.send_error(404)
|
|
677
|
+
|
|
678
|
+
def log_message(self, format, *args):
|
|
679
|
+
"""Suppress logging."""
|
|
680
|
+
pass
|
|
681
|
+
|
|
682
|
+
print(f"Starting File Test Dashboard on http://localhost:{port}")
|
|
683
|
+
print("Press Ctrl+C to stop the server")
|
|
684
|
+
|
|
685
|
+
# Open browser
|
|
686
|
+
webbrowser.open(f"http://localhost:{port}")
|
|
687
|
+
|
|
688
|
+
# Start server
|
|
689
|
+
with socketserver.TCPServer(("", port), DashboardHandler) as httpd:
|
|
690
|
+
httpd.allow_reuse_address = True
|
|
691
|
+
try:
|
|
692
|
+
httpd.serve_forever()
|
|
693
|
+
except KeyboardInterrupt:
|
|
694
|
+
print("\nDashboard server stopped.")
|
|
695
|
+
|
|
696
|
+
return 0
|