OrbitMC commited on
Commit
dd25227
·
verified ·
1 Parent(s): 9d2db55

Update panel.py

Browse files
Files changed (1) hide show
  1. panel.py +1521 -443
panel.py CHANGED
@@ -10,463 +10,1481 @@ import uvicorn
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
  # -----------------
19
- # HTML FRONTEND (Web3 / Modern SaaS Dashboard)
20
  # -----------------
21
  HTML_CONTENT = """
22
  <!DOCTYPE html>
23
- <html lang="en" class="dark">
24
  <head>
25
- <meta charset="UTF-8">
26
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
27
- <title>ServerSpace | Dashboard</title>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
30
- <script src="https://unpkg.com/@phosphor-icons/web"></script>
31
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css" />
32
- <script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
33
- <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit/lib/xterm-addon-fit.js"></script>
34
- <script src="https://cdn.tailwindcss.com"></script>
35
- <script>
36
- tailwind.config = {
37
- darkMode: 'class',
38
- theme: {
39
- extend: {
40
- fontFamily: { sans: ['Inter', 'sans-serif'], mono: ['JetBrains Mono', 'monospace'] },
41
- colors: {
42
- base: '#06080D',
43
- surface: '#10141F',
44
- surfaceHover: '#1A2133',
45
- border: '#232B40',
46
- primary: '#8B5CF6', /* Violet */
47
- secondary: '#D946EF', /* Fuchsia */
48
- accent: '#0EA5E9' /* Cyan */
49
- },
50
- boxShadow: {
51
- 'neon': '0 0 20px rgba(139, 92, 246, 0.3)',
52
- 'neon-strong': '0 0 30px rgba(217, 70, 239, 0.4)'
53
- }
54
- }
55
- }
56
- }
57
- </script>
58
- <style>
59
- body { background-color: theme('colors.base'); color: #F8FAFC; overflow: hidden; -webkit-font-smoothing: antialiased; }
60
 
61
- /* Ambient Background Glows */
62
- .ambient-glow-1 { position: absolute; top: -10%; left: -10%; width: 40vw; height: 40vw; background: radial-gradient(circle, rgba(139,92,246,0.15) 0%, rgba(0,0,0,0) 70%); border-radius: 50%; pointer-events: none; z-index: 0; filter: blur(60px); }
63
- .ambient-glow-2 { position: absolute; bottom: -20%; right: -10%; width: 50vw; height: 50vw; background: radial-gradient(circle, rgba(217,70,239,0.1) 0%, rgba(0,0,0,0) 70%); border-radius: 50%; pointer-events: none; z-index: 0; filter: blur(80px); }
64
-
65
- /* Dashboard Cards - Glassmorphism */
66
- .premium-card {
67
- background: rgba(16, 20, 31, 0.6);
68
- backdrop-filter: blur(16px);
69
- -webkit-backdrop-filter: blur(16px);
70
- border: 1px solid rgba(255, 255, 255, 0.05);
71
- border-radius: 24px;
72
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
73
- position: relative;
74
- overflow: hidden;
75
- z-index: 10;
76
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
- /* Gradients & Buttons */
79
- .text-gradient { background: linear-gradient(135deg, theme('colors.primary'), theme('colors.secondary')); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
80
- .bg-gradient-btn {
81
- background: linear-gradient(135deg, theme('colors.primary'), theme('colors.secondary'));
82
- box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3);
83
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
84
- position: relative;
85
- overflow: hidden;
86
  }
87
- .bg-gradient-btn::before {
88
- content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%;
89
- background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
90
- transition: all 0.5s ease;
 
 
 
 
91
  }
92
- .bg-gradient-btn:hover { transform: translateY(-2px); box-shadow: theme('boxShadow.neon-strong'); filter: brightness(1.1); }
93
- .bg-gradient-btn:hover::before { left: 100%; }
94
-
95
- /* Terminal Fixing for Mobile Wrapping */
96
- .term-container { flex: 1; min-width: 0; min-height: 0; width: 100%; height: 100%; overflow: hidden; position: relative; }
97
- .term-wrapper { padding: 16px; height: 100%; width: 100%; }
98
- .xterm .xterm-viewport { overflow-y: auto !important; width: 100% !important; background-color: transparent !important; }
99
- .xterm-screen { width: 100% !important; }
100
-
101
- /* Custom Scrollbar */
102
- ::-webkit-scrollbar { width: 6px; height: 6px; }
103
- ::-webkit-scrollbar-track { background: transparent; }
104
- ::-webkit-scrollbar-thumb { background: theme('colors.border'); border-radius: 10px; }
105
- ::-webkit-scrollbar-thumb:hover { background: theme('colors.primary'); }
106
-
107
- /* Navigation States */
108
- .nav-item { color: #64748B; transition: all 0.3s ease; position: relative; }
109
- .nav-item:hover { color: #F8FAFC; background: rgba(255,255,255,0.03); }
110
- .nav-item.active { color: #F8FAFC; background: linear-gradient(90deg, rgba(139, 92, 246, 0.15) 0%, transparent 100%); border-left: 3px solid theme('colors.primary'); }
111
-
112
- /* Mobile Nav Floating Glass */
113
- .mobile-nav-glass {
114
- background: rgba(16, 20, 31, 0.85);
115
- backdrop-filter: blur(20px);
116
- -webkit-backdrop-filter: blur(20px);
117
- border: 1px solid rgba(255, 255, 255, 0.08);
118
- box-shadow: 0 -10px 40px rgba(0,0,0,0.5);
119
- margin: 0 16px 16px 16px;
120
- border-radius: 24px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  }
122
 
123
- .mob-nav-item { color: #64748B; transition: color 0.3s; }
124
- .mob-nav-item.active { color: theme('colors.primary'); text-shadow: 0 0 15px rgba(139,92,246,0.5); }
125
-
126
- /* Animations */
127
- .fade-in { animation: fadeIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
128
- @keyframes fadeIn { from { opacity: 0; transform: translateY(10px) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } }
129
- .hidden-tab { display: none !important; }
130
- </style>
131
- </head>
132
- <body class="flex flex-col md:flex-row h-[100dvh] w-full text-sm md:text-base relative">
133
-
134
- <div class="ambient-glow-1"></div>
135
- <div class="ambient-glow-2"></div>
136
-
137
- <aside class="hidden md:flex flex-col w-[280px] bg-surface/40 backdrop-blur-xl border-r border-white/5 shrink-0 z-20 shadow-2xl">
138
- <div class="p-8 pb-4">
139
- <div class="flex items-center gap-4">
140
- <div class="w-12 h-12 rounded-2xl bg-gradient-btn flex items-center justify-center shadow-neon">
141
- <i class="ph ph-hexagon text-2xl text-white"></i>
142
- </div>
143
- <div>
144
- <h1 class="font-bold text-xl text-white tracking-tight">Server<span class="text-gradient">Space</span></h1>
145
- <p class="text-[11px] text-primary font-mono uppercase tracking-widest mt-0.5">Engine v2.0</p>
146
- </div>
147
- </div>
148
- </div>
149
-
150
- <div class="px-6 py-6 flex-grow">
151
- <div class="text-[11px] font-bold text-slate-500 uppercase tracking-widest mb-4 px-3">Dashboard</div>
152
- <nav class="flex flex-col gap-2">
153
- <button onclick="switchTab('console')" id="nav-console" class="nav-item active flex items-center gap-4 px-4 py-3.5 rounded-r-xl font-medium">
154
- <i class="ph ph-terminal-window text-xl"></i> Console
155
- </button>
156
- <button onclick="switchTab('files')" id="nav-files" class="nav-item flex items-center gap-4 px-4 py-3.5 rounded-r-xl font-medium border-l-3 border-transparent">
157
- <i class="ph ph-folder-notch text-xl"></i> File Explorer
158
- </button>
159
- </nav>
160
- </div>
161
-
162
- <div class="p-6">
163
- <div class="bg-black/30 border border-white/5 rounded-2xl p-4 flex items-center gap-4 backdrop-blur-md">
164
- <div class="relative flex h-4 w-4">
165
- <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-accent opacity-60"></span>
166
- <span class="relative inline-flex rounded-full h-4 w-4 bg-accent shadow-[0_0_10px_#0EA5E9]"></span>
167
- </div>
168
- <div>
169
- <div class="text-sm font-semibold text-white">System Online</div>
170
- <div class="text-xs font-mono text-slate-400 mt-0.5">Latency: 24ms</div>
171
- </div>
172
  </div>
173
- </div>
174
- </aside>
175
-
176
- <header class="md:hidden flex justify-between items-center px-6 py-5 bg-surface/80 backdrop-blur-md border-b border-white/5 shrink-0 z-20">
177
- <div class="flex items-center gap-3">
178
- <div class="w-10 h-10 rounded-xl bg-gradient-btn flex items-center justify-center">
179
- <i class="ph ph-hexagon text-xl text-white"></i>
180
  </div>
181
- <h1 class="font-bold text-lg text-white">Server<span class="text-gradient">Space</span></h1>
182
- </div>
183
- <div class="relative flex h-3 w-3">
184
- <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-accent opacity-60"></span>
185
- <span class="relative inline-flex rounded-full h-3 w-3 bg-accent"></span>
186
- </div>
187
- </header>
188
-
189
- <main class="flex-grow flex flex-col p-4 md:p-8 overflow-hidden min-w-0 relative z-10 pb-24 md:pb-8">
190
-
191
- <div id="tab-console" class="h-full flex flex-col fade-in min-w-0">
192
- <div class="mb-4 hidden md:flex justify-between items-end">
193
- <div>
194
- <h2 class="text-2xl font-bold text-white">Live Terminal</h2>
195
- <p class="text-slate-400 text-sm mt-1">Execute commands directly on the server container.</p>
196
- </div>
197
  </div>
 
 
198
 
199
- <div class="premium-card flex flex-col flex-grow min-h-0">
200
- <div class="bg-black/40 border-b border-white/5 px-5 py-4 flex items-center justify-between z-10 shrink-0">
201
- <div class="flex gap-2">
202
- <div class="w-3.5 h-3.5 rounded-full bg-red-500/80 shadow-[0_0_8px_rgba(239,68,68,0.5)]"></div>
203
- <div class="w-3.5 h-3.5 rounded-full bg-yellow-500/80 shadow-[0_0_8px_rgba(234,179,8,0.5)]"></div>
204
- <div class="w-3.5 h-3.5 rounded-full bg-green-500/80 shadow-[0_0_8px_rgba(34,197,94,0.5)]"></div>
205
- </div>
206
- <span class="text-xs font-mono text-slate-400 bg-white/5 px-3 py-1 rounded-full border border-white/5">root@serverspace:~</span>
207
- <div class="w-14"></div> </div>
208
-
209
- <div class="term-container bg-transparent">
210
- <div id="terminal" class="term-wrapper"></div>
211
- </div>
212
-
213
- <div class="p-3 md:p-5 bg-black/40 border-t border-white/5 z-10 shrink-0 backdrop-blur-xl">
214
- <div class="relative flex items-center">
215
- <i class="ph ph-caret-right text-primary absolute left-5 text-xl animate-pulse"></i>
216
- <input type="text" id="cmd-input" class="w-full bg-surfaceHover/50 border border-white/10 focus:border-primary/50 focus:bg-surfaceHover text-white rounded-xl pl-12 pr-14 py-3.5 md:py-4 text-sm font-mono transition-all outline-none shadow-inner" placeholder="Enter command...">
217
- <button onclick="sendCommand()" class="absolute right-2 p-2.5 bg-gradient-btn rounded-lg text-white">
218
- <i class="ph-bold ph-paper-plane-right text-lg"></i>
219
- </button>
220
- </div>
221
- </div>
222
- </div>
223
- </div>
224
 
225
- <div id="tab-files" class="hidden-tab h-full flex flex-col min-w-0">
226
-
227
- <div class="mb-4 hidden md:flex justify-between items-end">
228
- <div>
229
- <h2 class="text-2xl font-bold text-white">File Explorer</h2>
230
- <p class="text-slate-400 text-sm mt-1">Manage, upload, and edit your server configurations.</p>
231
- </div>
232
- </div>
233
 
234
- <div class="flex flex-col flex-grow premium-card overflow-hidden min-w-0">
235
- <div class="bg-black/40 px-5 md:px-6 py-4 md:py-5 border-b border-white/5 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 shrink-0">
236
- <div class="flex items-center gap-2 text-sm font-mono text-slate-300 overflow-x-auto whitespace-nowrap w-full sm:w-auto bg-surfaceHover/50 px-4 py-2 rounded-lg border border-white/5 shadow-inner" id="breadcrumbs">
237
- </div>
238
- <div class="flex gap-3 shrink-0">
239
- <input type="file" id="file-upload" class="hidden" onchange="uploadFile(event)">
240
- <button onclick="document.getElementById('file-upload').click()" class="bg-gradient-btn px-5 py-2.5 rounded-xl text-xs md:text-sm font-bold text-white flex items-center gap-2">
241
- <i class="ph-bold ph-upload-simple text-lg"></i> Upload
242
- </button>
243
- <button onclick="loadFiles(currentPath)" class="bg-white/5 border border-white/10 px-4 py-2.5 rounded-xl text-slate-300 hover:text-white hover:bg-white/10 transition-all shadow-lg">
244
- <i class="ph-bold ph-arrows-clockwise text-lg"></i>
245
- </button>
246
- </div>
247
- </div>
248
-
249
- <div class="hidden sm:grid grid-cols-12 gap-4 px-8 py-3 bg-white/[0.02] border-b border-white/5 text-[11px] font-bold text-slate-400 uppercase tracking-wider shrink-0">
250
- <div class="col-span-7">Filename</div>
251
- <div class="col-span-3 text-right">Size</div>
252
- <div class="col-span-2 text-right">Actions</div>
253
- </div>
254
-
255
- <div class="flex-grow overflow-y-auto bg-transparent p-3 md:p-4" id="file-list">
256
- </div>
257
- </div>
258
- </div>
259
- </main>
260
 
261
- <nav class="md:hidden mobile-nav-glass fixed bottom-0 left-0 right-0 py-3 px-8 flex justify-between items-center z-50">
262
- <button onclick="switchTab('console')" id="mob-console" class="mob-nav-item active flex flex-col items-center gap-1.5 w-20">
263
- <i class="ph-fill ph-terminal-window text-2xl"></i>
264
- <span class="text-[10px] font-semibold tracking-wide uppercase">Console</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  </button>
266
- <div class="w-12 h-12 bg-gradient-btn rounded-full flex items-center justify-center shadow-neon -mt-6 border-[4px] border-[#080B11]">
267
- <i class="ph ph-cube text-white text-xl"></i>
268
- </div>
269
- <button onclick="switchTab('files')" id="mob-files" class="mob-nav-item flex flex-col items-center gap-1.5 w-20">
270
- <i class="ph-fill ph-folder-notch text-2xl"></i>
271
- <span class="text-[10px] font-semibold tracking-wide uppercase">Files</span>
272
  </button>
273
- </nav>
274
-
275
- <div id="editor-modal" class="fixed inset-0 bg-black/60 backdrop-blur-md hidden items-center justify-center p-4 md:p-8 z-[100] opacity-0 transition-opacity duration-300">
276
- <div class="premium-card w-full max-w-5xl h-[90vh] flex flex-col transform scale-95 transition-transform duration-300 ring-1 ring-white/10 shadow-[0_0_50px_rgba(0,0,0,0.8)]" id="editor-card">
277
- <div class="bg-black/60 px-6 py-5 flex justify-between items-center border-b border-white/10 shrink-0 backdrop-blur-xl">
278
- <div class="flex items-center gap-3 text-sm font-mono text-white bg-white/5 px-4 py-2 rounded-lg">
279
- <i class="ph-fill ph-file-code text-primary text-xl"></i>
280
- <span id="editor-title">file.txt</span>
281
- </div>
282
- <div class="flex gap-3">
283
- <button onclick="closeEditor()" class="px-5 py-2.5 bg-white/5 hover:bg-white/10 border border-white/10 rounded-xl text-xs md:text-sm font-bold text-slate-300 transition-colors">Close</button>
284
- <button onclick="saveFile()" class="bg-gradient-btn px-6 py-2.5 rounded-xl text-xs md:text-sm font-bold text-white shadow-neon flex items-center gap-2">
285
- <i class="ph-bold ph-floppy-disk text-lg"></i> Save Changes
286
- </button>
287
- </div>
288
- </div>
289
- <textarea id="editor-content" class="flex-grow bg-[#06080D]/80 text-slate-200 p-6 font-mono text-sm md:text-base resize-none focus:outline-none w-full leading-relaxed" spellcheck="false"></textarea>
290
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  </div>
 
 
 
 
 
 
 
 
292
 
293
- <div id="toast-container" class="fixed top-6 right-6 md:top-8 md:right-8 z-[200] flex flex-col gap-4 pointer-events-none"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
 
295
- <script>
296
- // --- Tab Navigation ---
297
- function switchTab(tab) {
298
- document.getElementById('tab-console').classList.add('hidden-tab');
299
- document.getElementById('tab-files').classList.add('hidden-tab');
300
-
301
- // Reset Desktop
302
- document.getElementById('nav-console').className = "nav-item flex items-center gap-4 px-4 py-3.5 rounded-r-xl font-medium border-l-3 border-transparent";
303
- document.getElementById('nav-files').className = "nav-item flex items-center gap-4 px-4 py-3.5 rounded-r-xl font-medium border-l-3 border-transparent";
304
-
305
- // Reset Mobile
306
- document.getElementById('mob-console').classList.remove('active');
307
- document.getElementById('mob-files').classList.remove('active');
308
-
309
- // Activate
310
- document.getElementById('tab-' + tab).classList.remove('hidden-tab');
311
- document.getElementById('tab-' + tab).classList.add('fade-in');
312
-
313
- document.getElementById('nav-' + tab).className = "nav-item active flex items-center gap-4 px-4 py-3.5 rounded-r-xl font-medium";
314
- document.getElementById('mob-' + tab).classList.add('active');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
 
316
- if(tab === 'console' && fitAddon) setTimeout(() => fitAddon.fit(), 100);
317
- if(tab === 'files' && !window.filesLoaded) { loadFiles(''); window.filesLoaded = true; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  }
 
 
319
 
320
- // --- Terminal Engine ---
321
- const term = new Terminal({
322
- theme: { background: 'transparent', foreground: '#E2E8F0', cursor: '#8B5CF6', selectionBackground: 'rgba(139, 92, 246, 0.3)' },
323
- fontFamily: "'JetBrains Mono', monospace", fontSize: window.innerWidth < 768 ? 12 : 14, cursorBlink: true, convertEol: true
324
- });
325
- const fitAddon = new FitAddon.FitAddon();
326
- term.loadAddon(fitAddon);
327
- term.open(document.getElementById('terminal'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
 
329
- const ro = new ResizeObserver(() => {
330
- if(!document.getElementById('tab-console').classList.contains('hidden-tab')) {
331
- requestAnimationFrame(() => fitAddon.fit());
 
 
 
332
  }
333
  });
334
- ro.observe(document.querySelector('.term-container'));
335
- setTimeout(() => fitAddon.fit(), 200);
336
 
337
- const ws = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws');
338
- ws.onopen = () => term.write('\\x1b[38;5;135m\\x1b[1m[System]\\x1b[0m Secure engine connection established.\\r\\n');
339
- ws.onmessage = e => term.write(e.data + '\\n');
340
-
341
- const cmdInput = document.getElementById('cmd-input');
342
- cmdInput.addEventListener('keypress', e => { if (e.key === 'Enter') sendCommand(); });
343
- function sendCommand() {
344
- if(cmdInput.value.trim() && ws.readyState === WebSocket.OPEN) {
345
- term.write(`\\x1b[38;5;51m> ${cmdInput.value}\\x1b[0m\\r\\n`);
346
- ws.send(cmdInput.value); cmdInput.value = '';
347
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  }
 
349
 
350
- // --- File Manager ---
351
- let currentPath = '';
352
- let editPath = '';
 
353
 
354
- function showToast(msg, type='info') {
355
- const container = document.getElementById('toast-container');
356
- const el = document.createElement('div');
357
- let icon = '<i class="ph-fill ph-info text-accent text-2xl drop-shadow-[0_0_8px_#0EA5E9]"></i>';
358
- if(type==='success') icon = '<i class="ph-fill ph-check-circle text-green-400 text-2xl drop-shadow-[0_0_8px_#22C55E]"></i>';
359
- if(type==='error') icon = '<i class="ph-fill ph-warning-circle text-red-400 text-2xl drop-shadow-[0_0_8px_#EF4444]"></i>';
 
 
 
 
 
360
 
361
- el.className = `flex items-center gap-4 bg-surface/90 backdrop-blur-xl border border-white/10 text-white px-6 py-4 rounded-2xl shadow-[0_10px_40px_rgba(0,0,0,0.5)] translate-x-12 opacity-0 transition-all duration-300`;
362
- el.innerHTML = `${icon} <span class="font-medium text-sm tracking-wide">${msg}</span>`;
363
- container.appendChild(el);
364
-
365
- requestAnimationFrame(() => el.classList.remove('translate-x-12', 'opacity-0'));
366
- setTimeout(() => { el.classList.add('translate-x-12', 'opacity-0'); setTimeout(() => el.remove(), 300); }, 3500);
367
- }
368
 
369
- async function loadFiles(path) {
370
- currentPath = path;
371
- const parts = path.split('/').filter(p => p);
372
- let bc = `<button onclick="loadFiles('')" class="hover:text-primary transition"><i class="ph-fill ph-house text-lg"></i></button>`;
373
- let bp = '';
374
- parts.forEach((p, i) => {
375
- bp += (bp?'/':'') + p;
376
- bc += `<i class="ph-bold ph-caret-right text-xs mx-3 text-slate-600"></i>`;
377
- if(i === parts.length-1) bc += `<span class="text-primary font-bold bg-primary/10 px-2 py-0.5 rounded">${p}</span>`;
378
- else bc += `<button onclick="loadFiles('${bp}')" class="hover:text-primary transition">${p}</button>`;
379
- });
380
- document.getElementById('breadcrumbs').innerHTML = bc;
381
-
382
- const list = document.getElementById('file-list');
383
- list.innerHTML = `<div class="flex flex-col items-center justify-center py-20 gap-4"><i class="ph-bold ph-circle-notch animate-spin text-4xl text-primary"></i><span class="text-slate-500 font-mono text-sm">Syncing system files...</span></div>`;
384
-
385
- try {
386
- const res = await fetch(`/api/fs/list?path=${encodeURIComponent(path)}`);
387
- const files = await res.json();
388
- list.innerHTML = '';
389
-
390
- if (path !== '') {
391
- const parent = path.split('/').slice(0, -1).join('/');
392
- list.innerHTML += `
393
- <div class="flex items-center px-5 py-4 cursor-pointer hover:bg-white/5 rounded-2xl transition-all mb-2 border border-transparent" onclick="loadFiles('${parent}')">
394
- <div class="p-2 bg-white/5 rounded-lg mr-4"><i class="ph-bold ph-arrow-u-up-left text-slate-400 text-lg"></i></div>
395
- <span class="text-sm font-mono text-slate-300 font-semibold tracking-wide">.. / Return</span>
396
- </div>`;
397
- }
398
 
399
- files.forEach(f => {
400
- const icon = f.is_dir ? '<div class="p-3 bg-primary/10 border border-primary/20 rounded-xl text-primary shadow-[0_0_15px_rgba(139,92,246,0.15)] group-hover:bg-primary group-hover:text-white transition-all duration-300"><i class="ph-fill ph-folder text-xl"></i></div>' : '<div class="p-3 bg-surface border border-white/5 rounded-xl text-slate-400 group-hover:bg-white/10 group-hover:text-white transition-all duration-300"><i class="ph-fill ph-file-text text-xl"></i></div>';
401
- const sz = f.is_dir ? '<span class="px-2 py-1 bg-white/5 rounded text-[10px] text-slate-500">DIR</span>' : (f.size > 1048576 ? `<span class="px-2 py-1 bg-white/5 rounded text-[10px] text-slate-300">${(f.size/1048576).toFixed(1)} MB</span>` : `<span class="px-2 py-1 bg-white/5 rounded text-[10px] text-slate-400">${(f.size/1024).toFixed(1)} KB</span>`);
402
- const fp = path ? `${path}/${f.name}` : f.name;
403
-
404
- list.innerHTML += `
405
- <div class="flex flex-col sm:grid sm:grid-cols-12 items-start sm:items-center px-4 py-3 gap-3 group hover:bg-white/[0.03] rounded-2xl transition-all duration-300 mb-2 border border-transparent hover:border-white/5 hover:shadow-lg">
406
- <div class="col-span-7 flex items-center gap-5 w-full ${f.is_dir?'cursor-pointer':''}" ${f.is_dir?`onclick="loadFiles('${fp}')"`:''}>
407
- ${icon}
408
- <span class="text-sm font-mono text-slate-200 truncate group-hover:text-white transition font-medium tracking-wide">${f.name}</span>
409
- </div>
410
- <div class="col-span-3 text-right font-mono hidden sm:block">${sz}</div>
411
- <div class="col-span-2 flex justify-end gap-2 w-full sm:w-auto sm:opacity-0 group-hover:opacity-100 transition-opacity duration-300">
412
- ${!f.is_dir ? `<button onclick="editFile('${fp}')" class="p-2.5 bg-surface border border-white/5 hover:border-primary hover:text-primary hover:shadow-[0_0_10px_rgba(139,92,246,0.2)] rounded-xl transition-all"><i class="ph-bold ph-pencil-simple text-base"></i></button>` : ''}
413
- ${!f.is_dir ? `<a href="/api/fs/download?path=${encodeURIComponent(fp)}" class="p-2.5 bg-surface border border-white/5 hover:border-accent hover:text-accent hover:shadow-[0_0_10px_rgba(14,165,233,0.2)] rounded-xl transition-all"><i class="ph-bold ph-download-simple text-base"></i></a>` : ''}
414
- <button onclick="deleteFile('${fp}')" class="p-2.5 bg-surface border border-white/5 hover:border-secondary hover:text-secondary hover:shadow-[0_0_10px_rgba(217,70,239,0.2)] rounded-xl transition-all"><i class="ph-bold ph-trash text-base"></i></button>
415
- </div>
416
- </div>`;
417
- });
418
- } catch (err) { list.innerHTML = `<div class="text-center py-10 text-red-400 text-sm font-mono bg-red-500/10 border border-red-500/20 rounded-2xl mx-4">System fault: Unable to access directory mapping.</div>`; }
419
- }
420
 
421
- async function editFile(path) {
422
- try {
423
- const res = await fetch(`/api/fs/read?path=${encodeURIComponent(path)}`);
424
- if(res.ok) {
425
- editPath = path;
426
- document.getElementById('editor-content').value = await res.text();
427
- document.getElementById('editor-title').innerText = path.split('/').pop();
428
- const m = document.getElementById('editor-modal'); const c = document.getElementById('editor-card');
429
- m.classList.remove('hidden'); m.classList.add('flex');
430
- requestAnimationFrame(() => { m.classList.remove('opacity-0'); c.classList.remove('scale-95'); });
431
- } else showToast('Binary file cannot be parsed', 'error');
432
- } catch { showToast('Error accessing data block', 'error'); }
433
- }
434
 
435
- function closeEditor() {
436
- const m = document.getElementById('editor-modal'); const c = document.getElementById('editor-card');
437
- m.classList.add('opacity-0'); c.classList.add('scale-95');
438
- setTimeout(() => { m.classList.add('hidden'); m.classList.remove('flex'); }, 300);
439
- }
 
440
 
441
- async function saveFile() {
442
- const fd = new FormData(); fd.append('path', editPath); fd.append('content', document.getElementById('editor-content').value);
443
- try {
444
- const res = await fetch('/api/fs/write', { method: 'POST', body: fd });
445
- if(res.ok) { showToast('Block verified and saved', 'success'); closeEditor(); } else throw new Error();
446
- } catch { showToast('Write operation failed', 'error'); }
447
- }
448
 
449
- async function deleteFile(path) {
450
- if(confirm(`WARNING: Erase ${path.split('/').pop()} from the filesystem? This cannot be undone.`)) {
451
- const fd = new FormData(); fd.append('path', path);
452
- try {
453
- const res = await fetch('/api/fs/delete', { method: 'POST', body: fd });
454
- if(res.ok) { showToast('Data block purged', 'success'); loadFiles(currentPath); } else throw new Error();
455
- } catch { showToast('Purge operation failed', 'error'); }
456
- }
457
- }
458
 
459
- async function uploadFile(e) {
460
- if(!e.target.files.length) return;
461
- showToast('Injecting payload...', 'info');
462
- const fd = new FormData(); fd.append('path', currentPath); fd.append('file', e.target.files[0]);
463
- try {
464
- const res = await fetch('/api/fs/upload', { method: 'POST', body: fd });
465
- if(res.ok) { showToast('Payload injected successfully', 'success'); loadFiles(currentPath); } else throw new Error();
466
- } catch { showToast('Injection failed', 'error'); }
467
- e.target.value = '';
468
- }
469
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
470
  </body>
471
  </html>
472
  """
