code-meter 0.3.0__tar.gz → 0.5.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.3.0 → code_meter-0.5.0}/PKG-INFO +3 -2
  2. {code_meter-0.3.0 → code_meter-0.5.0}/README.md +2 -1
  3. {code_meter-0.3.0 → code_meter-0.5.0}/pyproject.toml +1 -1
  4. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/cli.py +23 -3
  5. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/storage/repository.py +82 -0
  6. code_meter-0.5.0/src/code_meter/ui/web/__init__.py +5 -0
  7. code_meter-0.5.0/src/code_meter/ui/web/server.py +196 -0
  8. code_meter-0.5.0/src/code_meter/ui/web/static.py +581 -0
  9. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter.egg-info/PKG-INFO +3 -2
  10. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter.egg-info/SOURCES.txt +3 -0
  11. {code_meter-0.3.0 → code_meter-0.5.0}/setup.cfg +0 -0
  12. {code_meter-0.3.0 → code_meter-0.5.0}/setup.py +0 -0
  13. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/__init__.py +0 -0
  14. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/analytics/__init__.py +0 -0
  15. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/analytics/costs.py +0 -0
  16. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/analytics/reports.py +0 -0
  17. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/analytics/tokens.py +0 -0
  18. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/config.py +0 -0
  19. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/models/__init__.py +0 -0
  20. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/models/project.py +0 -0
  21. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/models/usage.py +0 -0
  22. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/pricing/__init__.py +0 -0
  23. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/pricing/anthropic.py +0 -0
  24. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/pricing/engine.py +0 -0
  25. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/pricing/google.py +0 -0
  26. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/pricing/live.py +0 -0
  27. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/pricing/openai.py +0 -0
  28. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/providers/__init__.py +0 -0
  29. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/providers/antigravity.py +0 -0
  30. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/providers/base.py +0 -0
  31. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/providers/claude_code.py +0 -0
  32. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/providers/codex.py +0 -0
  33. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/storage/__init__.py +0 -0
  34. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/storage/database.py +0 -0
  35. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/ui/__init__.py +0 -0
  36. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/ui/dashboard.py +0 -0
  37. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/ui/tables.py +0 -0
  38. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/watcher.py +0 -0
  39. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter.egg-info/dependency_links.txt +0 -0
  40. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter.egg-info/entry_points.txt +0 -0
  41. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter.egg-info/requires.txt +0 -0
  42. {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter.egg-info/top_level.txt +0 -0
  43. {code_meter-0.3.0 → code_meter-0.5.0}/tests/test_analytics.py +0 -0
  44. {code_meter-0.3.0 → code_meter-0.5.0}/tests/test_database.py +0 -0
  45. {code_meter-0.3.0 → code_meter-0.5.0}/tests/test_pricing.py +0 -0
  46. {code_meter-0.3.0 → code_meter-0.5.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.3.0
3
+ Version: 0.5.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
@@ -106,7 +106,8 @@ This launches the interactive Rich terminal dashboard displaying key metrics, mo
106
106
  | Command | Description |
107
107
  | :--- | :--- |
108
108
  | `code-meter` | Open main interactive Rich terminal dashboard UI |
109
- | `code-meter ui` | Launch live interactive Rich terminal UI watching all AI log directories |
109
+ | `code-meter ui` | Launch live Web Dashboard on **http://localhost:5005** (auto-opens browser) |
110
+ | `code-meter ui --terminal` | Launch live interactive Rich terminal UI |
110
111
  | `code-meter watch` | Live watch mode (auto-refreshes UI as AI agents write logs) |
111
112
  | `code-meter update-pricing` | Fetch latest real-time token pricing for 2,800+ models |
112
113
  | `code-meter scan` | Force an incremental scan of local usage directories |
@@ -84,7 +84,8 @@ This launches the interactive Rich terminal dashboard displaying key metrics, mo
84
84
  | Command | Description |
85
85
  | :--- | :--- |
86
86
  | `code-meter` | Open main interactive Rich terminal dashboard UI |
87
- | `code-meter ui` | Launch live interactive Rich terminal UI watching all AI log directories |
87
+ | `code-meter ui` | Launch live Web Dashboard on **http://localhost:5005** (auto-opens browser) |
88
+ | `code-meter ui --terminal` | Launch live interactive Rich terminal UI |
88
89
  | `code-meter watch` | Live watch mode (auto-refreshes UI as AI agents write logs) |
89
90
  | `code-meter update-pricing` | Fetch latest real-time token pricing for 2,800+ models |
90
91
  | `code-meter scan` | Force an incremental scan of local usage directories |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "code-meter"
7
- version = "0.3.0"
7
+ version = "0.5.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" }]
@@ -423,9 +423,29 @@ def update_pricing_cmd():
423
423
 
424
424
 
425
425
  @app.command("ui")
