@adiholick/nvstudio-mcp 1.0.2 → 1.0.3

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.
@@ -10,6 +10,24 @@ const socket_io_1 = require("socket.io");
10
10
  const path_1 = __importDefault(require("path"));
11
11
  const child_process_1 = require("child_process");
12
12
  const task_queue_1 = require("./task-queue");
13
+ const activityLogs = [];
14
+ let totalTasks = 0;
15
+ let successCount = 0;
16
+ let errorCount = 0;
17
+ const sessionStart = new Date().toISOString();
18
+ function addLog(io, type, message) {
19
+ const log = {
20
+ id: Date.now().toString(),
21
+ time: new Date().toLocaleTimeString('id-ID'),
22
+ type,
23
+ message,
24
+ };
25
+ activityLogs.unshift(log); // newest first
26
+ if (activityLogs.length > 200)
27
+ activityLogs.pop();
28
+ io.emit('log', log);
29
+ io.emit('stats', { totalTasks, successCount, errorCount, sessionStart });
30
+ }
13
31
  function startBridgeServer(port = 3055) {
14
32
  const app = (0, express_1.default)();
15
33
  const httpServer = (0, http_1.createServer)(app);
@@ -18,8 +36,25 @@ function startBridgeServer(port = 3055) {
18
36
  app.use(express_1.default.json({ limit: '50mb' }));
19
37
  app.use(express_1.default.static(path_1.default.join(__dirname, '../public')));
20
38
  let dashboardOpened = false;
39
+ let studioConnected = false;
40
+ // Provide initial state to newly connected dashboard clients
41
+ io.on('connection', (socket) => {
42
+ socket.emit('init', {
43
+ logs: activityLogs,
44
+ stats: { totalTasks, successCount, errorCount, sessionStart, studioConnected, port },
45
+ });
46
+ });
47
+ // API endpoint to get current stats
48
+ app.get('/api/stats', (req, res) => {
49
+ res.json({ totalTasks, successCount, errorCount, sessionStart, studioConnected, port });
50
+ });
21
51
  // Endpoint Studio Polling
22
52
  app.get('/api/tasks', (req, res) => {
53
+ if (!studioConnected) {
54
+ studioConnected = true;
55
+ io.emit('studio-status', { connected: true });
56
+ addLog(io, 'system', 'Roblox Studio terhubung ke Bridge Server.');
57
+ }
23
58
  if (!dashboardOpened) {
24
59
  dashboardOpened = true;
25
60
  const url = `http://localhost:${port}`;
@@ -32,7 +67,8 @@ function startBridgeServer(port = 3055) {
32
67
  }
33
68
  if (task_queue_1.taskQueue.length > 0) {
34
69
  const task = task_queue_1.taskQueue.shift();
35
- io.emit('log', `[AI Task] Mengirim perintah '${task?.command}' ke Studio.`);
70
+ totalTasks++;
71
+ addLog(io, 'task', `Mengirim perintah '${task?.command}' ke Studio${task?.target ? ` → ${task.target}` : ''}.`);
36
72
  res.json(task);
37
73
  }
38
74
  else {
@@ -42,10 +78,14 @@ function startBridgeServer(port = 3055) {
42
78
  // Endpoint Studio Response
43
79
  app.post('/api/response', (req, res) => {
44
80
  const { id, status, result, error } = req.body;
45
- io.emit('log', `[Studio Response] Status: ${status}`);
46
- if (error)
47
- io.emit('log', `[Error] ${error}`);
48
- // Memanggil fungsi resolve secara sinkron (menghindari promise import circular palsu)
81
+ if (status === 'success') {
82
+ successCount++;
83
+ addLog(io, 'response', `Perintah selesai: ${String(result).substring(0, 120)}`);
84
+ }
85
+ else {
86
+ errorCount++;
87
+ addLog(io, 'error', `Gagal: ${error}`);
88
+ }
49
89
  (0, task_queue_1.resolvePendingTask)(id, status, result, error);
50
90
  res.json({ message: "Response diterima" });
51
91
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adiholick/nvstudio-mcp",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Local Bridge Server MCP for Roblox Studio",
5
5
  "main": "dist/index.js",
6
6
  "bin": "dist/index.js",
package/public/index.html CHANGED
@@ -1,34 +1,819 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en">
2
+ <html lang="id">
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 | Live Dashboard</title>
6
+ <title>nvstudio-mcp | Dashboard</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
7
10
  <style>
8
- body { font-family: 'Consolas', monospace; background-color: #0f172a; color: #38bdf8; margin: 0; padding: 20px; }
9
- h1 { color: #f8fafc; font-size: 1.5rem; border-bottom: 1px solid #334155; padding-bottom: 10px; }
10
- #log-container { background-color: #1e293b; padding: 15px; border-radius: 8px; height: 75vh; overflow-y: auto; box-shadow: 0 4px 6px rgba(0,0,0,0.3); }
11
- .log-entry { margin-bottom: 8px; border-bottom: 1px solid #334155; padding-bottom: 4px; }
12
- .log-time { color: #94a3b8; font-size: 0.85em; margin-right: 10px; }
11
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
12
+
13
+ :root {
14
+ --bg: #0a0a0a;
15
+ --surface: #111111;
16
+ --surface2: #1a1a1a;
17
+ --surface3: #222222;
18
+ --border: #2a2a2a;
19
+ --border2: #333333;
20
+ --text: #f0f0f0;
21
+ --text-muted: #888888;
22
+ --text-dim: #555555;
23
+ --white: #ffffff;
24
+ --accent: #ffffff;
25
+ --task: #a8a8a8;
26
+ --response: #d0d0d0;
27
+ --error: #ff5555;
28
+ --system: #888888;
29
+ --success: #888888;
30
+ --sidebar-w: 220px;
31
+ }
32
+
33
+ html, body {
34
+ height: 100%;
35
+ background: var(--bg);
36
+ color: var(--text);
37
+ font-family: 'Inter', sans-serif;
38
+ font-size: 14px;
39
+ overflow: hidden;
40
+ }
41
+
42
+ /* ── LAYOUT ── */
43
+ .layout {
44
+ display: flex;
45
+ height: 100vh;
46
+ }
47
+
48
+ /* ── SIDEBAR ── */
49
+ .sidebar {
50
+ width: var(--sidebar-w);
51
+ min-width: var(--sidebar-w);
52
+ background: var(--surface);
53
+ border-right: 1px solid var(--border);
54
+ display: flex;
55
+ flex-direction: column;
56
+ overflow: hidden;
57
+ }
58
+
59
+ .sidebar-logo {
60
+ padding: 24px 20px 20px;
61
+ border-bottom: 1px solid var(--border);
62
+ }
63
+ .sidebar-logo .logo-mark {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 10px;
67
+ }
68
+ .sidebar-logo .logo-icon {
69
+ width: 32px; height: 32px;
70
+ background: var(--white);
71
+ border-radius: 8px;
72
+ display: flex; align-items: center; justify-content: center;
73
+ flex-shrink: 0;
74
+ }
75
+ .sidebar-logo .logo-icon svg { color: #000; }
76
+ .sidebar-logo .logo-name {
77
+ font-size: 13px;
78
+ font-weight: 700;
79
+ color: var(--white);
80
+ letter-spacing: 0.02em;
81
+ }
82
+ .sidebar-logo .logo-sub {
83
+ font-size: 10px;
84
+ color: var(--text-dim);
85
+ margin-top: 2px;
86
+ letter-spacing: 0.08em;
87
+ text-transform: uppercase;
88
+ }
89
+
90
+ /* Status Indicator */
91
+ .sidebar-status {
92
+ padding: 14px 20px;
93
+ border-bottom: 1px solid var(--border);
94
+ display: flex;
95
+ align-items: center;
96
+ gap: 8px;
97
+ }
98
+ .status-dot {
99
+ width: 7px; height: 7px;
100
+ border-radius: 50%;
101
+ background: var(--text-dim);
102
+ flex-shrink: 0;
103
+ transition: background 0.4s;
104
+ }
105
+ .status-dot.connected { background: var(--white); box-shadow: 0 0 8px rgba(255,255,255,0.4); }
106
+ .status-dot.waiting { background: #777; animation: pulse-dot 1.5s infinite; }
107
+ .status-label {
108
+ font-size: 11px;
109
+ color: var(--text-muted);
110
+ font-weight: 500;
111
+ letter-spacing: 0.06em;
112
+ text-transform: uppercase;
113
+ }
114
+
115
+ @keyframes pulse-dot {
116
+ 0%, 100% { opacity: 1; }
117
+ 50% { opacity: 0.3; }
118
+ }
119
+
120
+ /* Nav */
121
+ .sidebar-nav {
122
+ flex: 1;
123
+ padding: 12px 0;
124
+ overflow-y: auto;
125
+ }
126
+ .nav-group-label {
127
+ font-size: 10px;
128
+ font-weight: 600;
129
+ color: var(--text-dim);
130
+ letter-spacing: 0.1em;
131
+ text-transform: uppercase;
132
+ padding: 8px 20px 4px;
133
+ }
134
+ .nav-item {
135
+ display: flex;
136
+ align-items: center;
137
+ gap: 10px;
138
+ padding: 9px 20px;
139
+ cursor: pointer;
140
+ color: var(--text-muted);
141
+ font-size: 13px;
142
+ font-weight: 500;
143
+ transition: color 0.15s, background 0.15s;
144
+ border-left: 2px solid transparent;
145
+ user-select: none;
146
+ }
147
+ .nav-item:hover { color: var(--text); background: var(--surface2); }
148
+ .nav-item.active { color: var(--white); border-left-color: var(--white); background: var(--surface2); }
149
+ .nav-item svg { width: 15px; height: 15px; flex-shrink: 0; stroke: currentColor; fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
150
+
151
+ .sidebar-footer {
152
+ padding: 16px 20px;
153
+ border-top: 1px solid var(--border);
154
+ }
155
+ .sidebar-footer .version {
156
+ font-size: 11px;
157
+ color: var(--text-dim);
158
+ font-family: 'JetBrains Mono', monospace;
159
+ }
160
+
161
+ /* ── MAIN ── */
162
+ .main {
163
+ flex: 1;
164
+ display: flex;
165
+ flex-direction: column;
166
+ overflow: hidden;
167
+ background: var(--bg);
168
+ }
169
+
170
+ /* Topbar */
171
+ .topbar {
172
+ height: 56px;
173
+ display: flex;
174
+ align-items: center;
175
+ justify-content: space-between;
176
+ padding: 0 28px;
177
+ border-bottom: 1px solid var(--border);
178
+ flex-shrink: 0;
179
+ }
180
+ .page-title {
181
+ font-size: 15px;
182
+ font-weight: 600;
183
+ color: var(--white);
184
+ }
185
+ .topbar-right {
186
+ display: flex;
187
+ align-items: center;
188
+ gap: 12px;
189
+ }
190
+ .topbar-badge {
191
+ font-size: 11px;
192
+ font-weight: 500;
193
+ font-family: 'JetBrains Mono', monospace;
194
+ color: var(--text-muted);
195
+ background: var(--surface2);
196
+ border: 1px solid var(--border);
197
+ padding: 4px 10px;
198
+ border-radius: 20px;
199
+ }
200
+ .clear-btn {
201
+ font-size: 11px;
202
+ font-weight: 500;
203
+ color: var(--text-dim);
204
+ background: none;
205
+ border: 1px solid var(--border);
206
+ padding: 5px 12px;
207
+ border-radius: 6px;
208
+ cursor: pointer;
209
+ transition: color 0.15s, border-color 0.15s;
210
+ }
211
+ .clear-btn:hover { color: var(--white); border-color: var(--border2); }
212
+
213
+ /* Content */
214
+ .content {
215
+ flex: 1;
216
+ overflow: hidden;
217
+ position: relative;
218
+ }
219
+
220
+ /* ── PAGES ── */
221
+ .page {
222
+ display: none;
223
+ height: 100%;
224
+ overflow-y: auto;
225
+ padding: 24px 28px;
226
+ flex-direction: column;
227
+ gap: 20px;
228
+ }
229
+ .page.active { display: flex; }
230
+
231
+ /* ── ACTIVITY LOG PAGE ── */
232
+ #page-activity {
233
+ padding: 0;
234
+ }
235
+ #page-activity .log-toolbar {
236
+ display: flex;
237
+ align-items: center;
238
+ justify-content: space-between;
239
+ padding: 14px 28px;
240
+ border-bottom: 1px solid var(--border);
241
+ flex-shrink: 0;
242
+ gap: 12px;
243
+ }
244
+ .filter-btns {
245
+ display: flex;
246
+ gap: 4px;
247
+ }
248
+ .filter-btn {
249
+ font-size: 11px;
250
+ font-weight: 500;
251
+ padding: 5px 12px;
252
+ border-radius: 5px;
253
+ border: 1px solid var(--border);
254
+ background: none;
255
+ color: var(--text-muted);
256
+ cursor: pointer;
257
+ transition: all 0.15s;
258
+ }
259
+ .filter-btn:hover { color: var(--white); border-color: var(--border2); }
260
+ .filter-btn.active { background: var(--white); color: #000; border-color: var(--white); }
261
+
262
+ .scroll-lock-btn {
263
+ display: flex;
264
+ align-items: center;
265
+ gap: 6px;
266
+ font-size: 11px;
267
+ color: var(--text-dim);
268
+ background: none;
269
+ border: 1px solid var(--border);
270
+ padding: 5px 10px;
271
+ border-radius: 5px;
272
+ cursor: pointer;
273
+ transition: all 0.15s;
274
+ }
275
+ .scroll-lock-btn:hover { color: var(--text-muted); }
276
+ .scroll-lock-btn.locked { color: var(--white); border-color: var(--border2); }
277
+ .scroll-lock-btn svg { width: 12px; height: 12px; }
278
+
279
+ .log-list {
280
+ flex: 1;
281
+ overflow-y: auto;
282
+ padding: 0;
283
+ }
284
+ .log-entry {
285
+ display: flex;
286
+ align-items: flex-start;
287
+ gap: 0;
288
+ padding: 11px 28px;
289
+ border-bottom: 1px solid var(--border);
290
+ font-family: 'JetBrains Mono', monospace;
291
+ font-size: 12px;
292
+ line-height: 1.6;
293
+ transition: background 0.1s;
294
+ }
295
+ .log-entry:hover { background: var(--surface2); }
296
+ .log-entry-time {
297
+ color: var(--text-dim);
298
+ min-width: 80px;
299
+ flex-shrink: 0;
300
+ margin-right: 16px;
301
+ }
302
+ .log-entry-badge {
303
+ display: inline-flex;
304
+ align-items: center;
305
+ font-size: 9px;
306
+ font-weight: 700;
307
+ letter-spacing: 0.08em;
308
+ text-transform: uppercase;
309
+ padding: 2px 6px;
310
+ border-radius: 3px;
311
+ margin-right: 12px;
312
+ flex-shrink: 0;
313
+ min-width: 60px;
314
+ justify-content: center;
315
+ font-family: 'Inter', sans-serif;
316
+ }
317
+ .badge-task { background: rgba(255,255,255,0.08); color: var(--task); }
318
+ .badge-response { background: rgba(255,255,255,0.06); color: var(--response); }
319
+ .badge-error { background: rgba(255,85,85,0.12); color: var(--error); }
320
+ .badge-system { background: rgba(255,255,255,0.04); color: var(--system); }
321
+ .log-entry-msg { color: var(--text); word-break: break-word; }
322
+
323
+ .log-empty {
324
+ display: flex;
325
+ flex-direction: column;
326
+ align-items: center;
327
+ justify-content: center;
328
+ height: 100%;
329
+ gap: 12px;
330
+ color: var(--text-dim);
331
+ }
332
+ .log-empty svg { width: 40px; height: 40px; stroke: var(--text-dim); fill: none; stroke-width: 1; stroke-linecap: round; stroke-linejoin: round; }
333
+ .log-empty p { font-size: 13px; }
334
+
335
+ /* ── STATS PAGE ── */
336
+ .stats-grid {
337
+ display: grid;
338
+ grid-template-columns: repeat(2, 1fr);
339
+ gap: 16px;
340
+ }
341
+ .stat-card {
342
+ background: var(--surface);
343
+ border: 1px solid var(--border);
344
+ border-radius: 10px;
345
+ padding: 22px 24px;
346
+ display: flex;
347
+ flex-direction: column;
348
+ gap: 8px;
349
+ }
350
+ .stat-card.full { grid-column: 1 / -1; }
351
+ .stat-card-label {
352
+ font-size: 11px;
353
+ font-weight: 600;
354
+ color: var(--text-dim);
355
+ letter-spacing: 0.08em;
356
+ text-transform: uppercase;
357
+ }
358
+ .stat-card-value {
359
+ font-size: 36px;
360
+ font-weight: 700;
361
+ color: var(--white);
362
+ font-family: 'JetBrains Mono', monospace;
363
+ line-height: 1;
364
+ }
365
+ .stat-card-sub {
366
+ font-size: 12px;
367
+ color: var(--text-muted);
368
+ }
369
+
370
+ .progress-bar-wrap {
371
+ height: 4px;
372
+ background: var(--surface3);
373
+ border-radius: 2px;
374
+ overflow: hidden;
375
+ margin-top: 4px;
376
+ }
377
+ .progress-bar {
378
+ height: 100%;
379
+ background: var(--white);
380
+ border-radius: 2px;
381
+ transition: width 0.6s ease;
382
+ min-width: 2px;
383
+ }
384
+ .progress-bar.error { background: var(--error); }
385
+
386
+ /* Session Info card */
387
+ .info-list { display: flex; flex-direction: column; gap: 12px; }
388
+ .info-row {
389
+ display: flex;
390
+ justify-content: space-between;
391
+ align-items: center;
392
+ font-size: 13px;
393
+ }
394
+ .info-row-label { color: var(--text-muted); }
395
+ .info-row-value {
396
+ font-family: 'JetBrains Mono', monospace;
397
+ font-size: 12px;
398
+ color: var(--white);
399
+ background: var(--surface2);
400
+ padding: 3px 8px;
401
+ border-radius: 4px;
402
+ }
403
+
404
+ /* ── ABOUT PAGE ── */
405
+ .about-section { max-width: 580px; }
406
+ .about-hero {
407
+ background: var(--surface);
408
+ border: 1px solid var(--border);
409
+ border-radius: 12px;
410
+ padding: 32px;
411
+ display: flex;
412
+ flex-direction: column;
413
+ gap: 12px;
414
+ margin-bottom: 20px;
415
+ }
416
+ .about-hero h2 { font-size: 22px; color: var(--white); }
417
+ .about-hero p { font-size: 13px; color: var(--text-muted); line-height: 1.7; }
418
+ .about-links { display: flex; gap: 10px; margin-top: 4px; }
419
+ .about-link {
420
+ font-size: 12px;
421
+ font-weight: 500;
422
+ color: var(--text-muted);
423
+ background: var(--surface2);
424
+ border: 1px solid var(--border);
425
+ padding: 7px 14px;
426
+ border-radius: 6px;
427
+ text-decoration: none;
428
+ transition: all 0.15s;
429
+ display: inline-flex;
430
+ align-items: center;
431
+ gap: 6px;
432
+ }
433
+ .about-link:hover { color: var(--white); border-color: var(--border2); }
434
+
435
+ .about-card {
436
+ background: var(--surface);
437
+ border: 1px solid var(--border);
438
+ border-radius: 10px;
439
+ padding: 20px 24px;
440
+ margin-bottom: 16px;
441
+ }
442
+ .about-card h3 { font-size: 13px; font-weight: 600; color: var(--white); margin-bottom: 10px; }
443
+ .about-card ul { display: flex; flex-direction: column; gap: 7px; padding-left: 16px; }
444
+ .about-card li { font-size: 13px; color: var(--text-muted); line-height: 1.5; }
445
+
446
+ /* Scrollbar */
447
+ ::-webkit-scrollbar { width: 4px; height: 4px; }
448
+ ::-webkit-scrollbar-track { background: transparent; }
449
+ ::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 2px; }
450
+ ::-webkit-scrollbar-thumb:hover { background: #555; }
451
+
452
+ /* Transitions */
453
+ .log-entry { animation: fadeIn 0.2s ease; }
454
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
13
455
  </style>
14
456
  </head>
15
457
  <body>
16
- <h1>nvstudio-mcp : Activity Dashboard (Port 3055)</h1>
17
- <div id="log-container"></div>
18
-
19
- <script src="/socket.io/socket.io.js"></script>
20
- <script>
21
- const socket = io();
22
- const container = document.getElementById('log-container');
23
-
24
- socket.on('log', (msg) => {
25
- const el = document.createElement('div');
26
- el.className = 'log-entry';
27
- const time = new Date().toLocaleTimeString();
28
- el.innerHTML = `<span class="log-time">[${time}]</span>${msg}`;
29
- container.appendChild(el);
30
- container.scrollTop = container.scrollHeight; // Auto-scroll
458
+ <div class="layout">
459
+
460
+ <!-- ── SIDEBAR ── -->
461
+ <aside class="sidebar">
462
+ <div class="sidebar-logo">
463
+ <div class="logo-mark">
464
+ <div class="logo-icon">
465
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
466
+ <path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/>
467
+ </svg>
468
+ </div>
469
+ <div>
470
+ <div class="logo-name">nvstudio-mcp</div>
471
+ <div class="logo-sub">Bridge Server</div>
472
+ </div>
473
+ </div>
474
+ </div>
475
+
476
+ <div class="sidebar-status">
477
+ <div class="status-dot waiting" id="status-dot"></div>
478
+ <span class="status-label" id="status-label">Waiting</span>
479
+ </div>
480
+
481
+ <nav class="sidebar-nav">
482
+ <div class="nav-group-label">Monitor</div>
483
+ <div class="nav-item active" data-page="activity">
484
+ <svg viewBox="0 0 24 24"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
485
+ Activity Log
486
+ </div>
487
+ <div class="nav-item" data-page="stats">
488
+ <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>
489
+ Statistics
490
+ </div>
491
+
492
+ <div class="nav-group-label" style="margin-top:12px;">Info</div>
493
+ <div class="nav-item" data-page="about">
494
+ <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>
495
+ About
496
+ </div>
497
+ </nav>
498
+
499
+ <div class="sidebar-footer">
500
+ <div class="version" id="pkg-version">v1.0.2</div>
501
+ </div>
502
+ </aside>
503
+
504
+ <!-- ── MAIN ── -->
505
+ <main class="main">
506
+ <div class="topbar">
507
+ <span class="page-title" id="topbar-title">Activity Log</span>
508
+ <div class="topbar-right">
509
+ <span class="topbar-badge" id="log-count">0 events</span>
510
+ <button class="clear-btn" id="clear-btn">Clear</button>
511
+ </div>
512
+ </div>
513
+
514
+ <div class="content">
515
+
516
+ <!-- Activity Log Page -->
517
+ <div class="page active" id="page-activity" style="padding:0; gap:0;">
518
+ <div class="log-toolbar">
519
+ <div class="filter-btns">
520
+ <button class="filter-btn active" data-filter="all">All</button>
521
+ <button class="filter-btn" data-filter="task">Task</button>
522
+ <button class="filter-btn" data-filter="response">Response</button>
523
+ <button class="filter-btn" data-filter="error">Error</button>
524
+ <button class="filter-btn" data-filter="system">System</button>
525
+ </div>
526
+ <button class="scroll-lock-btn locked" id="scroll-lock-btn">
527
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
528
+ <line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/>
529
+ </svg>
530
+ Auto Scroll
531
+ </button>
532
+ </div>
533
+ <div class="log-list" id="log-list">
534
+ <div class="log-empty" id="log-empty">
535
+ <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg>
536
+ <p>Menunggu aktivitas dari Roblox Studio...</p>
537
+ </div>
538
+ </div>
539
+ </div>
540
+
541
+ <!-- Statistics Page -->
542
+ <div class="page" id="page-stats">
543
+ <div class="stats-grid">
544
+ <div class="stat-card">
545
+ <div class="stat-card-label">Total Tasks</div>
546
+ <div class="stat-card-value" id="stat-total">0</div>
547
+ <div class="stat-card-sub">Sejak server dimulai</div>
548
+ </div>
549
+ <div class="stat-card">
550
+ <div class="stat-card-label">Success Rate</div>
551
+ <div class="stat-card-value" id="stat-rate">—</div>
552
+ <div class="progress-bar-wrap">
553
+ <div class="progress-bar" id="bar-success" style="width:0%"></div>
554
+ </div>
555
+ </div>
556
+ <div class="stat-card">
557
+ <div class="stat-card-label">Succeeded</div>
558
+ <div class="stat-card-value" id="stat-success">0</div>
559
+ <div class="stat-card-sub">Perintah berhasil</div>
560
+ </div>
561
+ <div class="stat-card">
562
+ <div class="stat-card-label">Failed</div>
563
+ <div class="stat-card-value" id="stat-error">0</div>
564
+ <div class="stat-card-sub">Perintah gagal</div>
565
+ <div class="progress-bar-wrap">
566
+ <div class="progress-bar error" id="bar-error" style="width:0%"></div>
567
+ </div>
568
+ </div>
569
+ <div class="stat-card full">
570
+ <div class="stat-card-label">Session Info</div>
571
+ <div class="info-list">
572
+ <div class="info-row">
573
+ <span class="info-row-label">Session dimulai</span>
574
+ <span class="info-row-value" id="info-session">—</span>
575
+ </div>
576
+ <div class="info-row">
577
+ <span class="info-row-label">Port aktif</span>
578
+ <span class="info-row-value">3055</span>
579
+ </div>
580
+ <div class="info-row">
581
+ <span class="info-row-label">Studio status</span>
582
+ <span class="info-row-value" id="info-studio">Disconnected</span>
583
+ </div>
584
+ <div class="info-row">
585
+ <span class="info-row-label">Uptime</span>
586
+ <span class="info-row-value" id="info-uptime">—</span>
587
+ </div>
588
+ </div>
589
+ </div>
590
+ </div>
591
+ </div>
592
+
593
+ <!-- About Page -->
594
+ <div class="page" id="page-about">
595
+ <div class="about-section">
596
+ <div class="about-hero">
597
+ <h2>nvstudio-mcp</h2>
598
+ <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>. AI Anda dapat membaca, menulis, dan mengeksekusi script layaknya Co-Pilot sejati.</p>
599
+ <div class="about-links">
600
+ <a class="about-link" href="https://github.com/Adiholick/nvstudio-mcp" target="_blank">
601
+ <svg width="13" height="13" 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>
602
+ GitHub
603
+ </a>
604
+ <a class="about-link" href="https://www.npmjs.com/package/@adiholick/nvstudio-mcp" target="_blank">
605
+ <svg width="13" height="13" 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>
606
+ NPM Package
607
+ </a>
608
+ </div>
609
+ </div>
610
+
611
+ <div class="about-card">
612
+ <h3>Cara Kerja</h3>
613
+ <ul>
614
+ <li>Plugin Roblox Studio melakukan polling ke server MCP Bridge ini setiap detik.</li>
615
+ <li>AI Agent mengirimkan perintah melalui tool <code>call_mcp_tool</code>.</li>
616
+ <li>Bridge meneruskan perintah ke Studio dan mengembalikan hasilnya ke AI.</li>
617
+ <li>Dashboard ini menampilkan semua aktivitas secara real-time via WebSocket.</li>
618
+ </ul>
619
+ </div>
620
+
621
+ <div class="about-card">
622
+ <h3>Commands yang Tersedia</h3>
623
+ <ul>
624
+ <li><code>get_children</code> — Daftar children dari instance</li>
625
+ <li><code>get_script_source</code> — Baca source code script</li>
626
+ <li><code>update_script_source</code> — Tulis/perbarui source code</li>
627
+ <li><code>rollback_script</code> — Rollback ke versi sebelumnya</li>
628
+ <li><code>get_logs</code> — Ambil log output Studio</li>
629
+ </ul>
630
+ </div>
631
+ </div>
632
+ </div>
633
+
634
+ </div>
635
+ </main>
636
+ </div>
637
+
638
+ <script src="/socket.io/socket.io.js"></script>
639
+ <script>
640
+ const socket = io();
641
+ let allLogs = [];
642
+ let activeFilter = 'all';
643
+ let autoScroll = true;
644
+ let sessionStartTime = null;
645
+
646
+ const logList = document.getElementById('log-list');
647
+ const logEmpty = document.getElementById('log-empty');
648
+ const logCount = document.getElementById('log-count');
649
+ const statusDot = document.getElementById('status-dot');
650
+ const statusLabel = document.getElementById('status-label');
651
+
652
+ /* ── Navigation ── */
653
+ const navItems = document.querySelectorAll('.nav-item');
654
+ const pages = document.querySelectorAll('.page');
655
+ const topbarTitle = document.getElementById('topbar-title');
656
+ const clearBtn = document.getElementById('clear-btn');
657
+ const pageTitles = { activity: 'Activity Log', stats: 'Statistics', about: 'About' };
658
+
659
+ navItems.forEach(item => {
660
+ item.addEventListener('click', () => {
661
+ const page = item.dataset.page;
662
+ navItems.forEach(n => n.classList.remove('active'));
663
+ pages.forEach(p => p.classList.remove('active'));
664
+ item.classList.add('active');
665
+ document.getElementById('page-' + page).classList.add('active');
666
+ topbarTitle.textContent = pageTitles[page] || page;
667
+
668
+ const isActivity = page === 'activity';
669
+ clearBtn.style.display = isActivity ? '' : 'none';
670
+ logCount.style.display = isActivity ? '' : 'none';
31
671
  });
32
- </script>
672
+ });
673
+
674
+ /* ── Clear ── */
675
+ clearBtn.addEventListener('click', () => {
676
+ allLogs = [];
677
+ renderLogs();
678
+ });
679
+
680
+ /* ── Filter ── */
681
+ document.querySelectorAll('.filter-btn').forEach(btn => {
682
+ btn.addEventListener('click', () => {
683
+ document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
684
+ btn.classList.add('active');
685
+ activeFilter = btn.dataset.filter;
686
+ renderLogs();
687
+ });
688
+ });
689
+
690
+ /* ── Scroll Lock ── */
691
+ const scrollLockBtn = document.getElementById('scroll-lock-btn');
692
+ scrollLockBtn.addEventListener('click', () => {
693
+ autoScroll = !autoScroll;
694
+ scrollLockBtn.classList.toggle('locked', autoScroll);
695
+ });
696
+
697
+ /* ── Render ── */
698
+ function renderLogs() {
699
+ const filtered = activeFilter === 'all' ? allLogs : allLogs.filter(l => l.type === activeFilter);
700
+ logCount.textContent = `${allLogs.length} event${allLogs.length !== 1 ? 's' : ''}`;
701
+
702
+ if (filtered.length === 0) {
703
+ logEmpty.style.display = 'flex';
704
+ // Remove all entries but keep empty
705
+ [...logList.children].forEach(c => { if (c !== logEmpty) c.remove(); });
706
+ return;
707
+ }
708
+ logEmpty.style.display = 'none';
709
+
710
+ // Full re-render
711
+ const frag = document.createDocumentFragment();
712
+ filtered.forEach(log => {
713
+ const el = createLogEl(log);
714
+ frag.appendChild(el);
715
+ });
716
+ logList.innerHTML = '';
717
+ logList.appendChild(logEmpty);
718
+ logEmpty.style.display = 'none';
719
+ logList.appendChild(frag);
720
+
721
+ if (autoScroll) logList.scrollTop = 0; // newest on top
722
+ }
723
+
724
+ function createLogEl(log) {
725
+ const el = document.createElement('div');
726
+ el.className = 'log-entry';
727
+ el.dataset.type = log.type;
728
+ const badges = { task: 'TASK', response: 'RESP', error: 'ERROR', system: 'SYS' };
729
+ el.innerHTML = `
730
+ <span class="log-entry-time">${log.time}</span>
731
+ <span class="log-entry-badge badge-${log.type}">${badges[log.type] || log.type}</span>
732
+ <span class="log-entry-msg">${escapeHtml(log.message)}</span>
733
+ `;
734
+ return el;
735
+ }
736
+
737
+ function escapeHtml(str) {
738
+ return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
739
+ }
740
+
741
+ function prependLog(log) {
742
+ allLogs.unshift(log);
743
+ if (allLogs.length > 500) allLogs.pop();
744
+ logCount.textContent = `${allLogs.length} event${allLogs.length !== 1 ? 's' : ''}`;
745
+
746
+ if (activeFilter !== 'all' && log.type !== activeFilter) return;
747
+
748
+ logEmpty.style.display = 'none';
749
+ const el = createLogEl(log);
750
+ const firstEntry = logList.querySelector('.log-entry');
751
+ if (firstEntry) logList.insertBefore(el, firstEntry);
752
+ else logList.appendChild(el);
753
+
754
+ if (autoScroll) logList.scrollTop = 0;
755
+ }
756
+
757
+ /* ── Stats ── */
758
+ function updateStats(s) {
759
+ document.getElementById('stat-total').textContent = s.totalTasks;
760
+ document.getElementById('stat-success').textContent = s.successCount;
761
+ document.getElementById('stat-error').textContent = s.errorCount;
762
+
763
+ const rate = s.totalTasks > 0 ? Math.round((s.successCount / s.totalTasks) * 100) : null;
764
+ document.getElementById('stat-rate').textContent = rate !== null ? rate + '%' : '—';
765
+ document.getElementById('bar-success').style.width = (rate || 0) + '%';
766
+ const errRate = s.totalTasks > 0 ? Math.round((s.errorCount / s.totalTasks) * 100) : 0;
767
+ document.getElementById('bar-error').style.width = errRate + '%';
768
+
769
+ if (s.sessionStart) {
770
+ sessionStartTime = new Date(s.sessionStart);
771
+ document.getElementById('info-session').textContent = sessionStartTime.toLocaleString('id-ID');
772
+ }
773
+ document.getElementById('info-studio').textContent = s.studioConnected ? 'Connected ✓' : 'Disconnected';
774
+ }
775
+
776
+ /* ── Uptime ── */
777
+ setInterval(() => {
778
+ if (!sessionStartTime) return;
779
+ const diff = Math.floor((Date.now() - sessionStartTime) / 1000);
780
+ const h = String(Math.floor(diff / 3600)).padStart(2, '0');
781
+ const m = String(Math.floor((diff % 3600) / 60)).padStart(2, '0');
782
+ const s = String(diff % 60).padStart(2, '0');
783
+ document.getElementById('info-uptime').textContent = `${h}:${m}:${s}`;
784
+ }, 1000);
785
+
786
+ /* ── Studio Status ── */
787
+ function setStudioStatus(connected) {
788
+ statusDot.className = 'status-dot ' + (connected ? 'connected' : 'waiting');
789
+ statusLabel.textContent = connected ? 'Studio Connected' : 'Waiting';
790
+ document.getElementById('info-studio').textContent = connected ? 'Connected ✓' : 'Disconnected';
791
+ }
792
+
793
+ /* ── Socket Events ── */
794
+ socket.on('connect', () => {});
795
+ socket.on('disconnect', () => setStudioStatus(false));
796
+
797
+ socket.on('init', ({ logs, stats }) => {
798
+ allLogs = logs || [];
799
+ renderLogs();
800
+ if (stats) {
801
+ updateStats(stats);
802
+ if (stats.studioConnected) setStudioStatus(true);
803
+ }
804
+ });
805
+
806
+ socket.on('log', (log) => {
807
+ prependLog(log);
808
+ });
809
+
810
+ socket.on('stats', (s) => {
811
+ updateStats(s);
812
+ });
813
+
814
+ socket.on('studio-status', ({ connected }) => {
815
+ setStudioStatus(connected);
816
+ });
817
+ </script>
33
818
  </body>
34
819
  </html>