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

Update panel.py

Browse files
Files changed (1) hide show
  1. panel.py +925 -558
panel.py CHANGED
@@ -3,6 +3,7 @@ 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
@@ -12,562 +13,897 @@ app = FastAPI()
12
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
13
 
14
  mc_process = None
15
- output_history = collections.deque(maxlen=300)
16
  connected_clients = set()
17
  BASE_DIR = os.path.abspath("/app")
 
 
 
18
 
19
- # -----------------
20
- # HTML FRONTEND (Ultra-Modern Web3 SaaS UI)
21
- # -----------------
22
  HTML_CONTENT = """
23
  <!DOCTYPE html>
24
- <html lang="en" class="dark">
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
-
385
- const cmdInput = document.getElementById('cmd-input');
386
- cmdInput.addEventListener('keypress', e => { if (e.key === 'Enter') sendCommand(); });
387
-
388
- function sendCommand() {
389
- const val = cmdInput.value.trim();
390
- if(val && ws && ws.readyState === WebSocket.OPEN) {
391
- term.write(`\\x1b[90m> ${val}\\x1b[0m\\r\\n`);
392
- ws.send(val);
393
- cmdInput.value = '';
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;
429
- let editingFilePath = '';
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)}`);
458
- if(!res.ok) throw new Error('Failed to load');
459
- const files = await res.json();
460
- list.innerHTML = '';
461
-
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
 
501
- async function editFile(path) {
502
- try {
503
- const res = await fetch(`/api/fs/read?path=${encodeURIComponent(path)}`);
504
- if(res.ok) {
505
- const text = await res.text();
506
- editingFilePath = path;
507
- document.getElementById('editor-content').value = text;
508
- document.getElementById('editor-title').innerText = path.split('/').pop();
509
-
510
- const modal = document.getElementById('editor-modal');
511
- const card = document.getElementById('editor-card');
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() {
524
- const modal = document.getElementById('editor-modal');
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
 
554
- async function uploadFile(e) {
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>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
  </body>
572
  </html>
573
  """
@@ -599,7 +935,8 @@ async def read_stream(stream, prefix=""):
599
  while True:
600
  try:
601
  line = await stream.readline()
602
- if not line: break
 
603
  line_str = line.decode('utf-8', errors='replace').rstrip('\r\n')
604
  await broadcast(prefix + line_str)
605
  except Exception:
@@ -653,68 +990,91 @@ async def websocket_endpoint(websocket: WebSocket):
653
  mc_process.stdin.write((cmd + "\n").encode('utf-8'))
654
  await mc_process.stdin.drain()
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)
692
- if not os.path.exists(target): return []
 
693
  items = []
694
- for f in os.listdir(target):
695
- fp = os.path.join(target, f)
696
- items.append({"name": f, "is_dir": os.path.isdir(fp), "size": os.path.getsize(fp) if not os.path.isdir(fp) else 0})
 
 
 
 
 
 
 
 
697
  return sorted(items, key=lambda x: (not x["is_dir"], x["name"].lower()))
698
 
699
  @app.get("/api/fs/read")
700
  def fs_read(path: str):
701
  target = get_safe_path(path)
702
- if not os.path.isfile(target): raise HTTPException(400, "Not a file")
 
703
  try:
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):
711
  target = get_safe_path(path)
712
- if not os.path.isfile(target): raise HTTPException(400, "Not a file")
 
713
  return FileResponse(target, filename=os.path.basename(target))
714
 
715
  @app.post("/api/fs/write")
716
  def fs_write(path: str = Form(...), content: str = Form(...)):
717
  target = get_safe_path(path)
 
718
  with open(target, 'w', encoding='utf-8') as f:
719
  f.write(content)
720
  return {"status": "ok"}
@@ -722,7 +1082,10 @@ def fs_write(path: str = Form(...), content: str = Form(...)):
722
  @app.post("/api/fs/upload")
723
  async def fs_upload(path: str = Form(""), file: UploadFile = File(...)):
724
  target_dir = get_safe_path(path)
 
725
  target_file = os.path.join(target_dir, file.filename)
 
 
726
  with open(target_file, "wb") as buffer:
727
  shutil.copyfileobj(file.file, buffer)
728
  return {"status": "ok"}
@@ -730,8 +1093,12 @@ async def fs_upload(path: str = Form(""), file: UploadFile = File(...)):
730
  @app.post("/api/fs/delete")
731
  def fs_delete(path: str = Form(...)):
732
  target = get_safe_path(path)
733
- if os.path.isdir(target): shutil.rmtree(target)
734
- else: os.remove(target)
 
 
 
 
735
  return {"status": "ok"}
736
 
737
  if __name__ == "__main__":
 
3
  import collections
4
  import shutil
5
  import psutil
6
+ import time
7
  from fastapi import FastAPI, WebSocket, Request, Response, Form, UploadFile, File, HTTPException
8
  from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
9
  from fastapi.middleware.cors import CORSMiddleware
 
13
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
14
 
15
  mc_process = None
16
+ output_history = collections.deque(maxlen=500)
17
  connected_clients = set()
18
  BASE_DIR = os.path.abspath("/app")
