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

Create panel.py

Browse files
Files changed (1) hide show
  1. panel.py +352 -0
panel.py ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import asyncio
3
+ import collections
4
+ from fastapi import FastAPI, WebSocket, Request, Response, Form, UploadFile, File, HTTPException
5
+ from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
6
+ from fastapi.middleware.cors import CORSMiddleware
7
+ import uvicorn
8
+ import shutil
9
+ from datetime import datetime
10
+
11
+ 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
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>
222
+ </html>
223
+ """
224
+
225
+ # -----------------
226
+ # UTILITIES
227
+ # -----------------
228
+ def get_safe_path(subpath: str):
229
+ subpath = (subpath or "").strip("/")
230
+ target = os.path.abspath(os.path.join(BASE_DIR, subpath))
231
+ if not target.startswith(BASE_DIR):
232
+ raise HTTPException(status_code=403, detail="Access denied outside /app")
233
+ return target
234
+
235
+ async def broadcast(message: str):
236
+ output_history.append(message)
237
+ dead_clients = set()
238
+ for client in connected_clients:
239
+ try:
240
+ await client.send_text(message)
241
+ except:
242
+ dead_clients.add(client)
243
+ connected_clients.difference_update(dead_clients)
244
+
245
+ # -----------------
246
+ # SERVER PROCESSES
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
257
+ java_args = [
258
+ "java", "-server", "-Xmx8G", "-Xms8G", "-XX:+UseG1GC", "-XX:+ParallelRefProcEnabled",
259
+ "-XX:ParallelGCThreads=2", "-XX:ConcGCThreads=1", "-XX:MaxGCPauseMillis=50",
260
+ "-XX:+UnlockExperimentalVMOptions", "-XX:+DisableExplicitGC", "-XX:+AlwaysPreTouch",
261
+ "-XX:G1NewSizePercent=30", "-XX:G1MaxNewSizePercent=50", "-XX:G1HeapRegionSize=16M",
262
+ "-XX:G1ReservePercent=15", "-XX:G1HeapWastePercent=5", "-XX:G1MixedGCCountTarget=3",
263
+ "-XX:InitiatingHeapOccupancyPercent=10", "-XX:G1MixedGCLiveThresholdPercent=90",
264
+ "-XX:G1RSetUpdatingPauseTimePercent=5", "-XX:SurvivorRatio=32", "-XX:+PerfDisableSharedMem",
265
+ "-XX:MaxTenuringThreshold=1", "-XX:G1SATBBufferEnqueueingThresholdPercent=30",
266
+ "-XX:G1ConcMarkStepDurationMillis=5", "-XX:G1ConcRSHotCardLimit=16",
267
+ "-XX:+UseStringDeduplication", "-Dfile.encoding=UTF-8", "-Dspring.output.ansi.enabled=ALWAYS",
268
+ "-jar", "purpur.jar", "--nogui"
269
+ ]
270
+ mc_process = await asyncio.create_subprocess_exec(
271
+ *java_args,
272
+ stdin=asyncio.subprocess.PIPE,
273
+ stdout=asyncio.subprocess.PIPE,
274
+ stderr=asyncio.subprocess.STDOUT,
275
+ cwd=BASE_DIR
276
+ )
277
+ asyncio.create_task(read_stream(mc_process.stdout))
278
+
279
+ @app.on_event("startup")
280
+ async def startup_event():
281
+ asyncio.create_task(start_minecraft())
282
+
283
+ # -----------------
284
+ # API ROUTING
285
+ # -----------------
286
+ @app.get("/")
287
+ def get_panel():
288
+ return HTMLResponse(content=HTML_CONTENT)
289
+
290
+ @app.websocket("/ws")
291
+ async def websocket_endpoint(websocket: WebSocket):
292
+ await websocket.accept()
293
+ connected_clients.add(websocket)
294
+ for line in output_history:
295
+ await websocket.send_text(line)
296
+ try:
297
+ while True:
298
+ cmd = await websocket.receive_text()
299
+ if mc_process and mc_process.stdin:
300
+ mc_process.stdin.write((cmd + "\n").encode('utf-8'))
301
+ await mc_process.stdin.drain()
302
+ except:
303
+ connected_clients.remove(websocket)
304
+
305
+ @app.get("/api/fs/list")
306
+ def fs_list(path: str = ""):
307
+ target = get_safe_path(path)
308
+ if not os.path.exists(target): return []
309
+ items = []
310
+ for f in os.listdir(target):
311
+ fp = os.path.join(target, f)
312
+ items.append({"name": f, "is_dir": os.path.isdir(fp), "size": os.path.getsize(fp) if not os.path.isdir(fp) else 0})
313
+ return sorted(items, key=lambda x: (not x["is_dir"], x["name"].lower()))
314
+
315
+ @app.get("/api/fs/read")
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):
324
+ target = get_safe_path(path)
325
+ if not os.path.isfile(target): raise HTTPException(400, "Not a file")
326
+ return FileResponse(target, filename=os.path.basename(target))
327
+
328
+ @app.post("/api/fs/write")
329
+ def fs_write(path: str = Form(...), content: str = Form(...)):
330
+ target = get_safe_path(path)
331
+ with open(target, 'w', encoding='utf-8') as f:
332
+ f.write(content)
333
+ return {"status": "ok"}
334
+
335
+ @app.post("/api/fs/upload")
336
+ async def fs_upload(path: str = Form(""), file: UploadFile = File(...)):
337
+ target_dir = get_safe_path(path)
338
+ target_file = os.path.join(target_dir, file.filename)
339
+ with open(target_file, "wb") as buffer:
340
+ shutil.copyfileobj(file.file, buffer)
341
+ return {"status": "ok"}
342
+
343
+ @app.post("/api/fs/delete")
344
+ def fs_delete(path: str = Form(...)):
345
+ target = get_safe_path(path)
346
+ if os.path.isdir(target): shutil.rmtree(target)
347
+ else: os.remove(target)
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")