Ditzzy AF commited on
Commit
056d297
·
1 Parent(s): 5e65aee

First Commmit

Browse files
Files changed (6) hide show
  1. Dockerfile +56 -0
  2. app.js +614 -0
  3. package.json +20 -0
  4. public/js/terminal.js +45 -0
  5. views/login.ejs +294 -0
  6. views/terminal.ejs +998 -0
Dockerfile ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Gunakan Node.js 22 berbasis Debian Bullseye
2
+ FROM node:22-bullseye
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Install dependencies sistem yang dibutuhkan untuk node-pty dan alat dasar
8
+ RUN apt-get update && apt-get install -y --no-install-recommends \
9
+ python3 \
10
+ make \
11
+ g++ \
12
+ bash \
13
+ curl \
14
+ git \
15
+ nano \
16
+ htop \
17
+ procps \
18
+ sudo \
19
+ ca-certificates \
20
+ && rm -rf /var/lib/apt/lists/*
21
+
22
+ # Copy package files untuk caching layer Docker
23
+ COPY package*.json ./
24
+
25
+ # Install Node.js dependencies, termasuk node-pty
26
+ RUN npm ci --only=production && \
27
+ npm install node-pty || echo "node-pty installation failed, fallback enabled" && \
28
+ npm cache clean --force
29
+
30
+ # Copy seluruh aplikasi
31
+ COPY . .
32
+
33
+ # Buat folder views
34
+ RUN mkdir -p views
35
+
36
+ # Set permissions (optional karena root user)
37
+ RUN chmod +x /app/app.js
38
+
39
+ # Buat file .env default
40
+ RUN echo "PORT=7860" > /app/.env.default && \
41
+ echo "NODE_ENV=production" >> /app/.env.default && \
42
+ echo "SESSION_SECRET=default-secret-please-change" >> /app/.env.default
43
+
44
+ # Expose port
45
+ EXPOSE 7860
46
+
47
+ # Health check
48
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
49
+ CMD curl -f http://localhost:7860/login || exit 1
50
+
51
+ # Default shell untuk terminal sessions
52
+ ENV SHELL=/bin/bash
53
+ ENV HOME=/root
54
+
55
+ # Start aplikasi
56
+ CMD ["node", "app.js"]
app.js ADDED
@@ -0,0 +1,614 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from "express";
2
+ import session from "express-session";
3
+ import bcrypt from "bcrypt";
4
+ import helmet from "helmet";
5
+ import { createServer } from "http";
6
+ import { Server as IOServer } from "socket.io";
7
+ import path from "path";
8
+ import { fileURLToPath } from "url";
9
+ import dotenv from "dotenv";
10
+ import os from "os";
11
+ import { spawn, exec } from "child_process";
12
+ import { promisify } from "util";
13
+ import fs from "fs/promises";
14
+
15
+ dotenv.config();
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = path.dirname(__filename);
18
+ const execAsync = promisify(exec);
19
+
20
+ const app = express();
21
+ const httpServer = createServer(app);
22
+ const io = new IOServer(httpServer, {
23
+ cors: {
24
+ origin: process.env.NODE_ENV === "production" ? false : ["http://localhost:3000"],
25
+ methods: ["GET", "POST"]
26
+ }
27
+ });
28
+
29
+ const PORT = process.env.PORT || 3000;
30
+ const SESSION_SECRET = process.env.SESSION_SECRET || "devsecret";
31
+ const PASSWORD_HASH = process.env.WEBTERM_PASSWORD_HASH;
32
+
33
+ // Platform detection
34
+ const isWindows = process.platform === "win32";
35
+ const isLinux = process.platform === "linux";
36
+ const isMacOS = process.platform === "darwin";
37
+
38
+ // === System Utilities ===
39
+ class SystemMonitor {
40
+ constructor() {
41
+ this.cpuHistory = [];
42
+ this.networkHistory = [];
43
+ this.previousNetworkStats = null;
44
+ }
45
+
46
+ async getCPUUsage() {
47
+ return new Promise((resolve) => {
48
+ const startMeasure = this.cpuAverage();
49
+ setTimeout(() => {
50
+ const endMeasure = this.cpuAverage();
51
+ const idleDifference = endMeasure.idle - startMeasure.idle;
52
+ const totalDifference = endMeasure.total - startMeasure.total;
53
+ const percentageCPU = 100 - Math.floor(100 * idleDifference / totalDifference);
54
+ resolve(Math.max(0, Math.min(100, percentageCPU)));
55
+ }, 100);
56
+ });
57
+ }
58
+
59
+ cpuAverage() {
60
+ const cpus = os.cpus();
61
+ let user = 0, nice = 0, sys = 0, idle = 0, irq = 0;
62
+
63
+ for (let cpu of cpus) {
64
+ user += cpu.times.user;
65
+ nice += cpu.times.nice || 0;
66
+ sys += cpu.times.sys;
67
+ irq += cpu.times.irq || 0;
68
+ idle += cpu.times.idle;
69
+ }
70
+
71
+ return {
72
+ idle: idle / cpus.length,
73
+ total: (user + nice + sys + idle + irq) / cpus.length
74
+ };
75
+ }
76
+
77
+ async getMemoryInfo() {
78
+ const totalMem = os.totalmem();
79
+ const freeMem = os.freemem();
80
+ const usedMem = totalMem - freeMem;
81
+
82
+ return {
83
+ total: Math.round(totalMem / 1024 / 1024), // MB
84
+ used: Math.round(usedMem / 1024 / 1024), // MB
85
+ free: Math.round(freeMem / 1024 / 1024), // MB
86
+ percentage: Math.round((usedMem / totalMem) * 100)
87
+ };
88
+ }
89
+
90
+ async getDiskUsage() {
91
+ try {
92
+ if (isWindows) {
93
+ const { stdout } = await execAsync('wmic logicaldisk get size,freespace,caption /format:csv');
94
+ const lines = stdout.split('\n').filter(line => line.includes(':'));
95
+
96
+ if (lines.length > 0) {
97
+ const parts = lines[0].split(',');
98
+ if (parts.length >= 3) {
99
+ const freeSpace = parseInt(parts[1]) || 0;
100
+ const totalSpace = parseInt(parts[2]) || 1;
101
+ const usedSpace = totalSpace - freeSpace;
102
+ const percentage = Math.round((usedSpace / totalSpace) * 100);
103
+
104
+ return {
105
+ total: Math.round(totalSpace / 1024 / 1024 / 1024), // GB
106
+ used: Math.round(usedSpace / 1024 / 1024 / 1024), // GB
107
+ free: Math.round(freeSpace / 1024 / 1024 / 1024), // GB
108
+ percentage: percentage
109
+ };
110
+ }
111
+ }
112
+ } else {
113
+ // Linux/macOS
114
+ const { stdout } = await execAsync('df -h / | tail -1');
115
+ const parts = stdout.trim().split(/\s+/);
116
+
117
+ if (parts.length >= 5) {
118
+ const percentage = parseInt(parts[4].replace('%', '')) || 0;
119
+ return {
120
+ total: parts[1],
121
+ used: parts[2],
122
+ free: parts[3],
123
+ percentage: percentage
124
+ };
125
+ }
126
+ }
127
+ } catch (error) {
128
+ console.warn('Disk usage detection failed:', error.message);
129
+ }
130
+
131
+ return { total: 0, used: 0, free: 0, percentage: 0 };
132
+ }
133
+
134
+ async getProcessList() {
135
+ try {
136
+ let processes = [];
137
+
138
+ if (isWindows) {
139
+ const { stdout } = await execAsync(
140
+ 'powershell "Get-Process | Sort-Object CPU -Descending | Select-Object -First 10 Name, CPU | ConvertTo-Csv -NoTypeInformation"'
141
+ );
142
+
143
+ const lines = stdout.split('\n').slice(1).filter(line => line.trim());
144
+ processes = lines.map(line => {
145
+ const [name, cpu] = line.replace(/"/g, '').split(',');
146
+ return {
147
+ name: name || 'Unknown',
148
+ cpu: parseFloat(cpu) || 0
149
+ };
150
+ }).filter(p => p.name !== 'Unknown').slice(0, 8);
151
+
152
+ } else {
153
+ // Linux/macOS - using ps command
154
+ const { stdout } = await execAsync('ps aux --sort=-%cpu | head -9 | tail -8');
155
+ const lines = stdout.split('\n').filter(line => line.trim());
156
+
157
+ processes = lines.map(line => {
158
+ const parts = line.trim().split(/\s+/);
159
+ return {
160
+ name: parts[10] ? path.basename(parts[10]) : 'unknown',
161
+ cpu: parseFloat(parts[2]) || 0
162
+ };
163
+ });
164
+ }
165
+
166
+ return processes;
167
+ } catch (error) {
168
+ console.warn('Process list detection failed:', error.message);
169
+ return [];
170
+ }
171
+ }
172
+
173
+ async getNetworkStats() {
174
+ try {
175
+ if (isWindows) {
176
+ // Windows network stats - simplified for now
177
+ const { stdout } = await execAsync('powershell "Get-Counter \\"\\Network Interface(*)\\Bytes Total/sec\\" -SampleInterval 1 -MaxSamples 1 | Select-Object -ExpandProperty CounterSamples | Where-Object {$_.InstanceName -ne \\"isatap*\\" -and $_.InstanceName -ne \\"Teredo*\\" -and $_.InstanceName -ne \\"_Total\\""} | Measure-Object CookedValue -Sum | Select-Object -ExpandProperty Sum"');
178
+ const totalBytes = parseFloat(stdout.trim()) || 0;
179
+ return { rx: totalBytes / 2, tx: totalBytes / 2 }; // Rough estimate
180
+ } else {
181
+ // Linux - read from /proc/net/dev
182
+ const data = await fs.readFile('/proc/net/dev', 'utf8');
183
+ const lines = data.split('\n');
184
+ let totalRx = 0, totalTx = 0;
185
+
186
+ for (let line of lines) {
187
+ if (line.includes(':') && !line.includes('lo:')) {
188
+ const parts = line.split(':')[1].trim().split(/\s+/);
189
+ totalRx += parseInt(parts[0]) || 0;
190
+ totalTx += parseInt(parts[8]) || 0;
191
+ }
192
+ }
193
+
194
+ // Calculate speed if we have previous data
195
+ if (this.previousNetworkStats) {
196
+ const rxSpeed = Math.max(0, totalRx - this.previousNetworkStats.rx);
197
+ const txSpeed = Math.max(0, totalTx - this.previousNetworkStats.tx);
198
+ this.previousNetworkStats = { rx: totalRx, tx: totalTx };
199
+ return { rx: rxSpeed, tx: txSpeed };
200
+ } else {
201
+ this.previousNetworkStats = { rx: totalRx, tx: totalTx };
202
+ return { rx: 0, tx: 0 };
203
+ }
204
+ }
205
+ } catch (error) {
206
+ console.warn('Network stats detection failed:', error.message);
207
+ return { rx: 0, tx: 0 };
208
+ }
209
+ }
210
+
211
+ formatUptime(seconds) {
212
+ const days = Math.floor(seconds / 86400);
213
+ const hours = Math.floor((seconds % 86400) / 3600);
214
+ const minutes = Math.floor((seconds % 3600) / 60);
215
+
216
+ if (days > 0) return `${days}d ${hours}h ${minutes}m`;
217
+ if (hours > 0) return `${hours}h ${minutes}m`;
218
+ return `${minutes}m`;
219
+ }
220
+
221
+ async getSystemInfo() {
222
+ try {
223
+ const [cpuUsage, memInfo, diskInfo, processes, networkStats] = await Promise.all([
224
+ this.getCPUUsage(),
225
+ this.getMemoryInfo(),
226
+ this.getDiskUsage(),
227
+ this.getProcessList(),
228
+ this.getNetworkStats()
229
+ ]);
230
+
231
+ const cpuInfo = os.cpus()[0];
232
+ const uptime = os.uptime();
233
+
234
+ return {
235
+ // System info
236
+ platform: os.platform(),
237
+ release: os.release(),
238
+ arch: os.arch(),
239
+ hostname: os.hostname(),
240
+ uptime: this.formatUptime(uptime),
241
+
242
+ // CPU info
243
+ cpu_model: cpuInfo.model,
244
+ cpu_cores: os.cpus().length,
245
+ cpu_usage: cpuUsage,
246
+
247
+ // Memory info
248
+ total_mem: memInfo.total,
249
+ used_mem: memInfo.used,
250
+ free_mem: memInfo.free,
251
+ mem_usage: memInfo.percentage,
252
+
253
+ // Disk info
254
+ disk_total: diskInfo.total,
255
+ disk_used: diskInfo.used,
256
+ disk_free: diskInfo.free,
257
+ disk_usage: diskInfo.percentage,
258
+
259
+ // Network info (speeds in bytes per second)
260
+ network_rx: networkStats.rx,
261
+ network_tx: networkStats.tx,
262
+
263
+ // Process list
264
+ processes: processes,
265
+
266
+ // Load average (Linux/macOS only)
267
+ loadavg: isWindows ? [0, 0, 0] : os.loadavg(),
268
+
269
+ // User info
270
+ user_info: os.userInfo(),
271
+
272
+ timestamp: Date.now()
273
+ };
274
+ } catch (error) {
275
+ console.error('System info error:', error);
276
+ return null;
277
+ }
278
+ }
279
+ }
280
+
281
+ const systemMonitor = new SystemMonitor();
282
+
283
+ // === Middleware ===
284
+ app.use(helmet({
285
+ contentSecurityPolicy: {
286
+ directives: {
287
+ defaultSrc: ["'self'"],
288
+ scriptSrc: [
289
+ "'self'",
290
+ "'unsafe-inline'",
291
+ "https://cdn.jsdelivr.net"
292
+ ],
293
+ styleSrc: [
294
+ "'self'",
295
+ "'unsafe-inline'",
296
+ "https://cdn.jsdelivr.net",
297
+ "https://fonts.googleapis.com"
298
+ ],
299
+ fontSrc: [
300
+ "'self'",
301
+ "https://fonts.googleapis.com",
302
+ "https://fonts.gstatic.com"
303
+ ],
304
+ imgSrc: ["'self'", "data:"],
305
+ connectSrc: ["'self'", "ws:", "wss:"]
306
+ }
307
+ },
308
+ crossOriginEmbedderPolicy: false
309
+ }));
310
+
311
+ app.use(express.urlencoded({ extended: false }));
312
+ app.use(express.json());
313
+ app.use(express.static(path.join(__dirname, "public")));
314
+ app.set("view engine", "ejs");
315
+ app.set("views", path.join(__dirname, "views"));
316
+
317
+ // === Session ===
318
+ app.use(
319
+ session({
320
+ secret: SESSION_SECRET,
321
+ resave: false,
322
+ saveUninitialized: false,
323
+ cookie: {
324
+ httpOnly: true,
325
+ sameSite: "lax",
326
+ secure: process.env.NODE_ENV === "production",
327
+ maxAge: 24 * 60 * 60 * 1000 // 24 hours
328
+ },
329
+ })
330
+ );
331
+
332
+ // === Auth Middleware ===
333
+ function requireAuth(req, res, next) {
334
+ if (req.session?.authed) return next();
335
+ return res.redirect("/login");
336
+ }
337
+
338
+ // === Routes ===
339
+ app.get("/login", (req, res) => {
340
+ if (req.session?.authed) return res.redirect("/");
341
+ res.render("login", { error: null });
342
+ });
343
+
344
+ app.post("/login", async (req, res) => {
345
+ const { password } = req.body;
346
+ try {
347
+ if (!PASSWORD_HASH) {
348
+ console.error("PASSWORD_HASH not set in environment variables");
349
+ return res.render("login", { error: "Server configuration error" });
350
+ }
351
+
352
+ const isValid = await bcrypt.compare(password || "", PASSWORD_HASH);
353
+ if (isValid) {
354
+ req.session.authed = true;
355
+ return res.redirect("/");
356
+ } else {
357
+ return res.render("login", { error: "Invalid password" });
358
+ }
359
+ } catch (error) {
360
+ console.error("Login error:", error);
361
+ return res.render("login", { error: "Authentication error" });
362
+ }
363
+ });
364
+
365
+ app.get("/logout", (req, res) => {
366
+ req.session.destroy(() => res.redirect("/login"));
367
+ });
368
+
369
+ app.get("/", requireAuth, (req, res) => {
370
+ res.render("terminal");
371
+ });
372
+
373
+ app.get("/sysinfo", requireAuth, async (req, res) => {
374
+ try {
375
+ const systemInfo = await systemMonitor.getSystemInfo();
376
+ if (systemInfo) {
377
+ res.json(systemInfo);
378
+ } else {
379
+ res.status(500).json({ error: "Failed to fetch system information" });
380
+ }
381
+ } catch (error) {
382
+ console.error("System info endpoint error:", error);
383
+ res.status(500).json({ error: "Internal server error" });
384
+ }
385
+ });
386
+
387
+ // === Terminal Class for better process management ===
388
+ class Terminal {
389
+ constructor(socket) {
390
+ this.socket = socket;
391
+ this.process = null;
392
+ this.isActive = true;
393
+ }
394
+
395
+ async initialize() {
396
+ try {
397
+ // Try to use node-pty first
398
+ const nodePty = await import("node-pty");
399
+ return this.initWithPty(nodePty);
400
+ } catch (error) {
401
+ console.warn("node-pty not available, using fallback:", error.message);
402
+ return this.initWithSpawn();
403
+ }
404
+ }
405
+
406
+ initWithPty(nodePty) {
407
+ const shell = this.getShell();
408
+ const env = { ...process.env };
409
+
410
+ // Set better environment for cross-platform compatibility
411
+ if (isWindows) {
412
+ env.TERM = 'windows-ansi';
413
+ } else {
414
+ env.TERM = 'xterm-256color';
415
+ env.COLORTERM = 'truecolor';
416
+ }
417
+
418
+ this.process = nodePty.spawn(shell, [], {
419
+ name: isWindows ? 'windows-ansi' : 'xterm-256color',
420
+ cols: 80,
421
+ rows: 24,
422
+ cwd: this.getInitialDirectory(),
423
+ env: env,
424
+ useConpty: isWindows, // Use ConPTY on Windows 10+
425
+ });
426
+
427
+ this.process.onData(data => {
428
+ if (this.isActive) {
429
+ this.socket.emit("terminal_output", data);
430
+ }
431
+ });
432
+
433
+ this.process.onExit(() => {
434
+ if (this.isActive) {
435
+ this.socket.emit("terminal_output", "\r\n\x1b[91mTerminal session ended\x1b[0m\r\n");
436
+ }
437
+ });
438
+
439
+ // Socket event handlers
440
+ this.socket.on("terminal_input", (data) => {
441
+ if (this.process && this.isActive) {
442
+ this.process.write(data);
443
+ }
444
+ });
445
+
446
+ this.socket.on("terminal_resize", ({ cols, rows }) => {
447
+ if (this.process && this.isActive) {
448
+ this.process.resize(cols || 80, rows || 24);
449
+ }
450
+ });
451
+
452
+ console.log(`Terminal initialized with node-pty (${shell})`);
453
+ return true;
454
+ }
455
+
456
+ initWithSpawn() {
457
+ const shell = this.getShell();
458
+ const args = this.getShellArgs();
459
+
460
+ this.process = spawn(shell, args, {
461
+ cwd: this.getInitialDirectory(),
462
+ env: { ...process.env, TERM: isWindows ? 'windows-ansi' : 'xterm-256color' },
463
+ stdio: ['pipe', 'pipe', 'pipe']
464
+ });
465
+
466
+ this.process.stdout.on("data", (data) => {
467
+ if (this.isActive) {
468
+ this.socket.emit("terminal_output", data.toString());
469
+ }
470
+ });
471
+
472
+ this.process.stderr.on("data", (data) => {
473
+ if (this.isActive) {
474
+ this.socket.emit("terminal_output", data.toString());
475
+ }
476
+ });
477
+
478
+ this.process.on("exit", (code) => {
479
+ if (this.isActive) {
480
+ this.socket.emit("terminal_output", `\r\n\x1b[91mProcess exited with code ${code}\x1b[0m\r\n`);
481
+ }
482
+ });
483
+
484
+ this.socket.on("terminal_input", (data) => {
485
+ if (this.process && this.process.stdin && this.isActive) {
486
+ this.process.stdin.write(data);
487
+ }
488
+ });
489
+
490
+ console.log(`Terminal initialized with spawn fallback (${shell})`);
491
+ return true;
492
+ }
493
+
494
+ getShell() {
495
+ if (isWindows) {
496
+ return process.env.COMSPEC || "cmd.exe";
497
+ } else {
498
+ return process.env.SHELL || "/bin/bash";
499
+ }
500
+ }
501
+
502
+ getShellArgs() {
503
+ if (isWindows) {
504
+ return [];
505
+ } else {
506
+ return ["-l"]; // Login shell
507
+ }
508
+ }
509
+
510
+ getInitialDirectory() {
511
+ if (isWindows) {
512
+ return process.env.USERPROFILE || process.cwd();
513
+ } else {
514
+ return process.env.HOME || process.cwd();
515
+ }
516
+ }
517
+
518
+ destroy() {
519
+ this.isActive = false;
520
+ if (this.process) {
521
+ try {
522
+ if (typeof this.process.kill === 'function') {
523
+ this.process.kill();
524
+ } else if (typeof this.process.destroy === 'function') {
525
+ this.process.destroy();
526
+ }
527
+ } catch (error) {
528
+ console.warn("Error destroying terminal process:", error.message);
529
+ }
530
+ }
531
+ }
532
+ }
533
+
534
+ // === Socket.IO Connection Handler ===
535
+ io.on("connection", async (socket) => {
536
+ console.log(`Socket connected: ${socket.id} from ${socket.handshake.address}`);
537
+
538
+ // Create terminal instance
539
+ const terminal = new Terminal(socket);
540
+ const initSuccess = await terminal.initialize();
541
+
542
+ if (!initSuccess) {
543
+ socket.emit("terminal_output", "\r\n\x1b[91mFailed to initialize terminal\x1b[0m\r\n");
544
+ }
545
+
546
+ // Send welcome message
547
+ const welcomeMessage = `\x1b[32m╭─────────────────────────────────────────────────╮\x1b[0m\r\n` +
548
+ `\x1b[32m│\x1b[0m \x1b[1mWeb Terminal v2.0\x1b[0m \x1b[32m│\x1b[0m\r\n` +
549
+ `\x1b[32m│\x1b[0m Platform: ${process.platform} (${process.arch})${' '.repeat(Math.max(0, 15 - process.platform.length - process.arch.length))} \x1b[32m│\x1b[0m\r\n` +
550
+ `\x1b[32m│\x1b[0m Shell: ${terminal.getShell()}${' '.repeat(Math.max(0, 25 - terminal.getShell().length))} \x1b[32m│\x1b[0m\r\n` +
551
+ `\x1b[32m╰─────────────────────────────────────────────────╯\x1b[0m\r\n\r\n`;
552
+
553
+ socket.emit("terminal_output", welcomeMessage);
554
+
555
+ // Real-time system monitoring
556
+ const systemInfoInterval = setInterval(async () => {
557
+ try {
558
+ const systemInfo = await systemMonitor.getSystemInfo();
559
+ if (systemInfo && socket.connected) {
560
+ socket.emit("system_info", systemInfo);
561
+ }
562
+ } catch (error) {
563
+ console.error("System monitoring error:", error);
564
+ }
565
+ }, 2000);
566
+
567
+ // Cleanup on disconnect
568
+ socket.on("disconnect", () => {
569
+ console.log(`Socket disconnected: ${socket.id}`);
570
+ clearInterval(systemInfoInterval);
571
+ terminal.destroy();
572
+ });
573
+
574
+ // Error handling
575
+ socket.on("error", (error) => {
576
+ console.error(`Socket error for ${socket.id}:`, error);
577
+ });
578
+ });
579
+
580
+ // === Error Handling ===
581
+ process.on('uncaughtException', (error) => {
582
+ console.error('Uncaught Exception:', error);
583
+ });
584
+
585
+ process.on('unhandledRejection', (reason, promise) => {
586
+ console.error('Unhandled Rejection at:', promise, 'reason:', reason);
587
+ });
588
+
589
+ // === Graceful Shutdown ===
590
+ process.on('SIGTERM', () => {
591
+ console.log('SIGTERM received, shutting down gracefully');
592
+ httpServer.close(() => {
593
+ console.log('Process terminated');
594
+ process.exit(0);
595
+ });
596
+ });
597
+
598
+ process.on('SIGINT', () => {
599
+ console.log('SIGINT received, shutting down gracefully');
600
+ httpServer.close(() => {
601
+ console.log('Process terminated');
602
+ process.exit(0);
603
+ });
604
+ });
605
+
606
+ // === Start Server ===
607
+ httpServer.listen(PORT, () => {
608
+ console.log(`✅ Web Terminal Server running on http://localhost:${PORT}`);
609
+ console.log(`📊 Platform: ${process.platform} (${process.arch})`);
610
+ console.log(`🐚 Default Shell: ${isWindows ? process.env.COMSPEC || "cmd.exe" : process.env.SHELL || "/bin/bash"}`);
611
+ console.log(`🔐 Authentication: ${PASSWORD_HASH ? 'Enabled' : 'Disabled (set WEBTERM_PASSWORD_HASH)'}`);
612
+ });
613
+
614
+ export default app;
package.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "web-terminal",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "start": "node app.js"
7
+ },
8
+ "dependencies": {
9
+ "bcrypt": "^5.1.0",
10
+ "dotenv": "^16.6.1",
11
+ "ejs": "^3.1.9",
12
+ "express": "^4.18.2",
13
+ "express-session": "^1.17.3",
14
+ "helmet": "^7.0.0",
15
+ "node-os-utils": "^1.3.7",
16
+ "node-pty": "^1.1.0-beta28",
17
+ "os": "^0.1.2",
18
+ "socket.io": "^4.7.2"
19
+ }
20
+ }
public/js/terminal.js ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const socket = io();
2
+
3
+ // --- Terminal setup ---
4
+ const term = new Terminal({
5
+ cursorBlink: true,
6
+ fontSize: 14,
7
+ theme: { background: '#0f172a', foreground: '#e5e7eb' }
8
+ });
9
+ term.open(document.getElementById("term"));
10
+ term.focus();
11
+
12
+ // Kirim input ke server
13
+ term.onData(data => socket.emit("input", data));
14
+
15
+ // Terima output dari server
16
+ socket.on("output", data => term.write(data));
17
+
18
+ // Resize terminal (tidak berfungsi sempurna tanpa node-pty)
19
+ function resizeTerminal() {
20
+ const cols = term.cols;
21
+ const rows = term.rows;
22
+ socket.emit("resize", { cols, rows });
23
+ }
24
+ window.addEventListener("resize", resizeTerminal);
25
+ resizeTerminal();
26
+
27
+ // --- Load system info ---
28
+ async function loadSysinfo() {
29
+ try {
30
+ const res = await fetch("/sysinfo");
31
+ const info = await res.json();
32
+ const el = document.getElementById("sysinfo");
33
+ el.innerHTML = `
34
+ <p><b>Host:</b> ${info.hostname} (${info.platform} ${info.release}, ${info.arch})</p>
35
+ <p><b>CPU:</b> ${info.cpu_model} (${info.cpu_cores} cores)</p>
36
+ <p><b>CPU Usage:</b> ${info.cpu_usage}%</p>
37
+ <p><b>Memory:</b> ${info.used_mem}MB / ${info.total_mem}MB (${info.mem_usage}%)</p>
38
+ <p><b>Uptime:</b> ${info.uptime}</p>
39
+ `;
40
+ } catch (err) {
41
+ console.error(err);
42
+ }
43
+ }
44
+ loadSysinfo();
45
+ setInterval(loadSysinfo, 5000);
views/login.ejs ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Web Terminal - Login</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=JetBrains+Mono:wght@300;400;500;600&display=swap" rel="stylesheet">
10
+ <style>
11
+ * {
12
+ margin: 0;
13
+ padding: 0;
14
+ box-sizing: border-box;
15
+ }
16
+
17
+ body {
18
+ font-family: 'JetBrains Mono', monospace;
19
+ background: linear-gradient(135deg, #0c0c0c 0%, #1a1a1a 100%);
20
+ color: #e4e4e7;
21
+ min-height: 100vh;
22
+ display: flex;
23
+ align-items: center;
24
+ justify-content: center;
25
+ overflow: hidden;
26
+ }
27
+
28
+ .login-container {
29
+ background: rgba(24, 24, 27, 0.95);
30
+ backdrop-filter: blur(10px);
31
+ border: 1px solid rgba(63, 63, 70, 0.5);
32
+ border-radius: 12px;
33
+ padding: 40px;
34
+ width: 100%;
35
+ max-width: 400px;
36
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
37
+ position: relative;
38
+ overflow: hidden;
39
+ }
40
+
41
+ .login-container::before {
42
+ content: '';
43
+ position: absolute;
44
+ top: 0;
45
+ left: 0;
46
+ right: 0;
47
+ height: 1px;
48
+ background: linear-gradient(90deg, transparent, #10b981, transparent);
49
+ }
50
+
51
+ .login-header {
52
+ text-align: center;
53
+ margin-bottom: 32px;
54
+ }
55
+
56
+ .logo {
57
+ display: flex;
58
+ align-items: center;
59
+ justify-content: center;
60
+ gap: 12px;
61
+ font-size: 24px;
62
+ font-weight: 600;
63
+ margin-bottom: 8px;
64
+ }
65
+
66
+ .status-dot {
67
+ width: 12px;
68
+ height: 12px;
69
+ background: #10b981;
70
+ border-radius: 50%;
71
+ animation: pulse 2s infinite;
72
+ }
73
+
74
+ @keyframes pulse {
75
+ 0%, 100% { opacity: 1; transform: scale(1); }
76
+ 50% { opacity: 0.3; transform: scale(1.1); }
77
+ }
78
+
79
+ .login-subtitle {
80
+ color: #a1a1aa;
81
+ font-size: 14px;
82
+ margin-top: 8px;
83
+ }
84
+
85
+ .form-group {
86
+ margin-bottom: 24px;
87
+ }
88
+
89
+ .form-label {
90
+ display: block;
91
+ color: #d4d4d8;
92
+ font-size: 13px;
93
+ font-weight: 500;
94
+ margin-bottom: 8px;
95
+ text-transform: uppercase;
96
+ letter-spacing: 0.5px;
97
+ }
98
+
99
+ .form-input {
100
+ width: 100%;
101
+ background: rgba(39, 39, 42, 0.8);
102
+ border: 1px solid rgba(63, 63, 70, 0.5);
103
+ border-radius: 8px;
104
+ padding: 12px 16px;
105
+ color: #e4e4e7;
106
+ font-family: inherit;
107
+ font-size: 14px;
108
+ transition: all 0.2s ease;
109
+ outline: none;
110
+ }
111
+
112
+ .form-input:focus {
113
+ border-color: #10b981;
114
+ box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
115
+ background: rgba(39, 39, 42, 0.95);
116
+ }
117
+
118
+ .form-input::placeholder {
119
+ color: #71717a;
120
+ }
121
+
122
+ .login-button {
123
+ width: 100%;
124
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
125
+ border: none;
126
+ border-radius: 8px;
127
+ padding: 14px 24px;
128
+ color: white;
129
+ font-family: inherit;
130
+ font-size: 14px;
131
+ font-weight: 500;
132
+ cursor: pointer;
133
+ transition: all 0.2s ease;
134
+ text-transform: uppercase;
135
+ letter-spacing: 0.5px;
136
+ }
137
+
138
+ .login-button:hover {
139
+ transform: translateY(-2px);
140
+ box-shadow: 0 8px 25px rgba(16, 185, 129, 0.3);
141
+ }
142
+
143
+ .login-button:active {
144
+ transform: translateY(0);
145
+ }
146
+
147
+ .error-message {
148
+ background: rgba(239, 68, 68, 0.1);
149
+ border: 1px solid rgba(239, 68, 68, 0.3);
150
+ border-radius: 6px;
151
+ padding: 12px;
152
+ color: #fca5a5;
153
+ font-size: 13px;
154
+ margin-bottom: 20px;
155
+ text-align: center;
156
+ }
157
+
158
+ .terminal-demo {
159
+ position: absolute;
160
+ top: 50%;
161
+ left: -300px;
162
+ transform: translateY(-50%);
163
+ background: rgba(12, 12, 12, 0.8);
164
+ border-radius: 8px;
165
+ padding: 20px;
166
+ width: 250px;
167
+ font-size: 11px;
168
+ line-height: 1.4;
169
+ border: 1px solid rgba(63, 63, 70, 0.3);
170
+ animation: float 6s ease-in-out infinite;
171
+ z-index: -1;
172
+ }
173
+
174
+ .terminal-demo::before {
175
+ content: '$ ';
176
+ color: #10b981;
177
+ }
178
+
179
+ @keyframes float {
180
+ 0%, 100% { transform: translateY(-50%) translateX(0); }
181
+ 50% { transform: translateY(-60%) translateX(10px); }
182
+ }
183
+
184
+ .terminal-demo-right {
185
+ left: auto;
186
+ right: -300px;
187
+ animation-delay: 3s;
188
+ }
189
+
190
+ @media (max-width: 768px) {
191
+ .login-container {
192
+ margin: 20px;
193
+ padding: 30px 24px;
194
+ }
195
+
196
+ .terminal-demo,
197
+ .terminal-demo-right {
198
+ display: none;
199
+ }
200
+ }
201
+
202
+ .footer {
203
+ text-align: center;
204
+ margin-top: 24px;
205
+ padding-top: 20px;
206
+ border-top: 1px solid rgba(63, 63, 70, 0.3);
207
+ color: #71717a;
208
+ font-size: 12px;
209
+ }
210
+ </style>
211
+ </head>
212
+ <body>
213
+ <div class="terminal-demo">
214
+ <div style="color: #10b981;">user@terminal:~$</div>
215
+ <div style="color: #e4e4e7;">ls -la</div>
216
+ <div style="color: #a1a1aa;">drwxr-xr-x 8 user user 4096</div>
217
+ <div style="color: #a1a1aa;">-rw-r--r-- 1 user user 220</div>
218
+ <div style="color: #10b981;">user@terminal:~$</div>
219
+ </div>
220
+
221
+ <div class="terminal-demo terminal-demo-right">
222
+ <div style="color: #10b981;">root@server:/#</div>
223
+ <div style="color: #e4e4e7;">systemctl status</div>
224
+ <div style="color: #22c55e;">● Active (running)</div>
225
+ <div style="color: #a1a1aa;">Memory: 512.3M</div>
226
+ <div style="color: #10b981;">root@server:/#</div>
227
+ </div>
228
+
229
+ <div class="login-container">
230
+ <div class="login-header">
231
+ <div class="logo">
232
+ <div class="status-dot"></div>
233
+ Web Terminal
234
+ </div>
235
+ <div class="login-subtitle">Secure Shell Access</div>
236
+ </div>
237
+
238
+ <% if (error) { %>
239
+ <div class="error-message">
240
+ ⚠️ <%= error %>
241
+ </div>
242
+ <% } %>
243
+
244
+ <form method="POST" action="/login">
245
+ <div class="form-group">
246
+ <label for="password" class="form-label">Password</label>
247
+ <input
248
+ type="password"
249
+ id="password"
250
+ name="password"
251
+ class="form-input"
252
+ placeholder="Enter your password"
253
+ required
254
+ autofocus
255
+ >
256
+ </div>
257
+
258
+ <button type="submit" class="login-button">
259
+ Access Terminal
260
+ </button>
261
+ </form>
262
+
263
+ <div class="footer">
264
+ <div>🔒 Secure connection established</div>
265
+ <div style="margin-top: 4px;">Web Terminal v2.0</div>
266
+ </div>
267
+ </div>
268
+
269
+ <script>
270
+ // Auto-focus password field
271
+ document.getElementById('password').focus();
272
+
273
+ // Handle form submission
274
+ document.querySelector('form').addEventListener('submit', function(e) {
275
+ const button = document.querySelector('.login-button');
276
+ button.textContent = 'Connecting...';
277
+ button.disabled = true;
278
+ });
279
+
280
+ // Add some visual flair
281
+ document.addEventListener('DOMContentLoaded', function() {
282
+ const container = document.querySelector('.login-container');
283
+ container.style.opacity = '0';
284
+ container.style.transform = 'translateY(20px)';
285
+
286
+ setTimeout(() => {
287
+ container.style.transition = 'all 0.5s ease';
288
+ container.style.opacity = '1';
289
+ container.style.transform = 'translateY(0)';
290
+ }, 100);
291
+ });
292
+ </script>
293
+ </body>
294
+ </html>
views/terminal.ejs ADDED
@@ -0,0 +1,998 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Terminal</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=JetBrains+Mono:wght@300;400;500;600&display=swap" rel="stylesheet">
10
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css">
11
+ <style>
12
+ * {
13
+ margin: 0;
14
+ padding: 0;
15
+ box-sizing: border-box;
16
+ }
17
+
18
+ body {
19
+ font-family: 'JetBrains Mono', monospace;
20
+ background: linear-gradient(135deg, #0c0c0c 0%, #1a1a1a 100%);
21
+ color: #e4e4e7;
22
+ min-height: 100vh;
23
+ overflow: hidden;
24
+ }
25
+
26
+ .container {
27
+ display: grid;
28
+ grid-template-columns: 1fr 320px;
29
+ grid-template-rows: 60px 1fr;
30
+ height: 100vh;
31
+ gap: 1px;
32
+ background: #27272a;
33
+ }
34
+
35
+ .header {
36
+ grid-column: 1 / -1;
37
+ background: rgba(39, 39, 42, 0.95);
38
+ backdrop-filter: blur(10px);
39
+ border-bottom: 1px solid #3f3f46;
40
+ display: flex;
41
+ align-items: center;
42
+ justify-content: space-between;
43
+ padding: 0 24px;
44
+ position: relative;
45
+ }
46
+
47
+ .header::before {
48
+ content: '';
49
+ position: absolute;
50
+ top: 0;
51
+ left: 0;
52
+ right: 0;
53
+ height: 1px;
54
+ background: linear-gradient(90deg, transparent, #10b981, transparent);
55
+ }
56
+
57
+ .logo {
58
+ display: flex;
59
+ align-items: center;
60
+ gap: 8px;
61
+ font-weight: 600;
62
+ font-size: 16px;
63
+ }
64
+
65
+ .status-dot {
66
+ width: 8px;
67
+ height: 8px;
68
+ background: #10b981;
69
+ border-radius: 50%;
70
+ animation: pulse 2s infinite;
71
+ }
72
+
73
+ @keyframes pulse {
74
+ 0%, 100% { opacity: 1; }
75
+ 50% { opacity: 0.3; }
76
+ }
77
+
78
+ .header-actions {
79
+ display: flex;
80
+ gap: 12px;
81
+ align-items: center;
82
+ }
83
+
84
+ .btn {
85
+ background: rgba(255, 255, 255, 0.05);
86
+ border: 1px solid rgba(255, 255, 255, 0.1);
87
+ color: #e4e4e7;
88
+ padding: 6px 12px;
89
+ border-radius: 6px;
90
+ font-size: 12px;
91
+ cursor: pointer;
92
+ transition: all 0.2s ease;
93
+ font-family: inherit;
94
+ }
95
+
96
+ .btn:hover {
97
+ background: rgba(255, 255, 255, 0.1);
98
+ border-color: rgba(255, 255, 255, 0.2);
99
+ }
100
+
101
+ .btn.danger:hover {
102
+ background: rgba(239, 68, 68, 0.2);
103
+ border-color: #ef4444;
104
+ }
105
+
106
+ .terminal-section {
107
+ background: #18181b;
108
+ border-right: 1px solid #3f3f46;
109
+ position: relative;
110
+ }
111
+
112
+ .terminal-header {
113
+ background: rgba(39, 39, 42, 0.8);
114
+ padding: 12px 20px;
115
+ border-bottom: 1px solid #3f3f46;
116
+ font-size: 13px;
117
+ font-weight: 500;
118
+ display: flex;
119
+ align-items: center;
120
+ gap: 8px;
121
+ }
122
+
123
+ .terminal-icon {
124
+ color: #10b981;
125
+ }
126
+
127
+ #terminal {
128
+ height: calc(100vh - 60px - 45px);
129
+ padding: 16px;
130
+ background: #0c0c0c;
131
+ }
132
+
133
+ .sidebar {
134
+ background: #18181b;
135
+ display: flex;
136
+ flex-direction: column;
137
+ }
138
+
139
+ .sidebar-section {
140
+ border-bottom: 1px solid #3f3f46;
141
+ padding: 20px;
142
+ background: rgba(24, 24, 27, 0.5);
143
+ margin: 0;
144
+ border-radius: 0;
145
+ }
146
+
147
+ .sidebar-section:last-child {
148
+ border-bottom: none;
149
+ flex: 1;
150
+ }
151
+
152
+ .section-title {
153
+ font-size: 13px;
154
+ font-weight: 600;
155
+ color: #a1a1aa;
156
+ margin-bottom: 12px;
157
+ text-transform: uppercase;
158
+ letter-spacing: 0.5px;
159
+ }
160
+
161
+ .system-info {
162
+ display: flex;
163
+ flex-direction: column;
164
+ gap: 8px;
165
+ }
166
+
167
+ .info-item {
168
+ display: flex;
169
+ justify-content: space-between;
170
+ align-items: center;
171
+ font-size: 12px;
172
+ line-height: 1.4;
173
+ }
174
+
175
+ .info-label {
176
+ color: #71717a;
177
+ display: flex;
178
+ align-items: center;
179
+ gap: 6px;
180
+ }
181
+
182
+ .info-value {
183
+ color: #e4e4e7;
184
+ font-weight: 500;
185
+ }
186
+
187
+ .metric-bar {
188
+ width: 100%;
189
+ height: 4px;
190
+ background: rgba(255, 255, 255, 0.05);
191
+ border-radius: 2px;
192
+ margin-top: 6px;
193
+ overflow: hidden;
194
+ }
195
+
196
+ .metric-fill {
197
+ height: 100%;
198
+ border-radius: 2px;
199
+ transition: width 0.3s ease;
200
+ }
201
+
202
+ .cpu-fill {
203
+ background: linear-gradient(90deg, #10b981, #059669);
204
+ }
205
+
206
+ .memory-fill {
207
+ background: linear-gradient(90deg, #3b82f6, #2563eb);
208
+ }
209
+
210
+ .disk-fill {
211
+ background: linear-gradient(90deg, #f59e0b, #d97706);
212
+ }
213
+
214
+ .network-fill {
215
+ background: linear-gradient(90deg, #8b5cf6, #7c3aed);
216
+ }
217
+
218
+ .mini-chart {
219
+ height: 60px;
220
+ background: rgba(255, 255, 255, 0.02);
221
+ border-radius: 8px;
222
+ margin-top: 12px;
223
+ position: relative;
224
+ overflow: hidden;
225
+ display: flex;
226
+ align-items: end;
227
+ padding: 4px;
228
+ gap: 1px;
229
+ }
230
+
231
+ .chart-bar {
232
+ flex: 1;
233
+ background: rgba(16, 185, 129, 0.3);
234
+ border-radius: 1px 1px 0 0;
235
+ min-height: 2px;
236
+ transition: height 0.3s ease;
237
+ }
238
+
239
+ .processes {
240
+ max-height: 200px;
241
+ overflow-y: auto;
242
+ }
243
+
244
+ .processes::-webkit-scrollbar {
245
+ width: 4px;
246
+ }
247
+
248
+ .processes::-webkit-scrollbar-track {
249
+ background: rgba(255, 255, 255, 0.05);
250
+ }
251
+
252
+ .processes::-webkit-scrollbar-thumb {
253
+ background: rgba(255, 255, 255, 0.2);
254
+ border-radius: 2px;
255
+ }
256
+
257
+ .process-item {
258
+ display: flex;
259
+ justify-content: space-between;
260
+ align-items: center;
261
+ padding: 6px 0;
262
+ font-size: 11px;
263
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
264
+ }
265
+
266
+ .process-item:last-child {
267
+ border-bottom: none;
268
+ }
269
+
270
+ .process-name {
271
+ color: #e4e4e7;
272
+ flex: 1;
273
+ white-space: nowrap;
274
+ overflow: hidden;
275
+ text-overflow: ellipsis;
276
+ }
277
+
278
+ .process-cpu {
279
+ color: #10b981;
280
+ font-weight: 500;
281
+ margin-left: 8px;
282
+ }
283
+
284
+ .connection-status {
285
+ position: absolute;
286
+ top: 12px;
287
+ right: 20px;
288
+ font-size: 11px;
289
+ color: #71717a;
290
+ display: flex;
291
+ align-items: center;
292
+ gap: 6px;
293
+ }
294
+
295
+ .connection-dot {
296
+ width: 6px;
297
+ height: 6px;
298
+ border-radius: 50%;
299
+ background: #ef4444;
300
+ transition: background-color 0.3s ease;
301
+ }
302
+
303
+ .connection-dot.connected {
304
+ background: #10b981;
305
+ }
306
+
307
+ /* Mobile Styles */
308
+ @media (max-width: 768px) {
309
+ .container {
310
+ grid-template-columns: 1fr;
311
+ grid-template-rows: 50px 1fr 0px;
312
+ gap: 0;
313
+ }
314
+
315
+ .header {
316
+ padding: 0 16px;
317
+ font-size: 14px;
318
+ z-index: 1000;
319
+ position: relative;
320
+ }
321
+
322
+ .logo {
323
+ font-size: 14px;
324
+ gap: 6px;
325
+ }
326
+
327
+ .status-dot {
328
+ width: 6px;
329
+ height: 6px;
330
+ }
331
+
332
+ .btn {
333
+ padding: 4px 8px;
334
+ font-size: 11px;
335
+ }
336
+
337
+ .terminal-section {
338
+ border-right: none;
339
+ position: relative;
340
+ display: flex;
341
+ flex-direction: column;
342
+ }
343
+
344
+ .terminal-header {
345
+ padding: 8px 16px;
346
+ font-size: 12px;
347
+ position: relative;
348
+ z-index: 100;
349
+ }
350
+
351
+ #terminal {
352
+ flex: 1;
353
+ height: auto;
354
+ min-height: 200px;
355
+ padding: 12px;
356
+ margin-bottom: 12px;
357
+ }
358
+
359
+ .connection-status {
360
+ top: 8px;
361
+ right: 16px;
362
+ font-size: 10px;
363
+ }
364
+
365
+ .sidebar {
366
+ display: none; /* Hidden by default on mobile */
367
+ position: fixed;
368
+ bottom: 0;
369
+ left: 0;
370
+ right: 0;
371
+ background: #18181b;
372
+ border-top: 2px solid #3f3f46;
373
+ max-height: 70vh;
374
+ overflow-y: auto;
375
+ z-index: 1001;
376
+ transform: translateY(100%);
377
+ transition: transform 0.3s ease;
378
+ border-radius: 16px 16px 0 0;
379
+ box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.6);
380
+ }
381
+
382
+ .sidebar.show {
383
+ display: flex;
384
+ transform: translateY(0);
385
+ }
386
+
387
+ .sidebar-section {
388
+ padding: 16px 20px;
389
+ margin: 8px 16px;
390
+ background: rgba(39, 39, 42, 0.6);
391
+ border-radius: 12px;
392
+ border: 1px solid rgba(63, 63, 70, 0.3);
393
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
394
+ backdrop-filter: blur(10px);
395
+ }
396
+
397
+ .sidebar-section:first-child {
398
+ margin-top: 16px;
399
+ }
400
+
401
+ .sidebar-section:last-child {
402
+ margin-bottom: 16px;
403
+ flex: none;
404
+ }
405
+
406
+ .section-title {
407
+ font-size: 12px;
408
+ margin-bottom: 12px;
409
+ color: #10b981;
410
+ display: flex;
411
+ align-items: center;
412
+ gap: 8px;
413
+ }
414
+
415
+ .section-title::before {
416
+ content: '';
417
+ width: 3px;
418
+ height: 12px;
419
+ background: linear-gradient(135deg, #10b981, #059669);
420
+ border-radius: 2px;
421
+ }
422
+
423
+ .info-item {
424
+ font-size: 11px;
425
+ padding: 4px 0;
426
+ }
427
+
428
+ .processes {
429
+ max-height: 120px;
430
+ }
431
+
432
+ .mini-chart {
433
+ height: 50px;
434
+ margin-top: 8px;
435
+ }
436
+
437
+ /* Mobile toggle button */
438
+ .mobile-toggle {
439
+ display: block;
440
+ position: fixed;
441
+ bottom: 20px;
442
+ right: 20px;
443
+ width: 56px;
444
+ height: 56px;
445
+ background: linear-gradient(135deg, #10b981, #059669);
446
+ border: none;
447
+ border-radius: 50%;
448
+ color: white;
449
+ font-size: 24px;
450
+ cursor: pointer;
451
+ z-index: 1002;
452
+ box-shadow: 0 8px 25px rgba(16, 185, 129, 0.4);
453
+ transition: all 0.3s ease;
454
+ display: flex;
455
+ align-items: center;
456
+ justify-content: center;
457
+ }
458
+
459
+ .mobile-toggle:hover {
460
+ transform: scale(1.1);
461
+ box-shadow: 0 12px 35px rgba(16, 185, 129, 0.6);
462
+ }
463
+
464
+ .mobile-toggle.active {
465
+ background: linear-gradient(135deg, #ef4444, #dc2626);
466
+ transform: rotate(45deg);
467
+ }
468
+
469
+ /* Mobile overlay */
470
+ .mobile-overlay {
471
+ display: none;
472
+ position: fixed;
473
+ top: 0;
474
+ left: 0;
475
+ right: 0;
476
+ bottom: 0;
477
+ background: rgba(0, 0, 0, 0.5);
478
+ z-index: 1000;
479
+ opacity: 0;
480
+ transition: opacity 0.3s ease;
481
+ }
482
+
483
+ .mobile-overlay.show {
484
+ display: block;
485
+ opacity: 1;
486
+ }
487
+ }
488
+
489
+ @media (max-width: 480px) {
490
+ .container {
491
+ grid-template-rows: 45px 1fr 0px;
492
+ }
493
+
494
+ .header {
495
+ padding: 0 12px;
496
+ }
497
+
498
+ .header-actions {
499
+ gap: 8px;
500
+ }
501
+
502
+ .sidebar-section {
503
+ margin: 6px 12px;
504
+ padding: 12px 16px;
505
+ }
506
+
507
+ .mobile-toggle {
508
+ width: 50px;
509
+ height: 50px;
510
+ bottom: 16px;
511
+ right: 16px;
512
+ font-size: 20px;
513
+ }
514
+ }
515
+
516
+ /* Desktop styles - hide mobile elements */
517
+ @media (min-width: 769px) {
518
+ .mobile-toggle,
519
+ .mobile-overlay {
520
+ display: none !important;
521
+ }
522
+
523
+ .sidebar {
524
+ display: flex !important;
525
+ position: relative !important;
526
+ transform: none !important;
527
+ }
528
+ }
529
+ </style>
530
+ </head>
531
+ <body>
532
+ <div class="container">
533
+ <div class="header">
534
+ <div class="logo">
535
+ <div class="status-dot"></div>
536
+ Terminal
537
+ </div>
538
+ <div class="header-actions">
539
+ <button class="btn" id="clear-btn">Clear</button>
540
+ <a href="/logout" class="btn danger">Logout</a>
541
+ </div>
542
+ </div>
543
+
544
+ <div class="terminal-section">
545
+ <div class="terminal-header">
546
+ <span class="terminal-icon">></span>
547
+ Shell Session
548
+ </div>
549
+ <div id="terminal"></div>
550
+ <div class="connection-status">
551
+ <div class="connection-dot" id="connection-dot"></div>
552
+ <span id="connection-text">Connecting...</span>
553
+ </div>
554
+ </div>
555
+
556
+ <div class="sidebar">
557
+ <div class="sidebar-section">
558
+ <div class="section-title">System Overview</div>
559
+ <div class="system-info">
560
+ <div class="info-item">
561
+ <div class="info-label">🖥️ Host</div>
562
+ <div class="info-value" id="hostname">-</div>
563
+ </div>
564
+ <div class="info-item">
565
+ <div class="info-label">⏱️ Uptime</div>
566
+ <div class="info-value" id="uptime">-</div>
567
+ </div>
568
+ <div class="info-item">
569
+ <div class="info-label">👥 Users</div>
570
+ <div class="info-value" id="users">-</div>
571
+ </div>
572
+ </div>
573
+ </div>
574
+
575
+ <div class="sidebar-section">
576
+ <div class="section-title">Performance</div>
577
+ <div class="system-info">
578
+ <div class="info-item">
579
+ <div class="info-label">🧠 CPU</div>
580
+ <div class="info-value" id="cpu-usage">0%</div>
581
+ </div>
582
+ <div class="metric-bar">
583
+ <div class="metric-fill cpu-fill" id="cpu-bar" style="width: 0%"></div>
584
+ </div>
585
+
586
+ <div class="info-item" style="margin-top: 16px;">
587
+ <div class="info-label">💾 Memory</div>
588
+ <div class="info-value" id="memory-usage">0%</div>
589
+ </div>
590
+ <div class="metric-bar">
591
+ <div class="metric-fill memory-fill" id="memory-bar" style="width: 0%"></div>
592
+ </div>
593
+
594
+ <div class="info-item" style="margin-top: 16px;">
595
+ <div class="info-label">💿 Disk</div>
596
+ <div class="info-value" id="disk-usage">0%</div>
597
+ </div>
598
+ <div class="metric-bar">
599
+ <div class="metric-fill disk-fill" id="disk-bar" style="width: 0%"></div>
600
+ </div>
601
+
602
+ <div class="mini-chart" id="cpu-chart"></div>
603
+ </div>
604
+ </div>
605
+
606
+ <div class="sidebar-section">
607
+ <div class="section-title">Network & Storage</div>
608
+ <div class="system-info">
609
+ <div class="info-item">
610
+ <div class="info-label">🌐 Network RX</div>
611
+ <div class="info-value" id="network-rx">0 KB/s</div>
612
+ </div>
613
+ <div class="info-item">
614
+ <div class="info-label">📡 Network TX</div>
615
+ <div class="info-value" id="network-tx">0 KB/s</div>
616
+ </div>
617
+ <div class="info-item" style="margin-top: 8px;">
618
+ <div class="info-label">💾 Memory Used</div>
619
+ <div class="info-value" id="memory-details">0 MB / 0 MB</div>
620
+ </div>
621
+ <div class="info-item">
622
+ <div class="info-label">💿 Disk Space</div>
623
+ <div class="info-value" id="disk-details">0 GB / 0 GB</div>
624
+ </div>
625
+ </div>
626
+ </div>
627
+
628
+ <div class="sidebar-section">
629
+ <div class="section-title">System Details</div>
630
+ <div class="system-info">
631
+ <div class="info-item">
632
+ <div class="info-label">🏗️ Platform</div>
633
+ <div class="info-value" id="platform">-</div>
634
+ </div>
635
+ <div class="info-item">
636
+ <div class="info-label">🔧 Architecture</div>
637
+ <div class="info-value" id="arch">-</div>
638
+ </div>
639
+ <div class="info-item">
640
+ <div class="info-label">🧠 CPU Model</div>
641
+ <div class="info-value" id="cpu-model">-</div>
642
+ </div>
643
+ <div class="info-item">
644
+ <div class="info-label">⚡ CPU Cores</div>
645
+ <div class="info-value" id="cpu-cores">-</div>
646
+ </div>
647
+ <div class="info-item">
648
+ <div class="info-label">📊 Load Avg</div>
649
+ <div class="info-value" id="load-avg">-</div>
650
+ </div>
651
+ </div>
652
+ </div>
653
+ <div class="sidebar-section">
654
+ <div class="section-title">Top Processes</div>
655
+ <div class="processes" id="processes">
656
+ <div class="process-item">
657
+ <div class="process-name">Loading...</div>
658
+ <div class="process-cpu">-</div>
659
+ </div>
660
+ </div>
661
+ </div>
662
+ </div>
663
+ </div>
664
+
665
+ <!-- Mobile elements -->
666
+ <div class="mobile-overlay" id="mobile-overlay"></div>
667
+ <button class="mobile-toggle" id="mobile-toggle">����</button>
668
+
669
+ <script src="/socket.io/socket.io.js"></script>
670
+ <script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
671
+ <script>
672
+ // Initialize terminal
673
+ let terminal;
674
+ let socket;
675
+ let isConnected = false;
676
+ let cpuHistory = [];
677
+
678
+ // Initialize when page loads
679
+ function initializeTerminal() {
680
+ terminal = new Terminal({
681
+ cursorBlink: true,
682
+ fontSize: 14,
683
+ fontFamily: 'JetBrains Mono, monospace',
684
+ theme: {
685
+ background: '#0c0c0c',
686
+ foreground: '#e4e4e7',
687
+ cursor: '#10b981',
688
+ selection: 'rgba(16, 185, 129, 0.3)',
689
+ black: '#27272a',
690
+ red: '#ef4444',
691
+ green: '#10b981',
692
+ yellow: '#f59e0b',
693
+ blue: '#3b82f6',
694
+ magenta: '#a855f7',
695
+ cyan: '#06b6d4',
696
+ white: '#f4f4f5'
697
+ }
698
+ });
699
+
700
+ terminal.open(document.getElementById('terminal'));
701
+ terminal.focus();
702
+
703
+ // Connect to server
704
+ connectToServer();
705
+
706
+ // Add event listeners for buttons
707
+ document.getElementById('clear-btn').addEventListener('click', clearTerminal);
708
+
709
+ // Mobile functionality
710
+ initializeMobileToggle();
711
+ }
712
+
713
+ function initializeMobileToggle() {
714
+ const mobileToggle = document.getElementById('mobile-toggle');
715
+ const sidebar = document.querySelector('.sidebar');
716
+ const overlay = document.getElementById('mobile-overlay');
717
+
718
+ if (mobileToggle && sidebar && overlay) {
719
+ mobileToggle.addEventListener('click', () => {
720
+ const isVisible = sidebar.classList.contains('show');
721
+
722
+ if (isVisible) {
723
+ hideMobileSidebar();
724
+ } else {
725
+ showMobileSidebar();
726
+ }
727
+ });
728
+
729
+ overlay.addEventListener('click', () => {
730
+ hideMobileSidebar();
731
+ });
732
+
733
+ // Handle escape key
734
+ document.addEventListener('keydown', (e) => {
735
+ if (e.key === 'Escape' && sidebar.classList.contains('show')) {
736
+ hideMobileSidebar();
737
+ }
738
+ });
739
+ }
740
+ }
741
+
742
+ function showMobileSidebar() {
743
+ const sidebar = document.querySelector('.sidebar');
744
+ const overlay = document.getElementById('mobile-overlay');
745
+ const toggle = document.getElementById('mobile-toggle');
746
+
747
+ sidebar.classList.add('show');
748
+ overlay.classList.add('show');
749
+ toggle.classList.add('active');
750
+ toggle.innerHTML = '✕';
751
+
752
+ // Prevent body scroll
753
+ document.body.style.overflow = 'hidden';
754
+ }
755
+
756
+ function hideMobileSidebar() {
757
+ const sidebar = document.querySelector('.sidebar');
758
+ const overlay = document.getElementById('mobile-overlay');
759
+ const toggle = document.getElementById('mobile-toggle');
760
+
761
+ sidebar.classList.remove('show');
762
+ overlay.classList.remove('show');
763
+ toggle.classList.remove('active');
764
+ toggle.innerHTML = '📊';
765
+
766
+ // Restore body scroll
767
+ document.body.style.overflow = '';
768
+ }
769
+
770
+ function connectToServer() {
771
+ // Initialize Socket.io connection
772
+ socket = io();
773
+
774
+ socket.on('connect', () => {
775
+ isConnected = true;
776
+ updateConnectionStatus(true);
777
+ console.log('Connected to server');
778
+ });
779
+
780
+ socket.on('disconnect', () => {
781
+ isConnected = false;
782
+ updateConnectionStatus(false);
783
+ console.log('Disconnected from server');
784
+ terminal.writeln('\r\n\x1b[31m✗ Connection lost. Reconnecting...\x1b[0m');
785
+ });
786
+
787
+ socket.on('connect_error', (error) => {
788
+ console.error('Connection error:', error);
789
+ updateConnectionStatus(false);
790
+ terminal.writeln('\r\n\x1b[31m✗ Connection failed\x1b[0m');
791
+ });
792
+
793
+ // Terminal communication
794
+ socket.on('terminal_output', (data) => {
795
+ terminal.write(data);
796
+ });
797
+
798
+ socket.on('system_info', (data) => {
799
+ updateSystemInfo(data);
800
+ });
801
+
802
+ // Handle user input
803
+ terminal.onData(data => {
804
+ if (isConnected && socket) {
805
+ socket.emit('terminal_input', data);
806
+ }
807
+ });
808
+
809
+ // Handle terminal resize
810
+ terminal.onResize(({ cols, rows }) => {
811
+ if (isConnected && socket) {
812
+ socket.emit('terminal_resize', { cols, rows });
813
+ }
814
+ });
815
+ }
816
+
817
+ function updateConnectionStatus(connected) {
818
+ const dot = document.getElementById('connection-dot');
819
+ const text = document.getElementById('connection-text');
820
+
821
+ if (connected) {
822
+ dot.classList.add('connected');
823
+ text.textContent = 'Connected';
824
+ } else {
825
+ dot.classList.remove('connected');
826
+ text.textContent = 'Disconnected';
827
+ }
828
+ }
829
+
830
+ function updateSystemInfo(data) {
831
+ if (!data) return;
832
+
833
+ // Update basic info
834
+ document.getElementById('hostname').textContent = data.hostname || '-';
835
+ document.getElementById('uptime').textContent = data.uptime || '-';
836
+ document.getElementById('users').textContent = data.user_info ? '1' : '-';
837
+
838
+ // Update metrics
839
+ document.getElementById('cpu-usage').textContent = `${Math.round(data.cpu_usage || 0)}%`;
840
+ document.getElementById('cpu-bar').style.width = `${data.cpu_usage || 0}%`;
841
+
842
+ document.getElementById('memory-usage').textContent = `${Math.round(data.mem_usage || 0)}%`;
843
+ document.getElementById('memory-bar').style.width = `${data.mem_usage || 0}%`;
844
+
845
+ document.getElementById('disk-usage').textContent = `${Math.round(data.disk_usage || 0)}%`;
846
+ document.getElementById('disk-bar').style.width = `${data.disk_usage || 0}%`;
847
+
848
+ // Update detailed info
849
+ document.getElementById('memory-details').textContent = `${data.used_mem || 0} MB / ${data.total_mem || 0} MB`;
850
+
851
+ // Format disk info
852
+ const diskUsed = typeof data.disk_used === 'number' ? `${data.disk_used} GB` : data.disk_used || '0';
853
+ const diskTotal = typeof data.disk_total === 'number' ? `${data.disk_total} GB` : data.disk_total || '0';
854
+ document.getElementById('disk-details').textContent = `${diskUsed} / ${diskTotal}`;
855
+
856
+ // Update network info
857
+ document.getElementById('network-rx').textContent = formatBytes(data.network_rx || 0) + '/s';
858
+ document.getElementById('network-tx').textContent = formatBytes(data.network_tx || 0) + '/s';
859
+
860
+ // Update system details
861
+ document.getElementById('platform').textContent = data.platform || '-';
862
+ document.getElementById('arch').textContent = data.arch || '-';
863
+ document.getElementById('cpu-model').textContent = truncateText(data.cpu_model || '-', 20);
864
+ document.getElementById('cpu-cores').textContent = data.cpu_cores || '-';
865
+
866
+ // Update load average
867
+ if (data.loadavg && Array.isArray(data.loadavg)) {
868
+ document.getElementById('load-avg').textContent = data.loadavg.map(l => l.toFixed(2)).join(', ');
869
+ } else {
870
+ document.getElementById('load-avg').textContent = '-';
871
+ }
872
+
873
+ // Update CPU history chart
874
+ if (data.cpu_usage !== undefined) {
875
+ cpuHistory.push(data.cpu_usage);
876
+ if (cpuHistory.length > 20) {
877
+ cpuHistory.shift();
878
+ }
879
+ updateCpuChart();
880
+ }
881
+
882
+ // Update processes
883
+ if (data.processes && Array.isArray(data.processes)) {
884
+ updateProcessList(data.processes);
885
+ }
886
+ }
887
+
888
+ function formatBytes(bytes) {
889
+ if (bytes === 0) return '0 B';
890
+ const k = 1024;
891
+ const sizes = ['B', 'KB', 'MB', 'GB'];
892
+ const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k));
893
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
894
+ }
895
+
896
+ function truncateText(text, maxLength) {
897
+ if (text.length <= maxLength) return text;
898
+ return text.substring(0, maxLength - 3) + '...';
899
+ }
900
+
901
+ function updateCpuChart() {
902
+ const chartContainer = document.getElementById('cpu-chart');
903
+ chartContainer.innerHTML = '';
904
+
905
+ cpuHistory.forEach(value => {
906
+ const bar = document.createElement('div');
907
+ bar.className = 'chart-bar';
908
+ bar.style.height = `${(value / 100) * 100}%`;
909
+ chartContainer.appendChild(bar);
910
+ });
911
+ }
912
+
913
+ function updateProcessList(processes) {
914
+ const container = document.getElementById('processes');
915
+ if (!processes || !Array.isArray(processes) || processes.length === 0) {
916
+ container.innerHTML = `
917
+ <div class="process-item">
918
+ <div class="process-name">No data</div>
919
+ <div class="process-cpu">0%</div>
920
+ </div>
921
+ `;
922
+ return;
923
+ }
924
+
925
+ container.innerHTML = processes.map(proc => `
926
+ <div class="process-item">
927
+ <div class="process-name">${proc.name || 'unknown'}</div>
928
+ <div class="process-cpu">${Math.round(proc.cpu || 0)}%</div>
929
+ </div>
930
+ `).join('');
931
+ }
932
+
933
+ function clearTerminal() {
934
+ if (terminal) {
935
+ terminal.clear();
936
+ }
937
+ }
938
+
939
+ // Initialize when page loads
940
+ document.addEventListener('DOMContentLoaded', initializeTerminal);
941
+
942
+ // Handle window resize with proper terminal fitting
943
+ let resizeTimeout;
944
+ window.addEventListener('resize', () => {
945
+ clearTimeout(resizeTimeout);
946
+ resizeTimeout = setTimeout(() => {
947
+ if (terminal && terminal.element && terminal.element.offsetParent !== null) {
948
+ try {
949
+ // Get the terminal container dimensions
950
+ const container = document.getElementById('terminal');
951
+ if (container) {
952
+ const containerRect = container.getBoundingClientRect();
953
+
954
+ // Only fit if container has dimensions
955
+ if (containerRect.width > 0 && containerRect.height > 0) {
956
+ // Use fitAddon if available, otherwise manual resize
957
+ if (window.FitAddon) {
958
+ const fitAddon = new FitAddon();
959
+ terminal.loadAddon(fitAddon);
960
+ fitAddon.fit();
961
+ } else {
962
+ // Manual calculation for terminal size
963
+ const cols = Math.floor(containerRect.width / 9); // approx char width
964
+ const rows = Math.floor(containerRect.height / 17); // approx line height
965
+
966
+ if (cols > 10 && rows > 5) { // minimum viable size
967
+ terminal.resize(cols, rows);
968
+
969
+ // Notify server of new size
970
+ if (isConnected && socket) {
971
+ socket.emit('terminal_resize', { cols, rows });
972
+ }
973
+ }
974
+ }
975
+ }
976
+ }
977
+ } catch (error) {
978
+ console.warn('Terminal resize failed:', error);
979
+ // Fallback: try simple resize
980
+ try {
981
+ terminal.resize(80, 24);
982
+ } catch (fallbackError) {
983
+ console.warn('Fallback resize also failed:', fallbackError);
984
+ }
985
+ }
986
+ }
987
+ }, 150); // Debounce resize events
988
+ });
989
+
990
+ // Handle orientation changes on mobile
991
+ window.addEventListener('orientationchange', () => {
992
+ setTimeout(() => {
993
+ window.dispatchEvent(new Event('resize'));
994
+ }, 500);
995
+ });
996
+ </script>
997
+ </body>
998
+ </html>