code-meter 0.5.0__tar.gz → 0.7.0__tar.gz
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.
- {code_meter-0.5.0 → code_meter-0.7.0}/PKG-INFO +1 -1
- {code_meter-0.5.0 → code_meter-0.7.0}/pyproject.toml +1 -1
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/storage/repository.py +52 -26
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/ui/web/server.py +71 -7
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/ui/web/static.py +261 -37
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter.egg-info/PKG-INFO +1 -1
- {code_meter-0.5.0 → code_meter-0.7.0}/README.md +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/setup.cfg +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/setup.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/__init__.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/analytics/__init__.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/analytics/costs.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/analytics/reports.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/analytics/tokens.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/cli.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/config.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/models/__init__.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/models/project.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/models/usage.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/pricing/__init__.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/pricing/anthropic.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/pricing/engine.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/pricing/google.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/pricing/live.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/pricing/openai.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/providers/__init__.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/providers/antigravity.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/providers/base.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/providers/claude_code.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/providers/codex.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/storage/__init__.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/storage/database.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/ui/__init__.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/ui/dashboard.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/ui/tables.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/ui/web/__init__.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/watcher.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter.egg-info/SOURCES.txt +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter.egg-info/dependency_links.txt +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter.egg-info/entry_points.txt +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter.egg-info/requires.txt +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter.egg-info/top_level.txt +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/tests/test_analytics.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/tests/test_database.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/tests/test_pricing.py +0 -0
- {code_meter-0.5.0 → code_meter-0.7.0}/tests/test_scanner.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "code-meter"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.0"
|
|
8
8
|
description = "Local AI coding token usage tracker and cost estimator for Claude Code and future agents."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "Dhanush Nayak", email = "dhanushnayak.ram@mail.com" }]
|
|
@@ -527,35 +527,11 @@ class UsageRepository:
|
|
|
527
527
|
"""
|
|
528
528
|
SELECT provider, model, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, timestamp
|
|
529
529
|
FROM usage_events
|
|
530
|
-
WHERE session_id =
|
|
530
|
+
WHERE session_id = ? OR (provider = ? AND session_id IS NULL);
|
|
531
531
|
""",
|
|
532
|
-
(session_id,),
|
|
532
|
+
(session_id, p.provider),
|
|
533
533
|
)
|
|
534
534
|
events = cursor.fetchall()
|
|
535
|
-
total_requests = len(events)
|
|
536
|
-
total_tokens = sum(
|
|
537
|
-
(e["input_tokens"] + e["output_tokens"] + e["cache_read_tokens"] + e["cache_write_tokens"])
|
|
538
|
-
for e in events
|
|
539
|
-
)
|
|
540
|
-
|
|
541
|
-
total_cost = 0.0
|
|
542
|
-
if pricing_engine and events:
|
|
543
|
-
for e in events:
|
|
544
|
-
try:
|
|
545
|
-
ts = datetime.fromisoformat(e["timestamp"])
|
|
546
|
-
except Exception:
|
|
547
|
-
ts = None
|
|
548
|
-
c = pricing_engine.calculate_cost(
|
|
549
|
-
provider=e["provider"],
|
|
550
|
-
model=e["model"],
|
|
551
|
-
input_tokens=e["input_tokens"],
|
|
552
|
-
output_tokens=e["output_tokens"],
|
|
553
|
-
cache_read_tokens=e["cache_read_tokens"],
|
|
554
|
-
cache_write_tokens=e["cache_write_tokens"],
|
|
555
|
-
timestamp=ts,
|
|
556
|
-
)
|
|
557
|
-
if c:
|
|
558
|
-
total_cost += c
|
|
559
535
|
|
|
560
536
|
cursor.execute(
|
|
561
537
|
"""
|
|
@@ -575,6 +551,56 @@ class UsageRepository:
|
|
|
575
551
|
for r in cursor.fetchall()
|
|
576
552
|
]
|
|
577
553
|
|
|
554
|
+
if events:
|
|
555
|
+
total_requests = len(events)
|
|
556
|
+
total_tokens = sum(
|
|
557
|
+
(e["input_tokens"] + e["output_tokens"] + e["cache_read_tokens"] + e["cache_write_tokens"])
|
|
558
|
+
for e in events
|
|
559
|
+
)
|
|
560
|
+
total_cost = 0.0
|
|
561
|
+
if pricing_engine:
|
|
562
|
+
for e in events:
|
|
563
|
+
try:
|
|
564
|
+
ts = datetime.fromisoformat(e["timestamp"])
|
|
565
|
+
except Exception:
|
|
566
|
+
ts = None
|
|
567
|
+
c = pricing_engine.calculate_cost(
|
|
568
|
+
provider=e["provider"],
|
|
569
|
+
model=e["model"],
|
|
570
|
+
input_tokens=e["input_tokens"],
|
|
571
|
+
output_tokens=e["output_tokens"],
|
|
572
|
+
cache_read_tokens=e["cache_read_tokens"],
|
|
573
|
+
cache_write_tokens=e["cache_write_tokens"],
|
|
574
|
+
timestamp=ts,
|
|
575
|
+
)
|
|
576
|
+
if c:
|
|
577
|
+
total_cost += c
|
|
578
|
+
else:
|
|
579
|
+
# Accurately compute turn metrics from prompt text & code edit outputs
|
|
580
|
+
internal_turns = max(len(changes), 1)
|
|
581
|
+
est_input_tokens = len(p.prompt_text) // 4 + 250
|
|
582
|
+
est_output_tokens = sum(len(c["diff_summary"]) for c in changes) // 4 + 300
|
|
583
|
+
total_requests = internal_turns
|
|
584
|
+
total_tokens = est_input_tokens + est_output_tokens
|
|
585
|
+
|
|
586
|
+
model_by_provider = {
|
|
587
|
+
"claude_code": "claude-3-7-sonnet-20250219",
|
|
588
|
+
"codex": "gpt-4o",
|
|
589
|
+
"antigravity": "gemini-2.5-flash",
|
|
590
|
+
}
|
|
591
|
+
fallback_model = model_by_provider.get(p.provider.lower(), "gpt-4o")
|
|
592
|
+
|
|
593
|
+
total_cost = 0.0
|
|
594
|
+
if pricing_engine:
|
|
595
|
+
c = pricing_engine.calculate_cost(
|
|
596
|
+
provider=p.provider,
|
|
597
|
+
model=fallback_model,
|
|
598
|
+
input_tokens=est_input_tokens,
|
|
599
|
+
output_tokens=est_output_tokens,
|
|
600
|
+
)
|
|
601
|
+
if c:
|
|
602
|
+
total_cost = c
|
|
603
|
+
|
|
578
604
|
details.append(
|
|
579
605
|
{
|
|
580
606
|
"prompt_id": p.prompt_id,
|
|
@@ -30,7 +30,9 @@ def create_web_handler(repo: UsageRepository, pricing_engine: PricingEngine, rep
|
|
|
30
30
|
pass
|
|
31
31
|
|
|
32
32
|
def do_GET(self) -> None:
|
|
33
|
-
|
|
33
|
+
parsed_path = self.path.split("?")
|
|
34
|
+
path = parsed_path[0]
|
|
35
|
+
query_str = parsed_path[1] if len(parsed_path) > 1 else ""
|
|
34
36
|
|
|
35
37
|
if path in ("/", "/index.html"):
|
|
36
38
|
self.send_response(200)
|
|
@@ -39,7 +41,7 @@ def create_web_handler(repo: UsageRepository, pricing_engine: PricingEngine, rep
|
|
|
39
41
|
self.wfile.write(HTML_DASHBOARD.encode("utf-8"))
|
|
40
42
|
|
|
41
43
|
elif path == "/api/summary":
|
|
42
|
-
self.handle_api_summary()
|
|
44
|
+
self.handle_api_summary(query_str)
|
|
43
45
|
|
|
44
46
|
elif path == "/api/prompts":
|
|
45
47
|
self.handle_api_prompts()
|
|
@@ -50,10 +52,42 @@ def create_web_handler(repo: UsageRepository, pricing_engine: PricingEngine, rep
|
|
|
50
52
|
else:
|
|
51
53
|
self.send_error(404, "Not Found")
|
|
52
54
|
|
|
53
|
-
def handle_api_summary(self) -> None:
|
|
55
|
+
def handle_api_summary(self, query_str: str = "") -> None:
|
|
56
|
+
# Parse filter params
|
|
57
|
+
from urllib.parse import parse_qs
|
|
58
|
+
params = parse_qs(query_str)
|
|
59
|
+
tf = params.get("timeframe", ["all"])[0].lower()
|
|
60
|
+
prov = params.get("provider", ["all"])[0].lower()
|
|
61
|
+
|
|
54
62
|
records = repo.query_usage_events()
|
|
55
|
-
prompts = repo.query_prompts(limit=
|
|
56
|
-
code_changes = repo.query_code_changes(limit=
|
|
63
|
+
prompts = repo.query_prompts(limit=50)
|
|
64
|
+
code_changes = repo.query_code_changes(limit=50)
|
|
65
|
+
|
|
66
|
+
# Filter by provider if specified
|
|
67
|
+
if prov != "all":
|
|
68
|
+
records = [r for r in records if r.provider.lower() == prov]
|
|
69
|
+
prompts = [p for p in prompts if p.provider.lower() == prov]
|
|
70
|
+
code_changes = [c for c in code_changes if c.provider.lower() == prov]
|
|
71
|
+
|
|
72
|
+
# Filter by timeframe if specified
|
|
73
|
+
now_dt = datetime.now(timezone.utc)
|
|
74
|
+
if tf == "today":
|
|
75
|
+
cutoff = now_dt.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
76
|
+
records = [r for r in records if r.timestamp >= cutoff]
|
|
77
|
+
prompts = [p for p in prompts if p.timestamp >= cutoff]
|
|
78
|
+
code_changes = [c for c in code_changes if c.timestamp >= cutoff]
|
|
79
|
+
elif tf == "week":
|
|
80
|
+
from datetime import timedelta
|
|
81
|
+
cutoff = now_dt - timedelta(days=7)
|
|
82
|
+
records = [r for r in records if r.timestamp >= cutoff]
|
|
83
|
+
prompts = [p for p in prompts if p.timestamp >= cutoff]
|
|
84
|
+
code_changes = [c for c in code_changes if c.timestamp >= cutoff]
|
|
85
|
+
elif tf == "month":
|
|
86
|
+
from datetime import timedelta
|
|
87
|
+
cutoff = now_dt - timedelta(days=30)
|
|
88
|
+
records = [r for r in records if r.timestamp >= cutoff]
|
|
89
|
+
prompts = [p for p in prompts if p.timestamp >= cutoff]
|
|
90
|
+
code_changes = [c for c in code_changes if c.timestamp >= cutoff]
|
|
57
91
|
|
|
58
92
|
costs = [
|
|
59
93
|
pricing_engine.calculate_cost(
|
|
@@ -62,11 +96,24 @@ def create_web_handler(repo: UsageRepository, pricing_engine: PricingEngine, rep
|
|
|
62
96
|
for r in records
|
|
63
97
|
]
|
|
64
98
|
|
|
65
|
-
summary = report_gen.generate_summary(records
|
|
99
|
+
summary = report_gen.generate_summary(records)
|
|
66
100
|
by_model = report_gen.aggregate_by_model(records, costs)
|
|
67
101
|
by_project = report_gen.aggregate_by_project(records, costs)
|
|
102
|
+
by_day = report_gen.aggregate_by_day(records, costs)
|
|
68
103
|
prompt_details = repo.query_prompt_details(limit=25, pricing_engine=pricing_engine)
|
|
69
104
|
|
|
105
|
+
if prov != "all":
|
|
106
|
+
prompt_details = [pd for pd in prompt_details if pd.get("provider", "").lower() == prov]
|
|
107
|
+
|
|
108
|
+
# Calculate Analytical Burn Rate & Forecast
|
|
109
|
+
total_tokens = summary.total_tokens
|
|
110
|
+
total_cost = summary.estimated_cost_usd or 0.0
|
|
111
|
+
num_days = 1 if tf == "today" else (7 if tf == "week" else (30 if tf == "month" else max(len(by_day), 1)))
|
|
112
|
+
|
|
113
|
+
daily_avg_cost = total_cost / float(num_days) if num_days > 0 else 0.0
|
|
114
|
+
hourly_tokens = (total_tokens / (float(num_days) * 24.0)) if num_days > 0 else 0.0
|
|
115
|
+
projected_monthly_cost = daily_avg_cost * 30.0
|
|
116
|
+
|
|
70
117
|
payload = {
|
|
71
118
|
"summary": {
|
|
72
119
|
"total_requests": summary.total_requests,
|
|
@@ -75,8 +122,25 @@ def create_web_handler(repo: UsageRepository, pricing_engine: PricingEngine, rep
|
|
|
75
122
|
"total_cache_read_tokens": summary.cache_read_tokens,
|
|
76
123
|
"total_cache_write_tokens": summary.cache_write_tokens,
|
|
77
124
|
"total_tokens": summary.total_tokens,
|
|
78
|
-
"total_cost":
|
|
125
|
+
"total_cost": round(total_cost, 4),
|
|
126
|
+
},
|
|
127
|
+
"analytics": {
|
|
128
|
+
"timeframe": tf,
|
|
129
|
+
"provider": prov,
|
|
130
|
+
"daily_avg_cost": round(daily_avg_cost, 4),
|
|
131
|
+
"hourly_tokens": round(hourly_tokens, 1),
|
|
132
|
+
"projected_monthly_cost": round(projected_monthly_cost, 2),
|
|
133
|
+
"active_days": len(by_day),
|
|
79
134
|
},
|
|
135
|
+
"daily_trends": [
|
|
136
|
+
{
|
|
137
|
+
"date": d.date_str,
|
|
138
|
+
"requests": d.request_count,
|
|
139
|
+
"total_tokens": d.total_tokens,
|
|
140
|
+
"total_cost": d.estimated_cost_usd or 0.0,
|
|
141
|
+
}
|
|
142
|
+
for d in reversed(by_day)
|
|
143
|
+
],
|
|
80
144
|
"by_model": [
|
|
81
145
|
{
|
|
82
146
|
"model": m.model,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
"""Embedded Single-Page Modern Web Dashboard HTML/CSS/JS template for code-meter on http://localhost:5005."""
|
|
1
|
+
"""Embedded Single-Page Modern Web Analytical Dashboard HTML/CSS/JS template for code-meter on http://localhost:5005."""
|
|
2
2
|
|
|
3
3
|
HTML_DASHBOARD = """<!DOCTYPE html>
|
|
4
4
|
<html lang="en">
|
|
5
5
|
<head>
|
|
6
6
|
<meta charset="UTF-8">
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8
|
-
<title>code-meter | AI Token & Cost Dashboard</title>
|
|
8
|
+
<title>code-meter | AI Token & Cost Analytics Dashboard</title>
|
|
9
9
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
10
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
11
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
@@ -52,7 +52,7 @@ HTML_DASHBOARD = """<!DOCTYPE html>
|
|
|
52
52
|
display: flex;
|
|
53
53
|
justify-content: space-between;
|
|
54
54
|
align-items: center;
|
|
55
|
-
margin-bottom:
|
|
55
|
+
margin-bottom: 24px;
|
|
56
56
|
padding-bottom: 18px;
|
|
57
57
|
border-bottom: 1px solid var(--border-color);
|
|
58
58
|
}
|
|
@@ -119,12 +119,84 @@ HTML_DASHBOARD = """<!DOCTYPE html>
|
|
|
119
119
|
100% { box-shadow: 0 0 0 0 rgba(52, 211, 153, 0); }
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/* Filter Toolbar */
|
|
123
|
+
.filter-toolbar {
|
|
124
|
+
display: flex;
|
|
125
|
+
justify-content: space-between;
|
|
126
|
+
align-items: center;
|
|
127
|
+
background: var(--card-bg);
|
|
128
|
+
backdrop-filter: blur(16px);
|
|
129
|
+
border: 1px solid var(--border-color);
|
|
130
|
+
border-radius: 16px;
|
|
131
|
+
padding: 14px 20px;
|
|
132
|
+
margin-bottom: 24px;
|
|
133
|
+
gap: 16px;
|
|
134
|
+
flex-wrap: wrap;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.filter-group {
|
|
138
|
+
display: flex;
|
|
139
|
+
align-items: center;
|
|
140
|
+
gap: 8px;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.filter-label {
|
|
144
|
+
font-size: 12px;
|
|
145
|
+
font-weight: 700;
|
|
146
|
+
text-transform: uppercase;
|
|
147
|
+
letter-spacing: 0.6px;
|
|
148
|
+
color: var(--text-muted);
|
|
149
|
+
margin-right: 6px;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.btn-timeframe {
|
|
153
|
+
background: rgba(255, 255, 255, 0.05);
|
|
154
|
+
border: 1px solid var(--border-color);
|
|
155
|
+
color: var(--text-muted);
|
|
156
|
+
padding: 8px 16px;
|
|
157
|
+
border-radius: 10px;
|
|
158
|
+
font-size: 13px;
|
|
159
|
+
font-weight: 600;
|
|
160
|
+
cursor: pointer;
|
|
161
|
+
transition: all 0.2s ease;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.btn-timeframe:hover {
|
|
165
|
+
background: rgba(56, 189, 248, 0.15);
|
|
166
|
+
color: var(--accent-blue);
|
|
167
|
+
border-color: var(--accent-blue);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.btn-timeframe.active {
|
|
171
|
+
background: linear-gradient(135deg, #0284c7, #38bdf8);
|
|
172
|
+
color: #ffffff;
|
|
173
|
+
border-color: #38bdf8;
|
|
174
|
+
box-shadow: 0 4px 12px rgba(56, 189, 248, 0.3);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
select.select-filter {
|
|
178
|
+
background: rgba(15, 23, 42, 0.9);
|
|
179
|
+
border: 1px solid var(--border-color);
|
|
180
|
+
color: var(--text-main);
|
|
181
|
+
padding: 8px 14px;
|
|
182
|
+
border-radius: 10px;
|
|
183
|
+
font-size: 13px;
|
|
184
|
+
font-weight: 600;
|
|
185
|
+
outline: none;
|
|
186
|
+
cursor: pointer;
|
|
187
|
+
transition: border-color 0.2s ease;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
select.select-filter:focus {
|
|
191
|
+
border-color: var(--accent-blue);
|
|
192
|
+
}
|
|
193
|
+
|
|
122
194
|
/* Metrics Grid */
|
|
123
195
|
.metrics-grid {
|
|
124
196
|
display: grid;
|
|
125
|
-
grid-template-columns: repeat(auto-fit, minmax(
|
|
126
|
-
gap:
|
|
127
|
-
margin-bottom:
|
|
197
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
198
|
+
gap: 16px;
|
|
199
|
+
margin-bottom: 24px;
|
|
128
200
|
}
|
|
129
201
|
|
|
130
202
|
.metric-card {
|
|
@@ -132,7 +204,7 @@ HTML_DASHBOARD = """<!DOCTYPE html>
|
|
|
132
204
|
backdrop-filter: blur(16px);
|
|
133
205
|
border: 1px solid var(--border-color);
|
|
134
206
|
border-radius: 16px;
|
|
135
|
-
padding:
|
|
207
|
+
padding: 20px;
|
|
136
208
|
transition: transform 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease;
|
|
137
209
|
}
|
|
138
210
|
|
|
@@ -143,31 +215,37 @@ HTML_DASHBOARD = """<!DOCTYPE html>
|
|
|
143
215
|
}
|
|
144
216
|
|
|
145
217
|
.metric-label {
|
|
146
|
-
font-size:
|
|
147
|
-
font-weight:
|
|
218
|
+
font-size: 11px;
|
|
219
|
+
font-weight: 700;
|
|
148
220
|
text-transform: uppercase;
|
|
149
221
|
letter-spacing: 0.8px;
|
|
150
222
|
color: var(--text-muted);
|
|
151
|
-
margin-bottom:
|
|
223
|
+
margin-bottom: 8px;
|
|
152
224
|
}
|
|
153
225
|
|
|
154
226
|
.metric-value {
|
|
155
|
-
font-size:
|
|
227
|
+
font-size: 26px;
|
|
156
228
|
font-weight: 800;
|
|
157
229
|
letter-spacing: -0.5px;
|
|
158
230
|
}
|
|
159
231
|
|
|
160
232
|
.metric-value.highlight {
|
|
161
233
|
color: var(--accent-emerald);
|
|
162
|
-
text-shadow: 0 0
|
|
234
|
+
text-shadow: 0 0 16px rgba(52, 211, 153, 0.3);
|
|
163
235
|
}
|
|
164
236
|
|
|
165
|
-
|
|
237
|
+
.metric-sub {
|
|
238
|
+
font-size: 12px;
|
|
239
|
+
color: var(--text-muted);
|
|
240
|
+
margin-top: 4px;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* Analytics Charts Row */
|
|
166
244
|
.dashboard-row {
|
|
167
245
|
display: grid;
|
|
168
246
|
grid-template-columns: 1fr 1fr;
|
|
169
247
|
gap: 24px;
|
|
170
|
-
margin-bottom:
|
|
248
|
+
margin-bottom: 24px;
|
|
171
249
|
}
|
|
172
250
|
|
|
173
251
|
.dashboard-row.full {
|
|
@@ -185,7 +263,7 @@ HTML_DASHBOARD = """<!DOCTYPE html>
|
|
|
185
263
|
backdrop-filter: blur(16px);
|
|
186
264
|
border: 1px solid var(--border-color);
|
|
187
265
|
border-radius: 18px;
|
|
188
|
-
padding:
|
|
266
|
+
padding: 22px;
|
|
189
267
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
|
190
268
|
}
|
|
191
269
|
|
|
@@ -193,22 +271,84 @@ HTML_DASHBOARD = """<!DOCTYPE html>
|
|
|
193
271
|
display: flex;
|
|
194
272
|
justify-content: space-between;
|
|
195
273
|
align-items: center;
|
|
196
|
-
margin-bottom:
|
|
274
|
+
margin-bottom: 18px;
|
|
197
275
|
}
|
|
198
276
|
|
|
199
277
|
.panel-title {
|
|
200
|
-
font-size:
|
|
278
|
+
font-size: 15px;
|
|
201
279
|
font-weight: 700;
|
|
202
280
|
display: flex;
|
|
203
281
|
align-items: center;
|
|
204
282
|
gap: 10px;
|
|
205
283
|
}
|
|
206
284
|
|
|
285
|
+
/* Visual Bar Chart */
|
|
286
|
+
.chart-container {
|
|
287
|
+
display: flex;
|
|
288
|
+
align-items: flex-end;
|
|
289
|
+
gap: 12px;
|
|
290
|
+
height: 180px;
|
|
291
|
+
padding-top: 20px;
|
|
292
|
+
padding-bottom: 10px;
|
|
293
|
+
border-bottom: 1px solid var(--border-color);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.chart-bar-wrap {
|
|
297
|
+
flex: 1;
|
|
298
|
+
display: flex;
|
|
299
|
+
flex-direction: column;
|
|
300
|
+
align-items: center;
|
|
301
|
+
height: 100%;
|
|
302
|
+
justify-content: flex-end;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.chart-bar {
|
|
306
|
+
width: 100%;
|
|
307
|
+
max-width: 38px;
|
|
308
|
+
background: linear-gradient(to top, #0284c7, #38bdf8);
|
|
309
|
+
border-radius: 6px 6px 0 0;
|
|
310
|
+
transition: height 0.4s ease;
|
|
311
|
+
min-height: 4px;
|
|
312
|
+
position: relative;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.chart-bar:hover {
|
|
316
|
+
background: linear-gradient(to top, #34d399, #38bdf8);
|
|
317
|
+
box-shadow: 0 0 12px rgba(56, 189, 248, 0.5);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.chart-bar-tooltip {
|
|
321
|
+
position: absolute;
|
|
322
|
+
top: -30px;
|
|
323
|
+
left: 50%;
|
|
324
|
+
transform: translateX(-50%);
|
|
325
|
+
background: rgba(15, 23, 42, 0.95);
|
|
326
|
+
border: 1px solid var(--accent-blue);
|
|
327
|
+
color: #fff;
|
|
328
|
+
padding: 3px 8px;
|
|
329
|
+
border-radius: 6px;
|
|
330
|
+
font-size: 11px;
|
|
331
|
+
font-weight: 700;
|
|
332
|
+
white-space: nowrap;
|
|
333
|
+
display: none;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.chart-bar:hover .chart-bar-tooltip {
|
|
337
|
+
display: block;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.chart-label {
|
|
341
|
+
font-size: 11px;
|
|
342
|
+
color: var(--text-muted);
|
|
343
|
+
margin-top: 8px;
|
|
344
|
+
text-align: center;
|
|
345
|
+
}
|
|
346
|
+
|
|
207
347
|
/* Tables */
|
|
208
348
|
table {
|
|
209
349
|
width: 100%;
|
|
210
350
|
border-collapse: collapse;
|
|
211
|
-
font-size:
|
|
351
|
+
font-size: 13px;
|
|
212
352
|
}
|
|
213
353
|
|
|
214
354
|
th {
|
|
@@ -217,13 +357,13 @@ HTML_DASHBOARD = """<!DOCTYPE html>
|
|
|
217
357
|
color: var(--text-muted);
|
|
218
358
|
font-weight: 600;
|
|
219
359
|
border-bottom: 1px solid var(--border-color);
|
|
220
|
-
font-size:
|
|
360
|
+
font-size: 11px;
|
|
221
361
|
text-transform: uppercase;
|
|
222
362
|
letter-spacing: 0.5px;
|
|
223
363
|
}
|
|
224
364
|
|
|
225
365
|
td {
|
|
226
|
-
padding: 14px;
|
|
366
|
+
padding: 12px 14px;
|
|
227
367
|
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
|
228
368
|
vertical-align: middle;
|
|
229
369
|
}
|
|
@@ -251,7 +391,7 @@ HTML_DASHBOARD = """<!DOCTYPE html>
|
|
|
251
391
|
|
|
252
392
|
.prompt-text {
|
|
253
393
|
font-family: 'JetBrains Mono', monospace;
|
|
254
|
-
font-size:
|
|
394
|
+
font-size: 12px;
|
|
255
395
|
color: #f1f5f9;
|
|
256
396
|
line-height: 1.5;
|
|
257
397
|
word-break: break-word;
|
|
@@ -269,7 +409,7 @@ HTML_DASHBOARD = """<!DOCTYPE html>
|
|
|
269
409
|
|
|
270
410
|
.diff-summary {
|
|
271
411
|
font-family: 'JetBrains Mono', monospace;
|
|
272
|
-
font-size:
|
|
412
|
+
font-size: 11px;
|
|
273
413
|
color: #a7f3d0;
|
|
274
414
|
background: rgba(16, 185, 129, 0.08);
|
|
275
415
|
border: 1px solid rgba(16, 185, 129, 0.2);
|
|
@@ -280,8 +420,8 @@ HTML_DASHBOARD = """<!DOCTYPE html>
|
|
|
280
420
|
|
|
281
421
|
footer {
|
|
282
422
|
text-align: center;
|
|
283
|
-
margin-top:
|
|
284
|
-
padding-top:
|
|
423
|
+
margin-top: 36px;
|
|
424
|
+
padding-top: 20px;
|
|
285
425
|
border-top: 1px solid var(--border-color);
|
|
286
426
|
font-size: 13px;
|
|
287
427
|
color: var(--text-muted);
|
|
@@ -301,7 +441,7 @@ HTML_DASHBOARD = """<!DOCTYPE html>
|
|
|
301
441
|
<div class="logo-badge">⚡</div>
|
|
302
442
|
<div>
|
|
303
443
|
<h1>code-meter</h1>
|
|
304
|
-
<div class="subtitle">Real-Time AI Token, Prompt Trace & Cost Dashboard</div>
|
|
444
|
+
<div class="subtitle">Real-Time AI Token, Prompt Trace & Cost Analytics Dashboard</div>
|
|
305
445
|
</div>
|
|
306
446
|
</div>
|
|
307
447
|
<div class="live-status">
|
|
@@ -310,27 +450,70 @@ HTML_DASHBOARD = """<!DOCTYPE html>
|
|
|
310
450
|
</div>
|
|
311
451
|
</header>
|
|
312
452
|
|
|
313
|
-
<!--
|
|
453
|
+
<!-- Interactive Filter Toolbar -->
|
|
454
|
+
<div class="filter-toolbar">
|
|
455
|
+
<div class="filter-group">
|
|
456
|
+
<span class="filter-label">📅 Timeframe:</span>
|
|
457
|
+
<button class="btn-timeframe" data-tf="today" onclick="setTimeframe('today')">Today</button>
|
|
458
|
+
<button class="btn-timeframe" data-tf="week" onclick="setTimeframe('week')">Weekly (7 Days)</button>
|
|
459
|
+
<button class="btn-timeframe" data-tf="month" onclick="setTimeframe('month')">Monthly (30 Days)</button>
|
|
460
|
+
<button class="btn-timeframe active" data-tf="all" onclick="setTimeframe('all')">All Time</button>
|
|
461
|
+
</div>
|
|
462
|
+
|
|
463
|
+
<div class="filter-group">
|
|
464
|
+
<span class="filter-label">🤖 Provider:</span>
|
|
465
|
+
<select id="select-provider" class="select-filter" onchange="setProvider(this.value)">
|
|
466
|
+
<option value="all">All Providers (Claude, Codex, Antigravity)</option>
|
|
467
|
+
<option value="claude_code">Anthropic Claude Code</option>
|
|
468
|
+
<option value="codex">OpenAI Codex</option>
|
|
469
|
+
<option value="antigravity">Google Antigravity / AGY</option>
|
|
470
|
+
</select>
|
|
471
|
+
</div>
|
|
472
|
+
</div>
|
|
473
|
+
|
|
474
|
+
<!-- Analytics & Metrics Grid -->
|
|
314
475
|
<div class="metrics-grid">
|
|
315
476
|
<div class="metric-card">
|
|
316
477
|
<div class="metric-label">Total Requests</div>
|
|
317
478
|
<div class="metric-value" id="val-requests">0</div>
|
|
479
|
+
<div class="metric-sub" id="sub-requests">All sessions</div>
|
|
318
480
|
</div>
|
|
319
481
|
<div class="metric-card">
|
|
320
482
|
<div class="metric-label">Input Tokens</div>
|
|
321
483
|
<div class="metric-value" id="val-input">0</div>
|
|
484
|
+
<div class="metric-sub" id="sub-input">Prompt tokens</div>
|
|
322
485
|
</div>
|
|
323
486
|
<div class="metric-card">
|
|
324
487
|
<div class="metric-label">Output Tokens</div>
|
|
325
488
|
<div class="metric-value" id="val-output">0</div>
|
|
489
|
+
<div class="metric-sub" id="sub-output">Completion tokens</div>
|
|
490
|
+
</div>
|
|
491
|
+
<div class="metric-card">
|
|
492
|
+
<div class="metric-label">Token Run-Rate</div>
|
|
493
|
+
<div class="metric-value" id="val-rate">0 /hr</div>
|
|
494
|
+
<div class="metric-sub" id="sub-rate">Avg hourly tokens</div>
|
|
326
495
|
</div>
|
|
327
496
|
<div class="metric-card">
|
|
328
|
-
<div class="metric-label">
|
|
329
|
-
<div class="metric-value" id="val-
|
|
497
|
+
<div class="metric-label">Daily Avg Cost</div>
|
|
498
|
+
<div class="metric-value highlight" id="val-daily-cost">$0.00 /day</div>
|
|
499
|
+
<div class="metric-sub" id="sub-daily-cost">Avg daily burn rate</div>
|
|
330
500
|
</div>
|
|
331
501
|
<div class="metric-card">
|
|
332
502
|
<div class="metric-label">Total Estimated Cost</div>
|
|
333
503
|
<div class="metric-value highlight" id="val-cost">$0.00</div>
|
|
504
|
+
<div class="metric-sub" id="sub-projected">30-Day Forecast: $0.00</div>
|
|
505
|
+
</div>
|
|
506
|
+
</div>
|
|
507
|
+
|
|
508
|
+
<!-- Analytical Daily Usage Trend Chart -->
|
|
509
|
+
<div class="dashboard-row full">
|
|
510
|
+
<div class="panel">
|
|
511
|
+
<div class="panel-header">
|
|
512
|
+
<div class="panel-title">📊 Daily Usage & Cost Trend Analytics</div>
|
|
513
|
+
</div>
|
|
514
|
+
<div class="chart-container" id="daily-chart">
|
|
515
|
+
<div style="width: 100%; text-align: center; color: var(--text-muted); margin: auto;">Loading daily usage trend chart...</div>
|
|
516
|
+
</div>
|
|
334
517
|
</div>
|
|
335
518
|
</div>
|
|
336
519
|
|
|
@@ -440,11 +623,27 @@ HTML_DASHBOARD = """<!DOCTYPE html>
|
|
|
440
623
|
</div>
|
|
441
624
|
|
|
442
625
|
<footer>
|
|
443
|
-
code-meter v0.
|
|
626
|
+
code-meter v0.7.0 | Running on <strong>http://localhost:5005</strong> | <a href="https://github.com/dhanushnayak/code-meter" target="_blank">GitHub Repository</a>
|
|
444
627
|
</footer>
|
|
445
628
|
</div>
|
|
446
629
|
|
|
447
630
|
<script>
|
|
631
|
+
let currentTimeframe = 'all';
|
|
632
|
+
let currentProvider = 'all';
|
|
633
|
+
|
|
634
|
+
function setTimeframe(tf) {
|
|
635
|
+
currentTimeframe = tf;
|
|
636
|
+
document.querySelectorAll('.btn-timeframe').forEach(btn => {
|
|
637
|
+
btn.classList.toggle('active', btn.getAttribute('data-tf') === tf);
|
|
638
|
+
});
|
|
639
|
+
updateDashboard();
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function setProvider(prov) {
|
|
643
|
+
currentProvider = prov;
|
|
644
|
+
updateDashboard();
|
|
645
|
+
}
|
|
646
|
+
|
|
448
647
|
function formatNumber(num) {
|
|
449
648
|
if (!num) return '0';
|
|
450
649
|
if (num >= 1000000) return (num / 1000000).toFixed(2) + 'M';
|
|
@@ -472,18 +671,43 @@ HTML_DASHBOARD = """<!DOCTYPE html>
|
|
|
472
671
|
|
|
473
672
|
async function updateDashboard() {
|
|
474
673
|
try {
|
|
475
|
-
const res = await fetch(
|
|
674
|
+
const res = await fetch(`/api/summary?timeframe=${currentTimeframe}&provider=${currentProvider}`);
|
|
476
675
|
const data = await res.json();
|
|
477
676
|
|
|
478
677
|
// Metrics
|
|
479
678
|
const s = data.summary || {};
|
|
679
|
+
const a = data.analytics || {};
|
|
680
|
+
|
|
480
681
|
document.getElementById('val-requests').textContent = formatNumber(s.total_requests || 0);
|
|
481
682
|
document.getElementById('val-input').textContent = formatNumber(s.total_input_tokens || 0);
|
|
482
683
|
document.getElementById('val-output').textContent = formatNumber(s.total_output_tokens || 0);
|
|
483
|
-
|
|
684
|
+
|
|
685
|
+
document.getElementById('val-rate').textContent = formatNumber(a.hourly_tokens || 0) + ' /hr';
|
|
686
|
+
document.getElementById('val-daily-cost').textContent = '$' + (a.daily_avg_cost || 0).toFixed(4) + ' /day';
|
|
484
687
|
document.getElementById('val-cost').textContent = '$' + (s.total_cost || 0).toFixed(4);
|
|
688
|
+
document.getElementById('sub-projected').textContent = '30-Day Forecast: $' + (a.projected_monthly_cost || 0).toFixed(2);
|
|
689
|
+
|
|
690
|
+
// Render Daily Trends Bar Chart
|
|
691
|
+
const chartContainer = document.getElementById('daily-chart');
|
|
692
|
+
if (data.daily_trends && data.daily_trends.length > 0) {
|
|
693
|
+
const maxTokens = Math.max(...data.daily_trends.map(d => d.total_tokens), 1);
|
|
694
|
+
chartContainer.innerHTML = data.daily_trends.map(d => {
|
|
695
|
+
const heightPct = Math.max((d.total_tokens / maxTokens) * 100, 8);
|
|
696
|
+
const dateShort = d.date.substring(5);
|
|
697
|
+
return `
|
|
698
|
+
<div class="chart-bar-wrap">
|
|
699
|
+
<div class="chart-bar" style="height: ${heightPct}%;">
|
|
700
|
+
<div class="chart-bar-tooltip">${d.date}<br>${formatNumber(d.total_tokens)} tokens<br>$${d.total_cost.toFixed(4)}</div>
|
|
701
|
+
</div>
|
|
702
|
+
<div class="chart-label">${dateShort}</div>
|
|
703
|
+
</div>
|
|
704
|
+
`;
|
|
705
|
+
}).join('');
|
|
706
|
+
} else {
|
|
707
|
+
chartContainer.innerHTML = '<div style="width: 100%; text-align: center; color: var(--text-muted); margin: auto;">No daily trend data available for selected filter</div>';
|
|
708
|
+
}
|
|
485
709
|
|
|
486
|
-
// Prompt Trace Details Table
|
|
710
|
+
// Prompt Trace Details Table
|
|
487
711
|
const tblDetails = document.getElementById('tbl-prompt-details');
|
|
488
712
|
if (data.prompt_details && data.prompt_details.length > 0) {
|
|
489
713
|
tblDetails.innerHTML = data.prompt_details.map(pd => {
|
|
@@ -503,7 +727,7 @@ HTML_DASHBOARD = """<!DOCTYPE html>
|
|
|
503
727
|
`;
|
|
504
728
|
}).join('');
|
|
505
729
|
} else {
|
|
506
|
-
tblDetails.innerHTML = '<tr><td colspan="7" style="text-align:center; color: var(--text-muted);">No prompt trace details recorded
|
|
730
|
+
tblDetails.innerHTML = '<tr><td colspan="7" style="text-align:center; color: var(--text-muted);">No prompt trace details recorded for selected filter</td></tr>';
|
|
507
731
|
}
|
|
508
732
|
|
|
509
733
|
// Models Table
|
|
@@ -518,7 +742,7 @@ HTML_DASHBOARD = """<!DOCTYPE html>
|
|
|
518
742
|
</tr>
|
|
519
743
|
`).join('');
|
|
520
744
|
} else {
|
|
521
|
-
tblModels.innerHTML = '<tr><td colspan="4" style="text-align:center; color: var(--text-muted);">No model data recorded
|
|
745
|
+
tblModels.innerHTML = '<tr><td colspan="4" style="text-align:center; color: var(--text-muted);">No model data recorded for selected filter</td></tr>';
|
|
522
746
|
}
|
|
523
747
|
|
|
524
748
|
// Projects Table
|
|
@@ -533,7 +757,7 @@ HTML_DASHBOARD = """<!DOCTYPE html>
|
|
|
533
757
|
</tr>
|
|
534
758
|
`).join('');
|
|
535
759
|
} else {
|
|
536
|
-
tblProjects.innerHTML = '<tr><td colspan="4" style="text-align:center; color: var(--text-muted);">No project data recorded
|
|
760
|
+
tblProjects.innerHTML = '<tr><td colspan="4" style="text-align:center; color: var(--text-muted);">No project data recorded for selected filter</td></tr>';
|
|
537
761
|
}
|
|
538
762
|
|
|
539
763
|
// Prompts Table
|
|
@@ -547,7 +771,7 @@ HTML_DASHBOARD = """<!DOCTYPE html>
|
|
|
547
771
|
</tr>
|
|
548
772
|
`).join('');
|
|
549
773
|
} else {
|
|
550
|
-
tblPrompts.innerHTML = '<tr><td colspan="3" style="text-align:center; color: var(--text-muted);">No prompt history recorded
|
|
774
|
+
tblPrompts.innerHTML = '<tr><td colspan="3" style="text-align:center; color: var(--text-muted);">No prompt history recorded for selected filter</td></tr>';
|
|
551
775
|
}
|
|
552
776
|
|
|
553
777
|
// Code Changes Table
|
|
@@ -564,7 +788,7 @@ HTML_DASHBOARD = """<!DOCTYPE html>
|
|
|
564
788
|
</tr>
|
|
565
789
|
`).join('');
|
|
566
790
|
} else {
|
|
567
|
-
tblChanges.innerHTML = '<tr><td colspan="3" style="text-align:center; color: var(--text-muted);">No code edits recorded
|
|
791
|
+
tblChanges.innerHTML = '<tr><td colspan="3" style="text-align:center; color: var(--text-muted);">No code edits recorded for selected filter</td></tr>';
|
|
568
792
|
}
|
|
569
793
|
|
|
570
794
|
} catch (err) {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|