@adiholick/nvstudio-mcp 2.2.2 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/public/index.html CHANGED
@@ -3,33 +3,41 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>nvstudio-mcp | Bridge Dashboard</title>
6
+ <title>nvstudio-mcp v2.3 | Bridge Dashboard</title>
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
10
10
  <style>
11
11
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
12
12
 
13
13
  :root {
14
- --bg: #000000;
15
- --surface: #0a0a0a;
16
- --surface-glass: rgba(10, 10, 10, 0.7);
17
- --surface-card: #121212;
18
- --surface-hover: #1c1c1c;
14
+ --bg: #0c0c0e;
15
+ --surface: #121216;
16
+ --surface-glass: rgba(18, 18, 22, 0.75);
17
+ --surface-card: #18181d;
18
+ --surface-hover: #222228;
19
19
  --border: rgba(255, 255, 255, 0.08);
20
- --border-highlight: rgba(255, 255, 255, 0.15);
20
+ --border-highlight: rgba(255, 255, 255, 0.16);
21
21
 
22
- --text: #f4f4f5;
22
+ --text: #f4f4f6;
23
23
  --text-muted: #a1a1aa;
24
24
  --text-dim: #71717a;
25
25
 
26
- --accent: #3b82f6;
27
- --accent-glow: rgba(59, 130, 246, 0.2);
26
+ /* NV-Series Accent Colors */
27
+ --accent: #00aaff;
28
+ --accent-glow: rgba(0, 170, 255, 0.25);
29
+ --cyan-glow: rgba(0, 255, 255, 0.2);
28
30
  --success: #10b981;
29
31
  --success-glow: rgba(16, 185, 129, 0.25);
30
32
  --error: #ef4444;
31
33
  --error-glow: rgba(239, 68, 68, 0.2);
32
- --task: #8b5cf6;
34
+ --warning: #f59e0b;
35
+ --warning-glow: rgba(245, 158, 11, 0.25);
36
+
37
+ --task: #a855f7;
38
+ --batch: #00aaff;
39
+ --security: #f59e0b;
40
+ --docs: #10b981;
33
41
  --response: #14b8a6;
34
42
  --system: #a1a1aa;
35
43
 
@@ -50,7 +58,7 @@
50
58
  .layout {
51
59
  display: flex;
52
60
  height: 100vh;
53
- background: radial-gradient(circle at 50% 0%, rgba(20, 20, 25, 1) 0%, rgba(0, 0, 0, 1) 100%);
61
+ background: radial-gradient(circle at 50% 0%, rgba(24, 24, 32, 1) 0%, rgba(12, 12, 14, 1) 100%);
54
62
  }
55
63
 
56
64
  /* ── SIDEBAR ── */
@@ -58,7 +66,7 @@
58
66
  width: var(--sidebar-w);
59
67
  min-width: var(--sidebar-w);
60
68
  background: var(--surface-glass);
61
- backdrop-filter: blur(12px);
69
+ backdrop-filter: blur(16px);
62
70
  border-right: 1px solid var(--border);
63
71
  display: flex;
64
72
  flex-direction: column;
@@ -77,11 +85,11 @@
77
85
  }