19
+ CONTAINER_CPU_CORES = 2
20
+ CONTAINER_RAM_MB = 16384
21
+ CONTAINER_STORAGE_GB = 50
22
 
 
 
 
23
  HTML_CONTENT = """
24
  <!DOCTYPE html>
25
+ <html lang="en">
26
  <head>
27
+ <meta charset="UTF-8">
28
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scale=no">
29
+ <title>MC Server Panel</title>
30
+ <style>
31
+ *,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
32
+ :root{
33
+ --bg-primary:#0a0a0f;--bg-secondary:#12121a;--bg-tertiary:#1a1a2e;--bg-card:#16162a;
34
+ --bg-hover:#1e1e3a;--bg-active:#252545;--border-primary:#2a2a4a;--border-glow:#7c3aed40;
35
+ --text-primary:#f0f0ff;--text-secondary:#a0a0c0;--text-muted:#606080;
36
+ --accent:#7c3aed;--accent-hover:#9333ea;--accent-glow:#7c3aed30;
37
+ --success:#10b981;--warning:#f59e0b;--danger:#ef4444;--info:#3b82f6;
38
+ --radius:12px;--radius-sm:8px;--radius-lg:16px;
39
+ --shadow:0 4px 24px rgba(0,0,0,0.4);--shadow-glow:0 0 30px var(--accent-glow);
40
+ --transition:all 0.2s cubic-bezier(0.4,0,0.2,1);
41
+ --font:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',sans-serif;
42
+ --font-mono:'SF Mono','Cascadia Code','Fira Code',Consolas,monospace;
43
+ }
44
+ html{font-family:var(--font);background:var(--bg-primary);color:var(--text-primary);
45
+ -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;overflow:hidden;height:100%}
46
+ body{height:100%;overflow:hidden}
47
+ ::-webkit-scrollbar{width:6px;height:6px}
48
+ ::-webkit-scrollbar-track{background:transparent}
49
+ ::-webkit-scrollbar-thumb{background:var(--border-primary);border-radius:3px}
50
+ ::-webkit-scrollbar-thumb:hover{background:var(--accent)}
51
+
52
+ .app{display:flex;flex-direction:column;height:100vh;overflow:hidden}
53
+
54
+ /* Top Bar */
55
+ .topbar{display:flex;align-items:center;justify-content:space-between;padding:0 16px;
56
+ height:52px;min-height:52px;background:var(--bg-secondary);
57
+ border-bottom:1px solid var(--border-primary);z-index:100;gap:12px;
58
+ backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px)}
59
+ .topbar-brand{display:flex;align-items:center;gap:10px;font-weight:700;font-size:15px;white-space:nowrap}
60
+ .topbar-brand svg{color:var(--accent)}
61
+ .topbar-status{display:flex;align-items:center;gap:6px;padding:4px 12px;
62
+ border-radius:20px;font-size:11px;font-weight:600;letter-spacing:0.5px;text-transform:uppercase}
63
+ .status-online{background:#10b98118;color:#10b981;border:1px solid #10b98130}
64
+ .status-offline{background:#ef444418;color:#ef4444;border:1px solid #ef444430}
65
+ .topbar-stats{display:flex;align-items:center;gap:4px}
66
+ .stat-chip{display:flex;align-items:center;gap:6px;padding:5px 10px;
67
+ background:var(--bg-tertiary);border:1px solid var(--border-primary);
68
+ border-radius:var(--radius-sm);font-size:11px;font-weight:500;white-space:nowrap}
69
+ .stat-chip .stat-val{font-weight:700;color:var(--text-primary);font-variant-numeric:tabular-nums}
70
+ .stat-chip .stat-lbl{color:var(--text-muted);font-size:10px}
71
+ .stat-bar-wrap{width:40px;height:4px;background:var(--bg-primary);border-radius:2px;overflow:hidden}
72
+ .stat-bar-fill{height:100%;border-radius:2px;transition:width 0.6s ease}
73
+ .hamburger{display:none;background:none;border:none;color:var(--text-primary);cursor:pointer;padding:4px}
74
+
75
+ /* Navigation Tabs */
76
+ .nav-tabs{display:flex;align-items:center;gap:2px;padding:0 16px;
77
+ height:42px;min-height:42px;background:var(--bg-secondary);
78
+ border-bottom:1px solid var(--border-primary);overflow-x:auto}
79
+ .nav-tab{display:flex;align-items:center;gap:6px;padding:8px 14px;
80
+ font-size:12px;font-weight:500;color:var(--text-secondary);
81
+ border-radius:var(--radius-sm) var(--radius-sm) 0 0;cursor:pointer;
82
+ transition:var(--transition);white-space:nowrap;border:none;background:none;
83
+ border-bottom:2px solid transparent;position:relative}
84
+ .nav-tab:hover{color:var(--text-primary);background:var(--bg-hover)}
85
+ .nav-tab.active{color:var(--accent);background:var(--bg-tertiary);border-bottom-color:var(--accent)}
86
+ .nav-tab svg{width:14px;height:14px;flex-shrink:0}
87
+
88
+ /* Main Content */
89
+ .main-content{flex:1;overflow:hidden;position:relative}
90
+ .panel{display:none;height:100%;flex-direction:column;overflow:hidden}
91
+ .panel.active{display:flex}
92
+
93
+ /* Console */
94
+ .console-wrap{flex:1;display:flex;flex-direction:column;overflow:hidden}
95
+ .console-output{flex:1;overflow-y:auto;padding:12px 16px;font-family:var(--font-mono);
96
+ font-size:12.5px;line-height:1.7;background:var(--bg-primary);scroll-behavior:smooth;
97
+ -webkit-overflow-scrolling:touch}
98
+ .console-line{padding:1px 0;word-break:break-all;animation:fadeIn 0.15s ease}
99
+ .console-line:hover{background:var(--bg-hover);border-radius:2px}
100
+ @keyframes fadeIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}
101
+ .console-line .timestamp{color:var(--text-muted);margin-right:8px;font-size:11px;user-select:none}
102
+ .log-info{color:#60a5fa}.log-warn{color:#fbbf24}.log-error{color:#f87171}
103
+ .log-server{color:#a78bfa}.log-chat{color:#34d399}.log-default{color:var(--text-secondary)}
104
+ .console-input-wrap{display:flex;gap:8px;padding:10px 16px;background:var(--bg-secondary);
105
+ border-top:1px solid var(--border-primary);align-items:center}
106
+ .console-prefix{color:var(--accent);font-family:var(--font-mono);font-size:13px;
107
+ font-weight:700;user-select:none;flex-shrink:0}
108
+ .console-input{flex:1;background:var(--bg-tertiary);border:1px solid var(--border-primary);
109
+ color:var(--text-primary);font-family:var(--font-mono);font-size:12.5px;padding:8px 12px;
110
+ border-radius:var(--radius-sm);outline:none;transition:var(--transition)}
111
+ .console-input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-glow)}
112
+ .console-input::placeholder{color:var(--text-muted)}
113
+ .btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:7px 14px;
114
+ font-size:12px;font-weight:600;border:1px solid var(--border-primary);border-radius:var(--radius-sm);
115
+ cursor:pointer;transition:var(--transition);background:var(--bg-tertiary);color:var(--text-primary);
116
+ white-space:nowrap;font-family:var(--font)}
117
+ .btn:hover{background:var(--bg-hover);border-color:var(--accent)}
118
+ .btn:active{transform:scale(0.97)}
119
+ .btn-accent{background:var(--accent);border-color:var(--accent);color:#fff}
120
+ .btn-accent:hover{background:var(--accent-hover);border-color:var(--accent-hover)}
121
+ .btn-danger{background:var(--danger);border-color:var(--danger);color:#fff}
122
+ .btn-danger:hover{background:#dc2626;border-color:#dc2626}
123
+ .btn-sm{padding:5px 10px;font-size:11px}
124
+ .btn-icon{padding:6px;width:32px;height:32px}
125
+ .btn svg{width:14px;height:14px;flex-shrink:0}
126
+ .console-toolbar{display:flex;align-items:center;gap:6px;padding:6px 16px;
127
+ background:var(--bg-secondary);border-bottom:1px solid var(--border-primary);
128
+ overflow-x:auto;flex-wrap:nowrap}
129
+ .console-toolbar .btn{flex-shrink:0}
130
+ .quick-cmds{display:flex;gap:4px;flex-wrap:nowrap;overflow-x:auto;flex:1}
131
+ .quick-cmds .btn{font-size:10px;padding:4px 8px;background:var(--bg-card);color:var(--text-secondary)}
132
+ .quick-cmds .btn:hover{color:var(--text-primary);background:var(--accent);border-color:var(--accent)}
133
+
134
+ /* File Manager */
135
+ .fm-container{display:flex;flex-direction:column;height:100%;overflow:hidden}
136
+ .fm-toolbar{display:flex;align-items:center;gap:8px;padding:10px 16px;
137
+ background:var(--bg-secondary);border-bottom:1px solid var(--border-primary);flex-wrap:wrap}
138
+ .fm-path{display:flex;align-items:center;gap:2px;flex:1;min-width:200px;overflow-x:auto;flex-wrap:nowrap}
139
+ .fm-crumb{padding:4px 8px;font-size:11px;color:var(--text-secondary);cursor:pointer;
140
+ border-radius:var(--radius-sm);transition:var(--transition);white-space:nowrap;
141
+ background:none;border:none;font-family:var(--font)}
142
+ .fm-crumb:hover{background:var(--bg-hover);color:var(--text-primary)}
143
+ .fm-crumb.active{color:var(--accent);font-weight:600}
144
+ .fm-crumb-sep{color:var(--text-muted);font-size:10px;user-select:none}
145
+ .fm-body{flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch}
146
+ .fm-list{width:100%}
147
+ .fm-item{display:flex;align-items:center;gap:10px;padding:9px 16px;cursor:pointer;
148
+ transition:var(--transition);border-bottom:1px solid var(--border-primary)}
149
+ .fm-item:hover{background:var(--bg-hover)}
150
+ .fm-item.selected{background:var(--accent-glow);border-left:3px solid var(--accent)}
151
+ .fm-icon{width:18px;height:18px;flex-shrink:0;display:flex;align-items:center;justify-content:center}
152
+ .fm-icon.folder{color:#fbbf24}.fm-icon.file{color:var(--text-muted)}
153
+ .fm-icon.jar{color:#ef4444}.fm-icon.yml{color:#10b981}.fm-icon.json{color:#f59e0b}
154
+ .fm-icon.properties{color:#3b82f6}.fm-icon.log{color:#8b5cf6}.fm-icon.img{color:#ec4899}
155
+ .fm-name{flex:1;font-size:13px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
156
+ .fm-size{font-size:11px;color:var(--text-muted);font-variant-numeric:tabular-nums;white-space:nowrap}
157
+ .fm-actions{display:flex;gap:2px;opacity:0;transition:var(--transition)}
158
+ .fm-item:hover .fm-actions{opacity:1}
159
+ .fm-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;
160
+ padding:60px 20px;color:var(--text-muted);gap:12px}
161
+ .fm-empty svg{width:48px;height:48px;opacity:0.3}
162
+
163
+ /* Editor */
164
+ .editor-container{display:flex;flex-direction:column;height:100%;overflow:hidden}
165
+ .editor-header{display:flex;align-items:center;gap:10px;padding:10px 16px;
166
+ background:var(--bg-secondary);border-bottom:1px solid var(--border-primary)}
167
+ .editor-filename{font-size:13px;font-weight:600;color:var(--accent);flex:1;
168
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
169
+ .editor-textarea{flex:1;width:100%;resize:none;background:var(--bg-primary);
170
+ color:var(--text-primary);font-family:var(--font-mono);font-size:12.5px;
171
+ line-height:1.6;padding:16px;border:none;outline:none;tab-size:4;
172
+ -webkit-overflow-scrolling:touch}
173
+ .editor-textarea::placeholder{color:var(--text-muted)}
174
+
175
+ /* Dashboard Cards */
176
+ .dashboard{padding:16px;overflow-y:auto;-webkit-overflow-scrolling:touch;height:100%}
177
+ .dash-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:12px}
178
+ .dash-card{background:var(--bg-card);border:1px solid var(--border-primary);
179
+ border-radius:var(--radius);padding:20px;transition:var(--transition)}
180
+ .dash-card:hover{border-color:var(--accent);box-shadow:var(--shadow-glow)}
181
+ .dash-card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}
182
+ .dash-card-title{font-size:13px;font-weight:600;color:var(--text-secondary);text-transform:uppercase;letter-spacing:0.5px}
183
+ .dash-card-icon{width:36px;height:36px;border-radius:var(--radius-sm);display:flex;
184
+ align-items:center;justify-content:center}
185
+ .dash-card-icon.cpu{background:#7c3aed20;color:#7c3aed}
186
+ .dash-card-icon.ram{background:#3b82f620;color:#3b82f6}
187
+ .dash-card-icon.disk{background:#10b98120;color:#10b981}
188
+ .dash-card-icon.status{background:#f59e0b20;color:#f59e0b}
189
+ .dash-metric{font-size:32px;font-weight:800;line-height:1;margin-bottom:4px;
190
+ font-variant-numeric:tabular-nums;letter-spacing:-1px}
191
+ .dash-metric-sub{font-size:12px;color:var(--text-muted)}
192
+ .dash-progress{width:100%;height:6px;background:var(--bg-primary);border-radius:3px;
193
+ margin-top:12px;overflow:hidden}
194
+ .dash-progress-fill{height:100%;border-radius:3px;transition:width 0.8s cubic-bezier(0.4,0,0.2,1)}
195
+ .color-green{color:var(--success)}.color-yellow{color:var(--warning)}
196
+ .color-red{color:var(--danger)}.color-blue{color:var(--info)}.color-purple{color:var(--accent)}
197
+ .fill-green{background:var(--success)}.fill-yellow{background:var(--warning)}
198
+ .fill-red{background:var(--danger)}.fill-blue{background:var(--info)}.fill-purple{background:var(--accent)}
199
+
200
+ /* Upload Overlay */
201
+ .upload-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);
202
+ backdrop-filter:blur(8px);z-index:200;align-items:center;justify-content:center}
203
+ .upload-overlay.active{display:flex}
204
+ .upload-modal{background:var(--bg-card);border:1px solid var(--border-primary);
205
+ border-radius:var(--radius-lg);padding:32px;width:min(440px,90vw);text-align:center}
206
+ .upload-zone{border:2px dashed var(--border-primary);border-radius:var(--radius);
207
+ padding:40px 20px;margin:20px 0;transition:var(--transition);cursor:pointer}
208
+ .upload-zone:hover,.upload-zone.dragover{border-color:var(--accent);background:var(--accent-glow)}
209
+ .upload-zone svg{width:40px;height:40px;color:var(--text-muted);margin-bottom:12px}
210
+ .upload-zone p{font-size:13px;color:var(--text-secondary)}
211
+
212
+ /* Toast Notifications */
213
+ .toast-container{position:fixed;bottom:20px;right:20px;z-index:300;display:flex;flex-direction:column;gap:8px}
214
+ .toast{padding:12px 16px;border-radius:var(--radius-sm);font-size:12px;font-weight:500;
215
+ animation:toastIn 0.3s ease,toastOut 0.3s ease 2.7s forwards;display:flex;align-items:center;gap:8px;
216
+ box-shadow:var(--shadow);max-width:min(360px,calc(100vw - 40px))}
217
+ .toast-success{background:#065f46;border:1px solid #10b98150;color:#d1fae5}
218
+ .toast-error{background:#7f1d1d;border:1px solid #ef444450;color:#fee2e2}
219
+ .toast-info{background:#1e3a5f;border:1px solid #3b82f650;color:#dbeafe}
220
+ @keyframes toastIn{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:translateX(0)}}
221
+ @keyframes toastOut{from{opacity:1}to{opacity:0;transform:translateY(10px)}}
222
+
223
+ /* Context Menu */
224
+ .ctx-menu{position:fixed;background:var(--bg-card);border:1px solid var(--border-primary);
225
+ border-radius:var(--radius-sm);padding:4px;z-index:250;min-width:160px;
226
+ box-shadow:var(--shadow);display:none}
227
+ .ctx-menu.active{display:block}
228
+ .ctx-item{display:flex;align-items:center;gap:8px;padding:7px 12px;font-size:12px;
229
+ color:var(--text-secondary);cursor:pointer;border-radius:4px;transition:var(--transition);
230
+ border:none;background:none;width:100%;text-align:left;font-family:var(--font)}
231
+ .ctx-item:hover{background:var(--bg-hover);color:var(--text-primary)}
232
+ .ctx-item.danger{color:var(--danger)}
233
+ .ctx-item.danger:hover{background:#ef444420}
234
+ .ctx-item svg{width:14px;height:14px;flex-shrink:0}
235
+ .ctx-sep{height:1px;background:var(--border-primary);margin:4px 0}
236
+
237
+ /* Connection indicator */
238
+ .conn-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
239
+ .conn-dot.connected{background:var(--success);box-shadow:0 0 6px var(--success)}
240
+ .conn-dot.disconnected{background:var(--danger);box-shadow:0 0 6px var(--danger)}
241
+
242
+ /* Responsive */
243
+ @media(max-width:768px){
244
+ .topbar{padding:0 10px;height:48px;min-height:48px}
245
+ .topbar-stats{display:none}
246
+ .topbar-brand span{display:none}
247
+ .hamburger{display:flex}
248
+ .nav-tabs{height:38px;min-height:38px;padding:0 8px;gap:0}
249
+ .nav-tab{padding:6px 10px;font-size:11px}
250
+ .nav-tab span{display:none}
251
+ .console-output{padding:8px;font-size:11px;line-height:1.5}
252
+ .console-input-wrap{padding:8px 10px}
253
+ .console-toolbar{padding:4px 8px}
254
+ .fm-toolbar{padding:8px 10px}
255
+ .fm-item{padding:8px 10px}
256
+ .fm-actions{opacity:1}
257
+ .dashboard{padding:10px}
258
+ .dash-grid{grid-template-columns:1fr}
259
+ .dash-metric{font-size:26px}
260
+ .stat-chip{padding:3px 6px;font-size:10px}
261
+ .mobile-stats{display:flex !important}
262
+ }
263
+ @media(max-width:480px){
264
+ .nav-tabs{overflow-x:auto}
265
+ .console-toolbar{flex-wrap:nowrap;overflow-x:auto}
266
+ .quick-cmds .btn{padding:3px 6px;font-size:9px}
267
+ }
268
+ .mobile-stats{display:none;gap:4px;padding:6px 10px;background:var(--bg-secondary);
269
+ border-bottom:1px solid var(--border-primary);overflow-x:auto}
270
+
271
+ /* Loading skeleton */
272
+ .skeleton{background:linear-gradient(90deg,var(--bg-tertiary) 25%,var(--bg-hover) 50%,var(--bg-tertiary) 75%);
273
+ background-size:200% 100%;animation:shimmer 1.5s infinite;border-radius:4px}
274
+ @keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
275
+
276
+ /* Smooth transitions for panels */
277
+ .panel{animation:panelIn 0.2s ease}
278
+ @keyframes panelIn{from{opacity:0}to{opacity:1}}
279
+
280
+ .search-input{background:var(--bg-tertiary);border:1px solid var(--border-primary);
281
+ color:var(--text-primary);font-size:12px;padding:5px 10px;border-radius:var(--radius-sm);
282
+ outline:none;transition:var(--transition);width:140px;font-family:var(--font)}
283
+ .search-input:focus{border-color:var(--accent);width:200px}
284
+ @media(max-width:768px){.search-input{width:100px}.search-input:focus{width:140px}}
285
+ </style>
286
  </head>
287
+ <body>
288
+ <div class="app">
289
+ <!-- Top Bar -->
290
+ <div class="topbar">
291
+ <div style="display:flex;align-items:center;gap:10px">
292
+ <button class="hamburger" onclick="toggleMobileStats()">
293
+ <svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h14M3 10h14M3 14h14"/></svg>
294
+ </button>
295
+ <div class="topbar-brand">
296
+ <svg width="22" height="22" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="18" rx="3"/><path d="M8 12h8M12 8v8"/></svg>
297
+ <span>MC Panel</span>
298
+ </div>
299
+ <div class="topbar-status" id="serverStatus">
300
+ <div class="conn-dot disconnected" id="connDot"></div>
301
+ <span id="statusText">CONNECTING</span>
302
+ </div>
303
+ </div>
304
+ <div class="topbar-stats" id="topbarStats">
305
+ <div class="stat-chip" title="CPU Usage (2 vCPU)">
306
+ <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="1" width="10" height="10" rx="2"/><path d="M4 4h4M4 6h4M4 8h2"/></svg>
307
+ <div><div class="stat-val" id="cpuTopVal">0%</div><div class="stat-lbl">CPU</div></div>
308
+ <div class="stat-bar-wrap"><div class="stat-bar-fill fill-purple" id="cpuTopBar" style="width:0%"></div></div>
309
+ </div>
310
+ <div class="stat-chip" title="RAM Usage (16 GB)">
311
+ <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="1" width="8" height="10" rx="1"/><path d="M4 11v1M8 11v1M1 4h10"/></svg>
312
+ <div><div class="stat-val" id="ramTopVal">0 MB</div><div class="stat-lbl">RAM</div></div>
313
+ <div class="stat-bar-wrap"><div class="stat-bar-fill fill-blue" id="ramTopBar" style="width:0%"></div></div>
314
+ </div>
315
+ <div class="stat-chip" title="Storage">
316
+ <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="6" cy="4" rx="5" ry="2"/><path d="M1 4v4c0 1.1 2.2 2 5 2s5-.9 5-2V4"/></svg>
317
+ <div><div class="stat-val" id="diskTopVal">-- GB</div><div class="stat-lbl">DISK</div></div>
318
+ <div class="stat-bar-wrap"><div class="stat-bar-fill fill-green" id="diskTopBar" style="width:0%"></div></div>
319
+ </div>
320
+ </div>
321
+ </div>
322
+
323
+ <!-- Mobile Stats (hidden by default) -->
324
+ <div class="mobile-stats" id="mobileStats">
325
+ <div class="stat-chip"><span class="stat-val" id="cpuMobVal">0%</span><span class="stat-lbl">CPU</span></div>
326
+ <div class="stat-chip"><span class="stat-val" id="ramMobVal">0 MB</span><span class="stat-lbl">RAM</span></div>
327
+ <div class="stat-chip"><span class="stat-val" id="diskMobVal">--</span><span class="stat-lbl">DISK</span></div>
328
+ </div>
329
+
330
+ <!-- Navigation Tabs -->
331
+ <div class="nav-tabs">
332
+ <button class="nav-tab active" data-tab="dashboard" onclick="switchTab('dashboard')">
333
+ <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
334
+ <span>Dashboard</span>
335
+ </button>
336
+ <button class="nav-tab" data-tab="console" onclick="switchTab('console')">
337
+ <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
338
+ <span>Console</span>
339
+ </button>
340
+ <button class="nav-tab" data-tab="files" onclick="switchTab('files')">
341
+ <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>
342
+ <span>Files</span>
343
+ </button>
344
+ <button class="nav-tab" data-tab="editor" onclick="switchTab('editor')" id="editorTab" style="display:none">
345
+ <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
346
+ <span>Editor</span>
347
+ </button>
348
+ </div>
349
 
350
+ <!-- Main Content Area -->
351
+ <div class="main-content">
352
+
353
+ <!-- Dashboard Panel -->
354
+ <div class="panel active" id="panel-dashboard">
355
+ <div class="dashboard">
356
+ <div class="dash-grid">
357
+ <div class="dash-card">
358
+ <div class="dash-card-header">
359
+ <div class="dash-card-title">CPU Usage</div>
360
+ <div class="dash-card-icon cpu">
361
+ <svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="16" height="16" rx="3"/><path d="M6 6h8M6 10h8M6 14h4"/></svg>
362
+ </div>
363
  </div>
364
+ <div class="dash-metric color-purple" id="cpuDashVal">0%</div>
365
+ <div class="dash-metric-sub">of 2 vCPU Cores</div>
366
+ <div class="dash-progress"><div class="dash-progress-fill fill-purple" id="cpuDashBar" style="width:0%"></div></div>
367
+ </div>
368
+ <div class="dash-card">
369
+ <div class="dash-card-header">
370
+ <div class="dash-card-title">Memory</div>
371
+ <div class="dash-card-icon ram">
372
+ <svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="1" width="14" height="16" rx="2"/><path d="M6 17v2M14 17v2M10 17v2M1 6h18"/></svg>
373
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  </div>
375
+ <div class="dash-metric color-blue" id="ramDashVal">0 MB</div>
376
+ <div class="dash-metric-sub" id="ramDashSub">of 16,384 MB</div>
377
+ <div class="dash-progress"><div class="dash-progress-fill fill-blue" id="ramDashBar" style="width:0%"></div></div>
378
+ </div>
379
+ <div class="dash-card">
380
+ <div class="dash-card-header">
381
+ <div class="dash-card-title">Storage</div>
382
+ <div class="dash-card-icon disk">
383
+ <svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="10" cy="6" rx="8" ry="3"/><path d="M2 6v5c0 1.7 3.6 3 8 3s8-1.3 8-3V6"/><path d="M2 11v5c0 1.7 3.6 3 8 3s8-1.3 8-3v-5"/></svg>
384
+ </div>
385
+ </div>
386
+ <div class="dash-metric color-green" id="diskDashVal">-- GB</div>
387
+ <div class="dash-metric-sub" id="diskDashSub">loading...</div>
388
+ <div class="dash-progress"><div class="dash-progress-fill fill-green" id="diskDashBar" style="width:0%"></div></div>
389
+ </div>
390
+ <div class="dash-card">
391
+ <div class="dash-card-header">
392
+ <div class="dash-card-title">Server Status</div>
393
+ <div class="dash-card-icon status">
394
+ <svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><circle cx="10" cy="10" r="8"/><polyline points="10 5 10 10 13 12"/></svg>
395
+ </div>
 
 
 
 
 
 
 
 
 
 
396
  </div>
397
+ <div class="dash-metric" id="uptimeVal" style="font-size:24px;color:var(--text-primary)">--</div>
398
+ <div class="dash-metric-sub" id="uptimeSub">Container uptime</div>
399
+ </div>
400
  </div>
401
+ </div>
402
+ </div>
403
 
404
+ <!-- Console Panel -->
405
+ <div class="panel" id="panel-console">
406
+ <div class="console-wrap">
407
+ <div class="console-toolbar">
408
+ <button class="btn btn-sm" onclick="sendCmd('list')" title="List Players">
409
+ <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 10a3 3 0 100-6 3 3 0 000 6zM1 12a6 6 0 0112 0"/></svg>
410
+ Players
411
+ </button>
412
+ <div class="quick-cmds">
413
+ <button class="btn" onclick="sendCmd('tps')">TPS</button>
414
+ <button class="btn" onclick="sendCmd('gc')">GC</button>
415
+ <button class="btn" onclick="sendCmd('mem')">Mem</button>
416
+ <button class="btn" onclick="sendCmd('version')">Ver</button>
417
+ <button class="btn" onclick="sendCmd('plugins')">Plugins</button>
418
+ <button class="btn" onclick="sendCmd('whitelist list')">WL</button>
419
+ <button class="btn" onclick="sendCmd('save-all')">Save</button>
420
+ </div>
421
+ <button class="btn btn-sm btn-danger" onclick="if(confirm('Stop the server?'))sendCmd('stop')" title="Stop Server">
422
+ <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="1" width="10" height="10" rx="1"/></svg>
423
+ Stop
424
+ </button>
425
+ <button class="btn btn-sm btn-icon" onclick="clearConsole()" title="Clear Console">
426
+ <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 1l10 10M11 1L1 11"/></svg>
427
+ </button>
428
+ <button class="btn btn-sm btn-icon" onclick="scrollToBottom()" title="Scroll to Bottom">
429
+ <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 1v10M2 7l4 4 4-4"/></svg>
430
+ </button>
431
+ </div>
432
+ <div class="console-output" id="consoleOutput"></div>
433
+ <div class="console-input-wrap">
434
+ <span class="console-prefix">$</span>
435
+ <input class="console-input" id="consoleInput" type="text" placeholder="Type a command..." autocomplete="off" spellcheck="false">
436
+ <button class="btn btn-accent" onclick="sendCurrentCmd()">
437
+ <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 1l11 6-11 6V1z"/></svg>
438
+ </button>
439
+ </div>
440
+ </div>
441
+ </div>
442
 
443
+ <!-- Files Panel -->
444
+ <div class="panel" id="panel-files">
445
+ <div class="fm-container">
446
+ <div class="fm-toolbar">
447
+ <div class="fm-path" id="fmBreadcrumb"></div>
448
+ <input class="search-input" type="text" placeholder="Filter..." id="fmSearch" oninput="filterFiles()">
449
+ <button class="btn btn-sm" onclick="refreshFiles()" title="Refresh">
450
+ <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 1v4h4M11 11v-4h-4"/><path d="M10.4 5A5 5 0 003 3.5L1 5M1.6 7a5 5 0 007.4 1.5L11 7"/></svg>
451
+ </button>
452
+ <button class="btn btn-sm" onclick="showUploadModal()" title="Upload File">
453
+ <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 10V2M2 6l4-4 4 4"/><path d="M1 10v1a1 1 0 001 1h8a1 1 0 001-1v-1"/></svg>
454
+ Upload
455
+ </button>
456
+ <button class="btn btn-sm" onclick="createNewFile()" title="New File">
457
+ <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 1v10M1 6h10"/></svg>
458
+ New
459
+ </button>
460
+ </div>
461
+ <div class="fm-body" id="fmBody">
462
+ <div class="fm-empty"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg><span>Loading files...</span></div>
463
  </div>
464
+ </div>
465
+ </div>
466
 
467
+ <!-- Editor Panel -->
468
+ <div class="panel" id="panel-editor">
469
+ <div class="editor-container">
470
+ <div class="editor-header">
471
+ <button class="btn btn-sm" onclick="closeEditor()">
472
+ <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 1L1 6l9 5"/></svg>
473
+ Back
474
+ </button>
475
+ <div class="editor-filename" id="editorFilename">No file open</div>
476
+ <button class="btn btn-sm btn-accent" onclick="saveFile()" id="editorSaveBtn">
477
+ <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 1h8l2 2v8a1 1 0 01-1 1H2a1 1 0 01-1-1V2a1 1 0 011-1z"/><path d="M4 1v3h4V1M3 7h6v4H3z"/></svg>
478
+ Save
479
+ </button>
480
+ <button class="btn btn-sm" onclick="downloadCurrentFile()">
481
+ <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 1v8M2 6l4 4 4-4"/><path d="M1 10v1a1 1 0 001 1h8a1 1 0 001-1v-1"/></svg>
482
+ </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
  </div>
484
+ <textarea class="editor-textarea" id="editorContent" placeholder="File contents will appear here..." spellcheck="false"></textarea>
485
+ </div>
486
  </div>
487
+ </div>
488
+ </div>
489
 
490
+ <!-- Upload Modal -->
491
+ <div class="upload-overlay" id="uploadOverlay" onclick="if(event.target===this)hideUploadModal()">
492
+ <div class="upload-modal">
493
+ <h3 style="font-size:16px;font-weight:700;margin-bottom:4px">Upload File</h3>
494
+ <p style="font-size:12px;color:var(--text-muted)" id="uploadPathDisplay">to /</p>
495
+ <div class="upload-zone" id="uploadZone" onclick="document.getElementById('uploadFileInput').click()">
496
+ <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path d="M12 16V4M8 8l4-4 4 4"/><path d="M20 16v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2"/></svg>
497
+ <p>Click or drag files here</p>
498
+ <p style="font-size:11px;color:var(--text-muted);margin-top:4px" id="uploadFileName">No file selected</p>
499
+ </div>
500
+ <input type="file" id="uploadFileInput" style="display:none" onchange="handleFileSelect(this)">
501
+ <div style="display:flex;gap:8px;justify-content:flex-end">
502
+ <button class="btn" onclick="hideUploadModal()">Cancel</button>
503
+ <button class="btn btn-accent" onclick="doUpload()" id="uploadBtn">Upload</button>
504
+ </div>
505
+ </div>
506
+ </div>
 
 
 
 
 
 
 
507
 
508
+ <!-- Context Menu -->
509
+ <div class="ctx-menu" id="ctxMenu">
510
+ <button class="ctx-item" onclick="ctxAction('open')"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>Open</button>
511
+ <button class="ctx-item" onclick="ctxAction('edit')"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>Edit</button>
512
+ <button class="ctx-item" onclick="ctxAction('download')"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M12 16V4M8 12l4 4 4-4"/><path d="M20 16v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2"/></svg>Download</button>
513
+ <button class="ctx-item" onclick="ctxAction('rename')"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M17 3a2.83 2.83 0 114 4L7.5 20.5 2 22l1.5-5.5L17 3z"/></svg>Rename</button>
514
+ <div class="ctx-sep"></div>
515
+ <button class="ctx-item danger" onclick="ctxAction('delete')"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/></svg>Delete</button>
516
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
 
518
+ <!-- Toast Container -->
519
+ <div class="toast-container" id="toastContainer"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
520
 
521
+ <script>
522
+ // ========== STATE ==========
523
+ let ws=null, wsConnected=false, currentPath='', editingFile='', cmdHistory=[], cmdHistoryIdx=-1;
524
+ let ctxTarget=null, allFileItems=[], autoScroll=true, startTime=Date.now();
525
+ const consoleEl=document.getElementById('consoleOutput');
526
+ const consoleInput=document.getElementById('consoleInput');
 
 
 
527
 
528
+ // ========== TABS ==========
529
+ function switchTab(tab){
530
+ document.querySelectorAll('.nav-tab').forEach(t=>t.classList.remove('active'));
531
+ document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));
532
+ document.querySelector(`[data-tab="${tab}"]`).classList.add('active');
533
+ document.getElementById('panel-'+tab).classList.add('active');
534
+ if(tab==='files')loadFiles(currentPath);
535
+ if(tab==='console')requestAnimationFrame(()=>scrollToBottom());
536
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
537
 
538
+ // ========== WEBSOCKET ==========
539
+ function connectWS(){
540
+ const proto=location.protocol==='https:'?'wss:':'ws:';
541
+ ws=new WebSocket(proto+'//'+location.host+'/ws');
542
+ ws.onopen=()=>{wsConnected=true;updateStatus(true);toast('Connected to server','success')};
543
+ ws.onclose=()=>{wsConnected=false;updateStatus(false);setTimeout(connectWS,2000)};
544
+ ws.onerror=()=>{wsConnected=false;updateStatus(false)};
545
+ ws.onmessage=e=>{appendConsole(e.data)};
546
+ }
547
+ function updateStatus(online){
548
+ const dot=document.getElementById('connDot');
549
+ const txt=document.getElementById('statusText');
550
+ const wrap=document.getElementById('serverStatus');
551
+ dot.className='conn-dot '+(online?'connected':'disconnected');
552
+ txt.textContent=online?'ONLINE':'OFFLINE';
553
+ wrap.className='topbar-status '+(online?'status-online':'status-offline');
554
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555
 
556
+ // ========== CONSOLE ==========
557
+ function classifyLine(text){
558
+ const t=text.toLowerCase();
559
+ if(t.includes('error')||t.includes('exception')||t.includes('severe')||t.includes('fatal'))return 'log-error';
560
+ if(t.includes('warn'))return 'log-warn';
561
+ if(t.includes('info'))return 'log-info';
562
+ if(t.includes('server')||t.includes('starting')||t.includes('done'))return 'log-server';
563
+ if(t.includes('<')||t.includes('joined')||t.includes('left'))return 'log-chat';
564
+ return 'log-default';
565
+ }
566
+ function appendConsole(text){
567
+ const div=document.createElement('div');
568
+ div.className='console-line '+classifyLine(text);
569
+ const now=new Date();
570
+ const ts=String(now.getHours()).padStart(2,'0')+':'+String(now.getMinutes()).padStart(2,'0')+':'+String(now.getSeconds()).padStart(2,'0');
571
+ div.innerHTML='<span class="timestamp">'+ts+'</span>'+escapeHtml(text);
572
+ consoleEl.appendChild(div);
573
+ // Keep max 500 lines in DOM
574
+ while(consoleEl.children.length>500)consoleEl.removeChild(consoleEl.firstChild);
575
+ if(autoScroll)scrollToBottom();
576
+ }
577
+ function scrollToBottom(){consoleEl.scrollTop=consoleEl.scrollHeight}
578
+ function clearConsole(){consoleEl.innerHTML='';toast('Console cleared','info')}
579
+ function escapeHtml(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML}
580
 
581
+ consoleEl.addEventListener('scroll',()=>{
582
+ const atBottom=consoleEl.scrollHeight-consoleEl.scrollTop-consoleEl.clientHeight<50;
583
+ autoScroll=atBottom;
584
+ });
 
 
 
585
 
586
+ function sendCmd(cmd){
587
+ if(ws&&ws.readyState===1){ws.send(cmd);appendConsole('> '+cmd)}
588
+ else toast('Not connected','error');
589
+ }
590
+ function sendCurrentCmd(){
591
+ const v=consoleInput.value.trim();
592
+ if(!v)return;
593
+ cmdHistory.unshift(v);if(cmdHistory.length>50)cmdHistory.pop();cmdHistoryIdx=-1;
594
+ sendCmd(v);consoleInput.value='';
595
+ }
596
+ consoleInput.addEventListener('keydown',e=>{
597
+ if(e.key==='Enter'){e.preventDefault();sendCurrentCmd()}
598
+ if(e.key==='ArrowUp'){e.preventDefault();if(cmdHistoryIdx<cmdHistory.length-1){cmdHistoryIdx++;consoleInput.value=cmdHistory[cmdHistoryIdx]}}
599
+ if(e.key==='ArrowDown'){e.preventDefault();if(cmdHistoryIdx>0){cmdHistoryIdx--;consoleInput.value=cmdHistory[cmdHistoryIdx]}else{cmdHistoryIdx=-1;consoleInput.value=''}}
600
+ if(e.key==='Tab'){e.preventDefault();/* Could add tab completion */}
601
+ });
602
 
603
+ // ========== STATS ==========
604
+ function getColorForPercent(p){return p>=90?'red':p>=70?'yellow':'green'}
605
+ async function fetchStats(){
606
+ try{
607
+ const r=await fetch('/api/stats');const d=await r.json();
608
+ const cpuP=Math.min(100,d.cpu_percent||0).toFixed(1);
609
+ const ramMB=(d.ram_used_mb||0).toFixed(0);
610
+ const ramP=((d.ram_used_mb||0)/16384*100).toFixed(1);
611
+ const diskUsedGB=(d.disk_used_gb||0).toFixed(1);
612
+ const diskTotalGB=(d.disk_total_gb||0).toFixed(1);
613
+ const diskP=diskTotalGB>0?((d.disk_used_gb/d.disk_total_gb)*100).toFixed(1):'0';
614
+ const cpuCol=getColorForPercent(cpuP);
615
+ const ramCol=getColorForPercent(ramP);
616
+ const diskCol=getColorForPercent(diskP);
617
+
618
+ // Topbar
619
+ document.getElementById('cpuTopVal').textContent=cpuP+'%';
620
+ document.getElementById('cpuTopBar').style.width=cpuP+'%';
621
+ document.getElementById('cpuTopBar').className='stat-bar-fill fill-'+cpuCol;
622
+ document.getElementById('ramTopVal').textContent=ramMB+' MB';
623
+ document.getElementById('ramTopBar').style.width=ramP+'%';
624
+ document.getElementById('ramTopBar').className='stat-bar-fill fill-'+ramCol;
625
+ document.getElementById('diskTopVal').textContent=diskUsedGB+' GB';
626
+ document.getElementById('diskTopBar').style.width=diskP+'%';
627
+ document.getElementById('diskTopBar').className='stat-bar-fill fill-'+diskCol;
628
 
629
+ // Mobile
630
+ document.getElementById('cpuMobVal').textContent=cpuP+'%';
631
+ document.getElementById('ramMobVal').textContent=ramMB+' MB';
632
+ document.getElementById('diskMobVal').textContent=diskUsedGB+'G';
633
+
634
+ // Dashboard
635
+ document.getElementById('cpuDashVal').textContent=cpuP+'%';
636
+ document.getElementById('cpuDashBar').style.width=cpuP+'%';
637
+ document.getElementById('cpuDashBar').className='dash-progress-fill fill-'+cpuCol;
638
+ document.getElementById('cpuDashVal').className='dash-metric color-'+cpuCol;
639
+
640
+ document.getElementById('ramDashVal').textContent=ramMB+' MB';
641
+ document.getElementById('ramDashSub').textContent=ramP+'% of 16,384 MB';
642
+ document.getElementById('ramDashBar').style.width=ramP+'%';
643
+ document.getElementById('ramDashBar').className='dash-progress-fill fill-'+ramCol;
644
+ document.getElementById('ramDashVal').className='dash-metric color-'+ramCol;
645
+
646
+ document.getElementById('diskDashVal').textContent=diskUsedGB+' GB';
647
+ document.getElementById('diskDashSub').textContent=diskP+'% of '+diskTotalGB+' GB';
648
+ document.getElementById('diskDashBar').style.width=diskP+'%';
649
+ document.getElementById('diskDashBar').className='dash-progress-fill fill-'+diskCol;
650
+ document.getElementById('diskDashVal').className='dash-metric color-'+diskCol;
651
+
652
+ // Uptime
653
+ const upSec=Math.floor((Date.now()-startTime)/1000);
654
+ const h=Math.floor(upSec/3600),m=Math.floor((upSec%3600)/60),s=upSec%60;
655
+ document.getElementById('uptimeVal').textContent=
656
+ (h>0?h+'h ':'')+(m>0?m+'m ':'')+s+'s';
657
+ document.getElementById('uptimeSub').textContent='Session uptime';
658
+ }catch(e){}
659
+ }
660
+ setInterval(fetchStats,2000);
661
+ fetchStats();
662
+
663
+ // ========== FILE MANAGER ==========
664
+ function formatSize(b){
665
+ if(b===0)return'--';
666
+ if(b<1024)return b+' B';
667
+ if(b<1048576)return(b/1024).toFixed(1)+' KB';
668
+ if(b<1073741824)return(b/1048576).toFixed(1)+' MB';
669
+ return(b/1073741824).toFixed(2)+' GB';
670
+ }
671
+ function getFileIcon(name,isDir){
672
+ if(isDir)return{cls:'folder',svg:'<path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/>'};
673
+ const ext=name.split('.').pop().toLowerCase();
674
+ const map={jar:{cls:'jar',i:'☕'},yml:{cls:'yml',i:'⚙'},yaml:{cls:'yml',i:'⚙'},
675
+ json:{cls:'json',i:'{}'},properties:{cls:'properties',i:'🔧'},log:{cls:'log',i:'📜'},
676
+ txt:{cls:'file',i:'📄'},png:{cls:'img',i:'🖼'},jpg:{cls:'img',i:'🖼'},gif:{cls:'img',i:'🖼'},
677
+ toml:{cls:'yml',i:'⚙'},conf:{cls:'properties',i:'🔧'},cfg:{cls:'properties',i:'🔧'},
678
+ sk:{cls:'yml',i:'📜'},sh:{cls:'log',i:'⚡'},bat:{cls:'log',i:'⚡'}};
679
+ const m=map[ext]||{cls:'file',i:'📄'};
680
+ return{cls:m.cls,text:m.i};
681
+ }
682
+
683
+ async function loadFiles(path){
684
+ currentPath=path||'';
685
+ buildBreadcrumb();
686
+ const body=document.getElementById('fmBody');
687
+ body.innerHTML='<div style="padding:20px;text-align:center;color:var(--text-muted)"><div class="skeleton" style="height:20px;width:60%;margin:8px auto"></div><div class="skeleton" style="height:20px;width:80%;margin:8px auto"></div><div class="skeleton" style="height:20px;width:50%;margin:8px auto"></div></div>';
688
+ try{
689
+ const r=await fetch('/api/fs/list?path='+encodeURIComponent(currentPath));
690
+ allFileItems=await r.json();
691
+ renderFiles(allFileItems);
692
+ }catch(e){body.innerHTML='<div class="fm-empty"><span>Failed to load</span></div>'}
693
+ }
694
+
695
+ function renderFiles(items){
696
+ const body=document.getElementById('fmBody');
697
+ const search=document.getElementById('fmSearch').value.toLowerCase();
698
+ let filtered=items;
699
+ if(search)filtered=items.filter(i=>i.name.toLowerCase().includes(search));
700
+
701
+ if(!filtered.length&&!currentPath){body.innerHTML='<div class="fm-empty"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg><span>Directory is empty</span></div>';return}
702
+
703
+ let html='';
704
+ // Parent directory
705
+ if(currentPath){
706
+ html+='<div class="fm-item" ondblclick="goUp()" onclick="goUp()"><div class="fm-icon folder"><svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg></div><div class="fm-name" style="color:var(--text-muted)">..</div><div class="fm-size"></div></div>';
707
+ }
708
+ filtered.forEach((item,idx)=>{
709
+ const icon=getFileIcon(item.name,item.is_dir);
710
+ const iconHtml=icon.svg
711
+ ?'<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">'+icon.svg+'</svg>'
712
+ :'<span style="font-size:16px">'+icon.text+'</span>';
713
+ html+=`<div class="fm-item" data-name="${escapeHtml(item.name)}" data-dir="${item.is_dir}" data-idx="${idx}"
714
+ ondblclick="fmDblClick('${escapeHtml(item.name)}',${item.is_dir})"
715
+ oncontextmenu="fmCtx(event,'${escapeHtml(item.name)}',${item.is_dir})">
716
+ <div class="fm-icon ${icon.cls}">${iconHtml}</div>
717
+ <div class="fm-name">${escapeHtml(item.name)}</div>
718
+ <div class="fm-size">${item.is_dir?'':formatSize(item.size)}</div>
719
+ <div class="fm-actions">
720
+ ${!item.is_dir?`<button class="btn btn-sm btn-icon" onclick="event.stopPropagation();editFile('${escapeHtml(item.name)}')" title="Edit"><svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M8.5 1.5a1.4 1.4 0 012 2L4 10l-2.5.5.5-2.5L8.5 1.5z"/></svg></button>`:''}
721
+ ${!item.is_dir?`<button class="btn btn-sm btn-icon" onclick="event.stopPropagation();downloadFile('${escapeHtml(item.name)}')" title="Download"><svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 1v8M3 6l3 3 3-3"/><path d="M1 9v2h10V9"/></svg></button>`:''}
722
+ <button class="btn btn-sm btn-icon" onclick="event.stopPropagation();deleteItem('${escapeHtml(item.name)}')" title="Delete" style="color:var(--danger)"><svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 3h10M3.5 3V2a1 1 0 011-1h3a1 1 0 011 1v1M9 3v7a1 1 0 01-1 1H4a1 1 0 01-1-1V3"/></svg></button>
723
+ </div>
724
+ </div>`;
725
+ });
726
+ body.innerHTML='<div class="fm-list">'+html+'</div>';
727
+ }
728
+
729
+ function filterFiles(){renderFiles(allFileItems)}
730
+ function buildBreadcrumb(){
731
+ const el=document.getElementById('fmBreadcrumb');
732
+ const parts=currentPath?currentPath.split('/').filter(Boolean):[];
733
+ let html='<button class="fm-crumb '+(parts.length===0?'active':'')+'" onclick="loadFiles(\\'\\')">root</button>';
734
+ let acc='';
735
+ parts.forEach((p,i)=>{
736
+ acc+=(acc?'/':'')+p;
737
+ const a=acc;
738
+ html+='<span class="fm-crumb-sep">/</span>';
739
+ html+='<button class="fm-crumb '+(i===parts.length-1?'active':'')+'" onclick="loadFiles(\''+a+'\')">'+escapeHtml(p)+'</button>';
740
+ });
741
+ el.innerHTML=html;
742
+ }
743
+ function goUp(){
744
+ const parts=currentPath.split('/').filter(Boolean);
745
+ parts.pop();
746
+ loadFiles(parts.join('/'));
747
+ }
748
+ function fmDblClick(name,isDir){
749
+ if(isDir)loadFiles((currentPath?currentPath+'/':'')+name);
750
+ else editFile(name);
751
+ }
752
+
753
+ async function editFile(name){
754
+ const fpath=(currentPath?currentPath+'/':'')+name;
755
+ try{
756
+ const r=await fetch('/api/fs/read?path='+encodeURIComponent(fpath));
757
+ if(!r.ok){toast('Cannot open: '+await r.text(),'error');return}
758
+ const text=await r.text();
759
+ editingFile=fpath;
760
+ document.getElementById('editorFilename').textContent=fpath;
761
+ document.getElementById('editorContent').value=text;
762
+ document.getElementById('editorTab').style.display='';
763
+ switchTab('editor');
764
+ toast('Opened '+name,'info');
765
+ }catch(e){toast('Failed to open file','error')}
766
+ }
767
+ async function saveFile(){
768
+ if(!editingFile)return;
769
+ try{
770
+ const fd=new FormData();
771
+ fd.append('path',editingFile);
772
+ fd.append('content',document.getElementById('editorContent').value);
773
+ const r=await fetch('/api/fs/write',{method:'POST',body:fd});
774
+ if(r.ok)toast('Saved '+editingFile,'success');
775
+ else toast('Save failed','error');
776
+ }catch(e){toast('Save error','error')}
777
+ }
778
+ function closeEditor(){
779
+ switchTab('files');
780
+ document.getElementById('editorTab').style.display='none';
781
+ }
782
+ function downloadFile(name){
783
+ const fpath=(currentPath?currentPath+'/':'')+name;
784
+ window.open('/api/fs/download?path='+encodeURIComponent(fpath),'_blank');
785
+ }
786
+ function downloadCurrentFile(){
787
+ if(editingFile)window.open('/api/fs/download?path='+encodeURIComponent(editingFile),'_blank');
788
+ }
789
+ async function deleteItem(name){
790
+ if(!confirm('Delete "'+name+'"?'))return;
791
+ const fpath=(currentPath?currentPath+'/':'')+name;
792
+ try{
793
+ const fd=new FormData();fd.append('path',fpath);
794
+ await fetch('/api/fs/delete',{method:'POST',body:fd});
795
+ toast('Deleted '+name,'success');
796
+ loadFiles(currentPath);
797
+ }catch(e){toast('Delete failed','error')}
798
+ }
799
+ async function createNewFile(){
800
+ const name=prompt('Enter filename (or foldername/):');
801
+ if(!name)return;
802
+ const fpath=(currentPath?currentPath+'/':'')+name;
803
+ if(name.endsWith('/')){
804
+ // Create directory by writing a temp file and deleting it — or we can use the write endpoint
805
+ toast('Creating directories not supported yet','info');
806
+ return;
807
+ }
808
+ try{
809
+ const fd=new FormData();fd.append('path',fpath);fd.append('content','');
810
+ await fetch('/api/fs/write',{method:'POST',body:fd});
811
+ toast('Created '+name,'success');
812
+ loadFiles(currentPath);
813
+ }catch(e){toast('Failed','error')}
814
+ }
815
+
816
+ // Upload
817
+ function showUploadModal(){
818
+ document.getElementById('uploadOverlay').classList.add('active');
819
+ document.getElementById('uploadPathDisplay').textContent='to /'+(currentPath||'');
820
+ document.getElementById('uploadFileName').textContent='No file selected';
821
+ document.getElementById('uploadFileInput').value='';
822
+ }
823
+ function hideUploadModal(){document.getElementById('uploadOverlay').classList.remove('active')}
824
+ function handleFileSelect(inp){
825
+ if(inp.files.length)document.getElementById('uploadFileName').textContent=inp.files[0].name;
826
+ }
827
+ async function doUpload(){
828
+ const inp=document.getElementById('uploadFileInput');
829
+ if(!inp.files.length){toast('Select a file first','error');return}
830
+ const fd=new FormData();fd.append('path',currentPath);fd.append('file',inp.files[0]);
831
+ document.getElementById('uploadBtn').textContent='Uploading...';
832
+ try{
833
+ await fetch('/api/fs/upload',{method:'POST',body:fd});
834
+ toast('Uploaded '+inp.files[0].name,'success');
835
+ hideUploadModal();
836
+ loadFiles(currentPath);
837
+ }catch(e){toast('Upload failed','error')}
838
+ document.getElementById('uploadBtn').textContent='Upload';
839
+ }
840
+ // Drag & drop
841
+ const uploadZone=document.getElementById('uploadZone');
842
+ uploadZone.addEventListener('dragover',e=>{e.preventDefault();uploadZone.classList.add('dragover')});
843
+ uploadZone.addEventListener('dragleave',()=>uploadZone.classList.remove('dragover'));
844
+ uploadZone.addEventListener('drop',e=>{
845
+ e.preventDefault();uploadZone.classList.remove('dragover');
846
+ if(e.dataTransfer.files.length){
847
+ document.getElementById('uploadFileInput').files=e.dataTransfer.files;
848
+ document.getElementById('uploadFileName').textContent=e.dataTransfer.files[0].name;
849
+ }
850
+ });
851
+
852
+ // Context Menu
853
+ function fmCtx(e,name,isDir){
854
+ e.preventDefault();e.stopPropagation();
855
+ ctxTarget={name,isDir};
856
+ const menu=document.getElementById('ctxMenu');
857
+ menu.classList.add('active');
858
+ // Position
859
+ const x=Math.min(e.clientX,window.innerWidth-170);
860
+ const y=Math.min(e.clientY,window.innerHeight-200);
861
+ menu.style.left=x+'px';menu.style.top=y+'px';
862
+ }
863
+ document.addEventListener('click',()=>document.getElementById('ctxMenu').classList.remove('active'));
864
+ function ctxAction(action){
865
+ if(!ctxTarget)return;
866
+ const{name,isDir}=ctxTarget;
867
+ document.getElementById('ctxMenu').classList.remove('active');
868
+ switch(action){
869
+ case'open':fmDblClick(name,isDir);break;
870
+ case'edit':if(!isDir)editFile(name);break;
871
+ case'download':if(!isDir)downloadFile(name);break;
872
+ case'rename':
873
+ const nn=prompt('Rename to:',name);
874
+ if(nn&&nn!==name)toast('Rename not implemented yet','info');
875
+ break;
876
+ case'delete':deleteItem(name);break;
877
+ }
878
+ }
879
+
880
+ // ========== TOAST ==========
881
+ function toast(msg,type='info'){
882
+ const c=document.getElementById('toastContainer');
883
+ const t=document.createElement('div');
884
+ t.className='toast toast-'+type;
885
+ const icons={success:'✓',error:'✕',info:'ℹ'};
886
+ t.innerHTML='<span>'+(icons[type]||'ℹ')+'</span><span>'+escapeHtml(msg)+'</span>';
887
+ c.appendChild(t);
888
+ setTimeout(()=>t.remove(),3000);
889
+ }
890
+
891
+ // ========== MOBILE ==========
892
+ function toggleMobileStats(){
893
+ const el=document.getElementById('mobileStats');
894
+ el.style.display=el.style.display==='flex'?'none':'flex';
895
+ }
896
+
897
+ // ========== KEYBOARD SHORTCUTS ==========
898
+ document.addEventListener('keydown',e=>{
899
+ if(e.ctrlKey&&e.key==='s'){e.preventDefault();if(editingFile)saveFile()}
900
+ if(e.ctrlKey&&e.key==='`'){e.preventDefault();switchTab('console');consoleInput.focus()}
901
+ });
902
+
903
+ // ========== INIT ==========
904
+ connectWS();
905
+ loadFiles('');
906
+ </script>
907
  </body>
908
  </html>
909
  """
 
