Compare commits

...

10 commits
v0.1 ... main

Author SHA1 Message Date
8ea8fd3567 Manager modów/pluginów pobiera zależności 2025-04-14 19:06:02 +02:00
694134ccec Usunięcie mapy z panelu
Zbyt dużo bugów
2025-04-14 00:30:32 +02:00
efa85c5070 Dodanie mapy serwera i poprawka pobierania modów
- Dodaje #16 - Mape serwera w panelu (z pluginem squaremap)
- Poprawia pobieranie modów z Modrinth (dobra wersja i typ)
2025-04-14 00:27:03 +02:00
ee189b7e72 Przeniesienie JS do osobnych plików 2025-04-13 18:43:46 +02:00
8ee15c1928 Instalowanie modów/pluginów z panelu
Kocham Modrinth :3
Dodaje #15
2025-04-13 17:38:54 +02:00
3b689196a0 Zabezpieczenie API 2025-04-13 17:11:29 +02:00
dfb6f5f370 Dodano ładniejszą strone pod error 404
Dodaje #12
2025-04-13 16:29:26 +02:00
2b9845ac85 Zmiana linku do donate'ów 2025-04-12 14:42:12 +02:00
b0e84a95eb Strona do donate'ów 2025-04-12 12:37:09 +02:00
bb4fc9e488 Reklamy
(tylko w 1 miejscu i fałszywe spokojnie)
Dodaje #9
2025-04-12 11:29:05 +02:00
16 changed files with 722 additions and 303 deletions

1
.gitignore vendored
View file

@ -128,3 +128,4 @@ dmypy.json
client_secrets.json client_secrets.json
ports.json ports.json
/servers/

View file

