bashstats 0.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/debug-hook.cjs +38 -0
- package/dist/chunk-2KXMOTBO.js +1370 -0
- package/dist/chunk-2KXMOTBO.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +396 -0
- package/dist/cli.js.map +1 -0
- package/dist/hooks/chunk-EFVDQUHM.js +566 -0
- package/dist/hooks/chunk-EFVDQUHM.js.map +1 -0
- package/dist/hooks/notification.js +8 -0
- package/dist/hooks/notification.js.map +1 -0
- package/dist/hooks/permission-request.js +8 -0
- package/dist/hooks/permission-request.js.map +1 -0
- package/dist/hooks/post-tool-failure.js +8 -0
- package/dist/hooks/post-tool-failure.js.map +1 -0
- package/dist/hooks/post-tool-use.js +8 -0
- package/dist/hooks/post-tool-use.js.map +1 -0
- package/dist/hooks/pre-compact.js +8 -0
- package/dist/hooks/pre-compact.js.map +1 -0
- package/dist/hooks/pre-tool-use.js +8 -0
- package/dist/hooks/pre-tool-use.js.map +1 -0
- package/dist/hooks/session-start.js +8 -0
- package/dist/hooks/session-start.js.map +1 -0
- package/dist/hooks/setup.js +8 -0
- package/dist/hooks/setup.js.map +1 -0
- package/dist/hooks/stop.js +8 -0
- package/dist/hooks/stop.js.map +1 -0
- package/dist/hooks/subagent-start.js +8 -0
- package/dist/hooks/subagent-start.js.map +1 -0
- package/dist/hooks/subagent-stop.js +8 -0
- package/dist/hooks/subagent-stop.js.map +1 -0
- package/dist/hooks/user-prompt-submit.js +8 -0
- package/dist/hooks/user-prompt-submit.js.map +1 -0
- package/dist/index.d.ts +355 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/static/index.html +1884 -0
- package/nul +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,1884 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>bashstats</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
|
10
|
+
<style>
|
|
11
|
+
/* ===== CSS CUSTOM PROPERTIES / THEMES ===== */
|
|
12
|
+
:root {
|
|
13
|
+
--bg-primary: #FFF8F0;
|
|
14
|
+
--bg-card: #FFFFFF;
|
|
15
|
+
--bg-accent: #FFE5D0;
|
|
16
|
+
--border: #1B2A4A;
|
|
17
|
+
--shadow: #1B2A4A;
|
|
18
|
+
--text-primary: #1B2A4A;
|
|
19
|
+
--text-secondary: #5A6B8A;
|
|
20
|
+
--accent: #FF8C5A;
|
|
21
|
+
--accent-light: #FFBF9B;
|
|
22
|
+
--success: #22c55e;
|
|
23
|
+
--error: #ef4444;
|
|
24
|
+
--warning: #fbbf24;
|
|
25
|
+
--tier-bronze: #CD7F32;
|
|
26
|
+
--tier-silver: #C0C0C0;
|
|
27
|
+
--tier-gold: #FFD700;
|
|
28
|
+
--tier-diamond: #B9F2FF;
|
|
29
|
+
--tier-obsidian: #2D1B69;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
:root[data-theme="dark"] {
|
|
33
|
+
--bg-primary: #1a1a2e;
|
|
34
|
+
--bg-card: #25253e;
|
|
35
|
+
--bg-accent: #2a2a4a;
|
|
36
|
+
--border: #FF8C5A;
|
|
37
|
+
--shadow: #0d0d1a;
|
|
38
|
+
--text-primary: #FFF8F0;
|
|
39
|
+
--text-secondary: #a0a8c0;
|
|
40
|
+
--accent: #FF8C5A;
|
|
41
|
+
--accent-light: #FFBF9B;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
:root[data-theme="teal"] {
|
|
45
|
+
--bg-primary: #FFFFFF;
|
|
46
|
+
--bg-card: #F5F5F5;
|
|
47
|
+
--bg-accent: #E0F2F1;
|
|
48
|
+
--border: #333333;
|
|
49
|
+
--shadow: #333333;
|
|
50
|
+
--text-primary: #333333;
|
|
51
|
+
--text-secondary: #666666;
|
|
52
|
+
--accent: #4DB6AC;
|
|
53
|
+
--accent-light: #80CBC4;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* ===== RESET & BASE ===== */
|
|
57
|
+
*, *::before, *::after {
|
|
58
|
+
margin: 0;
|
|
59
|
+
padding: 0;
|
|
60
|
+
box-sizing: border-box;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
body {
|
|
64
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
65
|
+
background: var(--bg-primary);
|
|
66
|
+
color: var(--text-primary);
|
|
67
|
+
line-height: 1.5;
|
|
68
|
+
min-height: 100vh;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.mono {
|
|
72
|
+
font-family: 'JetBrains Mono', monospace;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* ===== LAYOUT ===== */
|
|
76
|
+
.container {
|
|
77
|
+
max-width: 1200px;
|
|
78
|
+
margin: 0 auto;
|
|
79
|
+
padding: 0 16px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* ===== HEADER ===== */
|
|
83
|
+
.header {
|
|
84
|
+
background: var(--bg-card);
|
|
85
|
+
border-bottom: 3px solid var(--border);
|
|
86
|
+
padding: 16px 0;
|
|
87
|
+
position: sticky;
|
|
88
|
+
top: 0;
|
|
89
|
+
z-index: 100;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.header-inner {
|
|
93
|
+
display: flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
justify-content: space-between;
|
|
96
|
+
gap: 16px;
|
|
97
|
+
flex-wrap: wrap;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.header-left {
|
|
101
|
+
display: flex;
|
|
102
|
+
align-items: center;
|
|
103
|
+
gap: 16px;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.header-title {
|
|
107
|
+
font-family: 'JetBrains Mono', monospace;
|
|
108
|
+
font-size: 24px;
|
|
109
|
+
font-weight: 700;
|
|
110
|
+
color: var(--text-primary);
|
|
111
|
+
letter-spacing: -0.5px;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.header-title span {
|
|
115
|
+
color: var(--accent);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.rank-badge {
|
|
119
|
+
display: inline-flex;
|
|
120
|
+
align-items: center;
|
|
121
|
+
gap: 6px;
|
|
122
|
+
padding: 4px 12px;
|
|
123
|
+
border: 3px solid var(--border);
|
|
124
|
+
border-radius: 2px;
|
|
125
|
+
font-family: 'JetBrains Mono', monospace;
|
|
126
|
+
font-size: 13px;
|
|
127
|
+
font-weight: 700;
|
|
128
|
+
background: var(--bg-accent);
|
|
129
|
+
box-shadow: 3px 3px 0 var(--shadow);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.rank-dot {
|
|
133
|
+
width: 10px;
|
|
134
|
+
height: 10px;
|
|
135
|
+
border-radius: 50%;
|
|
136
|
+
display: inline-block;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.header-right {
|
|
140
|
+
display: flex;
|
|
141
|
+
align-items: center;
|
|
142
|
+
gap: 16px;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.xp-bar-container {
|
|
146
|
+
display: flex;
|
|
147
|
+
align-items: center;
|
|
148
|
+
gap: 8px;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.xp-bar-label {
|
|
152
|
+
font-family: 'JetBrains Mono', monospace;
|
|
153
|
+
font-size: 11px;
|
|
154
|
+
color: var(--text-secondary);
|
|
155
|
+
white-space: nowrap;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.xp-bar-track {
|
|
159
|
+
width: 140px;
|
|
160
|
+
height: 14px;
|
|
161
|
+
background: var(--bg-accent);
|
|
162
|
+
border: 2px solid var(--border);
|
|
163
|
+
border-radius: 2px;
|
|
164
|
+
overflow: hidden;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.xp-bar-fill {
|
|
168
|
+
height: 100%;
|
|
169
|
+
background: var(--accent);
|
|
170
|
+
transition: width 0.5s ease;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.streak-display {
|
|
174
|
+
display: flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
gap: 4px;
|
|
177
|
+
font-family: 'JetBrains Mono', monospace;
|
|
178
|
+
font-size: 14px;
|
|
179
|
+
font-weight: 600;
|
|
180
|
+
color: var(--accent);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* ===== TAB NAVIGATION ===== */
|
|
184
|
+
.tab-nav {
|
|
185
|
+
background: var(--bg-card);
|
|
186
|
+
border-bottom: 3px solid var(--border);
|
|
187
|
+
overflow-x: auto;
|
|
188
|
+
-webkit-overflow-scrolling: touch;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.tab-nav-inner {
|
|
192
|
+
display: flex;
|
|
193
|
+
gap: 0;
|
|
194
|
+
min-width: max-content;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.tab-btn {
|
|
198
|
+
font-family: 'Inter', sans-serif;
|
|
199
|
+
font-size: 14px;
|
|
200
|
+
font-weight: 600;
|
|
201
|
+
padding: 12px 24px;
|
|
202
|
+
border: none;
|
|
203
|
+
border-bottom: 3px solid transparent;
|
|
204
|
+
background: transparent;
|
|
205
|
+
color: var(--text-secondary);
|
|
206
|
+
cursor: pointer;
|
|
207
|
+
transition: all 0.15s ease;
|
|
208
|
+
white-space: nowrap;
|
|
209
|
+
margin-bottom: -3px;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.tab-btn:hover {
|
|
213
|
+
color: var(--text-primary);
|
|
214
|
+
background: var(--bg-accent);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.tab-btn.active {
|
|
218
|
+
color: var(--text-primary);
|
|
219
|
+
background: var(--bg-accent);
|
|
220
|
+
border-bottom: 3px solid var(--accent);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* ===== TAB CONTENT ===== */
|
|
224
|
+
.tab-content {
|
|
225
|
+
display: none;
|
|
226
|
+
padding: 24px 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.tab-content.active {
|
|
230
|
+
display: block;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* ===== CARDS ===== */
|
|
234
|
+
.card {
|
|
235
|
+
background: var(--bg-card);
|
|
236
|
+
border: 3px solid var(--border);
|
|
237
|
+
border-radius: 2px;
|
|
238
|
+
box-shadow: 6px 6px 0 var(--shadow);
|
|
239
|
+
padding: 20px;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.card-title {
|
|
243
|
+
font-size: 13px;
|
|
244
|
+
font-weight: 600;
|
|
245
|
+
color: var(--text-secondary);
|
|
246
|
+
text-transform: uppercase;
|
|
247
|
+
letter-spacing: 0.5px;
|
|
248
|
+
margin-bottom: 12px;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* ===== STAT NUMBER CARDS ===== */
|
|
252
|
+
.stat-grid {
|
|
253
|
+
display: grid;
|
|
254
|
+
grid-template-columns: repeat(3, 1fr);
|
|
255
|
+
gap: 16px;
|
|
256
|
+
margin-bottom: 24px;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.stat-card {
|
|
260
|
+
background: var(--bg-card);
|
|
261
|
+
border: 3px solid var(--border);
|
|
262
|
+
border-radius: 2px;
|
|
263
|
+
box-shadow: 6px 6px 0 var(--shadow);
|
|
264
|
+
padding: 20px;
|
|
265
|
+
text-align: center;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.stat-card-label {
|
|
269
|
+
font-size: 12px;
|
|
270
|
+
font-weight: 600;
|
|
271
|
+
color: var(--text-secondary);
|
|
272
|
+
text-transform: uppercase;
|
|
273
|
+
letter-spacing: 0.5px;
|
|
274
|
+
margin-bottom: 8px;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.stat-card-number {
|
|
278
|
+
font-family: 'JetBrains Mono', monospace;
|
|
279
|
+
font-size: 36px;
|
|
280
|
+
font-weight: 700;
|
|
281
|
+
color: var(--text-primary);
|
|
282
|
+
line-height: 1.1;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.stat-card-sub {
|
|
286
|
+
font-size: 11px;
|
|
287
|
+
color: var(--text-secondary);
|
|
288
|
+
margin-top: 4px;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* ===== SPARKLINE ===== */
|
|
292
|
+
.sparkline-section {
|
|
293
|
+
margin-bottom: 24px;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.sparkline-container {
|
|
297
|
+
display: flex;
|
|
298
|
+
align-items: flex-end;
|
|
299
|
+
gap: 3px;
|
|
300
|
+
height: 60px;
|
|
301
|
+
padding: 8px 0;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.sparkline-bar {
|
|
305
|
+
flex: 1;
|
|
306
|
+
min-width: 4px;
|
|
307
|
+
background: var(--accent);
|
|
308
|
+
border-radius: 1px 1px 0 0;
|
|
309
|
+
transition: height 0.3s ease;
|
|
310
|
+
position: relative;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.sparkline-bar:hover {
|
|
314
|
+
background: var(--border);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.sparkline-bar[data-tooltip]:hover::after {
|
|
318
|
+
content: attr(data-tooltip);
|
|
319
|
+
position: absolute;
|
|
320
|
+
bottom: 100%;
|
|
321
|
+
left: 50%;
|
|
322
|
+
transform: translateX(-50%);
|
|
323
|
+
background: var(--border);
|
|
324
|
+
color: var(--bg-primary);
|
|
325
|
+
padding: 2px 6px;
|
|
326
|
+
font-size: 10px;
|
|
327
|
+
font-family: 'JetBrains Mono', monospace;
|
|
328
|
+
white-space: nowrap;
|
|
329
|
+
border-radius: 2px;
|
|
330
|
+
margin-bottom: 4px;
|
|
331
|
+
z-index: 10;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.sparkline-labels {
|
|
335
|
+
display: flex;
|
|
336
|
+
justify-content: space-between;
|
|
337
|
+
font-size: 10px;
|
|
338
|
+
color: var(--text-secondary);
|
|
339
|
+
font-family: 'JetBrains Mono', monospace;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/* ===== OVERVIEW TOP ROW ===== */
|
|
343
|
+
.overview-top {
|
|
344
|
+
display: grid;
|
|
345
|
+
grid-template-columns: 1fr 1fr;
|
|
346
|
+
gap: 16px;
|
|
347
|
+
margin-bottom: 16px;
|
|
348
|
+
align-items: stretch;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.overview-top > .card {
|
|
352
|
+
display: flex;
|
|
353
|
+
flex-direction: column;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.overview-top > .card > .recent-badges-list,
|
|
357
|
+
.overview-top > .card > .rank-card-inner {
|
|
358
|
+
flex: 1;
|
|
359
|
+
display: flex;
|
|
360
|
+
align-items: center;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.overview-top > .card > .recent-badges-list {
|
|
364
|
+
align-items: flex-start;
|
|
365
|
+
align-content: center;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
@media (max-width: 768px) {
|
|
369
|
+
.overview-top {
|
|
370
|
+
grid-template-columns: 1fr;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/* ===== RECENT BADGES ===== */
|
|
375
|
+
.recent-badges-list {
|
|
376
|
+
display: flex;
|
|
377
|
+
flex-wrap: wrap;
|
|
378
|
+
gap: 10px;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.recent-badge-tile {
|
|
382
|
+
width: 80px;
|
|
383
|
+
height: 80px;
|
|
384
|
+
display: flex;
|
|
385
|
+
flex-direction: column;
|
|
386
|
+
align-items: center;
|
|
387
|
+
justify-content: center;
|
|
388
|
+
gap: 6px;
|
|
389
|
+
border: 3px solid var(--border);
|
|
390
|
+
border-radius: 2px;
|
|
391
|
+
box-shadow: 4px 4px 0 var(--shadow);
|
|
392
|
+
cursor: default;
|
|
393
|
+
transition: transform 0.1s ease, box-shadow 0.1s ease;
|
|
394
|
+
text-align: center;
|
|
395
|
+
padding: 6px;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.recent-badge-tile:hover {
|
|
399
|
+
transform: translate(-2px, -2px);
|
|
400
|
+
box-shadow: 6px 6px 0 var(--shadow);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.recent-badge-tile .badge-tile-icon {
|
|
404
|
+
font-size: 28px;
|
|
405
|
+
line-height: 1;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.recent-badge-tile .badge-tile-name {
|
|
409
|
+
font-weight: 700;
|
|
410
|
+
font-size: 10px;
|
|
411
|
+
line-height: 1.2;
|
|
412
|
+
overflow: hidden;
|
|
413
|
+
text-overflow: ellipsis;
|
|
414
|
+
display: -webkit-box;
|
|
415
|
+
-webkit-line-clamp: 2;
|
|
416
|
+
-webkit-box-orient: vertical;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.tier-dot {
|
|
420
|
+
width: 12px;
|
|
421
|
+
height: 12px;
|
|
422
|
+
border-radius: 50%;
|
|
423
|
+
flex-shrink: 0;
|
|
424
|
+
border: 1px solid var(--border);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/* ===== RANK CARD ===== */
|
|
428
|
+
.rank-card-inner {
|
|
429
|
+
display: flex;
|
|
430
|
+
align-items: center;
|
|
431
|
+
gap: 16px;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.rank-card-icon {
|
|
435
|
+
display: flex;
|
|
436
|
+
align-items: center;
|
|
437
|
+
justify-content: center;
|
|
438
|
+
width: 52px;
|
|
439
|
+
height: 52px;
|
|
440
|
+
border: 3px solid var(--border);
|
|
441
|
+
border-radius: 2px;
|
|
442
|
+
box-shadow: 4px 4px 0 var(--shadow);
|
|
443
|
+
flex-shrink: 0;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.rank-card-icon-text {
|
|
447
|
+
font-family: 'JetBrains Mono', monospace;
|
|
448
|
+
font-size: 22px;
|
|
449
|
+
font-weight: 700;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.rank-card-info {
|
|
453
|
+
flex: 1;
|
|
454
|
+
min-width: 0;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.rank-card-rank {
|
|
458
|
+
font-family: 'JetBrains Mono', monospace;
|
|
459
|
+
font-size: 18px;
|
|
460
|
+
font-weight: 700;
|
|
461
|
+
margin-bottom: 2px;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.rank-card-xp {
|
|
465
|
+
font-family: 'JetBrains Mono', monospace;
|
|
466
|
+
font-size: 12px;
|
|
467
|
+
color: var(--text-secondary);
|
|
468
|
+
margin-bottom: 8px;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.rank-progress-track {
|
|
472
|
+
width: 100%;
|
|
473
|
+
height: 14px;
|
|
474
|
+
background: var(--bg-accent);
|
|
475
|
+
border: 2px solid var(--border);
|
|
476
|
+
border-radius: 2px;
|
|
477
|
+
overflow: hidden;
|
|
478
|
+
margin-bottom: 4px;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.rank-progress-fill {
|
|
482
|
+
height: 100%;
|
|
483
|
+
background: var(--accent);
|
|
484
|
+
transition: width 0.5s ease;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.rank-progress-label {
|
|
488
|
+
font-size: 10px;
|
|
489
|
+
color: var(--text-secondary);
|
|
490
|
+
font-family: 'JetBrains Mono', monospace;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/* ===== STATS TAB TABLES ===== */
|
|
494
|
+
.stats-grid-layout {
|
|
495
|
+
display: grid;
|
|
496
|
+
grid-template-columns: repeat(2, 1fr);
|
|
497
|
+
gap: 16px;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
@media (max-width: 768px) {
|
|
501
|
+
.stats-grid-layout {
|
|
502
|
+
grid-template-columns: 1fr;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.stats-section {
|
|
507
|
+
margin-bottom: 0;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.stats-table {
|
|
511
|
+
width: 100%;
|
|
512
|
+
border-collapse: collapse;
|
|
513
|
+
font-size: 13px;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.stats-table th {
|
|
517
|
+
text-align: left;
|
|
518
|
+
font-weight: 600;
|
|
519
|
+
color: var(--text-secondary);
|
|
520
|
+
text-transform: uppercase;
|
|
521
|
+
letter-spacing: 0.5px;
|
|
522
|
+
font-size: 11px;
|
|
523
|
+
padding: 8px 12px;
|
|
524
|
+
border-bottom: 2px solid var(--border);
|
|
525
|
+
background: var(--bg-accent);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.stats-table td {
|
|
529
|
+
padding: 8px 12px;
|
|
530
|
+
border-bottom: 1px solid var(--bg-accent);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
.stats-table td:last-child {
|
|
534
|
+
font-family: 'JetBrains Mono', monospace;
|
|
535
|
+
font-weight: 600;
|
|
536
|
+
text-align: right;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.stats-table tr:hover td {
|
|
540
|
+
background: var(--bg-accent);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/* ===== ACHIEVEMENTS TAB ===== */
|
|
544
|
+
.badge-category-title {
|
|
545
|
+
font-size: 16px;
|
|
546
|
+
font-weight: 700;
|
|
547
|
+
margin-bottom: 12px;
|
|
548
|
+
padding-bottom: 6px;
|
|
549
|
+
border-bottom: 2px solid var(--border);
|
|
550
|
+
text-transform: capitalize;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
.badge-category-section {
|
|
554
|
+
margin-bottom: 28px;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.badge-grid {
|
|
558
|
+
display: grid;
|
|
559
|
+
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
560
|
+
gap: 12px;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.badge-card {
|
|
564
|
+
background: var(--bg-card);
|
|
565
|
+
border: 3px solid var(--border);
|
|
566
|
+
border-radius: 2px;
|
|
567
|
+
box-shadow: 4px 4px 0 var(--shadow);
|
|
568
|
+
padding: 14px;
|
|
569
|
+
display: flex;
|
|
570
|
+
flex-direction: column;
|
|
571
|
+
gap: 8px;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.badge-card.locked {
|
|
575
|
+
opacity: 0.6;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
.badge-card.secret-locked {
|
|
579
|
+
opacity: 0.5;
|
|
580
|
+
border-style: dashed;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.badge-header {
|
|
584
|
+
display: flex;
|
|
585
|
+
align-items: center;
|
|
586
|
+
gap: 8px;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.badge-name {
|
|
590
|
+
font-weight: 600;
|
|
591
|
+
font-size: 14px;
|
|
592
|
+
flex: 1;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.badge-tier-label {
|
|
596
|
+
font-size: 10px;
|
|
597
|
+
font-family: 'JetBrains Mono', monospace;
|
|
598
|
+
font-weight: 700;
|
|
599
|
+
text-transform: uppercase;
|
|
600
|
+
padding: 2px 6px;
|
|
601
|
+
border: 2px solid var(--border);
|
|
602
|
+
border-radius: 2px;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.badge-description {
|
|
606
|
+
font-size: 12px;
|
|
607
|
+
color: var(--text-secondary);
|
|
608
|
+
line-height: 1.4;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.badge-progress-row {
|
|
612
|
+
display: flex;
|
|
613
|
+
align-items: center;
|
|
614
|
+
gap: 8px;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
.badge-progress-track {
|
|
618
|
+
flex: 1;
|
|
619
|
+
height: 8px;
|
|
620
|
+
background: var(--bg-accent);
|
|
621
|
+
border: 1px solid var(--border);
|
|
622
|
+
border-radius: 1px;
|
|
623
|
+
overflow: hidden;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.badge-progress-fill {
|
|
627
|
+
height: 100%;
|
|
628
|
+
background: var(--accent);
|
|
629
|
+
transition: width 0.3s ease;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
.badge-progress-fill.maxed {
|
|
633
|
+
background: var(--success);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.badge-progress-text {
|
|
637
|
+
font-family: 'JetBrains Mono', monospace;
|
|
638
|
+
font-size: 11px;
|
|
639
|
+
color: var(--text-secondary);
|
|
640
|
+
white-space: nowrap;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/* ===== RECORDS TAB ===== */
|
|
644
|
+
.records-grid {
|
|
645
|
+
display: grid;
|
|
646
|
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
647
|
+
gap: 16px;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
.record-card {
|
|
651
|
+
background: var(--bg-card);
|
|
652
|
+
border: 3px solid var(--border);
|
|
653
|
+
border-radius: 2px;
|
|
654
|
+
box-shadow: 6px 6px 0 var(--shadow);
|
|
655
|
+
padding: 20px;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
.record-label {
|
|
659
|
+
font-size: 12px;
|
|
660
|
+
font-weight: 600;
|
|
661
|
+
color: var(--text-secondary);
|
|
662
|
+
text-transform: uppercase;
|
|
663
|
+
letter-spacing: 0.5px;
|
|
664
|
+
margin-bottom: 6px;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
.record-value {
|
|
668
|
+
font-family: 'JetBrains Mono', monospace;
|
|
669
|
+
font-size: 28px;
|
|
670
|
+
font-weight: 700;
|
|
671
|
+
color: var(--text-primary);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.record-sub {
|
|
675
|
+
font-size: 11px;
|
|
676
|
+
color: var(--text-secondary);
|
|
677
|
+
margin-top: 4px;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/* ===== HEATMAP ===== */
|
|
681
|
+
.heatmap-section {
|
|
682
|
+
margin-bottom: 24px;
|
|
683
|
+
overflow-x: auto;
|
|
684
|
+
display: flex;
|
|
685
|
+
flex-direction: column;
|
|
686
|
+
align-items: center;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.heatmap-wrapper {
|
|
690
|
+
display: inline-flex;
|
|
691
|
+
flex-direction: column;
|
|
692
|
+
gap: 4px;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.heatmap-months {
|
|
696
|
+
display: flex;
|
|
697
|
+
padding-left: 32px;
|
|
698
|
+
font-size: 10px;
|
|
699
|
+
color: var(--text-secondary);
|
|
700
|
+
font-family: 'JetBrains Mono', monospace;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.heatmap-month-label {
|
|
704
|
+
text-align: left;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.heatmap-year-label {
|
|
708
|
+
font-size: 10px;
|
|
709
|
+
color: var(--text-secondary);
|
|
710
|
+
font-family: 'JetBrains Mono', monospace;
|
|
711
|
+
display: flex;
|
|
712
|
+
align-items: center;
|
|
713
|
+
padding-left: 6px;
|
|
714
|
+
white-space: nowrap;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.heatmap-body {
|
|
718
|
+
display: flex;
|
|
719
|
+
gap: 2px;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
.heatmap-day-labels {
|
|
723
|
+
display: flex;
|
|
724
|
+
flex-direction: column;
|
|
725
|
+
gap: 2px;
|
|
726
|
+
padding-right: 4px;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.heatmap-day-label {
|
|
730
|
+
width: 24px;
|
|
731
|
+
height: 14px;
|
|
732
|
+
font-size: 9px;
|
|
733
|
+
font-family: 'JetBrains Mono', monospace;
|
|
734
|
+
color: var(--text-secondary);
|
|
735
|
+
display: flex;
|
|
736
|
+
align-items: center;
|
|
737
|
+
justify-content: flex-end;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
.heatmap-grid {
|
|
741
|
+
display: flex;
|
|
742
|
+
gap: 2px;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
.heatmap-week {
|
|
746
|
+
display: flex;
|
|
747
|
+
flex-direction: column;
|
|
748
|
+
gap: 2px;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.heatmap-cell {
|
|
752
|
+
width: 14px;
|
|
753
|
+
height: 14px;
|
|
754
|
+
border-radius: 1px;
|
|
755
|
+
border: 1px solid var(--bg-accent);
|
|
756
|
+
position: relative;
|
|
757
|
+
cursor: default;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
.heatmap-cell:hover::after {
|
|
761
|
+
content: attr(data-tooltip);
|
|
762
|
+
position: absolute;
|
|
763
|
+
bottom: 100%;
|
|
764
|
+
left: 50%;
|
|
765
|
+
transform: translateX(-50%);
|
|
766
|
+
background: var(--border);
|
|
767
|
+
color: var(--bg-primary);
|
|
768
|
+
padding: 3px 8px;
|
|
769
|
+
font-size: 10px;
|
|
770
|
+
font-family: 'JetBrains Mono', monospace;
|
|
771
|
+
white-space: nowrap;
|
|
772
|
+
border-radius: 2px;
|
|
773
|
+
margin-bottom: 4px;
|
|
774
|
+
z-index: 10;
|
|
775
|
+
pointer-events: none;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
.heatmap-legend {
|
|
779
|
+
display: flex;
|
|
780
|
+
align-items: center;
|
|
781
|
+
gap: 4px;
|
|
782
|
+
margin-top: 8px;
|
|
783
|
+
padding-left: 32px;
|
|
784
|
+
font-size: 10px;
|
|
785
|
+
color: var(--text-secondary);
|
|
786
|
+
font-family: 'JetBrains Mono', monospace;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
.heatmap-legend-cell {
|
|
790
|
+
width: 14px;
|
|
791
|
+
height: 14px;
|
|
792
|
+
border-radius: 1px;
|
|
793
|
+
border: 1px solid var(--bg-accent);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/* Session list */
|
|
797
|
+
.session-list {
|
|
798
|
+
display: flex;
|
|
799
|
+
flex-direction: column;
|
|
800
|
+
gap: 8px;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
.session-item {
|
|
804
|
+
display: grid;
|
|
805
|
+
grid-template-columns: 140px 1fr 100px 100px 100px;
|
|
806
|
+
gap: 12px;
|
|
807
|
+
align-items: center;
|
|
808
|
+
padding: 10px 14px;
|
|
809
|
+
background: var(--bg-card);
|
|
810
|
+
border: 2px solid var(--border);
|
|
811
|
+
border-radius: 2px;
|
|
812
|
+
font-size: 13px;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
.session-date {
|
|
816
|
+
font-family: 'JetBrains Mono', monospace;
|
|
817
|
+
font-size: 12px;
|
|
818
|
+
color: var(--text-secondary);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
.session-project {
|
|
822
|
+
font-weight: 600;
|
|
823
|
+
overflow: hidden;
|
|
824
|
+
text-overflow: ellipsis;
|
|
825
|
+
white-space: nowrap;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
.session-stat {
|
|
829
|
+
font-family: 'JetBrains Mono', monospace;
|
|
830
|
+
font-size: 12px;
|
|
831
|
+
text-align: right;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/* ===== SETTINGS TAB ===== */
|
|
835
|
+
.settings-section {
|
|
836
|
+
margin-bottom: 24px;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
.settings-row {
|
|
840
|
+
display: flex;
|
|
841
|
+
align-items: center;
|
|
842
|
+
justify-content: space-between;
|
|
843
|
+
padding: 12px 0;
|
|
844
|
+
border-bottom: 1px solid var(--bg-accent);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
.settings-label {
|
|
848
|
+
font-weight: 600;
|
|
849
|
+
font-size: 14px;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
.settings-desc {
|
|
853
|
+
font-size: 12px;
|
|
854
|
+
color: var(--text-secondary);
|
|
855
|
+
margin-top: 2px;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
select.settings-select {
|
|
859
|
+
font-family: 'Inter', sans-serif;
|
|
860
|
+
font-size: 14px;
|
|
861
|
+
padding: 8px 12px;
|
|
862
|
+
border: 3px solid var(--border);
|
|
863
|
+
border-radius: 2px;
|
|
864
|
+
background: var(--bg-card);
|
|
865
|
+
color: var(--text-primary);
|
|
866
|
+
cursor: pointer;
|
|
867
|
+
box-shadow: 3px 3px 0 var(--shadow);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
.settings-status {
|
|
871
|
+
display: inline-flex;
|
|
872
|
+
align-items: center;
|
|
873
|
+
gap: 6px;
|
|
874
|
+
font-family: 'JetBrains Mono', monospace;
|
|
875
|
+
font-size: 13px;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
.status-dot {
|
|
879
|
+
width: 8px;
|
|
880
|
+
height: 8px;
|
|
881
|
+
border-radius: 50%;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
.status-dot.active { background: var(--success); }
|
|
885
|
+
.status-dot.inactive { background: var(--error); }
|
|
886
|
+
|
|
887
|
+
.settings-link {
|
|
888
|
+
font-family: 'JetBrains Mono', monospace;
|
|
889
|
+
font-size: 13px;
|
|
890
|
+
color: var(--accent);
|
|
891
|
+
text-decoration: underline;
|
|
892
|
+
cursor: pointer;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
.settings-link:hover {
|
|
896
|
+
color: var(--text-primary);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/* ===== EMPTY STATE ===== */
|
|
900
|
+
.empty-state {
|
|
901
|
+
text-align: center;
|
|
902
|
+
padding: 60px 20px;
|
|
903
|
+
color: var(--text-secondary);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
.empty-state-icon {
|
|
907
|
+
font-size: 48px;
|
|
908
|
+
margin-bottom: 12px;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
.empty-state-text {
|
|
912
|
+
font-size: 16px;
|
|
913
|
+
font-weight: 600;
|
|
914
|
+
margin-bottom: 4px;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
.empty-state-sub {
|
|
918
|
+
font-size: 13px;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/* ===== LOADING ===== */
|
|
922
|
+
.loading-spinner {
|
|
923
|
+
display: flex;
|
|
924
|
+
align-items: center;
|
|
925
|
+
justify-content: center;
|
|
926
|
+
padding: 40px;
|
|
927
|
+
color: var(--text-secondary);
|
|
928
|
+
font-family: 'JetBrains Mono', monospace;
|
|
929
|
+
font-size: 14px;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
.loading-spinner::before {
|
|
933
|
+
content: '';
|
|
934
|
+
width: 20px;
|
|
935
|
+
height: 20px;
|
|
936
|
+
border: 3px solid var(--bg-accent);
|
|
937
|
+
border-top-color: var(--accent);
|
|
938
|
+
border-radius: 50%;
|
|
939
|
+
margin-right: 12px;
|
|
940
|
+
animation: spin 0.8s linear infinite;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
@keyframes spin {
|
|
944
|
+
to { transform: rotate(360deg); }
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/* ===== RESPONSIVE ===== */
|
|
948
|
+
@media (max-width: 768px) {
|
|
949
|
+
.stat-grid {
|
|
950
|
+
grid-template-columns: repeat(2, 1fr);
|
|
951
|
+
}
|
|
952
|
+
.stat-card-number {
|
|
953
|
+
font-size: 28px;
|
|
954
|
+
}
|
|
955
|
+
.overview-bottom {
|
|
956
|
+
grid-template-columns: 1fr;
|
|
957
|
+
}
|
|
958
|
+
.session-item {
|
|
959
|
+
grid-template-columns: 1fr 1fr;
|
|
960
|
+
gap: 6px;
|
|
961
|
+
}
|
|
962
|
+
.session-date {
|
|
963
|
+
grid-column: 1 / -1;
|
|
964
|
+
}
|
|
965
|
+
.header-inner {
|
|
966
|
+
flex-direction: column;
|
|
967
|
+
align-items: flex-start;
|
|
968
|
+
}
|
|
969
|
+
.badge-grid {
|
|
970
|
+
grid-template-columns: 1fr;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
@media (max-width: 480px) {
|
|
975
|
+
.stat-grid {
|
|
976
|
+
grid-template-columns: 1fr;
|
|
977
|
+
}
|
|
978
|
+
.tab-btn {
|
|
979
|
+
padding: 10px 16px;
|
|
980
|
+
font-size: 13px;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/* ===== TIER COLORS ===== */
|
|
985
|
+
.tier-locked { color: var(--text-secondary); }
|
|
986
|
+
.tier-bronze { color: var(--tier-bronze); }
|
|
987
|
+
.tier-silver { color: var(--tier-silver); }
|
|
988
|
+
.tier-gold { color: var(--tier-gold); }
|
|
989
|
+
.tier-diamond { color: var(--tier-diamond); }
|
|
990
|
+
.tier-obsidian { color: var(--tier-obsidian); }
|
|
991
|
+
|
|
992
|
+
.tier-bg-locked { background: var(--text-secondary); }
|
|
993
|
+
.tier-bg-bronze { background: var(--tier-bronze); }
|
|
994
|
+
.tier-bg-silver { background: var(--tier-silver); }
|
|
995
|
+
.tier-bg-gold { background: var(--tier-gold); }
|
|
996
|
+
.tier-bg-diamond { background: var(--tier-diamond); }
|
|
997
|
+
.tier-bg-obsidian { background: var(--tier-obsidian); }
|
|
998
|
+
|
|
999
|
+
/* ===== REFRESH INDICATOR ===== */
|
|
1000
|
+
.refresh-indicator {
|
|
1001
|
+
position: fixed;
|
|
1002
|
+
bottom: 16px;
|
|
1003
|
+
right: 16px;
|
|
1004
|
+
font-family: 'JetBrains Mono', monospace;
|
|
1005
|
+
font-size: 10px;
|
|
1006
|
+
color: var(--text-secondary);
|
|
1007
|
+
background: var(--bg-card);
|
|
1008
|
+
border: 2px solid var(--border);
|
|
1009
|
+
border-radius: 2px;
|
|
1010
|
+
padding: 4px 8px;
|
|
1011
|
+
box-shadow: 3px 3px 0 var(--shadow);
|
|
1012
|
+
}
|
|
1013
|
+
</style>
|
|
1014
|
+
</head>
|
|
1015
|
+
<body>
|
|
1016
|
+
|
|
1017
|
+
<!-- ===== HEADER ===== -->
|
|
1018
|
+
<header class="header">
|
|
1019
|
+
<div class="container header-inner">
|
|
1020
|
+
<div class="header-left">
|
|
1021
|
+
<div class="header-title">bash<span>stats</span></div>
|
|
1022
|
+
<div id="header-rank-badge" class="rank-badge" style="display:none;">
|
|
1023
|
+
<span class="rank-dot" id="header-rank-dot"></span>
|
|
1024
|
+
<span id="header-rank-text">Bronze</span>
|
|
1025
|
+
</div>
|
|
1026
|
+
</div>
|
|
1027
|
+
<div class="header-right">
|
|
1028
|
+
<div class="xp-bar-container" id="header-xp-container" style="display:none;">
|
|
1029
|
+
<span class="xp-bar-label" id="header-xp-label">0 XP</span>
|
|
1030
|
+
<div class="xp-bar-track">
|
|
1031
|
+
<div class="xp-bar-fill" id="header-xp-fill" style="width:0%"></div>
|
|
1032
|
+
</div>
|
|
1033
|
+
</div>
|
|
1034
|
+
<div class="streak-display" id="header-streak" style="display:none;">
|
|
1035
|
+
<span id="streak-icon">🔥</span>
|
|
1036
|
+
<span id="streak-days">0d</span>
|
|
1037
|
+
</div>
|
|
1038
|
+
</div>
|
|
1039
|
+
</div>
|
|
1040
|
+
</header>
|
|
1041
|
+
|
|
1042
|
+
<!-- ===== TAB NAVIGATION ===== -->
|
|
1043
|
+
<nav class="tab-nav">
|
|
1044
|
+
<div class="container tab-nav-inner">
|
|
1045
|
+
<button class="tab-btn active" data-tab="overview">Overview</button>
|
|
1046
|
+
<button class="tab-btn" data-tab="stats">Stats</button>
|
|
1047
|
+
<button class="tab-btn" data-tab="achievements">Achievements</button>
|
|
1048
|
+
<button class="tab-btn" data-tab="records">Records</button>
|
|
1049
|
+
<button class="tab-btn" data-tab="settings">Settings</button>
|
|
1050
|
+
</div>
|
|
1051
|
+
</nav>
|
|
1052
|
+
|
|
1053
|
+
<!-- ===== TAB CONTENT ===== -->
|
|
1054
|
+
<main class="container">
|
|
1055
|
+
|
|
1056
|
+
<!-- OVERVIEW TAB -->
|
|
1057
|
+
<div class="tab-content active" id="tab-overview">
|
|
1058
|
+
<div id="overview-loading" class="loading-spinner">Loading stats...</div>
|
|
1059
|
+
<div id="overview-content" style="display:none;">
|
|
1060
|
+
<!-- Top row: badges + rank -->
|
|
1061
|
+
<div class="overview-top">
|
|
1062
|
+
<div class="card">
|
|
1063
|
+
<div class="card-title">Recent Badges</div>
|
|
1064
|
+
<div id="overview-recent-badges" class="recent-badges-list"></div>
|
|
1065
|
+
</div>
|
|
1066
|
+
<div class="card">
|
|
1067
|
+
<div class="card-title">Current Rank</div>
|
|
1068
|
+
<div id="overview-rank-card" class="rank-card-inner"></div>
|
|
1069
|
+
</div>
|
|
1070
|
+
</div>
|
|
1071
|
+
<!-- Stat cards -->
|
|
1072
|
+
<div class="stat-grid" id="overview-stat-cards"></div>
|
|
1073
|
+
<!-- Sparkline -->
|
|
1074
|
+
<div class="sparkline-section card" id="overview-sparkline-card">
|
|
1075
|
+
<div class="card-title">30-Day Activity</div>
|
|
1076
|
+
<div class="sparkline-container" id="overview-sparkline"></div>
|
|
1077
|
+
<div class="sparkline-labels">
|
|
1078
|
+
<span id="sparkline-label-start"></span>
|
|
1079
|
+
<span id="sparkline-label-end"></span>
|
|
1080
|
+
</div>
|
|
1081
|
+
</div>
|
|
1082
|
+
<!-- Heatmap -->
|
|
1083
|
+
<div class="card heatmap-section" style="margin-top:16px;">
|
|
1084
|
+
<div class="card-title">Contribution Heatmap</div>
|
|
1085
|
+
<div class="heatmap-wrapper" id="overview-heatmap"></div>
|
|
1086
|
+
</div>
|
|
1087
|
+
<!-- Recent Sessions -->
|
|
1088
|
+
<div class="card" style="margin-top:16px;">
|
|
1089
|
+
<div class="card-title">Recent Sessions</div>
|
|
1090
|
+
<div class="session-list" id="overview-sessions"></div>
|
|
1091
|
+
</div>
|
|
1092
|
+
</div>
|
|
1093
|
+
</div>
|
|
1094
|
+
|
|
1095
|
+
<!-- STATS TAB -->
|
|
1096
|
+
<div class="tab-content" id="tab-stats">
|
|
1097
|
+
<div id="stats-loading" class="loading-spinner">Loading stats...</div>
|
|
1098
|
+
<div id="stats-content" style="display:none;"></div>
|
|
1099
|
+
</div>
|
|
1100
|
+
|
|
1101
|
+
<!-- ACHIEVEMENTS TAB -->
|
|
1102
|
+
<div class="tab-content" id="tab-achievements">
|
|
1103
|
+
<div id="achievements-loading" class="loading-spinner">Loading achievements...</div>
|
|
1104
|
+
<div id="achievements-content" style="display:none;"></div>
|
|
1105
|
+
</div>
|
|
1106
|
+
|
|
1107
|
+
<!-- RECORDS TAB -->
|
|
1108
|
+
<div class="tab-content" id="tab-records">
|
|
1109
|
+
<div id="records-loading" class="loading-spinner">Loading records...</div>
|
|
1110
|
+
<div id="records-content" style="display:none;"></div>
|
|
1111
|
+
</div>
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
<!-- SETTINGS TAB -->
|
|
1115
|
+
<div class="tab-content" id="tab-settings">
|
|
1116
|
+
<div class="card settings-section">
|
|
1117
|
+
<div class="card-title">Appearance</div>
|
|
1118
|
+
<div class="settings-row">
|
|
1119
|
+
<div>
|
|
1120
|
+
<div class="settings-label">Theme</div>
|
|
1121
|
+
<div class="settings-desc">Choose your dashboard color scheme</div>
|
|
1122
|
+
</div>
|
|
1123
|
+
<select class="settings-select" id="settings-theme">
|
|
1124
|
+
<option value="peach">Peach Cream (Default)</option>
|
|
1125
|
+
<option value="dark">Dark Mode</option>
|
|
1126
|
+
<option value="teal">Classic Teal</option>
|
|
1127
|
+
</select>
|
|
1128
|
+
</div>
|
|
1129
|
+
</div>
|
|
1130
|
+
<div class="card settings-section">
|
|
1131
|
+
<div class="card-title">Hook Status</div>
|
|
1132
|
+
<div class="settings-row">
|
|
1133
|
+
<div>
|
|
1134
|
+
<div class="settings-label">Session Hook</div>
|
|
1135
|
+
<div class="settings-desc">Tracks sessions via Claude Code hook system</div>
|
|
1136
|
+
</div>
|
|
1137
|
+
<div class="settings-status">
|
|
1138
|
+
<span class="status-dot" id="settings-hook-dot"></span>
|
|
1139
|
+
<span id="settings-hook-text">Checking...</span>
|
|
1140
|
+
</div>
|
|
1141
|
+
</div>
|
|
1142
|
+
</div>
|
|
1143
|
+
<div class="card settings-section">
|
|
1144
|
+
<div class="card-title">Data Management</div>
|
|
1145
|
+
<div class="settings-row">
|
|
1146
|
+
<div>
|
|
1147
|
+
<div class="settings-label">Export Data</div>
|
|
1148
|
+
<div class="settings-desc">Download all your stats as JSON</div>
|
|
1149
|
+
</div>
|
|
1150
|
+
<a class="settings-link" href="/api/stats" target="_blank">Export Stats JSON</a>
|
|
1151
|
+
</div>
|
|
1152
|
+
<div class="settings-row">
|
|
1153
|
+
<div>
|
|
1154
|
+
<div class="settings-label">API Endpoints</div>
|
|
1155
|
+
<div class="settings-desc">Access raw API data</div>
|
|
1156
|
+
</div>
|
|
1157
|
+
<div style="display:flex;flex-direction:column;gap:4px;align-items:flex-end;">
|
|
1158
|
+
<a class="settings-link" href="/api/stats" target="_blank">/api/stats</a>
|
|
1159
|
+
<a class="settings-link" href="/api/achievements" target="_blank">/api/achievements</a>
|
|
1160
|
+
<a class="settings-link" href="/api/activity" target="_blank">/api/activity</a>
|
|
1161
|
+
<a class="settings-link" href="/api/sessions" target="_blank">/api/sessions</a>
|
|
1162
|
+
</div>
|
|
1163
|
+
</div>
|
|
1164
|
+
</div>
|
|
1165
|
+
<div class="card settings-section">
|
|
1166
|
+
<div class="card-title">About</div>
|
|
1167
|
+
<div class="settings-row">
|
|
1168
|
+
<div>
|
|
1169
|
+
<div class="settings-label">bashstats</div>
|
|
1170
|
+
<div class="settings-desc">Session analytics for Claude Code. Track your coding sessions, earn badges, and climb the ranks.</div>
|
|
1171
|
+
</div>
|
|
1172
|
+
</div>
|
|
1173
|
+
</div>
|
|
1174
|
+
</div>
|
|
1175
|
+
|
|
1176
|
+
</main>
|
|
1177
|
+
|
|
1178
|
+
<!-- Refresh indicator -->
|
|
1179
|
+
<div class="refresh-indicator" id="refresh-indicator" style="display:none;">
|
|
1180
|
+
<span id="refresh-text"></span>
|
|
1181
|
+
</div>
|
|
1182
|
+
|
|
1183
|
+
<script>
|
|
1184
|
+
/* ===================================================================
|
|
1185
|
+
bashstats Dashboard - Single Page Application
|
|
1186
|
+
=================================================================== */
|
|
1187
|
+
|
|
1188
|
+
// ===== STATE =====
|
|
1189
|
+
const state = {
|
|
1190
|
+
stats: null,
|
|
1191
|
+
achievements: null,
|
|
1192
|
+
activity: null,
|
|
1193
|
+
sessions: null,
|
|
1194
|
+
lastFetch: null,
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
// ===== TIER HELPERS =====
|
|
1198
|
+
const TIER_NAMES = ['Locked', 'Bronze', 'Silver', 'Gold', 'Diamond', 'Obsidian'];
|
|
1199
|
+
const TIER_CLASSES = ['locked', 'bronze', 'silver', 'gold', 'diamond', 'obsidian'];
|
|
1200
|
+
|
|
1201
|
+
function tierClass(tier) {
|
|
1202
|
+
return TIER_CLASSES[tier] || 'locked';
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function tierColor(tier) {
|
|
1206
|
+
const colors = [
|
|
1207
|
+
'var(--text-secondary)',
|
|
1208
|
+
'var(--tier-bronze)',
|
|
1209
|
+
'var(--tier-silver)',
|
|
1210
|
+
'var(--tier-gold)',
|
|
1211
|
+
'var(--tier-diamond)',
|
|
1212
|
+
'var(--tier-obsidian)',
|
|
1213
|
+
];
|
|
1214
|
+
return colors[tier] || colors[0];
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function rankColor(rank) {
|
|
1218
|
+
const map = {
|
|
1219
|
+
'Bronze': 'var(--tier-bronze)',
|
|
1220
|
+
'Silver': 'var(--tier-silver)',
|
|
1221
|
+
'Gold': 'var(--tier-gold)',
|
|
1222
|
+
'Diamond': 'var(--tier-diamond)',
|
|
1223
|
+
'Obsidian': 'var(--tier-obsidian)',
|
|
1224
|
+
};
|
|
1225
|
+
return map[rank] || 'var(--tier-bronze)';
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// ===== FORMAT HELPERS =====
|
|
1229
|
+
function formatNumber(n) {
|
|
1230
|
+
if (n == null) return '0';
|
|
1231
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
1232
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
|
|
1233
|
+
return n.toLocaleString();
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function formatDuration(seconds) {
|
|
1237
|
+
if (!seconds) return '0m';
|
|
1238
|
+
const h = Math.floor(seconds / 3600);
|
|
1239
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
1240
|
+
if (h > 0) return h + 'h ' + m + 'm';
|
|
1241
|
+
return m + 'm';
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function formatHours(seconds) {
|
|
1245
|
+
if (!seconds) return '0';
|
|
1246
|
+
return (seconds / 3600).toFixed(1);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function formatDate(dateStr) {
|
|
1250
|
+
const d = new Date(dateStr);
|
|
1251
|
+
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function formatDateTime(dateStr) {
|
|
1255
|
+
const d = new Date(dateStr);
|
|
1256
|
+
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function escapeHtml(str) {
|
|
1260
|
+
const div = document.createElement('div');
|
|
1261
|
+
div.textContent = str;
|
|
1262
|
+
return div.innerHTML;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// ===== API FETCHING =====
|
|
1266
|
+
async function fetchJSON(url) {
|
|
1267
|
+
try {
|
|
1268
|
+
const res = await fetch(url);
|
|
1269
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
1270
|
+
return await res.json();
|
|
1271
|
+
} catch (e) {
|
|
1272
|
+
console.warn('Fetch failed for', url, e);
|
|
1273
|
+
return null;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
async function loadAllData() {
|
|
1278
|
+
const [stats, achievements, activity, sessions] = await Promise.all([
|
|
1279
|
+
fetchJSON('/api/stats'),
|
|
1280
|
+
fetchJSON('/api/achievements'),
|
|
1281
|
+
fetchJSON('/api/activity'),
|
|
1282
|
+
fetchJSON('/api/sessions'),
|
|
1283
|
+
]);
|
|
1284
|
+
|
|
1285
|
+
state.stats = stats;
|
|
1286
|
+
state.achievements = achievements;
|
|
1287
|
+
state.activity = activity;
|
|
1288
|
+
state.sessions = sessions;
|
|
1289
|
+
state.lastFetch = new Date();
|
|
1290
|
+
|
|
1291
|
+
renderAll();
|
|
1292
|
+
updateRefreshIndicator();
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
function updateRefreshIndicator() {
|
|
1296
|
+
const el = document.getElementById('refresh-indicator');
|
|
1297
|
+
const textEl = document.getElementById('refresh-text');
|
|
1298
|
+
if (state.lastFetch) {
|
|
1299
|
+
el.style.display = 'block';
|
|
1300
|
+
const time = state.lastFetch.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
1301
|
+
textEl.textContent = 'Updated ' + time;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// ===== TAB SWITCHING =====
|
|
1306
|
+
function setupTabs() {
|
|
1307
|
+
const btns = document.querySelectorAll('.tab-btn');
|
|
1308
|
+
btns.forEach(btn => {
|
|
1309
|
+
btn.addEventListener('click', () => {
|
|
1310
|
+
btns.forEach(b => b.classList.remove('active'));
|
|
1311
|
+
btn.classList.add('active');
|
|
1312
|
+
document.querySelectorAll('.tab-content').forEach(tc => tc.classList.remove('active'));
|
|
1313
|
+
const target = document.getElementById('tab-' + btn.dataset.tab);
|
|
1314
|
+
if (target) target.classList.add('active');
|
|
1315
|
+
});
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// ===== THEME SWITCHING =====
|
|
1320
|
+
function setupTheme() {
|
|
1321
|
+
const sel = document.getElementById('settings-theme');
|
|
1322
|
+
const saved = localStorage.getItem('bashstats-theme') || 'peach';
|
|
1323
|
+
sel.value = saved;
|
|
1324
|
+
applyTheme(saved);
|
|
1325
|
+
|
|
1326
|
+
sel.addEventListener('change', () => {
|
|
1327
|
+
const theme = sel.value;
|
|
1328
|
+
applyTheme(theme);
|
|
1329
|
+
localStorage.setItem('bashstats-theme', theme);
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function applyTheme(theme) {
|
|
1334
|
+
const root = document.documentElement;
|
|
1335
|
+
root.removeAttribute('data-theme');
|
|
1336
|
+
if (theme === 'dark') root.setAttribute('data-theme', 'dark');
|
|
1337
|
+
else if (theme === 'teal') root.setAttribute('data-theme', 'teal');
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// ===== RENDER ALL =====
|
|
1341
|
+
function renderAll() {
|
|
1342
|
+
renderHeader();
|
|
1343
|
+
renderOverview();
|
|
1344
|
+
renderStats();
|
|
1345
|
+
renderAchievements();
|
|
1346
|
+
renderRecords();
|
|
1347
|
+
renderSettings();
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// ===== RENDER: HEADER =====
|
|
1351
|
+
function renderHeader() {
|
|
1352
|
+
const xp = state.achievements?.xp;
|
|
1353
|
+
const time = state.stats?.time;
|
|
1354
|
+
|
|
1355
|
+
if (xp) {
|
|
1356
|
+
const rankBadge = document.getElementById('header-rank-badge');
|
|
1357
|
+
const rankDot = document.getElementById('header-rank-dot');
|
|
1358
|
+
const rankText = document.getElementById('header-rank-text');
|
|
1359
|
+
rankBadge.style.display = 'inline-flex';
|
|
1360
|
+
rankDot.style.background = rankColor(xp.rank);
|
|
1361
|
+
rankText.textContent = xp.rank;
|
|
1362
|
+
|
|
1363
|
+
const xpContainer = document.getElementById('header-xp-container');
|
|
1364
|
+
const xpLabel = document.getElementById('header-xp-label');
|
|
1365
|
+
const xpFill = document.getElementById('header-xp-fill');
|
|
1366
|
+
xpContainer.style.display = 'flex';
|
|
1367
|
+
xpLabel.textContent = formatNumber(xp.totalXP) + ' XP';
|
|
1368
|
+
xpFill.style.width = Math.min(100, (xp.progress || 0) * 100) + '%';
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
if (time) {
|
|
1372
|
+
const streakEl = document.getElementById('header-streak');
|
|
1373
|
+
const streakDays = document.getElementById('streak-days');
|
|
1374
|
+
if (time.currentStreak > 0) {
|
|
1375
|
+
streakEl.style.display = 'flex';
|
|
1376
|
+
streakDays.textContent = time.currentStreak + 'd';
|
|
1377
|
+
} else {
|
|
1378
|
+
streakEl.style.display = 'none';
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// ===== RENDER: OVERVIEW =====
|
|
1384
|
+
function renderOverview() {
|
|
1385
|
+
const loading = document.getElementById('overview-loading');
|
|
1386
|
+
const content = document.getElementById('overview-content');
|
|
1387
|
+
|
|
1388
|
+
if (!state.stats) {
|
|
1389
|
+
loading.style.display = 'flex';
|
|
1390
|
+
content.style.display = 'none';
|
|
1391
|
+
// Show empty state if fetch failed
|
|
1392
|
+
if (state.lastFetch) {
|
|
1393
|
+
loading.innerHTML = '';
|
|
1394
|
+
loading.style.display = 'none';
|
|
1395
|
+
content.style.display = 'block';
|
|
1396
|
+
renderEmptyOverview();
|
|
1397
|
+
}
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
loading.style.display = 'none';
|
|
1402
|
+
content.style.display = 'block';
|
|
1403
|
+
|
|
1404
|
+
const lt = state.stats.lifetime || {};
|
|
1405
|
+
const cards = [
|
|
1406
|
+
{ label: 'Sessions', value: formatNumber(lt.totalSessions || 0), sub: 'lifetime' },
|
|
1407
|
+
{ label: 'Prompts', value: formatNumber(lt.totalPrompts || 0), sub: 'messages sent' },
|
|
1408
|
+
{ label: 'Tool Calls', value: formatNumber(lt.totalToolCalls || 0), sub: 'total invocations' },
|
|
1409
|
+
{ label: 'Hours', value: formatHours(lt.totalDurationSeconds || 0), sub: 'coding time' },
|
|
1410
|
+
{ label: 'Files Read', value: formatNumber(lt.totalFilesRead || 0), sub: 'files accessed' },
|
|
1411
|
+
{ label: 'Bash Cmds', value: formatNumber(lt.totalBashCommands || lt.totalToolCalls || 0), sub: 'commands run' },
|
|
1412
|
+
];
|
|
1413
|
+
|
|
1414
|
+
const cardsEl = document.getElementById('overview-stat-cards');
|
|
1415
|
+
cardsEl.innerHTML = cards.map(c => `
|
|
1416
|
+
<div class="stat-card">
|
|
1417
|
+
<div class="stat-card-label">${escapeHtml(c.label)}</div>
|
|
1418
|
+
<div class="stat-card-number mono">${escapeHtml(c.value)}</div>
|
|
1419
|
+
<div class="stat-card-sub">${escapeHtml(c.sub)}</div>
|
|
1420
|
+
</div>
|
|
1421
|
+
`).join('');
|
|
1422
|
+
|
|
1423
|
+
// Sparkline
|
|
1424
|
+
renderSparkline();
|
|
1425
|
+
|
|
1426
|
+
// Heatmap + sessions in overview
|
|
1427
|
+
renderOverviewHeatmap();
|
|
1428
|
+
renderOverviewSessions();
|
|
1429
|
+
|
|
1430
|
+
// Recent badges
|
|
1431
|
+
renderRecentBadges();
|
|
1432
|
+
|
|
1433
|
+
// Rank card
|
|
1434
|
+
renderOverviewRank();
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
function renderEmptyOverview() {
|
|
1438
|
+
document.getElementById('overview-stat-cards').innerHTML = `
|
|
1439
|
+
<div class="stat-card"><div class="stat-card-label">Sessions</div><div class="stat-card-number mono">0</div></div>
|
|
1440
|
+
<div class="stat-card"><div class="stat-card-label">Prompts</div><div class="stat-card-number mono">0</div></div>
|
|
1441
|
+
<div class="stat-card"><div class="stat-card-label">Tool Calls</div><div class="stat-card-number mono">0</div></div>
|
|
1442
|
+
<div class="stat-card"><div class="stat-card-label">Hours</div><div class="stat-card-number mono">0</div></div>
|
|
1443
|
+
<div class="stat-card"><div class="stat-card-label">Files Read</div><div class="stat-card-number mono">0</div></div>
|
|
1444
|
+
<div class="stat-card"><div class="stat-card-label">Bash Cmds</div><div class="stat-card-number mono">0</div></div>
|
|
1445
|
+
`;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function renderSparkline() {
|
|
1449
|
+
const container = document.getElementById('overview-sparkline');
|
|
1450
|
+
const labelStart = document.getElementById('sparkline-label-start');
|
|
1451
|
+
const labelEnd = document.getElementById('sparkline-label-end');
|
|
1452
|
+
|
|
1453
|
+
const activity = state.activity || [];
|
|
1454
|
+
const today = new Date();
|
|
1455
|
+
const days = [];
|
|
1456
|
+
|
|
1457
|
+
for (let i = 29; i >= 0; i--) {
|
|
1458
|
+
const d = new Date(today);
|
|
1459
|
+
d.setDate(d.getDate() - i);
|
|
1460
|
+
const dateStr = d.toISOString().split('T')[0];
|
|
1461
|
+
const found = activity.find(a => a.date === dateStr);
|
|
1462
|
+
const val = found ? (found.sessions + found.prompts + found.tool_calls) : 0;
|
|
1463
|
+
days.push({ date: dateStr, value: val });
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
const maxVal = Math.max(1, ...days.map(d => d.value));
|
|
1467
|
+
|
|
1468
|
+
container.innerHTML = days.map(d => {
|
|
1469
|
+
const pct = Math.max(4, (d.value / maxVal) * 100);
|
|
1470
|
+
return `<div class="sparkline-bar" style="height:${pct}%" data-tooltip="${formatDate(d.date)}: ${d.value}"></div>`;
|
|
1471
|
+
}).join('');
|
|
1472
|
+
|
|
1473
|
+
labelStart.textContent = formatDate(days[0].date);
|
|
1474
|
+
labelEnd.textContent = formatDate(days[days.length - 1].date);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
function renderRecentBadges() {
|
|
1478
|
+
const el = document.getElementById('overview-recent-badges');
|
|
1479
|
+
const badges = state.achievements?.badges || [];
|
|
1480
|
+
const unlocked = badges.filter(b => b.unlocked).slice(-6).reverse();
|
|
1481
|
+
|
|
1482
|
+
if (unlocked.length === 0) {
|
|
1483
|
+
el.innerHTML = '<div class="empty-state" style="padding:20px;"><div class="empty-state-text">No badges yet</div><div class="empty-state-sub">Start a session to earn your first badge!</div></div>';
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
el.innerHTML = unlocked.map(b => {
|
|
1488
|
+
const tc = tierColor(b.tier);
|
|
1489
|
+
const icon = b.icon || '🏅';
|
|
1490
|
+
return `
|
|
1491
|
+
<div class="recent-badge-tile" style="border-color:${tc};background:${tc}15">
|
|
1492
|
+
<div class="badge-tile-icon">${icon}</div>
|
|
1493
|
+
<div class="badge-tile-name">${escapeHtml(b.name)}</div>
|
|
1494
|
+
</div>
|
|
1495
|
+
`;
|
|
1496
|
+
}).join('');
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
function renderOverviewRank() {
|
|
1500
|
+
const el = document.getElementById('overview-rank-card');
|
|
1501
|
+
const xp = state.achievements?.xp;
|
|
1502
|
+
|
|
1503
|
+
if (!xp) {
|
|
1504
|
+
el.innerHTML = '<div class="empty-state" style="padding:20px;"><div class="empty-state-text">No XP data</div></div>';
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
const pct = Math.min(100, (xp.progress || 0) * 100);
|
|
1509
|
+
const rc = rankColor(xp.rank);
|
|
1510
|
+
const initial = xp.rank.charAt(0);
|
|
1511
|
+
el.innerHTML = `
|
|
1512
|
+
<div class="rank-card-icon" style="border-color:${rc};background:${rc}22">
|
|
1513
|
+
<span class="rank-card-icon-text" style="color:${rc}">${escapeHtml(initial)}</span>
|
|
1514
|
+
</div>
|
|
1515
|
+
<div class="rank-card-info">
|
|
1516
|
+
<div class="rank-card-rank" style="color:${rc}">${escapeHtml(xp.rank)}</div>
|
|
1517
|
+
<div class="rank-card-xp">${formatNumber(xp.totalXP)} / ${formatNumber(xp.nextRankXP)} XP</div>
|
|
1518
|
+
<div class="rank-progress-track">
|
|
1519
|
+
<div class="rank-progress-fill" style="width:${pct}%;background:${rc}"></div>
|
|
1520
|
+
</div>
|
|
1521
|
+
<div class="rank-progress-label">${pct.toFixed(1)}% to ${escapeHtml(xp.rank === 'Obsidian' ? 'max rank' : 'next rank')}</div>
|
|
1522
|
+
</div>
|
|
1523
|
+
`;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// ===== RENDER: STATS =====
|
|
1527
|
+
function renderOverviewHeatmap() {
|
|
1528
|
+
const wrapper = document.getElementById('overview-heatmap');
|
|
1529
|
+
if (!wrapper) return;
|
|
1530
|
+
const activity = state.activity || [];
|
|
1531
|
+
const actMap = {};
|
|
1532
|
+
activity.forEach(a => { actMap[a.date] = (a.sessions || 0) + (a.prompts || 0) + (a.tool_calls || 0); });
|
|
1533
|
+
const today = new Date();
|
|
1534
|
+
const days = [];
|
|
1535
|
+
for (let i = 364; i >= 0; i--) {
|
|
1536
|
+
const d = new Date(today); d.setDate(d.getDate() - i);
|
|
1537
|
+
const dateStr = d.toISOString().split('T')[0];
|
|
1538
|
+
days.push({ date: dateStr, dayOfWeek: d.getDay(), value: actMap[dateStr] || 0, month: d.getMonth(), day: d.getDate(), year: d.getFullYear() });
|
|
1539
|
+
}
|
|
1540
|
+
const maxVal = Math.max(1, ...days.map(d => d.value));
|
|
1541
|
+
function heatColor(value) {
|
|
1542
|
+
if (value === 0) return 'var(--bg-accent)';
|
|
1543
|
+
const intensity = value / maxVal;
|
|
1544
|
+
if (intensity < 0.25) return '#FFDFCC';
|
|
1545
|
+
if (intensity < 0.5) return '#FFBF9B';
|
|
1546
|
+
if (intensity < 0.75) return '#FFA070';
|
|
1547
|
+
return '#FF8C5A';
|
|
1548
|
+
}
|
|
1549
|
+
const weeks = []; let currentWeek = [];
|
|
1550
|
+
const firstDow = days[0].dayOfWeek;
|
|
1551
|
+
for (let i = 0; i < firstDow; i++) currentWeek.push(null);
|
|
1552
|
+
days.forEach(d => { currentWeek.push(d); if (d.dayOfWeek === 6) { weeks.push(currentWeek); currentWeek = []; } });
|
|
1553
|
+
if (currentWeek.length > 0) { while (currentWeek.length < 7) currentWeek.push(null); weeks.push(currentWeek); }
|
|
1554
|
+
|
|
1555
|
+
// Build month labels with proper positional alignment
|
|
1556
|
+
const monthNames = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
1557
|
+
const weekWidth = 16; // 14px cell + 2px gap
|
|
1558
|
+
const totalWeeks = weeks.length;
|
|
1559
|
+
let monthLabels = '';
|
|
1560
|
+
let lastMonth = -1;
|
|
1561
|
+
let lastLabelWeek = -4; // track last label position to avoid overlap
|
|
1562
|
+
for (let i = 0; i < totalWeeks; i++) {
|
|
1563
|
+
const firstDay = weeks[i].find(d => d !== null);
|
|
1564
|
+
if (firstDay && firstDay.month !== lastMonth) {
|
|
1565
|
+
// Place label at the week where the new month starts
|
|
1566
|
+
if (firstDay.day <= 7 || lastMonth === -1) {
|
|
1567
|
+
const gap = i - (lastLabelWeek >= 0 ? lastLabelWeek + 1 : 0);
|
|
1568
|
+
if (gap > 0) {
|
|
1569
|
+
monthLabels += `<span style="min-width:${gap * weekWidth}px"></span>`;
|
|
1570
|
+
}
|
|
1571
|
+
monthLabels += `<span class="heatmap-month-label" style="min-width:${weekWidth}px">${monthNames[firstDay.month]}</span>`;
|
|
1572
|
+
lastMonth = firstDay.month;
|
|
1573
|
+
lastLabelWeek = i;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
// Determine the year to display (from the last day in the data)
|
|
1579
|
+
const displayYear = today.getFullYear();
|
|
1580
|
+
|
|
1581
|
+
const dayLabels = ['Sun','','Tue','','Thu','','Sat'];
|
|
1582
|
+
let html = `<div class="heatmap-months">${monthLabels}</div><div class="heatmap-body">`;
|
|
1583
|
+
html += `<div class="heatmap-day-labels">${dayLabels.map(l => `<div class="heatmap-day-label">${l}</div>`).join('')}</div>`;
|
|
1584
|
+
html += `<div class="heatmap-grid">`;
|
|
1585
|
+
weeks.forEach(week => {
|
|
1586
|
+
html += `<div class="heatmap-week">`;
|
|
1587
|
+
week.forEach(d => {
|
|
1588
|
+
if (!d) { html += `<div class="heatmap-cell" style="background:transparent;border-color:transparent;"></div>`; }
|
|
1589
|
+
else { html += `<div class="heatmap-cell" style="background:${heatColor(d.value)}" data-tooltip="${formatDate(d.date)}: ${d.value} activity"></div>`; }
|
|
1590
|
+
});
|
|
1591
|
+
html += `</div>`;
|
|
1592
|
+
});
|
|
1593
|
+
html += `</div>`;
|
|
1594
|
+
html += `<div class="heatmap-year-label">${displayYear}</div>`;
|
|
1595
|
+
html += `</div>`;
|
|
1596
|
+
html += `<div class="heatmap-legend"><span>Less</span>`;
|
|
1597
|
+
['var(--bg-accent)','#FFDFCC','#FFBF9B','#FFA070','#FF8C5A'].forEach(c => { html += `<div class="heatmap-legend-cell" style="background:${c}"></div>`; });
|
|
1598
|
+
html += `<span>More</span></div>`;
|
|
1599
|
+
wrapper.innerHTML = html;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
function renderOverviewSessions() {
|
|
1603
|
+
const el = document.getElementById('overview-sessions');
|
|
1604
|
+
if (!el) return;
|
|
1605
|
+
const sessions = state.sessions || [];
|
|
1606
|
+
if (sessions.length === 0) {
|
|
1607
|
+
el.innerHTML = '<div class="empty-state" style="padding:20px;"><div class="empty-state-text">No sessions recorded</div></div>';
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
const recent = sessions.slice(-20).reverse();
|
|
1611
|
+
el.innerHTML = `
|
|
1612
|
+
<div class="session-item" style="font-weight:600;background:var(--bg-accent);font-size:12px;">
|
|
1613
|
+
<div>Date</div><div>Project</div><div style="text-align:right">Duration</div><div style="text-align:right">Prompts</div><div style="text-align:right">Tools</div>
|
|
1614
|
+
</div>
|
|
1615
|
+
${recent.map(s => `
|
|
1616
|
+
<div class="session-item">
|
|
1617
|
+
<div class="session-date">${escapeHtml(formatDateTime(s.start_time || s.startTime || s.started_at || ''))}</div>
|
|
1618
|
+
<div class="session-project">${escapeHtml(s.project || s.projectName || 'Unknown')}</div>
|
|
1619
|
+
<div class="session-stat">${formatDuration(s.duration_seconds || s.durationSeconds || 0)}</div>
|
|
1620
|
+
<div class="session-stat">${formatNumber(s.prompt_count || s.prompts || s.promptCount || 0)}</div>
|
|
1621
|
+
<div class="session-stat">${formatNumber(s.tool_count || s.tool_calls || s.toolCalls || 0)}</div>
|
|
1622
|
+
</div>
|
|
1623
|
+
`).join('')}
|
|
1624
|
+
`;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
function renderStats() {
|
|
1628
|
+
const loading = document.getElementById('stats-loading');
|
|
1629
|
+
const content = document.getElementById('stats-content');
|
|
1630
|
+
|
|
1631
|
+
if (!state.stats) {
|
|
1632
|
+
if (state.lastFetch) {
|
|
1633
|
+
loading.style.display = 'none';
|
|
1634
|
+
content.style.display = 'block';
|
|
1635
|
+
content.innerHTML = '<div class="empty-state"><div class="empty-state-icon">📊</div><div class="empty-state-text">No stats yet</div><div class="empty-state-sub">Complete a session to start tracking.</div></div>';
|
|
1636
|
+
}
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
loading.style.display = 'none';
|
|
1641
|
+
content.style.display = 'block';
|
|
1642
|
+
|
|
1643
|
+
const lt = state.stats.lifetime || {};
|
|
1644
|
+
const tools = state.stats.tools || {};
|
|
1645
|
+
const time = state.stats.time || {};
|
|
1646
|
+
const sess = state.stats.sessions || {};
|
|
1647
|
+
const proj = state.stats.projects || {};
|
|
1648
|
+
|
|
1649
|
+
let html = '<div class="stats-grid-layout">';
|
|
1650
|
+
|
|
1651
|
+
// Lifetime Totals
|
|
1652
|
+
html += buildStatsSection('Lifetime Totals', [
|
|
1653
|
+
['Total Sessions', formatNumber(lt.totalSessions)],
|
|
1654
|
+
['Total Duration', formatDuration(lt.totalDurationSeconds)],
|
|
1655
|
+
['Total Prompts', formatNumber(lt.totalPrompts)],
|
|
1656
|
+
['Characters Typed', formatNumber(lt.totalCharsTyped)],
|
|
1657
|
+
['Total Tool Calls', formatNumber(lt.totalToolCalls)],
|
|
1658
|
+
['Files Read', formatNumber(lt.totalFilesRead)],
|
|
1659
|
+
]);
|
|
1660
|
+
|
|
1661
|
+
// Tool Breakdown
|
|
1662
|
+
const toolEntries = Object.entries(tools).sort((a, b) => b[1] - a[1]);
|
|
1663
|
+
if (toolEntries.length > 0) {
|
|
1664
|
+
html += buildStatsSection('Tool Breakdown', toolEntries.map(([name, count]) => [name, formatNumber(count)]));
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// Time & Streaks
|
|
1668
|
+
html += buildStatsSection('Time & Streaks', [
|
|
1669
|
+
['Current Streak', (time.currentStreak || 0) + ' days'],
|
|
1670
|
+
['Longest Streak', (time.longestStreak || 0) + ' days'],
|
|
1671
|
+
['Peak Hour', time.peakHour != null ? time.peakHour + ':00' : 'N/A'],
|
|
1672
|
+
['Average Session', formatDuration(time.averageSessionSeconds || 0)],
|
|
1673
|
+
]);
|
|
1674
|
+
|
|
1675
|
+
// Session Records
|
|
1676
|
+
html += buildStatsSection('Session Records', [
|
|
1677
|
+
['Longest Session', formatDuration(sess.longestSessionSeconds)],
|
|
1678
|
+
['Most Tools in Session', formatNumber(sess.mostToolsInSession)],
|
|
1679
|
+
['Most Prompts in Session', formatNumber(sess.mostPromptsInSession)],
|
|
1680
|
+
['Shortest Session', formatDuration(sess.shortestSessionSeconds)],
|
|
1681
|
+
]);
|
|
1682
|
+
|
|
1683
|
+
// Projects
|
|
1684
|
+
html += buildStatsSection('Projects', [
|
|
1685
|
+
['Unique Projects', formatNumber(proj.uniqueProjects)],
|
|
1686
|
+
['Most Visited', proj.mostVisitedProject || 'N/A'],
|
|
1687
|
+
['Total Project Sessions', formatNumber(proj.totalProjectSessions || lt.totalSessions)],
|
|
1688
|
+
]);
|
|
1689
|
+
|
|
1690
|
+
html += '</div>';
|
|
1691
|
+
|
|
1692
|
+
content.innerHTML = html;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
function buildStatsSection(title, rows) {
|
|
1696
|
+
return `
|
|
1697
|
+
<div class="stats-section card">
|
|
1698
|
+
<div class="card-title">${escapeHtml(title)}</div>
|
|
1699
|
+
<table class="stats-table">
|
|
1700
|
+
<thead><tr><th>Metric</th><th style="text-align:right">Value</th></tr></thead>
|
|
1701
|
+
<tbody>
|
|
1702
|
+
${rows.map(([label, value]) => `
|
|
1703
|
+
<tr><td>${escapeHtml(label)}</td><td>${escapeHtml(String(value || '0'))}</td></tr>
|
|
1704
|
+
`).join('')}
|
|
1705
|
+
</tbody>
|
|
1706
|
+
</table>
|
|
1707
|
+
</div>
|
|
1708
|
+
`;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
// ===== RENDER: ACHIEVEMENTS =====
|
|
1712
|
+
function renderAchievements() {
|
|
1713
|
+
const loading = document.getElementById('achievements-loading');
|
|
1714
|
+
const content = document.getElementById('achievements-content');
|
|
1715
|
+
|
|
1716
|
+
if (!state.achievements) {
|
|
1717
|
+
if (state.lastFetch) {
|
|
1718
|
+
loading.style.display = 'none';
|
|
1719
|
+
content.style.display = 'block';
|
|
1720
|
+
content.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🏆</div><div class="empty-state-text">No achievements yet</div><div class="empty-state-sub">Start using Claude Code to unlock badges.</div></div>';
|
|
1721
|
+
}
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
loading.style.display = 'none';
|
|
1726
|
+
content.style.display = 'block';
|
|
1727
|
+
|
|
1728
|
+
const badges = state.achievements.badges || [];
|
|
1729
|
+
|
|
1730
|
+
// Group by category
|
|
1731
|
+
const categories = {};
|
|
1732
|
+
badges.forEach(b => {
|
|
1733
|
+
const cat = b.category || 'general';
|
|
1734
|
+
if (!categories[cat]) categories[cat] = [];
|
|
1735
|
+
categories[cat].push(b);
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
let html = '';
|
|
1739
|
+
const catOrder = ['volume', 'tools', 'streaks', 'time', 'exploration', 'humor', 'secret'];
|
|
1740
|
+
const catNames = {
|
|
1741
|
+
volume: 'Volume',
|
|
1742
|
+
tools: 'Tools',
|
|
1743
|
+
streaks: 'Streaks',
|
|
1744
|
+
time: 'Time',
|
|
1745
|
+
exploration: 'Exploration',
|
|
1746
|
+
humor: 'Humor',
|
|
1747
|
+
secret: 'Secret',
|
|
1748
|
+
general: 'General',
|
|
1749
|
+
};
|
|
1750
|
+
|
|
1751
|
+
// Sort: known categories first, then others
|
|
1752
|
+
const allCats = [...new Set([...catOrder.filter(c => categories[c]), ...Object.keys(categories).filter(c => !catOrder.includes(c))])];
|
|
1753
|
+
|
|
1754
|
+
allCats.forEach(cat => {
|
|
1755
|
+
const catBadges = categories[cat];
|
|
1756
|
+
if (!catBadges || catBadges.length === 0) return;
|
|
1757
|
+
|
|
1758
|
+
html += `<div class="badge-category-section">`;
|
|
1759
|
+
html += `<div class="badge-category-title">${escapeHtml(catNames[cat] || cat)}</div>`;
|
|
1760
|
+
html += `<div class="badge-grid">`;
|
|
1761
|
+
|
|
1762
|
+
catBadges.forEach(b => {
|
|
1763
|
+
const isSecretLocked = b.secret && !b.unlocked;
|
|
1764
|
+
const isLocked = !b.unlocked;
|
|
1765
|
+
const cardClass = isSecretLocked ? 'badge-card secret-locked' : (isLocked ? 'badge-card locked' : 'badge-card');
|
|
1766
|
+
const displayName = isSecretLocked ? '???' : b.name;
|
|
1767
|
+
const displayDesc = isSecretLocked ? 'Unlock this secret badge to reveal it.' : (b.description || '');
|
|
1768
|
+
const pct = b.maxed ? 100 : Math.min(100, (b.progress || 0) * 100);
|
|
1769
|
+
const fillClass = b.maxed ? 'badge-progress-fill maxed' : 'badge-progress-fill';
|
|
1770
|
+
|
|
1771
|
+
const badgeIcon = isSecretLocked ? '❓' : (b.icon || '🏅');
|
|
1772
|
+
|
|
1773
|
+
html += `
|
|
1774
|
+
<div class="${cardClass}">
|
|
1775
|
+
<div class="badge-header">
|
|
1776
|
+
<span style="font-size:18px;line-height:1">${badgeIcon}</span>
|
|
1777
|
+
<span class="badge-name">${escapeHtml(displayName)}</span>
|
|
1778
|
+
<span class="badge-tier-label" style="color:${tierColor(b.tier)};border-color:${tierColor(b.tier)}">${escapeHtml(b.tierName || TIER_NAMES[b.tier] || 'Locked')}</span>
|
|
1779
|
+
</div>
|
|
1780
|
+
${displayDesc ? `<div class="badge-description">${escapeHtml(displayDesc)}</div>` : ''}
|
|
1781
|
+
<div class="badge-progress-row">
|
|
1782
|
+
<div class="badge-progress-track">
|
|
1783
|
+
<div class="${fillClass}" style="width:${pct}%"></div>
|
|
1784
|
+
</div>
|
|
1785
|
+
<span class="badge-progress-text">${isSecretLocked ? '?' : formatNumber(b.value || 0)}/${isSecretLocked ? '?' : formatNumber(b.nextThreshold || 0)}</span>
|
|
1786
|
+
</div>
|
|
1787
|
+
</div>
|
|
1788
|
+
`;
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
html += `</div></div>`;
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1794
|
+
content.innerHTML = html;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// ===== RENDER: RECORDS =====
|
|
1798
|
+
function renderRecords() {
|
|
1799
|
+
const loading = document.getElementById('records-loading');
|
|
1800
|
+
const content = document.getElementById('records-content');
|
|
1801
|
+
|
|
1802
|
+
if (!state.stats) {
|
|
1803
|
+
if (state.lastFetch) {
|
|
1804
|
+
loading.style.display = 'none';
|
|
1805
|
+
content.style.display = 'block';
|
|
1806
|
+
content.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🏅</div><div class="empty-state-text">No records yet</div><div class="empty-state-sub">Complete sessions to set personal bests.</div></div>';
|
|
1807
|
+
}
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
loading.style.display = 'none';
|
|
1812
|
+
content.style.display = 'block';
|
|
1813
|
+
|
|
1814
|
+
const sess = state.stats.sessions || {};
|
|
1815
|
+
const time = state.stats.time || {};
|
|
1816
|
+
const lt = state.stats.lifetime || {};
|
|
1817
|
+
|
|
1818
|
+
const avgPrompts = lt.totalSessions ? Math.round(lt.totalPrompts / lt.totalSessions) : 0;
|
|
1819
|
+
const avgTools = lt.totalSessions ? Math.round(lt.totalToolCalls / lt.totalSessions) : 0;
|
|
1820
|
+
const avgDuration = lt.totalSessions ? Math.round(lt.totalDurationSeconds / lt.totalSessions) : 0;
|
|
1821
|
+
|
|
1822
|
+
const records = [
|
|
1823
|
+
{ label: 'Longest Session', value: formatDuration(sess.longestSessionSeconds), sub: 'personal best' },
|
|
1824
|
+
{ label: 'Most Tools (Session)', value: formatNumber(sess.mostToolsInSession), sub: 'single session' },
|
|
1825
|
+
{ label: 'Most Prompts (Session)', value: formatNumber(sess.mostPromptsInSession), sub: 'single session' },
|
|
1826
|
+
{ label: 'Shortest Session', value: formatDuration(sess.shortestSessionSeconds), sub: 'speed run' },
|
|
1827
|
+
{ label: 'Average Prompts', value: formatNumber(avgPrompts), sub: 'per session' },
|
|
1828
|
+
{ label: 'Average Tools', value: formatNumber(avgTools), sub: 'per session' },
|
|
1829
|
+
{ label: 'Average Duration', value: formatDuration(avgDuration), sub: 'per session' },
|
|
1830
|
+
{ label: 'Longest Streak', value: (time.longestStreak || 0) + ' days', sub: 'consecutive days' },
|
|
1831
|
+
{ label: 'Peak Hour', value: time.peakHour != null ? time.peakHour + ':00' : 'N/A', sub: 'most active hour' },
|
|
1832
|
+
];
|
|
1833
|
+
|
|
1834
|
+
content.innerHTML = `
|
|
1835
|
+
<div class="records-grid">
|
|
1836
|
+
${records.map(r => `
|
|
1837
|
+
<div class="record-card">
|
|
1838
|
+
<div class="record-label">${escapeHtml(r.label)}</div>
|
|
1839
|
+
<div class="record-value mono">${escapeHtml(r.value)}</div>
|
|
1840
|
+
<div class="record-sub">${escapeHtml(r.sub)}</div>
|
|
1841
|
+
</div>
|
|
1842
|
+
`).join('')}
|
|
1843
|
+
</div>
|
|
1844
|
+
`;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
|
|
1848
|
+
// ===== RENDER: SETTINGS =====
|
|
1849
|
+
function renderSettings() {
|
|
1850
|
+
const hookDot = document.getElementById('settings-hook-dot');
|
|
1851
|
+
const hookText = document.getElementById('settings-hook-text');
|
|
1852
|
+
|
|
1853
|
+
// Determine hook status based on data presence
|
|
1854
|
+
if (state.stats) {
|
|
1855
|
+
hookDot.className = 'status-dot active';
|
|
1856
|
+
hookText.textContent = 'Active';
|
|
1857
|
+
} else if (state.lastFetch) {
|
|
1858
|
+
hookDot.className = 'status-dot inactive';
|
|
1859
|
+
hookText.textContent = 'No data';
|
|
1860
|
+
} else {
|
|
1861
|
+
hookDot.className = 'status-dot inactive';
|
|
1862
|
+
hookText.textContent = 'Checking...';
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// ===== AUTO-REFRESH =====
|
|
1867
|
+
function startAutoRefresh() {
|
|
1868
|
+
setInterval(() => {
|
|
1869
|
+
loadAllData();
|
|
1870
|
+
}, 30000);
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
// ===== INIT =====
|
|
1874
|
+
function init() {
|
|
1875
|
+
setupTabs();
|
|
1876
|
+
setupTheme();
|
|
1877
|
+
loadAllData();
|
|
1878
|
+
startAutoRefresh();
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
1882
|
+
</script>
|
|
1883
|
+
</body>
|
|
1884
|
+
</html>
|