OrbitMC commited on
Commit
81f44b5
·
verified ·
1 Parent(s): fcf51d8

Update panel.py

Browse files
Files changed (1) hide show
  1. panel.py +352 -190
panel.py CHANGED
@@ -1,11 +1,12 @@
1
  import os
2
  import asyncio
3
  import collections
 
 
4
  from fastapi import FastAPI, WebSocket, Request, Response, Form, UploadFile, File, HTTPException
5
  from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
6
  from fastapi.middleware.cors import CORSMiddleware
7
  import uvicorn
8
- import shutil
9
 
10
  app = FastAPI()
11
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
@@ -16,7 +17,7 @@ connected_clients = set()
16
  BASE_DIR = os.path.abspath("/app")
17
 
18
  # -----------------
19
- # HTML FRONTEND (Ultra-Modern UI)
20
  # -----------------
21
  HTML_CONTENT = """
22
  <!DOCTYPE html>
@@ -24,222 +25,360 @@ HTML_CONTENT = """
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>Server Console</title>
28
- <script src="https://cdn.tailwindcss.com"></script>
29
- <script src="https://unpkg.com/lucide@latest"></script>
 
 
 
 
30
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css" />
31
  <script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
32
  <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit/lib/xterm-addon-fit.js"></script>
33
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  <style>
35
- :root { --bg: #09090b; --surface: #18181b; --surface-hover: #27272a; --border: #27272a; --text: #fafafa; --text-muted: #a1a1aa; --accent: #3b82f6; }
36
- body { background-color: var(--bg); color: var(--text); font-family: 'Inter', sans-serif; overflow: hidden; -webkit-font-smoothing: antialiased; }
37
- .font-mono { font-family: 'JetBrains Mono', monospace; }
38
-
39
- /* Glass Navbar */
40
- .glass-nav { background: rgba(9, 9, 11, 0.8); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-bottom: 1px solid var(--border); z-index: 40; }
41
 
42
- /* Animations */
43
- .fade-in { animation: fadeIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
44
- @keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
45
-
46
- /* Terminal Styling with Top Fade */
47
- .term-container { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; position: relative; }
48
- .term-wrapper { padding: 12px; height: calc(100vh - 180px); width: 100%; mask-image: linear-gradient(to bottom, transparent 0%, black 5%, black 100%); -webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 5%, black 100%); }
49
- .xterm-viewport::-webkit-scrollbar { width: 8px; }
50
- .xterm-viewport::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 4px; }
 
51
 
52
- /* File Manager Layout */
53
- .file-row { transition: all 0.15s ease; border-bottom: 1px solid var(--border); }
54
- .file-row:hover { background: var(--surface-hover); }
55
- .file-row:last-child { border-bottom: none; }
 
 
 
 
 
 
56
 
57
  /* Custom Scrollbars */
58
- ::-webkit-scrollbar { width: 6px; height: 6px; }
59
  ::-webkit-scrollbar-track { background: transparent; }
