OrbitMC commited on
Commit
268a79d
·
verified ·
1 Parent(s): df819c1

Update panel.py

Browse files
Files changed (1) hide show
  1. panel.py +569 -966
panel.py CHANGED
@@ -3,7 +3,6 @@ import asyncio
3
  import collections
4
  import shutil
5
  import psutil
6
- import time
7
  from fastapi import FastAPI, WebSocket, Request, Response, Form, UploadFile, File, HTTPException
8
  from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
9
  from fastapi.middleware.cors import CORSMiddleware
@@ -13,932 +12,604 @@ app = FastAPI()
13
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
14
 
15
  mc_process = None
16
- output_history = collections.deque(maxlen=500)
17
  connected_clients = set()
18
  BASE_DIR = os.path.abspath("/app")
19
- CONTAINER_CPU_CORES = 2
20
- CONTAINER_RAM_MB = 16384
21
- CONTAINER_STORAGE_GB = 50
22
 
 
 
 
 
 
 
23
  HTML_CONTENT = """
24
  <!DOCTYPE html>
25
- <html lang="en">
26
  <head>
27
- <meta charset="UTF-8">
28
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scale=no">
29
- <title>MC Server Panel</title>
30
- <style>
31
- *,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
32
- :root{
33
- --bg-primary:#0a0a0f;--bg-secondary:#12121a;--bg-tertiary:#1a1a2e;--bg-card:#16162a;
34
- --bg-hover:#1e1e3a;--bg-active:#252545;--border-primary:#2a2a4a;--border-glow:#7c3aed40;
35
- --text-primary:#f0f0ff;--text-secondary:#a0a0c0;--text-muted:#606080;
36
- --accent:#7c3aed;--accent-hover:#9333ea;--accent-glow:#7c3aed30;
37
- --success:#10b981;--warning:#f59e0b;--danger:#ef4444;--info:#3b82f6;
38
- --radius:12px;--radius-sm:8px;--radius-lg:16px;
39
- --shadow:0 4px 24px rgba(0,0,0,0.4);--shadow-glow:0 0 30px var(--accent-glow);
40
- --transition:all 0.2s cubic-bezier(0.4,0,0.2,1);
41
- --font:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',sans-serif;
42
- --font-mono:'SF Mono','Cascadia Code','Fira Code',Consolas,monospace;
43
- }
44
- html{font-family:var(--font);background:var(--bg-primary);color:var(--text-primary);
45
- -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;overflow:hidden;height:100%}
46
- body{height:100%;overflow:hidden}
47
- ::-webkit-scrollbar{width:6px;height:6px}
48
- ::-webkit-scrollbar-track{background:transparent}
49
- ::-webkit-scrollbar-thumb{background:var(--border-primary);border-radius:3px}
50
- ::-webkit-scrollbar-thumb:hover{background:var(--accent)}
51
-
52
- .app{display:flex;flex-direction:column;height:100vh;overflow:hidden}
53
-
54
- /* Top Bar */
55
- .topbar{display:flex;align-items:center;justify-content:space-between;padding:0 16px;
56
- height:52px;min-height:52px;background:var(--bg-secondary);
57
- border-bottom:1px solid var(--border-primary);z-index:100;gap:12px;
58
- backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px)}
59
- .topbar-brand{display:flex;align-items:center;gap:10px;font-weight:700;font-size:15px;white-space:nowrap}
60
- .topbar-brand svg{color:var(--accent)}
61
- .topbar-status{display:flex;align-items:center;gap:6px;padding:4px 12px;
62
- border-radius:20px;font-size:11px;font-weight:600;letter-spacing:0.5px;text-transform:uppercase}
63
- .status-online{background:#10b98118;color:#10b981;border:1px solid #10b98130}
64
- .status-offline{background:#ef444418;color:#ef4444;border:1px solid #ef444430}
65
- .topbar-stats{display:flex;align-items:center;gap:4px}
66
- .stat-chip{display:flex;align-items:center;gap:6px;padding:5px 10px;
67
- background:var(--bg-tertiary);border:1px solid var(--border-primary);
68
- border-radius:var(--radius-sm);font-size:11px;font-weight:500;white-space:nowrap}
69
- .stat-chip .stat-val{font-weight:700;color:var(--text-primary);font-variant-numeric:tabular-nums}
70
- .stat-chip .stat-lbl{color:var(--text-muted);font-size:10px}
71
- .stat-bar-wrap{width:40px;height:4px;background:var(--bg-primary);border-radius:2px;overflow:hidden}
72
- .stat-bar-fill{height:100%;border-radius:2px;transition:width 0.6s ease}
73
- .hamburger{display:none;background:none;border:none;color:var(--text-primary);cursor:pointer;padding:4px}
74
-
75
- /* Navigation Tabs */
76
- .nav-tabs{display:flex;align-items:center;gap:2px;padding:0 16px;
77
- height:42px;min-height:42px;background:var(--bg-secondary);
78
- border-bottom:1px solid var(--border-primary);overflow-x:auto}
79
- .nav-tab{display:flex;align-items:center;gap:6px;padding:8px 14px;
80
- font-size:12px;font-weight:500;color:var(--text-secondary);
81
- border-radius:var(--radius-sm) var(--radius-sm) 0 0;cursor:pointer;
82
- transition:var(--transition);white-space:nowrap;border:none;background:none;
83
- border-bottom:2px solid transparent;position:relative}
84
- .nav-tab:hover{color:var(--text-primary);background:var(--bg-hover)}
85
- .nav-tab.active{color:var(--accent);background:var(--bg-tertiary);border-bottom-color:var(--accent)}
86
- .nav-tab svg{width:14px;height:14px;flex-shrink:0}
87
-
88
- /* Main Content */
89
- .main-content{flex:1;overflow:hidden;position:relative}
90
- .panel{display:none;height:100%;flex-direction:column;overflow:hidden}
91
- .panel.active{display:flex}
92
-
93
- /* Console */
94
- .console-wrap{flex:1;display:flex;flex-direction:column;overflow:hidden}
95
- .console-output{flex:1;overflow-y:auto;padding:12px 16px;font-family:var(--font-mono);
96
- font-size:12.5px;line-height:1.7;background:var(--bg-primary);scroll-behavior:smooth;
97
- -webkit-overflow-scrolling:touch}
98
- .console-line{padding:1px 0;word-break:break-all;animation:fadeIn 0.15s ease}
99
- .console-line:hover{background:var(--bg-hover);border-radius:2px}
100
- @keyframes fadeIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}
101
- .console-line .timestamp{color:var(--text-muted);margin-right:8px;font-size:11px;user-select:none}
102
- .log-info{color:#60a5fa}.log-warn{color:#fbbf24}.log-error{color:#f87171}
103
- .log-server{color:#a78bfa}.log-chat{color:#34d399}.log-default{color:var(--text-secondary)}
104
- .console-input-wrap{display:flex;gap:8px;padding:10px 16px;background:var(--bg-secondary);
105
- border-top:1px solid var(--border-primary);align-items:center}
106
- .console-prefix{color:var(--accent);font-family:var(--font-mono);font-size:13px;
107
- font-weight:700;user-select:none;flex-shrink:0}
108
- .console-input{flex:1;background:var(--bg-tertiary);border:1px solid var(--border-primary);
109
- color:var(--text-primary);font-family:var(--font-mono);font-size:12.5px;padding:8px 12px;
110
- border-radius:var(--radius-sm);outline:none;transition:var(--transition)}
111
- .console-input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-glow)}
112
- .console-input::placeholder{color:var(--text-muted)}
113
- .btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:7px 14px;
114
- font-size:12px;font-weight:600;border:1px solid var(--border-primary);border-radius:var(--radius-sm);
115
- cursor:pointer;transition:var(--transition);background:var(--bg-tertiary);color:var(--text-primary);
116
- white-space:nowrap;font-family:var(--font)}
117
- .btn:hover{background:var(--bg-hover);border-color:var(--accent)}
118
- .btn:active{transform:scale(0.97)}
119
- .btn-accent{background:var(--accent);border-color:var(--accent);color:#fff}
120
- .btn-accent:hover{background:var(--accent-hover);border-color:var(--accent-hover)}
121
- .btn-danger{background:var(--danger);border-color:var(--danger);color:#fff}
122
- .btn-danger:hover{background:#dc2626;border-color:#dc2626}
123
- .btn-sm{padding:5px 10px;font-size:11px}
124
- .btn-icon{padding:6px;width:32px;height:32px}
125
- .btn svg{width:14px;height:14px;flex-shrink:0}
126
- .console-toolbar{display:flex;align-items:center;gap:6px;padding:6px 16px;
127
- background:var(--bg-secondary);border-bottom:1px solid var(--border-primary);
128
- overflow-x:auto;flex-wrap:nowrap}
129
- .console-toolbar .btn{flex-shrink:0}
130
- .quick-cmds{display:flex;gap:4px;flex-wrap:nowrap;overflow-x:auto;flex:1}
131
- .quick-cmds .btn{font-size:10px;padding:4px 8px;background:var(--bg-card);color:var(--text-secondary)}
132
- .quick-cmds .btn:hover{color:var(--text-primary);background:var(--accent);border-color:var(--accent)}
133
-
134
- /* File Manager */
135
- .fm-container{display:flex;flex-direction:column;height:100%;overflow:hidden}
136
- .fm-toolbar{display:flex;align-items:center;gap:8px;padding:10px 16px;
137
- background:var(--bg-secondary);border-bottom:1px solid var(--border-primary);flex-wrap:wrap}
138
- .fm-path{display:flex;align-items:center;gap:2px;flex:1;min-width:200px;overflow-x:auto;flex-wrap:nowrap}
139
- .fm-crumb{padding:4px 8px;font-size:11px;color:var(--text-secondary);cursor:pointer;
140
- border-radius:var(--radius-sm);transition:var(--transition);white-space:nowrap;
141
- background:none;border:none;font-family:var(--font)}
142
- .fm-crumb:hover{background:var(--bg-hover);color:var(--text-primary)}
143
- .fm-crumb.active{color:var(--accent);font-weight:600}
144
- .fm-crumb-sep{color:var(--text-muted);font-size:10px;user-select:none}
145
- .fm-body{flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch}
146
- .fm-list{width:100%}
147
- .fm-item{display:flex;align-items:center;gap:10px;padding:9px 16px;cursor:pointer;
148
- transition:var(--transition);border-bottom:1px solid var(--border-primary)}
149
- .fm-item:hover{background:var(--bg-hover)}
150
- .fm-item.selected{background:var(--accent-glow);border-left:3px solid var(--accent)}
151
- .fm-icon{width:18px;height:18px;flex-shrink:0;display:flex;align-items:center;justify-content:center}
152
- .fm-icon.folder{color:#fbbf24}.fm-icon.file{color:var(--text-muted)}
153
- .fm-icon.jar{color:#ef4444}.fm-icon.yml{color:#10b981}.fm-icon.json{color:#f59e0b}
154
- .fm-icon.properties{color:#3b82f6}.fm-icon.log{color:#8b5cf6}.fm-icon.img{color:#ec4899}
155
- .fm-name{flex:1;font-size:13px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
156
- .fm-size{font-size:11px;color:var(--text-muted);font-variant-numeric:tabular-nums;white-space:nowrap}
157
- .fm-actions{display:flex;gap:2px;opacity:0;transition:var(--transition)}
158
- .fm-item:hover .fm-actions{opacity:1}
159
- .fm-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;
160
- padding:60px 20px;color:var(--text-muted);gap:12px}
161
- .fm-empty svg{width:48px;height:48px;opacity:0.3}
162
-
163
- /* Editor */
164
- .editor-container{display:flex;flex-direction:column;height:100%;overflow:hidden}
165
- .editor-header{display:flex;align-items:center;gap:10px;padding:10px 16px;
166
- background:var(--bg-secondary);border-bottom:1px solid var(--border-primary)}
167
- .editor-filename{font-size:13px;font-weight:600;color:var(--accent);flex:1;
168
- overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
169
- .editor-textarea{flex:1;width:100%;resize:none;background:var(--bg-primary);
170
- color:var(--text-primary);font-family:var(--font-mono);font-size:12.5px;
171
- line-height:1.6;padding:16px;border:none;outline:none;tab-size:4;
172
- -webkit-overflow-scrolling:touch}
173
- .editor-textarea::placeholder{color:var(--text-muted)}
174
-
175
- /* Dashboard Cards */
176
- .dashboard{padding:16px;overflow-y:auto;-webkit-overflow-scrolling:touch;height:100%}
177
- .dash-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:12px}
178
- .dash-card{background:var(--bg-card);border:1px solid var(--border-primary);
179
- border-radius:var(--radius);padding:20px;transition:var(--transition)}
180
- .dash-card:hover{border-color:var(--accent);box-shadow:var(--shadow-glow)}
181
- .dash-card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}
182
- .dash-card-title{font-size:13px;font-weight:600;color:var(--text-secondary);text-transform:uppercase;letter-spacing:0.5px}
183
- .dash-card-icon{width:36px;height:36px;border-radius:var(--radius-sm);display:flex;
184
- align-items:center;justify-content:center}
185
- .dash-card-icon.cpu{background:#7c3aed20;color:#7c3aed}
186
- .dash-card-icon.ram{background:#3b82f620;color:#3b82f6}
187
- .dash-card-icon.disk{background:#10b98120;color:#10b981}
188
- .dash-card-icon.status{background:#f59e0b20;color:#f59e0b}
189
- .dash-metric{font-size:32px;font-weight:800;line-height:1;margin-bottom:4px;
190
- font-variant-numeric:tabular-nums;letter-spacing:-1px}
191
- .dash-metric-sub{font-size:12px;color:var(--text-muted)}
192
- .dash-progress{width:100%;height:6px;background:var(--bg-primary);border-radius:3px;
193
- margin-top:12px;overflow:hidden}
194
- .dash-progress-fill{height:100%;border-radius:3px;transition:width 0.8s cubic-bezier(0.4,0,0.2,1)}
195
- .color-green{color:var(--success)}.color-yellow{color:var(--warning)}
196
- .color-red{color:var(--danger)}.color-blue{color:var(--info)}.color-purple{color:var(--accent)}
197
- .fill-green{background:var(--success)}.fill-yellow{background:var(--warning)}
198
- .fill-red{background:var(--danger)}.fill-blue{background:var(--info)}.fill-purple{background:var(--accent)}
199
-
200
- /* Upload Overlay */
201
- .upload-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);
202
- backdrop-filter:blur(8px);z-index:200;align-items:center;justify-content:center}
203
- .upload-overlay.active{display:flex}
204
- .upload-modal{background:var(--bg-card);border:1px solid var(--border-primary);
205
- border-radius:var(--radius-lg);padding:32px;width:min(440px,90vw);text-align:center}
206
- .upload-zone{border:2px dashed var(--border-primary);border-radius:var(--radius);
207
- padding:40px 20px;margin:20px 0;transition:var(--transition);cursor:pointer}
208
- .upload-zone:hover,.upload-zone.dragover{border-color:var(--accent);background:var(--accent-glow)}
209
- .upload-zone svg{width:40px;height:40px;color:var(--text-muted);margin-bottom:12px}
210
- .upload-zone p{font-size:13px;color:var(--text-secondary)}
211
-
212
- /* Toast Notifications */
213
- .toast-container{position:fixed;bottom:20px;right:20px;z-index:300;display:flex;flex-direction:column;gap:8px}
214
- .toast{padding:12px 16px;border-radius:var(--radius-sm);font-size:12px;font-weight:500;
215
- animation:toastIn 0.3s ease,toastOut 0.3s ease 2.7s forwards;display:flex;align-items:center;gap:8px;
216
- box-shadow:var(--shadow);max-width:min(360px,calc(100vw - 40px))}
217
- .toast-success{background:#065f46;border:1px solid #10b98150;color:#d1fae5}
218
- .toast-error{background:#7f1d1d;border:1px solid #ef444450;color:#fee2e2}
219
- .toast-info{background:#1e3a5f;border:1px solid #3b82f650;color:#dbeafe}
220
- @keyframes toastIn{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:translateX(0)}}
221
- @keyframes toastOut{from{opacity:1}to{opacity:0;transform:translateY(10px)}}
222
-
223
- /* Context Menu */
224
- .ctx-menu{position:fixed;background:var(--bg-card);border:1px solid var(--border-primary);
225
- border-radius:var(--radius-sm);padding:4px;z-index:250;min-width:160px;
226
- box-shadow:var(--shadow);display:none}
227
- .ctx-menu.active{display:block}
228
- .ctx-item{display:flex;align-items:center;gap:8px;padding:7px 12px;font-size:12px;
229
- color:var(--text-secondary);cursor:pointer;border-radius:4px;transition:var(--transition);
230
- border:none;background:none;width:100%;text-align:left;font-family:var(--font)}
231
- .ctx-item:hover{background:var(--bg-hover);color:var(--text-primary)}
232
- .ctx-item.danger{color:var(--danger)}
233
- .ctx-item.danger:hover{background:#ef444420}
234
- .ctx-item svg{width:14px;height:14px;flex-shrink:0}
235
- .ctx-sep{height:1px;background:var(--border-primary);margin:4px 0}
236
-
237
- /* Connection indicator */
238
- .conn-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
239
- .conn-dot.connected{background:var(--success);box-shadow:0 0 6px var(--success)}
240
- .conn-dot.disconnected{background:var(--danger);box-shadow:0 0 6px var(--danger)}
241
-
242
- /* Responsive */
243
- @media(max-width:768px){
244
- .topbar{padding:0 10px;height:48px;min-height:48px}
245
- .topbar-stats{display:none}
246
- .topbar-brand span{display:none}
247
- .hamburger{display:flex}
248
- .nav-tabs{height:38px;min-height:38px;padding:0 8px;gap:0}
249
- .nav-tab{padding:6px 10px;font-size:11px}
250
- .nav-tab span{display:none}
251
- .console-output{padding:8px;font-size:11px;line-height:1.5}
252
- .console-input-wrap{padding:8px 10px}
253
- .console-toolbar{padding:4px 8px}
254
- .fm-toolbar{padding:8px 10px}
255
- .fm-item{padding:8px 10px}
256
- .fm-actions{opacity:1}
257
- .dashboard{padding:10px}
258
- .dash-grid{grid-template-columns:1fr}
259
- .dash-metric{font-size:26px}
260
- .stat-chip{padding:3px 6px;font-size:10px}
261
- .mobile-stats{display:flex !important}
262
- }
263
- @media(max-width:480px){
264
- .nav-tabs{overflow-x:auto}
265
- .console-toolbar{flex-wrap:nowrap;overflow-x:auto}
266
- .quick-cmds .btn{padding:3px 6px;font-size:9px}
267
- }
268
- .mobile-stats{display:none;gap:4px;padding:6px 10px;background:var(--bg-secondary);
269
- border-bottom:1px solid var(--border-primary);overflow-x:auto}
270
-
271
- /* Loading skeleton */
272
- .skeleton{background:linear-gradient(90deg,var(--bg-tertiary) 25%,var(--bg-hover) 50%,var(--bg-tertiary) 75%);
273
- background-size:200% 100%;animation:shimmer 1.5s infinite;border-radius:4px}
274
- @keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
275
 
276
- /* Smooth transitions for panels */
277
- .panel{animation:panelIn 0.2s ease}
278
- @keyframes panelIn{from{opacity:0}to{opacity:1}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
 
280
- .search-input{background:var(--bg-tertiary);border:1px solid var(--border-primary);
281
- color:var(--text-primary);font-size:12px;padding:5px 10px;border-radius:var(--radius-sm);
282
- outline:none;transition:var(--transition);width:140px;font-family:var(--font)}
283
- .search-input:focus{border-color:var(--accent);width:200px}
284
- @media(max-width:768px){.search-input{width:100px}.search-input:focus{width:140px}}
285
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
286
  </head>
287
- <body>
288
- <div class="app">
289
- <!-- Top Bar -->
290
- <div class="topbar">
291
- <div style="display:flex;align-items:center;gap:10px">
292
- <button class="hamburger" onclick="toggleMobileStats()">
293
- <svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h14M3 10h14M3 14h14"/></svg>
294
- </button>
295
- <div class="topbar-brand">
296
- <svg width="22" height="22" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="18" rx="3"/><path d="M8 12h8M12 8v8"/></svg>
297
- <span>MC Panel</span>
298
- </div>
299
- <div class="topbar-status" id="serverStatus">
300
- <div class="conn-dot disconnected" id="connDot"></div>
301
- <span id="statusText">CONNECTING</span>
302
- </div>
303
- </div>
304
- <div class="topbar-stats" id="topbarStats">
305
- <div class="stat-chip" title="CPU Usage (2 vCPU)">
306
- <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="1" width="10" height="10" rx="2"/><path d="M4 4h4M4 6h4M4 8h2"/></svg>
307
- <div><div class="stat-val" id="cpuTopVal">0%</div><div class="stat-lbl">CPU</div></div>
308
- <div class="stat-bar-wrap"><div class="stat-bar-fill fill-purple" id="cpuTopBar" style="width:0%"></div></div>
309
- </div>
310
- <div class="stat-chip" title="RAM Usage (16 GB)">
311
- <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="1" width="8" height="10" rx="1"/><path d="M4 11v1M8 11v1M1 4h10"/></svg>
312
- <div><div class="stat-val" id="ramTopVal">0 MB</div><div class="stat-lbl">RAM</div></div>
313
- <div class="stat-bar-wrap"><div class="stat-bar-fill fill-blue" id="ramTopBar" style="width:0%"></div></div>
314
- </div>
315
- <div class="stat-chip" title="Storage">
316
- <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="6" cy="4" rx="5" ry="2"/><path d="M1 4v4c0 1.1 2.2 2 5 2s5-.9 5-2V4"/></svg>
317
- <div><div class="stat-val" id="diskTopVal">-- GB</div><div class="stat-lbl">DISK</div></div>
318
- <div class="stat-bar-wrap"><div class="stat-bar-fill fill-green" id="diskTopBar" style="width:0%"></div></div>
319
- </div>
320
- </div>
321
- </div>
322
-
323
- <!-- Mobile Stats (hidden by default) -->
324
- <div class="mobile-stats" id="mobileStats">
325
- <div class="stat-chip"><span class="stat-val" id="cpuMobVal">0%</span><span class="stat-lbl">CPU</span></div>
326
- <div class="stat-chip"><span class="stat-val" id="ramMobVal">0 MB</span><span class="stat-lbl">RAM</span></div>
327
- <div class="stat-chip"><span class="stat-val" id="diskMobVal">--</span><span class="stat-lbl">DISK</span></div>
328
- </div>
329
-
330
- <!-- Navigation Tabs -->
331
- <div class="nav-tabs">
332
- <button class="nav-tab active" data-tab="dashboard" onclick="switchTab('dashboard')">
333
- <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
334
- <span>Dashboard</span>
335
- </button>
336
- <button class="nav-tab" data-tab="console" onclick="switchTab('console')">
337
- <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
338
- <span>Console</span>
339
- </button>
340
- <button class="nav-tab" data-tab="files" onclick="switchTab('files')">
341
- <svg fill="none" viewBox="0 0 24 24" 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>
342
- <span>Files</span>
343
- </button>
344
- <button class="nav-tab" data-tab="editor" onclick="switchTab('editor')" id="editorTab" style="display:none">
345
- <svg fill="none" viewBox="0 0 24 24" 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.12 2.12 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
346
- <span>Editor</span>
347
- </button>
348
- </div>
349
-
350
- <!-- Main Content Area -->
351
- <div class="main-content">
352
-
353
- <!-- Dashboard Panel -->
354
- <div class="panel active" id="panel-dashboard">
355
- <div class="dashboard">
356
- <div class="dash-grid">
357
- <div class="dash-card">
358
- <div class="dash-card-header">
359
- <div class="dash-card-title">CPU Usage</div>
360
- <div class="dash-card-icon cpu">
361
- <svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="16" height="16" rx="3"/><path d="M6 6h8M6 10h8M6 14h4"/></svg>
362
- </div>
363
  </div>
364
- <div class="dash-metric color-purple" id="cpuDashVal">0%</div>
365
- <div class="dash-metric-sub">of 2 vCPU Cores</div>
366
- <div class="dash-progress"><div class="dash-progress-fill fill-purple" id="cpuDashBar" style="width:0%"></div></div>
367
- </div>
368
- <div class="dash-card">
369
- <div class="dash-card-header">
370
- <div class="dash-card-title">Memory</div>
371
- <div class="dash-card-icon ram">
372
- <svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="1" width="14" height="16" rx="2"/><path d="M6 17v2M14 17v2M10 17v2M1 6h18"/></svg>
373
- </div>
374
- </div>
375
- <div class="dash-metric color-blue" id="ramDashVal">0 MB</div>
376
- <div class="dash-metric-sub" id="ramDashSub">of 16,384 MB</div>
377
- <div class="dash-progress"><div class="dash-progress-fill fill-blue" id="ramDashBar" style="width:0%"></div></div>
378
- </div>
379
- <div class="dash-card">
380
- <div class="dash-card-header">
381
- <div class="dash-card-title">Storage</div>
382
- <div class="dash-card-icon disk">
383
- <svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="10" cy="6" rx="8" ry="3"/><path d="M2 6v5c0 1.7 3.6 3 8 3s8-1.3 8-3V6"/><path d="M2 11v5c0 1.7 3.6 3 8 3s8-1.3 8-3v-5"/></svg>
384
- </div>
385
- </div>
386
- <div class="dash-metric color-green" id="diskDashVal">-- GB</div>
387
- <div class="dash-metric-sub" id="diskDashSub">loading...</div>
388
- <div class="dash-progress"><div class="dash-progress-fill fill-green" id="diskDashBar" style="width:0%"></div></div>
389
- </div>
390
- <div class="dash-card">
391
- <div class="dash-card-header">
392
- <div class="dash-card-title">Server Status</div>
393
- <div class="dash-card-icon status">
394
- <svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><circle cx="10" cy="10" r="8"/><polyline points="10 5 10 10 13 12"/></svg>
395
- </div>
396
- </div>
397
- <div class="dash-metric" id="uptimeVal" style="font-size:24px;color:var(--text-primary)">--</div>
398
- <div class="dash-metric-sub" id="uptimeSub">Container uptime</div>
399
- </div>
400
  </div>
401
- </div>
402
- </div>
403
 
404
- <!-- Console Panel -->
405
- <div class="panel" id="panel-console">
406
- <div class="console-wrap">
407
- <div class="console-toolbar">
408
- <button class="btn btn-sm" onclick="sendCmd('list')" title="List Players">
409
- <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 10a3 3 0 100-6 3 3 0 000 6zM1 12a6 6 0 0112 0"/></svg>
410
- Players
411
- </button>
412
- <div class="quick-cmds">
413
- <button class="btn" onclick="sendCmd('tps')">TPS</button>
414
- <button class="btn" onclick="sendCmd('gc')">GC</button>
415
- <button class="btn" onclick="sendCmd('mem')">Mem</button>
416
- <button class="btn" onclick="sendCmd('version')">Ver</button>
417
- <button class="btn" onclick="sendCmd('plugins')">Plugins</button>
418
- <button class="btn" onclick="sendCmd('whitelist list')">WL</button>
419
- <button class="btn" onclick="sendCmd('save-all')">Save</button>
420
- </div>
421
- <button class="btn btn-sm btn-danger" onclick="if(confirm('Stop the server?'))sendCmd('stop')" title="Stop Server">
422
- <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="1" width="10" height="10" rx="1"/></svg>
423
- Stop
424
- </button>
425
- <button class="btn btn-sm btn-icon" onclick="clearConsole()" title="Clear Console">
426
- <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 1l10 10M11 1L1 11"/></svg>
427
- </button>
428
- <button class="btn btn-sm btn-icon" onclick="scrollToBottom()" title="Scroll to Bottom">
429
- <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 1v10M2 7l4 4 4-4"/></svg>
430
- </button>
431
  </div>
432
- <div class="console-output" id="consoleOutput"></div>
433
- <div class="console-input-wrap">
434
- <span class="console-prefix">$</span>
435
- <input class="console-input" id="consoleInput" type="text" placeholder="Type a command..." autocomplete="off" spellcheck="false">
436
- <button class="btn btn-accent" onclick="sendCurrentCmd()">
437
- <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 1l11 6-11 6V1z"/></svg>
438
- </button>
 
 
 
 
 
439
  </div>
440
- </div>
441
- </div>
442
 
443
- <!-- Files Panel -->
444
- <div class="panel" id="panel-files">
445
- <div class="fm-container">
446
- <div class="fm-toolbar">
447
- <div class="fm-path" id="fmBreadcrumb"></div>
448
- <input class="search-input" type="text" placeholder="Filter..." id="fmSearch" oninput="filterFiles()">
449
- <button class="btn btn-sm" onclick="refreshFiles()" title="Refresh">
450
- <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 1v4h4M11 11v-4h-4"/><path d="M10.4 5A5 5 0 003 3.5L1 5M1.6 7a5 5 0 007.4 1.5L11 7"/></svg>
451
- </button>
452
- <button class="btn btn-sm" onclick="showUploadModal()" title="Upload File">
453
- <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 10V2M2 6l4-4 4 4"/><path d="M1 10v1a1 1 0 001 1h8a1 1 0 001-1v-1"/></svg>
454
- Upload
455
- </button>
456
- <button class="btn btn-sm" onclick="createNewFile()" title="New File">
457
- <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 1v10M1 6h10"/></svg>
458
- New
459
- </button>
460
  </div>
461
- <div class="fm-body" id="fmBody">
462
- <div class="fm-empty"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg><span>Loading files...</span></div>
 
463
  </div>
464
- </div>
465
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
 
467
- <!-- Editor Panel -->
468
- <div class="panel" id="panel-editor">
469
- <div class="editor-container">
470
- <div class="editor-header">
471
- <button class="btn btn-sm" onclick="closeEditor()">
472
- <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 1L1 6l9 5"/></svg>
473
- Back
474
- </button>
475
- <div class="editor-filename" id="editorFilename">No file open</div>
476
- <button class="btn btn-sm btn-accent" onclick="saveFile()" id="editorSaveBtn">
477
- <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 1h8l2 2v8a1 1 0 01-1 1H2a1 1 0 01-1-1V2a1 1 0 011-1z"/><path d="M4 1v3h4V1M3 7h6v4H3z"/></svg>
478
- Save
479
- </button>
480
- <button class="btn btn-sm" onclick="downloadCurrentFile()">
481
- <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 1v8M2 6l4 4 4-4"/><path d="M1 10v1a1 1 0 001 1h8a1 1 0 001-1v-1"/></svg>
482
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
483
  </div>
484
- <textarea class="editor-textarea" id="editorContent" placeholder="File contents will appear here..." spellcheck="false"></textarea>
485
- </div>
486
- </div>
487
- </div>
488
- </div>
489
-
490
- <!-- Upload Modal -->
491
- <div class="upload-overlay" id="uploadOverlay" onclick="if(event.target===this)hideUploadModal()">
492
- <div class="upload-modal">
493
- <h3 style="font-size:16px;font-weight:700;margin-bottom:4px">Upload File</h3>
494
- <p style="font-size:12px;color:var(--text-muted)" id="uploadPathDisplay">to /</p>
495
- <div class="upload-zone" id="uploadZone" onclick="document.getElementById('uploadFileInput').click()">
496
- <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path d="M12 16V4M8 8l4-4 4 4"/><path d="M20 16v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2"/></svg>
497
- <p>Click or drag files here</p>
498
- <p style="font-size:11px;color:var(--text-muted);margin-top:4px" id="uploadFileName">No file selected</p>
499
- </div>
500
- <input type="file" id="uploadFileInput" style="display:none" onchange="handleFileSelect(this)">
501
- <div style="display:flex;gap:8px;justify-content:flex-end">
502
- <button class="btn" onclick="hideUploadModal()">Cancel</button>
503
- <button class="btn btn-accent" onclick="doUpload()" id="uploadBtn">Upload</button>
504
- </div>
505
- </div>
506
- </div>
507
-
508
- <!-- Context Menu -->
509
- <div class="ctx-menu" id="ctxMenu">
510
- <button class="ctx-item" onclick="ctxAction('open')"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>Open</button>
511
- <button class="ctx-item" onclick="ctxAction('edit')"><svg fill="none" viewBox="0 0 24 24" 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.12 2.12 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>Edit</button>
512
- <button class="ctx-item" onclick="ctxAction('download')"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M12 16V4M8 12l4 4 4-4"/><path d="M20 16v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2"/></svg>Download</button>
513
- <button class="ctx-item" onclick="ctxAction('rename')"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M17 3a2.83 2.83 0 114 4L7.5 20.5 2 22l1.5-5.5L17 3z"/></svg>Rename</button>
514
- <div class="ctx-sep"></div>
515
- <button class="ctx-item danger" onclick="ctxAction('delete')"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/></svg>Delete</button>
516
- </div>
517
-
518
- <!-- Toast Container -->
519
- <div class="toast-container" id="toastContainer"></div>
520
-
521
- <script>
522
- // ========== STATE ==========
523
- let ws=null, wsConnected=false, currentPath='', editingFile='', cmdHistory=[], cmdHistoryIdx=-1;
524
- let ctxTarget=null, allFileItems=[], autoScroll=true, startTime=Date.now();
525
- const consoleEl=document.getElementById('consoleOutput');
526
- const consoleInput=document.getElementById('consoleInput');
527
-
528
- // ========== TABS ==========
529
- function switchTab(tab){
530
- document.querySelectorAll('.nav-tab').forEach(t=>t.classList.remove('active'));
531
- document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));
532
- document.querySelector(`[data-tab="${tab}"]`).classList.add('active');
533
- document.getElementById('panel-'+tab).classList.add('active');
534
- if(tab==='files')loadFiles(currentPath);
535
- if(tab==='console')requestAnimationFrame(()=>scrollToBottom());
536
- }
537
-
538
- // ========== WEBSOCKET ==========
539
- function connectWS(){
540
- const proto=location.protocol==='https:'?'wss:':'ws:';
541
- ws=new WebSocket(proto+'//'+location.host+'/ws');
542
- ws.onopen=()=>{wsConnected=true;updateStatus(true);toast('Connected to server','success')};
543
- ws.onclose=()=>{wsConnected=false;updateStatus(false);setTimeout(connectWS,2000)};
544
- ws.onerror=()=>{wsConnected=false;updateStatus(false)};
545
- ws.onmessage=e=>{appendConsole(e.data)};
546
- }
547
- function updateStatus(online){
548
- const dot=document.getElementById('connDot');
549
- const txt=document.getElementById('statusText');
550
- const wrap=document.getElementById('serverStatus');
551
- dot.className='conn-dot '+(online?'connected':'disconnected');
552
- txt.textContent=online?'ONLINE':'OFFLINE';
553
- wrap.className='topbar-status '+(online?'status-online':'status-offline');
554
- }
555
-
556
- // ========== CONSOLE ==========
557
- function classifyLine(text){
558
- const t=text.toLowerCase();
559
- if(t.includes('error')||t.includes('exception')||t.includes('severe')||t.includes('fatal'))return 'log-error';
560
- if(t.includes('warn'))return 'log-warn';
561
- if(t.includes('info'))return 'log-info';
562
- if(t.includes('server')||t.includes('starting')||t.includes('done'))return 'log-server';
563
- if(t.includes('<')||t.includes('joined')||t.includes('left'))return 'log-chat';
564
- return 'log-default';
565
- }
566
- function appendConsole(text){
567
- const div=document.createElement('div');
568
- div.className='console-line '+classifyLine(text);
569
- const now=new Date();
570
- const ts=String(now.getHours()).padStart(2,'0')+':'+String(now.getMinutes()).padStart(2,'0')+':'+String(now.getSeconds()).padStart(2,'0');
571
- div.innerHTML='<span class="timestamp">'+ts+'</span>'+escapeHtml(text);
572
- consoleEl.appendChild(div);
573
- // Keep max 500 lines in DOM
574
- while(consoleEl.children.length>500)consoleEl.removeChild(consoleEl.firstChild);
575
- if(autoScroll)scrollToBottom();
576
- }
577
- function scrollToBottom(){consoleEl.scrollTop=consoleEl.scrollHeight}
578
- function clearConsole(){consoleEl.innerHTML='';toast('Console cleared','info')}
579
- function escapeHtml(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML}
580
-
581
- consoleEl.addEventListener('scroll',()=>{
582
- const atBottom=consoleEl.scrollHeight-consoleEl.scrollTop-consoleEl.clientHeight<50;
583
- autoScroll=atBottom;
584
- });
585
-
586
- function sendCmd(cmd){
587
- if(ws&&ws.readyState===1){ws.send(cmd);appendConsole('> '+cmd)}
588
- else toast('Not connected','error');
589
- }
590
- function sendCurrentCmd(){
591
- const v=consoleInput.value.trim();
592
- if(!v)return;
593
- cmdHistory.unshift(v);if(cmdHistory.length>50)cmdHistory.pop();cmdHistoryIdx=-1;
594
- sendCmd(v);consoleInput.value='';
595
- }
596
- consoleInput.addEventListener('keydown',e=>{
597
- if(e.key==='Enter'){e.preventDefault();sendCurrentCmd()}
598
- if(e.key==='ArrowUp'){e.preventDefault();if(cmdHistoryIdx<cmdHistory.length-1){cmdHistoryIdx++;consoleInput.value=cmdHistory[cmdHistoryIdx]}}
599
- if(e.key==='ArrowDown'){e.preventDefault();if(cmdHistoryIdx>0){cmdHistoryIdx--;consoleInput.value=cmdHistory[cmdHistoryIdx]}else{cmdHistoryIdx=-1;consoleInput.value=''}}
600
- if(e.key==='Tab'){e.preventDefault();/* Could add tab completion */}
601
- });
602
-
603
- // ========== STATS ==========
604
- function getColorForPercent(p){return p>=90?'red':p>=70?'yellow':'green'}
605
- async function fetchStats(){
606
- try{
607
- const r=await fetch('/api/stats');const d=await r.json();
608
- const cpuP=Math.min(100,d.cpu_percent||0).toFixed(1);
609
- const ramMB=(d.ram_used_mb||0).toFixed(0);
610
- const ramP=((d.ram_used_mb||0)/16384*100).toFixed(1);
611
- const diskUsedGB=(d.disk_used_gb||0).toFixed(1);
612
- const diskTotalGB=(d.disk_total_gb||0).toFixed(1);
613
- const diskP=diskTotalGB>0?((d.disk_used_gb/d.disk_total_gb)*100).toFixed(1):'0';
614
- const cpuCol=getColorForPercent(cpuP);
615
- const ramCol=getColorForPercent(ramP);
616
- const diskCol=getColorForPercent(diskP);
617
-
618
- // Topbar
619
- document.getElementById('cpuTopVal').textContent=cpuP+'%';
620
- document.getElementById('cpuTopBar').style.width=cpuP+'%';
621
- document.getElementById('cpuTopBar').className='stat-bar-fill fill-'+cpuCol;
622
- document.getElementById('ramTopVal').textContent=ramMB+' MB';
623
- document.getElementById('ramTopBar').style.width=ramP+'%';
624
- document.getElementById('ramTopBar').className='stat-bar-fill fill-'+ramCol;
625
- document.getElementById('diskTopVal').textContent=diskUsedGB+' GB';
626
- document.getElementById('diskTopBar').style.width=diskP+'%';
627
- document.getElementById('diskTopBar').className='stat-bar-fill fill-'+diskCol;
628
-
629
- // Mobile
630
- document.getElementById('cpuMobVal').textContent=cpuP+'%';
631
- document.getElementById('ramMobVal').textContent=ramMB+' MB';
632
- document.getElementById('diskMobVal').textContent=diskUsedGB+'G';
633
 
634
- // Dashboard
635
- document.getElementById('cpuDashVal').textContent=cpuP+'%';
636
- document.getElementById('cpuDashBar').style.width=cpuP+'%';
637
- document.getElementById('cpuDashBar').className='dash-progress-fill fill-'+cpuCol;
638
- document.getElementById('cpuDashVal').className='dash-metric color-'+cpuCol;
639
-
640
- document.getElementById('ramDashVal').textContent=ramMB+' MB';
641
- document.getElementById('ramDashSub').textContent=ramP+'% of 16,384 MB';
642
- document.getElementById('ramDashBar').style.width=ramP+'%';
643
- document.getElementById('ramDashBar').className='dash-progress-fill fill-'+ramCol;
644
- document.getElementById('ramDashVal').className='dash-metric color-'+ramCol;
645
-
646
- document.getElementById('diskDashVal').textContent=diskUsedGB+' GB';
647
- document.getElementById('diskDashSub').textContent=diskP+'% of '+diskTotalGB+' GB';
648
- document.getElementById('diskDashBar').style.width=diskP+'%';
649
- document.getElementById('diskDashBar').className='dash-progress-fill fill-'+diskCol;
650
- document.getElementById('diskDashVal').className='dash-metric color-'+diskCol;
651
-
652
- // Uptime
653
- const upSec=Math.floor((Date.now()-startTime)/1000);
654
- const h=Math.floor(upSec/3600),m=Math.floor((upSec%3600)/60),s=upSec%60;
655
- document.getElementById('uptimeVal').textContent=
656
- (h>0?h+'h ':'')+(m>0?m+'m ':'')+s+'s';
657
- document.getElementById('uptimeSub').textContent='Session uptime';
658
- }catch(e){}
659
- }
660
- setInterval(fetchStats,2000);
661
- fetchStats();
662
-
663
- // ========== FILE MANAGER ==========
664
- function formatSize(b){
665
- if(b===0)return'--';
666
- if(b<1024)return b+' B';
667
- if(b<1048576)return(b/1024).toFixed(1)+' KB';
668
- if(b<1073741824)return(b/1048576).toFixed(1)+' MB';
669
- return(b/1073741824).toFixed(2)+' GB';
670
- }
671
- function getFileIcon(name,isDir){
672
- if(isDir)return{cls:'folder',svg:'<path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/>'};
673
- const ext=name.split('.').pop().toLowerCase();
674
- const map={jar:{cls:'jar',i:'☕'},yml:{cls:'yml',i:'⚙'},yaml:{cls:'yml',i:'⚙'},
675
- json:{cls:'json',i:'{}'},properties:{cls:'properties',i:'🔧'},log:{cls:'log',i:'📜'},
676
- txt:{cls:'file',i:'📄'},png:{cls:'img',i:'🖼'},jpg:{cls:'img',i:'🖼'},gif:{cls:'img',i:'🖼'},
677
- toml:{cls:'yml',i:'⚙'},conf:{cls:'properties',i:'🔧'},cfg:{cls:'properties',i:'🔧'},
678
- sk:{cls:'yml',i:'📜'},sh:{cls:'log',i:'⚡'},bat:{cls:'log',i:'⚡'}};
679
- const m=map[ext]||{cls:'file',i:'📄'};
680
- return{cls:m.cls,text:m.i};
681
- }
682
 
683
- async function loadFiles(path){
684
- currentPath=path||'';
685
- buildBreadcrumb();
686
- const body=document.getElementById('fmBody');
687
- body.innerHTML='<div style="padding:20px;text-align:center;color:var(--text-muted)"><div class="skeleton" style="height:20px;width:60%;margin:8px auto"></div><div class="skeleton" style="height:20px;width:80%;margin:8px auto"></div><div class="skeleton" style="height:20px;width:50%;margin:8px auto"></div></div>';
688
- try{
689
- const r=await fetch('/api/fs/list?path='+encodeURIComponent(currentPath));
690
- allFileItems=await r.json();
691
- renderFiles(allFileItems);
692
- }catch(e){body.innerHTML='<div class="fm-empty"><span>Failed to load</span></div>'}
693
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
694
 
695
- function renderFiles(items){
696
- const body=document.getElementById('fmBody');
697
- const search=document.getElementById('fmSearch').value.toLowerCase();
698
- let filtered=items;
699
- if(search)filtered=items.filter(i=>i.name.toLowerCase().includes(search));
700
-
701
- if(!filtered.length&&!currentPath){body.innerHTML='<div class="fm-empty"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg><span>Directory is empty</span></div>';return}
702
-
703
- let html='';
704
- // Parent directory
705
- if(currentPath){
706
- html+='<div class="fm-item" ondblclick="goUp()" onclick="goUp()"><div class="fm-icon folder"><svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg></div><div class="fm-name" style="color:var(--text-muted)">..</div><div class="fm-size"></div></div>';
707
- }
708
- filtered.forEach((item,idx)=>{
709
- const icon=getFileIcon(item.name,item.is_dir);
710
- const iconHtml=icon.svg
711
- ?'<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">'+icon.svg+'</svg>'
712
- :'<span style="font-size:16px">'+icon.text+'</span>';
713
- html+=`<div class="fm-item" data-name="${escapeHtml(item.name)}" data-dir="${item.is_dir}" data-idx="${idx}"
714
- ondblclick="fmDblClick('${escapeHtml(item.name)}',${item.is_dir})"
715
- oncontextmenu="fmCtx(event,'${escapeHtml(item.name)}',${item.is_dir})">
716
- <div class="fm-icon ${icon.cls}">${iconHtml}</div>
717
- <div class="fm-name">${escapeHtml(item.name)}</div>
718
- <div class="fm-size">${item.is_dir?'':formatSize(item.size)}</div>
719
- <div class="fm-actions">
720
- ${!item.is_dir?`<button class="btn btn-sm btn-icon" onclick="event.stopPropagation();editFile('${escapeHtml(item.name)}')" title="Edit"><svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M8.5 1.5a1.4 1.4 0 012 2L4 10l-2.5.5.5-2.5L8.5 1.5z"/></svg></button>`:''}
721
- ${!item.is_dir?`<button class="btn btn-sm btn-icon" onclick="event.stopPropagation();downloadFile('${escapeHtml(item.name)}')" title="Download"><svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 1v8M3 6l3 3 3-3"/><path d="M1 9v2h10V9"/></svg></button>`:''}
722
- <button class="btn btn-sm btn-icon" onclick="event.stopPropagation();deleteItem('${escapeHtml(item.name)}')" title="Delete" style="color:var(--danger)"><svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 3h10M3.5 3V2a1 1 0 011-1h3a1 1 0 011 1v1M9 3v7a1 1 0 01-1 1H4a1 1 0 01-1-1V3"/></svg></button>
723
- </div>
724
- </div>`;
725
- });
726
- body.innerHTML='<div class="fm-list">'+html+'</div>';
727
- }
728
 
729
- function filterFiles(){renderFiles(allFileItems)}
730
- function buildBreadcrumb(){
731
- const el=document.getElementById('fmBreadcrumb');
732
- const parts=currentPath?currentPath.split('/').filter(Boolean):[];
733
- let html='<button class="fm-crumb '+(parts.length===0?'active':'')+'" onclick="loadFiles(\\'\\')">root</button>';
734
- let acc='';
735
- parts.forEach((p,i)=>{
736
- acc+=(acc?'/':'')+p;
737
- const a=acc;
738
- html+='<span class="fm-crumb-sep">/</span>';
739
- html+='<button class="fm-crumb '+(i===parts.length-1?'active':'')+'" onclick="loadFiles(\''+a+'\')">'+escapeHtml(p)+'</button>';
740
- });
741
- el.innerHTML=html;
742
- }
743
- function goUp(){
744
- const parts=currentPath.split('/').filter(Boolean);
745
- parts.pop();
746
- loadFiles(parts.join('/'));
747
- }
748
- function fmDblClick(name,isDir){
749
- if(isDir)loadFiles((currentPath?currentPath+'/':'')+name);
750
- else editFile(name);
751
- }
 
 
 
 
 
 
 
752
 
753
- async function editFile(name){
754
- const fpath=(currentPath?currentPath+'/':'')+name;
755
- try{
756
- const r=await fetch('/api/fs/read?path='+encodeURIComponent(fpath));
757
- if(!r.ok){toast('Cannot open: '+await r.text(),'error');return}
758
- const text=await r.text();
759
- editingFile=fpath;
760
- document.getElementById('editorFilename').textContent=fpath;
761
- document.getElementById('editorContent').value=text;
762
- document.getElementById('editorTab').style.display='';
763
- switchTab('editor');
764
- toast('Opened '+name,'info');
765
- }catch(e){toast('Failed to open file','error')}
766
- }
767
- async function saveFile(){
768
- if(!editingFile)return;
769
- try{
770
- const fd=new FormData();
771
- fd.append('path',editingFile);
772
- fd.append('content',document.getElementById('editorContent').value);
773
- const r=await fetch('/api/fs/write',{method:'POST',body:fd});
774
- if(r.ok)toast('Saved '+editingFile,'success');
775
- else toast('Save failed','error');
776
- }catch(e){toast('Save error','error')}
777
- }
778
- function closeEditor(){
779
- switchTab('files');
780
- document.getElementById('editorTab').style.display='none';
781
- }
782
- function downloadFile(name){
783
- const fpath=(currentPath?currentPath+'/':'')+name;
784
- window.open('/api/fs/download?path='+encodeURIComponent(fpath),'_blank');
785
- }
786
- function downloadCurrentFile(){
787
- if(editingFile)window.open('/api/fs/download?path='+encodeURIComponent(editingFile),'_blank');
788
- }
789
- async function deleteItem(name){
790
- if(!confirm('Delete "'+name+'"?'))return;
791
- const fpath=(currentPath?currentPath+'/':'')+name;
792
- try{
793
- const fd=new FormData();fd.append('path',fpath);
794
- await fetch('/api/fs/delete',{method:'POST',body:fd});
795
- toast('Deleted '+name,'success');
796
- loadFiles(currentPath);
797
- }catch(e){toast('Delete failed','error')}
798
- }
799
- async function createNewFile(){
800
- const name=prompt('Enter filename (or foldername/):');
801
- if(!name)return;
802
- const fpath=(currentPath?currentPath+'/':'')+name;
803
- if(name.endsWith('/')){
804
- // Create directory by writing a temp file and deleting it — or we can use the write endpoint
805
- toast('Creating directories not supported yet','info');
806
- return;
807
- }
808
- try{
809
- const fd=new FormData();fd.append('path',fpath);fd.append('content','');
810
- await fetch('/api/fs/write',{method:'POST',body:fd});
811
- toast('Created '+name,'success');
812
- loadFiles(currentPath);
813
- }catch(e){toast('Failed','error')}
814
- }
815
 
816
- // Upload
817
- function showUploadModal(){
818
- document.getElementById('uploadOverlay').classList.add('active');
819
- document.getElementById('uploadPathDisplay').textContent='to /'+(currentPath||'');
820
- document.getElementById('uploadFileName').textContent='No file selected';
821
- document.getElementById('uploadFileInput').value='';
822
- }
823
- function hideUploadModal(){document.getElementById('uploadOverlay').classList.remove('active')}
824
- function handleFileSelect(inp){
825
- if(inp.files.length)document.getElementById('uploadFileName').textContent=inp.files[0].name;
826
- }
827
- async function doUpload(){
828
- const inp=document.getElementById('uploadFileInput');
829
- if(!inp.files.length){toast('Select a file first','error');return}
830
- const fd=new FormData();fd.append('path',currentPath);fd.append('file',inp.files[0]);
831
- document.getElementById('uploadBtn').textContent='Uploading...';
832
- try{
833
- await fetch('/api/fs/upload',{method:'POST',body:fd});
834
- toast('Uploaded '+inp.files[0].name,'success');
835
- hideUploadModal();
836
- loadFiles(currentPath);
837
- }catch(e){toast('Upload failed','error')}
838
- document.getElementById('uploadBtn').textContent='Upload';
839
- }
840
- // Drag & drop
841
- const uploadZone=document.getElementById('uploadZone');
842
- uploadZone.addEventListener('dragover',e=>{e.preventDefault();uploadZone.classList.add('dragover')});
843
- uploadZone.addEventListener('dragleave',()=>uploadZone.classList.remove('dragover'));
844
- uploadZone.addEventListener('drop',e=>{
845
- e.preventDefault();uploadZone.classList.remove('dragover');
846
- if(e.dataTransfer.files.length){
847
- document.getElementById('uploadFileInput').files=e.dataTransfer.files;
848
- document.getElementById('uploadFileName').textContent=e.dataTransfer.files[0].name;
849
- }
850
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
851
 
852
- // Context Menu
853
- function fmCtx(e,name,isDir){
854
- e.preventDefault();e.stopPropagation();
855
- ctxTarget={name,isDir};
856
- const menu=document.getElementById('ctxMenu');
857
- menu.classList.add('active');
858
- // Position
859
- const x=Math.min(e.clientX,window.innerWidth-170);
860
- const y=Math.min(e.clientY,window.innerHeight-200);
861
- menu.style.left=x+'px';menu.style.top=y+'px';
862
- }
863
- document.addEventListener('click',()=>document.getElementById('ctxMenu').classList.remove('active'));
864
- function ctxAction(action){
865
- if(!ctxTarget)return;
866
- const{name,isDir}=ctxTarget;
867
- document.getElementById('ctxMenu').classList.remove('active');
868
- switch(action){
869
- case'open':fmDblClick(name,isDir);break;
870
- case'edit':if(!isDir)editFile(name);break;
871
- case'download':if(!isDir)downloadFile(name);break;
872
- case'rename':
873
- const nn=prompt('Rename to:',name);
874
- if(nn&&nn!==name)toast('Rename not implemented yet','info');
875
- break;
876
- case'delete':deleteItem(name);break;
877
- }
878
- }
879
 
880
- // ========== TOAST ==========
881
- function toast(msg,type='info'){
882
- const c=document.getElementById('toastContainer');
883
- const t=document.createElement('div');
884
- t.className='toast toast-'+type;
885
- const icons={success:'✓',error:'✕',info:'ℹ'};
886
- t.innerHTML='<span>'+(icons[type]||'ℹ')+'</span><span>'+escapeHtml(msg)+'</span>';
887
- c.appendChild(t);
888
- setTimeout(()=>t.remove(),3000);
889
- }
890
 
891
- // ========== MOBILE ==========
892
- function toggleMobileStats(){
893
- const el=document.getElementById('mobileStats');
894
- el.style.display=el.style.display==='flex'?'none':'flex';
895
- }
 
 
896
 
897
- // ========== KEYBOARD SHORTCUTS ==========
898
- document.addEventListener('keydown',e=>{
899
- if(e.ctrlKey&&e.key==='s'){e.preventDefault();if(editingFile)saveFile()}
900
- if(e.ctrlKey&&e.key==='`'){e.preventDefault();switchTab('console');consoleInput.focus()}
901
- });
 
 
 
 
902
 
903
- // ========== INIT ==========
904
- connectWS();
905
- loadFiles('');
906
- </script>
 
 
 
 
 
 
 
907
  </body>
908
  </html>
909
  """
