OrbitMC commited on
Commit
3caf9de
·
verified ·
1 Parent(s): 268a79d

Update panel.py

Browse files
Files changed (1) hide show
  1. panel.py +212 -331
panel.py CHANGED
@@ -2,9 +2,8 @@ import os
2
  import asyncio
3
  import collections
4
  import shutil
5
- import psutil
6
  from fastapi import FastAPI, WebSocket, Request, Response, Form, UploadFile, File, HTTPException
7
- from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
8
  from fastapi.middleware.cors import CORSMiddleware
9
  import uvicorn
10
 
@@ -16,11 +15,8 @@ output_history = collections.deque(maxlen=300)
16
  connected_clients = set()
17
  BASE_DIR = os.path.abspath("/app")
18
 
19
- # Global dict to store cached stats so the UI doesn't lag when polling
20
- cached_stats = { "cpu": 0, "ram_used": 0, "ram_total": 16, "storage_used": 0, "storage_total": 50 }
21
-
22
  # -----------------
23
- # HTML FRONTEND (Ultimate SaaS Web3 Dashboard)
24
  # -----------------
25
  HTML_CONTENT = """
26
  <!DOCTYPE html>
@@ -28,32 +24,32 @@ HTML_CONTENT = """
28
  <head>
29
  <meta charset="UTF-8">
30
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
31
- <title>Server Dashboard</title>
32
 
33
- <!-- Premium Fonts -->
34
- <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
35
- <!-- Phosphor Icons -->
36
  <script src="https://unpkg.com/@phosphor-icons/web"></script>
37
- <!-- Terminal -->
38
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css" />
39
  <script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
40
  <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit/lib/xterm-addon-fit.js"></script>
41
- <!-- Tailwind CSS -->
42
  <script src="https://cdn.tailwindcss.com"></script>
43
  <script>