60
- ::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 3px; }
61
- ::-webkit-scrollbar-thumb:hover { background: #52525b; }
62
 
63
- /* Loader */
64
- .loader { animation: spin 1s linear infinite; }
65
- @keyframes spin { 100% { transform: rotate(360deg); } }
66
 
67
- /* Utility */
 
 
68
  .hidden-tab { display: none !important; }
69
- input[type="text"]:focus, textarea:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
 
 
 
70
  </style>
71
  </head>
72
- <body class="flex flex-col h-screen w-full">
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
- <!-- Top Navigation -->
75
- <nav class="glass-nav w-full px-4 sm:px-6 py-3 flex justify-between items-center fixed top-0 left-0 right-0 h-[60px]">
76
- <div class="flex items-center gap-3">
77
- <div class="bg-blue-500/10 p-2 rounded-lg border border-blue-500/20">
78
- <i data-lucide="server" class="w-5 h-5 text-blue-400"></i>
 
 
 
 
79
  </div>
80
- <span class="font-semibold tracking-tight text-sm sm:text-base text-gray-100">Minecraft Engine</span>
81
- <span class="px-2 py-0.5 rounded-full bg-green-500/10 text-green-400 border border-green-500/20 text-[10px] font-bold tracking-wide uppercase hidden sm:block">Online</span>
82
  </div>
83
-
84
- <div class="flex gap-1 sm:gap-2 bg-zinc-900 p-1 rounded-lg border border-zinc-800">
85
- <button onclick="switchTab('console')" id="btn-console" class="flex items-center gap-2 px-3 py-1.5 sm:px-4 sm:py-2 bg-zinc-800 text-gray-100 rounded-md text-xs sm:text-sm font-medium transition-all shadow-sm">
86
- <i data-lucide="terminal" class="w-4 h-4"></i><span class="hidden sm:inline">Console</span>
87
  </button>
88
- <button onclick="switchTab('files')" id="btn-files" class="flex items-center gap-2 px-3 py-1.5 sm:px-4 sm:py-2 text-zinc-400 hover:text-gray-200 rounded-md text-xs sm:text-sm font-medium transition-all">
89
- <i data-lucide="folder-code" class="w-4 h-4"></i><span class="hidden sm:inline">Files</span>
90
  </button>
 
 
 
 
 
 
 
 
91
  </div>
92
- </nav>
93
 
94
  <!-- Main Content Area -->
95
- <main class="mt-[60px] flex-grow p-3 sm:p-4 overflow-hidden relative">
96
 
97
- <!-- Console Tab -->
98
- <div id="tab-console" class="h-full w-full max-w-7xl mx-auto flex flex-col fade-in">
99
- <div class="term-container shadow-2xl flex-grow flex flex-col">
100
- <div class="bg-zinc-900 border-b border-zinc-800 px-4 py-2 flex items-center gap-2 text-xs text-zinc-400 font-mono">
101
- <div class="flex gap-1.5"><div class="w-2.5 h-2.5 rounded-full bg-red-500/80"></div><div class="w-2.5 h-2.5 rounded-full bg-yellow-500/80"></div><div class="w-2.5 h-2.5 rounded-full bg-green-500/80"></div></div>
102
- <span class="ml-2">server-stdout</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  </div>
104
- <div id="terminal" class="term-wrapper"></div>
105
  </div>
106
-
107
- <div class="mt-3 sm:mt-4 relative">
108
- <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-zinc-500">
109
- <i data-lucide="chevron-right" class="w-5 h-5"></i>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  </div>
111
- <input type="text" id="cmd-input" class="w-full bg-zinc-900 border border-zinc-800 text-gray-200 rounded-lg pl-10 pr-12 py-3 text-sm font-mono transition-all shadow-inner placeholder-zinc-600" placeholder="Execute command...">
112
- <button onclick="sendCommand()" class="absolute inset-y-1 right-1 px-3 bg-blue-600 hover:bg-blue-500 text-white rounded-md transition-colors flex items-center justify-center">
113
- <i data-lucide="send" class="w-4 h-4"></i>
114
- </button>
115
  </div>
116
  </div>
117
 
118
- <!-- File Manager Tab -->
119
- <div id="tab-files" class="hidden-tab h-full w-full max-w-7xl mx-auto flex flex-col bg-[#18181b] rounded-xl border border-zinc-800 shadow-2xl overflow-hidden">
120
- <!-- File Header / Breadcrumbs -->
121
- <div class="bg-zinc-900/50 px-4 py-3 border-b border-zinc-800 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3">
122
- <div class="flex items-center text-sm font-mono text-zinc-400 overflow-x-auto whitespace-nowrap hide-scrollbar w-full sm:w-auto" id="breadcrumbs">
 
123
  <!-- Injected via JS -->
124
  </div>
125
- <div class="flex items-center gap-2 shrink-0">
 
126
  <input type="file" id="file-upload" class="hidden" onchange="uploadFile(event)">
127
- <button onclick="document.getElementById('file-upload').click()" class="flex items-center gap-1.5 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 px-3 py-1.5 rounded-md text-xs font-medium text-gray-200 transition-colors">
128
- <i data-lucide="upload-cloud" class="w-4 h-4 text-blue-400"></i> Upload
129
  </button>
130
- <button onclick="loadFiles(currentPath)" class="flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 w-8 h-8 rounded-md transition-colors">
131
- <i data-lucide="refresh-cw" class="w-4 h-4 text-zinc-400"></i>
132
  </button>
133
  </div>
134
  </div>
135
 
136
- <!-- File List Columns -->
137
- <div class="hidden sm:grid grid-cols-12 gap-4 px-5 py-2 border-b border-zinc-800 bg-zinc-900/80 text-xs font-semibold text-zinc-500 uppercase tracking-wider">
138
- <div class="col-span-7">Name</div>
139
  <div class="col-span-3 text-right">Size</div>
140
  <div class="col-span-2 text-right">Actions</div>
141
  </div>
142
 
143
- <!-- File List -->
144
- <div class="flex-grow overflow-y-auto" id="file-list">
145
  <!-- Injected via JS -->
146
  </div>
147
  </div>
 
148
  </main>
149
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  <!-- Code Editor Modal -->
151
- <div id="editor-modal" class="fixed inset-0 bg-black/60 backdrop-blur-sm hidden items-center justify-center p-2 sm:p-6 z-50 opacity-0 transition-opacity duration-300">
152
- <div class="bg-[#18181b] rounded-xl border border-zinc-800 w-full max-w-5xl h-[90vh] sm:h-[85vh] flex flex-col shadow-2xl transform scale-95 transition-transform duration-300" id="editor-card">
153
- <div class="bg-zinc-900 px-4 py-3 flex justify-between items-center border-b border-zinc-800 rounded-t-xl">
154
  <div class="flex items-center gap-2 text-sm font-mono text-gray-300">
155
- <i data-lucide="file-code" class="w-4 h-4 text-blue-400"></i>
156
  <span id="editor-title">filename.txt</span>
157
  </div>
158
  <div class="flex items-center gap-2">
159
- <button onclick="closeEditor()" class="px-3 py-1.5 hover:bg-zinc-800 rounded text-xs font-medium text-zinc-400 transition-colors">Cancel</button>
160
- <button onclick="saveFile()" class="px-4 py-1.5 bg-blue-600 hover:bg-blue-500 text-white rounded text-xs font-semibold transition-colors flex items-center gap-1.5 shadow-lg shadow-blue-500/20">
161
- <i data-lucide="save" class="w-3.5 h-3.5"></i> Save
162
  </button>
163
  </div>
164
  </div>
165
- <textarea id="editor-content" class="flex-grow bg-[#0e0e11] text-zinc-300 p-4 font-mono text-xs sm:text-sm resize-none focus:outline-none w-full leading-relaxed" spellcheck="false"></textarea>
166
  </div>
167
  </div>
168
 
169
- <!-- Toast Notifications -->
170
- <div id="toast-container" class="fixed bottom-4 right-4 z-[100] flex flex-col gap-2"></div>
171
 
172
  <script>
173
- // Initialize Lucide Icons
174
- lucide.createIcons();
175
-
176
  // --- Toast System ---
177
  function showToast(message, type = 'info') {
178
  const container = document.getElementById('toast-container');
179
  const toast = document.createElement('div');
180
 
181
- let icon = '<i data-lucide="info" class="w-4 h-4 text-blue-400"></i>';
182
- let border = 'border-blue-500/20';
183
- if(type === 'success') { icon = '<i data-lucide="check-circle" class="w-4 h-4 text-green-400"></i>'; border = 'border-green-500/20'; }
184
- if(type === 'error') { icon = '<i data-lucide="alert-circle" class="w-4 h-4 text-red-400"></i>'; border = 'border-red-500/20'; }
185
 
186
- toast.className = `flex items-center gap-3 bg-zinc-900 border ${border} text-sm text-gray-200 px-4 py-3 rounded-lg shadow-xl translate-y-8 opacity-0 transition-all duration-300`;
187
- toast.innerHTML = `${icon} <span>${message}</span>`;
188
 
189
  container.appendChild(toast);
190
- lucide.createIcons();
191
 
192
- // Animate In
193
- requestAnimationFrame(() => {
194
- toast.classList.remove('translate-y-8', 'opacity-0');
195
- });
196
-
197
- // Animate Out
198
  setTimeout(() => {
199
- toast.classList.add('translate-y-8', 'opacity-0');
200
  setTimeout(() => toast.remove(), 300);
201
  }, 3000);
202
  }
203
 
204
- // --- UI Navigation ---
205
  function switchTab(tab) {
206
- document.getElementById('tab-console').classList.add('hidden-tab');
 
207
  document.getElementById('tab-files').classList.add('hidden-tab');
208
 
209
- document.getElementById('btn-console').className = "flex items-center gap-2 px-3 py-1.5 sm:px-4 sm:py-2 text-zinc-400 hover:text-gray-200 rounded-md text-xs sm:text-sm font-medium transition-all";
210
- document.getElementById('btn-files').className = "flex items-center gap-2 px-3 py-1.5 sm:px-4 sm:py-2 text-zinc-400 hover:text-gray-200 rounded-md text-xs sm:text-sm font-medium transition-all";
 
211
 
 
 
 
 
 
212
  document.getElementById('tab-' + tab).classList.remove('hidden-tab');
213
  document.getElementById('tab-' + tab).classList.add('fade-in');
214
 
215
- const activeBtn = document.getElementById('btn-' + tab);
216
- activeBtn.className = "flex items-center gap-2 px-3 py-1.5 sm:px-4 sm:py-2 bg-zinc-800 text-gray-100 rounded-md text-xs sm:text-sm font-medium transition-all shadow-sm";
 
217
 
218
- if(tab === 'console' && fitAddon) { setTimeout(() => fitAddon.fit(), 50); }
 
219
  if(tab === 'files' && !currentPathLoaded) { loadFiles(''); currentPathLoaded = true; }
220
  }
221
 
222
- // --- Terminal Logic ---
223
  const term = new Terminal({
224
- theme: { background: 'transparent', foreground: '#e4e4e7', cursor: '#3b82f6', selectionBackground: 'rgba(59, 130, 246, 0.3)' },
225
- convertEol: true, cursorBlink: true, fontFamily: "'JetBrains Mono', monospace", fontSize: 13, fontWeight: 400
 
226
  });
227
  const fitAddon = new FitAddon.FitAddon();
228
  term.loadAddon(fitAddon);
229
  term.open(document.getElementById('terminal'));
230
 
231
- // Wait slightly for DOM to render to fit exactly
232
- setTimeout(() => fitAddon.fit(), 100);
233
- window.addEventListener('resize', () => { if(!document.getElementById('tab-console').classList.contains('hidden-tab')) fitAddon.fit(); });
 
 
 
 
 
234
 
235
  const wsUrl = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws';
236
  let ws;
237
 
238
  function connectWS() {
239
  ws = new WebSocket(wsUrl);
240
- ws.onopen = () => term.write('\\x1b[32m\\x1b[1m[Panel]\\x1b[0m Connected to server stream.\\r\\n');
241
  ws.onmessage = e => term.write(e.data + '\\n');
242
- ws.onclose = () => { term.write('\\r\\n\\x1b[31m\\x1b[1m[Panel]\\x1b[0m Connection lost. Reconnecting in 3s...\\r\\n'); setTimeout(connectWS, 3000); };
243
  }
244
  connectWS();
245
 
@@ -255,6 +394,35 @@ HTML_CONTENT = """
255
  }
256
  }
257
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  // --- File Manager Logic ---
259
  let currentPath = '';
260
  let currentPathLoaded = false;
@@ -262,30 +430,28 @@ HTML_CONTENT = """
262
 
263
  function renderBreadcrumbs(path) {
264
  const parts = path.split('/').filter(p => p);
265
- let html = `<button onclick="loadFiles('')" class="hover:text-gray-200 transition-colors"><i data-lucide="home" class="w-4 h-4"></i></button>`;
266
  let buildPath = '';
267
 
268
  if (parts.length > 0) {
269
  parts.forEach((part, index) => {
270
  buildPath += (buildPath ? '/' : '') + part;
271
- html += ` <i data-lucide="chevron-right" class="w-3.5 h-3.5 mx-1 opacity-50"></i> `;
272
  if(index === parts.length - 1) {
273
- html += `<span class="text-blue-400 font-medium">${part}</span>`;
274
  } else {
275
- html += `<button onclick="loadFiles('${buildPath}')" class="hover:text-gray-200 transition-colors">${part}</button>`;
276
  }
277
  });
278
  }
279
  document.getElementById('breadcrumbs').innerHTML = html;
280
- lucide.createIcons();
281
  }
282
 
283
  async function loadFiles(path) {
284
  currentPath = path;
285
  renderBreadcrumbs(path);
286
  const list = document.getElementById('file-list');
287
- list.innerHTML = `<div class="flex justify-center py-8"><i data-lucide="loader-2" class="w-6 h-6 text-zinc-500 loader"></i></div>`;
288
- lucide.createIcons();
289
 
290
  try {
291
  const res = await fetch(`/api/fs/list?path=${encodeURIComponent(path)}`);
@@ -296,43 +462,39 @@ HTML_CONTENT = """
296
  if (path !== '') {
297
  const parent = path.split('/').slice(0, -1).join('/');
298
  list.innerHTML += `
299
- <div class="file-row flex items-center px-5 py-3 cursor-pointer" onclick="loadFiles('${parent}')">
300
- <div class="flex items-center gap-3 w-full">
301
- <i data-lucide="corner-left-up" class="w-4 h-4 text-zinc-500"></i>
302
- <span class="text-sm font-mono text-zinc-400">..</span>
303
- </div>
304
  </div>`;
305
  }
306
 
307
  if(files.length === 0 && path === '') {
308
- list.innerHTML += `<div class="text-center py-8 text-zinc-500 text-sm">Directory is empty</div>`;
309
  }
310
 
311
  files.forEach(f => {
312
- const icon = f.is_dir ? '<i data-lucide="folder" class="w-4 h-4 text-blue-400 fill-blue-400/10"></i>' : '<i data-lucide="file" class="w-4 h-4 text-zinc-400"></i>';
313
  const sizeStr = f.is_dir ? '--' : (f.size > 1024*1024 ? (f.size/(1024*1024)).toFixed(1) + ' MB' : (f.size / 1024).toFixed(1) + ' KB');
314
  const fullPath = path ? `${path}/${f.name}` : f.name;
315
  const actionClick = f.is_dir ? `onclick="loadFiles('${fullPath}')"` : '';
316
  const pointer = f.is_dir ? 'cursor-pointer' : '';
317
 
318
  list.innerHTML += `
319
- <div class="file-row flex flex-col sm:grid sm:grid-cols-12 items-start sm:items-center px-5 py-3 gap-2 sm:gap-4 group">
320
  <div class="col-span-7 flex items-center gap-3 w-full ${pointer}" ${actionClick}>
321
  ${icon}
322
- <span class="text-sm font-mono text-gray-200 truncate group-hover:text-blue-400 transition-colors">${f.name}</span>
323
  </div>
324
- <div class="col-span-3 text-right text-xs text-zinc-500 font-mono hidden sm:block">${sizeStr}</div>
325
- <div class="col-span-2 flex justify-end gap-1 sm:gap-2 w-full sm:w-auto mt-2 sm:mt-0 sm:opacity-0 group-hover:opacity-100 transition-opacity">
326
- ${!f.is_dir ? `<button onclick="editFile('${fullPath}')" class="p-1.5 text-zinc-400 hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors" title="Edit"><i data-lucide="edit-3" class="w-4 h-4"></i></button>` : ''}
327
- ${!f.is_dir ? `<a href="/api/fs/download?path=${encodeURIComponent(fullPath)}" class="p-1.5 text-zinc-400 hover:text-green-400 hover:bg-green-500/10 rounded transition-colors inline-block" title="Download"><i data-lucide="download" class="w-4 h-4"></i></a>` : ''}
328
- <button onclick="deleteFile('${fullPath}')" class="p-1.5 text-zinc-400 hover:text-red-400 hover:bg-red-500/10 rounded transition-colors" title="Delete"><i data-lucide="trash-2" class="w-4 h-4"></i></button>
329
  </div>
330
  </div>`;
331
  });
332
- lucide.createIcons();
333
  } catch (err) {
334
  showToast("Failed to load directory", "error");
335
- list.innerHTML = `<div class="text-center py-8 text-red-400 text-sm">Error loading files</div>`;
336
  }
337
  }
338
 
@@ -350,17 +512,12 @@ HTML_CONTENT = """
350
  modal.classList.remove('hidden');
351
  modal.classList.add('flex');
352
 
353
- // Animate In
354
  requestAnimationFrame(() => {
355
  modal.classList.remove('opacity-0');
356
  card.classList.remove('scale-95');
357
  });
358
- } else {
359
- showToast('Cannot open file (might be binary)', 'error');
360
- }
361
- } catch {
362
- showToast('Failed to open file', 'error');
363
- }
364
  }