910
 
911
  # -----------------
912
- # UTILITIES
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
913
  # -----------------
914
  def get_safe_path(subpath: str):
915
  subpath = (subpath or "").strip("/")
916
  target = os.path.abspath(os.path.join(BASE_DIR, subpath))
917
  if not target.startswith(BASE_DIR):
918
- raise HTTPException(status_code=403, detail="Access denied outside /app")
919
  return target
920
 
921
  async def broadcast(message: str):
922
  output_history.append(message)
923
- dead_clients = set()
924
  for client in connected_clients:
925
  try:
926
  await client.send_text(message)
927
  except:
928
- dead_clients.add(client)
929
- connected_clients.difference_update(dead_clients)
930
 
931
- # -----------------
932
- # SERVER PROCESSES
933
- # -----------------
934
  async def read_stream(stream, prefix=""):
935
  while True:
936
  try:
937
  line = await stream.readline()
938
- if not line:
939
- break
940
- line_str = line.decode('utf-8', errors='replace').rstrip('\r\n')
941
- await broadcast(prefix + line_str)
942
  except Exception:
943
  break
944
 
@@ -958,31 +629,31 @@ async def start_minecraft():
958
  "-jar", "purpur.jar", "--nogui"
959
  ]
960
  mc_process = await asyncio.create_subprocess_exec(
961
- *java_args,
962
- stdin=asyncio.subprocess.PIPE,
963
- stdout=asyncio.subprocess.PIPE,
964
- stderr=asyncio.subprocess.STDOUT,
965
- cwd=BASE_DIR
966
  )
