bashstats 0.2.0 → 0.2.2

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.
@@ -26,7 +26,9 @@
26
26
  --tier-silver: #C0C0C0;
27
27
  --tier-gold: #FFD700;
28
28
  --tier-diamond: #B9F2FF;
29
- --tier-obsidian: #2D1B69;
29
+ --tier-singularity: #2D1B69;
30
+ --tier-obsidian: #1a0a3e;
31
+ --tier-system-anomaly: #00ff41;
30
32
  }
31
33
 
32
34
  :root[data-theme="dark"] {
@@ -371,6 +373,148 @@
371
373
  }
372
374
  }
373
375
 
376
+ /* ===== OVERVIEW MIDDLE ROW ===== */
377
+ .overview-mid {
378
+ display: grid;
379
+ grid-template-columns: 1fr 1fr 1fr;
380
+ gap: 16px;
381
+ margin-bottom: 16px;
382
+ }
383
+
384
+ .overview-mid > .card {
385
+ display: flex;
386
+ flex-direction: column;
387
+ }
388
+
389
+ @media (max-width: 768px) {
390
+ .overview-mid {
391
+ grid-template-columns: 1fr;
392
+ }
393
+ }
394
+
395
+ .weekly-challenge-item {
396
+ display: flex;
397
+ flex-direction: column;
398
+ gap: 4px;
399
+ padding: 8px 0;
400
+ border-bottom: 1px solid var(--bg-accent);
401
+ }
402
+
403
+ .weekly-challenge-item:last-child {
404
+ border-bottom: none;
405
+ }
406
+
407
+ .weekly-challenge-desc {
408
+ font-size: 12px;
409
+ color: var(--text-primary);
410
+ }
411
+
412
+ .weekly-challenge-meta {
413
+ display: flex;
414
+ justify-content: space-between;
415
+ align-items: center;
416
+ font-size: 11px;
417
+ font-family: 'JetBrains Mono', monospace;
418
+ }
419
+
420
+ .weekly-challenge-xp {
421
+ color: var(--accent);
422
+ font-weight: 600;
423
+ }
424
+
425
+ .weekly-multiplier {
426
+ font-family: 'JetBrains Mono', monospace;
427
+ font-size: 11px;
428
+ color: var(--accent);
429
+ font-weight: 700;
430
+ }
431
+
432
+ .today-streak {
433
+ display: flex;
434
+ align-items: center;
435
+ justify-content: center;
436
+ gap: 8px;
437
+ margin-bottom: 12px;
438
+ }
439
+
440
+ .today-streak-number {
441
+ font-family: 'JetBrains Mono', monospace;
442
+ font-size: 36px;
443
+ font-weight: 700;
444
+ color: var(--accent);
445
+ }
446
+
447
+ .today-streak-label {
448
+ font-size: 12px;
449
+ color: var(--text-secondary);
450
+ line-height: 1.3;
451
+ }
452
+
453
+ .today-stat-grid {
454
+ display: grid;
455
+ grid-template-columns: 1fr 1fr;
456
+ gap: 8px;
457
+ }
458
+
459
+ .today-stat-item {
460
+ text-align: center;
461
+ padding: 8px;
462
+ background: var(--bg-accent);
463
+ border-radius: 2px;
464
+ }
465
+
466
+ .today-stat-value {
467
+ font-family: 'JetBrains Mono', monospace;
468
+ font-size: 20px;
469
+ font-weight: 700;
470
+ }
471
+
472
+ .today-stat-label {
473
+ font-size: 10px;
474
+ color: var(--text-secondary);
475
+ text-transform: uppercase;
476
+ }
477
+
478
+ .badge-progress-summary {
479
+ display: flex;
480
+ align-items: center;
481
+ gap: 12px;
482
+ margin-bottom: 12px;
483
+ }
484
+
485
+ .badge-progress-count {
486
+ font-family: 'JetBrains Mono', monospace;
487
+ font-size: 28px;
488
+ font-weight: 700;
489
+ }
490
+
491
+ .badge-progress-total {
492
+ font-size: 12px;
493
+ color: var(--text-secondary);
494
+ }
495
+
496
+ .badge-next-item {
497
+ padding: 8px;
498
+ background: var(--bg-accent);
499
+ border-radius: 2px;
500
+ border-left: 3px solid var(--accent);
501
+ }
502
+
503
+ .badge-next-name {
504
+ font-size: 13px;
505
+ font-weight: 600;
506
+ margin-bottom: 4px;
507
+ }
508
+
509
+ .badge-next-meta {
510
+ display: flex;
511
+ justify-content: space-between;
512
+ font-size: 11px;
513
+ font-family: 'JetBrains Mono', monospace;
514
+ color: var(--text-secondary);
515
+ margin-top: 4px;
516
+ }
517
+
374
518
  /* ===== RECENT BADGES ===== */
375
519
  .recent-badges-list {
376
520
  display: flex;
@@ -608,6 +752,17 @@
608
752
  line-height: 1.4;
609
753
  }
610
754
 
755
+ .badge-trigger {
756
+ font-size: 11px;
757
+ font-family: 'JetBrains Mono', monospace;
758
+ color: var(--accent);
759
+ line-height: 1.3;
760
+ padding: 3px 6px;
761
+ background: var(--bg-accent);
762
+ border-radius: 2px;
763
+ border-left: 2px solid var(--accent);
764
+ }
765
+
611
766
  .badge-progress-row {
612
767
  display: flex;
613
768
  align-items: center;
@@ -982,19 +1137,19 @@
982
1137
  }
983
1138
 
984
1139
  /* ===== 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); }
1140
+ .tier-locked { color: var(--text-secondary); }
1141
+ .tier-bronze { color: var(--tier-bronze); }
1142
+ .tier-silver { color: var(--tier-silver); }
1143
+ .tier-gold { color: var(--tier-gold); }
1144
+ .tier-diamond { color: var(--tier-diamond); }
1145
+ .tier-singularity { color: var(--tier-singularity); }
1146
+
1147
+ .tier-bg-locked { background: var(--text-secondary); }
1148
+ .tier-bg-bronze { background: var(--tier-bronze); }
1149
+ .tier-bg-silver { background: var(--tier-silver); }
1150
+ .tier-bg-gold { background: var(--tier-gold); }
1151
+ .tier-bg-diamond { background: var(--tier-diamond); }
1152
+ .tier-bg-singularity { background: var(--tier-singularity); }
998
1153
 
999
1154
  /* ===== REFRESH INDICATOR ===== */
1000
1155
  .refresh-indicator {
@@ -1010,6 +1165,304 @@
1010
1165
  padding: 4px 8px;
1011
1166
  box-shadow: 3px 3px 0 var(--shadow);
1012
1167
  }