78
86
  .sidebar-logo .logo-icon {
79
87
  width: 38px; height: 38px;
80
- background: linear-gradient(135deg, #2563eb 0%, #3b82f6 100%);
88
+ background: linear-gradient(135deg, #0088cc 0%, #00aaff 100%);
81
89
  border-radius: 10px;
82
90
  display: flex; align-items: center; justify-content: center;
83
91
  flex-shrink: 0;
84
- box-shadow: 0 4px 12px var(--accent-glow);
92
+ box-shadow: 0 4px 14px var(--accent-glow);
85
93
  }
86
94
  .sidebar-logo .logo-icon svg { color: #fff; width: 22px; height: 22px; }
87
95
 
@@ -108,7 +116,7 @@
108
116
  display: flex;
109
117
  align-items: center;
110
118
  gap: 12px;
111
- background: rgba(0, 0, 0, 0.2);
119
+ background: rgba(0, 0, 0, 0.25);
112
120
  }
113
121
  .status-indicator-ring {
114
122
  position: relative;
@@ -176,7 +184,7 @@
176
184
  color: var(--text);
177
185
  }
178
186
  .nav-item.active {
179
- background: rgba(59, 130, 246, 0.1);
187
+ background: rgba(0, 170, 255, 0.12);
180
188
  color: var(--accent);
181
189
  border-left: 2px solid var(--accent);
182
190
  }
@@ -192,9 +200,9 @@
192
200
  color: var(--text-dim);
193
201
  }
194
202
  .badge-v2 {
195
- background: rgba(59, 130, 246, 0.15);
203
+ background: rgba(0, 170, 255, 0.15);
196
204
  color: var(--accent);
197
- border: 1px solid rgba(59, 130, 246, 0.3);
205
+ border: 1px solid rgba(0, 170, 255, 0.3);
198
206
  padding: 3px 8px;
199
207
  border-radius: 4px;
200
208
  font-weight: 600;
@@ -218,8 +226,8 @@
218
226
  display: flex;
219
227
  align-items: center;
220
228
  justify-content: space-between;
221
- background: rgba(0, 0, 0, 0.4);
222
- backdrop-filter: blur(10px);
229
+ background: rgba(12, 12, 14, 0.6);
230
+ backdrop-filter: blur(12px);
223
231
  z-index: 5;
224
232
  }
225
233
  .page-title {
@@ -286,8 +294,10 @@
286
294
  padding: 16px 32px;
287
295
  background: var(--surface-glass);
288
296
  border-bottom: 1px solid var(--border);
297
+ flex-wrap: wrap;
298
+ gap: 12px;
289
299
  }
290
- .filter-btns { display: flex; gap: 8px; }
300
+ .filter-btns { display: flex; gap: 8px; flex-wrap: wrap; }
291
301
  .filter-btn {
292
302
  background: var(--surface-card);
293
303
  border: 1px solid var(--border);
@@ -324,7 +334,7 @@
324
334
  display: flex;
325
335
  flex-direction: column;
326
336
  gap: 8px;
327
- height: calc(100vh - 140px); /* 70px topbar + ~70px toolbar */
337
+ height: calc(100vh - 142px);
328
338
  }
329
339
 
330
340
  .log-empty {
@@ -350,6 +360,7 @@
350
360
  font-size: 13px;
351
361
  line-height: 1.5;
352
362
  transition: transform 0.2s, background 0.2s;
363
+ align-items: flex-start;
353
364
  }
354
365
  .log-entry:hover {
355
366
  background: rgba(255, 255, 255, 0.04);
@@ -360,26 +371,33 @@
360
371
  font-size: 12px;
361
372
  flex-shrink: 0;
362
373
  white-space: nowrap;
374
+ margin-top: 1px;
363
375
  }
364
376
  .log-entry-badge {
365
377
  font-size: 10px; font-weight: 700;
366
378
  padding: 2px 8px; border-radius: 4px;
367
379
  height: fit-content; flex-shrink: 0;
380
+ letter-spacing: 0.03em;
368
381
  }
369
382
 
370
- .badge-task { color: var(--task); background: rgba(139, 92, 246, 0.15); border: 1px solid rgba(139, 92, 246, 0.3); }
383
+ .badge-task { color: var(--task); background: rgba(168, 85, 247, 0.15); border: 1px solid rgba(168, 85, 247, 0.3); }
384
+ .badge-batch { color: var(--batch); background: rgba(0, 170, 255, 0.15); border: 1px solid rgba(0, 170, 255, 0.3); }
385
+ .badge-security { color: var(--security); background: rgba(245, 158, 11, 0.15); border: 1px solid rgba(245, 158, 11, 0.3); }
386
+ .badge-docs { color: var(--docs); background: rgba(16, 185, 129, 0.15); border: 1px solid rgba(16, 185, 129, 0.3); }
387
+ .badge-patch { color: #38bdf8; background: rgba(56, 189, 248, 0.15); border: 1px solid rgba(56, 189, 248, 0.3); }
388
+ .badge-luau { color: #c084fc; background: rgba(192, 132, 252, 0.15); border: 1px solid rgba(192, 132, 252, 0.3); }
371
389
  .badge-response { color: var(--response); background: rgba(20, 184, 166, 0.15); border: 1px solid rgba(20, 184, 166, 0.3); }
372
390
  .badge-error { color: var(--error); background: rgba(239, 68, 68, 0.15); border: 1px solid rgba(239, 68, 68, 0.3); }
373
391
  .badge-system { color: var(--system); background: rgba(161, 161, 170, 0.15); border: 1px solid rgba(161, 161, 170, 0.3); }
374
392
 
375
- .log-entry-msg { color: var(--text-primary); word-break: break-word; }
393
+ .log-entry-msg { color: var(--text); word-break: break-word; flex: 1; }
376
394
 
377
- /* ── STATS PAGE ── */
395
+ /* ── STATS & SESSIONS PAGE ── */
378
396
  .stats-grid {
379
397
  display: grid;
380
398
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
381
399
  gap: 20px;
382
- max-width: 1000px;
400
+ max-width: 1040px;
383
401
  }
384
402
  .stat-card {
385
403
  background: var(--surface-card);
@@ -410,11 +428,212 @@
410
428
  .info-row:last-child { border-bottom: none; padding-bottom: 0; }
411
429
  .info-row-label { font-size: 13px; color: var(--text-muted); }
412
430
  .info-row-value { font-size: 14px; font-weight: 600; color: var(--text); font-family: 'JetBrains Mono', monospace; }
413
-
431
+
432
+ /* Studio Sessions Table */
433
+ .sessions-table {
434
+ width: 100%;
435
+ border-collapse: collapse;
436
+ margin-top: 16px;
437
+ font-size: 13px;
438
+ }
439
+ .sessions-table th {
440
+ text-align: left;
441
+ padding: 10px 14px;
442
+ color: var(--text-dim);
443
+ font-weight: 600;
444
+ border-bottom: 1px solid var(--border);
445
+ text-transform: uppercase;
446
+ font-size: 11px;
447
+ letter-spacing: 0.05em;
448
+ }
449
+ .sessions-table td {
450
+ padding: 12px 14px;
451
+ border-bottom: 1px solid rgba(255,255,255,0.04);
452
+ color: var(--text);
453
+ font-family: 'JetBrains Mono', monospace;
454
+ }
455
+ .sessions-table tr:hover td {
456
+ background: rgba(255,255,255,0.02);
457
+ }
458
+ .badge-pill {
459
+ display: inline-flex; align-items: center; gap: 6px;
460
+ font-size: 11px; padding: 2px 8px; border-radius: 12px; font-weight: 600;
461
+ }
462
+ .badge-ws { background: rgba(0, 170, 255, 0.15); color: var(--accent); border: 1px solid rgba(0, 170, 255, 0.3); }
463
+ .badge-poll { background: rgba(245, 158, 11, 0.15); color: var(--warning); border: 1px solid rgba(245, 158, 11, 0.3); }
464
+
465
+ /* ── DOCS EXPLORER PAGE ── */
466
+ .docs-container {
467
+ max-width: 900px;
468
+ display: flex;
469
+ flex-direction: column;
470
+ gap: 20px;
471
+ }
472
+ .docs-search-card {
473
+ background: var(--surface-card);
474
+ border: 1px solid var(--border);
475
+ border-radius: 12px;
476
+ padding: 24px;
477
+ display: flex;
478
+ flex-direction: column;
479
+ gap: 16px;
480
+ }
481
+ .docs-form {
482
+ display: flex;
483
+ gap: 12px;
484
+ flex-wrap: wrap;
485
+ }
486
+ .docs-input {
487
+ flex: 2;
488
+ min-width: 200px;
489
+ background: #0a0a0c;
490
+ border: 1px solid var(--border);
491
+ color: var(--text);
492
+ padding: 10px 14px;
493
+ border-radius: 8px;
494
+ font-family: inherit;
495
+ font-size: 14px;
496
+ outline: none;
497
+ transition: border-color 0.2s;
498
+ }
499
+ .docs-input:focus {
500
+ border-color: var(--accent);
501
+ box-shadow: 0 0 10px var(--accent-glow);
502
+ }
503
+ .docs-member-input {
504
+ flex: 1;
505
+ min-width: 140px;
506
+ background: #0a0a0c;
507
+ border: 1px solid var(--border);
508
+ color: var(--text);
509
+ padding: 10px 14px;
510
+ border-radius: 8px;
511
+ font-family: inherit;
512
+ font-size: 14px;
513
+ outline: none;
514
+ }
515
+ .docs-member-input:focus { border-color: var(--accent); }
516
+ .docs-btn {
517
+ background: linear-gradient(135deg, #0088cc 0%, #00aaff 100%);
518
+ color: #fff;
519
+ border: none;
520
+ padding: 10px 22px;
521
+ border-radius: 8px;
522
+ font-family: inherit;
523
+ font-size: 14px;
524
+ font-weight: 600;
525
+ cursor: pointer;
526
+ transition: transform 0.15s, box-shadow 0.2s;
527
+ display: flex; align-items: center; gap: 8px;
528
+ }
529
+ .docs-btn:hover {
530
+ transform: translateY(-1px);
531
+ box-shadow: 0 4px 14px var(--accent-glow);
532
+ }
533
+ .docs-chips {
534
+ display: flex;
535
+ gap: 8px;
536
+ flex-wrap: wrap;
537
+ align-items: center;
538
+ }
539
+ .chip-label { font-size: 12px; color: var(--text-dim); }
540
+ .doc-chip {
541
+ font-size: 12px;
542
+ background: rgba(255,255,255,0.04);
543
+ border: 1px solid var(--border);
544
+ color: var(--text-muted);
545
+ padding: 4px 10px;
546
+ border-radius: 6px;
547
+ cursor: pointer;
548
+ transition: all 0.2s;
549
+ font-family: 'JetBrains Mono', monospace;
550
+ }
551
+ .doc-chip:hover {
552
+ background: rgba(0, 170, 255, 0.15);
553
+ color: var(--accent);
554
+ border-color: var(--accent);
555
+ }
556
+
557
+ .docs-result-card {
558
+ background: var(--surface-card);
559
+ border: 1px solid var(--border);
560
+ border-radius: 12px;
561
+ padding: 28px;
562
+ min-height: 200px;
563
+ display: flex;
564
+ flex-direction: column;
565
+ position: relative;
566
+ }
567
+ .docs-result-header {
568
+ display: flex;
569
+ justify-content: space-between;
570
+ align-items: center;
571
+ margin-bottom: 16px;
572
+ padding-bottom: 12px;
573
+ border-bottom: 1px solid var(--border);
574
+ }
575
+ .docs-title {
576
+ font-size: 16px;
577
+ font-weight: 600;
578
+ color: var(--text);
579
+ display: flex; align-items: center; gap: 8px;
580
+ }
581
+ .docs-copy-btn {
582
+ background: transparent;
583
+ border: 1px solid var(--border);
584
+ color: var(--text-muted);
585
+ font-size: 12px;
586
+ padding: 4px 10px;
587
+ border-radius: 6px;
588
+ cursor: pointer;
589
+ transition: all 0.2s;
590
+ }
591
+ .docs-copy-btn:hover {
592
+ background: var(--surface-hover);
593
+ color: var(--text);
594
+ }
595
+ .docs-content {
596
+ font-family: 'Inter', sans-serif;
597
+ font-size: 14px;
598
+ line-height: 1.7;
599
+ color: var(--text);
600
+ overflow-x: auto;
601
+ }
602
+ .docs-content pre {
603
+ background: #09090b;
604
+ padding: 14px 18px;
605
+ border-radius: 8px;
606
+ border: 1px solid var(--border);
607
+ margin: 14px 0;
608
+ overflow-x: auto;
609
+ font-family: 'JetBrains Mono', monospace;
610
+ font-size: 13px;
611
+ color: #38bdf8;
612
+ }
613
+ .docs-content code {
614
+ font-family: 'JetBrains Mono', monospace;
615
+ background: rgba(255,255,255,0.06);
616
+ color: var(--accent);
617
+ padding: 2px 6px;
618
+ border-radius: 4px;
619
+ font-size: 12px;
620
+ }
621
+ .docs-content h1, .docs-content h2, .docs-content h3 {
622
+ margin: 18px 0 8px;
623
+ color: #fff;
624
+ }
625
+ .docs-content ul {
626
+ padding-left: 20px;
627
+ margin: 8px 0;
628
+ }
629
+ .docs-content li {
630
+ margin-bottom: 4px;
631
+ }
632
+
414
633
  /* ── ABOUT PAGE ── */
415
- .about-section { max-width: 680px; }
634
+ .about-section { max-width: 720px; }
416
635
  .about-hero {
417
- background: linear-gradient(145deg, var(--surface-card) 0%, rgba(10,10,10,1) 100%);
636
+ background: linear-gradient(145deg, var(--surface-card) 0%, rgba(14,14,18,1) 100%);
418
637
  border: 1px solid var(--border);
419
638
  border-radius: 16px;
420
639
  padding: 40px;
@@ -431,7 +650,7 @@
431
650
  pointer-events: none;
432
651
  }
433
652
  .about-hero h2 { font-size: 28px; font-weight: 700; color: var(--text); letter-spacing: -0.02em; display: flex; align-items: center; gap: 12px;}
434
- .v2-chip { font-size: 12px; background: var(--accent); color: white; padding: 2px 8px; border-radius: 12px; vertical-align: middle;}
653
+ .v2-chip { font-size: 12px; background: var(--accent); color: white; padding: 2px 8px; border-radius: 12px; vertical-align: middle; font-weight: 700;}
435
654
  .about-hero p { font-size: 15px; color: var(--text-muted); line-height: 1.6; }
436
655
 
437
656
  .about-links { display: flex; gap: 12px; margin-top: 8px; }
@@ -468,8 +687,8 @@
468
687
  /* Scrollbar */
469
688
  ::-webkit-scrollbar { width: 6px; height: 6px; }
470
689
  ::-webkit-scrollbar-track { background: transparent; }
471
- ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; }
472
- ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.2); }
690
+ ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 3px; }
691
+ ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.22); }
473
692
 