967
  asyncio.create_task(read_stream(mc_process.stdout))
968
 
969
  @app.on_event("startup")
970
  async def startup_event():
971
  asyncio.create_task(start_minecraft())
 
972
 
973
  # -----------------
974
  # API ROUTING
975
  # -----------------
976
  @app.get("/")
977
- def get_panel():
978
- return HTMLResponse(content=HTML_CONTENT)
 
 
 
 
979
 
980
  @app.websocket("/ws")
981
- async def websocket_endpoint(websocket: WebSocket):
982
  await websocket.accept()
983
  connected_clients.add(websocket)
984
- for line in output_history:
985
- await websocket.send_text(line)
986
  try:
987
  while True:
988
  cmd = await websocket.receive_text()
@@ -990,115 +661,47 @@ async def websocket_endpoint(websocket: WebSocket):
990
  mc_process.stdin.write((cmd + "\n").encode('utf-8'))
991
  await mc_process.stdin.drain()
992
  except:
993
- connected_clients.discard(websocket)
994
-
995
- @app.get("/api/stats")
996
- def get_stats():
997
- try:
998
- current_process = psutil.Process(os.getpid())
999
- mem_usage = current_process.memory_info().rss
1000
- cpu_percent = current_process.cpu_percent(interval=0)
1001
-
1002
- for child in current_process.children(recursive=True):
1003
- try:
1004
- mem_usage += child.memory_info().rss
1005
- cpu_percent += child.cpu_percent(interval=0)
1006
- except (psutil.NoSuchProcess, psutil.AccessDenied):
1007
- pass
1008
-
1009
- # Normalize CPU to 0-100% for 2 cores
1010
- normalized_cpu = min(100.0, cpu_percent / CONTAINER_CPU_CORES)
1011
-
1012
- # Disk usage for the container's BASE_DIR
1013
- try:
1014
- disk = shutil.disk_usage(BASE_DIR)
1015
- disk_used_gb = disk.used / (1024 ** 3)
1016
- disk_total_gb = disk.total / (1024 ** 3)
1017
- except Exception:
1018
- disk_used_gb = 0
1019
- disk_total_gb = CONTAINER_STORAGE_GB
1020
-
1021
- return {
1022
- "ram_used_mb": round(mem_usage / (1024 * 1024), 1),
1023
- "ram_total_mb": CONTAINER_RAM_MB,
1024
- "cpu_percent": round(normalized_cpu, 1),
1025
- "cpu_cores": CONTAINER_CPU_CORES,
1026
- "disk_used_gb": round(disk_used_gb, 2),
1027
- "disk_total_gb": round(disk_total_gb, 2),
1028
- }
1029
- except Exception:
1030
- return {
1031
- "ram_used_mb": 0, "ram_total_mb": CONTAINER_RAM_MB,
1032
- "cpu_percent": 0, "cpu_cores": CONTAINER_CPU_CORES,
1033
- "disk_used_gb": 0, "disk_total_gb": CONTAINER_STORAGE_GB,
1034
- }
1035
 