426
- def ui_cmd():
427
- """Launch the live interactive Rich terminal UI with auto-refresh watch mode."""
428
- _launch_live_ui()
426
+ @app.command("dashboard")
427
+ @app.command("web")
428
+ def ui_cmd(
429
+ port: int = typer.Option(5005, "--port", "-p", help="Port number for local dashboard server"),
430
+ host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host address for dashboard server"),
431
+ terminal: bool = typer.Option(False, "--terminal", "-t", help="Launch Rich terminal UI instead of Web UI"),
432
+ no_browser: bool = typer.Option(False, "--no-browser", help="Do not open web browser automatically"),
433
+ ):
434
+ """Launch local web dashboard UI on http://localhost:5005 (or terminal UI with --terminal)."""
435
+ if terminal:
436
+ _launch_live_ui()
437
+ else:
438
+ from code_meter.ui.web.server import start_web_server
439
+ config, repo, pricing_engine, _, report_gen = _get_services()
440
+ start_web_server(
441
+ host=host,
442
+ port=port,
443
+ open_browser=not no_browser,
444
+ config=config,
445
+ repo=repo,
446
+ pricing_engine=pricing_engine,
447
+ report_gen=report_gen,
448
+ )
429
449
 
430
450
 
431
451
  @app.command("watch")
@@ -511,3 +511,85 @@ class UsageRepository:
511
511
  conn.commit()
512
512
  finally:
513
513
  conn.close()
