srinetwork commited on
Commit
eb63f46
·
verified ·
1 Parent(s): 8f6dfc8

Update Dockerfile

Browse files
Files changed (1) hide show
  1. Dockerfile +143 -1089
Dockerfile CHANGED
@@ -77,7 +77,7 @@ echo "✅ Auto-backup enabled (every 5 minutes)"
77
  echo ""
78
 
79
  # ==========================================
80
- # WEB TERMINAL SERVER (Responsive + File Manager)
81
  # ==========================================
82
  cat > /app/web_terminal.py << 'PYTHON_SCRIPT'
83
  import os
@@ -88,8 +88,7 @@ import threading
88
  import time
89
  import queue
90
  import signal
91
- import shutil
92
- from flask import Flask, render_template_string, request, jsonify, send_file
93
  from flask_sock import Sock
94
  import psutil
95
 
@@ -101,10 +100,9 @@ output_queue = queue.Queue()
101
  command_queue = queue.Queue()
102
  server_ready = threading.Event()
103
 
104
- MC_DIR = "/app/mcserver"
105
-
106
  def start_minecraft():
107
  global mc_process
 
108
 
109
  print("[MC] Starting Minecraft server...", flush=True)
110
 
@@ -112,7 +110,7 @@ def start_minecraft():
112
  ["java", "-Xms1G", "-Xmx14G", "-XX:+UseG1GC",
113
  "-XX:+ParallelRefProcEnabled", "-XX:MaxGCPauseMillis=200",
114
  "-jar", "server.jar", "nogui"],
115
- cwd=MC_DIR,
116
  stdin=subprocess.PIPE,
117
  stdout=subprocess.PIPE,
118
  stderr=subprocess.STDOUT,
@@ -154,255 +152,132 @@ def start_minecraft():
154
 
155
  threading.Thread(target=process_commands, daemon=True).start()
156
 
157
- # ==========================================
158
- # FILE MANAGER FUNCTIONS
159
- # ==========================================
160
-
161
- def get_file_size(path):
162
- try:
163
- if os.path.isfile(path):
164
- return os.path.getsize(path)
165
- elif os.path.isdir(path):
166
- total = 0
167
- for dirpath, dirnames, filenames in os.walk(path):
168
- for f in filenames:
169
- fp = os.path.join(dirpath, f)
170
- total += os.path.getsize(fp)
171
- return total
172
- return 0
173
- except:
174
- return 0
175
-
176
- def format_size(size):
177
- for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
178
- if size < 1024.0:
179
- return f"{size:.1f} {unit}"
180
- size /= 1024.0
181
- return f"{size:.1f} PB"
182
-
183
- def get_files_list(path=""):
184
- try:
185
- full_path = os.path.join(MC_DIR, path)
186
- if not os.path.exists(full_path):
187
- return {"error": "Path not found"}
188
-
189
- items = []
190
- for item in os.listdir(full_path):
191
- item_path = os.path.join(full_path, item)
192
- rel_path = os.path.join(path, item)
193
- is_dir = os.path.isdir(item_path)
194
-
195
- items.append({
196
- "name": item,
197
- "path": rel_path,
198
- "is_dir": is_dir,
199
- "size": format_size(get_file_size(item_path)) if not is_dir else "--",
200
- "raw_size": get_file_size(item_path),
201
- "modified": time.ctime(os.path.getmtime(item_path))
202
- })
203
-
204
- items.sort(key=lambda x: (not x["is_dir"], x["name"].lower()))
205
-
206
- return {
207
- "path": path,
208
- "items": items,
209
- "parent": os.path.dirname(path) if path else None
210
- }
211
- except Exception as e:
212
- return {"error": str(e)}
213
-
214
- # ==========================================
215
- # RESPONSIVE HTML TEMPLATE
216
- # ==========================================
217
-
218
  HTML_TEMPLATE = '''
219
  <!DOCTYPE html>
220
  <html lang="en">
221
  <head>
222
  <meta charset="UTF-8">
223
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
224
  <title>Minecraft Server - Web Console</title>
225
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
226
  <style>
227
  * { margin: 0; padding: 0; box-sizing: border-box; }
228
 
229
- :root {
230
- --primary: #3b82f6;
231
- --success: #22c55e;
232
- --warning: #eab308;
233
- --danger: #dc2626;
234
- --dark: #0d1117;
235
- --darker: #010409;
236
- --gray: #161b22;
237
- --gray-light: #30363d;
238
- --text: #e6edf3;
239
- --text-muted: #8b949e;
240
- }
241
-
242
  body {
243
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
244
- background: var(--darker);
245
- color: var(--text);
246
  height: 100vh;
247
  overflow: hidden;
248
  }
249
 
250
- /* Mobile-First Navigation */
251
  .top-nav {
252
- background: var(--gray);
253
- border-bottom: 1px solid var(--gray-light);
254
- padding: 0 15px;
255
  display: flex;
256
  align-items: center;
257
- height: 55px;
258
- position: relative;
259
- z-index: 100;
260
- }
261
-
262
- .nav-tabs {
263
- display: flex;
264
- gap: 0;
265
- height: 100%;
266
- overflow-x: auto;
267
- scrollbar-width: none;
268
- -ms-overflow-style: none;
269
  }
270
 
271
- .nav-tabs::-webkit-scrollbar { display: none; }
272
 
273
  .nav-tab {
274
- padding: 0 15px;
275
  display: flex;
276
  align-items: center;
277
  gap: 8px;
278
- color: var(--text-muted);
279
  text-decoration: none;
280
  font-size: 14px;
281
  border-bottom: 2px solid transparent;
282
  transition: all 0.2s;
283
  cursor: pointer;
284
- white-space: nowrap;
285
- min-width: fit-content;
286
  }
287
 
288
- .nav-tab:hover { color: var(--text); background: rgba(255,255,255,0.05); }
289
- .nav-tab.active { color: #fff; border-bottom-color: var(--primary); background: rgba(59, 130, 246, 0.1); }
290
  .nav-tab i { font-size: 14px; }
291
 
292
  .nav-right {
293
  margin-left: auto;
294
  display: flex;
295
  align-items: center;
296
- gap: 5px;
297
  }
298
 
299
  .nav-btn {
300
  background: transparent;
301
  border: none;
302
- color: var(--text-muted);
303
- padding: 10px;
304
  cursor: pointer;
305
- border-radius: 6px;
306
  transition: all 0.2s;
307
- font-size: 16px;
308
  }
309
 
310
  .nav-btn:hover { color: #fff; background: rgba(255,255,255,0.1); }
311
 
312
- /* Main Container */
313
- .main-container {
314
- display: flex;
315
- height: calc(100vh - 55px);
316
- flex-direction: column;
317
- }
318
-
319
- @media (min-width: 1024px) {
320
- .main-container { flex-direction: row; }
321
- }
322
 
323
- /* Content Sections */
324
- .content-section {
325
  flex: 1;
326
- display: none;
327
  flex-direction: column;
328
- background: var(--dark);
329
- overflow: hidden;
330
  }
331
 
332
- .content-section.active { display: flex; }
333
-
334
- /* Console Section */
335
- .console-section { flex: 1; display: flex; flex-direction: column; }
336
-
337
  .console-header {
338
- background: var(--gray);
339
- padding: 12px 15px;
340
- border-bottom: 1px solid var(--gray-light);
341
  display: flex;
342
- flex-direction: column;
343
- gap: 10px;
344
- }
345
-
346
- @media (min-width: 640px) {
347
- .console-header {
348
- flex-direction: row;
349
- justify-content: space-between;
350
- align-items: center;
351
- }
352
  }
353
 
354
  .server-title {
355
- font-size: 16px;
356
  font-weight: 600;
357
  color: #f0f6fc;
358
  display: flex;
359
  align-items: center;
360
- gap: 8px;
361
  }
362
 
363
- .server-title i { color: var(--primary); }
364
 
365
- .control-buttons {
366
- display: flex;
367
- gap: 6px;
368
- flex-wrap: wrap;
369
- }
370
 
371
  .btn {
372
- padding: 8px 12px;
373
  border: none;
374
- border-radius: 6px;
375
- font-size: 12px;
376
  font-weight: 500;
377
  cursor: pointer;
378
  display: flex;
379
  align-items: center;
380
- gap: 5px;
381
  transition: all 0.2s;
382
- flex: 1;
383
- justify-content: center;
384
- }
385
-
386
- @media (min-width: 640px) {
387
- .btn {
388
- padding: 8px 16px;
389
- font-size: 13px;
390
- flex: none;
391
- }
392
  }
393
 
394
- .btn-primary { background: var(--primary); color: white; }
395
- .btn-primary:hover { background: #2563eb; }
396
  .btn-secondary { background: #374151; color: #e0e0e0; }
397
  .btn-secondary:hover { background: #4b5563; }
398
- .btn-danger { background: var(--danger); color: white; }
399
  .btn-danger:hover { background: #b91c1c; }
400
  .btn:disabled { opacity: 0.5; cursor: not-allowed; }
401
 
402
  /* Terminal */
403
  .terminal-container {
404
  flex: 1;
405
- background: var(--dark);
 
406
  overflow: hidden;
407
  position: relative;
408
  }
@@ -410,21 +285,17 @@ HTML_TEMPLATE = '''
410
  .terminal {
411
  width: 100%;
412
  height: 100%;
413
- padding: 10px;
414
  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
415
- font-size: 12px;
416
  line-height: 1.5;
417
- color: var(--text);
418
  overflow-y: auto;
419
  white-space: pre-wrap;
420
  word-wrap: break-word;
421
  }