935
  while True:
936
  try:
937
  line = await stream.readline()
938
+ if not line:
939
+ break
940
  line_str = line.decode('utf-8', errors='replace').rstrip('\r\n')
941
  await broadcast(prefix + line_str)
942
  except Exception:
 
990
  mc_process.stdin.write((cmd + "\n").encode('utf-8'))
991
  await mc_process.stdin.drain()
992
  except:
993
+ connected_clients.discard(websocket)
994
 
995
  @app.get("/api/stats")
996
  def get_stats():
 
 
 
 
997
  try:
998
  current_process = psutil.Process(os.getpid())
999
  mem_usage = current_process.memory_info().rss
1000
+ cpu_percent = current_process.cpu_percent(interval=0)
1001
 
 
1002
  for child in current_process.children(recursive=True):
1003
  try:
1004
  mem_usage += child.memory_info().rss
1005
+ cpu_percent += child.cpu_percent(interval=0)
1006
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
1007
  pass
1008
+
1009
+ # Normalize CPU to 0-100% for 2 cores
1010
+ normalized_cpu = min(100.0, cpu_percent / CONTAINER_CPU_CORES)
1011
+
1012
+ # Disk usage for the container's BASE_DIR
1013
+ try:
1014
+ disk = shutil.disk_usage(BASE_DIR)
1015
+ disk_used_gb = disk.used / (1024 ** 3)
1016
+ disk_total_gb = disk.total / (1024 ** 3)
1017
+ except Exception:
1018
+ disk_used_gb = 0
1019
+ disk_total_gb = CONTAINER_STORAGE_GB
1020
+
1021
  return {
1022
+ "ram_used_mb": round(mem_usage / (1024 * 1024), 1),
1023
+ "ram_total_mb": CONTAINER_RAM_MB,
1024
+ "cpu_percent": round(normalized_cpu, 1),
1025
+ "cpu_cores": CONTAINER_CPU_CORES,
1026
+ "disk_used_gb": round(disk_used_gb, 2),
1027
+ "disk_total_gb": round(disk_total_gb, 2),
1028
  }