44
  tailwind.config = {
45
  darkMode: 'class',
46
  theme: {
47
  extend: {
48
- fontFamily: { sans: ['Plus Jakarta Sans', 'sans-serif'], mono: ['JetBrains Mono', 'monospace'] },
49
  colors: {
50
- base: '#080B11',
51
- surface: '#121620',
52
- surfaceHover: '#1A1F2D',
53
- border: '#22283A',
54
- primary: '#6366F1',
55
- secondary: '#A855F7',
56
- accent: '#06B6D4'
 
 
 
 
57
  }
58
  }
59
  }
@@ -62,314 +58,276 @@ HTML_CONTENT = """
62
  <style>
63
  body { background-color: theme('colors.base'); color: #F8FAFC; overflow: hidden; -webkit-font-smoothing: antialiased; }
64
 
65
- /* Dashboard Cards */
 
 
 
 
66
  .premium-card {
67
- background: theme('colors.surface');
68
- border: 1px solid theme('colors.border');
69
- border-radius: 20px;
70
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
 
 
71
  position: relative;
72
  overflow: hidden;
 
73
  }
74
 
75
- /* Gradients & Glows */
76
  .text-gradient { background: linear-gradient(135deg, theme('colors.primary'), theme('colors.secondary')); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
77
- .bg-gradient-btn { background: linear-gradient(135deg, theme('colors.primary'), theme('colors.secondary')); box-shadow: 0 4px 15px rgba(99, 102, 241, 0.3); transition: all 0.2s ease; }
78
- .bg-gradient-btn:hover { transform: translateY(-1px); box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4); filter: brightness(1.1); }
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
  /* Terminal Fixing for Mobile Wrapping */
81
- .term-container { flex: 1; min-width: 0; min-height: 0; width: 100%; height: 100%; border-radius: 12px; overflow: hidden; position: relative; }
82
- .term-wrapper { padding: 12px; height: 100%; width: 100%; }
83
  .xterm .xterm-viewport { overflow-y: auto !important; width: 100% !important; background-color: transparent !important; }
84
  .xterm-screen { width: 100% !important; }
85
 
86
- /* Progress Bars */
87
- .progress-track { background: theme('colors.border'); border-radius: 999px; height: 6px; overflow: hidden; }
88
- .progress-fill { height: 100%; border-radius: 999px; transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1); }
89
-
90
  /* Custom Scrollbar */
91
  ::-webkit-scrollbar { width: 6px; height: 6px; }
92
  ::-webkit-scrollbar-track { background: transparent; }
93
- ::-webkit-scrollbar-thumb { background: theme('colors.border'); border-radius: 4px; }
94
- ::-webkit-scrollbar-thumb:hover { background: #333C52; }
95
 
96
- /* Mobile Bottom Nav Glass */
 
 
 
 
 
97
  .mobile-nav-glass {
98
- background: rgba(18, 22, 32, 0.85);
99
- backdrop-filter: blur(16px);
100
- -webkit-backdrop-filter: blur(16px);
101
- border-top: 1px solid theme('colors.border');
 
 
 
102
  }
103
 
104
- /* Nav active states */
105
- .nav-item { color: #64748B; transition: all 0.2s; }
106
- .nav-item:hover { color: #F8FAFC; background: theme('colors.surfaceHover'); }
107
- .nav-item.active { color: #F8FAFC; background: theme('colors.primary'); box-shadow: 0 4px 15px rgba(99, 102, 241, 0.2); }
108
 
109
- /* Mobile Nav active states */
110
- .mob-nav-item { color: #64748B; }
111
- .mob-nav-item.active { color: theme('colors.primary'); }
112
- .mob-nav-indicator { display: none; height: 4px; width: 4px; border-radius: 50%; background: theme('colors.primary'); margin-top: 2px; }
113
- .mob-nav-item.active .mob-nav-indicator { display: block; }
114
-
115
- .fade-in { animation: fadeIn 0.3s ease-out forwards; }
116
- @keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
117
  .hidden-tab { display: none !important; }
118
-
119
- /* SVG Background Wave */
120
- .bg-wave { position: absolute; bottom: 0; left: 0; right: 0; opacity: 0.1; transform: translateY(20%); pointer-events: none; }
121
  </style>
122
  </head>
123
- <body class="flex flex-col md:flex-row h-[100dvh] w-full text-sm">
124
-
125
- <!-- Desktop Sidebar -->
126
- <aside class="hidden md:flex flex-col w-[260px] premium-card m-4 mr-0 border-y-0 border-l-0 rounded-none rounded-l-2xl border-r border-border bg-surface shrink-0 z-20">
127
- <div class="p-6 pb-2">
128
- <div class="flex items-center gap-3">
129
- <div class="w-10 h-10 rounded-xl bg-gradient-to-br from-primary to-secondary flex items-center justify-center shadow-lg">
130
- <i class="ph ph-cube text-xl text-white"></i>
 
 
131
  </div>
132
  <div>
133
- <h1 class="font-bold text-lg text-white leading-tight">Server<span class="text-gradient">Space</span></h1>
134
- <p class="text-[10px] text-slate-400 font-semibold uppercase tracking-wider">Engine v2.0</p>
135
  </div>
136
  </div>
137
  </div>
138
 
139
- <div class="px-6 py-4">
140
- <div class="text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-3">Menu</div>
141
  <nav class="flex flex-col gap-2">
142
- <button onclick="switchTab('dashboard')" id="nav-dashboard" class="nav-item active flex items-center gap-3 px-4 py-3 rounded-xl font-medium">
143
- <i class="ph ph-squares-four text-lg"></i> Dashboard
144
  </button>
145
- <button onclick="switchTab('files')" id="nav-files" class="nav-item flex items-center gap-3 px-4 py-3 rounded-xl font-medium">
146
- <i class="ph ph-folder-open text-lg"></i> Files
147
  </button>
148
  </nav>
149
  </div>
150
 
151
- <div class="mt-auto p-6">
152
- <div class="bg-surfaceHover border border-border rounded-xl p-4 flex items-center gap-3">
153
- <div class="relative flex h-3 w-3">
154
- <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
155
- <span class="relative inline-flex rounded-full h-3 w-3 bg-green-500"></span>
156
  </div>
157
  <div>
158
- <div class="text-xs font-bold text-white">Status Online</div>
159
- <div class="text-[10px] text-slate-400">Container Active</div>
160
  </div>
161
  </div>
162
  </div>
163
  </aside>
164
 
165
- <!-- Mobile Header -->
166
- <header class="md:hidden flex justify-between items-center px-5 py-4 bg-surface border-b border-border shrink-0 z-20">
167
- <div class="flex items-center gap-2">
168
- <div class="w-8 h-8 rounded-lg bg-gradient-to-br from-primary to-secondary flex items-center justify-center">
169
- <i class="ph ph-cube text-white"></i>
170
  </div>
171
- <h1 class="font-bold text-base text-white">Server<span class="text-gradient">Space</span></h1>
172
  </div>
173
- <div class="relative flex h-2 w-2">
174
- <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
175
- <span class="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
176
  </div>
177
  </header>
178
 
179
- <!-- Main Workspace -->
180
- <main class="flex-grow flex flex-col p-4 md:p-6 overflow-hidden min-w-0 bg-base relative z-10">
181
 
182
- <!-- DASHBOARD TAB -->
183
- <div id="tab-dashboard" class="h-full flex flex-col gap-4 md:gap-6 fade-in min-w-0">
184
-
185
- <!-- Metrics Row (Strictly 16GB / 2 Cores / 50GB) -->
186
- <div class="grid grid-cols-3 gap-3 md:gap-6 shrink-0">
187
-
188
- <!-- RAM Card -->
189
- <div class="premium-card p-4 flex flex-col justify-between h-[100px] md:h-[130px]">
190
- <div class="flex justify-between items-start">
191
- <div class="p-2 rounded-lg bg-primary/10 text-primary hidden md:block"><i class="ph-fill ph-memory text-xl"></i></div>
192
- <i class="ph-fill ph-memory text-primary text-xl md:hidden"></i>
193
- <span class="text-[10px] md:text-xs font-bold text-slate-400 uppercase tracking-wide">RAM Usage</span>
194
- </div>
195
- <div>
196
- <div class="flex items-end gap-1 md:gap-2 mb-2">
197
- <span class="text-lg md:text-3xl font-bold text-white font-mono leading-none" id="ram-val">0.0</span>
198
- <span class="text-[10px] md:text-sm text-slate-500 font-mono mb-0.5">/ 16 GB</span>
199
- </div>
200
- <div class="progress-track"><div id="ram-bar" class="progress-fill bg-primary w-0"></div></div>
201
- </div>
202
- </div>
203
-
204
- <!-- CPU Card -->
205
- <div class="premium-card p-4 flex flex-col justify-between h-[100px] md:h-[130px]">
206
- <div class="flex justify-between items-start">
207
- <div class="p-2 rounded-lg bg-secondary/10 text-secondary hidden md:block"><i class="ph-fill ph-cpu text-xl"></i></div>
208
- <i class="ph-fill ph-cpu text-secondary text-xl md:hidden"></i>
209
- <span class="text-[10px] md:text-xs font-bold text-slate-400 uppercase tracking-wide">vCores</span>
210
- </div>
211
- <div>
212
- <div class="flex items-end gap-1 md:gap-2 mb-2">
213
- <span class="text-lg md:text-3xl font-bold text-white font-mono leading-none" id="cpu-val">0</span>
214
- <span class="text-[10px] md:text-sm text-slate-500 font-mono mb-0.5">% of 2</span>
215
- </div>
216
- <div class="progress-track"><div id="cpu-bar" class="progress-fill bg-secondary w-0"></div></div>
217
- </div>
218
- </div>
219
-
220
- <!-- Storage Card -->
221
- <div class="premium-card p-4 flex flex-col justify-between h-[100px] md:h-[130px]">
222
- <div class="flex justify-between items-start">
223
- <div class="p-2 rounded-lg bg-accent/10 text-accent hidden md:block"><i class="ph-fill ph-hard-drives text-xl"></i></div>
224
- <i class="ph-fill ph-hard-drives text-accent text-xl md:hidden"></i>
225
- <span class="text-[10px] md:text-xs font-bold text-slate-400 uppercase tracking-wide">Disk Space</span>
226
- </div>
227
- <div>
228
- <div class="flex items-end gap-1 md:gap-2 mb-2">
229
- <span class="text-lg md:text-3xl font-bold text-white font-mono leading-none" id="disk-val">0.0</span>
230
- <span class="text-[10px] md:text-sm text-slate-500 font-mono mb-0.5">/ 50 GB</span>
231
- </div>
232
- <div class="progress-track"><div id="disk-bar" class="progress-fill bg-accent w-0"></div></div>
233
- </div>
234
  </div>
235
  </div>
236
 
237
- <!-- Terminal Area -->
238
  <div class="premium-card flex flex-col flex-grow min-h-0">
239
- <!-- Mac-style Terminal Header -->
240
- <div class="bg-surface border-b border-border px-4 py-3 flex items-center justify-between z-10 shrink-0">
241
  <div class="flex gap-2">
242
- <div class="w-3 h-3 rounded-full bg-[#EF4444]"></div>
243
- <div class="w-3 h-3 rounded-full bg-[#F59E0B]"></div>
244
- <div class="w-3 h-3 rounded-full bg-[#10B981]"></div>
245
  </div>
246
- <span class="text-xs font-mono text-slate-400">server_console ~ /app</span>
247
- <div class="w-12"></div> <!-- Spacer for center alignment -->
248
- </div>
249
 
250
- <!-- Terminal Container -->
251
- <div class="term-container bg-[#080B11]">
252
  <div id="terminal" class="term-wrapper"></div>
253
  </div>
254
 
255
- <!-- Input Field -->
256
- <div class="p-3 md:p-4 bg-surface/80 border-t border-border z-10 shrink-0">
257
  <div class="relative flex items-center">
258
- <i class="ph ph-terminal text-primary absolute left-4 text-lg"></i>
259
- <input type="text" id="cmd-input" class="w-full bg-base border border-border focus:border-primary/50 text-white rounded-xl pl-12 pr-12 py-3 text-sm font-mono transition-all outline-none" placeholder="Enter command here...">
260
- <button onclick="sendCommand()" class="absolute right-2 p-2 bg-gradient-btn rounded-lg text-white">
261
- <i class="ph-bold ph-arrow-right"></i>
262
  </button>
263
  </div>
264
  </div>
265
  </div>
266
  </div>
267
 
268
- <!-- FILES TAB -->
269
- <div id="tab-files" class="hidden-tab h-full flex flex-col premium-card overflow-hidden min-w-0">
270
- <!-- Header -->
271
- <div class="bg-surface px-5 py-4 border-b border-border flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 shrink-0">
272
- <div class="flex items-center gap-2 text-sm font-mono text-slate-300 overflow-x-auto whitespace-nowrap w-full sm:w-auto" id="breadcrumbs">
273
- <!-- JS Breadcrumbs -->
274
- </div>
275
- <div class="flex gap-2 shrink-0">
276
- <input type="file" id="file-upload" class="hidden" onchange="uploadFile(event)">
277
- <button onclick="document.getElementById('file-upload').click()" class="bg-gradient-btn px-4 py-2 rounded-xl text-xs font-bold text-white flex items-center gap-2">
278
- <i class="ph-bold ph-upload-simple"></i> Upload
279
- </button>
280
- <button onclick="loadFiles(currentPath)" class="bg-surfaceHover border border-border px-3 py-2 rounded-xl text-slate-300 hover:text-white transition-colors">
281
- <i class="ph-bold ph-arrows-clockwise text-base"></i>
282
- </button>
283
- </div>
284
- </div>
285
 
286
- <!-- File Headers (Desktop only) -->
287
- <div class="hidden sm:grid grid-cols-12 gap-4 px-6 py-3 bg-[#0D1017] border-b border-border text-[11px] font-bold text-slate-500 uppercase tracking-wider shrink-0">
288
- <div class="col-span-7">Name</div>
289
- <div class="col-span-3 text-right">Size</div>
290
- <div class="col-span-2 text-right">Actions</div>
291
  </div>
292
 
293
- <!-- File List -->
294
- <div class="flex-grow overflow-y-auto bg-base p-2 md:p-3" id="file-list">
295
- <!-- JS Files -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  </div>
297
  </div>
298
  </main>
299
 
300
- <!-- Mobile Bottom Navigation -->
301
- <nav class="md:hidden mobile-nav-glass pb-safe pt-2 px-6 flex justify-around items-center shrink-0 z-50 rounded-t-2xl absolute bottom-0 w-full h-[70px]">
302
- <button onclick="switchTab('dashboard')" id="mob-dashboard" class="mob-nav-item active flex flex-col items-center gap-1 w-16">
303
- <i class="ph-fill ph-squares-four text-2xl"></i>
304
- <span class="text-[10px] font-semibold">Dash</span>
305
- <div class="mob-nav-indicator"></div>
306
  </button>
307
- <button onclick="switchTab('files')" id="mob-files" class="mob-nav-item flex flex-col items-center gap-1 w-16">
308
- <i class="ph-fill ph-folder text-2xl"></i>
309
- <span class="text-[10px] font-semibold">Files</span>
310
- <div class="mob-nav-indicator"></div>
 
 
311
  </button>
312
  </nav>
313
 
314
- <!-- File Editor Modal -->
315
- <div id="editor-modal" class="fixed inset-0 bg-black/80 backdrop-blur-sm hidden items-center justify-center p-4 z-[100] opacity-0 transition-opacity duration-300">
316
- <div class="premium-card w-full max-w-4xl h-[85vh] flex flex-col transform scale-95 transition-transform duration-300" id="editor-card">
317
- <div class="bg-surface px-5 py-4 flex justify-between items-center border-b border-border shrink-0">
318
- <div class="flex items-center gap-3 text-sm font-mono text-white">
319
  <i class="ph-fill ph-file-code text-primary text-xl"></i>
320
  <span id="editor-title">file.txt</span>
321
  </div>
322
- <div class="flex gap-2">
323
- <button onclick="closeEditor()" class="px-4 py-2 hover:bg-surfaceHover rounded-xl text-xs font-bold text-slate-400">Cancel</button>
324
- <button onclick="saveFile()" class="bg-gradient-btn px-5 py-2 rounded-xl text-xs font-bold text-white shadow-lg flex items-center gap-2">
325
- <i class="ph-bold ph-floppy-disk"></i> Save
326
  </button>
327
  </div>
328
  </div>
329
- <textarea id="editor-content" class="flex-grow bg-[#080B11] text-slate-200 p-5 font-mono text-sm resize-none focus:outline-none w-full leading-loose" spellcheck="false"></textarea>
330
  </div>
331
  </div>
332
 
333
- <!-- Notification Toasts -->
334
- <div id="toast-container" class="fixed top-6 right-6 z-[200] flex flex-col gap-3 pointer-events-none"></div>
335
 
336
  <script>
337
  // --- Tab Navigation ---
338
  function switchTab(tab) {
339
- document.getElementById('tab-dashboard').classList.add('hidden-tab');
340
  document.getElementById('tab-files').classList.add('hidden-tab');
341
 
342
  // Reset Desktop
343
- document.getElementById('nav-dashboard').className = "nav-item flex items-center gap-3 px-4 py-3 rounded-xl font-medium";
344
- document.getElementById('nav-files').className = "nav-item flex items-center gap-3 px-4 py-3 rounded-xl font-medium";
345
 
346
  // Reset Mobile
347
- document.getElementById('mob-dashboard').classList.remove('active');
348
  document.getElementById('mob-files').classList.remove('active');
349
 
350
  // Activate
351
  document.getElementById('tab-' + tab).classList.remove('hidden-tab');
352
  document.getElementById('tab-' + tab).classList.add('fade-in');
353
 
354
- document.getElementById('nav-' + tab).className = "nav-item active flex items-center gap-3 px-4 py-3 rounded-xl font-medium";
355
  document.getElementById('mob-' + tab).classList.add('active');
356
 
357
- if(tab === 'dashboard' && fitAddon) setTimeout(() => fitAddon.fit(), 100);
358
  if(tab === 'files' && !window.filesLoaded) { loadFiles(''); window.filesLoaded = true; }
359
  }
360
 
361
  // --- Terminal Engine ---
362
  const term = new Terminal({
363
- theme: { background: 'transparent', foreground: '#E2E8F0', cursor: '#6366F1', selectionBackground: 'rgba(99, 102, 241, 0.3)' },
364
- fontFamily: "'JetBrains Mono', monospace", fontSize: 13, cursorBlink: true, convertEol: true
365
  });
366
  const fitAddon = new FitAddon.FitAddon();
367
  term.loadAddon(fitAddon);
368
  term.open(document.getElementById('terminal'));
369
 
370
- // Exact fit tracking to fix wrapping
371
  const ro = new ResizeObserver(() => {
372
- if(!document.getElementById('tab-dashboard').classList.contains('hidden-tab')) {
373
  requestAnimationFrame(() => fitAddon.fit());
374
  }
375
  });
@@ -377,44 +335,18 @@ HTML_CONTENT = """
377
  setTimeout(() => fitAddon.fit(), 200);
378
 
379
  const ws = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws');
380
- ws.onopen = () => term.write('\\x1b[38;5;63m\\x1b[1m[Console]\\x1b[0m Engine connection established.\\r\\n');
381
  ws.onmessage = e => term.write(e.data + '\\n');
382
 
383
  const cmdInput = document.getElementById('cmd-input');
384
  cmdInput.addEventListener('keypress', e => { if (e.key === 'Enter') sendCommand(); });
385
  function sendCommand() {
386
  if(cmdInput.value.trim() && ws.readyState === WebSocket.OPEN) {
387
- term.write(`\\x1b[90m> ${cmdInput.value}\\x1b[0m\\r\\n`);
388
  ws.send(cmdInput.value); cmdInput.value = '';
389
  }
390
  }
391
 
392
- // --- Container Metrics Engine ---
393
- async function fetchStats() {
394
- try {
395
- const res = await fetch('/api/stats');
396
- const data = await res.json();
397
-
398
- // RAM (Max 16GB)
399
- const ramVal = Math.min(data.ram_used, 16.0);
400
- document.getElementById('ram-val').innerText = ramVal.toFixed(1);
401
- document.getElementById('ram-bar').style.width = `${(ramVal / 16.0) * 100}%`;
402
-
403
- // CPU (Max 100% representing 2 cores)
404
- const cpuVal = Math.min(data.cpu, 100);
405
- document.getElementById('cpu-val').innerText = Math.round(cpuVal);
406
- document.getElementById('cpu-bar').style.width = `${cpuVal}%`;
407
-
408
- // Storage (Max 50GB)
409
- const diskVal = Math.min(data.storage_used, 50.0);
410
- document.getElementById('disk-val').innerText = diskVal.toFixed(1);
411
- document.getElementById('disk-bar').style.width = `${(diskVal / 50.0) * 100}%`;
412
-
413
- } catch (e) {}
414
- }
415
- setInterval(fetchStats, 2000);
416
- fetchStats();
417
-
418
  // --- File Manager ---
419
  let currentPath = '';
420
  let editPath = '';
@@ -422,33 +354,33 @@ HTML_CONTENT = """
422
  function showToast(msg, type='info') {
423
  const container = document.getElementById('toast-container');
424
  const el = document.createElement('div');
425
- let icon = '<i class="ph-fill ph-info text-blue-400 text-xl"></i>';
426
- if(type==='success') icon = '<i class="ph-fill ph-check-circle text-green-400 text-xl"></i>';
427
- if(type==='error') icon = '<i class="ph-fill ph-warning-circle text-red-400 text-xl"></i>';
428
 
429
- el.className = `flex items-center gap-3 bg-surface border border-border text-white px-5 py-4 rounded-xl shadow-2xl translate-x-10 opacity-0 transition-all duration-300`;
430
- el.innerHTML = `${icon} <span class="font-bold text-sm tracking-wide">${msg}</span>`;
431
  container.appendChild(el);
432
 
433
- requestAnimationFrame(() => el.classList.remove('translate-x-10', 'opacity-0'));
434
- setTimeout(() => { el.classList.add('translate-x-10', 'opacity-0'); setTimeout(() => el.remove(), 300); }, 3000);
435
  }
436
 
437
  async function loadFiles(path) {
438
  currentPath = path;
439
  const parts = path.split('/').filter(p => p);
440
- let bc = `<button onclick="loadFiles('')" class="hover:text-white transition"><i class="ph-fill ph-house text-lg"></i></button>`;
441
  let bp = '';
442
  parts.forEach((p, i) => {
443
  bp += (bp?'/':'') + p;
444
- bc += `<i class="ph-bold ph-caret-right text-xs mx-2 opacity-30"></i>`;
445
- if(i === parts.length-1) bc += `<span class="text-primary font-bold">${p}</span>`;
446
- else bc += `<button onclick="loadFiles('${bp}')" class="hover:text-white transition">${p}</button>`;
447
  });
448
  document.getElementById('breadcrumbs').innerHTML = bc;
449
 
450
  const list = document.getElementById('file-list');
451
- list.innerHTML = `<div class="flex justify-center py-12"><i class="ph-bold ph-spinner-gap animate-spin text-3xl text-primary"></i></div>`;
452
 
453
  try {
454
  const res = await fetch(`/api/fs/list?path=${encodeURIComponent(path)}`);
@@ -458,32 +390,32 @@ HTML_CONTENT = """
458
  if (path !== '') {
459
  const parent = path.split('/').slice(0, -1).join('/');
460
  list.innerHTML += `
461
- <div class="flex items-center px-4 py-3 cursor-pointer hover:bg-surfaceHover rounded-xl transition mb-1 border border-transparent" onclick="loadFiles('${parent}')">
462
- <i class="ph-bold ph-arrow-u-up-left text-slate-500 mr-3 text-lg"></i>
463
- <span class="text-sm font-mono text-slate-400 font-semibold">.. back</span>
464
  </div>`;
465
  }
466
 
467
  files.forEach(f => {
468
- const icon = f.is_dir ? '<div class="p-2.5 bg-primary/10 rounded-lg text-primary"><i class="ph-fill ph-folder text-xl"></i></div>' : '<div class="p-2.5 bg-surface border border-border rounded-lg text-slate-400"><i class="ph-fill ph-file text-xl"></i></div>';
469
- const sz = f.is_dir ? '--' : (f.size > 1048576 ? (f.size/1048576).toFixed(1) + ' MB' : (f.size/1024).toFixed(1) + ' KB');
470
  const fp = path ? `${path}/${f.name}` : f.name;
471
 
472
  list.innerHTML += `
473
- <div class="flex flex-col sm:grid sm:grid-cols-12 items-start sm:items-center px-3 py-2 gap-3 group hover:bg-surfaceHover rounded-xl transition mb-1 border border-transparent hover:border-border">
474
- <div class="col-span-7 flex items-center gap-4 w-full ${f.is_dir?'cursor-pointer':''}" ${f.is_dir?`onclick="loadFiles('${fp}')"`:''}>
475
  ${icon}
476
- <span class="text-sm font-mono text-slate-200 truncate group-hover:text-primary transition font-medium">${f.name}</span>
477
  </div>
478
- <div class="col-span-3 text-right text-xs text-slate-500 font-mono hidden sm:block">${sz}</div>
479
- <div class="col-span-2 flex justify-end gap-2 w-full sm:w-auto sm:opacity-0 group-hover:opacity-100 transition">
480
- ${!f.is_dir ? `<button onclick="editFile('${fp}')" class="p-2 bg-surface border border-border hover:border-primary hover:text-primary rounded-lg transition"><i class="ph-bold ph-pencil-simple text-sm"></i></button>` : ''}
481
- ${!f.is_dir ? `<a href="/api/fs/download?path=${encodeURIComponent(fp)}" class="p-2 bg-surface border border-border hover:border-green-400 hover:text-green-400 rounded-lg transition"><i class="ph-bold ph-download-simple text-sm"></i></a>` : ''}
482
- <button onclick="deleteFile('${fp}')" class="p-2 bg-surface border border-border hover:border-red-400 hover:text-red-400 rounded-lg transition"><i class="ph-bold ph-trash text-sm"></i></button>
483
  </div>
484
  </div>`;
485
  });
486
- } catch (err) { list.innerHTML = `<div class="text-center py-8 text-red-400 text-sm">Failed to load files</div>`; }
487
  }
