release website style for panel

This commit is contained in:
MCHost
2025-07-06 21:11:43 -04:00
parent efea81e9bb
commit 452a27ece3
7 changed files with 3943 additions and 1256 deletions

1527
public/css/main-site.css Normal file

File diff suppressed because it is too large Load Diff

552
public/css/style.css Normal file
View File

@@ -0,0 +1,552 @@
@import "tailwindcss";
@font-face {
font-family: 'Minecraft';
src: url('../font/MinecraftRegular-Bmg3.otf') format('opentype');
font-weight: normal;
font-style: normal;
font-display: swap;
}
@layer base {
html {
@apply h-full;
margin: 0;
padding: 0;
box-sizing: border-box;
overflow-x: hidden;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
@apply min-h-full flex flex-col bg-gray-900 text-white;
font-family: 'Roboto', 'Verdana', sans-serif;
overflow-x: hidden;
}
h1,
h2,
h3,
h4,
h5,
h6 {
@apply font-[Minecraft] text-white;
letter-spacing: 0.5px;
}
.xterm {
background-color: #111426 !important;
}
#app {
@apply flex-grow;
}
main {
@apply flex-grow container mx-auto px-4 sm:px-6 lg:px-8 py-6;
}
footer {
@apply bg-gray-800 py-4 text-center;
}
canvas {
@apply w-full max-w-[150px] h-auto;
aspect-ratio: 1/1;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
@apply bg-gray-800;
}
::-webkit-scrollbar-thumb {
@apply bg-blue-600 rounded;
}
::-webkit-scrollbar-thumb:hover {
@apply bg-blue-700;
}
}
@layer components {
.section-bg {
@apply bg-gray-800 p-6 rounded-lg shadow-lg mb-6 w-full max-w-full;
overflow-x: hidden;
}
.modal {
@apply fixed inset-0 bg-black/75 flex items-center justify-center z-[1000];
}
.modal-content {
@apply bg-gray-800 p-6 rounded-lg w-full max-w-xl relative max-h-[80vh] overflow-y-auto;
scrollbar-width: thin;
scrollbar-color: #4B5563 #1F2937;
}
@media (max-width: 640px) {
.holesail-output-mobile-hidden {
display: none;
}
}
.modal-content::-webkit-scrollbar {
@apply w-2;
}
.modal-content::-webkit-scrollbar-track {
@apply bg-gray-800;
}
.modal-content::-webkit-scrollbar-thumb {
@apply bg-gray-600 rounded;
}
.modal-content::-webkit-scrollbar-thumb:hover {
@apply bg-gray-500;
}
.modal-close {
@apply absolute top-2 right-2 bg-transparent border-none text-white text-xl cursor-pointer;
}
.control-btn {
@apply text-white font-medium px-4 py-2 rounded transition duration-200;
}
.spinner {
@apply border-4 border-gray-700 border-t-blue-600 rounded-full w-6 h-6 animate-spin inline-block;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#notificationContainer {
@apply fixed bottom-4 right-4 z-[1000] flex flex-col-reverse gap-2;
}
.notification {
@apply bg-gray-800 p-3 rounded-lg text-white flex items-center gap-2 shadow-lg;
max-width: 90%;
font-size: 0.875rem;
}
.notification.success {
@apply bg-green-600;
}
.notification.error {
@apply bg-red-600;
}
#dockerLogsTerminal {
@apply bg-gray-900 p-4 rounded-lg w-full max-w-full box-border;
height: 250px;
overflow-y: hidden;
overflow-x: hidden;
}
.xterm .xterm-viewport {
@apply overflow-y-auto;
height: 250px;
scrollbar-width: thin;
scrollbar-color: #3b82f6 #1f2937;
}
.xterm .xterm-viewport::-webkit-scrollbar {
width: 8px;
}
.xterm .xterm-viewport::-webkit-scrollbar-track {
@apply bg-gray-800;
}
.xterm .xterm-viewport::-webkit-scrollbar-thumb {
@apply bg-blue-600 rounded;
}
.xterm .xterm-viewport::-webkit-scrollbar-thumb:hover {
@apply bg-blue-700;
}
.xterm .xterm-screen {
@apply w-full;
}
.item-entry {
@apply flex items-center gap-2 mb-2 w-full flex-wrap;
}
.item-entry input {
@apply bg-gray-700 p-2 rounded text-white w-full sm:w-auto;
}
.item-entry .item-amount {
@apply w-16 sm:w-12;
}
[role="switch"] {
@apply inline-block relative w-10 h-6 cursor-pointer;
}
[role="switch"]>div:first-child {
@apply block w-full h-full rounded-full bg-gray-700 transition-colors duration-200;
}
[role="switch"]>div:last-child {
@apply absolute w-4 h-4 rounded-full bg-white top-1 transition-all duration-200;
}
[role="switch"][aria-checked="true"]>div:first-child {
@apply bg-blue-600;
}
[role="switch"][aria-checked="true"]>div:last-child {
@apply left-5;
}
[role="switch"][aria-checked="false"]>div:last-child {
@apply left-1;
}
#sftpIframe {
@apply w-full rounded;
height: 50vh;
min-height: 300px;
}
#propertiesSearch {
@apply bg-gray-700 p-2 rounded text-white w-full;
}
#propertiesList {
@apply space-y-2;
}
.nav-btn {
@apply bg-blue-600 text-white font-medium px-3 py-1 rounded transition duration-200 text-sm;
}
.nav-btn2 {
@apply bg-blue-600 text-white font-medium px-3 py-1 rounded transition duration-200 text-sm;
}
.nav-btn:hover:not(.disabled) {
@apply bg-blue-700;
}
.nav-btn.current {
@apply bg-blue-800 border-2 border-blue-400 font-bold;
}
.nav-btn.disabled {
@apply bg-gray-600 cursor-not-allowed opacity-50;
}
.pagination-container {
@apply flex justify-center items-center gap-1 mt-4;
}
.pagination-container .nav-btn {
@apply px-2 py-0.5 text-xs rounded-sm;
}
.pagination-container .nav-btn.current {
@apply bg-blue-800 border-1 border-blue-400 font-bold;
}
.pagination-container .nav-btn.disabled {
@apply bg-gray-600 cursor-not-allowed opacity-50;
}
}
.hamburger {
@apply md:hidden flex flex-col justify-center items-center w-10 h-10 fixed top-4 right-4 z-[99999];
}
.hamburger .bar {
@apply w-6 h-0.5 bg-blue-400 mb-1.5 transition-all duration-300;
}
.hamburger.active .bar:nth-child(1) {
@apply translate-y-2 rotate-45;
}
.hamburger.active .bar:nth-child(2) {
@apply opacity-0;
}
.hamburger.active .bar:nth-child(3) {
@apply -translate-y-2 -rotate-45;
}
.mobile-nav-container {
@apply hidden fixed inset-0 flex-col items-center justify-center bg-gray-900 z-[99998] p-4;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.mobile-nav-container.active {
@apply flex opacity-100 visible;
}
.mobile-nav-container ul li a,
.mobile-nav-container ul li button {
@apply block px-4 py-2 rounded text-lg text-white bg-blue-600 hover:bg-blue-700 mb-4;
}
.control-btn {
@apply text-white font-medium px-4 py-2 rounded transition duration-200;
}
#startBtn {
@apply bg-green-600 hover:bg-green-700;
}
#stopBtn {
@apply bg-red-600 hover:bg-red-700;
}
#restartBtn {
@apply bg-yellow-500 hover:bg-yellow-600 text-black;
}
.disabled-btn {
@apply pointer-events-none cursor-not-allowed opacity-50;
}
.disabled-btn:hover {
@apply cursor-not-allowed;
}
.copy-key-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.25rem;
background-color: #374151;
border-radius: 0.25rem;
color: #ffffff;
transition: background-color 0.2s;
}
.copy-key-btn:hover {
background-color: #4b5563;
}
.copy-key-btn svg {
width: 1rem;
height: 1rem;
}
@media (max-width: 768px) {
.section-bg {
@apply p-4;
}
.modal-content {
@apply p-4 max-w-[95%];
}
.section-bg .grid {
@apply grid-cols-1;
}
.section-bg .flex {
@apply flex-col gap-2;
}
canvas {
@apply max-w-[120px];
}
.section-bg input,
.section-bg textarea,
.section-bg select {
@apply p-2 text-sm;
}
.section-bg pre {
@apply text-sm;
}
#sftpIframe {
@apply h-[40vh] min-h-[300px];
}
#consoleOutput {
@apply h-32 text-sm;
}
.notification {
@apply text-sm p-2 max-w-[80%];
}
h1 {
@apply text-3xl;
}
h2 {
@apply text-xl;
}
h3 {
@apply text-base;
}
p,
span,
a {
@apply text-sm;
}
.nav-btn {
@apply hidden;
}
.hamburger {
@apply flex;
}
.control-btn {
@apply px-3 py-1 text-sm;
}
.item-entry {
@apply flex-col gap-1;
}
.item-entry .item-amount {
@apply w-full;
}
}
@media (max-width: 640px) {
.section-bg {
@apply p-3;
}
.modal-content {
@apply p-3 max-w-[95%];
}
canvas {
@apply max-w-[100px];
}
.section-bg input,
.section-bg textarea,
.section-bg select {
@apply p-1 text-xs;
}
.section-bg pre {
@apply text-xs;
}
#sftpIframe {
@apply h-[35vh] min-h-[250px];
}
#consoleOutput {
@apply h-28 text-xs;
}
.notification {
@apply text-xs p-1 max-w-[90%];
}
h1 {
@apply text-2xl;
}
h2 {
@apply text-lg;
}
h3 {
@apply text-sm;
}
p,
span,
a {
@apply text-xs;
}
.control-btn {
@apply px-2 py-1 text-xs;
}
}
@media (max-width: 480px) {
.section-bg {
@apply p-2;
}
.modal-content {
@apply p-2 max-w-[98%];
}
canvas {
@apply max-w-[80px];
}
.section-bg input,
.section-bg textarea,
.section-bg select {
@apply p-1 text-xs;
}
.section-bg pre {
@apply text-xs;
}
#sftpIframe {
@apply h-[30vh] min-h-[200px];
}
#consoleOutput {
@apply h-24 text-xs;
}
.notification {
@apply text-xs p-1 max-w-[95%];
}
h1 {
@apply text-xl;
}
h2 {
@apply text-base;
}
h3 {
@apply text-xs;
}
p,
span,
a {
@apply text-xs;
}
.control-btn {
@apply px-2 py-1 text-xs;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,275 +0,0 @@
@font-face {
font-family: 'Minecraft';
src: url('../font/MinecraftRegular-Bmg3.otf') format('opentype');
font-weight: normal;
font-style: normal;
font-display: swap;
}
@media (max-width: 640px) {
.holesail-output-mobile-hidden {
display: none;
}
}
@import "tailwindcss";
@layer base {
/* Sticky footer base styles */
html {
@apply h-full;
}
body {
@apply min-h-full flex flex-col;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Minecraft', sans-serif;
}
#app {
@apply flex-1 flex flex-col;
}
main {
@apply flex-grow;
}
footer {
@apply flex-shrink-0;
}
}
@layer components {
.spinner {
@apply border-4 border-gray-700 border-t-white rounded-full w-6 h-6 animate-spin inline-block;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
#notificationContainer {
@apply fixed bottom-5 right-5 z-[1000] flex flex-col-reverse gap-2;
}
.notification {
@apply bg-gray-800 text-white p-4 rounded-lg flex items-center gap-3 shadow-lg transition-opacity duration-300;
}
.notification.success {
@apply bg-green-700;
}
.notification.error {
@apply bg-red-700;
}
#dockerLogsTerminal {
@apply bg-gray-800 p-4 rounded-lg max-h-48 w-full;
}
.xterm .xterm-viewport {
@apply overflow-y-auto;
scrollbar-width: thin;
scrollbar-color: #4B5563 #1F2937;
}
.xterm .xterm-viewport::-webkit-scrollbar {
@apply w-2;
}
.xterm .xterm-viewport::-webkit-scrollbar-track {
@apply bg-gray-800;
}
.xterm .xterm-viewport::-webkit-scrollbar-thumb {
@apply bg-gray-600 rounded;
}
.xterm .xterm-viewport::-webkit-scrollbar-thumb:hover {
@apply bg-gray-500;
}
.xterm .xterm-screen {
@apply w-full;
}
.control-btn {
@apply px-4 py-2 text-sm font-medium transition-all duration-200 min-w-[80px] text-center shadow-sm;
}
.control-btn:hover:not(.disabled-btn) {
@apply -translate-y-px;
}
.control-btn:active:not(.disabled-btn) {
@apply translate-y-0;
}
/* Player button styles */
.tell-player {
@apply bg-blue-600 hover:bg-blue-700 px-2 py-1 rounded text-sm font-[Minecraft] transition-all duration-200;
}
.give-player {
@apply bg-green-600 hover:bg-green-700 px-2 py-1 rounded text-sm font-[Minecraft] transition-all duration-200;
}
.op-player, .deop-player {
@apply bg-purple-600 hover:bg-purple-700 px-2 py-1 rounded text-sm font-[Minecraft] transition-all duration-200;
}
.kick-player, .ban-player {
@apply bg-red-600 hover:bg-red-700 px-2 py-1 rounded text-sm font-[Minecraft] transition-all duration-200;
}
.teleport-player {
@apply bg-cyan-600 hover:bg-cyan-700 px-2 py-1 rounded text-sm font-[Minecraft] transition-all duration-200;
}
.teleport-player:hover:not(.disabled-btn) {
@apply -translate-y-px;
}
.teleport-player:active:not(.disabled-btn) {
@apply translate-y-0;
}
.effect-player {
@apply bg-teal-600 hover:bg-teal-700 px-2 py-1 rounded text-sm font-[Minecraft] transition-all duration-200;
}
.effect-player:hover:not(.disabled-btn) {
@apply -translate-y-px;
}
.effect-player:active:not(.disabled-btn) {
@apply translate-y-0;
}
.modal {
@apply fixed inset-0 bg-black/75 flex items-center justify-center z-[1000];
}
.modal-content {
@apply bg-gray-800 p-6 rounded-lg w-full max-w-xl relative max-h-[80vh] overflow-y-auto;
scrollbar-width: thin;
scrollbar-color: #4B5563 #1F2937;
}
.modal-content::-webkit-scrollbar {
@apply w-2;
}
.modal-content::-webkit-scrollbar-track {
@apply bg-gray-800;
}
.modal-content::-webkit-scrollbar-thumb {
@apply bg-gray-600 rounded;
}
.modal-content::-webkit-scrollbar-thumb:hover {
@apply bg-gray-500;
}
.modal-close {
@apply absolute top-2 right-2 bg-transparent border-none text-white text-xl cursor-pointer;
}
.disabled-btn {
@apply opacity-50 cursor-not-allowed pointer-events-none;
}
.disabled-btn:hover {
@apply cursor-not-allowed;
}
/* Search box and properties fields */
#searchContainer {
@apply mb-4 block;
}
#propertiesSearch {
@apply bg-gray-700 px-4 py-2 rounded text-white w-full block;
}
#propertiesList {
@apply space-y-2;
}
/* Toggle switch styles */
[role="switch"] {
@apply inline-block relative w-10 h-6 cursor-pointer z-10;
}
[role="switch"] > div:first-child {
@apply w-full h-full rounded-full transition-colors duration-200 ease-in-out;
}
[role="switch"] > div:last-child {
@apply absolute w-4 h-4 rounded-full bg-white top-1 transition-all duration-200 ease-in-out z-[11];
}
[role="switch"][aria-checked="true"] > div:first-child {
@apply bg-green-600;
}
[role="switch"][aria-checked="false"] > div:first-child {
@apply bg-gray-600;
}
[role="switch"][aria-checked="true"] > div:last-child {
@apply left-6;
}
[role="switch"][aria-checked="false"] > div:last-child {
@apply left-1;
}
}
@layer utilities {
/* Custom utilities */
}
/* Media queries */
@media (max-width: 640px) {
.bg-gray-800.p-6.rounded-lg.shadow-lg .grid {
@apply grid-cols-1;
}
.bg-gray-800.p-6.rounded-lg.shadow-lg p {
@apply flex flex-col gap-2 break-all;
}
.bg-gray-800.p-6.rounded-lg.shadow-lg a,
.bg-gray-800.p-6.rounded-lg.shadow-lg span {
@apply break-words whitespace-normal;
}
.bg-gray-800.p-6.rounded-lg.shadow-lg button {
@apply w-full mt-2;
}
.tell-player,
.give-player,
.teleport-player,
.effect-player,
.op-player,
.deop-player,
.kick-player,
.ban-player {
@apply w-full text-center mt-1;
}
}
/* Additional styles */
.bg-gray-800.p-6.rounded-lg.shadow-lg .grid {
@apply overflow-x-hidden;
}
.bg-gray-800.p-6.rounded-lg.shadow-lg p {
@apply max-w-full;
}

