claude-usage-dashboard 1.4.0 → 1.4.1
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/package.json +1 -1
- package/server/routes/api.js +26 -15
package/package.json
CHANGED
package/server/routes/api.js
CHANGED
|
@@ -12,12 +12,23 @@ export function createApiRouter(logBaseDir, options = {}) {
|
|
|
12
12
|
let cachedRecords = [];
|
|
13
13
|
let lastRefreshed = null;
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
// Background sync: runs periodically without blocking API requests
|
|
16
|
+
if (options.syncDir) {
|
|
17
|
+
const SYNC_INTERVAL_MS = options.syncIntervalMs || 30000;
|
|
18
|
+
const runBackgroundSync = () => {
|
|
19
|
+
syncLocalToShared(logBaseDir, options.syncDir, options.machineName).catch(err => {
|
|
20
|
+
console.warn('Background sync failed:', err.message);
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
runBackgroundSync();
|
|
24
|
+
setInterval(runBackgroundSync, SYNC_INTERVAL_MS).unref();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function refreshRecords() {
|
|
16
28
|
const now = Date.now();
|
|
17
29
|
if (lastRefreshed && (now - lastRefreshed) < CACHE_TTL_MS) return cachedRecords;
|
|
18
30
|
try {
|
|
19
31
|
if (options.syncDir) {
|
|
20
|
-
await syncLocalToShared(logBaseDir, options.syncDir, options.machineName);
|
|
21
32
|
cachedRecords = parseMultiMachineDirectory(options.syncDir);
|
|
22
33
|
} else {
|
|
23
34
|
cachedRecords = parseLogDirectory(logBaseDir);
|
|
@@ -31,16 +42,16 @@ export function createApiRouter(logBaseDir, options = {}) {
|
|
|
31
42
|
return cachedRecords;
|
|
32
43
|
}
|
|
33
44
|
|
|
34
|
-
|
|
35
|
-
let records = filterByDateRange(
|
|
45
|
+
function applyFilters(query) {
|
|
46
|
+
let records = filterByDateRange(refreshRecords(), query.from, query.to);
|
|
36
47
|
if (query.project) records = records.filter(r => r.project === query.project);
|
|
37
48
|
if (query.model) records = records.filter(r => r.model === query.model);
|
|
38
49
|
return records;
|
|
39
50
|
}
|
|
40
51
|
|
|
41
|
-
router.get('/usage',
|
|
52
|
+
router.get('/usage', (req, res) => {
|
|
42
53
|
try {
|
|
43
|
-
const records =
|
|
54
|
+
const records = applyFilters(req.query);
|
|
44
55
|
const granularity = req.query.granularity || autoGranularity(req.query.from, req.query.to);
|
|
45
56
|
const buckets = aggregateByTime(records, granularity);
|
|
46
57
|
const total = { input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_creation_tokens: 0, estimated_api_cost_usd: 0 };
|
|
@@ -56,11 +67,11 @@ export function createApiRouter(logBaseDir, options = {}) {
|
|
|
56
67
|
}
|
|
57
68
|
});
|
|
58
69
|
|
|
59
|
-
router.get('/models',
|
|
60
|
-
router.get('/projects',
|
|
70
|
+
router.get('/models', (req, res) => { res.json({ models: aggregateByModel(applyFilters(req.query)) }); });
|
|
71
|
+
router.get('/projects', (req, res) => { res.json({ projects: aggregateByProject(applyFilters(req.query)) }); });
|
|
61
72
|
|
|
62
|
-
router.get('/sessions',
|
|
63
|
-
const records =
|
|
73
|
+
router.get('/sessions', (req, res) => {
|
|
74
|
+
const records = applyFilters(req.query);
|
|
64
75
|
let sessions = aggregateBySession(records);
|
|
65
76
|
const sort = req.query.sort || 'date';
|
|
66
77
|
const order = req.query.order || 'desc';
|
|
@@ -77,8 +88,8 @@ export function createApiRouter(logBaseDir, options = {}) {
|
|
|
77
88
|
res.json({ sessions, pagination: { page, limit, total_sessions: totalSessions, total_pages: totalPages }, totals: { total_tokens: totalTokens, estimated_cost_usd: Math.round(totalCost * 100) / 100 } });
|
|
78
89
|
});
|
|
79
90
|
|
|
80
|
-
router.get('/cost',
|
|
81
|
-
const records =
|
|
91
|
+
router.get('/cost', (req, res) => {
|
|
92
|
+
const records = applyFilters(req.query);
|
|
82
93
|
const plan = req.query.plan || 'max5x';
|
|
83
94
|
const customPrice = req.query.customPrice ? parseFloat(req.query.customPrice) : null;
|
|
84
95
|
const subscriptionCost = customPrice || PLAN_DEFAULTS[plan] || 100;
|
|
@@ -100,7 +111,7 @@ export function createApiRouter(logBaseDir, options = {}) {
|
|
|
100
111
|
res.json({ plan, subscription_cost_usd: subscriptionCost, api_equivalent_cost_usd: apiCost, savings_usd: Math.round(savings * 100) / 100, savings_percent: apiCost > 0 ? Math.round((savings / apiCost) * 1000) / 10 : 0, cost_per_day: costPerDay });
|
|
101
112
|
});
|
|
102
113
|
|
|
103
|
-
router.get('/cache',
|
|
114
|
+
router.get('/cache', (req, res) => { res.json(aggregateCache(applyFilters(req.query))); });
|
|
104
115
|
|
|
105
116
|
const quotaFetcher = options.quotaFetcher || createQuotaFetcher();
|
|
106
117
|
router.get('/quota', async (req, res) => {
|
|
@@ -117,8 +128,8 @@ export function createApiRouter(logBaseDir, options = {}) {
|
|
|
117
128
|
res.json(info || { plan: null, subscriptionType: null, rateLimitTier: null });
|
|
118
129
|
});
|
|
119
130
|
|
|
120
|
-
router.get('/status',
|
|
121
|
-
|
|
131
|
+
router.get('/status', (req, res) => {
|
|
132
|
+
refreshRecords();
|
|
122
133
|
res.json({
|
|
123
134
|
record_count: cachedRecords.length,
|
|
124
135
|
last_refreshed: lastRefreshed ? new Date(lastRefreshed).toISOString() : null,
|