488
 
489
  async function editFile(path) {
@@ -496,8 +428,8 @@ HTML_CONTENT = """
496
  const m = document.getElementById('editor-modal'); const c = document.getElementById('editor-card');
497
  m.classList.remove('hidden'); m.classList.add('flex');
498
  requestAnimationFrame(() => { m.classList.remove('opacity-0'); c.classList.remove('scale-95'); });
499
- } else showToast('Binary file cannot be edited', 'error');
500
- } catch { showToast('Error opening file', 'error'); }
501
  }
502
 
503
  function closeEditor() {
@@ -510,28 +442,28 @@ HTML_CONTENT = """
510
  const fd = new FormData(); fd.append('path', editPath); fd.append('content', document.getElementById('editor-content').value);
511
  try {
512
  const res = await fetch('/api/fs/write', { method: 'POST', body: fd });
513
- if(res.ok) { showToast('Saved securely', 'success'); closeEditor(); } else throw new Error();
514
- } catch { showToast('Save failed', 'error'); }
515
  }
516
 
517
  async function deleteFile(path) {
518
- if(confirm(`Erase ${path.split('/').pop()} permanentely?`)) {
519
  const fd = new FormData(); fd.append('path', path);
520
  try {
521
  const res = await fetch('/api/fs/delete', { method: 'POST', body: fd });
522
- if(res.ok) { showToast('Erased', 'success'); loadFiles(currentPath); } else throw new Error();
523
- } catch { showToast('Erase failed', 'error'); }
524
  }
525
  }
