@caius_kong/ccusage-dashboard 0.2.6 → 0.2.8
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.
- package/README.md +4 -3
- package/lib/index.html +27 -28
- package/lib/server.py +41 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,13 +56,14 @@ CCUSAGE_UI_NO_OPEN=1 npx @caius_kong/ccusage-dashboard # don't auto-open brows
|
|
|
56
56
|
|
|
57
57
|
| | |
|
|
58
58
|
|---|---|
|
|
59
|
-
| **Today / This Week / This Month / Custom Range** | totals + tokens + cache breakdown |
|
|
59
|
+
| **Today / This Week / This Month / Custom Range** | totals + tokens + cache breakdown + **cache hit rate** |
|
|
60
60
|
| **By model** | per-model cost, % of total, in/out/cache-read/cache-write tokens |
|
|
61
61
|
| **30-day trend** | daily cost bar chart (hover for values, weekends marked) |
|
|
62
|
-
| **Sessions** | per-session rows grouped by workdir name + short session id,
|
|
62
|
+
| **Sessions** | per-session rows grouped by workdir name + short session id, sorted by cost — shares the top time-filter tabs (today/week/month/custom) |
|
|
63
63
|
| **Budget alert** | monthly cap (default $300) — green <80%, yellow <100%, red ≥100% |
|
|
64
64
|
|
|
65
|
-
All costs in USD.
|
|
65
|
+
All costs in USD. **Cache hit rate** is the standard input-side metric:
|
|
66
|
+
`cacheReadTokens / (cacheReadTokens + non-cached inputTokens)`. Auto-refresh every 15s.
|
|
66
67
|
|
|
67
68
|
## How it works
|
|
68
69
|
|
package/lib/index.html
CHANGED
|
@@ -77,7 +77,6 @@
|
|
|
77
77
|
<div class="tab" role="tab" tabindex="0" aria-selected="false" data-tab="week">This Week</div>
|
|
78
78
|
<div class="tab" role="tab" tabindex="0" aria-selected="false" data-tab="month">This Month</div>
|
|
79
79
|
<div class="tab" role="tab" tabindex="0" aria-selected="false" data-tab="range">Custom Range</div>
|
|
80
|
-
<div class="tab" role="tab" tabindex="0" aria-selected="false" data-tab="sessions">Sessions</div>
|
|
81
80
|
<div class="tab-budget">
|
|
82
81
|
<span class="budget-pill" id="budgetPill">budget …</span>
|
|
83
82
|
</div>
|
|
@@ -93,6 +92,7 @@
|
|
|
93
92
|
<div class="card"><div class="label">Tokens</div><div class="value" id="c2">–</div></div>
|
|
94
93
|
<div class="card"><div class="label">Cache Read</div><div class="value" id="c3">–</div></div>
|
|
95
94
|
<div class="card"><div class="label">Cache Write</div><div class="value" id="c4">–</div></div>
|
|
95
|
+
<div class="card"><div class="label">Cache Hit Rate</div><div class="value" id="c5">–</div><div class="period" id="c5p"></div></div>
|
|
96
96
|
</div>
|
|
97
97
|
|
|
98
98
|
<div id="rangeRow" class="panel" style="display:none">
|
|
@@ -108,18 +108,11 @@
|
|
|
108
108
|
|
|
109
109
|
<div class="panel"><h2>Models <span class="tot" id="modelSum"></span></h2><div id="modelList"><div class="empty">loading…</div></div></div>
|
|
110
110
|
|
|
111
|
-
<div class="panel" id="sessionsPanel"
|
|
112
|
-
<h2>Sessions
|
|
111
|
+
<div class="panel" id="sessionsPanel">
|
|
112
|
+
<h2>Sessions
|
|
113
113
|
<span class="pill" id="sessRange"></span>
|
|
114
|
+
<span class="tot" id="sessSum" style="margin-left:8px"></span>
|
|
114
115
|
</h2>
|
|
115
|
-
<div class="range" style="margin-bottom:12px">
|
|
116
|
-
<span class="pill">last</span>
|
|
117
|
-
<button data-sdays="7" class="sday">7d</button>
|
|
118
|
-
<button data-sdays="30" class="sday active">30d</button>
|
|
119
|
-
<button data-sdays="90" class="sday">90d</button>
|
|
120
|
-
<button data-sdays="365" class="sday">all</button>
|
|
121
|
-
<span class="pill" style="margin-left:auto" id="sessHint"></span>
|
|
122
|
-
</div>
|
|
123
116
|
<div id="sessionsList"><div class="empty">loading…</div></div>
|
|
124
117
|
</div>
|
|
125
118
|
|
|
@@ -127,8 +120,8 @@
|
|
|
127
120
|
|
|
128
121
|
<script>
|
|
129
122
|
const $=(id)=>document.getElementById(id);
|
|
130
|
-
const tabs={today:{label:"Today",len:0},week:{label:"This Week",len:0},month:{label:"This Month",len:0},range:{label:"Custom Range",len:1}
|
|
131
|
-
let active="today", modelCtx=[]
|
|
123
|
+
const tabs={today:{label:"Today",len:0},week:{label:"This Week",len:0},month:{label:"This Month",len:0},range:{label:"Custom Range",len:1}};
|
|
124
|
+
let active="today", modelCtx=[];
|
|
132
125
|
|
|
133
126
|
function fmtUsd(n){return "$"+(n==null?"–":Number(n).toFixed(2));}
|
|
134
127
|
function fmtTok(n){
|
|
@@ -138,6 +131,7 @@ function fmtTok(n){
|
|
|
138
131
|
if(n>=1e3)return (n/1e3).toFixed(1)+"k";
|
|
139
132
|
return String(n);
|
|
140
133
|
}
|
|
134
|
+
function fmtPct(n){return n==null?"–":n.toFixed(1)+"%";}
|
|
141
135
|
function esc(s){const d=document.createElement("div");d.textContent=s;return d.innerHTML;}
|
|
142
136
|
|
|
143
137
|
function renderModels(data){
|
|
@@ -173,8 +167,17 @@ function applyBudget(month){
|
|
|
173
167
|
}
|
|
174
168
|
|
|
175
169
|
async function loadSessions(){
|
|
176
|
-
|
|
177
|
-
|
|
170
|
+
// follow the top-level time filter (today/week/month/custom range)
|
|
171
|
+
let q="";
|
|
172
|
+
if(active==="today")q="period=today";
|
|
173
|
+
else if(active==="week")q="period=week";
|
|
174
|
+
else if(active==="month")q="period=month";
|
|
175
|
+
else if(active==="range"){
|
|
176
|
+
const f=$("fromDate").value,t=$("toDate").value;
|
|
177
|
+
if(!f||!t){return;}
|
|
178
|
+
q=`from=${f}&to=${t}`;
|
|
179
|
+
}else{return;}
|
|
180
|
+
const data=await(await fetch(`/api/sessions?${q}`)).json();
|
|
178
181
|
if(data.error)throw new Error(data.error);
|
|
179
182
|
renderSessions(data);
|
|
180
183
|
}
|
|
@@ -182,9 +185,9 @@ async function loadSessions(){
|
|
|
182
185
|
function renderSessions(data){
|
|
183
186
|
const list=data.sessions||[];
|
|
184
187
|
const total=data.total||0;
|
|
185
|
-
const sum=list.reduce((a,s)=>a+(s.cost||0),0);
|
|
188
|
+
const sum=data.totalCost!=null?data.totalCost:list.reduce((a,s)=>a+(s.cost||0),0);
|
|
186
189
|
$("sessSum").textContent=fmtUsd(sum);
|
|
187
|
-
$("sessRange").textContent=`${total} sessions
|
|
190
|
+
$("sessRange").textContent=`${total} sessions`;
|
|
188
191
|
if(!list.length){$("sessionsList").innerHTML='<div class="empty">no sessions in range</div>';return;}
|
|
189
192
|
$("sessionsList").innerHTML=list.map(s=>{
|
|
190
193
|
const label = s.hasCwd
|
|
@@ -206,6 +209,11 @@ function applyCards(data){
|
|
|
206
209
|
$("c2").textContent=fmtTok(tt);
|
|
207
210
|
$("c3").textContent=fmtTok(data.cacheReadTokens);
|
|
208
211
|
$("c4").textContent=fmtTok(data.cacheCreationTokens);
|
|
212
|
+
// cache hit rate = cache-read input / (cache-read input + non-cached input)
|
|
213
|
+
const hitIn=data.cacheReadTokens||0, plainIn=data.inputTokens||0;
|
|
214
|
+
const hitRate = (hitIn+plainIn)>0 ? (hitIn/(hitIn+plainIn))*100 : null;
|
|
215
|
+
$("c5").innerHTML=fmtPct(hitRate)+(hitRate!=null?"<small> cache</small>":"");
|
|
216
|
+
$("c5p").textContent=hitRate!=null?`${fmtTok(plainIn)} miss · ${fmtTok(hitIn)} hit`:"";
|
|
209
217
|
$("c1label").textContent=tabs[active].label;
|
|
210
218
|
$("modelSum").textContent=fmtUsd(total);
|
|
211
219
|
}
|
|
@@ -298,6 +306,7 @@ async function tick(){
|
|
|
298
306
|
applyBudget(month);
|
|
299
307
|
await loadModels();
|
|
300
308
|
await loadTrend();
|
|
309
|
+
await loadSessions();
|
|
301
310
|
dot.className="dot ok";st.textContent="live";
|
|
302
311
|
$("lastUpd").textContent="last updated "+new Date().toLocaleTimeString();
|
|
303
312
|
}catch(e){dot.className="dot err";st.textContent="error: "+e.message;}
|
|
@@ -310,28 +319,18 @@ document.querySelectorAll(".tab").forEach(t=>{
|
|
|
310
319
|
t.setAttribute("aria-selected","true");
|
|
311
320
|
active=t.dataset.tab;
|
|
312
321
|
$("rangeRow").style.display = active==="range"?"block":"none";
|
|
313
|
-
$("sessionsPanel").style.display = active==="sessions"?"block":"none";
|
|
314
322
|
if(active==="range"){
|
|
315
323
|
if(!$("fromDate").value){const e=new Date();$("toDate").value=e.toISOString().slice(0,10);const s=new Date();s.setDate(s.getDate()-6);$("fromDate").value=s.toISOString().slice(0,10);}
|
|
316
324
|
rangeApply();
|
|
317
|
-
}else if(active==="sessions"){
|
|
318
|
-
loadSessions();
|
|
319
325
|
}else{
|
|
320
326
|
loadModels();
|
|
321
327
|
}
|
|
328
|
+
loadSessions();
|
|
322
329
|
};
|
|
323
330
|
t.addEventListener("click",activate);
|
|
324
331
|
t.addEventListener("keydown",(e)=>{if(e.key==="Enter"||e.key===" "||e.key==="Spacebar"){e.preventDefault();activate();}});
|
|
325
332
|
});
|
|
326
333
|
$("rangeGo").addEventListener("click",rangeApply);
|
|
327
|
-
document.querySelectorAll(".sday").forEach(b=>{
|
|
328
|
-
b.addEventListener("click",()=>{
|
|
329
|
-
document.querySelectorAll(".sday").forEach(x=>x.classList.remove("active"));
|
|
330
|
-
b.classList.add("active");
|
|
331
|
-
sessDays=parseInt(b.dataset.sdays,10);
|
|
332
|
-
loadSessions();
|
|
333
|
-
});
|
|
334
|
-
});
|
|
335
334
|
|
|
336
335
|
tick();
|
|
337
336
|
setInterval(tick,15000);
|
package/lib/server.py
CHANGED
|
@@ -194,10 +194,14 @@ def trend(days: int = 30) -> dict:
|
|
|
194
194
|
return {"start": start.isoformat(), "end": end.isoformat(), "days": out}
|
|
195
195
|
|
|
196
196
|
|
|
197
|
-
def sessions(since_days: int = 30) -> dict:
|
|
197
|
+
def sessions(since_days: int = 30, period: str = "", from_date: str = "", to_date: str = "") -> dict:
|
|
198
198
|
"""Return ccusage's session report, each with its real workdir (dirName) when
|
|
199
199
|
the agent's local session store records one. Every numeric cost/token field
|
|
200
200
|
comes 100% from ccusage; the workdir is only a display label.
|
|
201
|
+
|
|
202
|
+
Filtering: pass `period` in ("today" | "week" | "month") OR an explicit
|
|
203
|
+
date range via `from_date`/`to_date` (YYYY-MM-DD). `since_days` is kept for
|
|
204
|
+
backward compatibility with the old 7d/30d/90d/all selector.
|
|
201
205
|
"""
|
|
202
206
|
data = run_ccusage(
|
|
203
207
|
["session", "--by-agent", "--json", "--offline"],
|
|
@@ -205,26 +209,44 @@ def sessions(since_days: int = 30) -> dict:
|
|
|
205
209
|
)
|
|
206
210
|
rows = data.get("session") or []
|
|
207
211
|
|
|
208
|
-
|
|
212
|
+
# ---- choose the cutoff window ----------------
|
|
213
|
+
today = date.today()
|
|
214
|
+
lo = hi = None
|
|
215
|
+
if from_date and to_date:
|
|
216
|
+
try:
|
|
217
|
+
lo = date.fromisoformat(from_date)
|
|
218
|
+
hi = date.fromisoformat(to_date)
|
|
219
|
+
except ValueError:
|
|
220
|
+
lo = hi = None
|
|
221
|
+
elif period == "today":
|
|
222
|
+
lo = hi = today
|
|
223
|
+
elif period == "week":
|
|
224
|
+
lo = today - timedelta(days=6)
|
|
225
|
+
hi = today
|
|
226
|
+
elif period == "month":
|
|
227
|
+
lo = today.replace(day=1)
|
|
228
|
+
hi = today
|
|
229
|
+
elif period == "custom":
|
|
230
|
+
pass # fall through to since_days below (legacy)
|
|
231
|
+
if lo is None:
|
|
232
|
+
lo = today - timedelta(days=since_days)
|
|
233
|
+
hi = today
|
|
234
|
+
|
|
209
235
|
out = []
|
|
210
236
|
for r in rows:
|
|
211
237
|
meta = r.get("metadata") or {}
|
|
212
|
-
sid = r.get("period")
|
|
238
|
+
sid = r.get("period")
|
|
213
239
|
last_activity = meta.get("lastActivity") or ""
|
|
214
|
-
# date filter by last activity
|
|
240
|
+
# date filter by last activity against [lo, hi]
|
|
215
241
|
if last_activity:
|
|
216
242
|
try:
|
|
217
243
|
act_date = date.fromisoformat(last_activity[:10])
|
|
218
244
|
except ValueError:
|
|
219
245
|
act_date = None
|
|
220
|
-
if act_date is not None
|
|
221
|
-
|
|
246
|
+
if act_date is not None:
|
|
247
|
+
if act_date < lo or act_date > hi:
|
|
248
|
+
continue
|
|
222
249
|
project_raw = meta.get("projectPath") or ""
|
|
223
|
-
# Resolve the authoritative workdir (dir name) for every agent that stores one
|
|
224
|
-
# locally. ccusage's projectPath only covers pi and is lossy for hyphenated names
|
|
225
|
-
# ("fund-tracker" -> "tracker"). Each agent keeps its own session store whose
|
|
226
|
-
# files carry the real cwd, so we read those. This ONLY affects the display label
|
|
227
|
-
# (dirName/cwd) — every numeric cost/token field still comes 100% from ccusage.
|
|
228
250
|
agent_name = r.get("agent", "?")
|
|
229
251
|
cwd = ""
|
|
230
252
|
dir_name = ""
|
|
@@ -236,7 +258,6 @@ def sessions(since_days: int = 30) -> dict:
|
|
|
236
258
|
if cwd_base:
|
|
237
259
|
dir_name = cwd_base
|
|
238
260
|
if not cwd:
|
|
239
|
-
# fall back to a best-effort decode of ccusage's projectPath (pi only)
|
|
240
261
|
cwd = _decode_cwd(project_raw)
|
|
241
262
|
dir_name = _basename(project_raw)
|
|
242
263
|
out.append(
|
|
@@ -249,14 +270,15 @@ def sessions(since_days: int = 30) -> dict:
|
|
|
249
270
|
"cacheReadTokens": r.get("cacheReadTokens", 0),
|
|
250
271
|
"cacheCreationTokens": r.get("cacheCreationTokens", 0),
|
|
251
272
|
"lastActivity": last_activity,
|
|
252
|
-
"cwd": cwd,
|
|
253
|
-
"dirName": dir_name,
|
|
254
|
-
"projectKey": project_raw,
|
|
273
|
+
"cwd": cwd,
|
|
274
|
+
"dirName": dir_name,
|
|
275
|
+
"projectKey": project_raw,
|
|
255
276
|
"hasCwd": bool(cwd),
|
|
256
277
|
}
|
|
257
278
|
)
|
|
258
279
|
out.sort(key=lambda s: s["cost"], reverse=True)
|
|
259
|
-
|
|
280
|
+
total_cost = round(sum(s["cost"] for s in out), 4)
|
|
281
|
+
return {"total": len(out), "totalCost": total_cost, "sessions": out}
|
|
260
282
|
|
|
261
283
|
|
|
262
284
|
def _pi_cwd_from_disk(project_key: str):
|
|
@@ -498,8 +520,10 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
498
520
|
self._json(trend(days))
|
|
499
521
|
return
|
|
500
522
|
elif path == "/api/sessions":
|
|
523
|
+
period = params.get("period", "")
|
|
524
|
+
frm, to = params.get("from", ""), params.get("to", "")
|
|
501
525
|
days = max(1, min(366, int(params.get("days", "30"))))
|
|
502
|
-
self._json(sessions(days))
|
|
526
|
+
self._json(sessions(days, period, frm, to))
|
|
503
527
|
return
|
|
504
528
|
else:
|
|
505
529
|
self._send(404, b"not found", "text/plain")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@caius_kong/ccusage-dashboard",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "One-command local dashboard for ccusage: today/week/month/custom cost by model, 30-day trend, monthly budget alert. Numbers straight from ccusage.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|