thyscript 1.0.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.
Files changed (49) hide show
  1. package/bin/thy-node.js +332 -0
  2. package/bin/thy.js +828 -0
  3. package/bin/thys.js +25 -0
  4. package/examples/calculator.thy +274 -0
  5. package/examples/calculator.thy.js +628 -0
  6. package/examples/hello.thy +34 -0
  7. package/examples/login.thy +382 -0
  8. package/examples/login.thy.js +710 -0
  9. package/package.json +54 -0
  10. package/scripts/_class.js +18 -0
  11. package/scripts/_class2.js +11 -0
  12. package/scripts/_class3.js +21 -0
  13. package/scripts/_dstruct.js +20 -0
  14. package/scripts/_dstruct2.js +24 -0
  15. package/scripts/_scaffold.js +16 -0
  16. package/scripts/_scaffold2.js +16 -0
  17. package/scripts/_scaffold3.js +26 -0
  18. package/scripts/_tpl_compile.js +13 -0
  19. package/scripts/_tpl_test.js +9 -0
  20. package/scripts/_typeof.js +26 -0
  21. package/scripts/_typeof2.js +25 -0
  22. package/scripts/patch_genmethod.js +44 -0
  23. package/scripts/smoke_test.js +167 -0
  24. package/scripts/smoke_test2.js +151 -0
  25. package/scripts/test_control_panel.js +58 -0
  26. package/src/compiler/CodeGenerator.js +1449 -0
  27. package/src/compiler/JITCompiler.js +325 -0
  28. package/src/database/DatabaseEngine.js +931 -0
  29. package/src/errors/ErrorDetector.js +305 -0
  30. package/src/errors/ErrorMessages.js +497 -0
  31. package/src/index.js +169 -0
  32. package/src/lexer/Lexer.js +342 -0
  33. package/src/lexer/TokenTypes.js +1234 -0
  34. package/src/memory/MemoryManager.js +283 -0
  35. package/src/parser/AST.js +606 -0
  36. package/src/parser/Parser.js +1942 -0
  37. package/src/runtime/ThyRuntime.js +265 -0
  38. package/src/runtime/stdlib/ArrayLib.js +207 -0
  39. package/src/runtime/stdlib/FileLib.js +168 -0
  40. package/src/runtime/stdlib/MathLib.js +411 -0
  41. package/src/runtime/stdlib/StringLib.js +387 -0
  42. package/src/server/ControlPanel.js +1188 -0
  43. package/src/server/HTMLCSSEngine.js +866 -0
  44. package/src/server/SecurityEngine.js +714 -0
  45. package/src/server/ServerEngine.js +738 -0
  46. package/tests/test1_math.thy +82 -0
  47. package/tests/test2_oop.thy +133 -0
  48. package/tests/test3_server.thy +143 -0
  49. package/tests/test4_functions.thy +159 -0