526
 
527
  async function uploadFile(e) {
528
  if(!e.target.files.length) return;
529
- showToast('Uploading data...', 'info');
530
  const fd = new FormData(); fd.append('path', currentPath); fd.append('file', e.target.files[0]);
531
  try {
532
  const res = await fetch('/api/fs/upload', { method: 'POST', body: fd });
533
- if(res.ok) { showToast('Upload complete', 'success'); loadFiles(currentPath); } else throw new Error();
534
- } catch { showToast('Upload failed', 'error'); }
535
  e.target.value = '';
536
  }
537
  </script>
@@ -539,51 +471,6 @@ HTML_CONTENT = """
539
  </html>
540
  """
541
 
542
- # -----------------
543
- # STATS ENGINE BACKGROUND TASK (Container-Only Restrictions)
544
- # -----------------
545
- async def update_system_stats():
546
- """ Runs constantly in the background so the UI endpoint is instantly responsive. """
547
- while True:
548
- try:
549
- # 1. Gather Storage strictly for /app
550
- # Hugging Face usually provides around 50GB. We enforce this visual cap.
551
- total_st, used_st, free_st = shutil.disk_usage('/app')
552
- cached_stats["storage_used"] = used_st / (1024**3)
553
- cached_stats["storage_total"] = 50.0
554
-
555
- # 2. Gather RAM and CPU strictly for the python process + Java child (No Host Info)
556
- ram_used = 0
557
- cpu_percent_raw = 0.0
558
-
559
- try:
560
- main_proc = psutil.Process(os.getpid())
561
- ram_used += main_proc.memory_info().rss
562
- cpu_percent_raw += main_proc.cpu_percent()
563
-
564
- # Fetch Java child process metrics
565
- for child in main_proc.children(recursive=True):
566
- try:
567
- ram_used += child.memory_info().rss
568
- cpu_percent_raw += child.cpu_percent()
569
- except psutil.NoSuchProcess:
570
- pass
571
- except Exception:
572
- pass
573
-
574
- # Convert RAM to GB
575
- cached_stats["ram_used"] = ram_used / (1024**3)
576
- cached_stats["ram_total"] = 16.0 # Strict Hugging Face Limit
577
-
578
- # Normalize CPU to 2 vCores (where 200% raw = 100% full capacity)
579
- normalized_cpu = cpu_percent_raw / 2.0
580
- cached_stats["cpu"] = min(100.0, normalized_cpu)
581
-
582
- except Exception as e:
583
- pass # Failsafe
584
-
585
- await asyncio.sleep(2) # Update every 2 seconds
586
-
587
  # -----------------
