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.
- package/bin/thy-node.js +332 -0
- package/bin/thy.js +828 -0
- package/bin/thys.js +25 -0
- package/examples/calculator.thy +274 -0
- package/examples/calculator.thy.js +628 -0
- package/examples/hello.thy +34 -0
- package/examples/login.thy +382 -0
- package/examples/login.thy.js +710 -0
- package/package.json +54 -0
- package/scripts/_class.js +18 -0
- package/scripts/_class2.js +11 -0
- package/scripts/_class3.js +21 -0
- package/scripts/_dstruct.js +20 -0
- package/scripts/_dstruct2.js +24 -0
- package/scripts/_scaffold.js +16 -0
- package/scripts/_scaffold2.js +16 -0
- package/scripts/_scaffold3.js +26 -0
- package/scripts/_tpl_compile.js +13 -0
- package/scripts/_tpl_test.js +9 -0
- package/scripts/_typeof.js +26 -0
- package/scripts/_typeof2.js +25 -0
- package/scripts/patch_genmethod.js +44 -0
- package/scripts/smoke_test.js +167 -0
- package/scripts/smoke_test2.js +151 -0
- package/scripts/test_control_panel.js +58 -0
- package/src/compiler/CodeGenerator.js +1449 -0
- package/src/compiler/JITCompiler.js +325 -0
- package/src/database/DatabaseEngine.js +931 -0
- package/src/errors/ErrorDetector.js +305 -0
- package/src/errors/ErrorMessages.js +497 -0
- package/src/index.js +169 -0
- package/src/lexer/Lexer.js +342 -0
- package/src/lexer/TokenTypes.js +1234 -0
- package/src/memory/MemoryManager.js +283 -0
- package/src/parser/AST.js +606 -0
- package/src/parser/Parser.js +1942 -0
- package/src/runtime/ThyRuntime.js +265 -0
- package/src/runtime/stdlib/ArrayLib.js +207 -0
- package/src/runtime/stdlib/FileLib.js +168 -0
- package/src/runtime/stdlib/MathLib.js +411 -0
- package/src/runtime/stdlib/StringLib.js +387 -0
- package/src/server/ControlPanel.js +1188 -0
- package/src/server/HTMLCSSEngine.js +866 -0
- package/src/server/SecurityEngine.js +714 -0
- package/src/server/ServerEngine.js +738 -0
- package/tests/test1_math.thy +82 -0
- package/tests/test2_oop.thy +133 -0
- package/tests/test3_server.thy +143 -0
- 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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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;
|