OrbitMC commited on
Commit
0de7f14
·
verified ·
1 Parent(s): dd25227

Update panel.py

Browse files
Files changed (1) hide show
  1. panel.py +487 -1552
panel.py CHANGED
@@ -10,1488 +10,464 @@ import uvicorn
10
  app = FastAPI()
11
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
12
 
13
- # Global State
14
  mc_process = None
15
  output_history = collections.deque(maxlen=300)
16
  connected_clients = set()
 
17
 
18
- # Configuration
19
- # Change this to "." if running locally on Windows/Linux outside of Docker
20
- BASE_DIR = os.path.abspath(".")
21
- SERVER_JAR = "purpur.jar" # Change this to your jar name
22
-
23
- # -----------------
24
- # HTML FRONTEND (Your Design + Wired Logic)
25
- # -----------------
26
- HTML_CONTENT = """
27
- <!DOCTYPE html>
28
  <html lang="en">
29
  <head>
30
- <meta charset="UTF-8">
31
- <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
32
- <title>OrbitMC Server Panel</title>
 
 
33
  <style>
34
- *,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
35
  :root{
36
- --bg:#0a0a0f;
37
- --surface:#12121a;
38
- --surface2:#1a1a26;
39
- --surface3:#222233;
40
- --border:#2a2a3a;
41
- --border2:#333346;
42
- --text:#e8e8f0;
43
- --text2:#9999b0;
44
- --text3:#666680;
45
- --accent:#6c5ce7;
46
- --accent2:#7c6ef7;
47
- --accent-glow:rgba(108,92,231,.15);
48
- --green:#00d67e;
49
- --red:#ff4757;
50
- --orange:#ffa502;
51
- --yellow:#ffc312;
52
- --radius:12px;
53
- --radius-sm:8px;
54
- --radius-xs:6px;
55
- --transition:all .2s cubic-bezier(.4,0,.2,1);
56
- --shadow:0 4px 24px rgba(0,0,0,.4);
57
- --shadow-lg:0 8px 40px rgba(0,0,0,.6);
58
- }
59
-
60
- html{font-size:14px;-webkit-tap-highlight-color:transparent}
61
- body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--bg);color:var(--text);overflow:hidden;height:100dvh;width:100vw;display:flex;flex-direction:column}
62
-
63
- /* Scrollbar */
64
- ::-webkit-scrollbar{width:6px;height:6px}
65
- ::-webkit-scrollbar-track{background:transparent}
66
- ::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
67
- ::-webkit-scrollbar-thumb:hover{background:var(--border2)}
68
-
69
- /* Animations */
70
- @keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
71
- @keyframes fadeInScale{from{opacity:0;transform:scale(.95) translateY(8px)}to{opacity:1;transform:scale(1) translateY(0)}}
72
- @keyframes slideUp{from{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}
73
- @keyframes slideRight{from{opacity:0;transform:translateX(-20px)}to{opacity:1;transform:translateX(0)}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  @keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
75
- @keyframes spin{to{transform:rotate(360deg)}}
76
- @keyframes ripple{to{transform:scale(4);opacity:0}}
77
- @keyframes shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}
78
- @keyframes blink{0%,50%{opacity:1}51%,100%{opacity:0}}
79
- @keyframes toast-in{from{opacity:0;transform:translateX(100%)}to{opacity:1;transform:translateX(0)}}
80
- @keyframes toast-out{from{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(100%)}}
81
-
82
- /* Header */
83
- .header{
84
- display:flex;align-items:center;justify-content:space-between;
85
- padding:12px 20px;
86
- background:var(--surface);
87
- border-bottom:1px solid var(--border);
88
- flex-shrink:0;
89
- z-index:100;
90
- backdrop-filter:blur(20px);
91
- -webkit-backdrop-filter:blur(20px);
92
- }
93
- .logo{display:flex;align-items:center;gap:10px;font-weight:700;font-size:1.2rem;letter-spacing:-.02em}
94
- .logo-icon{
95
- width:32px;height:32px;
96
- background:linear-gradient(135deg,var(--accent),#a855f7);
97
- border-radius:8px;
98
- display:flex;align-items:center;justify-content:center;
99
- font-size:14px;font-weight:800;color:#fff;
100
- box-shadow:0 2px 12px rgba(108,92,231,.4);
101
- position:relative;
102
- overflow:hidden;
103
- }
104
- .logo-icon::after{
105
- content:'';position:absolute;inset:0;
106
- background:linear-gradient(135deg,transparent 40%,rgba(255,255,255,.15) 50%,transparent 60%);
107
- animation:shimmer 3s infinite;
108
- background-size:200% 100%;
109
- }
110
- .server-status{display:flex;align-items:center;gap:8px;font-size:.8rem;color:var(--text2)}
111
- .status-dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);animation:pulse 2s infinite}
112
- .status-dot.offline{background:var(--red);box-shadow:0 0 8px var(--red)}
113
-
114
- /* Nav */
115
- .nav{
116
- display:flex;
117
- gap:2px;
118
- padding:8px 12px;
119
- background:var(--surface);
120
- border-bottom:1px solid var(--border);
121
- flex-shrink:0;
122
- overflow-x:auto;
123
- -webkit-overflow-scrolling:touch;
124
- scrollbar-width:none;
125
- }
126
- .nav::-webkit-scrollbar{display:none}
127
- .nav-item{
128
- position:relative;
129
- display:flex;align-items:center;gap:8px;
130
- padding:10px 16px;
131
- border-radius:var(--radius-sm);
132
- font-size:.82rem;font-weight:500;
133
- color:var(--text2);
134
- cursor:pointer;
135
- transition:var(--transition);
136
- white-space:nowrap;
137
- user-select:none;
138
- -webkit-user-select:none;
139
- border:none;background:none;
140
- flex-shrink:0;
141
- }
142
- .nav-item:hover{color:var(--text);background:var(--surface2)}
143
- .nav-item.active{color:var(--text);background:var(--accent-glow)}
144
- .nav-item.active::after{
145
- content:'';position:absolute;bottom:0;left:50%;transform:translateX(-50%);
146
- width:20px;height:2px;background:var(--accent);border-radius:1px;
147
- }
148
- .nav-item svg{width:16px;height:16px;flex-shrink:0}
149
- .nav-badge{
150
- font-size:.6rem;padding:2px 6px;
151
- background:var(--surface3);color:var(--text3);
152
- border-radius:4px;font-weight:600;text-transform:uppercase;
153
- letter-spacing:.05em;
154
- }
155
- .nav-item.disabled{opacity:.4;pointer-events:none}
156
-
157
- /* Content */
158
- .content{flex:1;overflow:hidden;position:relative}
159
- .panel{display:none;height:100%;flex-direction:column;animation:fadeIn .3s ease}
160
- .panel.active{display:flex}
161
-
162
- /* Console */
163
- .console-wrap{flex:1;display:flex;flex-direction:column;position:relative;overflow:hidden}
164
- .console-terminal{
165
- flex:1;
166
- overflow-y:auto;
167
- padding:16px;
168
- font-family:'JetBrains Mono','Fira Code','Cascadia Code',monospace;
169
- font-size:.78rem;
170
- line-height:1.7;
171
- position:relative;
172
- scroll-behavior:smooth;
173
- overscroll-behavior:contain;
174
- }
175
- .console-terminal::before{
176
- content:'';position:sticky;top:0;left:0;right:0;
177
- display:block;height:40px;margin-bottom:-40px;
178
- background:linear-gradient(var(--bg),transparent);
179
- pointer-events:none;z-index:2;
180
- }
181
- .console-line{
182
- word-wrap:break-word;
183
- overflow-wrap:break-word;
184
- white-space:pre-wrap;
185
- padding:1px 0;
186
- animation:slideRight .15s ease;
187
- }
188
- .console-line .time{color:var(--text3);margin-right:8px;font-size:.72rem}
189
- .console-line .info{color:#5b9bd5}
190
- .console-line .warn{color:var(--orange)}
191
- .console-line .error{color:var(--red)}
192
- .console-line .server{color:var(--green)}
193
- .console-line .player{color:var(--yellow)}
194
- .console-line .cmd{color:var(--accent2)}
195
-
196
- .console-input-wrap{
197
- display:flex;gap:8px;
198
- padding:12px 16px;
199
- background:var(--surface);
200
- border-top:1px solid var(--border);
201
- flex-shrink:0;
202
- }
203
- .console-input-prefix{
204
- color:var(--accent);font-family:'JetBrains Mono',monospace;
205
- font-size:.82rem;display:flex;align-items:center;
206
- user-select:none;font-weight:600;flex-shrink:0;
207
- }
208
- .console-input{
209
- flex:1;background:none;border:none;outline:none;
210
- color:var(--text);font-family:'JetBrains Mono',monospace;
211
- font-size:.82rem;caret-color:var(--accent);
212
- }
213
- .console-input::placeholder{color:var(--text3)}
214
- .console-send-btn{
215
- background:var(--accent);color:#fff;border:none;
216
- padding:8px 16px;border-radius:var(--radius-xs);
217
- cursor:pointer;font-size:.78rem;font-weight:600;
218
- transition:var(--transition);display:flex;align-items:center;gap:6px;
219
- flex-shrink:0;
220
- }
221
- .console-send-btn:hover{background:var(--accent2);transform:translateY(-1px)}
222
- .console-send-btn:active{transform:translateY(0)}
223
- .console-send-btn svg{width:14px;height:14px}
224
-
225
- /* File Manager */
226
- .fm{display:flex;flex-direction:column;height:100%}
227
- .fm-toolbar{
228
- display:flex;align-items:center;gap:8px;
229
- padding:10px 16px;
230
- background:var(--surface);
231
- border-bottom:1px solid var(--border);
232
- flex-shrink:0;
233
- flex-wrap:wrap;
234
- }
235
- .fm-breadcrumb{
236
- display:flex;align-items:center;gap:2px;
237
- flex:1;min-width:0;
238
- overflow-x:auto;
239
- scrollbar-width:none;
240
- -webkit-overflow-scrolling:touch;
241
- padding:4px 0;
242
- }
243
- .fm-breadcrumb::-webkit-scrollbar{display:none}
244
- .fm-crumb{
245
- padding:4px 8px;border-radius:var(--radius-xs);
246
- font-size:.78rem;color:var(--text2);
247
- cursor:pointer;transition:var(--transition);
248
- white-space:nowrap;flex-shrink:0;
249
- background:none;border:none;
250
- }
251
- .fm-crumb:hover{color:var(--text);background:var(--surface2)}
252
- .fm-crumb.active{color:var(--text);font-weight:600}
253
- .fm-crumb-sep{color:var(--text3);font-size:.7rem;flex-shrink:0}
254
- .fm-actions{display:flex;gap:4px;flex-shrink:0}
255
- .fm-btn{
256
- display:flex;align-items:center;justify-content:center;gap:6px;
257
- padding:8px 12px;border-radius:var(--radius-xs);
258
- font-size:.75rem;font-weight:500;
259
- color:var(--text2);background:var(--surface2);
260
- border:1px solid var(--border);cursor:pointer;
261
- transition:var(--transition);white-space:nowrap;
262
- position:relative;overflow:hidden;
263
- }
264
- .fm-btn:hover{color:var(--text);background:var(--surface3);border-color:var(--border2)}
265
- .fm-btn:active{transform:scale(.97)}
266
- .fm-btn svg{width:14px;height:14px;flex-shrink:0}
267
- .fm-btn.accent{background:var(--accent);color:#fff;border-color:var(--accent)}
268
- .fm-btn.accent:hover{background:var(--accent2)}
269
- .fm-btn .btn-label{display:inline}
270
-
271
- .fm-list{flex:1;overflow-y:auto;padding:8px;overscroll-behavior:contain}
272
- .fm-item{
273
- display:flex;align-items:center;gap:12px;
274
- padding:10px 14px;border-radius:var(--radius-sm);
275
- cursor:pointer;transition:var(--transition);
276
- position:relative;
277
- animation:fadeIn .2s ease;
278
- user-select:none;
279
- -webkit-user-select:none;
280
- border:1px solid transparent;
281
- }
282
- .fm-item:hover{background:var(--surface2);border-color:var(--border)}
283
- .fm-item:active{background:var(--surface3)}
284
- .fm-item.selected{background:var(--accent-glow);border-color:rgba(108,92,231,.3)}
285
- .fm-item-icon{
286
- width:36px;height:36px;border-radius:var(--radius-xs);
287
- display:flex;align-items:center;justify-content:center;
288
- flex-shrink:0;font-size:.75rem;
289
- }
290
- .fm-item-icon.folder{background:rgba(255,165,2,.1);color:var(--orange)}
291
- .fm-item-icon.file{background:rgba(108,92,231,.1);color:var(--accent)}
292
- .fm-item-icon.jar{background:rgba(0,214,126,.1);color:var(--green)}
293
- .fm-item-icon.yml{background:rgba(91,155,213,.1);color:#5b9bd5}
294
- .fm-item-icon.log{background:rgba(255,71,87,.1);color:var(--red)}
295
- .fm-item-icon.json{background:rgba(255,195,18,.1);color:var(--yellow)}
296
- .fm-item-icon.properties{background:rgba(168,85,247,.1);color:#a855f7}
297
- .fm-item-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
298
- .fm-item-name{font-size:.82rem;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
299
- .fm-item-meta{font-size:.7rem;color:var(--text3);display:flex;gap:12px}
300
- .fm-item-actions{
301
- display:flex;gap:2px;
302
- opacity:0;transition:var(--transition);flex-shrink:0;
303
- }
304
- .fm-item:hover .fm-item-actions{opacity:1}
305
- .fm-action-btn{
306
- width:30px;height:30px;border-radius:var(--radius-xs);
307
- display:flex;align-items:center;justify-content:center;
308
- background:none;border:none;color:var(--text3);
309
- cursor:pointer;transition:var(--transition);
310
- }
311
- .fm-action-btn:hover{color:var(--text);background:var(--surface3)}
312
- .fm-action-btn.danger:hover{color:var(--red);background:rgba(255,71,87,.1)}
313
- .fm-action-btn svg{width:14px;height:14px}
314
-
315
- .fm-empty{
316
- display:flex;flex-direction:column;align-items:center;justify-content:center;
317
- height:100%;color:var(--text3);gap:12px;
318
- }
319
- .fm-empty svg{width:48px;height:48px;opacity:.3}
320
- .fm-empty-text{font-size:.85rem}
321
-
322
- /* Mobile file actions */
323
- .fm-mobile-actions{display:none}
324
-
325
- /* Config Panel */
326
- .config-wrap{display:flex;flex-direction:column;height:100%;overflow:hidden}
327
- .config-header{
328
- display:flex;align-items:center;justify-content:space-between;
329
- padding:16px 20px;
330
- background:var(--surface);
331
- border-bottom:1px solid var(--border);
332
- flex-shrink:0;
333
- }
334
- .config-title{font-size:.9rem;font-weight:600;display:flex;align-items:center;gap:8px}
335
- .config-title svg{width:18px;height:18px;color:var(--accent)}
336
- .config-body{flex:1;overflow-y:auto;padding:16px 20px;overscroll-behavior:contain}
337
- .config-section{margin-bottom:24px;animation:fadeIn .3s ease}
338
- .config-section-title{
339
- font-size:.72rem;font-weight:600;text-transform:uppercase;
340
- letter-spacing:.08em;color:var(--text3);margin-bottom:12px;
341
- padding-bottom:8px;border-bottom:1px solid var(--border);
342
- }
343
- .config-field{
344
- display:flex;flex-direction:column;gap:6px;
345
- padding:12px 0;
346
- border-bottom:1px solid rgba(42,42,58,.5);
347
- }
348
- .config-field:last-child{border-bottom:none}
349
- .config-label{
350
- display:flex;align-items:center;justify-content:space-between;
351
- }
352
- .config-label-text{font-size:.82rem;font-weight:500}
353
- .config-label-key{font-size:.68rem;color:var(--text3);font-family:monospace}
354
- .config-input{
355
- width:100%;padding:10px 12px;
356
- background:var(--surface2);border:1px solid var(--border);
357
- border-radius:var(--radius-xs);color:var(--text);
358
- font-size:.82rem;outline:none;transition:var(--transition);
359
- font-family:inherit;
360
- }
361
- .config-input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-glow)}
362
- .config-input::placeholder{color:var(--text3)}
363
- select.config-input{cursor:pointer;appearance:none;
364
- background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239999b0' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
365
- background-repeat:no-repeat;background-position:right 12px center;
366
- padding-right:32px;
367
- }
368
- .config-toggle{
369
- position:relative;width:44px;height:24px;flex-shrink:0;
370
- }
371
- .config-toggle input{opacity:0;width:0;height:0;position:absolute}
372
- .config-toggle-track{
373
- position:absolute;inset:0;
374
- background:var(--surface3);border-radius:12px;
375
- cursor:pointer;transition:var(--transition);
376
- border:1px solid var(--border);
377
- }
378
- .config-toggle-track::after{
379
- content:'';position:absolute;top:3px;left:3px;
380
- width:16px;height:16px;border-radius:50%;
381
- background:#fff;transition:var(--transition);
382
- box-shadow:0 1px 3px rgba(0,0,0,.3);
383
- }
384
- .config-toggle input:checked+.config-toggle-track{background:var(--accent);border-color:var(--accent)}
385
- .config-toggle input:checked+.config-toggle-track::after{transform:translateX(20px)}
386
-
387
- /* Modal / Overlay */
388
- .modal-overlay{
389
- position:fixed;inset:0;
390
- background:rgba(0,0,0,.7);
391
- backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
392
- z-index:1000;
393
- display:none;align-items:center;justify-content:center;
394
- padding:20px;
395
- opacity:0;transition:opacity .2s ease;
396
- }
397
- .modal-overlay.show{display:flex;opacity:1}
398
- .modal{
399
- background:var(--surface);
400
- border:1px solid var(--border);
401
- border-radius:var(--radius);
402
- width:100%;max-width:420px;
403
- box-shadow:var(--shadow-lg);
404
- animation:fadeInScale .25s ease;
405
- overflow:hidden;
406
- }
407
- .modal-head{
408
- display:flex;align-items:center;justify-content:space-between;
409
- padding:16px 20px;
410
- border-bottom:1px solid var(--border);
411
- }
412
- .modal-title{font-size:.9rem;font-weight:600;display:flex;align-items:center;gap:8px}
413
- .modal-close{
414
- width:28px;height:28px;border-radius:var(--radius-xs);
415
- display:flex;align-items:center;justify-content:center;
416
- background:none;border:none;color:var(--text3);cursor:pointer;
417
- transition:var(--transition);
418
- }
419
- .modal-close:hover{color:var(--text);background:var(--surface2)}
420
- .modal-close svg{width:16px;height:16px}
421
- .modal-body{padding:20px}
422
- .modal-body p{font-size:.82rem;color:var(--text2);line-height:1.6;margin-bottom:16px}
423
- .modal-input{
424
- width:100%;padding:10px 12px;
425
- background:var(--surface2);border:1px solid var(--border);
426
- border-radius:var(--radius-xs);color:var(--text);
427
- font-size:.82rem;outline:none;transition:var(--transition);
428
- font-family:inherit;
429
- }
430
- .modal-input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-glow)}
431
- .modal-input::placeholder{color:var(--text3)}
432
- .modal-foot{
433
- display:flex;gap:8px;justify-content:flex-end;
434
- padding:14px 20px;
435
- border-top:1px solid var(--border);
436
- background:rgba(0,0,0,.1);
437
- }
438
- .modal-btn{
439
- padding:8px 20px;border-radius:var(--radius-xs);
440
- font-size:.8rem;font-weight:500;cursor:pointer;
441
- transition:var(--transition);border:1px solid var(--border);
442
- background:var(--surface2);color:var(--text);
443
- }
444
- .modal-btn:hover{background:var(--surface3)}
445
- .modal-btn:active{transform:scale(.97)}
446
- .modal-btn.primary{background:var(--accent);color:#fff;border-color:var(--accent)}
447
- .modal-btn.primary:hover{background:var(--accent2)}
448
- .modal-btn.danger{background:var(--red);color:#fff;border-color:var(--red)}
449
- .modal-btn.danger:hover{background:#ff6b7a}
450
-
451
- /* Context Menu */
452
- .context-menu{
453
- position:fixed;
454
- background:var(--surface);
455
- border:1px solid var(--border);
456
- border-radius:var(--radius-sm);
457
- box-shadow:var(--shadow-lg);
458
- z-index:900;
459
- min-width:180px;
460
- padding:4px;
461
- animation:fadeInScale .15s ease;
462
- display:none;
463
- }
464
- .context-menu.show{display:block}
465
- .ctx-item{
466
- display:flex;align-items:center;gap:10px;
467
- padding:8px 12px;border-radius:var(--radius-xs);
468
- font-size:.78rem;color:var(--text2);
469
- cursor:pointer;transition:var(--transition);
470
- border:none;background:none;width:100%;text-align:left;
471
- }
472
- .ctx-item:hover{color:var(--text);background:var(--surface2)}
473
- .ctx-item.danger{color:var(--red)}
474
- .ctx-item.danger:hover{background:rgba(255,71,87,.1)}
475
- .ctx-item svg{width:14px;height:14px;flex-shrink:0}
476
- .ctx-sep{height:1px;background:var(--border);margin:4px 8px}
477
-
478
- /* Toast */
479
- .toast-container{
480
- position:fixed;top:70px;right:16px;
481
- z-index:2000;display:flex;flex-direction:column;gap:8px;
482
- pointer-events:none;
483
- }
484
- .toast{
485
- display:flex;align-items:center;gap:10px;
486
- padding:12px 16px;border-radius:var(--radius-sm);
487
- background:var(--surface);border:1px solid var(--border);
488
- box-shadow:var(--shadow);
489
- font-size:.78rem;color:var(--text);
490
- animation:toast-in .3s ease;
491
- pointer-events:auto;
492
- max-width:320px;
493
- }
494
- .toast.removing{animation:toast-out .3s ease forwards}
495
- .toast-icon{width:18px;height:18px;flex-shrink:0}
496
- .toast.success .toast-icon{color:var(--green)}
497
- .toast.error .toast-icon{color:var(--red)}
498
- .toast.warning .toast-icon{color:var(--orange)}
499
- .toast.info .toast-icon{color:var(--accent)}
500
-
501
- /* Upload area */
502
- .upload-area{
503
- border:2px dashed var(--border);
504
- border-radius:var(--radius);
505
- padding:32px;
506
- display:flex;flex-direction:column;align-items:center;justify-content:center;
507
- gap:12px;
508
- cursor:pointer;transition:var(--transition);
509
- min-height:120px;
510
- }
511
- .upload-area:hover,.upload-area.dragover{border-color:var(--accent);background:var(--accent-glow)}
512
- .upload-area svg{width:32px;height:32px;color:var(--text3)}
513
- .upload-area p{font-size:.8rem;color:var(--text2);text-align:center}
514
- .upload-area .small{font-size:.7rem;color:var(--text3)}
515
-
516
- /* Editor modal */
517
- .editor-overlay{
518
- position:fixed;inset:0;
519
- background:rgba(0,0,0,.85);
520
- backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
521
- z-index:1000;
522
- display:none;flex-direction:column;
523
- opacity:0;transition:opacity .2s ease;
524
- }
525
- .editor-overlay.show{display:flex;opacity:1}
526
- .editor-bar{
527
- display:flex;align-items:center;justify-content:space-between;
528
- padding:12px 20px;
529
- background:var(--surface);
530
- border-bottom:1px solid var(--border);
531
- flex-shrink:0;
532
- }
533
- .editor-filename{font-size:.82rem;font-weight:500;display:flex;align-items:center;gap:8px}
534
- .editor-filename svg{width:16px;height:16px;color:var(--accent)}
535
- .editor-actions{display:flex;gap:8px}
536
- .editor-textarea{
537
- flex:1;width:100%;
538
- background:var(--bg);
539
- border:none;outline:none;
540
- color:var(--text);
541
- font-family:'JetBrains Mono','Fira Code',monospace;
542
- font-size:.8rem;line-height:1.7;
543
- padding:20px;
544
- resize:none;
545
- tab-size:4;
546
- }
547
-
548
- /* Loading */
549
- .loading-bar{
550
- position:fixed;top:0;left:0;
551
- height:2px;background:linear-gradient(90deg,var(--accent),#a855f7);
552
- z-index:9999;
553
- transition:width .3s ease;
554
- box-shadow:0 0 10px var(--accent);
555
- }
556
-
557
- /* Mobile responsive */
558
- @media(max-width:768px){
559
- .header{padding:10px 14px}
560
- .logo{font-size:1rem}
561
- .logo-icon{width:28px;height:28px;font-size:12px}
562
- .server-status span{display:none}
563
- .nav{padding:6px 8px;gap:1px}
564
- .nav-item{padding:8px 12px;font-size:.78rem}
565
- .nav-item .nav-text{display:none}
566
- .nav-item svg{width:18px;height:18px}
567
-
568
- .console-terminal{padding:12px;font-size:.72rem}
569
- .console-input-wrap{padding:10px 12px}
570
- .console-send-btn span{display:none}
571
- .console-send-btn{padding:8px 12px}
572
-
573
- .fm-toolbar{padding:8px 10px;gap:4px}
574
- .fm-btn .btn-label{display:none}
575
- .fm-btn{padding:8px 10px}
576
- .fm-list{padding:4px}
577
- .fm-item{padding:10px 10px;gap:10px}
578
- .fm-item-icon{width:32px;height:32px}
579
- .fm-item-actions{opacity:1}
580
- .fm-action-btn{width:34px;height:34px}
581
-
582
- .config-body{padding:12px 14px}
583
-
584
- .modal{margin:12px;max-width:none}
585
- .toast-container{right:8px;left:8px}
586
- .toast{max-width:none}
587
- }
588
-
589
- @media(max-width:380px){
590
- .nav-badge{display:none}
591
- .fm-btn{padding:6px 8px}
592
- .fm-action-btn{width:28px;height:28px}
593
- }
594
-
595
- /* Coming soon overlay for panel */
596
- .coming-soon{
597
- flex:1;display:flex;flex-direction:column;
598
- align-items:center;justify-content:center;
599
- gap:16px;color:var(--text3);
600
- }
601
- .coming-soon svg{width:64px;height:64px;opacity:.2}
602
- .coming-soon h3{font-size:1.1rem;font-weight:600;color:var(--text2)}
603
- .coming-soon p{font-size:.82rem;max-width:300px;text-align:center;line-height:1.6}
604
-
605
- /* Ripple effect */
606
- .ripple{
607
- position:absolute;border-radius:50%;
608
- background:rgba(255,255,255,.12);
609
- transform:scale(0);animation:ripple .6s linear;
610
- pointer-events:none;
611
- }
612
-
613
- /* Drag ghost */
614
- .fm-item.dragging{opacity:.4}
615
-
616
- /* Selection highlight */
617
- ::selection{background:rgba(108,92,231,.3)}
618
  </style>
619
  </head>
620
  <body>
621
-
622
- <!-- Loading bar -->
623
- <div class="loading-bar" id="loadingBar" style="width:0"></div>
624
-
625
- <!-- Header -->
626
- <header class="header">
627
- <div class="logo">
628
- <div class="logo-icon">O</div>
629
- <span>OrbitMC</span>
630
- </div>
631
- <div class="server-status">
632
- <div class="status-dot" id="statusDot"></div>
633
- <span>Online</span>
634
- </div>
635
- </header>
636
-
637
- <!-- Navigation -->
638
- <nav class="nav" id="nav">
639
- <button class="nav-item active" data-panel="console" onclick="switchPanel('console')">
640
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
641
- <span class="nav-text">Console</span>
642
- </button>
643
- <button class="nav-item" data-panel="files" onclick="switchPanel('files')">
644
- <svg viewBox="0 0 24 24" fill="none" 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>
645
- <span class="nav-text">Files</span>
646
- </button>
647
- <button class="nav-item disabled" data-panel="plugins">
648
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="20" rx="3"/><path d="M12 8v8m-4-4h8"/></svg>
649
- <span class="nav-text">Plugins</span>
650
- <span class="nav-badge">Soon</span>
651
- </button>
652
- <button class="nav-item" data-panel="config" onclick="switchPanel('config')">
653
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
654
- <span class="nav-text">Config</span>
655
- </button>
656
  </nav>
657
 
658
- <!-- Content -->
659
- <main class="content">
660
-
661
- <!-- Console Panel -->
662
- <div class="panel active" id="panel-console">
663
- <div class="console-wrap">
664
- <div class="console-terminal" id="terminal"></div>
665
- <div class="console-input-wrap">
666
- <span class="console-input-prefix">›</span>
667
- <input class="console-input" id="cmdInput" type="text" placeholder="Enter command..." autocomplete="off" spellcheck="false">
668
- <button class="console-send-btn" onclick="sendCommand()">
669
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
670
- <span>Send</span>
671
- </button>
672
- </div>
673
- </div>
674
- </div>
675
-
676
- <!-- Files Panel -->
677
- <div class="panel" id="panel-files">
678
- <div class="fm">
679
- <div class="fm-toolbar">
680
- <div class="fm-breadcrumb" id="breadcrumb"></div>
681
- <div class="fm-actions">
682
- <button class="fm-btn" onclick="showModal('newFile')" title="New File">
683
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><line x1="9" y1="15" x2="15" y2="15"/></svg>
684
- <span class="btn-label">New File</span>
685
- </button>
686
- <button class="fm-btn" onclick="showModal('newFolder')" title="New Folder">
687
- <svg viewBox="0 0 24 24" fill="none" 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"/><line x1="12" y1="11" x2="12" y2="17"/><line x1="9" y1="14" x2="15" y2="14"/></svg>
688
- <span class="btn-label">New Folder</span>
689
- </button>
690
- <button class="fm-btn accent" onclick="showModal('upload')" title="Upload">
691
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.39 18.39A5 5 0 0018 9h-1.26A8 8 0 103 16.3"/></svg>
692
- <span class="btn-label">Upload</span>
693
- </button>
694
- </div>
695
- </div>
696
- <div class="fm-list" id="fileList"></div>
697
- </div>
698
- </div>
699
-
700
- <!-- Plugins Panel (Coming Soon) -->
701
- <div class="panel" id="panel-plugins">
702
- <div class="coming-soon">
703
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="2" width="20" height="20" rx="3"/><path d="M12 8v8m-4-4h8"/></svg>
704
- <h3>Plugins Manager</h3>
705
- <p>We're building something amazing. Plugin management is coming in the next update.</p>
706
- </div>
707
- </div>
708
-
709
- <!-- Config Panel -->
710
- <div class="panel" id="panel-config">
711
- <div class="config-wrap">
712
- <div class="config-header">
713
- <div class="config-title">
714
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
715
- server.properties
716
- </div>
717
- <button class="fm-btn accent" onclick="saveConfig()">
718
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
719
- <span class="btn-label">Save</span>
720
- </button>
721
- </div>
722
- <div class="config-body" id="configBody"></div>
723
- </div>
724
- </div>
725
-
726
- </main>
727
-
728
- <!-- Modal Overlay -->
729
- <div class="modal-overlay" id="modalOverlay" onclick="closeModalOutside(event)">
730
- <div class="modal" id="modalContent"></div>
731
- </div>
732
-
733
- <!-- Editor Overlay -->
734
- <div class="editor-overlay" id="editorOverlay">
735
- <div class="editor-bar">
736
- <div class="editor-filename" id="editorFilename">
737
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
738
- <span id="editorName"></span>
739
- </div>
740
- <div class="editor-actions">
741
- <button class="fm-btn accent" onclick="saveFile()">
742
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
743
- <span class="btn-label">Save</span>
744
- </button>
745
- <button class="fm-btn" onclick="closeEditor()">
746
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
747
- </button>
748
- </div>
749
- </div>
750
- <textarea class="editor-textarea" id="editorTextarea" spellcheck="false"></textarea>
751
- </div>
752
-
753
- <!-- Context Menu -->
754
- <div class="context-menu" id="contextMenu"></div>
755
-
756
- <!-- Toast Container -->
757
- <div class="toast-container" id="toastContainer"></div>
758
-
759
- <script>
760
- // ===== State =====
761
- let currentPath = '';
762
- let commandHistory = [];
763
- let historyIndex = -1;
764
- let currentEditFile = null;
765
- let ws = null;
766
- let serverProps = {};
767
-
768
- // ===== Console =====
769
- function initConsole() {
770
- const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
771
- ws = new WebSocket(`${proto}://${window.location.host}/ws`);
772
-
773
- ws.onopen = () => {
774
- addConsoleLine({time: getTime(), type: 'server', text: 'Connected to panel.'});
775
- document.getElementById('statusDot').classList.remove('offline');
776
- };
777
-
778
- ws.onclose = () => {
779
- addConsoleLine({time: getTime(), type: 'error', text: 'Disconnected from panel.'});
780
- document.getElementById('statusDot').classList.add('offline');
781
- setTimeout(initConsole, 3000);
782
- };
783
-
784
- ws.onmessage = (event) => {
785
- // Simple log parsing attempt
786
- const raw = event.data;
787
- let type = 'info';
788
- if (raw.toLowerCase().includes('error') || raw.toLowerCase().includes('exception')) type = 'error';
789
- else if (raw.toLowerCase().includes('warn')) type = 'warn';
790
- else if (raw.includes('<')) type = 'player';
791
-
792
- addConsoleLine({time: getTime(), type: type, text: raw});
793
- };
794
- }
795
-
796
- function getTime() {
797
- const now = new Date();
798
- return `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
799
- }
800
-
801
- function addConsoleLine(log) {
802
- const terminal = document.getElementById('terminal');
803
- const line = document.createElement('div');
804
- line.className = 'console-line';
805
- line.innerHTML = `<span class="time">${log.time}</span><span class="${log.type}">${escapeHtml(log.text)}</span>`;
806
- terminal.appendChild(line);
807
- terminal.scrollTop = terminal.scrollHeight;
808
- }
809
-
810
- function sendCommand() {
811
- const input = document.getElementById('cmdInput');
812
- const cmd = input.value.trim();
813
- if (!cmd) return;
814
-
815
- commandHistory.unshift(cmd);
816
- historyIndex = -1;
817
- input.value = '';
818
-
819
- if(ws && ws.readyState === WebSocket.OPEN) {
820
- ws.send(cmd);
821
- addConsoleLine({ time: getTime(), type: 'cmd', text: `> ${cmd}` });
822
- } else {
823
- toast('Not connected to server', 'error');
824
- }
825
- }
826
-
827
- document.getElementById('cmdInput').addEventListener('keydown', function(e) {
828
- if (e.key === 'Enter') {
829
- sendCommand();
830
- } else if (e.key === 'ArrowUp') {
831
- e.preventDefault();
832
- if (historyIndex < commandHistory.length - 1) {
833
- historyIndex++;
834
- this.value = commandHistory[historyIndex];
835
- }
836
- } else if (e.key === 'ArrowDown') {
837
- e.preventDefault();
838
- if (historyIndex > 0) {
839
- historyIndex--;
840
- this.value = commandHistory[historyIndex];
841
- } else {
842
- historyIndex = -1;
843
- this.value = '';
844
- }
845
- }
846
- });
847
-
848
- // ===== Panel Switching =====
849
- function switchPanel(name) {
850
- document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
851
- document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
852
- document.getElementById(`panel-${name}`).classList.add('active');
853
- document.querySelector(`[data-panel="${name}"]`).classList.add('active');
854
-
855
- if (name === 'files') renderFiles();
856
- if (name === 'config') renderConfig();
857
-
858
- showLoading();
859
- }
860
-
861
- // ===== Loading Bar =====
862
- function showLoading() {
863
- const bar = document.getElementById('loadingBar');
864
- bar.style.width = '0';
865
- bar.style.opacity = '1';
866
- requestAnimationFrame(() => {
867
- bar.style.width = '70%';
868
- setTimeout(() => {
869
- bar.style.width = '100%';
870
- setTimeout(() => {
871
- bar.style.opacity = '0';
872
- setTimeout(() => { bar.style.width = '0'; }, 300);
873
- }, 200);
874
- }, 300);
875
- });
876
- }
877
-
878
- // ===== File Manager =====
879
- async function renderFiles() {
880
- renderBreadcrumb();
881
- const list = document.getElementById('fileList');
882
- list.innerHTML = '';
883
-
884
- try {
885
- const res = await fetch(`/api/fs/list?path=${encodeURIComponent(currentPath)}`);
886
- const files = await res.json();
887
-
888
- if (files.length === 0) {
889
- list.innerHTML = `
890
- <div class="fm-empty">
891
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>
892
- <div class="fm-empty-text">This folder is empty</div>
893
- </div>`;
894
- return;
895
- }
896
-
897
- list.innerHTML = files.map((f, i) => {
898
- const type = f.is_dir ? 'folder' : 'file';
899
- const iconClass = getFileIconClass(f.name, type);
900
- const iconSvg = f.is_dir ? folderSvg : fileSvg;
901
- const sizeStr = f.is_dir ? '' : formatSize(f.size);
902
-
903
- return `
904
- <div class="fm-item" data-name="${f.name}" data-type="${type}"
905
- onclick="handleFileClick(event, '${f.name}', '${type}')"
906
- oncontextmenu="showContextMenu(event, '${f.name}', '${type}')"
907
- style="animation-delay:${i * 20}ms">
908
- <div class="fm-item-icon ${iconClass}">${iconSvg}</div>
909
- <div class="fm-item-info">
910
- <div class="fm-item-name">${f.name}</div>
911
- <div class="fm-item-meta">
912
- ${sizeStr ? `<span>${sizeStr}</span>` : ''}
913
- </div>
914
- </div>
915
- <div class="fm-item-actions">
916
- ${!f.is_dir ? `
917
- <button class="fm-action-btn" onclick="event.stopPropagation();editFile('${f.name}')" title="Edit">
918
- <svg viewBox="0 0 24 24" fill="none" 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.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
919
- </button>` : ''}
920
- <button class="fm-action-btn" onclick="event.stopPropagation();showModal('rename','${f.name}')" title="Rename">
921
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 3a2.828 2.828 0 114 4L7.5 20.5 2 22l1.5-5.5L17 3z"/></svg>
922
- </button>
923
- ${!f.is_dir ? `
924
- <button class="fm-action-btn" onclick="event.stopPropagation();downloadFile('${f.name}')" title="Download">
925
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
926
- </button>` : ''}
927
- <button class="fm-action-btn danger" onclick="event.stopPropagation();showModal('delete','${f.name}','${type}')" title="Delete">
928
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
929
- </button>
930
- </div>
931
- </div>`;
932
- }).join('');
933
-
934
- } catch (e) {
935
- toast('Failed to load files', 'error');
936
- }
937
- }
938
-
939
- const folderSvg = '<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>';
940
- const fileSvg = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>';
941
-
942
- function getFileIconClass(name, type) {
943
- if (type === 'folder') return 'folder';
944
- const ext = name.split('.').pop().toLowerCase();
945
- const map = { jar: 'jar', yml: 'yml', yaml: 'yml', log: 'log', gz: 'log', json: 'json', properties: 'properties' };
946
- return map[ext] || 'file';
947
- }
948
-
949
- function formatSize(bytes) {
950
- if (bytes === 0) return '0 B';
951
- const k = 1024;
952
- const sizes = ['B', 'KB', 'MB', 'GB'];
953
- const i = Math.floor(Math.log(bytes) / Math.log(k));
954
- return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
955
- }
956
-
957
- function handleFileClick(e, name, type) {
958
- if (e.target.closest('.fm-item-actions')) return;
959
- if (type === 'folder') {
960
- currentPath = currentPath ? `${currentPath}/${name}` : name;
961
- showLoading();
962
- renderFiles();
963
- } else {
964
- editFile(name);
965
- }
966
- }
967
-
968
- function renderBreadcrumb() {
969
- const bc = document.getElementById('breadcrumb');
970
- const parts = currentPath.split('/').filter(Boolean);
971
- let html = `<button class="fm-crumb ${parts.length === 0 ? 'active' : ''}" onclick="navigateTo('')">
972
- <svg style="width:14px;height:14px;vertical-align:middle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/></svg>
973
- </button>`;
974
- let path = '';
975
- parts.forEach((p, i) => {
976
- path += (path ? '/' : '') + p;
977
- const isLast = i === parts.length - 1;
978
- html += `<span class="fm-crumb-sep">›</span>
979
- <button class="fm-crumb ${isLast ? 'active' : ''}" onclick="navigateTo('${path}')">${p}</button>`;
980
- });
981
- bc.innerHTML = html;
982
- }
983
-
984
- function navigateTo(path) {
985
- currentPath = path;
986
- showLoading();
987
- renderFiles();
988
- }
989
-
990
- // ===== Context Menu =====
991
- function showContextMenu(e, name, type) {
992
- e.preventDefault();
993
- e.stopPropagation();
994
- const menu = document.getElementById('contextMenu');
995
-
996
- let items = '';
997
- if (type === 'folder') {
998
- items = `
999
- <button class="ctx-item" onclick="handleFileClick(event,'${name}','folder');hideContextMenu()">
1000
- <svg viewBox="0 0 24 24" fill="none" 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>
1001
- Open
1002
- </button>
1003
- <button class="ctx-item" onclick="showModal('rename','${name}');hideContextMenu()">
1004
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 3a2.828 2.828 0 114 4L7.5 20.5 2 22l1.5-5.5L17 3z"/></svg>
1005
- Rename
1006
- </button>
1007
- <div class="ctx-sep"></div>
1008
- <button class="ctx-item danger" onclick="showModal('delete','${name}','${type}');hideContextMenu()">
1009
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
1010
- Delete
1011
- </button>`;
1012
- } else {
1013
- items = `
1014
- <button class="ctx-item" onclick="editFile('${name}');hideContextMenu()">
1015
- <svg viewBox="0 0 24 24" fill="none" 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.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
1016
- Edit
1017
- </button>
1018
- <button class="ctx-item" onclick="showModal('rename','${name}');hideContextMenu()">
1019
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 3a2.828 2.828 0 114 4L7.5 20.5 2 22l1.5-5.5L17 3z"/></svg>
1020
- Rename
1021
- </button>
1022
- <button class="ctx-item" onclick="downloadFile('${name}');hideContextMenu()">
1023
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
1024
- Download
1025
- </button>
1026
- <div class="ctx-sep"></div>
1027
- <button class="ctx-item danger" onclick="showModal('delete','${name}','${type}');hideContextMenu()">
1028
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
1029
- Delete
1030
- </button>`;
1031
- }
1032
-
1033
- menu.innerHTML = items;
1034
- menu.classList.add('show');
1035
-
1036
- // Position
1037
- const x = e.clientX || e.touches?.[0]?.clientX || 100;
1038
- const y = e.clientY || e.touches?.[0]?.clientY || 100;
1039
- menu.style.left = Math.min(x, window.innerWidth - 200) + 'px';
1040
- menu.style.top = Math.min(y, window.innerHeight - 200) + 'px';
1041
-
1042
- setTimeout(() => document.addEventListener('click', hideContextMenu, { once: true }), 10);
1043
- }
1044
-
1045
- function hideContextMenu() {
1046
- document.getElementById('contextMenu').classList.remove('show');
1047
- }
1048
-
1049
- // ===== Modals =====
1050
- function showModal(type, arg1, arg2) {
1051
- const overlay = document.getElementById('modalOverlay');
1052
- const content = document.getElementById('modalContent');
1053
-
1054
- let html = '';
1055
- switch (type) {
1056
- case 'newFile':
1057
- html = `
1058
- <div class="modal-head">
1059
- <div class="modal-title">
1060
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:18px;height:18px;color:var(--accent)"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><line x1="9" y1="15" x2="15" y2="15"/></svg>
1061
- Create New File
1062
- </div>
1063
- <button class="modal-close" onclick="closeModal()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
1064
- </div>
1065
- <div class="modal-body">
1066
- <input class="modal-input" id="modalInput" type="text" placeholder="filename.txt" autofocus>
1067
- </div>
1068
- <div class="modal-foot">
1069
- <button class="modal-btn" onclick="closeModal()">Cancel</button>
1070
- <button class="modal-btn primary" onclick="createNewFile()">Create</button>
1071
- </div>`;
1072
- break;
1073
-
1074
- case 'newFolder':
1075
- html = `
1076
- <div class="modal-head">
1077
- <div class="modal-title">
1078
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:18px;height:18px;color:var(--orange)"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/><line x1="12" y1="11" x2="12" y2="17"/><line x1="9" y1="14" x2="15" y2="14"/></svg>
1079
- Create New Folder
1080
- </div>
1081
- <button class="modal-close" onclick="closeModal()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
1082
- </div>
1083
- <div class="modal-body">
1084
- <input class="modal-input" id="modalInput" type="text" placeholder="folder-name" autofocus>
1085
- </div>
1086
- <div class="modal-foot">
1087
- <button class="modal-btn" onclick="closeModal()">Cancel</button>
1088
- <button class="modal-btn primary" onclick="createNewFolder()">Create</button>
1089
- </div>`;
1090
- break;
1091
-
1092
- case 'rename':
1093
- html = `
1094
- <div class="modal-head">
1095
- <div class="modal-title">
1096
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:18px;height:18px;color:var(--accent)"><path d="M17 3a2.828 2.828 0 114 4L7.5 20.5 2 22l1.5-5.5L17 3z"/></svg>
1097
- Rename
1098
- </div>
1099
- <button class="modal-close" onclick="closeModal()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
1100
- </div>
1101
- <div class="modal-body">
1102
- <input class="modal-input" id="modalInput" type="text" value="${arg1}" autofocus>
1103
- </div>
1104
- <div class="modal-foot">
1105
- <button class="modal-btn" onclick="closeModal()">Cancel</button>
1106
- <button class="modal-btn primary" onclick="renameItem('${arg1}')">Rename</button>
1107
- </div>`;
1108
- break;
1109
-
1110
- case 'delete':
1111
- html = `
1112
- <div class="modal-head">
1113
- <div class="modal-title">
1114
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:18px;height:18px;color:var(--red)"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
1115
- Delete ${arg2 === 'folder' ? 'Folder' : 'File'}
1116
  </div>
1117
- <button class="modal-close" onclick="closeModal()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
 
 
 
1118
  </div>
1119
- <div class="modal-body">
1120
- <p>Are you sure you want to delete <strong style="color:var(--text)">${arg1}</strong>? This action cannot be undone.</p>
 
 
 
 
 
 
1121
  </div>
1122
- <div class="modal-foot">
1123
- <button class="modal-btn" onclick="closeModal()">Cancel</button>
1124
- <button class="modal-btn danger" onclick="deleteItem('${arg1}')">Delete</button>
1125
- </div>`;
1126
- break;
1127
 
1128
- case 'upload':
1129
- html = `
1130
- <div class="modal-head">
1131
- <div class="modal-title">
1132
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:18px;height:18px;color:var(--accent)"><polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.39 18.39A5 5 0 0018 9h-1.26A8 8 0 103 16.3"/></svg>
1133
- Upload Files
1134
- </div>
1135
- <button class="modal-close" onclick="closeModal()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
1136
  </div>
1137
- <div class="modal-body">
1138
- <div class="upload-area" id="uploadArea" onclick="document.getElementById('fileUpload').click()" ondragover="handleDragOver(event)" ondragleave="handleDragLeave(event)" ondrop="handleDrop(event)">
1139
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.39 18.39A5 5 0 0018 9h-1.26A8 8 0 103 16.3"/></svg>
1140
- <p>Drop files here or click to browse</p>
1141
- <p class="small">Max file size: 100MB</p>
 
 
 
1142
  </div>
1143
- <input type="file" id="fileUpload" multiple style="display:none" onchange="handleUpload(this.files)">
1144
- </div>
1145
- <div class="modal-foot">
1146
- <button class="modal-btn" onclick="closeModal()">Close</button>
1147
- </div>`;
1148
- break;
1149
- }
1150
-
1151
- content.innerHTML = html;
1152
- overlay.classList.add('show');
1153
-
1154
- // Focus input after animation
1155
- setTimeout(() => {
1156
- const inp = document.getElementById('modalInput');
1157
- if (inp) { inp.focus(); inp.select(); }
1158
- }, 100);
1159
-
1160
- // Enter key
1161
- setTimeout(() => {
1162
- const inp = document.getElementById('modalInput');
1163
- if (inp) {
1164
- inp.addEventListener('keydown', (e) => {
1165
- if (e.key === 'Enter') {
1166
- const btns = content.querySelectorAll('.modal-btn.primary, .modal-btn.danger');
1167
- if (btns.length) btns[btns.length - 1].click();
1168
- }
1169
- });
1170
- }
1171
- }, 50);
1172
- }
1173
-
1174
- function closeModal() {
1175
- document.getElementById('modalOverlay').classList.remove('show');
1176
- }
1177
-
1178
- function closeModalOutside(e) {
1179
- if (e.target === document.getElementById('modalOverlay')) closeModal();
1180
- }
1181
-
1182
- // ===== Actions via API =====
1183
- async function createNewFile() {
1184
- const name = document.getElementById('modalInput').value.trim();
1185
- if (!name) return toast('Please enter a file name', 'warning');
1186
-
1187
- const formData = new FormData();
1188
- const p = currentPath ? `${currentPath}/${name}` : name;
1189
- formData.append('path', p);
1190
- formData.append('content', '');
1191
-
1192
- try {
1193
- await fetch('/api/fs/write', { method: 'POST', body: formData });
1194
- closeModal();
1195
- renderFiles();
1196
- toast(`Created ${name}`, 'success');
1197
- } catch(e) { toast('Error creating file', 'error'); }
1198
- }
1199
-
1200
- async function createNewFolder() {
1201
- const name = document.getElementById('modalInput').value.trim();
1202
- if (!name) return toast('Please enter a folder name', 'warning');
1203
-
1204
- const formData = new FormData();
1205
- const p = currentPath ? `${currentPath}/${name}` : name;
1206
- formData.append('path', p);
1207
-
1208
- try {
1209
- await fetch('/api/fs/mkdir', { method: 'POST', body: formData });
1210
- closeModal();
1211
- renderFiles();
1212
- toast(`Created folder ${name}`, 'success');
1213
- } catch(e) { toast('Error creating folder', 'error'); }
1214
- }
1215
-
1216
- async function renameItem(oldName) {
1217
- const newName = document.getElementById('modalInput').value.trim();
1218
- if (!newName) return toast('Please enter a name', 'warning');
1219
- if (newName === oldName) { closeModal(); return; }
1220
-
1221
- const oldPath = currentPath ? `${currentPath}/${oldName}` : oldName;
1222
- const newPath = currentPath ? `${currentPath}/${newName}` : newName;
1223
-
1224
- const formData = new FormData();
1225
- formData.append('old_path', oldPath);
1226
- formData.append('new_path', newPath);
1227
-
1228
- try {
1229
- await fetch('/api/fs/move', { method: 'POST', body: formData });
1230
- closeModal();
1231
- renderFiles();
1232
- toast(`Renamed to ${newName}`, 'success');
1233
- } catch(e) { toast('Error renaming item', 'error'); }
1234
- }
1235
-
1236
- async function deleteItem(name) {
1237
- const p = currentPath ? `${currentPath}/${name}` : name;
1238
- const formData = new FormData();
1239
- formData.append('path', p);
1240
-
1241
- try {
1242
- await fetch('/api/fs/delete', { method: 'POST', body: formData });
1243
- closeModal();
1244
- renderFiles();
1245
- toast(`Deleted ${name}`, 'success');
1246
- } catch(e) { toast('Error deleting item', 'error'); }
1247
- }
1248
-
1249
- function downloadFile(name) {
1250
- const p = currentPath ? `${currentPath}/${name}` : name;
1251
- window.location.href = `/api/fs/download?path=${encodeURIComponent(p)}`;
1252
- toast(`Downloading ${name}...`, 'info');
1253
- }
1254
-
1255
- function handleDragOver(e) {
1256
- e.preventDefault();
1257
- e.currentTarget.classList.add('dragover');
1258
- }
1259
- function handleDragLeave(e) {
1260
- e.currentTarget.classList.remove('dragover');
1261
- }
1262
- function handleDrop(e) {
1263
- e.preventDefault();
1264
- e.currentTarget.classList.remove('dragover');
1265
- handleUpload(e.dataTransfer.files);
1266
- }
1267
- async function handleUpload(files) {
1268
- if (!files.length) return;
1269
-
1270
- for (let i = 0; i < files.length; i++) {
1271
- const formData = new FormData();
1272
- formData.append('path', currentPath);
1273
- formData.append('file', files[i]);
1274
- await fetch('/api/fs/upload', { method: 'POST', body: formData });
1275
- }
1276
-
1277
- closeModal();
1278
- renderFiles();
1279
- toast(`Uploaded ${files.length} file${files.length > 1 ? 's' : ''}`, 'success');
1280
- }
1281
-
1282
- // ===== File Editor =====
1283
- async function editFile(name) {
1284
- currentEditFile = name;
1285
- document.getElementById('editorName').textContent = name;
1286
-
1287
- const p = currentPath ? `${currentPath}/${name}` : name;
1288
- try {
1289
- const res = await fetch(`/api/fs/read?path=${encodeURIComponent(p)}`);
1290
- if(!res.ok) throw new Error('Cannot read file');
1291
- const text = await res.text();
1292
- document.getElementById('editorTextarea').value = text;
1293
- document.getElementById('editorOverlay').classList.add('show');
1294
- setTimeout(() => document.getElementById('editorTextarea').focus(), 100);
1295
- } catch(e) {
1296
- toast('Error reading file (maybe binary?)', 'error');
1297
- }
1298
- }
1299
-
1300
- async function saveFile() {
1301
- const content = document.getElementById('editorTextarea').value;
1302
- const p = currentPath ? `${currentPath}/${currentEditFile}` : currentEditFile;
1303
- const formData = new FormData();
1304
- formData.append('path', p);
1305
- formData.append('content', content);
1306
-
1307
- try {
1308
- await fetch('/api/fs/write', { method: 'POST', body: formData });
1309
- toast(`Saved ${currentEditFile}`, 'success');
1310
- } catch(e) {
1311
- toast('Error saving file', 'error');
1312
- }
1313
- }
1314
-
1315
- function closeEditor() {
1316
- document.getElementById('editorOverlay').classList.remove('show');
1317
- currentEditFile = null;
1318
- }
1319
-
1320
- // ===== Config Panel =====
1321
- // We parse server.properties for this UI
1322
- async function renderConfig() {
1323
- const body = document.getElementById('configBody');
1324
- body.innerHTML = '<div class="fm-empty-text">Loading configuration...</div>';
1325
-
1326
- try {
1327
- const res = await fetch('/api/fs/read?path=server.properties');
1328
- if(!res.ok) {
1329
- body.innerHTML = '<div class="fm-empty-text">server.properties not found</div>';
1330
- return;
1331
- }
1332
- const text = await res.text();
1333
-
1334
- // Parse properties
1335
- serverProps = {};
1336
- text.split('\n').forEach(line => {
1337
- if(!line.startsWith('#') && line.includes('=')) {
1338
- const [k, v] = line.split('=');
1339
- if(k) serverProps[k.trim()] = v ? v.trim() : '';
1340
- }
1341
- });
1342
-
1343
- // Define UI Structure mapping to keys
1344
- const configStruct = {
1345
- 'General': [
1346
- { key: 'motd', label: 'Server MOTD', type: 'text' },
1347
- { key: 'max-players', label: 'Max Players', type: 'text' },
1348
- { key: 'server-port', label: 'Server Port', type: 'text' },
1349
- { key: 'level-name', label: 'World Name', type: 'text' },
1350
- { key: 'level-seed', label: 'World Seed', type: 'text', placeholder: 'Leave blank for random' },
1351
- { key: 'gamemode', label: 'Default Gamemode', type: 'select', options: ['survival', 'creative', 'adventure', 'spectator'] },
1352
- { key: 'difficulty', label: 'Difficulty', type: 'select', options: ['peaceful', 'easy', 'normal', 'hard'] },
1353
- ],
1354
- 'Gameplay': [
1355
- { key: 'pvp', label: 'PvP', type: 'toggle' },
1356
- { key: 'allow-flight', label: 'Allow Flight', type: 'toggle' },
1357
- { key: 'allow-nether', label: 'Allow Nether', type: 'toggle' },
1358
- { key: 'generate-structures', label: 'Generate Structures', type: 'toggle' },
1359
- { key: 'enable-command-block', label: 'Command Blocks', type: 'toggle' },
1360
- { key: 'spawn-protection', label: 'Spawn Protection Radius', type: 'text' },
1361
- { key: 'view-distance', label: 'View Distance', type: 'text' },
1362
- ],
1363
- 'Security': [
1364
- { key: 'online-mode', label: 'Online Mode', type: 'toggle' },
1365
- { key: 'white-list', label: 'Whitelist', type: 'toggle' },
1366
- { key: 'enforce-secure-profile', label: 'Enforce Secure Profile', type: 'toggle' },
1367
- { key: 'enable-rcon', label: 'Enable RCON', type: 'toggle' },
1368
- ],
1369
- };
1370
-
1371
- let html = '';
1372
- for (const [section, fields] of Object.entries(configStruct)) {
1373
- html += `<div class="config-section">
1374
- <div class="config-section-title">${section}</div>`;
1375
- fields.forEach(f => {
1376
- const val = serverProps[f.key] || '';
1377
- html += `<div class="config-field">
1378
- <div class="config-label">
1379
- <span class="config-label-text">${f.label}</span>
1380
- <span class="config-label-key">${f.key}</span>
1381
- </div>`;
1382
-
1383
- if (f.type === 'toggle') {
1384
- const checked = val === 'true';
1385
- html += `<label class="config-toggle">
1386
- <input type="checkbox" ${checked ? 'checked' : ''} data-key="${f.key}">
1387
- <span class="config-toggle-track"></span>
1388
- </label>`;
1389
- } else if (f.type === 'select') {
1390
- html += `<select class="config-input" data-key="${f.key}">
1391
- ${f.options.map(o => `<option value="${o}" ${o === val ? 'selected' : ''}>${o}</option>`).join('')}
1392
- </select>`;
1393
- } else {
1394
- html += `<input class="config-input" type="text" value="${escapeHtml(val)}" placeholder="${f.placeholder || ''}" data-key="${f.key}">`;
1395
- }
1396
- html += '</div>';
1397
- });
1398
- html += '</div>';
1399
- }
1400
- body.innerHTML = html;
1401
 
1402
- } catch (e) {
1403
- body.innerHTML = '<div class="fm-empty-text">Error loading config</div>';
1404
- }
1405
- }
 
 
 
 
1406
 
1407
- async function saveConfig() {
1408
- showLoading();
1409
-
1410
- // Gather inputs
1411
- document.querySelectorAll('[data-key]').forEach(el => {
1412
- const key = el.getAttribute('data-key');
1413
- let val;
1414
- if(el.type === 'checkbox') val = el.checked ? 'true' : 'false';
1415
- else val = el.value;
1416
- serverProps[key] = val;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1417
  });
1418
-
1419
- // Reconstruct file content
1420
- let fileContent = '#Minecraft server properties\n#Generated by OrbitMC Panel\n';
1421
- for(const [k,v] of Object.entries(serverProps)) {
1422
- fileContent += `${k}=${v}\n`;
1423
- }
1424
-
1425
- const formData = new FormData();
1426
- formData.append('path', 'server.properties');
1427
- formData.append('content', fileContent);
1428
-
1429
- try {
1430
- await fetch('/api/fs/write', { method: 'POST', body: formData });
1431
- toast('Configuration saved successfully', 'success');
1432
- } catch(e) {
1433
- toast('Error saving configuration', 'error');
1434
- }
1435
- }
1436
-
1437
- // ===== Toast =====
1438
- function toast(message, type = 'info') {
1439
- const container = document.getElementById('toastContainer');
1440
- const t = document.createElement('div');
1441
- t.className = `toast ${type}`;
1442
-
1443
- const icons = {
1444
- success: '<svg class="toast-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
1445
- error: '<svg class="toast-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>',
1446
- warning: '<svg class="toast-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
1447
- info: '<svg class="toast-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>',
1448
- };
1449
-
1450
- t.innerHTML = `${icons[type] || icons.info}<span>${message}</span>`;
1451
- container.appendChild(t);
1452
-
1453
- setTimeout(() => {
1454
- t.classList.add('removing');
1455
- setTimeout(() => t.remove(), 300);
1456
- }, 3000);
1457
- }
1458
-
1459
- // ===== Utilities =====
1460
- function escapeHtml(text) {
1461
- if (!text) return "";
1462
- const div = document.createElement('div');
1463
- div.textContent = text;
1464
- return div.innerHTML;
1465
- }
1466
-
1467
- function pad(n) { return n.toString().padStart(2, '0'); }
1468
-
1469
- // ===== Keyboard Shortcuts =====
1470
- document.addEventListener('keydown', (e) => {
1471
- if (e.key === 'Escape') {
1472
- closeModal();
1473
- closeEditor();
1474
- hideContextMenu();
1475
- }
1476
- });
1477
-
1478
- // Close context menu on scroll
1479
- document.addEventListener('scroll', hideContextMenu, true);
1480
-
1481
- // ===== Init =====
1482
- window.addEventListener('DOMContentLoaded', () => {
1483
- showLoading();
1484
- initConsole();
1485
- renderFiles();
1486
- });
1487
  </script>
1488
  </body>
1489
- </html>
1490
- """
1491
 
