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.
Files changed (71) hide show
  1. {empathy_framework-5.1.1.dist-info → empathy_framework-5.2.1.dist-info}/METADATA +52 -3
  2. {empathy_framework-5.1.1.dist-info → empathy_framework-5.2.1.dist-info}/RECORD +69 -28
  3. empathy_os/cli_router.py +9 -0
  4. empathy_os/core_modules/__init__.py +15 -0
  5. empathy_os/mcp/__init__.py +10 -0
  6. empathy_os/mcp/server.py +506 -0
  7. empathy_os/memory/control_panel.py +1 -131
  8. empathy_os/memory/control_panel_support.py +145 -0
  9. empathy_os/memory/encryption.py +159 -0
  10. empathy_os/memory/long_term.py +41 -626
  11. empathy_os/memory/long_term_types.py +99 -0
  12. empathy_os/memory/mixins/__init__.py +25 -0
  13. empathy_os/memory/mixins/backend_init_mixin.py +244 -0
  14. empathy_os/memory/mixins/capabilities_mixin.py +199 -0
  15. empathy_os/memory/mixins/handoff_mixin.py +208 -0
  16. empathy_os/memory/mixins/lifecycle_mixin.py +49 -0
  17. empathy_os/memory/mixins/long_term_mixin.py +352 -0
  18. empathy_os/memory/mixins/promotion_mixin.py +109 -0
  19. empathy_os/memory/mixins/short_term_mixin.py +182 -0
  20. empathy_os/memory/short_term.py +7 -0
  21. empathy_os/memory/simple_storage.py +302 -0
  22. empathy_os/memory/storage_backend.py +167 -0
  23. empathy_os/memory/unified.py +21 -1120
  24. empathy_os/meta_workflows/cli_commands/__init__.py +56 -0
  25. empathy_os/meta_workflows/cli_commands/agent_commands.py +321 -0
  26. empathy_os/meta_workflows/cli_commands/analytics_commands.py +442 -0
  27. empathy_os/meta_workflows/cli_commands/config_commands.py +232 -0
  28. empathy_os/meta_workflows/cli_commands/memory_commands.py +182 -0
  29. empathy_os/meta_workflows/cli_commands/template_commands.py +354 -0
  30. empathy_os/meta_workflows/cli_commands/workflow_commands.py +382 -0
  31. empathy_os/meta_workflows/cli_meta_workflows.py +52 -1802
  32. empathy_os/models/telemetry/__init__.py +71 -0
  33. empathy_os/models/telemetry/analytics.py +594 -0
  34. empathy_os/models/telemetry/backend.py +196 -0
  35. empathy_os/models/telemetry/data_models.py +431 -0
  36. empathy_os/models/telemetry/storage.py +489 -0
  37. empathy_os/orchestration/__init__.py +35 -0
  38. empathy_os/orchestration/execution_strategies.py +481 -0
  39. empathy_os/orchestration/meta_orchestrator.py +488 -1
  40. empathy_os/routing/workflow_registry.py +36 -0
  41. empathy_os/telemetry/cli.py +19 -724
  42. empathy_os/telemetry/commands/__init__.py +14 -0
  43. empathy_os/telemetry/commands/dashboard_commands.py +696 -0
  44. empathy_os/tools.py +183 -0
  45. empathy_os/workflows/__init__.py +5 -0
  46. empathy_os/workflows/autonomous_test_gen.py +860 -161
  47. empathy_os/workflows/base.py +6 -2
  48. empathy_os/workflows/code_review.py +4 -1
  49. empathy_os/workflows/document_gen/__init__.py +25 -0
  50. empathy_os/workflows/document_gen/config.py +30 -0
  51. empathy_os/workflows/document_gen/report_formatter.py +162 -0
  52. empathy_os/workflows/document_gen/workflow.py +1426 -0
  53. empathy_os/workflows/document_gen.py +22 -1598
  54. empathy_os/workflows/security_audit.py +2 -2
  55. empathy_os/workflows/security_audit_phase3.py +7 -4
  56. empathy_os/workflows/seo_optimization.py +633 -0
  57. empathy_os/workflows/test_gen/__init__.py +52 -0
  58. empathy_os/workflows/test_gen/ast_analyzer.py +249 -0
  59. empathy_os/workflows/test_gen/config.py +88 -0
  60. empathy_os/workflows/test_gen/data_models.py +38 -0
  61. empathy_os/workflows/test_gen/report_formatter.py +289 -0
  62. empathy_os/workflows/test_gen/test_templates.py +381 -0
  63. empathy_os/workflows/test_gen/workflow.py +655 -0
  64. empathy_os/workflows/test_gen.py +42 -1905
  65. empathy_os/memory/types 2.py +0 -441
  66. empathy_os/models/telemetry.py +0 -1660
  67. {empathy_framework-5.1.1.dist-info → empathy_framework-5.2.1.dist-info}/WHEEL +0 -0
  68. {empathy_framework-5.1.1.dist-info → empathy_framework-5.2.1.dist-info}/entry_points.txt +0 -0
  69. {empathy_framework-5.1.1.dist-info → empathy_framework-5.2.1.dist-info}/licenses/LICENSE +0 -0
  70. {empathy_framework-5.1.1.dist-info → empathy_framework-5.2.1.dist-info}/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +0 -0
  71. {empathy_framework-5.1.1.dist-info → empathy_framework-5.2.1.dist-info}/top_level.txt +0 -0
