tokentab
§ Source-level Observatory

tokentab web dashboard

Web dashboard

★ 713
GitHub Stars
2.0K
LOC
24
Files
2026-08-27
Last commit
§ 00 · IMPORT CHAIN

Web dashboard的证据链

这一页拆本地 HTTP server、浏览器页面和终端渲染怎样复用同一份 Report。为了避免变成行号清单,本页每个源码定位都绑定具体设计含义:在 sources/tokentab/tokentab/web/server.py:24 这一处,页面把源码证据和设计判断绑定起来;在 sources/tokentab/tokentab/web/server.py:29 这一处,页面把源码证据和设计判断绑定起来;在 sources/tokentab/tokentab/web/server.py:33 这一处,页面把源码证据和设计判断绑定起来;在 sources/tokentab/tokentab/web/server.py:36 这一处,页面把源码证据和设计判断绑定起来;在 sources/tokentab/tokentab/web/server.py:43 这一处,页面把源码证据和设计判断绑定起来。

sources/tokentab/tokentab/web/server.py · L24-88Python
24def _make_handler(default_period: str):
25    class Handler(BaseHTTPRequestHandler):
26        def log_message(self, *_args):
27            pass  # quiet — we don't want a request log spewing in the terminal
28
29        def do_GET(self):
30            parsed = urlparse(self.path)
31
32            try:
33                if parsed.path == "/api/report":
34                    qs = parse_qs(parsed.query)
35                    period = (qs.get("period") or [default_period])[0]
36                    records = collect_all()
37                    report = build_report(records, parse_range(period))
38                    body = json.dumps(report_to_dict(report)).encode("utf-8")
39                    self._send(200, "application/json", body)
40                    return
41
42                if parsed.path == "/":
43                    self._send(200, "text/html; charset=utf-8", render_page().encode("utf-8"))
44                    return
45
46                self._send(404, "text/plain", b"not found")
47            except Exception as err:  # never let one bad request kill the server
48                self._send(500, "text/plain", f"error: {err}".encode("utf-8"))
49
50        def _send(self, code: int, ctype: str, body: bytes):
51            self.send_response(code)
52            self.send_header("Content-Type", ctype)
53            self.send_header("Content-Length", str(len(body)))
54            self.end_headers()
55            self.wfile.write(body)
56
57    return Handler
58
59
60def start_web(port: int, open_browser: bool, period: str) -> None:
61    handler = _make_handler(period)
62
63    # port 0 (or a taken port) -> let the OS pick a free one
64    try:
65        server = ThreadingHTTPServer(("127.0.0.1", port), handler)
66    except OSError:
67        server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
68
69    actual_port = server.server_address[1]
70    link = f"http://localhost:{actual_port}"
71    print(f"tokentab is running at {link}")
72    print("press ctrl-c to stop")
73
74    if open_browser:
75        threading.Timer(0.4, lambda: _safe_open(link)).start()
76
77    try:
78        server.serve_forever()
79    except KeyboardInterrupt:
80        print("\nstopped")
81        server.shutdown()
82
83
84def _safe_open(link: str) -> None:
85    try:
86        webbrowser.open(link)
87    except Exception:
88        pass  # opening a browser is a nicety, not a requirement
§ 01 · CORE IMPL

核心实现,逐段拆

▸页面 HTML 保持自包含

在 sources/tokentab/tokentab/web/page.py:11 这一处,页面把源码证据和设计判断绑定起来;在 sources/tokentab/tokentab/web/page.py:18 这一处,页面把源码证据和设计判断绑定起来;在 sources/tokentab/tokentab/web/page.py:24 这一处,页面把源码证据和设计判断绑定起来;在 sources/tokentab/tokentab/web/page.py:28 这一处,页面把源码证据和设计判断绑定起来;在 sources/tokentab/tokentab/web/page.py:44 这一处,页面把源码证据和设计判断绑定起来。这一段的关键不是单个语句,而是它和上下游契约如何相互约束:输入必须先被压到统一字段,后续 pricing、aggregate、serialize 才能保持无状态且可复用。

