@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 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, filterable 7d/30d/90d/all, sorted by cost |
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. Auto-refresh every 15s.
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" style="display:none">
112
- <h2>Sessions <span class="tot" id="sessSum"></span>
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},sessions:{label:"Sessions",len:0}};
131
- let active="today", modelCtx=[], sessDays=30;
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
- $("sessionsList").innerHTML='<div class="empty">loading sessions…</div>';
177
- const data=await(await fetch(`/api/sessions?days=${sessDays}`)).json();
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 · last ${sessDays===365?"year":sessDays+"d"}`;
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
- cutoff = date.today() - timedelta(days=since_days)
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") # session id in ccusage's session report
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 and act_date < cutoff:
221
- continue
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, # real workdir when resolved, else best-effort decode
253
- "dirName": dir_name, # real dir name when resolved, else last path segment
254
- "projectKey": project_raw, # raw ccusage projectPath
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
- return {"total": len(out), "sessions": out}
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.6",
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",