labgate 0.5.10 → 0.5.11

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/dist/lib/ui.html CHANGED
@@ -4,35 +4,80 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>LabGate Settings</title>
7
- <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='20' fill='%23171717'/><text x='50' y='68' font-size='52' font-family='system-ui' font-weight='700' fill='white' text-anchor='middle'>L</text></svg>">
7
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='20' fill='%231A7A3A'/><text x='50' y='68' font-size='52' font-family='system-ui' font-weight='700' fill='white' text-anchor='middle'>L</text></svg>">
8
8
  <style>
9
+ @font-face {
10
+ font-family: 'Geist';
11
+ src: url('/fonts/Geist-Regular.woff2') format('woff2');
12
+ font-weight: 400;
13
+ font-style: normal;
14
+ font-display: swap;
15
+ }
16
+ @font-face {
17
+ font-family: 'Geist';
18
+ src: url('/fonts/Geist-Medium.woff2') format('woff2');
19
+ font-weight: 500;
20
+ font-style: normal;
21
+ font-display: swap;
22
+ }
23
+ @font-face {
24
+ font-family: 'Geist';
25
+ src: url('/fonts/Geist-SemiBold.woff2') format('woff2');
26
+ font-weight: 600;
27
+ font-style: normal;
28
+ font-display: swap;
29
+ }
30
+ @font-face {
31
+ font-family: 'Geist';
32
+ src: url('/fonts/Geist-Bold.woff2') format('woff2');
33
+ font-weight: 700;
34
+ font-style: normal;
35
+ font-display: swap;
36
+ }
37
+ @font-face {
38
+ font-family: 'GeistMono';
39
+ src: url('/fonts/GeistMono-Regular.woff2') format('woff2');
40
+ font-weight: 400;
41
+ font-style: normal;
42
+ font-display: swap;
43
+ }
44
+ @font-face {
45
+ font-family: 'GeistMono';
46
+ src: url('/fonts/GeistMono-Medium.woff2') format('woff2');
47
+ font-weight: 500;
48
+ font-style: normal;
49
+ font-display: swap;
50
+ }
51
+
9
52
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
10
53
 
11
54
  :root {
12
- --bg-primary: #F7F7F4;
13
- --bg-secondary: #EFEFE9;
14
- --text-primary: #171717;
15
- --text-secondary: #525252;
16
- --text-muted: #a3a3a3;
17
- --border-color: #e5e5e0;
18
- --card-bg: #ffffff;
19
- --radius: 16px;
20
- --radius-sm: 12px;
21
- --green: #22c55e;
22
- --red: #cf222e;
55
+ --bg-primary: #FAFDF8;
56
+ --bg-secondary: #F0F4EC;
57
+ --text-primary: #0C1A0E;
58
+ --text-secondary: #4A6350;
59
+ --text-muted: #8A9E8E;
60
+ --border-color: #D5E0D5;
61
+ --card-bg: #FFFFFF;
62
+ --accent: #1A7A3A;
63
+ --accent-light: #E8F5E9;
64
+ --radius: 12px;
65
+ --radius-sm: 8px;
66
+ --green: #16a34a;
67
+ --red: #dc2626;
23
68
  --blue: #0969da;
24
- --yellow: #eab308;
69
+ --yellow: #f59e0b;
25
70
  }
26
71
 
27
72
  body {
28
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
73
+ font-family: 'Geist', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
29
74
  color: var(--text-primary);
30
75
  background: var(--bg-primary);
31
- line-height: 1.6;
76
+ line-height: 1.5;
32
77
  -webkit-font-smoothing: antialiased;
33
78
  }
34
79
 
35
- code { font-family: 'SF Mono', Monaco, Consolas, monospace; }
80
+ code { font-family: 'GeistMono', monospace; }
36
81
 
37
82
  /* ── Layout ─────────────────────────────────── */