@@ -0,0 +1,1188 @@
1
+ 'use strict';
2
+ /**
3
+ * ThyScript Control Panel Engine
4
+ * Turns the host computer into a live, remote-accessible ThyScript server
5
+ * with on-the-fly compilation, execution, live web IDE, system monitor,
6
+ * and service orchestrator without needing manual disk compilation.
7
+ */
8
+
9
+ const http = require('http');
10
+ const express = require('express');
11
+ const path = require('path');
12
+ const fs = require('fs');
13
+ const os = require('os');
14
+ const { performance } = require('perf_hooks');
15
+
16
+ const Lexer = require('../lexer/Lexer');
17
+ const Parser = require('../parser/Parser');
18
+ const JITCompiler = require('../compiler/JITCompiler');
19
+ const ThyRuntime = require('../runtime/ThyRuntime');
20
+ const ErrorDetector = require('../errors/ErrorDetector');
21
+
22
+ class ControlPanel {
23
+ constructor(options = {}) {
24
+ this.options = Object.assign({
25
+ port: 7777,
26
+ host: '0.0.0.0',
27
+ workspaceDir: process.cwd(),
28
+ authToken: options.authToken || null,
29
+ cors: true,
30
+ openBrowser: false,
31
+ }, options);
32
+
33
+ this.app = express();
34
+ this.server = null;
35
+ this.runtime = null;
36
+ this.activeServices = new Map(); // id -> { name, port, startTime, status, logs }
37
+ this.runHistory = [];
38
+ this.startTime = Date.now();
39
+ }
40
+
41
+ /**
42
+ * Get all local network IP addresses
43
+ */
44
+ getNetworkAddresses() {
45
+ const interfaces = os.networkInterfaces();
46
+ const addresses = [];
47
+ for (const name of Object.keys(interfaces)) {
48
+ for (const iface of interfaces[name] || []) {
49
+ if (iface.family === 'IPv4' && !iface.internal) {
50
+ addresses.push({ interface: name, address: iface.address });
51
+ }
52
+ }
53
+ }
54
+ return addresses;
55
+ }
56
+
57
+ /**
58
+ * Get current live hardware and process metrics
59
+ */
60
+ getSystemMetrics() {
61
+ const totalMem = os.totalmem();
62
+ const freeMem = os.freemem();
63
+ const usedMem = totalMem - freeMem;
64
+ const memUsage = process.memoryUsage();
65
+ const cpus = os.cpus();
66
+
67
+ return {
68
+ os: {
69
+ platform: os.platform(),
70
+ arch: os.arch(),
71
+ release: os.release(),
72
+ hostname: os.hostname(),
73
+ uptime: os.uptime(),
74
+ },
75
+ cpu: {
76
+ model: cpus[0] ? cpus[0].model : 'Unknown',
77
+ cores: cpus.length,
78
+ speed: cpus[0] ? cpus[0].speed : 0,
79
+ loadAvg: os.loadavg(),
80
+ },
81
+ memory: {
82
+ totalBytes: totalMem,
83
+ freeBytes: freeMem,
84
+ usedBytes: usedMem,
85
+ totalMB: +(totalMem / (1024 * 1024)).toFixed(1),
86
+ usedMB: +(usedMem / (1024 * 1024)).toFixed(1),
87
+ freeMB: +(freeMem / (1024 * 1024)).toFixed(1),
88
+ usagePercent: +((usedMem / totalMem) * 100).toFixed(1),
89
+ processHeapUsedMB: +(memUsage.heapUsed / (1024 * 1024)).toFixed(2),
90
+ processHeapTotalMB: +(memUsage.heapTotal / (1024 * 1024)).toFixed(2),
91
+ processRssMB: +(memUsage.rss / (1024 * 1024)).toFixed(2),
92
+ },
93
+ server: {
94
+ nodeVersion: process.version,
95
+ thyscriptVersion: '1.0.0',
96
+ uptimeSeconds: Math.floor((Date.now() - this.startTime) / 1000),
97
+ activeServicesCount: this.activeServices.size,
98
+ historyCount: this.runHistory.length,
99
+ }
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Scan workspace for ThyScript files
105
+ */
106
+ getWorkspaceFiles(dir = this.options.workspaceDir) {
107
+ const results = [];
108
+ const validExts = new Set(['.thy', '.tson', '.tyoml']);
109
+
110
+ const scan = (currentDir, relBase = '') => {
111
+ try {
112
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
113
+ for (const entry of entries) {
114
+ if (entry.name === 'node_modules' || entry.name === '.git') continue;
115
+ const fullPath = path.join(currentDir, entry.name);
116
+ const relPath = path.join(relBase, entry.name).replace(/\\/g, '/');
117
+
118
+ if (entry.isDirectory()) {
119
+ scan(fullPath, relPath);
120
+ } else {
121
+ const ext = path.extname(entry.name).toLowerCase();
122
+ if (validExts.has(ext)) {
123
+ const stat = fs.statSync(fullPath);
124
+ results.push({
125
+ name: entry.name,
126
+ path: relPath,
127
+ fullPath,
128
+ ext,
129
+ sizeBytes: stat.size,
130
+ modifiedAt: stat.mtime.toISOString(),
131
+ });
132
+ }
133
+ }
134
+ }
135
+ } catch (_) {}
136
+ };
137
+
138
+ scan(dir);
139
+ return results;
140
+ }
141
+
142
+ /**
143
+ * Execute ThyScript source code on the fly in memory (Zero Compile Step on disk)
144
+ */
145
+ async executeOnTheFly(source, filename = '<control-panel>') {
146
+ const logs = [];
147
+ const originalLog = console.log;
148
+ const originalWarn = console.warn;
149
+ const originalError = console.error;
150
+
151
+ // Capture execution stdout/stderr
152
+ const capture = (type, ...args) => {
153
+ const msg = args.map(a => (typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a))).join(' ');
154
+ logs.push({ type, message: msg, time: new Date().toLocaleTimeString() });
155
+ };
156
+
157
+ const t0 = performance.now();
158
+ let lexTime = 0, parseTime = 0, compileTime = 0, execTime = 0;
159
+ let tokens = [], ast = null, compiledJs = '';
160
+
161
+ try {
162
+ // 1. Lexing
163
+ const tLex0 = performance.now();
164
+ const lexer = new Lexer(source, filename);
165
+ tokens = lexer.tokenize();
166
+ lexTime = +(performance.now() - tLex0).toFixed(2);
167
+
168
+ // 2. Parsing
169
+ const tParse0 = performance.now();
170
+ const parser = new Parser(tokens, source, filename);
171
+ const parseResult = parser.parse();
172
+ ast = parseResult.ast || parseResult;
173
+ parseTime = +(performance.now() - tParse0).toFixed(2);
174
+
175
+ // 3. Error Detector Scan
176
+ const detector = new ErrorDetector();
177
+ const scanResult = detector.scan(tokens, ast, source, filename);
178
+
179
+ // 4. JIT Compile to in-memory JS
180
+ const tCompile0 = performance.now();
181
+ const compiler = new JITCompiler(ast, source, filename);
182
+ compiledJs = compiler.compile();
183
+ compileTime = +(performance.now() - tCompile0).toFixed(2);
184
+
185
+ // 5. Initialize Runtime & Custom Sandbox
186
+ if (!this.runtime) {
187
+ this.runtime = new ThyRuntime({ verbose: false });
188
+ await this.runtime.init();
189
+ }
190
+
191
+ const memBefore = process.memoryUsage().heapUsed;
192
+
193
+ // Custom context with log redirection
194
+ const customContext = {
195
+ print: (...args) => capture('log', ...args),
196
+ __thy_print: (...args) => capture('log', ...args),
197
+ };
198
+
199
+ const context = this.runtime.buildContext(customContext);
200
+
201
+ // Hook console and process.stdout/stderr temporarily during execution
202
+ const originalStdout = process.stdout.write;
203
+ const originalStderr = process.stderr.write;
204
+
205
+ process.stdout.write = (chunk, enc, cb) => {
206
+ const s = String(chunk);
207
+ if (s.trim()) capture('log', s.trimEnd());
208
+ return originalStdout.call(process.stdout, chunk, enc, cb);
209
+ };
210
+ process.stderr.write = (chunk, enc, cb) => {
211
+ const s = String(chunk);
212
+ if (s.trim()) capture('error', s.trimEnd());
213
+ return originalStderr.call(process.stderr, chunk, enc, cb);
214
+ };
215
+
216
+ console.log = (...args) => { originalLog(...args); capture('log', ...args); };
217
+ console.warn = (...args) => { originalWarn(...args); capture('warn', ...args); };
218
+ console.error = (...args) => { originalError(...args); capture('error', ...args); };
219
+
220
+ let execResult;
221
+ try {
222
+ const tExec0 = performance.now();
223
+ execResult = await compiler.execute(compiledJs, context);
224
+ execTime = +(performance.now() - tExec0).toFixed(2);
225
+ } finally {
226
+ process.stdout.write = originalStdout;
227
+ process.stderr.write = originalStderr;
228
+ console.log = originalLog;
229
+ console.warn = originalWarn;
230
+ console.error = originalError;
231
+ }
232
+
233
+ const memAfter = process.memoryUsage().heapUsed;
234
+ const totalTime = +(performance.now() - t0).toFixed(2);
235
+
236
+ const record = {
237
+ id: Date.now().toString(36),
238
+ filename,
239
+ success: true,
240
+ result: execResult !== undefined ? String(execResult) : null,
241
+ logs,
242
+ warnings: scanResult.warnings || [],
243
+ timings: {
244
+ lexMs: lexTime,
245
+ parseMs: parseTime,
246
+ compileMs: compileTime,
247
+ execMs: execTime,
248
+ totalMs: totalTime,
249
+ },
250
+ memoryDeltaKB: +((memAfter - memBefore) / 1024).toFixed(1),
251
+ timestamp: new Date().toISOString(),
252
+ };
253
+
254
+ this.runHistory.unshift(record);
255
+ if (this.runHistory.length > 50) this.runHistory.pop();
256
+
257
+ return record;
258
+ } catch (err) {
259
+ // Restore console on failure
260
+ console.log = originalLog;
261
+ console.warn = originalWarn;
262
+ console.error = originalError;
263
+
264
+ const totalTime = +(performance.now() - t0).toFixed(2);
265
+ const record = {
266
+ id: Date.now().toString(36),
267
+ filename,
268
+ success: false,
269
+ error: {
270
+ message: err.message,
271
+ stack: err.stack,
272
+ line: err.line || null,
273
+ column: err.column || null,
274
+ },
275
+ logs,
276
+ timings: {
277
+ lexMs: lexTime,
278
+ parseMs: parseTime,
279
+ compileMs: compileTime,
280
+ execMs: execTime,
281
+ totalMs: totalTime,
282
+ },
283
+ timestamp: new Date().toISOString(),
284
+ };
285
+
286
+ this.runHistory.unshift(record);
287
+ if (this.runHistory.length > 50) this.runHistory.pop();
288
+
289
+ return record;
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Setup Express routes and endpoints
295
+ */
296
+ setupRoutes() {
297
+ this.app.use(express.json({ limit: '10mb' }));
298
+ this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));
299
+
300
+ // CORS handler
301
+ this.app.use((req, res, next) => {
302
+ res.setHeader('Access-Control-Allow-Origin', '*');
303
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
304
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
305
+ if (req.method === 'OPTIONS') return res.sendStatus(200);
306
+ next();
307
+ });
308
+
309
+ // Optional Token Authentication Middleware
310
+ const authMiddleware = (req, res, next) => {
311
+ if (!this.options.authToken) return next();
312
+ const token = req.headers['authorization'] || req.query.token;
313
+ if (token === this.options.authToken || token === `Bearer ${this.options.authToken}`) {
314
+ return next();
315
+ }
316
+ return res.status(401).json({ success: false, message: 'Unauthorized. Invalid or missing token.' });
317
+ };
318
+
319
+ // ─── API: System Metrics ───────────────────────────────────────
320
+ this.app.get('/api/system', (req, res) => {
321
+ res.json({
322
+ success: true,
323
+ metrics: this.getSystemMetrics(),
324
+ networks: this.getNetworkAddresses(),
325
+ });
326
+ });
327
+
328
+ // ─── API: File Explorer ─────────────────────────────────────────
329
+ this.app.get('/api/files', authMiddleware, (req, res) => {
330
+ try {
331
+ const files = this.getWorkspaceFiles();
332
+ res.json({ success: true, count: files.length, files });
333
+ } catch (err) {
334
+ res.status(500).json({ success: false, message: err.message });
335
+ }
336
+ });
337
+
338
+ this.app.get('/api/files/read', authMiddleware, (req, res) => {
339
+ try {
340
+ const relPath = req.query.path;
341
+ if (!relPath) return res.status(400).json({ success: false, message: 'path query parameter is required' });
342
+ const absPath = path.resolve(this.options.workspaceDir, relPath);
343
+ if (!absPath.startsWith(path.resolve(this.options.workspaceDir))) {
344
+ return res.status(403).json({ success: false, message: 'Access denied: Path outside workspace' });
345
+ }
346
+ if (!fs.existsSync(absPath)) {
347
+ return res.status(404).json({ success: false, message: 'File not found' });
348
+ }
349
+ const content = fs.readFileSync(absPath, 'utf8');
350
+ res.json({ success: true, path: relPath, content });
351
+ } catch (err) {
352
+ res.status(500).json({ success: false, message: err.message });
353
+ }
354
+ });
355
+
356
+ this.app.post('/api/files/save', authMiddleware, (req, res) => {
357
+ try {
358
+ const { filePath, content } = req.body;
359
+ if (!filePath) return res.status(400).json({ success: false, message: 'filePath is required' });
360
+ const absPath = path.resolve(this.options.workspaceDir, filePath);
361
+ if (!absPath.startsWith(path.resolve(this.options.workspaceDir))) {
362
+ return res.status(403).json({ success: false, message: 'Access denied' });
363
+ }
364
+ fs.mkdirSync(path.dirname(absPath), { recursive: true });
365
+ fs.writeFileSync(absPath, content || '', 'utf8');
366
+ res.json({ success: true, message: 'File saved successfully', path: filePath });
367
+ } catch (err) {
368
+ res.status(500).json({ success: false, message: err.message });
369
+ }
370
+ });
371
+
372
+ // ─── API: On-The-Fly Code Runner (Zero Compile Step on Disk) ──
373
+ this.app.post('/api/run-code', authMiddleware, async (req, res) => {
374
+ try {
375
+ const { code, name } = req.body;
376
+ if (typeof code !== 'string') {
377
+ return res.status(400).json({ success: false, message: 'code string is required' });
378
+ }
379
+ const record = await this.executeOnTheFly(code, name || '<browser-repl>');
380
+ res.json(record);
381
+ } catch (err) {
382
+ res.status(500).json({ success: false, message: err.message });
383
+ }
384
+ });
385
+
386
+ this.app.post('/api/run-file', authMiddleware, async (req, res) => {
387
+ try {
388
+ const { filePath } = req.body;
389
+ if (!filePath) return res.status(400).json({ success: false, message: 'filePath is required' });
390
+ const workspaceRoot = path.resolve(this.options.workspaceDir);
391
+ const absPath = path.resolve(workspaceRoot, filePath);
392
+ // Confine file access to the workspace (mirrors /api/files/read & save)
393
+ if (absPath !== workspaceRoot && !absPath.startsWith(workspaceRoot + path.sep)) {
394
+ return res.status(403).json({ success: false, message: 'Access denied: Path outside workspace' });
395
+ }
396
+ if (!fs.existsSync(absPath)) {
397
+ return res.status(404).json({ success: false, message: 'File not found' });
398
+ }
399
+ const source = fs.readFileSync(absPath, 'utf8');
400
+ const record = await this.executeOnTheFly(source, path.basename(absPath));
401
+ res.json(record);
402
+ } catch (err) {
403
+ res.status(500).json({ success: false, message: err.message });
404
+ }
405
+ });
406
+
407
+ // ─── API: Run History ──────────────────────────────────────────
408
+ this.app.get('/api/history', authMiddleware, (req, res) => {
409
+ res.json({ success: true, history: this.runHistory });
410
+ });
411
+
412
+ // ─── Web UI: Main Control Panel Dashboard ───────────────────────
413
+ this.app.get('/', (req, res) => {
414
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
415
+ res.send(this.renderDashboardHtml());
416
+ });
417
+ }
418
+
419
+ /**
420
+ * HTML & CSS & Client JS for the Web Control Panel
421
+ */
422
+ renderDashboardHtml() {
423
+ const netAddresses = this.getNetworkAddresses();
424
+ const netHtml = netAddresses.map(n => `<li><span class="tag">${n.interface}</span> <strong>http://${n.address}:${this.options.port}</strong></li>`).join('');
425
+
426
+ return `<!DOCTYPE html>
427
+ <html lang="id">
428
+ <head>
429
+ <meta charset="UTF-8">
430
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
431
+ <title>ThyScript Server Control Panel</title>
432
+ <link rel="preconnect" href="https://fonts.googleapis.com">
433
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&family=Inter:wght@300;400;600;700;800&display=swap" rel="stylesheet">
434
+ <style>
435
+ :root {
436
+ --bg-base: #090a10;
437
+ --bg-card: rgba(18, 20, 32, 0.7);
438
+ --bg-card-hover: rgba(28, 31, 48, 0.8);
439
+ --border-subtle: rgba(255, 255, 255, 0.08);
440
+ --border-glow: rgba(99, 102, 241, 0.4);
441
+ --primary: #6366f1;
442
+ --primary-glow: #818cf8;
443
+ --accent-cyan: #06b6d4;
444
+ --accent-green: #10b981;
445
+ --accent-orange: #f59e0b;
446
+ --accent-red: #ef4444;
447
+ --text-main: #f8fafc;
448
+ --text-dim: #94a3b8;
449
+ --font-mono: 'JetBrains Mono', monospace;
450
+ --font-sans: 'Inter', sans-serif;
451
+ }
452
+
453
+ * { margin: 0; padding: 0; box-sizing: border-box; }
454
+ body {
455
+ background-color: var(--bg-base);
456
+ background-image:
457
+ radial-gradient(circle at 10% 20%, rgba(99, 102, 241, 0.12) 0%, transparent 40%),
458
+ radial-gradient(circle at 90% 80%, rgba(6, 182, 212, 0.1) 0%, transparent 40%);
459
+ color: var(--text-main);
460
+ font-family: var(--font-sans);
461
+ min-height: 100vh;
462
+ display: flex;
463
+ flex-direction: column;
464
+ }
465
+
466
+ /* Top Navigation Header */
467
+ header {
468
+ background: rgba(15, 17, 28, 0.85);
469
+ backdrop-filter: blur(20px);
470
+ border-bottom: 1px solid var(--border-subtle);
471
+ padding: 16px 28px;
472
+ display: flex;
473
+ align-items: center;
474
+ justify-content: space-between;
475
+ position: sticky;
476
+ top: 0;
477
+ z-index: 100;
478
+ }
479
+ .brand {
480
+ display: flex;
481
+ align-items: center;
482
+ gap: 12px;
483
+ }
484
+ .brand .logo-icon {
485
+ font-size: 26px;
486
+ background: linear-gradient(135deg, var(--primary), var(--accent-cyan));
487
+ -webkit-background-clip: text;
488
+ -webkit-text-fill-color: transparent;
489
+ font-weight: 900;
490
+ }
491
+ .brand h1 {
492
+ font-size: 19px;
493
+ font-weight: 700;
494
+ letter-spacing: -0.5px;
495
+ }
496
+ .brand span.badge {
497
+ background: rgba(99, 102, 241, 0.2);
498
+ color: var(--primary-glow);
499
+ border: 1px solid rgba(99, 102, 241, 0.35);
500
+ padding: 3px 8px;
501
+ border-radius: 20px;
502
+ font-size: 11px;
503
+ font-weight: 600;
504
+ font-family: var(--font-mono);
505
+ }
506
+ .header-actions {
507
+ display: flex;
508
+ align-items: center;
509
+ gap: 16px;
510
+ }
511
+ .status-pill {
512
+ display: flex;
513
+ align-items: center;
514
+ gap: 8px;
515
+ font-size: 13px;
516
+ background: rgba(16, 185, 129, 0.12);
517
+ border: 1px solid rgba(16, 185, 129, 0.3);
518
+ color: #34d399;
519
+ padding: 6px 12px;
520
+ border-radius: 20px;
521
+ font-weight: 500;
522
+ }
523
+ .status-dot {
524
+ width: 8px;
525
+ height: 8px;
526
+ border-radius: 50%;
527
+ background: #10b981;
528
+ box-shadow: 0 0 10px #10b981;
529
+ animation: pulse 2s infinite;
530
+ }
531
+ @keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.4; transform: scale(0.85); } }
532
+
533
+ /* Main Workspace Layout */
534
+ .main-layout {
535
+ display: grid;
536
+ grid-template-columns: 280px 1fr 340px;
537
+ gap: 20px;
538
+ padding: 24px;
539
+ flex: 1;
540
+ max-width: 1800px;
541
+ margin: 0 auto;
542
+ width: 100%;
543
+ }
544
+
545
+ /* Glass Cards */
546
+ .panel-card {
547
+ background: var(--bg-card);
548
+ backdrop-filter: blur(25px);
549
+ border: 1px solid var(--border-subtle);
550
+ border-radius: 18px;
551
+ display: flex;
552
+ flex-direction: column;
553
+ overflow: hidden;
554
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
555
+ }
556
+ .card-header {
557
+ padding: 16px 20px;
558
+ border-bottom: 1px solid var(--border-subtle);
559
+ display: flex;
560
+ align-items: center;
561
+ justify-content: space-between;
562
+ font-size: 14px;
563
+ font-weight: 600;
564
+ color: var(--text-dim);
565
+ letter-spacing: 0.5px;
566
+ text-transform: uppercase;
567
+ }
568
+ .card-header span.title {
569
+ color: var(--text-main);
570
+ display: flex;
571
+ align-items: center;
572
+ gap: 8px;
573
+ }
574
+
575
+ /* Left Sidebar: File Tree & Templates */
576
+ .file-list {
577
+ padding: 12px;
578
+ overflow-y: auto;
579
+ flex: 1;
580
+ list-style: none;
581
+ }
582
+ .file-item {
583
+ padding: 10px 14px;
584
+ border-radius: 10px;
585
+ margin-bottom: 4px;
586
+ cursor: pointer;
587
+ display: flex;
588
+ align-items: center;
589
+ justify-content: space-between;
590
+ transition: all 0.15s ease;
591
+ font-size: 13px;
592
+ color: var(--text-dim);
593
+ font-family: var(--font-mono);
594
+ }
595
+ .file-item:hover {
596
+ background: rgba(255, 255, 255, 0.05);
597
+ color: var(--text-main);
598
+ }
599
+ .file-item.active {
600
+ background: rgba(99, 102, 241, 0.18);
601
+ border: 1px solid rgba(99, 102, 241, 0.4);
602
+ color: #fff;
603
+ }
604
+ .file-item .file-size {
605
+ font-size: 11px;
606
+ opacity: 0.6;
607
+ }
608
+
609
+ .templates-box {
610
+ padding: 16px;
611
+ border-top: 1px solid var(--border-subtle);
612
+ background: rgba(0, 0, 0, 0.2);
613
+ }
614
+ .templates-box h4 {
615
+ font-size: 12px;
616
+ color: var(--text-dim);
617
+ margin-bottom: 10px;
618
+ text-transform: uppercase;
619
+ }
620
+ .template-btn {
621
+ width: 100%;
622
+ text-align: left;
623
+ background: rgba(255, 255, 255, 0.04);
624
+ border: 1px solid var(--border-subtle);
625
+ padding: 8px 12px;
626
+ border-radius: 8px;
627
+ color: var(--text-main);
628
+ font-size: 12px;
629
+ cursor: pointer;
630
+ margin-bottom: 6px;
631
+ transition: all 0.15s;
632
+ }
633
+ .template-btn:hover {
634
+ background: rgba(99, 102, 241, 0.2);
635
+ border-color: var(--primary);
636
+ }
637
+
638
+ /* Center Column: Live Editor & Console */
639
+ .editor-container {
640
+ display: flex;
641
+ flex-direction: column;
642
+ flex: 1;
643
+ }
644
+ .editor-toolbar {
645
+ padding: 12px 20px;
646
+ background: rgba(10, 12, 20, 0.6);
647
+ border-bottom: 1px solid var(--border-subtle);
648
+ display: flex;
649
+ align-items: center;
650
+ justify-content: space-between;
651
+ }
652
+ .editor-actions {
653
+ display: flex;
654
+ gap: 10px;
655
+ }
656
+ .btn {
657
+ border: none;
658
+ outline: none;
659
+ padding: 8px 16px;
660
+ border-radius: 10px;
661
+ font-size: 13px;
662
+ font-weight: 600;
663
+ cursor: pointer;
664
+ display: flex;
665
+ align-items: center;
666
+ gap: 6px;
667
+ transition: all 0.2s ease;
668
+ }
669
+ .btn-run {
670
+ background: linear-gradient(135deg, #10b981, #059669);
671
+ color: white;
672
+ box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3);
673
+ }
674
+ .btn-run:hover { transform: translateY(-1px); filter: brightness(1.1); }
675
+ .btn-secondary {
676
+ background: rgba(255, 255, 255, 0.06);
677
+ color: var(--text-main);
678
+ border: 1px solid var(--border-subtle);
679
+ }
680
+ .btn-secondary:hover { background: rgba(255, 255, 255, 0.1); }
681
+
682
+ textarea.code-editor {
683
+ flex: 1;
684
+ min-height: 380px;
685
+ background: transparent;
686
+ border: none;
687
+ outline: none;
688
+ color: #f1f5f9;
689
+ font-family: var(--font-mono);
690
+ font-size: 14px;
691
+ line-height: 1.6;
692
+ padding: 20px;
693
+ resize: none;
694
+ white-space: pre;
695
+ }
696
+
697
+ /* Console Output Box */
698
+ .console-container {
699
+ border-top: 1px solid var(--border-subtle);
700
+ background: rgba(5, 7, 12, 0.95);
701
+ display: flex;
702
+ flex-direction: column;
703
+ height: 260px;
704
+ }
705
+ .console-header {
706
+ padding: 10px 18px;
707
+ border-bottom: 1px solid var(--border-subtle);
708
+ display: flex;
709
+ align-items: center;
710
+ justify-content: space-between;
711
+ font-size: 12px;
712
+ color: var(--text-dim);
713
+ font-family: var(--font-mono);
714
+ }
715
+ .console-output {
716
+ flex: 1;
717
+ padding: 14px 18px;
718
+ overflow-y: auto;
719
+ font-family: var(--font-mono);
720
+ font-size: 13px;
721
+ line-height: 1.5;
722
+ }
723
+ .log-line { margin-bottom: 4px; word-break: break-all; }
724
+ .log-line.log { color: #e2e8f0; }
725
+ .log-line.warn { color: var(--accent-orange); }
726
+ .log-line.error { color: var(--accent-red); font-weight: 600; }
727
+ .log-line.success { color: #34d399; font-weight: 600; }
728
+ .log-time { color: #64748b; margin-right: 8px; font-size: 11px; }
729
+
730
+ /* Right Sidebar: Real Hardware & Network Info */
731
+ .metric-group {
732
+ padding: 18px;
733
+ border-bottom: 1px solid var(--border-subtle);
734
+ }
735
+ .metric-group:last-child { border-bottom: none; }
736
+ .metric-title {
737
+ font-size: 12px;
738
+ color: var(--text-dim);
739
+ margin-bottom: 12px;
740
+ text-transform: uppercase;
741
+ font-weight: 600;
742
+ }
743
+ .metric-row {
744
+ display: flex;
745
+ justify-content: space-between;
746
+ align-items: center;
747
+ margin-bottom: 8px;
748
+ font-size: 13px;
749
+ }
750
+ .metric-label { color: #94a3b8; }
751
+ .metric-val { font-family: var(--font-mono); font-weight: 600; color: #fff; }
752
+
753
+ .progress-bar {
754
+ height: 8px;
755
+ background: rgba(255, 255, 255, 0.08);
756
+ border-radius: 4px;
757
+ overflow: hidden;
758
+ margin-top: 6px;
759
+ }
760
+ .progress-fill {
761
+ height: 100%;
762
+ background: linear-gradient(90deg, var(--primary), var(--accent-cyan));
763
+ transition: width 0.3s ease;
764
+ }
765
+
766
+ .network-list {
767
+ list-style: none;
768
+ font-size: 12px;
769
+ font-family: var(--font-mono);
770
+ }
771
+ .network-list li {
772
+ background: rgba(0, 0, 0, 0.3);
773
+ border: 1px solid var(--border-subtle);
774
+ border-radius: 8px;
775
+ padding: 8px 12px;
776
+ margin-bottom: 8px;
777
+ display: flex;
778
+ align-items: center;
779
+ justify-content: space-between;
780
+ }
781
+ .tag {
782
+ background: rgba(99, 102, 241, 0.2);
783
+ color: var(--primary-glow);
784
+ padding: 2px 6px;
785
+ border-radius: 4px;
786
+ font-size: 10px;
787
+ }
788
+
789
+ /* Timing Badges */
790
+ .timing-pill {
791
+ display: inline-block;
792
+ background: rgba(255, 255, 255, 0.06);
793
+ border: 1px solid var(--border-subtle);
794
+ padding: 4px 10px;
795
+ border-radius: 12px;
796
+ font-size: 11px;
797
+ font-family: var(--font-mono);
798
+ color: #38bdf8;
799
+ margin-left: 8px;
800
+ }
801
+ </style>
802
+ </head>
803
+ <body>
804
+ <header>
805
+ <div class="brand">
806
+ <div class="logo-icon">⚡</div>
807
+ <div>
808
+ <h1>ThyScript Control Panel</h1>
809
+ </div>
810
+ <span class="badge">v1.0.0 HOST SERVER</span>
811
+ </div>
812
+ <div class="header-actions">
813
+ <div class="status-pill">
814
+ <div class="status-dot"></div>
815
+ <span>LIVE SERVER ONLINE</span>
816
+ </div>
817
+ </div>
818
+ </header>
819
+
820
+ <div class="main-layout">
821
+ <!-- Left: Workspace & Templates -->
822
+ <aside class="panel-card">
823
+ <div class="card-header">
824
+ <span class="title">📁 Workspace Files</span>
825
+ <button class="btn btn-secondary" style="padding: 4px 8px; font-size: 11px;" onclick="loadFiles()">↻</button>
826
+ </div>
827
+ <ul class="file-list" id="fileList">
828
+ <li style="padding: 20px; text-align: center; color: #64748b;">Loading workspace...</li>
829
+ </ul>
830
+
831
+ <div class="templates-box">
832
+ <h4>⚡ Quick Starters</h4>
833
+ <button class="template-btn" onclick="loadStarter('calc')">🧮 Fast Math & Arithmetic</button>
834
+ <button class="template-btn" onclick="loadStarter('oop')">🏛️ OOP Classes & Methods</button>
835
+ <button class="template-btn" onclick="loadStarter('server')">🌐 REST API & Web Server</button>
836
+ <button class="template-btn" onclick="loadStarter('func')">λ Recursion & High-Order Func</button>
837
+ </div>
838
+ </aside>
839
+
840
+ <!-- Center: Live Editor & Console -->
841
+ <main class="panel-card editor-container">
842
+ <div class="editor-toolbar">
843
+ <div style="display: flex; align-items: center; gap: 8px;">
844
+ <span id="currentFileName" style="font-family: var(--font-mono); font-size: 13px; color: #cbd5e1;">scratchpad.thy</span>
845
+ <span id="timingBadge" class="timing-pill" style="display: none;">0ms</span>
846
+ </div>
847
+ <div class="editor-actions">
848
+ <button class="btn btn-secondary" onclick="saveCurrentFile()">💾 Save to Host</button>
849
+ <button class="btn btn-secondary" onclick="clearConsole()">🧹 Clear Logs</button>
850
+ <button class="btn btn-run" onclick="runCode()">⚡ RUN (ZERO COMPILE)</button>
851
+ </div>
852
+ </div>
853
+
854
+ <textarea id="codeEditor" class="code-editor" spellcheck="false" placeholder="Tulis kode ThyScript di sini..."></textarea>
855
+
856
+ <div class="console-container">
857
+ <div class="console-header">
858
+ <span>EXECUTION CONSOLE & LOGS</span>
859
+ <span id="consoleStatus" style="color: #64748b;">Ready</span>
860
+ </div>
861
+ <div class="console-output" id="consoleOut">
862
+ <div class="log-line log"><span class="log-time">[System]</span> ThyScript Live Execution Sandbox initialized. Click RUN to execute instantly in host memory.</div>
863
+ </div>
864
+ </div>
865
+ </main>
866
+
867
+ <!-- Right: Hardware Monitor & Remote Access -->
868
+ <aside class="panel-card">
869
+ <div class="card-header">
870
+ <span class="title">🖥️ Server Hardware</span>
871
+ <span id="uptimeText" style="font-family: var(--font-mono); font-size: 11px;">0s</span>
872
+ </div>
873
+
874
+ <!-- RAM Metrics -->
875
+ <div class="metric-group">
876
+ <div class="metric-title">System RAM Memory</div>
877
+ <div class="metric-row">
878
+ <span class="metric-label">Used / Total:</span>
879
+ <span class="metric-val" id="memText">0 / 0 GB</span>
880
+ </div>
881
+ <div class="progress-bar">
882
+ <div class="progress-fill" id="memProgress" style="width: 0%;"></div>
883
+ </div>
884
+ <div class="metric-row" style="margin-top: 8px; font-size: 11px;">
885
+ <span class="metric-label">Heap Used:</span>
886
+ <span class="metric-val" id="heapText" style="color: #38bdf8;">0 MB</span>
887
+ </div>
888
+ </div>
889
+
890
+ <!-- CPU Metrics -->
891
+ <div class="metric-group">
892
+ <div class="metric-title">Host CPU & Cores</div>
893
+ <div class="metric-row">
894
+ <span class="metric-label">Cores:</span>
895
+ <span class="metric-val" id="cpuCores">0 Cores</span>
896
+ </div>
897
+ <div class="metric-row">
898
+ <span class="metric-label">Model:</span>
899
+ <span class="metric-val" id="cpuModel" style="font-size: 11px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 180px;">-</span>
900
+ </div>
901
+ </div>
902
+
903
+ <!-- Remote Access URLs -->
904
+ <div class="metric-group" style="flex: 1;">
905
+ <div class="metric-title">🌐 Remote Access URLs</div>
906
+ <p style="font-size: 11px; color: #94a3b8; margin-bottom: 10px;">
907
+ Komputer utama ini sekarang menjadi Server ThyScript. Buka URL ini di HP / Laptop lain dalam jaringan WiFi/LAN yang sama:
908
+ </p>
909
+ <ul class="network-list">
910
+ <li><span class="tag">Localhost</span> <strong>http://localhost:${this.options.port}</strong></li>
911
+ ${netHtml}
912
+ </ul>
913
+ </div>
914
+ </aside>
915
+ </div>
916
+
917
+ <script>
918
+ const editor = document.getElementById('codeEditor');
919
+ const consoleOut = document.getElementById('consoleOut');
920
+ const consoleStatus = document.getElementById('consoleStatus');
921
+ const timingBadge = document.getElementById('timingBadge');
922
+ let currentFilePath = null;
923
+
924
+ // Starters dictionary
925
+ const starters = {
926
+ calc: \`// ⚡ Fast Arithmetic & Math
927
+ const table a = 1250
928
+ const table b = 48
929
+ const table sum = calculate(a + b)
930
+ const table product = calculate(a * b)
931
+ const table power = calculate(2 ** 16)
932
+
933
+ print("A + B = " + sum)
934
+ print("A * B = " + product)
935
+ print("2^16 = " + power)
936
+
937
+ // Array calculations
938
+ const table values = [10, 20, 30, 40, 50, 100]
939
+ const table total = 0
940
+ for v in values:
941
+ add v in total
942
+ end()
943
+ print("Total sum: " + total)
944
+ \`,
945
+ oop: \`// 🏛️ OOP Classes & Methods
946
+ public class(Worker):
947
+ const table name = "Unknown"
948
+ const table tasksCompleted = 0
949
+
950
+ func completeTask(taskName):
951
+ add 1 in tasksCompleted
952
+ print(name + " completed: " + taskName + " (Total: " + tasksCompleted + ")")
953
+ end()
954
+ end()
955
+
956
+ const table dev = new Worker()
957
+ change(dev.name to("Antigravity Dev"))
958
+ dev.completeTask("JIT Compiler Optimization")
959
+ dev.completeTask("Remote Control Panel Engine")
960
+ \`,
961
+ server: \`// 🌐 Express REST API Server
962
+ server(5050):
963
+ print("⚡ Mini service live on port 5050!")
964
+ end()
965
+
966
+ route get "/api/ping":
967
+ response.json({ message: "pong from ThyScript!", time: Date.now() })
968
+ end()
969
+
970
+ print("Open http://localhost:5050/api/ping to test!")
971
+ \`,
972
+ func: \`// λ High-Order & Recursive Functions
973
+ func factorial(n):
974
+ if(n <= 1):
975
+ return 1
976
+ end()
977
+ return calculate(n * factorial(n - 1))
978
+ end()
979
+
980
+ print("5! = " + factorial(5))
981
+ print("10! = " + factorial(10))
982
+ \`
983
+ };
984
+
985
+ // Load starter into editor
986
+ function loadStarter(key) {
987
+ editor.value = starters[key] || '';
988
+ currentFilePath = null;
989
+ document.getElementById('currentFileName').innerText = key + '_starter.thy';
990
+ }
991
+
992
+ // Add log entry to console
993
+ function appendLog(type, msg, time) {
994
+ const row = document.createElement('div');
995
+ row.className = 'log-line ' + type;
996
+ const t = time || new Date().toLocaleTimeString();
997
+ row.innerHTML = '<span class="log-time">[' + t + ']</span> ' + escapeHtml(msg);
998
+ consoleOut.appendChild(row);
999
+ consoleOut.scrollTop = consoleOut.scrollHeight;
1000
+ }
1001
+
1002
+ function clearConsole() {
1003
+ consoleOut.innerHTML = '';
1004
+ consoleStatus.innerText = 'Cleared';
1005
+ }
1006
+
1007
+ function escapeHtml(str) {
1008
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1009
+ }
1010
+
1011
+ // Run code on the fly
1012
+ async function runCode() {
1013
+ const code = editor.value.trim();
1014
+ if (!code) return;
1015
+
1016
+ consoleStatus.innerText = '⚡ Executing in memory...';
1017
+ appendLog('log', '── Running on-the-fly without disk compile... ──');
1018
+
1019
+ const t0 = performance.now();
1020
+ try {
1021
+ const res = await fetch('/api/run-code', {
1022
+ method: 'POST',
1023
+ headers: { 'Content-Type': 'application/json' },
1024
+ body: JSON.stringify({ code, name: currentFilePath || 'scratchpad.thy' })
1025
+ });
1026
+ const data = await res.json();
1027
+
1028
+ if (data.success) {
1029
+ (data.logs || []).forEach(l => appendLog(l.type, l.message, l.time));
1030
+ if (data.result !== null && data.result !== undefined) {
1031
+ appendLog('success', '=> Return: ' + data.result);
1032
+ }
1033
+ const total = data.timings ? data.timings.totalMs : (performance.now() - t0).toFixed(1);
1034
+ timingBadge.style.display = 'inline-block';
1035
+ timingBadge.innerText = total + 'ms';
1036
+ consoleStatus.innerText = 'Finished (' + total + 'ms)';
1037
+ appendLog('success', '✔ Finished successfully in ' + total + 'ms (Memory Delta: ' + data.memoryDeltaKB + ' KB)');
1038
+ } else {
1039
+ (data.logs || []).forEach(l => appendLog(l.type, l.message, l.time));
1040
+ appendLog('error', '✖ Error: ' + (data.error ? data.error.message : 'Execution failed'));
1041
+ consoleStatus.innerText = 'Failed';
1042
+ }
1043
+ } catch (err) {
1044
+ appendLog('error', 'Network/Server Error: ' + err.message);
1045
+ consoleStatus.innerText = 'Error';
1046
+ }
1047
+ }
1048
+
1049
+ // Save current code to host file
1050
+ async function saveCurrentFile() {
1051
+ let target = currentFilePath;
1052
+ if (!target) {
1053
+ target = prompt('Enter filename to save in workspace (e.g. my_script.thy):', 'script.thy');
1054
+ if (!target) return;
1055
+ currentFilePath = target;
1056
+ document.getElementById('currentFileName').innerText = target;
1057
+ }
1058
+ try {
1059
+ const res = await fetch('/api/files/save', {
1060
+ method: 'POST',
1061
+ headers: { 'Content-Type': 'application/json' },
1062
+ body: JSON.stringify({ filePath: target, content: editor.value })
1063
+ });
1064
+ const data = await res.json();
1065
+ if (data.success) {
1066
+ appendLog('success', 'Saved ' + target + ' to host disk successfully.');
1067
+ loadFiles();
1068
+ } else {
1069
+ appendLog('error', 'Save error: ' + data.message);
1070
+ }
1071
+ } catch (e) {
1072
+ appendLog('error', 'Save failed: ' + e.message);
1073
+ }
1074
+ }
1075
+
1076
+ // Load files from host workspace
1077
+ async function loadFiles() {
1078
+ try {
1079
+ const res = await fetch('/api/files');
1080
+ const data = await res.json();
1081
+ const list = document.getElementById('fileList');
1082
+ list.innerHTML = '';
1083
+
1084
+ if (data.files && data.files.length) {
1085
+ data.files.forEach(f => {
1086
+ const li = document.createElement('li');
1087
+ li.className = 'file-item' + (currentFilePath === f.path ? ' active' : '');
1088
+ li.innerHTML = '<span>' + f.name + '</span><span class="file-size">' + (f.sizeBytes / 1024).toFixed(1) + ' KB</span>';
1089
+ li.onclick = () => openFile(f.path);
1090
+ list.appendChild(li);
1091
+ });
1092
+ } else {
1093
+ list.innerHTML = '<li style="padding: 14px; text-align: center; color: #64748b;">No .thy files found</li>';
1094
+ }
1095
+ } catch (err) {
1096
+ console.error(err);
1097
+ }
1098
+ }
1099
+
1100
+ // Open file content into editor
1101
+ async function openFile(relPath) {
1102
+ try {
1103
+ const res = await fetch('/api/files/read?path=' + encodeURIComponent(relPath));
1104
+ const data = await res.json();
1105
+ if (data.success) {
1106
+ editor.value = data.content;
1107
+ currentFilePath = relPath;
1108
+ document.getElementById('currentFileName').innerText = relPath;
1109
+ loadFiles();
1110
+ appendLog('log', 'Opened ' + relPath);
1111
+ }
1112
+ } catch (e) {
1113
+ appendLog('error', 'Failed to read file: ' + e.message);
1114
+ }
1115
+ }
1116
+
1117
+ // Update hardware metrics
1118
+ async function refreshMetrics() {
1119
+ try {
1120
+ const res = await fetch('/api/system');
1121
+ const data = await res.json();
1122
+ if (data.success && data.metrics) {
1123
+ const m = data.metrics;
1124
+ document.getElementById('memText').innerText = m.memory.usedMB + ' / ' + m.memory.totalMB + ' MB';
1125
+ document.getElementById('memProgress').style.width = m.memory.usagePercent + '%';
1126
+ document.getElementById('heapText').innerText = m.memory.processHeapUsedMB + ' MB';
1127
+ document.getElementById('cpuCores').innerText = m.cpu.cores + ' Cores';
1128
+ document.getElementById('cpuModel').innerText = m.cpu.model;
1129
+ document.getElementById('uptimeText').innerText = m.server.uptimeSeconds + 's uptime';
1130
+ }
1131
+ } catch (_) {}
1132
+ }
1133
+
1134
+ // Keyboard shortcuts: Ctrl+Enter to run, Ctrl+S to save
1135
+ window.addEventListener('keydown', (e) => {
1136
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
1137
+ e.preventDefault();
1138
+ runCode();
1139
+ }
1140
+ if ((e.ctrlKey || e.metaKey) && e.key === 's') {
1141
+ e.preventDefault();
1142
+ saveCurrentFile();
1143
+ }
1144
+ });
1145
+
1146
+ // Init on page load
1147
+ loadStarter('calc');
1148
+ loadFiles();
1149
+ refreshMetrics();
1150
+ setInterval(refreshMetrics, 2000);
1151
+ </script>
1152
+ </body>
1153
+ </html>`;
1154
+ }
1155
+
1156
+ /**
1157
+ * Start the Control Panel Server
1158
+ */
1159
+ start(port = this.options.port, host = this.options.host) {
1160
+ this.setupRoutes();
1161
+ return new Promise((resolve, reject) => {
1162
+ try {
1163
+ this.server = this.app.listen(port, host, () => {
1164
+ this.options.port = this.server.address().port;
1165
+ resolve(this);
1166
+ });
1167
+ this.server.on('error', reject);
1168
+ } catch (e) {
1169
+ reject(e);
1170
+ }
1171
+ });
1172
+ }
1173
+
1174
+ /**
1175
+ * Stop the Control Panel Server
1176
+ */
1177
+ stop() {
1178
+ return new Promise((resolve) => {
1179
+ if (this.server) {
1180
+ this.server.close(() => resolve());
1181
+ } else {
1182
+ resolve();
1183
+ }
1184
+ });
1185
+ }
1186
+ }
1187
+
1188
+ module.exports = ControlPanel;