@@ -491,37 +1509,44 @@ async def broadcast(message: str):
491
  dead.add(client)
492
  connected_clients.difference_update(dead)
493
 
494
- async def read_stream(stream, prefix=""):
495
  while True:
496
  try:
497
  line = await stream.readline()
498
  if not line: break
499
- await broadcast(prefix + line.decode('utf-8', errors='replace').rstrip('\r\n'))
500
  except Exception:
501
  break
502
 
503
  async def start_minecraft():
504
  global mc_process
 
 
 
 
 
 
505
  java_args = [
506
- "java", "-server", "-Xmx8G", "-Xms8G", "-XX:+UseG1GC", "-XX:+ParallelRefProcEnabled",
507
- "-XX:ParallelGCThreads=2", "-XX:ConcGCThreads=1", "-XX:MaxGCPauseMillis=50",
508
- "-XX:+UnlockExperimentalVMOptions", "-XX:+DisableExplicitGC", "-XX:+AlwaysPreTouch",
509
- "-XX:G1NewSizePercent=30", "-XX:G1MaxNewSizePercent=50", "-XX:G1HeapRegionSize=16M",
510
- "-XX:G1ReservePercent=15", "-XX:G1HeapWastePercent=5", "-XX:G1MixedGCCountTarget=3",
511
- "-XX:InitiatingHeapOccupancyPercent=10", "-XX:G1MixedGCLiveThresholdPercent=90",
512
- "-XX:G1RSetUpdatingPauseTimePercent=5", "-XX:SurvivorRatio=32", "-XX:+PerfDisableSharedMem",
513
- "-XX:MaxTenuringThreshold=1", "-XX:G1SATBBufferEnqueueingThresholdPercent=30",
514
- "-XX:G1ConcMarkStepDurationMillis=5", "-XX:G1ConcRSHotCardLimit=16",
515
- "-XX:+UseStringDeduplication", "-Dfile.encoding=UTF-8", "-Dspring.output.ansi.enabled=ALWAYS",
516
- "-jar", "purpur.jar", "--nogui"
517
  ]