View File

@@ -2,471 +2,546 @@
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Permissions-Policy" content="clipboard-write=(self https://sftp.my-mc.link)">
<title>My-MC Panel</title>
<link rel="stylesheet" href="/css/styles.min.css?p=1">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" rel="stylesheet" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
<link rel="manifest" href="/favicon/site.webmanifest">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Permissions-Policy" content="clipboard-write=(self https://sftp.my-mc.link)">
<title>My-MC Panel</title>
<link rel="stylesheet" href="css/style.min.css">
<link rel="stylesheet" href="css/main-site.css">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" rel="stylesheet" />
<link rel="icon" href="https://minecraft.wiki/images/Favicon.png" type="image/png">
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
<link rel="manifest" href="/favicon/site.webmanifest">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
</head>
<body class="bg-gray-900 text-white overflow-x-hidden min-h-full flex flex-col">
<div id="app" class="flex-grow">
<!-- Login Page and Modals (unchanged) -->
<div id="loginPage" class="hidden fixed inset-0 bg-gray-900 flex items-center justify-center">
<div class="bg-gray-800 p-8 rounded-lg shadow-lg w-full max-w-md">
<h2 class="text-2xl font-bold mb-6 text-center">My-MC Panel</h2>
<div class="mb-4">
<input id="loginApiKey" type="text" placeholder="Enter API Key"
class="bg-gray-700 px-4 py-2 rounded text-white w-full">
</div>
<button id="loginBtn" class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded w-full">Login</button>
<p id="loginError" class="text-red-500 text-sm mt-2 hidden"></p>
</div>
</div>
<div id="notificationContainer"></div>
<div id="tellModal" class="modal hidden">
<div class="modal-content">
<button class="modal-close">×</button>
<h2 class="text-xl font-semibold mb-4">Send Message to <span id="tellPlayerName"></span></h2>
<form id="tellForm">
<textarea id="tellMessage" placeholder="Enter your message"
class="bg-gray-700 px-4 py-2 rounded text-white w-full h-24 mb-4"></textarea>
<button type="submit" class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded w-full">Send</button>
</form>
</div>
</div>
<div id="giveModal" class="modal hidden">
<div class="modal-content">
<button class="modal-close">×</button>
<h2 class="text-xl font-semibold mb-4">Give Items to <span id="givePlayerName"></span></h2>
<form id="giveForm">
<div class="mb-4">
<label for="loadoutSelect" class="block text-sm font-medium mb-1">Select Loadout</label>
<select id="loadoutSelect" class="bg-gray-700 px-4 py-2 rounded text-white w-full">
<option value="custom">Custom</option>
<option value="starter">Starter Kit (Torches, Food)</option>
<option value="builder">Builder Kit (Stone, Wood)</option>
<option value="combat">Combat Kit (Sword, Armor)</option>
<option value="miner">Miner Kit (Pickaxe, Torches, Shovel)</option>
<option value="adventurer">Adventurer Kit (Bow, Arrows, Compass)</option>
<option value="alchemist">Alchemist Kit (Potions, Brewing Stand)</option>
<option value="enchanter">Enchanter Kit (Books, Lapis, Enchanting Table)</option>
<option value="farmer">Farmer Kit (Seeds, Hoe, Bone Meal)</option>
<option value="nether">Nether Survival Kit (Fire Resistance, Obsidian)</option>
<option value="end">End Prep Kit (Ender Pearls, Blaze Rods)</option>
</select>
</div>
<div id="customGiveFields" class="hidden mb-4">
<div id="itemList" class="space-y-2"></div>
<button type="button" id="addItemBtn" class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded mt-2">Add
Item</button>
</div>
<button type="submit" class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded w-full">Give</button>
</form>
</div>
</div>
<div id="teleportModal" class="modal hidden">
<div class="modal-content">
<button class="modal-close">×</button>
<h2 class="text-xl font-semibold mb-4">Teleport <span id="teleportPlayerName"></span></h2>
<form id="teleportForm">
<div class="mb-4">
<label for="teleportDestination" class="block text-sm font-medium mb-1">Select Destination Player</label>
<select id="teleportDestination" class="bg-gray-700 px-4 py-2 rounded text-white w-full">
<!-- Options populated dynamically -->
</select>
</div>
<button type="submit" class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded w-full">Teleport</button>
</form>
</div>
</div>
<div id="effectModal" class="modal hidden">
<div class="modal-content">
<button class="modal-close">×</button>
<h2 class="text-xl font-semibold mb-4">Apply Effect to <span id="effectPlayerName"></span></h2>
<form id="effectForm">
<div class="mb-4">
<label for="effectSelect" class="block text-sm font-medium mb-1">Select Effect</label>
<select id="effectSelect" class="bg-gray-700 px-4 py-2 rounded text-white w-full">
<option value="speed:30:1">Speed (30s, Level 1)</option>
<option value="strength:30:1">Strength (30s, Level 1)</option>
<option value="regeneration:30:1">Regeneration (30s, Level 1)</option>
<option value="jump_boost:30:1">Jump Boost (30s, Level 1)</option>
<option value="invisibility:30:1">Invisibility (30s, Level 1)</option>
<option value="night_vision:60:1">Night Vision (60s, Level 1)</option>
<option value="fire_resistance:60:1">Fire Resistance (60s, Level 1)</option>
<option value="water_breathing:60:1">Water Breathing (60s, Level 1)</option>
</select>
</div>
<button type="submit" class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded w-full">Apply Effect</button>
</form>
</div>
</div>
<div id="editPropertiesModal" class="modal hidden">
<div class="modal-content">
<button class="modal-close">×</button>
<h2 class="text-xl font-semibold mb-4">Edit server.properties</h2>
<form id="editPropertiesForm" class="space-y-4">
<div id="propertiesFields" class="space-y-2"></div>
<button type="submit" class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded w-full">Save</button>
</form>
</div>
</div>
<div id="updateModsModal"
class="modal hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-2 sm:p-4">
<div
class="modal-content bg-gray-900 rounded-lg max-w-[95%] w-full sm:w-[90%] md:w-[85%] lg:w-[80%] mx-auto flex flex-col max-h-[95vh]">
<div class="flex justify-between items-center p-4 border-b">
<h2 class="text-xl font-semibold text-white">Mod Update Output</h2>
<button class="modal-close text-2xl font-bold text-white">×</button>
</div>
<pre id="updateModsOutput"
class="flex-1 p-4 text-white text-sm max-h-[calc(95vh-10rem)] overflow-y-auto overflow-x-auto whitespace-pre-wrap break-words leading-relaxed"></pre>
<div class="p-4 border-t">
<button id="closeUpdateModsBtn"
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded w-full">Close</button>
</div>
</div>
</div>
<!-- Navigation (unchanged) -->
<nav class="bg-gray-800 p-4 shadow-lg">
<div class="container mx-auto flex justify-between items-center">
<h1 class="text-2xl font-bold">My-MC Panel</h1>
<div class="flex space-x-4">
<button id="refresh" class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded">Refresh</button>
<button id="backupBtn" class="bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded">Backup</button>
<div id="authControls">
<input id="apiKey" type="text" placeholder="Enter API Key" class="bg-gray-700 px-4 py-2 rounded text-white">
</div>
</div>
</div>
<body class="min-h-full flex flex-col">
<!-- Hamburger Menu Icon (visible on mobile) -->
<button class="hamburger md:hidden flex flex-col justify-center items-center w-10 h-10 focus:outline-none"
style="z-index: 99999; position: fixed; top: 1rem; right: 1rem; pointer-events: auto;">
<span class="bar w-6 h-0.5 bg-teal-400 mb-1.5 transition-all duration-300"></span>
<span class="bar w-6 h-0.5 bg-teal-400 mb-1.5 transition-all duration-300"></span>
<span class="bar w-6 h-0.5 bg-teal-400 transition-all duration-300"></span>
</button>
<!-- Mobile Navigation Menu -->
<nav class="mobile-nav mobile-nav-container hidden fixed inset-0 flex-col items-center justify-center md:hidden"
data-mobile-nav style="z-index: 99998; position: fixed; inset: 0; pointer-events: auto;">
<ul class="text-center">
<li class="mb-6">
<a href="https://my-mc.link"
class="text-lg minecraft-font text-white bg-gradient-to-r from-teal-400 to-blue-500 px-4 py-2 rounded-md hover:bg-gradient-to-r hover:from-blue-500 hover:to-teal-400">Home</a>
</li>
<li class="mb-6">
<a href="https://stats.my-mc.link"
class="text-lg minecraft-font text-white bg-gradient-to-r from-teal-400 to-blue-500 px-4 py-2 rounded-md hover:bg-gradient-to-r hover:from-blue-500 hover:to-teal-400">System
Stats</a>
</li>
<li class="mb-6">
<button id="mobileRefresh"
class="text-lg minecraft-font text-white bg-gradient-to-r from-teal-400 to-blue-500 px-4 py-2 rounded-md hover:bg-gradient-to-r hover:from-blue-500 hover:to-teal-400">Refresh</button>
</li>
<li class="mb-6">
<button id="mobileBackupBtn"
class="text-lg minecraft-font text-white bg-gradient-to-r from-teal-400 to-blue-500 px-4 py-2 rounded-md hover:bg-gradient-to-r hover:from-blue-500 hover:to-teal-400">Backup</button>
</li>
<li class="mb-6">
<a href="https://info.my-mc.link"
class="text-lg minecraft-font text-white bg-gradient-to-r from-teal-400 to-blue-500 px-4 py-2 rounded-md hover:bg-gradient-to-r hover:from-blue-500 hover:to-teal-400"
target="_blank">Wiki</a>
</li>
<li class="mb-6">
<button id="mobileLogoutBtn"
class="text-lg minecraft-font text-white bg-gradient-to-r from-teal-400 to-blue-500 px-4 py-2 rounded-md hover:bg-gradient-to-r hover:from-blue-500 hover:to-teal-400">Logout</button>
</li>
</ul>
</nav>
<!-- Particle Effects -->
<div class="particle"></div>
<div class="particle large"></div>
<div class="particle"></div>
<div class="particle large"></div>
<div class="particle"></div>
<!-- Main Content (unchanged except for Holesail Keys section) -->
<main id="mainContent" class="container mx-auto p-6">
<div class="bg-gray-800 p-6 rounded-lg shadow-lg mb-6" data-section="server-status">
<h2 class="text-xl font-semibold mb-4">Server Status</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<p><strong>User:</strong> <span id="user">Loading...</span></p>
<p><strong>Key Expiry:</strong> <span id="keyExpiry">Loading...</span></p>
<p><strong>Status:</strong> <span id="serverStatus">Loading...</span></p>
</div>
<div class="flex space-x-4 justify-center">
<div class="text-center">
<canvas id="memoryMeter" width="150" height="150"></canvas>
<p class="text-sm mt-2"><b>Memory Usage</b></p>
<p id="memoryPercent" class="text-lg font-bold">0%</p>
<header class="header-bg py-16 text-center relative z-2">
<div class="header-content flex items-center justify-between max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div>
<h1
class="text-5xl minecraft-font bg-clip-text text-transparent bg-gradient-to-r from-teal-400 to-blue-500">
My-MC Panel</h1>
<p class="text-lg mt-4 opacity-90 tracking-wide font-medium">Manage Your Minecraft Server</p>
</div>
<div class="text-center">
<canvas id="cpuMeter" width="150" height="150"></canvas>
<p class="text-sm mt-2"><b>CPU Usage</b></p>
<p id="cpuPercent" class="text-lg font-bold">0%</p>
</div>
</div>
<div class="flex flex-wrap space-x-2 justify-end items-center">
<div class="flex space-x-2">
<button id="startBtn"
class="bg-green-600 hover:bg-green-700 rounded font-medium control-btn">Start</button>
<button id="stopBtn" class="bg-red-600 hover:bg-red-700 rounded font-medium control-btn">Stop</button>
<button id="restartBtn"
class="bg-yellow-600 hover:bg-yellow-700 rounded font-medium control-btn">Restart</button>
</div>
<button id="editPropertiesBtn"
class="bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded control-btn w-full mt-2">Edit Server
Properties</button>
<button id="updateModsBtn"
class="bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded control-btn w-full mt-2">Update Mods</button>
</div>
<!-- Navigation Buttons (hidden on mobile) -->
<nav class="flex items-center gap-2 hidden md:flex">
<a href="https://my-mc.link" class="nav-btn">Home</a>
<a href="https://stats.my-mc.link" class="nav-btn" target="_blank">System Stats</a>
<button id="refresh" class="nav-btn">Refresh</button>
<button id="backupBtn" class="nav-btn">Backup</button>
<a href="https://info.my-mc.link" class="nav-btn" target="_blank">Wiki</a>
<button id="logoutBtn" class="nav-btn">Logout</button>
</nav>
</div>
</div>
</header>
<div class="bg-gray-800 p-6 rounded-lg shadow-lg mb-6">
<h2 class="text-xl font-semibold mb-4">Player Management</h2>
<div class="mb-4"></div>
<p><strong>Connected Players:</strong><br> <span id="playerList">Loading...</span></p>
</div>
<div class="bg-gray-800 p-6 rounded-lg shadow-lg mb-6">
<h2 class="text-xl font-semibold mb-4">Server Logs</h2>
<div id="dockerLogsTerminal" class="mt-4"></div>
</div>
<div class="bg-gray-800 p-6 rounded-lg shadow-lg mb-6">
<h2 class="text-xl font-semibold mb-4">Server Console</h2>
<form id="consoleForm" onsubmit="event.preventDefault(); sendConsoleCommand();">
<input id="consoleInput" type="text" placeholder="Enter RCON Command (Hit Enter To Submit)"
class="bg-gray-700 px-4 py-2 rounded text-white w-full mb-2">
<button id="sendConsole" type="submit" class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded"
style="display: none;">Send</button>
</form>
<pre id="consoleOutput" class="bg-gray-900 p-4 rounded mt-4 h-48 overflow-y-auto"></pre>
</div>
<div class="bg-gray-800 p-6 rounded-lg shadow-lg mb-6" id="sftpBrowserSection" style="display: none;">
<div class="flex justify-between items-baseline mb-4">
<h2 class="text-xl font-semibold">SFTP Browser</h2>
<button id="sftpBtn" class="px-3 py-1 rounded text-sm flex items-center space-x-1"
style="background-color: #121724; color: #ffffff;">
<i class="fas fa-sync-alt" style="color: #121724;"></i>
<span>Refresh SFTP</span>
</button>
</div>
<iframe id="sftpIframe" class="w-full rounded" style="height: 650px; min-height: 650px;"
allow="clipboard-read; clipboard-write"></iframe>
</div>
<div class="bg-gray-800 p-6 rounded-lg shadow-lg mb-6">
<h2 class="text-xl font-semibold mb-4">Mod Management</h2>
<form id="modSearchForm" onsubmit="event.preventDefault(); searchMods(1);">
<div class="mb-4 flex space-x-2">
<input id="modSearch" type="text" placeholder="Search Mods (Hit Enter To Submit)"
class="bg-gray-700 px-4 py-2 rounded text-white flex-grow">
<button id="searchBtn" type="submit" class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded"
style="display: none;">Search</button>
<button id="closeSearchBtn" type="button"
class="bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded hidden">Close</button>
</div>
</form>
<div id="modResults" class="grid grid-cols-1 md:grid-cols-2 gap-4"></div>
<div id="pagination" class="mt-4 flex justify-center space-x-2"></div>
<h3 class="text-lg font-semibold mt-4">Installed Mods</h3>
<div class="mb-4 flex space-x-2">
<input id="modListSearch" type="text" placeholder="Search Installed Mods"
class="bg-gray-700 px-4 py-2 rounded text-white flex-grow">
<button id="clearModListSearch" type="button"
class="bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded hidden">Clear</button>
</div>
<div id="modList" class="mt-2 grid grid-cols-1 md:grid-cols-2 gap-4"></div>
<div id="modListPagination" class="mt-4 flex justify-center space-x-2"></div>
</div>
<div class="bg-gray-800 p-6 rounded-lg shadow-lg mb-6">
<h2 class="text-xl font-semibold mb-4">Server Links</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<p><strong>Advanced Log URL:</strong> <a id="logUrl" href="#" class="text-blue-400"
target="_blank">Loading...</a></p>
<p><strong>Website URL:</strong> <a id="websiteUrl" href="#" class="text-blue-400"
target="_blank">Loading...</a></p>
<p><strong>BlueMap URL:</strong> <a id="mapUrl" href="#" class="text-blue-400" target="_blank">Loading...</a>
</p>
<div>
<p><strong>Connection Link:</strong> <span id="myLink">Link Not Created</span> <span
id="connectionStatus"></span></p>
<button id="generateMyLinkBtn" class="bg-blue-600 hover:bg-blue-700 px-4 py-1 rounded mt-2">Generate
Connection Link</button>
</div>
<div>
<p><strong>Geyser Link:</strong> <span id="geyserLink">Link Not Created</span> <span
id="geyserStatus"></span></p>
<button id="generateGeyserLinkBtn" class="bg-blue-600 hover:bg-blue-700 px-4 py-1 rounded mt-2">Generate
Geyser Link</button>
</div>
<div>
<p><strong>SFTP Link:</strong> <span id="sftpLink">Link Not Created</span> <span id="sftpStatus"></span></p>
<button id="generateSftpLinkBtn" class="bg-blue-600 hover:bg-blue-700 px-4 py-1 rounded mt-2">Generate SFTP
Link</button>
</div>
</div>
</div>
<!-- Updated Holesail Keys Section -->
<div class="bg-gray-800 p-6 rounded-lg shadow-lg mb-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-white">Holesail Keys</h2>
<button id="toggleTutorial"
class="bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded-md transition duration-200">Tutorial</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div class="flex items-center bg-gray-700 p-3 rounded-md">
<div class="flex-grow">
<p class="text-gray-300"><strong>Minecraft Key:</strong></p>
<p class="text-gray-100 break-all" id="holesailHash">Not Loaded</p>
<p class="text-sm text-gray-400">Port: 127.0.0.1:25565</p>
<div id="app" class="flex-grow">
<!-- Login Page and Modals -->
<div id="loginPage" class="modal hidden">
<div class="modal-content">
<button class="modal-close">×</button>
<h2 class="text-2xl minecraft-font mb-6 text-center">My-MC Panel Login</h2>
<div class="mb-4">
<input id="loginApiKey" type="text" placeholder="Enter API Key"
class="bg-[rgba(10,17,40,0.3)] backdrop-blur-[12px] px-4 py-2 rounded text-white w-full border border-[rgba(255,255,255,0.15)]">
</div>
<button id="loginBtn" class="nav-btn w-full">Login</button>
<p id="loginError" class="text-red-500 text-sm mt-2 hidden"></p>
</div>
<button class="copy-key-btn ml-2 text-gray-300 hover:text-white" data-key-id="holesailHash"
data-key-type="Minecraft" title="Copy">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z">
</path>
</svg>
</button>
</div>
<div class="flex items-center bg-gray-700 p-3 rounded-md">
<div class="flex-grow">
<p class="text-gray-300"><strong>Geyser Key:</strong></p>
<p class="text-gray-100 break-all" id="geyserHash">Not Loaded</p>
<p class="text-sm text-gray-400">Port: 127.0.0.1:19132</p>
</div>
<button class="copy-key-btn ml-2 text-gray-300 hover:text-white" data-key-id="geyserHash"
data-key-type="Geyser" title="Copy">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z">
</path>
</svg>
</button>
</div>
<div class="flex items-center bg-gray-700 p-3 rounded-md">
<div class="flex-grow">
<p class="text-gray-300"><strong>SFTP Key:</strong></p>
<p class="text-gray-100 break-all" id="sftpHash">Not Loaded</p>
<p class="text-sm text-gray-400">Port: 127.0.0.1:22</p>
</div>
<button class="copy-key-btn ml-2 text-gray-300 hover:text-white" data-key-id="sftpHash" data-key-type="SFTP"
title="Copy">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z">
</path>
</svg>
</button>
</div>
</div>
<div id="tutorialSection" class="hidden">
<div class="bg-gray-700 p-6 rounded-xl mb-6 border border-gray-600">
<h3 class="text-xl font-bold mb-4 text-white border-b border-gray-500 pb-2">What is Holesail.io?</h3>
<p class="text-gray-200 text-base leading-relaxed mb-4">
<a href="https://holesail.io" target="_blank"
class="text-blue-400 hover:underline font-medium">Holesail.io</a> is an open-source, peer-to-peer
networking tool that creates secure, encrypted tunnels that bypass network restrictions, firewalls, and
NAT.<BR>It exposes your local network to the internet without needing port forwarding, static IPs, or
Dynamic DNS, acting as a versatile tunneling and reverse proxy solution.
</p>
<div class="mb-4">
<p class="text-gray-200 font-semibold mb-2">With Holesail, you can:</p>
<ul class="list-disc pl-6 space-y-1 text-gray-200 text-base">
<li>Access machines over the internet securely.</li>
<li>Share locally running servers, websites, or AI models with other ports/services.</li>
<li>Transfer files and folders remotely without bandwidth or size limits.</li>
<li>Play LAN games like Minecraft with friends remotely.</li>
<li>Secure SSH servers by blocking IP access and using Holesail for connections.</li>
<li>Built for ANY application, Holesail supports both the TCP and UDP Protocols nativly.</li>
<li>Expose single ports to the peer-to-peer network, unlike VPNs you never expose your entire local
network.</li>
<BR>And so much more! With Holesail, the possiblities are endless!
</ul>
</div>
<p class="text-gray-200 text-base leading-relaxed">
Built with security in mind, Holesail ensures all data is encrypted and never touches third-party
servers.<BR>Connections are truly peer-to-peer, accessible only to those with whom you share your private
key, providing both ease of use and robust security.<BR>Other peers cannot detect your activity or
services.<BR>As an open-source tool, Holesail enables third-party services to integrate it, enhancing
their security and connectivity.
</p>
<p class="text-gray-200 text-base leading-relaxed">
<BR>Your Public My-MC Ports are Holesail connections hosted on a separate server, not our Minecraft
host.<BR>They use the same keys from the tutorial below. We hook 'em up at our jump host to go public.
Pretty dope, right?
</p>
</div>
<div id="notificationContainer"></div>
<div class="bg-gray-700 p-4 rounded-md mb-6">
<h3 class="text-lg font-semibold mb-3 text-white">How to Use Holesail Keys</h3>
<ol class="list-decimal list-inside space-y-2 text-gray-300">
<li>Ensure <a href="https://nodejs.org/" target="_blank" class="text-blue-400 hover:underline">Node.js</a>
is installed on your system.</li>
<li>Install Holesail by running:
<pre class="bg-gray-800 p-2 rounded mt-1 text-sm">npm i holesail@2.1.0</pre>
</li>
<li>Connect to a key using the appropriate command:
<ul class="list-disc list-inside ml-4 mt-1">
<li>For Minecraft Key (e.g., Minecraft server):
<pre
class="bg-gray-800 p-2 rounded mt-1 text-sm">holesail <span id="tutorialHolesailHash">Not Loaded</span></pre>
</li>
<li>For Geyser key (e.g., cross-platform Minecraft):
<pre
class="bg-gray-800 p-2 rounded mt-1 text-sm">holesail <span id="tutorialGeyserHash">Not Loaded</span></pre>
</li>
<li>For SFTP key (e.g., file transfer):
<pre
class="bg-gray-800 p-2 rounded mt-1 text-sm">holesail <span id="tutorialSftpHash">Not Loaded</span></pre>
</li>
</ul>
</li>
<li class="holesail-output-mobile-hidden">Holesail will confirm the connection. Example output for your
Minecraft server:
<pre class="bg-gray-800 p-2 rounded mt-1 text-sm">
~ holesail <span id="tutorialHolesailHashOutput">Not Loaded</span>
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
| |
| |
| Holesail TCP Client Started ⛵️ |
| Connection Mode: Private Connection String |
| Access application on http://127.0.0.1:25565/ |
| Connected to key: <span id="tutorialHolesailHashOutput2">Not Loaded</span> |
| NOTE: TREAT PRIVATE CONNECTION STRINGS HOW YOU WOULD TREAT SSH KEY, DO NOT SHARE IT WITH ANYONE YOU DO NOT TRUST |
| |
| |
| |
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
<div id="tellModal" class="modal hidden">
<div class="modal-content">
<button class="modal-close">×</button>
<h2 class="text-xl minecraft-font mb-4">Send Message to <span id="tellPlayerName"></span></h2>
<form id="tellForm">
<textarea id="tellMessage" placeholder="Enter your message"
class="bg-[rgba(10,17,40,0.3)] backdrop-blur-[12px] px-4 py-2 rounded text-white w-full h-24 mb-4 border border-[rgba(255,255,255,0.15)]"></textarea>
<button type="submit" class="nav-btn w-full">Send</button>
</form>
</div>
</div>
<div id="giveModal" class="modal hidden">
<div class="modal-content">
<button class="modal-close">×</button>
<h2 class="text-xl minecraft-font mb-4">Give Items to <span id="givePlayerName"></span></h2>
<form id="giveForm">
<div class="mb-4">
<label for="loadoutSelect" class="block text-sm font-medium mb-1">Select Loadout</label>
<select id="loadoutSelect"
class="bg-[rgba(10,17,40,0.3)] backdrop-blur-[12px] px-4 py-2 rounded text-white w-full border border-[rgba(255,255,255,0.15)]">
<option value="custom">Custom</option>
<option value="starter">Starter Kit (Torches, Food)</option>
<option value="builder">Builder Kit (Stone, Wood)</option>
<option value="combat">Combat Kit (Sword, Armor)</option>
<option value="miner">Miner Kit (Pickaxe, Torches, Shovel)</option>
<option value="adventurer">Adventurer Kit (Bow, Arrows, Compass)</option>
<option value="alchemist">Alchemist Kit (Potions, Brewing Stand)</option>
<option value="enchanter">Enchanter Kit (Books, Lapis, Enchanting Table)</option>
<option value="farmer">Farmer Kit (Seeds, Hoe, Bone Meal)</option>
<option value="nether">Nether Survival Kit (Fire Resistance, Obsidian)</option>
<option value="end">End Prep Kit (Ender Pearls, Blaze Rods)</option>
</select>
</div>
<div id="customGiveFields" class="hidden mb-4">
<div id="itemList" class="space-y-2"></div>
<button type="button" id="addItemBtn" class="nav-btn mt-2">Add Item</button>
</div>
<button type="submit" class="nav-btn w-full">Give</button>
</form>
</div>
</div>
<div id="teleportModal" class="modal hidden">
<div class="modal-content">
<button class="modal-close">×</button>
<h2 class="text-xl minecraft-font mb-4">Teleport <span id="teleportPlayerName"></span></h2>
<form id="teleportForm">
<div class="mb-4">
<label for="teleportDestination" class="block text-sm font-medium mb-1">Select Destination
Player</label>
<select id="teleportDestination"
class="bg-[rgba(10,17,40,0.3)] backdrop-blur-[12px] px-4 py-2 rounded text-white w-full border border-[rgba(255,255,255,0.15)]">
<!-- Options populated dynamically -->
</select>
</div>
<button type="submit" class="nav-btn w-full">Teleport</button>
</form>
</div>
</div>
<div id="effectModal" class="modal hidden">
<div class="modal-content">
<button class="modal-close">×</button>
<h2 class="text-xl minecraft-font mb-4">Apply Effect to <span id="effectPlayerName"></span></h2>
<form id="effectForm">
<div class="mb-4">
<label for="effectSelect" class="block text-sm font-medium mb-1">Select Effect</label>
<select id="effectSelect"
class="bg-[rgba(10,17,40,0.3)] backdrop-blur-[12px] px-4 py-2 rounded text-white w-full border border-[rgba(255,255,255,0.15)]">
<option value="speed:30:1">Speed (30s, Level 1)</option>
<option value="strength:30:1">Strength (30s, Level 1)</option>
<option value="regeneration:30:1">Regeneration (30s, Level 1)</option>
<option value="jump_boost:30:1">Jump Boost (30s, Level 1)</option>
<option value="invisibility:30:1">Invisibility (30s, Level 1)</option>
<option value="night_vision:60:1">Night Vision (60s, Level 1)</option>
<option value="fire_resistance:60:1">Fire Resistance (60s, Level 1)</option>
<option value="water_breathing:60:1">Water Breathing (60s, Level 1)</option>
</select>
</div>
<button type="submit" class="nav-btn w-full">Apply Effect</button>
</form>
</div>
</div>
<div id="editPropertiesModal" class="modal hidden">
<div class="modal-content">
<button class="modal-close">×</button>
<h2 class="text-xl minecraft-font mb-4">Edit server.properties</h2>
<form id="editPropertiesForm" class="space-y-4">
<div id="propertiesFields" class="space-y-2"></div>
<button type="submit" class="nav-btn w-full">Save</button>
</form>
</div>
</div>
<div id="updateModsModal" class="modal hidden">
<div class="modal-content flex flex-col">
<div class="flex justify-between items-center p-4 border-b border-[rgba(255,255,255,0.12)]">
<h2 class="text-xl minecraft-font">Mod Update Output</h2>
<button class="modal-close text-2xl">×</button>
</div>
<pre id="updateModsOutput"
class="flex-1 p-4 text-[#e0e7ff] text-sm max-h-[calc(95vh-10rem)] overflow-y-auto overflow-x-auto whitespace-pre-wrap break-words leading-relaxed"></pre>
<div class="p-4 border-t border-[rgba(255,255,255,0.12)]">
<button id="closeUpdateModsBtn" class="btn-minecraft w-full">Close</button>
</div>
</div>
</div>
<!-- Main Content -->
<main id="mainContent" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-16 relative z-2">
<div class="section-bg p-4 mb-6" data-section="server-status">
<div class="flex flex-col space-y-2 mb-3">
<h2 class="text-2xl minecraft-font">Server Status</h2>
<div class="flex justify-between items-center">
<div class="flex flex-col space-y-1 text-sm">
<p><strong>User:</strong> <span id="user">Loading...</span></p>
<p><strong>Key Expiry:</strong> <span id="keyExpiry">Loading...</span></p>
<p><strong>Status:</strong> <span id="serverStatus">Loading...</span></p>
</div>
<div class="flex space-x-3 justify-center">
<div class="text-center">
<canvas id="memoryMeter" width="100" height="100"></canvas>
<p class="text-xs mt-1"><b>Memory</b></p>
<p id="memoryPercent" class="text-base font-bold">0%</p>
</div>
<div class="text-center">
<canvas id="cpuMeter" width="100" height="100"></canvas>
<p class="text-xs mt-1"><b>CPU</b></p>
<p id="cpuPercent" class="text-base font-bold">0%</p>
</div>
</div>
<div class="flex flex-col space-y-1">
<div class="flex justify-end space-x-1">
<button id="startBtn" class="control-btn text-sm">Start</button>
<button id="stopBtn" class="control-btn text-sm">Stop</button>
<button id="restartBtn" class="control-btn text-sm">Restart</button>
</div>
<div class="flex justify-end space-x-1">
<button id="editPropertiesBtn" class="nav-btn control-btn text-sm">Edit Properties</button>
<button id="updateModsBtn" class="nav-btn text-sm">Update Mods</button>
</div>
</div>
</div>
</div>
</div>
<div class="section-bg mb-12">
<h2 class="text-3xl minecraft-font mb-8">Server Logs</h2>
<div id="dockerLogsTerminal" class="mt-4"></div>
</div>
<div class="section-bg mb-12">
<h2 class="text-3xl minecraft-font mb-8">Player Management</h2>
<p><strong>Connected Players:</strong><br> <span id="playerList">Loading...</span></p>
</div>
<div class="section-bg mb-12">
<h2 class="text-3xl minecraft-font mb-8">Server Console</h2>
<form id="consoleForm" onsubmit="event.preventDefault(); sendConsoleCommand();">
<input id="consoleInput" type="text" placeholder="Enter RCON Command (Hit Enter To Submit)"
class="bg-[rgba(10,17,40,0.3)] backdrop-blur-[12px] px-4 py-2 rounded text-white w-full mb-2 border border-[rgba(255,255,255,0.15)]">
<button id="sendConsole" type="submit" class="nav-btn" style="display: none;">Send</button>
</form>
<pre id="consoleOutput"
class="bg-[rgba(10,17,40,0.3)] backdrop-blur-[12px] p-4 rounded mt-4 h-48 overflow-y-auto border border-[rgba(255,255,255,0.15)]"></pre>
</div>
<div class="section-bg mb-12" id="sftpBrowserSection" style="display: none;">
<div class="flex justify-between items-baseline mb-4">
<h2 class="text-3xl minecraft-font">SFTP Browser</h2>
<button id="sftpBtn" class="nav-btn text-sm flex items-center space-x-1">
<i class="fas fa-sync-alt"></i>
<span>Refresh SFTP</span>
</button>
</div>
<iframe id="sftpIframe" class="w-full rounded" allow="clipboard-read; clipboard-write"></iframe>
</div>
<div class="section-bg mb-8">
<h2 class="text-2xl minecraft-font mb-6">Mod Management</h2>
<form id="modSearchForm" onsubmit="event.preventDefault(); searchMods(1);">
<div class="mb-3 flex space-x-2">
<input id="modSearch" type="text" placeholder="Search Mods (Hit Enter To Submit)"
class="bg-[rgba(10,17,40,0.3)] backdrop-blur-[12px] px-3 py-1 rounded text-sm text-white flex-grow border border-[rgba(255,255,255,0.15)]">
<button id="searchBtn" type="submit" class="btn-minecraft" style="display: none;">Search</button>
<button id="closeSearchBtn" type="button" class="btn-minecraft hidden">Close</button>
</div>
</form>
<div id="modResults" class="grid grid-cols-1 md:grid-cols-2 gap-4"></div>
<div id="pagination" class="pagination-container mt-3"></div>
<h3 class="text-lg minecraft-font mt-3">Installed Mods</h3>
<div class="mb-3 flex space-x-2">
<input id="modListSearch" type="text" placeholder="Search Installed Mods"
class="bg-[rgba(10,17,40,0.3)] backdrop-blur-[12px] px-3 py-1 rounded text-sm text-white flex-grow border border-[rgba(255,255,255,0.15)]">
<button id="clearModListSearch" type="button" class="btn-minecraft hidden">Clear</button>
</div>
<div id="modList" class="mt-2 grid grid-cols-1 md:grid-cols-2 gap-4"></div>
<div id="modListPagination" class="pagination-container mt-3"></div>
</div>
<div class="section-bg mb-8 p-6 rounded-lg">
<h2 class="text-2xl minecraft-font mb-6 font-bold text-white">Server Links</h2>
<div class="flex flex-row gap-4 mb-4">
<!-- Connection Link Card -->
<div class="bg-gray-800/40 p-4 rounded-xl shadow-md hover:shadow-lg transition-shadow flex-1 min-w-[250px]">
<h3 class="text-lg minecraft-font font-semibold text-white mb-2">Connection Link</h3>
<p class="text-gray-300 mb-3 break-all">
<span id="myLink" class="text-blue-400">Link Not Created</span>
<span id="connectionStatus" class="text-xs"></span>
</p>
<button id="generateMyLinkBtn" class="nav-btn" class="w-full bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded-md transition-colors text-sm">
Generate Connection Link
</button>
</div>
<!-- Geyser Link Card -->
<div class="bg-gray-800/40 p-4 rounded-xl shadow-md hover:shadow-lg transition-shadow flex-1 min-w-[250px]">
<h3 class="text-lg minecraft-font font-semibold text-white mb-2">Geyser Link</h3>
<p class="text-gray-300 mb-3 break-all">
<span id="geyserLink" class="text-blue-400">Link Not Created</span>
<span id="geyserStatus" class="text-xs"></span>
</p>
<button id="generateGeyserLinkBtn" class="nav-btn" class="w-full bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded-md transition-colors text-sm">
Generate Geyser Link
</button>
</div>
<!-- SFTP Link Card -->
<div class="bg-gray-800/40 p-4 rounded-xl shadow-md hover:shadow-lg transition-shadow flex-1 min-w-[250px]">
<h3 class="text-lg minecraft-font font-semibold text-white mb-2">SFTP Link</h3>
<p class="text-gray-300 mb-3 break-all">
<span id="sftpLink" class="text-blue-400">Link Not Created</span>
<span id="sftpStatus" class="text-xs"></span>
</p>
<button id="generateSftpLinkBtn" class="nav-btn" class="w-full bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded-md transition-colors text-sm">
Generate SFTP Link
</button>
</div>
</div>
<div class="flex flex-row gap-4">
<!-- Advanced Log URL Card -->
<div class="bg-gray-800/40 p-4 rounded-xl shadow-md hover:shadow-lg transition-shadow flex-1 min-w-[250px]">
<h3 class="text-lg minecraft-font font-semibold text-white mb-2">Advanced Log</h3>
<p class="text-gray-300 mb-3 break-all">
<a id="logUrl" href="#" class="text-blue-400 hover:underline" target="_blank">Loading...</a>
</p>
</div>
<!-- Website URL Card -->
<div class="bg-gray-800/40 p-4 rounded-xl shadow-md hover:shadow-lg transition-shadow flex-1 min-w-[250px]">
<h3 class="text-lg minecraft-font font-semibold text-white mb-2">Website</h3>
<p class="text-gray-300 mb-3 break-all">
<a id="websiteUrl" href="#" class="text-blue-400 hover:underline" target="_blank">Loading...</a>
</p>
</div>
<!-- BlueMap URL Card -->
<div class="bg-gray-800/40 p-4 rounded-xl shadow-md hover:shadow-lg transition-shadow flex-1 min-w-[250px]">
<h3 class="text-lg minecraft-font font-semibold text-white mb-2">BlueMap</h3>
<p class="text-gray-300 mb-3 break-all">
<a id="mapUrl" href="#" class="text-blue-400 hover:underline" target="_blank">Loading...</a>
</p>
</div>
</div>
</div>
<div class="section-bg mb-8">
<div class="flex items-center justify-between mb-3 flex-wrap">
<h2 class="text-2xl minecraft-font">Holesail Keys</h2>
<button id="toggleTutorial" class="nav-btn2 bg-blue-600 hover:bg-blue-700 text-white font-semibold py-1 px-3 rounded-md transition duration-200 md:mt-0 mt-2">Tutorial</button>
</div>
<div class="flex flex-row gap-4 mb-4">
<div class="flex items-center p-3 bg-gray-800 rounded-md min-w-0 flex-1">
<div class="flex-grow">
<p class="text-sm"><strong>Minecraft Key:</strong></p>
<p class="break-all text-sm" id="holesailHash">Not Loaded</p>
<p class="text-xs">Port: 127.0.0.1:25565</p>
</div>
<button class="copy-key-btn ml-2" data-key-id="holesailHash" data-key-type="Minecraft" title="Copy">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
</div>
<div class="flex items-center p-3 bg-gray-800 rounded-md min-w-0 flex-1">
<div class="flex-grow">
<p class="text-sm"><strong>Geyser Key:</strong></p>
<p class="break-all text-sm" id="geyserHash">Not Loaded</p>
<p class="text-xs">Port: 127.0.0.1:19132</p>
</div>
<button class="copy-key-btn ml-2" data-key-id="geyserHash" data-key-type="Geyser" title="Copy">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
</div>
<div class="flex items-center p-3 bg-gray-800 rounded-md min-w-0 flex-1">
<div class="flex-grow">
<p class="text-sm"><strong>SFTP Key:</strong></p>
<p class="break-all text-sm" id="sftpHash">Not Loaded</p>
<p class="text-xs">Port: 127.0.0.1:22</p>
</div>
<button class="copy-key-btn ml-2" data-key-id="sftpHash" data-key-type="SFTP" title="Copy">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
</div>
</div>
</div>
<div id="tutorialSection" class="hidden">
<div class="feature-card tilt-card p-6 mb-6">
<h3 class="text-xl minecraft-font mb-4">What is Holesail.io?</h3>
<p class="text-base leading-relaxed mb-4">
<a href="https://holesail.io" target="_blank"
class="text-teal-400 hover:text-blue-400">Holesail.io</a> is an open-source,
peer-to-peer networking tool that creates secure, encrypted tunnels that bypass network
restrictions, firewalls, and NAT. It exposes your local network to the internet without
needing port forwarding, static IPs, or Dynamic DNS, acting as a versatile tunneling and
reverse proxy solution.
</p>
<div class="mb-4">
<p class="font-semibold mb-2">With Holesail, you can:</p>
<ul class="list-disc pl-6 space-y-1 text-base">
<li>Access machines over the internet securely.</li>
<li>Share locally running servers, websites, or AI models with other ports/services.
</li>
<li>Transfer files and folders remotely without bandwidth or size limits.</li>
<li>Play LAN games like Minecraft with friends remotely.</li>
<li>Secure SSH servers by blocking IP access and using Holesail for connections.</li>
<li>Built for ANY application, Holesail supports both the TCP and UDP Protocols
natively.</li>
<li>Expose single ports to the peer-to-peer network, unlike VPNs you never expose your
entire local network.</li>
</ul>
</div>
<p class="text-base leading-relaxed">
Built with security in mind, Holesail ensures all data is encrypted and never touches
third-party servers. Connections are truly peer-to-peer, accessible only to those with whom
you share your private key, providing both ease of use and robust security. Other peers
cannot detect your activity or services. As an open-source tool, Holesail enables
third-party services to integrate it, enhancing their security and connectivity.
</p>
<p class="text-base leading-relaxed mt-4">
Your Public My-MC Ports are Holesail connections hosted on a separate server, not our
Minecraft host. They use the same keys from the tutorial below. We hook 'em up at our jump
host to go public.
</p>
</div>
<div class="feature-card tilt-card p-6 mb-6">
<h3 class="text-xl minecraft-font mb-4">How to Use Holesail Keys</h3>
<ol class="list-decimal list-inside space-y-2">
<li>Ensure <a href="https://nodejs.org/" target="_blank"
class="text-teal-400 hover:text-blue-400">Node.js</a> is installed on your system.
</li>
<li>Install Holesail by running:
<pre
class="bg-[rgba(10,17,40,0.3)] backdrop-blur-[12px] p-2 rounded mt-1 text-sm border border-[rgba(255,255,255,0.15)]">npm i holesail@2.1.0</pre>
</li>
<li>Connect to a key using the appropriate command:
<ul class="list-disc list-inside ml-4 mt-1">
<li>For Minecraft Key (e.g., Minecraft server):
<pre
class="bg-[rgba(10,17,40,0.3)] backdrop-blur-[12px] p-2 rounded mt-1 text-sm border border-[rgba(255,255,255,0.15)]">holesail <span id="tutorialHolesailHash">Not Loaded</span></pre>
</li>
<li>For Geyser key (e.g., cross-platform Minecraft):
<pre
class="bg-[rgba(10,17,40,0.3)] backdrop-blur-[12px] p-2 rounded mt-1 text-sm border border-[rgba(255,255,255,0.15)]">holesail <span id="tutorialGeyserHash">Not Loaded</span></pre>
</li>
<li>For SFTP key (e.g., file transfer):
<pre
class="bg-[rgba(10,17,40,0.3)] backdrop-blur-[12px] p-2 rounded mt-1 text-sm border border-[rgba(255,255,255,0.15)]">holesail <span id="tutorialSftpHash">Not Loaded</span></pre>
</li>
</ul>
</li>
<li class="holesail-output-mobile-hidden">Holesail will confirm the connection. Example
output for your Minecraft server:
<pre
class="bg-[rgba(10,17,40,0.3)] backdrop-blur-[12px] p-2 rounded mt-1 text-sm border border-[rgba(255,255,255,0.15)]">
~ holesail <span id="tutorialHolesailHashOutput">Not Loaded</span>
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
| |
| |
| Holesail TCP Client Started ⛵️ |
| Connection Mode: Private Connection String |
| Access application on http://127.0.0.1:25565/ |
| Connected to key: <span id="tutorialHolesailHashOutput2">Not Loaded</span> |
| NOTE: TREAT PRIVATE CONNECTION STRINGS HOW YOU WOULD TREAT SSH KEY, DO NOT SHARE IT WITH ANYONE YOU DO NOT TRUST |
| |
| |
| |
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
</pre>
</li>
<BR>Now you can connect to your own localhost within Minecraft!<BR><BR>Connect to:
<code class="bg-gray-800 px-1 rounded">127.0.0.1:25565</code><BR><BR>
<li>To share the port over your internet IP as an open port, add the <code
class="bg-gray-800 px-1 rounded">--host 0.0.0.0</code> switch:
<pre
class="bg-gray-800 p-2 rounded mt-1 text-sm">holesail <span id="tutorialHolesailHashHost">Not Loaded</span> --host 0.0.0.0</pre><BR>
Note: Using the --host 0.0.0.0 method will require you open your port within your router in order to serve the port from your IP Address remotely.
</li>
</ol>
</div>
</li>
<li>Now you can connect to your own localhost within Minecraft!<br>Connect to:
<code
class="bg-[rgba(10,17,40,0.3)] backdrop-blur-[12px] px-1 rounded border border-[rgba(255,255,255,0.15)]">127.0.0.1:25565</code><br><br>
</li>
<li>To share the port over your internet IP as an open port, add the <code
class="bg-[rgba(10,17,40,0.3)] backdrop-blur-[12px] px-1 rounded border border-[rgba(255,255,255,0.15)]">--host 0.0.0.0</code>
switch:
<pre
class="bg-[rgba(10,17,40,0.3)] backdrop-blur-[12px] p-2 rounded mt-1 text-sm border border-[rgba(255,255,255,0.15)]">holesail <span id="tutorialHolesailHashHost">Not Loaded</span> --host 0.0.0.0</pre>
<br>Note: Using the --host 0.0.0.0 method will require you open your port within your
router in order to serve the port from your IP Address remotely.
</li>
</ol>
</div>
<div class="bg-gray-700 p-4 rounded-md">
<h3 class="text-lg font-semibold mb-3 text-white">Share with Friends</h3>
<p class="text-gray-300 mb-3">
Share this tutorial with your friends so they can connect to your Minecraft server or other services on
their own localhosts!<BR>With Holesail, no public IPs are needed! Everything stays secure and
peer-to-peer!
<div class="feature-card tilt-card p-6">
<h3 class="text-xl minecraft-font mb-4">Share with Friends</h3>
<p class="mb-3">
Share this tutorial with your friends so they can connect to your Minecraft server or other
services on their own localhosts! With Holesail, no public IPs are needed! Everything stays
secure and peer-to-peer!
</p>
<button id="copyTutorial" class="nav-btn">Copy Tutorial</button>
</div>
</div>
</div>
</main>
<footer class="bg-[rgba(10,17,40,0.75)] backdrop-filter backdrop-blur-xl py-8 text-center relative z-2">
<p class="text-sm opacity-90">© 2025 My-MC.Link. All rights reserved.</p>
<p class="text-sm opacity-90 mt-3">
Powered by <a href="https://holesail.io" class="underline text-teal-400 hover:text-blue-400"
target="_blank">Holesail</a> with services
donated by <a href="https://raven-scott.fyi"
class="underline text-teal-400 hover:text-blue-400">SNXRaven</a> |
<a href="https://git.ssh.surf/hypermc/panel" class="underline text-teal-400 hover:text-blue-400"
target="_blank">Source Code</a>
</p>
<button id="copyTutorial"
class="bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded-md transition duration-200">Copy
Tutorial</button>
</div>
</div>
</footer>
</div>
</main>
<footer class="bg-gray-800 py-4">
<div class="container mx-auto px-6 text-center">
<p class="text-gray-400 text-sm">
© 2025 My-MC.Link. All rights reserved.<br>
<a href="https://raven-scott.fyi" target="_blank" class="text-blue-400 hover:text-blue-500">Made with ❤️ by
SNXRaven</a> | <a href="https://git.ssh.surf/hypermc/panel" target="_blank"
class="text-blue-400 hover:text-blue-500">Source Code</a>
</p>
</div>
</footer>
</div>
<script src="js/app.js"></script>
<script src="js/tutorial.js"></script>
<script src="js/app.js"></script>
<script src="js/tutorial.js"></script>
<script src="https://my-mc.link/js/main.js"></script>
</body>

View File

@@ -28,6 +28,61 @@ document.addEventListener('DOMContentLoaded', () => {
'enable-status'
];
const sections = document.querySelectorAll('.section-bg');
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry, index) => {
if (entry.isIntersecting) {
entry.target.style.transform = 'translateY(0)';
entry.target.style.opacity = '1';
entry.target.style.transitionDelay = `${index * 0.1}s`;
}
});
}, { threshold: 0.1 });
function throttle(fn, wait) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= wait) {
fn.apply(this, args);
lastTime = now;
}
};
}
// Hamburger Menu Toggle
const hamburger = document.querySelector('.hamburger');
const mobileNav = document.querySelector('[data-mobile-nav]');
const navLinks = mobileNav.querySelectorAll('a');
// Debounce function to prevent rapid clicks
function debounce(fn, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => fn.apply(this, args), wait);
};
}
hamburger.addEventListener('click', debounce(() => {
mobileNav.classList.toggle('active');
hamburger.classList.toggle('active');
}, 100));
navLinks.forEach(link => {
link.addEventListener('click', () => {
mobileNav.classList.remove('active');
hamburger.classList.remove('active');
});
});
document.addEventListener('click', (e) => {
if (!mobileNav.contains(e.target) && !hamburger.contains(e.target) && mobileNav.classList.contains('active')) {
mobileNav.classList.remove('active');
hamburger.classList.remove('active');
}
});
const elements = {
loginPage: document.getElementById('loginPage'),
loginApiKey: document.getElementById('loginApiKey'),
@@ -321,7 +376,7 @@ document.addEventListener('DOMContentLoaded', () => {
terminal.clear();
} else {
terminal = new Terminal({
rows: 8,
rows: 22,
fontSize: 14,
fontFamily: 'monospace',
theme: {
@@ -497,91 +552,178 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
function toggleSections(status) {
const sections = document.querySelectorAll('.bg-gray-800.p-6.rounded-lg.shadow-lg.mb-6');
const serverStatusSection = document.querySelector('.bg-gray-800.p-6.rounded-lg.shadow-lg.mb-6[data-section="server-status"]');
const editPropertiesBtn = elements.editPropertiesBtn;
const updateModsBtn = elements.updateModsBtn;
const backupBtn = elements.backupBtn;
const sftpBtn = elements.sftpBtn;
const startBtn = document.getElementById('startBtn');
const stopBtn = elements.stopBtn;
const restartBtn = elements.restartBtn;
const sftpBrowserSection = elements.sftpBrowserSection;
if (startBtn) {
if (status.toLowerCase() === 'running') {
startBtn.disabled = true;
startBtn.classList.add('disabled-btn');
} else {
startBtn.disabled = false;
startBtn.classList.remove('disabled-btn');
}
function updateDockerUI(message) {
if (message.error) {
console.log('Docker error detected, setting status to Not Running');
if (elements.serverStatus) elements.serverStatus.textContent = 'Not Running';
toggleSections('Not Running');
return;
}
if (status.toLowerCase() !== 'running') {
const memoryPercent = parseFloat(message.data?.memory?.percent) || 0;
if (state.memoryPercent !== memoryPercent && elements.memoryPercent && memoryMeter) {
memoryMeter.data.datasets[0].data = [memoryPercent, 100 - memoryPercent];
memoryMeter.update();
elements.memoryPercent.textContent = `${memoryPercent.toFixed(1)}%`;
state.memoryPercent = memoryPercent;
}
const cpuPercent = parseFloat(message.data?.cpu) || 0;
if (state.cpuPercent !== cpuPercent && elements.cpuPercent && cpuMeter) {
const scaledCpuPercent = Math.min((cpuPercent / 600) * 100, 100);
cpuMeter.data.datasets[0].data = [scaledCpuPercent, 100 - scaledCpuPercent];
cpuMeter.update();
elements.cpuPercent.textContent = `${cpuPercent.toFixed(1)}%`;
state.cpuPercent = cpuPercent;
}
const status = message.data?.status || 'Unknown';
if (elements.serverStatus) {
elements.serverStatus.textContent = status;
state.serverStatus = status;
toggleSections(status);
}
}
function toggleSections(status) {
const sections = document.querySelectorAll('.section-bg');
const serverStatusSection = document.querySelector('.section-bg[data-section="server-status"]');
const editPropertiesBtn = elements.editPropertiesBtn;
const updateModsBtn = elements.updateModsBtn;
const sftpBtn = elements.sftpBtn;
const startBtn = document.getElementById('startBtn');
const stopBtn = elements.stopBtn;
const restartBtn = elements.restartBtn;
const sftpBrowserSection = elements.sftpBrowserSection;
// Menu items in desktop and mobile nav
const refreshBtn = elements.refresh || document.getElementById('refresh');
const mobileRefreshBtn = elements.mobileRefresh || document.getElementById('mobileRefresh');
const backupBtn = elements.backupBtn;
const mobileBackupBtn = elements.mobileBackupBtn;
const logoutBtn = elements.logoutBtn || document.getElementById('logoutBtn');
const mobileLogoutBtn = elements.mobileLogoutBtn || document.getElementById('mobileLogoutBtn');
if (startBtn) {
if (status.toLowerCase() === 'running') {
startBtn.disabled = true;
startBtn.classList.add('disabled-btn');
} else {
startBtn.disabled = false;
startBtn.classList.remove('disabled-btn');
}
}
if (status.toLowerCase() !== 'running') {
// Hide all sections except Server Status
sections.forEach(section => {
if (section !== serverStatusSection) {
section.classList.add('hidden');
}
if (section !== serverStatusSection) {
section.classList.add('hidden');
}
});
// Hide control buttons
if (editPropertiesBtn) {
editPropertiesBtn.classList.add('hidden');
editPropertiesBtn.classList.add('hidden');
editPropertiesBtn.style.display = 'none';
}
if (updateModsBtn) {
updateModsBtn.classList.add('hidden');
}
if (backupBtn) {
backupBtn.classList.add('hidden');
updateModsBtn.classList.add('hidden');
updateModsBtn.style.display = 'none';
}
if (sftpBtn) {
sftpBtn.classList.add('hidden');
sftpBtn.classList.add('hidden');
}
if (stopBtn) {
stopBtn.disabled = true;
stopBtn.classList.add('disabled-btn');
stopBtn.disabled = true;
stopBtn.classList.add('disabled-btn');
}
if (restartBtn) {
restartBtn.disabled = true;
restartBtn.classList.add('disabled-btn');
restartBtn.disabled = true;
restartBtn.classList.add('disabled-btn');
}
if (sftpBrowserSection) {
sftpBrowserSection.style.display = 'none';
sftpBrowserSection.style.display = 'none';
}
// Hide Refresh, Backup, and Logout buttons in desktop nav
if (refreshBtn) {
refreshBtn.classList.add('hidden');
refreshBtn.style.display = 'none';
}
if (backupBtn) {
backupBtn.classList.add('hidden');
backupBtn.style.display = 'none';
}
if (logoutBtn) {
logoutBtn.classList.add('hidden');
logoutBtn.style.display = 'none';
}
// Hide Refresh, Backup, and Logout menu items in mobile nav
if (mobileRefreshBtn && mobileRefreshBtn.parentElement) {
mobileRefreshBtn.parentElement.classList.add('hidden');
}
if (mobileBackupBtn && mobileBackupBtn.parentElement) {
mobileBackupBtn.parentElement.classList.add('hidden');
}
if (mobileLogoutBtn && mobileLogoutBtn.parentElement) {
mobileLogoutBtn.parentElement.classList.add('hidden');
}
if (!state.hasShownStartNotification) {
showNotification('Server is stopped. Click "Start" to enable all features.', 'error', 'server-stopped');
state.hasShownStartNotification = true;
showNotification('Server is stopped. Click "Start" to enable all features.', 'error', 'server-stopped');
state.hasShownStartNotification = true;
}
} else {
} else {
// Show all sections
sections.forEach(section => {
section.classList.remove('hidden');
section.classList.remove('hidden');
});
// Show control buttons
if (editPropertiesBtn) {
editPropertiesBtn.classList.remove('hidden');
editPropertiesBtn.classList.remove('hidden');
editPropertiesBtn.style.display = '';
}
if (updateModsBtn) {
updateModsBtn.classList.remove('hidden');
}
if (backupBtn) {
backupBtn.classList.remove('hidden');
updateModsBtn.classList.remove('hidden');
updateModsBtn.style.display = '';
}
if (sftpBtn) {
sftpBtn.classList.remove('hidden');
sftpBtn.classList.remove('hidden');
}
if (stopBtn) {
stopBtn.disabled = false;
stopBtn.classList.remove('disabled-btn');
stopBtn.disabled = false;
stopBtn.classList.remove('disabled-btn');
}
if (restartBtn) {
restartBtn.disabled = false;
restartBtn.classList.remove('disabled-btn');
restartBtn.disabled = false;
stopBtn.classList.remove('disabled-btn');
}
if (sftpBrowserSection) {
sftpBrowserSection.style.display = 'block';
sftpBrowserSection.style.display = 'block';
}
// Show Refresh, Backup, and Logout buttons in desktop nav
if (refreshBtn) {
refreshBtn.classList.remove('hidden');
refreshBtn.style.display = '';
}
if (backupBtn) {
backupBtn.classList.remove('hidden');
refreshBtn.style.display = '';
}
if (logoutBtn) {
logoutBtn.classList.remove('hidden');
logoutBtn.style.display = '';
}
// Show Refresh, Backup, and Logout menu items in mobile nav
if (mobileRefreshBtn && mobileRefreshBtn.parentElement) {
mobileRefreshBtn.parentElement.classList.remove('hidden');
}
if (mobileBackupBtn && mobileBackupBtn.parentElement) {
mobileBackupBtn.parentElement.classList.remove('hidden');
}
if (mobileLogoutBtn && mobileLogoutBtn.parentElement) {
mobileLogoutBtn.parentElement.classList.remove('hidden');
}
state.hasShownStartNotification = false;
}
}
}
function updateDockerLogsUI(message) {
if (message.error) {
@@ -751,23 +893,52 @@ document.addEventListener('DOMContentLoaded', () => {
? players.map(player => `
<div class="flex items-center space-x-4 mb-2">
<span class="w-32">${player}</span>
<div class="flex flex-wrap gap-2">
<button class="tell-player bg-blue-600 hover:bg-blue-700 px-2 py-1 rounded text-sm" data-player="${player}">Tell</button>
<button class="give-player bg-green-600 hover:bg-green-700 px-2 py-1 rounded text-sm" data-player="${player}">Give</button>
<button class="teleport-player bg-cyan-600 hover:bg-cyan-700 px-2 py-1 rounded text-sm ${isSinglePlayer ? 'opacity-50 cursor-not-allowed' : ''}" data-player="${player}" ${isSinglePlayer ? 'disabled' : ''}>Teleport</button>
<button class="effect-player bg-teal-600 hover:bg-teal-700 px-2 py-1 rounded text-sm" data-player="${player}">Effect</button>
<button class="op-player bg-purple-600 hover:bg-purple-700 px-2 py-1 rounded text-sm" data-player="${player}">Op</button>
<button class="deop-player bg-purple-600 hover:bg-purple-700 px-2 py-1 rounded text-sm" data-player="${player}">Deop</button>
<button class="kick-player bg-red-600 hover:bg-red-700 px-2 py-1 rounded text-sm" data-player="${player}">Kick</button>
<button class="ban-player bg-red-600 hover:bg-red-700 px-2 py-1 rounded text-sm" data-player="${player}">Ban</button>
<div class="flex flex-col gap-2">
<button class="toggle-actions bg-gray-600 hover:bg-gray-700 px-2 py-1 rounded text-sm" data-player="${player}">Actions</button>
<div class="action-buttons hidden flex-wrap gap-2" data-player="${player}">
<button class="tell-player bg-blue-600 hover:bg-blue-700 px-2 py-1 rounded text-sm" data-player="${player}">Tell</button>
<button class="give-player bg-green-600 hover:bg-green-700 px-2 py-1 rounded text-sm" data-player="${player}">Give</button>
<button class="teleport-player bg-cyan-600 hover:bg-cyan-700 px-2 py-1 rounded text-sm ${isSinglePlayer ? 'opacity-50 cursor-not-allowed' : ''}" data-player="${player}" ${isSinglePlayer ? 'disabled' : ''}>Teleport</button>
<button class="effect-player bg-teal-600 hover:bg-teal-700 px-2 py-1 rounded text-sm" data-player="${player}">Effect</button>
<button class="op-player bg-purple-600 hover:bg-purple-700 px-2 py-1 rounded text-sm" data-player="${player}">Op</button>
<button class="deop-player bg-purple-600 hover:bg-purple-700 px-2 py-1 rounded text-sm" data-player="${player}">Deop</button>
<button class="kick-player bg-red-600 hover:bg-red-700 px-2 py-1 rounded text-sm" data-player="${player}">Kick</button>
<button class="ban-player bg-red-600 hover:bg-red-700 px-2 py-1 rounded text-sm" data-player="${player}">Ban</button>
<button class="close-actions bg-gray-600 hover:bg-gray-700 px-2 py-1 rounded text-sm" data-player="${player}">Close</button>
</div>
</div>
</div>
`).join('')
`).join('')
: 'None';
if (state.playerList !== playerListHtml && elements.playerList) {
elements.playerList.innerHTML = playerListHtml;
state.playerList = playerListHtml;
// Toggle action buttons
document.querySelectorAll('.toggle-actions').forEach(button => {
button.addEventListener('click', () => {
const player = button.getAttribute('data-player').trim();
const actionDiv = document.querySelector(`.action-buttons[data-player="${player}"]`);
if (actionDiv) {
actionDiv.classList.toggle('hidden');
button.textContent = actionDiv.classList.contains('hidden') ? 'Actions' : 'Hide';
}
});
});
// Close action buttons
document.querySelectorAll('.close-actions').forEach(button => {
button.addEventListener('click', () => {
const player = button.getAttribute('data-player').trim();
const actionDiv = document.querySelector(`.action-buttons[data-player="${player}"]`);
const toggleButton = document.querySelector(`.toggle-actions[data-player="${player}"]`);
if (actionDiv && toggleButton) {
actionDiv.classList.add('hidden');
toggleButton.textContent = 'Actions';
}
});
});
document.querySelectorAll('.tell-player').forEach(button => {
button.addEventListener('click', () => {
const player = button.getAttribute('data-player').trim();
@@ -780,7 +951,7 @@ document.addEventListener('DOMContentLoaded', () => {
elements.tellModal.classList.remove('hidden');
});
});
document.querySelectorAll('.give-player').forEach(button => {
button.addEventListener('click', () => {
const player = button.getAttribute('data-player').trim();
@@ -795,7 +966,7 @@ document.addEventListener('DOMContentLoaded', () => {
elements.giveModal.classList.remove('hidden');
});
});
document.querySelectorAll('.teleport-player').forEach(button => {
if (!button.disabled) {
button.addEventListener('click', () => {
@@ -813,7 +984,7 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
});
document.querySelectorAll('.effect-player').forEach(button => {
button.addEventListener('click', () => {
const player = button.getAttribute('data-player').trim();
@@ -826,7 +997,7 @@ document.addEventListener('DOMContentLoaded', () => {
elements.effectModal.classList.remove('hidden');
});
});
document.querySelectorAll('.kick-player').forEach(button => {
button.addEventListener('click', () => {
const player = button.getAttribute('data-player').trim();
@@ -858,7 +1029,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
});
document.querySelectorAll('.ban-player').forEach(button => {
button.addEventListener('click', () => {
const player = button.getAttribute('data-player').trim();
@@ -890,7 +1061,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
});
document.querySelectorAll('.op-player').forEach(button => {
button.addEventListener('click', () => {
const player = button.getAttribute('data-player').trim();
@@ -922,7 +1093,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
});
document.querySelectorAll('.deop-player').forEach(button => {
button.addEventListener('click', () => {
const player = button.getAttribute('data-player').trim();
@@ -1038,9 +1209,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
elements.loginPage.classList.remove('hidden');
elements.mainContent.classList.add('hidden');
elements.authControls.innerHTML = '<input id="apiKey" type="text" placeholder="Enter API Key" class="bg-gray-700 px-4 py-2 rounded text-white">';
elements.apiKeyInput = document.getElementById('apiKey');
elements.apiKeyInput.addEventListener('change', handleApiKeyChange);
if (ws) {
ws.close();
ws = null;
@@ -1069,13 +1238,35 @@ document.addEventListener('DOMContentLoaded', () => {
}
elements.loginPage.classList.add('hidden');
elements.mainContent.classList.remove('hidden');
elements.authControls.innerHTML = '<button id="logoutBtn" class="bg-red-600 hover:bg-red-700 px-4 py-2 rounded">Logout</button>';
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) {
logoutBtn.addEventListener('click', handleLogout);
} else {
console.error('Logout button not found after insertion');
}
// Verify buttons exist
const logoutBtn = document.getElementById('logoutBtn');
const mobileLogoutBtn = document.getElementById('mobileLogoutBtn');
if (logoutBtn) {
console.log('Desktop logout button found');
} else {
console.error('Desktop logout button (#logoutBtn) not found');
}
if (mobileLogoutBtn) {
console.log('Mobile logout button found');
} else {
console.error('Mobile logout button (#mobileLogoutBtn) not found');
}
// Remove any existing logout listeners to prevent duplicates
document.removeEventListener('click', handleLogoutClick);
// Event delegation for logout buttons
function handleLogoutClick(event) {
if (event.target.id === 'logoutBtn' || event.target.id === 'mobileLogoutBtn') {
console.log(`Logout button clicked: ${event.target.id}`);
handleLogout();
}
}
document.addEventListener('click', handleLogoutClick);
initializeCharts();
initializeTerminal();
}
@@ -1095,72 +1286,84 @@ document.addEventListener('DOMContentLoaded', () => {
showLoginPage();
}
async function handleRefresh() {
if (ws && ws.readyState === WebSocket.OPEN) {
const key = `action-refresh`;
const notification = showNotification('Refreshing server data...', 'loading', key);
ws.send(JSON.stringify({ type: 'refresh' }));
initializeTerminal();
setTimeout(() => updateNotification(notification, 'Server data refreshed successfully', 'success', key), 1000);
} else {
showNotification('Not connected to server. Please log in.', 'error', 'ws-disconnected');
}
}
function updatePagination() {
const totalPages = Math.max(1, Math.ceil(totalResults / resultsPerPage));
elements.pagination.innerHTML = '';
const createPageButton = (page, text, disabled = false) => {
const button = document.createElement('button');
button.textContent = text;
button.className = `px-3 py-1 rounded ${disabled || page === currentPage
? 'bg-gray-600 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700'
const button = document.createElement('button');
button.textContent = text;
button.className = `nav-btn ${disabled || page === currentPage
? 'disabled'
: 'active'
}`;
if (!disabled && page !== currentPage) {
button.addEventListener('click', () => {
currentPage = page;
searchMods();
});
}
elements.pagination.appendChild(button);
if (!disabled && page !== currentPage) {
button.addEventListener('click', () => {
currentPage = page;
searchMods();
});
}
elements.pagination.appendChild(button);
};
if (totalResults > 0) {
createPageButton(currentPage - 1, 'Previous', currentPage === 1);
const maxButtons = 5;
const startPage = Math.max(1, currentPage - Math.floor(maxButtons / 2));
const endPage = Math.min(totalPages, startPage + maxButtons - 1);
for (let i = startPage; i <= endPage; i++) {
createPageButton(i, i.toString());
}
createPageButton(currentPage + 1, 'Next', currentPage === totalPages);
createPageButton(currentPage - 1, 'Previous', currentPage === 1);
const maxButtons = 5;
const startPage = Math.max(1, currentPage - Math.floor(maxButtons / 2));
const endPage = Math.min(totalPages, startPage + maxButtons - 1);
for (let i = startPage; i <= endPage; i++) {
createPageButton(i, i.toString());
}
createPageButton(currentPage + 1, 'Next', currentPage === totalPages);
}
}
}
function updateModListPagination() {
function updateModListPagination() {
const filteredMods = state.allMods.filter(mod =>
mod.name.toLowerCase().includes(modListSearchQuery.toLowerCase())
mod.name.toLowerCase().includes(modListSearchQuery.toLowerCase())
);
const totalModPages = Math.max(1, Math.ceil(filteredMods.length / resultsPerPage));
elements.modListPagination.innerHTML = '';
const createPageButton = (page, text, disabled = false) => {
const button = document.createElement('button');
button.textContent = text;
button.className = `px-3 py-1 rounded ${disabled || page === modListCurrentPage
? 'bg-gray-600 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700'
const button = document.createElement('button');
button.textContent = text;
button.className = `nav-btn ${disabled || page === modListCurrentPage
? 'disabled'
: 'active'
}`;
if (!disabled && page !== modListCurrentPage) {
button.addEventListener('click', () => {
modListCurrentPage = page;
renderModList();
});
}
elements.modListPagination.appendChild(button);
if (!disabled && page !== modListCurrentPage) {
button.addEventListener('click', () => {
modListCurrentPage = page;
renderModList();
});
}
elements.modListPagination.appendChild(button);
};
if (filteredMods.length > 0) {
createPageButton(modListCurrentPage - 1, 'Previous', modListCurrentPage === 1);
const maxButtons = 5;
const startPage = Math.max(1, modListCurrentPage - Math.floor(maxButtons / 2));
const endPage = Math.min(totalModPages, startPage + maxButtons - 1);
for (let i = startPage; i <= endPage; i++) {
createPageButton(i, i.toString());
}
createPageButton(modListCurrentPage + 1, 'Next', modListCurrentPage === totalModPages);
createPageButton(modListCurrentPage - 1, 'Previous', modListCurrentPage === 1);
const maxButtons = 5;
const startPage = Math.max(1, modListCurrentPage - Math.floor(maxButtons / 2));
const endPage = Math.min(totalModPages, startPage + maxButtons - 1);
for (let i = startPage; i <= endPage; i++) {
createPageButton(i, i.toString());
}
createPageButton(modListCurrentPage + 1, 'Next', modListCurrentPage === totalModPages);
}
}
}
function closeSearch() {
elements.modSearch.value = '';
@@ -1648,9 +1851,6 @@ document.addEventListener('DOMContentLoaded', () => {
if (filteredSettings.includes(key)) {
return;
}
console.log(`Rendering field for ${key}: ${value}`);
const fieldDiv = document.createElement('div');
fieldDiv.className = 'flex items-center space-x-2';
fieldDiv.style.display = 'flex';
@@ -1687,7 +1887,6 @@ document.addEventListener('DOMContentLoaded', () => {
switchContainer.className = 'relative inline-block';
switchContainer.setAttribute('role', 'switch');
switchContainer.setAttribute('aria-checked', value.toLowerCase());
switchContainer.setAttribute('tabindex', '0');
switchContainer.dataset.name = key;
switchContainer.style.width = '40px';
switchContainer.style.height = '24px';
@@ -1918,7 +2117,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
elements.apiKeyInput.addEventListener('change', handleApiKeyChange);
// elements.apiKeyInput.addEventListener('change', handleApiKeyChange);
elements.generateMyLinkBtn.addEventListener('click', async () => {
try {
@@ -1969,46 +2168,47 @@ document.addEventListener('DOMContentLoaded', () => {
document.getElementById('startBtn').addEventListener('click', async () => {
try {
const key = `action-start`;
const notification = showNotification('Starting server...', 'loading', key);
await wsRequest('/start');
initializeTerminal();
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'subscribe', endpoints: ['docker', 'docker-logs'] }));
const messageHandler = (event) => {
try {
const message = JSON.parse(event.data);
if (message.type === 'docker') {
state.serverStatus = message.data?.status || 'Unknown';
elements.serverStatus.textContent = state.serverStatus;
toggleSections(state.serverStatus);
if (message.data?.status === 'running') {
updateNotification(notification, 'Server started successfully', 'success', key);
ws.removeEventListener('message', messageHandler);
}
}
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
};
ws.addEventListener('message', messageHandler);
setTimeout(() => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.removeEventListener('message', messageHandler);
if (state.serverStatus !== 'running') {
updateNotification(notification, 'Failed to start server. Please try again.', 'error', key);
toggleSections(state.serverStatus);
}
}
}, 30000);
} else {
updateNotification(notification, 'Not connected to server. Please log in.', 'error', key);
}
const key = `action-start`;
const notification = showNotification('Starting server...', 'loading', key);
await wsRequest('/start');
initializeTerminal();
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'subscribe', endpoints: ['docker', 'docker-logs'] }));
const messageHandler = (event) => {
try {
const message = JSON.parse(event.data);
if (message.type === 'docker') {
state.serverStatus = message.data?.status || 'Unknown';
elements.serverStatus.textContent = state.serverStatus;
toggleSections(state.serverStatus);
if (message.data?.status === 'running') {
updateNotification(notification, 'Server started successfully', 'success', key);
ws.removeEventListener('message', messageHandler);
}
}
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
};
ws.addEventListener('message', messageHandler);
setTimeout(() => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.removeEventListener('message', messageHandler);
if (state.serverStatus !== 'running') {
updateNotification(notification, 'Failed to start server. Please try again.', 'error', key);
toggleSections(state.serverStatus);
}
}
}, 30000);
} else {
updateNotification(notification, 'Not connected to server. Please log in.', 'error', key);
}
} catch (error) {
console.error('Start server error:', error);
showNotification(`Failed to start server: ${error.message}`, 'error', 'start-error');
console.error('Start server error:', error);
showNotification(`Failed to start server: ${error.message}`, 'error', 'start-error');
}
});
});
document.getElementById('stopBtn').addEventListener('click', async () => {
try {
@@ -2065,6 +2265,8 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
elements.updateModsBtn.addEventListener('click', updateMods);
elements.closeUpdateModsBtn.addEventListener('click', () => {
@@ -2177,4 +2379,42 @@ document.addEventListener('DOMContentLoaded', () => {
showLoginPage();
}
window.showNotification = showNotification;
// Add this at the end of the document.addEventListener('DOMContentLoaded', ...) block
function applyResponsiveStyles() {
const sections = document.querySelectorAll('.section-bg');
sections.forEach(section => {
section.style.padding = window.innerWidth <= 640 ? '1rem' : window.innerWidth <= 768 ? '1.5rem' : '2rem';
section.style.maxWidth = '100%';
section.style.overflowX = 'hidden';
});
const iframes = document.querySelectorAll('#sftpIframe');
iframes.forEach(iframe => {
iframe.style.height = window.innerWidth <= 640 ? '300px' : window.innerWidth <= 768 ? '400px' : '650px';
iframe.style.minHeight = iframe.style.height;
iframe.style.width = '100%';
iframe.style.maxWidth = '100%';
});
const canvases = document.querySelectorAll('#memoryMeter, #cpuMeter');
canvases.forEach(canvas => {
canvas.style.width = window.innerWidth <= 640 ? '80px' : window.innerWidth <= 768 ? '100px' : '150px';
canvas.style.height = canvas.style.width;
});
const terminals = document.querySelectorAll('#dockerLogsTerminal');
terminals.forEach(terminal => {
terminal.style.maxHeight = window.innerWidth <= 640 ? '7rem' : window.innerWidth <= 768 ? '8rem' : '12rem';
});
const consoles = document.querySelectorAll('#consoleOutput');
consoles.forEach(console => {
console.style.height = window.innerWidth <= 640 ? '7rem' : window.innerWidth <= 768 ? '8rem' : '12rem';
});
}
window.addEventListener('resize', applyResponsiveStyles);
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 642 KiB

After

Width:  |  Height:  |  Size: 942 KiB