@caius_kong/ccusage-dashboard 0.1.1 → 0.2.1
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 +57 -2
- package/lib/server.py +81 -1
- package/package.json +1 -1
package/lib/index.html
CHANGED
|
@@ -53,6 +53,8 @@
|
|
|
53
53
|
.range input{background:var(--panel-2);border:1px solid var(--border);color:var(--text);border-radius:6px;padding:5px 8px;font-size:12px;font-family:inherit}
|
|
54
54
|
.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}
|
|
55
55
|
.range button:hover{border-color:var(--accent)}
|
|
56
|
+
.range button.active{background:var(--accent);border-color:var(--accent);color:#0d1117;font-weight:600}
|
|
57
|
+
.sessid{font-variant-numeric:tabular-nums;letter-spacing:.3px}
|
|
56
58
|
.budget-band{margin:2px 0 14px;background:var(--panel-2);border-radius:8px;padding:12px 16px;display:flex;flex-direction:column;gap:8px}
|
|
57
59
|
.budget-band .bl{display:flex;justify-content:space-between;font-size:13px;color:var(--muted)}
|
|
58
60
|
.budget-band .bl b{color:var(--text)}
|
|
@@ -74,6 +76,7 @@
|
|
|
74
76
|
<div class="tab" data-tab="week">This Week</div>
|
|
75
77
|
<div class="tab" data-tab="month">This Month</div>
|
|
76
78
|
<div class="tab" data-tab="range">Custom Range</div>
|
|
79
|
+
<div class="tab" data-tab="sessions">Sessions</div>
|
|
77
80
|
<div class="tab-budget">
|
|
78
81
|
<span class="budget-pill" id="budgetPill">budget …</span>
|
|
79
82
|
</div>
|
|
@@ -104,12 +107,27 @@
|
|
|
104
107
|
|
|
105
108
|
<div class="panel"><h2>Models <span class="tot" id="modelSum"></span></h2><div id="modelList"><div class="empty">loading…</div></div></div>
|
|
106
109
|
|
|
110
|
+
<div class="panel" id="sessionsPanel" style="display:none">
|
|
111
|
+
<h2>Sessions <span class="tot" id="sessSum"></span>
|
|
112
|
+
<span class="pill" id="sessRange"></span>
|
|
113
|
+
</h2>
|
|
114
|
+
<div class="range" style="margin-bottom:12px">
|
|
115
|
+
<span class="pill">last</span>
|
|
116
|
+
<button data-sdays="7" class="sday">7d</button>
|
|
117
|
+
<button data-sdays="30" class="sday active">30d</button>
|
|
118
|
+
<button data-sdays="90" class="sday">90d</button>
|
|
119
|
+
<button data-sdays="365" class="sday">all</button>
|
|
120
|
+
<span class="pill" style="margin-left:auto" id="sessHint"></span>
|
|
121
|
+
</div>
|
|
122
|
+
<div id="sessionsList"><div class="empty">loading…</div></div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
107
125
|
<div class="hint">auto-refresh: today 15s · others 60s · costs in USD · <span id="lastUpd"></span></div>
|
|
108
126
|
|
|
109
127
|
<script>
|
|
110
128
|
const $=(id)=>document.getElementById(id);
|
|
111
|
-
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}};
|
|
112
|
-
let active="today", modelCtx=[];
|
|
129
|
+
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}};
|
|
130
|
+
let active="today", modelCtx=[], sessDays=30;
|
|
113
131
|
|
|
114
132
|
function fmtUsd(n){return "$"+(n==null?"–":Number(n).toFixed(2));}
|
|
115
133
|
function fmtTok(n){
|
|
@@ -152,6 +170,32 @@ function applyBudget(month){
|
|
|
152
170
|
const p=$("budgetPill");p.className="budget-pill "+cls;p.textContent=txt;
|
|
153
171
|
}
|
|
154
172
|
|
|
173
|
+
async function loadSessions(){
|
|
174
|
+
$("sessionsList").innerHTML='<div class="empty">loading sessions…</div>';
|
|
175
|
+
const data=await(await fetch(`/api/sessions?days=${sessDays}`)).json();
|
|
176
|
+
if(data.error)throw new Error(data.error);
|
|
177
|
+
renderSessions(data);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function renderSessions(data){
|
|
181
|
+
const list=data.sessions||[];
|
|
182
|
+
const total=data.total||0;
|
|
183
|
+
const sum=list.reduce((a,s)=>a+(s.cost||0),0);
|
|
184
|
+
$("sessSum").textContent=fmtUsd(sum);
|
|
185
|
+
$("sessRange").textContent=`${total} sessions · last ${sessDays===365?"year":sessDays+"d"}`;
|
|
186
|
+
if(!list.length){$("sessionsList").innerHTML='<div class="empty">no sessions in range</div>';return;}
|
|
187
|
+
$("sessionsList").innerHTML=list.map(s=>{
|
|
188
|
+
const label = s.hasCwd
|
|
189
|
+
? `<span class="name">${esc(s.dirName||s.cwd)} <span class="pill sessid">${esc(s.id.slice(0,8))}</span></span>`
|
|
190
|
+
: `<span class="name muted">${esc(s.id.slice(0,8))}</span>`;
|
|
191
|
+
const last=(s.lastActivity||"").slice(0,10);
|
|
192
|
+
return `<div class="row">
|
|
193
|
+
<div class="row-top"><span>${label} <span class="pill">${esc(s.agent)}</span></span><span class="cost">${fmtUsd(s.cost)}</span></div>
|
|
194
|
+
<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>
|
|
195
|
+
</div>`;
|
|
196
|
+
}).join("");
|
|
197
|
+
}
|
|
198
|
+
|
|
155
199
|
function applyCards(data){
|
|
156
200
|
const total=data.totalCost||0;
|
|
157
201
|
$("c1").innerHTML=fmtUsd(total)+(total?"<small> USD</small>":"");
|
|
@@ -263,15 +307,26 @@ document.querySelectorAll(".tab").forEach(t=>{
|
|
|
263
307
|
t.classList.add("active");
|
|
264
308
|
active=t.dataset.tab;
|
|
265
309
|
$("rangeRow").style.display = active==="range"?"block":"none";
|
|
310
|
+
$("sessionsPanel").style.display = active==="sessions"?"block":"none";
|
|
266
311
|
if(active==="range"){
|
|
267
312
|
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);}
|
|
268
313
|
rangeApply();
|
|
314
|
+
}else if(active==="sessions"){
|
|
315
|
+
loadSessions();
|
|
269
316
|
}else{
|
|
270
317
|
loadModels();
|
|
271
318
|
}
|
|
272
319
|
});
|
|
273
320
|
});
|
|
274
321
|
$("rangeGo").addEventListener("click",rangeApply);
|
|
322
|
+
document.querySelectorAll(".sday").forEach(b=>{
|
|
323
|
+
b.addEventListener("click",()=>{
|
|
324
|
+
document.querySelectorAll(".sday").forEach(x=>x.classList.remove("active"));
|
|
325
|
+
b.classList.add("active");
|
|
326
|
+
sessDays=parseInt(b.dataset.sdays,10);
|
|
327
|
+
loadSessions();
|
|
328
|
+
});
|
|
329
|
+
});
|
|
275
330
|
|
|
276
331
|
tick();
|
|
277
332
|
setInterval(tick,15000);
|
package/lib/server.py
CHANGED
|
@@ -94,7 +94,7 @@ def _node() -> str:
|
|
|
94
94
|
return n
|
|
95
95
|
|
|
96
96
|
BUDGET = 300.0 # monthly cap in USD (override via --budget or CCUSAGE_BUDGET)
|
|
97
|
-
TTL = {"/api/today": 15, "/api/week": 60, "/api/month": 60, "/api/range": 120, "/api/trend": 120}
|
|
97
|
+
TTL = {"/api/today": 15, "/api/week": 60, "/api/month": 60, "/api/range": 120, "/api/trend": 120, "/api/sessions": 60}
|
|
98
98
|
|
|
99
99
|
|
|
100
100
|
def run_ccusage(args: list[str], ttl: float) -> dict:
|
|
@@ -193,6 +193,81 @@ def trend(days: int = 30) -> dict:
|
|
|
193
193
|
return {"start": start.isoformat(), "end": end.isoformat(), "days": out}
|
|
194
194
|
|
|
195
195
|
|
|
196
|
+
def sessions(since_days: int = 30) -> dict:
|
|
197
|
+
"""Return ccusage's session report, with each session's cwd (projectPath) when
|
|
198
|
+
ccusage provides it. Data comes 100% from ccusage — we only re-shape it.
|
|
199
|
+
"""
|
|
200
|
+
data = run_ccusage(
|
|
201
|
+
["session", "--by-agent", "--json", "--offline"],
|
|
202
|
+
TTL["/api/sessions"],
|
|
203
|
+
)
|
|
204
|
+
rows = data.get("session") or []
|
|
205
|
+
|
|
206
|
+
cutoff = date.today() - timedelta(days=since_days)
|
|
207
|
+
out = []
|
|
208
|
+
for r in rows:
|
|
209
|
+
meta = r.get("metadata") or {}
|
|
210
|
+
sid = r.get("period") # session id in ccusage's session report
|
|
211
|
+
last_activity = meta.get("lastActivity") or ""
|
|
212
|
+
# date filter by last activity
|
|
213
|
+
if last_activity:
|
|
214
|
+
try:
|
|
215
|
+
act_date = date.fromisoformat(last_activity[:10])
|
|
216
|
+
except ValueError:
|
|
217
|
+
act_date = None
|
|
218
|
+
if act_date is not None and act_date < cutoff:
|
|
219
|
+
continue
|
|
220
|
+
project_raw = meta.get("projectPath") or ""
|
|
221
|
+
cwd = _decode_cwd(project_raw)
|
|
222
|
+
dir_name = _basename(project_raw)
|
|
223
|
+
out.append(
|
|
224
|
+
{
|
|
225
|
+
"id": sid or "",
|
|
226
|
+
"agent": r.get("agent", "?"),
|
|
227
|
+
"cost": round(r.get("totalCost", 0) or 0, 4),
|
|
228
|
+
"inputTokens": r.get("inputTokens", 0),
|
|
229
|
+
"outputTokens": r.get("outputTokens", 0),
|
|
230
|
+
"cacheReadTokens": r.get("cacheReadTokens", 0),
|
|
231
|
+
"cacheCreationTokens": r.get("cacheCreationTokens", 0),
|
|
232
|
+
"lastActivity": last_activity,
|
|
233
|
+
"cwd": cwd, # absolute-ish path, empty if ccusage didn't provide it
|
|
234
|
+
"dirName": dir_name, # last path segment; empty if ccusage didn't provide
|
|
235
|
+
"hasCwd": bool(project_raw),
|
|
236
|
+
}
|
|
237
|
+
)
|
|
238
|
+
out.sort(key=lambda s: s["cost"], reverse=True)
|
|
239
|
+
return {"total": len(out), "sessions": out}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _decode_cwd(raw: str) -> str:
|
|
243
|
+
"""Best-effort decode of ccusage's projectPath into a readable path.
|
|
244
|
+
|
|
245
|
+
pi's projectPath is a Claude-Code-style encoded dir name where '/' became '-'.
|
|
246
|
+
Example: '--Users-caius-kong-Documents-...-AutoTrans--' -> /Users/caius_kong/.../AutoTrans
|
|
247
|
+
"""
|
|
248
|
+
if not raw:
|
|
249
|
+
return ""
|
|
250
|
+
return raw.replace("-", "/")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _basename(raw: str) -> str:
|
|
254
|
+
"""Last path segment of projectPath as a readable dir name.
|
|
255
|
+
|
|
256
|
+
Works even for encoded paths (the trailing segment is unaffected by the
|
|
257
|
+
dash encoding except when the real dir name itself contains '-'s).
|
|
258
|
+
"""
|
|
259
|
+
if not raw:
|
|
260
|
+
return ""
|
|
261
|
+
cleaned = raw.strip("/-\\")
|
|
262
|
+
if not cleaned:
|
|
263
|
+
return ""
|
|
264
|
+
parts = cleaned.replace("-", "/").split("/")
|
|
265
|
+
last = parts[-1] if parts else cleaned
|
|
266
|
+
# If the real dir name itself got dash-encoded (e.g. 'my-proj' -> 'my_proj'),
|
|
267
|
+
# we can't perfectly recover it; fall back to a reasonable label.
|
|
268
|
+
return last or cleaned
|
|
269
|
+
|
|
270
|
+
|
|
196
271
|
class Handler(BaseHTTPRequestHandler):
|
|
197
272
|
server_version = "ccusage-ui/0.2"
|
|
198
273
|
|
|
@@ -243,6 +318,10 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
243
318
|
days = max(1, min(366, int(params.get("days", "30"))))
|
|
244
319
|
self._json(trend(days))
|
|
245
320
|
return
|
|
321
|
+
elif path == "/api/sessions":
|
|
322
|
+
days = max(1, min(366, int(params.get("days", "30"))))
|
|
323
|
+
self._json(sessions(days))
|
|
324
|
+
return
|
|
246
325
|
else:
|
|
247
326
|
self._send(404, b"not found", "text/plain")
|
|
248
327
|
return
|
|
@@ -276,6 +355,7 @@ def main() -> None:
|
|
|
276
355
|
threading.Thread(target=warm_one, args=(lambda: run_ccusage(["daily", "--last", "1", "--json", "--offline"], TTL["/api/today"]),)),
|
|
277
356
|
threading.Thread(target=warm_one, args=(lambda: run_ccusage(["weekly", "--last", "1", "--json", "--offline"], TTL["/api/week"]),)),
|
|
278
357
|
threading.Thread(target=warm_one, args=(lambda: run_ccusage(["monthly", "--last", "1", "--json", "--offline"], TTL["/api/month"]),)),
|
|
358
|
+
threading.Thread(target=warm_one, args=(lambda: run_ccusage(["session", "--by-agent", "--json", "--offline"], TTL["/api/sessions"]),)),
|
|
279
359
|
threading.Thread(target=warm_one, args=(lambda: trend(30),)),
|
|
280
360
|
]
|
|
281
361
|
for t in threads:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@caius_kong/ccusage-dashboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
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",
|