1036
  @app.get("/api/fs/list")
1037
  def fs_list(path: str = ""):
1038
  target = get_safe_path(path)
1039
- if not os.path.exists(target):
1040
- return []
1041
  items = []
1042
- try:
1043
- for f in os.listdir(target):
1044
- fp = os.path.join(target, f)
1045
- try:
1046
- is_dir = os.path.isdir(fp)
1047
- size = 0 if is_dir else os.path.getsize(fp)
1048
- items.append({"name": f, "is_dir": is_dir, "size": size})
1049
- except OSError:
1050
- pass
1051
- except PermissionError:
1052
- pass
1053
  return sorted(items, key=lambda x: (not x["is_dir"], x["name"].lower()))
1054
 
1055
  @app.get("/api/fs/read")
1056
  def fs_read(path: str):
1057
  target = get_safe_path(path)
1058
- if not os.path.isfile(target):
1059
- raise HTTPException(400, "Not a file")
1060
  try:
1061
- with open(target, 'r', encoding='utf-8', errors='replace') as f:
1062
- content = f.read(5 * 1024 * 1024) # Max 5MB read
1063
- return Response(content=content, media_type="text/plain")
1064
- except Exception as e:
1065
- raise HTTPException(400, f"Cannot read: {str(e)}")
1066
 