1168
+
1169
+ .agent-filter {
1170
+ background: var(--bg-card);
1171
+ color: var(--text-primary);
1172
+ border: 3px solid var(--border);
1173
+ border-radius: 2px;
1174
+ padding: 4px 12px;
1175
+ font-family: 'JetBrains Mono', monospace;
1176
+ font-size: 13px;
1177
+ font-weight: 600;
1178
+ cursor: pointer;
1179
+ outline: none;
1180
+ box-shadow: 3px 3px 0 var(--shadow);
1181
+ }
1182
+ .agent-filter:focus {
1183
+ border-color: var(--accent);
1184
+ }
1185
+
1186
+ .agent-badge {
1187
+ display: inline-block;
1188
+ font-size: 0.65rem;
1189
+ padding: 1px 5px;
1190
+ border-radius: 2px;
1191
+ background: var(--bg-card);
1192
+ border: 2px solid var(--border);
1193
+ margin-left: 6px;
1194
+ vertical-align: middle;
1195
+ }
1196
+ .agent-badge.claude-code { border-color: #d97706; color: #d97706; }
1197
+ .agent-badge.gemini-cli { border-color: #4285f4; color: #4285f4; }
1198
+ .agent-badge.copilot-cli { border-color: #6e40c9; color: #6e40c9; }
1199
+ .agent-badge.opencode { border-color: #10b981; color: #10b981; }
1200
+
1201
+ .stat-na {
1202
+ opacity: 0.4;
1203
+ font-style: italic;
1204
+ }
1205
+
1206
+ .agent-breakdown-card {
1207
+ margin-top: 1rem;
1208
+ }
1209
+ .agent-bar {
1210
+ display: flex;
1211
+ align-items: center;
1212
+ gap: 8px;
1213
+ margin: 6px 0;
1214
+ }
1215
+ .agent-bar-label {
1216
+ width: 100px;
1217
+ font-size: 0.8rem;
1218
+ text-align: right;
1219
+ white-space: nowrap;
1220
+ }
1221
+ .agent-bar-track {
1222
+ flex: 1;
1223
+ background: var(--border);
1224
+ border-radius: 2px;
1225
+ height: 20px;
1226
+ overflow: hidden;
1227
+ }
1228
+ .agent-bar-fill {
1229
+ height: 100%;
1230
+ border-radius: 2px;
1231
+ transition: width 0.3s ease;
1232
+ min-width: 3px;
1233
+ }
1234
+ .agent-bar-value {
1235
+ font-size: 0.8rem;
1236
+ width: 100px;
1237
+ text-align: right;
1238
+ white-space: nowrap;
1239
+ }
1240
+
1241
+ /* ===== RANK PROGRESSION MODAL ===== */
1242
+ .rank-modal-overlay {
1243
+ position: fixed;
1244
+ inset: 0;
1245
+ background: rgba(0,0,0,0.5);
1246
+ backdrop-filter: blur(4px);
1247
+ z-index: 200;
1248
+ display: flex;
1249
+ align-items: center;
1250
+ justify-content: center;
1251
+ padding: 20px;
1252
+ }
1253
+
1254
+ .rank-modal {
1255
+ background: var(--bg-card);
1256
+ border: 3px solid var(--border);
1257
+ border-radius: 2px;
1258
+ box-shadow: 8px 8px 0 var(--shadow);
1259
+ width: 100%;
1260
+ max-width: 520px;
1261
+ max-height: 85vh;
1262
+ display: flex;
1263
+ flex-direction: column;
1264
+ }
1265
+
1266
+ .rank-modal-header {
1267
+ display: flex;
1268
+ align-items: center;
1269
+ justify-content: space-between;
1270
+ padding: 16px 20px;
1271
+ border-bottom: 3px solid var(--border);
1272
+ flex-shrink: 0;
1273
+ }
1274
+
1275
+ .rank-modal-title {
1276
+ font-family: 'JetBrains Mono', monospace;
1277
+ font-size: 16px;
1278
+ font-weight: 700;
1279
+ }
1280
+
1281
+ .rank-modal-close {
1282
+ background: none;
1283
+ border: 2px solid var(--border);
1284
+ border-radius: 2px;
1285
+ width: 32px;
1286
+ height: 32px;
1287
+ font-size: 20px;
1288
+ line-height: 1;
1289
+ cursor: pointer;
1290
+ color: var(--text-primary);
1291
+ display: flex;
1292
+ align-items: center;
1293
+ justify-content: center;
1294
+ box-shadow: 2px 2px 0 var(--shadow);
1295
+ transition: transform 0.1s, box-shadow 0.1s;
1296
+ }
1297
+
1298
+ .rank-modal-close:hover {
1299
+ transform: translate(-1px, -1px);
1300
+ box-shadow: 3px 3px 0 var(--shadow);
1301
+ }
1302
+
1303
+ .rank-modal-body {
1304
+ overflow-y: auto;
1305
+ padding: 20px;
1306
+ flex: 1;
1307
+ }
1308
+
1309
+ /* Tier sections */
1310
+ .rank-tier-section {
1311
+ margin-bottom: 24px;
1312
+ padding-left: 16px;
1313
+ border-left: 3px solid var(--text-secondary);
1314
+ }
1315
+
1316
+ .rank-tier-section:last-child {
1317
+ margin-bottom: 0;
1318
+ }
1319
+
1320
+ .rank-tier-header {
1321
+ display: flex;
1322
+ align-items: center;
1323
+ justify-content: space-between;
1324
+ margin-bottom: 12px;
1325
+ padding-bottom: 8px;
1326
+ border-bottom: 1px solid var(--bg-accent);
1327
+ }
1328
+
1329
+ .rank-tier-name {
1330
+ font-family: 'JetBrains Mono', monospace;
1331
+ font-size: 14px;
1332
+ font-weight: 700;
1333
+ }
1334
+
1335
+ .rank-tier-range {
1336
+ font-family: 'JetBrains Mono', monospace;
1337
+ font-size: 11px;
1338
+ color: var(--text-secondary);
1339
+ }
1340
+
1341
+ /* Track nodes */
1342
+ .rank-track {
1343
+ position: relative;
1344
+ padding-left: 20px;
1345
+ }
1346
+
1347
+ .rank-track::before {
1348
+ content: '';
1349
+ position: absolute;
1350
+ left: 7px;
1351
+ top: 0;
1352
+ bottom: 0;
1353
+ width: 2px;
1354
+ background: var(--bg-accent);
1355
+ }
1356
+
1357
+ .rank-node {
1358
+ position: relative;
1359
+ display: flex;
1360
+ align-items: center;
1361
+ gap: 12px;
1362
+ padding: 6px 0;
1363
+ }
1364
+
1365
+ .rank-node-dot {
1366
+ width: 16px;
1367
+ height: 16px;
1368
+ border-radius: 50%;
1369
+ border: 2px solid var(--text-secondary);
1370
+ background: var(--bg-card);
1371
+ flex-shrink: 0;
1372
+ position: relative;
1373
+ z-index: 1;
1374
+ }
1375
+
1376
+ .rank-node-dot.unlocked {
1377
+ border-color: currentColor;
1378
+ background: currentColor;
1379
+ }
1380
+
1381
+ .rank-node-dot.milestone {
1382
+ width: 20px;
1383
+ height: 20px;
1384
+ }
1385
+
1386
+ .rank-node-info {
1387
+ display: flex;
1388
+ align-items: baseline;
1389
+ gap: 8px;
1390
+ flex: 1;
1391
+ min-width: 0;
1392
+ }
1393
+
1394
+ .rank-node-number {
1395
+ font-family: 'JetBrains Mono', monospace;
1396
+ font-size: 13px;
1397
+ font-weight: 700;
1398
+ white-space: nowrap;
1399
+ }
1400
+
1401
+ .rank-node-xp {
1402
+ font-family: 'JetBrains Mono', monospace;
1403
+ font-size: 11px;
1404
+ color: var(--text-secondary);
1405
+ white-space: nowrap;
1406
+ }
1407
+
1408
+ .rank-node-label {
1409
+ font-family: 'JetBrains Mono', monospace;
1410
+ font-size: 10px;
1411
+ font-weight: 700;
1412
+ padding: 1px 6px;
1413
+ border: 2px solid;
1414
+ border-radius: 2px;
1415
+ white-space: nowrap;
1416
+ flex-shrink: 0;
1417
+ }
1418
+
1419
+ /* Current rank node */
1420
+ .rank-node.current .rank-node-dot {
1421
+ width: 22px;
1422
+ height: 22px;
1423
+ box-shadow: 0 0 0 3px var(--bg-card), 0 0 0 5px currentColor;
1424
+ animation: rankPulse 2s ease-in-out infinite;
1425
+ }
1426
+
1427
+ .rank-node.current {
1428
+ padding: 10px 0;
1429
+ }
1430
+
1431
+ /* Locked/future nodes */
1432
+ .rank-node.locked .rank-node-number {
1433
+ color: var(--text-secondary);
1434
+ }
1435
+
1436
+ .rank-node.locked .rank-node-xp {
1437
+ color: var(--text-secondary);
1438
+ opacity: 0.6;
1439
+ }
1440
+
1441
+ @keyframes rankPulse {
1442
+ 0%, 100% { box-shadow: 0 0 0 3px var(--bg-card), 0 0 0 5px currentColor; }
1443
+ 50% { box-shadow: 0 0 0 3px var(--bg-card), 0 0 8px 5px currentColor; }
1444
+ }
1445
+
1446
+ /* Clickable rank badge and rank card */
1447
+ #header-rank-badge {
1448
+ cursor: pointer;
1449
+ transition: transform 0.1s, box-shadow 0.1s;
1450
+ }
1451
+
1452
+ #header-rank-badge:hover {
1453
+ transform: translate(-2px, -2px);
1454
+ box-shadow: 5px 5px 0 var(--shadow);
1455
+ }
1456
+
1457
+ .overview-rank-clickable {
1458
+ cursor: pointer;
1459
+ transition: transform 0.1s, box-shadow 0.1s;
1460
+ }
1461
+
1462
+ .overview-rank-clickable:hover {
1463
+ transform: translate(-2px, -2px);
1464
+ box-shadow: 8px 8px 0 var(--shadow);
1465
+ }
1013
1466
  </style>
1014
1467
  </head>
1015
1468
  <body>
@@ -1025,12 +1478,13 @@
1025
1478
  </div>
1026
1479
  </div>
1027
1480
  <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>
1481
+ <select id="agent-filter" class="agent-filter" title="Filter by agent">
1482
+ <option value="">All Agents</option>
1483
+ <option value="claude-code">Claude Code</option>
1484
+ <option value="gemini-cli">Gemini CLI</option>
1485
+ <option value="copilot-cli">Copilot CLI</option>
1486
+ <option value="opencode">OpenCode</option>
1487
+ </select>
1034
1488
  <div class="streak-display" id="header-streak" style="display:none;">
1035
1489
  <span id="streak-icon">&#128293;</span>
1036
1490
  <span id="streak-days">0d</span>
@@ -1063,13 +1517,30 @@
1063
1517
  <div class="card-title">Recent Badges</div>
1064
1518
  <div id="overview-recent-badges" class="recent-badges-list"></div>
1065
1519
  </div>
1066
- <div class="card">
1520
+ <div class="card overview-rank-clickable" onclick="openRankModal()">
1067
1521
  <div class="card-title">Current Rank</div>
1068
1522
  <div id="overview-rank-card" class="rank-card-inner"></div>
1069
1523
  </div>
1070
1524
  </div>
1525
+ <!-- Middle row: Weekly Goals + Streak & Today + Badge Progress -->
1526
+ <div class="overview-mid">
1527
+ <div class="card">
1528
+ <div class="card-title">Weekly Goals <span class="weekly-multiplier" id="weekly-multiplier-label"></span></div>
1529
+ <div id="overview-weekly-goals"></div>
1530
+ </div>
1531
+ <div class="card">
1532
+ <div class="card-title">Streak & Today</div>
1533
+ <div id="overview-streak-today"></div>
1534
+ </div>
1535
+ <div class="card">
1536
+ <div class="card-title">Badge Progress</div>
1537
+ <div id="overview-badge-progress"></div>
1538
+ </div>
1539
+ </div>
1071
1540
  <!-- Stat cards -->
1072
1541
  <div class="stat-grid" id="overview-stat-cards"></div>
1542
+ <!-- Agent breakdown -->
1543
+ <div id="overview-agent-breakdown"></div>
1073
1544
  <!-- Sparkline -->
1074
1545
  <div class="sparkline-section card" id="overview-sparkline-card">
1075
1546
  <div class="card-title">30-Day Activity</div>
@@ -1159,6 +1630,7 @@
1159
1630
  <a class="settings-link" href="/api/achievements" target="_blank">/api/achievements</a>
1160
1631
  <a class="settings-link" href="/api/activity" target="_blank">/api/activity</a>
1161
1632
  <a class="settings-link" href="/api/sessions" target="_blank">/api/sessions</a>
1633
+ <a class="settings-link" href="/api/weekly-goals" target="_blank">/api/weekly-goals</a>
1162
1634
  </div>
1163
1635
  </div>
1164
1636
  </div>
@@ -1180,23 +1652,38 @@
1180
1652
  <span id="refresh-text"></span>
1181
1653
  </div>
1182
1654
 
1655
+ <!-- Rank Progression Modal -->
1656
+ <div id="rank-modal-overlay" class="rank-modal-overlay" style="display:none">
1657
+ <div class="rank-modal">
1658
+ <div class="rank-modal-header">
1659
+ <div class="rank-modal-title">Rank Progression</div>
1660
+ <button class="rank-modal-close" onclick="closeRankModal()">&times;</button>
1661
+ </div>
1662
+ <div class="rank-modal-body" id="rank-modal-body"></div>
1663
+ </div>
1664
+ </div>
1665
+
1183
1666
  <script>
1184
1667
  /* ===================================================================
1185
1668
  bashstats Dashboard - Single Page Application
1186
1669
  =================================================================== */
1187
1670
 
1188
1671
  // ===== STATE =====
1672
+ let selectedAgent = ''
1673
+ let agentBreakdown = null
1674
+
1189
1675
  const state = {
1190
1676
  stats: null,
1191
1677
  achievements: null,
1192
1678
  activity: null,
1193
1679
  sessions: null,
1680
+ weeklyGoals: null,
1194
1681
  lastFetch: null,
1195
1682
  };
1196
1683
 
1197
1684
  // ===== TIER HELPERS =====
1198
- const TIER_NAMES = ['Locked', 'Bronze', 'Silver', 'Gold', 'Diamond', 'Obsidian'];
1199
- const TIER_CLASSES = ['locked', 'bronze', 'silver', 'gold', 'diamond', 'obsidian'];
1685
+ const TIER_NAMES = ['Locked', 'Bronze', 'Silver', 'Gold', 'Diamond', 'Singularity'];
1686
+ const TIER_CLASSES = ['locked', 'bronze', 'silver', 'gold', 'diamond', 'singularity'];
1200
1687
 
1201
1688
  function tierClass(tier) {
1202
1689
  return TIER_CLASSES[tier] || 'locked';
@@ -1209,20 +1696,21 @@
1209
1696
  'var(--tier-silver)',
1210
1697
  'var(--tier-gold)',
1211
1698
  'var(--tier-diamond)',
1212
- 'var(--tier-obsidian)',
1699
+ 'var(--tier-singularity)',
1213
1700
  ];
1214
1701
  return colors[tier] || colors[0];
1215
1702
  }
1216
1703
 
1217
- function rankColor(rank) {
1704
+ function rankColor(rankTier) {
1218
1705
  const map = {
1219
1706
  'Bronze': 'var(--tier-bronze)',
1220
1707
  'Silver': 'var(--tier-silver)',
1221
1708
  'Gold': 'var(--tier-gold)',
1222
1709
  'Diamond': 'var(--tier-diamond)',
1223
1710
  'Obsidian': 'var(--tier-obsidian)',
1711
+ 'System Anomaly': 'var(--tier-system-anomaly)',
1224
1712
  };
1225
- return map[rank] || 'var(--tier-bronze)';
1713
+ return map[rankTier] || 'var(--tier-bronze)';
1226
1714
  }
1227
1715
 
1228
1716
  // ===== FORMAT HELPERS =====
@@ -1283,17 +1771,22 @@
1283
1771
  }
1284
1772
 
1285
1773
  async function loadAllData() {
1286
- const [stats, achievements, activity, sessions] = await Promise.all([
1287
- fetchJSON('/api/stats'),
1288
- fetchJSON('/api/achievements'),
1289
- fetchJSON('/api/activity'),
1290
- fetchJSON('/api/sessions'),
1774
+ const agentParam = selectedAgent ? '?agent=' + selectedAgent : ''
1775
+ const [stats, achievements, activity, sessions, agents, weeklyGoals] = await Promise.all([
1776
+ fetchJSON('/api/stats' + agentParam),
1777
+ fetchJSON('/api/achievements' + agentParam),
1778
+ fetchJSON('/api/activity?days=365'),
1779
+ fetchJSON('/api/sessions' + agentParam),
1780
+ fetchJSON('/api/agents'),
1781
+ fetchJSON('/api/weekly-goals'),
1291
1782
  ]);
1292
1783
 
1293
- state.stats = stats;
1294
- state.achievements = achievements;
1295
- state.activity = activity;
1296
- state.sessions = sessions;
1784
+ if (stats) state.stats = stats;
1785
+ if (achievements) state.achievements = achievements;
1786
+ if (activity) state.activity = activity;
1787
+ if (sessions) state.sessions = sessions;
1788
+ if (agents) agentBreakdown = agents;
1789
+ if (weeklyGoals) state.weeklyGoals = weeklyGoals;
1297
1790
  state.lastFetch = new Date();
1298
1791
 
1299
1792
  renderAll();
@@ -1324,6 +1817,14 @@
1324
1817
  });
1325
1818
  }
1326
1819
 
1820
+ // ===== AGENT FILTER =====
1821
+ function setupAgentFilter() {
1822
+ document.getElementById('agent-filter').addEventListener('change', (e) => {
1823
+ selectedAgent = e.target.value
1824
+ loadAllData()
1825
+ })
1826
+ }
1827
+
1327
1828
  // ===== THEME SWITCHING =====
1328
1829
  function setupTheme() {
1329
1830
  const sel = document.getElementById('settings-theme');
@@ -1365,15 +1866,8 @@
1365
1866
  const rankDot = document.getElementById('header-rank-dot');
1366
1867
  const rankText = document.getElementById('header-rank-text');
1367
1868
  rankBadge.style.display = 'inline-flex';
1368
- rankDot.style.background = rankColor(xp.rank);
1369
- rankText.textContent = xp.rank;
1370
-
1371
- const xpContainer = document.getElementById('header-xp-container');
1372
- const xpLabel = document.getElementById('header-xp-label');
1373
- const xpFill = document.getElementById('header-xp-fill');
1374
- xpContainer.style.display = 'flex';
1375
- xpLabel.textContent = formatNumber(xp.totalXP) + ' XP';
1376
- xpFill.style.width = Math.min(100, (xp.progress || 0) * 100) + '%';
1869
+ rankDot.style.background = rankColor(xp.rankTier);
1870
+ rankText.textContent = `Rank ${xp.rankNumber}`;
1377
1871
  }
1378
1872
 
1379
1873
  if (time) {
@@ -1410,13 +1904,14 @@
1410
1904
  content.style.display = 'block';
1411
1905
 
1412
1906
  const lt = state.stats.lifetime || {};
1907
+ const filesTouched = (lt.totalFilesRead || 0) + (lt.totalFilesEdited || 0) + (lt.totalFilesCreated || 0);
1413
1908
  const cards = [
1414
1909
  { label: 'Sessions', value: formatNumber(lt.totalSessions || 0), sub: 'lifetime' },
1415
1910
  { label: 'Prompts', value: formatNumber(lt.totalPrompts || 0), sub: 'messages sent' },
1416
1911
  { label: 'Tool Calls', value: formatNumber(lt.totalToolCalls || 0), sub: 'total invocations' },
1417
1912
  { label: 'Hours', value: formatHours(lt.totalDurationSeconds || 0), sub: 'coding time' },
1418
- { label: 'Tokens Used', value: formatTokens(lt.totalTokens || 0), sub: 'input + output' },
1419
- { label: 'Bash Cmds', value: formatNumber(lt.totalBashCommands || lt.totalToolCalls || 0), sub: 'commands run' },
1913
+ { label: 'Tokens Used', value: formatTokens(lt.totalTokens || 0), sub: 'all token types' },
1914
+ { label: 'Files Touched', value: formatNumber(filesTouched), sub: 'read + edit + create' },
1420
1915
  ];
1421
1916
 
1422
1917
  const cardsEl = document.getElementById('overview-stat-cards');
@@ -1428,6 +1923,10 @@
1428
1923
  </div>
1429
1924
  `).join('');
1430
1925
 
1926
+ // Agent breakdown panel (only shown for "All Agents")
1927
+ const breakdownEl = document.getElementById('overview-agent-breakdown');
1928
+ if (breakdownEl) breakdownEl.innerHTML = renderAgentBreakdown();
1929
+
1431
1930
  // Sparkline
1432
1931
  renderSparkline();
1433
1932
 
@@ -1440,6 +1939,11 @@
1440
1939
 
1441
1940
  // Rank card
1442
1941
  renderOverviewRank();
1942
+
1943
+ // Middle row cards
1944
+ renderWeeklyGoals();
1945
+ renderStreakToday();
1946
+ renderBadgeProgress();
1443
1947
  }
1444
1948
 
1445
1949
  function renderEmptyOverview() {
@@ -1449,7 +1953,7 @@
1449
1953
  <div class="stat-card"><div class="stat-card-label">Tool Calls</div><div class="stat-card-number mono">0</div></div>
1450
1954
  <div class="stat-card"><div class="stat-card-label">Hours</div><div class="stat-card-number mono">0</div></div>
1451
1955
  <div class="stat-card"><div class="stat-card-label">Tokens Used</div><div class="stat-card-number mono">0</div></div>
1452
- <div class="stat-card"><div class="stat-card-label">Bash Cmds</div><div class="stat-card-number mono">0</div></div>
1956
+ <div class="stat-card"><div class="stat-card-label">Files Touched</div><div class="stat-card-number mono">0</div></div>
1453
1957
  `;
1454
1958
  }
1455
1959
 
@@ -1465,7 +1969,7 @@
1465
1969
  for (let i = 29; i >= 0; i--) {
1466
1970
  const d = new Date(today);
1467
1971
  d.setDate(d.getDate() - i);
1468
- const dateStr = d.toISOString().split('T')[0];
1972
+ const dateStr = d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0');
1469
1973
  const found = activity.find(a => a.date === dateStr);
1470
1974
  const val = found ? (found.sessions + found.prompts + found.tool_calls) : 0;
1471
1975
  days.push({ date: dateStr, value: val });
@@ -1514,23 +2018,164 @@
1514
2018
  }
1515
2019
 
1516
2020
  const pct = Math.min(100, (xp.progress || 0) * 100);
1517
- const rc = rankColor(xp.rank);
1518
- const initial = xp.rank.charAt(0);
2021
+ const rc = rankColor(xp.rankTier);
2022
+ const rankNum = xp.rankNumber || 0;
1519
2023
  el.innerHTML = `
1520
2024
  <div class="rank-card-icon" style="border-color:${rc};background:${rc}22">
1521
- <span class="rank-card-icon-text" style="color:${rc}">${escapeHtml(initial)}</span>
2025
+ <span class="rank-card-icon-text" style="color:${rc}">${rankNum}</span>
1522
2026
  </div>
1523
2027
  <div class="rank-card-info">
1524
- <div class="rank-card-rank" style="color:${rc}">${escapeHtml(xp.rank)}</div>
2028
+ <div class="rank-card-rank" style="color:${rc}">Rank ${rankNum} &middot; ${escapeHtml(xp.rankTier)}</div>
1525
2029
  <div class="rank-card-xp">${formatNumber(xp.totalXP)} / ${formatNumber(xp.nextRankXP)} XP</div>
1526
2030
  <div class="rank-progress-track">
1527
2031
  <div class="rank-progress-fill" style="width:${pct}%;background:${rc}"></div>
1528
2032
  </div>
1529
- <div class="rank-progress-label">${pct.toFixed(1)}% to ${escapeHtml(xp.rank === 'Obsidian' ? 'max rank' : 'next rank')}</div>
2033
+ <div class="rank-progress-label">${pct.toFixed(1)}% to ${escapeHtml(rankNum >= 500 ? 'System Anomaly' : 'Rank ' + (rankNum + 1))}</div>
1530
2034
  </div>
1531
2035
  `;
1532
2036
  }