474
693
  /* Transitions */
475
694
  .log-entry { animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1); }
@@ -490,7 +709,7 @@
490
709
  </div>
491
710
  <div class="logo-text-wrapper">
492
711
  <div class="logo-name">nvstudio-mcp</div>
493
- <div class="logo-sub">Zero-Delay Engine</div>
712
+ <div class="logo-sub">Zero-Delay Bridge</div>
494
713
  </div>
495
714
  </div>
496
715
  </div>
@@ -511,19 +730,25 @@
511
730
  </div>
512
731
  <div class="nav-item" data-page="stats">
513
732
  <svg viewBox="0 0 24 24"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
514
- Statistics
733
+ Diagnostics & Sessions
734
+ </div>
735
+
736
+ <div class="nav-group-label" style="margin-top:20px;">Engine Tools</div>
737
+ <div class="nav-item" data-page="docs">
738
+ <svg viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
739
+ Docs Explorer
515
740
  </div>
516
741
 
517
742
  <div class="nav-group-label" style="margin-top:20px;">Info</div>
518
743
  <div class="nav-item" data-page="about">
519
744
  <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
520
- About v2.0
745
+ About v2.3
521
746
  </div>
522
747
  </nav>
523
748
 
524
749
  <div class="sidebar-footer">
525
- <span class="badge-v2" id="pkg-version">v2.0.1</span>
526
- <span style="font-size:10px; opacity:0.5;">Model Context Protocol</span>
750
+ <span class="badge-v2" id="pkg-version">v2.3.0</span>
751
+ <span style="font-size:10px; opacity:0.6;">Model Context Protocol</span>
527
752
  </div>
528
753
  </aside>
529
754
 
@@ -544,9 +769,11 @@
544
769
  <div class="log-toolbar">
545
770
  <div class="filter-btns">
546
771
  <button class="filter-btn active" data-filter="all">All Logs</button>
