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.
Files changed (46) hide show
  1. {code_meter-0.5.0 → code_meter-0.7.0}/PKG-INFO +1 -1
  2. {code_meter-0.5.0 → code_meter-0.7.0}/pyproject.toml +1 -1
  3. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/storage/repository.py +52 -26
  4. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/ui/web/server.py +71 -7
  5. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/ui/web/static.py +261 -37
  6. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter.egg-info/PKG-INFO +1 -1
  7. {code_meter-0.5.0 → code_meter-0.7.0}/README.md +0 -0
  8. {code_meter-0.5.0 → code_meter-0.7.0}/setup.cfg +0 -0
  9. {code_meter-0.5.0 → code_meter-0.7.0}/setup.py +0 -0
  10. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/__init__.py +0 -0
  11. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/analytics/__init__.py +0 -0
  12. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/analytics/costs.py +0 -0
  13. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/analytics/reports.py +0 -0
  14. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/analytics/tokens.py +0 -0
  15. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/cli.py +0 -0
  16. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/config.py +0 -0
  17. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/models/__init__.py +0 -0
  18. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/models/project.py +0 -0
  19. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/models/usage.py +0 -0
  20. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/pricing/__init__.py +0 -0
  21. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/pricing/anthropic.py +0 -0
  22. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/pricing/engine.py +0 -0
  23. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/pricing/google.py +0 -0
  24. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/pricing/live.py +0 -0
  25. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/pricing/openai.py +0 -0
  26. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/providers/__init__.py +0 -0
  27. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/providers/antigravity.py +0 -0
  28. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/providers/base.py +0 -0
  29. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/providers/claude_code.py +0 -0
  30. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/providers/codex.py +0 -0
  31. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/storage/__init__.py +0 -0
  32. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/storage/database.py +0 -0
  33. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/ui/__init__.py +0 -0
  34. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/ui/dashboard.py +0 -0
  35. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/ui/tables.py +0 -0
  36. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/ui/web/__init__.py +0 -0
  37. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter/watcher.py +0 -0
  38. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter.egg-info/SOURCES.txt +0 -0
  39. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter.egg-info/dependency_links.txt +0 -0
  40. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter.egg-info/entry_points.txt +0 -0
  41. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter.egg-info/requires.txt +0 -0
  42. {code_meter-0.5.0 → code_meter-0.7.0}/src/code_meter.egg-info/top_level.txt +0 -0
  43. {code_meter-0.5.0 → code_meter-0.7.0}/tests/test_analytics.py +0 -0
  44. {code_meter-0.5.0 → code_meter-0.7.0}/tests/test_database.py +0 -0
  45. {code_meter-0.5.0 → code_meter-0.7.0}/tests/test_pricing.py +0 -0
  46. {code_meter-0.5.0 → code_meter-0.7.0}/tests/test_scanner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-meter
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary: Local AI coding token usage tracker and cost estimator for Claude Code and future agents.
5
5
  Author-email: Dhanush Nayak <dhanushnayak.ram@mail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "code-meter"
7
- version = "0.5.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
- path = self.path.split("?")[0]
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=15)
56
- code_changes = repo.query_code_changes(limit=15)
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, filter_date_str=datetime.now(timezone.utc).strftime("%Y-%m-%d"))
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": summary.estimated_cost_usd or 0.0,
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: 28px;
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(220px, 1fr));
126
- gap: 18px;
127
- margin-bottom: 28px;
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: 22px;
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: 12px;
147
- font-weight: 600;
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: 10px;
223
+ margin-bottom: 8px;
152
224
  }
153
225
 
154
226
  .metric-value {
155
- font-size: 28px;
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 20px rgba(52, 211, 153, 0.3);
234
+ text-shadow: 0 0 16px rgba(52, 211, 153, 0.3);
163
235
  }
164
236
 
165
- /* Dashboard Layout */
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: 28px;
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: 24px;
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: 20px;
274
+ margin-bottom: 18px;
197
275
  }
198
276
 
199
277
  .panel-title {
200
- font-size: 16px;
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: 14px;
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: 12px;
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: 13px;
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: 12px;
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: 40px;
284
- padding-top: 24px;
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
- <!-- Metrics Cards -->
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">Cache Read / Write</div>
329
- <div class="metric-value" id="val-cache">0</div>
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.4.0 | Running on <strong>http://localhost:5005</strong> | <a href="https://github.com/dhanushnayak/code-meter" target="_blank">GitHub Repository</a>
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('/api/summary');
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
- document.getElementById('val-cache').textContent = formatNumber((s.total_cache_read_tokens || 0) + (s.total_cache_write_tokens || 0));
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 (Query, Consumed Tokens, Internal Requests & Outputs)
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 yet</td></tr>';
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 yet</td></tr>';
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 yet</td></tr>';
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 yet</td></tr>';
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 yet</td></tr>';
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) {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-meter
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary: Local AI coding token usage tracker and cost estimator for Claude Code and future agents.
5
5
  Author-email: Dhanush Nayak <dhanushnayak.ram@mail.com>
6
6
  License: MIT
File without changes
File without changes
File without changes