@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 +41 -8
- package/lib/server.py +12 -16
- package/package.json +1 -1
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
|
|
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||
|
|
195
|
-
: `<span class="name muted" title="no workdir found for this ${esc(s.agent)} session">${
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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",
|