@caius_kong/ccusage-dashboard 0.2.6 → 0.2.7
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 +25 -27
- 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,10 @@
|
|
|
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"
|
|
111
|
+
<div class="panel" id="sessionsPanel">
|
|
112
112
|
<h2>Sessions <span class="tot" id="sessSum"></span>
|
|
113
113
|
<span class="pill" id="sessRange"></span>
|
|
114
114
|
</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
115
|
<div id="sessionsList"><div class="empty">loading…</div></div>
|
|
124
116
|
</div>
|
|
125
117
|
|
|
@@ -127,8 +119,8 @@
|
|
|
127
119
|
|
|
128
120
|
<script>
|
|
129
121
|
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=[]
|
|
122
|
+
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}};
|
|
123
|
+
let active="today", modelCtx=[];
|
|
132
124
|
|
|
133
125
|
function fmtUsd(n){return "$"+(n==null?"–":Number(n).toFixed(2));}
|
|
134
126
|
function fmtTok(n){
|
|
@@ -138,6 +130,7 @@ function fmtTok(n){
|
|
|
138
130
|
if(n>=1e3)return (n/1e3).toFixed(1)+"k";
|
|
139
131
|
return String(n);
|
|
140
132
|
}
|
|
133
|
+
function fmtPct(n){return n==null?"–":n.toFixed(1)+"%";}
|
|
141
134
|
function esc(s){const d=document.createElement("div");d.textContent=s;return d.innerHTML;}
|
|
142
135
|
|
|
143
136
|
function renderModels(data){
|
|
@@ -173,8 +166,17 @@ function applyBudget(month){
|
|
|
173
166
|
}
|
|
174
167
|
|
|
175
168
|
async function loadSessions(){
|
|
176
|
-
|
|
177
|
-
|
|
169
|
+
// follow the top-level time filter (today/week/month/custom range)
|
|
170
|
+
let q="";
|
|
171
|
+
if(active==="today")q="period=today";
|
|
172
|
+
else if(active==="week")q="period=week";
|
|
173
|
+
else if(active==="month")q="period=month";
|
|
174
|
+
else if(active==="range"){
|
|
175
|
+
const f=$("fromDate").value,t=$("toDate").value;
|
|
176
|
+
if(!f||!t){return;}
|
|
177
|
+
q=`from=${f}&to=${t}`;
|
|
178
|
+
}else{return;}
|
|
179
|
+
const data=await(await fetch(`/api/sessions?${q}`)).json();
|
|
178
180
|
if(data.error)throw new Error(data.error);
|
|
179
181
|
renderSessions(data);
|
|
180
182
|
}
|
|
@@ -182,9 +184,9 @@ async function loadSessions(){
|
|
|
182
184
|
function renderSessions(data){
|
|
183
185
|
const list=data.sessions||[];
|
|
184
186
|
const total=data.total||0;
|
|
185
|
-
const sum=list.reduce((a,s)=>a+(s.cost||0),0);
|
|
187
|
+
const sum=data.totalCost!=null?data.totalCost:list.reduce((a,s)=>a+(s.cost||0),0);
|
|
186
188
|
$("sessSum").textContent=fmtUsd(sum);
|
|
187
|
-
$("sessRange").textContent=`${total} sessions
|
|
189
|
+
$("sessRange").textContent=`${total} sessions`;
|
|
188
190
|
if(!list.length){$("sessionsList").innerHTML='<div class="empty">no sessions in range</div>';return;}
|
|
189
191
|
$("sessionsList").innerHTML=list.map(s=>{
|
|
190
192
|
const label = s.hasCwd
|
|
@@ -206,6 +208,11 @@ function applyCards(data){
|
|
|
206
208
|
$("c2").textContent=fmtTok(tt);
|
|
207
209
|
$("c3").textContent=fmtTok(data.cacheReadTokens);
|
|
208
210
|
$("c4").textContent=fmtTok(data.cacheCreationTokens);
|
|
211
|
+
// cache hit rate = cache-read input / (cache-read input + non-cached input)
|
|
212
|
+
const hitIn=data.cacheReadTokens||0, plainIn=data.inputTokens||0;
|
|
213
|
+
const hitRate = (hitIn+plainIn)>0 ? (hitIn/(hitIn+plainIn))*100 : null;
|
|
214
|
+
$("c5").innerHTML=fmtPct(hitRate)+(hitRate!=null?"<small> cache</small>":"");
|
|
215
|
+
$("c5p").textContent=hitRate!=null?`${fmtTok(plainIn)} miss · ${fmtTok(hitIn)} hit`:"";
|
|
209
216
|
$("c1label").textContent=tabs[active].label;
|
|
210
217
|
$("modelSum").textContent=fmtUsd(total);
|
|
211
218
|
}
|
|
@@ -298,6 +305,7 @@ async function tick(){
|
|
|
298
305
|
applyBudget(month);
|
|
299
306
|
await loadModels();
|
|
300
307
|
await loadTrend();
|
|
308
|
+
await loadSessions();
|
|
301
309
|
dot.className="dot ok";st.textContent="live";
|
|
302
310
|
$("lastUpd").textContent="last updated "+new Date().toLocaleTimeString();
|
|
303
311
|
}catch(e){dot.className="dot err";st.textContent="error: "+e.message;}
|
|
@@ -310,28 +318,18 @@ document.querySelectorAll(".tab").forEach(t=>{
|
|
|
310
318
|
t.setAttribute("aria-selected","true");
|
|
311
319
|
active=t.dataset.tab;
|
|
312
320
|
$("rangeRow").style.display = active==="range"?"block":"none";
|
|
313
|
-
$("sessionsPanel").style.display = active==="sessions"?"block":"none";
|
|
314
321
|
if(active==="range"){
|
|
315
322
|
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
323
|
rangeApply();
|
|
317
|
-
}else if(active==="sessions"){
|
|
318
|
-
loadSessions();
|
|
319
324
|
}else{
|
|
320
325
|
loadModels();
|
|
321
326
|
}
|
|
327
|
+
loadSessions();
|
|
322
328
|
};
|
|
323
329
|
t.addEventListener("click",activate);
|
|
324
330
|
t.addEventListener("keydown",(e)=>{if(e.key==="Enter"||e.key===" "||e.key==="Spacebar"){e.preventDefault();activate();}});
|
|
325
331
|
});
|
|
326
332
|
$("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
333
|
|
|
336
334
|
tick();
|
|
337
335
|
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.7",
|
|
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",
|