@@ -10,7 +10,6 @@ import csv
10
10
  import json
11
11
  import sys
12
12
  from datetime import datetime
13
- from pathlib import Path
14
13
  from typing import Any
15
14
 
16
15
  try:
@@ -24,49 +23,12 @@ except ImportError:
24
23
  RICH_AVAILABLE = False
25
24
  Console = None # type: ignore
26
25
 
27
- from .usage_tracker import UsageTracker
28
-
29
-
30
- def _validate_file_path(path: str, allowed_dir: str | None = None) -> Path:
31
- """Validate file path to prevent path traversal and arbitrary writes.
32
-
33
- Args:
34
- path: File path to validate
35
- allowed_dir: Optional directory to restrict writes to
36
-
37
- Returns:
38
- Validated Path object
39
-
40
- Raises:
41
- ValueError: If path is invalid or unsafe
42
- """
43
- if not path or not isinstance(path, str):
44
- raise ValueError("path must be a non-empty string")
45
-
46
- # Check for null bytes
47
- if "\x00" in path:
48
- raise ValueError("path contains null bytes")
49
-
50
- try:
51
- resolved = Path(path).resolve()
52
- except (OSError, RuntimeError) as e:
53
- raise ValueError(f"Invalid path: {e}")
54
-
55
- # Check if within allowed directory
56
- if allowed_dir:
57
- try:
58
- allowed = Path(allowed_dir).resolve()
59
- resolved.relative_to(allowed)
60
- except ValueError:
61
- raise ValueError(f"path must be within {allowed_dir}")
26
+ from empathy_os.config import _validate_file_path
62
27
 
63
- # Check for dangerous system paths
64
- dangerous_paths = ["/etc", "/sys", "/proc", "/dev"]
65
- for dangerous in dangerous_paths:
66
- if str(resolved).startswith(dangerous):
67
- raise ValueError(f"Cannot write to system directory: {dangerous}")
28
+ from .usage_tracker import UsageTracker
68
29
 
69
- return resolved
30
+ # _validate_file_path is now imported from empathy_os.config
31
+ # This eliminates the duplicate definition that previously existed here (lines 30-69)
70
32
 
71
33
 
72
34
  def cmd_telemetry_show(args: Any) -> int:
@@ -621,269 +583,28 @@ def cmd_telemetry_export(args: Any) -> int:
621
583
  return 0
622
584
 
623
585
 