1492
- # -----------------
1493
- # UTILITIES & SERVER
1494
- # -----------------
1495
  def get_safe_path(subpath: str):
1496
  subpath = (subpath or "").strip("/")
1497
  target = os.path.abspath(os.path.join(BASE_DIR, subpath))
@@ -1520,76 +496,57 @@ async def read_stream(stream):
1520
 
1521
  async def start_minecraft():
1522
  global mc_process
1523
- jar_path = os.path.join(BASE_DIR, SERVER_JAR)
1524
-
1525
- if not os.path.exists(jar_path):
1526
- await broadcast(f"Error: {SERVER_JAR} not found in {BASE_DIR}")
1527
- return
1528
-
1529
  java_args = [
1530
- "java", "-server", "-Xmx4G", "-Xms1G",
1531
- "-jar", SERVER_JAR, "--nogui"
 
 
 
 
 
 
 
 
 
1532
  ]
1533
-
1534
- try:
1535
- mc_process = await asyncio.create_subprocess_exec(
1536
- *java_args,
1537
- stdin=asyncio.subprocess.PIPE,
1538
- stdout=asyncio.subprocess.PIPE,
1539
- stderr=asyncio.subprocess.STDOUT,
1540
- cwd=BASE_DIR
1541
- )
1542
- await broadcast(f"Server started ({SERVER_JAR})")
1543
- asyncio.create_task(read_stream(mc_process.stdout))
1544
- except Exception as e:
1545
- await broadcast(f"Failed to start server: {e}")
1546
 
