@nockdev/hsa 1.0.0 → 1.0.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.
package/dashboard.html ADDED
@@ -0,0 +1,2348 @@
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>HSA Dashboard</title>
7
+ <meta
8
+ name="description"
9
+ content="HSA Engine — Real-time monitoring dashboard"
10
+ />
11
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
12
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
13
+ <link
14
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap"
15
+ rel="stylesheet"
16
+ />
17
+ <style>
18
+ /* ================================================================
19
+ DESIGN SYSTEM — Vercel/GitHub Dark Theme
20
+ ================================================================ */
21
+ :root {
22
+ --bg-base: #0d1117;
23
+ --bg-surface: #161b22;
24
+ --bg-elevated: #1c2128;
25
+ --bg-overlay: #21262d;
26
+ --bg-hover: rgba(255, 255, 255, 0.04);
27
+ --border: rgba(240, 246, 252, 0.1);
28
+ --border-hover: rgba(240, 246, 252, 0.2);
29
+ --border-active: #388bfd;
30
+ --text-primary: #e6edf3;
31
+ --text-secondary: #8b949e;
32
+ --text-tertiary: #6e7681;
33
+ --text-link: #58a6ff;
34
+ --accent: #388bfd;
35
+ --accent-subtle: rgba(56, 139, 253, 0.1);
36
+ --green: #3fb950;
37
+ --green-subtle: rgba(63, 185, 80, 0.15);
38
+ --yellow: #d29922;
39
+ --yellow-subtle: rgba(210, 153, 34, 0.15);
40
+ --red: #f85149;
41
+ --red-subtle: rgba(248, 81, 73, 0.15);
42
+ --purple: #bc8cff;
43
+ --purple-subtle: rgba(188, 140, 255, 0.15);
44
+ --orange: #db6d28;
45
+ --cyan: #39d2c0;
46
+ --pink: #f778ba;
47
+ --cat-language: #388bfd;
48
+ --cat-framework: #bc8cff;
49
+ --cat-library: #39d2c0;
50
+ --cat-build-tool: #d29922;
51
+ --cat-test-framework: #3fb950;
52
+ --cat-runtime: #f778ba;
53
+ --cat-database: #db6d28;
54
+ --cat-devops: #8b949e;
55
+ --radius-sm: 6px;
56
+ --radius-md: 8px;
57
+ --radius-lg: 12px;
58
+ --font-sans:
59
+ "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
60
+ --font-mono: "JetBrains Mono", "Cascadia Code", "Fira Code", monospace;
61
+ --transition: 150ms cubic-bezier(0.4, 0, 0.2, 1);
62
+ --sidebar-width: 280px;
63
+ }
64
+
65
+ /* PageRank panel */
66
+ .pagerank-panel {
67
+ background: var(--bg-surface);
68
+ border: 1px solid var(--border);
69
+ border-radius: var(--radius-lg);
70
+ padding: 20px;
71
+ margin-top: 16px;
72
+ }
73
+ .pagerank-list {
74
+ display: flex;
75
+ flex-direction: column;
76
+ gap: 4px;
77
+ max-height: 320px;
78
+ overflow-y: auto;
79
+ }
80
+ .pagerank-item {
81
+ display: grid;
82
+ grid-template-columns: 20px 1fr 60px;
83
+ gap: 8px;
84
+ align-items: center;
85
+ padding: 5px 8px;
86
+ border-radius: var(--radius-sm);
87
+ transition: background var(--transition);
88
+ }
89
+ .pagerank-item:hover {
90
+ background: var(--bg-hover);
91
+ }
92
+ .pagerank-rank {
93
+ font-family: var(--font-mono);
94
+ font-size: 10px;
95
+ color: var(--text-tertiary);
96
+ text-align: right;
97
+ }
98
+ .pagerank-file {
99
+ font-family: var(--font-mono);
100
+ font-size: 12px;
101
+ color: var(--text-primary);
102
+ overflow: hidden;
103
+ text-overflow: ellipsis;
104
+ white-space: nowrap;
105
+ }
106
+ .pagerank-score {
107
+ display: flex;
108
+ align-items: center;
109
+ gap: 4px;
110
+ }
111
+ .pagerank-bar {
112
+ height: 4px;
113
+ border-radius: 2px;
114
+ background: linear-gradient(90deg, var(--accent), var(--purple));
115
+ min-width: 2px;
116
+ transition: width 0.3s;
117
+ }
118
+ .pagerank-val {
119
+ font-family: var(--font-mono);
120
+ font-size: 10px;
121
+ color: var(--text-tertiary);
122
+ min-width: 26px;
123
+ text-align: right;
124
+ }
125
+ .pagerank-meta {
126
+ display: flex;
127
+ gap: 16px;
128
+ margin-bottom: 8px;
129
+ font-size: 11px;
130
+ color: var(--text-tertiary);
131
+ font-family: var(--font-mono);
132
+ }
133
+ .pagerank-meta span strong {
134
+ color: var(--text-secondary);
135
+ }
136
+
137
+ *,
138
+ *::before,
139
+ *::after {
140
+ box-sizing: border-box;
141
+ margin: 0;
142
+ padding: 0;
143
+ }
144
+ html {
145
+ font-size: 14px;
146
+ -webkit-font-smoothing: antialiased;
147
+ }
148
+ body {
149
+ font-family: var(--font-sans);
150
+ background: var(--bg-base);
151
+ color: var(--text-primary);
152
+ line-height: 1.5;
153
+ min-height: 100vh;
154
+ }
155
+
156
+ /* ================================================================
157
+ LAYOUT — Sidebar + Main
158
+ ================================================================ */
159
+ .app-layout {
160
+ display: grid;
161
+ grid-template-columns: var(--sidebar-width) 1fr;
162
+ grid-template-rows: auto auto 1fr;
163
+ grid-template-areas:
164
+ "header header"
165
+ "context context"
166
+ "sidebar main";
167
+ min-height: 100vh;
168
+ }
169
+
170
+ /* HEADER */
171
+ .header {
172
+ grid-area: header;
173
+ display: flex;
174
+ align-items: center;
175
+ justify-content: space-between;
176
+ padding: 12px 24px;
177
+ border-bottom: 1px solid var(--border);
178
+ background: var(--bg-surface);
179
+ position: sticky;
180
+ top: 0;
181
+ z-index: 100;
182
+ }
183
+ .header-left {
184
+ display: flex;
185
+ align-items: center;
186
+ gap: 12px;
187
+ }
188
+ .logo {
189
+ display: flex;
190
+ align-items: center;
191
+ gap: 8px;
192
+ }
193
+ .logo-dot {
194
+ width: 8px;
195
+ height: 8px;
196
+ border-radius: 50%;
197
+ background: var(--green);
198
+ box-shadow: 0 0 8px rgba(63, 185, 80, 0.4);
199
+ transition: background var(--transition);
200
+ }
201
+ .logo-dot.error {
202
+ background: var(--red);
203
+ box-shadow: 0 0 8px rgba(248, 81, 73, 0.4);
204
+ }
205
+ .logo-dot.connecting {
206
+ background: var(--yellow);
207
+ animation: pulse 1.5s infinite;
208
+ }
209
+ @keyframes pulse {
210
+ 0%,
211
+ 100% {
212
+ opacity: 1;
213
+ }
214
+ 50% {
215
+ opacity: 0.4;
216
+ }
217
+ }
218
+ .logo-text {
219
+ font-weight: 700;
220
+ font-size: 15px;
221
+ letter-spacing: -0.3px;
222
+ }
223
+ .logo-version {
224
+ font-size: 11px;
225
+ color: var(--text-tertiary);
226
+ font-family: var(--font-mono);
227
+ background: var(--bg-base);
228
+ padding: 2px 6px;
229
+ border-radius: 4px;
230
+ border: 1px solid var(--border);
231
+ }
232
+ .header-right {
233
+ display: flex;
234
+ align-items: center;
235
+ gap: 8px;
236
+ }
237
+ .header-nav {
238
+ display: flex;
239
+ gap: 4px;
240
+ }
241
+ .nav-link {
242
+ font-size: 12px;
243
+ color: var(--text-secondary);
244
+ padding: 4px 8px;
245
+ border-radius: 4px;
246
+ text-decoration: none;
247
+ transition: all var(--transition);
248
+ }
249
+ .nav-link:hover {
250
+ color: var(--text-primary);
251
+ background: var(--bg-hover);
252
+ }
253
+ .nav-link.active {
254
+ color: var(--accent);
255
+ background: var(--accent-subtle);
256
+ }
257
+ .endpoint-input {
258
+ background: var(--bg-base);
259
+ border: 1px solid var(--border);
260
+ color: var(--text-primary);
261
+ font-family: var(--font-mono);
262
+ font-size: 12px;
263
+ padding: 5px 10px;
264
+ border-radius: var(--radius-sm);
265
+ width: 200px;
266
+ transition: border-color var(--transition);
267
+ }
268
+ .endpoint-input:focus {
269
+ outline: none;
270
+ border-color: var(--accent);
271
+ }
272
+
273
+ .btn {
274
+ background: var(--bg-elevated);
275
+ border: 1px solid var(--border);
276
+ color: var(--text-secondary);
277
+ font-family: var(--font-sans);
278
+ font-size: 12px;
279
+ font-weight: 500;
280
+ padding: 5px 10px;
281
+ border-radius: var(--radius-sm);
282
+ cursor: pointer;
283
+ transition: all var(--transition);
284
+ display: inline-flex;
285
+ align-items: center;
286
+ gap: 4px;
287
+ }
288
+ .btn:hover {
289
+ background: var(--bg-overlay);
290
+ color: var(--text-primary);
291
+ border-color: var(--border-hover);
292
+ }
293
+ .btn.active {
294
+ background: var(--accent-subtle);
295
+ border-color: var(--accent);
296
+ color: var(--accent);
297
+ }
298
+
299
+ /* CONTEXT SELECTOR BAR */
300
+ .context-bar {
301
+ grid-area: context;
302
+ display: flex;
303
+ align-items: center;
304
+ gap: 12px;
305
+ padding: 8px 24px;
306
+ border-bottom: 1px solid var(--border);
307
+ background: var(--bg-surface);
308
+ flex-wrap: wrap;
309
+ }
310
+ .context-bar label {
311
+ font-size: 11px;
312
+ color: var(--text-tertiary);
313
+ text-transform: uppercase;
314
+ letter-spacing: 0.5px;
315
+ font-weight: 600;
316
+ }
317
+ .context-select {
318
+ background: var(--bg-base);
319
+ border: 1px solid var(--border);
320
+ color: var(--text-primary);
321
+ font-family: var(--font-sans);
322
+ font-size: 12px;
323
+ padding: 4px 24px 4px 8px;
324
+ border-radius: 16px;
325
+ cursor: pointer;
326
+ transition: border-color var(--transition);
327
+ appearance: none;
328
+ -webkit-appearance: none;
329
+ background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%238b949e' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
330
+ background-repeat: no-repeat;
331
+ background-position: right 8px center;
332
+ }
333
+ .context-select:focus {
334
+ outline: none;
335
+ border-color: var(--accent);
336
+ }
337
+ .context-group {
338
+ display: flex;
339
+ align-items: center;
340
+ gap: 6px;
341
+ }
342
+ .context-separator {
343
+ width: 1px;
344
+ height: 20px;
345
+ background: var(--border);
346
+ margin: 0 4px;
347
+ }
348
+
349
+ /* SIDEBAR */
350
+ .sidebar {
351
+ grid-area: sidebar;
352
+ background: var(--bg-surface);
353
+ border-right: 1px solid var(--border);
354
+ overflow-y: auto;
355
+ padding: 16px 0;
356
+ position: sticky;
357
+ top: 84px;
358
+ height: calc(100vh - 84px);
359
+ }
360
+ .sidebar::-webkit-scrollbar {
361
+ width: 4px;
362
+ }
363
+ .sidebar::-webkit-scrollbar-thumb {
364
+ background: var(--bg-overlay);
365
+ border-radius: 2px;
366
+ }
367
+
368
+ .sidebar-section {
369
+ padding: 0 16px;
370
+ margin-bottom: 20px;
371
+ }
372
+ .sidebar-title {
373
+ font-size: 11px;
374
+ font-weight: 600;
375
+ color: var(--text-tertiary);
376
+ text-transform: uppercase;
377
+ letter-spacing: 0.5px;
378
+ margin-bottom: 8px;
379
+ padding: 0 4px;
380
+ display: flex;
381
+ align-items: center;
382
+ justify-content: space-between;
383
+ }
384
+ .sidebar-count {
385
+ font-size: 10px;
386
+ font-family: var(--font-mono);
387
+ padding: 1px 6px;
388
+ background: var(--bg-base);
389
+ border-radius: 8px;
390
+ border: 1px solid var(--border);
391
+ }
392
+
393
+ /* Session cards in sidebar */
394
+ .ide-group {
395
+ margin-bottom: 12px;
396
+ }
397
+ .ide-group-header {
398
+ display: flex;
399
+ align-items: center;
400
+ gap: 6px;
401
+ padding: 4px;
402
+ border-radius: var(--radius-sm);
403
+ cursor: pointer;
404
+ transition: background var(--transition);
405
+ }
406
+ .ide-group-header:hover {
407
+ background: var(--bg-hover);
408
+ }
409
+ .ide-group-header .ide-dot {
410
+ width: 6px;
411
+ height: 6px;
412
+ border-radius: 50%;
413
+ flex-shrink: 0;
414
+ }
415
+ .ide-group-header .ide-name {
416
+ font-size: 12px;
417
+ font-weight: 600;
418
+ }
419
+ .ide-group-header .ide-count {
420
+ font-size: 10px;
421
+ color: var(--text-tertiary);
422
+ font-family: var(--font-mono);
423
+ margin-left: auto;
424
+ }
425
+
426
+ .session-item {
427
+ display: flex;
428
+ align-items: center;
429
+ gap: 8px;
430
+ padding: 6px 8px 6px 20px;
431
+ border-radius: var(--radius-sm);
432
+ cursor: pointer;
433
+ transition: all var(--transition);
434
+ font-size: 12px;
435
+ }
436
+ .session-item:hover {
437
+ background: var(--bg-hover);
438
+ }
439
+ .session-item.selected {
440
+ background: var(--accent-subtle);
441
+ border-left: 2px solid var(--accent);
442
+ }
443
+ .session-item .session-dot {
444
+ width: 6px;
445
+ height: 6px;
446
+ border-radius: 50%;
447
+ background: var(--green);
448
+ flex-shrink: 0;
449
+ }
450
+ .session-item .session-dot.stale {
451
+ background: var(--yellow);
452
+ }
453
+ .session-item .session-name {
454
+ color: var(--text-secondary);
455
+ overflow: hidden;
456
+ text-overflow: ellipsis;
457
+ white-space: nowrap;
458
+ flex: 1;
459
+ }
460
+ .session-item .session-files {
461
+ font-family: var(--font-mono);
462
+ font-size: 10px;
463
+ color: var(--text-tertiary);
464
+ }
465
+
466
+ .sidebar-summary {
467
+ padding: 12px 16px;
468
+ margin: 0 16px;
469
+ background: var(--bg-base);
470
+ border-radius: var(--radius-md);
471
+ font-size: 11px;
472
+ font-family: var(--font-mono);
473
+ color: var(--text-secondary);
474
+ display: grid;
475
+ grid-template-columns: 1fr 1fr;
476
+ gap: 4px;
477
+ }
478
+ .sidebar-summary-item {
479
+ display: flex;
480
+ align-items: center;
481
+ gap: 4px;
482
+ }
483
+ .sidebar-summary-item strong {
484
+ color: var(--text-primary);
485
+ }
486
+
487
+ /* MAIN CONTENT */
488
+ .main {
489
+ grid-area: main;
490
+ padding: 20px 24px;
491
+ overflow-y: auto;
492
+ }
493
+
494
+ /* Section Title */
495
+ .section-title {
496
+ font-size: 12px;
497
+ font-weight: 600;
498
+ color: var(--text-secondary);
499
+ text-transform: uppercase;
500
+ letter-spacing: 0.5px;
501
+ display: flex;
502
+ align-items: center;
503
+ gap: 8px;
504
+ margin-bottom: 12px;
505
+ }
506
+
507
+ /* KPI GRID — 5 cards */
508
+ .kpi-grid {
509
+ display: grid;
510
+ grid-template-columns: repeat(5, 1fr);
511
+ gap: 12px;
512
+ margin-bottom: 20px;
513
+ }
514
+ .kpi-card {
515
+ background: var(--bg-surface);
516
+ border: 1px solid var(--border);
517
+ border-radius: var(--radius-lg);
518
+ padding: 16px;
519
+ transition: border-color var(--transition);
520
+ }
521
+ .kpi-card:hover {
522
+ border-color: var(--border-hover);
523
+ }
524
+ .kpi-label {
525
+ font-size: 11px;
526
+ font-weight: 500;
527
+ color: var(--text-secondary);
528
+ text-transform: uppercase;
529
+ letter-spacing: 0.5px;
530
+ margin-bottom: 6px;
531
+ display: flex;
532
+ align-items: center;
533
+ gap: 5px;
534
+ }
535
+ .kpi-value {
536
+ font-size: 24px;
537
+ font-weight: 700;
538
+ letter-spacing: -1px;
539
+ font-variant-numeric: tabular-nums;
540
+ line-height: 1;
541
+ }
542
+ .kpi-sub {
543
+ font-size: 11px;
544
+ color: var(--text-tertiary);
545
+ margin-top: 4px;
546
+ font-family: var(--font-mono);
547
+ }
548
+
549
+ /* TECH STACK — compact */
550
+ .stack-row {
551
+ display: flex;
552
+ gap: 12px;
553
+ margin-bottom: 20px;
554
+ align-items: stretch;
555
+ }
556
+ .stack-panel {
557
+ background: var(--bg-surface);
558
+ border: 1px solid var(--border);
559
+ border-radius: var(--radius-lg);
560
+ padding: 16px;
561
+ flex: 1;
562
+ }
563
+ .stack-badges {
564
+ display: flex;
565
+ flex-wrap: wrap;
566
+ gap: 6px;
567
+ }
568
+ .skill-badge {
569
+ display: inline-flex;
570
+ align-items: center;
571
+ gap: 5px;
572
+ padding: 4px 10px;
573
+ border-radius: 16px;
574
+ font-size: 11px;
575
+ font-weight: 500;
576
+ border: 1px solid;
577
+ transition: all var(--transition);
578
+ }
579
+ .skill-badge:hover {
580
+ transform: translateY(-1px);
581
+ }
582
+ .skill-badge .skill-version {
583
+ font-family: var(--font-mono);
584
+ font-size: 10px;
585
+ opacity: 0.7;
586
+ }
587
+ .skill-badge .skill-dot {
588
+ width: 5px;
589
+ height: 5px;
590
+ border-radius: 50%;
591
+ }
592
+ .skill-badge[data-cat="language"] {
593
+ background: rgba(56, 139, 253, 0.08);
594
+ border-color: rgba(56, 139, 253, 0.2);
595
+ color: var(--cat-language);
596
+ }
597
+ .skill-badge[data-cat="language"] .skill-dot {
598
+ background: var(--cat-language);
599
+ }
600
+ .skill-badge[data-cat="framework"] {
601
+ background: rgba(188, 140, 255, 0.08);
602
+ border-color: rgba(188, 140, 255, 0.2);
603
+ color: var(--cat-framework);
604
+ }
605
+ .skill-badge[data-cat="framework"] .skill-dot {
606
+ background: var(--cat-framework);
607
+ }
608
+ .skill-badge[data-cat="library"] {
609
+ background: rgba(57, 210, 192, 0.08);
610
+ border-color: rgba(57, 210, 192, 0.2);
611
+ color: var(--cat-library);
612
+ }
613
+ .skill-badge[data-cat="library"] .skill-dot {
614
+ background: var(--cat-library);
615
+ }
616
+ .skill-badge[data-cat="build-tool"] {
617
+ background: rgba(210, 153, 34, 0.08);
618
+ border-color: rgba(210, 153, 34, 0.2);
619
+ color: var(--cat-build-tool);
620
+ }
621
+ .skill-badge[data-cat="build-tool"] .skill-dot {
622
+ background: var(--cat-build-tool);
623
+ }
624
+ .skill-badge[data-cat="test-framework"] {
625
+ background: rgba(63, 185, 80, 0.08);
626
+ border-color: rgba(63, 185, 80, 0.2);
627
+ color: var(--cat-test-framework);
628
+ }
629
+ .skill-badge[data-cat="test-framework"] .skill-dot {
630
+ background: var(--cat-test-framework);
631
+ }
632
+ .skill-badge[data-cat="runtime"] {
633
+ background: rgba(247, 120, 186, 0.08);
634
+ border-color: rgba(247, 120, 186, 0.2);
635
+ color: var(--cat-runtime);
636
+ }
637
+ .skill-badge[data-cat="runtime"] .skill-dot {
638
+ background: var(--cat-runtime);
639
+ }
640
+ .skill-badge[data-cat="database"] {
641
+ background: rgba(219, 109, 40, 0.08);
642
+ border-color: rgba(219, 109, 40, 0.2);
643
+ color: var(--cat-database);
644
+ }
645
+ .skill-badge[data-cat="database"] .skill-dot {
646
+ background: var(--cat-database);
647
+ }
648
+ .skill-badge[data-cat="devops"] {
649
+ background: rgba(139, 148, 158, 0.08);
650
+ border-color: rgba(139, 148, 158, 0.2);
651
+ color: var(--cat-devops);
652
+ }
653
+ .skill-badge[data-cat="devops"] .skill-dot {
654
+ background: var(--cat-devops);
655
+ }
656
+ .build-info {
657
+ margin-top: 8px;
658
+ padding-top: 8px;
659
+ border-top: 1px solid var(--border);
660
+ display: flex;
661
+ gap: 16px;
662
+ font-size: 11px;
663
+ color: var(--text-secondary);
664
+ flex-wrap: wrap;
665
+ }
666
+ .build-info code {
667
+ font-family: var(--font-mono);
668
+ background: var(--bg-base);
669
+ padding: 1px 5px;
670
+ border-radius: 4px;
671
+ font-size: 10px;
672
+ color: var(--text-primary);
673
+ }
674
+
675
+ /* CHARTS ROW */
676
+ .charts-row {
677
+ display: grid;
678
+ grid-template-columns: 1fr 1fr;
679
+ gap: 12px;
680
+ margin-bottom: 20px;
681
+ }
682
+ .chart-card {
683
+ background: var(--bg-surface);
684
+ border: 1px solid var(--border);
685
+ border-radius: var(--radius-lg);
686
+ padding: 16px;
687
+ }
688
+ .chart-canvas {
689
+ width: 100%;
690
+ height: 160px;
691
+ display: block;
692
+ }
693
+ .latency-bars {
694
+ display: flex;
695
+ flex-direction: column;
696
+ gap: 10px;
697
+ padding: 4px 0;
698
+ }
699
+ .latency-row {
700
+ display: flex;
701
+ align-items: center;
702
+ gap: 10px;
703
+ }
704
+ .latency-label {
705
+ font-size: 12px;
706
+ font-weight: 600;
707
+ font-family: var(--font-mono);
708
+ color: var(--text-secondary);
709
+ min-width: 28px;
710
+ }
711
+ .latency-bar-track {
712
+ flex: 1;
713
+ height: 20px;
714
+ background: var(--bg-base);
715
+ border-radius: 4px;
716
+ overflow: hidden;
717
+ }
718
+ .latency-bar-fill {
719
+ height: 100%;
720
+ border-radius: 4px;
721
+ transition: width 0.6s cubic-bezier(0.16, 1, 0.3, 1);
722
+ }
723
+ .latency-bar-fill.p50 {
724
+ background: linear-gradient(90deg, #388bfd, #58a6ff);
725
+ }
726
+ .latency-bar-fill.p95 {
727
+ background: linear-gradient(90deg, #d29922, #e3b341);
728
+ }
729
+ .latency-bar-fill.p99 {
730
+ background: linear-gradient(90deg, #f85149, #ff7b72);
731
+ }
732
+ .latency-value {
733
+ font-size: 12px;
734
+ font-weight: 600;
735
+ font-family: var(--font-mono);
736
+ min-width: 55px;
737
+ text-align: right;
738
+ }
739
+
740
+ /* BOTTOM GRID */
741
+ .bottom-grid {
742
+ display: grid;
743
+ grid-template-columns: 1fr 1fr;
744
+ gap: 12px;
745
+ margin-bottom: 20px;
746
+ }
747
+ .panel {
748
+ background: var(--bg-surface);
749
+ border: 1px solid var(--border);
750
+ border-radius: var(--radius-lg);
751
+ padding: 16px;
752
+ }
753
+ .tree-container {
754
+ max-height: 480px;
755
+ overflow-y: auto;
756
+ font-family: var(--font-mono);
757
+ font-size: 12px;
758
+ }
759
+ .tree-container::-webkit-scrollbar {
760
+ width: 4px;
761
+ }
762
+ .tree-container::-webkit-scrollbar-thumb {
763
+ background: var(--bg-overlay);
764
+ border-radius: 2px;
765
+ }
766
+ .tree-dir {
767
+ display: flex;
768
+ align-items: center;
769
+ gap: 6px;
770
+ padding: 3px 0;
771
+ color: var(--text-secondary);
772
+ }
773
+ .tree-dir-icon {
774
+ color: var(--accent);
775
+ }
776
+ .tree-dir-count {
777
+ font-size: 10px;
778
+ color: var(--text-tertiary);
779
+ background: var(--bg-base);
780
+ padding: 1px 5px;
781
+ border-radius: 3px;
782
+ }
783
+
784
+ .symbol-grid {
785
+ display: grid;
786
+ grid-template-columns: 1fr 1fr;
787
+ gap: 6px;
788
+ margin-top: 12px;
789
+ padding-top: 10px;
790
+ border-top: 1px solid var(--border);
791
+ }
792
+ .symbol-stat {
793
+ display: flex;
794
+ align-items: center;
795
+ justify-content: space-between;
796
+ padding: 4px 8px;
797
+ background: var(--bg-base);
798
+ border-radius: var(--radius-sm);
799
+ font-size: 11px;
800
+ }
801
+ .symbol-stat-label {
802
+ color: var(--text-secondary);
803
+ }
804
+ .symbol-stat-value {
805
+ font-weight: 600;
806
+ font-family: var(--font-mono);
807
+ }
808
+
809
+ /* EXPLORER — Interactive Project Explorer */
810
+ .explorer-header {
811
+ display: flex;
812
+ align-items: center;
813
+ gap: 8px;
814
+ margin-bottom: 10px;
815
+ }
816
+ .explorer-search {
817
+ flex: 1;
818
+ padding: 5px 10px;
819
+ background: var(--bg-base);
820
+ border: 1px solid var(--border);
821
+ border-radius: var(--radius-sm);
822
+ color: var(--text-primary);
823
+ font-size: 12px;
824
+ font-family: var(--font-mono);
825
+ outline: none;
826
+ transition: border-color var(--transition);
827
+ }
828
+ .explorer-search:focus {
829
+ border-color: var(--accent);
830
+ }
831
+ .explorer-search::placeholder {
832
+ color: var(--text-tertiary);
833
+ }
834
+ .explorer-summary {
835
+ display: flex;
836
+ gap: 12px;
837
+ padding: 6px 10px;
838
+ background: var(--bg-base);
839
+ border-radius: var(--radius-sm);
840
+ margin-bottom: 8px;
841
+ font-size: 11px;
842
+ font-family: var(--font-mono);
843
+ color: var(--text-secondary);
844
+ flex-wrap: wrap;
845
+ }
846
+ .explorer-summary .es-val {
847
+ color: var(--text-primary);
848
+ font-weight: 600;
849
+ }
850
+ .exp-dir {
851
+ cursor: pointer;
852
+ user-select: none;
853
+ display: flex;
854
+ align-items: center;
855
+ gap: 6px;
856
+ padding: 4px 6px;
857
+ border-radius: var(--radius-sm);
858
+ transition: background var(--transition);
859
+ }
860
+ .exp-dir:hover {
861
+ background: var(--bg-hover);
862
+ }
863
+ .exp-dir-arrow {
864
+ font-size: 9px;
865
+ color: var(--text-tertiary);
866
+ width: 12px;
867
+ text-align: center;
868
+ transition: transform 0.15s;
869
+ }
870
+ .exp-dir-arrow.open {
871
+ transform: rotate(90deg);
872
+ }
873
+ .exp-dir-name {
874
+ color: var(--accent);
875
+ font-weight: 500;
876
+ }
877
+ .exp-dir-stats {
878
+ margin-left: auto;
879
+ font-size: 10px;
880
+ color: var(--text-tertiary);
881
+ display: flex;
882
+ gap: 8px;
883
+ }
884
+ .exp-files {
885
+ padding-left: 24px;
886
+ overflow: hidden;
887
+ }
888
+ .exp-file {
889
+ display: flex;
890
+ align-items: center;
891
+ gap: 5px;
892
+ padding: 2px 6px;
893
+ border-radius: var(--radius-sm);
894
+ transition: background var(--transition);
895
+ cursor: pointer;
896
+ }
897
+ .exp-file:hover {
898
+ background: var(--bg-hover);
899
+ }
900
+ .exp-file-icon {
901
+ font-size: 10px;
902
+ color: var(--text-tertiary);
903
+ }
904
+ .exp-file-name {
905
+ color: var(--text-primary);
906
+ font-size: 12px;
907
+ }
908
+ .exp-file-stats {
909
+ margin-left: auto;
910
+ font-size: 10px;
911
+ color: var(--text-tertiary);
912
+ }
913
+ .exp-symbols {
914
+ padding-left: 40px;
915
+ overflow: hidden;
916
+ }
917
+ .exp-sym {
918
+ display: flex;
919
+ align-items: center;
920
+ gap: 6px;
921
+ padding: 1px 6px;
922
+ font-size: 11px;
923
+ }
924
+ .exp-sym-badge {
925
+ display: inline-block;
926
+ padding: 0 5px;
927
+ border-radius: 3px;
928
+ font-size: 9px;
929
+ font-weight: 600;
930
+ letter-spacing: 0.3px;
931
+ text-transform: uppercase;
932
+ }
933
+ .exp-sym-badge.fn {
934
+ background: rgba(56, 139, 253, 0.12);
935
+ color: #58a6ff;
936
+ }
937
+ .exp-sym-badge.class {
938
+ background: rgba(188, 140, 255, 0.12);
939
+ color: #bc8cff;
940
+ }
941
+ .exp-sym-badge.interface {
942
+ background: rgba(63, 185, 80, 0.12);
943
+ color: #3fb950;
944
+ }
945
+ .exp-sym-badge.type {
946
+ background: rgba(210, 153, 34, 0.12);
947
+ color: #d29922;
948
+ }
949
+ .exp-sym-badge.enum {
950
+ background: rgba(219, 109, 40, 0.12);
951
+ color: #db6d28;
952
+ }
953
+ .exp-sym-badge.variable {
954
+ background: rgba(139, 148, 158, 0.12);
955
+ color: #8b949e;
956
+ }
957
+ .exp-sym-badge.method {
958
+ background: rgba(57, 210, 192, 0.12);
959
+ color: #39d2c0;
960
+ }
961
+ .exp-sym-name {
962
+ color: var(--text-primary);
963
+ }
964
+ .exp-sym-line {
965
+ margin-left: auto;
966
+ font-size: 10px;
967
+ color: var(--text-tertiary);
968
+ font-family: var(--font-mono);
969
+ }
970
+ .exp-sym-export {
971
+ font-size: 9px;
972
+ color: var(--green);
973
+ margin-left: 2px;
974
+ }
975
+
976
+ /* Activity Feed */
977
+ .activity-feed {
978
+ max-height: 220px;
979
+ overflow-y: auto;
980
+ }
981
+ .activity-feed::-webkit-scrollbar {
982
+ width: 4px;
983
+ }
984
+ .activity-feed::-webkit-scrollbar-thumb {
985
+ background: var(--bg-overlay);
986
+ border-radius: 2px;
987
+ }
988
+ .activity-item {
989
+ display: flex;
990
+ align-items: center;
991
+ justify-content: space-between;
992
+ padding: 6px 8px;
993
+ border-bottom: 1px solid var(--border);
994
+ font-size: 12px;
995
+ transition: background var(--transition);
996
+ }
997
+ .activity-item:hover {
998
+ background: var(--bg-hover);
999
+ }
1000
+ .activity-item:last-child {
1001
+ border-bottom: none;
1002
+ }
1003
+ .activity-query {
1004
+ color: var(--text-primary);
1005
+ font-family: var(--font-mono);
1006
+ overflow: hidden;
1007
+ text-overflow: ellipsis;
1008
+ white-space: nowrap;
1009
+ flex: 1;
1010
+ }
1011
+ .activity-time {
1012
+ color: var(--text-tertiary);
1013
+ font-size: 11px;
1014
+ white-space: nowrap;
1015
+ margin-left: 8px;
1016
+ }
1017
+
1018
+ /* Telemetry grid */
1019
+ .telemetry-section {
1020
+ margin-top: 12px;
1021
+ padding-top: 10px;
1022
+ border-top: 1px solid var(--border);
1023
+ }
1024
+ .telemetry-grid {
1025
+ display: grid;
1026
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
1027
+ gap: 6px;
1028
+ }
1029
+ .counter-item {
1030
+ display: flex;
1031
+ align-items: center;
1032
+ justify-content: space-between;
1033
+ padding: 5px 8px;
1034
+ background: var(--bg-base);
1035
+ border-radius: var(--radius-sm);
1036
+ font-size: 11px;
1037
+ }
1038
+ .counter-name {
1039
+ color: var(--text-secondary);
1040
+ font-family: var(--font-mono);
1041
+ font-size: 10px;
1042
+ }
1043
+ .counter-value {
1044
+ font-weight: 600;
1045
+ font-family: var(--font-mono);
1046
+ color: var(--text-primary);
1047
+ }
1048
+
1049
+ /* IDE Badge colors */
1050
+ .ide-badge {
1051
+ display: inline-flex;
1052
+ align-items: center;
1053
+ gap: 3px;
1054
+ font-size: 10px;
1055
+ font-weight: 500;
1056
+ padding: 1px 6px;
1057
+ border-radius: 8px;
1058
+ border: 1px solid;
1059
+ }
1060
+ .ide-badge[data-ide="cursor"] {
1061
+ background: rgba(168, 85, 247, 0.12);
1062
+ border-color: rgba(168, 85, 247, 0.3);
1063
+ color: #a855f7;
1064
+ }
1065
+ .ide-badge[data-ide="vscode"] {
1066
+ background: rgba(56, 139, 253, 0.12);
1067
+ border-color: rgba(56, 139, 253, 0.3);
1068
+ color: #388bfd;
1069
+ }
1070
+ .ide-badge[data-ide="antigravity"] {
1071
+ background: rgba(63, 185, 80, 0.12);
1072
+ border-color: rgba(63, 185, 80, 0.3);
1073
+ color: #3fb950;
1074
+ }
1075
+ .ide-badge[data-ide="windsurf"] {
1076
+ background: rgba(210, 153, 34, 0.12);
1077
+ border-color: rgba(210, 153, 34, 0.3);
1078
+ color: #d29922;
1079
+ }
1080
+ .ide-badge[data-ide="claude-desktop"] {
1081
+ background: rgba(248, 81, 73, 0.12);
1082
+ border-color: rgba(248, 81, 73, 0.3);
1083
+ color: #f85149;
1084
+ }
1085
+ .ide-badge[data-ide="gemini-cli"] {
1086
+ background: rgba(56, 139, 253, 0.12);
1087
+ border-color: rgba(56, 139, 253, 0.3);
1088
+ color: #388bfd;
1089
+ }
1090
+ .ide-badge[data-ide="codex"] {
1091
+ background: rgba(63, 185, 80, 0.12);
1092
+ border-color: rgba(63, 185, 80, 0.3);
1093
+ color: #3fb950;
1094
+ }
1095
+ .ide-badge[data-ide="neovim"] {
1096
+ background: rgba(63, 185, 80, 0.12);
1097
+ border-color: rgba(63, 185, 80, 0.3);
1098
+ color: #3fb950;
1099
+ }
1100
+ .ide-badge[data-ide="zed"] {
1101
+ background: rgba(247, 120, 186, 0.12);
1102
+ border-color: rgba(247, 120, 186, 0.3);
1103
+ color: #f778ba;
1104
+ }
1105
+ .ide-badge[data-ide="unknown"] {
1106
+ background: rgba(139, 148, 158, 0.12);
1107
+ border-color: rgba(139, 148, 158, 0.3);
1108
+ color: #8b949e;
1109
+ }
1110
+
1111
+ /* IDE dot colors */
1112
+ [data-ide="cursor"] .ide-dot {
1113
+ background: #a855f7;
1114
+ }
1115
+ [data-ide="vscode"] .ide-dot {
1116
+ background: #388bfd;
1117
+ }
1118
+ [data-ide="antigravity"] .ide-dot {
1119
+ background: #3fb950;
1120
+ }
1121
+ [data-ide="windsurf"] .ide-dot {
1122
+ background: #d29922;
1123
+ }
1124
+ [data-ide="claude-desktop"] .ide-dot {
1125
+ background: #f85149;
1126
+ }
1127
+ [data-ide="gemini-cli"] .ide-dot {
1128
+ background: #388bfd;
1129
+ }
1130
+ [data-ide="codex"] .ide-dot {
1131
+ background: #3fb950;
1132
+ }
1133
+ [data-ide="neovim"] .ide-dot {
1134
+ background: #3fb950;
1135
+ }
1136
+ [data-ide="zed"] .ide-dot {
1137
+ background: #f778ba;
1138
+ }
1139
+ [data-ide="unknown"] .ide-dot {
1140
+ background: #8b949e;
1141
+ }
1142
+
1143
+ /* EMPTY & CONNECTING */
1144
+ .empty-state {
1145
+ display: flex;
1146
+ flex-direction: column;
1147
+ align-items: center;
1148
+ justify-content: center;
1149
+ padding: 32px 16px;
1150
+ color: var(--text-tertiary);
1151
+ font-size: 12px;
1152
+ text-align: center;
1153
+ gap: 6px;
1154
+ }
1155
+ .empty-state .empty-icon {
1156
+ font-size: 28px;
1157
+ opacity: 0.4;
1158
+ }
1159
+
1160
+ /* RESPONSIVE */
1161
+ @media (max-width: 1200px) {
1162
+ .kpi-grid {
1163
+ grid-template-columns: repeat(3, 1fr);
1164
+ }
1165
+ }
1166
+ @media (max-width: 1024px) {
1167
+ .app-layout {
1168
+ grid-template-columns: 1fr;
1169
+ grid-template-areas: "header" "context" "main";
1170
+ }
1171
+ .sidebar {
1172
+ display: none;
1173
+ }
1174
+ .kpi-grid {
1175
+ grid-template-columns: repeat(2, 1fr);
1176
+ }
1177
+ .charts-row {
1178
+ grid-template-columns: 1fr;
1179
+ }
1180
+ .bottom-grid {
1181
+ grid-template-columns: 1fr;
1182
+ }
1183
+ }
1184
+ @media (max-width: 640px) {
1185
+ .main {
1186
+ padding: 12px;
1187
+ }
1188
+ .kpi-grid {
1189
+ grid-template-columns: 1fr;
1190
+ }
1191
+ .header {
1192
+ flex-direction: column;
1193
+ gap: 8px;
1194
+ align-items: stretch;
1195
+ padding: 8px 12px;
1196
+ }
1197
+ .header-right {
1198
+ justify-content: center;
1199
+ flex-wrap: wrap;
1200
+ }
1201
+ .context-bar {
1202
+ padding: 6px 12px;
1203
+ }
1204
+ }
1205
+ </style>
1206
+ </head>
1207
+ <body>
1208
+ <div class="app-layout">
1209
+ <!-- HEADER -->
1210
+ <header class="header">
1211
+ <div class="header-left">
1212
+ <div class="logo">
1213
+ <span class="logo-dot" id="statusDot"></span>
1214
+ <span class="logo-text">HSA Dashboard</span>
1215
+ </div>
1216
+ <span class="logo-version" id="versionBadge">—</span>
1217
+ <nav class="header-nav">
1218
+ <a href="/dashboard" class="nav-link active">Dashboard</a>
1219
+ <a href="/logs" class="nav-link">Logs</a>
1220
+ </nav>
1221
+ </div>
1222
+ <div class="header-right">
1223
+ <input
1224
+ type="text"
1225
+ class="endpoint-input"
1226
+ id="endpointInput"
1227
+ value="http://localhost:13100"
1228
+ placeholder="HSA endpoint"
1229
+ />
1230
+ <button class="btn" id="connectBtn" onclick="connect()">
1231
+ Connect
1232
+ </button>
1233
+ <button class="btn" id="refreshToggle" onclick="toggleAutoRefresh()">
1234
+ <span>⟳</span>
1235
+ <span
1236
+ style="font-size: 11px; font-family: var(--font-mono)"
1237
+ id="refreshBadge"
1238
+ >5s</span
1239
+ >
1240
+ </button>
1241
+ </div>
1242
+ </header>
1243
+
1244
+ <!-- CONTEXT SELECTOR BAR -->
1245
+ <div class="context-bar">
1246
+ <div class="context-group">
1247
+ <label>Project</label>
1248
+ <select
1249
+ class="context-select"
1250
+ id="ctxProject"
1251
+ onchange="applyContext()"
1252
+ >
1253
+ <option value="">All Projects</option>
1254
+ </select>
1255
+ </div>
1256
+ <div class="context-separator"></div>
1257
+ <div class="context-group">
1258
+ <label>IDE</label>
1259
+ <select class="context-select" id="ctxIde" onchange="applyContext()">
1260
+ <option value="">All IDEs</option>
1261
+ </select>
1262
+ </div>
1263
+ <div class="context-separator"></div>
1264
+ <div class="context-group">
1265
+ <label>Session</label>
1266
+ <select
1267
+ class="context-select"
1268
+ id="ctxSession"
1269
+ onchange="applyContext()"
1270
+ >
1271
+ <option value="">All Sessions</option>
1272
+ </select>
1273
+ </div>
1274
+ </div>
1275
+
1276
+ <!-- SIDEBAR — Sessions grouped by IDE -->
1277
+ <aside class="sidebar">
1278
+ <div class="sidebar-section">
1279
+ <div class="sidebar-title">
1280
+ <span>Sessions</span>
1281
+ <span class="sidebar-count" id="sidebarSessionCount">0</span>
1282
+ </div>
1283
+ <div id="sidebarSessions">
1284
+ <div class="empty-state" style="padding: 16px">
1285
+ <span class="empty-icon">🖥️</span>No sessions
1286
+ </div>
1287
+ </div>
1288
+ </div>
1289
+
1290
+ <div
1291
+ class="sidebar-summary"
1292
+ id="sidebarSummary"
1293
+ style="display: none"
1294
+ ></div>
1295
+ </aside>
1296
+
1297
+ <!-- MAIN CONTENT -->
1298
+ <main class="main">
1299
+ <!-- KPIs -->
1300
+ <div class="kpi-grid">
1301
+ <div class="kpi-card">
1302
+ <div class="kpi-label">📁 Files</div>
1303
+ <div class="kpi-value" id="kpiFiles">—</div>
1304
+ <div class="kpi-sub" id="kpiFilesDetail"></div>
1305
+ </div>
1306
+ <div class="kpi-card">
1307
+ <div class="kpi-label">📊 BM25 Index</div>
1308
+ <div class="kpi-value" id="kpiBm25">—</div>
1309
+ <div class="kpi-sub" id="kpiBm25Detail"></div>
1310
+ </div>
1311
+ <div class="kpi-card">
1312
+ <div class="kpi-label">🎯 Cache</div>
1313
+ <div class="kpi-value" id="kpiCacheHit" style="color: var(--green)">
1314
+
1315
+ </div>
1316
+ <div class="kpi-sub" id="kpiCacheDetail"></div>
1317
+ </div>
1318
+ <div class="kpi-card">
1319
+ <div class="kpi-label">⚡ Tool Calls</div>
1320
+ <div class="kpi-value" id="kpiToolCalls">—</div>
1321
+ <div class="kpi-sub" id="kpiToolCallsDetail"></div>
1322
+ </div>
1323
+ <div class="kpi-card">
1324
+ <div class="kpi-label">⏱️ Uptime</div>
1325
+ <div class="kpi-value" id="kpiUptime">—</div>
1326
+ <div class="kpi-sub" id="kpiUptimeDetail"></div>
1327
+ </div>
1328
+ </div>
1329
+
1330
+ <!-- Tech Stack (compact) -->
1331
+ <div class="stack-row">
1332
+ <div class="stack-panel">
1333
+ <div class="section-title">
1334
+ <span>🔧 Tech Stack</span>
1335
+ <span
1336
+ id="stackConfidence"
1337
+ style="
1338
+ font-size: 10px;
1339
+ color: var(--text-tertiary);
1340
+ font-family: var(--font-mono);
1341
+ margin-left: auto;
1342
+ "
1343
+ ></span>
1344
+ </div>
1345
+ <div class="stack-badges" id="stackBadges">
1346
+ <div class="empty-state" style="padding: 12px">
1347
+ <span style="color: var(--text-tertiary)">Connecting...</span>
1348
+ </div>
1349
+ </div>
1350
+ <div class="build-info" id="buildInfo" style="display: none"></div>
1351
+ </div>
1352
+ </div>
1353
+
1354
+ <!-- Project Explorer + Activity (moved above charts) -->
1355
+ <div class="bottom-grid">
1356
+ <div class="panel">
1357
+ <div class="explorer-header">
1358
+ <div class="section-title" style="margin-bottom: 0">
1359
+ 📂 Project Explorer
1360
+ </div>
1361
+ <input
1362
+ type="text"
1363
+ class="explorer-search"
1364
+ id="explorerSearch"
1365
+ placeholder="Filter files or symbols..."
1366
+ oninput="filterExplorer()"
1367
+ />
1368
+ </div>
1369
+ <div
1370
+ class="explorer-summary"
1371
+ id="explorerSummary"
1372
+ style="display: none"
1373
+ ></div>
1374
+ <div class="tree-container" id="fileTree">
1375
+ <div class="empty-state">
1376
+ <span class="empty-icon">📂</span>No data
1377
+ </div>
1378
+ </div>
1379
+ <div
1380
+ class="symbol-grid"
1381
+ id="symbolGrid"
1382
+ style="display: none"
1383
+ ></div>
1384
+ </div>
1385
+ <div class="panel">
1386
+ <div class="section-title">🔍 Activity Feed</div>
1387
+ <div class="activity-feed" id="activityFeed">
1388
+ <div class="empty-state" style="padding: 24px">
1389
+ <span class="empty-icon">🔍</span>No queries yet
1390
+ </div>
1391
+ </div>
1392
+ <div class="telemetry-section">
1393
+ <div class="section-title" style="margin-bottom: 6px">
1394
+ 📊 Telemetry
1395
+ </div>
1396
+ <div class="telemetry-grid" id="telemetryGrid">
1397
+ <div class="empty-state" style="padding: 8px">
1398
+ <span style="color: var(--text-tertiary); font-size: 10px"
1399
+ >No telemetry</span
1400
+ >
1401
+ </div>
1402
+ </div>
1403
+ </div>
1404
+ </div>
1405
+ </div>
1406
+
1407
+ <!-- Charts -->
1408
+ <div class="charts-row">
1409
+ <div class="chart-card">
1410
+ <div class="section-title">📈 Cache Performance</div>
1411
+ <canvas class="chart-canvas" id="cacheChart"></canvas>
1412
+ </div>
1413
+ <div class="chart-card">
1414
+ <div class="section-title">⚡ Search Latency</div>
1415
+ <div class="latency-bars" id="latencyBars">
1416
+ <div class="latency-row">
1417
+ <span class="latency-label">p50</span>
1418
+ <div class="latency-bar-track">
1419
+ <div
1420
+ class="latency-bar-fill p50"
1421
+ id="barP50"
1422
+ style="width: 0"
1423
+ ></div>
1424
+ </div>
1425
+ <span class="latency-value" id="valP50">—</span>
1426
+ </div>
1427
+ <div class="latency-row">
1428
+ <span class="latency-label">p95</span>
1429
+ <div class="latency-bar-track">
1430
+ <div
1431
+ class="latency-bar-fill p95"
1432
+ id="barP95"
1433
+ style="width: 0"
1434
+ ></div>
1435
+ </div>
1436
+ <span class="latency-value" id="valP95">—</span>
1437
+ </div>
1438
+ <div class="latency-row">
1439
+ <span class="latency-label">p99</span>
1440
+ <div class="latency-bar-track">
1441
+ <div
1442
+ class="latency-bar-fill p99"
1443
+ id="barP99"
1444
+ style="width: 0"
1445
+ ></div>
1446
+ </div>
1447
+ <span class="latency-value" id="valP99">—</span>
1448
+ </div>
1449
+ <div
1450
+ style="
1451
+ display: flex;
1452
+ justify-content: space-between;
1453
+ margin-top: 2px;
1454
+ font-size: 10px;
1455
+ color: var(--text-tertiary);
1456
+ font-family: var(--font-mono);
1457
+ "
1458
+ >
1459
+ <span id="latencyCount">— queries</span>
1460
+ <span id="latencyAvg">avg: —</span>
1461
+ </div>
1462
+ </div>
1463
+ </div>
1464
+ </div>
1465
+
1466
+ <!-- PageRank Rankings -->
1467
+ <div class="pagerank-panel">
1468
+ <div class="section-title">🏆 PageRank — Top Files</div>
1469
+ <div class="pagerank-meta" id="pagerankMeta"></div>
1470
+ <div class="pagerank-list" id="pagerankList">
1471
+ <div class="empty-state" style="padding: 16px">
1472
+ <span style="color: var(--text-tertiary)">No CodeGraph data</span>
1473
+ </div>
1474
+ </div>
1475
+ </div>
1476
+ </main>
1477
+ </div>
1478
+
1479
+ <script>
1480
+ /* ================================================================
1481
+ STATE
1482
+ ================================================================ */
1483
+ let endpoint =
1484
+ localStorage.getItem("hsa_endpoint") || "http://localhost:13100";
1485
+ let autoRefresh = true;
1486
+ let refreshInterval = null;
1487
+ let cacheHistory = [];
1488
+ const MAX_HISTORY = 40;
1489
+ let allSessions = [];
1490
+ let selectedSessionId = "";
1491
+ let selectedIde = "";
1492
+ let selectedProject = "";
1493
+
1494
+ document.getElementById("endpointInput").value = endpoint;
1495
+
1496
+ /* ================================================================
1497
+ CONNECTION
1498
+ ================================================================ */
1499
+ function connect() {
1500
+ endpoint = document
1501
+ .getElementById("endpointInput")
1502
+ .value.replace(/\/$/, "");
1503
+ localStorage.setItem("hsa_endpoint", endpoint);
1504
+ setStatus("connecting");
1505
+ fetchDashboard();
1506
+ fetchSessions();
1507
+ }
1508
+
1509
+ function setStatus(status) {
1510
+ const dot = document.getElementById("statusDot");
1511
+ dot.className = "logo-dot " + (status === "connected" ? "" : status);
1512
+ }
1513
+
1514
+ function toggleAutoRefresh() {
1515
+ autoRefresh = !autoRefresh;
1516
+ const btn = document.getElementById("refreshToggle");
1517
+ const badge = document.getElementById("refreshBadge");
1518
+ if (autoRefresh) {
1519
+ btn.classList.add("active");
1520
+ badge.textContent = "5s";
1521
+ startRefresh();
1522
+ } else {
1523
+ btn.classList.remove("active");
1524
+ badge.textContent = "off";
1525
+ stopRefresh();
1526
+ }
1527
+ }
1528
+ function startRefresh() {
1529
+ stopRefresh();
1530
+ refreshInterval = setInterval(() => {
1531
+ fetchDashboard();
1532
+ fetchSessions();
1533
+ }, 5000);
1534
+ }
1535
+ function stopRefresh() {
1536
+ if (refreshInterval) {
1537
+ clearInterval(refreshInterval);
1538
+ refreshInterval = null;
1539
+ }
1540
+ }
1541
+
1542
+ /* ================================================================
1543
+ CONTEXT SELECTORS
1544
+ ================================================================ */
1545
+ function applyContext() {
1546
+ selectedProject = document.getElementById("ctxProject").value;
1547
+ selectedIde = document.getElementById("ctxIde").value;
1548
+ selectedSessionId = document.getElementById("ctxSession").value;
1549
+ fetchDashboard();
1550
+ updateSidebarHighlight();
1551
+ }
1552
+
1553
+ function populateContextSelectors(sessions) {
1554
+ const ideSet = new Set();
1555
+ const projectSet = new Set();
1556
+ sessions.forEach((s) => {
1557
+ ideSet.add(s.ideName);
1558
+ projectSet.add(s.projectName);
1559
+ });
1560
+
1561
+ const ideSelect = document.getElementById("ctxIde");
1562
+ const projSelect = document.getElementById("ctxProject");
1563
+ const sessSelect = document.getElementById("ctxSession");
1564
+
1565
+ // Preserve selections
1566
+ const curIde = ideSelect.value;
1567
+ const curProj = projSelect.value;
1568
+ const curSess = sessSelect.value;
1569
+
1570
+ ideSelect.innerHTML =
1571
+ '<option value="">All IDEs</option>' +
1572
+ [...ideSet]
1573
+ .map((i) => `<option value="${esc(i)}">${esc(i)}</option>`)
1574
+ .join("");
1575
+ projSelect.innerHTML =
1576
+ '<option value="">All Projects</option>' +
1577
+ [...projectSet]
1578
+ .map((p) => `<option value="${esc(p)}">${esc(p)}</option>`)
1579
+ .join("");
1580
+ sessSelect.innerHTML =
1581
+ '<option value="">All Sessions</option>' +
1582
+ sessions
1583
+ .map(
1584
+ (s) =>
1585
+ `<option value="${esc(s.sessionId)}">${esc(s.projectName)} (${esc(s.ideName)})</option>`,
1586
+ )
1587
+ .join("");
1588
+
1589
+ ideSelect.value = curIde;
1590
+ projSelect.value = curProj;
1591
+ sessSelect.value = curSess;
1592
+ }
1593
+
1594
+ /* ================================================================
1595
+ FETCH & RENDER
1596
+ ================================================================ */
1597
+ async function fetchDashboard() {
1598
+ try {
1599
+ let url = endpoint + "/api/dashboard";
1600
+ // Note: /api/dashboard doesn't support session filtering yet, handled client-side
1601
+ const res = await fetch(url, { signal: AbortSignal.timeout(8000) });
1602
+ if (!res.ok) throw new Error("HTTP " + res.status);
1603
+ const data = await res.json();
1604
+ setStatus("connected");
1605
+ render(data);
1606
+ } catch (err) {
1607
+ setStatus("error");
1608
+ console.warn("HSA fetch error:", err.message);
1609
+ }
1610
+ }
1611
+
1612
+ function render(d) {
1613
+ document.getElementById("versionBadge").textContent =
1614
+ "v" + (d.meta?.version || "?");
1615
+
1616
+ // KPI — Files
1617
+ document.getElementById("kpiFiles").textContent = fmt(
1618
+ d.project?.trackedFiles,
1619
+ );
1620
+ document.getElementById("kpiFilesDetail").textContent =
1621
+ d.project?.root || "";
1622
+
1623
+ // KPI — BM25
1624
+ document.getElementById("kpiBm25").textContent = fmt(
1625
+ d.indexes?.bm25?.documents,
1626
+ );
1627
+ document.getElementById("kpiBm25Detail").textContent =
1628
+ fmt(d.indexes?.bm25?.terms) +
1629
+ " terms · avg " +
1630
+ (d.indexes?.bm25?.avgDocLength || 0).toFixed(0) +
1631
+ " len";
1632
+
1633
+ // KPI — Cache
1634
+ const hitRate = (d.cache?.hitRate ?? 0) * 100;
1635
+ const kpiCacheEl = document.getElementById("kpiCacheHit");
1636
+ kpiCacheEl.textContent = hitRate.toFixed(1) + "%";
1637
+ kpiCacheEl.style.color =
1638
+ hitRate > 80
1639
+ ? "var(--green)"
1640
+ : hitRate > 50
1641
+ ? "var(--yellow)"
1642
+ : "var(--red)";
1643
+ document.getElementById("kpiCacheDetail").textContent =
1644
+ fmt(d.cache?.hits) + " hits · " + fmt(d.cache?.misses) + " misses";
1645
+
1646
+ // KPI — Tool Calls (from telemetry counters)
1647
+ const toolCallCount = d.telemetry?.counters
1648
+ ? Object.entries(d.telemetry.counters)
1649
+ .filter(([k]) => k.endsWith(".calls") || k === "context.queries")
1650
+ .reduce((sum, [, v]) => sum + v, 0)
1651
+ : 0;
1652
+ const uptimeMin = (d.meta?.uptimeMs || 1) / 60000;
1653
+ document.getElementById("kpiToolCalls").textContent =
1654
+ fmt(toolCallCount);
1655
+ document.getElementById("kpiToolCallsDetail").textContent =
1656
+ (toolCallCount / uptimeMin).toFixed(1) +
1657
+ "/min · " +
1658
+ fmt(d.telemetry?.counters?.["context.cache_miss"] ?? 0) +
1659
+ " misses";
1660
+
1661
+ // KPI — Uptime
1662
+ document.getElementById("kpiUptime").textContent = fmtUptime(
1663
+ d.meta?.uptimeMs || 0,
1664
+ );
1665
+ document.getElementById("kpiUptimeDetail").textContent = d.meta
1666
+ ?.timestamp
1667
+ ? new Date(d.meta.timestamp).toLocaleTimeString()
1668
+ : "";
1669
+
1670
+ // Stack
1671
+ renderStack(d.stack);
1672
+
1673
+ // Cache chart
1674
+ cacheHistory.push({
1675
+ hits: d.cache?.hits || 0,
1676
+ misses: d.cache?.misses || 0,
1677
+ t: Date.now(),
1678
+ });
1679
+ if (cacheHistory.length > MAX_HISTORY) cacheHistory.shift();
1680
+ drawCacheChart();
1681
+
1682
+ // Latency
1683
+ renderLatency(d.telemetry);
1684
+
1685
+ // Project Explorer
1686
+ renderExplorer(d.explorer, d.fileTree, d.indexes?.codeGraph);
1687
+
1688
+ // Activity Feed
1689
+ renderActivity(d.session);
1690
+
1691
+ // Telemetry
1692
+ renderTelemetry(d.telemetry);
1693
+
1694
+ // PageRank
1695
+ renderPageRank(d.pageRank);
1696
+ }
1697
+
1698
+ /* ================================================================
1699
+ RENDER HELPERS
1700
+ ================================================================ */
1701
+ function renderStack(stack) {
1702
+ const container = document.getElementById("stackBadges");
1703
+ const buildEl = document.getElementById("buildInfo");
1704
+
1705
+ if (!stack || !stack.skills || stack.skills.length === 0) {
1706
+ container.innerHTML =
1707
+ '<div class="empty-state" style="padding:12px"><span style="color:var(--text-tertiary)">No stack detected</span></div>';
1708
+ buildEl.style.display = "none";
1709
+ return;
1710
+ }
1711
+
1712
+ document.getElementById("stackConfidence").textContent =
1713
+ (stack.confidence * 100).toFixed(0) + "% confidence";
1714
+ container.innerHTML = stack.skills
1715
+ .map(
1716
+ (s) =>
1717
+ `<span class="skill-badge" data-cat="${s.category}"><span class="skill-dot"></span><span>${s.name}</span>${s.version ? `<span class="skill-version" title="${s.versionSource || ""}">${s.version}</span>` : ""}</span>`,
1718
+ )
1719
+ .join("");
1720
+
1721
+ if (stack.buildSystem) {
1722
+ buildEl.style.display = "flex";
1723
+ buildEl.innerHTML = [
1724
+ stack.buildSystem.buildCommand
1725
+ ? `<span>Build: <code>${stack.buildSystem.buildCommand}</code></span>`
1726
+ : "",
1727
+ stack.buildSystem.testCommand
1728
+ ? `<span>Test: <code>${stack.buildSystem.testCommand}</code></span>`
1729
+ : "",
1730
+ stack.buildSystem.devCommand
1731
+ ? `<span>Dev: <code>${stack.buildSystem.devCommand}</code></span>`
1732
+ : "",
1733
+ ]
1734
+ .filter(Boolean)
1735
+ .join("");
1736
+ } else {
1737
+ buildEl.style.display = "none";
1738
+ }
1739
+ }
1740
+
1741
+ function renderLatency(telemetry) {
1742
+ if (!telemetry?.histograms) return;
1743
+ const hist =
1744
+ telemetry.histograms["context.duration_ms"] ||
1745
+ telemetry.histograms["context.duration"] ||
1746
+ telemetry.histograms["search.duration"];
1747
+ if (!hist) return;
1748
+ const maxVal = Math.max(hist.p99 || 1, 1);
1749
+ setBar("barP50", "valP50", hist.p50, maxVal);
1750
+ setBar("barP95", "valP95", hist.p95, maxVal);
1751
+ setBar("barP99", "valP99", hist.p99, maxVal);
1752
+ document.getElementById("latencyCount").textContent =
1753
+ hist.count + " queries";
1754
+ document.getElementById("latencyAvg").textContent =
1755
+ "avg: " + hist.avg.toFixed(1) + "ms";
1756
+ }
1757
+ function setBar(barId, valId, value, max) {
1758
+ const pct = Math.min((value / max) * 100, 100);
1759
+ document.getElementById(barId).style.width = pct + "%";
1760
+ document.getElementById(valId).textContent =
1761
+ value < 1 ? value.toFixed(2) + "ms" : value.toFixed(1) + "ms";
1762
+ }
1763
+
1764
+ // ── Explorer state ──
1765
+ let explorerCache = null;
1766
+ const expandedDirs = new Set();
1767
+ const expandedFiles = new Set();
1768
+
1769
+ function renderExplorer(explorer, tree, cg) {
1770
+ const container = document.getElementById("fileTree");
1771
+ const symbolGrid = document.getElementById("symbolGrid");
1772
+ const summaryEl = document.getElementById("explorerSummary");
1773
+
1774
+ // Fallback to basic file tree if no explorer data
1775
+ if (!explorer || !explorer.files || explorer.files.length === 0) {
1776
+ summaryEl.style.display = "none";
1777
+ if (!tree || tree.length === 0) {
1778
+ container.innerHTML =
1779
+ '<div class="empty-state"><span class="empty-icon">📂</span>No files tracked</div>';
1780
+ symbolGrid.style.display = "none";
1781
+ return;
1782
+ }
1783
+ container.innerHTML = tree
1784
+ .map(
1785
+ (entry) =>
1786
+ `<div class="tree-dir"><span class="tree-dir-icon">📁</span><span>${esc(entry.dir)}/</span><span class="tree-dir-count">${entry.files}</span></div>`,
1787
+ )
1788
+ .join("");
1789
+ if (cg && cg.byKind) {
1790
+ symbolGrid.style.display = "grid";
1791
+ symbolGrid.innerHTML = Object.entries(cg.byKind)
1792
+ .map(
1793
+ ([kind, count]) =>
1794
+ `<div class="symbol-stat"><span class="symbol-stat-label">${kind}</span><span class="symbol-stat-value">${count}</span></div>`,
1795
+ )
1796
+ .join("");
1797
+ } else {
1798
+ symbolGrid.style.display = "none";
1799
+ }
1800
+ return;
1801
+ }
1802
+
1803
+ // Save cache for filter
1804
+ explorerCache = explorer;
1805
+
1806
+ // Summary bar
1807
+ const s = explorer.summary;
1808
+ summaryEl.style.display = "flex";
1809
+ const kindStr = Object.entries(s.byKind || {})
1810
+ .map(([k, v]) => `<span class="es-val">${v}</span> ${k}`)
1811
+ .join(" · ");
1812
+ summaryEl.innerHTML = `<span><span class="es-val">${s.totalFiles}</span> files</span><span><span class="es-val">${s.totalSymbols}</span> symbols</span><span>${kindStr}</span>`;
1813
+
1814
+ // Symbol grid summary
1815
+ if (s.byKind && Object.keys(s.byKind).length > 0) {
1816
+ symbolGrid.style.display = "grid";
1817
+ symbolGrid.innerHTML =
1818
+ Object.entries(s.byKind)
1819
+ .map(
1820
+ ([kind, count]) =>
1821
+ `<div class="symbol-stat"><span class="symbol-stat-label">${kind}</span><span class="symbol-stat-value">${count}</span></div>`,
1822
+ )
1823
+ .join("") +
1824
+ `<div class="symbol-stat" style="grid-column:span 2;background:transparent;justify-content:center;color:var(--text-tertiary)">Total: ${s.totalSymbols} · ${s.totalFiles} files</div>`;
1825
+ } else {
1826
+ symbolGrid.style.display = "none";
1827
+ }
1828
+
1829
+ // Build tree
1830
+ buildExplorerTree(explorer, "");
1831
+ }
1832
+
1833
+ function buildExplorerTree(explorer, filter) {
1834
+ const container = document.getElementById("fileTree");
1835
+ const lc = filter.toLowerCase();
1836
+
1837
+ // Group files by directory
1838
+ const dirMap = new Map();
1839
+ for (const f of explorer.files) {
1840
+ // Apply filter
1841
+ if (lc) {
1842
+ const matchFile = f.path.toLowerCase().includes(lc);
1843
+ const matchSym = f.symbols.some((s) =>
1844
+ s.name.toLowerCase().includes(lc),
1845
+ );
1846
+ if (!matchFile && !matchSym) continue;
1847
+ }
1848
+ const dir = f.dir || ".";
1849
+ if (!dirMap.has(dir)) dirMap.set(dir, []);
1850
+ dirMap.get(dir).push(f);
1851
+ }
1852
+
1853
+ if (dirMap.size === 0) {
1854
+ container.innerHTML =
1855
+ '<div class="empty-state" style="padding:12px"><span style="color:var(--text-tertiary)">No matches found</span></div>';
1856
+ return;
1857
+ }
1858
+
1859
+ // Sort dirs
1860
+ const sortedDirs = [...dirMap.entries()].sort(([a], [b]) =>
1861
+ a.localeCompare(b),
1862
+ );
1863
+
1864
+ let html = "";
1865
+ for (const [dir, files] of sortedDirs) {
1866
+ const dirOpen = expandedDirs.has(dir) || !!lc;
1867
+ const totalSyms = files.reduce((s, f) => s + f.symbols.length, 0);
1868
+ html += `<div class="exp-dir" onclick="toggleDir('${esc(dir)}')">`;
1869
+ html += `<span class="exp-dir-arrow ${dirOpen ? "open" : ""}">▶</span>`;
1870
+ html += `<span class="exp-dir-name">📁 ${esc(dir)}/</span>`;
1871
+ html += `<span class="exp-dir-stats"><span>${files.length} files</span><span>${totalSyms} syms</span></span>`;
1872
+ html += `</div>`;
1873
+
1874
+ if (dirOpen) {
1875
+ html += `<div class="exp-files">`;
1876
+ for (const f of files.sort((a, b) =>
1877
+ a.path.localeCompare(b.path),
1878
+ )) {
1879
+ const fname = f.path.split("/").pop() || f.path;
1880
+ const fileKey = f.path;
1881
+ const fileOpen = expandedFiles.has(fileKey) || !!lc;
1882
+ const symCount = f.symbols.length;
1883
+ const fileIcon = getFileIcon(fname);
1884
+
1885
+ html += `<div class="exp-file" onclick="event.stopPropagation();toggleFile('${esc(fileKey)}')">`;
1886
+ html += `<span class="exp-file-icon">${fileOpen ? "▾" : "▸"}</span>`;
1887
+ html += `<span class="exp-file-icon">${fileIcon}</span>`;
1888
+ html += `<span class="exp-file-name">${esc(fname)}</span>`;
1889
+ html += `<span class="exp-file-stats">${symCount} syms</span>`;
1890
+ html += `</div>`;
1891
+
1892
+ if (fileOpen && f.symbols.length > 0) {
1893
+ html += `<div class="exp-symbols">`;
1894
+ // Sort: classes first, then functions, then others
1895
+ const sortOrder = {
1896
+ class: 0,
1897
+ interface: 1,
1898
+ enum: 2,
1899
+ type: 3,
1900
+ function: 4,
1901
+ method: 5,
1902
+ variable: 6,
1903
+ };
1904
+ const sorted = [...f.symbols].sort(
1905
+ (a, b) =>
1906
+ (sortOrder[a.kind] ?? 9) - (sortOrder[b.kind] ?? 9) ||
1907
+ a.name.localeCompare(b.name),
1908
+ );
1909
+ for (const sym of sorted) {
1910
+ const badgeClass = sym.kind === "function" ? "fn" : sym.kind;
1911
+ const matchHL = lc && sym.name.toLowerCase().includes(lc);
1912
+ html += `<div class="exp-sym" ${matchHL ? 'style="background:rgba(56,139,253,0.06)"' : ""}>`;
1913
+ html += `<span class="exp-sym-badge ${badgeClass}">${shortKind(sym.kind)}</span>`;
1914
+ html += `<span class="exp-sym-name">${esc(sym.name)}</span>`;
1915
+ if (sym.exported)
1916
+ html += `<span class="exp-sym-export">✓</span>`;
1917
+ html += `<span class="exp-sym-line">L${sym.line}</span>`;
1918
+ html += `</div>`;
1919
+ }
1920
+ html += `</div>`;
1921
+ }
1922
+ }
1923
+ html += `</div>`;
1924
+ }
1925
+ }
1926
+ container.innerHTML = html;
1927
+ }
1928
+
1929
+ function toggleDir(dir) {
1930
+ if (expandedDirs.has(dir)) expandedDirs.delete(dir);
1931
+ else expandedDirs.add(dir);
1932
+ if (explorerCache)
1933
+ buildExplorerTree(
1934
+ explorerCache,
1935
+ document.getElementById("explorerSearch").value,
1936
+ );
1937
+ }
1938
+
1939
+ function toggleFile(file) {
1940
+ if (expandedFiles.has(file)) expandedFiles.delete(file);
1941
+ else expandedFiles.add(file);
1942
+ if (explorerCache)
1943
+ buildExplorerTree(
1944
+ explorerCache,
1945
+ document.getElementById("explorerSearch").value,
1946
+ );
1947
+ }
1948
+
1949
+ function filterExplorer() {
1950
+ if (explorerCache)
1951
+ buildExplorerTree(
1952
+ explorerCache,
1953
+ document.getElementById("explorerSearch").value,
1954
+ );
1955
+ }
1956
+
1957
+ function shortKind(kind) {
1958
+ const map = {
1959
+ function: "fn",
1960
+ class: "cls",
1961
+ interface: "ifc",
1962
+ type: "type",
1963
+ enum: "enum",
1964
+ variable: "var",
1965
+ method: "mth",
1966
+ };
1967
+ return map[kind] || kind;
1968
+ }
1969
+
1970
+ function getFileIcon(name) {
1971
+ const ext = name.split(".").pop()?.toLowerCase();
1972
+ const icons = {
1973
+ ts: "🔷",
1974
+ js: "🟡",
1975
+ tsx: "⚛️",
1976
+ jsx: "⚛️",
1977
+ py: "🐍",
1978
+ go: "🔵",
1979
+ rs: "🦀",
1980
+ json: "📋",
1981
+ md: "📝",
1982
+ css: "🎨",
1983
+ html: "🌐",
1984
+ yaml: "⚙️",
1985
+ yml: "⚙️",
1986
+ };
1987
+ return icons[ext] || "📄";
1988
+ }
1989
+
1990
+ function renderActivity(session) {
1991
+ const feed = document.getElementById("activityFeed");
1992
+ if (!session?.recentQueries || session.recentQueries.length === 0) {
1993
+ feed.innerHTML =
1994
+ '<div class="empty-state" style="padding:24px"><span class="empty-icon">🔍</span>No queries yet</div>';
1995
+ return;
1996
+ }
1997
+ feed.innerHTML = session.recentQueries
1998
+ .map((q) => {
1999
+ const text = typeof q === "string" ? q : q.query || q;
2000
+ return `<div class="activity-item"><span class="activity-query">"${esc(text)}"</span></div>`;
2001
+ })
2002
+ .join("");
2003
+ }
2004
+
2005
+ function renderTelemetry(telemetry) {
2006
+ const grid = document.getElementById("telemetryGrid");
2007
+ if (
2008
+ !telemetry?.counters ||
2009
+ Object.keys(telemetry.counters).length === 0
2010
+ ) {
2011
+ grid.innerHTML =
2012
+ '<div class="empty-state" style="padding:8px"><span style="color:var(--text-tertiary);font-size:10px">No telemetry</span></div>';
2013
+ return;
2014
+ }
2015
+ grid.innerHTML = Object.entries(telemetry.counters)
2016
+ .sort(([, a], [, b]) => b - a)
2017
+ .map(
2018
+ ([name, val]) =>
2019
+ `<div class="counter-item"><span class="counter-name">${name}</span><span class="counter-value">${fmt(val)}</span></div>`,
2020
+ )
2021
+ .join("");
2022
+ }
2023
+
2024
+ function renderPageRank(pr) {
2025
+ const meta = document.getElementById("pagerankMeta");
2026
+ const list = document.getElementById("pagerankList");
2027
+ if (!pr || !pr.topFiles || pr.topFiles.length === 0) {
2028
+ meta.innerHTML = "";
2029
+ list.innerHTML =
2030
+ '<div class="empty-state" style="padding:16px"><span style="color:var(--text-tertiary)">No CodeGraph data</span></div>';
2031
+ return;
2032
+ }
2033
+ meta.innerHTML = `<span><strong>${fmt(pr.nodeCount)}</strong> files</span><span><strong>${fmt(pr.edgeCount)}</strong> edges</span>`;
2034
+ const maxScore = pr.topFiles[0]?.score ?? 1;
2035
+ list.innerHTML = pr.topFiles
2036
+ .map((f, i) => {
2037
+ const pct = Math.max(4, (f.score / maxScore) * 100);
2038
+ const name = f.file.replace(/^.*[\/\\]/, "");
2039
+ const dir = f.file.replace(/[\/\\][^\/\\]*$/, "");
2040
+ return `<div class="pagerank-item" title="${esc(f.file)}">
2041
+ <span class="pagerank-rank">${i + 1}</span>
2042
+ <span class="pagerank-file"><span style="color:var(--text-tertiary)">${esc(dir)}/</span>${esc(name)}</span>
2043
+ <div class="pagerank-score">
2044
+ <div class="pagerank-bar" style="width:${pct}%"></div>
2045
+ <span class="pagerank-val">${(f.score * 100).toFixed(1)}</span>
2046
+ </div>
2047
+ </div>`;
2048
+ })
2049
+ .join("");
2050
+ }
2051
+
2052
+ /* ================================================================
2053
+ CACHE CHART
2054
+ ================================================================ */
2055
+ function drawCacheChart() {
2056
+ const canvas = document.getElementById("cacheChart");
2057
+ const ctx = canvas.getContext("2d");
2058
+ const dpr = window.devicePixelRatio || 1;
2059
+ canvas.width = canvas.clientWidth * dpr;
2060
+ canvas.height = canvas.clientHeight * dpr;
2061
+ ctx.scale(dpr, dpr);
2062
+ const w = canvas.clientWidth,
2063
+ h = canvas.clientHeight;
2064
+ ctx.clearRect(0, 0, w, h);
2065
+
2066
+ if (cacheHistory.length < 2) {
2067
+ // Show cumulative totals as initial data instead of empty chart
2068
+ const last = cacheHistory[cacheHistory.length - 1];
2069
+ if (last && (last.hits > 0 || last.misses > 0)) {
2070
+ const total = last.hits + last.misses;
2071
+ const maxVal = Math.max(1, total);
2072
+ const chartH = h - 30;
2073
+ const barW = Math.max(40, w / 4);
2074
+ const cx = w / 2 - barW - 4;
2075
+ const hitH = (last.hits / maxVal) * chartH;
2076
+ ctx.fillStyle = "rgba(56,139,253,0.7)";
2077
+ ctx.beginPath();
2078
+ ctx.roundRect(
2079
+ cx,
2080
+ 10 + chartH - hitH,
2081
+ barW,
2082
+ hitH || 2,
2083
+ [4, 4, 0, 0],
2084
+ );
2085
+ ctx.fill();
2086
+ const missH = (last.misses / maxVal) * chartH;
2087
+ ctx.fillStyle = "rgba(248,81,73,0.5)";
2088
+ ctx.beginPath();
2089
+ ctx.roundRect(
2090
+ cx + barW + 8,
2091
+ 10 + chartH - missH,
2092
+ barW,
2093
+ missH || 2,
2094
+ [4, 4, 0, 0],
2095
+ );
2096
+ ctx.fill();
2097
+ // Labels
2098
+ ctx.fillStyle = "#8b949e";
2099
+ ctx.font = "11px Inter, sans-serif";
2100
+ ctx.textAlign = "center";
2101
+ ctx.fillText(
2102
+ "Hits: " + last.hits,
2103
+ cx + barW / 2,
2104
+ 10 + chartH - hitH - 6,
2105
+ );
2106
+ ctx.fillText(
2107
+ "Misses: " + last.misses,
2108
+ cx + barW + 8 + barW / 2,
2109
+ 10 + chartH - missH - 6,
2110
+ );
2111
+ } else {
2112
+ ctx.fillStyle = "#6e7681";
2113
+ ctx.font = "12px Inter, sans-serif";
2114
+ ctx.textAlign = "center";
2115
+ ctx.fillText("Collecting data...", w / 2, h / 2);
2116
+ }
2117
+ return;
2118
+ }
2119
+ const deltas = [];
2120
+ for (let i = 1; i < cacheHistory.length; i++) {
2121
+ deltas.push({
2122
+ hits: cacheHistory[i].hits - cacheHistory[i - 1].hits,
2123
+ misses: cacheHistory[i].misses - cacheHistory[i - 1].misses,
2124
+ });
2125
+ }
2126
+ const maxVal = Math.max(
2127
+ 1,
2128
+ ...deltas.map((d) => Math.max(d.hits, d.misses)),
2129
+ );
2130
+ const barW = Math.max(4, (w - 40) / deltas.length - 2);
2131
+ const chartH = h - 30;
2132
+
2133
+ ctx.strokeStyle = "rgba(240,246,252,0.05)";
2134
+ ctx.lineWidth = 1;
2135
+ for (let i = 0; i <= 4; i++) {
2136
+ const y = 10 + (chartH * i) / 4;
2137
+ ctx.beginPath();
2138
+ ctx.moveTo(30, y);
2139
+ ctx.lineTo(w, y);
2140
+ ctx.stroke();
2141
+ }
2142
+
2143
+ ctx.fillStyle = "#6e7681";
2144
+ ctx.font = '10px "JetBrains Mono", monospace';
2145
+ ctx.textAlign = "right";
2146
+ for (let i = 0; i <= 4; i++) {
2147
+ const y = 10 + (chartH * i) / 4;
2148
+ ctx.fillText((maxVal - (maxVal * i) / 4).toFixed(0), 26, y + 3);
2149
+ }
2150
+
2151
+ deltas.forEach((d, i) => {
2152
+ const x = 34 + i * (barW + 2);
2153
+ const hitH = (d.hits / maxVal) * chartH;
2154
+ ctx.fillStyle = "rgba(56,139,253,0.7)";
2155
+ ctx.beginPath();
2156
+ ctx.roundRect(
2157
+ x,
2158
+ 10 + chartH - hitH,
2159
+ barW / 2 - 1,
2160
+ hitH,
2161
+ [2, 2, 0, 0],
2162
+ );
2163
+ ctx.fill();
2164
+ const missH = (d.misses / maxVal) * chartH;
2165
+ ctx.fillStyle = "rgba(248,81,73,0.5)";
2166
+ ctx.beginPath();
2167
+ ctx.roundRect(
2168
+ x + barW / 2,
2169
+ 10 + chartH - missH,
2170
+ barW / 2 - 1,
2171
+ missH,
2172
+ [2, 2, 0, 0],
2173
+ );
2174
+ ctx.fill();
2175
+ });
2176
+
2177
+ ctx.fillStyle = "rgba(56,139,253,0.7)";
2178
+ ctx.fillRect(34, h - 12, 8, 8);
2179
+ ctx.fillStyle = "#8b949e";
2180
+ ctx.font = "10px Inter, sans-serif";
2181
+ ctx.textAlign = "left";
2182
+ ctx.fillText("Hits", 46, h - 4);
2183
+ ctx.fillStyle = "rgba(248,81,73,0.5)";
2184
+ ctx.fillRect(80, h - 12, 8, 8);
2185
+ ctx.fillStyle = "#8b949e";
2186
+ ctx.fillText("Misses", 92, h - 4);
2187
+ }
2188
+
2189
+ /* ================================================================
2190
+ SESSIONS — Sidebar
2191
+ ================================================================ */
2192
+ let sessionSSE = null;
2193
+
2194
+ function fetchSessions() {
2195
+ fetch(endpoint + "/api/sessions", { signal: AbortSignal.timeout(5000) })
2196
+ .then((r) => r.json())
2197
+ .then((data) => {
2198
+ allSessions = data.sessions || [];
2199
+ renderSidebar(allSessions);
2200
+ populateContextSelectors(allSessions);
2201
+ })
2202
+ .catch(() => {});
2203
+ }
2204
+
2205
+ function connectSessionSSE() {
2206
+ if (sessionSSE) {
2207
+ sessionSSE.close();
2208
+ sessionSSE = null;
2209
+ }
2210
+ try {
2211
+ sessionSSE = new EventSource(endpoint + "/api/sessions/stream");
2212
+ sessionSSE.onmessage = (e) => {
2213
+ try {
2214
+ const event = JSON.parse(e.data);
2215
+ if (event.type === "connected" && event.sessions) {
2216
+ allSessions = event.sessions;
2217
+ renderSidebar(allSessions);
2218
+ populateContextSelectors(allSessions);
2219
+ } else {
2220
+ fetchSessions();
2221
+ }
2222
+ } catch {}
2223
+ };
2224
+ sessionSSE.onerror = () => {
2225
+ sessionSSE.close();
2226
+ sessionSSE = null;
2227
+ };
2228
+ } catch {}
2229
+ }
2230
+
2231
+ function renderSidebar(sessions) {
2232
+ const container = document.getElementById("sidebarSessions");
2233
+ const countEl = document.getElementById("sidebarSessionCount");
2234
+ const summaryEl = document.getElementById("sidebarSummary");
2235
+ countEl.textContent = sessions.length;
2236
+
2237
+ if (!sessions.length) {
2238
+ container.innerHTML =
2239
+ '<div class="empty-state" style="padding:16px"><span class="empty-icon">🖥️</span>No sessions</div>';
2240
+ summaryEl.style.display = "none";
2241
+ return;
2242
+ }
2243
+
2244
+ // Group by IDE
2245
+ const groups = {};
2246
+ sessions.forEach((s) => {
2247
+ const ide = s.ideName || "unknown";
2248
+ if (!groups[ide]) groups[ide] = [];
2249
+ groups[ide].push(s);
2250
+ });
2251
+
2252
+ const now = Date.now();
2253
+ container.innerHTML = Object.entries(groups)
2254
+ .map(
2255
+ ([ide, list]) => `
2256
+ <div class="ide-group" data-ide="${esc(ide)}">
2257
+ <div class="ide-group-header">
2258
+ <span class="ide-dot"></span>
2259
+ <span class="ide-name">${esc(ide)}</span>
2260
+ <span class="ide-count">${list.length}</span>
2261
+ </div>
2262
+ ${list
2263
+ .map((s) => {
2264
+ const isStale = now - s.lastHeartbeat > 60000;
2265
+ const isSelected = selectedSessionId === s.sessionId;
2266
+ return `<div class="session-item${isSelected ? " selected" : ""}" data-session-id="${esc(s.sessionId)}" onclick="selectSession('${esc(s.sessionId)}')">
2267
+ <span class="session-dot${isStale ? " stale" : ""}"></span>
2268
+ <span class="session-name">${esc(s.projectName)}</span>
2269
+ <span class="session-files">${fmt(s.stats?.filesTracked ?? 0)}</span>
2270
+ </div>`;
2271
+ })
2272
+ .join("")}
2273
+ </div>
2274
+ `,
2275
+ )
2276
+ .join("");
2277
+
2278
+ // Summary
2279
+ const projects = new Set(sessions.map((s) => s.projectName));
2280
+ const ides = new Set(sessions.map((s) => s.ideName));
2281
+ const totalFiles = sessions.reduce(
2282
+ (sum, s) => sum + (s.stats?.filesTracked ?? 0),
2283
+ 0,
2284
+ );
2285
+ summaryEl.style.display = "grid";
2286
+ summaryEl.innerHTML = `
2287
+ <span class="sidebar-summary-item"><strong>${sessions.length}</strong> sessions</span>
2288
+ <span class="sidebar-summary-item"><strong>${projects.size}</strong> projects</span>
2289
+ <span class="sidebar-summary-item"><strong>${ides.size}</strong> IDEs</span>
2290
+ <span class="sidebar-summary-item"><strong>${fmt(totalFiles)}</strong> files</span>
2291
+ `;
2292
+ }
2293
+
2294
+ function selectSession(sessionId) {
2295
+ if (selectedSessionId === sessionId) {
2296
+ selectedSessionId = "";
2297
+ document.getElementById("ctxSession").value = "";
2298
+ } else {
2299
+ selectedSessionId = sessionId;
2300
+ document.getElementById("ctxSession").value = sessionId;
2301
+ }
2302
+ updateSidebarHighlight();
2303
+ fetchDashboard();
2304
+ }
2305
+
2306
+ function updateSidebarHighlight() {
2307
+ document.querySelectorAll(".session-item").forEach((el) => {
2308
+ el.classList.toggle(
2309
+ "selected",
2310
+ el.dataset.sessionId === selectedSessionId,
2311
+ );
2312
+ });
2313
+ }
2314
+
2315
+ /* ================================================================
2316
+ UTILS
2317
+ ================================================================ */
2318
+ function fmt(n) {
2319
+ return n == null ? "—" : Number(n).toLocaleString();
2320
+ }
2321
+ function esc(s) {
2322
+ return String(s)
2323
+ .replace(/&/g, "&amp;")
2324
+ .replace(/</g, "&lt;")
2325
+ .replace(/>/g, "&gt;")
2326
+ .replace(/"/g, "&quot;");
2327
+ }
2328
+ function fmtUptime(ms) {
2329
+ const s = Math.floor(ms / 1000);
2330
+ if (s < 60) return s + "s";
2331
+ const m = Math.floor(s / 60);
2332
+ if (m < 60) return m + "m " + (s % 60) + "s";
2333
+ const h = Math.floor(m / 60);
2334
+ if (h < 24) return h + "h " + (m % 60) + "m";
2335
+ return Math.floor(h / 24) + "d " + (h % 24) + "h";
2336
+ }
2337
+
2338
+ /* ================================================================
2339
+ INIT
2340
+ ================================================================ */
2341
+ document.getElementById("refreshToggle").classList.add("active");
2342
+ connect();
2343
+ startRefresh();
2344
+ connectSessionSSE();
2345
+ window.addEventListener("resize", drawCacheChart);
2346
+ </script>
2347
+ </body>
2348
+ </html>