547
- <button class="filter-btn" data-filter="task">Task / Patch</button>
772
+ <button class="filter-btn" data-filter="task">Task / Command</button>
773
+ <button class="filter-btn" data-filter="batch">Batch Builder</button>
774
+ <button class="filter-btn" data-filter="security">Security / Scan</button>
548
775
  <button class="filter-btn" data-filter="response">Response</button>
549
- <button class="filter-btn" data-filter="error">Error / Validate</button>
776
+ <button class="filter-btn" data-filter="error">Errors</button>
550
777
  <button class="filter-btn" data-filter="system">System</button>
551
778
  </div>
552
779
  <button class="scroll-lock-btn locked" id="scroll-lock-btn">
@@ -559,12 +786,12 @@
559
786
  <div class="log-list" id="log-list">
560
787
  <div class="log-empty" id="log-empty">
561
788
  <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg>
562
- <p>Menunggu aktivitas dari Roblox Studio...</p>
789
+ <p>Menunggu aktivitas perintah dari AI / Roblox Studio...</p>
563
790
  </div>
564
791
  </div>
565
792
  </div>
566
793
 
567
- <!-- Statistics Page -->
794
+ <!-- Diagnostics & Sessions Page -->
568
795
  <div class="page" id="page-stats">
569
796
  <div class="stats-grid">
570
797
  <div class="stat-card">
@@ -592,23 +819,50 @@
592
819
  <div class="progress-bar error" id="bar-error" style="width:0%"></div>
593
820
  </div>
594
821
  </div>
822
+
823
+ <!-- Active Connected Studios -->
824
+ <div class="stat-card full">
825
+ <div class="stat-card-label" style="display:flex; justify-content:space-between; align-items:center;">
826
+ <span>🎮 Active Studio Sessions</span>
827
+ <span id="studios-count-badge" class="badge-v2">0 Terhubung</span>
828
+ </div>
829
+ <div id="studios-container" style="margin-top:12px;">
830
+ <table class="sessions-table" id="sessions-table" style="display:none;">
831
+ <thead>
832
+ <tr>
833
+ <th>Studio ID</th>
834
+ <th>Place ID / Name</th>
835
+ <th>Transport</th>
836
+ <th>Status</th>
837
+ <th>Last Ping</th>
838
+ </tr>
839
+ </thead>
840
+ <tbody id="sessions-tbody"></tbody>
841
+ </table>
842
+ <div id="sessions-empty" style="padding:24px; text-align:center; color:var(--text-dim);">
843
+ Tidak ada sesi Studio aktif saat ini. Buka game di Roblox Studio dan pastikan plugin <code>nvstudio_mcp</code> aktif.
844
+ </div>
845
+ </div>
846
+ </div>
847
+
848
+ <!-- Diagnostics Summary -->
595
849
  <div class="stat-card full">
596
850
  <div class="stat-card-label">Session Diagnostics</div>
597
851
  <div class="info-list">
598
852
  <div class="info-row">
599
- <span class="info-row-label">Mode Polling</span>
600
- <span class="info-row-value" style="color:var(--accent);">Zero-Delay Long-Polling (&lt;50ms)</span>
853
+ <span class="info-row-label">Transport Mode</span>
854
+ <span class="info-row-value" id="info-transport" style="color:var(--accent);">Dual-Mode (WebSocket ⚡ + Long-Poll)</span>
601
855
  </div>
602
856
  <div class="info-row">
603
857
  <span class="info-row-label">Luau Pre-Validation</span>
604
- <span class="info-row-value" style="color:var(--success);">Active ✓</span>
858
+ <span class="info-row-value" style="color:var(--success);">Active (AST-Protected) ✓</span>
605
859
  </div>
606
860
  <div class="info-row">
607
861
  <span class="info-row-label">Port Server</span>
608
862
  <span class="info-row-value">3055</span>
609
863
  </div>
610
864
  <div class="info-row">
611
- <span class="info-row-label">Studio Connection</span>
865
+ <span class="info-row-label">Overall Studio Status</span>
612
866
  <span class="info-row-value" id="info-studio">Disconnected</span>
613
867
  </div>
614
868
  <div class="info-row">
@@ -620,16 +874,59 @@
620
874
  </div>
621
875
  </div>
622
876
 
877
+ <!-- Docs Explorer Page -->
878
+ <div class="page" id="page-docs">
879
+ <div class="docs-container">
880
+ <div class="docs-search-card">
881
+ <div style="font-size:16px; font-weight:600; color:#fff; display:flex; align-items:center; gap:8px;">
882
+ <span>📚 Roblox Creator Docs Lookup</span>
883
+ <span class="badge-v2">Official Engine API</span>
884
+ </div>
885
+ <p style="font-size:13px; color:var(--text-muted);">
886
+ Cari dokumentasi resmi Class, Service, Datatype, Enum, atau Library Roblox langsung dari jembatan lokal (dilengkapi cache disk ~2 ms).
887
+ </p>
888
+ <form class="docs-form" id="docs-form">
889
+ <input type="text" class="docs-input" id="docs-query" placeholder="Nama Class / Service (contoh: TweenService, CanvasGroup, UDim2, task)" required />
890
+ <input type="text" class="docs-member-input" id="docs-member" placeholder="Member opsional (misal: Create)" />
891
+ <button type="submit" class="docs-btn" id="docs-btn">
892
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
893
+ Cari Docs
894
+ </button>
895
+ </form>
896
+ <div class="docs-chips">
897
+ <span class="chip-label">Pencarian Cepat:</span>
898
+ <span class="doc-chip" data-q="TweenService">TweenService</span>
899
+ <span class="doc-chip" data-q="CanvasGroup">CanvasGroup</span>
900
+ <span class="doc-chip" data-q="Frame">Frame</span>
901
+ <span class="doc-chip" data-q="UDim2">UDim2</span>
902
+ <span class="doc-chip" data-q="task">task</span>
903
+ <span class="doc-chip" data-q="UserInputService">UserInputService</span>
904
+ <span class="doc-chip" data-q="PathfindingService">PathfindingService</span>
905
+ </div>
906
+ </div>
907
+
908
+ <div class="docs-result-card" id="docs-result-card">
909
+ <div class="docs-result-header">
910
+ <span class="docs-title" id="docs-result-title">Hasil Dokumentasi</span>
911
+ <button class="docs-copy-btn" id="docs-copy-btn" style="display:none;">Salin Markdown</button>
912
+ </div>
913
+ <div class="docs-content" id="docs-content">
914
+ <span style="color:var(--text-dim); font-size:13px;">Ketik kata kunci class di atas atau klik salah satu chip pencarian cepat untuk menampilkan dokumentasi resmi Roblox Engine.</span>
915
+ </div>
916
+ </div>
917
+ </div>
918
+ </div>
919
+
623
920
  <!-- About Page -->