1533
2037
 
2038
+ // ===== RENDER: WEEKLY GOALS =====
2039
+ function renderWeeklyGoals() {
2040
+ const el = document.getElementById('overview-weekly-goals');
2041
+ const label = document.getElementById('weekly-multiplier-label');
2042
+ const goals = state.weeklyGoals;
2043
+
2044
+ if (!goals || !goals.challenges) {
2045
+ el.innerHTML = '<div style="font-size:12px;color:var(--text-secondary);padding:8px 0;">No weekly goals data</div>';
2046
+ if (label) label.textContent = '';
2047
+ return;
2048
+ }
2049
+
2050
+ if (label) label.textContent = goals.multiplier > 1 ? '\u00B7 ' + goals.multiplier.toFixed(1) + 'x' : '';
2051
+
2052
+ let html = goals.challenges.map(c => {
2053
+ const pct = Math.min(100, c.progress * 100);
2054
+ return '<div class="weekly-challenge-item">' +
2055
+ '<div class="weekly-challenge-desc">' + (c.completed ? '<span style="color:var(--success);font-weight:700">\u2713</span> ' : '') + escapeHtml(c.description) + '</div>' +
2056
+ '<div class="badge-progress-row">' +
2057
+ '<div class="badge-progress-track">' +
2058
+ '<div class="badge-progress-fill' + (c.completed ? ' maxed' : '') + '" style="width:' + pct + '%"></div>' +
2059
+ '</div>' +
2060
+ '<span class="badge-progress-text">' + c.current + '/' + c.threshold + '</span>' +
2061
+ '</div>' +
2062
+ '<div class="weekly-challenge-meta">' +
2063
+ '<span></span>' +
2064
+ '<span class="weekly-challenge-xp">+' + c.xpReward + ' XP</span>' +
2065
+ '</div>' +
2066
+ '</div>';
2067
+ }).join('');
2068
+
2069
+ html += '<div style="margin-top:8px;font-size:11px;color:var(--text-secondary);font-family:\'JetBrains Mono\',monospace;">' +
2070
+ goals.daysActive + '/7 days active \u00B7 ' + goals.multiplier.toFixed(1) + 'x multiplier</div>';
2071
+
2072
+ el.innerHTML = html;
2073
+ }
2074
+
2075
+ // ===== RENDER: STREAK & TODAY =====
2076
+ function renderStreakToday() {
2077
+ const el = document.getElementById('overview-streak-today');
2078
+ const time = state.stats?.time;
2079
+ const activity = state.activity || [];
2080
+
2081
+ const today = new Date();
2082
+ const todayStr = today.getFullYear() + '-' + String(today.getMonth()+1).padStart(2,'0') + '-' + String(today.getDate()).padStart(2,'0');
2083
+ const todayActivity = activity.find(a => a.date === todayStr);
2084
+
2085
+ const streak = time?.currentStreak || 0;
2086
+ const todaySessions = todayActivity?.sessions || 0;
2087
+ const todayPrompts = todayActivity?.prompts || 0;
2088
+ const todayTools = todayActivity?.tool_calls || 0;
2089
+ const todayHours = todayActivity ? (todayActivity.duration_seconds / 3600).toFixed(1) : '0';
2090
+
2091
+ el.innerHTML =
2092
+ '<div class="today-streak">' +
2093
+ '<span class="today-streak-number">' + streak + '</span>' +
2094
+ '<span class="today-streak-label">day streak' + (streak > 0 ? '<br>\uD83D\uDD25' : '') + '</span>' +
2095
+ '</div>' +
2096
+ '<div class="today-stat-grid">' +
2097
+ '<div class="today-stat-item"><div class="today-stat-value">' + todaySessions + '</div><div class="today-stat-label">Sessions</div></div>' +
2098
+ '<div class="today-stat-item"><div class="today-stat-value">' + todayPrompts + '</div><div class="today-stat-label">Prompts</div></div>' +
2099
+ '<div class="today-stat-item"><div class="today-stat-value">' + todayTools + '</div><div class="today-stat-label">Tools</div></div>' +
2100
+ '<div class="today-stat-item"><div class="today-stat-value">' + todayHours + '</div><div class="today-stat-label">Hours</div></div>' +
2101
+ '</div>';
2102
+ }
2103
+
2104
+ // ===== RENDER: BADGE PROGRESS =====
2105
+ function renderBadgeProgress() {
2106
+ const el = document.getElementById('overview-badge-progress');
2107
+ const badges = state.achievements?.badges || [];
2108
+
2109
+ const totalBadges = badges.length;
2110
+ const unlockedBadges = badges.filter(b => b.unlocked).length;
2111
+
2112
+ const locked = badges.filter(b => !b.unlocked && !b.secret);
2113
+ locked.sort((a, b) => (b.progress || 0) - (a.progress || 0));
2114
+ const nextBadge = locked[0];
2115
+
2116
+ const pct = totalBadges > 0 ? Math.round((unlockedBadges / totalBadges) * 100) : 0;
2117
+
2118
+ let html =
2119
+ '<div class="badge-progress-summary">' +
2120
+ '<span class="badge-progress-count">' + unlockedBadges + '</span>' +
2121
+ '<span class="badge-progress-total">/ ' + totalBadges + ' badges unlocked (' + pct + '%)</span>' +
2122
+ '</div>' +
2123
+ '<div class="badge-progress-row" style="margin-bottom:12px;">' +
2124
+ '<div class="badge-progress-track">' +
2125
+ '<div class="badge-progress-fill" style="width:' + pct + '%"></div>' +
2126
+ '</div>' +
2127
+ '</div>';
2128
+
2129
+ if (nextBadge) {
2130
+ const nextPct = Math.min(100, (nextBadge.progress || 0) * 100);
2131
+ html +=
2132
+ '<div class="badge-next-item">' +
2133
+ '<div class="badge-next-name">' + (nextBadge.icon || '\uD83C\uDFC5') + ' ' + escapeHtml(nextBadge.name) + '</div>' +
2134
+ '<div class="badge-progress-row">' +
2135
+ '<div class="badge-progress-track">' +
2136
+ '<div class="badge-progress-fill" style="width:' + nextPct + '%"></div>' +
2137
+ '</div>' +
2138
+ '<span class="badge-progress-text">' + formatNumber(nextBadge.value || 0) + '/' + formatNumber(nextBadge.nextThreshold || 0) + '</span>' +
2139
+ '</div>' +
2140
+ '<div class="badge-next-meta">' +
2141
+ '<span>' + escapeHtml(nextBadge.description || '') + '</span>' +
2142
+ '<span>' + nextPct.toFixed(0) + '%</span>' +
2143
+ '</div>' +
2144
+ '</div>';
2145
+ }
2146
+
2147
+ el.innerHTML = html;
2148
+ }
2149
+
2150
+ // ===== RENDER: AGENT BREAKDOWN =====
2151
+ function renderAgentBreakdown() {
2152
+ if (selectedAgent || !agentBreakdown || !agentBreakdown.sessionsPerAgent) return ''
2153
+ const entries = Object.entries(agentBreakdown.sessionsPerAgent).sort(([,a], [,b]) => b - a)
2154
+ if (entries.length <= 1) return ''
2155
+ const maxSessions = Math.max(...entries.map(([,c]) => c), 1)
2156
+ const agentColors = { 'claude-code': '#d97706', 'gemini-cli': '#4285f4', 'copilot-cli': '#6e40c9', 'opencode': '#10b981' }
2157
+ const agentNames = { 'claude-code': 'Claude Code', 'gemini-cli': 'Gemini CLI', 'copilot-cli': 'Copilot CLI', 'opencode': 'OpenCode' }
2158
+
2159
+ const bars = entries.map(([agent, count]) => {
2160
+ const pct = Math.round(count / maxSessions * 100)
2161
+ const hours = agentBreakdown.hoursPerAgent[agent] ?? 0
2162
+ const color = agentColors[agent] ?? 'var(--accent)'
2163
+ const name = agentNames[agent] ?? agent
2164
+ return '<div class="agent-bar">' +
2165
+ '<span class="agent-bar-label">' + escapeHtml(name) + '</span>' +
2166
+ '<div class="agent-bar-track"><div class="agent-bar-fill" style="width:' + pct + '%;background:' + color + '"></div></div>' +
2167
+ '<span class="agent-bar-value">' + count + ' / ' + hours + 'h</span>' +
2168
+ '</div>'
2169
+ }).join('')
2170
+
2171
+ const favName = agentNames[agentBreakdown.favoriteAgent] ?? agentBreakdown.favoriteAgent
2172
+ return '<div class="card agent-breakdown-card">' +
2173
+ '<h3>Agent Breakdown</h3>' +
2174
+ '<p style="margin:0 0 8px;font-size:0.85rem">Favorite: <strong>' + escapeHtml(favName) + '</strong> &middot; ' + agentBreakdown.distinctAgents + ' agent' + (agentBreakdown.distinctAgents !== 1 ? 's' : '') + ' used</p>' +
2175
+ bars +
2176
+ '</div>'
2177
+ }
2178
+
1534
2179
  // ===== RENDER: STATS =====