1547
  @app.on_event("startup")
1548
  async def startup_event():
1549
- # Only start if the jar exists, otherwise we wait for upload
1550
  asyncio.create_task(start_minecraft())
1551
 
1552
- # -----------------
1553
- # API ROUTING
1554
- # -----------------
1555
  @app.get("/")
1556
- def get_panel(): return HTMLResponse(content=HTML_CONTENT)
 
1557
 
1558
  @app.websocket("/ws")
1559
  async def ws_endpoint(websocket: WebSocket):
1560
  await websocket.accept()
1561
  connected_clients.add(websocket)
1562
- # Send history on connect
1563
- for line in output_history: await websocket.send_text(line)
1564
  try:
1565
  while True:
1566
  cmd = await websocket.receive_text()
1567
  if mc_process and mc_process.stdin:
1568
- try:
1569
- mc_process.stdin.write((cmd + "\n").encode('utf-8'))
1570
- await mc_process.stdin.drain()
1571
- except Exception as e:
1572
- await websocket.send_text(f"Error sending command: {e}")
1573
- else:
1574
- await websocket.send_text("Server is not running.")
1575
  except:
1576
- connected_clients.remove(websocket)
1577
 
1578
  @app.get("/api/fs/list")
1579
  def fs_list(path: str = ""):