1067
  @app.get("/api/fs/download")
1068
  def fs_download(path: str):
1069
  target = get_safe_path(path)
1070
- if not os.path.isfile(target):
1071
- raise HTTPException(400, "Not a file")
1072
  return FileResponse(target, filename=os.path.basename(target))
1073
 
1074
  @app.post("/api/fs/write")
1075
  def fs_write(path: str = Form(...), content: str = Form(...)):
1076
- target = get_safe_path(path)
1077
- os.makedirs(os.path.dirname(target), exist_ok=True)
1078
- with open(target, 'w', encoding='utf-8') as f:
1079
- f.write(content)
1080
  return {"status": "ok"}
1081
 
1082
  @app.post("/api/fs/upload")
1083
  async def fs_upload(path: str = Form(""), file: UploadFile = File(...)):
1084
- target_dir = get_safe_path(path)
1085
- os.makedirs(target_dir, exist_ok=True)
1086
- target_file = os.path.join(target_dir, file.filename)
1087
- if not os.path.abspath(target_file).startswith(BASE_DIR):
1088
- raise HTTPException(403, "Access denied")
1089
- with open(target_file, "wb") as buffer:
1090
- shutil.copyfileobj(file.file, buffer)
1091
  return {"status": "ok"}
1092
 
1093
  @app.post("/api/fs/delete")
1094
  def fs_delete(path: str = Form(...)):
1095
- target = get_safe_path(path)
1096
- if not os.path.exists(target):
1097
- raise HTTPException(404, "Not found")
1098
- if os.path.isdir(target):
1099
- shutil.rmtree(target)
1100
- else:
1101
- os.remove(target)
1102
  return {"status": "ok"}
1103
 
1104
  if __name__ == "__main__":
 
3
  import collections
4
  import shutil
5
  import psutil
 
