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.
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/METADATA +79 -6
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/RECORD +83 -64
- empathy_os/__init__.py +1 -1
- empathy_os/cache/hybrid.py +5 -1
- empathy_os/cli/commands/batch.py +8 -0
- empathy_os/cli/commands/profiling.py +4 -0
- empathy_os/cli/commands/workflow.py +8 -4
- empathy_os/cli_router.py +9 -0
- empathy_os/config.py +15 -2
- empathy_os/core_modules/__init__.py +15 -0
- empathy_os/dashboard/simple_server.py +62 -30
- 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 +46 -631
- 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 +249 -0
- empathy_os/memory/mixins/capabilities_mixin.py +208 -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 +61 -12
- empathy_os/memory/simple_storage.py +302 -0
- empathy_os/memory/storage_backend.py +167 -0
- empathy_os/memory/types.py +8 -3
- 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/agent_coordination.py +2 -3
- empathy_os/telemetry/agent_tracking.py +26 -7
- empathy_os/telemetry/approval_gates.py +18 -24
- 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/telemetry/event_streaming.py +7 -3
- empathy_os/telemetry/feedback_loop.py +28 -15
- 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.py → document_gen/workflow.py} +5 -184
- empathy_os/workflows/output.py +4 -1
- empathy_os/workflows/progress.py +8 -2
- 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/cli/parsers/cache 2.py +0 -65
- empathy_os/cli_router 2.py +0 -416
- empathy_os/dashboard/app 2.py +0 -512
- empathy_os/dashboard/simple_server 2.py +0 -403
- empathy_os/dashboard/standalone_server 2.py +0 -536
- empathy_os/memory/types 2.py +0 -441
- empathy_os/models/adaptive_routing 2.py +0 -437
- empathy_os/models/telemetry.py +0 -1660
- empathy_os/project_index/scanner_parallel 2.py +0 -291
- empathy_os/telemetry/agent_coordination 2.py +0 -478
- empathy_os/telemetry/agent_tracking 2.py +0 -350
- empathy_os/telemetry/approval_gates 2.py +0 -563
- empathy_os/telemetry/event_streaming 2.py +0 -405
- empathy_os/telemetry/feedback_loop 2.py +0 -557
- empathy_os/vscode_bridge 2.py +0 -173
- 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
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/WHEEL +0 -0
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/entry_points.txt +0 -0
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/licenses/LICENSE +0 -0
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +0 -0
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/top_level.txt +0 -0
empathy_os/telemetry/cli.py
CHANGED
|
@@ -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 .
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
|
|
878
|
-
|
|
595
|
+
# ==============================================================================
|
|
596
|
+
# Tier 1 Automation Monitoring CLI Commands
|
|
597
|
+
# ==============================================================================
|
|
879
598
|
|
|
880
|
-
return 0
|
|
881
599
|
|
|
882
600
|
|
|
883
601
|
# ==============================================================================
|
|
884
|
-
#
|
|
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
|