claude-usage-dashboard 1.2.0 → 1.2.2

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 CHANGED
@@ -10,11 +10,13 @@ A self-hosted dashboard that visualizes your [Claude Code](https://claude.ai/cod
10
10
 
11
11
  - **Token tracking** — Total tokens with breakdown by input, output, cache read, and cache write
12
12
  - **Cost estimation** — API cost equivalent at standard pricing, compared against your subscription plan (Pro / Max 5x / Max 20x)
13
+ - **Subscription quota** — Real-time utilization gauges (5-hour, 7-day, per-model) pulled from the Anthropic API with auto-detection of your plan tier
13
14
  - **Token consumption trend** — Stacked bar chart with hourly, daily, weekly, or monthly granularity
14
15
  - **Model distribution** — Donut chart showing usage across Claude models
15
16
  - **Cache efficiency** — Visual breakdown of cache read, cache creation, and uncached requests
16
17
  - **Project distribution** — Horizontal bar chart comparing token usage across projects
17
18
  - **Session details** — Sortable, paginated table of every session with cost and duration
19
+ - **Auto-refresh** — Dashboard polls every 30s for new usage data; quota refreshes every 2 minutes
18
20
 
19
21
  ## Quick Start
20
22
 
@@ -43,7 +45,9 @@ PORT=8080 npx claude-usage-dashboard
43
45
 
44
46
  ## How It Works
45
47
 
46
- The dashboard reads Claude Code session logs from `~/.claude/projects/` — if you use Claude Code, these already exist on your machine. Logs are parsed once at startup; restart the server to pick up new session data.
48
+ The dashboard reads Claude Code session logs from `~/.claude/projects/` — if you use Claude Code, these already exist on your machine. Logs are automatically re-read every 5 seconds, and new usage appears without restarting the server.
49
+
50
+ Subscription quota data is fetched from the Anthropic API using your local OAuth credentials (`~/.claude/.credentials.json`). Your plan tier (Pro / Max 5x / Max 20x) is auto-detected from the same file.
47
51
 
48
52
  ## Tech Stack
49
53
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-usage-dashboard",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Dashboard that visualizes Claude Code usage from local session logs",
5
5
  "main": "server/index.js",
6
6
  "bin": {
package/public/js/api.js CHANGED
@@ -13,3 +13,4 @@ export async function fetchCost(params = {}) { return (await fetch(`${BASE}/cost
13
13
  export async function fetchCache(params = {}) { return (await fetch(`${BASE}/cache${qs(params)}`)).json(); }
14
14
  export async function fetchStatus() { return (await fetch(`${BASE}/status`)).json(); }
15
15
  export async function fetchQuota() { return (await fetch(`${BASE}/quota`)).json(); }
16
+ export async function fetchSubscription() { return (await fetch(`${BASE}/subscription`)).json(); }
package/public/js/app.js CHANGED
@@ -1,4 +1,4 @@
1
- import { fetchUsage, fetchModels, fetchProjects, fetchSessions, fetchCost, fetchCache, fetchStatus, fetchQuota } from './api.js';
1
+ import { fetchUsage, fetchModels, fetchProjects, fetchSessions, fetchCost, fetchCache, fetchStatus, fetchQuota, fetchSubscription } from './api.js';
2
2
  import { initDatePicker } from './components/date-picker.js';
3
3
  import { initPlanSelector } from './components/plan-selector.js';
4
4
  import { renderTokenTrend } from './charts/token-trend.js';
@@ -190,6 +190,20 @@ function init() {
190
190
  }
191
191
  });
192
192
 
193
+ // Auto-detect subscription tier
194
+ fetchSubscription().then(info => {
195
+ if (info.plan) {
196
+ planSelector.setDetectedPlan(info.plan);
197
+ state.plan = planSelector.getPlan();
198
+ }
199
+ const tierLabels = { pro: 'Pro', max5x: 'Max 5x', max20x: 'Max 20x' };
200
+ const label = tierLabels[info.plan];
201
+ if (label) {
202
+ const h2 = document.querySelector('#quota-section h2');
203
+ if (h2) h2.textContent = `Subscription Quota (${label})`;
204
+ }
205
+ }).catch(() => {});
206
+
193
207
  loadAll();
194
208
  loadQuota();
195
209
  startAutoRefresh();
@@ -7,6 +7,7 @@ const PLANS = {
7
7
  export function initPlanSelector(container, onChange) {
8
8
  const saved = localStorage.getItem('selectedPlan') || 'max20x';
9
9
  const savedPrice = localStorage.getItem('customPrice') || '';
10
+ let detectedPlan = null;
10
11
 
11
12
  container.innerHTML = `
12
13
  <select id="plan-select">
@@ -33,5 +34,24 @@ export function initPlanSelector(container, onChange) {
33
34
  customInput.style.display = customInput.style.display === 'none' ? 'inline-block' : 'none';
34
35
  });
35
36
 
36
- return { getPlan: () => ({ plan: select.value, customPrice: customInput.value ? parseFloat(customInput.value) : null }) };
37
+ function setDetectedPlan(planKey) {
38
+ if (!planKey || !PLANS[planKey]) return;
39
+ detectedPlan = planKey;
40
+ // Only auto-select if user hasn't manually chosen a plan
41
+ if (!localStorage.getItem('selectedPlan')) {
42
+ select.value = planKey;
43
+ emitChange();
44
+ }
45
+ // Update option labels to show which is detected
46
+ for (const opt of select.options) {
47
+ const p = PLANS[opt.value];
48
+ const suffix = opt.value === planKey ? ' ✓' : '';
49
+ opt.textContent = `${p.label} ($${p.price}/mo)${suffix}`;
50
+ }
51
+ }
52
+
53
+ return {
54
+ getPlan: () => ({ plan: select.value, customPrice: customInput.value ? parseFloat(customInput.value) : null }),
55
+ setDetectedPlan,
56
+ };
37
57
  }
@@ -14,6 +14,21 @@ export function readCredentials(credentialsPath = CREDENTIALS_PATH) {
14
14
  }
15
15
  }
16
16
 
17
+ export function getSubscriptionInfo(credentialsPath = CREDENTIALS_PATH) {
18
+ const creds = readCredentials(credentialsPath);
19
+ if (!creds) return null;
20
+
21
+ const { subscriptionType, rateLimitTier } = creds;
22
+ const combined = `${subscriptionType || ''} ${rateLimitTier || ''}`.toLowerCase();
23
+
24
+ let plan = null;
25
+ if (combined.includes('20x')) plan = 'max20x';
26
+ else if (combined.includes('5x')) plan = 'max5x';
27
+ else if (combined.includes('pro')) plan = 'pro';
28
+
29
+ return { subscriptionType: subscriptionType || null, rateLimitTier: rateLimitTier || null, plan };
30
+ }
31
+
17
32
  export function getAccessToken(credentialsPath = CREDENTIALS_PATH) {
18
33
  const creds = readCredentials(credentialsPath);
19
34
  if (!creds || !creds.accessToken) return null;
@@ -3,6 +3,7 @@ import { parseLogDirectory } from '../parser.js';
3
3
  import { filterByDateRange, autoGranularity, aggregateByTime, aggregateBySession, aggregateByProject, aggregateByModel, aggregateCache } from '../aggregator.js';
4
4
  import { calculateRecordCost, PLAN_DEFAULTS } from '../pricing.js';
5
5
  import { createQuotaFetcher } from '../quota.js';
6
+ import { getSubscriptionInfo } from '../credentials.js';
6
7
 
7
8
  export function createApiRouter(logBaseDir, options = {}) {
8
9
  const router = Router();
@@ -109,6 +110,11 @@ export function createApiRouter(logBaseDir, options = {}) {
109
110
  }
110
111
  });
111
112
 
113
+ router.get('/subscription', (req, res) => {
114
+ const info = options.getSubscriptionInfo ? options.getSubscriptionInfo() : getSubscriptionInfo();
115
+ res.json(info || { plan: null, subscriptionType: null, rateLimitTier: null });
116
+ });
117
+
112
118
  router.get('/status', (req, res) => {
113
119
  res.json({
114
120
  record_count: cachedRecords.length,