1029
  except Exception:
1030
+ return {
1031
+ "ram_used_mb": 0, "ram_total_mb": CONTAINER_RAM_MB,
1032
+ "cpu_percent": 0, "cpu_cores": CONTAINER_CPU_CORES,
1033
+ "disk_used_gb": 0, "disk_total_gb": CONTAINER_STORAGE_GB,
1034
+ }
1035
 
1036
  @app.get("/api/fs/list")
1037
  def fs_list(path: str = ""):
1038
  target = get_safe_path(path)
1039
+ if not os.path.exists(target):
1040
+ return []
1041
  items = []
1042
+ try:
1043
+ for f in os.listdir(target):
1044
+ fp = os.path.join(target, f)
1045
+ try:
1046
+ is_dir = os.path.isdir(fp)
1047
+ size = 0 if is_dir else os.path.getsize(fp)
1048
+ items.append({"name": f, "is_dir": is_dir, "size": size})
1049
+ except OSError:
1050
+ pass
1051
+ except PermissionError:
1052
+ pass
1053
  return sorted(items, key=lambda x: (not x["is_dir"], x["name"].lower()))
1054
 
1055
  @app.get("/api/fs/read")
1056
  def fs_read(path: str):
1057
  target = get_safe_path(path)
1058
+ if not os.path.isfile(target):
1059
+ raise HTTPException(400, "Not a file")
1060
  try:
