@caius_kong/ccusage-dashboard 0.2.13 → 0.2.15

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/lib/index.html CHANGED
@@ -55,7 +55,9 @@
55
55
  .range button{background:var(--panel-2);border:1px solid var(--border);color:var(--text);border-radius:6px;padding:5px 12px;cursor:pointer;font-size:12px}
56
56
  .range button:hover{border-color:var(--accent)}
57
57
  .range button.active{background:var(--accent);border-color:var(--accent);color:#0d1117;font-weight:600}
58
- .sessid{font-variant-numeric:tabular-nums;letter-spacing:.3px}
58
+ .sessid{font-variant-numeric:tabular-nums;letter-spacing:.3px;cursor:pointer;transition:background .15s,border-color .15s,color .15s}
59
+ .sessid:hover{background:var(--accent);border-color:var(--accent);color:#0d1117}
60
+ .sessid.copied{background:var(--green);border-color:var(--green);color:#0d1117}
59
61
  .budget-band{margin:2px 0 14px;background:var(--panel-2);border-radius:8px;padding:12px 16px;display:flex;flex-direction:column;gap:8px}
60
62
  .budget-band .bl{display:flex;justify-content:space-between;font-size:13px;color:var(--muted)}
61
63
  .budget-band .bl b{color:var(--text)}
@@ -132,7 +134,37 @@ function fmtTok(n){
132
134
  return String(n);
133
135
  }
134
136
  function fmtPct(n){return n==null?"–":n.toFixed(1)+"%";}
137
+ // cache hit rate (input side): read / (read + non-cached input). Shared by the
138
+ // c5 summary card and per-row hit spans.
139
+ function cacheHitStats(o){
140
+ const readIn=(o&&o.cacheReadTokens)||0, missIn=(o&&o.inputTokens)||0;
141
+ const rate=(readIn+missIn)>0?(readIn/(readIn+missIn))*100:null;
142
+ return {readIn, missIn, rate};
143
+ }
144
+ function cacheHitSpan(o){
145
+ const {readIn, missIn, rate}=cacheHitStats(o);
146
+ const tip=rate!=null?`cache hit rate: ${fmtPct(rate)} (read ${fmtTok(readIn)} / in ${fmtTok(missIn)})`:"no cache read data";
147
+ return `<span title="${tip}">hit ${fmtPct(rate)}</span>`;
148
+ }
135
149
  function esc(s){const d=document.createElement("div");d.textContent=s;return d.innerHTML;}
150
+ async function copySessionId(id, ev){
151
+ ev.stopPropagation();
152
+ const el=ev.currentTarget; // capture before any await; currentTarget is nulled after dispatch
153
+ try{
154
+ await navigator.clipboard.writeText(id);
155
+ }catch(e){
156
+ // fallback for non-secure contexts / older browsers
157
+ const ta=document.createElement("textarea");
158
+ ta.value=id;ta.style.position="fixed";ta.style.opacity="0";
159
+ document.body.appendChild(ta);ta.select();
160
+ try{document.execCommand("copy");}catch(_){}
161
+ document.body.removeChild(ta);
162
+ }
163
+ el.classList.add("copied");
164
+ const old=el.textContent;
165
+ el.textContent="✓ copied";
166
+ setTimeout(()=>{el.textContent=old;el.classList.remove("copied");},1200);
167
+ }
136
168
 
137
169
  function renderModels(data){
138
170
  const total=data.totalCost||0;
@@ -145,7 +177,7 @@ function renderModels(data){
145
177
  const badge=m2?`<span class="pill">${esc(m2[1])}</span>`:"";
146
178
  const mn=m2?esc(m2[2]||m2[1]):esc(m.name);
147
179
  return `<div class="row"><div class="row-top"><span class="name">${mn}${badge ? " " + badge : ""}</span><span class="cost">${fmtUsd(m.cost)} · ${pct.toFixed(1)}%</span></div>
148
- <div class="toks"><span title="input">in ${fmtTok(m.inputTokens)}</span><span title="output">out ${fmtTok(m.outputTokens)}</span><span title="cache read">read ${fmtTok(m.cacheReadTokens)}</span><span title="cache write">write ${fmtTok(m.cacheCreationTokens)}</span></div>
180
+ <div class="toks"><span title="input">in ${fmtTok(m.inputTokens)}</span><span title="output">out ${fmtTok(m.outputTokens)}</span><span title="cache read">read ${fmtTok(m.cacheReadTokens)}</span>${cacheHitSpan(m)}<span title="cache write">write ${fmtTok(m.cacheCreationTokens)}</span></div>
149
181
  <div class="bar"><div style="width:${Math.max(pct,1.2)}%"></div></div></div>`;
150
182
  }).join("");
151
183
  }
@@ -190,13 +222,15 @@ function renderSessions(data){
190
222
  $("sessRange").textContent=`${total} sessions`;
191
223
  if(!list.length){$("sessionsList").innerHTML='<div class="empty">no sessions in range</div>';return;}
192
224
  $("sessionsList").innerHTML=list.map(s=>{
225
+ const shortId=s.id.slice(0,8);
226
+ const idPill=`<span class="pill sessid" title="click to copy full session id" onclick="copySessionId('${s.id.replace(/[\\']/g,"\\$&")}',event)">${esc(shortId)}</span>`;
193
227
  const label = s.hasCwd
194
- ? `<span class="name" title="workdir: ${esc(s.cwd||s.projectKey)}">${esc(s.dirName||s.id.slice(0,8))} <span class="pill sessid">${esc(s.id.slice(0,8))}</span></span>`
195
- : `<span class="name muted" title="no workdir found for this ${esc(s.agent)} session">${esc(s.id.slice(0,8))}</span>`;
228
+ ? `<span class="name" title="workdir: ${esc(s.cwd||s.projectKey)}">${esc(s.dirName||shortId)} ${idPill}</span>`
229
+ : `<span class="name muted" title="no workdir found for this ${esc(s.agent)} session">${idPill}</span>`;
196
230
  const last=(s.lastActivity||"").slice(0,10);
197
231
  return `<div class="row">
198
232
  <div class="row-top"><span>${label} <span class="pill">${esc(s.agent)}</span></span><span class="cost">${fmtUsd(s.cost)}</span></div>
199
- <div class="toks">${last?`<span>${esc(last)}</span>`:""}<span title="input">in ${fmtTok(s.inputTokens)}</span><span title="output">out ${fmtTok(s.outputTokens)}</span><span title="cache read">read ${fmtTok(s.cacheReadTokens)}</span><span title="cache write">write ${fmtTok(s.cacheCreationTokens)}</span></div>
233
+ <div class="toks">${last?`<span>${esc(last)}</span>`:""}<span title="input">in ${fmtTok(s.inputTokens)}</span><span title="output">out ${fmtTok(s.outputTokens)}</span><span title="cache read">read ${fmtTok(s.cacheReadTokens)}</span>${cacheHitSpan(s)}<span title="cache write">write ${fmtTok(s.cacheCreationTokens)}</span></div>
200
234
  </div>`;
201
235
  }).join("");
202
236
  }
@@ -210,10 +244,9 @@ function applyCards(data){
210
244
  $("c3").textContent=fmtTok(data.cacheReadTokens);
211
245
  $("c4").textContent=fmtTok(data.cacheCreationTokens);
212
246
  // 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;
247
+ const {readIn, missIn, rate: hitRate}=cacheHitStats(data);
215
248
  $("c5").innerHTML=fmtPct(hitRate)+(hitRate!=null?"<small> cache</small>":"");
216
- $("c5p").textContent=hitRate!=null?`${fmtTok(plainIn)} miss · ${fmtTok(hitIn)} hit`:"";
249
+ $("c5p").textContent=hitRate!=null?`${fmtTok(missIn)} miss · ${fmtTok(readIn)} hit`:"";
217
250
  $("c1label").textContent=tabs[active].label;
218
251
  $("modelSum").textContent=fmtUsd(total);
219
252
  }
package/lib/server.py CHANGED
@@ -202,14 +202,13 @@ def sessions(since_days: int = 30, period: str = "", from_date: str = "", to_dat
202
202
  Filtering: pass `period` in ("today" | "week" | "month") OR an explicit
203
203
  date range via `from_date`/`to_date` (YYYY-MM-DD). `since_days` is kept for
204
204
  backward compatibility with the old 7d/30d/90d/all selector.
205
- """
206
- data = run_ccusage(
207
- ["session", "--by-agent", "--json", "--offline"],
208
- TTL["/api/sessions"],
209
- )
210
- rows = data.get("session") or []
211
205
 
212
- # ---- choose the cutoff window ----------------
206
+ The window is passed to ccusage as --since/--until so it filters by usage
207
+ EVENT date (entry.date) before summarising, exactly like the CLI session
208
+ report. Filtering rows ourselves by lastActivity would keep a whole session's
209
+ lifetime cost even when only a few events fall in the window, inflating the
210
+ total (observed: today $229 vs ccusage's true $0.09).
211
+ """
213
212
  today = date.today()
214
213
  lo = hi = None
215
214
  if from_date and to_date:
@@ -232,20 +231,17 @@ def sessions(since_days: int = 30, period: str = "", from_date: str = "", to_dat
232
231
  lo = today - timedelta(days=since_days)
233
232
  hi = today
234
233
 
234
+ ccusage_args = ["session", "--json", "--offline"]
235
+ if lo and hi:
236
+ ccusage_args += ["--since", lo.isoformat(), "--until", hi.isoformat()]
237
+ data = run_ccusage(ccusage_args, TTL["/api/sessions"])
238
+ rows = data.get("session") or []
239
+
235
240
  out = []
236
241
  for r in rows:
237
242
  meta = r.get("metadata") or {}
238
243
  sid = r.get("period")
239
244
  last_activity = meta.get("lastActivity") or ""
240
- # date filter by last activity against [lo, hi]
241
- if last_activity:
242
- try:
243
- act_date = date.fromisoformat(last_activity[:10])
244
- except ValueError:
245
- act_date = None
246
- if act_date is not None:
247
- if act_date < lo or act_date > hi:
248
- continue
249
245
  project_raw = meta.get("projectPath") or ""
250
246
  agent_name = r.get("agent", "?")
251
247
  cwd = ""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caius_kong/ccusage-dashboard",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
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",