@ -4,7 +4,8 @@ import shutil
import subprocess import subprocess
import docker import docker
from flask import Blueprint, jsonify, request, send_from_directory, abort import requests
from flask import Blueprint, jsonify, request, send_from_directory, abort, Response
from .auth import oidc from .auth import oidc
from .docker_utils import start_server, stop_server, restart_server, get_logs, delete_server, save_server_info, \ from .docker_utils import start_server, stop_server, restart_server, get_logs, delete_server, save_server_info, \
@ -17,6 +18,7 @@ client = docker.from_env()
# Server Deployment # Server Deployment
@api.route('/setup', methods=['POST']) @api.route('/setup', methods=['POST'])
@oidc.require_login
def setup_server(): def setup_server():
data = request.get_json() data = request.get_json()
username = oidc.user_getfield('preferred_username') username = oidc.user_getfield('preferred_username')
@ -31,9 +33,9 @@ def setup_server():
@api.route('/delete', methods=['POST']) @api.route('/delete', methods=['POST'])
@oidc.require_login
def delete(): def delete():
data = request.get_json() username = oidc.user_getfield('preferred_username')
username = data.get("username")
if not username: if not username:
return jsonify({"error": "Brak nazwy użytkownika"}), 400 return jsonify({"error": "Brak nazwy użytkownika"}), 400
@ -44,8 +46,9 @@ def delete():
# Server Controls # Server Controls
@api.route('/start', methods=['POST']) @api.route('/start', methods=['POST'])
@oidc.require_login
def start(): def start():
username = request.json['username'] username = oidc.user_getfield('preferred_username')
setup_file_path = f"./servers/mc-{username}/server_info.json" setup_file_path = f"./servers/mc-{username}/server_info.json"
if not os.path.exists(setup_file_path): if not os.path.exists(setup_file_path):
@ -66,22 +69,25 @@ def start():
@api.route('/stop', methods=['POST']) @api.route('/stop', methods=['POST'])
@oidc.require_login
def stop(): def stop():
username = request.json['username'] username = oidc.user_getfield('preferred_username')
stop_server(username) stop_server(username)
return jsonify({"status": "stopped"}) return jsonify({"status": "stopped"})
@api.route('/restart', methods=['POST']) @api.route('/restart', methods=['POST'])
@oidc.require_login
def restart(): def restart():
username = request.json['username'] username = oidc.user_getfield('preferred_username')
restart_server(username) restart_server(username)
return jsonify({"status": "restarted"}) return jsonify({"status": "restarted"})
@api.route('/logs', methods=['GET']) @api.route('/logs', methods=['GET'])
@oidc.require_login
def logs(): def logs():
username = request.args.get('username') username = oidc.user_getfield('preferred_username')
return jsonify({"logs": get_logs(username)}) return jsonify({"logs": get_logs(username)})
@ -92,6 +98,9 @@ def send_command():
username = data['username'] username = data['username']
command = data['command'] command = data['command']
if username != oidc.user_getfield('preferred_username'):
return jsonify({"error": "Unauthorized request."}), 403
container_name = f"mc-{username}" container_name = f"mc-{username}"
try: try:
@ -107,8 +116,9 @@ def send_command():
# Files APIs (Upload, download, delete) # Files APIs (Upload, download, delete)
@api.route('/files', methods=['GET']) @api.route('/files', methods=['GET'])
@oidc.require_login
def list_files(): def list_files():
username = request.args.get('username') username = oidc.user_getfield('preferred_username')
path = request.args.get('path', '') path = request.args.get('path', '')
base_path = os.path.abspath(f'./servers/mc-{username}') base_path = os.path.abspath(f'./servers/mc-{username}')
@ -134,8 +144,9 @@ def list_files():
@api.route('/files/download', methods=['GET']) @api.route('/files/download', methods=['GET'])
@oidc.require_login
def download_file(): def download_file():
username = request.args.get('username') username = oidc.user_getfield('preferred_username')
path = request.args.get('path') path = request.args.get('path')
base_path = os.path.abspath(f'./servers/mc-{username}') base_path = os.path.abspath(f'./servers/mc-{username}')
@ -150,8 +161,9 @@ def download_file():
@api.route('/files/upload', methods=['POST']) @api.route('/files/upload', methods=['POST'])
@oidc.require_login
def upload_file(): def upload_file():
username = request.form.get('username') username = oidc.user_getfield('preferred_username')
path = request.form.get('path', '') path = request.form.get('path', '')
file = request.files['files'] file = request.files['files']
base_path = os.path.abspath(f'./servers/mc-{username}') base_path = os.path.abspath(f'./servers/mc-{username}')
@ -167,12 +179,17 @@ def upload_file():
@api.route('/files/delete', methods=['POST']) @api.route('/files/delete', methods=['POST'])
@oidc.require_login
def delete_file_or_folder(): def delete_file_or_folder():
data = request.get_json() data = request.get_json()
username = data.get('username') username = data.get('username')
if username != oidc.user_getfield('preferred_username'):
return jsonify({"error": "Unauthorized request."}), 403
path = data.get('path') path = data.get('path')
base_path = os.path.abspath(f'./servers/mc-{username}') base_path = os.path.abspath(f'./servers/mc-{username}')
target_path = os.path.abspath(os.path.join(base_path, path)) target_path = os.path.abspath(os.path.join(base_path, path))
if not target_path.startswith(base_path): if not target_path.startswith(base_path):
return abort(403) return abort(403)
if not os.path.exists(target_path): if not os.path.exists(target_path):
@ -189,21 +206,27 @@ def delete_file_or_folder():
# Server config # Server config
@api.route('/config') @api.route('/config')
@oidc.require_login
def get_config(): def get_config():
username = request.args.get('username') username = oidc.user_getfield('preferred_username')
server_info_path = f'./servers/mc-{username}/server_info.json' server_info_path = f'./servers/mc-{username}/server_info.json'
if not os.path.exists(server_info_path): if not os.path.exists(server_info_path):
return jsonify({"success": False, "message": "Server config not found"}) return jsonify({"success": False, "message": "Server config not found"})
with open(server_info_path, 'r') as f: with open(server_info_path, 'r') as f:
server_info = json.load(f) server_info = json.load(f)
return jsonify({"success": True, "config": server_info["config"], "version": server_info["version"], "type": server_info["type"]}) return jsonify({"success": True, "config": server_info["config"], "version": server_info["version"],
"type": server_info["type"]})
@api.route('/config', methods=['POST']) @api.route('/config', methods=['POST'])
@oidc.require_login
def update_config(): def update_config():
data = request.json data = request.json
username = data.get('username') username = data.get('username')
if username != oidc.user_getfield('preferred_username'):
return jsonify({"error": "Unauthorized request."}), 403
incoming_config = data.get('config', {}) incoming_config = data.get('config', {})
server_info_path = f'./servers/mc-{username}/server_info.json' server_info_path = f'./servers/mc-{username}/server_info.json'
if os.path.exists(server_info_path): if os.path.exists(server_info_path):
@ -211,9 +234,11 @@ def update_config():
server_info = json.load(f) server_info = json.load(f)
else: else:
server_info = DEFAULT_CONFIG.copy() server_info = DEFAULT_CONFIG.copy()
for key in ["type", "version"]: for key in ["type", "version"]:
if key in incoming_config: if key in incoming_config:
server_info[key] = incoming_config.pop(key) server_info[key] = incoming_config.pop(key)
formatted_config = {key.replace('_', '-'): value for key, value in incoming_config.items()} formatted_config = {key.replace('_', '-'): value for key, value in incoming_config.items()}
server_info["config"] = formatted_config server_info["config"] = formatted_config
with open(server_info_path, 'w') as f: with open(server_info_path, 'w') as f:
@ -235,8 +260,9 @@ def status():
@api.route('/stats', methods=['GET']) @api.route('/stats', methods=['GET'])
@oidc.require_login
def stats(): def stats():
username = request.args.get("username") username = oidc.user_getfield("preferred_username")
container_name = f"mc-{username}" container_name = f"mc-{username}"
try: try:
@ -268,3 +294,100 @@ def stats():
}) })
except docker.errors.NotFound: except docker.errors.NotFound:
return jsonify({"error": "Container not found"}), 404 return jsonify({"error": "Container not found"}), 404
# Modrinth
@api.route('/modrinth/search')
def modrinth_search():
query = request.args.get("query")
server_type = request.args.get("type") # 'fabric', 'paper', etc.
version = request.args.get("version")
categories = {
'fabric': 'mod',
'paper': 'plugin'
}
category = categories.get(server_type, 'mod') # fallback
response = requests.get(
f"https://api.modrinth.com/v2/search",
params={
"query": query,
"facets": f'[["project_type:{category}"],["categories:{server_type}"],["versions:{version}"]]',
"limit": 10
}
)
return jsonify(response.json())
@api.route('/modrinth/download', methods=['POST'])
@oidc.require_login
def download_mod():
data = request.json
project_id = data['project_id']
username = data['username']
server_type = data['type']
server_version = data['version']
base_path = f"./servers/mc-{username}/{'plugins' if server_type == 'paper' else 'mods'}"
os.makedirs(base_path, exist_ok=True)
downloaded_files = []
visited_versions = set()
def download_project(pid, version_id=None):
if version_id:
version_data = requests.get(f"https://api.modrinth.com/v2/version/{version_id}").json()
else:
all_versions = requests.get(f"https://api.modrinth.com/v2/project/{pid}/version").json()
version_data = next(
(v for v in all_versions
if server_version in v['game_versions']
and server_type in v['loaders']
and v.get('server_side') != 'unsupported'),
None)
if not version_data:
print(f"[SKIP] No compatible version found for {pid}")
return
if version_data["id"] in visited_versions:
return
visited_versions.add(version_data["id"])
if version_data.get('server_side') == 'unsupported':
print(f"[SKIP] {version_data.get('name', 'Nieznana nazwa')} is client-only")
return
file = next((f for f in version_data['files'] if f.get('primary', True)), version_data['files'][0])
file_path = os.path.join(base_path, file['filename'])
if not os.path.exists(file_path):
print(f"[DOWNLOAD] {file['filename']}")
with requests.get(file['url'], stream=True) as r:
with open(file_path, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
downloaded_files.append({
"filename": file['filename'],
"name": version_data.get("name") or version_data.get("version_number") or "Nieznana wersja",
"project_id": version_data["project_id"],
"version_id": version_data["id"]
})
for dep in version_data.get('dependencies', []):
if dep.get('version_id'):
try:
download_project(dep['project_id'], dep['version_id'])
except Exception as e:
print(f"[ERROR] Failed to fetch dep version {dep['version_id']}: {e}")
elif dep.get('project_id'):
try:
download_project(dep['project_id'])
except Exception as e:
print(f"[ERROR] Failed to fetch dep project {dep['project_id']}: {e}")
download_project(project_id)
return jsonify({"success": True, "downloaded": downloaded_files})

View file

@ -55,8 +55,6 @@ def get_server_config(username):
server_info["config"] = server_config server_info["config"] = server_config
with open(server_info_path, "w") as f: with open(server_info_path, "w") as f:
json.dump(server_info, f, indent=4) json.dump(server_info, f, indent=4)
print(f"Loaded config: {server_config}")
return server_config, server_type, server_version return server_config, server_type, server_version
@ -91,6 +89,7 @@ def start_server(username):
environment = { environment = {
"EULA": "TRUE", "EULA": "TRUE",
"SERVER_PORT": ports[0], "SERVER_PORT": ports[0],
"ENABLE_RCON": "TRUE",
"MOTD": f"Serwer użytkownika §9{username}", "MOTD": f"Serwer użytkownika §9{username}",
"TYPE": server_type, "TYPE": server_type,
"VERSION": server_version, "VERSION": server_version,

View file

@ -1,11 +1,12 @@
import os import os
import random
from flask import Blueprint, render_template from flask import Blueprint, render_template, jsonify
from .auth import oidc from .auth import oidc
from .port_utils import get_user_ports from .port_utils import get_user_ports
from dotenv import load_dotenv from dotenv import load_dotenv
main = Blueprint('main', __name__) main = Blueprint('main', __name__, static_folder='static')
load_dotenv() load_dotenv()
@ -33,7 +34,29 @@ def dashboard():
has_server = os.path.exists(server_path) has_server = os.path.exists(server_path)
ip = os.getenv("SERVER_IP") ip = os.getenv("SERVER_IP")
ports = get_user_ports(username) ports = get_user_ports(username)
if (has_server):
if has_server:
return render_template('dashboard.html', username=username, ip=ip, ports=ports) return render_template('dashboard.html', username=username, ip=ip, ports=ports)
else: else:
return render_template('setup.html', username=username, ip=ip, ports=ports) return render_template('setup.html', username=username, ip=ip, ports=ports)
@main.route('/donate')
def donate():
return render_template('donate.html')
@main.route("/ads/list")
def list_ads():
ads_dir = os.path.join(main.static_folder, "ads")
files = [
f"/static/ads/{f}"
for f in os.listdir(ads_dir)
if f.lower().endswith(".png")
]
return jsonify(files)
@main.app_errorhandler(404)
def page_not_found(e):
return render_template("404.html"), 404

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

BIN
app/static/ads/iPhoneAd.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

52
app/static/js/config.js Normal file
View file

@ -0,0 +1,52 @@
function loadConfig() {
fetch(`/api/config?username=${username}`)
.then(response => response.json())
.then(data => {
const cfg = data.config;
document.getElementById("server-type").value = data.type;
document.getElementById("server-version").value = data.version;
document.getElementById("max-players").value = cfg["max-players"];
document.getElementById("pvp").checked = cfg["pvp"];
document.getElementById("difficulty").value = cfg["difficulty"];
document.getElementById("online-mode").checked = cfg["online-mode"];
document.getElementById("spawn-monsters").checked = cfg["spawn-monsters"];
document.getElementById("spawn-animals").checked = cfg["spawn-animals"];
document.getElementById("allow-nether").checked = cfg["allow-nether"];
document.getElementById("max-build-height").value = cfg["max-build-height"];
document.getElementById("view-distance").value = cfg["view-distance"];
})
.catch(err => {
console.error("Błąd wczytywania konfiguracji: ", err);
alert("Nie udało się załadować konfiguracji serwera.");
});
}
document.querySelector('[onclick="showTab(\'config\')"]').addEventListener('click', loadConfig);
function saveConfig() {
const config = {
type: document.getElementById("server-type").value,
version: document.getElementById("server-version").value,
max_players: document.getElementById("max-players").value,
pvp: document.getElementById("pvp").checked,
difficulty: document.getElementById("difficulty").value,
online_mode: document.getElementById("online-mode").checked,
spawn_monsters: document.getElementById("spawn-monsters").checked,
spawn_animals: document.getElementById("spawn-animals").checked,
allow_nether: document.getElementById("allow-nether").checked,
max_build_height: document.getElementById("max-build-height").value,
view_distance: document.getElementById("view-distance").value
};
fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, config })
}).then(res => res.json()).then(data => {
if (data.success) {
alert("Konfiguracja została zapisana!");
} else {
alert("Błąd zapisywania konfiguracji.");
}
});
}