514
+
515
+ def query_prompt_details(self, limit: int = 50, pricing_engine: Optional[Any] = None) -> List[Dict[str, Any]]:
516
+ """Query user prompts enriched with consumed tokens, internal request count, cost, and output code edits."""
517
+ prompts = self.query_prompts(limit=limit)
518
+ details: List[Dict[str, Any]] = []
519
+
520
+ conn = self.db.get_connection()
521
+ try:
522
+ cursor = conn.cursor()
523
+ for p in prompts:
524
+ session_id = p.session_id
525
+
526
+ cursor.execute(
527
+ """
528
+ SELECT provider, model, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, timestamp
529
+ FROM usage_events
530
+ WHERE session_id = ?;
531
+ """,
532
+ (session_id,),
533
+ )
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
+
560
+ cursor.execute(
561
+ """
562
+ SELECT change_type, file_path, diff_summary
563
+ FROM session_code_changes
564
+ WHERE session_id = ?
565
+ ORDER BY timestamp DESC;
566
+ """,
567
+ (session_id,),
568
+ )
569
+ changes = [
570
+ {
571
+ "change_type": r["change_type"],
572
+ "file_path": r["file_path"],
573
+ "diff_summary": r["diff_summary"],
574
+ }
575
+ for r in cursor.fetchall()
576
+ ]
577
+
578
+ details.append(
579
+ {
580
+ "prompt_id": p.prompt_id,
581
+ "session_id": p.session_id,
582
+ "provider": p.provider,
583
+ "timestamp": p.timestamp.isoformat(),
584
+ "prompt_text": p.prompt_text,
585
+ "project_id": p.project_id or "",
586
+ "internal_requests": max(total_requests, 1),
587
+ "total_tokens": total_tokens,
588
+ "cost_usd": round(total_cost, 4),
589
+ "outputs": changes,
590
+ }
591
+ )
592
+ finally:
593
+ conn.close()
594
+
595
+ return details
@@ -0,0 +1,5 @@
1
+ """Web UI package for code-meter dashboard."""
2
+
3
+ from code_meter.ui.web.server import start_web_server
4
+
5
+ __all__ = ["start_web_server"]
@@ -0,0 +1,196 @@
1
+ """Lightweight Web Dashboard HTTP Server running on http://localhost:5005 for code-meter."""
2
+
3
+ import json
4
+ import logging
5
+ import threading
6
+ import time
7
+ import webbrowser
8
+ from datetime import datetime, timezone
9
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
10
+ from typing import Any, Dict, Optional
11
+
12
+ from code_meter.analytics.reports import ReportGenerator
13
+ from code_meter.pricing.engine import PricingEngine
14
+ from code_meter.providers.antigravity import AntigravityProvider
15
+ from code_meter.providers.claude_code import ClaudeCodeProvider
16
+ from code_meter.providers.codex import CodexProvider
17
+ from code_meter.storage.repository import UsageRepository
18
+ from code_meter.ui.web.static import HTML_DASHBOARD
19
+ from code_meter.watcher import run_watch_loop
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def create_web_handler(repo: UsageRepository, pricing_engine: PricingEngine, report_gen: ReportGenerator):
25
+ """Factory creating HTTPRequestHandler with injected service dependencies."""
26
+
27
+ class CodeMeterWebHandler(BaseHTTPRequestHandler):
28
+ def log_message(self, format: str, *args: Any) -> None:
29
+ # Suppress default noisy HTTP request logs
30
+ pass
31
+
32
+ def do_GET(self) -> None:
33
+ path = self.path.split("?")[0]
34
+
35
+ if path in ("/", "/index.html"):
36
+ self.send_response(200)
37
+ self.send_header("Content-Type", "text/html; charset=utf-8")
38
+ self.end_headers()
39
+ self.wfile.write(HTML_DASHBOARD.encode("utf-8"))
40
+
41
+ elif path == "/api/summary":
42
+ self.handle_api_summary()
43
+
44
+ elif path == "/api/prompts":
45
+ self.handle_api_prompts()
46
+
47
+ elif path == "/api/changes":
48
+ self.handle_api_changes()
49
+
50
+ else:
51
+ self.send_error(404, "Not Found")
52
+
53
+ def handle_api_summary(self) -> None:
54
+ records = repo.query_usage_events()
55
+ prompts = repo.query_prompts(limit=15)
56
+ code_changes = repo.query_code_changes(limit=15)
57
+
58
+ costs = [
59
+ pricing_engine.calculate_cost(
60
+ r.provider, r.model, r.input_tokens, r.output_tokens, r.cache_read_tokens, r.cache_write_tokens, r.timestamp
61
+ )
62
+ for r in records
63
+ ]
64
+
65
+ summary = report_gen.generate_summary(records, filter_date_str=datetime.now(timezone.utc).strftime("%Y-%m-%d"))
66
+ by_model = report_gen.aggregate_by_model(records, costs)
67
+ by_project = report_gen.aggregate_by_project(records, costs)
68
+ prompt_details = repo.query_prompt_details(limit=25, pricing_engine=pricing_engine)
69
+
70
+ payload = {
71
+ "summary": {
72
+ "total_requests": summary.total_requests,
73
+ "total_input_tokens": summary.input_tokens,
74
+ "total_output_tokens": summary.output_tokens,
75
+ "total_cache_read_tokens": summary.cache_read_tokens,
76
+ "total_cache_write_tokens": summary.cache_write_tokens,
77
+ "total_tokens": summary.total_tokens,
78
+ "total_cost": summary.estimated_cost_usd or 0.0,
79
+ },
80
+ "by_model": [
81
+ {
82
+ "model": m.model,
83
+ "requests": m.request_count,
84
+ "total_tokens": m.total_tokens,
85
+ "total_cost": m.estimated_cost_usd or 0.0,
86
+ }
87
+ for m in by_model
88
+ ],
89
+ "by_project": [
90
+ {
91
+ "project_name": p.project_name,
92
+ "requests": p.request_count,
93
+ "total_tokens": p.total_tokens,
94
+ "total_cost": p.estimated_cost_usd or 0.0,
95
+ }
96
+ for p in by_project
97
+ ],
98
+ "prompt_details": prompt_details,
99
+ "prompts": [
100
+ {
101
+ "timestamp": p.timestamp,
102
+ "provider": p.provider,
103
+ "prompt_text": p.prompt_text,
104
+ "project_id": p.project_id or "",
105
+ }
106
+ for p in prompts
107
+ ],
108
+ "code_changes": [
109
+ {
110
+ "timestamp": c.timestamp,
111
+ "provider": c.provider,
112
+ "file_path": c.file_path,
113
+ "change_type": c.change_type,
114
+ "diff_summary": c.diff_summary,
115
+ }
116
+ for c in code_changes
117
+ ],
118
+ }
119
+
120
+ self.send_json_response(payload)
121
+
122
+ def handle_api_prompts(self) -> None:
123
+ prompts = repo.query_prompts(limit=50)
124
+ self.send_json_response({"prompts": [p.model_dump() for p in prompts]})
125
+
126
+ def handle_api_changes(self) -> None:
127
+ changes = repo.query_code_changes(limit=50)
128
+ self.send_json_response({"code_changes": [c.model_dump() for c in changes]})
129
+
130
+ def send_json_response(self, data: Dict[str, Any]) -> None:
131
+ body = json.dumps(data, indent=2, default=str).encode("utf-8")
132
+ self.send_response(200)
133
+ self.send_header("Content-Type", "application/json")
134
+ self.send_header("Access-Control-Allow-Origin", "*")
135
+ self.end_headers()
136
+ self.wfile.write(body)
137
+
138
+ return CodeMeterWebHandler
139
+
140
+
141
+ def start_web_server(
142
+ host: str = "127.0.0.1",
143
+ port: int = 5005,
144
+ open_browser: bool = True,
145
+ config: Optional[Any] = None,
146
+ repo: Optional[UsageRepository] = None,
147
+ pricing_engine: Optional[PricingEngine] = None,
148
+ report_gen: Optional[ReportGenerator] = None,
149
+ ) -> None:
150
+ """Launch HTTP server on specified host/port serving localhost web dashboard."""
151
+ if not (repo and pricing_engine and report_gen and config):
152
+ from code_meter.cli import _get_services
153
+ config, repo, pricing_engine, providers, report_gen = _get_services()
154
+ else:
155
+ providers = [
156
+ ClaudeCodeProvider(config.get_claude_dir()),
157
+ CodexProvider(config.get_codex_dir()),
158
+ AntigravityProvider(config.get_antigravity_dir()),
159
+ ]
160
+
161
+ # Background scanner loop
162
+ def background_scan_and_watch():
163
+ def do_scan():
164
+ states = repo.get_scan_states()
165
+ for p in providers:
166
+ res = p.scan(states)
167
+ repo.save_records(res.records)
168
+ repo.save_prompts(res.prompts)
169
+ repo.save_code_changes(res.code_changes)
170
+ repo.update_scan_states(res.updated_states)
171
+
172
+ target_dirs = [config.get_claude_dir(), config.get_codex_dir(), config.get_antigravity_dir()]
173
+ run_watch_loop(target_dir=target_dirs, on_change_callback=do_scan, refresh_seconds=config.get_refresh_seconds())
174
+
175
+ watch_thread = threading.Thread(target=background_scan_and_watch, daemon=True)
176
+ watch_thread.start()
177
+
178
+ handler_cls = create_web_handler(repo, pricing_engine, report_gen)
179
+ server = ThreadingHTTPServer((host, port), handler_cls)
180
+
181
+ url = f"http://localhost:{port}"
182
+ print(f"\n[code-meter] Web Dashboard running at: {url}")
183
+ print("Press Ctrl+C to stop the web server.\n")
184
+
185
+ if open_browser:
186
+ def open_browser_later():
187
+ time.sleep(0.5)
188
+ webbrowser.open(url)
189
+ threading.Thread(target=open_browser_later, daemon=True).start()
190
+
191
+ try:
192
+ server.serve_forever()
193
+ except KeyboardInterrupt:
194
+ print("\nShutting down web dashboard server...")
195
+ server.shutdown()
196
+ server.server_close()
@@ -0,0 +1,581 @@
1
+ """Embedded Single-Page Modern Web Dashboard HTML/CSS/JS template for code-meter on http://localhost:5005."""
2
+
3
+ HTML_DASHBOARD = """<!DOCTYPE html>
4
+ <html lang="en">
5
+ <head>
6
+ <meta charset="UTF-8">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+ <title>code-meter | AI Token & Cost Dashboard</title>
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
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">
12
+ <style>
13
+ :root {
14
+ --bg-dark: #090d16;
15
+ --card-bg: rgba(15, 23, 42, 0.75);
16
+ --border-color: rgba(255, 255, 255, 0.08);
17
+ --border-hover: rgba(56, 189, 248, 0.3);
18
+ --text-main: #f8fafc;
19
+ --text-muted: #94a3b8;
20
+ --accent-blue: #38bdf8;
21
+ --accent-emerald: #34d399;
22
+ --accent-purple: #c084fc;
23
+ --accent-amber: #fbbf24;
24
+ --accent-orange: #fb923c;
25
+ --shadow-glow: 0 10px 30px -5px rgba(56, 189, 248, 0.2);
26
+ }
27
+
28
+ * {
29
+ box-sizing: border-box;
30
+ margin: 0;
31
+ padding: 0;
32
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
33
+ }
34
+
35
+ body {
36
+ background-color: var(--bg-dark);
37
+ color: var(--text-main);
38
+ padding: 24px;
39
+ min-height: 100vh;
40
+ background-image:
41
+ radial-gradient(at 0% 0%, rgba(56, 189, 248, 0.12) 0px, transparent 50%),
42
+ radial-gradient(at 100% 100%, rgba(192, 132, 252, 0.12) 0px, transparent 50%),
43
+ radial-gradient(at 50% 50%, rgba(52, 211, 153, 0.05) 0px, transparent 60%);
44
+ }
45
+
46
+ .container {
47
+ max-width: 1400px;
48
+ margin: 0 auto;
49
+ }
50
+
51
+ header {
52
+ display: flex;
53
+ justify-content: space-between;
54
+ align-items: center;
55
+ margin-bottom: 28px;
56
+ padding-bottom: 18px;
57
+ border-bottom: 1px solid var(--border-color);
58
+ }
59
+
60
+ .brand {
61
+ display: flex;
62
+ align-items: center;
63
+ gap: 14px;
64
+ }
65
+
66
+ .logo-badge {
67
+ background: linear-gradient(135deg, #0284c7, #9333ea);
68
+ width: 46px;
69
+ height: 46px;
70
+ border-radius: 12px;
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ font-weight: 800;
75
+ font-size: 22px;
76
+ box-shadow: 0 4px 16px rgba(147, 51, 234, 0.4);
77
+ }
78
+
79
+ h1 {
80
+ font-size: 26px;
81
+ font-weight: 800;
82
+ letter-spacing: -0.5px;
83
+ background: linear-gradient(to right, #ffffff, #94a3b8);
84
+ -webkit-background-clip: text;
85
+ -webkit-text-fill-color: transparent;
86
+ }
87
+
88
+ .subtitle {
89
+ font-size: 13px;
90
+ color: var(--text-muted);
91
+ }
92
+
93
+ .live-status {
94
+ display: flex;
95
+ align-items: center;
96
+ gap: 10px;
97
+ background: rgba(52, 211, 153, 0.08);
98
+ border: 1px solid rgba(52, 211, 153, 0.25);
99
+ padding: 8px 16px;
100
+ border-radius: 24px;
101
+ font-size: 13px;
102
+ font-weight: 600;
103
+ color: var(--accent-emerald);
104
+ backdrop-filter: blur(8px);
105
+ }
106
+
107
+ .pulse {
108
+ width: 9px;
109
+ height: 9px;
110
+ background-color: var(--accent-emerald);
111
+ border-radius: 50%;
112
+ box-shadow: 0 0 0 0 rgba(52, 211, 153, 0.7);
113
+ animation: pulse-ring 2s infinite;
114
+ }
115
+
116
+ @keyframes pulse-ring {
117
+ 0% { box-shadow: 0 0 0 0 rgba(52, 211, 153, 0.7); }
118
+ 70% { box-shadow: 0 0 0 10px rgba(52, 211, 153, 0); }
119
+ 100% { box-shadow: 0 0 0 0 rgba(52, 211, 153, 0); }
120
+ }
121
+
122
+ /* Metrics Grid */
123
+ .metrics-grid {
124
+ display: grid;
125
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
126
+ gap: 18px;
127
+ margin-bottom: 28px;
128
+ }
129
+
130
+ .metric-card {
131
+ background: var(--card-bg);
132
+ backdrop-filter: blur(16px);
133
+ border: 1px solid var(--border-color);
134
+ border-radius: 16px;
135
+ padding: 22px;
136
+ transition: transform 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease;
137
+ }
138
+
139
+ .metric-card:hover {
140
+ transform: translateY(-3px);
141
+ border-color: var(--border-hover);
142
+ box-shadow: var(--shadow-glow);
143
+ }
144
+
145
+ .metric-label {
146
+ font-size: 12px;
147
+ font-weight: 600;
148
+ text-transform: uppercase;
149
+ letter-spacing: 0.8px;
150
+ color: var(--text-muted);
151
+ margin-bottom: 10px;
152
+ }
153
+
154
+ .metric-value {
155
+ font-size: 28px;
156
+ font-weight: 800;
157
+ letter-spacing: -0.5px;
158
+ }
159
+
160
+ .metric-value.highlight {
161
+ color: var(--accent-emerald);
162
+ text-shadow: 0 0 20px rgba(52, 211, 153, 0.3);
163
+ }
164
+
165
+ /* Dashboard Layout */
166
+ .dashboard-row {
167
+ display: grid;
168
+ grid-template-columns: 1fr 1fr;
169
+ gap: 24px;
170
+ margin-bottom: 28px;
171
+ }
172
+
173
+ .dashboard-row.full {
174
+ grid-template-columns: 1fr;
175
+ }
176
+
177
+ @media (max-width: 900px) {
178
+ .dashboard-row {
179
+ grid-template-columns: 1fr;
180
+ }
181
+ }
182
+
183
+ .panel {
184
+ background: var(--card-bg);
185
+ backdrop-filter: blur(16px);
186
+ border: 1px solid var(--border-color);
187
+ border-radius: 18px;
188
+ padding: 24px;
189
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
190
+ }
191
+
192
+ .panel-header {
193
+ display: flex;
194
+ justify-content: space-between;
195
+ align-items: center;
196
+ margin-bottom: 20px;
197
+ }
198
+
199
+ .panel-title {
200
+ font-size: 16px;
201
+ font-weight: 700;
202
+ display: flex;
203
+ align-items: center;
204
+ gap: 10px;
205
+ }
206
+
207
+ /* Tables */
208
+ table {
209
+ width: 100%;
210
+ border-collapse: collapse;
211
+ font-size: 14px;
212
+ }
213
+
214
+ th {
215
+ text-align: left;
216
+ padding: 12px 14px;
217
+ color: var(--text-muted);
218
+ font-weight: 600;
219
+ border-bottom: 1px solid var(--border-color);
220
+ font-size: 12px;
221
+ text-transform: uppercase;
222
+ letter-spacing: 0.5px;
223
+ }
224
+
225
+ td {
226
+ padding: 14px;
227
+ border-bottom: 1px solid rgba(255, 255, 255, 0.04);
228
+ vertical-align: middle;
229
+ }
230
+
231
+ tr:last-child td {
232
+ border-bottom: none;
233
+ }
234
+
235
+ .badge {
236
+ display: inline-flex;
237
+ align-items: center;
238
+ gap: 4px;
239
+ padding: 4px 10px;
240
+ border-radius: 8px;
241
+ font-size: 11px;
242
+ font-weight: 700;
243
+ text-transform: uppercase;
244
+ letter-spacing: 0.5px;
245
+ }
246
+
247
+ .badge-claude { background: rgba(249, 115, 22, 0.12); color: var(--accent-orange); border: 1px solid rgba(249, 115, 22, 0.3); }
248
+ .badge-codex { background: rgba(56, 189, 248, 0.12); color: var(--accent-blue); border: 1px solid rgba(56, 189, 248, 0.3); }
249
+ .badge-antigravity { background: rgba(192, 132, 252, 0.12); color: var(--accent-purple); border: 1px solid rgba(192, 132, 252, 0.3); }
250
+ .badge-emerald { background: rgba(52, 211, 153, 0.12); color: var(--accent-emerald); border: 1px solid rgba(52, 211, 153, 0.3); }
251
+
252
+ .prompt-text {
253
+ font-family: 'JetBrains Mono', monospace;
254
+ font-size: 13px;
255
+ color: #f1f5f9;
256
+ line-height: 1.5;
257
+ word-break: break-word;
258
+ }
259
+
260
+ .file-path {
261
+ font-family: 'JetBrains Mono', monospace;
262
+ font-size: 12px;
263
+ color: var(--accent-blue);
264
+ display: flex;
265
+ align-items: center;
266
+ gap: 6px;
267
+ margin-bottom: 4px;
268
+ }
269
+
270
+ .diff-summary {
271
+ font-family: 'JetBrains Mono', monospace;
272
+ font-size: 12px;
273
+ color: #a7f3d0;
274
+ background: rgba(16, 185, 129, 0.08);
275
+ border: 1px solid rgba(16, 185, 129, 0.2);
276
+ padding: 6px 10px;
277
+ border-radius: 6px;
278
+ white-space: pre-wrap;
279
+ }
280
+
281
+ footer {
282
+ text-align: center;
283
+ margin-top: 40px;
284
+ padding-top: 24px;
285
+ border-top: 1px solid var(--border-color);
286
+ font-size: 13px;
287
+ color: var(--text-muted);
288
+ }
289
+
290
+ footer a {
291
+ color: var(--accent-blue);
292
+ text-decoration: none;
293
+ font-weight: 600;
294
+ }
295
+ </style>
296
+ </head>
297
+ <body>
298
+ <div class="container">
299
+ <header>
300
+ <div class="brand">
301
+ <div class="logo-badge">⚡</div>
302
+ <div>
303
+ <h1>code-meter</h1>
304
+ <div class="subtitle">Real-Time AI Token, Prompt Trace & Cost Dashboard</div>
305
+ </div>
306
+ </div>
307
+ <div class="live-status">
308
+ <div class="pulse"></div>
309
+ <span>Live Web Dashboard (localhost:5005)</span>
310
+ </div>
311
+ </header>
312
+
313
+ <!-- Metrics Cards -->
314
+ <div class="metrics-grid">
315
+ <div class="metric-card">
316
+ <div class="metric-label">Total Requests</div>
317
+ <div class="metric-value" id="val-requests">0</div>
318
+ </div>
319
+ <div class="metric-card">
320
+ <div class="metric-label">Input Tokens</div>
321
+ <div class="metric-value" id="val-input">0</div>
322
+ </div>
323
+ <div class="metric-card">
324
+ <div class="metric-label">Output Tokens</div>
325
+ <div class="metric-value" id="val-output">0</div>
326
+ </div>
327
+ <div class="metric-card">
328
+ <div class="metric-label">Cache Read / Write</div>
329
+ <div class="metric-value" id="val-cache">0</div>
330
+ </div>
331
+ <div class="metric-card">
332
+ <div class="metric-label">Total Estimated Cost</div>
333
+ <div class="metric-value highlight" id="val-cost">$0.00</div>
334
+ </div>
335
+ </div>
336
+
337
+ <!-- Row 1: Detailed User Prompt Trace (Queries, Consumed Tokens, Internal Requests & Output Edits) -->
338
+ <div class="dashboard-row full">
339
+ <div class="panel">
340
+ <div class="panel-header">
341
+ <div class="panel-title">💡 User Prompt Traces (Queries, Consumed Tokens, Internal Requests & Output Edits)</div>
342
+ </div>
343
+ <table>
344
+ <thead>
345
+ <tr>
346
+ <th style="width: 8%;">Time</th>
347
+ <th style="width: 10%;">Provider</th>
348
+ <th style="width: 32%;">User Prompt / Query</th>
349
+ <th style="width: 12%;">Internal Requests</th>
350
+ <th style="width: 12%;">Tokens Consumed</th>
351
+ <th style="width: 10%;">Cost ($)</th>
352
+ <th style="width: 16%;">Output Code Edits</th>
353
+ </tr>
354
+ </thead>
355
+ <tbody id="tbl-prompt-details">
356
+ <tr><td colspan="7" style="text-align:center; color: var(--text-muted);">Loading user prompt trace details...</td></tr>
357
+ </tbody>
358
+ </table>
359
+ </div>
360
+ </div>
361
+
362
+ <!-- Row 2: Model & Project Breakdown -->
363
+ <div class="dashboard-row">
364
+ <div class="panel">
365
+ <div class="panel-header">
366
+ <div class="panel-title">🤖 Model Usage & Costs</div>
367
+ </div>
368
+ <table>
369
+ <thead>
370
+ <tr>
371
+ <th>Model Name</th>
372
+ <th>Requests</th>
373
+ <th>Total Tokens</th>
374
+ <th>Est. Cost</th>
375
+ </tr>
376
+ </thead>
377
+ <tbody id="tbl-models">
378
+ <tr><td colspan="4" style="text-align:center; color: var(--text-muted);">Loading model stats...</td></tr>
379
+ </tbody>
380
+ </table>
381
+ </div>
382
+
383
+ <div class="panel">
384
+ <div class="panel-header">
385
+ <div class="panel-title">📁 Project Directory Breakdown</div>
386
+ </div>
387
+ <table>
388
+ <thead>
389
+ <tr>
390
+ <th>Project Directory</th>
391
+ <th>Requests</th>
392
+ <th>Total Tokens</th>
393
+ <th>Est. Cost</th>
394
+ </tr>
395
+ </thead>
396
+ <tbody id="tbl-projects">
397
+ <tr><td colspan="4" style="text-align:center; color: var(--text-muted);">Loading project stats...</td></tr>
398
+ </tbody>
399
+ </table>
400
+ </div>
401
+ </div>
402
+
403
+ <!-- Row 3: Recent User Prompts & Output Code Edits -->
404
+ <div class="dashboard-row">
405
+ <div class="panel">
406
+ <div class="panel-header">
407
+ <div class="panel-title">💬 Recent User Prompt Log</div>
408
+ </div>
409
+ <table>
410
+ <thead>
411
+ <tr>
412
+ <th>Time</th>
413
+ <th>Provider</th>
414
+ <th>Prompt / Query</th>
415
+ </tr>
416
+ </thead>
417
+ <tbody id="tbl-prompts">
418
+ <tr><td colspan="3" style="text-align:center; color: var(--text-muted);">Loading prompt log...</td></tr>
419
+ </tbody>
420
+ </table>
421
+ </div>
422
+
423
+ <div class="panel">
424
+ <div class="panel-header">
425
+ <div class="panel-title">🛠️ Recent Code Edits & Diff Actions</div>
426
+ </div>
427
+ <table>
428
+ <thead>
429
+ <tr>
430
+ <th>Time</th>
431
+ <th>Action</th>
432
+ <th>File & Diff Summary</th>
433
+ </tr>
434
+ </thead>
435
+ <tbody id="tbl-changes">
436
+ <tr><td colspan="3" style="text-align:center; color: var(--text-muted);">Loading code edits...</td></tr>
437
+ </tbody>
438
+ </table>
439
+ </div>
440
+ </div>
441
+
442
+ <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>
444
+ </footer>
445
+ </div>
446
+
447
+ <script>
448
+ function formatNumber(num) {
449
+ if (!num) return '0';
450
+ if (num >= 1000000) return (num / 1000000).toFixed(2) + 'M';
451
+ if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
452
+ return num.toLocaleString();
453
+ }
454
+
455
+ function formatTime(isoStr) {
456
+ if (!isoStr) return '';
457
+ try {
458
+ const d = new Date(isoStr);
459
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
460
+ } catch (e) {
461
+ return (isoStr || '').substring(11, 16);
462
+ }
463
+ }
464
+
465
+ function getBadgeClass(provider) {
466
+ const p = (provider || '').toLowerCase();
467
+ if (p.includes('claude')) return 'badge-claude';
468
+ if (p.includes('codex')) return 'badge-codex';
469
+ if (p.includes('antigravity')) return 'badge-antigravity';
470
+ return 'badge-codex';
471
+ }
472
+
473
+ async function updateDashboard() {
474
+ try {
475
+ const res = await fetch('/api/summary');
476
+ const data = await res.json();
477
+
478
+ // Metrics
479
+ const s = data.summary || {};
480
+ document.getElementById('val-requests').textContent = formatNumber(s.total_requests || 0);
481
+ document.getElementById('val-input').textContent = formatNumber(s.total_input_tokens || 0);
482
+ 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));
484
+ document.getElementById('val-cost').textContent = '$' + (s.total_cost || 0).toFixed(4);
485
+
486
+ // Prompt Trace Details Table (Query, Consumed Tokens, Internal Requests & Outputs)
487
+ const tblDetails = document.getElementById('tbl-prompt-details');
488
+ if (data.prompt_details && data.prompt_details.length > 0) {
489
+ tblDetails.innerHTML = data.prompt_details.map(pd => {
490
+ const outputsHtml = (pd.outputs && pd.outputs.length > 0)
491
+ ? pd.outputs.map(o => `<div class="file-path">📄 ${o.file_path} <span class="badge badge-emerald">${o.change_type}</span></div>`).join('')
492
+ : '<span style="color:var(--text-muted); font-size:12px;">No code edits</span>';
493
+ return `
494
+ <tr>
495
+ <td style="white-space:nowrap; color:var(--text-muted);">${formatTime(pd.timestamp)}</td>
496
+ <td><span class="badge ${getBadgeClass(pd.provider)}">${pd.provider}</span></td>
497
+ <td><div class="prompt-text">${pd.prompt_text}</div></td>
498
+ <td><span class="badge badge-codex">${pd.internal_requests} turn${pd.internal_requests > 1 ? 's' : ''}</span></td>
499
+ <td><strong>${formatNumber(pd.total_tokens)}</strong></td>
500
+ <td><strong style="color:var(--accent-emerald);">$${(pd.cost_usd || 0).toFixed(4)}</strong></td>
501
+ <td>${outputsHtml}</td>
502
+ </tr>
503
+ `;
504
+ }).join('');
505
+ } else {
506
+ tblDetails.innerHTML = '<tr><td colspan="7" style="text-align:center; color: var(--text-muted);">No prompt trace details recorded yet</td></tr>';
507
+ }
508
+
509
+ // Models Table
510
+ const tblModels = document.getElementById('tbl-models');
511
+ if (data.by_model && data.by_model.length > 0) {
512
+ tblModels.innerHTML = data.by_model.map(m => `
513
+ <tr>
514
+ <td><strong>${m.model}</strong></td>
515
+ <td>${formatNumber(m.requests)}</td>
516
+ <td>${formatNumber(m.total_tokens)}</td>
517
+ <td><strong style="color:var(--accent-emerald);">$${(m.total_cost || 0).toFixed(4)}</strong></td>
518
+ </tr>
519
+ `).join('');
520
+ } else {
521
+ tblModels.innerHTML = '<tr><td colspan="4" style="text-align:center; color: var(--text-muted);">No model data recorded yet</td></tr>';
522
+ }
523
+
524
+ // Projects Table
525
+ const tblProjects = document.getElementById('tbl-projects');
526
+ if (data.by_project && data.by_project.length > 0) {
527
+ tblProjects.innerHTML = data.by_project.map(p => `
528
+ <tr>
529
+ <td><span class="file-path">${p.project_name || 'default'}</span></td>
530
+ <td>${formatNumber(p.requests)}</td>
531
+ <td>${formatNumber(p.total_tokens)}</td>
532
+ <td><strong style="color:var(--accent-emerald);">$${(p.total_cost || 0).toFixed(4)}</strong></td>
533
+ </tr>
534
+ `).join('');
535
+ } else {
536
+ tblProjects.innerHTML = '<tr><td colspan="4" style="text-align:center; color: var(--text-muted);">No project data recorded yet</td></tr>';
537
+ }
538
+
539
+ // Prompts Table
540
+ const tblPrompts = document.getElementById('tbl-prompts');
541
+ if (data.prompts && data.prompts.length > 0) {
542
+ tblPrompts.innerHTML = data.prompts.map(p => `
543
+ <tr>
544
+ <td style="white-space:nowrap; color:var(--text-muted);">${formatTime(p.timestamp)}</td>
545
+ <td><span class="badge ${getBadgeClass(p.provider)}">${p.provider}</span></td>
546
+ <td><div class="prompt-text">${p.prompt_text}</div></td>
547
+ </tr>
548
+ `).join('');
549
+ } else {
550
+ tblPrompts.innerHTML = '<tr><td colspan="3" style="text-align:center; color: var(--text-muted);">No prompt history recorded yet</td></tr>';
551
+ }
552
+
553
+ // Code Changes Table
554
+ const tblChanges = document.getElementById('tbl-changes');
555
+ if (data.code_changes && data.code_changes.length > 0) {
556
+ tblChanges.innerHTML = data.code_changes.map(c => `
557
+ <tr>
558
+ <td style="white-space:nowrap; color:var(--text-muted);">${formatTime(c.timestamp)}</td>
559
+ <td><span class="badge badge-emerald">${c.change_type}</span></td>
560
+ <td>
561
+ <div class="file-path">📄 ${c.file_path}</div>
562
+ <div class="diff-summary">${c.diff_summary}</div>
563
+ </td>
564
+ </tr>
565
+ `).join('');
566
+ } else {
567
+ tblChanges.innerHTML = '<tr><td colspan="3" style="text-align:center; color: var(--text-muted);">No code edits recorded yet</td></tr>';
568
+ }
569
+
570
+ } catch (err) {
571
+ console.error('Error fetching dashboard update:', err);
572
+ }
573
+ }
574
+
575
+ // Initial update and auto-poll every 2 seconds
576
+ updateDashboard();
577
+ setInterval(updateDashboard, 2000);
578
+ </script>
579
+ </body>
580
+ </html>
581
+ """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-meter
3
- Version: 0.3.0
3
+ Version: 0.5.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
@@ -106,7 +106,8 @@ This launches the interactive Rich terminal dashboard displaying key metrics, mo
106
106
  | Command | Description |
107
107
  | :--- | :--- |
108
108
  | `code-meter` | Open main interactive Rich terminal dashboard UI |
109
- | `code-meter ui` | Launch live interactive Rich terminal UI watching all AI log directories |
109
+ | `code-meter ui` | Launch live Web Dashboard on **http://localhost:5005** (auto-opens browser) |
110
+ | `code-meter ui --terminal` | Launch live interactive Rich terminal UI |
110
111
  | `code-meter watch` | Live watch mode (auto-refreshes UI as AI agents write logs) |
111
112
  | `code-meter update-pricing` | Fetch latest real-time token pricing for 2,800+ models |
112
113
  | `code-meter scan` | Force an incremental scan of local usage directories |
@@ -35,6 +35,9 @@ src/code_meter/storage/repository.py
35
35
  src/code_meter/ui/__init__.py
36
36
  src/code_meter/ui/dashboard.py
37
37
  src/code_meter/ui/tables.py
38
+ src/code_meter/ui/web/__init__.py
39
+ src/code_meter/ui/web/server.py
40
+ src/code_meter/ui/web/static.py
38
41
  tests/test_analytics.py
39
42
  tests/test_database.py
40
43
  tests/test_pricing.py
File without changes
File without changes