sources/tokentab/tokentab/web/page.py · L11-60Python
11PAGE_HTML = r"""<!doctype html>
12<html lang="en">
13<head>
14<meta charset="utf-8" />
15<meta name="viewport" content="width=device-width, initial-scale=1" />
16<title>tokentab</title>
17<style>
18  :root {
19    --stock: #f6f2e9;
20    --stock-line: #ece5d6;
21    --ink: #1f2a44;
22    --ink-soft: #5c6474;
23    --ink-faint: #9a9384;
24    --rule: #d9d0bd;
25    --stamp: #9c2b28;
26    --stamp-soft: #b8514d;
27    --pos: #2f5d50;
28    /* system stacks only — the tool stays fully offline, no font CDN */
29    --serif: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Georgia, serif;
30    --mono: ui-monospace, "SF Mono", "SFMono-Regular", Menlo, "DejaVu Sans Mono", Consolas, monospace;
31  }
32  * { box-sizing: border-box; }
33  html, body { margin: 0; padding: 0; }
34  body {
35    background: #e8e2d4;
36    color: var(--ink);
37    font-family: var(--mono);
38    font-size: 13.5px;
39    line-height: 1.5;
40    -webkit-font-smoothing: antialiased;
41    padding: 40px 18px 70px;
42  }
43
44  .bill {
45    max-width: 720px;
46    margin: 0 auto;
47    background: var(--stock);
48    border: 1px solid var(--rule);
49    /* faint horizontal ledger lines, like ruled accounting paper */
50    background-image: repeating-linear-gradient(
51      var(--stock) 0, var(--stock) 33px, var(--stock-line) 33px, var(--stock-line) 34px
52    );
53    box-shadow: 0 30px 60px -34px rgba(40,34,20,0.55);
54    padding: 0;
55  }
56  .inner { padding: 40px 46px 44px; }
57
58  /* ---- letterhead ---- */
59  .head {
60    display: flex;

▸终端 rich 表格

在 sources/tokentab/tokentab/report/render.py:30 这一处,页面把源码证据和设计判断绑定起来;在 sources/tokentab/tokentab/report/render.py:43 这一处,页面把源码证据和设计判断绑定起来;在 sources/tokentab/tokentab/report/render.py:68 这一处,页面把源码证据和设计判断绑定起来;在 sources/tokentab/tokentab/report/render.py:79 这一处,页面把源码证据和设计判断绑定起来;在 sources/tokentab/tokentab/report/render.py:100 这一处,页面把源码证据和设计判断绑定起来。这一段的关键不是单个语句,而是它和上下游契约如何相互约束:输入必须先被压到统一字段,后续 pricing、aggregate、serialize 才能保持无状态且可复用。

sources/tokentab/tokentab/report/render.py · L30-121Python
30def render_report(report: Report, console: Console) -> None:
31    totals = report.totals
32
33    if report.range.frm.timestamp() == 0:
34        range_label = "all time"
35    else:
36        range_label = f"{_fmt_day(report.range.frm)} → {_fmt_day(report.range.to)}"
37
38    console.print()
39    console.print(Text("  tokentab  ", style="bold white") + Text(range_label, style="grey58"))
40    console.print()
41
42    # ---- headline totals ----
43    console.print(Text("Totals", style="bold"))
44    parts = Text("  ")
45    parts.append("cost ", style="grey58")
46    parts.append(money(totals.cost) + "   ·   ", style="green")
47    parts.append("tokens ", style="grey58")
48    parts.append(compact_tokens(totals.tokens.total()) + "   ·   ")
49    parts.append("calls ", style="grey58")
50    parts.append(f"{totals.calls:,}" + "   ·   ")
51    parts.append("cache hit ", style="grey58")
52    parts.append(pct(totals.cache_hit_rate))
53    console.print(parts)
54    console.print()
55
56    if totals.calls == 0:
57        console.print(Text("  No usage found for this window.", style="yellow"))
58        console.print(
59            Text(
60                "  Either nothing ran in this period, or none of the supported tools have logs on disk yet.",
61                style="grey58",
62            )
63        )
64        console.print()
65        return
66
67    # ---- by tool ----
68    console.print(Text("By tool", style="bold"))
69    t = Table(box=SIMPLE_HEAVY, show_edge=True, pad_edge=False)
70    t.add_column("Tool")
71    t.add_column("Cost", justify="right", style="green")
72    t.add_column("Tokens", justify="right")
73    t.add_column("Share", justify="right")
74    for r in report.by_tool:
75        t.add_row(label_for(r.label), money(r.cost), compact_tokens(r.tokens), share(r.cost, totals.cost))
76    console.print(t)
77
78    # ---- top models ----
79    console.print(Text("Top models", style="bold"))
80    t = Table(box=SIMPLE_HEAVY, pad_edge=False)
81    t.add_column("Model")
82    t.add_column("Cost", justify="right", style="green")
83    t.add_column("Tokens", justify="right")
84    t.add_column("Calls", justify="right")
85    for r in report.by_model[:8]:
86        t.add_row(r.label, money(r.cost), compact_tokens(r.tokens), str(r.calls))
87    console.print(t)
88
89    # ---- top projects ----
90    if len(report.by_project) > 1:
91        console.print(Text("Top projects", style="bold"))
92        t = Table(box=SIMPLE_HEAVY, pad_edge=False)
93        t.add_column("Project")
94        t.add_column("Cost", justify="right", style="green")
95        t.add_column("Tokens", justify="right")
96        for r in report.by_project[:8]:
97            t.add_row(r.label, money(r.cost), compact_tokens(r.tokens))
98        console.print(t)
99
100    # ---- by activity ----
101    console.print(Text("By activity", style="bold"))
102    t = Table(box=SIMPLE_HEAVY, pad_edge=False)
103    t.add_column("Activity")
104    t.add_column("Cost", justify="right", style="green")
105    t.add_column("Share", justify="right")
106    for r in report.by_activity:
107        t.add_row(r.label, money(r.cost), share(r.cost, totals.cost))
108    console.print(t)
109
110    # ---- per day ----
111    if len(report.by_day) > 1:
112        console.print(Text("Per day", style="bold"))
113        max_cost = max((d.cost for d in report.by_day), default=0.0)
114        for d in report.by_day[-14:]:
115            line = Text("  ")
116            line.append(d.date + "  ", style="grey58")
117            line.append(_bar(d.cost, max_cost, 24))
118            line.append("  ")
119            line.append(money(d.cost), style="green")
120            console.print(line)
121        console.print()

▸项目脚本入口风险

在 sources/tokentab/pyproject.toml:1 这一处,页面把源码证据和设计判断绑定起来;在 sources/tokentab/pyproject.toml:9 这一处,页面把源码证据和设计判断绑定起来;在 sources/tokentab/pyproject.toml:13 这一处,页面把源码证据和设计判断绑定起来;在 sources/tokentab/pyproject.toml:14 这一处,页面把源码证据和设计判断绑定起来;在 sources/tokentab/pyproject.toml:20 这一处,页面把源码证据和设计判断绑定起来。这一段的关键不是单个语句,而是它和上下游契约如何相互约束:输入必须先被压到统一字段,后续 pricing、aggregate、serialize 才能保持无状态且可复用。

sources/tokentab/pyproject.toml · L1-21Python
1[project]
2name = "tokentab"  
3version = "0.1.0" 
4description = "Adds up what your AI coding tools cost by reading their local session logs. Supports Claude Code, Codex, and Gemini CLI. CLI + web dashboard, runs offline."
5readme = "README.md"
6requires-python = ">=3.10"
7license = { text = "MIT" }
8keywords = ["cli", "claude-code", "codex", "gemini", "token-usage", "cost-tracking", "ai-coding"]
9dependencies = [
10    "rich>=13.0",
11]
12
13[project.scripts]
14tokentab = "tokentab.cli:main"
15
16[build-system]
17requires = ["hatchling"]
18build-backend = "hatchling.build"
19
20[tool.hatch.build.targets.wheel]
21packages = ["tokentab"]
§ 02 · KEY FLOW

关键流程

disk logs → tolerant parser → UsageRecord → PricedRecord → grouped Report → CLI table / browser invoice

在 sources/tokentab/pyproject.toml:1 这一处,页面把源码证据和设计判断绑定起来;在 sources/tokentab/pyproject.toml:9 这一处,页面把源码证据和设计判断绑定起来;在 sources/tokentab/pyproject.toml:13 这一处,页面把源码证据和设计判断绑定起来;在 sources/tokentab/pyproject.toml:14 这一处,页面把源码证据和设计判断绑定起来;在 sources/tokentab/pyproject.toml:20 这一处,页面把源码证据和设计判断绑定起来。这条流程的优点是每层只承认一种输入形状;风险是入口文件一旦没有走到这个链路,底层解析器再干净也无法被用户正常调用。

§ 03 · EDGE

边界与失败

边界主要集中在日志格式漂移、模型价格表滞后、缓存 token 含义不一致和 provider 文件不可读。源码用 try_json、walk_files、provider try/except 和 unknown model warning 做了软失败处理;这适合本地观测工具,因为用户更需要“尽量给出已有数据”,而不是因为一个工具坏了让整份账单失败。