empathy-framework 5.1.1__py3-none-any.whl → 5.3.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.
Files changed (106) hide show
  1. {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/METADATA +79 -6
  2. {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/RECORD +83 -64
  3. empathy_os/__init__.py +1 -1
  4. empathy_os/cache/hybrid.py +5 -1
  5. empathy_os/cli/commands/batch.py +8 -0
  6. empathy_os/cli/commands/profiling.py +4 -0
  7. empathy_os/cli/commands/workflow.py +8 -4
  8. empathy_os/cli_router.py +9 -0
  9. empathy_os/config.py +15 -2
  10. empathy_os/core_modules/__init__.py +15 -0
  11. empathy_os/dashboard/simple_server.py +62 -30
  12. empathy_os/mcp/__init__.py +10 -0
  13. empathy_os/mcp/server.py +506 -0
  14. empathy_os/memory/control_panel.py +1 -131
  15. empathy_os/memory/control_panel_support.py +145 -0
  16. empathy_os/memory/encryption.py +159 -0
  17. empathy_os/memory/long_term.py +46 -631
  18. empathy_os/memory/long_term_types.py +99 -0
  19. empathy_os/memory/mixins/__init__.py +25 -0
  20. empathy_os/memory/mixins/backend_init_mixin.py +249 -0
  21. empathy_os/memory/mixins/capabilities_mixin.py +208 -0
  22. empathy_os/memory/mixins/handoff_mixin.py +208 -0
  23. empathy_os/memory/mixins/lifecycle_mixin.py +49 -0
  24. empathy_os/memory/mixins/long_term_mixin.py +352 -0
  25. empathy_os/memory/mixins/promotion_mixin.py +109 -0
  26. empathy_os/memory/mixins/short_term_mixin.py +182 -0
  27. empathy_os/memory/short_term.py +61 -12
  28. empathy_os/memory/simple_storage.py +302 -0
  29. empathy_os/memory/storage_backend.py +167 -0
  30. empathy_os/memory/types.py +8 -3
  31. empathy_os/memory/unified.py +21 -1120
  32. empathy_os/meta_workflows/cli_commands/__init__.py +56 -0
  33. empathy_os/meta_workflows/cli_commands/agent_commands.py +321 -0
  34. empathy_os/meta_workflows/cli_commands/analytics_commands.py +442 -0
  35. empathy_os/meta_workflows/cli_commands/config_commands.py +232 -0
  36. empathy_os/meta_workflows/cli_commands/memory_commands.py +182 -0
  37. empathy_os/meta_workflows/cli_commands/template_commands.py +354 -0
  38. empathy_os/meta_workflows/cli_commands/workflow_commands.py +382 -0
  39. empathy_os/meta_workflows/cli_meta_workflows.py +52 -1802
  40. empathy_os/models/telemetry/__init__.py +71 -0
  41. empathy_os/models/telemetry/analytics.py +594 -0
  42. empathy_os/models/telemetry/backend.py +196 -0
  43. empathy_os/models/telemetry/data_models.py +431 -0
  44. empathy_os/models/telemetry/storage.py +489 -0
  45. empathy_os/orchestration/__init__.py +35 -0
  46. empathy_os/orchestration/execution_strategies.py +481 -0
  47. empathy_os/orchestration/meta_orchestrator.py +488 -1
  48. empathy_os/routing/workflow_registry.py +36 -0
  49. empathy_os/telemetry/agent_coordination.py +2 -3
  50. empathy_os/telemetry/agent_tracking.py +26 -7
  51. empathy_os/telemetry/approval_gates.py +18 -24
  52. empathy_os/telemetry/cli.py +19 -724
  53. empathy_os/telemetry/commands/__init__.py +14 -0
  54. empathy_os/telemetry/commands/dashboard_commands.py +696 -0
  55. empathy_os/telemetry/event_streaming.py +7 -3
  56. empathy_os/telemetry/feedback_loop.py +28 -15
  57. empathy_os/tools.py +183 -0
  58. empathy_os/workflows/__init__.py +5 -0
  59. empathy_os/workflows/autonomous_test_gen.py +860 -161
  60. empathy_os/workflows/base.py +6 -2
  61. empathy_os/workflows/code_review.py +4 -1
  62. empathy_os/workflows/document_gen/__init__.py +25 -0
  63. empathy_os/workflows/document_gen/config.py +30 -0
  64. empathy_os/workflows/document_gen/report_formatter.py +162 -0
  65. empathy_os/workflows/{document_gen.py → document_gen/workflow.py} +5 -184
  66. empathy_os/workflows/output.py +4 -1
  67. empathy_os/workflows/progress.py +8 -2
  68. empathy_os/workflows/security_audit.py +2 -2
  69. empathy_os/workflows/security_audit_phase3.py +7 -4
  70. empathy_os/workflows/seo_optimization.py +633 -0
  71. empathy_os/workflows/test_gen/__init__.py +52 -0
  72. empathy_os/workflows/test_gen/ast_analyzer.py +249 -0
  73. empathy_os/workflows/test_gen/config.py +88 -0
  74. empathy_os/workflows/test_gen/data_models.py +38 -0
  75. empathy_os/workflows/test_gen/report_formatter.py +289 -0
  76. empathy_os/workflows/test_gen/test_templates.py +381 -0
  77. empathy_os/workflows/test_gen/workflow.py +655 -0
  78. empathy_os/workflows/test_gen.py +42 -1905
  79. empathy_os/cli/parsers/cache 2.py +0 -65
  80. empathy_os/cli_router 2.py +0 -416
  81. empathy_os/dashboard/app 2.py +0 -512
  82. empathy_os/dashboard/simple_server 2.py +0 -403
  83. empathy_os/dashboard/standalone_server 2.py +0 -536
  84. empathy_os/memory/types 2.py +0 -441
  85. empathy_os/models/adaptive_routing 2.py +0 -437
  86. empathy_os/models/telemetry.py +0 -1660
  87. empathy_os/project_index/scanner_parallel 2.py +0 -291
  88. empathy_os/telemetry/agent_coordination 2.py +0 -478
  89. empathy_os/telemetry/agent_tracking 2.py +0 -350
  90. empathy_os/telemetry/approval_gates 2.py +0 -563
  91. empathy_os/telemetry/event_streaming 2.py +0 -405
  92. empathy_os/telemetry/feedback_loop 2.py +0 -557
  93. empathy_os/vscode_bridge 2.py +0 -173
  94. empathy_os/workflows/progressive/__init__ 2.py +0 -92
  95. empathy_os/workflows/progressive/cli 2.py +0 -242
  96. empathy_os/workflows/progressive/core 2.py +0 -488
  97. empathy_os/workflows/progressive/orchestrator 2.py +0 -701
  98. empathy_os/workflows/progressive/reports 2.py +0 -528
  99. empathy_os/workflows/progressive/telemetry 2.py +0 -280
  100. empathy_os/workflows/progressive/test_gen 2.py +0 -514
  101. empathy_os/workflows/progressive/workflow 2.py +0 -628
  102. {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/WHEEL +0 -0
  103. {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/entry_points.txt +0 -0
  104. {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/licenses/LICENSE +0 -0
  105. {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +0 -0
  106. {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.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