@adiholick/nvstudio-mcp 1.0.1 → 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.
- package/dist/index.js +1 -0
- package/dist/roblox-bridge.js +45 -5
- package/package.json +2 -1
- package/public/index.html +808 -23
package/dist/index.js
CHANGED
package/dist/roblox-bridge.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
io
|
|
48
|
-
|
|
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
package/public/index.html
CHANGED
|
@@ -1,34 +1,819 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="
|
|
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 |
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
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>
|