Spaces:
Sleeping
Sleeping
Update panel.py
Browse files
panel.py
CHANGED
|
@@ -17,358 +17,663 @@ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True,
|
|
| 17 |
BASE_DIR = os.environ.get("SERVER_DIR", os.path.abspath("/app"))
|
| 18 |
PLUGINS_DIR = os.path.join(BASE_DIR, "plugins")
|
| 19 |
mc_process = None
|
| 20 |
-
output_history = collections.deque(maxlen=
|
| 21 |
connected_clients = set()
|
| 22 |
|
| 23 |
-
#
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
<style>
|
| 28 |
-
:
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
<body>
|
| 40 |
-
<!-- MAIN LAYOUT -->
|
| 41 |
-
<div class="flex flex-1 overflow-hidden">
|
| 42 |
-
<!-- DESKTOP SIDEBAR -->
|
| 43 |
-
<aside class="hidden sm:flex flex-col w-14 bg-black border-r border-[#1a1a1a] items-center py-6 gap-6 z-20">
|
| 44 |
-
<div class="text-green-500 drop-shadow-md"><i data-lucide="cpu" class="w-6 h-6"></i></div>
|
| 45 |
-
<nav class="flex flex-col gap-6 w-full items-center">
|
| 46 |
-
<button onclick="tab('console')" id="d-console" class="nav-btn active p-2 hover:text-white" title="Console"><i data-lucide="terminal-square" class="w-5 h-5"></i></button>
|
| 47 |
-
<button onclick="tab('files')" id="d-files" class="nav-btn p-2 hover:text-white" title="Files"><i data-lucide="folder-tree" class="w-5 h-5"></i></button>
|
| 48 |
-
<button onclick="tab('plugins')" id="d-plugins" class="nav-btn p-2 hover:text-white" title="Plugins"><i data-lucide="package-search" class="w-5 h-5"></i></button>
|
| 49 |
-
</nav>
|
| 50 |
-
</aside>
|
| 51 |
-
|
| 52 |
-
<main class="flex-1 flex flex-col relative bg-[#050505] overflow-hidden">
|
| 53 |
-
|
| 54 |
-
<!-- CONSOLE -->
|
| 55 |
-
<div id="tab-console" class="tab-pane active p-2 sm:p-4">
|
| 56 |
-
<div class="flex-1 bg-black border border-[#1a1a1a] rounded-lg flex flex-col overflow-hidden shadow-2xl">
|
| 57 |
-
<div class="h-8 bg-[#0a0a0a] border-b border-[#1a1a1a] flex items-center px-3 gap-2">
|
| 58 |
-
<div class="w-2 h-2 rounded-full bg-green-500 shadow-[0_0_6px_#22c55e]"></div><span class="text-[10px] font-mono text-zinc-500 uppercase tracking-wider">Live Stream</span>
|
| 59 |
-
</div>
|
| 60 |
-
<div id="logs" class="flex-1 overflow-y-auto p-3 text-zinc-300 scroll-smooth"></div>
|
| 61 |
-
<div class="p-2 bg-[#0a0a0a] border-t border-[#1a1a1a] flex gap-2">
|
| 62 |
-
<input id="cmd" type="text" class="flex-1 bg-[#050505] border border-[#222] rounded text-xs px-3 py-2 font-mono text-green-400 placeholder-zinc-700" placeholder="Type a command..." autocomplete="off">
|
| 63 |
-
<button onclick="sendCmd()" class="bg-[#1a1a1a] hover:bg-[#222] text-white p-2 rounded border border-[#222]"><i data-lucide="send-horizontal" class="w-4 h-4"></i></button>
|
| 64 |
-
</div>
|
| 65 |
-
</div>
|
| 66 |
-
</div>
|
| 67 |
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
</div>
|
| 80 |
-
|
| 81 |
-
<
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
</div>
|
| 121 |
-
|
|
|
|
|
|
|
| 122 |
</div>
|
| 123 |
|
| 124 |
<!-- MOBILE NAV -->
|
| 125 |
-
<nav class="
|
| 126 |
-
|
| 127 |
-
<button
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
</nav>
|
| 130 |
|
| 131 |
-
<
|
| 132 |
-
<div id="toasts" class="fixed bottom-16 sm:bottom-6 right-4 z-50 flex flex-col gap-2 pointer-events-none"></div>
|
| 133 |
|
| 134 |
<script>
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
//
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
}
|
| 158 |
|
| 159 |
-
//
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
};
|
| 170 |
-
function sendCmd() {
|
| 171 |
-
const i = document.getElementById("cmd"); if(!i.value.trim()) return;
|
| 172 |
-
ws.send(i.value); i.value = "";
|
| 173 |
}
|
| 174 |
|
| 175 |
-
//
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
lucide.createIcons();
|
| 204 |
-
} catch(e) { toast("Failed to load files", true); }
|
| 205 |
}
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
|
|
|
|
|
|
| 211 |
}
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
}
|
|
|
|
| 217 |
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
document.getElementById("pv-installed").className = `px-3 py-1 text-[10px] font-bold rounded transition-all ${v==='installed'?'bg-[#222] text-white':'text-zinc-500 hover:text-white'}`;
|
| 223 |
-
document.getElementById("search-box").style.display = v === 'browser' ? 'flex' : 'none';
|
| 224 |
-
if(v === 'browser') {
|
| 225 |
-
document.getElementById("pl-list").innerHTML = `<div class="col-span-full flex flex-col items-center justify-center text-zinc-600 h-64 gap-2"><i data-lucide="search" class="w-8 h-8 opacity-20"></i><span class="text-xs">Ready to search.</span></div>`;
|
| 226 |
-
lucide.createIcons();
|
| 227 |
-
} else loadInstalled();
|
| 228 |
}
|
| 229 |
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
</div>
|
| 261 |
-
<div class="mt-auto pt-2 border-t border-[#1a1a1a]">
|
| 262 |
-
<button onclick="resolveInstall('${p.project_id}', '${p.title.replace(/'/g, "")}')" id="btn-${p.project_id}" class="w-full bg-[#111] hover:bg-green-600 hover:text-black text-zinc-400 text-[10px] font-bold py-1.5 rounded transition-colors flex items-center justify-center gap-1">
|
| 263 |
-
<i data-lucide="download" class="w-3 h-3"></i> Install
|
| 264 |
-
</button>
|
| 265 |
-
</div>
|
| 266 |
-
`;
|
| 267 |
-
list.appendChild(card);
|
| 268 |
-
});
|
| 269 |
-
lucide.createIcons();
|
| 270 |
-
} catch(e) {
|
| 271 |
-
list.innerHTML = `<div class="col-span-full text-center text-xs text-red-400 py-8">Error connecting to Modrinth API.</div>`;
|
| 272 |
-
}
|
| 273 |
}
|
| 274 |
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
if(loaderRaw === 'waterfall') loaders = ['bungeecord', 'waterfall'];
|
| 290 |
-
|
| 291 |
-
try {
|
| 292 |
-
// Construct array string for API: '["paper", "spigot"]'
|
| 293 |
-
const lQuery = JSON.stringify(loaders);
|
| 294 |
-
const vQuery = JSON.stringify([version]);
|
| 295 |
-
|
| 296 |
-
const res = await fetch(`https://api.modrinth.com/v2/project/${id}/version?loaders=${lQuery}&game_versions=${vQuery}`);
|
| 297 |
-
const versions = await res.json();
|
| 298 |
-
|
| 299 |
-
if(!versions.length) {
|
| 300 |
-
toast(`No version found for ${loaderRaw} ${version}`, true);
|
| 301 |
-
btn.innerHTML = `<span class="text-red-400">Incompatible</span>`;
|
| 302 |
-
setTimeout(() => { btn.innerHTML = ogHtml; btn.disabled = false; }, 2000);
|
| 303 |
-
return;
|
| 304 |
-
}
|
| 305 |
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
toast(`Installed ${name}`);
|
| 320 |
-
btn.className = "w-full bg-green-600 text-black text-[10px] font-bold py-1.5 rounded flex items-center justify-center gap-1 cursor-default";
|
| 321 |
-
btn.innerHTML = `<i data-lucide="check" class="w-3 h-3"></i> Installed`;
|
| 322 |
-
lucide.createIcons();
|
| 323 |
-
} else {
|
| 324 |
-
throw new Error("Server error");
|
| 325 |
-
}
|
| 326 |
-
} catch(e) {
|
| 327 |
-
toast("Installation failed", true);
|
| 328 |
-
btn.innerHTML = `<span class="text-red-400">Error</span>`;
|
| 329 |
-
setTimeout(() => { btn.innerHTML = ogHtml; btn.disabled = false; }, 2000);
|
| 330 |
-
}
|
| 331 |
}
|
| 332 |
|
| 333 |
-
async function
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
}
|
|
|
|
| 367 |
}
|
| 368 |
-
</script>
|
|
|
|
|
|
|
| 369 |
"""
|
| 370 |
|
| 371 |
-
#
|
|
|
|
|
|
|
| 372 |
def get_path(p: str):
|
| 373 |
safe = os.path.abspath(os.path.join(BASE_DIR, (p or "").strip("/")))
|
| 374 |
if not safe.startswith(BASE_DIR): raise HTTPException(403, "Access Denied")
|
|
@@ -389,17 +694,45 @@ async def stream_output(pipe):
|
|
| 389 |
async def boot_mc():
|
| 390 |
global mc_process
|
| 391 |
jar = os.path.join(BASE_DIR, "purpur.jar")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
if not os.path.exists(jar):
|
| 393 |
-
output_history.append("\
|
| 394 |
return
|
| 395 |
-
|
| 396 |
-
|
| 397 |
mc_process = await asyncio.create_subprocess_exec(
|
| 398 |
-
"java", "-Xmx4G", "-Xms1G",
|
| 399 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
)
|
| 401 |
asyncio.create_task(stream_output(mc_process.stdout))
|
| 402 |
|
|
|
|
|
|
|
|
|
|
| 403 |
@app.get("/")
|
| 404 |
def index(): return HTMLResponse(HTML_CONTENT)
|
| 405 |
|
|
@@ -407,35 +740,37 @@ def index(): return HTMLResponse(HTML_CONTENT)
|
|
| 407 |
async def ws_end(ws: WebSocket):
|
| 408 |
await ws.accept()
|
| 409 |
connected_clients.add(ws)
|
| 410 |
-
for
|
| 411 |
try:
|
| 412 |
while True:
|
| 413 |
cmd = await ws.receive_text()
|
| 414 |
if mc_process and mc_process.stdin:
|
| 415 |
mc_process.stdin.write((cmd + "\n").encode())
|
| 416 |
await mc_process.stdin.drain()
|
| 417 |
-
except:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 418 |
|
| 419 |
-
# FS API
|
| 420 |
@app.get("/api/fs/list")
|
| 421 |
-
def list_fs(path: str=""):
|
| 422 |
t = get_path(path)
|
| 423 |
if not os.path.exists(t): return []
|
| 424 |
-
res = []
|
| 425 |
-
for x in os.listdir(t):
|
| 426 |
-
fp = os.path.join(t, x)
|
| 427 |
-
res.append({"name": x, "is_dir": os.path.isdir(fp)})
|
| 428 |
return sorted(res, key=lambda k: (not k["is_dir"], k["name"].lower()))
|
| 429 |
|
| 430 |
@app.post("/api/fs/upload")
|
| 431 |
-
async def upload(path: str=Form(""), file: UploadFile=File(...)):
|
| 432 |
t = get_path(path)
|
| 433 |
os.makedirs(t, exist_ok=True)
|
| 434 |
-
with open(os.path.join(t, file.filename), "wb") as f:
|
|
|
|
| 435 |
return "ok"
|
| 436 |
|
| 437 |
@app.post("/api/fs/delete")
|
| 438 |
-
def delete(path: str=Form(...)):
|
| 439 |
t = get_path(path)
|
| 440 |
if os.path.isdir(t): shutil.rmtree(t)
|
| 441 |
else: os.remove(t)
|
|
@@ -444,38 +779,41 @@ def delete(path: str=Form(...)):
|
|
| 444 |
@app.get("/api/fs/read")
|
| 445 |
def read(path: str):
|
| 446 |
try:
|
| 447 |
-
with open(get_path(path), "r", encoding="utf-8") as f:
|
| 448 |
-
|
|
|
|
|
|
|
| 449 |
|
| 450 |
-
# PLUGIN INSTALLER
|
| 451 |
@app.post("/api/plugins/install")
|
| 452 |
-
def install_pl(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 453 |
try:
|
| 454 |
-
# Download
|
| 455 |
dest = os.path.join(PLUGINS_DIR, filename)
|
| 456 |
-
req = urllib.request.Request(url, headers={
|
| 457 |
-
with urllib.request.urlopen(req) as r, open(dest,
|
| 458 |
shutil.copyfileobj(r, f)
|
| 459 |
-
|
| 460 |
-
# Update JSON Record
|
| 461 |
j_path = os.path.join(PLUGINS_DIR, "plugins.json")
|
| 462 |
data = {}
|
| 463 |
if os.path.exists(j_path):
|
| 464 |
try:
|
| 465 |
-
with open(j_path,
|
| 466 |
except: pass
|
| 467 |
-
|
| 468 |
data[project_id] = {
|
| 469 |
-
"name": name,
|
| 470 |
-
"
|
| 471 |
-
"version_id": version_id,
|
| 472 |
-
"installed_at": time.time()
|
| 473 |
}
|
| 474 |
-
|
| 475 |
-
with open(j_path, 'w') as f: json.dump(data, f, indent=2)
|
| 476 |
return "ok"
|
| 477 |
except Exception as e:
|
| 478 |
raise HTTPException(500, str(e))
|
| 479 |
|
| 480 |
if __name__ == "__main__":
|
| 481 |
-
uvicorn.run(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
BASE_DIR = os.environ.get("SERVER_DIR", os.path.abspath("/app"))
|
| 18 |
PLUGINS_DIR = os.path.join(BASE_DIR, "plugins")
|
| 19 |
mc_process = None
|
| 20 |
+
output_history = collections.deque(maxlen=500)
|
| 21 |
connected_clients = set()
|
| 22 |
|
| 23 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 24 |
+
# HTML GUI β macOS / Apple-style UI
|
| 25 |
+
# Raw string (r"""β¦""") so backslashes in JS
|
| 26 |
+
# regex patterns pass through unchanged.
|
| 27 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 28 |
+
HTML_CONTENT = r"""<!DOCTYPE html>
|
| 29 |
+
<html lang="en" data-theme="dark">
|
| 30 |
+
<head>
|
| 31 |
+
<meta charset="UTF-8">
|
| 32 |
+
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
| 33 |
+
<title>MC Panel</title>
|
| 34 |
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
| 35 |
<style>
|
| 36 |
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
| 37 |
+
:root{
|
| 38 |
+
--f:-apple-system,BlinkMacSystemFont,'SF Pro Display','SF Pro Text',system-ui,sans-serif;
|
| 39 |
+
--mono:'JetBrains Mono','SF Mono','Cascadia Code',monospace;
|
| 40 |
+
--r:12px;--r-sm:8px;--r-lg:16px;--dur:0.18s
|
| 41 |
+
}
|
| 42 |
+
[data-theme=dark]{
|
| 43 |
+
--bg:#000;--s1:#1C1C1E;--s2:#2C2C2E;--s3:#3A3A3C;
|
| 44 |
+
--bd:rgba(255,255,255,.08);
|
| 45 |
+
--t1:#fff;--t2:rgba(255,255,255,.55);--t3:rgba(255,255,255,.22);
|
| 46 |
+
--acc:#32D74B;--acc-bg:rgba(50,215,75,.12);
|
| 47 |
+
--red:#FF453A;--yel:#FFD60A;--blu:#0A84FF;
|
| 48 |
+
--glass:rgba(28,28,30,.8);
|
| 49 |
+
--sh:0 8px 40px rgba(0,0,0,.7),0 2px 8px rgba(0,0,0,.4);
|
| 50 |
+
--sh-sm:0 2px 12px rgba(0,0,0,.5)
|
| 51 |
+
}
|
| 52 |
+
[data-theme=light]{
|
| 53 |
+
--bg:#EBEBEB;--s1:#fff;--s2:#F5F5F7;--s3:#E5E5EA;
|
| 54 |
+
--bd:rgba(0,0,0,.09);
|
| 55 |
+
--t1:#1C1C1E;--t2:rgba(0,0,0,.5);--t3:rgba(0,0,0,.22);
|
| 56 |
+
--acc:#28CD41;--acc-bg:rgba(40,205,65,.1);
|
| 57 |
+
--red:#FF3B30;--yel:#FF9F0A;--blu:#007AFF;
|
| 58 |
+
--glass:rgba(255,255,255,.85);
|
| 59 |
+
--sh:0 8px 40px rgba(0,0,0,.12),0 2px 8px rgba(0,0,0,.06);
|
| 60 |
+
--sh-sm:0 2px 12px rgba(0,0,0,.1)
|
| 61 |
+
}
|
| 62 |
+
html,body{height:100%;height:100dvh}
|
| 63 |
+
body{font-family:var(--f);background:var(--bg);color:var(--t1);
|
| 64 |
+
display:flex;flex-direction:column;overflow:hidden;
|
| 65 |
+
-webkit-font-smoothing:antialiased;transition:background var(--dur),color var(--dur)}
|
| 66 |
+
::-webkit-scrollbar{width:5px;height:5px}
|
| 67 |
+
::-webkit-scrollbar-track{background:transparent}
|
| 68 |
+
::-webkit-scrollbar-thumb{background:var(--s3);border-radius:99px}
|
| 69 |
+
::-webkit-scrollbar-thumb:hover{background:var(--t3)}
|
| 70 |
+
|
| 71 |
+
/* TOOLBAR */
|
| 72 |
+
.toolbar{height:52px;min-height:52px;background:var(--glass);
|
| 73 |
+
backdrop-filter:blur(24px) saturate(180%);-webkit-backdrop-filter:blur(24px) saturate(180%);
|
| 74 |
+
border-bottom:1px solid var(--bd);display:flex;align-items:center;
|
| 75 |
+
padding:0 16px;gap:14px;position:relative;z-index:100;flex-shrink:0;user-select:none}
|
| 76 |
+
.tl-wrap{display:flex;gap:6px;align-items:center;flex-shrink:0}
|
| 77 |
+
.tl{width:12px;height:12px;border-radius:50%;transition:filter var(--dur);cursor:default}
|
| 78 |
+
.tl:hover{filter:brightness(1.25)}
|
| 79 |
+
.tl-r{background:var(--red)}.tl-y{background:var(--yel)}.tl-g{background:var(--acc)}
|
| 80 |
+
.tb-title{position:absolute;left:50%;transform:translateX(-50%);
|
| 81 |
+
font-size:13px;font-weight:600;letter-spacing:-.3px;color:var(--t1);
|
| 82 |
+
pointer-events:none;display:flex;align-items:center;gap:7px;white-space:nowrap}
|
| 83 |
+
.pip{width:7px;height:7px;border-radius:50%;background:var(--acc);
|
| 84 |
+
box-shadow:0 0 6px var(--acc);animation:pip 2.5s ease-in-out infinite}
|
| 85 |
+
@keyframes pip{0%,100%{opacity:1}50%{opacity:.3}}
|
| 86 |
+
.tb-actions{margin-left:auto;display:flex;align-items:center;gap:6px}
|
| 87 |
+
.icon-btn{width:30px;height:30px;border-radius:var(--r-sm);border:1px solid var(--bd);
|
| 88 |
+
background:var(--s1);color:var(--t2);display:flex;align-items:center;justify-content:center;
|
| 89 |
+
cursor:pointer;transition:all var(--dur)}
|
| 90 |
+
.icon-btn:hover{color:var(--t1);background:var(--s2)}
|
| 91 |
+
.icon-btn svg{width:15px;height:15px;pointer-events:none}
|
| 92 |
+
|
| 93 |
+
/* LAYOUT */
|
| 94 |
+
.app-body{flex:1;display:flex;overflow:hidden;min-height:0}
|
| 95 |
+
|
| 96 |
+
/* SIDEBAR */
|
| 97 |
+
.sidebar{width:192px;flex-shrink:0;background:var(--glass);
|
| 98 |
+
backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
|
| 99 |
+
border-right:1px solid var(--bd);display:flex;flex-direction:column;
|
| 100 |
+
padding:10px 8px 16px;gap:2px}
|
| 101 |
+
@media(max-width:640px){.sidebar{display:none}}
|
| 102 |
+
.sb-label{font-size:10px;font-weight:700;text-transform:uppercase;
|
| 103 |
+
letter-spacing:.6px;color:var(--t3);padding:10px 10px 4px}
|
| 104 |
+
.nav-item{display:flex;align-items:center;gap:9px;padding:8px 10px;
|
| 105 |
+
border-radius:var(--r-sm);font-size:13px;font-weight:500;color:var(--t2);
|
| 106 |
+
cursor:pointer;transition:all var(--dur);border:none;background:none;width:100%;text-align:left}
|
| 107 |
+
.nav-item:hover{background:var(--s2);color:var(--t1)}
|
| 108 |
+
.nav-item.active{background:var(--acc-bg);color:var(--acc)}
|
| 109 |
+
.nav-item svg{width:16px;height:16px;flex-shrink:0}
|
| 110 |
+
|
| 111 |
+
/* MAIN */
|
| 112 |
+
.main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-height:0}
|
| 113 |
+
.tab-pane{display:none;flex:1;flex-direction:column;overflow:hidden;padding:14px;min-height:0}
|
| 114 |
+
.tab-pane.active{display:flex;animation:fu var(--dur) ease-out}
|
| 115 |
+
@keyframes fu{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}
|
| 116 |
+
|
| 117 |
+
/* WINDOW */
|
| 118 |
+
.win{flex:1;display:flex;flex-direction:column;background:var(--s1);
|
| 119 |
+
border:1px solid var(--bd);border-radius:var(--r-lg);overflow:hidden;
|
| 120 |
+
box-shadow:var(--sh);min-height:0;transition:background var(--dur),border-color var(--dur);
|
| 121 |
+
position:relative}
|
| 122 |
+
.win-bar{height:40px;min-height:40px;background:var(--s2);border-bottom:1px solid var(--bd);
|
| 123 |
+
display:flex;align-items:center;padding:0 14px;gap:10px;flex-shrink:0}
|
| 124 |
+
.win-bar-title{flex:1;text-align:center;font-size:12px;font-weight:600;color:var(--t2)}
|
| 125 |
+
.live-dot{width:6px;height:6px;border-radius:50%;background:var(--acc);box-shadow:0 0 5px var(--acc)}
|
| 126 |
+
|
| 127 |
+
/* CONSOLE */
|
| 128 |
+
.log-out{flex:1;overflow-y:auto;padding:12px 14px;font-family:var(--mono);
|
| 129 |
+
font-size:11.5px;line-height:1.65;color:var(--t2);min-height:0}
|
| 130 |
+
.log-line{word-break:break-all;padding:.5px 0}
|
| 131 |
+
.cmd-bar{display:flex;align-items:center;gap:8px;padding:8px 10px;
|
| 132 |
+
background:var(--s2);border-top:1px solid var(--bd);flex-shrink:0}
|
| 133 |
+
.cmd-prompt{font-family:var(--mono);font-size:13px;color:var(--acc);flex-shrink:0}
|
| 134 |
+
.cmd-in{flex:1;background:var(--s1);border:1px solid var(--bd);border-radius:var(--r-sm);
|
| 135 |
+
padding:6px 12px;font-family:var(--mono);font-size:12px;color:var(--t1);
|
| 136 |
+
outline:none;transition:border-color var(--dur),box-shadow var(--dur)}
|
| 137 |
+
.cmd-in:focus{border-color:var(--acc);box-shadow:0 0 0 3px var(--acc-bg)}
|
| 138 |
+
.cmd-in::placeholder{color:var(--t3)}
|
| 139 |
+
.send-btn{width:32px;height:32px;flex-shrink:0;background:var(--acc);border:none;
|
| 140 |
+
border-radius:var(--r-sm);color:#fff;display:flex;align-items:center;justify-content:center;
|
| 141 |
+
cursor:pointer;transition:opacity var(--dur),transform var(--dur)}
|
| 142 |
+
.send-btn:hover{opacity:.85;transform:scale(.95)}
|
| 143 |
+
.send-btn svg{width:14px;height:14px}
|
| 144 |
+
|
| 145 |
+
/* FILES */
|
| 146 |
+
.fm-bar{height:44px;min-height:44px;background:var(--s2);border-bottom:1px solid var(--bd);
|
| 147 |
+
display:flex;align-items:center;padding:0 12px;gap:8px;flex-shrink:0}
|
| 148 |
+
.breadcrumb{flex:1;display:flex;align-items:center;gap:3px;font-size:12px;
|
| 149 |
+
overflow-x:auto;white-space:nowrap}
|
| 150 |
+
.breadcrumb button{background:none;border:none;color:var(--t2);cursor:pointer;
|
| 151 |
+
padding:3px 5px;border-radius:5px;font-size:12px;font-family:var(--f)}
|
| 152 |
+
.breadcrumb button:hover{color:var(--acc);background:var(--acc-bg)}
|
| 153 |
+
.breadcrumb .sep{color:var(--t3);font-size:10px;margin:0 1px}
|
| 154 |
+
.file-list{flex:1;overflow-y:auto}
|
| 155 |
+
.file-row{display:flex;align-items:center;gap:10px;padding:9px 16px;
|
| 156 |
+
border-bottom:1px solid var(--bd);cursor:pointer;transition:background var(--dur)}
|
| 157 |
+
.file-row:hover{background:var(--s2)}
|
| 158 |
+
.file-row:last-child{border-bottom:none}
|
| 159 |
+
.file-name{flex:1;font-size:13px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
| 160 |
+
.file-del{opacity:0;background:none;border:none;color:var(--red);cursor:pointer;padding:4px;
|
| 161 |
+
border-radius:6px;display:flex;align-items:center;transition:opacity var(--dur),background var(--dur)}
|
| 162 |
+
.file-row:hover .file-del{opacity:1}
|
| 163 |
+
.file-del:hover{background:rgba(255,59,48,.1)}
|
| 164 |
+
.drag-ov{position:absolute;inset:0;background:rgba(50,215,75,.06);
|
| 165 |
+
border:2px dashed var(--acc);border-radius:var(--r-lg);
|
| 166 |
+
display:none;align-items:center;justify-content:center;
|
| 167 |
+
font-size:15px;font-weight:600;color:var(--acc);z-index:50}
|
| 168 |
+
.drag-ov.on{display:flex}
|
| 169 |
+
|
| 170 |
+
/* PLUGINS */
|
| 171 |
+
.pl-hd{padding:12px 14px;background:var(--s2);border-bottom:1px solid var(--bd);
|
| 172 |
+
flex-shrink:0;display:flex;flex-direction:column;gap:10px}
|
| 173 |
+
.segment{display:inline-flex;background:var(--s3);border-radius:var(--r-sm);padding:2px}
|
| 174 |
+
.seg-btn{padding:5px 16px;border-radius:6px;border:none;background:none;
|
| 175 |
+
color:var(--t2);font-size:12px;font-weight:500;cursor:pointer;
|
| 176 |
+
transition:all var(--dur);font-family:var(--f)}
|
| 177 |
+
.seg-btn.active{background:var(--s1);color:var(--t1);box-shadow:var(--sh-sm)}
|
| 178 |
+
.pl-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
| 179 |
+
.sf-sel,.sf-ver{background:var(--s1);border:1px solid var(--bd);border-radius:var(--r-sm);
|
| 180 |
+
padding:6px 10px;font-size:12px;color:var(--t1);outline:none;
|
| 181 |
+
transition:border-color var(--dur);font-family:var(--f)}
|
| 182 |
+
.sf-sel:focus,.sf-ver:focus{border-color:var(--blu)}
|
| 183 |
+
.sf-ver{width:70px;text-align:center}
|
| 184 |
+
.srch-wrap{position:relative;flex:1;min-width:160px}
|
| 185 |
+
.srch-wrap svg{position:absolute;left:10px;top:50%;transform:translateY(-50%);
|
| 186 |
+
width:13px;height:13px;color:var(--t3);pointer-events:none}
|
| 187 |
+
.sf-srch{width:100%;background:var(--s1);border:1px solid var(--bd);border-radius:var(--r-sm);
|
| 188 |
+
padding:7px 10px 7px 32px;font-size:12px;color:var(--t1);outline:none;
|
| 189 |
+
transition:border-color var(--dur),box-shadow var(--dur);font-family:var(--f)}
|
| 190 |
+
.sf-srch:focus{border-color:var(--blu);box-shadow:0 0 0 3px rgba(10,132,255,.15)}
|
| 191 |
+
.sf-srch::placeholder{color:var(--t3)}
|
| 192 |
+
.srch-btn{padding:7px 14px;background:var(--blu);border:none;border-radius:var(--r-sm);
|
| 193 |
+
color:#fff;font-size:12px;font-weight:600;cursor:pointer;transition:opacity var(--dur);
|
| 194 |
+
flex-shrink:0;font-family:var(--f)}
|
| 195 |
+
.srch-btn:hover{opacity:.85}
|
| 196 |
+
.pl-grid{flex:1;overflow-y:auto;padding:14px;
|
| 197 |
+
display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:12px;align-content:start}
|
| 198 |
+
@media(max-width:640px){.pl-grid{grid-template-columns:1fr}}
|
| 199 |
+
.pl-card{background:var(--s2);border:1px solid var(--bd);border-radius:var(--r);padding:14px;
|
| 200 |
+
display:flex;flex-direction:column;gap:10px;transition:border-color var(--dur),box-shadow var(--dur)}
|
| 201 |
+
.pl-card:hover{border-color:var(--acc);box-shadow:0 0 0 1px var(--acc-bg)}
|
| 202 |
+
.pl-head{display:flex;gap:10px}
|
| 203 |
+
.pl-ico{width:36px;height:36px;border-radius:8px;flex-shrink:0;object-fit:cover;background:var(--s3)}
|
| 204 |
+
.pl-meta{flex:1;min-width:0}
|
| 205 |
+
.pl-name{font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
| 206 |
+
.pl-desc{font-size:11px;color:var(--t2);line-height:1.5;overflow:hidden;
|
| 207 |
+
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
|
| 208 |
+
.pl-dl{font-size:10px;color:var(--t3);background:var(--s3);padding:2px 6px;
|
| 209 |
+
border-radius:4px;white-space:nowrap;flex-shrink:0;align-self:flex-start}
|
| 210 |
+
.inst-btn{width:100%;padding:7px;border-radius:var(--r-sm);border:none;
|
| 211 |
+
background:var(--acc);color:#fff;font-size:12px;font-weight:600;cursor:pointer;
|
| 212 |
+
transition:opacity var(--dur);display:flex;align-items:center;justify-content:center;
|
| 213 |
+
gap:5px;font-family:var(--f)}
|
| 214 |
+
.inst-btn:hover{opacity:.85}.inst-btn:disabled{opacity:.4;cursor:not-allowed}
|
| 215 |
+
.pl-empty{grid-column:1/-1;display:flex;flex-direction:column;align-items:center;
|
| 216 |
+
justify-content:center;gap:10px;color:var(--t3);padding:60px 0}
|
| 217 |
+
.pl-empty svg{width:40px;height:40px;opacity:.25}
|
| 218 |
+
.pl-empty p{font-size:13px}
|
| 219 |
+
|
| 220 |
+
/* SPINNER */
|
| 221 |
+
.spin{width:16px;height:16px;border-radius:50%;border:2px solid var(--bd);
|
| 222 |
+
border-top-color:var(--acc);animation:sp .6s linear infinite;flex-shrink:0}
|
| 223 |
+
@keyframes sp{to{transform:rotate(360deg)}}
|
| 224 |
+
|
| 225 |
+
/* TOAST */
|
| 226 |
+
.toasts{position:fixed;bottom:72px;right:14px;z-index:9999;
|
| 227 |
+
display:flex;flex-direction:column;gap:8px;pointer-events:none}
|
| 228 |
+
@media(min-width:641px){.toasts{bottom:20px}}
|
| 229 |
+
.toast{background:var(--glass);backdrop-filter:blur(20px) saturate(180%);
|
| 230 |
+
-webkit-backdrop-filter:blur(20px) saturate(180%);
|
| 231 |
+
border:1px solid var(--bd);border-radius:var(--r);padding:10px 14px;
|
| 232 |
+
display:flex;align-items:center;gap:9px;font-size:13px;font-weight:500;
|
| 233 |
+
box-shadow:var(--sh);pointer-events:auto;
|
| 234 |
+
transform:translateY(10px);opacity:0;transition:transform .3s ease,opacity .3s ease}
|
| 235 |
+
.toast.show{transform:translateY(0);opacity:1}
|
| 236 |
+
|
| 237 |
+
/* MOBILE NAV */
|
| 238 |
+
.mob-nav{display:none;background:var(--glass);
|
| 239 |
+
backdrop-filter:blur(20px) saturate(180%);-webkit-backdrop-filter:blur(20px) saturate(180%);
|
| 240 |
+
border-top:1px solid var(--bd);padding-bottom:env(safe-area-inset-bottom,0px);flex-shrink:0}
|
| 241 |
+
@media(max-width:640px){.mob-nav{display:block}}
|
| 242 |
+
.mob-inner{display:flex}
|
| 243 |
+
.mob-btn{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;
|
| 244 |
+
gap:3px;padding:9px 4px;border:none;background:none;color:var(--t3);
|
| 245 |
+
font-size:10px;font-weight:500;cursor:pointer;transition:color var(--dur);font-family:var(--f)}
|
| 246 |
+
.mob-btn.active{color:var(--acc)}
|
| 247 |
+
.mob-btn svg{width:22px;height:22px}
|
| 248 |
+
</style>
|
| 249 |
+
</head>
|
| 250 |
<body>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
|
| 252 |
+
<!-- TOOLBAR -->
|
| 253 |
+
<header class="toolbar">
|
| 254 |
+
<div class="tl-wrap">
|
| 255 |
+
<div class="tl tl-r"></div>
|
| 256 |
+
<div class="tl tl-y"></div>
|
| 257 |
+
<div class="tl tl-g"></div>
|
| 258 |
+
</div>
|
| 259 |
+
<div class="tb-title">
|
| 260 |
+
<div class="pip" id="pip"></div>
|
| 261 |
+
<span>Minecraft Panel</span>
|
| 262 |
+
</div>
|
| 263 |
+
<div class="tb-actions">
|
| 264 |
+
<button class="icon-btn" onclick="toggleTheme()" id="theme-btn" title="Toggle theme">
|
| 265 |
+
<svg id="theme-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 266 |
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
| 267 |
+
</svg>
|
| 268 |
+
</button>
|
| 269 |
+
</div>
|
| 270 |
+
</header>
|
| 271 |
+
|
| 272 |
+
<div class="app-body">
|
| 273 |
+
<!-- SIDEBAR -->
|
| 274 |
+
<aside class="sidebar">
|
| 275 |
+
<div class="sb-label">Navigation</div>
|
| 276 |
+
<button class="nav-item active" id="d-console" onclick="tab('console')">
|
| 277 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/></svg>
|
| 278 |
+
Console
|
| 279 |
+
</button>
|
| 280 |
+
<button class="nav-item" id="d-files" onclick="tab('files')">
|
| 281 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
| 282 |
+
Files
|
| 283 |
+
</button>
|
| 284 |
+
<button class="nav-item" id="d-plugins" onclick="tab('plugins')">
|
| 285 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2H2v10l9.29 9.29c.94.94 2.48.94 3.42 0l6.58-6.58c.94-.94.94-2.48 0-3.42L12 2Z"/><path d="M7 7h.01"/></svg>
|
| 286 |
+
Plugins
|
| 287 |
+
</button>
|
| 288 |
+
</aside>
|
| 289 |
+
|
| 290 |
+
<main class="main">
|
| 291 |
+
<!-- CONSOLE -->
|
| 292 |
+
<div class="tab-pane active" id="tab-console">
|
| 293 |
+
<div class="win">
|
| 294 |
+
<div class="win-bar">
|
| 295 |
+
<div class="live-dot"></div>
|
| 296 |
+
<div class="win-bar-title">Live Console</div>
|
| 297 |
+
<button class="icon-btn" onclick="clearLog()" title="Clear log">
|
| 298 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4h6v2"/></svg>
|
| 299 |
+
</button>
|
| 300 |
</div>
|
| 301 |
+
<div class="log-out" id="logs"></div>
|
| 302 |
+
<div class="cmd-bar">
|
| 303 |
+
<span class="cmd-prompt">β</span>
|
| 304 |
+
<input class="cmd-in" id="cmd" type="text" placeholder="Type a commandβ¦" autocomplete="off"
|
| 305 |
+
onkeydown="if(event.key==='Enter')sendCmd()">
|
| 306 |
+
<button class="send-btn" onclick="sendCmd()" title="Send">
|
| 307 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
|
| 308 |
+
</button>
|
| 309 |
+
</div>
|
| 310 |
+
</div>
|
| 311 |
+
</div>
|
| 312 |
+
|
| 313 |
+
<!-- FILES -->
|
| 314 |
+
<div class="tab-pane" id="tab-files">
|
| 315 |
+
<div class="win" id="drop-zone">
|
| 316 |
+
<div class="drag-ov" id="drag-ov">Drop file to upload</div>
|
| 317 |
+
<div class="fm-bar">
|
| 318 |
+
<div class="breadcrumb" id="path-bread"></div>
|
| 319 |
+
<button class="icon-btn" onclick="document.getElementById('up-in').click()" title="Upload">
|
| 320 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"/></svg>
|
| 321 |
+
</button>
|
| 322 |
+
<button class="icon-btn" onclick="refreshFiles()" title="Refresh">
|
| 323 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
| 324 |
+
</button>
|
| 325 |
+
<input type="file" id="up-in" style="display:none" onchange="uploadFile()">
|
| 326 |
+
</div>
|
| 327 |
+
<div class="file-list" id="file-list"></div>
|
| 328 |
+
</div>
|
| 329 |
+
</div>
|
| 330 |
+
|
| 331 |
+
<!-- PLUGINS -->
|
| 332 |
+
<div class="tab-pane" id="tab-plugins">
|
| 333 |
+
<div class="win">
|
| 334 |
+
<div class="pl-hd">
|
| 335 |
+
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
| 336 |
+
<div class="segment">
|
| 337 |
+
<button class="seg-btn active" id="pv-browser" onclick="setPView('browser')">Browse</button>
|
| 338 |
+
<button class="seg-btn" id="pv-installed" onclick="setPView('installed')">Installed</button>
|
| 339 |
+
</div>
|
| 340 |
+
<div class="pl-row" id="pl-ctrl">
|
| 341 |
+
<select class="sf-sel" id="pl-loader">
|
| 342 |
+
<option value="paper">Paper / Spigot</option>
|
| 343 |
+
<option value="purpur">Purpur</option>
|
| 344 |
+
<option value="velocity">Velocity</option>
|
| 345 |
+
<option value="waterfall">Waterfall</option>
|
| 346 |
+
<option value="fabric">Fabric</option>
|
| 347 |
+
</select>
|
| 348 |
+
<input class="sf-ver" id="pl-ver" type="text" value="1.20.4" placeholder="1.x.x">
|
| 349 |
</div>
|
| 350 |
+
</div>
|
| 351 |
+
<div id="srch-row" style="display:flex;gap:8px">
|
| 352 |
+
<div class="srch-wrap">
|
| 353 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
| 354 |
+
<input class="sf-srch" id="pl-q" type="text" placeholder="Search Modrinth (e.g. LuckPerms)β¦"
|
| 355 |
+
onkeydown="if(event.key==='Enter')searchPlugins()">
|
| 356 |
+
</div>
|
| 357 |
+
<button class="srch-btn" onclick="searchPlugins()">Search</button>
|
| 358 |
+
</div>
|
| 359 |
+
</div>
|
| 360 |
+
<div class="pl-grid" id="pl-list">
|
| 361 |
+
<div class="pl-empty">
|
| 362 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
| 363 |
+
<p>Select loader & version, then search.</p>
|
| 364 |
+
</div>
|
| 365 |
</div>
|
| 366 |
+
</div>
|
| 367 |
+
</div>
|
| 368 |
+
</main>
|
| 369 |
</div>
|
| 370 |
|
| 371 |
<!-- MOBILE NAV -->
|
| 372 |
+
<nav class="mob-nav">
|
| 373 |
+
<div class="mob-inner">
|
| 374 |
+
<button class="mob-btn active" id="m-console" onclick="tab('console')">
|
| 375 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/></svg>
|
| 376 |
+
Console
|
| 377 |
+
</button>
|
| 378 |
+
<button class="mob-btn" id="m-files" onclick="tab('files')">
|
| 379 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
| 380 |
+
Files
|
| 381 |
+
</button>
|
| 382 |
+
<button class="mob-btn" id="m-plugins" onclick="tab('plugins')">
|
| 383 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2H2v10l9.29 9.29c.94.94 2.48.94 3.42 0l6.58-6.58c.94-.94.94-2.48 0-3.42L12 2Z"/><path d="M7 7h.01"/></svg>
|
| 384 |
+
Plugins
|
| 385 |
+
</button>
|
| 386 |
+
</div>
|
| 387 |
</nav>
|
| 388 |
|
| 389 |
+
<div class="toasts" id="toasts"></div>
|
|
|
|
| 390 |
|
| 391 |
<script>
|
| 392 |
+
// ββ THEME ββββββββββββββββββββββββββββββββββββββββββ
|
| 393 |
+
const html=document.documentElement;
|
| 394 |
+
const MOON=`<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>`;
|
| 395 |
+
const SUN=`<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>`;
|
| 396 |
+
function applyTheme(t){
|
| 397 |
+
html.dataset.theme=t;
|
| 398 |
+
localStorage.setItem('mc-theme',t);
|
| 399 |
+
document.querySelector('#theme-icon').innerHTML=t==='dark'?MOON:SUN;
|
| 400 |
+
}
|
| 401 |
+
function toggleTheme(){applyTheme(html.dataset.theme==='dark'?'light':'dark');}
|
| 402 |
+
(function(){
|
| 403 |
+
const s=localStorage.getItem('mc-theme');
|
| 404 |
+
if(s)applyTheme(s);
|
| 405 |
+
else if(window.matchMedia&&window.matchMedia('(prefers-color-scheme:light)').matches)applyTheme('light');
|
| 406 |
+
else applyTheme('dark');
|
| 407 |
+
})();
|
| 408 |
+
|
| 409 |
+
// ββ TOAST ββββββββββββββββββββββββββββββββββββββββββ
|
| 410 |
+
function toast(msg,err=false){
|
| 411 |
+
const c=document.getElementById('toasts');
|
| 412 |
+
const d=document.createElement('div');d.className='toast';
|
| 413 |
+
const col=err?'var(--red)':'var(--acc)';
|
| 414 |
+
const ic=err
|
| 415 |
+
?`<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="${col}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>`
|
| 416 |
+
:`<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="${col}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`;
|
| 417 |
+
d.innerHTML=ic+`<span>${msg}</span>`;
|
| 418 |
+
c.appendChild(d);
|
| 419 |
+
requestAnimationFrame(()=>requestAnimationFrame(()=>d.classList.add('show')));
|
| 420 |
+
setTimeout(()=>{d.classList.remove('show');setTimeout(()=>d.remove(),350);},3000);
|
| 421 |
}
|
| 422 |
|
| 423 |
+
// ββ TABS ββββββββββββββββββββββββββββββββββββββββββββ
|
| 424 |
+
let curTab='console';
|
| 425 |
+
function tab(id){
|
| 426 |
+
curTab=id;
|
| 427 |
+
document.querySelectorAll('.tab-pane').forEach(e=>e.classList.remove('active'));
|
| 428 |
+
document.querySelectorAll('.nav-item,.mob-btn').forEach(e=>e.classList.remove('active'));
|
| 429 |
+
document.getElementById('tab-'+id).classList.add('active');
|
| 430 |
+
['d-','m-'].forEach(p=>{const el=document.getElementById(p+id);if(el)el.classList.add('active');});
|
| 431 |
+
if(id==='files'&&!curPath)refreshFiles();
|
| 432 |
+
if(id==='plugins'&&curView==='installed')loadInstalled();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
}
|
| 434 |
|
| 435 |
+
// ββ CONSOLE βββββββββββββββββββββββββββββββββββββββββ
|
| 436 |
+
const logs=document.getElementById('logs');
|
| 437 |
+
let ws,wsRetries=0;
|
| 438 |
+
|
| 439 |
+
function ansiToHtml(raw){
|
| 440 |
+
let s=raw.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
| 441 |
+
// \x1b is the ESC character in JS β matches actual ANSI escape sequences
|
| 442 |
+
s=s.replace(/\x1b\[(\d+(?:;\d+)*)m/g,(_,codes)=>{
|
| 443 |
+
let o='';
|
| 444 |
+
for(const n of codes.split(';').map(Number)){
|
| 445 |
+
if(n===0)o+='</span>';
|
| 446 |
+
else if(n===1)o+='<span style="font-weight:700">';
|
| 447 |
+
else if(n===2)o+='<span style="opacity:.55">';
|
| 448 |
+
else if(n===3)o+='<span style="font-style:italic">';
|
| 449 |
+
else if(n===30)o+='<span style="color:#555">';
|
| 450 |
+
else if(n===31)o+='<span style="color:#FF6B6B">';
|
| 451 |
+
else if(n===32)o+='<span style="color:#51CF66">';
|
| 452 |
+
else if(n===33)o+='<span style="color:#FFD43B">';
|
| 453 |
+
else if(n===34)o+='<span style="color:#74C0FC">';
|
| 454 |
+
else if(n===35)o+='<span style="color:#CC5DE8">';
|
| 455 |
+
else if(n===36)o+='<span style="color:#22D3EE">';
|
| 456 |
+
else if(n===37)o+='<span style="color:#F8F9FA">';
|
| 457 |
+
else if(n===90)o+='<span style="color:#666">';
|
| 458 |
+
}
|
| 459 |
+
return o;
|
| 460 |
+
});
|
| 461 |
+
s=s.replace(/\x1b\[[^m]*m/g,''); // strip unknown sequences
|
| 462 |
+
return s;
|
|
|
|
|
|
|
| 463 |
}
|
| 464 |
+
|
| 465 |
+
function appendLog(text){
|
| 466 |
+
const l=document.createElement('div');l.className='log-line';
|
| 467 |
+
l.innerHTML=ansiToHtml(text);
|
| 468 |
+
logs.appendChild(l);
|
| 469 |
+
if(logs.children.length>500)logs.removeChild(logs.firstChild);
|
| 470 |
+
if(logs.scrollHeight-logs.scrollTop<logs.clientHeight+80)logs.scrollTop=logs.scrollHeight;
|
| 471 |
}
|
| 472 |
+
function clearLog(){logs.innerHTML='';}
|
| 473 |
+
|
| 474 |
+
function connectWS(){
|
| 475 |
+
const proto=location.protocol==='https:'?'wss:':'ws:';
|
| 476 |
+
ws=new WebSocket(`${proto}//${location.host}/ws`);
|
| 477 |
+
ws.onopen=()=>{
|
| 478 |
+
wsRetries=0;
|
| 479 |
+
document.getElementById('pip').style.background='var(--acc)';
|
| 480 |
+
document.getElementById('pip').style.boxShadow='0 0 6px var(--acc)';
|
| 481 |
+
};
|
| 482 |
+
ws.onmessage=e=>appendLog(e.data);
|
| 483 |
+
ws.onclose=()=>{
|
| 484 |
+
document.getElementById('pip').style.background='var(--red)';
|
| 485 |
+
document.getElementById('pip').style.boxShadow='none';
|
| 486 |
+
const delay=Math.min(1000*(2**wsRetries),30000);wsRetries++;
|
| 487 |
+
appendLog(`\x1b[33m[Panel] Disconnected β reconnecting in ${Math.round(delay/1000)}s\x1b[0m`);
|
| 488 |
+
setTimeout(connectWS,delay);
|
| 489 |
+
};
|
| 490 |
+
ws.onerror=()=>ws.close();
|
| 491 |
}
|
| 492 |
+
connectWS();
|
| 493 |
|
| 494 |
+
function sendCmd(){
|
| 495 |
+
const i=document.getElementById('cmd'),v=i.value.trim();if(!v)return;
|
| 496 |
+
if(ws&&ws.readyState===WebSocket.OPEN){ws.send(v);i.value='';}
|
| 497 |
+
else toast('Not connected to server',true);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
}
|
| 499 |
|
| 500 |
+
// ββ FILES βββββββββββββββββββββββββββββββββββββββββββ
|
| 501 |
+
let curPath='';
|
| 502 |
+
function buildBread(p){
|
| 503 |
+
const el=document.getElementById('path-bread');
|
| 504 |
+
const parts=p.split('/').filter(Boolean);
|
| 505 |
+
let h=`<button onclick="refreshFiles('')"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg></button>`;
|
| 506 |
+
parts.forEach((x,i,a)=>{
|
| 507 |
+
const p2=a.slice(0,i+1).join('/');
|
| 508 |
+
h+=`<span class="sep">βΊ</span><button onclick="refreshFiles('${p2}')">${x}</button>`;
|
| 509 |
+
});
|
| 510 |
+
el.innerHTML=h;
|
| 511 |
+
}
|
| 512 |
+
async function refreshFiles(p=curPath){
|
| 513 |
+
curPath=p;buildBread(p);
|
| 514 |
+
const l=document.getElementById('file-list');
|
| 515 |
+
l.innerHTML=`<div style="display:flex;justify-content:center;padding:32px"><div class="spin"></div></div>`;
|
| 516 |
+
try{
|
| 517 |
+
const r=await fetch(`/api/fs/list?path=${encodeURIComponent(p)}`);
|
| 518 |
+
const d=await r.json();l.innerHTML='';
|
| 519 |
+
if(p)d.unshift({name:'..',is_dir:true,parent:true});
|
| 520 |
+
if(!d.length){l.innerHTML=`<div style="padding:48px;text-align:center;font-size:13px;color:var(--t3)">Empty directory</div>`;return;}
|
| 521 |
+
d.forEach(f=>{
|
| 522 |
+
const row=document.createElement('div');row.className='file-row';
|
| 523 |
+
const fp=(p?p+'/':'')+f.name;
|
| 524 |
+
if(f.parent){
|
| 525 |
+
row.onclick=()=>refreshFiles(p.split('/').slice(0,-1).join('/'));
|
| 526 |
+
row.innerHTML=`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--t3)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg><span class="file-name" style="color:var(--t2)">Back</span>`;
|
| 527 |
+
}else{
|
| 528 |
+
row.onclick=()=>f.is_dir?refreshFiles(fp):null;
|
| 529 |
+
const ico=f.is_dir
|
| 530 |
+
?`<svg width="16" height="16" viewBox="0 0 24 24" fill="var(--acc)" stroke="none" opacity=".85"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`
|
| 531 |
+
:`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--t3)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>`;
|
| 532 |
+
row.innerHTML=`${ico}<span class="file-name">${f.name}</span>
|
| 533 |
+
<button class="file-del" onclick="event.stopPropagation();delFile('${fp}')" title="Delete">
|
| 534 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/></svg>
|
| 535 |
+
</button>`;
|
| 536 |
+
}
|
| 537 |
+
l.appendChild(row);
|
| 538 |
+
});
|
| 539 |
+
}catch(e){toast('Failed to load files',true);}
|
| 540 |
+
}
|
| 541 |
|
| 542 |
+
async function uploadFile(){
|
| 543 |
+
const f=document.getElementById('up-in').files[0];if(!f)return;
|
| 544 |
+
const fd=new FormData();fd.append('path',curPath);fd.append('file',f);
|
| 545 |
+
toast('Uploadingβ¦');
|
| 546 |
+
const r=await fetch('/api/fs/upload',{method:'POST',body:fd});
|
| 547 |
+
r.ok?(toast('Uploaded β'),refreshFiles()):toast('Upload failed',true);
|
| 548 |
+
document.getElementById('up-in').value='';
|
| 549 |
+
}
|
| 550 |
+
async function delFile(p){
|
| 551 |
+
if(!confirm('Delete '+p+'?'))return;
|
| 552 |
+
const fd=new FormData();fd.append('path',p);
|
| 553 |
+
const r=await fetch('/api/fs/delete',{method:'POST',body:fd});
|
| 554 |
+
r.ok?(toast('Deleted'),refreshFiles()):toast('Delete failed',true);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 555 |
}
|
| 556 |
|
| 557 |
+
// Drag & drop
|
| 558 |
+
const dz=document.getElementById('drop-zone'),dov=document.getElementById('drag-ov');
|
| 559 |
+
if(dz){
|
| 560 |
+
dz.addEventListener('dragover',e=>{e.preventDefault();dov.classList.add('on');});
|
| 561 |
+
dz.addEventListener('dragleave',()=>dov.classList.remove('on'));
|
| 562 |
+
dz.addEventListener('drop',async e=>{
|
| 563 |
+
e.preventDefault();dov.classList.remove('on');
|
| 564 |
+
const file=e.dataTransfer.files[0];if(!file)return;
|
| 565 |
+
const fd=new FormData();fd.append('path',curPath);fd.append('file',file);
|
| 566 |
+
toast('Uploadingβ¦');
|
| 567 |
+
const r=await fetch('/api/fs/upload',{method:'POST',body:fd});
|
| 568 |
+
r.ok?(toast('Uploaded β'),refreshFiles()):toast('Upload failed',true);
|
| 569 |
+
});
|
| 570 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 571 |
|
| 572 |
+
// ββ PLUGINS βββββββββββββββββββββββββββββββββββββββββ
|
| 573 |
+
let curView='browser';
|
| 574 |
+
function setPView(v){
|
| 575 |
+
curView=v;
|
| 576 |
+
document.getElementById('pv-browser').className='seg-btn'+(v==='browser'?' active':'');
|
| 577 |
+
document.getElementById('pv-installed').className='seg-btn'+(v==='installed'?' active':'');
|
| 578 |
+
document.getElementById('srch-row').style.display=v==='browser'?'flex':'none';
|
| 579 |
+
document.getElementById('pl-ctrl').style.display=v==='browser'?'flex':'none';
|
| 580 |
+
if(v==='browser'){
|
| 581 |
+
document.getElementById('pl-list').innerHTML=`<div class="pl-empty">
|
| 582 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
| 583 |
+
<p>Ready to search.</p></div>`;
|
| 584 |
+
}else loadInstalled();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 585 |
}
|
| 586 |
|
| 587 |
+
async function searchPlugins(){
|
| 588 |
+
const q=document.getElementById('pl-q').value.trim();if(!q)return;
|
| 589 |
+
const list=document.getElementById('pl-list');
|
| 590 |
+
list.innerHTML=`<div style="grid-column:1/-1;display:flex;justify-content:center;padding:40px"><div class="spin"></div></div>`;
|
| 591 |
+
try{
|
| 592 |
+
const res=await fetch(`https://api.modrinth.com/v2/search?query=${encodeURIComponent(q)}&facets=[["project_type:plugin"]]&limit=20`);
|
| 593 |
+
const data=await res.json();list.innerHTML='';
|
| 594 |
+
if(!data.hits.length){list.innerHTML=`<div class="pl-empty"><p>No results on Modrinth.</p></div>`;return;}
|
| 595 |
+
data.hits.forEach(p=>{
|
| 596 |
+
const card=document.createElement('div');card.className='pl-card';
|
| 597 |
+
card.innerHTML=`
|
| 598 |
+
<div class="pl-head">
|
| 599 |
+
<img class="pl-ico" src="${p.icon_url||''}" onerror="this.src='https://placehold.co/36x36/3A3A3C/888?text=?'" alt="">
|
| 600 |
+
<div class="pl-meta">
|
| 601 |
+
<div style="display:flex;justify-content:space-between;gap:4px;align-items:flex-start">
|
| 602 |
+
<div class="pl-name" title="${p.title}">${p.title}</div>
|
| 603 |
+
<div class="pl-dl">${p.downloads.toLocaleString()} dl</div>
|
| 604 |
+
</div>
|
| 605 |
+
<div class="pl-desc">${p.description}</div>
|
| 606 |
+
</div>
|
| 607 |
+
</div>
|
| 608 |
+
<button class="inst-btn" id="btn-${p.project_id}"
|
| 609 |
+
onclick="resolveInstall('${p.project_id}','${p.title.replace(/'/g,'')}')">
|
| 610 |
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
| 611 |
+
Install
|
| 612 |
+
</button>`;
|
| 613 |
+
list.appendChild(card);
|
| 614 |
+
});
|
| 615 |
+
}catch(e){list.innerHTML=`<div class="pl-empty" style="color:var(--red)"><p>Error connecting to Modrinth.</p></div>`;}
|
| 616 |
+
}
|
| 617 |
|
| 618 |
+
async function resolveInstall(id,name){
|
| 619 |
+
const loaderRaw=document.getElementById('pl-loader').value;
|
| 620 |
+
const version=document.getElementById('pl-ver').value.trim();
|
| 621 |
+
const btn=document.getElementById('btn-'+id);
|
| 622 |
+
const ogHtml=btn.innerHTML;
|
| 623 |
+
btn.innerHTML=`<div class="spin" style="border-top-color:#fff;width:13px;height:13px"></div> Checkingβ¦`;
|
| 624 |
+
btn.disabled=true;
|
| 625 |
+
let loaders=[loaderRaw];
|
| 626 |
+
if(loaderRaw==='purpur')loaders=['paper','spigot','purpur'];
|
| 627 |
+
if(loaderRaw==='paper')loaders=['paper','spigot'];
|
| 628 |
+
if(loaderRaw==='waterfall')loaders=['bungeecord','waterfall'];
|
| 629 |
+
try{
|
| 630 |
+
const res=await fetch(`https://api.modrinth.com/v2/project/${id}/version?loaders=${JSON.stringify(loaders)}&game_versions=${JSON.stringify([version])}`);
|
| 631 |
+
const versions=await res.json();
|
| 632 |
+
if(!versions.length){toast(`No version for ${loaderRaw} ${version}`,true);setTimeout(()=>{btn.innerHTML=ogHtml;btn.disabled=false;},2000);return;}
|
| 633 |
+
const file=versions[0].files.find(f=>f.primary)||versions[0].files[0];
|
| 634 |
+
btn.innerHTML=`Downloadingβ¦`;
|
| 635 |
+
const fd=new FormData();
|
| 636 |
+
fd.append('url',file.url);fd.append('filename',file.filename);
|
| 637 |
+
fd.append('project_id',id);fd.append('version_id',versions[0].id);fd.append('name',name);
|
| 638 |
+
const dl=await fetch('/api/plugins/install',{method:'POST',body:fd});
|
| 639 |
+
if(dl.ok){
|
| 640 |
+
toast(`Installed ${name} β`);
|
| 641 |
+
btn.style.cssText='background:var(--s3);color:var(--acc)';
|
| 642 |
+
btn.innerHTML=`<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg> Installed`;
|
| 643 |
+
}else throw new Error();
|
| 644 |
+
}catch(e){toast('Installation failed',true);setTimeout(()=>{btn.innerHTML=ogHtml;btn.disabled=false;},2000);}
|
| 645 |
+
}
|
| 646 |
+
|
| 647 |
+
async function loadInstalled(){
|
| 648 |
+
const l=document.getElementById('pl-list');
|
| 649 |
+
l.innerHTML=`<div style="grid-column:1/-1;display:flex;justify-content:center;padding:40px"><div class="spin"></div></div>`;
|
| 650 |
+
try{
|
| 651 |
+
const r=await fetch('/api/fs/read?path=plugins/plugins.json');if(!r.ok)throw new Error();
|
| 652 |
+
const data=await r.json();l.innerHTML='';
|
| 653 |
+
if(!Object.keys(data).length){l.innerHTML=`<div class="pl-empty"><p>No plugins installed via Panel.</p></div>`;return;}
|
| 654 |
+
for(const[pid,d]of Object.entries(data)){
|
| 655 |
+
const card=document.createElement('div');card.className='pl-card';
|
| 656 |
+
card.innerHTML=`
|
| 657 |
+
<div style="display:flex;justify-content:space-between;align-items:flex-start">
|
| 658 |
+
<div class="pl-name">${d.name}</div>
|
| 659 |
+
<button onclick="delFile('plugins/${d.filename}')" style="background:none;border:none;color:var(--t3);cursor:pointer;padding:2px;border-radius:4px" title="Remove">
|
| 660 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/></svg>
|
| 661 |
+
</button>
|
| 662 |
+
</div>
|
| 663 |
+
<div style="font-family:var(--mono);font-size:10px;color:var(--t3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${d.filename}</div>
|
| 664 |
+
<div style="background:var(--s3);color:var(--t2);font-size:11px;padding:5px 10px;border-radius:var(--r-sm);text-align:center">Installed</div>`;
|
| 665 |
+
l.appendChild(card);
|
| 666 |
}
|
| 667 |
+
}catch(e){l.innerHTML=`<div class="pl-empty"><p>No plugins.json record found.</p></div>`;}
|
| 668 |
}
|
| 669 |
+
</script>
|
| 670 |
+
</body>
|
| 671 |
+
</html>
|
| 672 |
"""
|
| 673 |
|
| 674 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 675 |
+
# BACKEND
|
| 676 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 677 |
def get_path(p: str):
|
| 678 |
safe = os.path.abspath(os.path.join(BASE_DIR, (p or "").strip("/")))
|
| 679 |
if not safe.startswith(BASE_DIR): raise HTTPException(403, "Access Denied")
|
|
|
|
| 694 |
async def boot_mc():
|
| 695 |
global mc_process
|
| 696 |
jar = os.path.join(BASE_DIR, "purpur.jar")
|
| 697 |
+
|
| 698 |
+
# ββ Wait for background world download (started by start.sh) ββββββββββ
|
| 699 |
+
# start.sh runs: (python3 download_world.py; touch /tmp/world_dl_done) &
|
| 700 |
+
# Without this wait, Minecraft would start before the world is copied in.
|
| 701 |
+
if os.environ.get("FOLDER_URL"):
|
| 702 |
+
output_history.append("\u23f3 [Panel] World download is running in background, waiting\u2026")
|
| 703 |
+
for i in range(600): # up to 10 min
|
| 704 |
+
if os.path.exists("/tmp/world_dl_done"):
|
| 705 |
+
output_history.append("\u2705 [Panel] World download finished! Starting Minecraft\u2026")
|
| 706 |
+
break
|
| 707 |
+
if i > 0 and i % 30 == 0:
|
| 708 |
+
output_history.append(f"\u23f3 [Panel] Still waiting\u2026 ({i}s elapsed)")
|
| 709 |
+
await asyncio.sleep(1)
|
| 710 |
+
else:
|
| 711 |
+
output_history.append("\u26a0 [Panel] Download wait timed out. Starting Minecraft anyway\u2026")
|
| 712 |
+
else:
|
| 713 |
+
open("/tmp/world_dl_done", "w").close() # mark done immediately
|
| 714 |
+
|
| 715 |
if not os.path.exists(jar):
|
| 716 |
+
output_history.append("\u26a0 [Panel] purpur.jar not found in /app \u2014 upload it via the Files tab.")
|
| 717 |
return
|
| 718 |
+
|
| 719 |
+
output_history.append("\U0001f680 [Panel] Starting Minecraft server\u2026")
|
| 720 |
mc_process = await asyncio.create_subprocess_exec(
|
| 721 |
+
"java", "-Xmx4G", "-Xms1G",
|
| 722 |
+
"-Dfile.encoding=UTF-8",
|
| 723 |
+
"-XX:+UseG1GC", "-XX:+ParallelRefProcEnabled",
|
| 724 |
+
"-XX:MaxGCPauseMillis=200",
|
| 725 |
+
"-jar", jar, "--nogui",
|
| 726 |
+
stdin=asyncio.subprocess.PIPE,
|
| 727 |
+
stdout=asyncio.subprocess.PIPE,
|
| 728 |
+
stderr=asyncio.subprocess.STDOUT,
|
| 729 |
+
cwd=BASE_DIR
|
| 730 |
)
|
| 731 |
asyncio.create_task(stream_output(mc_process.stdout))
|
| 732 |
|
| 733 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 734 |
+
# ROUTES
|
| 735 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 736 |
@app.get("/")
|
| 737 |
def index(): return HTMLResponse(HTML_CONTENT)
|
| 738 |
|
|
|
|
| 740 |
async def ws_end(ws: WebSocket):
|
| 741 |
await ws.accept()
|
| 742 |
connected_clients.add(ws)
|
| 743 |
+
for line in output_history: await ws.send_text(line)
|
| 744 |
try:
|
| 745 |
while True:
|
| 746 |
cmd = await ws.receive_text()
|
| 747 |
if mc_process and mc_process.stdin:
|
| 748 |
mc_process.stdin.write((cmd + "\n").encode())
|
| 749 |
await mc_process.stdin.drain()
|
| 750 |
+
except:
|
| 751 |
+
connected_clients.discard(ws)
|
| 752 |
+
|
| 753 |
+
@app.get("/api/status")
|
| 754 |
+
def api_status():
|
| 755 |
+
return {"running": mc_process is not None and mc_process.returncode is None}
|
| 756 |
|
|
|
|
| 757 |
@app.get("/api/fs/list")
|
| 758 |
+
def list_fs(path: str = ""):
|
| 759 |
t = get_path(path)
|
| 760 |
if not os.path.exists(t): return []
|
| 761 |
+
res = [{"name": x, "is_dir": os.path.isdir(os.path.join(t, x))} for x in os.listdir(t)]
|
|
|
|
|
|
|
|
|
|
| 762 |
return sorted(res, key=lambda k: (not k["is_dir"], k["name"].lower()))
|
| 763 |
|
| 764 |
@app.post("/api/fs/upload")
|
| 765 |
+
async def upload(path: str = Form(""), file: UploadFile = File(...)):
|
| 766 |
t = get_path(path)
|
| 767 |
os.makedirs(t, exist_ok=True)
|
| 768 |
+
with open(os.path.join(t, file.filename), "wb") as f:
|
| 769 |
+
shutil.copyfileobj(file.file, f)
|
| 770 |
return "ok"
|
| 771 |
|
| 772 |
@app.post("/api/fs/delete")
|
| 773 |
+
def delete(path: str = Form(...)):
|
| 774 |
t = get_path(path)
|
| 775 |
if os.path.isdir(t): shutil.rmtree(t)
|
| 776 |
else: os.remove(t)
|
|
|
|
| 779 |
@app.get("/api/fs/read")
|
| 780 |
def read(path: str):
|
| 781 |
try:
|
| 782 |
+
with open(get_path(path), "r", encoding="utf-8") as f:
|
| 783 |
+
return json.load(f) if path.endswith(".json") else Response(f.read())
|
| 784 |
+
except:
|
| 785 |
+
raise HTTPException(404)
|
| 786 |
|
|
|
|
| 787 |
@app.post("/api/plugins/install")
|
| 788 |
+
def install_pl(
|
| 789 |
+
url: str = Form(...), filename: str = Form(...),
|
| 790 |
+
project_id: str = Form(...), version_id: str = Form(...),
|
| 791 |
+
name: str = Form(...)
|
| 792 |
+
):
|
| 793 |
try:
|
|
|
|
| 794 |
dest = os.path.join(PLUGINS_DIR, filename)
|
| 795 |
+
req = urllib.request.Request(url, headers={"User-Agent": "HF-Panel/1.0"})
|
| 796 |
+
with urllib.request.urlopen(req) as r, open(dest, "wb") as f:
|
| 797 |
shutil.copyfileobj(r, f)
|
|
|
|
|
|
|
| 798 |
j_path = os.path.join(PLUGINS_DIR, "plugins.json")
|
| 799 |
data = {}
|
| 800 |
if os.path.exists(j_path):
|
| 801 |
try:
|
| 802 |
+
with open(j_path, "r") as f: data = json.load(f)
|
| 803 |
except: pass
|
|
|
|
| 804 |
data[project_id] = {
|
| 805 |
+
"name": name, "filename": filename,
|
| 806 |
+
"version_id": version_id, "installed_at": time.time()
|
|
|
|
|
|
|
| 807 |
}
|
| 808 |
+
with open(j_path, "w") as f: json.dump(data, f, indent=2)
|
|
|
|
| 809 |
return "ok"
|
| 810 |
except Exception as e:
|
| 811 |
raise HTTPException(500, str(e))
|
| 812 |
|
| 813 |
if __name__ == "__main__":
|
| 814 |
+
uvicorn.run(
|
| 815 |
+
app,
|
| 816 |
+
host="0.0.0.0",
|
| 817 |
+
port=int(os.environ.get("PORT", 7860)),
|
| 818 |
+
log_level="error"
|
| 819 |
+
)
|