38
83
  .header {
@@ -52,7 +97,7 @@
52
97
  .config-path {
53
98
  font-size: 0.8125rem;
54
99
  color: var(--text-muted);
55
- font-family: 'SF Mono', Monaco, Consolas, monospace;
100
+ font-family: 'GeistMono', monospace;
56
101
  }
57
102
 
58
103
  .container {
@@ -86,13 +131,42 @@
86
131
 
87
132
  .tab:hover { color: var(--text-primary); }
88
133
  .tab.active {
89
- color: var(--text-primary);
90
- border-bottom-color: var(--text-primary);
134
+ color: var(--accent);
135
+ border-bottom-color: var(--accent);
91
136
  }
92
137
 
93
138
  .tab-panel { display: none; }
94
139
  .tab-panel.active { display: block; }
95
140
 
141
+ /* ── Sub-tabs (within Settings) ──────────────── */
142
+ .sub-tabs {
143
+ display: flex;
144
+ gap: 2px;
145
+ border-bottom: 1px solid var(--border-color);
146
+ margin-bottom: 24px;
147
+ overflow-x: auto;
148
+ }
149
+ .sub-tab {
150
+ padding: 7px 13px;
151
+ font-size: 0.8125rem;
152
+ font-weight: 500;
153
+ color: var(--text-muted);
154
+ background: none;
155
+ border: none;
156
+ border-bottom: 2px solid transparent;
157
+ cursor: pointer;
158
+ white-space: nowrap;
159
+ transition: all 0.15s;
160
+ font-family: inherit;
161
+ }
162
+ .sub-tab:hover { color: var(--text-primary); }
163
+ .sub-tab.active {
164
+ color: var(--accent);
165
+ border-bottom-color: var(--accent);
166
+ }
167
+ .sub-panel { display: none; }
168
+ .sub-panel.active { display: block; }
169
+
96
170
  /* ── Cards ──────────────────────────────────── */
97
171
  .card {
98
172
  background: var(--card-bg);
@@ -178,7 +252,7 @@
178
252
  border: none;
179
253
  }
180
254
 
181
- .toggle:checked { background: var(--green); }
255
+ .toggle:checked { background: var(--accent); }
182
256
 
183
257
  .toggle::after {
184
258
  content: '';
@@ -214,7 +288,7 @@
214
288
  align-items: center;
215
289
  padding: 6px 10px;
216
290
  font-size: 0.8125rem;
217
- font-family: 'SF Mono', Monaco, Consolas, monospace;
291
+ font-family: 'GeistMono', monospace;
218
292
  border-bottom: 1px solid var(--border-color);
219
293
  }
220
294
 
@@ -244,7 +318,7 @@
244
318
  flex: 1;
245
319
  padding: 6px 10px;
246
320
  font-size: 0.8125rem;
247
- font-family: 'SF Mono', Monaco, Consolas, monospace;
321
+ font-family: 'GeistMono', monospace;
248
322
  border: 1px solid var(--border-color);
249
323
  border-radius: 6px;
250
324
  background: var(--bg-primary);
@@ -299,7 +373,7 @@
299
373
  padding: 8px 24px;
300
374
  font-size: 0.875rem;
301
375
  font-weight: 500;
302
- background: var(--text-primary);
376
+ background: var(--accent);
303
377
  color: #fff;
304
378
  border: none;
305
379
  border-radius: 8px;
@@ -308,7 +382,7 @@
308
382
  transition: background 0.15s;
309
383
  }
310
384
 
311
- .btn-save:hover { background: #404040; }
385
+ .btn-save:hover { background: #15632F; }
312
386
 
313
387
  .btn-reset {
314
388
  padding: 8px 16px;
@@ -341,7 +415,7 @@
341
415
  }
342
416
 
343
417
  .toast.visible { transform: translateX(0); }
344
- .toast.success { background: #16a34a; }
418
+ .toast.success { background: var(--accent); }
345
419
  .toast.error { background: var(--red); }
346
420
 
347
421
  /* ── Sessions & Logs ───────────────────────── */
@@ -364,7 +438,7 @@
364
438
  padding: 8px 12px;
365
439
  border-bottom: 1px solid var(--border-color);
366
440
  color: var(--text-secondary);
367
- font-family: 'SF Mono', Monaco, Consolas, monospace;
441
+ font-family: 'GeistMono', monospace;
368
442
  font-size: 0.75rem;
369
443
  }
370
444
 
@@ -403,7 +477,7 @@
403
477
  }
404
478
 
405
479
  .mount-item:last-child { border-bottom: none; }
406
- .mount-item .mount-path { flex: 1; font-family: 'SF Mono', Monaco, Consolas, monospace; }
480
+ .mount-item .mount-path { flex: 1; font-family: 'GeistMono', monospace; }
407
481
  .mount-item .mount-mode {
408
482
  font-size: 0.75rem;
409
483
  padding: 2px 6px;
@@ -414,19 +488,324 @@
414
488
  .mount-item .mount-mode.ro { background: #dbeafe; color: #1e40af; }
415
489
 
416
490
  /* ── Dataset editor ──────────────────────── */
417
- .dataset-item {
491
+
492
+ /* Empty state */
493
+ .dataset-empty {
494
+ text-align: center;
495
+ padding: 56px 24px 48px;
496
+ }
497
+
498
+ .dataset-empty-icon {
499
+ display: inline-flex;
500
+ align-items: center;
501
+ justify-content: center;
502
+ width: 64px;
503
+ height: 64px;
504
+ margin-bottom: 20px;
505
+ background: var(--bg-secondary);
506
+ border-radius: 16px;
507
+ color: var(--text-muted);
508
+ }
509
+
510
+ .dataset-empty-icon svg { width: 28px; height: 28px; }
511
+
512
+ .dataset-empty-title {
513
+ font-size: 0.9375rem;
514
+ font-weight: 600;
515
+ color: var(--text-primary);
516
+ margin-bottom: 6px;
517
+ }
518
+
519
+ .dataset-empty-subtitle {
520
+ font-size: 0.8125rem;
521
+ color: var(--text-muted);
522
+ line-height: 1.5;
523
+ max-width: 340px;
524
+ margin: 0 auto;
525
+ }
526
+
527
+ /* Dataset cards grid */
528
+ @keyframes dataset-fade-in {
529
+ from { opacity: 0; transform: translateY(6px); }
530
+ to { opacity: 1; transform: translateY(0); }
531
+ }
532
+
533
+ .dataset-grid {
534
+ display: grid;
535
+ gap: 10px;
536
+ margin-bottom: 4px;
537
+ }
538
+
539
+ .dataset-card {
540
+ background: var(--card-bg);
541
+ border: 1px solid var(--border-color);
542
+ border-radius: var(--radius-sm);
543
+ padding: 14px 16px;
544
+ transition: border-color 0.15s, box-shadow 0.15s;
545
+ animation: dataset-fade-in 0.25s ease-out;
546
+ position: relative;
547
+ }
548
+
549
+ .dataset-card:hover {
550
+ border-color: #B8CBB8;
551
+ box-shadow: 0 2px 8px rgba(0,0,0,0.04);
552
+ }
553
+
554
+ .dataset-card-header {
418
555
  display: flex;
419
556
  align-items: center;
420
557
  gap: 10px;
421
- padding: 8px 12px;
558
+ margin-bottom: 8px;
559
+ }
560
+
561
+ .dataset-card-icon {
562
+ display: flex;
563
+ align-items: center;
564
+ justify-content: center;
565
+ width: 32px;
566
+ height: 32px;
567
+ border-radius: 8px;
568
+ background: var(--bg-secondary);
569
+ color: var(--text-muted);
570
+ flex-shrink: 0;
571
+ }
572
+
573
+ .dataset-card-icon svg { width: 16px; height: 16px; }
574
+
575
+ .dataset-card-title {
576
+ display: flex;
577
+ align-items: center;
578
+ gap: 8px;
579
+ flex: 1;
580
+ min-width: 0;
581
+ }
582
+
583
+ .dataset-card .dataset-name {
584
+ font-weight: 600;
585
+ font-size: 0.875rem;
586
+ color: var(--text-primary);
587
+ }
588
+
589
+ .dataset-card .mount-mode {
590
+ font-size: 0.6875rem;
591
+ padding: 2px 8px;
592
+ border-radius: 10px;
593
+ font-weight: 600;
594
+ text-transform: uppercase;
595
+ letter-spacing: 0.03em;
596
+ flex-shrink: 0;
597
+ }
598
+
599
+ .mount-mode.ro { background: #dbeafe; color: #1e40af; }
600
+ .mount-mode.rw { background: #fef3c7; color: #92400e; }
601
+
602
+ .dataset-card .remove-btn {
603
+ background: none;
604
+ border: none;
605
+ color: var(--text-muted);
606
+ cursor: pointer;
607
+ font-size: 1rem;
608
+ width: 28px;
609
+ height: 28px;
610
+ display: flex;
611
+ align-items: center;
612
+ justify-content: center;
613
+ border-radius: 6px;
614
+ opacity: 0;
615
+ transition: opacity 0.15s, background 0.15s, color 0.15s;
616
+ flex-shrink: 0;
617
+ }
618
+
619
+ .dataset-card:hover .remove-btn { opacity: 1; }
620
+ .dataset-card .remove-btn:hover { background: #fef2f2; color: var(--red); }
621
+
622
+ .dataset-card-desc {
422
623
  font-size: 0.8125rem;
423
- border-bottom: 1px solid var(--border-color);
624
+ color: var(--text-secondary);
625
+ padding-left: 42px;
626
+ margin-bottom: 10px;
627
+ line-height: 1.4;
628
+ }
629
+
630
+ .dataset-card-paths {
631
+ display: flex;
632
+ align-items: center;
633
+ gap: 6px;
634
+ padding-left: 42px;
635
+ flex-wrap: wrap;
636
+ }
637
+
638
+ .dataset-card .dataset-path {
639
+ font-family: 'GeistMono', monospace;
640
+ font-size: 0.6875rem;
641
+ color: var(--text-secondary);
642
+ background: var(--bg-primary);
643
+ padding: 3px 8px;
644
+ border-radius: 6px;
645
+ border: 1px solid var(--border-color);
646
+ }
647
+
648
+ .dataset-card .dataset-path-container {
649
+ color: var(--blue);
650
+ border-color: #dbeafe;
651
+ background: #f0f7ff;
652
+ }
653
+
654
+ .dataset-path-arrow {
655
+ color: var(--text-muted);
656
+ display: flex;
657
+ align-items: center;
658
+ flex-shrink: 0;
659
+ }
660
+
661
+ .dataset-count {
662
+ font-size: 0.75rem;
663
+ font-weight: 600;
664
+ color: var(--text-muted);
665
+ background: var(--bg-secondary);
666
+ padding: 2px 10px;
667
+ border-radius: 10px;
668
+ white-space: nowrap;
669
+ flex-shrink: 0;
670
+ }
671
+
672
+ /* Dataset restart notice */
673
+ .dataset-notice {
674
+ display: flex;
675
+ align-items: center;
676
+ gap: 8px;
677
+ padding: 10px 14px;
678
+ margin-bottom: 12px;
679
+ font-size: 0.8125rem;
680
+ color: #92400e;
681
+ background: #fffbeb;
682
+ border: 1px solid #fef3c7;
683
+ border-radius: 8px;
684
+ }
685
+
686
+ .dataset-notice svg { flex-shrink: 0; color: #d97706; }
687
+ .dataset-notice-text { flex: 1; }
688
+
689
+ .dataset-notice-btn {
690
+ flex-shrink: 0;
691
+ padding: 5px 14px;
692
+ font-size: 0.75rem;
693
+ font-weight: 600;
694
+ background: #f59e0b;
695
+ color: #fff;
696
+ border: none;
697
+ border-radius: 6px;
698
+ cursor: pointer;
699
+ font-family: inherit;
700
+ transition: background 0.15s;
701
+ white-space: nowrap;
702
+ }
703
+
704
+ .dataset-notice-btn:hover { background: #d97706; }
705
+ .dataset-notice-btn:disabled { opacity: 0.6; cursor: default; }
706
+
707
+ /* Dataset add form */
708
+ .dataset-add-form {
709
+ background: var(--card-bg);
710
+ border: 1px solid var(--border-color);
711
+ border-radius: var(--radius-sm);
712
+ padding: 20px;
713
+ margin-top: 4px;
714
+ }
715
+
716
+ .dataset-add-form .form-title {
717
+ font-size: 0.8125rem;
718
+ font-weight: 600;
719
+ color: var(--text-primary);
720
+ margin-bottom: 16px;
721
+ display: flex;
722
+ align-items: center;
723
+ gap: 8px;
724
+ }
725
+
726
+ .dataset-add-form .form-title-icon {
727
+ display: flex;
728
+ align-items: center;
729
+ justify-content: center;
730
+ width: 24px;
731
+ height: 24px;
732
+ border-radius: 6px;
733
+ background: var(--accent);
734
+ color: #fff;
735
+ }
736
+
737
+ .dataset-add-form .form-title-icon svg { width: 14px; height: 14px; }
738
+
739
+ .dataset-add-form .field-grid {
740
+ display: grid;
741
+ grid-template-columns: 2fr 1fr;
742
+ gap: 12px;
743
+ margin-bottom: 16px;
744
+ }
745
+
746
+ .dataset-add-form .field-full { grid-column: 1 / -1; }
747
+
748
+ .dataset-add-form .form-footer {
749
+ display: flex;
750
+ align-items: flex-end;
751
+ justify-content: space-between;
752
+ gap: 12px;
753
+ padding-top: 4px;
754
+ }
755
+
756
+ .dataset-add-form .btn-add-dataset {
757
+ padding: 8px 20px;
758
+ font-size: 0.8125rem;
759
+ font-weight: 500;
760
+ background: var(--accent);
761
+ color: #fff;
762
+ border: none;
763
+ border-radius: 8px;
764
+ cursor: pointer;
765
+ font-family: inherit;
766
+ transition: background 0.15s;
767
+ }
768
+
769
+ .dataset-add-form .btn-add-dataset:hover { background: #15632F; }
770
+
771
+ /* Mode pill toggle */
772
+ .mode-toggle {
773
+ display: inline-flex;
774
+ border: 1px solid var(--border-color);
775
+ border-radius: 8px;
776
+ overflow: hidden;
777
+ background: var(--bg-primary);
778
+ padding: 2px;
779
+ gap: 2px;
780
+ }
781
+
782
+ .mode-toggle input[type="radio"] { display: none; }
783
+
784
+ .mode-toggle label {
785
+ padding: 6px 16px;
786
+ font-size: 0.8125rem;
787
+ font-weight: 500;
788
+ cursor: pointer;
789
+ color: var(--text-muted);
790
+ transition: all 0.15s;
791
+ user-select: none;
792
+ border-radius: 6px;
793
+ border: none;
794
+ }
795
+
796
+ .mode-toggle label:hover { color: var(--text-secondary); }
797
+
798
+ .mode-toggle input[type="radio"]:checked + label.mode-ro {
799
+ background: #dbeafe;
800
+ color: #1e40af;
801
+ box-shadow: 0 1px 2px rgba(0,0,0,0.06);
802
+ }
803
+
804
+ .mode-toggle input[type="radio"]:checked + label.mode-rw {
805
+ background: #fef3c7;
806
+ color: #92400e;
807
+ box-shadow: 0 1px 2px rgba(0,0,0,0.06);
424
808
  }
425
- .dataset-item:last-child { border-bottom: none; }
426
- .dataset-item .dataset-name { font-weight: 600; min-width: 100px; }
427
- .dataset-item .dataset-path { flex: 1; font-family: 'SF Mono', Monaco, Consolas, monospace; color: var(--text-secondary); }
428
- .dataset-item .dataset-desc { color: var(--text-muted); font-style: italic; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
429
- .dataset-item .mount-mode { font-size: 0.75rem; padding: 2px 6px; border-radius: 4px; font-weight: 500; }
430
809
 
431
810
  /* ── Session cards ─────────────────────────── */
432
811
  .sessions-summary {
@@ -449,7 +828,7 @@
449
828
  transition: border-color 0.15s;
450
829
  }
451
830
 
452
- .session-card:hover { border-color: #c5c5c0; }
831
+ .session-card:hover { border-color: #B8CBB8; }
453
832
 
454
833
  .session-card.waiting {
455
834
  border-color: var(--yellow);
@@ -481,7 +860,7 @@
481
860
  padding: 1px 4px;
482
861
  border-radius: 3px;
483
862
  font-size: 0.75rem;
484
- font-family: 'SF Mono', Monaco, Consolas, monospace;
863
+ font-family: 'GeistMono', monospace;
485
864
  }
486
865
  .btn-restart-stop {
487
866
  padding: 4px 12px;
@@ -519,7 +898,7 @@
519
898
  }
520
899
 
521
900
  .session-id {
522
- font-family: 'SF Mono', Monaco, Consolas, monospace;
901
+ font-family: 'GeistMono', monospace;
523
902
  font-size: 0.8125rem;
524
903
  color: var(--text-secondary);
525
904
  }
@@ -534,7 +913,7 @@
534
913
  flex-shrink: 0;
535
914
  }
536
915
 
537
- .status-dot.active { background: var(--green); box-shadow: 0 0 6px rgba(34,197,94,0.4); }
916
+ .status-dot.active { background: var(--green); box-shadow: 0 0 6px rgba(22,163,74,0.4); }
538
917
  .status-dot.idle { background: var(--text-muted); }
539
918
  .status-dot.running { background: var(--yellow); }
540
919
 
@@ -544,8 +923,8 @@
544
923
  .status-dot.unknown { background: var(--text-muted); }
545
924
 
546
925
  @keyframes pulse-dot {
547
- 0%, 100% { opacity: 1; box-shadow: 0 0 4px rgba(34,197,94,0.3); }
548
- 50% { opacity: 0.5; box-shadow: 0 0 8px rgba(34,197,94,0.6); }
926
+ 0%, 100% { opacity: 1; box-shadow: 0 0 4px rgba(22,163,74,0.3); }
927
+ 50% { opacity: 0.5; box-shadow: 0 0 8px rgba(22,163,74,0.6); }
549
928
  }
550
929
 
551
930
  .status-label {
@@ -589,7 +968,7 @@
589
968
 
590
969
  .activity-detail {
591
970
  color: var(--text-muted);
592
- font-family: 'SF Mono', Monaco, Consolas, monospace;
971
+ font-family: 'GeistMono', monospace;
593
972
  font-size: 0.75rem;
594
973
  overflow: hidden;
595
974
  text-overflow: ellipsis;
@@ -635,7 +1014,7 @@
635
1014
  gap: 10px;
636
1015
  align-items: center;
637
1016
  font-size: 0.625rem;
638
- font-family: 'SF Mono', Monaco, Consolas, monospace;
1017
+ font-family: 'GeistMono', monospace;
639
1018
  color: var(--text-muted);
640
1019
  pointer-events: none;
641
1020
  box-shadow: 0 2px 8px rgba(0,0,0,0.12);
@@ -681,7 +1060,7 @@
681
1060
 
682
1061
  .resource-label {
683
1062
  font-size: 0.6875rem;
684
- font-family: 'SF Mono', Monaco, Consolas, monospace;
1063
+ font-family: 'GeistMono', monospace;
685
1064
  color: var(--text-muted);
686
1065
  min-width: 30px;
687
1066
  }
@@ -764,61 +1143,14 @@
764
1143
  box-shadow: 0 1px 2px rgba(0,0,0,0.06);
765
1144
  }
766
1145
 
767
- /* ── Security summary ──────────────────────── */
768
- .security-bar {
769
- display: flex;
770
- gap: 12px;
1146
+ /* ── Blocked events ────────────────────────── */
1147
+ .blocked-events-list {
771
1148
  margin-bottom: 16px;
772
- flex-wrap: wrap;
773
- }
774
-
775
- .security-stat {
776
- display: flex;
777
- align-items: center;
778
- gap: 8px;
779
- padding: 10px 16px;
780
- background: var(--card-bg);
781
- border: 1px solid var(--border-color);
782
- border-radius: var(--radius-sm);
783
- flex: 1;
784
- min-width: 140px;
785
- }
786
-
787
- .security-stat-info {
788
- display: flex;
789
- flex-direction: column;
790
- gap: 1px;
791
- }
792
-
793
- .security-stat-value {
794
- font-size: 1.125rem;
795
- font-weight: 600;
796
- line-height: 1.2;
797
- }
798
-
799
- .security-stat-label {
800
- font-size: 0.6875rem;
801
- color: var(--text-muted);
802
- text-transform: uppercase;
803
- letter-spacing: 0.04em;
804
- }
805
-
806
- .security-stat.has-blocked {
807
- border-color: var(--red);
808
- background: #fef2f2;
809
- }
810
-
811
- .security-stat.has-blocked .security-stat-value {
812
- color: var(--red);
813
- }
814
-
815
- .blocked-events-list {
816
- margin-top: 8px;
817
- background: var(--card-bg);
818
- border: 1px solid var(--border-color);
819
- border-radius: var(--radius-sm);
820
- overflow: hidden;
821
- display: none;
1149
+ background: var(--card-bg);
1150
+ border: 1px solid var(--border-color);
1151
+ border-radius: var(--radius-sm);
1152
+ overflow: hidden;
1153
+ display: none;
822
1154
  }
823
1155
 
824
1156
  .blocked-events-list.visible { display: block; }
@@ -841,7 +1173,7 @@
841
1173
  }
842
1174
 
843
1175
  .blocked-event-cmd {
844
- font-family: 'SF Mono', Monaco, Consolas, monospace;
1176
+ font-family: 'GeistMono', monospace;
845
1177
  font-weight: 500;
846
1178
  color: var(--text-primary);
847
1179
  }
@@ -881,7 +1213,7 @@
881
1213
  display: inline-block;
882
1214
  padding: 1px 5px;
883
1215
  font-size: 0.5625rem;
884
- font-family: 'SF Mono', Monaco, Consolas, monospace;
1216
+ font-family: 'GeistMono', monospace;
885
1217
  background: #e8e8e3;
886
1218
  border-radius: 3px;
887
1219
  color: #b0b0a8;
@@ -928,7 +1260,7 @@
928
1260
  display: inline-block;
929
1261
  padding: 1px 5px;
930
1262
  font-size: 0.5625rem;
931
- font-family: 'SF Mono', Monaco, Consolas, monospace;
1263
+ font-family: 'GeistMono', monospace;
932
1264
  background: #dbeafe;
933
1265
  border-radius: 3px;
934
1266
  color: #1e40af;
@@ -1007,7 +1339,7 @@
1007
1339
 
1008
1340
  .session-detail span {
1009
1341
  color: var(--text-secondary);
1010
- font-family: 'SF Mono', Monaco, Consolas, monospace;
1342
+ font-family: 'GeistMono', monospace;
1011
1343
  font-size: 0.75rem;
1012
1344
  overflow: hidden;
1013
1345
  text-overflow: ellipsis;
@@ -1070,7 +1402,7 @@
1070
1402
  background: var(--bg-secondary);
1071
1403
  border: 1px solid var(--border-color);
1072
1404
  border-radius: 6px;
1073
- font-family: 'SF Mono', Monaco, Consolas, monospace;
1405
+ font-family: 'GeistMono', monospace;
1074
1406
  font-size: 0.8125rem;
1075
1407
  color: var(--text-primary);
1076
1408
  }
@@ -1094,7 +1426,7 @@
1094
1426
  color: var(--text-secondary);
1095
1427
  }
1096
1428
 
1097
- /* ── Instructions modal ───────────────────── */
1429
+ /* ── Modal backdrop (shared) ────────────── */
1098
1430
  .modal-backdrop {
1099
1431
  position: fixed;
1100
1432
  inset: 0;
@@ -1122,6 +1454,106 @@
1122
1454
  box-shadow: 0 18px 48px rgba(0,0,0,0.15);
1123
1455
  }
1124
1456
 
1457
+ /* ── Instructions sidebar (right panel) ── */
1458
+ #instructionsModal {
1459
+ background: rgba(0,0,0,0.25);
1460
+ padding: 0;
1461
+ display: none;
1462
+ align-items: stretch;
1463
+ justify-content: flex-end;
1464
+ }
1465
+
1466
+ #instructionsModal.visible {
1467
+ display: flex;
1468
+ }
1469
+
1470
+ #instructionsModal .instructions-modal {
1471
+ position: fixed;
1472
+ top: 0;
1473
+ right: 0;
1474
+ width: min(520px, 90vw);
1475
+ height: 100vh;
1476
+ max-height: 100vh;
1477
+ border-radius: 0;
1478
+ border: none;
1479
+ border-left: 1px solid var(--border-color);
1480
+ box-shadow: -4px 0 24px rgba(0,0,0,0.12);
1481
+ transform: translateX(100%);
1482
+ transition: transform 0.25s ease;
1483
+ }
1484
+
1485
+ #instructionsModal.visible .instructions-modal {
1486
+ transform: translateX(0);
1487
+ }
1488
+
1489
+ /* ── Sidebar resize handle ────────────────── */
1490
+ .sidebar-resize-handle {
1491
+ position: absolute;
1492
+ top: 0;
1493
+ left: -3px;
1494
+ width: 6px;
1495
+ height: 100%;
1496
+ cursor: col-resize;
1497
+ z-index: 10;
1498
+ display: flex;
1499
+ align-items: center;
1500
+ justify-content: center;
1501
+ }
1502
+
1503
+ .sidebar-resize-handle::before {
1504
+ content: '';
1505
+ position: absolute;
1506
+ top: 0;
1507
+ left: 2px;
1508
+ width: 2px;
1509
+ height: 100%;
1510
+ background: transparent;
1511
+ transition: background 0.15s;
1512
+ border-radius: 1px;
1513
+ }
1514
+
1515
+ .sidebar-resize-handle:hover::before,
1516
+ .sidebar-resize-handle.dragging::before {
1517
+ background: var(--accent);
1518
+ }
1519
+
1520
+ /* Sidebar collapse toggle */
1521
+ .sidebar-collapse-btn {
1522
+ position: absolute;
1523
+ top: 50%;
1524
+ left: -14px;
1525
+ transform: translateY(-50%);
1526
+ width: 14px;
1527
+ height: 40px;
1528
+ display: flex;
1529
+ align-items: center;
1530
+ justify-content: center;
1531
+ background: var(--card-bg);
1532
+ border: 1px solid var(--border-color);
1533
+ border-right: none;
1534
+ border-radius: 6px 0 0 6px;
1535
+ cursor: pointer;
1536
+ z-index: 11;
1537
+ color: var(--text-muted);
1538
+ transition: color 0.15s, background 0.15s, border-color 0.15s;
1539
+ padding: 0;
1540
+ }
1541
+
1542
+ .sidebar-collapse-btn:hover {
1543
+ color: var(--accent);
1544
+ border-color: var(--accent);
1545
+ background: var(--accent-light);
1546
+ }
1547
+
1548
+ .sidebar-collapse-btn svg {
1549
+ width: 10px;
1550
+ height: 10px;
1551
+ }
1552
+
1553
+ #instructionsModal .instructions-modal.resizing {
1554
+ transition: none;
1555
+ }
1556
+
1125
1557
  .instructions-header {
1126
1558
  display: flex;
1127
1559
  align-items: center;
@@ -1140,7 +1572,7 @@
1140
1572
  .instructions-subtitle {
1141
1573
  font-size: 0.6875rem;
1142
1574
  color: var(--text-muted);
1143
- font-family: 'SF Mono', Monaco, Consolas, monospace;
1575
+ font-family: 'GeistMono', monospace;
1144
1576
  }
1145
1577
 
1146
1578
  .instructions-tabs {
@@ -1158,7 +1590,7 @@
1158
1590
  background: transparent;
1159
1591
  color: var(--text-muted);
1160
1592
  cursor: pointer;
1161
- font-family: 'SF Mono', Monaco, Consolas, monospace;
1593
+ font-family: 'GeistMono', monospace;
1162
1594
  transition: all 0.15s;
1163
1595
  }
1164
1596
 
@@ -1173,7 +1605,7 @@
1173
1605
  color: var(--text-muted);
1174
1606
  padding: 8px 12px;
1175
1607
  border-bottom: 1px solid var(--border-color);
1176
- font-family: 'SF Mono', Monaco, Consolas, monospace;
1608
+ font-family: 'GeistMono', monospace;
1177
1609
  white-space: nowrap;
1178
1610
  overflow: hidden;
1179
1611
  text-overflow: ellipsis;
@@ -1207,14 +1639,14 @@
1207
1639
  font-size: 0.6875rem;
1208
1640
  line-height: 1.45;
1209
1641
  color: var(--text-secondary);
1210
- font-family: 'SF Mono', Monaco, Consolas, monospace;
1642
+ font-family: 'GeistMono', monospace;
1211
1643
  }
1212
1644
 
1213
1645
  .instructions-editor {
1214
1646
  width: 100%;
1215
- min-height: 300px;
1216
- height: 50vh;
1217
- resize: vertical;
1647
+ min-height: 200px;
1648
+ flex: 1;
1649
+ resize: none;
1218
1650
  border: none;
1219
1651
  outline: none;
1220
1652
  padding: 12px;
@@ -1222,7 +1654,7 @@
1222
1654
  color: var(--text-primary);
1223
1655
  font-size: 0.75rem;
1224
1656
  line-height: 1.45;
1225
- font-family: 'SF Mono', Monaco, Consolas, monospace;
1657
+ font-family: 'GeistMono', monospace;
1226
1658
  }
1227
1659
 
1228
1660
  .instructions-actions {
@@ -1285,6 +1717,282 @@
1285
1717
  min-width: 16px;
1286
1718
  text-align: center;
1287
1719
  }
1720
+ .results-toolbar {
1721
+ display: flex;
1722
+ gap: 8px;
1723
+ margin-bottom: 16px;
1724
+ align-items: center;
1725
+ flex-wrap: wrap;
1726
+ }
1727
+ .results-toolbar input[type="text"] {
1728
+ padding: 6px 10px;
1729
+ border: 1px solid var(--border-color);
1730
+ border-radius: 6px;
1731
+ background: var(--card-bg);
1732
+ color: var(--text-primary);
1733
+ font-size: 0.8125rem;
1734
+ font-family: 'GeistMono', monospace;
1735
+ }
1736
+ .results-toolbar input[type="text"]:focus {
1737
+ outline: none;
1738
+ border-color: var(--accent);
1739
+ }
1740
+ .results-toolbar .results-search {
1741
+ flex: 1;
1742
+ min-width: 220px;
1743
+ max-width: 420px;
1744
+ }
1745
+ .results-toolbar .results-tag {
1746
+ width: 160px;
1747
+ }
1748
+
1749
+ /* ── Results card grid ─────────────────── */
1750
+ @keyframes result-fade-in {
1751
+ from { opacity: 0; transform: translateY(6px); }
1752
+ to { opacity: 1; transform: translateY(0); }
1753
+ }
1754
+ .results-grid {
1755
+ display: grid;
1756
+ gap: 10px;
1757
+ }
1758
+ .result-card {
1759
+ background: var(--card-bg);
1760
+ border: 1px solid var(--border-color);
1761
+ border-radius: var(--radius-sm);
1762
+ padding: 14px 16px;
1763
+ transition: border-color 0.15s, box-shadow 0.15s;
1764
+ animation: result-fade-in 0.25s ease-out;
1765
+ position: relative;
1766
+ }
1767
+ .result-card:hover {
1768
+ border-color: #B8CBB8;
1769
+ box-shadow: 0 2px 8px rgba(0,0,0,0.04);
1770
+ }
1771
+ .result-card-header {
1772
+ display: flex;
1773
+ align-items: center;
1774
+ gap: 10px;
1775
+ margin-bottom: 6px;
1776
+ }
1777
+ .result-card-icon {
1778
+ display: flex;
1779
+ align-items: center;
1780
+ justify-content: center;
1781
+ width: 32px;
1782
+ height: 32px;
1783
+ border-radius: 8px;
1784
+ background: var(--bg-secondary);
1785
+ color: var(--text-muted);
1786
+ flex-shrink: 0;
1787
+ }
1788
+ .result-card-icon svg { width: 16px; height: 16px; }
1789
+ .result-card-title {
1790
+ display: flex;
1791
+ align-items: center;
1792
+ gap: 8px;
1793
+ flex: 1;
1794
+ min-width: 0;
1795
+ }
1796
+ .result-card-title .result-name {
1797
+ font-weight: 600;
1798
+ font-size: 0.875rem;
1799
+ color: var(--text-primary);
1800
+ overflow: hidden;
1801
+ text-overflow: ellipsis;
1802
+ white-space: nowrap;
1803
+ }
1804
+ .result-source-badge {
1805
+ font-size: 0.6875rem;
1806
+ padding: 2px 8px;
1807
+ border-radius: 10px;
1808
+ font-weight: 600;
1809
+ text-transform: uppercase;
1810
+ letter-spacing: 0.03em;
1811
+ flex-shrink: 0;
1812
+ }
1813
+ .result-source-badge.source-claude { background: #f0e6ff; color: #7c3aed; }
1814
+ .result-source-badge.source-codex { background: #dbeafe; color: #1e40af; }
1815
+ .result-source-badge.source-default { background: var(--bg-secondary); color: var(--text-secondary); }
1816
+ .result-card-actions {
1817
+ display: flex;
1818
+ gap: 4px;
1819
+ margin-left: auto;
1820
+ opacity: 0;
1821
+ transition: opacity 0.15s;
1822
+ flex-shrink: 0;
1823
+ }
1824
+ .result-card:hover .result-card-actions { opacity: 1; }
1825
+ .result-card-actions button {
1826
+ background: none;
1827
+ border: none;
1828
+ color: var(--text-muted);
1829
+ cursor: pointer;
1830
+ font-size: 0.75rem;
1831
+ width: 28px;
1832
+ height: 28px;
1833
+ display: flex;
1834
+ align-items: center;
1835
+ justify-content: center;
1836
+ border-radius: 6px;
1837
+ transition: background 0.15s, color 0.15s;
1838
+ }
1839
+ .result-card-actions button:hover { background: var(--bg-secondary); color: var(--text-primary); }
1840
+ .result-card-actions button.danger:hover { background: #fef2f2; color: var(--red); }
1841
+ .result-card-summary {
1842
+ font-size: 0.8125rem;
1843
+ color: var(--text-secondary);
1844
+ padding-left: 42px;
1845
+ margin-bottom: 8px;
1846
+ line-height: 1.4;
1847
+ display: -webkit-box;
1848
+ -webkit-line-clamp: 2;
1849
+ -webkit-box-orient: vertical;
1850
+ overflow: hidden;
1851
+ }
1852
+ .result-card-tags {
1853
+ display: flex;
1854
+ gap: 4px;
1855
+ flex-wrap: wrap;
1856
+ padding-left: 42px;
1857
+ margin-bottom: 8px;
1858
+ }
1859
+ .result-tag-pill {
1860
+ display: inline-block;
1861
+ font-size: 0.6875rem;
1862
+ padding: 2px 8px;
1863
+ border-radius: 10px;
1864
+ background: #dbeafe;
1865
+ color: #1e40af;
1866
+ border: 1px solid #bfdbfe;
1867
+ font-family: 'GeistMono', monospace;
1868
+ white-space: nowrap;
1869
+ }
1870
+ .result-card-meta {
1871
+ display: flex;
1872
+ align-items: center;
1873
+ gap: 12px;
1874
+ padding-left: 42px;
1875
+ flex-wrap: wrap;
1876
+ }
1877
+ .result-meta-item {
1878
+ display: flex;
1879
+ align-items: center;
1880
+ gap: 4px;
1881
+ font-size: 0.6875rem;
1882
+ color: var(--text-muted);
1883
+ }
1884
+ .result-meta-item svg { width: 12px; height: 12px; flex-shrink: 0; }
1885
+ .result-meta-item span {
1886
+ font-family: 'GeistMono', monospace;
1887
+ font-size: 0.6875rem;
1888
+ }
1889
+ .result-card-details-toggle {
1890
+ display: flex;
1891
+ align-items: center;
1892
+ gap: 6px;
1893
+ padding: 8px 0 0 42px;
1894
+ margin-top: 8px;
1895
+ border-top: 1px solid var(--border-color);
1896
+ cursor: pointer;
1897
+ font-size: 0.75rem;
1898
+ color: var(--text-muted);
1899
+ user-select: none;
1900
+ }
1901
+ .result-card-details-toggle:hover { color: var(--text-secondary); }
1902
+ .result-card-details-toggle .chevron {
1903
+ transition: transform 0.2s;
1904
+ font-size: 0.625rem;
1905
+ }
1906
+ .result-card-details-toggle.expanded .chevron { transform: rotate(90deg); }
1907
+ .result-card-details-content {
1908
+ padding: 8px 0 4px 42px;
1909
+ font-size: 0.8125rem;
1910
+ color: var(--text-secondary);
1911
+ line-height: 1.5;
1912
+ white-space: pre-wrap;
1913
+ font-family: 'GeistMono', monospace;
1914
+ max-height: 200px;
1915
+ overflow-y: auto;
1916
+ display: none;
1917
+ }
1918
+ .result-card-details-content.visible { display: block; }
1919
+ .results-empty {
1920
+ text-align: center;
1921
+ padding: 56px 24px 48px;
1922
+ }
1923
+ .results-empty-icon {
1924
+ display: inline-flex;
1925
+ align-items: center;
1926
+ justify-content: center;
1927
+ width: 64px;
1928
+ height: 64px;
1929
+ margin-bottom: 20px;
1930
+ background: var(--bg-secondary);
1931
+ border-radius: 16px;
1932
+ color: var(--text-muted);
1933
+ }
1934
+ .results-empty-icon svg { width: 28px; height: 28px; }
1935
+ .results-empty-title {
1936
+ font-size: 0.9375rem;
1937
+ font-weight: 600;
1938
+ color: var(--text-primary);
1939
+ margin-bottom: 6px;
1940
+ }
1941
+ .results-empty-subtitle {
1942
+ font-size: 0.8125rem;
1943
+ color: var(--text-muted);
1944
+ line-height: 1.5;
1945
+ max-width: 340px;
1946
+ margin: 0 auto;
1947
+ }
1948
+ .btn-add-result {
1949
+ padding: 4px 12px;
1950
+ font-size: 0.75rem;
1951
+ font-weight: 500;
1952
+ background: var(--accent);
1953
+ color: #fff;
1954
+ border: 1px solid var(--accent);
1955
+ border-radius: 6px;
1956
+ cursor: pointer;
1957
+ font-family: inherit;
1958
+ transition: all 0.15s;
1959
+ }
1960
+ .btn-add-result:hover { background: #15692f; }
1961
+
1962
+ .result-form-grid {
1963
+ display: grid;
1964
+ grid-template-columns: 1fr 1fr;
1965
+ gap: 10px;
1966
+ margin-top: 10px;
1967
+ }
1968
+ .result-field-full { grid-column: 1 / -1; }
1969
+ .result-label {
1970
+ display: block;
1971
+ margin-bottom: 4px;
1972
+ font-size: 12px;
1973
+ color: var(--text-muted);
1974
+ font-weight: 600;
1975
+ }
1976
+ .result-input,
1977
+ .result-textarea {
1978
+ width: 100%;
1979
+ border: 1px solid var(--border-color);
1980
+ border-radius: 6px;
1981
+ padding: 8px 10px;
1982
+ background: var(--card-bg);
1983
+ color: var(--text-primary);
1984
+ font-size: 13px;
1985
+ font-family: 'GeistMono', monospace;
1986
+ }
1987
+ .result-textarea {
1988
+ min-height: 90px;
1989
+ resize: vertical;
1990
+ }
1991
+ .result-input:focus,
1992
+ .result-textarea:focus {
1993
+ outline: none;
1994
+ border-color: var(--accent);
1995
+ }
1288
1996
  .slurm-summary {
1289
1997
  display: flex;
1290
1998
  gap: 12px;
@@ -1312,9 +2020,9 @@
1312
2020
  margin-top: 2px;
1313
2021
  }
1314
2022
  .slurm-stat-pending .slurm-stat-value { color: #f59e0b; }
1315
- .slurm-stat-running .slurm-stat-value { color: #22c55e; }
2023
+ .slurm-stat-running .slurm-stat-value { color: #16a34a; }
1316
2024
  .slurm-stat-completed .slurm-stat-value { color: #60a5fa; }
1317
- .slurm-stat-failed .slurm-stat-value { color: #ef4444; }
2025
+ .slurm-stat-failed .slurm-stat-value { color: #dc2626; }
1318
2026
  .slurm-stat-other .slurm-stat-value { color: var(--muted); }
1319
2027
 
1320
2028
  .slurm-filters {
@@ -1371,14 +2079,14 @@
1371
2079
  text-transform: uppercase;
1372
2080
  }
1373
2081
  .slurm-state-PENDING { background: rgba(245,158,11,0.15); color: #f59e0b; }
1374
- .slurm-state-RUNNING { background: rgba(34,197,94,0.15); color: #22c55e; }
1375
- .slurm-state-COMPLETING { background: rgba(34,197,94,0.1); color: #86efac; }
2082
+ .slurm-state-RUNNING { background: rgba(22,163,74,0.15); color: #16a34a; }
2083
+ .slurm-state-COMPLETING { background: rgba(22,163,74,0.1); color: #86efac; }
1376
2084
  .slurm-state-COMPLETED { background: rgba(96,165,250,0.15); color: #60a5fa; }
1377
- .slurm-state-FAILED { background: rgba(239,68,68,0.15); color: #ef4444; }
2085
+ .slurm-state-FAILED { background: rgba(239,68,68,0.15); color: #dc2626; }
1378
2086
  .slurm-state-CANCELLED { background: rgba(156,163,175,0.15); color: #9ca3af; }
1379
2087
  .slurm-state-TIMEOUT { background: rgba(249,115,22,0.15); color: #f97316; }
1380
- .slurm-state-NODE_FAIL { background: rgba(239,68,68,0.15); color: #ef4444; }
1381
- .slurm-state-OUT_OF_MEMORY { background: rgba(239,68,68,0.15); color: #ef4444; }
2088
+ .slurm-state-NODE_FAIL { background: rgba(239,68,68,0.15); color: #dc2626; }
2089
+ .slurm-state-OUT_OF_MEMORY { background: rgba(239,68,68,0.15); color: #dc2626; }
1382
2090
  .slurm-state-UNKNOWN { background: rgba(156,163,175,0.1); color: #9ca3af; }
1383
2091
 
1384
2092
  .slurm-output-btn {
@@ -1393,7 +2101,7 @@
1393
2101
  margin-right: 4px;
1394
2102
  }
1395
2103
  .slurm-output-btn:hover { background: rgba(255,255,255,0.05); }
1396
- .slurm-output-btn.has-content { color: #22c55e; border-color: #22c55e; }
2104
+ .slurm-output-btn.has-content { color: #16a34a; border-color: #16a34a; }
1397
2105
  .slurm-output-btn.is-empty { color: var(--muted); }
1398
2106
  .slurm-output-btn.not-found { color: var(--muted); opacity: 0.5; cursor: default; }
1399
2107
 
@@ -1420,7 +2128,7 @@
1420
2128
  padding: 2px 8px;
1421
2129
  cursor: pointer;
1422
2130
  font-size: 11px;
1423
- color: #ef4444;
2131
+ color: #dc2626;
1424
2132
  }
1425
2133
  .slurm-cancel-btn:hover { background: rgba(239,68,68,0.1); }
1426
2134
  .slurm-notes-cell { max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
@@ -1434,7 +2142,7 @@
1434
2142
  width: 6px;
1435
2143
  height: 6px;
1436
2144
  border-radius: 50%;
1437
- background: #22c55e;
2145
+ background: #16a34a;
1438
2146
  margin-right: 4px;
1439
2147
  animation: pulse 1.5s ease-in-out infinite;
1440
2148
  }
@@ -1445,7 +2153,10 @@
1445
2153
  .header { flex-direction: column; gap: 8px; }
1446
2154
  .session-card-body { grid-template-columns: 1fr; }
1447
2155
  .instructions-modal { width: 100%; max-height: 94vh; }
2156
+ #instructionsModal .instructions-modal { width: 100vw; max-height: 100vh; }
1448
2157
  .instructions-editor { height: 48vh; min-height: 240px; }
2158
+ .result-card-meta { flex-direction: column; align-items: flex-start; }
2159
+ .result-form-grid { grid-template-columns: 1fr; }
1449
2160
  }
1450
2161
  /* ── Enterprise: locked fields ─────────────── */
1451
2162
  .field-locked label::before { content: '\1F512 '; font-size: 0.75em; }
@@ -1455,7 +2166,8 @@
1455
2166
  .field-locked .add-row { display: none; }
1456
2167
  .list-item.locked { opacity: 0.7; }
1457
2168
  .list-item.locked .remove-btn { display: none; }
1458
- .list-item.locked::after { content: 'admin'; font-size: 0.65rem; color: var(--text-muted); margin-left: 8px; }
2169
+ .list-item.locked::after { content: 'set by admin'; font-size: 0.65rem; color: var(--text-muted); margin-left: 8px; }
2170
+ .set-by-admin-note { font-size: 0.7rem; color: var(--text-muted); font-weight: 500; margin-left: 6px; }
1459
2171
  .enterprise-badge { font-size: 0.75rem; color: var(--text-muted); font-weight: 400; margin-left: 8px; }
1460
2172
  .constraint-hint { font-size: 0.7rem; color: var(--text-muted); margin-left: 4px; }
1461
2173
  .net-mode-pill.locked { opacity: 0.5; pointer-events: none; }
@@ -1471,7 +2183,7 @@
1471
2183
  color: #92400e;
1472
2184
  font-size: 0.8rem;
1473
2185
  }
1474
- .policy-editor { width: 100%; min-height: 300px; font-family: 'SF Mono', Monaco, Consolas, monospace; font-size: 0.8rem; padding: 12px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); background: var(--bg-secondary); resize: vertical; }
2186
+ .policy-editor { width: 100%; min-height: 300px; font-family: 'GeistMono', monospace; font-size: 0.8rem; padding: 12px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); background: var(--bg-secondary); resize: vertical; }
1475
2187
  .admin-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
1476
2188
  .admin-table th, .admin-table td { text-align: left; padding: 8px 12px; border-bottom: 1px solid var(--border-color); }
1477
2189
  .admin-table th { font-weight: 600; color: var(--text-secondary); }
@@ -1483,6 +2195,209 @@
1483
2195
  .license-days.green { color: var(--green); }
1484
2196
  .license-days.yellow { color: var(--yellow); }
1485
2197
  .license-days.red { color: var(--red); }
2198
+
2199
+ /* ── MCP Servers ─────────────────────────── */
2200
+ .mcp-server-card {
2201
+ background: var(--card-bg);
2202
+ border: 1px solid var(--border-color);
2203
+ border-radius: var(--radius-sm);
2204
+ padding: 18px 20px;
2205
+ margin-bottom: 12px;
2206
+ transition: border-color 0.15s, box-shadow 0.15s;
2207
+ }
2208
+ .mcp-server-card:hover {
2209
+ border-color: #B8CBB8;
2210
+ box-shadow: 0 2px 8px rgba(0,0,0,0.04);
2211
+ }
2212
+ .mcp-server-header {
2213
+ display: flex;
2214
+ align-items: center;
2215
+ gap: 10px;
2216
+ margin-bottom: 10px;
2217
+ }
2218
+ .mcp-server-icon {
2219
+ display: flex;
2220
+ align-items: center;
2221
+ justify-content: center;
2222
+ width: 32px;
2223
+ height: 32px;
2224
+ border-radius: 8px;
2225
+ background: var(--bg-secondary);
2226
+ color: var(--text-muted);
2227
+ flex-shrink: 0;
2228
+ }
2229
+ .mcp-server-icon svg { width: 16px; height: 16px; }
2230
+ .mcp-server-name {
2231
+ font-size: 0.9375rem;
2232
+ font-weight: 600;
2233
+ color: var(--text-primary);
2234
+ }
2235
+ .mcp-server-status {
2236
+ display: inline-block;
2237
+ font-size: 0.6875rem;
2238
+ font-weight: 600;
2239
+ padding: 2px 8px;
2240
+ border-radius: 9px;
2241
+ margin-left: auto;
2242
+ text-transform: uppercase;
2243
+ letter-spacing: 0.03em;
2244
+ }
2245
+ .mcp-server-status.active {
2246
+ background: rgba(22,163,74,0.15);
2247
+ color: #16a34a;
2248
+ }
2249
+ .mcp-server-status.inactive {
2250
+ background: rgba(163,163,163,0.15);
2251
+ color: var(--text-muted);
2252
+ }
2253
+ .mcp-server-status.partial {
2254
+ background: rgba(245,158,11,0.18);
2255
+ color: #92400e;
2256
+ }
2257
+ .mcp-server-states {
2258
+ display: flex;
2259
+ align-items: center;
2260
+ gap: 6px;
2261
+ margin-bottom: 10px;
2262
+ flex-wrap: wrap;
2263
+ }
2264
+ .mcp-state-pill {
2265
+ display: inline-flex;
2266
+ align-items: center;
2267
+ gap: 4px;
2268
+ font-size: 0.6875rem;
2269
+ font-weight: 600;
2270
+ border-radius: 999px;
2271
+ padding: 2px 8px;
2272
+ border: 1px solid var(--border-color);
2273
+ color: var(--text-muted);
2274
+ background: var(--bg-secondary);
2275
+ white-space: nowrap;
2276
+ }
2277
+ .mcp-state-pill.on {
2278
+ border-color: #bbf7d0;
2279
+ color: #166534;
2280
+ background: #f0fdf4;
2281
+ }
2282
+ .mcp-state-pill.off {
2283
+ border-color: #e5e7eb;
2284
+ color: #6b7280;
2285
+ background: #f9fafb;
2286
+ }
2287
+ .mcp-server-reason {
2288
+ font-size: 0.75rem;
2289
+ color: #92400e;
2290
+ background: #fffbeb;
2291
+ border: 1px solid #fef3c7;
2292
+ border-radius: 6px;
2293
+ padding: 6px 10px;
2294
+ margin-bottom: 12px;
2295
+ }
2296
+ .mcp-server-desc {
2297
+ font-size: 0.8125rem;
2298
+ color: var(--text-secondary);
2299
+ margin-bottom: 12px;
2300
+ }
2301
+ .mcp-server-command {
2302
+ font-size: 0.75rem;
2303
+ font-family: 'GeistMono', monospace;
2304
+ color: var(--text-muted);
2305
+ background: var(--bg-secondary);
2306
+ padding: 6px 10px;
2307
+ border-radius: 6px;
2308
+ margin-bottom: 14px;
2309
+ overflow-x: auto;
2310
+ white-space: nowrap;
2311
+ }
2312
+ .mcp-tools-label {
2313
+ font-size: 0.75rem;
2314
+ font-weight: 600;
2315
+ color: var(--text-secondary);
2316
+ text-transform: uppercase;
2317
+ letter-spacing: 0.05em;
2318
+ margin-bottom: 8px;
2319
+ }
2320
+ .mcp-tool-list { display: grid; gap: 0; }
2321
+ .mcp-tool-item {
2322
+ display: block;
2323
+ font-size: 0.8125rem;
2324
+ padding: 7px 0;
2325
+ border-bottom: 1px solid var(--border-color);
2326
+ }
2327
+ .mcp-tool-item:last-child { border-bottom: none; }
2328
+ .mcp-tool-main {
2329
+ display: flex;
2330
+ align-items: center;
2331
+ gap: 8px;
2332
+ margin-bottom: 2px;
2333
+ }
2334
+ .mcp-tool-name {
2335
+ font-family: 'GeistMono', monospace;
2336
+ font-size: 0.75rem;
2337
+ font-weight: 500;
2338
+ color: var(--text-primary);
2339
+ background: var(--bg-secondary);
2340
+ padding: 1px 6px;
2341
+ border-radius: 4px;
2342
+ white-space: nowrap;
2343
+ flex-shrink: 0;
2344
+ }
2345
+ .mcp-tool-title {
2346
+ color: var(--text-secondary);
2347
+ font-size: 0.75rem;
2348
+ font-weight: 500;
2349
+ }
2350
+ .mcp-tool-desc {
2351
+ color: var(--text-muted);
2352
+ font-size: 0.8125rem;
2353
+ }
2354
+ .mcp-tool-example {
2355
+ margin-top: 3px;
2356
+ color: var(--text-secondary);
2357
+ font-size: 0.75rem;
2358
+ font-style: italic;
2359
+ }
2360
+ .mcp-empty {
2361
+ text-align: center;
2362
+ padding: 56px 24px 48px;
2363
+ }
2364
+ .mcp-empty-icon {
2365
+ display: inline-flex;
2366
+ align-items: center;
2367
+ justify-content: center;
2368
+ width: 64px;
2369
+ height: 64px;
2370
+ margin-bottom: 20px;
2371
+ background: var(--bg-secondary);
2372
+ border-radius: 16px;
2373
+ color: var(--text-muted);
2374
+ }
2375
+ .mcp-empty-icon svg { width: 28px; height: 28px; }
2376
+ .mcp-empty-title {
2377
+ font-size: 0.9375rem;
2378
+ font-weight: 600;
2379
+ color: var(--text-primary);
2380
+ margin-bottom: 6px;
2381
+ }
2382
+ .mcp-empty-subtitle {
2383
+ font-size: 0.8125rem;
2384
+ color: var(--text-muted);
2385
+ line-height: 1.5;
2386
+ max-width: 380px;
2387
+ margin: 0 auto;
2388
+ }
2389
+ .mcp-badge {
2390
+ display: inline-block;
2391
+ background: var(--accent);
2392
+ color: #fff;
2393
+ font-size: 11px;
2394
+ font-weight: 700;
2395
+ padding: 1px 6px;
2396
+ border-radius: 9px;
2397
+ margin-left: 4px;
2398
+ min-width: 16px;
2399
+ text-align: center;
2400
+ }
1486
2401
  </style>
1487
2402
  </head>
1488
2403
  <body>
@@ -1495,184 +2410,210 @@
1495
2410
  <div class="container">
1496
2411
  <div class="tabs">
1497
2412
  <button class="tab active" data-tab="sessions">Dashboard</button>
1498
- <button class="tab" data-tab="runtime">Runtime</button>
1499
- <button class="tab" data-tab="network">Network</button>
1500
- <button class="tab" data-tab="filesystem">Filesystem</button>
1501
2413
  <button class="tab" data-tab="datasets">Datasets</button>
1502
- <button class="tab" data-tab="commands">Commands</button>
1503
- <button class="tab" data-tab="audit">Audit</button>
1504
- <button class="tab" data-tab="logs">Logs</button>
1505
- <button class="tab" data-tab="slurm" id="slurmTab" style="display:none">SLURM Jobs <span class="slurm-badge" id="slurmBadge" style="display:none"></span></button>
2414
+ <button class="tab" data-tab="results">Results</button>
2415
+ <button class="tab" data-tab="jobs">Jobs <span class="slurm-badge" id="slurmBadge" style="display:none"></span></button>
2416
+ <button class="tab" data-tab="settings">Settings <span class="mcp-badge" id="mcpBadge" style="display:none"></span></button>
1506
2417
  </div>
1507
2418
 
1508
- <!-- Runtime -->
1509
- <div class="tab-panel" id="panel-runtime">
1510
- <div class="card">
1511
- <h3>Runtime &amp; Image</h3>
1512
- <p class="card-description">Apptainer (primary) or Podman runtime and base image for sandbox sessions.</p>
1513
- <div class="field-row">
1514
- <div class="field">
1515
- <label for="runtime">Runtime</label>
1516
- <select id="runtime">
1517
- <option value="auto">auto (prefer apptainer, then podman)</option>
1518
- <option value="apptainer">apptainer</option>
1519
- <option value="podman">podman</option>
1520
- </select>
2419
+ <!-- Settings (sub-tabs) -->
2420
+ <div class="tab-panel" id="panel-settings">
2421
+ <div class="sub-tabs">
2422
+ <button class="sub-tab active" data-subtab="runtime">Runtime</button>
2423
+ <button class="sub-tab" data-subtab="network">Network</button>
2424
+ <button class="sub-tab" data-subtab="filesystem">Filesystem</button>
2425
+ <button class="sub-tab" data-subtab="commands">Commands</button>
2426
+ <button class="sub-tab" data-subtab="audit">Audit &amp; Logs</button>
2427
+ <button class="sub-tab" data-subtab="mcp">MCP Servers</button>
2428
+ </div>
2429
+
2430
+ <!-- Runtime sub-panel -->
2431
+ <div class="sub-panel active" id="subpanel-runtime">
2432
+ <div class="card">
2433
+ <h3>Runtime &amp; Image</h3>
2434
+ <p class="card-description">Apptainer (primary) or Podman runtime and base image for sandbox sessions.</p>
2435
+ <div class="field-row">
2436
+ <div class="field">
2437
+ <label for="runtime">Runtime</label>
2438
+ <select id="runtime">
2439
+ <option value="auto">auto (prefer apptainer, then podman)</option>
2440
+ <option value="apptainer">apptainer</option>
2441
+ <option value="podman">podman</option>
2442
+ </select>
2443
+ </div>
2444
+ <div class="field">
2445
+ <label for="timeout">Session Timeout (hours)</label>
2446
+ <input type="number" id="timeout" min="0" step="1" value="8">
2447
+ </div>
1521
2448
  </div>
1522
2449
  <div class="field">
1523
- <label for="timeout">Session Timeout (hours)</label>
1524
- <input type="number" id="timeout" min="0" step="1" value="8">
2450
+ <label for="image">Container Image</label>
2451
+ <input type="text" id="image" placeholder="docker.io/library/node:20-slim">
1525
2452
  </div>
1526
2453
  </div>
1527
- <div class="field">
1528
- <label for="image">Container Image</label>
1529
- <input type="text" id="image" placeholder="docker.io/library/node:20-slim">
1530
- </div>
1531
2454
  </div>
1532
- </div>
1533
2455
 
1534
- <!-- Network -->
1535
- <div class="tab-panel" id="panel-network">
1536
- <div class="card">
1537
- <h3>Network Policy</h3>
1538
- <p class="card-description">Control how sandboxed agents access the network.</p>
1539
- <div class="field">
1540
- <label for="networkMode">Mode</label>
1541
- <select id="networkMode">
1542
- <option value="none">none (fully isolated)</option>
1543
- <option value="filtered">filtered (proxy + domain allowlist)</option>
1544
- <option value="host">host (full network access)</option>
1545
- </select>
1546
- </div>
1547
- </div>
1548
- <div class="card">
1549
- <h3>Allowed Domains</h3>
1550
- <p class="card-description">Domains agents can reach in filtered mode.</p>
1551
- <div class="list-editor" id="domainsList"></div>
1552
- <div class="list-add">
1553
- <input type="text" id="domainInput" placeholder="api.example.com" onkeydown="if(event.key==='Enter'){addDomain()}">
1554
- <button class="btn-add" onclick="addDomain()">Add</button>
1555
- </div>
1556
- </div>
1557
- </div>
1558
-
1559
- <!-- Filesystem -->
1560
- <div class="tab-panel" id="panel-filesystem">
1561
- <div class="card">
1562
- <h3>Blocked Patterns</h3>
1563
- <p class="card-description">Glob patterns hidden from the sandbox via empty overlays.</p>
1564
- <div class="list-editor" id="blockedList"></div>
1565
- <div class="list-add">
1566
- <input type="text" id="blockedInput" placeholder="**/.ssh" onkeydown="if(event.key==='Enter'){addBlocked()}">
1567
- <button class="btn-add" onclick="addBlocked()">Add</button>
2456
+ <!-- Network sub-panel -->
2457
+ <div class="sub-panel" id="subpanel-network">
2458
+ <div class="card">
2459
+ <h3>Network Policy</h3>
2460
+ <p class="card-description">Control how sandboxed agents access the network.</p>
2461
+ <div class="field">
2462
+ <label for="networkMode">Mode</label>
2463
+ <select id="networkMode">
2464
+ <option value="host">host (full network access)</option>
2465
+ <option value="filtered">filtered (proxy + domain allowlist)</option>
2466
+ </select>
2467
+ </div>
1568
2468
  </div>
1569
- </div>
1570
- <div class="card">
1571
- <h3>Extra Mount Paths</h3>
1572
- <p class="card-description">Additional host paths to mount into the sandbox.</p>
1573
- <div class="list-editor" id="mountsList"></div>
1574
- <div class="list-add">
1575
- <input type="text" id="mountPathInput" placeholder="/data/shared" onkeydown="if(event.key==='Enter'){addMount()}">
1576
- <select id="mountModeInput">
1577
- <option value="ro">ro</option>
1578
- <option value="rw">rw</option>
1579
- </select>
1580
- <button class="btn-add" onclick="addMount()">Add</button>
2469
+ <div class="card">
2470
+ <h3>Allowed Domains</h3>
2471
+ <p class="card-description">Domains agents can reach in filtered mode.</p>
2472
+ <div class="list-editor" id="domainsList"></div>
2473
+ <div class="list-add">
2474
+ <input type="text" id="domainInput" placeholder="api.example.com" onkeydown="if(event.key==='Enter'){addDomain()}">
2475
+ <button class="btn-add" onclick="addDomain()">Add</button>
2476
+ </div>
1581
2477
  </div>
1582
2478
  </div>
1583
- </div>
1584
2479
 
1585
- <!-- Datasets -->
1586
- <div class="tab-panel" id="panel-datasets">
1587
- <div class="card">
1588
- <h3>Datasets</h3>
1589
- <p class="card-description">Named datasets mounted under <code>/datasets/{name}</code> in the sandbox. The AI agent is told about each dataset by name and description.</p>
1590
- <div class="list-editor" id="datasetsList"></div>
1591
- <div style="padding: 12px 16px; border-top: 1px solid var(--border-color);">
1592
- <div style="display:flex; gap:8px; margin-bottom:8px;">
1593
- <input type="text" id="datasetPathInput" placeholder="/data/genomes" style="flex:2" onkeydown="if(event.key==='Enter'){addDataset()}">
1594
- <input type="text" id="datasetNameInput" placeholder="Name (auto)" style="flex:1">
2480
+ <!-- Filesystem sub-panel -->
2481
+ <div class="sub-panel" id="subpanel-filesystem">
2482
+ <div class="card">
2483
+ <h3>Blocked Patterns</h3>
2484
+ <p class="card-description">Glob patterns hidden from the sandbox via empty overlays.</p>
2485
+ <div class="list-editor" id="blockedList"></div>
2486
+ <div class="list-add">
2487
+ <input type="text" id="blockedInput" placeholder="**/.ssh" onkeydown="if(event.key==='Enter'){addBlocked()}">
2488
+ <button class="btn-add" onclick="addBlocked()">Add</button>
1595
2489
  </div>
1596
- <div style="display:flex; gap:8px; align-items:flex-end;">
1597
- <input type="text" id="datasetDescInput" placeholder="Description (optional)" style="flex:2">
1598
- <select id="datasetModeInput" style="flex:0 0 auto;">
1599
- <option value="ro" selected>ro</option>
2490
+ </div>
2491
+ <div class="card">
2492
+ <h3>Extra Mount Paths</h3>
2493
+ <p class="card-description">Additional host paths to mount into the sandbox.</p>
2494
+ <div class="list-editor" id="mountsList"></div>
2495
+ <div class="list-add">
2496
+ <input type="text" id="mountPathInput" placeholder="/data/shared" onkeydown="if(event.key==='Enter'){addMount()}">
2497
+ <select id="mountModeInput">
2498
+ <option value="ro">ro</option>
1600
2499
  <option value="rw">rw</option>
1601
2500
  </select>
1602
- <button class="btn-add" onclick="addDataset()">Add Dataset</button>
2501
+ <button class="btn-add" onclick="addMount()">Add</button>
1603
2502
  </div>
1604
2503
  </div>
1605
2504
  </div>
1606
- </div>
1607
2505
 
1608
- <!-- Commands -->
1609
- <div class="tab-panel" id="panel-commands">
1610
- <div class="card">
1611
- <h3>Command Blacklist</h3>
1612
- <p class="card-description">Commands blocked inside the sandbox. Agents get a clear error instead of silent failure.</p>
1613
- <div class="list-editor" id="blacklistList"></div>
1614
- <div class="list-add">
1615
- <input type="text" id="blacklistInput" placeholder="ssh" onkeydown="if(event.key==='Enter'){addBlacklist()}">
1616
- <button class="btn-add" onclick="addBlacklist()">Add</button>
2506
+ <!-- Commands sub-panel -->
2507
+ <div class="sub-panel" id="subpanel-commands">
2508
+ <div class="card">
2509
+ <h3>Command Blacklist</h3>
2510
+ <p class="card-description">Commands blocked inside the sandbox. Agents get a clear error instead of silent failure.</p>
2511
+ <div class="list-editor" id="blacklistList"></div>
2512
+ <div class="list-add">
2513
+ <input type="text" id="blacklistInput" placeholder="ssh" onkeydown="if(event.key==='Enter'){addBlacklist()}">
2514
+ <button class="btn-add" onclick="addBlacklist()">Add</button>
2515
+ </div>
1617
2516
  </div>
1618
2517
  </div>
1619
- </div>
1620
2518
 
1621
- <!-- Audit -->
1622
- <div class="tab-panel" id="panel-audit">
1623
- <div class="card">
1624
- <h3>Audit Logging</h3>
1625
- <p class="card-description">Record session events to structured JSONL files.</p>
1626
- <div class="field">
1627
- <div class="toggle-wrap">
1628
- <input type="checkbox" class="toggle" id="auditEnabled">
1629
- <span class="toggle-label">Enable audit logging</span>
2519
+ <!-- Audit & Logs sub-panel (merged) -->
2520
+ <div class="sub-panel" id="subpanel-audit">
2521
+ <div class="card">
2522
+ <h3>Audit Logging</h3>
2523
+ <p class="card-description">Record session events to structured JSONL files.</p>
2524
+ <div class="field">
2525
+ <div class="toggle-wrap">
2526
+ <input type="checkbox" class="toggle" id="auditEnabled">
2527
+ <span class="toggle-label">Enable audit logging</span>
2528
+ </div>
2529
+ </div>
2530
+ <div class="field" style="margin-top: 16px">
2531
+ <label for="logDir">Log Directory</label>
2532
+ <input type="text" id="logDir" placeholder="~/.labgate/logs">
1630
2533
  </div>
1631
2534
  </div>
1632
- <div class="field" style="margin-top: 16px">
1633
- <label for="logDir">Log Directory</label>
1634
- <input type="text" id="logDir" placeholder="~/.labgate/logs">
2535
+ <div class="card">
2536
+ <h3>Recent Audit Logs <button class="refresh-btn" onclick="loadLogs()">Refresh</button></h3>
2537
+ <div id="logsContent"><div class="empty-state">Loading...</div></div>
1635
2538
  </div>
1636
2539
  </div>
1637
- </div>
1638
2540
 
1639
- <!-- Sessions / Dashboard -->
1640
- <div class="tab-panel active" id="panel-sessions">
1641
- <div class="net-switcher" id="netSwitcher">
1642
- <span class="net-switcher-label">Network</span>
1643
- <span class="spacer"></span>
1644
- <div class="net-mode-pills">
1645
- <button class="net-mode-pill" data-mode="none" onclick="switchNetMode('none')">None</button>
1646
- <button class="net-mode-pill" data-mode="filtered" onclick="switchNetMode('filtered')">Filtered</button>
1647
- <button class="net-mode-pill" data-mode="host" onclick="switchNetMode('host')">Host</button>
2541
+ <!-- MCP Servers sub-panel -->
2542
+ <div class="sub-panel" id="subpanel-mcp">
2543
+ <div style="display:flex;align-items:baseline;justify-content:space-between;margin-bottom:16px">
2544
+ <p class="card-description" style="margin:0">MCP servers registered for the AI agent. These provide specialized tools the agent can use during sessions.</p>
2545
+ <button class="refresh-btn" onclick="loadMcpServers()">Refresh</button>
1648
2546
  </div>
2547
+ <div id="mcpContent"><div class="empty-state">Loading...</div></div>
1649
2548
  </div>
1650
- <div class="security-bar" id="securityBar">
1651
- <div class="security-stat" id="secStatBlocked">
1652
- <div class="security-stat-info">
1653
- <span class="security-stat-value" id="blockedCountValue">0</span>
1654
- <span class="security-stat-label">Blocked</span>
1655
- </div>
2549
+
2550
+ </div><!-- /panel-settings -->
2551
+
2552
+ <!-- Datasets -->
2553
+ <div class="tab-panel" id="panel-datasets">
2554
+ <div style="display:flex;align-items:baseline;justify-content:space-between;margin-bottom:16px">
2555
+ <p class="card-description" style="margin:0">Named datasets mounted under <code>/datasets/{name}</code> in the sandbox. The AI agent is told about each dataset by name and description.</p>
2556
+ <span class="dataset-count" id="datasetCount" style="display:none"></span>
2557
+ </div>
2558
+ <div class="dataset-notice" id="datasetNotice">
2559
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
2560
+ <span class="dataset-notice-text">Save config, then restart active sessions from this page (or run: labgate restart &lt;session-id&gt;).</span>
2561
+ <button class="dataset-notice-btn" id="datasetRestartBtn" style="display:none" onclick="restartOutdatedSessions()">Restart Sessions Now</button>
2562
+ </div>
2563
+ <div id="datasetsList"></div>
2564
+ <div class="dataset-add-form">
2565
+ <div class="form-title">
2566
+ <span class="form-title-icon">
2567
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
2568
+ </span>
2569
+ Add a dataset
1656
2570
  </div>
1657
- <div class="security-stat">
1658
- <div class="security-stat-info">
1659
- <span class="security-stat-value" id="blacklistCountValue">-</span>
1660
- <span class="security-stat-label">Cmd rules</span>
2571
+ <div class="field-grid">
2572
+ <div class="field">
2573
+ <label for="datasetPathInput">Host Path</label>
2574
+ <input type="text" id="datasetPathInput" placeholder="/data/genomes" onkeydown="if(event.key==='Enter'){addDataset()}">
1661
2575
  </div>
1662
- </div>
1663
- <div class="security-stat">
1664
- <div class="security-stat-info">
1665
- <span class="security-stat-value" id="patternsCountValue">-</span>
1666
- <span class="security-stat-label">Hidden paths</span>
2576
+ <div class="field">
2577
+ <label for="datasetNameInput">Name <span style="font-weight:400;color:var(--text-muted)">(auto)</span></label>
2578
+ <input type="text" id="datasetNameInput" placeholder="genomes">
2579
+ </div>
2580
+ <div class="field field-full">
2581
+ <label for="datasetDescInput">Description <span style="font-weight:400;color:var(--text-muted)">(optional)</span></label>
2582
+ <input type="text" id="datasetDescInput" placeholder="Human reference genomes collection">
1667
2583
  </div>
1668
2584
  </div>
1669
- <div class="security-stat">
1670
- <div class="security-stat-info">
1671
- <span class="security-stat-value" id="netModeValue">-</span>
1672
- <span class="security-stat-label">Network</span>
2585
+ <div class="form-footer">
2586
+ <div class="field" style="margin-bottom:0">
2587
+ <label style="margin-bottom:4px">Access Mode</label>
2588
+ <div class="mode-toggle">
2589
+ <input type="radio" name="datasetMode" id="datasetModeRO" value="ro" checked>
2590
+ <label for="datasetModeRO" class="mode-ro">Read-only</label>
2591
+ <input type="radio" name="datasetMode" id="datasetModeRW" value="rw">
2592
+ <label for="datasetModeRW" class="mode-rw">Read-write</label>
2593
+ </div>
1673
2594
  </div>
2595
+ <button class="btn-add-dataset" onclick="addDataset()">Add Dataset</button>
1674
2596
  </div>
1675
2597
  </div>
2598
+ </div>
2599
+
2600
+ <!-- Results -->
2601
+ <div class="tab-panel" id="panel-results">
2602
+ <div style="display:flex;align-items:baseline;justify-content:space-between;margin-bottom:16px">
2603
+ <p class="card-description" style="margin:0">Structured findings saved by Claude/Codex via LabGate results tools.</p>
2604
+ <button class="refresh-btn" onclick="loadResults()">Refresh</button>
2605
+ </div>
2606
+ <div class="results-toolbar">
2607
+ <input type="text" class="results-search" id="resultsSearchInput" placeholder="Search title, summary, details, tags..." oninput="debounceResultsSearch()">
2608
+ <input type="text" class="results-tag" id="resultsTagInput" placeholder="tag filter (exact)" oninput="debounceResultsSearch()">
2609
+ <input type="text" class="results-tag" id="resultsSourceInput" placeholder="source filter (claude/codex)" oninput="debounceResultsSearch()">
2610
+ <button class="btn-add-result" onclick="openResultEditor()">+ Add Result</button>
2611
+ </div>
2612
+ <div id="resultsContent"><div class="empty-state">Loading...</div></div>
2613
+ </div>
2614
+
2615
+ <!-- Sessions / Dashboard -->
2616
+ <div class="tab-panel active" id="panel-sessions">
1676
2617
  <div class="blocked-events-list" id="blockedEventsList"></div>
1677
2618
  <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:16px">
1678
2619
  <div class="sessions-summary" id="sessionsSummary"></div>
@@ -1681,16 +2622,15 @@
1681
2622
  <div id="sessionsContent"><div class="empty-state">Loading...</div></div>
1682
2623
  </div>
1683
2624
 
1684
- <!-- Logs -->
1685
- <div class="tab-panel" id="panel-logs">
1686
- <div class="card">
1687
- <h3>Recent Audit Logs <button class="refresh-btn" onclick="loadLogs()">Refresh</button></h3>
1688
- <div id="logsContent"><div class="empty-state">Loading...</div></div>
2625
+ <!-- Jobs panel (SLURM) -->
2626
+ <div class="tab-panel" id="panel-jobs">
2627
+ <div id="jobsEmptyState" class="empty-state" style="padding: 56px 24px">
2628
+ <div style="font-weight: 600; margin-bottom: 6px;">No Job Scheduler</div>
2629
+ <div style="font-size: 0.8125rem; color: var(--text-muted); max-width: 340px; margin: 0 auto;">
2630
+ Enable SLURM integration in Settings to monitor cluster jobs here.
2631
+ </div>
1689
2632
  </div>
1690
- </div>
1691
-
1692
- <!-- SLURM Jobs panel -->
1693
- <div class="tab-panel" id="panel-slurm">
2633
+ <div id="jobsSlurmContent" style="display:none">
1694
2634
  <div class="slurm-summary" id="slurmSummary">
1695
2635
  <div class="slurm-stat slurm-stat-pending"><span class="slurm-stat-value" id="slurmPending">0</span><span class="slurm-stat-label">Pending</span></div>
1696
2636
  <div class="slurm-stat slurm-stat-running"><span class="slurm-stat-value" id="slurmRunning">0</span><span class="slurm-stat-label">Running</span></div>
@@ -1714,10 +2654,11 @@
1714
2654
  <div class="card">
1715
2655
  <div id="slurmJobsContent"><div class="empty-state">Loading SLURM jobs...</div></div>
1716
2656
  </div>
1717
- </div>
2657
+ </div><!-- /jobsSlurmContent -->
2658
+ </div><!-- /panel-jobs -->
1718
2659
 
1719
2660
  <!-- SLURM output modal -->
1720
- <div class="modal-backdrop" id="slurmOutputModal" onclick="if(event.target===this){closeSlurmOutput()}" style="display:none">
2661
+ <div class="modal-backdrop" id="slurmOutputModal" onclick="if(event.target===this){closeSlurmOutput()}">
1721
2662
  <div class="instructions-modal">
1722
2663
  <div class="instructions-header">
1723
2664
  <div>
@@ -1738,7 +2679,7 @@
1738
2679
  </div>
1739
2680
 
1740
2681
  <!-- SLURM notes modal -->
1741
- <div class="modal-backdrop" id="slurmNotesModal" onclick="if(event.target===this){closeSlurmNotes()}" style="display:none">
2682
+ <div class="modal-backdrop" id="slurmNotesModal" onclick="if(event.target===this){closeSlurmNotes()}">
1742
2683
  <div class="instructions-modal" style="max-width:600px">
1743
2684
  <div class="instructions-header">
1744
2685
  <div>
@@ -1756,6 +2697,55 @@
1756
2697
  </div>
1757
2698
  </div>
1758
2699
 
2700
+ <!-- Results editor modal -->
2701
+ <div class="modal-backdrop" id="resultsModal" onclick="if(event.target===this){closeResultEditor()}">
2702
+ <div class="instructions-modal" style="max-width:920px;width:min(920px,94vw)">
2703
+ <div class="instructions-header">
2704
+ <div>
2705
+ <div class="instructions-title" id="resultEditorTitle">Add Result</div>
2706
+ <div class="instructions-subtitle" id="resultEditorSubtitle"></div>
2707
+ </div>
2708
+ <span class="spacer"></span>
2709
+ <button class="instructions-btn-secondary" onclick="closeResultEditor()">Cancel</button>
2710
+ <button class="instructions-btn-primary" onclick="saveResultEditor()">Save</button>
2711
+ </div>
2712
+ <div class="result-form-grid">
2713
+ <div>
2714
+ <label class="result-label" for="resultTitleInput">Title</label>
2715
+ <input class="result-input" id="resultTitleInput" type="text" placeholder="What was achieved?">
2716
+ </div>
2717
+ <div>
2718
+ <label class="result-label" for="resultSourceInputModal">Source</label>
2719
+ <input class="result-input" id="resultSourceInputModal" type="text" placeholder="claude">
2720
+ </div>
2721
+ <div class="result-field-full">
2722
+ <label class="result-label" for="resultSummaryInput">Summary</label>
2723
+ <input class="result-input" id="resultSummaryInput" type="text" placeholder="One-line summary">
2724
+ </div>
2725
+ <div class="result-field-full">
2726
+ <label class="result-label" for="resultDetailsInput">Details</label>
2727
+ <textarea class="result-textarea" id="resultDetailsInput" placeholder="Longer details (optional)"></textarea>
2728
+ </div>
2729
+ <div>
2730
+ <label class="result-label" for="resultTagsInput">Tags (comma-separated)</label>
2731
+ <input class="result-input" id="resultTagsInput" type="text" placeholder="slurm, gpu, benchmark">
2732
+ </div>
2733
+ <div>
2734
+ <label class="result-label" for="resultArtifactsInput">Artifacts (comma-separated paths)</label>
2735
+ <input class="result-input" id="resultArtifactsInput" type="text" placeholder="/work/out/report.md, /work/out/plot.png">
2736
+ </div>
2737
+ <div>
2738
+ <label class="result-label" for="resultSessionIdInput">Session ID</label>
2739
+ <input class="result-input" id="resultSessionIdInput" type="text" placeholder="optional">
2740
+ </div>
2741
+ <div>
2742
+ <label class="result-label" for="resultWorkdirInput">Workdir</label>
2743
+ <input class="result-input" id="resultWorkdirInput" type="text" placeholder="/work">
2744
+ </div>
2745
+ </div>
2746
+ </div>
2747
+ </div>
2748
+
1759
2749
  <!-- Admin: Policy (hidden by default, shown for admins) -->
1760
2750
  <div class="tab-panel" id="panel-admin-policy" style="display:none">
1761
2751
  <div class="card admin-card">
@@ -1799,6 +2789,10 @@
1799
2789
  <div class="toast" id="toast"></div>
1800
2790
  <div class="modal-backdrop" id="instructionsModal" onclick="if(event.target===this){closeInstructions()}">
1801
2791
  <div class="instructions-modal">
2792
+ <div class="sidebar-resize-handle" id="sidebarResizeHandle"></div>
2793
+ <button class="sidebar-collapse-btn" onclick="closeInstructions()" title="Close sidebar">
2794
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/></svg>
2795
+ </button>
1802
2796
  <div class="instructions-header">
1803
2797
  <div>
1804
2798
  <div class="instructions-title">Session Instructions</div>
@@ -1843,6 +2837,12 @@ var instructionsState = {
1843
2837
  template: '',
1844
2838
  loading: false
1845
2839
  };
2840
+ var mcpAutoRefreshTimer = null;
2841
+ var mcpAutoRegisterInFlight = false;
2842
+ var MCP_AUTO_REFRESH_MS = 12000;
2843
+ var resultsSearchTimeout = null;
2844
+ var resultsCache = [];
2845
+ var resultEditorId = null;
1846
2846
 
1847
2847
  // ── Activity sparkline timeline buffer ───────
1848
2848
  var activityTimeline = new Map(); // sessionId → [{status, ts}]
@@ -1914,12 +2914,37 @@ function renderResourceRow(stats) {
1914
2914
  // ── Tab navigation ───────────────────────────
1915
2915
  document.querySelectorAll('.tab').forEach(function(tab) {
1916
2916
  tab.addEventListener('click', function() {
2917
+ var tabName = tab.dataset.tab;
1917
2918
  document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
1918
2919
  document.querySelectorAll('.tab-panel').forEach(function(p) { p.classList.remove('active'); });
1919
2920
  tab.classList.add('active');
1920
- document.getElementById('panel-' + tab.dataset.tab).classList.add('active');
1921
- if (tab.dataset.tab === 'sessions') loadSessions();
1922
- if (tab.dataset.tab === 'logs') loadLogs();
2921
+ document.getElementById('panel-' + tabName).classList.add('active');
2922
+ if (tabName === 'sessions') loadSessions();
2923
+ if (tabName === 'datasets') checkDatasetRestartNeeded();
2924
+ if (tabName === 'results') loadResults();
2925
+ if (tabName === 'jobs' && slurmEnabled) loadSlurmJobs();
2926
+ if (tabName === 'settings') {
2927
+ var activeSubTab = document.querySelector('.sub-tab.active');
2928
+ if (activeSubTab && activeSubTab.dataset.subtab === 'mcp') { setMcpTabActive(true); loadMcpServers(); }
2929
+ else if (activeSubTab && activeSubTab.dataset.subtab === 'audit') { loadLogs(); }
2930
+ else { setMcpTabActive(false); }
2931
+ } else {
2932
+ setMcpTabActive(false);
2933
+ }
2934
+ });
2935
+ });
2936
+
2937
+ // ── Sub-tab navigation (within Settings) ─────
2938
+ document.querySelectorAll('.sub-tab').forEach(function(tab) {
2939
+ tab.addEventListener('click', function() {
2940
+ var subtabName = tab.dataset.subtab;
2941
+ document.querySelectorAll('.sub-tab').forEach(function(t) { t.classList.remove('active'); });
2942
+ document.querySelectorAll('.sub-panel').forEach(function(p) { p.classList.remove('active'); });
2943
+ tab.classList.add('active');
2944
+ document.getElementById('subpanel-' + subtabName).classList.add('active');
2945
+ setMcpTabActive(subtabName === 'mcp');
2946
+ if (subtabName === 'mcp') loadMcpServers();
2947
+ if (subtabName === 'audit') loadLogs();
1923
2948
  });
1924
2949
  });
1925
2950
 
@@ -1960,6 +2985,10 @@ function markDirty() {
1960
2985
  document.getElementById('saveBar').classList.toggle('visible', dirty);
1961
2986
  }
1962
2987
 
2988
+ function normalizeUiNetworkMode(mode) {
2989
+ return mode === 'filtered' ? 'filtered' : 'host';
2990
+ }
2991
+
1963
2992
  // ── Populate UI from config ──────────────────
1964
2993
  function populateUI() {
1965
2994
  document.getElementById('runtime').value = config.runtime || 'auto';
@@ -1968,7 +2997,9 @@ function populateUI() {
1968
2997
  ? config.session_timeout_hours
1969
2998
  : 8;
1970
2999
  document.getElementById('timeout').value = String(timeout);
1971
- document.getElementById('networkMode').value = config.network ? config.network.mode : 'none';
3000
+ if (!config.network) config.network = {};
3001
+ config.network.mode = normalizeUiNetworkMode(config.network.mode);
3002
+ document.getElementById('networkMode').value = config.network.mode;
1972
3003
  document.getElementById('auditEnabled').checked = config.audit ? config.audit.enabled : true;
1973
3004
  document.getElementById('logDir').value = config.audit ? config.audit.log_dir : '~/.labgate/logs';
1974
3005
 
@@ -1986,7 +3017,7 @@ function collectConfig() {
1986
3017
  var parsedTimeout = parseFloat(document.getElementById('timeout').value);
1987
3018
  config.session_timeout_hours = Number.isFinite(parsedTimeout) ? parsedTimeout : 8;
1988
3019
  if (!config.network) config.network = {};
1989
- config.network.mode = document.getElementById('networkMode').value;
3020
+ config.network.mode = normalizeUiNetworkMode(document.getElementById('networkMode').value);
1990
3021
  if (!config.audit) config.audit = {};
1991
3022
  config.audit.enabled = document.getElementById('auditEnabled').checked;
1992
3023
  config.audit.log_dir = document.getElementById('logDir').value;
@@ -2112,22 +3143,60 @@ function removeMount(i) {
2112
3143
  function renderDatasets() {
2113
3144
  var container = document.getElementById('datasetsList');
2114
3145
  var datasets = config.datasets || [];
3146
+ var dbIcon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">'
3147
+ + '<ellipse cx="12" cy="5" rx="9" ry="3"></ellipse>'
3148
+ + '<path d="M21 12c0 1.66-4.03 3-9 3s-9-1.34-9-3"></path>'
3149
+ + '<path d="M3 5v14c0 1.66 4.03 3 9 3s9-1.34 9-3V5"></path>'
3150
+ + '</svg>';
3151
+ var arrowIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>';
3152
+ // Toggle notice visibility and count badge
3153
+ var notice = document.getElementById('datasetNotice');
3154
+ if (notice) notice.style.display = datasets.length > 0 ? '' : 'none';
3155
+ var countEl = document.getElementById('datasetCount');
3156
+ if (countEl) {
3157
+ if (datasets.length > 0) {
3158
+ countEl.textContent = datasets.length + ' dataset' + (datasets.length > 1 ? 's' : '');
3159
+ countEl.style.display = '';
3160
+ } else {
3161
+ countEl.style.display = 'none';
3162
+ }
3163
+ }
2115
3164
  if (datasets.length === 0) {
2116
- container.innerHTML = '<div class="empty-state" style="padding:16px">No datasets configured</div>';
3165
+ container.innerHTML = '<div class="dataset-empty">'
3166
+ + '<div class="dataset-empty-icon">' + dbIcon + '</div>'
3167
+ + '<div class="dataset-empty-title">No datasets configured</div>'
3168
+ + '<div class="dataset-empty-subtitle">Add a host directory below to make it available as a named dataset in the sandbox.</div>'
3169
+ + '</div>';
2117
3170
  return;
2118
3171
  }
2119
- container.innerHTML = datasets.map(function(ds, i) {
2120
- var desc = ds.description ? '<span class="dataset-desc" title="' + escapeHtml(ds.description) + '">' + escapeHtml(ds.description) + '</span>' : '';
2121
- return '<div class="dataset-item">'
3172
+ container.innerHTML = '<div class="dataset-grid">' + datasets.map(function(ds, i) {
3173
+ var descHtml = ds.description
3174
+ ? '<div class="dataset-card-desc">' + escapeHtml(ds.description) + '</div>'
3175
+ : '';
3176
+ return '<div class="dataset-card">'
3177
+ + '<div class="dataset-card-header">'
3178
+ + '<div class="dataset-card-icon">' + dbIcon + '</div>'
3179
+ + '<div class="dataset-card-title">'
2122
3180
  + '<span class="dataset-name">' + escapeHtml(ds.name) + '</span>'
2123
- + '<span class="dataset-path">' + escapeHtml(ds.path) + '</span>'
2124
- + desc
2125
3181
  + '<span class="mount-mode ' + ds.mode + '">' + ds.mode + '</span>'
2126
- + '<button class="remove-btn" data-index="' + i + '">&times;</button>'
3182
+ + '</div>'
3183
+ + '<button class="remove-btn" data-index="' + i + '" title="Remove dataset">&times;</button>'
3184
+ + '</div>'
3185
+ + descHtml
3186
+ + '<div class="dataset-card-paths">'
3187
+ + '<span class="dataset-path">' + escapeHtml(ds.path) + '</span>'
3188
+ + '<span class="dataset-path-arrow">' + arrowIcon + '</span>'
3189
+ + '<span class="dataset-path dataset-path-container">/datasets/' + escapeHtml(ds.name) + '</span>'
3190
+ + '</div>'
2127
3191
  + '</div>';
2128
- }).join('');
3192
+ }).join('') + '</div>';
2129
3193
  container.querySelectorAll('.remove-btn').forEach(function(btn) {
2130
- btn.addEventListener('click', function() { removeDataset(parseInt(btn.dataset.index)); });
3194
+ btn.addEventListener('click', function() {
3195
+ var idx = parseInt(btn.dataset.index);
3196
+ var ds = config.datasets[idx];
3197
+ if (!confirm('Remove dataset "' + ds.name + '"?')) return;
3198
+ removeDataset(idx);
3199
+ });
2131
3200
  });
2132
3201
  }
2133
3202
 
@@ -2135,7 +3204,6 @@ function addDataset() {
2135
3204
  var pathInput = document.getElementById('datasetPathInput');
2136
3205
  var nameInput = document.getElementById('datasetNameInput');
2137
3206
  var descInput = document.getElementById('datasetDescInput');
2138
- var modeInput = document.getElementById('datasetModeInput');
2139
3207
 
2140
3208
  var path = pathInput.value.trim();
2141
3209
  if (!path) { showToast('Dataset path is required', 'error'); return; }
@@ -2152,7 +3220,8 @@ function addDataset() {
2152
3220
  return;
2153
3221
  }
2154
3222
 
2155
- var mode = modeInput.value;
3223
+ var modeEl = document.querySelector('input[name="datasetMode"]:checked');
3224
+ var mode = modeEl ? modeEl.value : 'ro';
2156
3225
  var desc = descInput.value.trim();
2157
3226
 
2158
3227
  // Preflight path validation (advisory — still adds even if path not found)
@@ -2189,8 +3258,10 @@ function doAddDataset(path, name, desc, mode) {
2189
3258
  document.getElementById('datasetPathInput').value = '';
2190
3259
  document.getElementById('datasetNameInput').value = '';
2191
3260
  document.getElementById('datasetDescInput').value = '';
3261
+ document.getElementById('datasetModeRO').checked = true;
2192
3262
  renderDatasets();
2193
3263
  markDirty();
3264
+ showToast('Dataset "' + name + '" added', 'success');
2194
3265
  }
2195
3266
 
2196
3267
  function removeDataset(i) {
@@ -2200,48 +3271,399 @@ function removeDataset(i) {
2200
3271
  markDirty();
2201
3272
  }
2202
3273
 
3274
+ // ── Results ─────────────────────────────
3275
+ function parseCsvList(value) {
3276
+ if (!value) return [];
3277
+ return value.split(',').map(function(s) { return s.trim(); }).filter(Boolean);
3278
+ }
3279
+
3280
+ function formatResultTime(iso) {
3281
+ if (!iso) return '-';
3282
+ try {
3283
+ return new Date(iso).toLocaleString();
3284
+ } catch {
3285
+ return iso;
3286
+ }
3287
+ }
3288
+
3289
+ function getResultById(id) {
3290
+ if (!id) return null;
3291
+ return resultsCache.find(function(r) { return r.id === id; }) || null;
3292
+ }
3293
+
3294
+ // Result card icons
3295
+ var _resultIcon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 12l2 2 4-4"/><circle cx="12" cy="12" r="10"/></svg>';
3296
+ var _clockIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>';
3297
+ var _folderIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
3298
+ var _linkIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg>';
3299
+ var _fileIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>';
3300
+ var _editIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>';
3301
+ var _trashIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
3302
+
3303
+ function resultSourceClass(source) {
3304
+ var s = (source || '').toLowerCase();
3305
+ if (s === 'claude') return 'source-claude';
3306
+ if (s === 'codex') return 'source-codex';
3307
+ return 'source-default';
3308
+ }
3309
+
3310
+ function formatRelativeTime(iso) {
3311
+ if (!iso) return '';
3312
+ try {
3313
+ var diff = Date.now() - new Date(iso).getTime();
3314
+ var mins = Math.floor(diff / 60000);
3315
+ if (mins < 1) return 'just now';
3316
+ if (mins < 60) return mins + 'm ago';
3317
+ var hours = Math.floor(mins / 60);
3318
+ if (hours < 24) return hours + 'h ago';
3319
+ var days = Math.floor(hours / 24);
3320
+ if (days < 30) return days + 'd ago';
3321
+ return new Date(iso).toLocaleDateString();
3322
+ } catch(e) {
3323
+ return iso;
3324
+ }
3325
+ }
3326
+
3327
+ function toggleResultDetails(toggleEl, contentId) {
3328
+ var content = document.getElementById(contentId);
3329
+ if (!content) return;
3330
+ var isVisible = content.classList.contains('visible');
3331
+ if (isVisible) {
3332
+ content.classList.remove('visible');
3333
+ toggleEl.classList.remove('expanded');
3334
+ toggleEl.querySelector('span:last-child').textContent = 'Show details';
3335
+ } else {
3336
+ content.classList.add('visible');
3337
+ toggleEl.classList.add('expanded');
3338
+ toggleEl.querySelector('span:last-child').textContent = 'Hide details';
3339
+ }
3340
+ }
3341
+
3342
+ function renderResults(data) {
3343
+ var container = document.getElementById('resultsContent');
3344
+ var rows = (data && Array.isArray(data.results)) ? data.results : [];
3345
+ resultsCache = rows.slice();
3346
+
3347
+ if (rows.length === 0) {
3348
+ container.innerHTML = '<div class="results-empty">'
3349
+ + '<div class="results-empty-icon">' + _resultIcon + '</div>'
3350
+ + '<div class="results-empty-title">No results recorded yet</div>'
3351
+ + '<div class="results-empty-subtitle">Use the MCP <code>register_result</code> tool or click "+ Add Result" to create a structured result entry.</div>'
3352
+ + '</div>';
3353
+ return;
3354
+ }
3355
+
3356
+ var html = '<div class="results-grid">';
3357
+ rows.forEach(function(r) {
3358
+ var tags = Array.isArray(r.tags) ? r.tags : [];
3359
+ var artifacts = Array.isArray(r.artifacts) ? r.artifacts : [];
3360
+ var hasDetails = !!(r.details && r.details.trim());
3361
+
3362
+ html += '<div class="result-card">';
3363
+
3364
+ // Header: icon + title + source badge + actions
3365
+ html += '<div class="result-card-header">';
3366
+ html += '<div class="result-card-icon">' + _resultIcon + '</div>';
3367
+ html += '<div class="result-card-title">';
3368
+ html += '<span class="result-name">' + escapeHtml(r.title || '(untitled)') + '</span>';
3369
+ html += '<span class="result-source-badge ' + resultSourceClass(r.source) + '">' + escapeHtml(r.source || 'unknown') + '</span>';
3370
+ html += '</div>';
3371
+ html += '<div class="result-card-actions">';
3372
+ html += '<button title="Edit" onclick="openResultEditor(\'' + escapeHtml(r.id) + '\')">' + _editIcon + '</button>';
3373
+ html += '<button class="danger" title="Delete" onclick="deleteResult(\'' + escapeHtml(r.id) + '\')">' + _trashIcon + '</button>';
3374
+ html += '</div>';
3375
+ html += '</div>';
3376
+
3377
+ // Summary
3378
+ if (r.summary) {
3379
+ html += '<div class="result-card-summary">' + escapeHtml(r.summary) + '</div>';
3380
+ }
3381
+
3382
+ // Tags
3383
+ if (tags.length > 0) {
3384
+ html += '<div class="result-card-tags">';
3385
+ tags.forEach(function(tag) {
3386
+ html += '<span class="result-tag-pill">' + escapeHtml(tag) + '</span>';
3387
+ });
3388
+ html += '</div>';
3389
+ }
3390
+
3391
+ // Metadata row
3392
+ var metaItems = [];
3393
+ metaItems.push('<span class="result-meta-item">' + _clockIcon + '<span title="' + escapeHtml(r.updated_at || '') + '">' + escapeHtml(formatRelativeTime(r.updated_at)) + '</span></span>');
3394
+ if (r.session_id) {
3395
+ metaItems.push('<span class="result-meta-item">' + _linkIcon + '<span>' + escapeHtml(r.session_id.slice(0, 8)) + '</span></span>');
3396
+ }
3397
+ if (r.workdir) {
3398
+ metaItems.push('<span class="result-meta-item">' + _folderIcon + '<span title="' + escapeHtml(r.workdir) + '">' + escapeHtml(r.workdir) + '</span></span>');
3399
+ }
3400
+ if (artifacts.length > 0) {
3401
+ metaItems.push('<span class="result-meta-item">' + _fileIcon + '<span>' + artifacts.length + ' artifact' + (artifacts.length !== 1 ? 's' : '') + '</span></span>');
3402
+ }
3403
+ html += '<div class="result-card-meta">' + metaItems.join('') + '</div>';
3404
+
3405
+ // Details toggle
3406
+ if (hasDetails) {
3407
+ var detailsId = 'result-details-' + r.id.replace(/[^a-zA-Z0-9-]/g, '');
3408
+ html += '<div class="result-card-details-toggle" onclick="toggleResultDetails(this, \'' + detailsId + '\')">';
3409
+ html += '<span class="chevron">&#9654;</span>';
3410
+ html += '<span>Show details</span>';
3411
+ html += '</div>';
3412
+ html += '<div class="result-card-details-content" id="' + detailsId + '">' + escapeHtml(r.details) + '</div>';
3413
+ }
3414
+
3415
+ html += '</div>';
3416
+ });
3417
+ html += '</div>';
3418
+ container.innerHTML = html;
3419
+ }
3420
+
3421
+ function loadResults() {
3422
+ var container = document.getElementById('resultsContent');
3423
+ if (container) container.innerHTML = '<div class="empty-state">Loading...</div>';
3424
+
3425
+ var search = (document.getElementById('resultsSearchInput').value || '').trim();
3426
+ var tag = (document.getElementById('resultsTagInput').value || '').trim();
3427
+ var source = (document.getElementById('resultsSourceInput').value || '').trim();
3428
+
3429
+ var url = '/api/results?limit=200';
3430
+ if (search) url += '&search=' + encodeURIComponent(search);
3431
+ if (tag) url += '&tag=' + encodeURIComponent(tag);
3432
+ if (source) url += '&source=' + encodeURIComponent(source);
3433
+
3434
+ fetch(url).then(function(r) { return r.json(); }).then(function(data) {
3435
+ if (!data.ok) {
3436
+ container.innerHTML = '<div class="empty-state">Could not load results</div>';
3437
+ return;
3438
+ }
3439
+ renderResults(data);
3440
+ }).catch(function(err) {
3441
+ container.innerHTML = '<div class="empty-state">Error loading results: ' + escapeHtml(err.message) + '</div>';
3442
+ });
3443
+ }
3444
+
3445
+ function debounceResultsSearch() {
3446
+ if (resultsSearchTimeout) clearTimeout(resultsSearchTimeout);
3447
+ resultsSearchTimeout = setTimeout(loadResults, 250);
3448
+ }
3449
+
3450
+ function fillResultEditor(result) {
3451
+ document.getElementById('resultTitleInput').value = (result && result.title) || '';
3452
+ document.getElementById('resultSummaryInput').value = (result && result.summary) || '';
3453
+ document.getElementById('resultDetailsInput').value = (result && result.details) || '';
3454
+ document.getElementById('resultSourceInputModal').value = (result && result.source) || 'claude';
3455
+ document.getElementById('resultSessionIdInput').value = (result && result.session_id) || '';
3456
+ document.getElementById('resultWorkdirInput').value = (result && result.workdir) || '';
3457
+ document.getElementById('resultTagsInput').value = (result && Array.isArray(result.tags)) ? result.tags.join(', ') : '';
3458
+ document.getElementById('resultArtifactsInput').value = (result && Array.isArray(result.artifacts)) ? result.artifacts.join(', ') : '';
3459
+ }
3460
+
3461
+ function openResultEditor(id) {
3462
+ resultEditorId = id || null;
3463
+ var modal = document.getElementById('resultsModal');
3464
+ if (!modal) return;
3465
+
3466
+ var titleEl = document.getElementById('resultEditorTitle');
3467
+ var subtitleEl = document.getElementById('resultEditorSubtitle');
3468
+ if (resultEditorId) {
3469
+ var result = getResultById(resultEditorId);
3470
+ fillResultEditor(result);
3471
+ titleEl.textContent = 'Edit Result';
3472
+ subtitleEl.textContent = result ? ('Result ' + result.id) : ('Result ' + resultEditorId);
3473
+ } else {
3474
+ fillResultEditor(null);
3475
+ titleEl.textContent = 'Add Result';
3476
+ subtitleEl.textContent = 'Create a new structured result entry';
3477
+ }
3478
+
3479
+ modal.classList.add('visible');
3480
+ document.body.style.overflow = 'hidden';
3481
+ document.getElementById('resultTitleInput').focus();
3482
+ }
3483
+
3484
+ function closeResultEditor() {
3485
+ var modal = document.getElementById('resultsModal');
3486
+ if (!modal) return;
3487
+ modal.classList.remove('visible');
3488
+ document.body.style.overflow = '';
3489
+ resultEditorId = null;
3490
+ }
3491
+
3492
+ function buildResultEditorPayload() {
3493
+ var title = (document.getElementById('resultTitleInput').value || '').trim();
3494
+ if (!title) {
3495
+ showToast('Result title is required.', 'error');
3496
+ return null;
3497
+ }
3498
+ var summary = (document.getElementById('resultSummaryInput').value || '').trim();
3499
+ var details = (document.getElementById('resultDetailsInput').value || '').trim();
3500
+ var source = (document.getElementById('resultSourceInputModal').value || '').trim() || 'claude';
3501
+ var sessionId = (document.getElementById('resultSessionIdInput').value || '').trim();
3502
+ var workdir = (document.getElementById('resultWorkdirInput').value || '').trim();
3503
+ var tags = parseCsvList(document.getElementById('resultTagsInput').value || '');
3504
+ var artifacts = parseCsvList(document.getElementById('resultArtifactsInput').value || '');
3505
+
3506
+ return {
3507
+ title: title,
3508
+ summary: summary,
3509
+ details: details || null,
3510
+ source: source,
3511
+ session_id: sessionId || null,
3512
+ workdir: workdir || null,
3513
+ tags: tags,
3514
+ artifacts: artifacts,
3515
+ };
3516
+ }
3517
+
3518
+ function saveResultEditor() {
3519
+ var payload = buildResultEditorPayload();
3520
+ if (!payload) return;
3521
+
3522
+ var isUpdate = !!resultEditorId;
3523
+ var endpoint = isUpdate ? '/api/results/' + encodeURIComponent(resultEditorId) : '/api/results';
3524
+ var method = isUpdate ? 'PUT' : 'POST';
3525
+
3526
+ fetch(endpoint, {
3527
+ method: method,
3528
+ headers: apiWriteHeaders(),
3529
+ body: JSON.stringify(payload)
3530
+ }).then(parseApiResponse).then(function(resp) {
3531
+ if (!resp.ok || !resp.data || !resp.data.ok) {
3532
+ throw new Error((resp.data && resp.data.error) || ('HTTP ' + resp.status));
3533
+ }
3534
+ showToast(isUpdate ? 'Result updated' : 'Result created', 'success');
3535
+ closeResultEditor();
3536
+ loadResults();
3537
+ }).catch(function(err) {
3538
+ showToast('Save failed: ' + err.message, 'error');
3539
+ });
3540
+ }
3541
+
3542
+ function deleteResult(id) {
3543
+ if (!id) return;
3544
+ if (!confirm('Delete result ' + id + '?')) return;
3545
+ fetch('/api/results/' + encodeURIComponent(id), {
3546
+ method: 'DELETE',
3547
+ headers: apiWriteHeaders(),
3548
+ }).then(parseApiResponse).then(function(resp) {
3549
+ if (!resp.ok || !resp.data || !resp.data.ok) {
3550
+ throw new Error((resp.data && resp.data.error) || ('HTTP ' + resp.status));
3551
+ }
3552
+ showToast('Result deleted', 'success');
3553
+ loadResults();
3554
+ }).catch(function(err) {
3555
+ showToast('Delete failed: ' + err.message, 'error');
3556
+ });
3557
+ }
3558
+
3559
+ // ── Dataset restart helpers ─────────────────
3560
+ function checkDatasetRestartNeeded() {
3561
+ fetch('/api/sessions').then(function(r) { return r.json(); }).then(function(data) {
3562
+ var sessions = data.sessions || [];
3563
+ var needsRestart = sessions.filter(function(s) { return s.restartRequired; });
3564
+ var btn = document.getElementById('datasetRestartBtn');
3565
+ var notice = document.getElementById('datasetNotice');
3566
+ var text = notice ? notice.querySelector('.dataset-notice-text') : null;
3567
+ if (needsRestart.length > 0) {
3568
+ if (text) text.textContent = needsRestart.length + ' active session' + (needsRestart.length > 1 ? 's' : '') + ' running with outdated config. Restart them now to apply dataset changes.';
3569
+ if (btn) { btn.style.display = ''; btn.disabled = false; btn.textContent = 'Restart Sessions Now'; }
3570
+ } else if (sessions.length > 0) {
3571
+ if (text) text.textContent = 'Dataset changes are saved. Active sessions keep old mounts until you restart them.';
3572
+ if (btn) btn.style.display = 'none';
3573
+ } else {
3574
+ if (text) text.textContent = 'No active sessions. Dataset changes will be used the next time you start a session.';
3575
+ if (btn) btn.style.display = 'none';
3576
+ }
3577
+ }).catch(function() {});
3578
+ }
3579
+
3580
+ function restartOutdatedSessions() {
3581
+ var btn = document.getElementById('datasetRestartBtn');
3582
+ if (btn) { btn.disabled = true; btn.textContent = 'Checking...'; }
3583
+
3584
+ fetch('/api/sessions').then(function(r) { return r.json(); }).then(function(data) {
3585
+ var sessions = data.sessions || [];
3586
+ var needsRestart = sessions.filter(function(s) { return s.restartRequired; });
3587
+ if (needsRestart.length === 0) {
3588
+ showToast('No sessions need restarting', 'success');
3589
+ if (btn) { btn.style.display = 'none'; }
3590
+ return;
3591
+ }
3592
+
3593
+ var msg = 'Restarting will stop and relaunch ' + needsRestart.length + ' active session' + (needsRestart.length > 1 ? 's' : '')
3594
+ + ' with the latest config. Unsaved work inside those sessions may be lost. Continue?';
3595
+ if (!confirm(msg)) {
3596
+ if (btn) {
3597
+ btn.disabled = false;
3598
+ btn.textContent = 'Restart Sessions Now';
3599
+ }
3600
+ return;
3601
+ }
3602
+ if (btn) { btn.disabled = true; btn.textContent = 'Restarting...'; }
3603
+
3604
+ var restartPromises = needsRestart.map(function(s) {
3605
+ return fetch('/api/sessions/restart', {
3606
+ method: 'POST',
3607
+ headers: apiWriteHeaders(),
3608
+ body: JSON.stringify({ id: s.id })
3609
+ }).then(parseApiResponse).then(function(resp) {
3610
+ var data = resp.data || {};
3611
+ return { ok: !!(resp.ok && data.ok), error: data.error || ('HTTP ' + resp.status) };
3612
+ });
3613
+ });
3614
+
3615
+ Promise.all(restartPromises).then(function(results) {
3616
+ var restarted = results.filter(function(r) { return r.ok; }).length;
3617
+ var failed = results.length - restarted;
3618
+ if (failed > 0) {
3619
+ showToast(
3620
+ restarted + ' session' + (restarted !== 1 ? 's' : '') + ' restarted, '
3621
+ + failed + ' failed.',
3622
+ 'error'
3623
+ );
3624
+ } else {
3625
+ showToast(restarted + ' session' + (restarted !== 1 ? 's' : '') + ' restarted with fresh config.', 'success');
3626
+ }
3627
+ if (btn) { btn.textContent = failed > 0 ? 'Retry Restart' : 'Restarted'; }
3628
+ loadSessions();
3629
+ setTimeout(function() { checkDatasetRestartNeeded(); }, 1200);
3630
+ }).catch(function(err) {
3631
+ showToast('Failed to restart sessions: ' + err.message, 'error');
3632
+ if (btn) { btn.disabled = false; btn.textContent = 'Restart Sessions Now'; }
3633
+ });
3634
+ }).catch(function(err) {
3635
+ showToast('Failed to fetch sessions: ' + err.message, 'error');
3636
+ if (btn) { btn.disabled = false; btn.textContent = 'Restart Sessions Now'; }
3637
+ });
3638
+ }
3639
+
2203
3640
  // ── Restart from UI ─────────────────────────
2204
- function restartFromUI(id, agent, workdir, btn) {
2205
- if (!confirm('Stop session ' + id.slice(0, 8) + ' and copy restart command?')) return;
3641
+ function restartFromUI(id, btn) {
3642
+ if (!confirm('Restart session ' + id.slice(0, 8) + ' now? Unsaved work in that session may be lost.')) return;
2206
3643
  btn.disabled = true;
2207
- btn.textContent = 'Stopping...';
2208
- fetch('/api/sessions/stop', {
3644
+ btn.textContent = 'Restarting...';
3645
+ fetch('/api/sessions/restart', {
2209
3646
  method: 'POST',
2210
3647
  headers: apiWriteHeaders(),
2211
3648
  body: JSON.stringify({ id: id })
2212
- }).then(function(r) { return r.json(); }).then(function(data) {
2213
- if (data.ok) {
2214
- var cmd = 'labgate ' + agent + ' ' + workdir;
2215
- copyToClipboard(cmd);
2216
- showToast('Session stopped. Restart command copied: ' + cmd, 'success');
3649
+ }).then(parseApiResponse).then(function(resp) {
3650
+ var data = resp.data || {};
3651
+ if (resp.ok && data.ok) {
3652
+ showToast('Session restarted with fresh config', 'success');
3653
+ loadSessions();
3654
+ checkDatasetRestartNeeded();
2217
3655
  } else {
2218
- showToast('Stop failed: ' + (data.error || 'unknown'), 'error');
3656
+ showToast('Restart failed: ' + (data.error || ('HTTP ' + resp.status)), 'error');
2219
3657
  btn.disabled = false;
2220
- btn.textContent = 'Stop & Copy Command';
3658
+ btn.textContent = 'Restart Now';
2221
3659
  }
2222
3660
  }).catch(function(err) {
2223
- showToast('Stop failed: ' + err.message, 'error');
3661
+ showToast('Restart failed: ' + err.message, 'error');
2224
3662
  btn.disabled = false;
2225
- btn.textContent = 'Stop & Copy Command';
3663
+ btn.textContent = 'Restart Now';
2226
3664
  });
2227
3665
  }
2228
3666
 
2229
- function copyToClipboard(text) {
2230
- try {
2231
- navigator.clipboard.writeText(text);
2232
- } catch(e) {
2233
- // Fallback for non-HTTPS or older browsers
2234
- var ta = document.createElement('textarea');
2235
- ta.value = text;
2236
- ta.style.position = 'fixed';
2237
- ta.style.left = '-9999px';
2238
- document.body.appendChild(ta);
2239
- ta.select();
2240
- try { document.execCommand('copy'); } catch(e2) {}
2241
- document.body.removeChild(ta);
2242
- }
2243
- }
2244
-
2245
3667
  // ── Change listeners on simple fields ────────
2246
3668
  ['runtime', 'image', 'timeout', 'networkMode', 'auditEnabled', 'logDir'].forEach(function(id) {
2247
3669
  var el = document.getElementById(id);
@@ -2255,7 +3677,9 @@ function copyToClipboard(text) {
2255
3677
  function loadConfig() {
2256
3678
  fetch('/api/config').then(function(r) { return r.json(); }).then(function(data) {
2257
3679
  config = data;
2258
- originalConfig = JSON.stringify(data);
3680
+ if (!config.network) config.network = {};
3681
+ config.network.mode = normalizeUiNetworkMode(config.network.mode);
3682
+ originalConfig = JSON.stringify(config);
2259
3683
  populateUI();
2260
3684
  updateNetSwitcher();
2261
3685
  }).catch(function(err) {
@@ -2276,6 +3700,7 @@ function saveConfig() {
2276
3700
  dirty = false;
2277
3701
  document.getElementById('saveBar').classList.remove('visible');
2278
3702
  showToast('Config saved', 'success');
3703
+ checkDatasetRestartNeeded();
2279
3704
  } else {
2280
3705
  var msg = data.error || (data.errors || []).join(', ') || 'unknown error';
2281
3706
  showToast('Save failed: ' + msg, 'error');
@@ -2300,7 +3725,7 @@ function loadConfigPath() {
2300
3725
 
2301
3726
  // ── Network mode switcher ────────────────────
2302
3727
  function updateNetSwitcher() {
2303
- var mode = config.network ? config.network.mode : 'none';
3728
+ var mode = normalizeUiNetworkMode(config.network ? config.network.mode : 'host');
2304
3729
  document.querySelectorAll('.net-mode-pill').forEach(function(pill) {
2305
3730
  var m = pill.dataset.mode;
2306
3731
  pill.className = 'net-mode-pill' + (m === mode ? ' active-' + m : '');
@@ -2308,6 +3733,7 @@ function updateNetSwitcher() {
2308
3733
  }
2309
3734
 
2310
3735
  function switchNetMode(mode) {
3736
+ mode = normalizeUiNetworkMode(mode);
2311
3737
  collectConfig();
2312
3738
  config.network.mode = mode;
2313
3739
  updateNetSwitcher();
@@ -2427,7 +3853,7 @@ function renderSessions(data) {
2427
3853
  var dotClass = actStatus !== 'unknown' ? actStatus : (s.status || 'running');
2428
3854
  var dotLabel = activity.label || s.status || 'running';
2429
3855
  var cardClass = 'session-card' + (actStatus === 'waiting' ? ' waiting' : '');
2430
- html += '<div class="' + cardClass + '">';
3856
+ html += '<div class="' + cardClass + '" onclick="onSessionCardClick(event, \'' + escapeHtml(s.id || '') + '\')" style="cursor:pointer">';
2431
3857
  html += '<div class="session-card-header">';
2432
3858
  html += '<span class="badge ' + agentBadgeClass(agent) + '">' + escapeHtml(agent) + '</span>';
2433
3859
  html += '<span class="session-id">' + escapeHtml(shortId) + '</span>';
@@ -2446,7 +3872,7 @@ function renderSessions(data) {
2446
3872
  html += '<span class="restart-banner-icon">&#9888;</span>';
2447
3873
  html += '<span class="restart-banner-text">' + escapeHtml(reasons) + '. Restart to apply. ';
2448
3874
  html += '<code>labgate restart ' + escapeHtml(shortId) + '</code></span>';
2449
- html += '<button class="btn-restart-stop" onclick="restartFromUI(\'' + escapeHtml(s.id || '') + '\', \'' + escapeHtml(agent) + '\', \'' + escapeHtml(s.workdir || '') + '\', this)">Stop &amp; Copy Command</button>';
3875
+ html += '<button class="btn-restart-stop" onclick="restartFromUI(\'' + escapeHtml(s.id || '') + '\', this)">Restart Now</button>';
2450
3876
  html += '</div>';
2451
3877
  }
2452
3878
  // Sparkline timeline + resource usage
@@ -2668,19 +4094,6 @@ function stopSession(id, btn) {
2668
4094
 
2669
4095
  // ── Security stats rendering ─────────────────
2670
4096
  function renderSecurity(data) {
2671
- document.getElementById('blockedCountValue').textContent = data.blockedCount || '0';
2672
- document.getElementById('blacklistCountValue').textContent = data.protection ? data.protection.blacklistedCommands : '-';
2673
- document.getElementById('patternsCountValue').textContent = data.protection ? data.protection.blockedPatterns : '-';
2674
- document.getElementById('netModeValue').textContent = data.protection ? data.protection.networkMode : '-';
2675
-
2676
- var statEl = document.getElementById('secStatBlocked');
2677
- if (data.blockedCount > 0) {
2678
- statEl.classList.add('has-blocked');
2679
- } else {
2680
- statEl.classList.remove('has-blocked');
2681
- }
2682
-
2683
- // Render blocked events list
2684
4097
  var listEl = document.getElementById('blockedEventsList');
2685
4098
  var cmds = data.blockedCommands || [];
2686
4099
  if (cmds.length === 0) {
@@ -2725,14 +4138,16 @@ function connectSSE() {
2725
4138
  try {
2726
4139
  var d = JSON.parse(e.data);
2727
4140
  if (d.stats) updateSlurmStats(d.stats);
2728
- // Show the SLURM tab if not yet visible
4141
+ // Show SLURM content if not yet visible
2729
4142
  if (!slurmEnabled && d.stats && d.stats.total > 0) {
2730
4143
  slurmEnabled = true;
2731
- var tab = document.getElementById('slurmTab');
2732
- if (tab) tab.style.display = '';
4144
+ var emptyEl = document.getElementById('jobsEmptyState');
4145
+ var contentEl = document.getElementById('jobsSlurmContent');
4146
+ if (emptyEl) emptyEl.style.display = 'none';
4147
+ if (contentEl) contentEl.style.display = '';
2733
4148
  }
2734
4149
  // Update table if panel is active
2735
- var panel = document.getElementById('panel-slurm');
4150
+ var panel = document.getElementById('panel-jobs');
2736
4151
  if (panel && panel.classList.contains('active') && d.jobs) {
2737
4152
  var el = document.getElementById('slurmJobsContent');
2738
4153
  if (d.jobs.length === 0) {
@@ -2819,6 +4234,76 @@ function getInstructionButtonLabel(agent) {
2819
4234
  return getDefaultInstructionFile(agent);
2820
4235
  }
2821
4236
 
4237
+ // ── Sidebar resize ─────────────────────────
4238
+ (function initSidebarResize() {
4239
+ var handle = document.getElementById('sidebarResizeHandle');
4240
+ if (!handle) return;
4241
+ var panel = handle.closest('.instructions-modal');
4242
+ var minWidth = 320;
4243
+ var maxWidthPct = 85;
4244
+ var dragging = false;
4245
+
4246
+ function onMouseDown(e) {
4247
+ e.preventDefault();
4248
+ e.stopPropagation();
4249
+ dragging = true;
4250
+ handle.classList.add('dragging');
4251
+ panel.classList.add('resizing');
4252
+ document.body.style.cursor = 'col-resize';
4253
+ document.body.style.userSelect = 'none';
4254
+ document.addEventListener('mousemove', onMouseMove);
4255
+ document.addEventListener('mouseup', onMouseUp);
4256
+ }
4257
+
4258
+ function onMouseMove(e) {
4259
+ if (!dragging) return;
4260
+ var vw = window.innerWidth;
4261
+ var newWidth = vw - e.clientX;
4262
+ if (newWidth < minWidth) newWidth = minWidth;
4263
+ var maxPx = vw * maxWidthPct / 100;
4264
+ if (newWidth > maxPx) newWidth = maxPx;
4265
+ panel.style.width = newWidth + 'px';
4266
+ }
4267
+
4268
+ function onMouseUp() {
4269
+ dragging = false;
4270
+ handle.classList.remove('dragging');
4271
+ panel.classList.remove('resizing');
4272
+ document.body.style.cursor = '';
4273
+ document.body.style.userSelect = '';
4274
+ document.removeEventListener('mousemove', onMouseMove);
4275
+ document.removeEventListener('mouseup', onMouseUp);
4276
+ }
4277
+
4278
+ handle.addEventListener('mousedown', onMouseDown);
4279
+
4280
+ // Touch support
4281
+ handle.addEventListener('touchstart', function(e) {
4282
+ e.preventDefault();
4283
+ dragging = true;
4284
+ handle.classList.add('dragging');
4285
+ panel.classList.add('resizing');
4286
+ }, { passive: false });
4287
+
4288
+ document.addEventListener('touchmove', function(e) {
4289
+ if (!dragging) return;
4290
+ var touch = e.touches[0];
4291
+ var vw = window.innerWidth;
4292
+ var newWidth = vw - touch.clientX;
4293
+ if (newWidth < minWidth) newWidth = minWidth;
4294
+ var maxPx = vw * maxWidthPct / 100;
4295
+ if (newWidth > maxPx) newWidth = maxPx;
4296
+ panel.style.width = newWidth + 'px';
4297
+ });
4298
+
4299
+ document.addEventListener('touchend', function() {
4300
+ if (!dragging) return;
4301
+ dragging = false;
4302
+ handle.classList.remove('dragging');
4303
+ panel.classList.remove('resizing');
4304
+ });
4305
+ })();
4306
+
2822
4307
  function formatInstructionTime(ms) {
2823
4308
  if (!ms || !Number.isFinite(ms)) return '';
2824
4309
  try { return new Date(ms).toLocaleString(); } catch { return ''; }
@@ -2862,6 +4347,16 @@ function renderManagedInstruction() {
2862
4347
  if (text) text.textContent = hasManaged ? instructionsState.managedText : '';
2863
4348
  }
2864
4349
 
4350
+ function onSessionCardClick(event, sessionId) {
4351
+ // Don't open sidebar if user clicked a button or link inside the card
4352
+ var t = event.target;
4353
+ while (t && t !== event.currentTarget) {
4354
+ if (t.tagName === 'BUTTON' || t.tagName === 'A') return;
4355
+ t = t.parentElement;
4356
+ }
4357
+ openInstructions(sessionId);
4358
+ }
4359
+
2865
4360
  function openInstructions(sessionId) {
2866
4361
  if (!sessionId) return;
2867
4362
  var modal = document.getElementById('instructionsModal');
@@ -3032,9 +4527,214 @@ function saveInstructionsFile() {
3032
4527
  }
3033
4528
 
3034
4529
  document.addEventListener('keydown', function(event) {
3035
- if (event.key === 'Escape') { closeInstructions(); closeSlurmOutput(); closeSlurmNotes(); }
4530
+ if (event.key === 'Escape') { closeInstructions(); closeSlurmOutput(); closeSlurmNotes(); closeResultEditor(); }
3036
4531
  });
3037
4532
 
4533
+ // ── MCP Servers ──────────────────────────────
4534
+
4535
+ function setMcpTabActive(active) {
4536
+ if (active) {
4537
+ if (!mcpAutoRefreshTimer) {
4538
+ mcpAutoRefreshTimer = setInterval(function() {
4539
+ loadMcpServers({ background: true, autoRepair: false });
4540
+ }, MCP_AUTO_REFRESH_MS);
4541
+ }
4542
+ } else if (mcpAutoRefreshTimer) {
4543
+ clearInterval(mcpAutoRefreshTimer);
4544
+ mcpAutoRefreshTimer = null;
4545
+ }
4546
+ }
4547
+
4548
+ function triggerMcpAutoRegister() {
4549
+ if (mcpAutoRegisterInFlight) return;
4550
+ mcpAutoRegisterInFlight = true;
4551
+ fetch('/api/mcp/reregister', {
4552
+ method: 'POST',
4553
+ headers: apiWriteHeaders(),
4554
+ body: '{}',
4555
+ }).then(parseApiResponse).then(function(resp) {
4556
+ if (!resp.ok || !resp.data || !resp.data.ok) {
4557
+ var msg = (resp.data && resp.data.error) || ('HTTP ' + resp.status);
4558
+ showToast('MCP auto-setup failed: ' + msg, 'error');
4559
+ return;
4560
+ }
4561
+ loadMcpServers({ background: true, autoRepair: false });
4562
+ }).catch(function(err) {
4563
+ showToast('MCP auto-setup failed: ' + err.message, 'error');
4564
+ }).finally(function() {
4565
+ mcpAutoRegisterInFlight = false;
4566
+ });
4567
+ }
4568
+
4569
+ function loadMcpServers(options) {
4570
+ var opts = options || {};
4571
+ var background = !!opts.background;
4572
+ var autoRepair = opts.autoRepair !== false;
4573
+ var el = document.getElementById('mcpContent');
4574
+ if (!background) {
4575
+ el.innerHTML = '<div class="empty-state">Loading...</div>';
4576
+ }
4577
+
4578
+ fetch('/api/mcp').then(function(r) { return r.json(); }).then(function(data) {
4579
+ if (!data.ok || !data.servers) {
4580
+ el.innerHTML = '<div class="empty-state">Could not load MCP server data</div>';
4581
+ return;
4582
+ }
4583
+
4584
+ // Update badge
4585
+ var badge = document.getElementById('mcpBadge');
4586
+ if (data.activeCount > 0) {
4587
+ badge.textContent = data.activeCount;
4588
+ badge.style.display = '';
4589
+ } else {
4590
+ badge.style.display = 'none';
4591
+ }
4592
+
4593
+ var activeServers = data.servers.filter(function(s) { return s.active; });
4594
+ var inactiveServers = data.servers.filter(function(s) { return !s.active; });
4595
+
4596
+ if (activeServers.length === 0 && inactiveServers.length === 0) {
4597
+ el.innerHTML = renderMcpEmptyState();
4598
+ return;
4599
+ }
4600
+
4601
+ var html = '';
4602
+ activeServers.forEach(function(s) { html += renderMcpServerCard(s); });
4603
+ inactiveServers.forEach(function(s) { html += renderMcpServerCard(s); });
4604
+ el.innerHTML = html;
4605
+
4606
+ if (autoRepair) {
4607
+ var needsRegistration = data.servers.some(function(s) {
4608
+ return !!s.configured && !s.registered;
4609
+ });
4610
+ if (needsRegistration) {
4611
+ triggerMcpAutoRegister();
4612
+ }
4613
+ }
4614
+ }).catch(function() {
4615
+ if (!background) {
4616
+ el.innerHTML = '<div class="empty-state">Error loading MCP servers</div>';
4617
+ }
4618
+ });
4619
+ }
4620
+
4621
+ function renderMcpEmptyState() {
4622
+ var iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg>';
4623
+ return '<div class="mcp-empty">' +
4624
+ '<div class="mcp-empty-icon">' + iconSvg + '</div>' +
4625
+ '<div class="mcp-empty-title">No MCP Servers</div>' +
4626
+ '<div class="mcp-empty-subtitle">MCP servers are automatically registered when you enable features like SLURM tracking or configure datasets. Start a session to activate them.</div>' +
4627
+ '</div>';
4628
+ }
4629
+
4630
+ function renderMcpServerCard(server) {
4631
+ var ready = !!server.ready;
4632
+ var configured = !!server.configured;
4633
+ var registered = !!server.registered;
4634
+ var statusClass = ready ? 'active' : (configured ? 'partial' : 'inactive');
4635
+ var statusLabel = ready ? 'Ready' : (configured ? 'Needs Setup' : 'Disabled');
4636
+
4637
+ var iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"></polyline><line x1="12" y1="19" x2="20" y2="19"></line></svg>';
4638
+
4639
+ var html = '<div class="mcp-server-card">';
4640
+ html += '<div class="mcp-server-header">';
4641
+ html += '<div class="mcp-server-icon">' + iconSvg + '</div>';
4642
+ html += '<span class="mcp-server-name">' + escapeHtml(server.name) + '</span>';
4643
+ html += '<span class="mcp-server-status ' + statusClass + '">' + statusLabel + '</span>';
4644
+ html += '</div>';
4645
+ html += '<div class="mcp-server-states">'
4646
+ + '<span class="mcp-state-pill ' + (configured ? 'on' : 'off') + '">Configured: ' + (configured ? 'yes' : 'no') + '</span>'
4647
+ + '<span class="mcp-state-pill ' + (registered ? 'on' : 'off') + '">Registered: ' + (registered ? 'yes' : 'no') + '</span>'
4648
+ + '<span class="mcp-state-pill ' + (ready ? 'on' : 'off') + '">Ready: ' + (ready ? 'yes' : 'no') + '</span>'
4649
+ + '</div>';
4650
+ var reason = mcpReasonText(server.reason);
4651
+ if (reason) {
4652
+ html += '<div class="mcp-server-reason">' + escapeHtml(reason) + '</div>';
4653
+ }
4654
+ html += '<div class="mcp-server-desc">' + escapeHtml(server.description) + '</div>';
4655
+
4656
+ if (server.command) {
4657
+ var cmdStr = server.command;
4658
+ if (server.args && server.args.length > 0) {
4659
+ cmdStr += ' ' + server.args.join(' ');
4660
+ }
4661
+ html += '<div class="mcp-server-command">' + escapeHtml(cmdStr) + '</div>';
4662
+ }
4663
+
4664
+ if (server.tools && server.tools.length > 0) {
4665
+ html += '<div class="mcp-tools-label">Commands (' + server.tools.length + ')</div>';
4666
+ html += '<div class="mcp-tool-list">';
4667
+ server.tools.forEach(function(tool) {
4668
+ var title = tool.title || tool.name;
4669
+ var example = mcpToolExample(tool.name);
4670
+ html += '<div class="mcp-tool-item">';
4671
+ html += '<div class="mcp-tool-main">';
4672
+ html += '<span class="mcp-tool-name">' + escapeHtml(tool.name) + '</span>';
4673
+ html += '<span class="mcp-tool-title">' + escapeHtml(title) + '</span>';
4674
+ html += '</div>';
4675
+ html += '<div class="mcp-tool-desc">' + escapeHtml(tool.description) + '</div>';
4676
+ if (example) {
4677
+ html += '<div class="mcp-tool-example">Try: ' + escapeHtml(example) + '</div>';
4678
+ }
4679
+ html += '</div>';
4680
+ });
4681
+ html += '</div>';
4682
+ }
4683
+
4684
+ html += '</div>';
4685
+ return html;
4686
+ }
4687
+
4688
+ function mcpToolExample(name) {
4689
+ var prompts = {
4690
+ get_cluster_partitions: 'Show cluster partitions and their limits.',
4691
+ get_partition_limits: 'Show detailed limits for partition gpu.',
4692
+ get_queue_pressure: 'Show queue pressure by partition.',
4693
+ find_module: 'Find available modules matching openmpi.',
4694
+ list_slurm_jobs: 'Show my running SLURM jobs.',
4695
+ get_slurm_job: 'Show details for SLURM job 12345.',
4696
+ get_slurm_output: 'Show the latest stdout for SLURM job 12345.',
4697
+ cancel_slurm_job: 'Cancel SLURM job 12345.',
4698
+ set_slurm_job_notes: 'Add a note to SLURM job 12345: GPU queue is congested.',
4699
+ list_datasets: 'List all datasets I can access.',
4700
+ inspect_dataset: 'Inspect the genomes dataset.',
4701
+ search_dataset: 'Search genomes for files matching "*.fa".',
4702
+ get_dataset_summary: 'Summarize the genomes dataset.',
4703
+ read_dataset_file: 'Read /README.md from the genomes dataset.',
4704
+ validate_dataset: 'Validate dataset path /data/genomes with name genomes in read-only mode.',
4705
+ register_dataset: 'Register /data/genomes as dataset genomes (read-only).',
4706
+ update_dataset: 'Update dataset genomes description to "Human reference genomes".',
4707
+ unregister_dataset: 'Remove dataset genomes.',
4708
+ list_results: 'List results tagged gpu from source claude.',
4709
+ register_result: 'Register result: GPU tuning reduced runtime by 22%.',
4710
+ get_result: 'Get result by id 123e4567-e89b-12d3-a456-426614174000.',
4711
+ update_result: 'Update result 123e... with a clearer summary.',
4712
+ delete_result: 'Delete result 123e...'
4713
+ };
4714
+ return prompts[name] || '';
4715
+ }
4716
+
4717
+ function mcpReasonText(reason) {
4718
+ if (!reason || reason === 'ready') return '';
4719
+ if (reason === 'disabled_in_config') return 'Disabled in config: enable the feature first.';
4720
+ if (reason === 'not_registered_yet') return 'Preparing registration. Open a session if this does not clear automatically.';
4721
+ if (reason === 'missing_command') return 'Invalid MCP entry: missing command.';
4722
+ if (reason === 'missing_bundle_or_script') return 'Registered command path is missing on disk.';
4723
+ if (reason === 'missing_dependency_better_sqlite3') return 'Missing runtime dependency: better-sqlite3 for SLURM MCP.';
4724
+ return 'Not ready: ' + reason;
4725
+ }
4726
+
4727
+ function initMcpBadge() {
4728
+ fetch('/api/mcp').then(function(r) { return r.json(); }).then(function(data) {
4729
+ if (!data.ok) return;
4730
+ var badge = document.getElementById('mcpBadge');
4731
+ if (data.activeCount > 0) {
4732
+ badge.textContent = data.activeCount;
4733
+ badge.style.display = '';
4734
+ }
4735
+ }).catch(function() {});
4736
+ }
4737
+
3038
4738
  // ── SLURM Jobs ───────────────────────────────
3039
4739
  var slurmEnabled = false;
3040
4740
  var slurmSearchTimeout = null;
@@ -3046,8 +4746,10 @@ function initSlurmTab() {
3046
4746
  fetch('/api/config').then(function(r) { return r.json(); }).then(function(cfg) {
3047
4747
  if (cfg && cfg.slurm && cfg.slurm.enabled) {
3048
4748
  slurmEnabled = true;
3049
- var tab = document.getElementById('slurmTab');
3050
- if (tab) tab.style.display = '';
4749
+ var emptyEl = document.getElementById('jobsEmptyState');
4750
+ var contentEl = document.getElementById('jobsSlurmContent');
4751
+ if (emptyEl) emptyEl.style.display = 'none';
4752
+ if (contentEl) contentEl.style.display = '';
3051
4753
  loadSlurmJobs();
3052
4754
  }
3053
4755
  }).catch(function() {});
@@ -3229,12 +4931,14 @@ function editSlurmNotes(jobId) {
3229
4931
  var job = _slurmJobsCache.find(function(j){ return j.job_id === jobId; });
3230
4932
  document.getElementById('slurmNotesSubtitle').textContent = 'Job ' + jobId + (job && job.name ? ' (' + job.name + ')' : '');
3231
4933
  document.getElementById('slurmNotesTextarea').value = (job && job.notes) || '';
3232
- document.getElementById('slurmNotesModal').style.display = 'flex';
4934
+ document.getElementById('slurmNotesModal').classList.add('visible');
4935
+ document.body.style.overflow = 'hidden';
3233
4936
  document.getElementById('slurmNotesTextarea').focus();
3234
4937
  }
3235
4938
 
3236
4939
  function closeSlurmNotes() {
3237
- document.getElementById('slurmNotesModal').style.display = 'none';
4940
+ document.getElementById('slurmNotesModal').classList.remove('visible');
4941
+ document.body.style.overflow = '';
3238
4942
  _notesJobId = null;
3239
4943
  }
3240
4944
 
@@ -3262,7 +4966,8 @@ function openSlurmOutput(jobId, stream) {
3262
4966
  document.getElementById('slurmOutStdout').classList.toggle('active', currentSlurmOutputStream === 'stdout');
3263
4967
  document.getElementById('slurmOutStderr').classList.toggle('active', currentSlurmOutputStream === 'stderr');
3264
4968
 
3265
- document.getElementById('slurmOutputModal').style.display = '';
4969
+ document.getElementById('slurmOutputModal').classList.add('visible');
4970
+ document.body.style.overflow = 'hidden';
3266
4971
  refreshSlurmOutput();
3267
4972
  }
3268
4973
 
@@ -3298,7 +5003,8 @@ function refreshSlurmOutput() {
3298
5003
  }
3299
5004
 
3300
5005
  function closeSlurmOutput() {
3301
- document.getElementById('slurmOutputModal').style.display = 'none';
5006
+ document.getElementById('slurmOutputModal').classList.remove('visible');
5007
+ document.body.style.overflow = '';
3302
5008
  currentSlurmOutputJobId = null;
3303
5009
  }
3304
5010
 
@@ -3370,7 +5076,16 @@ function applyFieldLocks(data) {
3370
5076
  var field = el.closest('.field');
3371
5077
  if (field) field.classList.add('field-locked');
3372
5078
  el.disabled = true;
3373
- el.title = 'Managed by your institution\'s admin';
5079
+ el.title = 'Set by admin';
5080
+ if (field) {
5081
+ var label = field.querySelector('label');
5082
+ if (label && !label.querySelector('.set-by-admin-note')) {
5083
+ var note = document.createElement('span');
5084
+ note.className = 'set-by-admin-note';
5085
+ note.textContent = 'Set by admin';
5086
+ label.appendChild(note);
5087
+ }
5088
+ }
3374
5089
  });
3375
5090
 
3376
5091
  // Lock network mode pills if network.mode is locked
@@ -3462,6 +5177,7 @@ function initAdminTabs(bootstrapOnly) {
3462
5177
  document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
3463
5178
  document.querySelectorAll('.tab-panel').forEach(function(p) { p.classList.remove('active'); });
3464
5179
  btn.classList.add('active');
5180
+ setMcpTabActive(false);
3465
5181
  var panel = document.getElementById('panel-admin-' + name.toLowerCase());
3466
5182
  if (panel) { panel.style.display = ''; panel.classList.add('active'); }
3467
5183
  if (name === 'Policy') loadAdminPolicy();
@@ -3577,6 +5293,7 @@ loadConfig();
3577
5293
  loadConfigPath();
3578
5294
  loadSessions();
3579
5295
  loadSecurity();
5296
+ initMcpBadge();
3580
5297
  initSlurmTab();
3581
5298
  initEnterprise();
3582
5299
  </script>