23
app/static/js/console.js Normal file
View file

@ -0,0 +1,23 @@
function sendCommand() {
const input = $("console-command");
const command = input.value.trim();
if (!command) return;
fetch('/api/command', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, command })
}).then(res => {
if (!res.ok) throw new Error("Nie udało się wysłać komendy.");
input.value = '';
}).catch(err => {
alert("Błąd wysyłania komendy.");
console.error(err);
});
}
function loadLogs() {
fetch(`/api/logs?username=${username}`)
.then(r => r.json())
.then(data => { $("console-log").textContent = data.logs; });
}

19
app/static/js/controls.js vendored Normal file
View file

@ -0,0 +1,19 @@
function sendAction(action) {
fetch(`/api/${action}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
}).then(r => r.json()).then(console.log);
}
function deleteServer() {
if (!confirm("Na pewno chcesz usunąć swój serwer? Tej operacji nie można cofnąć!")) return;
fetch('/api/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
})
.then(res => res.ok ? res.json() : res.text().then(t => { throw new Error(t); }))
.then(data => { alert(data.message); location.reload(); })
.catch(err => { alert("Błąd usuwania serwera."); console.error(err); });
}

123
app/static/js/files.js Normal file
View file

@ -0,0 +1,123 @@
function loadFileList(path = '') {
currentPath = path;
$("current-path").textContent = "/" + path;
fetch(`/api/files?username=${username}&path=${encodeURIComponent(path)}`)
.then(r => r.json())
.then(data => {
const list = $("file-list");
list.innerHTML = '';
if (path) {
list.innerHTML += `<li><a class="non-link" href="#" onclick="loadFileList('${path.split('/').slice(0, -1).join('/')}')">⬅️ ..</a></li>`;
}
data.sort((a, b) => a.name.localeCompare(b.name));
data.forEach(entry => {
const itemPath = path ? `${path}/${entry.name}` : entry.name;
const html = entry.is_dir
? `<li>📁 <a class="non-link" href="#" onclick="loadFileList('${itemPath}')">${entry.name}</a>
<a class="non-link" onclick="deleteItem('${itemPath}')">
<i class="fa-solid fa-trash"></i>
</a>
</li>`
: `<li>📄 ${entry.name}
<a class="non-link" href="/api/files/download?username=${username}&path=${encodeURIComponent(itemPath)}" target="_blank">
<i class="fa-solid fa-download"></i>
</a>
<a class="non-link" onclick="deleteItem('${itemPath}')">
<i class="fa-solid fa-trash"></i>
</a>
</li>`;
list.innerHTML += html;
});
});
}
function deleteItem(path) {
if (!confirm(`Czy na pewno chcesz usunąć: ${path}?`)) return;
fetch('/api/files/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, path })
})
.then(res => res.json())
.then(data => {
if (data.success) {
loadFileList(currentPath);
} else {
alert("Błąd podczas usuwania: " + (data.error || "Nieznany"));
}
})
.catch(err => {
console.error(err);
alert("Błąd połączenia z serwerem.");
});
}
document.querySelector('[onclick="showTab(\'files\')"]').addEventListener('click', () => loadFileList());
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('file-input');
const progressBar = document.getElementById('progress-bar');
const progressWrapper = document.getElementById('upload-progress');
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', e => {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', e => {
e.preventDefault();
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', e => {
e.preventDefault();
dropZone.classList.remove('dragover');
handleFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', () => {
handleFiles(fileInput.files);
});
function handleFiles(files) {
if (!files.length) return;
const formData = new FormData();
for (const file of files) {
formData.append('files', file);
}
formData.append('username', username);
formData.append('path', currentPath);
progressWrapper.classList.remove('hidden');
progressBar.style.width = '0%';
const xhr = new XMLHttpRequest();
xhr.open('POST', `/api/files/upload?username=${username}&path=${encodeURIComponent(currentPath)}`);
xhr.upload.addEventListener('progress', e => {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100;
progressBar.style.width = `${percent}%`;
}
});
xhr.onload = () => {
progressWrapper.classList.add('hidden');
if (xhr.status === 200) {
loadFileList(currentPath);
} else {
alert("Upload się nie udał.");
}
};
xhr.onerror = () => {
progressWrapper.classList.add('hidden');
alert("Upload się nie udał.");
};
xhr.send(formData);
}

68
app/static/js/mods.js Normal file
View file

@ -0,0 +1,68 @@
function searchMods() {
const query = document.getElementById("mod-search").value;
fetch('/api/config')
.then(response => response.json())
.then(configData => {
console.log(configData);
if (configData.success) {
const serverType = configData.type;
const serverVersion = configData.version;
if (serverType && serverVersion) {
fetch(`/api/modrinth/search?query=${encodeURIComponent(query)}&type=${serverType}&version=${serverVersion}`)
.then(r => r.json())
.then(data => {
const results = document.getElementById("mod-results");
results.innerHTML = "";
data.hits.forEach(mod => {
const modEl = document.createElement("div");
modEl.innerHTML = `
<strong>${mod.title}</strong> - <a onclick="installMod('${mod.project_id}')">Zainstaluj</a><hr>`;
results.appendChild(modEl);
});
});
} else {
console.error('Server type or version not available');
}
} else {
console.error('Server config not found');
}
})
.catch(err => {
console.error('Error fetching server config:', err);
});
}
function installMod(projectId) {
fetch('/api/config')
.then(response => response.json())
.then(configData => {
if (configData.success) {
const serverType = configData.type;
const serverVersion = configData.version;
if (serverType && serverVersion) {
fetch('/api/modrinth/download', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ project_id: projectId, username, type: serverType, version: serverVersion }),
})
.then(r => r.json())
.then(data => {
if (data.success) {
const modFiles = data.downloaded.map(mod => "- " + mod.filename).join("\n");
alert(`Zainstalowano:\n${modFiles}`);
loadFileList();
} else {
alert("Nie udało się zainstalować moda.");
}
});
} else {
console.error('Server type or version not available');
}
} else {
console.error('Server config not found');
}
})
.catch(err => {
console.error('Error fetching server config:', err);
});
}

View file

@ -86,6 +86,20 @@ h2 {
transition: all 0.3s ease; transition: all 0.3s ease;
} }
/* Ads */
.ad-banner {
display: flex;
justify-content: center;
margin: 20px 0;
}
.ad-banner img {
width: 200px;
height: auto;
border-radius: 12px;
box-shadow: 0 0 12px rgba(0, 0, 0, 0.3);
}
/* Tabs */ /* Tabs */
.tabs { .tabs {
display: grid; display: grid;
@ -119,7 +133,7 @@ h2 {
.square-button { .square-button {
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
width: 100px; width: 200px;
background-color: #1a1a2e; background-color: #1a1a2e;
border: 2px solid #2c2c3c; border: 2px solid #2c2c3c;
border-radius: 16px; border-radius: 16px;
@ -514,22 +528,166 @@ a.non-link:hover {
justify-content: center; justify-content: center;
} }
/* Mobile Tweaks */ /* 404 Error */
@media (max-width: 600px) { .error-container {
body { max-width: 800px;
padding: 24px 12px; margin: 0 auto;
padding: 4rem 2rem;
text-align: center;
color: #e0e0e0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
} }
.glitch-title {
font-size: 6rem;
font-weight: 900;
color: #ff0066;
text-shadow: 2px 2px #00ffff, -2px -2px #ff00ff;
animation: glitch 1s infinite;
}
@keyframes glitch {
0% { text-shadow: 2px 2px #00ffff, -2px -2px #ff00ff; }
20% { text-shadow: -2px -2px #00ffff, 2px 2px #ff00ff; }
40% { text-shadow: 2px -2px #00ffff, -2px 2px #ff00ff; }
60% { text-shadow: -2px 2px #00ffff, 2px -2px #ff00ff; }
80% { text-shadow: 2px 2px #00ffff, -2px -2px #ff00ff; }
100% { text-shadow: -2px -2px #00ffff, 2px 2px #ff00ff; }
}
.error-message {
font-size: 1.5rem;
margin-bottom: 2rem;
}
.back-button {
display: inline-block;
padding: 0.75rem 1.5rem;
background-color: #222;
color: #fff;
border: 2px solid #fff;
border-radius: 8px;
text-decoration: none;
font-weight: bold;
transition: background-color 0.3s, color 0.3s;
}
.back-button:hover {
background-color: #fff;
color: #000;
}
/* Mobile Tweaks */
@media (max-width: 480px) {
html {
font-size: clamp(14px, 2.5vw, 16px);
}
/* Heading adjustment */
h2 {
font-size: 1.5rem;
}
/* Dashboard Card */
.dashboard-card { .dashboard-card {
padding: 16px;
max-width: 100%;
}
/* Tab buttons */
.tab-button {
padding: 10px;
font-size: 0.85rem;
}
/* Square button adjustments */
.square-button {
width: 100%;
aspect-ratio: unset;
flex-direction: row;
justify-content: flex-start;
gap: 12px;
padding: 10px;
font-size: 0.9rem;
}
.square-button img {
width: 32px;
margin: 0;
}
/* Navigation Bar */
.top-nav {
padding: 12px;
font-size: 0.9rem;
flex-direction: column;
align-items: flex-start;
}
.nav-links a {
margin-left: 10px;
font-size: 0.9rem;
}
/* Console Output */
.console-output {
font-size: 0.8rem;
height: 160px;
}
/* Chart container */
.chart-container {
width: 100%;
height: auto;
padding: 12px;
}
/* Buttons */
.btn {
font-size: 0.9rem;
padding: 12px;
width: 100%;
}
/* Form layout */
form {
padding: 0 8px;
}
/* Tab content spacing */
.tab-content {
margin-top: 8px;
}
/* Particle background on small screens */
#particle-background {
display: none; /* Improves performance */
}
/* Charts Row - Allow horizontal scroll */
.charts-row {
overflow-x: auto;
padding-bottom: 10px;
}
/* Input Fields */
input[type="text"], input[type="number"], select {
width: 100%;
}
/* Adjust drop zone */
#drop-zone {
padding: 20px; padding: 20px;
} }
.btn { /* Adjust text area height */
width: 100%; .console-output {
margin: 8px 0; height: 160px;
}
} }
.console-output { /* Additional general fixes for small screens */
height: 180px; * {
} max-width: 100%;
word-wrap: break-word;
} }

26
app/templates/404.html Normal file
View file

@ -0,0 +1,26 @@
{% extends "layout.html" %}
{% block title %}404 - Zgubiłeś się w multiversum{% endblock %}
{% block content %}
<div class="error-container">
<h1 class="glitch-title">404</h1>
<p id="error-message" class="error-message"></p>
<a href="/" class="back-button">Powrót do bezpiecznego świata</a>
</div>
<script>
const messages = [
"Właśnie próbowałeś wejść na Peron 9 i ¾ bez rozpędu. Niestety, ściana nie puściła.",
"Strona zniknęła w glitchu Matrixa. Może weź czerwoną pigułkę?",
"Demogorgon pożarł zawartość strony. Sorki...",
"Strona zniknęła w anomalii czasoprzestrzennej. Też nie wiemy, co się stało.",
"Thanos pstryknął palcami i ta strona zniknęła.",
"Ups. Gandalf powiedział: \"Nie przejdziesz!\" i strona się rozpadła.",
"Uwaga! Ta strona została zawieszona przez Biuro Czasu TVA.",
"Błąd jak z Simsów - ktoś wyjął drabinkę z basenu tej strony. Ona już nie wróci."
];
const randomMessage = messages[Math.floor(Math.random() * messages.length)];
document.getElementById('error-message').textContent = randomMessage;
</script>
{% endblock %}

View file

@ -1,13 +1,16 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block content %} {% block content %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<h2>Twój serwer Minecraft</h2> <h2>Twój serwer Minecraft</h2>
<div class="tabs"> <div class="tabs">
<button class="tab-button" onclick="showTab('controls')">Sterowanie 🎛️</button> <button class="tab-button" onclick="showTab('controls')">Sterowanie 🎛️</button>
<button class="tab-button" onclick="showTab('console')">Konsola 📟</button> <button class="tab-button" onclick="showTab('console')">Konsola 📟</button>
<button class="tab-button" onclick="showTab('files')">Pliki 📁</button> <button class="tab-button" onclick="showTab('files')">Pliki 📁</button>
</div>
<div class="tabs">
<button class="tab-button" onclick="showTab('config')">Konfiguracja 🛠️</button> <button class="tab-button" onclick="showTab('config')">Konfiguracja 🛠️</button>
<button class="tab-button" onclick="showTab('mods')">Mody/Pluginy 🧩</button>
<button class="tab-button" onclick="showTab('statistics')">Statystyki 📈</button> <button class="tab-button" onclick="showTab('statistics')">Statystyki 📈</button>
</div> </div>
@ -109,15 +112,26 @@
</form> </form>
</div> </div>
<div class="tab-panel" id="mods">
<h2>Zainstaluj mody/pluginy z Modrinth</h2>
<input type="text" id="mod-search" placeholder="Wyszukaj mod/plugin..." oninput="searchMods()">
<div id="mod-results"></div>
</div>
<div class="tab-panel" id="statistics"> <div class="tab-panel" id="statistics">
<div class="charts-row"> <div class="charts-row">
<div class="chart-container"><canvas id="cpuChart"></canvas></div> <div class="chart-container"><canvas id="cpuChart"></canvas></div>
<div class="chart-container"><canvas id="ramChart"></canvas></div> <div class="chart-container"><canvas id="ramChart"></canvas></div>
<div class="chart-container"><canvas id="diskChart"></canvas></div>
</div> </div>
</div> </div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="/static/js/controls.js"></script>
<script src="/static/js/console.js"></script>
<script src="/static/js/files.js"></script>
<script src="/static/js/config.js"></script>
<script src="/static/js/mods.js"></script>
<script> <script>
const username = "{{ username }}"; const username = "{{ username }}";
@ -132,225 +146,28 @@
$(id).classList.add('active'); $(id).classList.add('active');
} }
function sendAction(action) {
fetch(`/api/${action}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
}).then(r => r.json()).then(console.log);
}
function sendCommand() {
const input = $("console-command");
const command = input.value.trim();
if (!command) return;
fetch('/api/command', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, command })
}).then(res => {
if (!res.ok) throw new Error("Nie udało się wysłać komendy.");
input.value = '';
}).catch(err => {
alert("Błąd wysyłania komendy.");
console.error(err);
});
}
function deleteServer() {
if (!confirm("Na pewno chcesz usunąć swój serwer? Tej operacji nie można cofnąć!")) return;
fetch('/api/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
})
.then(res => res.ok ? res.json() : res.text().then(t => { throw new Error(t); }))
.then(data => { alert(data.message); location.reload(); })
.catch(err => { alert("Błąd usuwania serwera."); console.error(err); });
}
function loadLogs() {
fetch(`/api/logs?username=${username}`)
.then(r => r.json())
.then(data => { $("console-log").textContent = data.logs; });
}
let currentPath = ''; let currentPath = '';
function loadFileList(path = '') { function checkServerStatus() {
currentPath = path; fetch(`/api/status?username=${username}`)
$("current-path").textContent = "/" + path;
fetch(`/api/files?username=${username}&path=${encodeURIComponent(path)}`)
.then(r => r.json()) .then(r => r.json())
.then(data => { .then(data => {
const list = $("file-list"); const statusEl = $("server-status");
list.innerHTML = '';
if (path) {
list.innerHTML += `<li><a class="non-link" href="#" onclick="loadFileList('${path.split('/').slice(0, -1).join('/')}')">⬅️ ..</a></li>`;
}
data.sort((a, b) => a.name.localeCompare(b.name));
data.forEach(entry => {
const itemPath = path ? `${path}/${entry.name}` : entry.name;
const html = entry.is_dir
? `<li>📁 <a class="non-link" href="#" onclick="loadFileList('${itemPath}')">${entry.name}</a>
<a class="non-link" onclick="deleteItem('${itemPath}')">
<i class="fa-solid fa-trash"></i>
</a>
</li>`
: `<li>📄 ${entry.name}
<a class="non-link" href="/api/files/download?username=${username}&path=${encodeURIComponent(itemPath)}" target="_blank">
<i class="fa-solid fa-download"></i>
</a>
<a class="non-link" onclick="deleteItem('${itemPath}')">
<i class="fa-solid fa-trash"></i>
</a>
</li>`;
list.innerHTML += html;
});
});
}
function deleteItem(path) { if (data.running) {
if (!confirm(`Czy na pewno chcesz usunąć: ${path}?`)) return; statusEl.textContent = "Online";
statusEl.style.color = "#2ecc71";
fetch('/api/files/delete', { if (currentStatus !== true) {
method: 'POST', startStats();
headers: { 'Content-Type': 'application/json' }, currentStatus = true;
body: JSON.stringify({ username, path }) }
})
.then(res => res.json())
.then(data => {
if (data.success) {
loadFileList(currentPath);
} else { } else {
alert("Błąd podczas usuwania: " + (data.error || "Nieznany")); statusEl.textContent = "Offline";
statusEl.style.color = "#e74c3c";
if (currentStatus !== false) {
stopStats();
currentStatus = false;
} }
})
.catch(err => {
console.error(err);
alert("Błąd połączenia z serwerem.");
});
}
document.querySelector('[onclick="showTab(\'files\')"]').addEventListener('click', () => loadFileList());
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('file-input');
const progressBar = document.getElementById('progress-bar');
const progressWrapper = document.getElementById('upload-progress');
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', e => {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', e => {
e.preventDefault();
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', e => {
e.preventDefault();
dropZone.classList.remove('dragover');
handleFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', () => {
handleFiles(fileInput.files);
});
function handleFiles(files) {
if (!files.length) return;
const formData = new FormData();
for (const file of files) {
formData.append('files', file);
}
formData.append('username', username);
formData.append('path', currentPath);
progressWrapper.classList.remove('hidden');
progressBar.style.width = '0%';
const xhr = new XMLHttpRequest();
xhr.open('POST', `/api/files/upload?username=${username}&path=${encodeURIComponent(currentPath)}`);
xhr.upload.addEventListener('progress', e => {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100;
progressBar.style.width = `${percent}%`;
}
});
xhr.onload = () => {
progressWrapper.classList.add('hidden');
if (xhr.status === 200) {
loadFileList(currentPath);
} else {
alert("Upload failed.");
}
};
xhr.onerror = () => {
progressWrapper.classList.add('hidden');
alert("Upload failed.");
};
xhr.send(formData);
}
function loadConfig() {
fetch(`/api/config?username=${username}`)
.then(response => response.json())
.then(data => {
const cfg = data.config;
document.getElementById("server-type").value = data.type;
document.getElementById("server-version").value = data.version;
document.getElementById("max-players").value = cfg["max-players"];
document.getElementById("pvp").checked = cfg["pvp"];
document.getElementById("difficulty").value = cfg["difficulty"];
document.getElementById("online-mode").checked = cfg["online-mode"];
document.getElementById("spawn-monsters").checked = cfg["spawn-monsters"];
document.getElementById("spawn-animals").checked = cfg["spawn-animals"];
document.getElementById("allow-nether").checked = cfg["allow-nether"];
document.getElementById("max-build-height").value = cfg["max-build-height"];
document.getElementById("view-distance").value = cfg["view-distance"];
})
.catch(err => {
console.error("Błąd wczytywania konfiguracji: ", err);
alert("Nie udało się załadować konfiguracji serwera.");
});
}
document.querySelector('[onclick="showTab(\'config\')"]').addEventListener('click', loadConfig);
function saveConfig() {
const config = {
type: document.getElementById("server-type").value,
version: document.getElementById("server-version").value,
max_players: document.getElementById("max-players").value,
pvp: document.getElementById("pvp").checked,
difficulty: document.getElementById("difficulty").value,
online_mode: document.getElementById("online-mode").checked,
spawn_monsters: document.getElementById("spawn-monsters").checked,
spawn_animals: document.getElementById("spawn-animals").checked,
allow_nether: document.getElementById("allow-nether").checked,
max_build_height: document.getElementById("max-build-height").value,
view_distance: document.getElementById("view-distance").value
};
fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, config })
}).then(res => res.json()).then(data => {
if (data.success) {
alert("Konfiguracja została zapisana!");
} else {
alert("Błąd zapisywania konfiguracji.");
} }
}); });
} }
@ -394,33 +211,6 @@
chart.data.datasets[0].data.push(values[i]); chart.data.datasets[0].data.push(values[i]);
chart.update(); chart.update();
}); });
diskChart.data.datasets[0].data[0] = data.disk;
diskChart.update();
});
}
function checkServerStatus() {
fetch(`/api/status?username=${username}`)
.then(r => r.json())
.then(data => {
const statusEl = $("server-status");
if (data.running) {
statusEl.textContent = "Online";
statusEl.style.color = "#2ecc71";
if (currentStatus !== true) {
startStats();
currentStatus = true;
}
} else {
statusEl.textContent = "Offline";
statusEl.style.color = "#e74c3c";
if (currentStatus !== false) {
stopStats();
currentStatus = false;
}
}
}); });
} }
@ -476,24 +266,6 @@
options: chartOptions options: chartOptions
}); });
const diskChart = new Chart($("diskChart"), {
type: 'bar',
data: {
labels: ['Dysk (GB)'],
datasets: [{
label: 'Dysk (GB)',
data: [0],
backgroundColor: '#f39c12',
borderColor: '#f39c12',
borderWidth: 1
}]
},
options: {
...chartOptions,
scales: { ...chartOptions.scales, y: { min: 0, max: 15, ticks: { color: '#aaa' } } }
}
});
checkServerStatus(); checkServerStatus();
setInterval(checkServerStatus, 5000); setInterval(checkServerStatus, 5000);
</script> </script>

