@caius_kong/ccusage-dashboard 0.2.20 → 0.2.21
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 +28 -9
- package/lib/server.py +32 -19
- package/package.json +1 -1
package/lib/index.html
CHANGED
|
@@ -346,10 +346,9 @@ async function rangeApply(){
|
|
|
346
346
|
}
|
|
347
347
|
|
|
348
348
|
async function loadTrend(){
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
$("trendSum").textContent=fmtUsd((d.days||[]).reduce((a,x)=>a+(x.totalCost||0),0));
|
|
349
|
+
// Used by the resize redraw and first paint; a failed trend must not surface
|
|
350
|
+
// as an unhandled rejection (auxTick reports status via the tick() indicator).
|
|
351
|
+
try{renderTrend(await fetchTrend());}catch(e){}
|
|
353
352
|
}
|
|
354
353
|
|
|
355
354
|
function drawTrend(days){
|
|
@@ -415,12 +414,30 @@ async function fetchMonth(){
|
|
|
415
414
|
return d;
|
|
416
415
|
}
|
|
417
416
|
|
|
417
|
+
async function fetchTrend(){
|
|
418
|
+
const d=await(await fetch("/api/trend?days=30")).json();
|
|
419
|
+
if(d.error)throw new Error(d.error);
|
|
420
|
+
return d;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function renderTrend(d){
|
|
424
|
+
drawTrend(d.days||[]);
|
|
425
|
+
$("trendSum").textContent=fmtUsd((d.days||[]).reduce((a,x)=>a+(x.totalCost||0),0));
|
|
426
|
+
}
|
|
427
|
+
|
|
418
428
|
async function tick(){ // the active view + the sessions panel
|
|
419
429
|
const dot=$("dot"),st=$("status");
|
|
420
430
|
try{
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
431
|
+
// These two hit independent endpoints with independent caches and render
|
|
432
|
+
// into different panels, so run them together. Awaiting one before starting
|
|
433
|
+
// the other would make a cold tick cost `today + sessions` worth of scans
|
|
434
|
+
// (~13-24s each) — close enough to the 60s refresh window to risk the next
|
|
435
|
+
// tick arriving before this one finishes. The server's run gate is sized for
|
|
436
|
+
// exactly this small overlap.
|
|
437
|
+
await Promise.all([
|
|
438
|
+
active==="range"?rangeApply():loadModels(),
|
|
439
|
+
loadSessions(),
|
|
440
|
+
]);
|
|
424
441
|
dot.className="dot ok";st.textContent="live";
|
|
425
442
|
$("lastUpd").textContent="last updated "+new Date().toLocaleTimeString();
|
|
426
443
|
}catch(e){dot.className="dot err";st.textContent="error: "+e.message;}
|
|
@@ -428,8 +445,10 @@ async function tick(){ // the active view + the sessions panel
|
|
|
428
445
|
|
|
429
446
|
async function auxTick(){ // always-visible panels: budget + 30d trend
|
|
430
447
|
try{
|
|
431
|
-
|
|
432
|
-
await
|
|
448
|
+
// Independent endpoints again: fetch both, then render.
|
|
449
|
+
const [month,trend]=await Promise.all([fetchMonth(), fetchTrend()]);
|
|
450
|
+
applyBudget(month);
|
|
451
|
+
renderTrend(trend);
|
|
433
452
|
}catch(e){/* status is reported by tick() */}
|
|
434
453
|
}
|
|
435
454
|
|
package/lib/server.py
CHANGED
|
@@ -81,12 +81,9 @@ BUDGET = 300.0 # monthly cap in USD (override via --budget or CCUSAGE_BUDGET)
|
|
|
81
81
|
# the heavier reports cost ~12-16 CPU-seconds each and are rarely watched, so
|
|
82
82
|
# they re-scan a tenth as often. Inactive views are never polled at all.
|
|
83
83
|
#
|
|
84
|
-
# A value here is the refresh PERIOD, not a
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
# the refresh cadence independent of how long a scan happens to take — with a
|
|
88
|
-
# plain "now + ttl", a scan finishing `runtime` after the tick would keep the
|
|
89
|
-
# entry alive past the next tick and silently halve the refresh rate.
|
|
84
|
+
# A value here is the refresh PERIOD, not a length of time an entry is kept.
|
|
85
|
+
# run_ccusage expires entries slightly before their period elapses, so each new
|
|
86
|
+
# period triggers exactly one fresh scan regardless of how long a scan takes.
|
|
90
87
|
_REFRESH_INTERVAL = 60 # browser poll interval; must match lib/index.html
|
|
91
88
|
_SLOW_INTERVAL = 600 # cadence for the non-default views
|
|
92
89
|
TTL = {
|
|
@@ -100,9 +97,16 @@ TTL = {
|
|
|
100
97
|
|
|
101
98
|
# A transient failure is remembered only briefly: long enough that concurrent
|
|
102
99
|
# waiters share it instead of each retrying, short enough that the endpoint
|
|
103
|
-
# recovers quickly rather than staying
|
|
100
|
+
# recovers quickly rather than staying whole a full period.
|
|
104
101
|
_ERROR_TTL = 30
|
|
105
102
|
|
|
103
|
+
# Entries expire this many seconds before their period ends, so the next poll of
|
|
104
|
+
# the same cadence always triggers a fresh scan. Expiry is measured from the
|
|
105
|
+
# REQUEST, so it does not care when the scan started; the margin only has to
|
|
106
|
+
# cover the gap between a poll and the entry it replaces. Must stay well below
|
|
107
|
+
# the smallest period.
|
|
108
|
+
_EXPIRY_MARGIN = 5
|
|
109
|
+
|
|
106
110
|
# --- optional self-update check (purely user-triggered) ----------------------
|
|
107
111
|
# The dashboard is otherwise fully offline: it makes a network request ONLY when
|
|
108
112
|
# the user clicks the "check update" button in the UI. A short debounce stops
|
|
@@ -120,15 +124,22 @@ _last_check = None # (timestamp, result dict) — debounce + last outcome
|
|
|
120
124
|
def run_ccusage(args: list[str], ttl: float) -> dict:
|
|
121
125
|
"""Run ccusage and cache the JSON report under its argv for one `ttl` window.
|
|
122
126
|
|
|
123
|
-
``ttl`` is a refresh PERIOD
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
past the next tick and
|
|
127
|
+
``ttl`` here is a refresh PERIOD (the cadence the browser polls this report
|
|
128
|
+
at), and the entry is made to expire just before that period elapses — see
|
|
129
|
+
_EXPIRY_MARGIN. Expiry is measured from the REQUEST time (the tick), not from
|
|
130
|
+
when the scan happened to start, so queueing behind other scans cannot push
|
|
131
|
+
an entry past the next tick and silently double the cadence.
|
|
132
|
+
|
|
133
|
+
Two failure modes this avoids:
|
|
134
|
+
|
|
135
|
+
* Stamping the pre-run clock gave an entry a lifetime of `ttl - runtime`,
|
|
136
|
+
i.e. negative when a run outlasted its period, so the cache never hit and
|
|
137
|
+
every poll re-executed ccusage.
|
|
138
|
+
* Anchoring to the wall clock ("end of the current minute") expires an entry
|
|
139
|
+
on write whenever a run starts near a boundary.
|
|
128
140
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
entry stale and triggers one fresh scan, no matter how long the scan took.
|
|
141
|
+
Post-condition for any run shorter than `ttl - _EXPIRY_MARGIN`: the entry is
|
|
142
|
+
valid now, and stale by the next poll of the same cadence.
|
|
132
143
|
|
|
133
144
|
Requests for the same key are coalesced (single-flight): the first caller
|
|
134
145
|
runs ccusage and the rest wait for its result, so N concurrent misses spawn
|
|
@@ -147,11 +158,11 @@ def run_ccusage(args: list[str], ttl: float) -> dict:
|
|
|
147
158
|
if waiter is None:
|
|
148
159
|
waiter = threading.Event()
|
|
149
160
|
_inflight[key] = waiter
|
|
161
|
+
requested_at = now
|
|
150
162
|
break
|
|
151
163
|
waiter.wait() # someone else is already running this exact key
|
|
152
164
|
try:
|
|
153
165
|
with _run_gate:
|
|
154
|
-
started = time.time()
|
|
155
166
|
try:
|
|
156
167
|
proc = subprocess.run(
|
|
157
168
|
resolve_ccusage() + args,
|
|
@@ -164,9 +175,11 @@ def run_ccusage(args: list[str], ttl: float) -> dict:
|
|
|
164
175
|
data = {"error": f"{exc}"}
|
|
165
176
|
is_error = isinstance(data, dict) and bool(data.get("error"))
|
|
166
177
|
window = _ERROR_TTL if is_error else ttl
|
|
167
|
-
# Expire
|
|
168
|
-
#
|
|
169
|
-
|
|
178
|
+
# Expire just before this period ends, measured from the REQUEST, so the
|
|
179
|
+
# next poll of this cadence always finds it stale even if the scan was
|
|
180
|
+
# queued behind others. Never let the margin swallow a short window.
|
|
181
|
+
margin = min(_EXPIRY_MARGIN, window / 4)
|
|
182
|
+
expires_at = requested_at + window - margin
|
|
170
183
|
with _lock:
|
|
171
184
|
_cache[key] = (expires_at, data)
|
|
172
185
|
return data
|
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.21",
|
|
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",
|