365
 
366
  function closeEditor() {
@@ -368,49 +525,29 @@ HTML_CONTENT = """
368
  const card = document.getElementById('editor-card');
369
  modal.classList.add('opacity-0');
370
  card.classList.add('scale-95');
371
- setTimeout(() => {
372
- modal.classList.add('hidden');
373
- modal.classList.remove('flex');
374
- }, 300);
375
  }
376
 
377
  async function saveFile() {
378
- const btn = document.querySelector('#editor-modal button.bg-blue-600');
379
- const originalHTML = btn.innerHTML;
380
- btn.innerHTML = `<i data-lucide="loader-2" class="w-3.5 h-3.5 loader"></i> Saving...`;
381
- lucide.createIcons();
382
-
383
  const content = document.getElementById('editor-content').value;
384
  const formData = new FormData();
385
  formData.append('path', editingFilePath);
386
  formData.append('content', content);
387
-
388
  try {
389
  const res = await fetch('/api/fs/write', { method: 'POST', body: formData });
390
- if(res.ok) {
391
- showToast('File saved successfully', 'success');
392
- closeEditor();
393
- } else throw new Error();
394
- } catch {
395
- showToast('Failed to save file', 'error');
396
- } finally {
397
- btn.innerHTML = originalHTML;
398
- lucide.createIcons();
399
- }
400
  }
401
 
402
  async function deleteFile(path) {
403
- if(confirm('Are you sure you want to delete ' + path.split('/').pop() + '?')) {
404
  const formData = new FormData(); formData.append('path', path);
405
  try {
406
  const res = await fetch('/api/fs/delete', { method: 'POST', body: formData });
407
- if(res.ok) {
408
- showToast('Deleted successfully', 'success');
409
- loadFiles(currentPath);
410
- } else throw new Error();
411
- } catch {
412
- showToast('Failed to delete', 'error');
413
- }
414
  }
415
  }
416
 
@@ -418,21 +555,16 @@ HTML_CONTENT = """
418
  const fileInput = e.target;
419
  if(!fileInput.files.length) return;
420
 
421
- showToast('Uploading ' + fileInput.files[0].name + '...', 'info');
422
-
423
  const formData = new FormData();
424
  formData.append('path', currentPath);
425
  formData.append('file', fileInput.files[0]);
426
 
427
  try {
428
  const res = await fetch('/api/fs/upload', { method: 'POST', body: formData });
429
- if(res.ok) {
430
- showToast('Upload complete', 'success');
431
- loadFiles(currentPath);
432
- } else throw new Error();
433
- } catch {
434
- showToast('Upload failed', 'error');
435
- }
436
  fileInput.value = '';
437
  }