17
app/templates/donate.html Normal file
View file

@ -0,0 +1,17 @@
{% extends "layout.html" %}
{% block content %}
<div style="text-align: center;">
<h2>Donate</h2>
<h3>Dlaczego nawet mały donate ma znaczenie:</h3>
<p>Hosting Minecrafta jest całkowicie darmowy - robię to z pasji i dla frajdy.</p>
<p>Serwery działają obecnie na darmowym <a href="https://oracle.com/cloud">Oracle Cloud Free Tier</a>,
ale pracuję też nad rozwojem tego panelu (MCPanel) żeby zarządzanie serwerami <br>było jeszcze wygodniejsze i bardziej funkcjonalne.</p><br>
<p>W przyszłości backupy Waszych serwerów będą przechowywane na moim prywatnym serwerze <a href="https://www.netcup.com/en/server/arm-server/vps-2000-arm-g11-iv-mnz">NetCup</a>,
<br>za który już płacę z własnej kieszeni. A donate, nawet najmniejszy, to dla mnie:</p>
<ul style="list-style-position: inside; display: inline-block; text-align: left;">
<li><b>Motywacja</b> do dalszego rozwijania panelu</li>
<li><b>Wsparcie</b> w opłacaniu serwera na backupy</li>
</ul>
<p>Donate możesz wysłać <a href="https://ko-fi.com/andusdev">tutaj</a></p>
</div>
{% endblock %}

