@caius_kong/ccusage-dashboard 0.2.12 → 0.2.14
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 +6 -5
- package/bin/ccusage-ui.js +10 -11
- package/lib/index.html +25 -3
- package/lib/server.py +12 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,16 +50,17 @@ for you to open.
|
|
|
50
50
|
npx @caius_kong/ccusage-dashboard --port 9000 # change port
|
|
51
51
|
npx @caius_kong/ccusage-dashboard --budget 500 # monthly budget cap (default $300)
|
|
52
52
|
npx @caius_kong/ccusage-dashboard --no-warm # skip background warm-up
|
|
53
|
-
npx @caius_kong/ccusage-dashboard --
|
|
54
|
-
npx @caius_kong/ccusage-dashboard --stop # stop
|
|
53
|
+
npx @caius_kong/ccusage-dashboard --foreground # run attached to this terminal (Ctrl+C stops it)
|
|
54
|
+
npx @caius_kong/ccusage-dashboard --stop # stop the background instance
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
Once started, the launcher prints the dashboard URL — open it in your browser
|
|
58
58
|
(no browser is auto-launched).
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
By default the dashboard runs **in the background**: after printing its URL the
|
|
61
|
+
launcher exits and the server keeps running, so you can close the terminal. Use
|
|
62
|
+
`--stop` to shut it down; running it again reuses the already-running instance.
|
|
63
|
+
`--foreground` keeps it attached to the terminal instead (Ctrl+C to stop).
|
|
63
64
|
|
|
64
65
|
## What it shows
|
|
65
66
|
|
package/bin/ccusage-ui.js
CHANGED
|
@@ -7,15 +7,13 @@
|
|
|
7
7
|
* ccusage for all numbers, so the dashboard always matches ccusage.
|
|
8
8
|
*
|
|
9
9
|
* Modes:
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* already up on the port, it reuses it instead of
|
|
16
|
-
* starting a second one.
|
|
10
|
+
* default — start the server detached in the background, print
|
|
11
|
+
* the URL, and exit immediately. The server keeps running
|
|
12
|
+
* after this terminal closes. If a server is already up on
|
|
13
|
+
* the port, it reuses it instead of starting a second one.
|
|
14
|
+
* --foreground — run the server attached to this terminal (Ctrl+C stops it).
|
|
17
15
|
*
|
|
18
|
-
* Stop a
|
|
16
|
+
* Stop a background instance with: npx @caius_kong/ccusage-dashboard --stop
|
|
19
17
|
*/
|
|
20
18
|
'use strict';
|
|
21
19
|
|
|
@@ -63,11 +61,12 @@ function argValue(name, fallback) {
|
|
|
63
61
|
const host = argValue('--host', '127.0.0.1');
|
|
64
62
|
const port = parseInt(argValue('--port', '8799'), 10);
|
|
65
63
|
const url = `http://${host}:${port}`;
|
|
66
|
-
const wantDaemon = rawArgs.includes('--
|
|
64
|
+
const wantDaemon = !rawArgs.includes('--foreground'); // background by default
|
|
67
65
|
const wantStop = rawArgs.includes('--stop');
|
|
66
|
+
const wantForeground = rawArgs.includes('--foreground');
|
|
68
67
|
|
|
69
68
|
// Filter launcher-only flags before forwarding to server.py.
|
|
70
|
-
const serverArgs = rawArgs.filter((a) => !['--daemon', '--stop'].includes(a));
|
|
69
|
+
const serverArgs = rawArgs.filter((a) => !['--daemon', '--stop', '--foreground'].includes(a));
|
|
71
70
|
|
|
72
71
|
// --- daemon pid file (per host:port) ----------------------------------------
|
|
73
72
|
function pidFile() {
|
|
@@ -104,7 +103,7 @@ function printBanner() {
|
|
|
104
103
|
console.log(` Open it manually: ${url}`);
|
|
105
104
|
console.log(line);
|
|
106
105
|
if (wantDaemon) {
|
|
107
|
-
console.log(` (background
|
|
106
|
+
console.log(` (background — stop with: npx @caius_kong/ccusage-dashboard --stop)`);
|
|
108
107
|
console.log('');
|
|
109
108
|
}
|
|
110
109
|
}
|
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)}
|
|
@@ -133,6 +135,24 @@ function fmtTok(n){
|
|
|
133
135
|
}
|
|
134
136
|
function fmtPct(n){return n==null?"–":n.toFixed(1)+"%";}
|
|
135
137
|
function esc(s){const d=document.createElement("div");d.textContent=s;return d.innerHTML;}
|
|
138
|
+
async function copySessionId(id, ev){
|
|
139
|
+
ev.stopPropagation();
|
|
140
|
+
const el=ev.currentTarget; // capture before any await; currentTarget is nulled after dispatch
|
|
141
|
+
try{
|
|
142
|
+
await navigator.clipboard.writeText(id);
|
|
143
|
+
}catch(e){
|
|
144
|
+
// fallback for non-secure contexts / older browsers
|
|
145
|
+
const ta=document.createElement("textarea");
|
|
146
|
+
ta.value=id;ta.style.position="fixed";ta.style.opacity="0";
|
|
147
|
+
document.body.appendChild(ta);ta.select();
|
|
148
|
+
try{document.execCommand("copy");}catch(_){}
|
|
149
|
+
document.body.removeChild(ta);
|
|
150
|
+
}
|
|
151
|
+
el.classList.add("copied");
|
|
152
|
+
const old=el.textContent;
|
|
153
|
+
el.textContent="✓ copied";
|
|
154
|
+
setTimeout(()=>{el.textContent=old;el.classList.remove("copied");},1200);
|
|
155
|
+
}
|
|
136
156
|
|
|
137
157
|
function renderModels(data){
|
|
138
158
|
const total=data.totalCost||0;
|
|
@@ -190,9 +210,11 @@ function renderSessions(data){
|
|
|
190
210
|
$("sessRange").textContent=`${total} sessions`;
|
|
191
211
|
if(!list.length){$("sessionsList").innerHTML='<div class="empty">no sessions in range</div>';return;}
|
|
192
212
|
$("sessionsList").innerHTML=list.map(s=>{
|
|
213
|
+
const shortId=s.id.slice(0,8);
|
|
214
|
+
const idPill=`<span class="pill sessid" title="click to copy full session id" onclick="copySessionId('${s.id.replace(/[\\']/g,"\\$&")}',event)">${esc(shortId)}</span>`;
|
|
193
215
|
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">${
|
|
216
|
+
? `<span class="name" title="workdir: ${esc(s.cwd||s.projectKey)}">${esc(s.dirName||shortId)} ${idPill}</span>`
|
|
217
|
+
: `<span class="name muted" title="no workdir found for this ${esc(s.agent)} session">${idPill}</span>`;
|
|
196
218
|
const last=(s.lastActivity||"").slice(0,10);
|
|
197
219
|
return `<div class="row">
|
|
198
220
|
<div class="row-top"><span>${label} <span class="pill">${esc(s.agent)}</span></span><span class="cost">${fmtUsd(s.cost)}</span></div>
|
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.14",
|
|
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",
|