624
921
  <div class="page" id="page-about">
625
922
  <div class="about-section">
626
923
  <div class="about-hero">
627
- <h2>nvstudio-mcp <span class="v2-chip">v2.0.0</span></h2>
628
- <p>Jembatan pintar berbasis <strong>Model Context Protocol (MCP)</strong> yang menghubungkan AI Agent (Antigravity IDE, Cursor, Claude) secara langsung ke dalam <strong>Roblox Studio</strong>.</p>
924
+ <h2>nvstudio-mcp <span class="v2-chip" id="about-version-chip">v2.3.0</span></h2>
925
+ <p>Jembatan pintar berbasis <strong>Model Context Protocol (MCP)</strong> yang menghubungkan AI Agent (Antigravity IDE, Cursor, Claude Desktop, Windsurf) secara langsung ke dalam <strong>Roblox Studio</strong>.</p>
629
926
  <div class="about-links">
630
927
  <a class="about-link" href="https://github.com/Adiholick/nvstudio-mcp" target="_blank">
631
928
  <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
632
- GitHub
929
+ GitHub Repository
633
930
  </a>
634
931
  <a class="about-link" href="https://www.npmjs.com/package/@adiholick/nvstudio-mcp" target="_blank">
635
932
  <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M0 0h24v24H0V0zm7.315 5.876H2.17v12.248H12v-9.6h-4.685V5.876zm9.515 0h-5.145v12.248h5.145V5.876zm-7.315 4.8H9.87v4.8H9.515v-4.8z"/></svg>
@@ -639,22 +936,26 @@
639
936
  </div>
640
937
 
641
938
  <div class="about-card">
642
- <h3>⚡ Arsitektur v2.0 Baru</h3>
939
+ <h3>🚀 Fitur Baru v2.3.0</h3>
643
940
  <ul>
644
- <li><strong>Zero-Delay Long-Polling:</strong> Plugin Studio kini menunggu task secara realtime hingga 20 detik tanpa putus. Memangkas latensi drastis menjadi &lt; 50ms (dibandingkan polling 1-detik di v1).</li>
645
- <li><strong>Luau Pre-Commit Validator:</strong> Mencegah engine Roblox crash dengan memvalidasi sintaks Luau <i>sebelum</i> kode dikirim ke Studio.</li>
646
- <li><strong>Partial Script Patching:</strong> AI kini menggunakan metode <code>patch_script_source</code> (pengeditan parsial per baris) yang jauh lebih cepat dan menghemat ribuan token.</li>
941
+ <li><strong>⚡ Batch Instance Builder:</strong> Membangun pohon hierarki GUI puluhan tingkat dalam 1 kali roundtrip via <code>batch_create_instances</code> dan update massal via <code>batch_set_properties</code>.</li>
942
+ <li><strong>🛡️ Asset Security Scanner:</strong> Memindai dan memusnahkan 10 jenis ancaman backdoor/malware pada aset sebelum dimasukkan ke game via <code>preview_asset</code> dan <code>scan_security</code>.</li>
943
+ <li><strong>📚 Live Creator Docs Lookup:</strong> Mengambil dokumentasi engine resmi dari Roblox Creator Docs secara offline/online dengan disk cache berkecepatan ~2 ms via <code>get_roblox_docs</code>.</li>
944
+ <li><strong>🎨 NV-Series GUI Standard Taste:</strong> Standardisasi tata letak Frame, LayoutOrder teratur, Spacer margin ScrollingFrame, dan deteksi perangkat responsif pintar (PC, Phone, Tablet, Console).</li>
647
945
  </ul>
648
946
  </div>
649
947
 
650
948
  <div class="about-card">
651
- <h3>🛠️ Commands MCP Unggulan</h3>
949
+ <h3>🛠️ Daftar Semantic MCP Tools Lengkap</h3>
652
950
  <ul>
653
- <li><code>patch_script_source</code> — Mengedit bagian spesifik dari script (super cepat).</li>
654
- <li><code>execute_luau</code> — Menjalankan script Luau secara realtime di Studio.</li>
655
- <li><code>insert_asset</code> — Memasukkan model 3D/asset dari library Roblox.</li>
656
- <li><code>generate_terrain</code> — Men-generate atau memanipulasi terrain voxel.</li>
657
- <li><code>get_children</code> / <code>get_script_source</code> — Inspeksi hirarki dan source code.</li>
951
+ <li><code>batch_create_instances</code> — Pembuatan objek/hierarki massal pohon bersarang.</li>
952
+ <li><code>batch_set_properties</code> — Pembaruan properti multi-instance secara atomik.</li>
953
+ <li><code>get_roblox_docs</code> — Lookup dokumentasi resmi engine Roblox (Class, Datatype, Service, Enum).</li>
954
+ <li><code>preview_asset</code> / <code>scan_security</code> — Audit keamanan backdoor & sanitasi script model.</li>
955
+ <li><code>patch_script_source</code> — Mengedit bagian baris kode tertentu secara parsial.</li>
956
+ <li><code>execute_luau</code> — Eksekusi sembarang kode Luau dinamis di Studio.</li>
957
+ <li><code>get_full_tree</code> / <code>get_children</code> — Eksplorasi hierarki DataModel.</li>
958
+ <li><code>insert_asset</code> / <code>generate_terrain</code> — Memasukkan model atau membuat terrain voxel.</li>
658
959
  </ul>
659
960
  </div>
660
961
  </div>
@@ -671,19 +972,27 @@
671
972
  let activeFilter = 'all';
672
973
  let autoScroll = true;
673
974
  let sessionStartTime = null;
975
+ let latestDocsMarkdown = '';
674
976
 
675
977
  const logList = document.getElementById('log-list');
676
978
  const logEmpty = document.getElementById('log-empty');
677
979
  const logCount = document.getElementById('log-count');
678
980
  const statusDot = document.getElementById('status-dot');
679
981
  const statusLabel = document.getElementById('status-label');
982
+ const pkgVersion = document.getElementById('pkg-version');
983
+ const aboutVersionChip = document.getElementById('about-version-chip');
680
984
 
681
985
  /* ── Navigation ── */
682
986
  const navItems = document.querySelectorAll('.nav-item');
683
987
  const pages = document.querySelectorAll('.page');
684
988
  const topbarTitle = document.getElementById('topbar-title');
685
989
  const clearBtn = document.getElementById('clear-btn');
686
- const pageTitles = { activity: 'Activity Log', stats: 'Session Diagnostics', about: 'About v2.0' };
990
+ const pageTitles = {
991
+ activity: 'Activity Log',
992
+ stats: 'Session Diagnostics & Studios',
993
+ docs: 'Roblox Docs Explorer',
994
+ about: 'About v2.3'
995
+ };
687
996
 