588
  # UTILITIES & SERVER
589
  # -----------------
@@ -636,7 +523,6 @@ async def start_minecraft():
636
  @app.on_event("startup")
637
  async def startup_event():
638
  asyncio.create_task(start_minecraft())
639
- asyncio.create_task(update_system_stats()) # Start background polling loop
640
 
641
  # -----------------
642
  # API ROUTING
@@ -644,11 +530,6 @@ async def startup_event():
644
  @app.get("/")
645
  def get_panel(): return HTMLResponse(content=HTML_CONTENT)
646
 
647
- @app.get("/api/stats")
648
- def api_stats():
649
- # Returns the background-calculated stats instantly
650
- return JSONResponse(content=cached_stats)
651
-
652
  @app.websocket("/ws")
653
  async def ws_endpoint(websocket: WebSocket):
654
  await websocket.accept()
 
2
  import asyncio
3
  import collections
4
  import shutil
 
5
  from fastapi import FastAPI, WebSocket, Request, Response, Form, UploadFile, File, HTTPException
6
+ from fastapi.responses import HTMLResponse, FileResponse
7
  from fastapi.middleware.cors import CORSMiddleware
8
  import uvicorn
9
 
 
15
  connected_clients = set()
16
  BASE_DIR = os.path.abspath("/app")
17
 
 
 
 
18
  # -----------------
19
+ # HTML FRONTEND (Web3 / Modern SaaS Dashboard)
20
  # -----------------