6
  from fastapi import FastAPI, WebSocket, Request, Response, Form, UploadFile, File, HTTPException
7
  from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
8
  from fastapi.middleware.cors import CORSMiddleware
 
12
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
13
 
14
  mc_process = None
15
+ output_history = collections.deque(maxlen=300)
16
  connected_clients = set()
17
  BASE_DIR = os.path.abspath("/app")
 
 
 
18
 
19
+ # Global dict to store cached stats so the UI doesn't lag when polling
20
+ cached_stats = { "cpu": 0, "ram_used": 0, "ram_total": 16, "storage_used": 0, "storage_total": 50 }
21
+
22
+ # -----------------
23
+ # HTML FRONTEND (Ultimate SaaS Web3 Dashboard)
24
+ # -----------------
25
  HTML_CONTENT = """
26
  <!DOCTYPE html>
27
+ <html lang="en" class="dark">
28
  <head>
29
+ <meta charset="UTF-8">
30
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
31
+ <title>Server Dashboard</title>
32
+
33
+ <!-- Premium Fonts -->
34
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
35
+ <!-- Phosphor Icons -->
36
+ <script src="https://unpkg.com/@phosphor-icons/web"></script>
37
+ <!-- Terminal -->
38
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css" />
39
+ <script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
40
+ <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit/lib/xterm-addon-fit.js"></script>
41
+ <!-- Tailwind CSS -->
42
+ <script src="https://cdn.tailwindcss.com"></script>
43
+ <script>
44
+ tailwind.config = {
45
+ darkMode: 'class',
46
+ theme: {
47
+ extend: {
48
+ fontFamily: { sans: ['Plus Jakarta Sans', 'sans-serif'], mono: ['JetBrains Mono', 'monospace'] },
49
+ colors: {
50
+ base: '#080B11',
51
+ surface: '#121620',
52
+ surfaceHover: '#1A1F2D',
53
+ border: '#22283A',
54
+ primary: '#6366F1',
55
+ secondary: '#A855F7',
56
+ accent: '#06B6D4'
57
+ }
58
+ }
59
+ }
60
+ }
61
+ </script>
62
+ <style>
63
+ body { background-color: theme('colors.base'); color: #F8FAFC; overflow: hidden; -webkit-font-smoothing: antialiased; }
64
+
65
+ /* Dashboard Cards */
66
+ .premium-card {
67
+ background: theme('colors.surface');
68
+ border: 1px solid theme('colors.border');
69
+ border-radius: 20px;
70
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
71
+ position: relative;
72
+ overflow: hidden;
73
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
+ /* Gradients & Glows */
76
+ .text-gradient { background: linear-gradient(135deg, theme('colors.primary'), theme('colors.secondary')); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
77
+ .bg-gradient-btn { background: linear-gradient(135deg, theme('colors.primary'), theme('colors.secondary')); box-shadow: 0 4px 15px rgba(99, 102, 241, 0.3); transition: all 0.2s ease; }
78
+ .bg-gradient-btn:hover { transform: translateY(-1px); box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4); filter: brightness(1.1); }
79
+
80
+ /* Terminal Fixing for Mobile Wrapping */
81
+ .term-container { flex: 1; min-width: 0; min-height: 0; width: 100%; height: 100%; border-radius: 12px; overflow: hidden; position: relative; }
82
+ .term-wrapper { padding: 12px; height: 100%; width: 100%; }
83
+ .xterm .xterm-viewport { overflow-y: auto !important; width: 100% !important; background-color: transparent !important; }
84
+ .xterm-screen { width: 100% !important; }
85
+
86
+ /* Progress Bars */
87
+ .progress-track { background: theme('colors.border'); border-radius: 999px; height: 6px; overflow: hidden; }
88
+ .progress-fill { height: 100%; border-radius: 999px; transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1); }
89
+
90
+ /* Custom Scrollbar */
91
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
92
+ ::-webkit-scrollbar-track { background: transparent; }
93
+ ::-webkit-scrollbar-thumb { background: theme('colors.border'); border-radius: 4px; }
94
+ ::-webkit-scrollbar-thumb:hover { background: #333C52; }
95
+
96
+ /* Mobile Bottom Nav Glass */
97
+ .mobile-nav-glass {
98
+ background: rgba(18, 22, 32, 0.85);
99
+ backdrop-filter: blur(16px);
100
+ -webkit-backdrop-filter: blur(16px);
101
+ border-top: 1px solid theme('colors.border');
102
+ }
103
 
104
+ /* Nav active states */
105
+ .nav-item { color: #64748B; transition: all 0.2s; }
106
+ .nav-item:hover { color: #F8FAFC; background: theme('colors.surfaceHover'); }
107
+ .nav-item.active { color: #F8FAFC; background: theme('colors.primary'); box-shadow: 0 4px 15px rgba(99, 102, 241, 0.2); }
108
+
109
+ /* Mobile Nav active states */
110
+ .mob-nav-item { color: #64748B; }
111
+ .mob-nav-item.active { color: theme('colors.primary'); }
112
+ .mob-nav-indicator { display: none; height: 4px; width: 4px; border-radius: 50%; background: theme('colors.primary'); margin-top: 2px; }
113
+ .mob-nav-item.active .mob-nav-indicator { display: block; }
114
+
115
+ .fade-in { animation: fadeIn 0.3s ease-out forwards; }
116
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
117
+ .hidden-tab { display: none !important; }
118
+
119
+ /* SVG Background Wave */
120
+ .bg-wave { position: absolute; bottom: 0; left: 0; right: 0; opacity: 0.1; transform: translateY(20%); pointer-events: none; }
121
+ </style>
122
  </head>
123
+ <body class="flex flex-col md:flex-row h-[100dvh] w-full text-sm">
124
+
125
+ <!-- Desktop Sidebar -->
126
+ <aside class="hidden md:flex flex-col w-[260px] premium-card m-4 mr-0 border-y-0 border-l-0 rounded-none rounded-l-2xl border-r border-border bg-surface shrink-0 z-20">
127
+ <div class="p-6 pb-2">
128
+ <div class="flex items-center gap-3">
129
+ <div class="w-10 h-10 rounded-xl bg-gradient-to-br from-primary to-secondary flex items-center justify-center shadow-lg">
130
+ <i class="ph ph-cube text-xl text-white"></i>
131
+ </div>
132
+ <div>
133
+ <h1 class="font-bold text-lg text-white leading-tight">Server<span class="text-gradient">Space</span></h1>
134
+ <p class="text-[10px] text-slate-400 font-semibold uppercase tracking-wider">Engine v2.0</p>
135
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  </div>
 
 
138
 
139
+ <div class="px-6 py-4">
140
+ <div class="text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-3">Menu</div>
141
+ <nav class="flex flex-col gap-2">
142
+ <button onclick="switchTab('dashboard')" id="nav-dashboard" class="nav-item active flex items-center gap-3 px-4 py-3 rounded-xl font-medium">
143
+ <i class="ph ph-squares-four text-lg"></i> Dashboard
144
+ </button>
145
+ <button onclick="switchTab('files')" id="nav-files" class="nav-item flex items-center gap-3 px-4 py-3 rounded-xl font-medium">
146
+ <i class="ph ph-folder-open text-lg"></i> Files
147
+ </button>
148
+ </nav>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  </div>
150
+
151
+ <div class="mt-auto p-6">
152
+ <div class="bg-surfaceHover border border-border rounded-xl p-4 flex items-center gap-3">
153
+ <div class="relative flex h-3 w-3">
154
+ <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
155
+ <span class="relative inline-flex rounded-full h-3 w-3 bg-green-500"></span>
156
+ </div>
157
+ <div>
158
+ <div class="text-xs font-bold text-white">Status Online</div>
159
+ <div class="text-[10px] text-slate-400">Container Active</div>
160
+ </div>
161
+ </div>
162
  </div>
163
+ </aside>
 
164
 
165
+ <!-- Mobile Header -->
166
+ <header class="md:hidden flex justify-between items-center px-5 py-4 bg-surface border-b border-border shrink-0 z-20">
167
+ <div class="flex items-center gap-2">
168
+ <div class="w-8 h-8 rounded-lg bg-gradient-to-br from-primary to-secondary flex items-center justify-center">
169
+ <i class="ph ph-cube text-white"></i>
170
+ </div>
171
+ <h1 class="font-bold text-base text-white">Server<span class="text-gradient">Space</span></h1>
 
 
 
 
 
 
 
 
 
 
172
  </div>
173
+ <div class="relative flex h-2 w-2">
174
+ <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
175
+ <span class="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
176
  </div>
177
+ </header>
178
+
179
+ <!-- Main Workspace -->
180
+ <main class="flex-grow flex flex-col p-4 md:p-6 overflow-hidden min-w-0 bg-base relative z-10">
181
+
182
+ <!-- DASHBOARD TAB -->
183
+ <div id="tab-dashboard" class="h-full flex flex-col gap-4 md:gap-6 fade-in min-w-0">
184
+
185
+ <!-- Metrics Row (Strictly 16GB / 2 Cores / 50GB) -->
186
+ <div class="grid grid-cols-3 gap-3 md:gap-6 shrink-0">
187
+
188
+ <!-- RAM Card -->
189
+ <div class="premium-card p-4 flex flex-col justify-between h-[100px] md:h-[130px]">
190
+ <div class="flex justify-between items-start">
191
+ <div class="p-2 rounded-lg bg-primary/10 text-primary hidden md:block"><i class="ph-fill ph-memory text-xl"></i></div>
192
+ <i class="ph-fill ph-memory text-primary text-xl md:hidden"></i>
193
+ <span class="text-[10px] md:text-xs font-bold text-slate-400 uppercase tracking-wide">RAM Usage</span>
194
+ </div>
195
+ <div>
196
+ <div class="flex items-end gap-1 md:gap-2 mb-2">
197
+ <span class="text-lg md:text-3xl font-bold text-white font-mono leading-none" id="ram-val">0.0</span>
198
+ <span class="text-[10px] md:text-sm text-slate-500 font-mono mb-0.5">/ 16 GB</span>
199
+ </div>
200
+ <div class="progress-track"><div id="ram-bar" class="progress-fill bg-primary w-0"></div></div>
201
+ </div>
202
+ </div>
203
+
204
+ <!-- CPU Card -->
205
+ <div class="premium-card p-4 flex flex-col justify-between h-[100px] md:h-[130px]">
206
+ <div class="flex justify-between items-start">
207
+ <div class="p-2 rounded-lg bg-secondary/10 text-secondary hidden md:block"><i class="ph-fill ph-cpu text-xl"></i></div>
208
+ <i class="ph-fill ph-cpu text-secondary text-xl md:hidden"></i>
209
+ <span class="text-[10px] md:text-xs font-bold text-slate-400 uppercase tracking-wide">vCores</span>
210
+ </div>
211
+ <div>
212
+ <div class="flex items-end gap-1 md:gap-2 mb-2">
213
+ <span class="text-lg md:text-3xl font-bold text-white font-mono leading-none" id="cpu-val">0</span>
214
+ <span class="text-[10px] md:text-sm text-slate-500 font-mono mb-0.5">% of 2</span>
215
+ </div>
216
+ <div class="progress-track"><div id="cpu-bar" class="progress-fill bg-secondary w-0"></div></div>
217
+ </div>
218
+ </div>
219
+
220
+ <!-- Storage Card -->
221
+ <div class="premium-card p-4 flex flex-col justify-between h-[100px] md:h-[130px]">
222
+ <div class="flex justify-between items-start">
223
+ <div class="p-2 rounded-lg bg-accent/10 text-accent hidden md:block"><i class="ph-fill ph-hard-drives text-xl"></i></div>
224
+ <i class="ph-fill ph-hard-drives text-accent text-xl md:hidden"></i>
225
+ <span class="text-[10px] md:text-xs font-bold text-slate-400 uppercase tracking-wide">Disk Space</span>
226
+ </div>
227
+ <div>
228
+ <div class="flex items-end gap-1 md:gap-2 mb-2">
229
+ <span class="text-lg md:text-3xl font-bold text-white font-mono leading-none" id="disk-val">0.0</span>
230
+ <span class="text-[10px] md:text-sm text-slate-500 font-mono mb-0.5">/ 50 GB</span>
231
+ </div>
232
+ <div class="progress-track"><div id="disk-bar" class="progress-fill bg-accent w-0"></div></div>
233
+ </div>
234
+ </div>
235
+ </div>
236
 
237
+ <!-- Terminal Area -->
238
+ <div class="premium-card flex flex-col flex-grow min-h-0">
239
+ <!-- Mac-style Terminal Header -->
240
+ <div class="bg-surface border-b border-border px-4 py-3 flex items-center justify-between z-10 shrink-0">
241
+ <div class="flex gap-2">
242
+ <div class="w-3 h-3 rounded-full bg-[#EF4444]"></div>
243
+ <div class="w-3 h-3 rounded-full bg-[#F59E0B]"></div>
244
+ <div class="w-3 h-3 rounded-full bg-[#10B981]"></div>
245
+ </div>
246
+ <span class="text-xs font-mono text-slate-400">server_console ~ /app</span>
247
+ <div class="w-12"></div> <!-- Spacer for center alignment -->
248
+ </div>
249
+
250
+ <!-- Terminal Container -->
251
+ <div class="term-container bg-[#080B11]">
252
+ <div id="terminal" class="term-wrapper"></div>
253
+ </div>
254
+
255
+ <!-- Input Field -->
256
+ <div class="p-3 md:p-4 bg-surface/80 border-t border-border z-10 shrink-0">
257
+ <div class="relative flex items-center">
258
+ <i class="ph ph-terminal text-primary absolute left-4 text-lg"></i>
259
+ <input type="text" id="cmd-input" class="w-full bg-base border border-border focus:border-primary/50 text-white rounded-xl pl-12 pr-12 py-3 text-sm font-mono transition-all outline-none" placeholder="Enter command here...">
260
+ <button onclick="sendCommand()" class="absolute right-2 p-2 bg-gradient-btn rounded-lg text-white">
261
+ <i class="ph-bold ph-arrow-right"></i>
262
+ </button>
263
+ </div>
264
+ </div>
265
+ </div>
266
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
+ <!-- FILES TAB -->
269
+ <div id="tab-files" class="hidden-tab h-full flex flex-col premium-card overflow-hidden min-w-0">
270
+ <!-- Header -->
271
+ <div class="bg-surface px-5 py-4 border-b border-border flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 shrink-0">
272
+ <div class="flex items-center gap-2 text-sm font-mono text-slate-300 overflow-x-auto whitespace-nowrap w-full sm:w-auto" id="breadcrumbs">
273
+ <!-- JS Breadcrumbs -->
274
+ </div>
275
+ <div class="flex gap-2 shrink-0">
276
+ <input type="file" id="file-upload" class="hidden" onchange="uploadFile(event)">
277
+ <button onclick="document.getElementById('file-upload').click()" class="bg-gradient-btn px-4 py-2 rounded-xl text-xs font-bold text-white flex items-center gap-2">
278
+ <i class="ph-bold ph-upload-simple"></i> Upload
279
+ </button>
280
+ <button onclick="loadFiles(currentPath)" class="bg-surfaceHover border border-border px-3 py-2 rounded-xl text-slate-300 hover:text-white transition-colors">
281
+ <i class="ph-bold ph-arrows-clockwise text-base"></i>
282
+ </button>
283
+ </div>
284
+ </div>
285
+
286
+ <!-- File Headers (Desktop only) -->
287
+ <div class="hidden sm:grid grid-cols-12 gap-4 px-6 py-3 bg-[#0D1017] border-b border-border text-[11px] font-bold text-slate-500 uppercase tracking-wider shrink-0">
288
+ <div class="col-span-7">Name</div>
289
+ <div class="col-span-3 text-right">Size</div>
290
+ <div class="col-span-2 text-right">Actions</div>
291
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
 
293
+ <!-- File List -->
294
+ <div class="flex-grow overflow-y-auto bg-base p-2 md:p-3" id="file-list">
295
+ <!-- JS Files -->
296
+ </div>
297
+ </div>
298
+ </main>
299
+
300
+ <!-- Mobile Bottom Navigation -->
301
+ <nav class="md:hidden mobile-nav-glass pb-safe pt-2 px-6 flex justify-around items-center shrink-0 z-50 rounded-t-2xl absolute bottom-0 w-full h-[70px]">
302
+ <button onclick="switchTab('dashboard')" id="mob-dashboard" class="mob-nav-item active flex flex-col items-center gap-1 w-16">
303
+ <i class="ph-fill ph-squares-four text-2xl"></i>
304
+ <span class="text-[10px] font-semibold">Dash</span>
305
+ <div class="mob-nav-indicator"></div>
306
+ </button>
307
+ <button onclick="switchTab('files')" id="mob-files" class="mob-nav-item flex flex-col items-center gap-1 w-16">
308
+ <i class="ph-fill ph-folder text-2xl"></i>
309
+ <span class="text-[10px] font-semibold">Files</span>
310
+ <div class="mob-nav-indicator"></div>
311
+ </button>
312
+ </nav>
313
+
314
+ <!-- File Editor Modal -->
315
+ <div id="editor-modal" class="fixed inset-0 bg-black/80 backdrop-blur-sm hidden items-center justify-center p-4 z-[100] opacity-0 transition-opacity duration-300">
316
+ <div class="premium-card w-full max-w-4xl h-[85vh] flex flex-col transform scale-95 transition-transform duration-300" id="editor-card">
317
+ <div class="bg-surface px-5 py-4 flex justify-between items-center border-b border-border shrink-0">
318
+ <div class="flex items-center gap-3 text-sm font-mono text-white">
319
+ <i class="ph-fill ph-file-code text-primary text-xl"></i>
320
+ <span id="editor-title">file.txt</span>
321
+ </div>
322
+ <div class="flex gap-2">
323
+ <button onclick="closeEditor()" class="px-4 py-2 hover:bg-surfaceHover rounded-xl text-xs font-bold text-slate-400">Cancel</button>
324
+ <button onclick="saveFile()" class="bg-gradient-btn px-5 py-2 rounded-xl text-xs font-bold text-white shadow-lg flex items-center gap-2">
325
+ <i class="ph-bold ph-floppy-disk"></i> Save
326
+ </button>
327
+ </div>
328
+ </div>
329
+ <textarea id="editor-content" class="flex-grow bg-[#080B11] text-slate-200 p-5 font-mono text-sm resize-none focus:outline-none w-full leading-loose" spellcheck="false"></textarea>
330
+ </div>
331
+ </div>
332
 
333
+ <!-- Notification Toasts -->
334
+ <div id="toast-container" class="fixed top-6 right-6 z-[200] flex flex-col gap-3 pointer-events-none"></div>
335
+
336
+ <script>
337
+ // --- Tab Navigation ---
338
+ function switchTab(tab) {
339
+ document.getElementById('tab-dashboard').classList.add('hidden-tab');
340
+ document.getElementById('tab-files').classList.add('hidden-tab');
341
+
342
+ // Reset Desktop
343
+ document.getElementById('nav-dashboard').className = "nav-item flex items-center gap-3 px-4 py-3 rounded-xl font-medium";
344
+ document.getElementById('nav-files').className = "nav-item flex items-center gap-3 px-4 py-3 rounded-xl font-medium";
345
+
346
+ // Reset Mobile
347
+ document.getElementById('mob-dashboard').classList.remove('active');
348
+ document.getElementById('mob-files').classList.remove('active');
349
+
350
+ // Activate
351
+ document.getElementById('tab-' + tab).classList.remove('hidden-tab');
352
+ document.getElementById('tab-' + tab).classList.add('fade-in');
353
+
354
+ document.getElementById('nav-' + tab).className = "nav-item active flex items-center gap-3 px-4 py-3 rounded-xl font-medium";
355
+ document.getElementById('mob-' + tab).classList.add('active');
356
+
357
+ if(tab === 'dashboard' && fitAddon) setTimeout(() => fitAddon.fit(), 100);
358
+ if(tab === 'files' && !window.filesLoaded) { loadFiles(''); window.filesLoaded = true; }
359
+ }
 
 
 
 
 
 
360
 
361
+ // --- Terminal Engine ---
362
+ const term = new Terminal({
363
+ theme: { background: 'transparent', foreground: '#E2E8F0', cursor: '#6366F1', selectionBackground: 'rgba(99, 102, 241, 0.3)' },
364
+ fontFamily: "'JetBrains Mono', monospace", fontSize: 13, cursorBlink: true, convertEol: true
365
+ });
366
+ const fitAddon = new FitAddon.FitAddon();
367
+ term.loadAddon(fitAddon);
368
+ term.open(document.getElementById('terminal'));
369
+
370
+ // Exact fit tracking to fix wrapping
371
+ const ro = new ResizeObserver(() => {
372
+ if(!document.getElementById('tab-dashboard').classList.contains('hidden-tab')) {
373
+ requestAnimationFrame(() => fitAddon.fit());
374
+ }
375
+ });
376
+ ro.observe(document.querySelector('.term-container'));
377
+ setTimeout(() => fitAddon.fit(), 200);
378
+
379
+ const ws = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws');
380
+ ws.onopen = () => term.write('\\x1b[38;5;63m\\x1b[1m[Console]\\x1b[0m Engine connection established.\\r\\n');
381
+ ws.onmessage = e => term.write(e.data + '\\n');
382
+
383
+ const cmdInput = document.getElementById('cmd-input');
384
+ cmdInput.addEventListener('keypress', e => { if (e.key === 'Enter') sendCommand(); });
385
+ function sendCommand() {
386
+ if(cmdInput.value.trim() && ws.readyState === WebSocket.OPEN) {
387
+ term.write(`\\x1b[90m> ${cmdInput.value}\\x1b[0m\\r\\n`);
388
+ ws.send(cmdInput.value); cmdInput.value = '';
389
+ }
390
+ }
391
 
392
+ // --- Container Metrics Engine ---
393
+ async function fetchStats() {
394
+ try {
395
+ const res = await fetch('/api/stats');
396
+ const data = await res.json();
397
+
398
+ // RAM (Max 16GB)
399
+ const ramVal = Math.min(data.ram_used, 16.0);
400
+ document.getElementById('ram-val').innerText = ramVal.toFixed(1);
401
+ document.getElementById('ram-bar').style.width = `${(ramVal / 16.0) * 100}%`;
402
+
403
+ // CPU (Max 100% representing 2 cores)
404
+ const cpuVal = Math.min(data.cpu, 100);
405
+ document.getElementById('cpu-val').innerText = Math.round(cpuVal);
406
+ document.getElementById('cpu-bar').style.width = `${cpuVal}%`;
407
+
408
+ // Storage (Max 50GB)
409
+ const diskVal = Math.min(data.storage_used, 50.0);
410
+ document.getElementById('disk-val').innerText = diskVal.toFixed(1);
411
+ document.getElementById('disk-bar').style.width = `${(diskVal / 50.0) * 100}%`;
412
+
413
+ } catch (e) {}
414
+ }
415
+ setInterval(fetchStats, 2000);
416
+ fetchStats();
417
+
418
+ // --- File Manager ---
419
+ let currentPath = '';
420
+ let editPath = '';
421
+
422
+ function showToast(msg, type='info') {
423
+ const container = document.getElementById('toast-container');
424
+ const el = document.createElement('div');
425
+ let icon = '<i class="ph-fill ph-info text-blue-400 text-xl"></i>';
426
+ if(type==='success') icon = '<i class="ph-fill ph-check-circle text-green-400 text-xl"></i>';
427
+ if(type==='error') icon = '<i class="ph-fill ph-warning-circle text-red-400 text-xl"></i>';
428
+
429
+ el.className = `flex items-center gap-3 bg-surface border border-border text-white px-5 py-4 rounded-xl shadow-2xl translate-x-10 opacity-0 transition-all duration-300`;
430
+ el.innerHTML = `${icon} <span class="font-bold text-sm tracking-wide">${msg}</span>`;
431
+ container.appendChild(el);
432
+
433
+ requestAnimationFrame(() => el.classList.remove('translate-x-10', 'opacity-0'));
434
+ setTimeout(() => { el.classList.add('translate-x-10', 'opacity-0'); setTimeout(() => el.remove(), 300); }, 3000);
435
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436
 
437
+ async function loadFiles(path) {
438
+ currentPath = path;
439
+ const parts = path.split('/').filter(p => p);
440
+ let bc = `<button onclick="loadFiles('')" class="hover:text-white transition"><i class="ph-fill ph-house text-lg"></i></button>`;
441
+ let bp = '';
442
+ parts.forEach((p, i) => {
443
+ bp += (bp?'/':'') + p;
444
+ bc += `<i class="ph-bold ph-caret-right text-xs mx-2 opacity-30"></i>`;
445
+ if(i === parts.length-1) bc += `<span class="text-primary font-bold">${p}</span>`;
446
+ else bc += `<button onclick="loadFiles('${bp}')" class="hover:text-white transition">${p}</button>`;
447
+ });
448
+ document.getElementById('breadcrumbs').innerHTML = bc;
449
+
450
+ const list = document.getElementById('file-list');
451
+ list.innerHTML = `<div class="flex justify-center py-12"><i class="ph-bold ph-spinner-gap animate-spin text-3xl text-primary"></i></div>`;
452
+
453
+ try {
454
+ const res = await fetch(`/api/fs/list?path=${encodeURIComponent(path)}`);
455
+ const files = await res.json();
456
+ list.innerHTML = '';
457
+
458
+ if (path !== '') {
459
+ const parent = path.split('/').slice(0, -1).join('/');
460
+ list.innerHTML += `
461
+ <div class="flex items-center px-4 py-3 cursor-pointer hover:bg-surfaceHover rounded-xl transition mb-1 border border-transparent" onclick="loadFiles('${parent}')">
462
+ <i class="ph-bold ph-arrow-u-up-left text-slate-500 mr-3 text-lg"></i>
463
+ <span class="text-sm font-mono text-slate-400 font-semibold">.. back</span>
464
+ </div>`;
465
+ }
466
+
467
+ files.forEach(f => {
468
+ const icon = f.is_dir ? '<div class="p-2.5 bg-primary/10 rounded-lg text-primary"><i class="ph-fill ph-folder text-xl"></i></div>' : '<div class="p-2.5 bg-surface border border-border rounded-lg text-slate-400"><i class="ph-fill ph-file text-xl"></i></div>';
469
+ const sz = f.is_dir ? '--' : (f.size > 1048576 ? (f.size/1048576).toFixed(1) + ' MB' : (f.size/1024).toFixed(1) + ' KB');
470
+ const fp = path ? `${path}/${f.name}` : f.name;
471
+
472
+ list.innerHTML += `
473
+ <div class="flex flex-col sm:grid sm:grid-cols-12 items-start sm:items-center px-3 py-2 gap-3 group hover:bg-surfaceHover rounded-xl transition mb-1 border border-transparent hover:border-border">
474
+ <div class="col-span-7 flex items-center gap-4 w-full ${f.is_dir?'cursor-pointer':''}" ${f.is_dir?`onclick="loadFiles('${fp}')"`:''}>
475
+ ${icon}
476
+ <span class="text-sm font-mono text-slate-200 truncate group-hover:text-primary transition font-medium">${f.name}</span>
477
+ </div>
478
+ <div class="col-span-3 text-right text-xs text-slate-500 font-mono hidden sm:block">${sz}</div>
479
+ <div class="col-span-2 flex justify-end gap-2 w-full sm:w-auto sm:opacity-0 group-hover:opacity-100 transition">
480
+ ${!f.is_dir ? `<button onclick="editFile('${fp}')" class="p-2 bg-surface border border-border hover:border-primary hover:text-primary rounded-lg transition"><i class="ph-bold ph-pencil-simple text-sm"></i></button>` : ''}
481
+ ${!f.is_dir ? `<a href="/api/fs/download?path=${encodeURIComponent(fp)}" class="p-2 bg-surface border border-border hover:border-green-400 hover:text-green-400 rounded-lg transition"><i class="ph-bold ph-download-simple text-sm"></i></a>` : ''}
482
+ <button onclick="deleteFile('${fp}')" class="p-2 bg-surface border border-border hover:border-red-400 hover:text-red-400 rounded-lg transition"><i class="ph-bold ph-trash text-sm"></i></button>
483
+ </div>
484
+ </div>`;
485
+ });
486
+ } catch (err) { list.innerHTML = `<div class="text-center py-8 text-red-400 text-sm">Failed to load files</div>`; }
487
+ }
488
 
489
+ async function editFile(path) {
490
+ try {
491
+ const res = await fetch(`/api/fs/read?path=${encodeURIComponent(path)}`);
492
+ if(res.ok) {
493
+ editPath = path;
494
+ document.getElementById('editor-content').value = await res.text();
495
+ document.getElementById('editor-title').innerText = path.split('/').pop();
496
+ const m = document.getElementById('editor-modal'); const c = document.getElementById('editor-card');
497
+ m.classList.remove('hidden'); m.classList.add('flex');
498
+ requestAnimationFrame(() => { m.classList.remove('opacity-0'); c.classList.remove('scale-95'); });
499
+ } else showToast('Binary file cannot be edited', 'error');
500
+ } catch { showToast('Error opening file', 'error'); }
501
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
502
 
503
+ function closeEditor() {
504
+ const m = document.getElementById('editor-modal'); const c = document.getElementById('editor-card');
505
+ m.classList.add('opacity-0'); c.classList.add('scale-95');
506
+ setTimeout(() => { m.classList.add('hidden'); m.classList.remove('flex'); }, 300);
507
+ }
 
 
 
 
 
508
 
509
+ async function saveFile() {
510
+ const fd = new FormData(); fd.append('path', editPath); fd.append('content', document.getElementById('editor-content').value);
511
+ try {
512
+ const res = await fetch('/api/fs/write', { method: 'POST', body: fd });
513
+ if(res.ok) { showToast('Saved securely', 'success'); closeEditor(); } else throw new Error();
514
+ } catch { showToast('Save failed', 'error'); }
515
+ }
516
 
517
+ async function deleteFile(path) {
518
+ if(confirm(`Erase ${path.split('/').pop()} permanentely?`)) {
519
+ const fd = new FormData(); fd.append('path', path);
520
+ try {
521
+ const res = await fetch('/api/fs/delete', { method: 'POST', body: fd });
522
+ if(res.ok) { showToast('Erased', 'success'); loadFiles(currentPath); } else throw new Error();
523
+ } catch { showToast('Erase failed', 'error'); }
524
+ }
525
+ }
526
 
527
+ async function uploadFile(e) {
528
+ if(!e.target.files.length) return;
529
+ showToast('Uploading data...', 'info');
530
+ const fd = new FormData(); fd.append('path', currentPath); fd.append('file', e.target.files[0]);
531
+ try {
532
+ const res = await fetch('/api/fs/upload', { method: 'POST', body: fd });
533
+ if(res.ok) { showToast('Upload complete', 'success'); loadFiles(currentPath); } else throw new Error();
534
+ } catch { showToast('Upload failed', 'error'); }
535
+ e.target.value = '';
536
+ }
537
+ </script>
538
  </body>
539
  </html>
540
  """