1580
  target = get_safe_path(path)
1581
  if not os.path.exists(target): return []
1582
  items = []
1583
- try:
1584
- for f in os.listdir(target):
1585
- fp = os.path.join(target, f)
1586
- items.append({
1587
- "name": f,
1588
- "is_dir": os.path.isdir(fp),
1589
- "size": os.path.getsize(fp) if not os.path.isdir(fp) else 0
1590
- })
1591
- except Exception:
1592
- pass
1593
  return sorted(items, key=lambda x: (not x["is_dir"], x["name"].lower()))
1594
 
1595
  @app.get("/api/fs/read")
@@ -1597,13 +554,8 @@ def fs_read(path: str):
1597
  target = get_safe_path(path)
1598
  if not os.path.isfile(target): raise HTTPException(400, "Not a file")
1599
  try:
1600
- # Try reading as text
1601
- with open(target, 'r', encoding='utf-8') as f:
1602
- return Response(content=f.read(), media_type="text/plain")
1603
- except UnicodeDecodeError:
1604
- raise HTTPException(400, "File is binary")
1605
- except Exception as e:
1606
- raise HTTPException(500, str(e))
1607
 
1608
  @app.get("/api/fs/download")
1609
  def fs_download(path: str):
@@ -1613,55 +565,38 @@ def fs_download(path: str):
1613
 
