OrbitMC commited on
Commit
1bc1573
·
verified ·
1 Parent(s): bfca8fe

Update panel.py

Browse files
Files changed (1) hide show
  1. panel.py +346 -122
panel.py CHANGED
@@ -6,7 +6,6 @@ from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
6
  from fastapi.middleware.cors import CORSMiddleware
7
  import uvicorn
8
  import shutil
9
- from datetime import datetime
10
 
11
  app = FastAPI()
12
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
@@ -17,205 +16,424 @@ connected_clients = set()
17
  BASE_DIR = os.path.abspath("/app")
18
 
19
  # -----------------
20
- # HTML FRONTEND
21
  # -----------------
22
  HTML_CONTENT = """
23
  <!DOCTYPE html>
24
- <html lang="en">
25
  <head>
26
  <meta charset="UTF-8">
27
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
28
- <title>Server Control Panel</title>
29
  <script src="https://cdn.tailwindcss.com"></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 rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
34
  <style>
35
- .terminal-container { height: calc(100vh - 180px); width: 100%; padding: 10px; background: #1e1d23; border-radius: 8px;}
36
- body { background-color: #0f172a; color: #f8fafc; }
37
- .hidden-tab { display: none; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  </style>
39
  </head>
40
- <body class="flex flex-col h-screen font-sans">
41
- <!-- Navbar -->
42
- <nav class="bg-slate-800 border-b border-slate-700 px-6 py-4 flex justify-between items-center shadow-lg">
43
- <div class="text-xl font-bold flex items-center gap-2"><i class="fa-solid fa-server text-blue-500"></i> Server Panel</div>
44
- <div class="flex gap-4">
45
- <button onclick="switchTab('console')" id="btn-console" class="px-4 py-2 bg-blue-600 rounded-lg font-semibold shadow hover:bg-blue-500 transition"><i class="fa-solid fa-terminal"></i> Console</button>
46
- <button onclick="switchTab('files')" id="btn-files" class="px-4 py-2 bg-slate-700 rounded-lg font-semibold shadow hover:bg-slate-600 transition"><i class="fa-solid fa-folder"></i> Files</button>
 
 
 
 
 
 
 
 
 
 
 
 
47
  </div>
48
  </nav>
49
 
50
- <!-- Main Content -->
51
- <main class="flex-grow p-4 md:p-6 overflow-hidden flex flex-col">
 
52
  <!-- Console Tab -->
53
- <div id="tab-console" class="flex flex-col h-full w-full max-w-6xl mx-auto">
54
- <div id="terminal" class="terminal-container shadow-2xl"></div>
55
- <div class="mt-4 flex gap-2">
56
- <input type="text" id="cmd-input" class="flex-grow bg-slate-800 border border-slate-600 rounded-lg px-4 py-3 focus:outline-none focus:border-blue-500 shadow-inner" placeholder="Type a console command and press Enter...">
57
- <button onclick="sendCommand()" class="bg-blue-600 px-6 py-3 rounded-lg font-bold hover:bg-blue-500 transition shadow"><i class="fa-solid fa-paper-plane"></i></button>
 
 
 
 
 
 
 
 
 
 
 
 
58
  </div>
59
  </div>
60
 
61
  <!-- File Manager Tab -->
62
- <div id="tab-files" class="hidden-tab flex flex-col h-full w-full max-w-6xl mx-auto bg-slate-800 rounded-lg shadow-xl overflow-hidden border border-slate-700">
63
- <div class="bg-slate-900 px-4 py-3 flex justify-between items-center border-b border-slate-700">
64
- <div class="flex items-center gap-2 text-sm md:text-base font-mono bg-slate-800 px-3 py-1 rounded text-green-400" id="breadcrumbs">/app</div>
65
- <div class="flex gap-2">
66
- <input type="file" id="file-upload" class="hidden" onchange="uploadFile()">
67
- <button onclick="document.getElementById('file-upload').click()" class="bg-green-600 px-3 py-1 md:px-4 md:py-2 rounded text-sm font-bold hover:bg-green-500 transition"><i class="fa-solid fa-upload"></i> Upload</button>
68
- <button onclick="loadFiles(currentPath)" class="bg-slate-700 px-3 py-1 md:px-4 md:py-2 rounded text-sm font-bold hover:bg-slate-600 transition"><i class="fa-solid fa-rotate-right"></i> Refresh</button>
 
 
 
 
 
 
 
69
  </div>
70
  </div>
71
- <div class="overflow-y-auto flex-grow p-0">
72
- <table class="w-full text-left border-collapse">
73
- <thead class="bg-slate-900 sticky top-0 shadow">
74
- <tr>
75
- <th class="p-3 text-slate-300">Name</th>
76
- <th class="p-3 text-slate-300 w-24 md:w-32">Size</th>
77
- <th class="p-3 text-slate-300 w-32 md:w-48 text-right">Actions</th>
78
- </tr>
79
- </thead>
80
- <tbody id="file-list" class="divide-y divide-slate-700"></tbody>
81
- </table>
82
  </div>
83
  </div>
84
  </main>
85
 
86
- <!-- Editor Modal -->
87
- <div id="editor-modal" class="fixed inset-0 bg-black/80 hidden items-center justify-center p-4 z-50">
88
- <div class="bg-slate-800 rounded-xl w-full max-w-4xl h-[80vh] flex flex-col overflow-hidden border border-slate-600 shadow-2xl">
89
- <div class="bg-slate-900 p-3 flex justify-between items-center border-b border-slate-700">
90
- <h3 class="font-mono text-green-400" id="editor-title">file.txt</h3>
91
- <div class="flex gap-2">
92
- <button onclick="saveFile()" class="bg-blue-600 px-4 py-1 rounded hover:bg-blue-500 font-bold">Save</button>
93
- <button onclick="closeEditor()" class="bg-slate-700 px-4 py-1 rounded hover:bg-slate-600 font-bold">Close</button>
 
 
 
 
 
94
  </div>
95
  </div>
96
- <textarea id="editor-content" class="flex-grow bg-[#1e1e1e] text-slate-200 p-4 font-mono text-sm resize-none focus:outline-none" spellcheck="false"></textarea>
97
  </div>
98
  </div>
99
 
 
 
 
100
  <script>
101
- // --- UI Logic ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  function switchTab(tab) {
103
  document.getElementById('tab-console').classList.add('hidden-tab');
104
  document.getElementById('tab-files').classList.add('hidden-tab');
105
- document.getElementById('btn-console').classList.replace('bg-blue-600', 'bg-slate-700');
106
- document.getElementById('btn-files').classList.replace('bg-blue-600', 'bg-slate-700');
 
107
 
108
  document.getElementById('tab-' + tab).classList.remove('hidden-tab');
109
- document.getElementById('btn-' + tab).classList.replace('bg-slate-700', 'bg-blue-600');
 
 
 
110
 
111
- if(tab === 'console' && fitAddon) setTimeout(() => fitAddon.fit(), 100);
112
- if(tab === 'files') loadFiles(currentPath);
113
  }
114
 
115
  // --- Terminal Logic ---
116
- const term = new Terminal({ theme: { background: '#1e1d23' }, convertEol: true });
 
 
 
117
  const fitAddon = new FitAddon.FitAddon();
118
  term.loadAddon(fitAddon);
119
  term.open(document.getElementById('terminal'));
120
- fitAddon.fit();
121
- window.addEventListener('resize', () => fitAddon.fit());
 
 
122
 
123
  const wsUrl = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws';
124
- const ws = new WebSocket(wsUrl);
125
- ws.onmessage = e => term.write(e.data + '\\n');
 
 
 
 
 
 
 
126
 
127
  const cmdInput = document.getElementById('cmd-input');
128
- cmdInput.addEventListener('keypress', e => {
129
- if (e.key === 'Enter' && cmdInput.value.trim() !== '') {
130
- sendCommand();
131
- }
132
- });
133
 
134
  function sendCommand() {
135
- if(cmdInput.value) { ws.send(cmdInput.value); cmdInput.value = ''; }
 
 
 
 
 
136
  }
137
 
138
  // --- File Manager Logic ---
139
  let currentPath = '';
 
140
  let editingFilePath = '';
141
 
142
- async function loadFiles(path) {
143
- currentPath = path;
144
- document.getElementById('breadcrumbs').innerText = '/app' + (path ? '/' + path : '');
145
- const res = await fetch(`/api/fs/list?path=${encodeURIComponent(path)}`);
146
- const files = await res.json();
147
- const list = document.getElementById('file-list');
148
- list.innerHTML = '';
149
 
150
- if (path !== '') {
151
- const parent = path.split('/').slice(0, -1).join('/');
152
- list.innerHTML += `<tr class="hover:bg-slate-700/50 cursor-pointer transition" onclick="loadFiles('${parent}')">
153
- <td class="p-3"><i class="fa-solid fa-level-up-alt text-slate-400 mr-2"></i> ..</td>
154
- <td></td><td></td>
155
- </tr>`;
 
 
 
 
156
  }
 
 
 
157
 
158
- files.forEach(f => {
159
- const icon = f.is_dir ? '<i class="fa-solid fa-folder text-blue-400"></i>' : '<i class="fa-solid fa-file text-slate-400"></i>';
160
- const size = f.is_dir ? '-' : (f.size / 1024).toFixed(1) + ' KB';
161
- const actionClick = f.is_dir ? `onclick="loadFiles('${path ? path+'/'+f.name : f.name}')"` : '';
162
-
163
- let row = `<tr class="hover:bg-slate-700/50 transition border-t border-slate-700">
164
- <td class="p-3 font-mono text-sm cursor-pointer" ${actionClick}>${icon} &nbsp;${f.name}</td>
165
- <td class="p-3 text-slate-400 text-sm">${size}</td>
166
- <td class="p-3 text-right">`;
 
 
 
167
 
168
- if (!f.is_dir) {
169
- row += `<button onclick="editFile('${path ? path+'/'+f.name : f.name}')" class="text-blue-400 hover:text-blue-300 mx-2" title="Edit"><i class="fa-solid fa-edit"></i></button>`;
170
- row += `<a href="/api/fs/download?path=${encodeURIComponent(path ? path+'/'+f.name : f.name)}" class="text-green-400 hover:text-green-300 mx-2" title="Download"><i class="fa-solid fa-download"></i></a>`;
 
 
 
 
 
 
171
  }
172
- row += `<button onclick="deleteFile('${path ? path+'/'+f.name : f.name}')" class="text-red-400 hover:text-red-300 ml-2" title="Delete"><i class="fa-solid fa-trash"></i></button>`;
173
- row += `</td></tr>`;
174
- list.innerHTML += row;
175
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  }
177
 
178
  async function editFile(path) {
179
- editingFilePath = path;
180
- const res = await fetch(`/api/fs/read?path=${encodeURIComponent(path)}`);
181
- if(res.ok) {
182
- const text = await res.text();
183
- document.getElementById('editor-content').value = text;
184
- document.getElementById('editor-title').innerText = path;
185
- document.getElementById('editor-modal').classList.replace('hidden', 'flex');
186
- } else {
187
- alert('Cannot read file (might not be text)');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  }
189
  }
190
 
191
- function closeEditor() { document.getElementById('editor-modal').classList.replace('flex', 'hidden'); }
 
 
 
 
 
 
 
 
 
192
 
193
  async function saveFile() {
 
 
 
 
 
194
  const content = document.getElementById('editor-content').value;
195
  const formData = new FormData();
196
  formData.append('path', editingFilePath);
197
  formData.append('content', content);
198
- const res = await fetch('/api/fs/write', { method: 'POST', body: formData });
199
- if(res.ok) { closeEditor(); } else { alert('Failed to save file.'); }
 
 
 
 
 
 
 
 
 
 
 
200
  }
201
 
202
  async function deleteFile(path) {
203
- if(confirm('Are you sure you want to delete ' + path + '?')) {
204
  const formData = new FormData(); formData.append('path', path);
205
- await fetch('/api/fs/delete', { method: 'POST', body: formData });
206
- loadFiles(currentPath);
 
 
 
 
 
 
 
207
  }
208
  }
209
 
210
- async function uploadFile() {
211
- const fileInput = document.getElementById('file-upload');
212
  if(!fileInput.files.length) return;
 
 
 
213
  const formData = new FormData();
214
  formData.append('path', currentPath);
215
  formData.append('file', fileInput.files[0]);
216
- await fetch('/api/fs/upload', { method: 'POST', body: formData });
 
 
 
 
 
 
 
 
 
217
  fileInput.value = '';
218
- loadFiles(currentPath);
219
  }
220
  </script>
221
  </body>
@@ -247,10 +465,13 @@ async def broadcast(message: str):
247
  # -----------------
248
  async def read_stream(stream, prefix=""):
249
  while True:
250
- line = await stream.readline()
251
- if not line: break
252
- line_str = line.decode('utf-8', errors='replace').rstrip('\r\n')
253
- await broadcast(prefix + line_str)
 
 
 
254
 
255
  async def start_minecraft():
256
  global mc_process
@@ -316,8 +537,12 @@ def fs_list(path: str = ""):
316
  def fs_read(path: str):
317
  target = get_safe_path(path)
318
  if not os.path.isfile(target): raise HTTPException(400, "Not a file")
319
- with open(target, 'r', encoding='utf-8', errors='ignore') as f:
320
- return Response(content=f.read(), media_type="text/plain")
 
 
 
 
321
 
322
  @app.get("/api/fs/download")
323
  def fs_download(path: str):
@@ -348,5 +573,4 @@ def fs_delete(path: str = Form(...)):
348
  return {"status": "ok"}
349
 
350
  if __name__ == "__main__":
351
- # Binds Web UI to Port 7860 to satisfy Hugging Face HTTP Health Checks!
352
  uvicorn.run(app, host="0.0.0.0", port=7860, log_level="warning")
 
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
  BASE_DIR = os.path.abspath("/app")
17
 
18
  # -----------------
19
+ # HTML FRONTEND (Ultra-Modern UI)
20
  # -----------------
21
  HTML_CONTENT = """
