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 +5 -1
- package/package.json +1 -1
- package/public/js/api.js +1 -0
- package/public/js/app.js +15 -1
- package/public/js/components/plan-selector.js +21 -1
- package/server/credentials.js +15 -0
- package/server/routes/api.js +6 -0
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
|
|
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
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
|
-
|
|
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
|
}
|
package/server/credentials.js
CHANGED
|
@@ -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;
|
package/server/routes/api.js
CHANGED
|
@@ -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,
|