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 +1 -1
- package/public/css/style.css +30 -0
- package/public/index.html +7 -0
- package/public/js/api.js +1 -0
- package/public/js/app.js +41 -1
- package/server/routes/api.js +31 -8
package/package.json
CHANGED
package/public/css/style.css
CHANGED
|
@@ -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);
|
package/server/routes/api.js
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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(
|
|
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
|
}
|