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.
- {code_meter-0.3.0 → code_meter-0.5.0}/PKG-INFO +3 -2
- {code_meter-0.3.0 → code_meter-0.5.0}/README.md +2 -1
- {code_meter-0.3.0 → code_meter-0.5.0}/pyproject.toml +1 -1
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/cli.py +23 -3
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/storage/repository.py +82 -0
- code_meter-0.5.0/src/code_meter/ui/web/__init__.py +5 -0
- code_meter-0.5.0/src/code_meter/ui/web/server.py +196 -0
- code_meter-0.5.0/src/code_meter/ui/web/static.py +581 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter.egg-info/PKG-INFO +3 -2
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter.egg-info/SOURCES.txt +3 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/setup.cfg +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/setup.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/__init__.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/analytics/__init__.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/analytics/costs.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/analytics/reports.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/analytics/tokens.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/config.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/models/__init__.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/models/project.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/models/usage.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/pricing/__init__.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/pricing/anthropic.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/pricing/engine.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/pricing/google.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/pricing/live.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/pricing/openai.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/providers/__init__.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/providers/antigravity.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/providers/base.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/providers/claude_code.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/providers/codex.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/storage/__init__.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/storage/database.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/ui/__init__.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/ui/dashboard.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/ui/tables.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter/watcher.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter.egg-info/dependency_links.txt +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter.egg-info/entry_points.txt +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter.egg-info/requires.txt +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/src/code_meter.egg-info/top_level.txt +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/tests/test_analytics.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/tests/test_database.py +0 -0
- {code_meter-0.3.0 → code_meter-0.5.0}/tests/test_pricing.py +0 -0
- {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
|
+
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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,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
|
+
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|