21
  HTML_CONTENT = """
22
  <!DOCTYPE html>
 
24
  <head>
25
  <meta charset="UTF-8">
26
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
27
+ <title>ServerSpace | Dashboard</title>
28
 
29
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
 
 
30
  <script src="https://unpkg.com/@phosphor-icons/web"></script>
 
31
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css" />
32
  <script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
33
  <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit/lib/xterm-addon-fit.js"></script>
 
34
  <script src="https://cdn.tailwindcss.com"></script>
35
  <script>
36
  tailwind.config = {
37
  darkMode: 'class',
38
  theme: {
39
  extend: {
40
+ fontFamily: { sans: ['Inter', 'sans-serif'], mono: ['JetBrains Mono', 'monospace'] },
41
  colors: {
42
+ base: '#06080D',
43
+ surface: '#10141F',
44
+ surfaceHover: '#1A2133',
45
+ border: '#232B40',
46
+ primary: '#8B5CF6', /* Violet */
47
+ secondary: '#D946EF', /* Fuchsia */
48
+ accent: '#0EA5E9' /* Cyan */
49
+ },
50
+ boxShadow: {
51
+ 'neon': '0 0 20px rgba(139, 92, 246, 0.3)',
52
+ 'neon-strong': '0 0 30px rgba(217, 70, 239, 0.4)'
53
  }
54
  }
55
  }
 
58
  <style>
59
  body { background-color: theme('colors.base'); color: #F8FAFC; overflow: hidden; -webkit-font-smoothing: antialiased; }
60
 
61
+ /* Ambient Background Glows */
62
+ .ambient-glow-1 { position: absolute; top: -10%; left: -10%; width: 40vw; height: 40vw; background: radial-gradient(circle, rgba(139,92,246,0.15) 0%, rgba(0,0,0,0) 70%); border-radius: 50%; pointer-events: none; z-index: 0; filter: blur(60px); }
63
+ .ambient-glow-2 { position: absolute; bottom: -20%; right: -10%; width: 50vw; height: 50vw; background: radial-gradient(circle, rgba(217,70,239,0.1) 0%, rgba(0,0,0,0) 70%); border-radius: 50%; pointer-events: none; z-index: 0; filter: blur(80px); }
64
+
65
+ /* Dashboard Cards - Glassmorphism */
66
  .premium-card {
67
+ background: rgba(16, 20, 31, 0.6);
68
+ backdrop-filter: blur(16px);
69
+ -webkit-backdrop-filter: blur(16px);
70
+ border: 1px solid rgba(255, 255, 255, 0.05);
71
+ border-radius: 24px;
72
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
73
  position: relative;
74
  overflow: hidden;
75
+ z-index: 10;
76
  }
77
 
78
+ /* Gradients & Buttons */
79
  .text-gradient { background: linear-gradient(135deg, theme('colors.primary'), theme('colors.secondary')); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
80
+ .bg-gradient-btn {
81
+ background: linear-gradient(135deg, theme('colors.primary'), theme('colors.secondary'));
82
+ box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3);
83
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
84
+ position: relative;
85
+ overflow: hidden;
86
+ }
87
+ .bg-gradient-btn::before {
88
+ content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%;
89
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
90
+ transition: all 0.5s ease;
91
+ }
92
+ .bg-gradient-btn:hover { transform: translateY(-2px); box-shadow: theme('boxShadow.neon-strong'); filter: brightness(1.1); }
93
+ .bg-gradient-btn:hover::before { left: 100%; }
94
 
95
  /* Terminal Fixing for Mobile Wrapping */
96
+ .term-container { flex: 1; min-width: 0; min-height: 0; width: 100%; height: 100%; overflow: hidden; position: relative; }
97
+ .term-wrapper { padding: 16px; height: 100%; width: 100%; }
98
  .xterm .xterm-viewport { overflow-y: auto !important; width: 100% !important; background-color: transparent !important; }
99
  .xterm-screen { width: 100% !important; }
100
 
 
 
 
 
101
  /* Custom Scrollbar */
102
  ::-webkit-scrollbar { width: 6px; height: 6px; }
103
  ::-webkit-scrollbar-track { background: transparent; }
104
+ ::-webkit-scrollbar-thumb { background: theme('colors.border'); border-radius: 10px; }
105
+ ::-webkit-scrollbar-thumb:hover { background: theme('colors.primary'); }
106
 
107
+ /* Navigation States */
108
+ .nav-item { color: #64748B; transition: all 0.3s ease; position: relative; }
109
+ .nav-item:hover { color: #F8FAFC; background: rgba(255,255,255,0.03); }
110
+ .nav-item.active { color: #F8FAFC; background: linear-gradient(90deg, rgba(139, 92, 246, 0.15) 0%, transparent 100%); border-left: 3px solid theme('colors.primary'); }
111
+
112
+ /* Mobile Nav Floating Glass */
113
  .mobile-nav-glass {
114
+ background: rgba(16, 20, 31, 0.85);
115
+ backdrop-filter: blur(20px);
116
+ -webkit-backdrop-filter: blur(20px);
117
+ border: 1px solid rgba(255, 255, 255, 0.08);
118
+ box-shadow: 0 -10px 40px rgba(0,0,0,0.5);
119
+ margin: 0 16px 16px 16px;
120
+ border-radius: 24px;
121
  }
122
 
123
+ .mob-nav-item { color: #64748B; transition: color 0.3s; }
124
+ .mob-nav-item.active { color: theme('colors.primary'); text-shadow: 0 0 15px rgba(139,92,246,0.5); }
 
 
125
 
126
+ /* Animations */
127
+ .fade-in { animation: fadeIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
128
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(10px) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } }
 
 
 
 
 
129
  .hidden-tab { display: none !important; }
 
 
 
130
  </style>
131
  </head>
132
+ <body class="flex flex-col md:flex-row h-[100dvh] w-full text-sm md:text-base relative">
133
+
134
+ <div class="ambient-glow-1"></div>
135
+ <div class="ambient-glow-2"></div>
136
+
137
+ <aside class="hidden md:flex flex-col w-[280px] bg-surface/40 backdrop-blur-xl border-r border-white/5 shrink-0 z-20 shadow-2xl">
138
+ <div class="p-8 pb-4">
139
+ <div class="flex items-center gap-4">
140
+ <div class="w-12 h-12 rounded-2xl bg-gradient-btn flex items-center justify-center shadow-neon">
141
+ <i class="ph ph-hexagon text-2xl text-white"></i>
142
  </div>
143
  <div>
144
+ <h1 class="font-bold text-xl text-white tracking-tight">Server<span class="text-gradient">Space</span></h1>
145
+ <p class="text-[11px] text-primary font-mono uppercase tracking-widest mt-0.5">Engine v2.0</p>
146
  </div>
147
  </div>
148
  </div>
149
 
150
+ <div class="px-6 py-6 flex-grow">
151
+ <div class="text-[11px] font-bold text-slate-500 uppercase tracking-widest mb-4 px-3">Dashboard</div>
152
  <nav class="flex flex-col gap-2">
153
+ <button onclick="switchTab('console')" id="nav-console" class="nav-item active flex items-center gap-4 px-4 py-3.5 rounded-r-xl font-medium">
154
+ <i class="ph ph-terminal-window text-xl"></i> Console
155
  </button>
156
+ <button onclick="switchTab('files')" id="nav-files" class="nav-item flex items-center gap-4 px-4 py-3.5 rounded-r-xl font-medium border-l-3 border-transparent">
157
+ <i class="ph ph-folder-notch text-xl"></i> File Explorer
158
  </button>
159
  </nav>
160
  </div>
161
 
162
+ <div class="p-6">
163
+ <div class="bg-black/30 border border-white/5 rounded-2xl p-4 flex items-center gap-4 backdrop-blur-md">
164
+ <div class="relative flex h-4 w-4">
165
+ <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-accent opacity-60"></span>
166
+ <span class="relative inline-flex rounded-full h-4 w-4 bg-accent shadow-[0_0_10px_#0EA5E9]"></span>
167
  </div>
168
  <div>
169
+ <div class="text-sm font-semibold text-white">System Online</div>
170
+ <div class="text-xs font-mono text-slate-400 mt-0.5">Latency: 24ms</div>
171
  </div>
172
  </div>
173
  </div>
174
  </aside>
175
 
176
+ <header class="md:hidden flex justify-between items-center px-6 py-5 bg-surface/80 backdrop-blur-md border-b border-white/5 shrink-0 z-20">
177
+ <div class="flex items-center gap-3">
178
+ <div class="w-10 h-10 rounded-xl bg-gradient-btn flex items-center justify-center">
179
+ <i class="ph ph-hexagon text-xl text-white"></i>
 
180
  </div>
181
+ <h1 class="font-bold text-lg text-white">Server<span class="text-gradient">Space</span></h1>
182
  </div>
183
+ <div class="relative flex h-3 w-3">
184
+ <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-accent opacity-60"></span>
185
+ <span class="relative inline-flex rounded-full h-3 w-3 bg-accent"></span>
186
  </div>
187
  </header>
188
 
189
+ <main class="flex-grow flex flex-col p-4 md:p-8 overflow-hidden min-w-0 relative z-10 pb-24 md:pb-8">
 
190
 
191
+ <div id="tab-console" class="h-full flex flex-col fade-in min-w-0">
192
+ <div class="mb-4 hidden md:flex justify-between items-end">
193
+ <div>
194
+ <h2 class="text-2xl font-bold text-white">Live Terminal</h2>
195
+ <p class="text-slate-400 text-sm mt-1">Execute commands directly on the server container.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  </div>
197
  </div>
198
 
 
199
  <div class="premium-card flex flex-col flex-grow min-h-0">
200
+ <div class="bg-black/40 border-b border-white/5 px-5 py-4 flex items-center justify-between z-10 shrink-0">
 
201
  <div class="flex gap-2">
202
+ <div class="w-3.5 h-3.5 rounded-full bg-red-500/80 shadow-[0_0_8px_rgba(239,68,68,0.5)]"></div>
203
+ <div class="w-3.5 h-3.5 rounded-full bg-yellow-500/80 shadow-[0_0_8px_rgba(234,179,8,0.5)]"></div>
204
+ <div class="w-3.5 h-3.5 rounded-full bg-green-500/80 shadow-[0_0_8px_rgba(34,197,94,0.5)]"></div>
205
  </div>
206
+ <span class="text-xs font-mono text-slate-400 bg-white/5 px-3 py-1 rounded-full border border-white/5">root@serverspace:~</span>
207
+ <div class="w-14"></div> </div>
 
208
 
209
+ <div class="term-container bg-transparent">
 
210
  <div id="terminal" class="term-wrapper"></div>
211
  </div>
212
 
213
+ <div class="p-3 md:p-5 bg-black/40 border-t border-white/5 z-10 shrink-0 backdrop-blur-xl">
 
214
  <div class="relative flex items-center">
215
+ <i class="ph ph-caret-right text-primary absolute left-5 text-xl animate-pulse"></i>
216
+ <input type="text" id="cmd-input" class="w-full bg-surfaceHover/50 border border-white/10 focus:border-primary/50 focus:bg-surfaceHover text-white rounded-xl pl-12 pr-14 py-3.5 md:py-4 text-sm font-mono transition-all outline-none shadow-inner" placeholder="Enter command...">
217
+ <button onclick="sendCommand()" class="absolute right-2 p-2.5 bg-gradient-btn rounded-lg text-white">
218
+ <i class="ph-bold ph-paper-plane-right text-lg"></i>
219
  </button>
220
  </div>
221
  </div>
222
  </div>
223
  </div>
224
 
225
+ <div id="tab-files" class="hidden-tab h-full flex flex-col min-w-0">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
 
227
+ <div class="mb-4 hidden md:flex justify-between items-end">
228
+ <div>
229
+ <h2 class="text-2xl font-bold text-white">File Explorer</h2>
230
+ <p class="text-slate-400 text-sm mt-1">Manage, upload, and edit your server configurations.</p>
231
+ </div>
232
  </div>
233
 
234
+ <div class="flex flex-col flex-grow premium-card overflow-hidden min-w-0">
235
+ <div class="bg-black/40 px-5 md:px-6 py-4 md:py-5 border-b border-white/5 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 shrink-0">
236
+ <div class="flex items-center gap-2 text-sm font-mono text-slate-300 overflow-x-auto whitespace-nowrap w-full sm:w-auto bg-surfaceHover/50 px-4 py-2 rounded-lg border border-white/5 shadow-inner" id="breadcrumbs">
237
+ </div>
238
+ <div class="flex gap-3 shrink-0">
239
+ <input type="file" id="file-upload" class="hidden" onchange="uploadFile(event)">
240
+ <button onclick="document.getElementById('file-upload').click()" class="bg-gradient-btn px-5 py-2.5 rounded-xl text-xs md:text-sm font-bold text-white flex items-center gap-2">
241
+ <i class="ph-bold ph-upload-simple text-lg"></i> Upload
242
+ </button>
243
+ <button onclick="loadFiles(currentPath)" class="bg-white/5 border border-white/10 px-4 py-2.5 rounded-xl text-slate-300 hover:text-white hover:bg-white/10 transition-all shadow-lg">
244
+ <i class="ph-bold ph-arrows-clockwise text-lg"></i>
245
+ </button>
246
+ </div>
247
+ </div>
248
+
249
+ <div class="hidden sm:grid grid-cols-12 gap-4 px-8 py-3 bg-white/[0.02] border-b border-white/5 text-[11px] font-bold text-slate-400 uppercase tracking-wider shrink-0">
250
+ <div class="col-span-7">Filename</div>
251
+ <div class="col-span-3 text-right">Size</div>
252
+ <div class="col-span-2 text-right">Actions</div>
253
+ </div>
254
+
255
+ <div class="flex-grow overflow-y-auto bg-transparent p-3 md:p-4" id="file-list">
256
+ </div>
257
  </div>
258
  </div>
259
  </main>
260
 
261
+ <nav class="md:hidden mobile-nav-glass fixed bottom-0 left-0 right-0 py-3 px-8 flex justify-between items-center z-50">
262
+ <button onclick="switchTab('console')" id="mob-console" class="mob-nav-item active flex flex-col items-center gap-1.5 w-20">
263
+ <i class="ph-fill ph-terminal-window text-2xl"></i>
264
+ <span class="text-[10px] font-semibold tracking-wide uppercase">Console</span>
 
 
265
  </button>
266
+ <div class="w-12 h-12 bg-gradient-btn rounded-full flex items-center justify-center shadow-neon -mt-6 border-[4px] border-[#080B11]">
267
+ <i class="ph ph-cube text-white text-xl"></i>
268
+ </div>
269
+ <button onclick="switchTab('files')" id="mob-files" class="mob-nav-item flex flex-col items-center gap-1.5 w-20">
270
+ <i class="ph-fill ph-folder-notch text-2xl"></i>
271
+ <span class="text-[10px] font-semibold tracking-wide uppercase">Files</span>
272
  </button>
273
  </nav>
274
 
275
+ <div id="editor-modal" class="fixed inset-0 bg-black/60 backdrop-blur-md hidden items-center justify-center p-4 md:p-8 z-[100] opacity-0 transition-opacity duration-300">
276
+ <div class="premium-card w-full max-w-5xl h-[90vh] flex flex-col transform scale-95 transition-transform duration-300 ring-1 ring-white/10 shadow-[0_0_50px_rgba(0,0,0,0.8)]" id="editor-card">
277
+ <div class="bg-black/60 px-6 py-5 flex justify-between items-center border-b border-white/10 shrink-0 backdrop-blur-xl">
278
+ <div class="flex items-center gap-3 text-sm font-mono text-white bg-white/5 px-4 py-2 rounded-lg">
 
279
  <i class="ph-fill ph-file-code text-primary text-xl"></i>
280
  <span id="editor-title">file.txt</span>
281
  </div>
282
+ <div class="flex gap-3">
283
+ <button onclick="closeEditor()" class="px-5 py-2.5 bg-white/5 hover:bg-white/10 border border-white/10 rounded-xl text-xs md:text-sm font-bold text-slate-300 transition-colors">Close</button>
284
+ <button onclick="saveFile()" class="bg-gradient-btn px-6 py-2.5 rounded-xl text-xs md:text-sm font-bold text-white shadow-neon flex items-center gap-2">
285
+ <i class="ph-bold ph-floppy-disk text-lg"></i> Save Changes
286
  </button>
287
  </div>
288
  </div>
289
+ <textarea id="editor-content" class="flex-grow bg-[#06080D]/80 text-slate-200 p-6 font-mono text-sm md:text-base resize-none focus:outline-none w-full leading-relaxed" spellcheck="false"></textarea>
290
  </div>
291
  </div>
292
 
293
+ <div id="toast-container" class="fixed top-6 right-6 md:top-8 md:right-8 z-[200] flex flex-col gap-4 pointer-events-none"></div>
 
294
 
295
  <script>
296
  // --- Tab Navigation ---
297
  function switchTab(tab) {
298
+ document.getElementById('tab-console').classList.add('hidden-tab');
299
  document.getElementById('tab-files').classList.add('hidden-tab');
300
 
301
  // Reset Desktop
302
+ document.getElementById('nav-console').className = "nav-item flex items-center gap-4 px-4 py-3.5 rounded-r-xl font-medium border-l-3 border-transparent";
303
+ document.getElementById('nav-files').className = "nav-item flex items-center gap-4 px-4 py-3.5 rounded-r-xl font-medium border-l-3 border-transparent";
304
 
305
  // Reset Mobile
306
+ document.getElementById('mob-console').classList.remove('active');
307
  document.getElementById('mob-files').classList.remove('active');
308
 
309
  // Activate
310
  document.getElementById('tab-' + tab).classList.remove('hidden-tab');
311
  document.getElementById('tab-' + tab).classList.add('fade-in');
312
 
313
+ document.getElementById('nav-' + tab).className = "nav-item active flex items-center gap-4 px-4 py-3.5 rounded-r-xl font-medium";
314
  document.getElementById('mob-' + tab).classList.add('active');
315
 
316
+ if(tab === 'console' && fitAddon) setTimeout(() => fitAddon.fit(), 100);
317
  if(tab === 'files' && !window.filesLoaded) { loadFiles(''); window.filesLoaded = true; }
318
  }
319
 
320
  // --- Terminal Engine ---
321
  const term = new Terminal({
322
+ theme: { background: 'transparent', foreground: '#E2E8F0', cursor: '#8B5CF6', selectionBackground: 'rgba(139, 92, 246, 0.3)' },
323
+ fontFamily: "'JetBrains Mono', monospace", fontSize: window.innerWidth < 768 ? 12 : 14, cursorBlink: true, convertEol: true
324
  });
325
  const fitAddon = new FitAddon.FitAddon();
326
  term.loadAddon(fitAddon);
327
  term.open(document.getElementById('terminal'));
328
 
 
329
  const ro = new ResizeObserver(() => {
330
+ if(!document.getElementById('tab-console').classList.contains('hidden-tab')) {
331
  requestAnimationFrame(() => fitAddon.fit());
332
  }
333
  });
 
335
  setTimeout(() => fitAddon.fit(), 200);
336
 
337
  const ws = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws');
338
+ ws.onopen = () => term.write('\\x1b[38;5;135m\\x1b[1m[System]\\x1b[0m Secure engine connection established.\\r\\n');
339
  ws.onmessage = e => term.write(e.data + '\\n');
340
 
341
  const cmdInput = document.getElementById('cmd-input');
342
  cmdInput.addEventListener('keypress', e => { if (e.key === 'Enter') sendCommand(); });
343
  function sendCommand() {
344
  if(cmdInput.value.trim() && ws.readyState === WebSocket.OPEN) {
345
+ term.write(`\\x1b[38;5;51m> ${cmdInput.value}\\x1b[0m\\r\\n`);
346
  ws.send(cmdInput.value); cmdInput.value = '';
347
  }
348
  }
349
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  // --- File Manager ---
351
  let currentPath = '';
352
  let editPath = '';
 
354
  function showToast(msg, type='info') {
355
  const container = document.getElementById('toast-container');
356
  const el = document.createElement('div');
357
+ let icon = '<i class="ph-fill ph-info text-accent text-2xl drop-shadow-[0_0_8px_#0EA5E9]"></i>';
358
+ if(type==='success') icon = '<i class="ph-fill ph-check-circle text-green-400 text-2xl drop-shadow-[0_0_8px_#22C55E]"></i>';
359
+ if(type==='error') icon = '<i class="ph-fill ph-warning-circle text-red-400 text-2xl drop-shadow-[0_0_8px_#EF4444]"></i>';
360
 
361
+ el.className = `flex items-center gap-4 bg-surface/90 backdrop-blur-xl border border-white/10 text-white px-6 py-4 rounded-2xl shadow-[0_10px_40px_rgba(0,0,0,0.5)] translate-x-12 opacity-0 transition-all duration-300`;
362
+ el.innerHTML = `${icon} <span class="font-medium text-sm tracking-wide">${msg}</span>`;
363
  container.appendChild(el);
364
 
365
+ requestAnimationFrame(() => el.classList.remove('translate-x-12', 'opacity-0'));
366
+ setTimeout(() => { el.classList.add('translate-x-12', 'opacity-0'); setTimeout(() => el.remove(), 300); }, 3500);
367
  }
368
 
369
  async function loadFiles(path) {
370
  currentPath = path;
371
  const parts = path.split('/').filter(p => p);
372
+ let bc = `<button onclick="loadFiles('')" class="hover:text-primary transition"><i class="ph-fill ph-house text-lg"></i></button>`;
373
  let bp = '';
374
  parts.forEach((p, i) => {
375
  bp += (bp?'/':'') + p;
376
+ bc += `<i class="ph-bold ph-caret-right text-xs mx-3 text-slate-600"></i>`;
377
+ if(i === parts.length-1) bc += `<span class="text-primary font-bold bg-primary/10 px-2 py-0.5 rounded">${p}</span>`;
378
+ else bc += `<button onclick="loadFiles('${bp}')" class="hover:text-primary transition">${p}</button>`;
379
  });
380
  document.getElementById('breadcrumbs').innerHTML = bc;
381
 
382
  const list = document.getElementById('file-list');
383
+ list.innerHTML = `<div class="flex flex-col items-center justify-center py-20 gap-4"><i class="ph-bold ph-circle-notch animate-spin text-4xl text-primary"></i><span class="text-slate-500 font-mono text-sm">Syncing system files...</span></div>`;
384
 
385
  try {
386
  const res = await fetch(`/api/fs/list?path=${encodeURIComponent(path)}`);
 
390
  if (path !== '') {
391
  const parent = path.split('/').slice(0, -1).join('/');
392
  list.innerHTML += `
393
+ <div class="flex items-center px-5 py-4 cursor-pointer hover:bg-white/5 rounded-2xl transition-all mb-2 border border-transparent" onclick="loadFiles('${parent}')">
394
+ <div class="p-2 bg-white/5 rounded-lg mr-4"><i class="ph-bold ph-arrow-u-up-left text-slate-400 text-lg"></i></div>
395
+ <span class="text-sm font-mono text-slate-300 font-semibold tracking-wide">.. / Return</span>
396
  </div>`;
397
  }
398
 
399
  files.forEach(f => {
400
+ const icon = f.is_dir ? '<div class="p-3 bg-primary/10 border border-primary/20 rounded-xl text-primary shadow-[0_0_15px_rgba(139,92,246,0.15)] group-hover:bg-primary group-hover:text-white transition-all duration-300"><i class="ph-fill ph-folder text-xl"></i></div>' : '<div class="p-3 bg-surface border border-white/5 rounded-xl text-slate-400 group-hover:bg-white/10 group-hover:text-white transition-all duration-300"><i class="ph-fill ph-file-text text-xl"></i></div>';
401
+ const sz = f.is_dir ? '<span class="px-2 py-1 bg-white/5 rounded text-[10px] text-slate-500">DIR</span>' : (f.size > 1048576 ? `<span class="px-2 py-1 bg-white/5 rounded text-[10px] text-slate-300">${(f.size/1048576).toFixed(1)} MB</span>` : `<span class="px-2 py-1 bg-white/5 rounded text-[10px] text-slate-400">${(f.size/1024).toFixed(1)} KB</span>`);
402
  const fp = path ? `${path}/${f.name}` : f.name;
403
 
404
  list.innerHTML += `
405
+ <div class="flex flex-col sm:grid sm:grid-cols-12 items-start sm:items-center px-4 py-3 gap-3 group hover:bg-white/[0.03] rounded-2xl transition-all duration-300 mb-2 border border-transparent hover:border-white/5 hover:shadow-lg">
406
+ <div class="col-span-7 flex items-center gap-5 w-full ${f.is_dir?'cursor-pointer':''}" ${f.is_dir?`onclick="loadFiles('${fp}')"`:''}>
407
  ${icon}
408
+ <span class="text-sm font-mono text-slate-200 truncate group-hover:text-white transition font-medium tracking-wide">${f.name}</span>
409
  </div>
410
+ <div class="col-span-3 text-right font-mono hidden sm:block">${sz}</div>
411
+ <div class="col-span-2 flex justify-end gap-2 w-full sm:w-auto sm:opacity-0 group-hover:opacity-100 transition-opacity duration-300">
412
+ ${!f.is_dir ? `<button onclick="editFile('${fp}')" class="p-2.5 bg-surface border border-white/5 hover:border-primary hover:text-primary hover:shadow-[0_0_10px_rgba(139,92,246,0.2)] rounded-xl transition-all"><i class="ph-bold ph-pencil-simple text-base"></i></button>` : ''}
413
+ ${!f.is_dir ? `<a href="/api/fs/download?path=${encodeURIComponent(fp)}" class="p-2.5 bg-surface border border-white/5 hover:border-accent hover:text-accent hover:shadow-[0_0_10px_rgba(14,165,233,0.2)] rounded-xl transition-all"><i class="ph-bold ph-download-simple text-base"></i></a>` : ''}
414
+ <button onclick="deleteFile('${fp}')" class="p-2.5 bg-surface border border-white/5 hover:border-secondary hover:text-secondary hover:shadow-[0_0_10px_rgba(217,70,239,0.2)] rounded-xl transition-all"><i class="ph-bold ph-trash text-base"></i></button>
415
  </div>
416
  </div>`;
417
  });
418
+ } catch (err) { list.innerHTML = `<div class="text-center py-10 text-red-400 text-sm font-mono bg-red-500/10 border border-red-500/20 rounded-2xl mx-4">System fault: Unable to access directory mapping.</div>`; }
419
  }
420
 
421
  async function editFile(path) {
 
428
  const m = document.getElementById('editor-modal'); const c = document.getElementById('editor-card');
429
  m.classList.remove('hidden'); m.classList.add('flex');
430
  requestAnimationFrame(() => { m.classList.remove('opacity-0'); c.classList.remove('scale-95'); });
431
+ } else showToast('Binary file cannot be parsed', 'error');
432
+ } catch { showToast('Error accessing data block', 'error'); }
433
  }
434
 
435
  function closeEditor() {
 
442
  const fd = new FormData(); fd.append('path', editPath); fd.append('content', document.getElementById('editor-content').value);
443
  try {
444
  const res = await fetch('/api/fs/write', { method: 'POST', body: fd });
445
+ if(res.ok) { showToast('Block verified and saved', 'success'); closeEditor(); } else throw new Error();
446
+ } catch { showToast('Write operation failed', 'error'); }
447
  }
448
 
449
  async function deleteFile(path) {
450
+ if(confirm(`WARNING: Erase ${path.split('/').pop()} from the filesystem? This cannot be undone.`)) {
451
  const fd = new FormData(); fd.append('path', path);
452
  try {
453
  const res = await fetch('/api/fs/delete', { method: 'POST', body: fd });
454
+ if(res.ok) { showToast('Data block purged', 'success'); loadFiles(currentPath); } else throw new Error();
455
+ } catch { showToast('Purge operation failed', 'error'); }
456
  }
457
  }
458
 
459
  async function uploadFile(e) {
460
  if(!e.target.files.length) return;
461
+ showToast('Injecting payload...', 'info');
462
  const fd = new FormData(); fd.append('path', currentPath); fd.append('file', e.target.files[0]);
463
  try {
464
  const res = await fetch('/api/fs/upload', { method: 'POST', body: fd });
465
+ if(res.ok) { showToast('Payload injected successfully', 'success'); loadFiles(currentPath); } else throw new Error();
466
+ } catch { showToast('Injection failed', 'error'); }
467
  e.target.value = '';
468
  }
469
  </script>
 
471
  </html>
472
  """
473
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
474
  # -----------------
475
  # UTILITIES & SERVER
476
  # -----------------
 
523
  @app.on_event("startup")
524
  async def startup_event():
525
  asyncio.create_task(start_minecraft())
 
526
 
527
  # -----------------
528
  # API ROUTING
 
530
  @app.get("/")
531
  def get_panel(): return HTMLResponse(content=HTML_CONTENT)
532
 
 
 
 
 
 
533
  @app.websocket("/ws")
534
  async def ws_endpoint(websocket: WebSocket):
535
  await websocket.accept()