1061
+ with open(target, 'r', encoding='utf-8', errors='replace') as f:
1062
+ content = f.read(5 * 1024 * 1024) # Max 5MB read
1063
+ return Response(content=content, media_type="text/plain")
1064
+ except Exception as e:
1065
+ raise HTTPException(400, f"Cannot read: {str(e)}")
1066
 
1067
  @app.get("/api/fs/download")
1068
  def fs_download(path: str):
1069
  target = get_safe_path(path)
1070
+ if not os.path.isfile(target):
1071
+ raise HTTPException(400, "Not a file")
1072
  return FileResponse(target, filename=os.path.basename(target))
1073
 
1074
  @app.post("/api/fs/write")
1075
  def fs_write(path: str = Form(...), content: str = Form(...)):
1076
  target = get_safe_path(path)
1077
+ os.makedirs(os.path.dirname(target), exist_ok=True)
1078
  with open(target, 'w', encoding='utf-8') as f:
1079
  f.write(content)
1080
  return {"status": "ok"}
 
1082
  @app.post("/api/fs/upload")
1083
  async def fs_upload(path: str = Form(""), file: UploadFile = File(...)):
1084
  target_dir = get_safe_path(path)
1085
+ os.makedirs(target_dir, exist_ok=True)
1086
  target_file = os.path.join(target_dir, file.filename)
1087
+ if not os.path.abspath(target_file).startswith(BASE_DIR):
1088
+ raise HTTPException(403, "Access denied")
1089
  with open(target_file, "wb") as buffer:
1090
  shutil.copyfileobj(file.file, buffer)
1091
  return {"status": "ok"}
 
1093
  @app.post("/api/fs/delete")
1094
  def fs_delete(path: str = Form(...)):
1095
  target = get_safe_path(path)
1096
+ if not os.path.exists(target):
1097
+ raise HTTPException(404, "Not found")
1098
+ if os.path.isdir(target):
1099
+ shutil.rmtree(target)
1100
+ else:
1101
+ os.remove(target)
1102
  return {"status": "ok"}
1103
 
1104
  if __name__ == "__main__":