claude-usage-dashboard 1.0.4 → 1.0.6
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 +62 -52
- package/bin/cli.js +20 -20
- package/package.json +40 -40
- package/public/css/style.css +221 -221
- package/public/index.html +93 -93
- package/public/js/api.js +13 -13
- package/public/js/app.js +140 -140
- package/public/js/charts/cache-efficiency.js +29 -29
- package/public/js/charts/cost-comparison.js +39 -39
- package/public/js/charts/model-distribution.js +48 -48
- package/public/js/charts/project-distribution.js +92 -92
- package/public/js/charts/session-stats.js +117 -117
- package/public/js/charts/token-trend.js +113 -113
- package/public/js/components/date-picker.js +21 -21
- package/public/js/components/plan-selector.js +37 -37
- package/server/aggregator.js +147 -147
- package/server/index.js +33 -33
- package/server/parser.js +92 -92
- package/server/pricing.js +52 -52
- package/server/routes/api.js +87 -87
package/README.md
CHANGED
|
@@ -1,52 +1,62 @@
|
|
|
1
|
-
# Claude Usage Dashboard
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- **Token
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
##
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
1
|
+
# Claude Usage Dashboard
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/claude-usage-dashboard)
|
|
4
|
+
|
|
5
|
+
A self-hosted dashboard that visualizes your [Claude Code](https://claude.ai/code) usage by parsing local JSONL session logs from `~/.claude/projects/`.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Token tracking** — Total tokens with breakdown by input, output, cache read, and cache write
|
|
12
|
+
- **Cost estimation** — API cost equivalent at standard pricing, compared against your subscription plan (Pro / Max 5x / Max 20x)
|
|
13
|
+
- **Token consumption trend** — Stacked bar chart with hourly, daily, weekly, or monthly granularity
|
|
14
|
+
- **Model distribution** — Donut chart showing usage across Claude models
|
|
15
|
+
- **Cache efficiency** — Visual breakdown of cache read, cache creation, and uncached requests
|
|
16
|
+
- **Project distribution** — Horizontal bar chart comparing token usage across projects
|
|
17
|
+
- **Session details** — Sortable, paginated table of every session with cost and duration
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
Run directly without installing:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx claude-usage-dashboard
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Open http://localhost:3000 in your browser.
|
|
28
|
+
|
|
29
|
+
### From Source
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
git clone https://github.com/ludengz/claudeUsageDashboard.git
|
|
33
|
+
cd claudeUsageDashboard
|
|
34
|
+
npm install
|
|
35
|
+
npm start
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Custom Port
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
PORT=8080 npx claude-usage-dashboard
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## How It Works
|
|
45
|
+
|
|
46
|
+
The dashboard reads Claude Code session logs from `~/.claude/projects/` — if you use Claude Code, these already exist on your machine. Logs are parsed once at startup; restart the server to pick up new session data.
|
|
47
|
+
|
|
48
|
+
## Tech Stack
|
|
49
|
+
|
|
50
|
+
- **Backend:** Node.js, Express 5
|
|
51
|
+
- **Frontend:** Vanilla JS (ES modules), D3.js v7
|
|
52
|
+
- **Tests:** Mocha + Chai
|
|
53
|
+
|
|
54
|
+
## Running Tests
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm test
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
ISC
|
package/bin/cli.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { spawn } from 'child_process';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
|
|
6
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const server = path.join(__dirname, '..', 'server', 'index.js');
|
|
8
|
-
|
|
9
|
-
const child = spawn(process.execPath, [server], {
|
|
10
|
-
stdio: 'inherit',
|
|
11
|
-
// Attach child to the terminal's foreground process group
|
|
12
|
-
detached: false,
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
child.on('exit', (code) => process.exit(code ?? 0));
|
|
16
|
-
|
|
17
|
-
// Forward signals to child
|
|
18
|
-
for (const sig of ['SIGINT', 'SIGTERM']) {
|
|
19
|
-
process.on(sig, () => child.kill(sig));
|
|
20
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const server = path.join(__dirname, '..', 'server', 'index.js');
|
|
8
|
+
|
|
9
|
+
const child = spawn(process.execPath, [server], {
|
|
10
|
+
stdio: 'inherit',
|
|
11
|
+
// Attach child to the terminal's foreground process group
|
|
12
|
+
detached: false,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
16
|
+
|
|
17
|
+
// Forward signals to child
|
|
18
|
+
for (const sig of ['SIGINT', 'SIGTERM']) {
|
|
19
|
+
process.on(sig, () => child.kill(sig));
|
|
20
|
+
}
|
package/package.json
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "claude-usage-dashboard",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Dashboard that visualizes Claude Code usage from local session logs",
|
|
5
|
-
"main": "server/index.js",
|
|
6
|
-
"bin": {
|
|
7
|
-
"claude-usage-dashboard": "bin/cli.js"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"bin/",
|
|
11
|
-
"server/",
|
|
12
|
-
"public/"
|
|
13
|
-
],
|
|
14
|
-
"scripts": {
|
|
15
|
-
"start": "node server/index.js",
|
|
16
|
-
"test": "mocha test/**/*.test.js --timeout 5000"
|
|
17
|
-
},
|
|
18
|
-
"keywords": [
|
|
19
|
-
"claude",
|
|
20
|
-
"usage",
|
|
21
|
-
"dashboard",
|
|
22
|
-
"token",
|
|
23
|
-
"cost"
|
|
24
|
-
],
|
|
25
|
-
"author": "",
|
|
26
|
-
"license": "ISC",
|
|
27
|
-
"repository": {
|
|
28
|
-
"type": "git",
|
|
29
|
-
"url": "https://github.com/ludengz/claudeUsageDashboard.git"
|
|
30
|
-
},
|
|
31
|
-
"type": "module",
|
|
32
|
-
"dependencies": {
|
|
33
|
-
"d3": "^7.9.0",
|
|
34
|
-
"express": "^5.2.1"
|
|
35
|
-
},
|
|
36
|
-
"devDependencies": {
|
|
37
|
-
"chai": "^6.2.2",
|
|
38
|
-
"mocha": "^11.7.5"
|
|
39
|
-
}
|
|
40
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-usage-dashboard",
|
|
3
|
+
"version": "1.0.6",
|
|
4
|
+
"description": "Dashboard that visualizes Claude Code usage from local session logs",
|
|
5
|
+
"main": "server/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claude-usage-dashboard": "bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"server/",
|
|
12
|
+
"public/"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"start": "node server/index.js",
|
|
16
|
+
"test": "mocha test/**/*.test.js --timeout 5000"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"claude",
|
|
20
|
+
"usage",
|
|
21
|
+
"dashboard",
|
|
22
|
+
"token",
|
|
23
|
+
"cost"
|
|
24
|
+
],
|
|
25
|
+
"author": "",
|
|
26
|
+
"license": "ISC",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/ludengz/claudeUsageDashboard.git"
|
|
30
|
+
},
|
|
31
|
+
"type": "module",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"d3": "^7.9.0",
|
|
34
|
+
"express": "^5.2.1"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"chai": "^6.2.2",
|
|
38
|
+
"mocha": "^11.7.5"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/public/css/style.css
CHANGED
|
@@ -1,221 +1,221 @@
|
|
|
1
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
2
|
-
|
|
3
|
-
:root {
|
|
4
|
-
--bg-primary: #0f172a;
|
|
5
|
-
--bg-card: #1e293b;
|
|
6
|
-
--bg-input: #334155;
|
|
7
|
-
--border: #475569;
|
|
8
|
-
--text-primary: #f8fafc;
|
|
9
|
-
--text-secondary: #94a3b8;
|
|
10
|
-
--text-muted: #64748b;
|
|
11
|
-
--blue: #3b82f6;
|
|
12
|
-
--blue-light: #60a5fa;
|
|
13
|
-
--purple: #8b5cf6;
|
|
14
|
-
--orange: #f97316;
|
|
15
|
-
--amber: #f59e0b;
|
|
16
|
-
--green: #4ade80;
|
|
17
|
-
--red: #ef4444;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
body {
|
|
21
|
-
background: var(--bg-primary);
|
|
22
|
-
color: var(--text-primary);
|
|
23
|
-
font-family: system-ui, -apple-system, sans-serif;
|
|
24
|
-
padding: 0 24px 40px;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
.top-bar {
|
|
28
|
-
display: flex;
|
|
29
|
-
justify-content: space-between;
|
|
30
|
-
align-items: center;
|
|
31
|
-
padding: 16px 0;
|
|
32
|
-
border-bottom: 1px solid var(--bg-card);
|
|
33
|
-
margin-bottom: 20px;
|
|
34
|
-
}
|
|
35
|
-
.logo { font-size: 18px; font-weight: 700; }
|
|
36
|
-
.controls { display: flex; gap: 12px; align-items: center; }
|
|
37
|
-
|
|
38
|
-
.summary-cards {
|
|
39
|
-
display: grid;
|
|
40
|
-
grid-template-columns: repeat(4, 1fr);
|
|
41
|
-
gap: 12px;
|
|
42
|
-
margin-bottom: 20px;
|
|
43
|
-
}
|
|
44
|
-
.card {
|
|
45
|
-
background: var(--bg-card);
|
|
46
|
-
border-radius: 8px;
|
|
47
|
-
padding: 16px;
|
|
48
|
-
}
|
|
49
|
-
.card-label {
|
|
50
|
-
font-size: 11px;
|
|
51
|
-
color: var(--text-muted);
|
|
52
|
-
text-transform: uppercase;
|
|
53
|
-
letter-spacing: 1px;
|
|
54
|
-
}
|
|
55
|
-
.card-value {
|
|
56
|
-
font-size: 24px;
|
|
57
|
-
font-weight: 700;
|
|
58
|
-
margin-top: 4px;
|
|
59
|
-
}
|
|
60
|
-
.card-sub {
|
|
61
|
-
font-size: 11px;
|
|
62
|
-
color: var(--text-secondary);
|
|
63
|
-
margin-top: 2px;
|
|
64
|
-
}
|
|
65
|
-
#val-api-cost { color: var(--amber); }
|
|
66
|
-
#val-savings { color: var(--green); }
|
|
67
|
-
#val-cache-rate { color: var(--blue-light); }
|
|
68
|
-
|
|
69
|
-
.chart-section {
|
|
70
|
-
background: var(--bg-card);
|
|
71
|
-
border-radius: 8px;
|
|
72
|
-
padding: 20px;
|
|
73
|
-
margin-bottom: 12px;
|
|
74
|
-
}
|
|
75
|
-
.chart-section h2 {
|
|
76
|
-
font-size: 14px;
|
|
77
|
-
font-weight: 600;
|
|
78
|
-
margin-bottom: 16px;
|
|
79
|
-
}
|
|
80
|
-
.chart-header {
|
|
81
|
-
display: flex;
|
|
82
|
-
justify-content: space-between;
|
|
83
|
-
align-items: center;
|
|
84
|
-
margin-bottom: 16px;
|
|
85
|
-
}
|
|
86
|
-
.chart-header h2 { margin-bottom: 0; }
|
|
87
|
-
.chart-container { min-height: 200px; }
|
|
88
|
-
|
|
89
|
-
.chart-row-3 {
|
|
90
|
-
display: grid;
|
|
91
|
-
grid-template-columns: repeat(3, 1fr);
|
|
92
|
-
gap: 12px;
|
|
93
|
-
margin-bottom: 12px;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
.granularity-toggle { display: flex; gap: 4px; }
|
|
97
|
-
.granularity-toggle button {
|
|
98
|
-
padding: 4px 12px;
|
|
99
|
-
background: var(--bg-input);
|
|
100
|
-
border: none;
|
|
101
|
-
border-radius: 4px;
|
|
102
|
-
color: var(--text-secondary);
|
|
103
|
-
font-size: 12px;
|
|
104
|
-
cursor: pointer;
|
|
105
|
-
}
|
|
106
|
-
.granularity-toggle button.active {
|
|
107
|
-
background: var(--blue);
|
|
108
|
-
color: white;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
.table-controls { display: flex; gap: 8px; }
|
|
112
|
-
.filter-input, .sort-select {
|
|
113
|
-
padding: 6px 10px;
|
|
114
|
-
background: var(--bg-input);
|
|
115
|
-
border: 1px solid var(--border);
|
|
116
|
-
border-radius: 6px;
|
|
117
|
-
color: var(--text-primary);
|
|
118
|
-
font-size: 12px;
|
|
119
|
-
}
|
|
120
|
-
.filter-input { width: 180px; }
|
|
121
|
-
|
|
122
|
-
.table-container { overflow-x: auto; }
|
|
123
|
-
.table-container table {
|
|
124
|
-
width: 100%;
|
|
125
|
-
border-collapse: collapse;
|
|
126
|
-
font-size: 12px;
|
|
127
|
-
}
|
|
128
|
-
.table-container th {
|
|
129
|
-
text-align: left;
|
|
130
|
-
padding: 10px 8px;
|
|
131
|
-
border-bottom: 2px solid var(--bg-input);
|
|
132
|
-
color: var(--text-muted);
|
|
133
|
-
text-transform: uppercase;
|
|
134
|
-
font-size: 10px;
|
|
135
|
-
letter-spacing: 1px;
|
|
136
|
-
cursor: pointer;
|
|
137
|
-
}
|
|
138
|
-
.table-container th.align-right,
|
|
139
|
-
.table-container td.align-right { text-align: right; }
|
|
140
|
-
.table-container td {
|
|
141
|
-
padding: 10px 8px;
|
|
142
|
-
border-bottom: 1px solid var(--bg-primary);
|
|
143
|
-
color: var(--text-secondary);
|
|
144
|
-
}
|
|
145
|
-
.table-container tfoot td {
|
|
146
|
-
border-top: 2px solid var(--bg-input);
|
|
147
|
-
font-weight: 600;
|
|
148
|
-
color: var(--text-primary);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
.tag {
|
|
152
|
-
display: inline-block;
|
|
153
|
-
padding: 2px 8px;
|
|
154
|
-
border-radius: 4px;
|
|
155
|
-
font-size: 11px;
|
|
156
|
-
}
|
|
157
|
-
.tag-project { background: #1e3a5f; color: var(--blue-light); }
|
|
158
|
-
.tag-model-sonnet { background: #1e3a5f; color: var(--blue-light); }
|
|
159
|
-
.tag-model-opus { background: #3b1764; color: #c084fc; }
|
|
160
|
-
.tag-model-haiku { background: #1a2e1a; color: var(--green); }
|
|
161
|
-
|
|
162
|
-
.pagination {
|
|
163
|
-
display: flex;
|
|
164
|
-
justify-content: center;
|
|
165
|
-
gap: 4px;
|
|
166
|
-
margin-top: 12px;
|
|
167
|
-
}
|
|
168
|
-
.pagination button {
|
|
169
|
-
padding: 4px 10px;
|
|
170
|
-
background: var(--bg-input);
|
|
171
|
-
border: none;
|
|
172
|
-
border-radius: 4px;
|
|
173
|
-
font-size: 11px;
|
|
174
|
-
color: var(--text-secondary);
|
|
175
|
-
cursor: pointer;
|
|
176
|
-
}
|
|
177
|
-
.pagination button.active {
|
|
178
|
-
background: var(--blue);
|
|
179
|
-
color: white;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
.date-picker {
|
|
183
|
-
display: flex;
|
|
184
|
-
align-items: center;
|
|
185
|
-
gap: 8px;
|
|
186
|
-
}
|
|
187
|
-
.date-picker input {
|
|
188
|
-
padding: 6px 10px;
|
|
189
|
-
background: var(--bg-input);
|
|
190
|
-
border: 1px solid var(--border);
|
|
191
|
-
border-radius: 6px;
|
|
192
|
-
color: var(--text-primary);
|
|
193
|
-
font-size: 12px;
|
|
194
|
-
}
|
|
195
|
-
.date-picker span { color: var(--text-secondary); font-size: 12px; }
|
|
196
|
-
|
|
197
|
-
.plan-selector select, .plan-selector input {
|
|
198
|
-
padding: 6px 10px;
|
|
199
|
-
background: var(--bg-input);
|
|
200
|
-
border: 1px solid var(--border);
|
|
201
|
-
border-radius: 6px;
|
|
202
|
-
color: var(--text-primary);
|
|
203
|
-
font-size: 12px;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
.d3-tooltip {
|
|
207
|
-
position: absolute;
|
|
208
|
-
padding: 8px 12px;
|
|
209
|
-
background: rgba(15, 23, 42, 0.95);
|
|
210
|
-
border: 1px solid var(--border);
|
|
211
|
-
border-radius: 6px;
|
|
212
|
-
font-size: 12px;
|
|
213
|
-
color: var(--text-primary);
|
|
214
|
-
pointer-events: none;
|
|
215
|
-
z-index: 100;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
@media (max-width: 768px) {
|
|
219
|
-
.summary-cards { grid-template-columns: repeat(2, 1fr); }
|
|
220
|
-
.chart-row-3 { grid-template-columns: 1fr; }
|
|
221
|
-
}
|
|
1
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--bg-primary: #0f172a;
|
|
5
|
+
--bg-card: #1e293b;
|
|
6
|
+
--bg-input: #334155;
|
|
7
|
+
--border: #475569;
|
|
8
|
+
--text-primary: #f8fafc;
|
|
9
|
+
--text-secondary: #94a3b8;
|
|
10
|
+
--text-muted: #64748b;
|
|
11
|
+
--blue: #3b82f6;
|
|
12
|
+
--blue-light: #60a5fa;
|
|
13
|
+
--purple: #8b5cf6;
|
|
14
|
+
--orange: #f97316;
|
|
15
|
+
--amber: #f59e0b;
|
|
16
|
+
--green: #4ade80;
|
|
17
|
+
--red: #ef4444;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
body {
|
|
21
|
+
background: var(--bg-primary);
|
|
22
|
+
color: var(--text-primary);
|
|
23
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
24
|
+
padding: 0 24px 40px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.top-bar {
|
|
28
|
+
display: flex;
|
|
29
|
+
justify-content: space-between;
|
|
30
|
+
align-items: center;
|
|
31
|
+
padding: 16px 0;
|
|
32
|
+
border-bottom: 1px solid var(--bg-card);
|
|
33
|
+
margin-bottom: 20px;
|
|
34
|
+
}
|
|
35
|
+
.logo { font-size: 18px; font-weight: 700; }
|
|
36
|
+
.controls { display: flex; gap: 12px; align-items: center; }
|
|
37
|
+
|
|
38
|
+
.summary-cards {
|
|
39
|
+
display: grid;
|
|
40
|
+
grid-template-columns: repeat(4, 1fr);
|
|
41
|
+
gap: 12px;
|
|
42
|
+
margin-bottom: 20px;
|
|
43
|
+
}
|
|
44
|
+
.card {
|
|
45
|
+
background: var(--bg-card);
|
|
46
|
+
border-radius: 8px;
|
|
47
|
+
padding: 16px;
|
|
48
|
+
}
|
|
49
|
+
.card-label {
|
|
50
|
+
font-size: 11px;
|
|
51
|
+
color: var(--text-muted);
|
|
52
|
+
text-transform: uppercase;
|
|
53
|
+
letter-spacing: 1px;
|
|
54
|
+
}
|
|
55
|
+
.card-value {
|
|
56
|
+
font-size: 24px;
|
|
57
|
+
font-weight: 700;
|
|
58
|
+
margin-top: 4px;
|
|
59
|
+
}
|
|
60
|
+
.card-sub {
|
|
61
|
+
font-size: 11px;
|
|
62
|
+
color: var(--text-secondary);
|
|
63
|
+
margin-top: 2px;
|
|
64
|
+
}
|
|
65
|
+
#val-api-cost { color: var(--amber); }
|
|
66
|
+
#val-savings { color: var(--green); }
|
|
67
|
+
#val-cache-rate { color: var(--blue-light); }
|
|
68
|
+
|
|
69
|
+
.chart-section {
|
|
70
|
+
background: var(--bg-card);
|
|
71
|
+
border-radius: 8px;
|
|
72
|
+
padding: 20px;
|
|
73
|
+
margin-bottom: 12px;
|
|
74
|
+
}
|
|
75
|
+
.chart-section h2 {
|
|
76
|
+
font-size: 14px;
|
|
77
|
+
font-weight: 600;
|
|
78
|
+
margin-bottom: 16px;
|
|
79
|
+
}
|
|
80
|
+
.chart-header {
|
|
81
|
+
display: flex;
|
|
82
|
+
justify-content: space-between;
|
|
83
|
+
align-items: center;
|
|
84
|
+
margin-bottom: 16px;
|
|
85
|
+
}
|
|
86
|
+
.chart-header h2 { margin-bottom: 0; }
|
|
87
|
+
.chart-container { min-height: 200px; }
|
|
88
|
+
|
|
89
|
+
.chart-row-3 {
|
|
90
|
+
display: grid;
|
|
91
|
+
grid-template-columns: repeat(3, 1fr);
|
|
92
|
+
gap: 12px;
|
|
93
|
+
margin-bottom: 12px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.granularity-toggle { display: flex; gap: 4px; }
|
|
97
|
+
.granularity-toggle button {
|
|
98
|
+
padding: 4px 12px;
|
|
99
|
+
background: var(--bg-input);
|
|
100
|
+
border: none;
|
|
101
|
+
border-radius: 4px;
|
|
102
|
+
color: var(--text-secondary);
|
|
103
|
+
font-size: 12px;
|
|
104
|
+
cursor: pointer;
|
|
105
|
+
}
|
|
106
|
+
.granularity-toggle button.active {
|
|
107
|
+
background: var(--blue);
|
|
108
|
+
color: white;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.table-controls { display: flex; gap: 8px; }
|
|
112
|
+
.filter-input, .sort-select {
|
|
113
|
+
padding: 6px 10px;
|
|
114
|
+
background: var(--bg-input);
|
|
115
|
+
border: 1px solid var(--border);
|
|
116
|
+
border-radius: 6px;
|
|
117
|
+
color: var(--text-primary);
|
|
118
|
+
font-size: 12px;
|
|
119
|
+
}
|
|
120
|
+
.filter-input { width: 180px; }
|
|
121
|
+
|
|
122
|
+
.table-container { overflow-x: auto; }
|
|
123
|
+
.table-container table {
|
|
124
|
+
width: 100%;
|
|
125
|
+
border-collapse: collapse;
|
|
126
|
+
font-size: 12px;
|
|
127
|
+
}
|
|
128
|
+
.table-container th {
|
|
129
|
+
text-align: left;
|
|
130
|
+
padding: 10px 8px;
|
|
131
|
+
border-bottom: 2px solid var(--bg-input);
|
|
132
|
+
color: var(--text-muted);
|
|
133
|
+
text-transform: uppercase;
|
|
134
|
+
font-size: 10px;
|
|
135
|
+
letter-spacing: 1px;
|
|
136
|
+
cursor: pointer;
|
|
137
|
+
}
|
|
138
|
+
.table-container th.align-right,
|
|
139
|
+
.table-container td.align-right { text-align: right; }
|
|
140
|
+
.table-container td {
|
|
141
|
+
padding: 10px 8px;
|
|
142
|
+
border-bottom: 1px solid var(--bg-primary);
|
|
143
|
+
color: var(--text-secondary);
|
|
144
|
+
}
|
|
145
|
+
.table-container tfoot td {
|
|
146
|
+
border-top: 2px solid var(--bg-input);
|
|
147
|
+
font-weight: 600;
|
|
148
|
+
color: var(--text-primary);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.tag {
|
|
152
|
+
display: inline-block;
|
|
153
|
+
padding: 2px 8px;
|
|
154
|
+
border-radius: 4px;
|
|
155
|
+
font-size: 11px;
|
|
156
|
+
}
|
|
157
|
+
.tag-project { background: #1e3a5f; color: var(--blue-light); }
|
|
158
|
+
.tag-model-sonnet { background: #1e3a5f; color: var(--blue-light); }
|
|
159
|
+
.tag-model-opus { background: #3b1764; color: #c084fc; }
|
|
160
|
+
.tag-model-haiku { background: #1a2e1a; color: var(--green); }
|
|
161
|
+
|
|
162
|
+
.pagination {
|
|
163
|
+
display: flex;
|
|
164
|
+
justify-content: center;
|
|
165
|
+
gap: 4px;
|
|
166
|
+
margin-top: 12px;
|
|
167
|
+
}
|
|
168
|
+
.pagination button {
|
|
169
|
+
padding: 4px 10px;
|
|
170
|
+
background: var(--bg-input);
|
|
171
|
+
border: none;
|
|
172
|
+
border-radius: 4px;
|
|
173
|
+
font-size: 11px;
|
|
174
|
+
color: var(--text-secondary);
|
|
175
|
+
cursor: pointer;
|
|
176
|
+
}
|
|
177
|
+
.pagination button.active {
|
|
178
|
+
background: var(--blue);
|
|
179
|
+
color: white;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.date-picker {
|
|
183
|
+
display: flex;
|
|
184
|
+
align-items: center;
|
|
185
|
+
gap: 8px;
|
|
186
|
+
}
|
|
187
|
+
.date-picker input {
|
|
188
|
+
padding: 6px 10px;
|
|
189
|
+
background: var(--bg-input);
|
|
190
|
+
border: 1px solid var(--border);
|
|
191
|
+
border-radius: 6px;
|
|
192
|
+
color: var(--text-primary);
|
|
193
|
+
font-size: 12px;
|
|
194
|
+
}
|
|
195
|
+
.date-picker span { color: var(--text-secondary); font-size: 12px; }
|
|
196
|
+
|
|
197
|
+
.plan-selector select, .plan-selector input {
|
|
198
|
+
padding: 6px 10px;
|
|
199
|
+
background: var(--bg-input);
|
|
200
|
+
border: 1px solid var(--border);
|
|
201
|
+
border-radius: 6px;
|
|
202
|
+
color: var(--text-primary);
|
|
203
|
+
font-size: 12px;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.d3-tooltip {
|
|
207
|
+
position: absolute;
|
|
208
|
+
padding: 8px 12px;
|
|
209
|
+
background: rgba(15, 23, 42, 0.95);
|
|
210
|
+
border: 1px solid var(--border);
|
|
211
|
+
border-radius: 6px;
|
|
212
|
+
font-size: 12px;
|
|
213
|
+
color: var(--text-primary);
|
|
214
|
+
pointer-events: none;
|
|
215
|
+
z-index: 100;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
@media (max-width: 768px) {
|
|
219
|
+
.summary-cards { grid-template-columns: repeat(2, 1fr); }
|
|
220
|
+
.chart-row-3 { grid-template-columns: 1fr; }
|
|
221
|
+
}
|