624
- def cmd_telemetry_dashboard(args: Any) -> int:
625
- """Open interactive telemetry dashboard in browser.
626
-
627
- Args:
628
- args: Parsed command-line arguments
629
-
630
- Returns:
631
- Exit code (0 for success)
632
-
633
- """
634
- import tempfile
635
- import webbrowser
636
- from collections import Counter
637
-
638
- tracker = UsageTracker.get_instance()
639
- entries = tracker.export_to_dict(days=getattr(args, "days", 30))
640
-
641
- if not entries:
642
- print("No telemetry data available.")
643
- return 0
644
-
645
- # Calculate statistics
646
- total_cost = sum(e.get("cost", 0) for e in entries)
647
- total_calls = len(entries)
648
- avg_duration = (
649
- sum(e.get("duration_ms", 0) for e in entries) / total_calls if total_calls > 0 else 0
650
- )
651
-
652
- # Tier distribution
653
- tiers = [e.get("tier", "UNKNOWN") for e in entries]
654
- tier_counts = Counter(tiers)
655
- tier_distribution = {tier: (count / total_calls) * 100 for tier, count in tier_counts.items()}
656
-
657
- # Calculate savings (baseline: all PREMIUM tier)
658
- premium_input_cost = 0.015 / 1000 # per token
659
- premium_output_cost = 0.075 / 1000 # per token
660
-
661
- baseline_cost = sum(
662
- (e.get("tokens", {}).get("input", 0) * premium_input_cost)
663
- + (e.get("tokens", {}).get("output", 0) * premium_output_cost)
664
- for e in entries
665
- )
666
-
667
- saved = baseline_cost - total_cost
668
- savings_pct = (saved / baseline_cost * 100) if baseline_cost > 0 else 0
669
-
670
- # Generate HTML
671
- html_content = f"""<!DOCTYPE html>
672
- <html lang="en">
673
- <head>
674
- <meta charset="UTF-8">
675
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
676
- <title>Empathy Telemetry Dashboard</title>
677
- <style>
678
- * {{ margin: 0; padding: 0; box-sizing: border-box; }}
679
- body {{
680
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
681
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
682
- padding: 20px;
683
- min-height: 100vh;
684
- }}
685
- .container {{
686
- max-width: 1400px;
687
- margin: 0 auto;
688
- }}
689
- .header {{
690
- color: white;
691
- text-align: center;
692
- margin-bottom: 40px;
693
- }}
694
- .header h1 {{
695
- font-size: 48px;
696
- font-weight: 700;
697
- margin-bottom: 10px;
698
- }}
699
- .header p {{
700
- font-size: 18px;
701
- opacity: 0.9;
702
- }}
703
- .stats-grid {{
704
- display: grid;
705
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
706
- gap: 20px;
707
- margin-bottom: 30px;
708
- }}
709
- .stat-card {{
710
- background: white;
711
- border-radius: 12px;
712
- padding: 30px;
713
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
714
- }}
715
- .savings-card {{
716
- grid-column: span 2;
717
- background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
718
- color: white;
719
- }}
720
- .stat-label {{
721
- font-size: 14px;
722
- text-transform: uppercase;
723
- letter-spacing: 1px;
724
- margin-bottom: 10px;
725
- opacity: 0.8;
726
- }}
727
- .stat-value {{
728
- font-size: 56px;
729
- font-weight: 700;
730
- margin-bottom: 5px;
731
- }}
732
- .stat-sublabel {{
733
- font-size: 16px;
734
- opacity: 0.7;
735
- }}
736
- .tier-distribution {{
737
- display: flex;
738
- gap: 10px;
739
- margin-top: 15px;
740
- height: 50px;
741
- }}
742
- .tier-bar {{
743
- flex: 1;
744
- display: flex;
745
- align-items: center;
746
- justify-content: center;
747
- border-radius: 8px;
748
- font-weight: 600;
749
- color: white;
750
- font-size: 14px;
751
- }}
752
- .tier-premium {{ background: linear-gradient(135deg, #9c27b0, #7b1fa2); }}
753
- .tier-capable {{ background: linear-gradient(135deg, #2196f3, #1976d2); }}
754
- .tier-cheap {{ background: linear-gradient(135deg, #4caf50, #388e3c); }}
755
- table {{
756
- width: 100%;
757
- background: white;
758
- border-radius: 12px;
759
- overflow: hidden;
760
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
761
- }}
762
- th, td {{
763
- padding: 16px;
764
- text-align: left;
765
- }}
766
- th {{
767
- background: #f5f5f5;
768
- font-weight: 600;
769
- font-size: 13px;
770
- text-transform: uppercase;
771
- letter-spacing: 0.5px;
772
- color: #666;
773
- }}
774
- tr:hover {{
775
- background: #f9f9f9;
776
- }}
777
- .tier-badge {{
778
- display: inline-block;
779
- padding: 4px 10px;
780
- border-radius: 4px;
781
- font-size: 11px;
782
- font-weight: 600;
783
- color: white;
784
- }}
785
- .badge-premium {{ background: #9c27b0; }}
786
- .badge-capable {{ background: #2196f3; }}
787
- .badge-cheap {{ background: #4caf50; }}
788
- .cache-hit {{ color: #4caf50; font-weight: 600; }}
789
- .cache-miss {{ color: #999; }}
790
- </style>
791
- </head>
792
- <body>
793
- <div class="container">
794
- <div class="header">
795
- <h1>📊 Empathy Telemetry Dashboard</h1>
796
- <p>Last {len(entries)} LLM API calls • Real-time cost tracking</p>
797
- </div>
798
-
799
- <div class="stats-grid">
800
- <div class="stat-card savings-card">
801
- <div class="stat-label">Cost Savings (Tier Routing)</div>
802
- <div class="stat-value">${saved:.2f}</div>
803
- <div class="stat-sublabel">
804
- {savings_pct:.1f}% saved • Baseline: ${baseline_cost:.2f} • Actual: ${
805
- total_cost:.2f}
806
- </div>
807
- </div>
808
-
809
- <div class="stat-card">
810
- <div class="stat-label">Total Cost</div>
811
- <div class="stat-value">${total_cost:.2f}</div>
812
- <div class="stat-sublabel">{total_calls} API calls</div>
813
- </div>
814
-
815
- <div class="stat-card">
816
- <div class="stat-label">Avg Duration</div>
817
- <div class="stat-value">{avg_duration / 1000:.1f}s</div>
818
- <div class="stat-sublabel">Per API call</div>
819
- </div>
820
- </div>
821
-
822
- <div class="stat-card">
823
- <div class="stat-label">Tier Distribution</div>
824
- <div class="tier-distribution">
825
- {
826
- "".join(
827
- f'<div class="tier-bar tier-{tier.lower()}">{tier}: {pct:.1f}%</div>'
828
- for tier, pct in tier_distribution.items()
829
- )
830
- }
831
- </div>
832
- </div>
833
-
834
- <h2 style="color: white; margin: 40px 0 20px 0; font-size: 28px;">Recent LLM Calls</h2>
835
- <table>
836
- <thead>
837
- <tr>
838
- <th>Time</th>
839
- <th>Workflow</th>
840
- <th>Stage</th>
841
- <th>Tier</th>
842
- <th>Cost</th>
843
- <th>Tokens</th>
844
- <th>Cache</th>
845
- <th>Duration</th>
846
- </tr>
847
- </thead>
848
- <tbody>
849
- {
850
- "".join(
851
- f'''<tr>
852
- <td>{datetime.fromisoformat(e.get("ts", "").replace("Z", "+00:00")).strftime("%H:%M:%S")}</td>
853
- <td>{e.get("workflow", "")}</td>
854
- <td>{e.get("stage", "")}</td>
855
- <td><span class="tier-badge badge-{e.get("tier", "").lower()}">{e.get("tier", "")}</span></td>
856
- <td>${e.get("cost", 0):.4f}</td>
857
- <td>{e.get("tokens", {}).get("input", 0)}/{e.get("tokens", {}).get("output", 0)}</td>
858
- <td class="cache-{"hit" if e.get("cache", {}).get("hit") else "miss"}">
859
- {"HIT" if e.get("cache", {}).get("hit") else "MISS"}
860
- </td>
861
- <td>{e.get("duration_ms", 0) / 1000:.1f}s</td>
862
- </tr>'''
863
- for e in list(reversed(entries))[:20]
864
- )
865
- }
866
- </tbody>
867
- </table>
868
- </div>
869
- </body>
870
- </html>"""
586
+ # ==============================================================================
587
+ # Dashboard Commands
588
+ # ==============================================================================
589
+ # cmd_telemetry_dashboard and cmd_file_test_dashboard have been moved to:
590
+ # src/empathy_os/telemetry/commands/dashboard_commands.py
591
+ # They are imported at the top of this file for backward compatibility.
592
+ # ==============================================================================
871
593
 
