claude-usage-dashboard 1.0.6 → 1.1.0

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.0.6",
3
+ "version": "1.1.0",
4
4
  "description": "Dashboard that visualizes Claude Code usage from local session logs",
5
5
  "main": "server/index.js",
6
6
  "bin": {
@@ -194,6 +194,36 @@ body {
194
194
  }
195
195
  .date-picker span { color: var(--text-secondary); font-size: 12px; }
196
196
 
197
+ .refresh-controls {
198
+ display: flex;
199
+ align-items: center;
200
+ gap: 8px;
201
+ }
202
+ .btn-refresh {
203
+ padding: 4px 10px;
204
+ background: var(--bg-input);
205
+ border: 1px solid var(--border);
206
+ border-radius: 6px;
207
+ color: var(--text-primary);
208
+ font-size: 16px;
209
+ cursor: pointer;
210
+ line-height: 1;
211
+ }
212
+ .btn-refresh:hover { background: var(--blue); }
213
+ .auto-refresh-label {
214
+ display: flex;
215
+ align-items: center;
216
+ gap: 4px;
217
+ font-size: 12px;
218
+ color: var(--text-secondary);
219
+ cursor: pointer;
220
+ }
221
+ .last-updated {
222
+ font-size: 11px;
223
+ color: var(--text-muted);
224
+ white-space: nowrap;
225
+ }
226
+
197
227
  .plan-selector select, .plan-selector input {
198
228
  padding: 6px 10px;
199
229
  background: var(--bg-input);
package/public/index.html CHANGED
@@ -13,6 +13,13 @@
13
13
  <div class="controls">
14
14
  <div id="date-picker" class="date-picker"></div>
15
15
  <div id="plan-selector" class="plan-selector"></div>
16
+ <div class="refresh-controls">
17
+ <button id="btn-refresh" class="btn-refresh" title="Refresh now">↻</button>
18
+ <label class="auto-refresh-label">
19
+ <input type="checkbox" id="auto-refresh-toggle" checked> Auto
20
+ </label>
21
+ <span id="last-updated" class="last-updated"></span>
22
+ </div>
16
23
  </div>
17
24
  </header>
18
25
 
package/public/js/api.js CHANGED
@@ -11,3 +11,4 @@ export async function fetchProjects(params = {}) { return (await fetch(`${BASE}/
11
11
  export async function fetchSessions(params = {}) { return (await fetch(`${BASE}/sessions${qs(params)}`)).json(); }
12
12
  export async function fetchCost(params = {}) { return (await fetch(`${BASE}/cost${qs(params)}`)).json(); }
13
13
  export async function fetchCache(params = {}) { return (await fetch(`${BASE}/cache${qs(params)}`)).json(); }
14
+ export async function fetchStatus() { return (await fetch(`${BASE}/status`)).json(); }
package/public/js/app.js CHANGED
@@ -1,4 +1,4 @@
1
- import { fetchUsage, fetchModels, fetchProjects, fetchSessions, fetchCost, fetchCache } from './api.js';
1
+ import { fetchUsage, fetchModels, fetchProjects, fetchSessions, fetchCost, fetchCache, fetchStatus } 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';
@@ -16,6 +16,9 @@ const state = {
16
16
  sessionOrder: 'desc',
17
17
  sessionPage: 1,
18
18
  sessionProject: '',
19
+ autoRefresh: true,
20
+ autoRefreshInterval: 30,
21
+ _refreshTimer: null,
19
22
  };
20
23
 
21
24
  let datePicker, planSelector;
@@ -26,6 +29,28 @@ function formatNumber(n) {
26
29
  return n.toString();
27
30
  }
28
31
 
32
+ function updateLastUpdated() {
33
+ const el = document.getElementById('last-updated');
34
+ if (el) {
35
+ const now = new Date();
36
+ el.textContent = `Updated ${now.toLocaleTimeString()}`;
37
+ }
38
+ }
39
+
40
+ function startAutoRefresh() {
41
+ stopAutoRefresh();
42
+ if (state.autoRefresh) {
43
+ state._refreshTimer = setInterval(() => loadAll(), state.autoRefreshInterval * 1000);
44
+ }
45
+ }
46
+
47
+ function stopAutoRefresh() {
48
+ if (state._refreshTimer) {
49
+ clearInterval(state._refreshTimer);
50
+ state._refreshTimer = null;
51
+ }
52
+ }
53
+
29
54
  async function loadAll() {
30
55
  const params = { ...state.dateRange };
31
56
  const planParams = { ...state.dateRange, plan: state.plan.plan };
@@ -93,6 +118,8 @@ async function loadAll() {
93
118
  loadAll();
94
119
  },
95
120
  });
121
+
122
+ updateLastUpdated();
96
123
  }
97
124
 
98
125
  function init() {
@@ -134,7 +161,20 @@ function init() {
134
161
  loadAll();
135
162
  });
136
163
 
164
+ document.getElementById('btn-refresh').addEventListener('click', () => loadAll());
165
+
166
+ const autoToggle = document.getElementById('auto-refresh-toggle');
167
+ autoToggle.addEventListener('change', () => {
168
+ state.autoRefresh = autoToggle.checked;
169
+ if (state.autoRefresh) {
170
+ startAutoRefresh();
171
+ } else {
172
+ stopAutoRefresh();
173
+ }
174
+ });
175
+
137
176
  loadAll();
177
+ startAutoRefresh();
138
178
  }
139
179
 
140
180
  document.addEventListener('DOMContentLoaded', init);
@@ -3,18 +3,32 @@ 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
 
6
- export function createApiRouter(logBaseDir) {
6
+ export function createApiRouter(logBaseDir, options = {}) {
7
7
  const router = Router();
8
- let allRecords = [];
9
- try {
10
- allRecords = parseLogDirectory(logBaseDir);
11
- console.log(`Parsed ${allRecords.length} records from ${logBaseDir}`);
12
- } catch (err) {
13
- console.error('Failed to parse log directory:', err.message);
8
+ const CACHE_TTL_MS = options.cacheTtlMs || 5000;
9
+ let cachedRecords = [];
10
+ let lastRefreshed = null;
11
+
12
+ function refreshRecords() {
13
+ const now = Date.now();
14
+ if (lastRefreshed && (now - lastRefreshed) < CACHE_TTL_MS) return cachedRecords;
15
+ try {
16
+ cachedRecords = parseLogDirectory(logBaseDir);
17
+ lastRefreshed = now;
18
+ console.log(`Parsed ${cachedRecords.length} records from ${logBaseDir}`);
19
+ } catch (err) {
20
+ console.error('Failed to parse log directory:', err.message);
21
+ // Keep stale data on failure
22
+ if (!lastRefreshed) lastRefreshed = now;
23
+ }
24
+ return cachedRecords;
14
25
  }
15
26
 
27
+ // Initial parse
28
+ refreshRecords();
29
+
16
30
  function applyFilters(query) {
17
- let records = filterByDateRange(allRecords, query.from, query.to);
31
+ let records = filterByDateRange(refreshRecords(), query.from, query.to);
18
32
  if (query.project) records = records.filter(r => r.project === query.project);
19
33
  if (query.model) records = records.filter(r => r.model === query.model);
20
34
  return records;
@@ -83,5 +97,14 @@ export function createApiRouter(logBaseDir) {
83
97
  });
84
98
 
85
99
  router.get('/cache', (req, res) => { res.json(aggregateCache(applyFilters(req.query))); });
100
+
101
+ router.get('/status', (req, res) => {
102
+ res.json({
103
+ record_count: cachedRecords.length,
104
+ last_refreshed: lastRefreshed ? new Date(lastRefreshed).toISOString() : null,
105
+ cache_ttl_ms: CACHE_TTL_MS,
106
+ });
107
+ });
108
+
86
109
  return router;
87
110
  }