1614
  @app.post("/api/fs/write")
1615
  def fs_write(path: str = Form(...), content: str = Form(...)):
1616
- target = get_safe_path(path)
1617
- try:
1618
- with open(target, 'w', encoding='utf-8') as f:
1619
- f.write(content)
1620
- return {"status": "ok"}
1621
- except Exception as e:
1622
- raise HTTPException(500, str(e))
1623
-
1624
- @app.post("/api/fs/mkdir")
1625
- def fs_mkdir(path: str = Form(...)):
1626
- target = get_safe_path(path)
1627
- try:
1628
- os.makedirs(target, exist_ok=True)
1629
- return {"status": "ok"}
1630
- except Exception as e:
1631
- raise HTTPException(500, str(e))
1632
-
1633
- @app.post("/api/fs/move")
1634
- def fs_move(old_path: str = Form(...), new_path: str = Form(...)):
1635
- src = get_safe_path(old_path)
1636
- dst = get_safe_path(new_path)
1637
- try:
1638
- shutil.move(src, dst)
1639
- return {"status": "ok"}
1640
- except Exception as e:
1641
- raise HTTPException(500, str(e))
1642
 
1643
  @app.post("/api/fs/upload")
1644
  async def fs_upload(path: str = Form(""), file: UploadFile = File(...)):