872
- # Write to temp file
873
- with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
874
- f.write(html_content)
875
- temp_path = f.name
876
594
 
877
- print(f"📊 Opening dashboard in browser: {temp_path}")
878
- webbrowser.open(f"file://{temp_path}")
595
+ # ==============================================================================
596
+ # Tier 1 Automation Monitoring CLI Commands
597
+ # ==============================================================================
879
598
 
880
- return 0
881
599
 
882
600
 
883
601
  # ==============================================================================
884
- # Tier 1 Automation Monitoring CLI Commands
602
+ # Dashboard Commands (Extracted to Separate Module)
603
+ # ==============================================================================
604
+ # cmd_telemetry_dashboard and cmd_file_test_dashboard moved to:
605
+ # src/empathy_os/telemetry/commands/dashboard_commands.py
606
+ # Imported at top of file for backward compatibility.
885
607
  # ==============================================================================
886
-
887
608
 
888
609
  def cmd_tier1_status(args: Any) -> int:
889
610
  """Show comprehensive Tier 1 automation status.
@@ -1508,429 +1229,3 @@ def cmd_file_test_status(args: Any) -> int:
1508
1229
  return 0
1509
1230
 
1510
1231
 
1511
- def cmd_file_test_dashboard(args: Any) -> int:
1512
- """Open interactive file test status dashboard in browser.
1513
-
1514
- Args:
1515
- args: Parsed command-line arguments
1516
- - port: Port to serve on (default: 8765)
1517
-
1518
- Returns:
1519
- Exit code (0 for success)
1520
- """
1521
- import http.server
1522
- import socketserver
1523
- import webbrowser
1524
-
1525
- from empathy_os.models.telemetry import get_telemetry_store
1526
-
1527
- port = getattr(args, "port", 8765)
1528
-
1529
- def generate_dashboard_html() -> str:
1530
- """Generate the dashboard HTML with current data."""
1531
- store = get_telemetry_store()
1532
- all_records = store.get_file_tests(limit=100000)
1533
-
1534
- if not all_records:
1535
- return _generate_empty_dashboard()
1536
-
1537
- # Get latest record per file
1538
- latest_by_file: dict[str, Any] = {}
1539
- for record in all_records:
1540
- existing = latest_by_file.get(record.file_path)
1541
- if existing is None or record.timestamp > existing.timestamp:
1542
- latest_by_file[record.file_path] = record
1543
-
1544
- records = list(latest_by_file.values())
1545
-
1546
- # Calculate stats
1547
- total = len(records)
1548
- passed = sum(1 for r in records if r.last_test_result == "passed")
1549
- failed = sum(1 for r in records if r.last_test_result in ("failed", "error"))
1550
- no_tests = sum(1 for r in records if r.last_test_result == "no_tests")
1551
- stale = sum(1 for r in records if r.is_stale)
1552
-
1553
- # Sort by status priority: failed > stale > no_tests > passed
1554
- def sort_key(r):
1555
- if r.last_test_result in ("failed", "error"):
1556
- return (0, r.file_path)
1557
- if r.is_stale:
1558
- return (1, r.file_path)
1559
- if r.last_test_result == "no_tests":
1560
- return (2, r.file_path)
1561
- return (3, r.file_path)
1562
-
1563
- records.sort(key=sort_key)
1564
-
1565
- # Generate table rows
1566
- rows_html = ""
1567
- for record in records:
1568
- result = record.last_test_result
1569
- if result == "passed":
1570
- status_class = "passed"
1571
- status_icon = "✅"
1572
- elif result in ("failed", "error"):
1573
- status_class = "failed"
1574
- status_icon = "❌"
1575
- elif result == "no_tests":
1576
- status_class = "no-tests"
1577
- status_icon = "⚠️"
1578
- else:
1579
- status_class = "skipped"
1580
- status_icon = "⏭️"
1581
-
1582
- stale_badge = '<span class="badge stale">STALE</span>' if record.is_stale else ""
1583
-
1584
- try:
1585
- dt = datetime.fromisoformat(record.timestamp.rstrip("Z"))
1586
- ts_display = dt.strftime("%Y-%m-%d %H:%M")
1587
- except (ValueError, AttributeError):
1588
- ts_display = record.timestamp[:16] if record.timestamp else "-"
1589
-
1590
- rows_html += f"""
1591
- <tr class="{status_class}">
1592
- <td class="file-path">{record.file_path}</td>
1593
- <td class="status">{status_icon} {result.upper()} {stale_badge}</td>
1594
- <td class="numeric">{record.test_count}</td>
1595
- <td class="numeric passed-count">{record.passed}</td>
1596
- <td class="numeric failed-count">{record.failed + record.errors}</td>
1597
- <td class="numeric">{record.duration_seconds:.1f}s</td>
1598
- <td class="timestamp">{ts_display}</td>
1599
- </tr>
1600
- """
1601
-
1602
- return f"""<!DOCTYPE html>
1603
- <html lang="en">
1604
- <head>
1605
- <meta charset="UTF-8">
1606
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1607
- <title>File Test Status Dashboard</title>
1608
- <style>
1609
- * {{ margin: 0; padding: 0; box-sizing: border-box; }}
1610
- body {{
1611
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1612
- background: #ffffff;
1613
- color: #333;
1614
- padding: 20px;
1615
- min-height: 100vh;
1616
- }}
1617
- .container {{ max-width: 1600px; margin: 0 auto; }}
1618
- .header {{
1619
- display: flex;
1620
- justify-content: space-between;
1621
- align-items: center;
1622
- margin-bottom: 30px;
1623
- padding-bottom: 20px;
1624
- border-bottom: 1px solid #e0e0e0;
1625
- }}
1626
- .header h1 {{ font-size: 28px; color: #333; }}
1627
- .refresh-btn {{
1628
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1629
- color: white;
1630
- border: none;
1631
- padding: 12px 24px;
1632
- border-radius: 8px;
1633
- font-size: 16px;
1634
- cursor: pointer;
1635
- display: flex;
1636
- align-items: center;
1637
- gap: 8px;
1638
- transition: transform 0.2s, box-shadow 0.2s;
1639
- }}
1640
- .refresh-btn:hover {{
1641
- transform: translateY(-2px);
1642
- box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
1643
- }}
1644
- .refresh-btn:active {{ transform: translateY(0); }}
1645
- .refresh-btn.spinning .icon {{ animation: spin 1s linear infinite; }}
1646
- @keyframes spin {{ 100% {{ transform: rotate(360deg); }} }}
1647
- .stats {{
1648
- display: grid;
1649
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
1650
- gap: 20px;
1651
- margin-bottom: 30px;
1652
- }}
1653
- .stat-card {{
1654
- background: #f8f9fa;
1655
- border-radius: 12px;
1656
- padding: 20px;
1657
- text-align: center;
1658
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
1659
- }}
1660
- .stat-card.passed {{ border-left: 4px solid #22c55e; }}
1661
- .stat-card.failed {{ border-left: 4px solid #ef4444; }}
1662
- .stat-card.no-tests {{ border-left: 4px solid #f59e0b; }}
1663
- .stat-card.stale {{ border-left: 4px solid #8b5cf6; }}
1664
- .stat-card.total {{ border-left: 4px solid #3b82f6; }}
1665
- .stat-value {{ font-size: 36px; font-weight: bold; }}
1666
- .stat-label {{ font-size: 14px; color: #666; margin-top: 5px; }}
1667
- .stat-card.passed .stat-value {{ color: #22c55e; }}
1668
- .stat-card.failed .stat-value {{ color: #ef4444; }}
1669
- .stat-card.no-tests .stat-value {{ color: #f59e0b; }}
1670
- .stat-card.stale .stat-value {{ color: #8b5cf6; }}
1671
- .stat-card.total .stat-value {{ color: #3b82f6; }}
1672
- .filter-bar {{
1673
- display: flex;
1674
- gap: 10px;
1675
- margin-bottom: 20px;
1676
- flex-wrap: wrap;
1677
- }}
1678
- .filter-btn {{
1679
- background: #f8f9fa;
1680
- color: #666;
1681
- border: 1px solid #e0e0e0;
1682
- padding: 8px 16px;
1683
- border-radius: 6px;
1684
- cursor: pointer;
1685
- transition: all 0.2s;
1686
- }}
1687
- .filter-btn:hover, .filter-btn.active {{
1688
- background: #667eea;
1689
- color: #fff;
1690
- border-color: #667eea;
1691
- }}
1692
- .search-input {{
1693
- flex: 1;
1694
- min-width: 200px;
1695
- background: #fff;
1696
- border: 1px solid #e0e0e0;
1697
- color: #333;
1698
- padding: 8px 16px;
1699
- border-radius: 6px;
1700
- font-size: 14px;
1701
- }}
1702
- .search-input:focus {{
1703
- outline: none;
1704
- border-color: #667eea;
1705
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
1706
- }}
1707
- table {{
1708
- width: 100%;
1709
- border-collapse: collapse;
1710
- background: #fff;
1711
- border-radius: 12px;
1712
- overflow: hidden;
1713
- box-shadow: 0 2px 8px rgba(0,0,0,0.08);
1714
- }}
1715
- th, td {{ padding: 12px 16px; text-align: left; }}
1716
- th {{
1717
- background: #f8f9fa;
1718
- font-weight: 600;
1719
- color: #333;
1720
- position: sticky;
1721
- top: 0;
1722
- border-bottom: 2px solid #e0e0e0;
1723
- }}
1724
- tr {{ border-bottom: 1px solid #f0f0f0; }}
1725
- tr:hover {{ background: #f8f9fa; }}
1726
- tr.failed {{ background: rgba(239, 68, 68, 0.08); }}
1727
- tr.no-tests {{ background: rgba(245, 158, 11, 0.05); }}
1728
- .file-path {{ font-family: 'Monaco', 'Menlo', monospace; font-size: 13px; color: #333; }}
1729
- .numeric {{ text-align: right; font-family: monospace; }}
1730
- .passed-count {{ color: #22c55e; }}
1731
- .failed-count {{ color: #ef4444; }}
1732
- .timestamp {{ color: #888; font-size: 12px; }}
1733
- .badge {{
1734
- display: inline-block;
1735
- padding: 2px 8px;
1736
- border-radius: 4px;
1737
- font-size: 10px;
1738
- font-weight: bold;
1739
- margin-left: 8px;
1740
- }}
1741
- .badge.stale {{ background: #8b5cf6; color: #fff; }}
1742
- .hidden {{ display: none; }}
1743
- .last-updated {{ color: #888; font-size: 12px; margin-top: 20px; text-align: center; }}
1744
- </style>
1745
- </head>
1746
- <body>
1747
- <div class="container">
1748
- <div class="header">
1749
- <h1>📊 File Test Status Dashboard</h1>
1750
- <button class="refresh-btn" onclick="refreshData()">
1751
- <span class="icon">🔄</span>
1752
- <span>Refresh</span>
1753
- </button>
1754
- </div>
1755
-
1756
- <div class="stats">
1757
- <div class="stat-card total">
1758
- <div class="stat-value">{total}</div>
1759
- <div class="stat-label">Total Files</div>
1760
- </div>
1761
- <div class="stat-card passed">
1762
- <div class="stat-value">{passed}</div>
1763
- <div class="stat-label">Passed</div>
1764
- </div>
1765
- <div class="stat-card failed">
1766
- <div class="stat-value">{failed}</div>
1767
- <div class="stat-label">Failed</div>
1768
- </div>
1769
- <div class="stat-card no-tests">
1770
- <div class="stat-value">{no_tests}</div>
1771
- <div class="stat-label">No Tests</div>
1772
- </div>
1773
- <div class="stat-card stale">
1774
- <div class="stat-value">{stale}</div>
1775
- <div class="stat-label">Stale</div>
1776
- </div>
1777
- </div>
1778
-
1779
- <div class="filter-bar">
1780
- <button class="filter-btn active" data-filter="all">All</button>
1781
- <button class="filter-btn" data-filter="passed">✅ Passed</button>
1782
- <button class="filter-btn" data-filter="failed">❌ Failed</button>
1783
- <button class="filter-btn" data-filter="no-tests">⚠️ No Tests</button>
1784
- <button class="filter-btn" data-filter="stale">🔄 Stale</button>
1785
- <input type="text" class="search-input" placeholder="Search files..." id="searchInput">
1786
- </div>
1787
-
1788
- <table id="fileTable">
1789
- <thead>
1790
- <tr>
1791
- <th>File Path</th>
1792
- <th>Status</th>
1793
- <th>Tests</th>
1794
- <th>Passed</th>
1795
- <th>Failed</th>
1796
- <th>Duration</th>
1797
- <th>Last Run</th>
1798
- </tr>
1799
- </thead>
1800
- <tbody>
1801
- {rows_html}
1802
- </tbody>
1803
- </table>
1804
-
1805
- <div class="last-updated">
1806
- Last updated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
1807
- </div>
1808
- </div>
1809
-
1810
- <script>
1811
- // Filter functionality
1812
- const filterBtns = document.querySelectorAll('.filter-btn');
1813
- const rows = document.querySelectorAll('#fileTable tbody tr');
1814
- const searchInput = document.getElementById('searchInput');
1815
-
1816
- let currentFilter = 'all';
1817
-
1818
- filterBtns.forEach(btn => {{
1819
- btn.addEventListener('click', () => {{
1820
- filterBtns.forEach(b => b.classList.remove('active'));
1821
- btn.classList.add('active');
1822
- currentFilter = btn.dataset.filter;
1823
- applyFilters();
1824
- }});
1825
- }});
1826
-
1827
- searchInput.addEventListener('input', applyFilters);
1828
-
1829
- function applyFilters() {{
1830
- const searchTerm = searchInput.value.toLowerCase();
1831
- rows.forEach(row => {{
1832
- const filePath = row.querySelector('.file-path').textContent.toLowerCase();
1833
- const matchesSearch = filePath.includes(searchTerm);
1834
- const matchesFilter = currentFilter === 'all' ||
1835
- (currentFilter === 'passed' && row.classList.contains('passed')) ||
1836
- (currentFilter === 'failed' && row.classList.contains('failed')) ||
1837
- (currentFilter === 'no-tests' && row.classList.contains('no-tests')) ||
1838
- (currentFilter === 'stale' && row.innerHTML.includes('STALE'));
1839
-
1840
- row.classList.toggle('hidden', !(matchesSearch && matchesFilter));
1841
- }});
1842
- }}
1843
-
1844
- // Refresh functionality
1845
- function refreshData() {{
1846
- const btn = document.querySelector('.refresh-btn');
1847
- btn.classList.add('spinning');
1848
- btn.disabled = true;
1849
-
1850
- // Reload the page to get fresh data
1851
- setTimeout(() => {{
1852
- window.location.reload();
1853
- }}, 500);
1854
- }}
1855
-
1856
- // Auto-refresh every 60 seconds (optional)
1857
- // setInterval(refreshData, 60000);
1858
- </script>
1859
- </body>
1860
- </html>"""
1861
-
1862
- def _generate_empty_dashboard() -> str:
1863
- """Generate dashboard HTML when no data available."""
1864
- return """<!DOCTYPE html>
1865
- <html lang="en">
1866
- <head>
1867
- <meta charset="UTF-8">
1868
- <title>File Test Status Dashboard</title>
1869
- <style>
1870
- body {
1871
- font-family: -apple-system, sans-serif;
1872
- background: #ffffff;
1873
- color: #333;
1874
- display: flex;
1875
- justify-content: center;
1876
- align-items: center;
1877
- height: 100vh;
1878
- text-align: center;
1879
- }
1880
- .message { max-width: 500px; }
1881
- h1 { margin-bottom: 20px; color: #333; }
1882
- code {
1883
- background: #f8f9fa;
1884
- color: #333;
1885
- padding: 10px 20px;
1886
- border-radius: 6px;
1887
- display: block;
1888
- margin-top: 20px;
1889
- border: 1px solid #e0e0e0;
1890
- }
1891
- </style>
1892
- </head>
1893
- <body>
1894
- <div class="message">
1895
- <h1>📊 No Test Data Available</h1>
1896
- <p>Run the file test tracker to populate data:</p>
1897
- <code>empathy file-tests --scan</code>
1898
- <p style="margin-top: 20px; color: #888;">Or track individual files:</p>
1899
- <code>python -c "from empathy_os.workflows.test_runner import track_file_tests; track_file_tests('src/your_file.py')"</code>
1900
- </div>
1901
- </body>
1902
- </html>"""
1903
-
1904
- class DashboardHandler(http.server.SimpleHTTPRequestHandler):
1905
- """Custom handler for the dashboard."""
1906
-
1907
- def do_GET(self):
1908
- """Handle GET requests."""
1909
- if self.path == "/" or self.path == "/index.html":
1910
- self.send_response(200)
1911
- self.send_header("Content-type", "text/html")
1912
- self.end_headers()
1913
- html = generate_dashboard_html()
1914
- self.wfile.write(html.encode())
1915
- else:
1916
- self.send_error(404)
1917
-
1918
- def log_message(self, format, *args):
1919
- """Suppress logging."""
1920
- pass
1921
-
1922
- print(f"Starting File Test Dashboard on http://localhost:{port}")
1923
- print("Press Ctrl+C to stop the server")
1924
-
1925
- # Open browser
1926
- webbrowser.open(f"http://localhost:{port}")
1927
-
1928
- # Start server
1929
- with socketserver.TCPServer(("", port), DashboardHandler) as httpd:
1930
- httpd.allow_reuse_address = True
1931
- try:
1932
- httpd.serve_forever()
1933
- except KeyboardInterrupt:
1934
- print("\nDashboard server stopped.")
1935
-
1936
- return 0