541
 
542
  # -----------------
543
+ # STATS ENGINE BACKGROUND TASK (Container-Only Restrictions)
544
+ # -----------------
545
+ async def update_system_stats():
546
+ """ Runs constantly in the background so the UI endpoint is instantly responsive. """
547
+ while True:
548
+ try:
549
+ # 1. Gather Storage strictly for /app
550
+ # Hugging Face usually provides around 50GB. We enforce this visual cap.
551
+ total_st, used_st, free_st = shutil.disk_usage('/app')
552
+ cached_stats["storage_used"] = used_st / (1024**3)
553
+ cached_stats["storage_total"] = 50.0
554
+
555
+ # 2. Gather RAM and CPU strictly for the python process + Java child (No Host Info)
556
+ ram_used = 0
557
+ cpu_percent_raw = 0.0
558
+
559
+ try:
560
+ main_proc = psutil.Process(os.getpid())
561
+ ram_used += main_proc.memory_info().rss
562
+ cpu_percent_raw += main_proc.cpu_percent()
563
+
564
+ # Fetch Java child process metrics
565
+ for child in main_proc.children(recursive=True):
566
+ try:
567
+ ram_used += child.memory_info().rss
568
+ cpu_percent_raw += child.cpu_percent()
569
+ except psutil.NoSuchProcess:
570
+ pass
571
+ except Exception:
572
+ pass
573
+
574
+ # Convert RAM to GB
575
+ cached_stats["ram_used"] = ram_used / (1024**3)
576
+ cached_stats["ram_total"] = 16.0 # Strict Hugging Face Limit
577
+
578
+ # Normalize CPU to 2 vCores (where 200% raw = 100% full capacity)
579
+ normalized_cpu = cpu_percent_raw / 2.0
580
+ cached_stats["cpu"] = min(100.0, normalized_cpu)
581
+
582
+ except Exception as e:
583
+ pass # Failsafe
584
+
585
+ await asyncio.sleep(2) # Update every 2 seconds
586
+
587
+ # -----------------
588
+ # UTILITIES & SERVER
589
  # -----------------