438
  </script>
@@ -523,6 +655,37 @@ async def websocket_endpoint(websocket: WebSocket):
523
  except:
524
  connected_clients.remove(websocket)
525
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
  @app.get("/api/fs/list")
527
  def fs_list(path: str = ""):
528
  target = get_safe_path(path)
@@ -541,8 +704,7 @@ def fs_read(path: str):
541
  with open(target, 'r', encoding='utf-8') as f:
542
  return Response(content=f.read(), media_type="text/plain")
543
  except UnicodeDecodeError:
544
- # Prevent binary files (like .jar or .world) from crashing the API/Frontend
545
- raise HTTPException(400, "File is binary or unsupported encoding")
546
 
547
  @app.get("/api/fs/download")
548
  def fs_download(path: str):
 
1
  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
 
11
  app = FastAPI()
12
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
 
17
  BASE_DIR = os.path.abspath("/app")
18
 
19
  # -----------------
20
+ # HTML FRONTEND (Ultra-Modern Web3 SaaS UI)
21
  # -----------------
22
  HTML_CONTENT = """
23
  <!DOCTYPE html>
 
25
  <head>
26
  <meta charset="UTF-8">
27
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
28
+ <title>DeProxy / MineSpace Engine</title>
29
+
30
+ <!-- Fonts & Icons -->
31
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
32
+ <script src="https://unpkg.com/@phosphor-icons/web"></script>
33
+
34
+ <!-- Terminal -->
35
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css" />
36
  <script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
37
  <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit/lib/xterm-addon-fit.js"></script>
38
+
39
+ <!-- Tailwind CSS (Custom Web3 Config) -->
40
+ <script src="https://cdn.tailwindcss.com"></script>
41
+ <script>
42
+ tailwind.config = {
43
+ darkMode: 'class',
44
+ theme: {
45
+ extend: {
46
+ colors: {
47
+ dark: '#05050A',
48
+ panel: '#0F1017',
49
+ surface: '#181A24',
50
+ primary: '#9D4EDD',
51
+ secondary: '#FF79C6',
52
+ accent: '#8BE9FD',
53
+ border: '#2A2C3E'
54
+ },
55
+ fontFamily: {
56
+ sans: ['Outfit', 'sans-serif'],
57
+ mono: ['JetBrains Mono', 'monospace']
58
+ },
59
+ boxShadow: {
60
+ 'neon': '0 0 20px rgba(157, 78, 221, 0.15)',
61
+ 'neon-strong': '0 0 30px rgba(255, 121, 198, 0.3)',
62
+ }
63
+ }
64
+ }
65
+ }
66
+ </script>
67
+
68
  <style>
69
+ body { background-color: theme('colors.dark'); color: #e2e8f0; overflow: hidden; -webkit-font-smoothing: antialiased; }
 
 
 
 
 
70
 
71
+ /* Glassmorphism & Cards */
72
+ .glass-card {
73
+ background: rgba(15, 16, 23, 0.7);
74
+ backdrop-filter: blur(16px);
75
+ -webkit-backdrop-filter: blur(16px);
76
+ border: 1px solid theme('colors.border');
77
+ border-radius: 20px;
78
+ box-shadow: 0 10px 30px -10px rgba(0,0,0,0.5);
79
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
80
+ }
81
 
82
+ /* Gradients */
83
+ .text-gradient { background: linear-gradient(135deg, theme('colors.secondary'), theme('colors.primary')); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
84
+ .bg-gradient-btn { background: linear-gradient(135deg, theme('colors.primary'), theme('colors.secondary')); transition: opacity 0.3s ease; }
85
+ .bg-gradient-btn:hover { opacity: 0.9; box-shadow: theme('boxShadow.neon-strong'); }
86
+
87
+ /* Terminal Fixes - Crucial for Wrapping */
88
+ .term-container { min-width: 0; width: 100%; height: 100%; border-radius: 16px; overflow: hidden; position: relative; }
89
+ .term-wrapper { padding: 16px; height: 100%; width: 100%; }
90
+ .xterm .xterm-viewport { overflow-y: auto !important; width: 100% !important; }
91
+ .xterm-screen { width: 100% !important; }
92
 
93
  /* Custom Scrollbars */
94
+ ::-webkit-scrollbar { width: 4px; height: 4px; }
95
  ::-webkit-scrollbar-track { background: transparent; }
96
+ ::-webkit-scrollbar-thumb { background: theme('colors.border'); border-radius: 10px; }
97
+ ::-webkit-scrollbar-thumb:hover { background: theme('colors.primary'); }
98
 
99
+ /* SVG Circular Progress */
100
+ .progress-ring__circle { transition: stroke-dashoffset 0.5s ease-in-out; transform: rotate(-90deg); transform-origin: 50% 50%; }
 
101
 
102
+ /* Layout Transitions */
103
+ .fade-in { animation: fadeIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
104
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
105
  .hidden-tab { display: none !important; }
106
+
107
+ /* Pulse Animation */
108
+ @keyframes pulse-glow { 0%, 100% { opacity: 1; box-shadow: 0 0 10px #8BE9FD; } 50% { opacity: 0.5; box-shadow: 0 0 2px #8BE9FD; } }
109
+ .status-dot { width: 8px; height: 8px; background-color: theme('colors.accent'); border-radius: 50%; animation: pulse-glow 2s infinite; }
110
  </style>
111
  </head>
112
+ <body class="flex flex-col md:flex-row h-[100dvh] w-full selection:bg-primary/30 selection:text-white">
113
+
114
+ <!-- Mobile Top Header -->
115
+ <header class="md:hidden glass-card mx-4 mt-4 mb-2 p-4 flex justify-between items-center z-20 shrink-0 border-white/5 rounded-2xl relative overflow-hidden">
116
+ <div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-primary to-secondary opacity-50"></div>
117
+ <div class="flex items-center gap-2">
118
+ <i class="ph-fill ph-hexagon text-3xl text-secondary"></i>
119
+ <h1 class="font-bold text-lg tracking-wide text-white">Mine<span class="text-gradient">Space</span></h1>
120
+ </div>
121
+ <div class="flex items-center gap-2 px-3 py-1 bg-accent/10 border border-accent/20 rounded-full">
122
+ <div class="status-dot"></div>
123
+ <span class="text-xs font-semibold text-accent uppercase tracking-wider">Online</span>
124
+ </div>
125
+ </header>
126
 
127
+ <!-- Desktop Sidebar -->
128
+ <aside class="hidden md:flex flex-col w-64 glass-card m-4 mr-0 p-6 z-20 shrink-0 border-white/5 rounded-3xl relative overflow-hidden shadow-neon">
129
+ <div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-primary to-secondary"></div>
130
+
131
+ <div class="flex items-center gap-3 mb-12">
132
+ <i class="ph-fill ph-hexagon text-4xl text-secondary"></i>
133
+ <div>
134
+ <h1 class="font-bold text-xl tracking-wide text-white leading-tight">Mine<span class="text-gradient">Space</span></h1>
135
+ <p class="text-[10px] text-gray-500 font-mono uppercase tracking-widest">Engine Server</p>
136
  </div>
 
 
137
  </div>
138
+
139
+ <nav class="flex-grow flex flex-col gap-2">
140
+ <button onclick="switchTab('dashboard')" id="btn-desktop-dashboard" class="flex items-center gap-3 w-full px-4 py-3 rounded-xl bg-gradient-to-r from-primary/20 to-secondary/10 text-white border border-white/10 font-medium transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.1)]">
141
+ <i class="ph ph-squares-four text-xl text-secondary"></i> Dashboard
142
  </button>
143
+ <button onclick="switchTab('files')" id="btn-desktop-files" class="flex items-center gap-3 w-full px-4 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-surface border border-transparent transition-all">
144
+ <i class="ph ph-folder text-xl"></i> File Manager
145
  </button>
146
+ </nav>
147
+
148
+ <div class="mt-auto bg-surface/50 border border-border p-4 rounded-2xl flex items-center justify-between">
149
+ <div class="flex flex-col">
150
+ <span class="text-xs text-gray-400">Status</span>
151
+ <span class="text-sm font-semibold text-accent">Active Container</span>
152
+ </div>
153
+ <div class="status-dot"></div>
154
  </div>
155
+ </aside>
156
 
157
  <!-- Main Content Area -->
158
+ <main class="flex-grow flex flex-col p-4 overflow-hidden min-w-0">
159
 
160
+ <!-- DASHBOARD TAB -->
161
+ <div id="tab-dashboard" class="h-full flex flex-col gap-4 fade-in min-w-0">
162
+
163
+ <!-- Top Stats Row -->
164
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-4 shrink-0">
165
+ <!-- Data Usage (RAM) Card -->
166
+ <div class="glass-card p-4 md:p-5 flex items-center gap-4 col-span-1 md:col-span-2 relative overflow-hidden group">
167
+ <div class="absolute -right-10 -top-10 w-32 h-32 bg-primary/10 rounded-full blur-2xl group-hover:bg-primary/20 transition-all"></div>
168
+ <div class="relative w-16 h-16 shrink-0">
169
+ <svg class="w-full h-full" viewBox="0 0 100 100">
170
+ <circle class="text-surface stroke-current" stroke-width="8" cx="50" cy="50" r="40" fill="transparent"></circle>
171
+ <circle id="ram-ring" class="text-primary stroke-current progress-ring__circle" stroke-width="8" stroke-linecap="round" cx="50" cy="50" r="40" fill="transparent" stroke-dasharray="251.2" stroke-dashoffset="251.2"></circle>
172
+ </svg>
173
+ <div class="absolute inset-0 flex items-center justify-center"><i class="ph ph-memory text-primary text-xl"></i></div>
174
+ </div>
175
+ <div>
176
+ <p class="text-xs text-gray-400 uppercase tracking-wider font-semibold">Memory Usage</p>
177
+ <div class="flex items-baseline gap-1 mt-1">
178
+ <h2 class="text-2xl font-bold text-white font-mono" id="ram-text">0.0</h2>
179
+ <span class="text-sm text-gray-500 font-mono">/ 16 GB</span>
180
+ </div>
181
+ </div>
182
+ </div>
183
+
184
+ <!-- CPU Card -->
185
+ <div class="glass-card p-4 md:p-5 flex items-center gap-4 col-span-1 md:col-span-2 relative overflow-hidden group">
186
+ <div class="absolute -right-10 -top-10 w-32 h-32 bg-secondary/10 rounded-full blur-2xl group-hover:bg-secondary/20 transition-all"></div>
187
+ <div class="relative w-16 h-16 shrink-0">
188
+ <svg class="w-full h-full" viewBox="0 0 100 100">
189
+ <circle class="text-surface stroke-current" stroke-width="8" cx="50" cy="50" r="40" fill="transparent"></circle>
190
+ <circle id="cpu-ring" class="text-secondary stroke-current progress-ring__circle" stroke-width="8" stroke-linecap="round" cx="50" cy="50" r="40" fill="transparent" stroke-dasharray="251.2" stroke-dashoffset="251.2"></circle>
191
+ </svg>
192
+ <div class="absolute inset-0 flex items-center justify-center"><i class="ph ph-cpu text-secondary text-xl"></i></div>
193
+ </div>
194
+ <div>
195
+ <p class="text-xs text-gray-400 uppercase tracking-wider font-semibold">Processor</p>
196
+ <div class="flex items-baseline gap-1 mt-1">
197
+ <h2 class="text-2xl font-bold text-white font-mono" id="cpu-text">0%</h2>
198
+ <span class="text-sm text-gray-500 font-mono">of 2 Cores</span>
199
+ </div>
200
+ </div>
201
  </div>
 
202
  </div>
203
+
204
+ <!-- Terminal Area -->
205
+ <div class="glass-card flex-grow flex flex-col overflow-hidden shadow-neon relative border-white/10">
206
+ <!-- Terminal Header -->
207
+ <div class="bg-surface/80 px-4 py-3 flex justify-between items-center border-b border-border z-10">
208
+ <div class="flex items-center gap-2">
209
+ <i class="ph ph-terminal-window text-gray-400"></i>
210
+ <span class="text-sm font-semibold text-gray-200">Live Console</span>
211
+ </div>
212
+ <div class="flex gap-1.5">
213
+ <div class="w-3 h-3 rounded-full bg-red-500/80"></div>
214
+ <div class="w-3 h-3 rounded-full bg-yellow-500/80"></div>
215
+ <div class="w-3 h-3 rounded-full bg-green-500/80"></div>
216
+ </div>
217
+ </div>
218
+
219
+ <!-- Actual Terminal -->
220
+ <div class="term-container bg-[#08080C] flex-grow">
221
+ <div id="terminal" class="term-wrapper"></div>
222
+ </div>
223
+
224
+ <!-- Input Box -->
225
+ <div class="p-3 bg-surface/50 border-t border-border z-10 flex gap-2">
226
+ <div class="relative flex-grow">
227
+ <i class="ph ph-caret-right absolute left-3 top-1/2 -translate-y-1/2 text-primary text-lg"></i>
228
+ <input type="text" id="cmd-input" class="w-full bg-[#0B0C10] border border-border focus:border-primary text-gray-200 rounded-xl pl-9 pr-4 py-2.5 text-sm font-mono transition-all outline-none" placeholder="Enter server command...">
229
+ </div>
230
+ <button onclick="sendCommand()" class="bg-gradient-btn px-4 rounded-xl text-white shadow-lg flex items-center justify-center shrink-0">
231
+ <i class="ph ph-paper-plane-right text-lg"></i>
232
+ </button>
233
  </div>
 
 
 
 
234
  </div>
235
  </div>
236
 
237
+ <!-- FILES TAB -->
238
+ <div id="tab-files" class="hidden-tab h-full flex flex-col glass-card border-white/10 overflow-hidden shadow-neon">
239
+
240
+ <!-- File Header / Actions -->
241
+ <div class="bg-surface/80 p-4 border-b border-border flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
242
+ <div class="flex items-center gap-2 text-sm font-mono text-gray-400 overflow-x-auto whitespace-nowrap w-full sm:w-auto" id="breadcrumbs">
243
  <!-- Injected via JS -->
244
  </div>
245
+
246
+ <div class="flex items-center gap-2 shrink-0 self-end sm:self-auto">
247
  <input type="file" id="file-upload" class="hidden" onchange="uploadFile(event)">
248
+ <button onclick="document.getElementById('file-upload').click()" class="bg-gradient-btn flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-semibold text-white transition-all shadow-lg">
249
+ <i class="ph ph-upload-simple text-base"></i> Upload
250
  </button>
251
+ <button onclick="loadFiles(currentPath)" class="bg-surface hover:bg-border border border-border px-3 py-2 rounded-xl text-gray-300 transition-colors">
252
+ <i class="ph ph-arrows-clockwise text-base"></i>
253
  </button>
254
  </div>
255
  </div>
256
 
257
+ <!-- List Headers -->
258
+ <div class="hidden sm:grid grid-cols-12 gap-4 px-6 py-3 bg-[#0B0C10]/50 border-b border-border text-xs font-bold text-gray-500 uppercase tracking-wider">
259
+ <div class="col-span-7">File Name</div>
260
  <div class="col-span-3 text-right">Size</div>
261
  <div class="col-span-2 text-right">Actions</div>
262
  </div>
263
 
264
+ <!-- File List Items -->
265
+ <div class="flex-grow overflow-y-auto bg-[#08080C] p-2" id="file-list">
266
  <!-- Injected via JS -->
267
  </div>
268
  </div>
269
+
270
  </main>
271
 
272
+ <!-- Mobile Bottom Navigation -->
273
+ <nav class="md:hidden glass-card mx-4 mb-4 mt-0 p-2 flex justify-around items-center z-20 shrink-0 border-white/5 rounded-2xl">
274
+ <button onclick="switchTab('dashboard')" id="btn-mobile-dashboard" class="flex flex-col items-center gap-1 p-2 w-16 rounded-xl text-primary transition-all">
275
+ <i class="ph-fill ph-squares-four text-2xl"></i>
276
+ <span class="text-[10px] font-semibold">Panel</span>
277
+ </button>
278
+ <button onclick="switchTab('files')" id="btn-mobile-files" class="flex flex-col items-center gap-1 p-2 w-16 rounded-xl text-gray-500 transition-all">
279
+ <i class="ph-fill ph-folder text-2xl"></i>
280
+ <span class="text-[10px] font-semibold">Files</span>
281
+ </button>
282
+ </nav>
283
+
284
  <!-- Code Editor Modal -->
285
+ <div id="editor-modal" class="fixed inset-0 bg-black/80 backdrop-blur-md hidden items-center justify-center p-4 z-50 opacity-0 transition-opacity duration-300">
286
+ <div class="glass-card border-white/10 w-full max-w-4xl h-[85vh] flex flex-col shadow-[0_0_50px_rgba(0,0,0,0.8)] transform scale-95 transition-transform duration-300" id="editor-card">
287
+ <div class="bg-surface/80 px-4 py-3 flex justify-between items-center border-b border-border">
288
  <div class="flex items-center gap-2 text-sm font-mono text-gray-300">
289
+ <i class="ph ph-file-code text-secondary text-lg"></i>
290
  <span id="editor-title">filename.txt</span>
291
  </div>
292
  <div class="flex items-center gap-2">
293
+ <button onclick="closeEditor()" class="px-3 py-1.5 hover:bg-border rounded-lg text-xs font-medium text-gray-400 transition-colors">Cancel</button>
294
+ <button onclick="saveFile()" class="bg-gradient-btn px-4 py-1.5 text-white rounded-lg text-xs font-bold transition-all shadow-neon flex items-center gap-1.5">
295
+ <i class="ph ph-floppy-disk"></i> Save
296
  </button>
297
  </div>
298
  </div>
299
+ <textarea id="editor-content" class="flex-grow bg-[#05050A] text-gray-300 p-4 font-mono text-xs sm:text-sm resize-none focus:outline-none w-full leading-relaxed" spellcheck="false"></textarea>
300
  </div>
301
  </div>
302
 
303
+ <!-- Modern Toast Notifications -->
304
+ <div id="toast-container" class="fixed top-4 right-4 z-[100] flex flex-col gap-2 pointer-events-none"></div>
305
 
306
  <script>
 
 
 
307
  // --- Toast System ---
308
  function showToast(message, type = 'info') {
309
  const container = document.getElementById('toast-container');
310
  const toast = document.createElement('div');
311
 
312
+ let icon = '<i class="ph-fill ph-info text-blue-400 text-lg"></i>';
313
+ if(type === 'success') icon = '<i class="ph-fill ph-check-circle text-green-400 text-lg"></i>';
314
+ if(type === 'error') icon = '<i class="ph-fill ph-warning-circle text-red-400 text-lg"></i>';
 
315
 
316
+ toast.className = `flex items-center gap-3 bg-surface border border-border text-sm text-white px-4 py-3 rounded-xl shadow-2xl translate-x-10 opacity-0 transition-all duration-300`;
317
+ toast.innerHTML = `${icon} <span class="font-medium">${message}</span>`;
318
 
319
  container.appendChild(toast);
 
320
 
321
+ requestAnimationFrame(() => toast.classList.remove('translate-x-10', 'opacity-0'));
 
 
 
 
 
322
  setTimeout(() => {
323
+ toast.classList.add('translate-x-10', 'opacity-0');
324
  setTimeout(() => toast.remove(), 300);
325
  }, 3000);
326
  }
327
 
328
+ // --- Navigation Logic ---
329
  function switchTab(tab) {
330
+ // Hide all
331
+ document.getElementById('tab-dashboard').classList.add('hidden-tab');
332
  document.getElementById('tab-files').classList.add('hidden-tab');
333
 
334
+ // Reset Desktop Nav
335
+ document.getElementById('btn-desktop-dashboard').className = "flex items-center gap-3 w-full px-4 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-surface border border-transparent transition-all";
336
+ document.getElementById('btn-desktop-files').className = "flex items-center gap-3 w-full px-4 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-surface border border-transparent transition-all";
337
 
338
+ // Reset Mobile Nav
339
+ document.getElementById('btn-mobile-dashboard').className = "flex flex-col items-center gap-1 p-2 w-16 rounded-xl text-gray-500 transition-all";
340
+ document.getElementById('btn-mobile-files').className = "flex flex-col items-center gap-1 p-2 w-16 rounded-xl text-gray-500 transition-all";
341
+
342
+ // Activate Tab
343
  document.getElementById('tab-' + tab).classList.remove('hidden-tab');
344
  document.getElementById('tab-' + tab).classList.add('fade-in');
345
 
346
+ // Activate Buttons
347
+ document.getElementById('btn-desktop-' + tab).className = "flex items-center gap-3 w-full px-4 py-3 rounded-xl bg-gradient-to-r from-primary/20 to-secondary/10 text-white border border-white/10 font-medium transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.1)]";
348
+ document.getElementById('btn-mobile-' + tab).className = "flex flex-col items-center gap-1 p-2 w-16 rounded-xl text-primary transition-all";
349
 
350
+ // Fit terminal if dashboard is opened
351
+ if(tab === 'dashboard' && fitAddon) { setTimeout(() => fitAddon.fit(), 100); }
352
  if(tab === 'files' && !currentPathLoaded) { loadFiles(''); currentPathLoaded = true; }
353
  }
354
 
355
+ // --- Terminal Logic (Wrapped heavily) ---
356
  const term = new Terminal({
357
+ theme: { background: 'transparent', foreground: '#f8f8f2', cursor: '#9D4EDD', selectionBackground: 'rgba(157, 78, 221, 0.4)' },
358
+ convertEol: true, cursorBlink: true, fontFamily: "'JetBrains Mono', monospace", fontSize: 13, fontWeight: 400,
359
+ disableStdin: false
360
  });
361
  const fitAddon = new FitAddon.FitAddon();
362
  term.loadAddon(fitAddon);
363
  term.open(document.getElementById('terminal'));
364
 
365
+ // Ensure resizing works properly
366
+ const resizeObserver = new ResizeObserver(() => {
367
+ if(!document.getElementById('tab-dashboard').classList.contains('hidden-tab')) {
368
+ requestAnimationFrame(() => fitAddon.fit());
369
+ }
370
+ });
371
+ resizeObserver.observe(document.querySelector('.term-container'));
372
+ setTimeout(() => fitAddon.fit(), 200);
373
 
374
  const wsUrl = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws';
375
  let ws;
376
 
377
  function connectWS() {
378
  ws = new WebSocket(wsUrl);
379
+ ws.onopen = () => term.write('\\x1b[35m\\x1b[1m[System]\\x1b[0m Connected to secure datastream.\\r\\n');
380
  ws.onmessage = e => term.write(e.data + '\\n');
381
+ ws.onclose = () => { term.write('\\r\\n\\x1b[31m\\x1b[1m[System]\\x1b[0m Link severed. Reconnecting...\\r\\n'); setTimeout(connectWS, 3000); };
382
  }
383
  connectWS();
384
 
 
394
  }
395
  }
396
 
397
+ // --- System Stats Polling ---
398
+ function setProgress(id, percent) {
399
+ const circle = document.getElementById(id);
400
+ const radius = circle.r.baseVal.value;
401
+ const circumference = radius * 2 * Math.PI;
402
+ const offset = circumference - (percent / 100) * circumference;
403
+ circle.style.strokeDasharray = `${circumference} ${circumference}`;
404
+ circle.style.strokeDashoffset = offset;
405
+ }
406
+
407
+ async function fetchStats() {
408
+ try {
409
+ const res = await fetch('/api/stats');
410
+ const data = await res.json();
411
+
412
+ // Update RAM
413
+ const ramPercent = Math.min(100, (data.ram_used_mb / 1024 / 16) * 100);
414
+ document.getElementById('ram-text').innerText = (data.ram_used_mb / 1024).toFixed(1);
415
+ setProgress('ram-ring', ramPercent);
416
+
417
+ // Update CPU (data.cpu_percent is 0 to 100 relative to 2 cores)
418
+ document.getElementById('cpu-text').innerText = data.cpu_percent.toFixed(0) + '%';
419
+ setProgress('cpu-ring', data.cpu_percent);
420
+
421
+ } catch (e) { console.error('Stats error:', e); }
422
+ }
423
+ setInterval(fetchStats, 2000);
424
+ fetchStats();
425
+
426
  // --- File Manager Logic ---
427
  let currentPath = '';
428
  let currentPathLoaded = false;
 
430
 
431
  function renderBreadcrumbs(path) {
432
  const parts = path.split('/').filter(p => p);
433
+ let html = `<button onclick="loadFiles('')" class="hover:text-white transition-colors"><i class="ph-fill ph-house text-lg"></i></button>`;
434
  let buildPath = '';
435
 
436
  if (parts.length > 0) {
437
  parts.forEach((part, index) => {
438
  buildPath += (buildPath ? '/' : '') + part;
439
+ html += ` <i class="ph ph-caret-right text-xs mx-2 opacity-50"></i> `;
440
  if(index === parts.length - 1) {
441
+ html += `<span class="text-secondary font-semibold">${part}</span>`;
442
  } else {
443
+ html += `<button onclick="loadFiles('${buildPath}')" class="hover:text-white transition-colors">${part}</button>`;
444
  }
445
  });
446
  }
447
  document.getElementById('breadcrumbs').innerHTML = html;
 
448
  }
449
 
450
  async function loadFiles(path) {
451
  currentPath = path;
452
  renderBreadcrumbs(path);
453
  const list = document.getElementById('file-list');
454
+ list.innerHTML = `<div class="flex justify-center py-10"><i class="ph ph-spinner-gap animate-spin text-3xl text-primary"></i></div>`;
 
455
 
456
  try {
457
  const res = await fetch(`/api/fs/list?path=${encodeURIComponent(path)}`);
 
462
  if (path !== '') {
463
  const parent = path.split('/').slice(0, -1).join('/');
464
  list.innerHTML += `
465
+ <div class="flex items-center px-4 py-3 cursor-pointer hover:bg-surface/80 rounded-xl transition-all mb-1 border border-transparent hover:border-border" onclick="loadFiles('${parent}')">
466
+ <i class="ph ph-arrow-u-up-left text-gray-500 mr-3 text-lg"></i>
467
+ <span class="text-sm font-mono text-gray-400">Return to parent directory</span>
 
 
468
  </div>`;
469
  }
470
 
471
  if(files.length === 0 && path === '') {
472
+ list.innerHTML += `<div class="text-center py-12 text-gray-600 text-sm">Space is empty</div>`;
473
  }
474
 
475
  files.forEach(f => {
476
+ const icon = f.is_dir ? '<div class="p-2 bg-blue-500/10 rounded-lg text-blue-400"><i class="ph-fill ph-folder text-xl"></i></div>' : '<div class="p-2 bg-surface border border-border rounded-lg text-gray-400"><i class="ph-fill ph-file text-xl"></i></div>';
477
  const sizeStr = f.is_dir ? '--' : (f.size > 1024*1024 ? (f.size/(1024*1024)).toFixed(1) + ' MB' : (f.size / 1024).toFixed(1) + ' KB');
478
  const fullPath = path ? `${path}/${f.name}` : f.name;
479
  const actionClick = f.is_dir ? `onclick="loadFiles('${fullPath}')"` : '';
480
  const pointer = f.is_dir ? 'cursor-pointer' : '';
481
 
482
  list.innerHTML += `
483
+ <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-surface/50 rounded-xl transition-all mb-1 border border-transparent hover:border-border">
484
  <div class="col-span-7 flex items-center gap-3 w-full ${pointer}" ${actionClick}>
485
  ${icon}
486
+ <span class="text-sm font-mono text-gray-300 truncate group-hover:text-primary transition-colors">${f.name}</span>
487
  </div>
488
+ <div class="col-span-3 text-right text-xs text-gray-500 font-mono hidden sm:block">${sizeStr}</div>
489
+ <div class="col-span-2 flex justify-end gap-2 w-full sm:w-auto mt-2 sm:mt-0 sm:opacity-0 group-hover:opacity-100 transition-opacity">
490
+ ${!f.is_dir ? `<button onclick="editFile('${fullPath}')" class="p-2 bg-surface border border-border text-gray-400 hover:text-accent hover:border-accent/50 rounded-lg transition-colors" title="Edit"><i class="ph ph-pencil-simple text-sm"></i></button>` : ''}
491
+ ${!f.is_dir ? `<a href="/api/fs/download?path=${encodeURIComponent(fullPath)}" class="p-2 bg-surface border border-border text-gray-400 hover:text-green-400 hover:border-green-400/50 rounded-lg transition-colors inline-block" title="Download"><i class="ph ph-download-simple text-sm"></i></a>` : ''}
492
+ <button onclick="deleteFile('${fullPath}')" class="p-2 bg-surface border border-border text-gray-400 hover:text-red-400 hover:border-red-400/50 rounded-lg transition-colors" title="Delete"><i class="ph ph-trash text-sm"></i></button>
493
  </div>
494
  </div>`;
495
  });
 
496
  } catch (err) {
497
  showToast("Failed to load directory", "error");
 
498
  }
499
  }
500
 
 
512
  modal.classList.remove('hidden');
513
  modal.classList.add('flex');
514
 
 
515
  requestAnimationFrame(() => {
516
  modal.classList.remove('opacity-0');
517
  card.classList.remove('scale-95');
518
  });
519
+ } else { showToast('Cannot open file (might be binary)', 'error'); }
520
+ } catch { showToast('Failed to open file', 'error'); }
 
 
 
 
521
  }
522
 
523
  function closeEditor() {
 
525
  const card = document.getElementById('editor-card');
526
  modal.classList.add('opacity-0');
527
  card.classList.add('scale-95');
528
+ setTimeout(() => { modal.classList.add('hidden'); modal.classList.remove('flex'); }, 300);
 
 
 
529
  }
530
 
531
  async function saveFile() {
 
 
 
 
 
532
  const content = document.getElementById('editor-content').value;
533
  const formData = new FormData();
534
  formData.append('path', editingFilePath);
535
  formData.append('content', content);
 
536
  try {
537
  const res = await fetch('/api/fs/write', { method: 'POST', body: formData });
538
+ if(res.ok) { showToast('File saved securely', 'success'); closeEditor(); }
539
+ else throw new Error();
540
+ } catch { showToast('Failed to save file', 'error'); }
 
 
 
 
 
 
 
541
  }
542
 
543
  async function deleteFile(path) {
544
+ if(confirm('Delete ' + path.split('/').pop() + ' permanently?')) {
545
  const formData = new FormData(); formData.append('path', path);
546
  try {
547
  const res = await fetch('/api/fs/delete', { method: 'POST', body: formData });
548
+ if(res.ok) { showToast('Data purged', 'success'); loadFiles(currentPath); }
549
+ else throw new Error();
550
+ } catch { showToast('Failed to delete', 'error'); }
 
 
 
 
551
  }
552
  }
553
 
 
555
  const fileInput = e.target;
556
  if(!fileInput.files.length) return;
557
 
558
+ showToast('Encrypting & Uploading...', 'info');
 
559
  const formData = new FormData();
560
  formData.append('path', currentPath);
561
  formData.append('file', fileInput.files[0]);
562
 
563
  try {
564
  const res = await fetch('/api/fs/upload', { method: 'POST', body: formData });
565
+ if(res.ok) { showToast('Upload complete', 'success'); loadFiles(currentPath); }
566
+ else throw new Error();
567
+ } catch { showToast('Upload failed', 'error'); }
 
 
 
 
568
  fileInput.value = '';
569
  }
570
  </script>
 
655
  except:
656
  connected_clients.remove(websocket)
657
 
658
+ @app.get("/api/stats")
659
+ def get_stats():
660
+ """
661
+ Calculates CPU & RAM only for the Python Panel and its Minecraft Child Process.
662
+ Limits visual CPU percentage to 2 Cores (HuggingFace free limit).
663
+ """
664
+ try:
665
+ current_process = psutil.Process(os.getpid())
666
+ mem_usage = current_process.memory_info().rss
667
+ cpu_percent = current_process.cpu_percent()
668
+
669
+ # Add all children (The Java Minecraft Process)
670
+ for child in current_process.children(recursive=True):
671
+ try:
672
+ mem_usage += child.memory_info().rss
673
+ cpu_percent += child.cpu_percent()
674
+ except psutil.NoSuchProcess:
675
+ pass
676
+
677
+ # CPU returned by psutil can be 200% for 2 cores.
678
+ # We divide by 2 to get a standard 0-100% representation for 2 cores.
679
+ normalized_cpu = min(100.0, cpu_percent / 2.0)
680
+
681
+ return {
682
+ "ram_used_mb": mem_usage / (1024 * 1024),
683
+ "cpu_percent": normalized_cpu
684
+ }
685
+ except Exception:
686
+ # Fallback if psutil fails entirely to not crash UI
687
+ return {"ram_used_mb": 0, "cpu_percent": 0}
688
+
689
  @app.get("/api/fs/list")
690
  def fs_list(path: str = ""):
691
  target = get_safe_path(path)
 
704
  with open(target, 'r', encoding='utf-8') as f:
705
  return Response(content=f.read(), media_type="text/plain")
706
  except UnicodeDecodeError:
707
+ raise HTTPException(400, "File is binary")
 
708
 
709
  @app.get("/api/fs/download")
710
  def fs_download(path: str):