518
- mc_process = await asyncio.create_subprocess_exec(
519
- *java_args, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, cwd=BASE_DIR
520
- )
521
- asyncio.create_task(read_stream(mc_process.stdout))
 
 
 
 
 
 
 
 
 
522
 
523
  @app.on_event("startup")
524
  async def startup_event():
 
525
  asyncio.create_task(start_minecraft())
526
 
527
  # -----------------
@@ -534,13 +1559,19 @@ def get_panel(): return HTMLResponse(content=HTML_CONTENT)
534
  async def ws_endpoint(websocket: WebSocket):
535
  await websocket.accept()
536
  connected_clients.add(websocket)
 
537
  for line in output_history: await websocket.send_text(line)
538
  try:
539
  while True:
540
  cmd = await websocket.receive_text()
541
  if mc_process and mc_process.stdin:
542
- mc_process.stdin.write((cmd + "\n").encode('utf-8'))
543
- await mc_process.stdin.drain()
 
 
 
 
 
544
  except:
545
  connected_clients.remove(websocket)
546
 
@@ -549,9 +1580,16 @@ def fs_list(path: str = ""):
549
  target = get_safe_path(path)
550
  if not os.path.exists(target): return []
551
  items = []
552
- for f in os.listdir(target):
553
- fp = os.path.join(target, f)
554
- items.append({"name": f, "is_dir": os.path.isdir(fp), "size": os.path.getsize(fp) if not os.path.isdir(fp) else 0})
 
 
 
 
 
 
 