View file

@ -19,10 +19,10 @@
<div id="particle-background"></div> <div id="particle-background"></div>
<nav class="top-nav"> <nav class="top-nav">
<div class="nav-logo">MCPanel <h6>BETA</h6></div> <div class="nav-logo"><a href="/">MCPanel <h6>BETA</h6></a></div>
<div class="nav-links"> <div class="nav-links">
<a href="/">Strona Główna</a>
<a href="/dashboard">Panel</a> <a href="/dashboard">Panel</a>
<a style="color: cornflowerblue;" href="/donate">Donate</a>
<a href="/logout">Wyloguj</a> <a href="/logout">Wyloguj</a>
</div> </div>
</nav> </nav>
@ -30,12 +30,12 @@
<div class="dashboard-card"> <div class="dashboard-card">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
<p>Wersja: v0.1</p> <div id="ad-banner" class="ad-banner"></div>
<p>Wersja: v0.2</p>
<script> <script>
window.addEventListener('dragover', e => e.preventDefault()); window.addEventListener('dragover', e => e.preventDefault());
window.addEventListener('drop', e => e.preventDefault()); window.addEventListener('drop', e => e.preventDefault());
const hoverSound = new Audio("/static/sounds/hover.wav"); const hoverSound = new Audio("/static/sounds/hover.wav");
//const clickSound = new Audio("/static/sounds/click.wav"); //const clickSound = new Audio("/static/sounds/click.wav");
@ -108,6 +108,21 @@
} }
setInterval(createParticle, 100); setInterval(createParticle, 100);
fetch("/ads/list")
.then(response => response.json())
.then(images => {
if (images.length === 0) return;
const randomImage = images[Math.floor(Math.random() * images.length)];
const img = document.createElement("img");
img.src = randomImage;
img.alt = "Ad Banner";
img.style.maxWidth = "100%";
img.style.height = "auto";
img.style.borderRadius = "8px";
img.style.boxShadow = "0 0 12px rgba(0,0,0,0.3)";
document.getElementById("ad-banner").appendChild(img);
});
animate(); animate();
</script> </script>
</body> </body>