590
  def get_safe_path(subpath: str):
591
  subpath = (subpath or "").strip("/")
592
  target = os.path.abspath(os.path.join(BASE_DIR, subpath))
593
  if not target.startswith(BASE_DIR):
594
+ raise HTTPException(status_code=403, detail="Access denied")
595
  return target
596
 
597
  async def broadcast(message: str):
598
  output_history.append(message)
599
+ dead = set()
600
  for client in connected_clients:
601
  try:
602
  await client.send_text(message)
603
  except:
604
+ dead.add(client)
605
+ connected_clients.difference_update(dead)
606
 
 
 
 
607
  async def read_stream(stream, prefix=""):
608
  while True:
609
  try:
610
  line = await stream.readline()
611
+ if not line: break
612
+ await broadcast(prefix + line.decode('utf-8', errors='replace').rstrip('\r\n'))
 
 
613
  except Exception:
614
  break
615
 
 
629
  "-jar", "purpur.jar", "--nogui"
630
  ]
631
  mc_process = await asyncio.create_subprocess_exec(
632
+ *java_args, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, cwd=BASE_DIR
 
 
 
 
633
  )
634
  asyncio.create_task(read_stream(mc_process.stdout))
635
 
636
  @app.on_event("startup")
637
  async def startup_event():
638
  asyncio.create_task(start_minecraft())
639
+ asyncio.create_task(update_system_stats()) # Start background polling loop
640
 
641
  # -----------------
642
  # API ROUTING
643
  # -----------------
644
  @app.get("/")
645
+ def get_panel(): return HTMLResponse(content=HTML_CONTENT)
646
+
647
+ @app.get("/api/stats")
648
+ def api_stats():
649
+ # Returns the background-calculated stats instantly
650
+ return JSONResponse(content=cached_stats)
651
 
652
  @app.websocket("/ws")
653
+ async def ws_endpoint(websocket: WebSocket):
654
  await websocket.accept()
655
  connected_clients.add(websocket)
656
+ for line in output_history: await websocket.send_text(line)
 
657
  try:
658
  while True:
659
  cmd = await websocket.receive_text()
 
661
  mc_process.stdin.write((cmd + "\n").encode('utf-8'))
662
  await mc_process.stdin.drain()
663
  except:
664
+ connected_clients.remove(websocket)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
665
 
666
  @app.get("/api/fs/list")
667
  def fs_list(path: str = ""):
668
  target = get_safe_path(path)
669
+ if not os.path.exists(target): return []
 
670
  items = []
671
+ for f in os.listdir(target):
672
+ fp = os.path.join(target, f)
673
+ items.append({"name": f, "is_dir": os.path.isdir(fp), "size": os.path.getsize(fp) if not os.path.isdir(fp) else 0})
 
 
 
 
 
 
 
 
674
  return sorted(items, key=lambda x: (not x["is_dir"], x["name"].lower()))
675
 
676
  @app.get("/api/fs/read")
677
  def fs_read(path: str):
678
  target = get_safe_path(path)
679
+ if not os.path.isfile(target): raise HTTPException(400, "Not a file")
 
680
  try:
681
+ with open(target, 'r', encoding='utf-8') as f: return Response(content=f.read(), media_type="text/plain")
682
+ except: raise HTTPException(400, "File is binary")
 
 
 
683
 
684
  @app.get("/api/fs/download")
685
  def fs_download(path: str):
686
  target = get_safe_path(path)
687
+ if not os.path.isfile(target): raise HTTPException(400, "Not a file")
 
688
  return FileResponse(target, filename=os.path.basename(target))
689
 
690
  @app.post("/api/fs/write")
691
  def fs_write(path: str = Form(...), content: str = Form(...)):
692
+ with open(get_safe_path(path), 'w', encoding='utf-8') as f: f.write(content)
 
 
 
693
  return {"status": "ok"}
694
 
695
  @app.post("/api/fs/upload")
696
  async def fs_upload(path: str = Form(""), file: UploadFile = File(...)):
697
+ with open(os.path.join(get_safe_path(path), file.filename), "wb") as buffer: shutil.copyfileobj(file.file, buffer)
 
 
 
 
 
 
698
  return {"status": "ok"}
699
 
700
  @app.post("/api/fs/delete")
701
  def fs_delete(path: str = Form(...)):
702
+ t = get_safe_path(path)
703
+ if os.path.isdir(t): shutil.rmtree(t)
704
+ else: os.remove(t)
 
 
 
 
705
  return {"status": "ok"}
706
 
707
  if __name__ == "__main__":