enigma-cli 1.15.9 → 1.16.0

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.
@@ -1,488 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Enigma - Context Savings</title>
7
- <style>
8
- :root {
9
- --bg: #0b0e14; --surface: #151a23; --surface2: #1b2230; --border: #232a36;
10
- --text: #e6e6e6; --muted: #8a93a3; --accent: #e0a458; --accent2: #d7875f; --good: #6fcf97;
11
- }
12
- * { box-sizing: border-box; }
13
- body {
14
- margin: 0; background: var(--bg); color: var(--text);
15
- font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
16
- -webkit-font-smoothing: antialiased; padding: 24px; max-width: 1000px; margin: 0 auto;
17
- }
18
- a { color: var(--accent2); }
19
- header { display: flex; align-items: center; gap: 12px; margin-bottom: 24px; }
20
- .wordmark { font-size: 20px; font-weight: 700; letter-spacing: 0.5px; }
21
- .wordmark span { color: var(--accent); }
22
- .sub { color: var(--muted); font-size: 13px; }
23
- .live { display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 12px; }
24
- .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--good); box-shadow: 0 0 8px var(--good); }
25
- .dot.stale { background: var(--muted); box-shadow: none; }
26
- .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-bottom: 20px; }
27
- .card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 16px; }
28
- .card .label { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.6px; }
29
- .card .value { font-size: 28px; font-weight: 700; margin-top: 6px; }
30
- .card .value.accent { color: var(--accent); }
31
- .card .value.good { color: var(--good); }
32
- .card .foot { color: var(--muted); font-size: 12px; margin-top: 4px; }
33
- .panel { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 18px; margin-bottom: 20px; }
34
- .panel-head { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
35
- .panel h2 { font-size: 14px; font-weight: 600; margin: 0; color: var(--text); }
36
- .panel h2 small { color: var(--muted); font-weight: 400; margin-left: 8px; }
37
- .ctrls { margin-left: auto; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
38
- .ranges { display: flex; gap: 4px; }
39
- .ranges button {
40
- background: var(--surface2); color: var(--muted); border: 1px solid var(--border);
41
- border-radius: 7px; padding: 4px 10px; font-size: 12px; cursor: pointer; font-family: inherit;
42
- }
43
- .ranges button:hover { color: var(--text); }
44
- .ranges button.active { background: var(--accent); color: #1a1206; border-color: var(--accent); font-weight: 600; }
45
- .chartwrap { position: relative; width: 100%; height: 280px; }
46
- /* Suppress the charting library's attribution logo. */
47
- #tv-attr-logo { display: none !important; }
48
- .nav { margin-left: auto; display: flex; align-items: center; gap: 18px; }
49
- .links { display: flex; gap: 14px; align-items: center; }
50
- .links a { color: var(--muted); display: inline-flex; transition: color 0.15s; }
51
- .links a:hover { color: var(--accent); }
52
- .links svg { width: 19px; height: 19px; display: block; }
53
- .tip {
54
- position: absolute; pointer-events: none; z-index: 5; background: var(--surface2);
55
- border: 1px solid var(--border); border-radius: 8px; padding: 6px 10px; font-size: 12px;
56
- box-shadow: 0 4px 16px rgba(0,0,0,0.4); white-space: nowrap;
57
- }
58
- .tip .tip-d { color: var(--muted); }
59
- .tip .tip-v { color: var(--accent); font-weight: 600; margin-top: 2px; }
60
- .bar-row { display: flex; align-items: center; gap: 12px; margin: 10px 0; font-size: 13px; }
61
- .bar-row .name { width: 70px; color: var(--muted); }
62
- .bar-track { flex: 1; height: 14px; background: var(--surface2); border-radius: 7px; overflow: hidden; }
63
- .bar-fill { height: 100%; border-radius: 7px; }
64
- .bar-row .num { width: 90px; text-align: right; font-variant-numeric: tabular-nums; }
65
- .bar-row .money { width: 78px; text-align: right; color: var(--good); font-variant-numeric: tabular-nums; }
66
- .empty { color: var(--muted); font-size: 14px; text-align: center; padding: 40px 0; }
67
- .tbl { width: 100%; border-collapse: collapse; font-size: 13px; }
68
- .tbl th { text-align: left; color: var(--muted); font-weight: 500; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; padding: 6px 10px; border-bottom: 1px solid var(--border); }
69
- .tbl td { padding: 7px 10px; border-bottom: 1px solid var(--surface2); font-variant-numeric: tabular-nums; }
70
- .tbl tr:last-child td { border-bottom: none; }
71
- .tbl .r { text-align: right; }
72
- .tbl .good { color: var(--good); }
73
- .tbl-wrap { max-height: 320px; overflow-y: auto; }
74
- .stat-line { display: flex; gap: 24px; margin-bottom: 12px; flex-wrap: wrap; }
75
- .stat-line div { font-size: 13px; color: var(--muted); }
76
- .stat-line b { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }
77
- footer { color: var(--muted); font-size: 12px; line-height: 1.6; margin-top: 8px; }
78
- code { background: var(--surface2); padding: 1px 6px; border-radius: 5px; color: var(--accent2); }
79
- @media (max-width: 720px) { .grid { grid-template-columns: repeat(2, 1fr); } }
80
- </style>
81
- </head>
82
- <body>
83
- <header>
84
- <div>
85
- <div class="wordmark">Enigma<span>.</span></div>
86
- <div class="sub">Context Compression Savings</div>
87
- </div>
88
- <div class="nav">
89
- <span class="live"><span id="dot" class="dot stale"></span><span id="updated">Connecting...</span></span>
90
- <nav class="links">
91
- <a href="https://github.com/FJRG2007/enigma" target="_blank" rel="noopener noreferrer" title="GitHub repository" aria-label="GitHub repository">
92
- <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.52 11.52 0 0 1 12 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222 0 1.606-.014 2.898-.014 3.293 0 .322.216.694.825.576C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
93
- </a>
94
- <a href="https://tpe.li/dsc" target="_blank" rel="noopener noreferrer" title="Discord" aria-label="Discord">
95
- <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20.317 4.3698a19.7913 19.7913 0 0 0-4.8851-1.5152.0741.0741 0 0 0-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 0 0-.0785-.037 19.7363 19.7363 0 0 0-4.8852 1.515.0699.0699 0 0 0-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 0 0 .0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 0 0 .0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 0 0-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 0 1-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 0 1 .0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 0 1 .0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 0 1-.0066.1276 12.2986 12.2986 0 0 1-1.873.8914.0766.0766 0 0 0-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 0 0 .0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 0 0 .0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 0 0-.0312-.0286ZM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189Zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9461 2.4189-2.1568 2.4189Z"/></svg>
96
- </a>
97
- <a href="https://ko-fi.com/fjrg2007" target="_blank" rel="noopener noreferrer" title="Support on Ko-fi" aria-label="Support on Ko-fi">
98
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 8h1a4 4 0 1 1 0 8h-1"/><path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z"/><line x1="6" y1="2" x2="6" y2="4"/><line x1="10" y1="2" x2="10" y2="4"/><line x1="14" y1="2" x2="14" y2="4"/></svg>
99
- </a>
100
- </nav>
101
- </div>
102
- </header>
103
-
104
- <div class="grid">
105
- <div class="card"><div class="label">Money Saved</div><div id="money" class="value good">-</div><div id="moneyFoot" class="foot">Estimated, by source</div></div>
106
- <div class="card"><div class="label">Tokens Saved</div><div id="saved" class="value">-</div><div id="savedFoot" class="foot">Across all compressions</div></div>
107
- <div class="card"><div class="label">Savings Rate</div><div id="rate" class="value accent">-</div><div class="foot">Of input tokens removed</div></div>
108
- <div class="card"><div class="label">Compressions</div><div id="calls" class="value">-</div><div class="foot">Reversible via CCR</div></div>
109
- <div class="card"><div class="label">Avg / Compression</div><div id="avg" class="value">-</div><div class="foot">Tokens saved per call</div></div>
110
- <div class="card"><div class="label">Best Compression</div><div id="best" class="value">-</div><div class="foot">Most saved in one call</div></div>
111
- </div>
112
-
113
- <div class="panel">
114
- <div class="panel-head">
115
- <h2>Savings Per Day <small id="rangeLabel"></small></h2>
116
- <div class="ctrls">
117
- <div class="ranges" id="metricToggle">
118
- <button type="button" data-m="tokens" class="active">Tokens</button>
119
- <button type="button" data-m="usd">$</button>
120
- </div>
121
- <div class="ranges" id="modeToggle">
122
- <button type="button" data-o="daily" class="active">Daily</button>
123
- <button type="button" data-o="cumulative">Cumulative</button>
124
- </div>
125
- <div class="ranges" id="ranges">
126
- <button type="button" data-d="7">7D</button>
127
- <button type="button" data-d="30" class="active">30D</button>
128
- <button type="button" data-d="90">90D</button>
129
- <button type="button" data-d="0">All</button>
130
- </div>
131
- </div>
132
- </div>
133
- <div id="chartWrap" class="chartwrap"></div>
134
- <div id="chartEmpty" class="empty" style="display:none">No compression activity recorded yet. Run <code>enigma compress &lt;file&gt;</code> or enable the compression MCP (<code>enigma config compress on</code>).</div>
135
- </div>
136
-
137
- <div class="panel">
138
- <h2 style="margin-bottom:14px">Savings By Source <small>which app/CLI compressed</small></h2>
139
- <div id="sources"></div>
140
- </div>
141
-
142
- <div class="panel">
143
- <h2 style="margin-bottom:14px">Savings By Content Type <small>what enigma compressed</small></h2>
144
- <div id="types"></div>
145
- </div>
146
-
147
- <div class="panel">
148
- <h2 style="margin-bottom:14px">Before vs. After <small id="ratioLabel">cumulative tokens</small></h2>
149
- <div class="bar-row"><span class="name">Before</span><div class="bar-track"><div id="barBefore" class="bar-fill" style="background:var(--accent2)"></div></div><span id="numBefore" class="num">-</span></div>
150
- <div class="bar-row"><span class="name">After</span><div class="bar-track"><div id="barAfter" class="bar-fill" style="background:var(--good)"></div></div><span id="numAfter" class="num">-</span></div>
151
- <div class="stat-line" style="margin:14px 0 0"><div>Removed <b id="removed">-</b></div><div>Reduction <b id="reduction">-</b></div><div>Compression ratio <b id="ratio">-</b></div></div>
152
- </div>
153
-
154
- <div class="panel">
155
- <div class="panel-head">
156
- <h2>Savings History <small id="histStats"></small></h2>
157
- <div class="ctrls">
158
- <div class="ranges" id="histToggle">
159
- <button type="button" data-p="day" class="active">Daily</button>
160
- <button type="button" data-p="week">Weekly</button>
161
- <button type="button" data-p="month">Monthly</button>
162
- </div>
163
- </div>
164
- </div>
165
- <div class="tbl-wrap"><div id="histBody"></div></div>
166
- </div>
167
-
168
- <div class="panel">
169
- <h2 style="margin-bottom:14px">Recent Compressions <small>newest first</small></h2>
170
- <div class="tbl-wrap"><div id="recent"></div></div>
171
- </div>
172
-
173
- <div class="panel">
174
- <h2 style="margin-bottom:14px">Reversible Cache (CCR) <small>recoverable originals on disk</small></h2>
175
- <div id="cache"></div>
176
- </div>
177
-
178
- <footer>
179
- Polling pauses automatically when this tab is hidden, and the server caches each snapshot - so an open dashboard costs almost nothing.
180
- Data source: <code>~/.enigma/ccr/stats.json</code> + <code>history.jsonl</code> - reset it with <code>enigma compress --clear</code>. Enigma <span id="ver"></span>
181
- </footer>
182
-
183
- <script src="/lib/chart.min.js"></script>
184
- <script>
185
- const POLL_MS = 5000;
186
- const DAY = 86400000;
187
- const $ = (id) => document.getElementById(id);
188
-
189
- function fmt(n) {
190
- n = Number(n) || 0;
191
- if (n >= 1e9) return (n / 1e9).toFixed(2) + "B";
192
- if (n >= 1e6) return (n / 1e6).toFixed(2) + "M";
193
- if (n >= 1e3) return (n / 1e3).toFixed(1) + "k";
194
- return String(Math.round(n));
195
- }
196
- function fmtUsd(n) {
197
- n = Number(n) || 0;
198
- if (n >= 1e3) return "$" + (n / 1e3).toFixed(2) + "k";
199
- if (n >= 1) return "$" + n.toFixed(2);
200
- return "$" + n.toFixed(n >= 0.1 ? 2 : 3);
201
- }
202
- function fmtBytes(n) {
203
- n = Number(n) || 0;
204
- if (n >= 1048576) return (n / 1048576).toFixed(1) + " MB";
205
- if (n >= 1024) return (n / 1024).toFixed(1) + " KB";
206
- return n + " B";
207
- }
208
-
209
- // Estimated price per 1M INPUT tokens, by source. enigma is not a proxy, so it
210
- // can't know the real model - these are sensible defaults, overridable globally
211
- // with `enigma config token-price <usd>` (priceOverride from the payload).
212
- const PRICE_1M = {
213
- "claude-code": 3, "claude": 3, "claude-desktop": 3,
214
- "opencode": 3, "codex": 2.5, "codex-cli": 2.5, "cli": 3, "unknown": 3,
215
- };
216
- const DEFAULT_PRICE_1M = 3;
217
- let priceOverride = 0;
218
- function priceFor(source) { return priceOverride > 0 ? priceOverride : (PRICE_1M[source] ?? DEFAULT_PRICE_1M); }
219
- function moneyOf(source, savedTokens) { return savedTokens * priceFor(source) / 1e6; }
220
-
221
- // Friendly names for known MCP clients / content types; unknown -> title-cased raw.
222
- const SOURCE_LABELS = {
223
- "claude-code": "Claude Code", "claude": "Claude Code", "claude-desktop": "Claude Desktop",
224
- "opencode": "OpenCode", "codex": "Codex", "codex-cli": "Codex",
225
- "cli": "CLI (enigma compress)", "unknown": "Unattributed",
226
- };
227
- function prettySource(k) { return SOURCE_LABELS[k] || String(k).replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); }
228
- const TYPE_LABELS = { json: "JSON", log: "Logs", text: "Text", code: "Code", diff: "Diffs", markdown: "Markdown", unknown: "Other" };
229
- function prettyType(k) { return TYPE_LABELS[k] || String(k).replace(/\b\w/g, (c) => c.toUpperCase()); }
230
-
231
- // One bar per key (source or type), sorted by tokens saved, with an optional $ column.
232
- function renderBreakdown(el, map, label, money) {
233
- const entries = Object.entries(map || {}).sort((a, b) => (b[1].tokensSaved || 0) - (a[1].tokensSaved || 0));
234
- if (!entries.length) {
235
- el.innerHTML = '<div class="empty" style="padding:20px 0">No data yet - this fills in as compressions are recorded.</div>';
236
- return;
237
- }
238
- const max = Math.max(1, ...entries.map(([, v]) => v.tokensSaved || 0));
239
- el.innerHTML = entries.map(([k, v]) => {
240
- const saved = v.tokensSaved || 0, pct = Math.round(saved / max * 100);
241
- const m = money ? '<span class="money">' + fmtUsd(moneyOf(k, saved)) + '</span>' : '';
242
- return '<div class="bar-row"><span class="name" style="width:140px">' + label(k)
243
- + '</span><div class="bar-track"><div class="bar-fill" style="width:' + pct + '%;background:var(--accent)"></div></div>'
244
- + '<span class="num">' + fmt(saved) + '</span>' + m
245
- + '<span class="foot" style="width:64px;text-align:right;margin:0">' + fmt(v.calls || 0) + ' calls</span></div>';
246
- }).join("");
247
- }
248
-
249
- // A millisecond timestamp -> the UTC calendar day as the YYYY-MM-DD string the
250
- // chart's time scale expects (matches the server's UTC-day history buckets).
251
- function dayKey(ms) { return new Date(ms).toISOString().slice(0, 10); }
252
-
253
- const savedOf = (p) => Math.max(0, (p.b || 0) - (p.a || 0));
254
-
255
- // Recent compressions table (newest first), one row per history point.
256
- function renderRecent(history) {
257
- const rows = (history || []).slice(-20).reverse();
258
- const el = $("recent");
259
- if (!rows.length) { el.innerHTML = '<div class="empty" style="padding:24px 0">No compressions recorded yet.</div>'; return; }
260
- el.innerHTML = '<table class="tbl"><thead><tr><th>Time</th><th>Type</th><th>Source</th>'
261
- + '<th class="r">Before</th><th class="r">After</th><th class="r">Saved</th></tr></thead><tbody>'
262
- + rows.map((p) => {
263
- const saved = savedOf(p), pct = p.b ? Math.round(saved / p.b * 100) : 0;
264
- return '<tr><td>' + new Date(p.t).toLocaleString() + '</td><td>' + prettyType(p.c || "unknown")
265
- + '</td><td>' + prettySource(p.s || "unknown") + '</td><td class="r">' + fmt(p.b || 0)
266
- + '</td><td class="r">' + fmt(p.a || 0) + '</td><td class="r good">' + fmt(saved) + " (" + pct + "%)</td></tr>";
267
- }).join("") + "</tbody></table>";
268
- }
269
-
270
- // ISO-ish period key for grouping (day = YYYY-MM-DD, week = YYYY-Www, month = YYYY-MM).
271
- function periodKey(t, period) {
272
- const d = new Date(t);
273
- if (period === "month") return d.toISOString().slice(0, 7);
274
- if (period === "week") {
275
- const onejan = Date.UTC(d.getUTCFullYear(), 0, 1);
276
- const week = Math.ceil(((d.getTime() - onejan) / DAY + 1) / 7);
277
- return d.getUTCFullYear() + "-W" + String(week).padStart(2, "0");
278
- }
279
- return dayKey(t);
280
- }
281
-
282
- let histPeriod = "day";
283
- function renderHistory(history) {
284
- const hist = history || [];
285
- const days = new Set(hist.map((p) => dayKey(p.t))).size;
286
- const total = hist.reduce((s, p) => s + savedOf(p), 0);
287
- $("histStats").textContent = days ? days + " active day" + (days === 1 ? "" : "s") + " · avg " + fmt(Math.round(total / days)) + "/day" : "";
288
- const byP = new Map();
289
- for (const p of hist) {
290
- const k = periodKey(p.t, histPeriod);
291
- const cur = byP.get(k) || { tokens: 0, usd: 0, calls: 0 };
292
- cur.tokens += savedOf(p);
293
- cur.usd += moneyOf(p.s || "unknown", savedOf(p));
294
- cur.calls++;
295
- byP.set(k, cur);
296
- }
297
- const rows = [...byP.entries()].sort((a, b) => a[0] < b[0] ? 1 : -1);
298
- const el = $("histBody");
299
- if (!rows.length) { el.innerHTML = '<div class="empty" style="padding:24px 0">No history yet.</div>'; return; }
300
- el.innerHTML = '<table class="tbl"><thead><tr><th>Period</th><th class="r">Compressions</th>'
301
- + '<th class="r">Tokens Saved</th><th class="r">$ Saved</th></tr></thead><tbody>'
302
- + rows.map(([k, v]) => '<tr><td>' + k + '</td><td class="r">' + v.calls + '</td><td class="r good">'
303
- + fmt(v.tokens) + '</td><td class="r">' + fmtUsd(v.usd) + "</td></tr>").join("") + "</tbody></table>";
304
- }
305
-
306
- function renderCache(c) {
307
- const el = $("cache");
308
- if (!c) { el.innerHTML = '<div class="empty" style="padding:20px 0">No cache data.</div>'; return; }
309
- const pct = c.cap ? Math.round((c.count || 0) / c.cap * 100) : 0;
310
- el.innerHTML = '<div class="bar-row"><span class="name" style="width:170px">Cached originals</span>'
311
- + '<div class="bar-track"><div class="bar-fill" style="width:' + pct + '%;background:var(--accent)"></div></div>'
312
- + '<span class="num">' + fmt(c.count || 0) + " / " + fmt(c.cap || 0) + '</span></div>'
313
- + '<div class="stat-line" style="margin-top:6px"><div>On disk <b>' + fmtBytes(c.bytes || 0) + '</b></div>'
314
- + '<div>Retention <b>newest ' + fmt(c.cap || 0) + '</b></div>'
315
- + '<div>Restore <b>enigma compress --retrieve &lt;hash&gt;</b></div></div>';
316
- }
317
-
318
- // Aggregate raw history points into one {time, tokens, usd} per day (ascending,
319
- // unique times). $ per point uses that point's recorded source price.
320
- function dailySeries(history) {
321
- const byDay = new Map();
322
- for (const p of history || []) {
323
- const saved = Math.max(0, (p.b || 0) - (p.a || 0));
324
- const key = dayKey(p.t);
325
- const cur = byDay.get(key) || { tokens: 0, usd: 0 };
326
- cur.tokens += saved;
327
- cur.usd += moneyOf(p.s || "unknown", saved);
328
- byDay.set(key, cur);
329
- }
330
- return [...byDay.entries()].sort((a, b) => a[0] < b[0] ? -1 : 1)
331
- .map(([time, v]) => ({ time, tokens: v.tokens, usd: v.usd }));
332
- }
333
-
334
- let chart = null, series = null, dailyData = [], lastHistory = [], currentRange = 30, metric = "tokens", mode = "daily";
335
- const metricFmt = (v) => metric === "usd" ? fmtUsd(v) : fmt(v);
336
-
337
- // Map the per-day data to LC {time, value} for the active metric and mode.
338
- function seriesValues() {
339
- let cum = 0;
340
- return dailyData.map((d) => {
341
- const base = metric === "usd" ? d.usd : d.tokens;
342
- if (mode === "cumulative") { cum += base; return { time: d.time, value: cum }; }
343
- return { time: d.time, value: base };
344
- });
345
- }
346
-
347
- function initChart() {
348
- if (!window.LightweightCharts) return false;
349
- const LC = window.LightweightCharts;
350
- const el = $("chartWrap");
351
- chart = LC.createChart(el, {
352
- autoSize: true,
353
- layout: { background: { type: "solid", color: "transparent" }, textColor: "#8a93a3", fontSize: 12 },
354
- grid: { vertLines: { color: "rgba(35,42,54,0.5)" }, horzLines: { color: "rgba(35,42,54,0.5)" } },
355
- rightPriceScale: { borderColor: "#232a36" },
356
- timeScale: { borderColor: "#232a36", fixLeftEdge: true, fixRightEdge: true },
357
- crosshair: {
358
- mode: LC.CrosshairMode.Magnet,
359
- vertLine: { color: "#3a4150", labelBackgroundColor: "#e0a458" },
360
- horzLine: { color: "#3a4150", labelBackgroundColor: "#e0a458" },
361
- },
362
- });
363
- series = chart.addSeries(LC.AreaSeries, {
364
- lineColor: "#e0a458", topColor: "rgba(224,164,88,0.35)", bottomColor: "rgba(224,164,88,0.02)",
365
- lineWidth: 2, priceLineVisible: false, lastValueVisible: false,
366
- priceFormat: { type: "custom", minMove: 1, formatter: metricFmt },
367
- });
368
- setupTooltip(el);
369
- // Button groups: select one, restyle siblings, run the picker.
370
- const group = (id, onPick) => $(id).addEventListener("click", (e) => {
371
- const b = e.target.closest("button");
372
- if (!b) return;
373
- for (const x of $(id).children) x.classList.toggle("active", x === b);
374
- onPick(b);
375
- });
376
- group("ranges", (b) => { currentRange = Number(b.dataset.d); applyRange(); });
377
- group("metricToggle", (b) => { metric = b.dataset.m; redraw(); });
378
- group("modeToggle", (b) => { mode = b.dataset.o; redraw(); });
379
- group("histToggle", (b) => { histPeriod = b.dataset.p; renderHistory(lastHistory); });
380
- return true;
381
- }
382
-
383
- // Floating crosshair tooltip: hovered date + that point's value (tokens or $).
384
- function setupTooltip(container) {
385
- const tip = document.createElement("div");
386
- tip.className = "tip";
387
- tip.style.display = "none";
388
- container.appendChild(tip);
389
- chart.subscribeCrosshairMove((param) => {
390
- if (!param.time || !param.point || param.point.x < 0 || param.point.y < 0) { tip.style.display = "none"; return; }
391
- const d = param.seriesData.get(series);
392
- if (!d) { tip.style.display = "none"; return; }
393
- const suffix = (metric === "usd" ? " saved" : " tokens saved") + (mode === "cumulative" ? " (total)" : "");
394
- tip.innerHTML = '<div class="tip-d">' + param.time + '</div><div class="tip-v">' + metricFmt(d.value) + suffix + "</div>";
395
- tip.style.display = "block";
396
- const left = Math.max(0, Math.min(param.point.x + 14, container.clientWidth - 160));
397
- tip.style.left = left + "px";
398
- tip.style.top = Math.max(0, param.point.y - 10) + "px";
399
- });
400
- }
401
-
402
- // Push the active metric/mode data into the series and reformat the axis.
403
- function redraw() {
404
- if (!series) return;
405
- series.applyOptions({ priceFormat: { type: "custom", minMove: metric === "usd" ? 0.0001 : 1, formatter: metricFmt } });
406
- const has = dailyData.length > 0;
407
- $("chartEmpty").style.display = has ? "none" : "block";
408
- $("chartWrap").style.display = has ? "block" : "none";
409
- if (has) { series.setData(seriesValues()); applyRange(); }
410
- }
411
-
412
- // Constrain the visible window to the selected range (All = fit everything).
413
- function applyRange() {
414
- if (!chart || !dailyData.length) return;
415
- const ts = chart.timeScale();
416
- if (currentRange === 0) { ts.fitContent(); $("rangeLabel").textContent = "all time"; return; }
417
- const to = dailyData[dailyData.length - 1].time;
418
- const from = dayKey(Date.now() - currentRange * DAY);
419
- try { ts.setVisibleRange({ from, to }); }
420
- catch { ts.fitContent(); }
421
- $("rangeLabel").textContent = "last " + currentRange + " days";
422
- }
423
-
424
- // Total estimated $ saved: sum each source's saved tokens at its price (legacy
425
- // data with no attribution falls back to the default price).
426
- function totalMoney(s) {
427
- const bs = s.bySource || {};
428
- const keys = Object.keys(bs);
429
- if (keys.length) return keys.reduce((sum, k) => sum + moneyOf(k, bs[k].tokensSaved || 0), 0);
430
- return moneyOf("unknown", s.tokensSaved || 0);
431
- }
432
-
433
- function render(data) {
434
- priceOverride = Number(data.priceOverride) || 0;
435
- const s = data.stats || {};
436
- const before = s.tokensBefore || 0, after = s.tokensAfter || 0, saved = s.tokensSaved || 0, calls = s.calls || 0;
437
- const rate = before ? Math.round((saved / before) * 100) : 0;
438
- $("money").textContent = fmtUsd(totalMoney(s));
439
- $("moneyFoot").textContent = priceOverride > 0 ? "~$" + priceOverride + "/1M tokens" : "Estimated, by source";
440
- $("saved").textContent = fmt(saved);
441
- $("savedFoot").textContent = saved ? "~" + saved.toLocaleString() + " tokens" : "Across all compressions";
442
- $("rate").textContent = rate + "%";
443
- $("calls").textContent = fmt(calls);
444
- $("avg").textContent = calls ? fmt(Math.round(saved / calls)) : "0";
445
- $("best").textContent = fmt(s.best || 0);
446
- $("numBefore").textContent = fmt(before);
447
- $("numAfter").textContent = fmt(after);
448
- const max = Math.max(1, before);
449
- $("barBefore").style.width = (before / max * 100) + "%";
450
- $("barAfter").style.width = (after / max * 100) + "%";
451
- $("removed").textContent = fmt(saved) + " (" + rate + "%)";
452
- $("reduction").textContent = before ? (100 - rate) + "% of input sent" : "-";
453
- $("ratio").textContent = after ? "~" + (before / after).toFixed(1) + ":1" : "-";
454
- renderBreakdown($("sources"), s.bySource, prettySource, true);
455
- renderBreakdown($("types"), s.byType, prettyType, false);
456
-
457
- lastHistory = data.history || [];
458
- renderRecent(lastHistory);
459
- renderHistory(lastHistory);
460
- renderCache(data.cache);
461
-
462
- dailyData = dailySeries(data.history);
463
- redraw();
464
-
465
- $("ver").textContent = data.version ? "v" + data.version : "";
466
- $("dot").classList.remove("stale");
467
- $("updated").textContent = "Updated " + new Date(data.generatedAt || Date.now()).toLocaleTimeString();
468
- }
469
-
470
- async function poll() {
471
- // Headroom pattern: never hit the server while the tab is hidden.
472
- if (document.hidden) return;
473
- try {
474
- const res = await fetch("/api/stats", { cache: "no-store" });
475
- render(await res.json());
476
- } catch {
477
- $("dot").classList.add("stale");
478
- $("updated").textContent = "Disconnected";
479
- }
480
- }
481
-
482
- initChart();
483
- poll();
484
- setInterval(poll, POLL_MS);
485
- document.addEventListener("visibilitychange", () => { if (!document.hidden) poll(); });
486
- </script>
487
- </body>
488
- </html>