code-meter 0.3.0__tar.gz → 0.4.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.4.0}/PKG-INFO +3 -2
  2. {code_meter-0.3.0 → code_meter-0.4.0}/README.md +2 -1
  3. {code_meter-0.3.0 → code_meter-0.4.0}/pyproject.toml +1 -1
  4. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/cli.py +23 -3
  5. code_meter-0.4.0/src/code_meter/ui/web/__init__.py +5 -0
  6. code_meter-0.4.0/src/code_meter/ui/web/server.py +194 -0
  7. code_meter-0.4.0/src/code_meter/ui/web/static.py +497 -0
  8. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter.egg-info/PKG-INFO +3 -2
  9. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter.egg-info/SOURCES.txt +3 -0
  10. {code_meter-0.3.0 → code_meter-0.4.0}/setup.cfg +0 -0
  11. {code_meter-0.3.0 → code_meter-0.4.0}/setup.py +0 -0
  12. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/__init__.py +0 -0
  13. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/analytics/__init__.py +0 -0
  14. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/analytics/costs.py +0 -0
  15. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/analytics/reports.py +0 -0
  16. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/analytics/tokens.py +0 -0
  17. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/config.py +0 -0
  18. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/models/__init__.py +0 -0
  19. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/models/project.py +0 -0
  20. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/models/usage.py +0 -0
  21. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/pricing/__init__.py +0 -0
  22. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/pricing/anthropic.py +0 -0
  23. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/pricing/engine.py +0 -0
  24. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/pricing/google.py +0 -0
  25. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/pricing/live.py +0 -0
  26. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/pricing/openai.py +0 -0
  27. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/providers/__init__.py +0 -0
  28. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/providers/antigravity.py +0 -0
  29. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/providers/base.py +0 -0
  30. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/providers/claude_code.py +0 -0
  31. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/providers/codex.py +0 -0
  32. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/storage/__init__.py +0 -0
  33. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/storage/database.py +0 -0
  34. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/storage/repository.py +0 -0
  35. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/ui/__init__.py +0 -0
  36. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/ui/dashboard.py +0 -0
  37. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/ui/tables.py +0 -0
  38. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/watcher.py +0 -0
  39. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter.egg-info/dependency_links.txt +0 -0
  40. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter.egg-info/entry_points.txt +0 -0
  41. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter.egg-info/requires.txt +0 -0
  42. {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter.egg-info/top_level.txt +0 -0
  43. {code_meter-0.3.0 → code_meter-0.4.0}/tests/test_analytics.py +0 -0
  44. {code_meter-0.3.0 → code_meter-0.4.0}/tests/test_database.py +0 -0
  45. {code_meter-0.3.0 → code_meter-0.4.0}/tests/test_pricing.py +0 -0
  46. {code_meter-0.3.0 → code_meter-0.4.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.4.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.4.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")
@@ -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,194 @@
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
+
69
+ payload = {
70
+ "summary": {
71
+ "total_requests": summary.total_requests,
72
+ "total_input_tokens": summary.input_tokens,
73
+ "total_output_tokens": summary.output_tokens,
74
+ "total_cache_read_tokens": summary.cache_read_tokens,
75
+ "total_cache_write_tokens": summary.cache_write_tokens,
76
+ "total_tokens": summary.total_tokens,
77
+ "total_cost": summary.estimated_cost_usd or 0.0,
78
+ },
79
+ "by_model": [
80
+ {
81
+ "model": m.model,
82
+ "requests": m.request_count,
83
+ "total_tokens": m.total_tokens,
84
+ "total_cost": m.estimated_cost_usd or 0.0,
85
+ }
86
+ for m in by_model
87
+ ],
88
+ "by_project": [
89
+ {
90
+ "project_name": p.project_name,
91
+ "requests": p.request_count,
92
+ "total_tokens": p.total_tokens,
93
+ "total_cost": p.estimated_cost_usd or 0.0,
94
+ }
95
+ for p in by_project
96
+ ],
97
+ "prompts": [
98
+ {
99
+ "timestamp": p.timestamp,
100
+ "provider": p.provider,
101
+ "prompt_text": p.prompt_text,
102
+ "project_id": p.project_id or "",
103
+ }
104
+ for p in prompts
105
+ ],
106
+ "code_changes": [
107
+ {
108
+ "timestamp": c.timestamp,
109
+ "provider": c.provider,
110
+ "file_path": c.file_path,
111
+ "change_type": c.change_type,
112
+ "diff_summary": c.diff_summary,
113
+ }
114
+ for c in code_changes
115
+ ],
116
+ }
117
+
118
+ self.send_json_response(payload)
119
+
120
+ def handle_api_prompts(self) -> None:
121
+ prompts = repo.query_prompts(limit=50)
122
+ self.send_json_response({"prompts": [p.model_dump() for p in prompts]})
123
+
124
+ def handle_api_changes(self) -> None:
125
+ changes = repo.query_code_changes(limit=50)
126
+ self.send_json_response({"code_changes": [c.model_dump() for c in changes]})
127
+
128
+ def send_json_response(self, data: Dict[str, Any]) -> None:
129
+ body = json.dumps(data, indent=2, default=str).encode("utf-8")
130
+ self.send_response(200)
131
+ self.send_header("Content-Type", "application/json")
132
+ self.send_header("Access-Control-Allow-Origin", "*")
133
+ self.end_headers()
134
+ self.wfile.write(body)
135
+
136
+ return CodeMeterWebHandler
137
+
138
+
139
+ def start_web_server(
140
+ host: str = "127.0.0.1",
141
+ port: int = 5005,
142
+ open_browser: bool = True,
143
+ config: Optional[Any] = None,
144
+ repo: Optional[UsageRepository] = None,
145
+ pricing_engine: Optional[PricingEngine] = None,
146
+ report_gen: Optional[ReportGenerator] = None,
147
+ ) -> None:
148
+ """Launch HTTP server on specified host/port serving localhost web dashboard."""
149
+ if not (repo and pricing_engine and report_gen and config):
150
+ from code_meter.cli import _get_services
151
+ config, repo, pricing_engine, providers, report_gen = _get_services()
152
+ else:
153
+ providers = [
154
+ ClaudeCodeProvider(config.get_claude_dir()),
155
+ CodexProvider(config.get_codex_dir()),
156
+ AntigravityProvider(config.get_antigravity_dir()),
157
+ ]
158
+
159
+ # Background scanner loop
160
+ def background_scan_and_watch():
161
+ def do_scan():
162
+ states = repo.get_scan_states()
163
+ for p in providers:
164
+ res = p.scan(states)
165
+ repo.save_records(res.records)
166
+ repo.save_prompts(res.prompts)
167
+ repo.save_code_changes(res.code_changes)
168
+ repo.update_scan_states(res.updated_states)
169
+
170
+ target_dirs = [config.get_claude_dir(), config.get_codex_dir(), config.get_antigravity_dir()]
171
+ run_watch_loop(target_dir=target_dirs, on_change_callback=do_scan, refresh_seconds=config.get_refresh_seconds())
172
+
173
+ watch_thread = threading.Thread(target=background_scan_and_watch, daemon=True)
174
+ watch_thread.start()
175
+
176
+ handler_cls = create_web_handler(repo, pricing_engine, report_gen)
177
+ server = ThreadingHTTPServer((host, port), handler_cls)
178
+
179
+ url = f"http://localhost:{port}"
180
+ print(f"\n[code-meter] Web Dashboard running at: {url}")
181
+ print("Press Ctrl+C to stop the web server.\n")
182
+
183
+ if open_browser:
184
+ def open_browser_later():
185
+ time.sleep(0.5)
186
+ webbrowser.open(url)
187
+ threading.Thread(target=open_browser_later, daemon=True).start()
188
+
189
+ try:
190
+ server.serve_forever()
191
+ except KeyboardInterrupt:
192
+ print("\nShutting down web dashboard server...")
193
+ server.shutdown()
194
+ server.server_close()
@@ -0,0 +1,497 @@
1
+ """Embedded Single-Page 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
+ <style>
10
+ :root {
11
+ --bg-dark: #0f172a;
12
+ --card-bg: rgba(30, 41, 59, 0.7);
13
+ --border-color: rgba(255, 255, 255, 0.1);
14
+ --text-main: #f8fafc;
15
+ --text-muted: #94a3b8;
16
+ --accent-blue: #38bdf8;
17
+ --accent-emerald: #34d399;
18
+ --accent-purple: #c084fc;
19
+ --accent-amber: #fbbf24;
20
+ --shadow-glow: 0 10px 25px -5px rgba(56, 189, 248, 0.15);
21
+ }
22
+
23
+ * {
24
+ box-sizing: border-box;
25
+ margin: 0;
26
+ padding: 0;
27
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
28
+ }
29
+
30
+ body {
31
+ background-color: var(--bg-dark);
32
+ color: var(--text-main);
33
+ padding: 24px;
34
+ min-height: 100vh;
35
+ background-image:
36
+ radial-gradient(at 0% 0%, rgba(56, 189, 248, 0.08) 0px, transparent 50%),
37
+ radial-gradient(at 100% 100%, rgba(192, 132, 252, 0.08) 0px, transparent 50%);
38
+ }
39
+
40
+ .container {
41
+ max-width: 1300px;
42
+ margin: 0 auto;
43
+ }
44
+
45
+ header {
46
+ display: flex;
47
+ justify-content: space-between;
48
+ align-items: center;
49
+ margin-bottom: 28px;
50
+ padding-bottom: 16px;
51
+ border-bottom: 1px solid var(--border-color);
52
+ }
53
+
54
+ .brand {
55
+ display: flex;
56
+ align-items: center;
57
+ gap: 12px;
58
+ }
59
+
60
+ .logo-badge {
61
+ background: linear-gradient(135deg, #0284c7, #7e22ce);
62
+ width: 42px;
63
+ height: 42px;
64
+ border-radius: 10px;
65
+ display: flex;
66
+ align-items: center;
67
+ justify-content: center;
68
+ font-weight: 800;
69
+ font-size: 20px;
70
+ box-shadow: 0 4px 12px rgba(126, 34, 206, 0.3);
71
+ }
72
+
73
+ h1 {
74
+ font-size: 24px;
75
+ font-weight: 700;
76
+ letter-spacing: -0.5px;
77
+ }
78
+
79
+ .subtitle {
80
+ font-size: 13px;
81
+ color: var(--text-muted);
82
+ }
83
+
84
+ .live-status {
85
+ display: flex;
86
+ align-items: center;
87
+ gap: 8px;
88
+ background: rgba(52, 211, 153, 0.1);
89
+ border: 1px solid rgba(52, 211, 153, 0.2);
90
+ padding: 6px 14px;
91
+ border-radius: 20px;
92
+ font-size: 13px;
93
+ color: var(--accent-emerald);
94
+ }
95
+
96
+ .pulse {
97
+ width: 8px;
98
+ height: 8px;
99
+ background-color: var(--accent-emerald);
100
+ border-radius: 50%;
101
+ box-shadow: 0 0 0 0 rgba(52, 211, 153, 0.7);
102
+ animation: pulse-ring 2s infinite;
103
+ }
104
+
105
+ @keyframes pulse-ring {
106
+ 0% { box-shadow: 0 0 0 0 rgba(52, 211, 153, 0.7); }
107
+ 70% { box-shadow: 0 0 0 10px rgba(52, 211, 153, 0); }
108
+ 100% { box-shadow: 0 0 0 0 rgba(52, 211, 153, 0); }
109
+ }
110
+
111
+ /* Metrics Grid */
112
+ .metrics-grid {
113
+ display: grid;
114
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
115
+ gap: 16px;
116
+ margin-bottom: 28px;
117
+ }
118
+
119
+ .metric-card {
120
+ background: var(--card-bg);
121
+ backdrop-filter: blur(12px);
122
+ border: 1px solid var(--border-color);
123
+ border-radius: 14px;
124
+ padding: 20px;
125
+ transition: transform 0.2s, box-shadow 0.2s;
126
+ }
127
+
128
+ .metric-card:hover {
129
+ transform: translateY(-2px);
130
+ box-shadow: var(--shadow-glow);
131
+ }
132
+
133
+ .metric-label {
134
+ font-size: 12px;
135
+ text-transform: uppercase;
136
+ letter-spacing: 0.5px;
137
+ color: var(--text-muted);
138
+ margin-bottom: 8px;
139
+ }
140
+
141
+ .metric-value {
142
+ font-size: 26px;
143
+ font-weight: 700;
144
+ letter-spacing: -0.5px;
145
+ }
146
+
147
+ .metric-value.highlight {
148
+ color: var(--accent-emerald);
149
+ }
150
+
151
+ /* Dashboard Layout */
152
+ .dashboard-row {
153
+ display: grid;
154
+ grid-template-columns: 1fr 1fr;
155
+ gap: 24px;
156
+ margin-bottom: 28px;
157
+ }
158
+
159
+ @media (max-width: 900px) {
160
+ .dashboard-row {
161
+ grid-template-columns: 1fr;
162
+ }
163
+ }
164
+
165
+ .panel {
166
+ background: var(--card-bg);
167
+ backdrop-filter: blur(12px);
168
+ border: 1px solid var(--border-color);
169
+ border-radius: 16px;
170
+ padding: 24px;
171
+ }
172
+
173
+ .panel-header {
174
+ display: flex;
175
+ justify-content: space-between;
176
+ align-items: center;
177
+ margin-bottom: 18px;
178
+ }
179
+
180
+ .panel-title {
181
+ font-size: 16px;
182
+ font-weight: 600;
183
+ display: flex;
184
+ align-items: center;
185
+ gap: 8px;
186
+ }
187
+
188
+ /* Tables */
189
+ table {
190
+ width: 100%;
191
+ border-collapse: collapse;
192
+ font-size: 14px;
193
+ }
194
+
195
+ th {
196
+ text-align: left;
197
+ padding: 10px 12px;
198
+ color: var(--text-muted);
199
+ font-weight: 500;
200
+ border-bottom: 1px solid var(--border-color);
201
+ font-size: 12px;
202
+ text-transform: uppercase;
203
+ }
204
+
205
+ td {
206
+ padding: 12px;
207
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
208
+ }
209
+
210
+ tr:last-child td {
211
+ border-bottom: none;
212
+ }
213
+
214
+ .badge {
215
+ display: inline-block;
216
+ padding: 3px 8px;
217
+ border-radius: 6px;
218
+ font-size: 11px;
219
+ font-weight: 600;
220
+ text-transform: uppercase;
221
+ }
222
+
223
+ .badge-claude { background: rgba(249, 115, 22, 0.15); color: #fb923c; border: 1px solid rgba(249, 115, 22, 0.3); }
224
+ .badge-codex { background: rgba(56, 189, 248, 0.15); color: #38bdf8; border: 1px solid rgba(56, 189, 248, 0.3); }
225
+ .badge-antigravity { background: rgba(192, 132, 252, 0.15); color: #c084fc; border: 1px solid rgba(192, 132, 252, 0.3); }
226
+
227
+ .prompt-text {
228
+ font-family: monospace;
229
+ font-size: 13px;
230
+ color: #e2e8f0;
231
+ word-break: break-word;
232
+ }
233
+
234
+ .file-path {
235
+ font-family: monospace;
236
+ font-size: 12px;
237
+ color: var(--accent-blue);
238
+ }
239
+
240
+ .diff-summary {
241
+ font-family: monospace;
242
+ font-size: 12px;
243
+ color: var(--accent-emerald);
244
+ white-space: pre-wrap;
245
+ }
246
+
247
+ footer {
248
+ text-align: center;
249
+ margin-top: 40px;
250
+ padding-top: 20px;
251
+ border-top: 1px solid var(--border-color);
252
+ font-size: 13px;
253
+ color: var(--text-muted);
254
+ }
255
+
256
+ footer a {
257
+ color: var(--accent-blue);
258
+ text-decoration: none;
259
+ }
260
+ </style>
261
+ </head>
262
+ <body>
263
+ <div class="container">
264
+ <header>
265
+ <div class="brand">
266
+ <div class="logo-badge">⚡</div>
267
+ <div>
268
+ <h1>code-meter</h1>
269
+ <div class="subtitle">Local AI Coding Token & Cost Dashboard</div>
270
+ </div>
271
+ </div>
272
+ <div class="live-status">
273
+ <div class="pulse"></div>
274
+ <span>Live Dashboard (localhost:5005)</span>
275
+ </div>
276
+ </header>
277
+
278
+ <!-- Metrics Cards -->
279
+ <div class="metrics-grid">
280
+ <div class="metric-card">
281
+ <div class="metric-label">Total Requests</div>
282
+ <div class="metric-value" id="val-requests">0</div>
283
+ </div>
284
+ <div class="metric-card">
285
+ <div class="metric-label">Input Tokens</div>
286
+ <div class="metric-value" id="val-input">0</div>
287
+ </div>
288
+ <div class="metric-card">
289
+ <div class="metric-label">Output Tokens</div>
290
+ <div class="metric-value" id="val-output">0</div>
291
+ </div>
292
+ <div class="metric-card">
293
+ <div class="metric-label">Cache Read / Write</div>
294
+ <div class="metric-value" id="val-cache">0</div>
295
+ </div>
296
+ <div class="metric-card">
297
+ <div class="metric-label">Total Cost</div>
298
+ <div class="metric-value highlight" id="val-cost">$0.00</div>
299
+ </div>
300
+ </div>
301
+
302
+ <!-- Row 1: Model & Project Breakdown -->
303
+ <div class="dashboard-row">
304
+ <div class="panel">
305
+ <div class="panel-header">
306
+ <div class="panel-title">🤖 Model Breakdown & Costs</div>
307
+ </div>
308
+ <table>
309
+ <thead>
310
+ <tr>
311
+ <th>Model</th>
312
+ <th>Provider</th>
313
+ <th>Tokens</th>
314
+ <th>Est. Cost</th>
315
+ </tr>
316
+ </thead>
317
+ <tbody id="tbl-models">
318
+ <tr><td colspan="4" style="text-align:center; color: var(--text-muted);">Loading data...</td></tr>
319
+ </tbody>
320
+ </table>
321
+ </div>
322
+
323
+ <div class="panel">
324
+ <div class="panel-header">
325
+ <div class="panel-title">📁 Project Usage Breakdown</div>
326
+ </div>
327
+ <table>
328
+ <thead>
329
+ <tr>
330
+ <th>Project</th>
331
+ <th>Requests</th>
332
+ <th>Tokens</th>
333
+ <th>Est. Cost</th>
334
+ </tr>
335
+ </thead>
336
+ <tbody id="tbl-projects">
337
+ <tr><td colspan="4" style="text-align:center; color: var(--text-muted);">Loading data...</td></tr>
338
+ </tbody>
339
+ </table>
340
+ </div>
341
+ </div>
342
+
343
+ <!-- Row 2: User Prompts & Code Edits -->
344
+ <div class="dashboard-row">
345
+ <div class="panel">
346
+ <div class="panel-header">
347
+ <div class="panel-title">💬 Recent User Queries & Prompts</div>
348
+ </div>
349
+ <table>
350
+ <thead>
351
+ <tr>
352
+ <th>Time</th>
353
+ <th>Provider</th>
354
+ <th>Prompt / Query</th>
355
+ </tr>
356
+ </thead>
357
+ <tbody id="tbl-prompts">
358
+ <tr><td colspan="3" style="text-align:center; color: var(--text-muted);">Loading prompts...</td></tr>
359
+ </tbody>
360
+ </table>
361
+ </div>
362
+
363
+ <div class="panel">
364
+ <div class="panel-header">
365
+ <div class="panel-title">🛠️ Output Code Edits & Actions</div>
366
+ </div>
367
+ <table>
368
+ <thead>
369
+ <tr>
370
+ <th>Time</th>
371
+ <th>Action</th>
372
+ <th>File & Diff Summary</th>
373
+ </tr>
374
+ </thead>
375
+ <tbody id="tbl-changes">
376
+ <tr><td colspan="3" style="text-align:center; color: var(--text-muted);">Loading code edits...</td></tr>
377
+ </tbody>
378
+ </table>
379
+ </div>
380
+ </div>
381
+
382
+ <footer>
383
+ code-meter v0.3.0 | Running on <strong>http://localhost:5005</strong> | <a href="https://github.com/dhanushnayak/code-meter" target="_blank">GitHub Repository</a>
384
+ </footer>
385
+ </div>
386
+
387
+ <script>
388
+ function formatNumber(num) {
389
+ if (num >= 1000000) return (num / 1000000).toFixed(2) + 'M';
390
+ if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
391
+ return num.toLocaleString();
392
+ }
393
+
394
+ function formatTime(isoStr) {
395
+ if (!isoStr) return '';
396
+ try {
397
+ const d = new Date(isoStr);
398
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
399
+ } catch (e) {
400
+ return isoStr.substring(11, 16);
401
+ }
402
+ }
403
+
404
+ function getBadgeClass(provider) {
405
+ const p = (provider || '').toLowerCase();
406
+ if (p.includes('claude')) return 'badge-claude';
407
+ if (p.includes('codex')) return 'badge-codex';
408
+ if (p.includes('antigravity')) return 'badge-antigravity';
409
+ return 'badge-codex';
410
+ }
411
+
412
+ async function updateDashboard() {
413
+ try {
414
+ const res = await fetch('/api/summary');
415
+ const data = await res.json();
416
+
417
+ // Metrics
418
+ const s = data.summary || {};
419
+ document.getElementById('val-requests').textContent = formatNumber(s.total_requests || 0);
420
+ document.getElementById('val-input').textContent = formatNumber(s.total_input_tokens || 0);
421
+ document.getElementById('val-output').textContent = formatNumber(s.total_output_tokens || 0);
422
+ document.getElementById('val-cache').textContent = formatNumber((s.total_cache_read_tokens || 0) + (s.total_cache_write_tokens || 0));
423
+ document.getElementById('val-cost').textContent = '$' + (s.total_cost || 0).toFixed(4);
424
+
425
+ // Models Table
426
+ const tblModels = document.getElementById('tbl-models');
427
+ if (data.by_model && data.by_model.length > 0) {
428
+ tblModels.innerHTML = data.by_model.map(m => `
429
+ <tr>
430
+ <td><strong>${m.model}</strong></td>
431
+ <td><span class="badge ${getBadgeClass(m.provider)}">${m.provider}</span></td>
432
+ <td>${formatNumber(m.total_tokens)}</td>
433
+ <td><strong>$${(m.total_cost || 0).toFixed(4)}</strong></td>
434
+ </tr>
435
+ `).join('');
436
+ } else {
437
+ tblModels.innerHTML = '<tr><td colspan="4" style="text-align:center; color: var(--text-muted);">No model data recorded yet</td></tr>';
438
+ }
439
+
440
+ // Projects Table
441
+ const tblProjects = document.getElementById('tbl-projects');
442
+ if (data.by_project && data.by_project.length > 0) {
443
+ tblProjects.innerHTML = data.by_project.map(p => `
444
+ <tr>
445
+ <td><span class="file-path">${p.project_id || 'default'}</span></td>
446
+ <td>${formatNumber(p.requests)}</td>
447
+ <td>${formatNumber(p.total_tokens)}</td>
448
+ <td><strong>$${(p.total_cost || 0).toFixed(4)}</strong></td>
449
+ </tr>
450
+ `).join('');
451
+ } else {
452
+ tblProjects.innerHTML = '<tr><td colspan="4" style="text-align:center; color: var(--text-muted);">No project data recorded yet</td></tr>';
453
+ }
454
+
455
+ // Prompts Table
456
+ const tblPrompts = document.getElementById('tbl-prompts');
457
+ if (data.prompts && data.prompts.length > 0) {
458
+ tblPrompts.innerHTML = data.prompts.map(p => `
459
+ <tr>
460
+ <td style="white-space:nowrap; color:var(--text-muted);">${formatTime(p.timestamp)}</td>
461
+ <td><span class="badge ${getBadgeClass(p.provider)}">${p.provider}</span></td>
462
+ <td><div class="prompt-text">${p.prompt_text}</div></td>
463
+ </tr>
464
+ `).join('');
465
+ } else {
466
+ tblPrompts.innerHTML = '<tr><td colspan="3" style="text-align:center; color: var(--text-muted);">No prompt history recorded yet</td></tr>';
467
+ }
468
+
469
+ // Code Changes Table
470
+ const tblChanges = document.getElementById('tbl-changes');
471
+ if (data.code_changes && data.code_changes.length > 0) {
472
+ tblChanges.innerHTML = data.code_changes.map(c => `
473
+ <tr>
474
+ <td style="white-space:nowrap; color:var(--text-muted);">${formatTime(c.timestamp)}</td>
475
+ <td><span class="badge badge-emerald">${c.change_type}</span></td>
476
+ <td>
477
+ <div class="file-path">${c.file_path}</div>
478
+ <div class="diff-summary">${c.diff_summary}</div>
479
+ </td>
480
+ </tr>
481
+ `).join('');
482
+ } else {
483
+ tblChanges.innerHTML = '<tr><td colspan="3" style="text-align:center; color: var(--text-muted);">No code edits recorded yet</td></tr>';
484
+ }
485
+
486
+ } catch (err) {
487
+ console.error('Error fetching dashboard update:', err);
488
+ }
489
+ }
490
+
491
+ // Initial update and auto-poll every 2 seconds
492
+ updateDashboard();
493
+ setInterval(updateDashboard, 2000);
494
+ </script>
495
+ </body>
496
+ </html>
497
+ """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-meter
3
- Version: 0.3.0
3
+ Version: 0.4.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