1645
- target_dir = get_safe_path(path)
1646
- file_path = os.path.join(target_dir, file.filename)
1647
- try:
1648
- with open(file_path, "wb") as buffer:
1649
- shutil.copyfileobj(file.file, buffer)
1650
- return {"status": "ok"}
1651
- except Exception as e:
1652
- raise HTTPException(500, str(e))
1653
 
1654
  @app.post("/api/fs/delete")
1655
  def fs_delete(path: str = Form(...)):
1656
  t = get_safe_path(path)
1657
- try:
1658
- if os.path.isdir(t): shutil.rmtree(t)
1659
- else: os.remove(t)
1660
- return {"status": "ok"}
1661
- except Exception as e:
1662
- raise HTTPException(500, str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1663
 
1664
  if __name__ == "__main__":
1665
- print(f"Starting panel on http://localhost:7860")
1666
- print(f"Serving files from: {BASE_DIR}")
1667
- uvicorn.run(app, host="0.0.0.0", port=7860, log_level="error")
 
10
  app = FastAPI()
11
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
12
 
 
13
  mc_process = None
14
  output_history = collections.deque(maxlen=300)
15
  connected_clients = set()
16
+ BASE_DIR = os.path.abspath("/app")
17
 
18
+ HTML_CONTENT = """<!DOCTYPE html>
 
 
 
 
 
 
 
 
 
19
  <html lang="en">
20
  <head>
21
+ <meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
22
+ <title>OrbitMC</title>
23
+ <link rel="preconnect" href="https://fonts.googleapis.com">
24
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Geist:wght@300;400;500;600&display=swap" rel="stylesheet">
25
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
26
  <style>
27
+ *{box-sizing:border-box;margin:0;padding:0}
28
  :root{
29
+ --bg:#0a0a0a;--s1:#111;--s2:#181818;--s3:#222;
30
+ --b1:#2a2a2a;--b2:#333;
31
+ --t1:#f0f0f0;--t2:#999;--t3:#555;
32
+ --accent:#4ade80;--accent2:#22c55e;
33
+ --red:#f87171;--blue:#60a5fa;--yellow:#fbbf24;
34
+ --r:8px;--font:'Geist',sans-serif;--mono:'JetBrains Mono',monospace;
35
+ --trans:all .15s ease;
36
+ }
37
+ body{background:var(--bg);color:var(--t1);font-family:var(--font);font-size:14px;display:flex;height:100vh;overflow:hidden}
38
+
39
+ /* SIDEBAR */
40
+ .sidebar{width:56px;background:var(--s1);border-right:1px solid var(--b1);display:flex;flex-direction:column;align-items:center;padding:16px 0;gap:4px;z-index:10;flex-shrink:0}
41
+ .nav-btn{width:40px;height:40px;border:none;background:transparent;color:var(--t3);border-radius:var(--r);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:16px;transition:var(--trans);position:relative}
42
+ .nav-btn:hover{background:var(--s3);color:var(--t2)}
43
+ .nav-btn.active{background:rgba(74,222,128,.12);color:var(--accent)}
44
+ .nav-btn .tooltip{position:absolute;left:52px;background:#1a1a1a;border:1px solid var(--b1);color:var(--t1);padding:4px 10px;border-radius:6px;font-size:12px;white-space:nowrap;pointer-events:none;opacity:0;transition:opacity .15s;z-index:100}
45
+ .nav-btn:hover .tooltip{opacity:1}
46
+
47
+ /* MAIN */
48
+ .main{flex:1;display:flex;flex-direction:column;overflow:hidden}
49
+ .panel{display:none;flex:1;overflow:hidden}
50
+ .panel.active{display:flex;flex-direction:column}
51
+
52
+ /* CONSOLE */
53
+ .console-wrap{flex:1;position:relative;overflow:hidden;background:var(--s1)}
54
+ .console-blur{position:absolute;top:0;left:0;right:0;height:60px;background:linear-gradient(to bottom,var(--s1) 0%,transparent 100%);z-index:2;pointer-events:none}
55
+ .console-out{position:absolute;inset:0;overflow-y:auto;padding:16px;font-family:var(--mono);font-size:12.5px;line-height:1.7;scrollbar-width:thin;scrollbar-color:var(--b2) transparent}
56
+ .console-out::-webkit-scrollbar{width:4px}
57
+ .console-out::-webkit-scrollbar-thumb{background:var(--b2);border-radius:2px}
58
+ .log-line{animation:fadeUp .25s ease forwards;opacity:0;word-break:break-all}
59
+ @keyframes fadeUp{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
60
+ .log-line.info{color:#94a3b8}.log-line.warn{color:var(--yellow)}.log-line.error{color:var(--red)}.log-line.ok{color:var(--accent)}
61
+ .console-input-bar{padding:12px 16px 20px;background:var(--s1);border-top:1px solid var(--b1);display:flex;gap:8px;align-items:center}
62
+ .console-input-bar .prompt{color:var(--accent);font-family:var(--mono);font-size:13px;flex-shrink:0}
63
+ .console-input-bar input{flex:1;background:var(--s2);border:1px solid var(--b1);color:var(--t1);font-family:var(--mono);font-size:13px;padding:8px 12px;border-radius:var(--r);outline:none;transition:var(--trans)}
64
+ .console-input-bar input:focus{border-color:var(--accent);box-shadow:0 0 0 2px rgba(74,222,128,.1)}
65
+ .send-btn{background:var(--accent);color:#000;border:none;padding:8px 16px;border-radius:var(--r);font-family:var(--font);font-weight:600;font-size:13px;cursor:pointer;transition:var(--trans);flex-shrink:0}
66
+ .send-btn:hover{background:var(--accent2)}
67
+
68
+ /* FILE MANAGER */
69
+ .fm-wrap{display:flex;flex-direction:column;flex:1;overflow:hidden}
70
+ .fm-toolbar{padding:12px 16px;background:var(--s1);border-bottom:1px solid var(--b1);display:flex;align-items:center;gap:8px;flex-wrap:wrap}
71
+ .fm-breadcrumb{flex:1;display:flex;align-items:center;gap:4px;font-size:13px;color:var(--t2);overflow:hidden;min-width:0}
72
+ .fm-breadcrumb span{cursor:pointer;transition:color .15s;white-space:nowrap}
73
+ .fm-breadcrumb span:hover{color:var(--accent)}
74
+ .fm-breadcrumb .sep{color:var(--t3)}
75
+ .tb-btn{height:32px;padding:0 12px;background:var(--s2);border:1px solid var(--b1);color:var(--t2);border-radius:6px;cursor:pointer;font-size:12px;font-family:var(--font);display:flex;align-items:center;gap:6px;transition:var(--trans);white-space:nowrap}
76
+ .tb-btn:hover{background:var(--s3);color:var(--t1)}
77
+ .tb-btn.danger:hover{border-color:var(--red);color:var(--red)}
78
+ .fm-list{flex:1;overflow-y:auto;padding:8px;scrollbar-width:thin;scrollbar-color:var(--b2) transparent}
79
+ .fm-list::-webkit-scrollbar{width:4px}
80
+ .fm-list::-webkit-scrollbar-thumb{background:var(--b2)}
81
+ .fm-empty{display:flex;align-items:center;justify-content:center;height:100%;color:var(--t3);font-size:13px}
82
+ .fm-item{display:flex;align-items:center;padding:9px 12px;border-radius:6px;cursor:pointer;transition:background .1s;gap:10px;user-select:none}
83
+ .fm-item:hover{background:var(--s2)}
84
+ .fm-item.selected{background:rgba(74,222,128,.08);outline:1px solid rgba(74,222,128,.2)}
85
+ .fm-icon{width:20px;text-align:center;font-size:14px;flex-shrink:0}
86
+ .fi-dir{color:var(--blue)}.fi-cfg{color:var(--yellow)}.fi-jar{color:var(--accent)}.fi-log{color:var(--t3)}.fi-other{color:var(--t2)}
87
+ .fm-name{flex:1;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
88
+ .fm-size{font-size:11px;color:var(--t3);flex-shrink:0}
89
+ .ctx-menu{position:fixed;background:#1a1a1a;border:1px solid var(--b1);border-radius:8px;padding:6px;z-index:1000;min-width:160px;box-shadow:0 8px 32px rgba(0,0,0,.6);animation:ctxIn .12s ease}
90
+ @keyframes ctxIn{from{opacity:0;transform:scale(.95)}to{opacity:1;transform:none}}
91
+ .ctx-item{padding:7px 12px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t2);display:flex;align-items:center;gap:8px;transition:var(--trans)}
92
+ .ctx-item:hover{background:var(--s3);color:var(--t1)}
93
+ .ctx-item.danger{color:var(--red)}.ctx-item.danger:hover{background:rgba(248,113,113,.1)}
94
+ .ctx-sep{height:1px;background:var(--b1);margin:4px 0}
95
+
96
+ /* CONFIG */
97
+ .cfg-wrap{flex:1;overflow-y:auto;padding:16px;scrollbar-width:thin;scrollbar-color:var(--b2) transparent}
98
+ .cfg-section{background:var(--s1);border:1px solid var(--b1);border-radius:10px;margin-bottom:16px;overflow:hidden}
99
+ .cfg-section-head{padding:12px 16px;border-bottom:1px solid var(--b1);font-size:12px;font-weight:600;color:var(--t3);text-transform:uppercase;letter-spacing:.06em}
100
+ .cfg-row{display:flex;align-items:center;padding:10px 16px;border-bottom:1px solid rgba(255,255,255,.03);gap:12px}
101
+ .cfg-row:last-child{border-bottom:none}
102
+ .cfg-key{flex:1;font-family:var(--mono);font-size:12.5px;color:var(--t2)}
103
+ .cfg-val{flex:1;background:var(--s2);border:1px solid var(--b1);color:var(--t1);font-family:var(--mono);font-size:12px;padding:5px 10px;border-radius:6px;outline:none;transition:var(--trans)}
104
+ .cfg-val:focus{border-color:var(--accent)}
105
+ .cfg-save{margin:0 16px 16px;background:var(--accent);color:#000;border:none;padding:9px 20px;border-radius:var(--r);font-weight:600;font-size:13px;cursor:pointer;transition:var(--trans)}
106
+ .cfg-save:hover{background:var(--accent2)}
107
+ .coming-soon{display:flex;align-items:center;justify-content:center;height:100%;flex-direction:column;gap:12px;color:var(--t3)}
108
+ .coming-soon i{font-size:36px;opacity:.3}
109
+
110
+ /* MODALS */
111
+ .overlay{position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:500;display:flex;align-items:center;justify-content:center;animation:fadeIn .15s ease;backdrop-filter:blur(4px)}
112
+ @keyframes fadeIn{from{opacity:0}to{opacity:1}}
113
+ .modal{background:#161616;border:1px solid var(--b1);border-radius:12px;padding:24px;width:90%;max-width:480px;animation:modalIn .2s ease;box-shadow:0 24px 64px rgba(0,0,0,.6)}
114
+ .modal.wide{max-width:760px}
115
+ @keyframes modalIn{from{opacity:0;transform:translateY(12px) scale(.98)}to{opacity:1;transform:none}}
116
+ .modal h3{font-size:15px;font-weight:600;margin-bottom:16px;color:var(--t1)}
117
+ .modal input,.modal textarea{width:100%;background:var(--s2);border:1px solid var(--b1);color:var(--t1);font-family:var(--mono);font-size:13px;padding:9px 12px;border-radius:var(--r);outline:none;transition:var(--trans);margin-bottom:12px}
118
+ .modal input:focus,.modal textarea:focus{border-color:var(--accent)}
119
+ .modal textarea{min-height:320px;resize:vertical;line-height:1.6}
120
+ .modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:4px}
121
+ .btn-ghost{background:transparent;border:1px solid var(--b1);color:var(--t2);padding:7px 16px;border-radius:6px;cursor:pointer;font-family:var(--font);font-size:13px;transition:var(--trans)}
122
+ .btn-ghost:hover{border-color:var(--b2);color:var(--t1)}
123
+ .btn-primary{background:var(--accent);color:#000;border:none;padding:7px 16px;border-radius:6px;font-family:var(--font);font-weight:600;font-size:13px;cursor:pointer;transition:var(--trans)}
124
+ .btn-primary:hover{background:var(--accent2)}
125
+ .btn-danger{background:transparent;border:1px solid var(--red);color:var(--red);padding:7px 16px;border-radius:6px;cursor:pointer;font-family:var(--font);font-size:13px;transition:var(--trans)}
126
+ .btn-danger:hover{background:rgba(248,113,113,.1)}
127
+ .upload-zone{border:2px dashed var(--b2);border-radius:8px;padding:32px;text-align:center;color:var(--t3);cursor:pointer;transition:var(--trans);margin-bottom:12px}
128
+ .upload-zone:hover,.upload-zone.drag{border-color:var(--accent);color:var(--accent);background:rgba(74,222,128,.04)}
129
+ .upload-zone i{font-size:24px;margin-bottom:8px;display:block}
130
+ .upload-zone p{font-size:13px}
131
+ .status-dot{width:7px;height:7px;border-radius:50%;background:var(--accent);box-shadow:0 0 6px var(--accent);display:inline-block;margin-right:6px;animation:pulse 2s infinite}
132
  @keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
133
+ @media(max-width:600px){.sidebar{width:48px}.tb-btn span{display:none}.tb-btn{padding:0 10px}.fm-size{display:none}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  </style>
135
  </head>
136
  <body>
137
+ <nav class="sidebar">
138
+ <button class="nav-btn active" data-tab="console" onclick="switchTab('console',this)"><i class="fa-solid fa-terminal"></i><span class="tooltip">Console</span></button>
139
+ <button class="nav-btn" data-tab="files" onclick="switchTab('files',this)"><i class="fa-solid fa-folder-open"></i><span class="tooltip">Files</span></button>
140
+ <button class="nav-btn" data-tab="config" onclick="switchTab('config',this)"><i class="fa-solid fa-sliders"></i><span class="tooltip">Config</span></button>
141
+ <button class="nav-btn" data-tab="plugins" onclick="switchTab('plugins',this)" style="margin-top:auto"><i class="fa-solid fa-puzzle-piece"></i><span class="tooltip">Plugins</span></button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  </nav>
143
 
144
+ <div class="main">
145
+ <!-- CONSOLE -->
146
+ <div class="panel active" id="tab-console">
147
+ <div class="console-wrap">
148
+ <div class="console-blur"></div>
149
+ <div class="console-out" id="console-out"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  </div>
151
+ <div class="console-input-bar">
152
+ <span class="prompt">$</span>
153
+ <input id="cmd-input" type="text" placeholder="Enter command..." autocomplete="off" spellcheck="false" onkeydown="cmdKey(event)">
154
+ <button class="send-btn" onclick="sendCmd()"><i class="fa-solid fa-paper-plane"></i></button>
155
  </div>
156
+ </div>
157
+
158
+ <!-- FILES -->
159
+ <div class="panel" id="tab-files">
160
+ <div class="fm-toolbar">
161
+ <div class="fm-breadcrumb" id="breadcrumb"></div>
162
+ <button class="tb-btn" onclick="showMkdir()"><i class="fa-solid fa-folder-plus"></i><span>New Folder</span></button>
163
+ <button class="tb-btn" onclick="showUpload()"><i class="fa-solid fa-upload"></i><span>Upload</span></button>
164
  </div>
165
+ <div class="fm-list" id="fm-list"></div>
166
+ </div>
 
 
 
167
 
168
+ <!-- CONFIG -->
169
+ <div class="panel" id="tab-config">
170
+ <div class="cfg-wrap" id="cfg-wrap">
171
+ <div class="fm-empty" style="height:80px"><i class="fa-solid fa-spinner fa-spin"></i>&nbsp;Loading...</div>
 
 
 
 
172
  </div>
173
+ </div>
174
+
175
+ <!-- PLUGINS -->
176
+ <div class="panel" id="tab-plugins">
177
+ <div class="coming-soon">
178
+ <i class="fa-solid fa-puzzle-piece"></i>
179
+ <p style="font-size:15px;font-weight:600;color:var(--t2)">Plugins</p>
180
+ <p style="font-size:13px">Coming soon</p>
181
  </div>
182
+ </div>
183
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
+ <!-- MODALS -->
186
+ <div id="overlay" class="overlay" style="display:none" onclick="closeModal(event)">
187
+ <div class="modal" id="modal" onclick="event.stopPropagation()">
188
+ <h3 id="modal-title"></h3>
189
+ <div id="modal-body"></div>
190
+ <div class="modal-actions" id="modal-actions"></div>
191
+ </div>
192
+ </div>
193
 
194
+ <script>
195
+ // WS
196
+ const wsProto = location.protocol === 'https:' ? 'wss' : 'ws';
197
+ let ws, cmdHistory=[], histIdx=-1;
198
+ function connectWS(){
199
+ ws = new WebSocket(`${wsProto}://${location.host}/ws`);
200
+ ws.onmessage = e => addLog(e.data);
201
+ ws.onclose = () => setTimeout(connectWS, 2000);
202
+ }
203
+ connectWS();
204
+
205
+ function classify(l){
206
+ if(/\[WARN|WARN\]/i.test(l)) return 'warn';
207
+ if(/\[ERROR|ERROR\]/i.test(l)||/exception/i.test(l)) return 'error';
208
+ if(/Done|started|enabled|loaded/i.test(l)) return 'ok';
209
+ return 'info';
210
+ }
211
+ function addLog(txt){
212
+ const out = document.getElementById('console-out');
213
+ const atBottom = out.scrollHeight - out.clientHeight - out.scrollTop < 40;
214
+ const d = document.createElement('div');
215
+ d.className = `log-line ${classify(txt)}`;
216
+ d.textContent = txt;
217
+ out.appendChild(d);
218
+ if(out.children.length > 400) out.removeChild(out.firstChild);
219
+ if(atBottom) out.scrollTop = out.scrollHeight;
220
+ }
221
+ function sendCmd(){
222
+ const i = document.getElementById('cmd-input');
223
+ const v = i.value.trim();
224
+ if(!v || !ws) return;
225
+ ws.send(v);
226
+ cmdHistory.unshift(v); histIdx=-1;
227
+ i.value='';
228
+ }
229
+ function cmdKey(e){
230
+ if(e.key==='Enter') sendCmd();
231
+ else if(e.key==='ArrowUp'){histIdx=Math.min(histIdx+1,cmdHistory.length-1);document.getElementById('cmd-input').value=cmdHistory[histIdx]||'';}
232
+ else if(e.key==='ArrowDown'){histIdx=Math.max(histIdx-1,-1);document.getElementById('cmd-input').value=histIdx>=0?cmdHistory[histIdx]:'';}
233
+ }
234
+
235
+ // TABS
236
+ function switchTab(id,btn){
237
+ document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));
238
+ document.querySelectorAll('.nav-btn').forEach(b=>b.classList.remove('active'));
239
+ document.getElementById('tab-'+id).classList.add('active');
240
+ btn.classList.add('active');
241
+ if(id==='files') loadDir(currentPath);
242
+ if(id==='config') loadConfig();
243
+ }
244
+
245
+ // FILE MANAGER
246
+ let currentPath='', selectedItem=null;
247
+ const api = p => fetch(p).then(r=>r.json());
248
+ const apiPost = (p,fd) => fetch(p,{method:'POST',body:fd});
249
+
250
+ async function loadDir(path=''){
251
+ currentPath = path;
252
+ renderBreadcrumb(path);
253
+ const list = document.getElementById('fm-list');
254
+ list.innerHTML = '<div class="fm-empty"><i class="fa-solid fa-spinner fa-spin"></i>&nbsp;Loading...</div>';
255
+ const items = await api(`/api/fs/list?path=${encodeURIComponent(path)}`);
256
+ list.innerHTML = '';
257
+ if(!items.length){list.innerHTML='<div class="fm-empty">Empty folder</div>';return;}
258
+ items.forEach(item => {
259
+ const el = document.createElement('div');
260
+ el.className = 'fm-item';
261
+ const icon = getIcon(item);
262
+ el.innerHTML = `<i class="fm-icon ${icon.cls} ${icon.ic}"></i><span class="fm-name">${item.name}</span><span class="fm-size">${item.is_dir?'—':fmtSize(item.size)}</span>`;
263
+ el.addEventListener('click',()=>selectItem(item,el));
264
+ el.addEventListener('dblclick',()=>openItem(item));
265
+ el.addEventListener('contextmenu',e=>{e.preventDefault();selectItem(item,el);showCtx(e,item);});
266
+ list.appendChild(el);
267
+ });
268
+ }
269
+ function selectItem(item,el){
270
+ document.querySelectorAll('.fm-item').forEach(e=>e.classList.remove('selected'));
271
+ el.classList.add('selected'); selectedItem=item;
272
+ }
273
+ function getIcon(item){
274
+ if(item.is_dir) return {cls:'fi-dir',ic:'fa-solid fa-folder'};
275
+ const ext = item.name.split('.').pop().toLowerCase();
276
+ if(['yml','yaml','json','toml','cfg','conf'].includes(ext)) return {cls:'fi-cfg',ic:'fa-solid fa-file-code'};
277
+ if(ext==='jar') return {cls:'fi-jar',ic:'fa-solid fa-cube'};
278
+ if(ext==='log') return {cls:'fi-log',ic:'fa-solid fa-file-lines'};
279
+ if(['txt','md'].includes(ext)) return {cls:'fi-other',ic:'fa-solid fa-file-alt'};
280
+ return {cls:'fi-other',ic:'fa-solid fa-file'};
281
+ }
282
+ function fmtSize(b){if(b<1024)return b+'B';if(b<1048576)return (b/1024).toFixed(1)+'KB';return (b/1048576).toFixed(1)+'MB';}
283
+ function renderBreadcrumb(path){
284
+ const bc = document.getElementById('breadcrumb');
285
+ const parts = path ? path.split('/').filter(Boolean) : [];
286
+ let html = `<span onclick="loadDir('')"><i class="fa-solid fa-server" style="color:var(--accent)"></i></span>`;
287
+ let acc = '';
288
+ parts.forEach(p=>{acc+=(acc?'/':'')+p;const cp=acc;html+=`<span class="sep">/</span><span onclick="loadDir('${cp}')">${p}</span>`;});
289
+ bc.innerHTML = html;
290
+ }
291
+ function openItem(item){
292
+ const fp = (currentPath ? currentPath+'/' : '') + item.name;
293
+ if(item.is_dir){loadDir(fp);return;}
294
+ const ext = item.name.split('.').pop().toLowerCase();
295
+ const editable = ['yml','yaml','json','toml','cfg','conf','txt','md','properties','log','sh','py','js'].includes(ext);
296
+ if(editable) openEditor(fp,item.name);
297
+ else downloadFile(fp,item.name);
298
+ }
299
+ function fullPath(name){return (currentPath ? currentPath+'/' : '') + name;}
300
+
301
+ async function openEditor(fp,name){
302
+ const res = await fetch(`/api/fs/read?path=${encodeURIComponent(fp)}`);
303
+ if(!res.ok){toast('Cannot read binary file');return;}
304
+ const text = await res.text();
305
+ openModal('Edit — '+name,'wide');
306
+ M.body.innerHTML = `<textarea id="editor-ta" spellcheck="false">${escHtml(text)}</textarea>`;
307
+ M.actions.innerHTML = `<button class="btn-ghost" onclick="closeModal()">Cancel</button><button class="btn-primary" onclick="saveFile('${fp}')">Save</button>`;
308
+ }
309
+ async function saveFile(fp){
310
+ const content = document.getElementById('editor-ta').value;
311
+ const fd = new FormData(); fd.append('path',fp); fd.append('content',content);
312
+ await apiPost('/api/fs/write',fd);
313
+ toast('Saved'); closeModal();
314
+ }
315
+ function downloadFile(fp,name){window.location='/api/fs/download?path='+encodeURIComponent(fp);}
316
+
317
+ function showCtx(e,item){
318
+ document.querySelectorAll('.ctx-menu').forEach(c=>c.remove());
319
+ const fp = fullPath(item.name);
320
+ const m = document.createElement('div'); m.className='ctx-menu';
321
+ const items = [
322
+ {icon:'fa-solid fa-pen',label:'Rename',fn:()=>showRename(fp,item.name)},
323
+ ...(item.is_dir?[]:[{icon:'fa-solid fa-edit',label:'Edit',fn:()=>openEditor(fp,item.name)},{icon:'fa-solid fa-download',label:'Download',fn:()=>downloadFile(fp,item.name)}]),
324
+ {sep:true},
325
+ {icon:'fa-solid fa-trash',label:'Delete',fn:()=>showDelete(fp,item.name),danger:true},
326
+ ];
327
+ items.forEach(it=>{
328
+ if(it.sep){const s=document.createElement('div');s.className='ctx-sep';m.appendChild(s);return;}
329
+ const d=document.createElement('div');d.className='ctx-item'+(it.danger?' danger':'');
330
+ d.innerHTML=`<i class="${it.icon}"></i>${it.label}`;
331
+ d.onclick=()=>{m.remove();it.fn();};
332
+ m.appendChild(d);
333
+ });
334
+ m.style.top = Math.min(e.clientY,window.innerHeight-m.offsetHeight-10)+'px';
335
+ m.style.left = Math.min(e.clientX,window.innerWidth-180)+'px';
336
+ document.body.appendChild(m);
337
+ setTimeout(()=>document.addEventListener('click',()=>m.remove(),{once:true}),10);
338
+ }
339
+
340
+ function showRename(fp,name){
341
+ openModal('Rename');
342
+ M.body.innerHTML=`<input id="rename-in" value="${name}" autocomplete="off">`;
343
+ M.actions.innerHTML=`<button class="btn-ghost" onclick="closeModal()">Cancel</button><button class="btn-primary" onclick="doRename('${fp}')">Rename</button>`;
344
+ setTimeout(()=>{const i=document.getElementById('rename-in');i.focus();i.select();},50);
345
+ }
346
+ async function doRename(fp){
347
+ const nv = document.getElementById('rename-in').value.trim();
348
+ if(!nv) return;
349
+ const fd=new FormData();fd.append('path',fp);fd.append('new_name',nv);
350
+ await apiPost('/api/fs/rename',fd);
351
+ closeModal(); loadDir(currentPath);
352
+ }
353
+ function showDelete(fp,name){
354
+ openModal('Delete');
355
+ M.body.innerHTML=`<p style="color:var(--t2);font-size:13px;margin-bottom:16px">Delete <strong style="color:var(--t1)">${name}</strong>? This cannot be undone.</p>`;
356
+ M.actions.innerHTML=`<button class="btn-ghost" onclick="closeModal()">Cancel</button><button class="btn-danger" onclick="doDelete('${fp}')">Delete</button>`;
357
+ }
358
+ async function doDelete(fp){
359
+ const fd=new FormData();fd.append('path',fp);
360
+ await apiPost('/api/fs/delete',fd);
361
+ closeModal(); loadDir(currentPath);
362
+ }
363
+ function showMkdir(){
364
+ openModal('New Folder');
365
+ M.body.innerHTML=`<input id="mkdir-in" placeholder="Folder name" autocomplete="off">`;
366
+ M.actions.innerHTML=`<button class="btn-ghost" onclick="closeModal()">Cancel</button><button class="btn-primary" onclick="doMkdir()">Create</button>`;
367
+ setTimeout(()=>document.getElementById('mkdir-in').focus(),50);
368
+ }
369
+ async function doMkdir(){
370
+ const n=document.getElementById('mkdir-in').value.trim();if(!n)return;
371
+ const fd=new FormData();fd.append('path',(currentPath?currentPath+'/':'')+n);
372
+ await apiPost('/api/fs/mkdir',fd);
373
+ closeModal(); loadDir(currentPath);
374
+ }
375
+ function showUpload(){
376
+ openModal('Upload Files');
377
+ M.body.innerHTML=`<div class="upload-zone" id="drop-zone" onclick="document.getElementById('file-in').click()">
378
+ <i class="fa-solid fa-cloud-arrow-up"></i><p>Click or drag & drop files</p></div>
379
+ <input type="file" id="file-in" multiple style="display:none" onchange="handleUpload(this.files)">
380
+ <div id="upload-prog"></div>`;
381
+ M.actions.innerHTML=`<button class="btn-ghost" onclick="closeModal()">Close</button>`;
382
+ const dz=document.getElementById('drop-zone');
383
+ dz.ondragover=e=>{e.preventDefault();dz.classList.add('drag')};
384
+ dz.ondragleave=()=>dz.classList.remove('drag');
385
+ dz.ondrop=e=>{e.preventDefault();dz.classList.remove('drag');handleUpload(e.dataTransfer.files);};
386
+ }
387
+ async function handleUpload(files){
388
+ const prog=document.getElementById('upload-prog');
389
+ for(const file of files){
390
+ prog.innerHTML=`<p style="font-size:12px;color:var(--t2);margin-bottom:4px">Uploading ${file.name}...</p>`;
391
+ const fd=new FormData();fd.append('path',currentPath);fd.append('file',file);
392
+ await apiPost('/api/fs/upload',fd);
393
+ }
394
+ prog.innerHTML=`<p style="font-size:12px;color:var(--accent)">Done!</p>`;
395
+ loadDir(currentPath);
396
+ }
397
+
398
+ // CONFIG
399
+ async function loadConfig(){
400
+ const wrap = document.getElementById('cfg-wrap');
401
+ wrap.innerHTML='<div class="fm-empty"><i class="fa-solid fa-spinner fa-spin"></i>&nbsp;Loading...</div>';
402
+ const res = await fetch('/api/fs/read?path='+encodeURIComponent('server.properties'));
403
+ if(!res.ok){wrap.innerHTML='<div class="fm-empty" style="flex-direction:column;gap:8px"><i class="fa-solid fa-circle-exclamation" style="color:var(--t3)"></i><p style="font-size:13px;color:var(--t3)">server.properties not found</p></div>';return;}
404
+ const text = await res.text();
405
+ const groups = {General:[],World:[],Network:[],Game:[],Performance:[]};
406
+ const gmap={motd:'General',server_ip:'Network','server-port':'Network','max-players':'General','online-mode':'Network','enable-rcon':'Network','rcon.port':'Network','rcon.password':'Network','level-name':'World','level-seed':'World','gamemode':'Game','difficulty':'Game','hardcore':'Game','pvp':'Game','spawn-monsters':'Game','spawn-animals':'Game','spawn-npcs':'Game','view-distance':'Performance','simulation-distance':'Performance','max-tick-time':'Performance','network-compression-threshold':'Performance'};
407
+ const lines=text.split('\n').filter(l=>l&&!l.startsWith('#'));
408
+ const entries=[];
409
+ lines.forEach(l=>{const eq=l.indexOf('=');if(eq<0)return;const k=l.slice(0,eq).trim(),v=l.slice(eq+1).trim();entries.push({k,v});});
410
+ const grouped={General:[],World:[],Network:[],Game:[],Performance:[],Other:[]};
411
+ entries.forEach(e=>{const g=gmap[e.k]||'Other';grouped[g].push(e);});
412
+ wrap.innerHTML='';
413
+ Object.entries(grouped).forEach(([g,rows])=>{
414
+ if(!rows.length) return;
415
+ const sec=document.createElement('div');sec.className='cfg-section';
416
+ sec.innerHTML=`<div class="cfg-section-head">${g}</div>`;
417
+ rows.forEach(({k,v})=>{
418
+ const r=document.createElement('div');r.className='cfg-row';
419
+ r.innerHTML=`<span class="cfg-key">${k}</span><input class="cfg-val" data-key="${k}" value="${escHtml(v)}">`;
420
+ sec.appendChild(r);
421
  });
422
+ wrap.appendChild(sec);
423
+ });
424
+ const btn=document.createElement('button');btn.className='cfg-save';btn.textContent='Save Changes';
425
+ btn.onclick=saveConfig;wrap.appendChild(btn);
426
+ }
427
+ async function saveConfig(){
428
+ const res=await fetch('/api/fs/read?path=server.properties');
429
+ let text=await res.text();
430
+ document.querySelectorAll('.cfg-val').forEach(inp=>{
431
+ const k=inp.dataset.key,v=inp.value;
432
+ text=text.replace(new RegExp(`^(${k}\\s*=).*$`,'m'),`$1${v}`);
433
+ });
434
+ const fd=new FormData();fd.append('path','server.properties');fd.append('content',text);
435
+ await apiPost('/api/fs/write',fd);
436
+ toast('server.properties saved');
437
+ }
438
+
439
+ // MODAL
440
+ const M={el:null,body:null,actions:null};
441
+ function openModal(title,cls=''){
442
+ const ov=document.getElementById('overlay');
443
+ const mo=document.getElementById('modal');
444
+ mo.className='modal'+(cls?' '+cls:'');
445
+ document.getElementById('modal-title').textContent=title;
446
+ M.body=document.getElementById('modal-body');M.body.innerHTML='';
447
+ M.actions=document.getElementById('modal-actions');M.actions.innerHTML='';
448
+ ov.style.display='flex';
449
+ }
450
+ function closeModal(e){
451
+ if(e&&e.target!==document.getElementById('overlay'))return;
452
+ document.getElementById('overlay').style.display='none';
453
+ }
454
+ document.addEventListener('keydown',e=>{if(e.key==='Escape')document.getElementById('overlay').style.display='none';});
455
+
456
+ // TOAST
457
+ function toast(msg){
458
+ const t=document.createElement('div');
459
+ t.style.cssText='position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:#1a1a1a;border:1px solid var(--b1);color:var(--t1);padding:8px 18px;border-radius:8px;font-size:13px;z-index:9999;animation:fadeIn .2s ease;white-space:nowrap;box-shadow:0 8px 24px rgba(0,0,0,.5)';
460
+ t.textContent=msg;document.body.appendChild(t);
461
+ setTimeout(()=>t.remove(),2200);
462
+ }
463
+ function escHtml(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
464
+
465
+ // Init
466
+ loadDir('');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
467
  </script>
468
  </body>
469
+ </html>"""
 
470
 
 
 
 
471
  def get_safe_path(subpath: str):
472
  subpath = (subpath or "").strip("/")
473
  target = os.path.abspath(os.path.join(BASE_DIR, subpath))
 
496
 
497
  async def start_minecraft():
498
  global mc_process
 
 
 
 
 
 
499
  java_args = [
500
+ "java", "-server", "-Xmx8G", "-Xms8G", "-XX:+UseG1GC", "-XX:+ParallelRefProcEnabled",
501
+ "-XX:ParallelGCThreads=2", "-XX:ConcGCThreads=1", "-XX:MaxGCPauseMillis=50",
502
+ "-XX:+UnlockExperimentalVMOptions", "-XX:+DisableExplicitGC", "-XX:+AlwaysPreTouch",
503
+ "-XX:G1NewSizePercent=30", "-XX:G1MaxNewSizePercent=50", "-XX:G1HeapRegionSize=16M",
504
+ "-XX:G1ReservePercent=15", "-XX:G1HeapWastePercent=5", "-XX:G1MixedGCCountTarget=3",
505
+ "-XX:InitiatingHeapOccupancyPercent=10", "-XX:G1MixedGCLiveThresholdPercent=90",
506
+ "-XX:G1RSetUpdatingPauseTimePercent=5", "-XX:SurvivorRatio=32", "-XX:+PerfDisableSharedMem",
507
+ "-XX:MaxTenuringThreshold=1", "-XX:G1SATBBufferEnqueueingThresholdPercent=30",
508
+ "-XX:G1ConcMarkStepDurationMillis=5", "-XX:G1ConcRSHotCardLimit=16",
509
+ "-XX:+UseStringDeduplication", "-Dfile.encoding=UTF-8", "-Dspring.output.ansi.enabled=ALWAYS",
510
+ "-jar", "purpur.jar", "--nogui"
511
  ]
512
+ mc_process = await asyncio.create_subprocess_exec(
513
+ *java_args, stdin=asyncio.subprocess.PIPE,
514
+ stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT,
515
+ cwd=BASE_DIR
516
+ )
517
+ asyncio.create_task(read_stream(mc_process.stdout))
 
 
 
 
 
 
 
518
 
519
  @app.on_event("startup")
520
  async def startup_event():
 
521
  asyncio.create_task(start_minecraft())
522
 
 
 
 
523
  @app.get("/")
524
+ def get_panel():
525
+ return HTMLResponse(content=HTML_CONTENT)
526
 
527
  @app.websocket("/ws")
528
  async def ws_endpoint(websocket: WebSocket):
529
  await websocket.accept()
530
  connected_clients.add(websocket)
531
+ for line in output_history:
532
+ await websocket.send_text(line)
533
  try:
534
  while True:
535
  cmd = await websocket.receive_text()
536
  if mc_process and mc_process.stdin:
537
+ mc_process.stdin.write((cmd + "\n").encode('utf-8'))
538
+ await mc_process.stdin.drain()
 
 
 
 
 
539
  except:
540
+ connected_clients.discard(websocket)
541
 
542
  @app.get("/api/fs/list")
543
  def fs_list(path: str = ""):
544
  target = get_safe_path(path)
545
  if not os.path.exists(target): return []
546
  items = []
547
+ for f in os.listdir(target):
548
+ fp = os.path.join(target, f)
549
+ items.append({"name": f, "is_dir": os.path.isdir(fp), "size": os.path.getsize(fp) if not os.path.isdir(fp) else 0})
 
 
 
 
 
 
 
550
  return sorted(items, key=lambda x: (not x["is_dir"], x["name"].lower()))
551
 
552
  @app.get("/api/fs/read")
 
554
  target = get_safe_path(path)
555
  if not os.path.isfile(target): raise HTTPException(400, "Not a file")
556
  try:
557
+ with open(target, 'r', encoding='utf-8') as f: return Response(content=f.read(), media_type="text/plain")
558
+ except: raise HTTPException(400, "File is binary")
 
 
 
 
 
559
 
560
  @app.get("/api/fs/download")
561
  def fs_download(path: str):
 
565
 
566
  @app.post("/api/fs/write")
567
  def fs_write(path: str = Form(...), content: str = Form(...)):
568
+ with open(get_safe_path(path), 'w', encoding='utf-8') as f: f.write(content)
569
+ return {"status": "ok"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570
 
571
  @app.post("/api/fs/upload")
572
  async def fs_upload(path: str = Form(""), file: UploadFile = File(...)):
573
+ with open(os.path.join(get_safe_path(path), file.filename), "wb") as buffer:
574
+ shutil.copyfileobj(file.file, buffer)
575
+ return {"status": "ok"}
 
 
 
 
 
576
 
577
  @app.post("/api/fs/delete")
578
  def fs_delete(path: str = Form(...)):
579
  t = get_safe_path(path)
580
+ if os.path.isdir(t): shutil.rmtree(t)
581
+ else: os.remove(t)
582
+ return {"status": "ok"}
583
+
584
+ @app.post("/api/fs/mkdir")
585
+ def fs_mkdir(path: str = Form(...)):
586
+ os.makedirs(get_safe_path(path), exist_ok=True)
587
+ return {"status": "ok"}
588
+
589
+ @app.post("/api/fs/rename")
590
+ def fs_rename(path: str = Form(...), new_name: str = Form(...)):
591
+ src = get_safe_path(path)
592
+ dst = os.path.join(os.path.dirname(src), new_name)
593
+ os.rename(src, dst)
594
+ return {"status": "ok"}
595
+
596
+ @app.post("/api/fs/move")
597
+ def fs_move(src_path: str = Form(...), dst_path: str = Form(...)):
598
+ shutil.move(get_safe_path(src_path), get_safe_path(dst_path))
599
+ return {"status": "ok"}
600
 
601
  if __name__ == "__main__":
602
+ uvicorn.run(app, host="0.0.0.0", port=7860, log_level="warning")