1535
2180
  function renderOverviewHeatmap() {
1536
2181
  const wrapper = document.getElementById('overview-heatmap');
@@ -1621,11 +2266,12 @@
1621
2266
  <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><div style="text-align:right">Tokens</div>
1622
2267
  </div>
1623
2268
  ${recent.map(s => {
1624
- const sessionTokens = (s.input_tokens || 0) + (s.output_tokens || 0);
2269
+ const sessionTokens = (s.input_tokens || 0) + (s.output_tokens || 0) + (s.cache_creation_input_tokens || 0) + (s.cache_read_input_tokens || 0);
2270
+ const agentBadge = '<span class="agent-badge ' + (s.agent || 'claude-code') + '">' + escapeHtml(s.agent || 'claude-code') + '</span>';
1625
2271
  return `
1626
2272
  <div class="session-item">
1627
2273
  <div class="session-date">${escapeHtml(formatDateTime(s.start_time || s.startTime || s.started_at || ''))}</div>
1628
- <div class="session-project">${escapeHtml(s.project || s.projectName || 'Unknown')}</div>
2274
+ <div class="session-project">${escapeHtml(s.project || s.projectName || 'Unknown')}${agentBadge}</div>
1629
2275
  <div class="session-stat">${formatDuration(s.duration_seconds || s.durationSeconds || 0)}</div>
1630
2276
  <div class="session-stat">${formatNumber(s.prompt_count || s.prompts || s.promptCount || 0)}</div>
1631
2277
  <div class="session-stat">${formatNumber(s.tool_count || s.tool_calls || s.toolCalls || 0)}</div>
@@ -1671,12 +2317,12 @@
1671
2317
 
1672
2318
  // Token Usage
1673
2319
  html += buildStatsSection('Token Usage', [
1674
- ['Total Tokens', formatTokens((lt.totalInputTokens || 0) + (lt.totalOutputTokens || 0))],
2320
+ ['Total Tokens', formatTokens(lt.totalTokens || 0)],
1675
2321
  ['Input Tokens', formatTokens(lt.totalInputTokens)],
1676
2322
  ['Output Tokens', formatTokens(lt.totalOutputTokens)],
1677
2323
  ['Cache Read Tokens', formatTokens(lt.totalCacheReadTokens)],
1678
2324
  ['Cache Creation Tokens', formatTokens(lt.totalCacheCreationTokens)],
1679
- ['Avg Tokens / Session', formatTokens(lt.totalSessions ? Math.round(((lt.totalInputTokens || 0) + (lt.totalOutputTokens || 0)) / lt.totalSessions) : 0)],
2325
+ ['Avg Tokens / Session', formatTokens(lt.totalSessions ? Math.round((lt.totalTokens || 0) / lt.totalSessions) : 0)],
1680
2326
  ]);
1681
2327
 
1682
2328
  // Tool Breakdown
@@ -1757,16 +2403,24 @@
1757
2403
  });
1758
2404
 
1759
2405
  let html = '';
1760
- const catOrder = ['volume', 'tools', 'streaks', 'time', 'exploration', 'humor', 'secret'];
2406
+ const catOrder = ['volume', 'token_usage', 'tool_mastery', 'time', 'session_behavior', 'behavioral', 'prompt_patterns', 'resilience', 'error_recovery', 'tool_combos', 'shipping', 'project_dedication', 'multi_agent', 'wild_card', 'aspirational', 'secret'];
1761
2407
  const catNames = {
1762
2408
  volume: 'Volume',
1763
- tools: 'Tools',
1764
- streaks: 'Streaks',
1765
- time: 'Time',
1766
- exploration: 'Exploration',
1767
- humor: 'Humor',
2409
+ token_usage: 'Token Usage',
2410
+ tool_mastery: 'Tool Mastery',
2411
+ time: 'Time & Patterns',
2412
+ session_behavior: 'Session Behavior',
2413
+ behavioral: 'Behavioral',
2414
+ prompt_patterns: 'Prompt Patterns',
2415
+ resilience: 'Resilience',
2416
+ error_recovery: 'Error & Recovery',
2417
+ tool_combos: 'Tool Combos',
2418
+ shipping: 'Shipping & Projects',
2419
+ project_dedication: 'Project Dedication',
2420
+ multi_agent: 'Multi-Agent',
2421
+ wild_card: 'Wild Card',
2422
+ aspirational: 'Aspirational',
1768
2423
  secret: 'Secret',
1769
- general: 'General',
1770
2424
  };
1771
2425
 
1772
2426
  // Sort: known categories first, then others
@@ -1786,6 +2440,7 @@
1786
2440
  const cardClass = isSecretLocked ? 'badge-card secret-locked' : (isLocked ? 'badge-card locked' : 'badge-card');
1787
2441
  const displayName = isSecretLocked ? '???' : b.name;
1788
2442
  const displayDesc = isSecretLocked ? 'Unlock this secret badge to reveal it.' : (b.description || '');
2443
+ const displayTrigger = isSecretLocked ? '' : (b.trigger || '');
1789
2444
  const pct = b.maxed ? 100 : Math.min(100, (b.progress || 0) * 100);
1790
2445
  const fillClass = b.maxed ? 'badge-progress-fill maxed' : 'badge-progress-fill';
1791
2446
 
@@ -1799,6 +2454,7 @@
1799
2454
  <span class="badge-tier-label" style="color:${tierColor(b.tier)};border-color:${tierColor(b.tier)}">${escapeHtml(b.tierName || TIER_NAMES[b.tier] || 'Locked')}</span>
1800
2455
  </div>
1801
2456
  ${displayDesc ? `<div class="badge-description">${escapeHtml(displayDesc)}</div>` : ''}
2457
+ ${displayTrigger ? `<div class="badge-trigger">${escapeHtml(displayTrigger)}</div>` : ''}
1802
2458
  <div class="badge-progress-row">
1803
2459
  <div class="badge-progress-track">
1804
2460
  <div class="${fillClass}" style="width:${pct}%"></div>
@@ -1886,6 +2542,106 @@
1886
2542
  }
1887
2543
  }
1888
2544
 
2545
+ // ===== RANK PROGRESSION MODAL =====
2546
+ const RANK_TIER_BRACKETS = [
2547
+ { tier: 'Bronze', minRank: 1, maxRank: 100, color: 'var(--tier-bronze)' },
2548
+ { tier: 'Silver', minRank: 101, maxRank: 200, color: 'var(--tier-silver)' },
2549
+ { tier: 'Gold', minRank: 201, maxRank: 300, color: 'var(--tier-gold)' },
2550
+ { tier: 'Diamond', minRank: 301, maxRank: 400, color: 'var(--tier-diamond)' },
2551
+ { tier: 'Obsidian', minRank: 401, maxRank: 499, color: 'var(--tier-obsidian)' },
2552
+ { tier: 'System Anomaly', minRank: 500, maxRank: 500, color: 'var(--tier-system-anomaly)' },
2553
+ ];
2554
+
2555
+ function xpForRankClient(rank) {
2556
+ if (rank <= 0) return 0;
2557
+ return Math.floor(10 * Math.pow(rank, 2.2));
2558
+ }
2559
+
2560
+ function openRankModal() {
2561
+ const xp = state.achievements?.xp;
2562
+ if (!xp) return;
2563
+
2564
+ const currentRank = xp.rankNumber || 1;
2565
+ const totalXP = xp.totalXP || 0;
2566
+ const body = document.getElementById('rank-modal-body');
2567
+ let html = '';
2568
+
2569
+ RANK_TIER_BRACKETS.forEach(bracket => {
2570
+ const isCurrent = currentRank >= bracket.minRank && currentRank <= bracket.maxRank;
2571
+ const isPast = currentRank > bracket.maxRank;
2572
+ const tierOpacity = !isPast && !isCurrent ? ' opacity:0.5;' : '';
2573
+ const xpStart = formatNumber(xpForRankClient(bracket.minRank));
2574
+ const xpEnd = formatNumber(xpForRankClient(bracket.maxRank));
2575
+
2576
+ html += `<div class="rank-tier-section" style="border-left-color:${bracket.color};${tierOpacity}">`;
2577
+ html += `<div class="rank-tier-header">`;
2578
+ html += `<span class="rank-tier-name" style="color:${bracket.color}">${escapeHtml(bracket.tier)}</span>`;
2579
+ html += `<span class="rank-tier-range">Rank ${bracket.minRank}-${bracket.maxRank} &middot; ${xpStart}-${xpEnd} XP</span>`;
2580
+ html += `</div>`;
2581
+ html += `<div class="rank-track" style="color:${bracket.color}">`;
2582
+
2583
+ // Determine milestone ranks: tier start, every 25th within tier, tier end, plus current rank
2584
+ const milestones = new Set();
2585
+ milestones.add(bracket.minRank);
2586
+ for (let r = bracket.minRank; r <= bracket.maxRank; r += 25) {
2587
+ milestones.add(r);
2588
+ }
2589
+ milestones.add(bracket.maxRank);
2590
+
2591
+ // Always include the current rank if it's in this tier
2592
+ if (isCurrent) {
2593
+ milestones.add(currentRank);
2594
+ }
2595
+
2596
+ const sortedMilestones = [...milestones].sort((a, b) => a - b);
2597
+
2598
+ sortedMilestones.forEach(rank => {
2599
+ const xpRequired = xpForRankClient(rank);
2600
+ const isUnlocked = rank <= currentRank;
2601
+ const isCurrentRank = rank === currentRank;
2602
+ const isMilestoneRank = rank % 25 === 0 || rank === bracket.minRank || rank === bracket.maxRank;
2603
+
2604
+ const nodeClasses = ['rank-node'];
2605
+ if (isCurrentRank) nodeClasses.push('current');
2606
+ if (!isUnlocked) nodeClasses.push('locked');
2607
+
2608
+ const dotClasses = ['rank-node-dot'];
2609
+ if (isUnlocked) dotClasses.push('unlocked');
2610
+ if (isMilestoneRank || isCurrentRank) dotClasses.push('milestone');
2611
+
2612
+ html += `<div class="${nodeClasses.join(' ')}" ${isCurrentRank ? 'id="rank-current-node"' : ''}>`;
2613
+ html += `<div class="${dotClasses.join(' ')}"></div>`;
2614
+ html += `<div class="rank-node-info">`;
2615
+ html += `<span class="rank-node-number" ${isUnlocked ? `style="color:${bracket.color}"` : ''}>Rank ${rank}</span>`;
2616
+ html += `<span class="rank-node-xp">${formatNumber(xpRequired)} XP</span>`;
2617
+ html += `</div>`;
2618
+ if (isCurrentRank) {
2619
+ html += `<span class="rank-node-label" style="color:${bracket.color};border-color:${bracket.color}">CURRENT</span>`;
2620
+ }
2621
+ html += `</div>`;
2622
+ });
2623
+
2624
+ html += `</div></div>`;
2625
+ });
2626
+
2627
+ body.innerHTML = html;
2628
+
2629
+ const overlay = document.getElementById('rank-modal-overlay');
2630
+ overlay.style.display = 'flex';
2631
+
2632
+ // Scroll to current rank after a tick to allow rendering
2633
+ requestAnimationFrame(() => {
2634
+ const currentNode = document.getElementById('rank-current-node');
2635
+ if (currentNode) {
2636
+ currentNode.scrollIntoView({ behavior: 'smooth', block: 'center' });
2637
+ }
2638
+ });
2639
+ }
2640
+
2641
+ function closeRankModal() {
2642
+ document.getElementById('rank-modal-overlay').style.display = 'none';
2643
+ }
2644
+
1889
2645
  // ===== AUTO-REFRESH =====
1890
2646
  function startAutoRefresh() {
1891
2647
  setInterval(() => {
@@ -1896,9 +2652,25 @@
1896
2652
  // ===== INIT =====
1897
2653
  function init() {
1898
2654
  setupTabs();
2655
+ setupAgentFilter();
1899
2656
  setupTheme();
1900
2657
  loadAllData();
1901
2658
  startAutoRefresh();
2659
+
2660
+ // Rank modal: header badge click
2661
+ document.getElementById('header-rank-badge').addEventListener('click', openRankModal);
2662
+
2663
+ // Rank modal: overlay click to close
2664
+ document.getElementById('rank-modal-overlay').addEventListener('click', (e) => {
2665
+ if (e.target === e.currentTarget) closeRankModal();
2666
+ });
2667
+
2668
+ // Rank modal: escape key
2669
+ document.addEventListener('keydown', (e) => {
2670
+ if (e.key === 'Escape' && document.getElementById('rank-modal-overlay').style.display !== 'none') {
2671
+ closeRankModal();
2672
+ }
2673
+ });
1902
2674
  }
1903
2675
 
1904
2676
  document.addEventListener('DOMContentLoaded', init);