22
  <!DOCTYPE html>
23
+ <html lang="en" class="dark">
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
 
246
  const cmdInput = document.getElementById('cmd-input');
247
+ cmdInput.addEventListener('keypress', e => { if (e.key === 'Enter') sendCommand(); });
 
 
 
 
248
 
249
  function sendCommand() {
250
+ const val = cmdInput.value.trim();
251
+ if(val && ws && ws.readyState === WebSocket.OPEN) {
252
+ term.write(`\\x1b[90m> ${val}\\x1b[0m\\r\\n`);
253
+ ws.send(val);
254
+ cmdInput.value = '';
255
+ }
256
  }
257
 
258
  // --- File Manager Logic ---
259
  let currentPath = '';
260
+ let currentPathLoaded = false;
261
  let editingFilePath = '';
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)}`);
292
+ if(!res.ok) throw new Error('Failed to load');
293
+ const files = await res.json();
294
+ list.innerHTML = '';
295
 
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
 
339
  async function editFile(path) {
340
+ try {
341
+ const res = await fetch(`/api/fs/read?path=${encodeURIComponent(path)}`);
342
+ if(res.ok) {
343
+ const text = await res.text();
344
+ editingFilePath = path;
345
+ document.getElementById('editor-content').value = text;
346
+ document.getElementById('editor-title').innerText = path.split('/').pop();
347
+
348
+ const modal = document.getElementById('editor-modal');
349
+ const card = document.getElementById('editor-card');
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() {
367
+ const modal = document.getElementById('editor-modal');
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
 
417
+ async function uploadFile(e) {
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>
439
  </body>
 
465
  # -----------------
466
  async def read_stream(stream, prefix=""):
467
  while True:
468
+ try:
469
+ line = await stream.readline()
470
+ if not line: break
471
+ line_str = line.decode('utf-8', errors='replace').rstrip('\r\n')
472
+ await broadcast(prefix + line_str)
473
+ except Exception:
474
+ break
475
 
476
  async def start_minecraft():
477
  global mc_process
 
537
  def fs_read(path: str):
538
  target = get_safe_path(path)
539
  if not os.path.isfile(target): raise HTTPException(400, "Not a file")
540
+ try:
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):
 
573
  return {"status": "ok"}
574
 
575
  if __name__ == "__main__":
 
576
  uvicorn.run(app, host="0.0.0.0", port=7860, log_level="warning")