422
 
423
- @media (min-width: 640px) {
424
- .terminal { font-size: 13px; padding: 15px; }
425
- }
426
-
427
- .terminal-line { margin: 1px 0; padding: 1px 0; }
428
  .terminal-line.info { color: #58a6ff; }
429
  .terminal-line.warn { color: #d29922; }
430
  .terminal-line.error { color: #f85149; }
@@ -432,42 +303,37 @@ HTML_TEMPLATE = '''
432
 
433
  /* Command Input Area */
434
  .command-area {
435
- background: var(--gray);
436
- border-top: 1px solid var(--gray-light);
437
- padding: 10px;
438
  display: flex;
439
- gap: 8px;
440
  align-items: center;
441
  }
442
 
443
- @media (min-width: 640px) {
444
- .command-area { padding: 12px 15px; }
445
- }
446
-
447
  .command-input-wrapper {
448
  flex: 1;
449
  display: flex;
450
  align-items: center;
451
- background: var(--dark);
452
- border: 1px solid var(--gray-light);
453
- border-radius: 8px;
454
- padding: 0 12px;
455
  transition: border-color 0.2s;
456
  }
457
 
458
  .command-input-wrapper:focus-within { border-color: #58a6ff; }
459
- .command-input-wrapper i { color: #6e7681; margin-right: 8px; font-size: 12px; }
460
 
461
  #commandInput {
462
  flex: 1;
463
  background: transparent;
464
  border: none;
465
- color: var(--text);
466
- padding: 10px 0;
467
  font-size: 14px;
468
  font-family: 'SFMono-Regular', Consolas, monospace;
469
  outline: none;
470
- width: 100%;
471
  }
472
 
473
  #commandInput::placeholder { color: #6e7681; }
@@ -476,418 +342,108 @@ HTML_TEMPLATE = '''
476
  background: #238636;
477
  color: white;
478
  border: none;
479
- padding: 10px 16px;
480
- border-radius: 8px;
481
  cursor: pointer;
482
  font-weight: 500;
483
  display: flex;
484
  align-items: center;
485
- gap: 5px;
486
  transition: background 0.2s;
487
- white-space: nowrap;
488
- }
489
-
490
- @media (min-width: 640px) {
491
- .send-btn { padding: 12px 20px; }
492
  }
493
 
494
  .send-btn:hover { background: #2ea043; }
495
  .send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
496
 
497
- /* File Manager Styles */
498
- .file-manager {
499
- flex: 1;
500
- display: flex;
501
- flex-direction: column;
502
- overflow: hidden;
503
- }
504
-
505
- .file-toolbar {
506
- background: var(--gray);
507
- padding: 10px;
508
- border-bottom: 1px solid var(--gray-light);
509
- display: flex;
510
- flex-direction: column;
511
- gap: 10px;
512
- }
513
-
514
- @media (min-width: 640px) {
515
- .file-toolbar {
516
- flex-direction: row;
517
- justify-content: space-between;
518
- align-items: center;
519
- padding: 12px 15px;
520
- }
521
- }
522
-
523
- .breadcrumb {
524
- display: flex;
525
- align-items: center;
526
- gap: 5px;
527
- font-size: 13px;
528
- color: var(--text-muted);
529
- overflow-x: auto;
530
- scrollbar-width: none;
531
- -ms-overflow-style: none;
532
- }
533
-
534
- .breadcrumb::-webkit-scrollbar { display: none; }
535
-
536
- .breadcrumb a {
537
- color: var(--primary);
538
- text-decoration: none;
539
- white-space: nowrap;
540
- }
541
-
542
- .breadcrumb a:hover { text-decoration: underline; }
543
-
544
- .file-actions {
545
- display: flex;
546
- gap: 6px;
547
- flex-wrap: wrap;
548
- }
549
-
550
- .file-actions .btn {
551
- padding: 6px 10px;
552
- font-size: 11px;
553
- }
554
-
555
- @media (min-width: 640px) {
556
- .file-actions .btn {
557
- padding: 8px 12px;
558
- font-size: 12px;
559
- }
560
- }
561
-
562
- .file-list {
563
- flex: 1;
564
- overflow-y: auto;
565
- padding: 10px;
566
- }
567
-
568
- .file-item {
569
- display: flex;
570
- align-items: center;
571
- padding: 12px;
572
- background: var(--gray);
573
- border: 1px solid var(--gray-light);
574
- border-radius: 8px;
575
- margin-bottom: 8px;
576
- cursor: pointer;
577
- transition: all 0.2s;
578
- gap: 12px;
579
- }
580
-
581
- .file-item:hover {
582
- background: #1f2428;
583
- transform: translateX(5px);
584
- }
585
-
586
- .file-item.selected {
587
- background: rgba(59, 130, 246, 0.2);
588
- border-color: var(--primary);
589
- }
590
-
591
- .file-icon {
592
- width: 40px;
593
- height: 40px;
594
- border-radius: 8px;
595
- display: flex;
596
- align-items: center;
597
- justify-content: center;
598
- font-size: 18px;
599
- flex-shrink: 0;
600
- }
601
-
602
- .file-icon.folder { background: rgba(234, 179, 8, 0.2); color: var(--warning); }
603
- .file-icon.file { background: rgba(59, 130, 246, 0.2); color: var(--primary); }
604
- .file-icon.image { background: rgba(34, 197, 94, 0.2); color: var(--success); }
605
- .file-icon.code { background: rgba(168, 85, 247, 0.2); color: #a855f7; }
606
-
607
- .file-info { flex: 1; min-width: 0; }
608
- .file-name {
609
- font-size: 14px;
610
- font-weight: 500;
611
- color: var(--text);
612
- white-space: nowrap;
613
- overflow: hidden;
614
- text-overflow: ellipsis;
615
- }
616
- .file-meta {
617
- font-size: 11px;
618
- color: var(--text-muted);
619
- margin-top: 2px;
620
- }
621
-
622
- .file-actions-menu {
623
- display: flex;
624
- gap: 5px;
625
- }
626
-
627
- .file-action-btn {
628
- background: transparent;
629
- border: none;
630
- color: var(--text-muted);
631
- padding: 8px;
632
- cursor: pointer;
633
- border-radius: 4px;
634
- transition: all 0.2s;
635
- }
636
-
637
- .file-action-btn:hover {
638
- background: rgba(255,255,255,0.1);
639
- color: var(--text);
640
- }
641
-
642
- .file-action-btn.danger:hover { color: var(--danger); }
643
-
644
  /* Sidebar */
645
  .sidebar {
 
646
  background: #1f2428;
647
- border-top: 1px solid var(--gray-light);
648
- padding: 15px;
649
  overflow-y: auto;
650
- max-height: 200px;
651
- }
652
-
653
- @media (min-width: 1024px) {
654
- .sidebar {
655
- width: 280px;
656
- border-left: 1px solid var(--gray-light);
657
- border-top: none;
658
- max-height: none;
659
- }
660
  }
661
 
662
  .stat-card {
663
- background: var(--gray);
664
- border: 1px solid var(--gray-light);
665
  border-radius: 8px;
666
- padding: 12px;
667
- margin-bottom: 10px;
668
  display: flex;
669
  align-items: center;
670
- gap: 12px;
 
671
  }
672
 
673
- @media (min-width: 640px) and (max-width: 1023px) {
674
- .stat-card {
675
- display: inline-flex;
676
- width: calc(50% - 5px);
677
- margin-right: 5px;
678
- }
679
- }
680
 
681
  .stat-icon {
682
- width: 40px;
683
- height: 40px;
684
  border-radius: 8px;
685
  display: flex;
686
  align-items: center;
687
  justify-content: center;
688
- font-size: 18px;
689
- flex-shrink: 0;
690
  }
691
 
692
- .stat-icon.blue { background: rgba(59, 130, 246, 0.2); color: var(--primary); }
693
- .stat-icon.green { background: rgba(34, 197, 94, 0.2); color: var(--success); }
694
- .stat-icon.red { background: rgba(239, 68, 68, 0.2); color: var(--danger); }
695
- .stat-icon.yellow { background: rgba(234, 179, 8, 0.2); color: var(--warning); }
696
  .stat-icon.purple { background: rgba(168, 85, 247, 0.2); color: #a855f7; }
697
  .stat-icon.cyan { background: rgba(6, 182, 212, 0.2); color: #06b6d4; }
698
 
699
- .stat-info { flex: 1; min-width: 0; }
700
- .stat-label { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; }
701
- .stat-value { font-size: 16px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
702
- .stat-sub { font-size: 10px; color: #6b7280; margin-top: 2px; }
703
 
704
  /* Status Badge */
705
  .status-indicator {
706
  display: inline-flex;
707
  align-items: center;
708
- gap: 5px;
709
- padding: 3px 8px;
710
  border-radius: 20px;
711
- font-size: 10px;
712
  font-weight: 600;
713
  text-transform: uppercase;
714
  }
715
 
716
- .status-online { background: rgba(34, 197, 94, 0.2); color: var(--success); }
717
- .status-offline { background: rgba(239, 68, 68, 0.2); color: var(--danger); }
718
- .status-starting { background: rgba(234, 179, 8, 0.2); color: var(--warning); }
719
 
720
- .pulse { width: 6px; height: 6px; border-radius: 50%; background: currentColor; animation: pulse 2s infinite; }
721
  @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
722
 
723
  /* Scrollbar */
724
- ::-webkit-scrollbar { width: 6px; height: 6px; }
725
- ::-webkit-scrollbar-track { background: var(--dark); }
726
- ::-webkit-scrollbar-thumb { background: #4b5563; border-radius: 3px; }
727
  ::-webkit-scrollbar-thumb:hover { background: #6b7280; }
728
 
729
  /* Connection Status */
730
  .connection-bar {
731
- position: absolute;
732
- top: 0;
733
  left: 0;
734
- right: 0;
735
- padding: 6px 15px;
736
- font-size: 11px;
737
  font-weight: 500;
738
  display: flex;
739
  align-items: center;
740
- gap: 6px;
741
- z-index: 50;
742
  transition: all 0.3s;
743
  }
744
 
745
- .connection-bar.connected { background: rgba(34, 197, 94, 0.15); color: var(--success); }
746
- .connection-bar.disconnected { background: rgba(239, 68, 68, 0.15); color: var(--danger); }
747
- .connection-bar.connecting { background: rgba(234, 179, 8, 0.15); color: var(--warning); }
748
-
749
- /* Modal */
750
- .modal {
751
- display: none;
752
- position: fixed;
753
- top: 0;
754
- left: 0;
755
- right: 0;
756
- bottom: 0;
757
- background: rgba(0,0,0,0.8);
758
- z-index: 1000;
759
- justify-content: center;
760
- align-items: center;
761
- padding: 20px;
762
- }
763
-
764
- .modal.active { display: flex; }
765
-
766
- .modal-content {
767
- background: var(--gray);
768
- border: 1px solid var(--gray-light);
769
- border-radius: 12px;
770
- width: 100%;
771
- max-width: 400px;
772
- padding: 20px;
773
- }
774
-
775
- .modal-header {
776
- font-size: 18px;
777
- font-weight: 600;
778
- margin-bottom: 15px;
779
- color: var(--text);
780
- }
781
-
782
- .modal-input {
783
- width: 100%;
784
- background: var(--dark);
785
- border: 1px solid var(--gray-light);
786
- border-radius: 6px;
787
- padding: 10px;
788
- color: var(--text);
789
- font-size: 14px;
790
- margin-bottom: 15px;
791
- }
792
-
793
- .modal-input:focus {
794
- outline: none;
795
- border-color: var(--primary);
796
- }
797
-
798
- .modal-actions {
799
- display: flex;
800
- gap: 10px;
801
- justify-content: flex-end;
802
- }
803
-
804
- /* Upload Area */
805
- .upload-area {
806
- border: 2px dashed var(--gray-light);
807
- border-radius: 8px;
808
- padding: 30px;
809
- text-align: center;
810
- color: var(--text-muted);
811
- cursor: pointer;
812
- transition: all 0.2s;
813
- margin: 10px;
814
- }
815
-
816
- .upload-area:hover, .upload-area.dragover {
817
- border-color: var(--primary);
818
- background: rgba(59, 130, 246, 0.1);
819
- }
820
-
821
- .upload-area i {
822
- font-size: 40px;
823
- margin-bottom: 10px;
824
- display: block;
825
- }
826
-
827
- /* Empty State */
828
- .empty-state {
829
- text-align: center;
830
- padding: 40px;
831
- color: var(--text-muted);
832
- }
833
-
834
- .empty-state i {
835
- font-size: 48px;
836
- margin-bottom: 15px;
837
- opacity: 0.5;
838
- }
839
-
840
- /* Toast Notification */
841
- .toast {
842
- position: fixed;
843
- bottom: 20px;
844
- right: 20px;
845
- background: var(--gray);
846
- border: 1px solid var(--gray-light);
847
- border-radius: 8px;
848
- padding: 12px 20px;
849
- display: flex;
850
- align-items: center;
851
- gap: 10px;
852
- z-index: 2000;
853
- transform: translateX(400px);
854
- transition: transform 0.3s;
855
- max-width: 90vw;
856
- }
857
-
858
- .toast.show { transform: translateX(0); }
859
- .toast.success { border-left: 3px solid var(--success); }
860
- .toast.error { border-left: 3px solid var(--danger); }
861
-
862
- /* Loading Spinner */
863
- .spinner {
864
- display: inline-block;
865
- width: 16px;
866
- height: 16px;
867
- border: 2px solid rgba(255,255,255,0.3);
868
- border-radius: 50%;
869
- border-top-color: #fff;
870
- animation: spin 1s ease-in-out infinite;
871
- }
872
-
873
- @keyframes spin { to { transform: rotate(360deg); } }
874
-
875
- /* Mobile Optimizations */
876
- @media (max-width: 480px) {
877
- .top-nav { padding: 0 10px; height: 50px; }
878
- .nav-tab { padding: 0 12px; font-size: 13px; }
879
- .nav-tab i { font-size: 12px; }
880
- .control-buttons { width: 100%; }
881
- .btn { padding: 8px; font-size: 11px; flex: 1; }
882
- .btn span { display: none; }
883
- .file-item { padding: 10px; }
884
- .file-icon { width: 35px; height: 35px; font-size: 16px; }
885
- .file-name { font-size: 13px; }
886
- .sidebar { padding: 10px; }
887
- .stat-card { padding: 10px; }
888
- .stat-icon { width: 35px; height: 35px; font-size: 16px; }
889
- .stat-value { font-size: 14px; }
890
- }
891
  </style>
892
  </head>
893
  <body>
@@ -895,102 +451,60 @@ HTML_TEMPLATE = '''
895
  <nav class="top-nav">
896
  <div class="nav-tabs">
897
  <a class="nav-tab active" onclick="switchTab('console', this)">
898
- <i class="fas fa-terminal"></i> <span>Console</span>
899
  </a>
900
  <a class="nav-tab" onclick="switchTab('files', this)">
901
- <i class="fas fa-folder"></i> <span>Files</span>
902
  </a>
903
  </div>
904
  <div class="nav-right">
905
- <button class="nav-btn" title="Settings" onclick="showToast('Settings coming soon!', 'info')">
906
- <i class="fas fa-cog"></i>
907
- </button>
908
- <button class="nav-btn" title="Full Screen" onclick="toggleFullscreen()">
909
- <i class="fas fa-expand"></i>
910
- </button>
911
  </div>
912
  </nav>
913
 
914
  <!-- Main Container -->
915
  <div class="main-container">
916
  <!-- Console Section -->
917
- <div class="content-section active" id="consoleSection">
918
- <div class="console-section">
919
- <div class="connection-bar connecting" id="connectionBar">
920
- <i class="fas fa-circle-notch fa-spin"></i>
921
- <span>Connecting to server...</span>
922
- </div>
923
-
924
- <div class="console-header">
925
- <div class="server-title">
926
- <i class="fas fa-server"></i>
927
- <span>Minecraft Server</span>
928
- <span class="status-indicator status-offline" id="serverStatus">
929
- <span class="pulse"></span> Offline
930
- </span>
931
- </div>
932
- <div class="control-buttons">
933
- <button class="btn btn-primary" id="startBtn" onclick="sendCmd('start')" disabled>
934
- <i class="fas fa-play"></i> <span>Start</span>
935
- </button>
936
- <button class="btn btn-secondary" id="restartBtn" onclick="sendCmd('restart')" disabled>
937
- <i class="fas fa-redo"></i> <span>Restart</span>
938
- </button>
939
- <button class="btn btn-danger" id="stopBtn" onclick="sendCmd('stop')" disabled>
940
- <i class="fas fa-stop"></i> <span>Stop</span>
941
- </button>
942
- </div>
943
- </div>
944
-
945
- <div class="terminal-container">
946
- <div class="terminal" id="terminal"></div>
947
  </div>
948
-
949
- <div class="command-area">
950
- <div class="command-input-wrapper">
951
- <i class="fas fa-angle-right"></i>
952
- <input type="text" id="commandInput" placeholder="Type a command..." autocomplete="off" disabled>
953
- </div>
954
- <button class="send-btn" id="sendBtn" onclick="sendInput()" disabled>
955
- <i class="fas fa-paper-plane"></i> <span>Send</span>
 
956
  </button>
957
  </div>
958
  </div>
959
- </div>
960
-
961
- <!-- Files Section -->
962
- <div class="content-section" id="filesSection">
963
- <div class="file-manager">
964
- <div class="file-toolbar">
965
- <div class="breadcrumb" id="breadcrumb">
966
- <a href="#" onclick="navigateTo('')">Home</a>
967
- </div>
968
- <div class="file-actions">
969
- <button class="btn btn-primary" onclick="showUploadModal()">
970
- <i class="fas fa-upload"></i> <span>Upload</span>
971
- </button>
972
- <button class="btn btn-secondary" onclick="showNewFolderModal()">
973
- <i class="fas fa-folder-plus"></i> <span>New Folder</span>
974
- </button>
975
- <button class="btn btn-secondary" onclick="refreshFiles()">
976
- <i class="fas fa-sync"></i> <span>Refresh</span>
977
- </button>
978
- </div>
979
- </div>
980
-
981
- <div class="upload-area" id="uploadArea" ondrop="handleDrop(event)" ondragover="handleDragOver(event)" ondragleave="handleDragLeave(event)" onclick="document.getElementById('fileInput').click()">
982
- <i class="fas fa-cloud-upload-alt"></i>
983
- <div>Drop files here or click to upload</div>
984
- <small style="opacity: 0.6; margin-top: 5px; display: block;">Max file size: 100MB</small>
985
- </div>
986
- <input type="file" id="fileInput" multiple style="display: none" onchange="handleFileSelect(event)">
987
-
988
- <div class="file-list" id="fileList">
989
- <div class="empty-state">
990
- <i class="fas fa-folder-open"></i>
991
- <div>Loading files...</div>
992
- </div>
993
  </div>
 
 
 
994
  </div>
995
  </div>
996
 
@@ -1051,36 +565,6 @@ HTML_TEMPLATE = '''
1051
  </div>
1052
  </div>
1053
  </div>
1054
-
1055
- <!-- Rename Modal -->
1056
- <div class="modal" id="renameModal">
1057
- <div class="modal-content">
1058
- <div class="modal-header">Rename Item</div>
1059
- <input type="text" class="modal-input" id="renameInput" placeholder="New name">
1060
- <div class="modal-actions">
1061
- <button class="btn btn-secondary" onclick="closeModal('renameModal')">Cancel</button>
1062
- <button class="btn btn-primary" onclick="confirmRename()">Rename</button>
1063
- </div>
1064
- </div>
1065
- </div>
1066
-
1067
- <!-- New Folder Modal -->
1068
- <div class="modal" id="folderModal">
1069
- <div class="modal-content">
1070
- <div class="modal-header">Create New Folder</div>
1071
- <input type="text" class="modal-input" id="folderInput" placeholder="Folder name">
1072
- <div class="modal-actions">
1073
- <button class="btn btn-secondary" onclick="closeModal('folderModal')">Cancel</button>
1074
- <button class="btn btn-primary" onclick="confirmNewFolder()">Create</button>
1075
- </div>
1076
- </div>
1077
- </div>
1078
-
1079
- <!-- Toast -->
1080
- <div class="toast" id="toast">
1081
- <i class="fas fa-check-circle"></i>
1082
- <span id="toastMessage">Operation successful</span>
1083
- </div>
1084
 
1085
  <script>
1086
  const terminal = document.getElementById('terminal');
@@ -1096,11 +580,6 @@ HTML_TEMPLATE = '''
1096
  let maxReconnectAttempts = 10;
1097
  let serverStartTime = null;
1098
  let isServerRunning = false;
1099
- let currentPath = '';
1100
- let selectedItem = null;
1101
- let itemToRename = null;
1102
-
1103
- // ==================== CONSOLE FUNCTIONS ====================
1104
 
1105
  function updateConnectionStatus(status, message) {
1106
  connectionBar.className = 'connection-bar ' + status;
@@ -1195,9 +674,6 @@ HTML_TEMPLATE = '''
1195
  if (data.players !== undefined) {
1196
  document.getElementById('playerCount').textContent = data.players;
1197
  }
1198
- if (data.disk) {
1199
- document.getElementById('diskUsage').textContent = data.disk;
1200
- }
1201
  }
1202
  };
1203
 
@@ -1238,313 +714,9 @@ HTML_TEMPLATE = '''
1238
  if (e.key === 'Enter') sendInput();
1239
  });
1240
 
1241
- // ==================== FILE MANAGER FUNCTIONS ====================
1242
-
1243
  function switchTab(tab, element) {
1244
  document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
1245
  element.classList.add('active');
1246
-
1247
- document.querySelectorAll('.content-section').forEach(s => s.classList.remove('active'));
1248
-
1249
- if (tab === 'console') {
1250
- document.getElementById('consoleSection').classList.add('active');
1251
- } else if (tab === 'files') {
1252
- document.getElementById('filesSection').classList.add('active');
1253
- loadFiles(currentPath);
1254
- }
1255
- }
1256
-
1257
- async function loadFiles(path = '') {
1258
- currentPath = path;
1259
- try {
1260
- const response = await fetch('/api/files?path=' + encodeURIComponent(path));
1261
- const data = await response.json();
1262
-
1263
- if (data.error) {
1264
- showToast(data.error, 'error');
1265
- return;
1266
- }
1267
-
1268
- updateBreadcrumb(path);
1269
- renderFileList(data.items, data.parent);
1270
- } catch (error) {
1271
- showToast('Failed to load files', 'error');
1272
- }
1273
- }
1274
-
1275
- function updateBreadcrumb(path) {
1276
- const breadcrumb = document.getElementById('breadcrumb');
1277
- let html = '<a href="#" onclick="navigateTo(\'\')">Home</a>';
1278
-
1279
- if (path) {
1280
- const parts = path.split('/');
1281
- let current = '';
1282
- parts.forEach((part, index) => {
1283
- current += (current ? '/' : '') + part;
1284
- html += ' <i class="fas fa-chevron-right" style="font-size: 10px;"></i> ';
1285
- if (index === parts.length - 1) {
1286
- html += '<span>' + part + '</span>';
1287
- } else {
1288
- html += '<a href="#" onclick="navigateTo(\'' + current + '\')">' + part + '</a>';
1289
- }
1290
- });
1291
- }
1292
-
1293
- breadcrumb.innerHTML = html;
1294
- }
1295
-
1296
- function navigateTo(path) {
1297
- currentPath = path;
1298
- loadFiles(path);
1299
- }
1300
-
1301
- function getFileIconClass(name, isDir) {
1302
- if (isDir) return 'folder';
1303
- const ext = name.split('.').pop().toLowerCase();
1304
- const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'];
1305
- const codeExts = ['js', 'py', 'java', 'cpp', 'c', 'h', 'html', 'css', 'json', 'xml', 'yml', 'yaml', 'properties', 'txt'];
1306
-
1307
- if (imageExts.includes(ext)) return 'image';
1308
- if (codeExts.includes(ext)) return 'code';
1309
- return 'file';
1310
- }
1311
-
1312
- function renderFileList(items, parent) {
1313
- const fileList = document.getElementById('fileList');
1314
-
1315
- if (items.length === 0 && !parent) {
1316
- fileList.innerHTML = `
1317
- <div class="empty-state">
1318
- <i class="fas fa-folder-open"></i>
1319
- <div>This folder is empty</div>
1320
- </div>
1321
- `;
1322
- return;
1323
- }
1324
-
1325
- let html = '';
1326
-
1327
- if (parent !== null) {
1328
- html += `
1329
- <div class="file-item" onclick="navigateTo('${parent}')">
1330
- <div class="file-icon folder"><i class="fas fa-level-up-alt"></i></div>
1331
- <div class="file-info">
1332
- <div class="file-name">..</div>
1333
- <div class="file-meta">Parent folder</div>
1334
- </div>
1335
- </div>
1336
- `;
1337
- }
1338
-
1339
- items.forEach(item => {
1340
- const iconClass = getFileIconClass(item.name, item.is_dir);
1341
- const icon = item.is_dir ? 'fa-folder' : 'fa-file';
1342
-
1343
- html += `
1344
- <div class="file-item ${selectedItem === item.path ? 'selected' : ''}"
1345
- onclick="selectItem('${item.path}', ${item.is_dir})"
1346
- ondblclick="${item.is_dir ? `navigateTo('${item.path}')` : `downloadFile('${item.path}')`}">
1347
- <div class="file-icon ${iconClass}"><i class="fas ${icon}"></i></div>
1348
- <div class="file-info">
1349
- <div class="file-name">${item.name}</div>
1350
- <div class="file-meta">${item.size} • ${item.modified}</div>
1351
- </div>
1352
- <div class="file-actions-menu">
1353
- ${!item.is_dir ? `<button class="file-action-btn" onclick="event.stopPropagation(); downloadFile('${item.path}')" title="Download"><i class="fas fa-download"></i></button>` : ''}
1354
- <button class="file-action-btn" onclick="event.stopPropagation(); showRenameModal('${item.path}', '${item.name}')" title="Rename"><i class="fas fa-edit"></i></button>
1355
- <button class="file-action-btn danger" onclick="event.stopPropagation(); deleteItem('${item.path}', ${item.is_dir})" title="Delete"><i class="fas fa-trash"></i></button>
1356
- </div>
1357
- </div>
1358
- `;
1359
- });
1360
-
1361
- fileList.innerHTML = html;
1362
- }
1363
-
1364
- function selectItem(path, isDir) {
1365
- selectedItem = path;
1366
- document.querySelectorAll('.file-item').forEach(item => item.classList.remove('selected'));
1367
- event.currentTarget.classList.add('selected');
1368
- }
1369
-
1370
- // ==================== FILE OPERATIONS ====================
1371
-
1372
- function showUploadModal() {
1373
- document.getElementById('fileInput').click();
1374
- }
1375
-
1376
- function handleFileSelect(event) {
1377
- const files = event.target.files;
1378
- if (files.length > 0) {
1379
- uploadFiles(files);
1380
- }
1381
- }
1382
-
1383
- function handleDragOver(event) {
1384
- event.preventDefault();
1385
- document.getElementById('uploadArea').classList.add('dragover');
1386
- }
1387
-
1388
- function handleDragLeave(event) {
1389
- event.preventDefault();
1390
- document.getElementById('uploadArea').classList.remove('dragover');
1391
- }
1392
-
1393
- function handleDrop(event) {
1394
- event.preventDefault();
1395
- document.getElementById('uploadArea').classList.remove('dragover');
1396
- const files = event.dataTransfer.files;
1397
- if (files.length > 0) {
1398
- uploadFiles(files);
1399
- }
1400
- }
1401
-
1402
- async function uploadFiles(files) {
1403
- for (let file of files) {
1404
- if (file.size > 100 * 1024 * 1024) {
1405
- showToast('File too large: ' + file.name, 'error');
1406
- continue;
1407
- }
1408
-
1409
- const formData = new FormData();
1410
- formData.append('file', file);
1411
- formData.append('path', currentPath);
1412
-
1413
- try {
1414
- const response = await fetch('/api/upload', {
1415
- method: 'POST',
1416
- body: formData
1417
- });
1418
- const result = await response.json();
1419
-
1420
- if (result.success) {
1421
- showToast('Uploaded: ' + file.name, 'success');
1422
- } else {
1423
- showToast('Failed to upload: ' + file.name, 'error');
1424
- }
1425
- } catch (error) {
1426
- showToast('Error uploading: ' + file.name, 'error');
1427
- }
1428
- }
1429
- loadFiles(currentPath);
1430
- }
1431
-
1432
- function downloadFile(path) {
1433
- window.open('/api/download?path=' + encodeURIComponent(path), '_blank');
1434
- }
1435
-
1436
- async function deleteItem(path, isDir) {
1437
- if (!confirm('Are you sure you want to delete ' + (isDir ? 'this folder and all its contents?' : 'this file?'))) {
1438
- return;
1439
- }
1440
-
1441
- try {
1442
- const response = await fetch('/api/delete', {
1443
- method: 'POST',
1444
- headers: {'Content-Type': 'application/json'},
1445
- body: JSON.stringify({path: path})
1446
- });
1447
- const result = await response.json();
1448
-
1449
- if (result.success) {
1450
- showToast('Deleted successfully', 'success');
1451
- loadFiles(currentPath);
1452
- } else {
1453
- showToast(result.error || 'Delete failed', 'error');
1454
- }
1455
- } catch (error) {
1456
- showToast('Delete failed', 'error');
1457
- }
1458
- }
1459
-
1460
- function showRenameModal(path, name) {
1461
- itemToRename = path;
1462
- document.getElementById('renameInput').value = name;
1463
- document.getElementById('renameModal').classList.add('active');
1464
- }
1465
-
1466
- function closeModal(modalId) {
1467
- document.getElementById(modalId).classList.remove('active');
1468
- }
1469
-
1470
- async function confirmRename() {
1471
- const newName = document.getElementById('renameInput').value.trim();
1472
- if (!newName || !itemToRename) return;
1473
-
1474
- try {
1475
- const response = await fetch('/api/rename', {
1476
- method: 'POST',
1477
- headers: {'Content-Type': 'application/json'},
1478
- body: JSON.stringify({path: itemToRename, new_name: newName})
1479
- });
1480
- const result = await response.json();
1481
-
1482
- if (result.success) {
1483
- showToast('Renamed successfully', 'success');
1484
- closeModal('renameModal');
1485
- loadFiles(currentPath);
1486
- } else {
1487
- showToast(result.error || 'Rename failed', 'error');
1488
- }
1489
- } catch (error) {
1490
- showToast('Rename failed', 'error');
1491
- }
1492
- }
1493
-
1494
- function showNewFolderModal() {
1495
- document.getElementById('folderInput').value = '';
1496
- document.getElementById('folderModal').classList.add('active');
1497
- }
1498
-
1499
- async function confirmNewFolder() {
1500
- const name = document.getElementById('folderInput').value.trim();
1501
- if (!name) return;
1502
-
1503
- try {
1504
- const response = await fetch('/api/mkdir', {
1505
- method: 'POST',
1506
- headers: {'Content-Type': 'application/json'},
1507
- body: JSON.stringify({path: currentPath, name: name})
1508
- });
1509
- const result = await response.json();
1510
-
1511
- if (result.success) {
1512
- showToast('Folder created', 'success');
1513
- closeModal('folderModal');
1514
- loadFiles(currentPath);
1515
- } else {
1516
- showToast(result.error || 'Failed to create folder', 'error');
1517
- }
1518
- } catch (error) {
1519
- showToast('Failed to create folder', 'error');
1520
- }
1521
- }
1522
-
1523
- function refreshFiles() {
1524
- loadFiles(currentPath);
1525
- showToast('Refreshed', 'success');
1526
- }
1527
-
1528
- // ==================== UTILITIES ====================
1529
-
1530
- function showToast(message, type = 'success') {
1531
- const toast = document.getElementById('toast');
1532
- const toastMessage = document.getElementById('toastMessage');
1533
- const icon = toast.querySelector('i');
1534
-
1535
- toastMessage.textContent = message;
1536
- toast.className = 'toast ' + type;
1537
-
1538
- if (type === 'success') {
1539
- icon.className = 'fas fa-check-circle';
1540
- } else if (type === 'error') {
1541
- icon.className = 'fas fa-exclamation-circle';
1542
- } else {
1543
- icon.className = 'fas fa-info-circle';
1544
- }
1545
-
1546
- toast.classList.add('show');
1547
- setTimeout(() => toast.classList.remove('show'), 3000);
1548
  }
1549
 
1550
  function toggleFullscreen() {
@@ -1557,9 +729,6 @@ HTML_TEMPLATE = '''
1557
 
1558
  document.getElementById('serverAddress').textContent = window.location.hostname;
1559
  addTerminalLine('Connecting to Minecraft server...', 'info');
1560
-
1561
- // Load initial files
1562
- loadFiles('');
1563
  </script>
1564
  </body>
1565
  </html>
@@ -1576,113 +745,6 @@ def health():
1576
  'minecraft': mc_process is not None and mc_process.poll() is None
1577
  }
1578
 
1579
- # ==========================================
1580
- # FILE MANAGER API ROUTES
1581
- # ==========================================
1582
-
1583
- @app.route('/api/files')
1584
- def get_files():
1585
- path = request.args.get('path', '')
1586
- return jsonify(get_files_list(path))
1587
-
1588
- @app.route('/api/upload', methods=['POST'])
1589
- def upload_file():
1590
- try:
1591
- if 'file' not in request.files:
1592
- return jsonify({'success': False, 'error': 'No file provided'})
1593
-
1594
- file = request.files['file']
1595
- path = request.form.get('path', '')
1596
-
1597
- if file.filename == '':
1598
- return jsonify({'success': False, 'error': 'No file selected'})
1599
-
1600
- upload_path = os.path.join(MC_DIR, path, file.filename)
1601
-
1602
- # Security check
1603
- if not os.path.abspath(upload_path).startswith(os.path.abspath(MC_DIR)):
1604
- return jsonify({'success': False, 'error': 'Invalid path'})
1605
-
1606
- file.save(upload_path)
1607
- return jsonify({'success': True})
1608
- except Exception as e:
1609
- return jsonify({'success': False, 'error': str(e)})
1610
-
1611
- @app.route('/api/download')
1612
- def download_file():
1613
- try:
1614
- path = request.args.get('path', '')
1615
- file_path = os.path.join(MC_DIR, path)
1616
-
1617
- # Security check
1618
- if not os.path.abspath(file_path).startswith(os.path.abspath(MC_DIR)):
1619
- return jsonify({'success': False, 'error': 'Invalid path'}), 403
1620
-
1621
- if not os.path.exists(file_path):
1622
- return jsonify({'success': False, 'error': 'File not found'}), 404
1623
-
1624
- return send_file(file_path, as_attachment=True)
1625
- except Exception as e:
1626
- return jsonify({'success': False, 'error': str(e)}), 500
1627
-
1628
- @app.route('/api/delete', methods=['POST'])
1629
- def delete_item():
1630
- try:
1631
- data = request.get_json()
1632
- path = data.get('path', '')
1633
- full_path = os.path.join(MC_DIR, path)
1634
-
1635
- # Security check
1636
- if not os.path.abspath(full_path).startswith(os.path.abspath(MC_DIR)):
1637
- return jsonify({'success': False, 'error': 'Invalid path'})
1638
-
1639
- if os.path.isdir(full_path):
1640
- shutil.rmtree(full_path)
1641
- else:
1642
- os.remove(full_path)
1643
-
1644
- return jsonify({'success': True})
1645
- except Exception as e:
1646
- return jsonify({'success': False, 'error': str(e)})
1647
-
1648
- @app.route('/api/rename', methods=['POST'])
1649
- def rename_item():
1650
- try:
1651
- data = request.get_json()
1652
- path = data.get('path', '')
1653
- new_name = data.get('new_name', '')
1654
-
1655
- old_path = os.path.join(MC_DIR, path)
1656
- new_path = os.path.join(os.path.dirname(old_path), new_name)
1657
-
1658
- # Security check
1659
- if not os.path.abspath(old_path).startswith(os.path.abspath(MC_DIR)) or \
1660
- not os.path.abspath(new_path).startswith(os.path.abspath(MC_DIR)):
1661
- return jsonify({'success': False, 'error': 'Invalid path'})
1662
-
1663
- os.rename(old_path, new_path)
1664
- return jsonify({'success': True})
1665
- except Exception as e:
1666
- return jsonify({'success': False, 'error': str(e)})
1667
-
1668
- @app.route('/api/mkdir', methods=['POST'])
1669
- def create_folder():
1670
- try:
1671
- data = request.get_json()
1672
- path = data.get('path', '')
1673
- name = data.get('name', '')
1674
-
1675
- new_folder = os.path.join(MC_DIR, path, name)
1676
-
1677
- # Security check
1678
- if not os.path.abspath(new_folder).startswith(os.path.abspath(MC_DIR)):
1679
- return jsonify({'success': False, 'error': 'Invalid path'})
1680
-
1681
- os.makedirs(new_folder, exist_ok=True)
1682
- return jsonify({'success': True})
1683
- except Exception as e:
1684
- return jsonify({'success': False, 'error': str(e)})
1685
-
1686
  @sock.route('/ws')
1687
  def websocket(ws):
1688
  print("[WS] Client connected", flush=True)
@@ -1714,21 +776,13 @@ def websocket(ws):
1714
  memory = psutil.virtual_memory()
1715
  running = mc_process is not None and mc_process.poll() is None
1716
 
1717
- # Calculate disk usage
1718
- disk_size = 0
1719
- for dirpath, dirnames, filenames in os.walk(MC_DIR):
1720
- for f in filenames:
1721
- fp = os.path.join(dirpath, f)
1722
- disk_size += os.path.getsize(fp)
1723
-
1724
  ws.send(json.dumps({
1725
  'type': 'stats',
1726
  'cpu': cpu,
1727
  'memory': memory.percent,
1728
  'memoryTotal': memory.total,
1729
  'running': running,
1730
- 'players': 0,
1731
- 'disk': format_size(disk_size)
1732
  }))
1733
  except Exception as e:
1734
  print(f"[WS] Stats error: {e}", flush=True)
 
77
  echo ""
78
 
79
  # ==========================================
80
+ # WEB TERMINAL SERVER (Pterodactyl Style)
81
  # ==========================================
82
  cat > /app/web_terminal.py << 'PYTHON_SCRIPT'
83
  import os
 
88
  import time
89
  import queue
90
  import signal
91
+ from flask import Flask, render_template_string
 
92
  from flask_sock import Sock
93
  import psutil
94
 
 
100
  command_queue = queue.Queue()
101
  server_ready = threading.Event()
102
 
 
 
103
  def start_minecraft():
104
  global mc_process
105
+ mc_dir = "/app/mcserver"
106
 
107
  print("[MC] Starting Minecraft server...", flush=True)
108
 
 
110
  ["java", "-Xms1G", "-Xmx14G", "-XX:+UseG1GC",
111
  "-XX:+ParallelRefProcEnabled", "-XX:MaxGCPauseMillis=200",
112
  "-jar", "server.jar", "nogui"],
113
+ cwd=mc_dir,
114
  stdin=subprocess.PIPE,
115
  stdout=subprocess.PIPE,
116
  stderr=subprocess.STDOUT,
 
152
 
153
  threading.Thread(target=process_commands, daemon=True).start()
154
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  HTML_TEMPLATE = '''
156
  <!DOCTYPE html>
157
  <html lang="en">
158
  <head>
159
  <meta charset="UTF-8">
160
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
161
  <title>Minecraft Server - Web Console</title>
162
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
163
  <style>
164
  * { margin: 0; padding: 0; box-sizing: border-box; }
165
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  body {
167
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
168
+ background: #1a1d21;
169
+ color: #e0e0e0;
170
  height: 100vh;
171
  overflow: hidden;
172
  }
173
 
174
+ /* Top Navigation */
175
  .top-nav {
176
+ background: #252a30;
177
+ border-bottom: 1px solid #363b42;
178
+ padding: 0 20px;
179
  display: flex;
180
  align-items: center;
181
+ height: 50px;
 
 
 
 
 
 
 
 
 
 
 
182
  }
183
 
184
+ .nav-tabs { display: flex; gap: 0; height: 100%; }
185
 
186
  .nav-tab {
187
+ padding: 0 20px;
188
  display: flex;
189
  align-items: center;
190
  gap: 8px;
191
+ color: #9ca3af;
192
  text-decoration: none;
193
  font-size: 14px;
194
  border-bottom: 2px solid transparent;
195
  transition: all 0.2s;
196
  cursor: pointer;
 
 
197
  }
198
 
199
+ .nav-tab:hover { color: #e0e0e0; background: rgba(255,255,255,0.05); }
200
+ .nav-tab.active { color: #fff; border-bottom-color: #3b82f6; background: rgba(59, 130, 246, 0.1); }
201
  .nav-tab i { font-size: 14px; }
202
 
203
  .nav-right {
204
  margin-left: auto;
205
  display: flex;
206
  align-items: center;
207
+ gap: 10px;
208
  }
209
 
210
  .nav-btn {
211
  background: transparent;
212
  border: none;
213
+ color: #9ca3af;
214
+ padding: 8px;
215
  cursor: pointer;
216
+ border-radius: 4px;
217
  transition: all 0.2s;
 
218
  }
219
 
220
  .nav-btn:hover { color: #fff; background: rgba(255,255,255,0.1); }
221
 
222
+ /* Main Layout */
223
+ .main-container { display: flex; height: calc(100vh - 50px); }
 
 
 
 
 
 
 
 
224
 
225
+ /* Console Area */
226
+ .console-section {
227
  flex: 1;
228
+ display: flex;
229
  flex-direction: column;
230
+ background: #0d1117;
 
231
  }
232
 
 
 
 
 
 
233
  .console-header {
234
+ background: #161b22;
235
+ padding: 15px 20px;
236
+ border-bottom: 1px solid #30363d;
237
  display: flex;
238
+ justify-content: space-between;
239
+ align-items: center;
 
 
 
 
 
 
 
 
240
  }
241
 
242
  .server-title {
243
+ font-size: 20px;
244
  font-weight: 600;
245
  color: #f0f6fc;
246
  display: flex;
247
  align-items: center;
248
+ gap: 10px;
249
  }
250
 
251
+ .server-title i { color: #3b82f6; }
252
 
253
+ .control-buttons { display: flex; gap: 8px; }
 
 
 
 
254
 
255
  .btn {
256
+ padding: 8px 16px;
257
  border: none;
258
+ border-radius: 4px;
259
+ font-size: 13px;
260
  font-weight: 500;
261
  cursor: pointer;
262
  display: flex;
263
  align-items: center;
264
+ gap: 6px;
265
  transition: all 0.2s;
 
 
 
 
 
 
 
 
 
 
266
  }
267
 
268
+ .btn-primary { background: #2563eb; color: white; }
269
+ .btn-primary:hover { background: #1d4ed8; }
270
  .btn-secondary { background: #374151; color: #e0e0e0; }
271
  .btn-secondary:hover { background: #4b5563; }
272
+ .btn-danger { background: #dc2626; color: white; }
273
  .btn-danger:hover { background: #b91c1c; }
274
  .btn:disabled { opacity: 0.5; cursor: not-allowed; }
275
 
276
  /* Terminal */
277
  .terminal-container {
278
  flex: 1;
279
+ background: #0d1117;
280
+ padding: 0;
281
  overflow: hidden;
282
  position: relative;
283
  }
 
285
  .terminal {
286
  width: 100%;
287
  height: 100%;
288
+ padding: 15px;
289
  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
290
+ font-size: 13px;
291
  line-height: 1.5;
292
+ color: #e6edf3;
293
  overflow-y: auto;
294
  white-space: pre-wrap;
295
  word-wrap: break-word;
296
  }
297
 
298
+ .terminal-line { margin: 2px 0; padding: 1px 0; }
 
 
 
 
299
  .terminal-line.info { color: #58a6ff; }
300
  .terminal-line.warn { color: #d29922; }
301
  .terminal-line.error { color: #f85149; }
 
303
 
304
  /* Command Input Area */
305
  .command-area {
306
+ background: #161b22;
307
+ border-top: 1px solid #30363d;
308
+ padding: 12px 20px;
309
  display: flex;
310
+ gap: 10px;
311
  align-items: center;
312
  }
313
 
 
 
 
 
314
  .command-input-wrapper {
315
  flex: 1;
316
  display: flex;
317
  align-items: center;
318
+ background: #0d1117;
319
+ border: 1px solid #30363d;
320
+ border-radius: 6px;
321
+ padding: 0 15px;
322
  transition: border-color 0.2s;
323
  }
324
 
325
  .command-input-wrapper:focus-within { border-color: #58a6ff; }
326
+ .command-input-wrapper i { color: #6e7681; margin-right: 10px; }
327
 
328
  #commandInput {
329
  flex: 1;
330
  background: transparent;
331
  border: none;
332
+ color: #e6edf3;
333
+ padding: 12px 0;
334
  font-size: 14px;
335
  font-family: 'SFMono-Regular', Consolas, monospace;
336
  outline: none;
 
337
  }
338
 
339
  #commandInput::placeholder { color: #6e7681; }
 
342
  background: #238636;
343
  color: white;
344
  border: none;
345
+ padding: 12px 24px;
346
+ border-radius: 6px;
347
  cursor: pointer;
348
  font-weight: 500;
349
  display: flex;
350
  align-items: center;
351
+ gap: 6px;
352
  transition: background 0.2s;
 
 
 
 
 
353
  }
354
 
355
  .send-btn:hover { background: #2ea043; }
356
  .send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
357
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  /* Sidebar */
359
  .sidebar {
360
+ width: 300px;
361
  background: #1f2428;
362
+ border-left: 1px solid #363b42;
363
+ padding: 20px;
364
  overflow-y: auto;
 
 
 
 
 
 
 
 
 
 
365
  }
366
 
367
  .stat-card {
368
+ background: #252a30;
369
+ border: 1px solid #363b42;
370
  border-radius: 8px;
371
+ padding: 16px;
372
+ margin-bottom: 12px;
373
  display: flex;
374
  align-items: center;
375
+ gap: 15px;
376
+ transition: transform 0.2s, box-shadow 0.2s;
377
  }
378
 
379
+ .stat-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
 
 
 
 
 
 
380
 
381
  .stat-icon {
382
+ width: 45px;
383
+ height: 45px;
384
  border-radius: 8px;
385
  display: flex;
386
  align-items: center;
387
  justify-content: center;
388
+ font-size: 20px;
 
389
  }
390
 
391
+ .stat-icon.blue { background: rgba(59, 130, 246, 0.2); color: #3b82f6; }
392
+ .stat-icon.green { background: rgba(34, 197, 94, 0.2); color: #22c55e; }
393
+ .stat-icon.red { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
394
+ .stat-icon.yellow { background: rgba(234, 179, 8, 0.2); color: #eab308; }
395
  .stat-icon.purple { background: rgba(168, 85, 247, 0.2); color: #a855f7; }
396
  .stat-icon.cyan { background: rgba(6, 182, 212, 0.2); color: #06b6d4; }
397
 
398
+ .stat-info { flex: 1; }
399
+ .stat-label { font-size: 12px; color: #9ca3af; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
400
+ .stat-value { font-size: 18px; font-weight: 600; color: #f0f6fc; }
401
+ .stat-sub { font-size: 11px; color: #6b7280; margin-top: 2px; }
402
 
403
  /* Status Badge */
404
  .status-indicator {
405
  display: inline-flex;
406
  align-items: center;
407
+ gap: 6px;
408
+ padding: 4px 10px;
409
  border-radius: 20px;
410
+ font-size: 11px;
411
  font-weight: 600;
412
  text-transform: uppercase;
413
  }
414
 
415
+ .status-online { background: rgba(34, 197, 94, 0.2); color: #22c55e; }
416
+ .status-offline { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
417
+ .status-starting { background: rgba(234, 179, 8, 0.2); color: #eab308; }
418
 
419
+ .pulse { width: 8px; height: 8px; border-radius: 50%; background: currentColor; animation: pulse 2s infinite; }
420
  @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
421
 
422
  /* Scrollbar */
423
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
424
+ ::-webkit-scrollbar-track { background: #1a1d21; }
425
+ ::-webkit-scrollbar-thumb { background: #4b5563; border-radius: 4px; }
426
  ::-webkit-scrollbar-thumb:hover { background: #6b7280; }
427
 
428
  /* Connection Status */
429
  .connection-bar {
430
+ position: fixed;
431
+ top: 50px;
432
  left: 0;
433
+ right: 300px;
434
+ padding: 8px 20px;
435
+ font-size: 12px;
436
  font-weight: 500;
437
  display: flex;
438
  align-items: center;
439
+ gap: 8px;
440
+ z-index: 100;
441
  transition: all 0.3s;
442
  }
443
 
444
+ .connection-bar.connected { background: rgba(34, 197, 94, 0.15); color: #22c55e; border-bottom: 1px solid rgba(34, 197, 94, 0.3); }
445
+ .connection-bar.disconnected { background: rgba(239, 68, 68, 0.15); color: #ef4444; border-bottom: 1px solid rgba(239, 68, 68, 0.3); }
446
+ .connection-bar.connecting { background: rgba(234, 179, 8, 0.15); color: #eab308; border-bottom: 1px solid rgba(234, 179, 8, 0.3); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
447
  </style>
448
  </head>
449
  <body>
 
451
  <nav class="top-nav">
452
  <div class="nav-tabs">
453
  <a class="nav-tab active" onclick="switchTab('console', this)">
454
+ <i class="fas fa-terminal"></i> Console
455
  </a>
456
  <a class="nav-tab" onclick="switchTab('files', this)">
457
+ <i class="fas fa-folder"></i> Files
458
  </a>
459
  </div>
460
  <div class="nav-right">
461
+ <button class="nav-btn" title="Settings"><i class="fas fa-cog"></i></button>
462
+ <button class="nav-btn" title="Full Screen" onclick="toggleFullscreen()"><i class="fas fa-expand"></i></button>
 
 
 
 
463
  </div>
464
  </nav>
465
 
466
  <!-- Main Container -->
467
  <div class="main-container">
468
  <!-- Console Section -->
469
+ <div class="console-section" id="consoleSection">
470
+ <div class="connection-bar connecting" id="connectionBar">
471
+ <i class="fas fa-circle-notch fa-spin"></i>
472
+ <span>Connecting to server...</span>
473
+ </div>
474
+
475
+ <div class="console-header">
476
+ <div class="server-title">
477
+ <i class="fas fa-server"></i>
478
+ <span>Minecraft Server</span>
479
+ <span class="status-indicator status-offline" id="serverStatus">
480
+ <span class="pulse"></span> Offline
481
+ </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482
  </div>
483
+ <div class="control-buttons">
484
+ <button class="btn btn-primary" id="startBtn" onclick="sendCmd('start')" disabled>
485
+ <i class="fas fa-play"></i> Start
486
+ </button>
487
+ <button class="btn btn-secondary" id="restartBtn" onclick="sendCmd('restart')" disabled>
488
+ <i class="fas fa-redo"></i> Restart
489
+ </button>
490
+ <button class="btn btn-danger" id="stopBtn" onclick="sendCmd('stop')" disabled>
491
+ <i class="fas fa-stop"></i> Stop
492
  </button>
493
  </div>
494
  </div>
495
+
496
+ <div class="terminal-container">
497
+ <div class="terminal" id="terminal"></div>
498
+ </div>
499
+
500
+ <div class="command-area">
501
+ <div class="command-input-wrapper">
502
+ <i class="fas fa-angle-right"></i>
503
+ <input type="text" id="commandInput" placeholder="Type a command..." autocomplete="off" disabled>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504
  </div>
505
+ <button class="send-btn" id="sendBtn" onclick="sendInput()" disabled>
506
+ <i class="fas fa-paper-plane"></i> Send
507
+ </button>
508
  </div>
509
  </div>
510
 
 
565
  </div>
566
  </div>
567
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
568
 
569
  <script>
570
  const terminal = document.getElementById('terminal');
 
580
  let maxReconnectAttempts = 10;
581
  let serverStartTime = null;
582
  let isServerRunning = false;
 
 
 
 
 
583
 
584
  function updateConnectionStatus(status, message) {
585
  connectionBar.className = 'connection-bar ' + status;
 
674
  if (data.players !== undefined) {
675
  document.getElementById('playerCount').textContent = data.players;
676
  }
 
 
 
677
  }
678
  };
679
 
 
714
  if (e.key === 'Enter') sendInput();
715
  });
716
 
 
 
717
  function switchTab(tab, element) {
718
  document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
719
  element.classList.add('active');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
720
  }
721
 
722
  function toggleFullscreen() {
 
729
 
730
  document.getElementById('serverAddress').textContent = window.location.hostname;
731
  addTerminalLine('Connecting to Minecraft server...', 'info');
 
 
 
732
  </script>
733
  </body>
734
  </html>
 
745
  'minecraft': mc_process is not None and mc_process.poll() is None
746
  }
747
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
748
  @sock.route('/ws')
749
  def websocket(ws):
750
  print("[WS] Client connected", flush=True)
 
776
  memory = psutil.virtual_memory()
777
  running = mc_process is not None and mc_process.poll() is None
778
 
 
 
 
 
 
 
 
779
  ws.send(json.dumps({
780
  'type': 'stats',
781
  'cpu': cpu,
782
  'memory': memory.percent,
783
  'memoryTotal': memory.total,
784
  'running': running,
785
+ 'players': 0
 
786
  }))
787
  except Exception as e:
788
  print(f"[WS] Stats error: {e}", flush=True)