Use Anthropic's real usage API for the weekly bar (with local estimate as fallback)#149
Open
changsunglim wants to merge 19 commits into
Open
Use Anthropic's real usage API for the weekly bar (with local estimate as fallback)#149changsunglim wants to merge 19 commits into
changsunglim wants to merge 19 commits into
Conversation
…view - limits.py: plan auto-detect via keychain OAuth + 24h cache; per-plan session-5h + weekly budgets for pro/max_5x/max_20x; rolling 7d split by opus/sonnet/haiku - Dashboard top banner: weekly stacked bar (Opus/Sonnet/Haiku) with per-model breakdown line; ThreadingHTTPServer so /api/data and /api/limits don't block each other - Session Health card: heuristic "start new session" warning when context >150k, cache-hit <40%, avg >60k tok/turn, or age >3h - Token Efficiency: weighted grader (Cache Hit 40% + Reuse Ratio 25% + Cost Efficiency 20% + Output Discipline 15%) with info tooltip; replaces "By Model" pie; respects date-range filter - Per-message live view: /api/session/<id> groups turns by user prompt; click row → polls every 5s; filters skill/system-reminder noise so list shows only real human turns - Session titles from first JSONL user prompt (replaces opaque project IDs) - Local TZ auto-detect via datetime().astimezone().utcoffset() - Hourly chart: average divides by calendar days in range (not active-only days); SQL pre-shifts so JS no longer double-shifts - Auto-rescan on every /api/data fetch - Bug fix: cutoff → start/end ReferenceError that killed all charts
- Fork banner with link to upstream - Fork features section: limits widget, plan budget table, session health heuristic, efficiency grader formula, live view, new endpoints - Add limits.py to files table
Anthropic's weekly limit is anchored per-user to the first message of the cycle and resets exactly 7 days later. The old rolling-7d sum kept counting tokens from before Anthropic's reset, so the bar disagreed with Settings → Usage immediately after a fresh reset. - Persist anchor at ~/.claude/usage-weekly-anchor.json - Auto-advance: when anchor + 7d <= now, jump to the first turn in the new window (or now if no turns yet) - Manual override via POST /api/weekly/sync-reset, exposed as a "Sync reset to now" button on the weekly card - Replace "7d rolling" footer with live countdown + anchor source - Tests covering first-run, manual persistence, auto-advance, reset_at = anchor + 7d
Extends the anchored-reset mechanism to the 5h session window and
adds a manual edit form so users can correct anchor + percent when
they miss the sync moment.
- New SESSION_ANCHOR_PATH at ~/.claude/usage-session-anchor.json
- set_session_anchor() / clear_session_anchor() helpers; expired
manual session anchors are cleared on read
- Both weekly and session anchor files now persist baseline_used,
so compute_weekly / compute_session_5h return
total = baseline + sum(turns since anchor). Auto-advance resets
baseline to 0.
- POST /api/weekly/sync-reset and /api/session/sync-reset accept
JSON body { anchor_at, percent | baseline_used, clear }; percent
is converted to baseline tokens against the detected plan cap.
- UI: weekly + new session limit cards each have Sync / Edit /
(session: Clear) buttons. Edit reveals datetime-local + percent
inputs.
- Tests cover baseline_used round-trip.
The edit form retained the previously-saved anchor on reopen, so a manual percent entered with a stale anchor (e.g. midnight) got added to all turns since that anchor — producing a percent that did not match what the user typed. - On every Edit open, reset anchor input to current time and clear percent input, so typing '7%' makes the bar show 7% immediately (anchor = now → no tokens accumulated since) - Enter saves, Escape cancels in both inputs - Auto-focus percent field on open - Clarify label: '% shown in Claude Settings right NOW'
…future-anchor bug - Rename 'Session · 5h' → 'Current Session · 5h' - Session Edit form now shows 'time remaining to reset' input (h:mm) instead of a raw datetime field. Anchor is auto-calculated as now - (5h - remaining) and shown read-only. Enter=save, Esc=cancel. - Server-side: clamp anchor_at <= now on save. A future anchor (caused by clock skew or sub-second delay) made delta=0 permanently so the bar never advanced past baseline. - Add disclaimer note under session bar: auto-count uses a community-derived cap estimate; use Edit to sync with claude /usage.
…0m–7d) The 5h session window relied on Anthropic's undocumented cost-weighted formula and could not be reproduced from local JSONL. Removed the entire widget end-to-end (UI, API route, compute fn, anchor helpers, plan budget field) rather than keep inaccurate state visible. Replaced with a single user-configurable usage window: - Presets: 30m, 1h, 3h, 6h, 12h, 1d, 3d, 7d - Custom: 30–10080 minutes - Cap scales proportionally from the 7d plan budget - Edit form persists window_seconds alongside anchor/baseline/save_at - Title and anchor source line show active window length Also wires the save_at fix from the prior commit through window changes: delta accumulates from save_at (or anchor_at if absent) so manually setting "32%" no longer double-counts turns between anchor and save. Backend: - PLAN_BUDGETS drops session_5h_tokens - SESSION_ANCHOR_PATH, SESSION_WINDOW removed - compute_session_5h, set/clear/load/save_session_anchor removed - clear_weekly_anchor added (replaces session-only clear path) - get_limits drops session_5h block, returns full_cap + window_seconds - MIN/MAX_WINDOW_SECONDS bounds enforced in API + persistence
Replaces the weekly-card window picker (reverted) with a dashboard-wide
custom range so token efficiency, stats, charts, and tables all reflect
the chosen period — not just the weekly bar.
UI:
- New "Custom…" button in the global range row
- Input panel: number + unit (minutes/hours/days), Apply button
- Min 1, max 525,600 minutes (1 year)
- Panel restored from URL on first load
Filter semantics:
- Range encoded as `custom:<minutes>` in URL/state
- getRangeBounds returns startMs/endMs for minute precision
- Sessions filtered by last_iso (new field, full UTC ISO) when startMs set
- Sub-day ranges (<24h) recompute by-model totals from sessions, since
daily_by_model is day-bucketed and would otherwise collapse to today's
full-day total. Caveat noted in UI: precision = session-level.
- Daily/hourly charts still day-bucketed (no per-turn data in client);
chart titles use rangeLabel(selectedRange) for the custom label
Reverted from prior commit:
- lc-weekly-window-preset / lc-weekly-window-custom UI
- window_seconds field in weekly anchor JSON
- cap scaling in get_limits
- MIN/MAX_WINDOW_SECONDS, WEEKLY_WINDOW_SECONDS constants
- window_seconds parameter on set_weekly_anchor + POST handler
Kept from prior commit:
- Session 5h widget removal (still gone end-to-end)
- save_at fix preventing baseline double-count
Brings in: anchored weekly window with sync/edit, session 5h widget removal, save_at fix (no baseline double-count), custom global range filter (minutes/hours/days).
…locks pass set_weekly_anchor no longer auto-sets save_at=real_now when baseline_used>0 (broke tests that exercise compute with a fake 'now' parameter — save_at fell after fake_now+1h so subsequent turns were excluded from delta). API handler now explicitly passes save_at=now when baseline>0, preserving the no-double-count behavior for real users.
Weekly all-models bar and per-model breakdown summed every model's tokens 1:1, undercounting Opus-heavy weeks and forcing recurring manual edits. Apply per-model weights (Opus ~5x, Sonnet 1x, Haiku ~0.25x) anchored to the Sonnet-equivalent plan caps; weight cache_creation 1.25x input. Add accuracy disclaimer under the weekly card. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
compute_efficiency_warning's turn query didn't SELECT model, so the
per-model _billable() lookup raised KeyError ("No item with that key"),
crashing /api/limits and blanking the whole limits widget. Add model
to the SELECT.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
KeepAlive-managed launchd jobs restart the server on crash/login; the auto-opened browser tab would spam on every restart. --no-browser runs headless. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The weekly bar was computed from local JSONL with cost-weighted token heuristics, drifting badly from Claude's actual numbers (showed 100% when Settings → Usage showed 58%). Anthropic exposes the ground-truth figures at GET /api/oauth/usage — the same source `claude /usage` reads — so use it directly and keep the local estimate only as an offline fallback. limits.py: - fetch_official_usage(): 60s process-cached, lock-guarded, serves the last good payload if a refresh transiently fails. Parses five_hour / seven_day / seven_day_sonnet utilization + reset times, with a fallback to the typed `limits[]` array on schema drift. - get_limits(): official seven_day percent + reset_at override the local estimate when available; local value retained as `local_percent`. weekly_all.source flags official_api / official_api_stale / local_estimate. - _ssl_context(): python.org/pyenv builds don't trust the macOS keychain CA store, so every HTTPS call was silently dying with CERTIFICATE_VERIFY_FAILED — which is why plan auto-detect never worked. Use certifi's bundle (fall back to stdlib default); verification stays on. - plan detection now reads organization.rate_limit_tier (resolves default_claude_max_5x), so the hardcoded plan override is unnecessary. dashboard.py: - Official mode: solid fill at the real percent, "Live from Anthropic" badge with 5h + Sonnet-7d, hides the now-obsolete Sync/Edit/Clear manual controls and drift disclaimer. Manual sync survives as offline fallback. - Bar fill uses a neutral purple (was green, which collided with haiku's swatch color and read as "haiku usage"). - Local activity split sorted by token volume desc and relabeled, so a small background model (haiku from memory/summaries) never shows alone or on top; cap-relative percents dropped in official mode. - no-store on the HTML response: JS is inlined, so the browser was serving stale pages after every code change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ocked" The dashboard is a ThreadingHTTPServer that re-scans (writes) the DB on every /api/data and /api/session poll. With the default rollback journal, overlapping reads/writes raised sqlite3.OperationalError: database is locked, which was swallowed by a bare except — so the page loaded but silently stopped updating. - scanner.get_db: enable WAL (readers run during a write) + busy_timeout (contenders wait instead of erroring) + connect timeout. - dashboard + limits read connections: busy_timeout so reads queue rather than throw under write contention. - _safe_rescan(): a process-wide lock serializes incremental scans; concurrent pollers skip a redundant scan instead of colliding. - /api/rescan (delete + full rebuild) holds the lock blocking so it can never race an in-flight scan or reader. - Bump ThreadingHTTPServer listen backlog (default 5) to 128 so a burst of concurrent fetches on page load doesn't get connection-refused. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`HTML_TEMPLATE` is a Python raw string (r\"\"\"). `\\'` in a raw string emits two literal backslashes, so the single-quoted JS string `'...Anthropic\\'s...'` was parsed as: escaped-backslash + quote-end + `s billed...` → SyntaxError → entire script block dead → page stuck on Loading... Fix: single `\'` in the raw string → JS `\'` = escaped apostrophe → valid. Also adds a background startup scan in `serve()` so the DB is warm before the first browser request arrives (cold launchd boot no longer serves an empty dashboard). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The live weekly/session bars were computed from local JSONL using cost-weighted token heuristics. Those drift badly from what Claude actually reports — on my account the weekly bar showed 100% while Settings → Usage showed 58%.
Anthropic exposes the ground-truth numbers at
GET https://api.anthropic.com/api/oauth/usage— the same sourceclaude /usagereads. This PR reads that endpoint directly and uses it as the primary source, keeping the existing local estimate as an offline fallback (no token / expired token / network error → old behavior, including the manual sync controls).Changes
limits.pyfetch_official_usage()— 60s process-cached, lock-guarded, serves the last-good payload if a refresh transiently fails. Parsesfive_hour/seven_day/seven_day_sonnetutilization + reset times, with a fallback to the typedlimits[]array if the schema drifts.get_limits()— officialseven_daypercent +reset_atoverride the estimate when available; the local value is retained aslocal_percent.weekly_all.sourceflagsofficial_api/official_api_stale/local_estimate._ssl_context()— python.org / pyenv builds don't trust the macOS keychain CA store, so every HTTPS call was silently failing withCERTIFICATE_VERIFY_FAILED(this is why API-based plan detection never worked). Usescertifi's bundle, falls back to the stdlib default. Verification is never disabled.organization.rate_limit_tier, sodefault_claude_max_5xresolves correctly without a manual override.dashboard.pyCache-Control: no-storeon the HTML route — the JS is inlined, so the browser was serving stale pages after updates.DB concurrency (
scanner.py+dashboard.py)database is locked(swallowed by a bare except, so the page silently stopped updating). Enable WAL + busy_timeout, serialize incremental scans behind a process lock, and make/api/rescanhold it blocking. Bumped the listen backlog so a page's concurrent fetches don't get connection-refused.Compatibility / disclosure
/api/oauth/usageis an undocumented OAuth endpoint and may change; everything degrades gracefully to the existing local estimate when it's unavailable.api.anthropic.com, exactly asclaudeitself does.Verification
/api/limits→source: official_api, matchesclaude /usageexactly (weekly %, reset time, 5h, Sonnet-7d)./api/databurst → no lock errors,journal_mode=wal.