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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-usage-dashboard",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Dashboard that visualizes Claude Code usage from local session logs",
5
5
  "main": "server/index.js",
6
6
  "bin": {
@@ -12,12 +12,23 @@ export function createApiRouter(logBaseDir, options = {}) {
12
12
  let cachedRecords = [];
13
13
  let lastRefreshed = null;
14
14
 
15
- async function refreshRecords() {
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
- async function applyFilters(query) {
35
- let records = filterByDateRange(await refreshRecords(), query.from, query.to);
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', async (req, res) => {
52
+ router.get('/usage', (req, res) => {
42
53
  try {
43
- const records = await applyFilters(req.query);
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', async (req, res) => { res.json({ models: aggregateByModel(await applyFilters(req.query)) }); });
60
- router.get('/projects', async (req, res) => { res.json({ projects: aggregateByProject(await applyFilters(req.query)) }); });
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', async (req, res) => {
63
- const records = await applyFilters(req.query);
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', async (req, res) => {
81
- const records = await applyFilters(req.query);
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', async (req, res) => { res.json(aggregateCache(await applyFilters(req.query))); });
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', async (req, res) => {
121
- await refreshRecords();
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,