555
  return sorted(items, key=lambda x: (not x["is_dir"], x["name"].lower()))
556
 
557
  @app.get("/api/fs/read")
@@ -559,8 +1597,13 @@ def fs_read(path: str):
559
  target = get_safe_path(path)
560
  if not os.path.isfile(target): raise HTTPException(400, "Not a file")
561
  try:
562
- with open(target, 'r', encoding='utf-8') as f: return Response(content=f.read(), media_type="text/plain")
563
- except: raise HTTPException(400, "File is binary")
 
 
 
 
 
564
 
565
  @app.get("/api/fs/download")
566
  def fs_download(path: str):
@@ -570,20 +1613,55 @@ def fs_download(path: str):
570
 
571
  @app.post("/api/fs/write")
572
  def fs_write(path: str = Form(...), content: str = Form(...)):
573
- with open(get_safe_path(path), 'w', encoding='utf-8') as f: f.write(content)
574
- return {"status": "ok"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
575
 
576
  @app.post("/api/fs/upload")
577
  async def fs_upload(path: str = Form(""), file: UploadFile = File(...)):
578
- with open(os.path.join(get_safe_path(path), file.filename), "wb") as buffer: shutil.copyfileobj(file.file, buffer)
579
- return {"status": "ok"}
 
 
 
 
 
 
580
 
581
  @app.post("/api/fs/delete")
582
  def fs_delete(path: str = Form(...)):
583
  t = get_safe_path(path)
584
- if os.path.isdir(t): shutil.rmtree(t)
585
- else: os.remove(t)
586
- return {"status": "ok"}
 
 
 
587
 
588
  if __name__ == "__main__":
589
- uvicorn.run(app, host="0.0.0.0", port=7860, log_level="warning")
 
 
 
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
  """
 
1509
  dead.add(client)
1510
  connected_clients.difference_update(dead)
1511
 
1512
+ async def read_stream(stream):
1513
  while True:
1514
  try:
1515
  line = await stream.readline()
1516
  if not line: break
1517
+ await broadcast(line.decode('utf-8', errors='replace').rstrip('\r\n'))
1518
  except Exception:
1519
  break
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
  # -----------------
 
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
 
 
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
  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
 
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")