688
997
  navItems.forEach(item => {
689
998
  item.addEventListener('click', () => {
@@ -691,7 +1000,8 @@
691
1000
  navItems.forEach(n => n.classList.remove('active'));
692
1001
  pages.forEach(p => p.classList.remove('active'));
693
1002
  item.classList.add('active');
694
- document.getElementById('page-' + page).classList.add('active');
1003
+ const targetPage = document.getElementById('page-' + page);
1004
+ if (targetPage) targetPage.classList.add('active');
695
1005
  topbarTitle.textContent = pageTitles[page] || page;
696
1006
 
697
1007
  const isActivity = page === 'activity';
@@ -700,7 +1010,7 @@
700
1010
  });
701
1011
  });
702
1012
 
703
- /* ── Clear ── */
1013
+ /* ── Clear Logs ── */
704
1014
  clearBtn.addEventListener('click', () => {
705
1015
  allLogs = [];
706
1016
  renderLogs();
@@ -723,47 +1033,50 @@
723
1033
  scrollLockBtn.classList.toggle('locked', autoScroll);
724
1034
  });
725
1035
 
726
- /* ── Render ── */
727
- function renderLogs() {
728
- const filtered = activeFilter === 'all' ? allLogs : allLogs.filter(l => l.type === activeFilter);
729
- logCount.textContent = `${allLogs.length} event${allLogs.length !== 1 ? 's' : ''}`;
730
-
731
- if (filtered.length === 0) {
732
- logEmpty.style.display = 'flex';
733
- [...logList.children].forEach(c => { if (c !== logEmpty) c.remove(); });
734
- return;
735
- }
736
- logEmpty.style.display = 'none';
737
-
738
- const frag = document.createDocumentFragment();
739
- filtered.forEach(log => {
740
- const el = createLogEl(log);
741
- frag.appendChild(el);
742
- });
743
- logList.innerHTML = '';
744
- logList.appendChild(logEmpty);
745
- logEmpty.style.display = 'none';
746
- logList.appendChild(frag);
747
-
748
- if (autoScroll) logList.scrollTop = 0;
1036
+ /* ── Categorize Log ── */
1037
+ function getLogCategory(log) {
1038
+ const msg = log.message || '';
1039
+ if (msg.includes('batch_create') || msg.includes('batch_set')) return 'batch';
1040
+ if (msg.includes('preview_asset') || msg.includes('scan_security') || msg.includes('security') || msg.includes('strip_scripts')) return 'security';
1041
+ if (msg.includes('get_roblox_docs')) return 'docs';
1042
+ if (msg.includes('patch_script_source')) return 'patch';
1043
+ if (msg.includes('execute_luau')) return 'luau';
1044
+ return log.type;
749
1045
  }
750
1046
 
1047
+ /* ── Create Log Element ── */
751
1048
  function createLogEl(log) {
752
1049
  const el = document.createElement('div');
753
1050
  el.className = 'log-entry';
754
- el.dataset.type = log.type;
1051
+ const cat = getLogCategory(log);
1052
+ el.dataset.category = cat;
755
1053
 
756
1054
  let displayType = log.type.toUpperCase();
757
- // Custom badges for v2 features based on message content
758
- if (log.type === 'task' && log.message.includes('patch_script_source')) {
1055
+ let badgeClass = 'badge-' + log.type;
1056
+
1057
+ if (cat === 'batch') {
1058
+ displayType = 'BATCH';
1059
+ badgeClass = 'badge-batch';
1060
+ } else if (cat === 'security') {
1061
+ displayType = 'SECURITY';
1062
+ badgeClass = 'badge-security';
1063
+ } else if (cat === 'docs') {
1064
+ displayType = 'DOCS';
1065
+ badgeClass = 'badge-docs';
1066
+ } else if (cat === 'patch') {
759
1067
  displayType = 'PATCH';
1068
+ badgeClass = 'badge-patch';
1069
+ } else if (cat === 'luau') {
1070
+ displayType = 'LUAU';
1071
+ badgeClass = 'badge-luau';
760
1072
  } else if (log.type === 'error' && log.message.includes('Syntax Error')) {
761
1073
  displayType = 'SYNTAX';
1074
+ badgeClass = 'badge-error';
762
1075
  }
763
1076
 
764
1077
  el.innerHTML = `
765
1078
  <span class="log-entry-time">${log.time}</span>
766
- <span class="log-entry-badge badge-${log.type}">${displayType}</span>
1079
+ <span class="log-entry-badge ${badgeClass}">${displayType}</span>
767
1080
  <span class="log-entry-msg">${escapeHtml(log.message)}</span>
768
1081
  `;
769
1082
  return el;
@@ -773,12 +1086,49 @@
773
1086
  return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
774
1087
  }
775
1088
 
1089
+ function shouldShowLog(log) {
1090
+ if (activeFilter === 'all') return true;
1091
+ const cat = getLogCategory(log);
1092
+ if (activeFilter === 'batch') return cat === 'batch';
1093
+ if (activeFilter === 'security') return cat === 'security';
1094
+ if (activeFilter === 'task') return log.type === 'task';
1095
+ if (activeFilter === 'response') return log.type === 'response';
1096
+ if (activeFilter === 'error') return log.type === 'error';
1097
+ if (activeFilter === 'system') return log.type === 'system';
1098
+ return false;
1099
+ }
1100
+
1101
+ /* ── Render Logs ── */
1102
+ function renderLogs() {
1103
+ const filtered = allLogs.filter(shouldShowLog);
1104
+ logCount.textContent = `${allLogs.length} event${allLogs.length !== 1 ? 's' : ''}`;
1105
+
1106
+ if (filtered.length === 0) {
1107
+ logEmpty.style.display = 'flex';
1108
+ [...logList.children].forEach(c => { if (c !== logEmpty) c.remove(); });
1109
+ return;
1110
+ }
1111
+ logEmpty.style.display = 'none';
1112
+
1113
+ const frag = document.createDocumentFragment();
1114
+ filtered.forEach(log => {
1115
+ const el = createLogEl(log);
1116
+ frag.appendChild(el);
1117
+ });
1118
+ logList.innerHTML = '';
1119
+ logList.appendChild(logEmpty);
1120
+ logEmpty.style.display = 'none';
1121
+ logList.appendChild(frag);
1122
+
1123
+ if (autoScroll) logList.scrollTop = 0;
1124
+ }
1125
+
776
1126
  function prependLog(log) {
777
1127
  allLogs.unshift(log);
778
- if (allLogs.length > 1000) allLogs.pop(); // Keep max 1000 logs in memory
1128
+ if (allLogs.length > 1000) allLogs.pop();
779
1129
  logCount.textContent = `${allLogs.length} event${allLogs.length !== 1 ? 's' : ''}`;
780
1130
 
781
- if (activeFilter !== 'all' && log.type !== activeFilter) return;
1131
+ if (!shouldShowLog(log)) return;
782
1132
 
783
1133
  logEmpty.style.display = 'none';
784
1134
  const el = createLogEl(log);
@@ -789,17 +1139,69 @@
789
1139
  if (autoScroll) logList.scrollTop = 0;
790
1140
  }
791
1141
 
792
- /* ── Stats ── */
1142
+ /* ── Update Connected Studios Table ── */
1143
+ function updateStudiosTable(studios) {
1144
+ const tbody = document.getElementById('sessions-tbody');
1145
+ const table = document.getElementById('sessions-table');
1146
+ const empty = document.getElementById('sessions-empty');
1147
+ const badge = document.getElementById('studios-count-badge');
1148
+
1149
+ const list = Array.isArray(studios) ? studios : [];
1150
+ badge.textContent = `${list.length} Terhubung`;
1151
+
1152
+ if (list.length === 0) {
1153
+ table.style.display = 'none';
1154
+ empty.style.display = 'block';
1155
+ return;
1156
+ }
1157
+
1158
+ empty.style.display = 'none';
1159
+ table.style.display = 'table';
1160
+ tbody.innerHTML = '';
1161
+
1162
+ const now = Date.now();
1163
+ list.forEach(s => {
1164
+ const tr = document.createElement('tr');
1165
+ const sidShort = s.studioId ? (s.studioId.length > 12 ? s.studioId.substring(0, 10) + '…' : s.studioId) : 'anon';
1166
+ const placeText = (s.placeId && s.placeId > 0) ? `Place #${s.placeId}` : 'Local Place (ID: 0)';
1167
+ const isWs = s.transport === 'websocket';
1168
+ const transportBadge = isWs
1169
+ ? '<span class="badge-pill badge-ws">WebSocket ⚡</span>'
1170
+ : '<span class="badge-pill badge-poll">Long-Poll 🔄</span>';
1171
+ const statusBadge = s.isBusy
1172
+ ? '<span style="color:var(--warning);">● Sibuk</span>'
1173
+ : '<span style="color:var(--success);">● Siap</span>';
1174
+ const elapsed = s.lastSeen ? Math.max(0, Math.round((now - s.lastSeen) / 1000)) + 'd lalu' : 'Baru saja';
1175
+
1176
+ tr.innerHTML = `
1177
+ <td title="${s.studioId || ''}"><code>${sidShort}</code></td>
1178
+ <td><strong>${placeText}</strong></td>
1179
+ <td>${transportBadge}</td>
1180
+ <td>${statusBadge}</td>
1181
+ <td style="color:var(--text-dim); font-size:12px;">${elapsed}</td>
1182
+ `;
1183
+ tbody.appendChild(tr);
1184
+ });
1185
+ }
1186
+
1187
+ /* ── Update Statistics ── */
793
1188
  function updateStats(s) {
794
- document.getElementById('stat-total').textContent = s.totalTasks;
795
- document.getElementById('stat-success').textContent = s.successCount;
796
- document.getElementById('stat-error').textContent = s.errorCount;
1189
+ if (!s) return;
1190
+ if (s.totalTasks !== undefined) document.getElementById('stat-total').textContent = s.totalTasks;
1191
+ if (s.successCount !== undefined) document.getElementById('stat-success').textContent = s.successCount;
1192
+ if (s.errorCount !== undefined) document.getElementById('stat-error').textContent = s.errorCount;
797
1193
 
798
- const rate = s.totalTasks > 0 ? Math.round((s.successCount / s.totalTasks) * 100) : null;
1194
+ if (s.version) {
1195
+ if (pkgVersion) pkgVersion.textContent = 'v' + s.version;
1196
+ if (aboutVersionChip) aboutVersionChip.textContent = 'v' + s.version;
1197
+ }
1198
+
1199
+ const total = s.totalTasks || 0;
1200
+ const rate = total > 0 ? Math.round(((s.successCount || 0) / total) * 100) : null;
799
1201
  document.getElementById('stat-rate').textContent = rate !== null ? rate + '%' : '—';
800
1202
  document.getElementById('bar-success').style.width = (rate || 0) + '%';
801
1203
 
802
- const errRate = s.totalTasks > 0 ? Math.round((s.errorCount / s.totalTasks) * 100) : 0;
1204
+ const errRate = total > 0 ? Math.round(((s.errorCount || 0) / total) * 100) : 0;
803
1205
  document.getElementById('bar-error').style.width = errRate + '%';
804
1206
 
805
1207
  if (s.sessionStart) {
@@ -807,16 +1209,34 @@
807
1209
  }
808
1210
 
809
1211
  const statusEl = document.getElementById('info-studio');
1212
+ const transportEl = document.getElementById('info-transport');
810
1213
  if (s.studioConnected) {
811
- statusEl.textContent = 'Connected ✓';
1214
+ statusEl.textContent = `Connected ✓ (${s.studioCount || 1} Studio)`;
812
1215
  statusEl.style.color = 'var(--success)';
1216
+ if (s.wsStudios && s.wsStudios > 0) {
1217
+ if (transportEl) {
1218
+ transportEl.textContent = `WebSocket Duplex ⚡ (${s.wsStudios} Active)`;
1219
+ transportEl.style.color = 'var(--success)';
1220
+ }
1221
+ } else if (transportEl) {
1222
+ transportEl.textContent = 'Long-Polling (Active Fallback)';
1223
+ transportEl.style.color = 'var(--accent)';
1224
+ }
813
1225
  } else {
814
1226
  statusEl.textContent = 'Disconnected';
815
1227
  statusEl.style.color = 'var(--text-muted)';
1228
+ if (transportEl) {
1229
+ transportEl.textContent = 'Dual-Mode (WebSocket ⚡ + Long-Poll)';
1230
+ transportEl.style.color = 'var(--accent)';
1231
+ }
1232
+ }
1233
+
1234
+ if (s.connectedStudios) {
1235
+ updateStudiosTable(s.connectedStudios);
816
1236
  }
817
1237
  }
818
1238
 
819
- /* ── Uptime ── */
1239
+ /* ── Uptime Timer ── */
820
1240
  setInterval(() => {
821
1241
  if (!sessionStartTime) return;
822
1242
  const diff = Math.floor((Date.now() - sessionStartTime) / 1000);
@@ -826,14 +1246,15 @@
826
1246
  document.getElementById('info-uptime').textContent = `${h}:${m}:${s}`;
827
1247
  }, 1000);
828
1248
 
829
- /* ── Studio Status ── */
830
- function setStudioStatus(connected) {
1249
+ /* ── Studio Status Indicator ── */
1250
+ function setStudioStatus(connected, studioCount) {
831
1251
  statusDot.className = 'status-dot ' + (connected ? 'connected' : 'waiting');
832
- statusLabel.textContent = connected ? 'Studio Connected' : 'Menunggu Studio...';
1252
+ const countText = studioCount && studioCount > 1 ? ` (${studioCount} Studios)` : '';
1253
+ statusLabel.textContent = connected ? `Studio Connected${countText}` : 'Menunggu Studio...';
833
1254
 
834
1255
  const statusEl = document.getElementById('info-studio');
835
1256
  if (connected) {
836
- statusEl.textContent = 'Connected ✓';
1257
+ statusEl.textContent = `Connected ✓${countText}`;
837
1258
  statusEl.style.color = 'var(--success)';
838
1259
  } else {
839
1260
  statusEl.textContent = 'Disconnected';
@@ -841,7 +1262,109 @@
841
1262
  }
842
1263
  }
843
1264
 
844
- /* ── Socket Events ── */
1265
+ /* ── Simple Markdown Formatter for Docs ── */
1266
+ function formatMarkdown(md) {
1267
+ if (!md) return '';
1268
+ let html = escapeHtml(md);
1269
+
1270
+ // Fenced code blocks ```
1271
+ html = html.replace(/```([a-zA-Z]*)\n([\s\S]*?)```/g, function(match, lang, code) {
1272
+ return '<pre><code>' + code + '</code></pre>';
1273
+ });
1274
+
1275
+ // Inline code `...`
1276
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
1277
+
1278
+ // Headers
1279
+ html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>');
1280
+ html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>');
1281
+ html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>');
1282
+
1283
+ // Bold & Italic
1284
+ html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
1285
+ html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
1286
+
1287
+ // Unordered Lists
1288
+ html = html.replace(/^\- (.*$)/gim, '<li>$1</li>');
1289
+ html = html.replace(/(<li>.*<\/li>)/gim, '<ul>$1</ul>');
1290
+
1291
+ // Clean double <ul> tags
1292
+ html = html.replace(/<\/ul>\s*<ul>/g, '');
1293
+
1294
+ // Paragraph breaks
1295
+ html = html.replace(/\n\n/g, '<br><br>');
1296
+
1297
+ return html;
1298
+ }
1299
+
1300
+ /* ── Docs Explorer Handler ── */
1301
+ const docsForm = document.getElementById('docs-form');
1302
+ const docsQuery = document.getElementById('docs-query');
1303
+ const docsMember = document.getElementById('docs-member');
1304
+ const docsBtn = document.getElementById('docs-btn');
1305
+ const docsContent = document.getElementById('docs-content');
1306
+ const docsResultTitle = document.getElementById('docs-result-title');
1307
+ const docsCopyBtn = document.getElementById('docs-copy-btn');
1308
+
1309
+ async function searchDocs(query, member) {
1310
+ if (!query) return;
1311
+ docsQuery.value = query;
1312
+ if (member) docsMember.value = member;
1313
+
1314
+ docsBtn.disabled = true;
1315
+ docsBtn.innerHTML = 'Mencari...';
1316
+ docsContent.innerHTML = '<span style="color:var(--accent);">Mengambil dokumentasi resmi Creator Docs...</span>';
1317
+
1318
+ try {
1319
+ const url = new URL('/api/docs', window.location.origin);
1320
+ url.searchParams.set('query', query);
1321
+ if (member) url.searchParams.set('member', member);
1322
+
1323
+ const res = await fetch(url.toString());
1324
+ const data = await res.json();
1325
+
1326
+ if (!res.ok || data.error) {
1327
+ docsContent.innerHTML = `<span style="color:var(--error);">Error: ${escapeHtml(data.error || 'Gagal mengambil dokumentasi')}</span>`;
1328
+ docsCopyBtn.style.display = 'none';
1329
+ } else {
1330
+ latestDocsMarkdown = data.result;
1331
+ docsResultTitle.innerHTML = `Dokumentasi: <code>${escapeHtml(query)}${member ? '.' + escapeHtml(member) : ''}</code>`;
1332
+ docsContent.innerHTML = formatMarkdown(data.result);
1333
+ docsCopyBtn.style.display = 'inline-block';
1334
+ }
1335
+ } catch (err) {
1336
+ docsContent.innerHTML = `<span style="color:var(--error);">Koneksi gagal: ${escapeHtml(err.message)}</span>`;
1337
+ docsCopyBtn.style.display = 'none';
1338
+ } finally {
1339
+ docsBtn.disabled = false;
1340
+ docsBtn.innerHTML = `
1341
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
1342
+ Cari Docs
1343
+ `;
1344
+ }
1345
+ }
1346
+
1347
+ docsForm.addEventListener('submit', (e) => {
1348
+ e.preventDefault();
1349
+ searchDocs(docsQuery.value.trim(), docsMember.value.trim());
1350
+ });
1351
+
1352
+ document.querySelectorAll('.doc-chip').forEach(chip => {
1353
+ chip.addEventListener('click', () => {
1354
+ const q = chip.dataset.q;
1355
+ searchDocs(q, '');
1356
+ });
1357
+ });
1358
+
1359
+ docsCopyBtn.addEventListener('click', () => {
1360
+ if (latestDocsMarkdown) {
1361
+ navigator.clipboard.writeText(latestDocsMarkdown);
1362
+ docsCopyBtn.textContent = 'Tersalin ✓';
1363
+ setTimeout(() => { docsCopyBtn.textContent = 'Salin Markdown'; }, 2000);
1364
+ }
1365
+ });
1366
+
1367
+ /* ── Socket.IO Event Listeners ── */
845
1368
  socket.on('connect', () => {});
846
1369
  socket.on('disconnect', () => setStudioStatus(false));
847
1370
 
@@ -850,7 +1373,7 @@
850
1373
  renderLogs();
851
1374
  if (stats) {
852
1375
  updateStats(stats);
853
- if (stats.studioConnected) setStudioStatus(true);
1376
+ if (stats.studioConnected) setStudioStatus(true, stats.studioCount);
854
1377
  }
855
1378
  });
856
1379
 
@@ -862,9 +1385,11 @@
862
1385
  updateStats(s);
863
1386
  });
864
1387
 
865
- socket.on('studio-status', ({ connected }) => {
866
- setStudioStatus(connected);
1388
+ socket.on('studio-status', ({ connected, studioCount, connectedStudios, version }) => {
1389
+ setStudioStatus(connected, studioCount);
1390
+ if (connectedStudios) updateStudiosTable(connectedStudios);
1391
+ if (version && pkgVersion) pkgVersion.textContent = 'v' + version;
867
1392
  });
868
1393
  </script>
869
1394
  </body>
870
- </html>
1395
+ </html>