Compare commits

..

No commits in common. "main" and "v0.1" have entirely different histories.
main ... v0.1

21 changed files with 327 additions and 1141 deletions

1
.gitignore vendored
View file

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

1
README.md Normal file
View file

@ -0,0 +1 @@
# Hosting

View file

@ -4,8 +4,7 @@ import shutil
import subprocess import subprocess
import docker import docker
import requests from flask import Blueprint, jsonify, request, send_from_directory, abort
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, \
@ -18,7 +17,6 @@ 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')
@ -33,9 +31,9 @@ def setup_server():
@api.route('/delete', methods=['POST']) @api.route('/delete', methods=['POST'])
@oidc.require_login
def delete(): def delete():
username = oidc.user_getfield('preferred_username') data = request.get_json()
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
@ -46,9 +44,8 @@ def delete():
# Server Controls # Server Controls
@api.route('/start', methods=['POST']) @api.route('/start', methods=['POST'])
@oidc.require_login
def start(): def start():
username = oidc.user_getfield('preferred_username') username = request.json['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):
@ -69,25 +66,22 @@ def start():
@api.route('/stop', methods=['POST']) @api.route('/stop', methods=['POST'])
@oidc.require_login
def stop(): def stop():
username = oidc.user_getfield('preferred_username') username = request.json['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 = oidc.user_getfield('preferred_username') username = request.json['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 = oidc.user_getfield('preferred_username') username = request.args.get('username')
return jsonify({"logs": get_logs(username)}) return jsonify({"logs": get_logs(username)})
@ -98,9 +92,6 @@ 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:
@ -116,9 +107,8 @@ 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 = oidc.user_getfield('preferred_username') username = request.args.get('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}')
@ -144,9 +134,8 @@ 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 = oidc.user_getfield('preferred_username') username = request.args.get('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}')
@ -161,9 +150,8 @@ 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 = oidc.user_getfield('preferred_username') username = request.form.get('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}')
@ -179,17 +167,12 @@ 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):
@ -204,74 +187,23 @@ def delete_file_or_folder():
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
@api.route('/files/content', methods=['GET'])
@oidc.require_login
def get_file_content():
username = oidc.user_getfield('preferred_username')
path = request.args.get('path')
base_path = os.path.abspath(f'./servers/mc-{username}')
file_path = os.path.abspath(os.path.join(base_path, path))
if not file_path.startswith(base_path) or not os.path.isfile(file_path):
return abort(403)
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
return jsonify({"success": True, "content": content})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@api.route('/files/content', methods=['POST'])
@oidc.require_login
def save_file_content():
data = request.get_json()
username = data.get('username')
if username != oidc.user_getfield('preferred_username'):
return jsonify({"error": "Unauthorized request."}), 403
path = data.get('path')
content = data.get('content')
base_path = os.path.abspath(f'./servers/mc-{username}')
file_path = os.path.abspath(os.path.join(base_path, path))
if not file_path.startswith(base_path) or not os.path.isfile(file_path):
return abort(403)
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
return jsonify({"success": True})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
# Server config # Server config
@api.route('/config') @api.route('/config')
@oidc.require_login
def get_config(): def get_config():
username = oidc.user_getfield('preferred_username') username = request.args.get('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"], return jsonify({"success": True, "config": server_info["config"], "version": server_info["version"], "type": server_info["type"]})
"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):
@ -279,11 +211,9 @@ 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:
@ -305,9 +235,8 @@ def status():
@api.route('/stats', methods=['GET']) @api.route('/stats', methods=['GET'])
@oidc.require_login
def stats(): def stats():
username = oidc.user_getfield("preferred_username") username = request.args.get("username")
container_name = f"mc-{username}" container_name = f"mc-{username}"
try: try:
@ -339,100 +268,3 @@ 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,6 +55,8 @@ 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
@ -89,7 +91,6 @@ 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,
@ -115,9 +116,9 @@ def start_server(username):
f"{ports[1]}/tcp": ports[1], f"{ports[1]}/tcp": ports[1],
f"{ports[2]}/tcp": ports[2], f"{ports[2]}/tcp": ports[2],
}, },
volumes=[ volumes={
f"{os.path.abspath(path)}:/data" os.path.abspath(path): {'bind': '/data', 'mode': 'rw'}
], },
environment=environment, environment=environment,
restart_policy={"Name": "unless-stopped"} restart_policy={"Name": "unless-stopped"}
) )

View file

@ -1,27 +1,22 @@
import os import os
import random
from flask import Blueprint, render_template, jsonify from flask import Blueprint, render_template
from .auth import oidc from .auth import oidc
from .port_utils import get_user_ports from .port_utils import get_user_ports
from .docker_utils import client
from docker.errors import NotFound
from dotenv import load_dotenv from dotenv import load_dotenv
main = Blueprint('main', __name__, static_folder='static') main = Blueprint('main', __name__)
load_dotenv() load_dotenv()
@main.route("/") @main.route("/")
@oidc.require_login
def home(): def home():
is_logged_in = oidc.user_loggedin username = oidc.user_getfield("preferred_username")
has_server = False server_path = f"./servers/mc-{username}"
if is_logged_in: has_server = os.path.exists(server_path)
username = oidc.user_getfield("preferred_username") return render_template("home.html", has_server=has_server)
server_path = f"./servers/mc-{username}"
has_server = os.path.exists(server_path)
return render_template("home.html", has_server=has_server, is_logged_in=is_logged_in)
@main.route('/setup') @main.route('/setup')
@ -38,55 +33,7 @@ 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.route("/status")
def servers_status():
servers_dir = "./servers"
server_dirs = [
d for d in os.listdir(servers_dir)
if os.path.isdir(os.path.join(servers_dir, d)) and d.startswith('mc-')
]
server_data = []
for server_dir in server_dirs:
server_name = server_dir
server_status = "Offline"
try:
container = client.containers.get(server_name)
if container.status == "running":
server_status = "Online"
except NotFound:
pass
server_data.append({"name": server_name, "status": server_status})
return render_template('servers_list.html', servers=server_data)
@main.app_errorhandler(404)
def page_not_found(e):
return render_template("404.html"), 404

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

View file

@ -1,52 +0,0 @@
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.");
}
});
}

View file

@ -1,23 +0,0 @@
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; });
}

View file

@ -1,19 +0,0 @@
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); });
}

View file

@ -1,134 +0,0 @@
let codeMirrorEditor = null;
let currentEditingFilePath = null;
function getFileExtension(filename) {
const lastDot = filename.lastIndexOf('.');
if (lastDot === -1) return '';
return filename.slice(lastDot + 1);
}
function getCodeMirrorMode(filename) {
const ext = getFileExtension(filename).toLowerCase();
switch (ext) {
case 'js':
return 'javascript';
case 'json':
return {
name: "javascript",
json: true
};
case 'html':
case 'htm':
return 'htmlmixed';
case 'css':
return 'css';
case 'xml':
return 'xml';
case 'yml':
case 'yaml':
return 'yaml';
case 'properties':
return 'properties';
case 'log':
case 'txt':
return 'text/plain';
default:
return null;
}
}
async function openFileEditor(filePath) {
const fileExtension = getFileExtension(filePath);
const textFileExtensions = ['txt', 'log', 'json', 'yml', 'yaml', 'properties', 'html', 'css', 'js', 'xml', 'py'];
if (!textFileExtensions.includes(fileExtension.toLowerCase())) {
alert('Ten typ pliku nie może być edytowany bezpośrednio. Możesz go pobrać.');
return;
}
try {
const response = await fetch(`/api/files/content?username=${username}&path=${encodeURIComponent(filePath)}`);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Server responded with status ${response.status}: ${errorText}`);
}
const data = await response.json();
if (data.success) {
document.getElementById('file-list').style.display = 'none';
document.getElementById('drop-zone').style.display = 'none';
document.getElementById('file-editor-container').style.display = 'block';
document.getElementById('editing-filename').textContent = filePath.split('/').pop();
currentEditingFilePath = filePath;
if (!codeMirrorEditor) {
codeMirrorEditor = CodeMirror.fromTextArea(document.getElementById('file-editor'), {
lineNumbers: true,
theme: 'material-darker',
mode: getCodeMirrorMode(filePath),
indentUnit: 4,
tabSize: 4,
indentWithTabs: false,
lineWrapping: true
});
} else {
codeMirrorEditor.setValue(data.content);
codeMirrorEditor.setOption('mode', getCodeMirrorMode(filePath));
}
codeMirrorEditor.refresh();
} else {
alert('Błąd podczas ładowania pliku: ' + (data.error || 'Nieznany błąd.'));
}
} catch (error) {
console.error('Błąd:', error);
alert('Wystąpił błąd podczas ładowania pliku: ' + error.message);
}
}
async function saveFileContent() {
if (!codeMirrorEditor || !currentEditingFilePath) {
alert('Brak pliku do zapisania.');
return;
}
const content = codeMirrorEditor.getValue();
try {
const response = await fetch('/api/files/content', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: username,
path: currentEditingFilePath,
content: content
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Server responded with status ${response.status}: ${errorText}`);
}
const data = await response.json();
if (data.success) {
alert('Plik został pomyślnie zapisany!');
} else {
alert('Błąd podczas zapisywania pliku: ' + (data.error || 'Nieznany błąd.'));
}
} catch (error) {
console.error('Błąd:', error);
alert('Wystąpił błąd podczas zapisywania pliku: ' + error.message);
}
}
function closeFileEditor() {
document.getElementById('file-editor-container').style.display = 'none';
document.getElementById('file-list').style.display = 'block';
document.getElementById('drop-zone').style.display = 'block';
currentEditingFilePath = null;
if (codeMirrorEditor) {
codeMirrorEditor.setValue('');
}
}

View file

@ -1,142 +0,0 @@
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) => {
if (a.is_dir === b.is_dir) {
return a.name.localeCompare(b.name);
}
return a.is_dir ? -1 : 1;
});
data.forEach(entry => {
const itemPath = path ? `${path}/${entry.name}` : entry.name;
let html = '';
if (entry.is_dir) {
html = `<li>📁 <a class="non-link" href="#" onclick="loadFileList('${itemPath}')">${entry.name}</a>
<a class="non-link action-icon" onclick="deleteItem('${itemPath}')" title="Usuń folder">
<i class="fa-solid fa-trash"></i>
</a>
</li>`;
} else {
const fileExtension = getFileExtension(entry.name);
const editableExtensions = ['txt', 'log', 'json', 'yml', 'yaml', 'properties', 'html', 'css', 'js', 'xml', 'py'];
const isEditable = editableExtensions.includes(fileExtension.toLowerCase());
html = `<li>📄 ${entry.name}
${isEditable ? `
<a class="non-link action-icon" onclick="openFileEditor('${itemPath}')" title="Edytuj plik">
<i class="fa-solid fa-edit"></i>
</a>` : ''}
<a class="non-link action-icon" href="/api/files/download?username=${username}&path=${encodeURIComponent(itemPath)}" target="_blank" title="Pobierz plik">
<i class="fa-solid fa-download"></i>
</a>
<a class="non-link action-icon" onclick="deleteItem('${itemPath}')" title="Usuń plik">
<i class="fa-solid fa-trash"></i>
</a>
</li>`;
}
list.innerHTML += html;
});
closeFileEditor();
});
}
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(); // Load root directory when the tab is opened
});
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);
}

View file

@ -1,68 +0,0 @@
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

@ -1,6 +1,3 @@
/* Imports */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap');
/* Reset & Base Styles */ /* Reset & Base Styles */
* { * {
margin: 0; margin: 0;
@ -89,25 +86,10 @@ 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 {
/* Desktop default layout */
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); /* Original desktop layout */ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 14px; gap: 14px;
margin-bottom: 26px; margin-bottom: 26px;
} }
@ -122,11 +104,6 @@ h2 {
color: #ccc; color: #ccc;
transition: background-color 0.25s, transform 0.2s; transition: background-color 0.25s, transform 0.2s;
box-shadow: 0 0 8px transparent; box-shadow: 0 0 8px transparent;
/* Flexbox for text and icon alignment */
display: flex;
align-items: center;
justify-content: center; /* Center content horizontally */
gap: 8px; /* Space between text and icon */
} }
.tab-button:hover { .tab-button:hover {
@ -140,19 +117,9 @@ h2 {
box-shadow: 0 0 14px #4a5aef99; box-shadow: 0 0 14px #4a5aef99;
} }
.tab-button .tab-icon {
font-size: 1.2em; /* Adjust icon size as needed */
}
.tab-button .tab-text {
/* By default, text is visible on desktop */
display: block;
}
.square-button { .square-button {
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
width: 200px; width: 100px;
background-color: #1a1a2e; background-color: #1a1a2e;
border: 2px solid #2c2c3c; border: 2px solid #2c2c3c;
border-radius: 16px; border-radius: 16px;
@ -228,7 +195,6 @@ h2 {
.btn-primary { background: #4a5aef; color: white; box-shadow: 0 0 10px #4a5aef99; } .btn-primary { background: #4a5aef; color: white; box-shadow: 0 0 10px #4a5aef99; }
.btn-success { background: #2ecc71; color: white; box-shadow: 0 0 10px #2ecc7199; } .btn-success { background: #2ecc71; color: white; box-shadow: 0 0 10px #2ecc7199; }
.btn-danger { background: #e74c3c; color: white; box-shadow: 0 0 10px #e74c3c99; } .btn-danger { background: #e74c3c; color: white; box-shadow: 0 0 10px #e74c3c99; }
.btn-worsedanger { background: #8b0000; color: white; box-shadow: 0 0 10px #8b000099; }
.btn-warning { background: #f39c12; color: black; box-shadow: 0 0 10px #f39c1288; } .btn-warning { background: #f39c12; color: black; box-shadow: 0 0 10px #f39c1288; }
.btn-info { background: #3498db; color: white; box-shadow: 0 0 10px #3498db99; } .btn-info { background: #3498db; color: white; box-shadow: 0 0 10px #3498db99; }
@ -276,7 +242,18 @@ h2 {
background-color: #666; background-color: #666;
} }
body > *:not(.cursor-glow) { /* Chart Container */
.chart-container {
background: #12121c;
padding: 20px;
border-radius: 14px;
margin-top: 30px;
border: 1px solid #232334;
box-shadow: 0 0 12px #1a1a2a;
overflow-x: auto;
}
body > *:not(script):not(style):not(.cursor-glow) {
animation: growIn 0.6s ease-out; animation: growIn 0.6s ease-out;
} }
@ -537,246 +514,22 @@ a.non-link:hover {
justify-content: center; justify-content: center;
} }
/* 404 Error */
.error-container {
max-width: 800px;
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;
}
/* Servers Status */
.server-table-container {
overflow-x: auto;
padding: 1rem;
border-radius: 16px;
background: rgba(14, 14, 26, 0.6);
backdrop-filter: blur(12px);
border: 1px solid #1a1a2a;
box-shadow: 0 0 24px rgba(74, 90, 239, 0.15);
}
.server-table {
width: 100%;
border-collapse: collapse;
color: #f0f0f0;
font-size: 0.95rem;
border-radius: 12px;
overflow: hidden;
}
.server-table thead {
background: linear-gradient(145deg, #1a1a2f, #23233a);
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 0.85rem;
color: #b9c5ff;
}
.server-table th,
.server-table td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.server-table tbody tr:hover {
background: rgba(255, 255, 255, 0.03);
transition: background 0.3s ease;
}
.server-table th:first-child,
.server-table td:first-child {
border-left: none;
}
.server-table th:last-child,
.server-table td:last-child {
border-right: none;
}
.server-table tbody tr {
box-shadow: inset 0 0 0 rgba(0, 0, 0, 0);
transition: box-shadow 0.2s ease;
}
.server-table tbody tr:hover {
box-shadow: inset 0 0 12px rgba(255, 255, 255, 0.03);
}
.status-label {
display: inline-block;
padding: 0.4em 0.75em;
border-radius: 999px;
font-weight: 600;
font-size: 0.875rem;
text-transform: capitalize;
color: white;
}
.status-label.online {
background-color: #2ecc71;
}
.status-label.offline {
background-color: #e74c3c;
}
/* Mobile Tweaks */ /* Mobile Tweaks */
@media (max-width: 768px) { @media (max-width: 600px) {
html {
font-size: clamp(15px, 4vw, 16px);
}
body { body {
padding: 0 8px; padding: 24px 12px;
}
h1, h2, h3 {
line-height: 1.2;
} }
.dashboard-card { .dashboard-card {
padding: 16px; padding: 20px;
max-width: 100%;
box-sizing: border-box;
}
/* Tabs on Mobile: Single row, icons only */
.tabs {
display: flex; /* Use flexbox for a single row */
grid-template-columns: unset; /* Override grid for mobile */
justify-content: space-around; /* Distribute items evenly */
flex-wrap: nowrap; /* Keep tabs in a single row */
overflow-x: auto; /* Enable horizontal scrolling if buttons exceed screen width */
-webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */
padding-bottom: 5px; /* Add some padding for scrollbar if present */
gap: 8px; /* Slightly smaller gap on mobile */
}
.tab-button {
flex-shrink: 0; /* Prevent buttons from shrinking */
padding: 10px 15px; /* Adjust padding for icon-only buttons */
/* Center content for icon-only */
justify-content: center;
width: auto; /* Allow buttons to size to content */
}
.tab-button .tab-text {
display: none; /* Hide the text on mobile */
}
.square-button {
display: flex;
align-items: center;
width: 100%;
flex-direction: row;
justify-content: flex-start;
gap: 12px;
padding: 12px;
font-size: 0.95rem;
}
.square-button img {
width: 32px;
height: auto;
margin: 0;
}
.top-nav {
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.nav-links {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 12px;
}
.chart-container {
width: 100%;
height: auto;
padding: 12px;
} }
.btn { .btn {
font-size: 0.95rem;
padding: 14px;
width: 100%; width: 100%;
margin: 8px 0;
} }
form { .console-output {
padding: 0 8px; height: 180px;
}
.tab-content {
margin-top: 12px;
}
.charts-row {
overflow-x: auto;
padding-bottom: 10px;
}
input[type="text"], input[type="number"], select, textarea {
width: 100%;
font-size: 1rem;
padding: 10px;
box-sizing: border-box;
}
#drop-zone {
padding: 20px;
} }
} }
/* Universal Fixes */
* {
max-width: 100%;
word-wrap: break-word;
box-sizing: border-box;
}

View file

@ -1,26 +0,0 @@
{% 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,26 +1,14 @@
{% 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')"> <button class="tab-button" onclick="showTab('controls')">Sterowanie 🎛️</button>
<span class="tab-text">Sterowanie</span> <span class="tab-icon">🎛️</span> <button class="tab-button" onclick="showTab('console')">Konsola 📟</button>
</button> <button class="tab-button" onclick="showTab('files')">Pliki 📁</button>
<button class="tab-button" onclick="showTab('console')"> <button class="tab-button" onclick="showTab('config')">Konfiguracja 🛠️</button>
<span class="tab-text">Konsola</span> <span class="tab-icon">📟</span> <button class="tab-button" onclick="showTab('statistics')">Statystyki 📈</button>
</button>
<button class="tab-button" onclick="showTab('files')">
<span class="tab-text">Pliki</span> <span class="tab-icon">📁</span>
</button>
<button class="tab-button" onclick="showTab('config')">
<span class="tab-text">Konfiguracja</span> <span class="tab-icon">🛠️</span>
</button>
<button class="tab-button" onclick="showTab('mods')">
<span class="tab-text">Mody/Pluginy</span> <span class="tab-icon">🧩</span>
</button>
<button class="tab-button" onclick="showTab('statistics')">
<span class="tab-text">Statystyki</span> <span class="tab-icon">📈</span>
</button>
</div> </div>
<div class="tab-content"> <div class="tab-content">
@ -34,7 +22,7 @@
<button class="btn btn-success" onclick="sendAction('start')">Start</button> <button class="btn btn-success" onclick="sendAction('start')">Start</button>
<button class="btn btn-warning" onclick="sendAction('restart')">Restart</button> <button class="btn btn-warning" onclick="sendAction('restart')">Restart</button>
<button class="btn btn-danger" onclick="sendAction('stop')">Stop</button><br> <button class="btn btn-danger" onclick="sendAction('stop')">Stop</button><br>
<button class="btn btn-worsedanger" onclick="deleteServer()">Usuń Serwer</button> <button class="btn btn-danger" onclick="deleteServer()">Usuń Serwer</button>
</div> </div>
<div class="tab-panel" id="console"> <div class="tab-panel" id="console">
@ -58,13 +46,6 @@
<br> <br>
<h4>Lista plików:</h4> <h4>Lista plików:</h4>
<ul id="file-list"></ul> <ul id="file-list"></ul>
<div id="file-editor-container" style="display: none;">
<h3>Edytuj plik: <span id="editing-filename"></span></h3>
<textarea id="file-editor" style="width: 100%; height: 400px;"></textarea>
<button class="btn btn-primary" onclick="saveFileContent()">Zapisz zmiany</button>
<button class="btn btn-secondary" onclick="closeFileEditor()">Zamknij</button>
</div>
</div> </div>
<div class="tab-panel" id="config"> <div class="tab-panel" id="config">
@ -128,28 +109,15 @@
</form> </form>
</div> </div>
<div class="tab-panel" id="mods">
<h4>Zainstaluj mody/pluginy z <span style="color: #2ecc71;">Modrinth</span></h4>
<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>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/theme/material-darker.min.css">
<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 }}";
@ -162,39 +130,231 @@
function showTab(id) { function showTab(id) {
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active')); document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
$(id).classList.add('active'); $(id).classList.add('active');
// Close the mobile menu if it's open (optional, depending on your menu implementation) }
const tabsContainer = document.querySelector('.tabs');
if (tabsContainer.classList.contains('mobile-active')) { function sendAction(action) {
tabsContainer.classList.remove('mobile-active'); 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 checkServerStatus() { function loadFileList(path = '') {
fetch(`/api/status?username=${username}`) currentPath = path;
$("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 statusEl = $("server-status"); const list = $("file-list");
list.innerHTML = '';
if (data.running) { if (path) {
statusEl.textContent = "Online"; list.innerHTML += `<li><a class="non-link" href="#" onclick="loadFileList('${path.split('/').slice(0, -1).join('/')}')">⬅️ ..</a></li>`;
statusEl.style.color = "#2ecc71";
if (currentStatus !== true) {
startStats();
currentStatus = true;
}
} else {
statusEl.textContent = "Offline";
statusEl.style.color = "#e74c3c";
if (currentStatus !== false) {
stopStats();
currentStatus = false;
}
} }
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 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.");
}
});
}
function startStats() { function startStats() {
if (!statsInterval) { if (!statsInterval) {
statsInterval = setInterval(updateStats, 4000); statsInterval = setInterval(updateStats, 4000);
@ -234,6 +394,33 @@
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;
}
}
}); });
} }
@ -289,20 +476,26 @@
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>
<!-- CSS do Edytora Plików --> {% endblock %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/javascript/javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/xml/xml.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/css/css.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/htmlmixed/htmlmixed.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/clike/clike.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/python/python.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/yaml/yaml.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/properties/properties.min.js"></script>
<script src="/static/js/file_editor.js"></script>
{% endblock %}

View file

@ -1,17 +0,0 @@
{% 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

@ -2,30 +2,20 @@
{% block content %} {% block content %}
<div class="logo-wrapper"> <div class="logo-wrapper">
<h2 class="logo-text">MCPanel</h2> <h2 class="logo-text">MCPanel</h2>
<p id="quote" class="splash-text"></p> <p id="quote" class="splash-text">Jesteśmy 1.5x lepsi od Aternosa!</p>
</div> </div>
<div class="tabs"> <div class="tabs">
{% if is_logged_in %} {% if has_server %}
{% if has_server %} <a class="tab-button" href="{{ url_for('main.dashboard') }}">Idź do panelu</a>
<a class="tab-button" href="{{ url_for('main.dashboard') }}">Idź do panelu</a>
{% else %}
<a class="tab-button" href="{{ url_for('main.setup') }}">Ustaw własny serwer</a>
{% endif %}
{% else %} {% else %}
<a class="tab-button" href="{{ url_for('main.dashboard') }}">Zaloguj się</a> <a class="tab-button" href="{{ url_for('main.setup') }}">Ustaw własny serwer</a>
{% endif %} {% endif %}
<a class="tab-button" href="#news">Wiadomości</a>
</div>
<div id="news">
<!-- Z Tagami jak: WERSJA PANELU, PROBLEM -->
<h3>Wiadomości:</h3>
<p>Soon&trade;</p>
</div> </div>
<script> <script>
const quotes = [ const quotes = [
"Jesteśmy 2x lepsi od Aternosa!",
"Twój serwer, Twoje zasady!", "Twój serwer, Twoje zasady!",
"Stabilność? Nie znam takiego słowa!", "Stabilność? Nie znam takiego słowa!",
"Zero kolejki, tylko gra!", "Zero kolejki, tylko gra!",

View file

@ -3,7 +3,6 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>MCPanel</title> <title>MCPanel</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet"> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
</head> </head>
@ -20,24 +19,23 @@
<div id="particle-background"></div> <div id="particle-background"></div>
<nav class="top-nav"> <nav class="top-nav">
<div class="nav-logo"><a href="/">MCPanel <h6>BETA</h6></a></div> <div class="nav-logo">MCPanel <h6>BETA</h6></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 href="/status">Statusy</a> <a href="/logout">Wyloguj</a>
<a style="color: cornflowerblue;" href="/donate">Donate</a>
<a style="color: red;" href="/logout">Wyloguj</a>
</div> </div>
</nav> </nav>
<div class="dashboard-card"> <div class="dashboard-card">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
<div id="ad-banner" class="ad-banner"></div> <p>Wersja: v0.1</p>
<p>Wersja: v0.3</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");
@ -110,21 +108,6 @@
} }
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>

View file

@ -1,32 +0,0 @@
{% extends "layout.html" %}
{% block content %}
<h1 class="page-title">Wszystkie Serwery</h1>
<div class="server-table-container">
<table class="server-table">
<thead>
<tr>
<th>Nazwa</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for server in servers %}
<tr>
<td>{{ server.name }}</td>
<td>
<span class="status-label {{ 'online' if server.status == 'Online' else 'offline' }}">
{{ server.status }}
</span>
</td>
</tr>
{% else %}
<tr>
<td colspan="2" class="empty-row">Nie znaleziono serwerów</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View file

@ -4,7 +4,7 @@
<div style="display: flex; justify-content: center; gap: 2rem; margin-bottom: 2rem;"> <div style="display: flex; justify-content: center; gap: 2rem; margin-bottom: 2rem;">
<button class="square-button" onclick="selectType(this, 'paper')"> <button class="square-button" onclick="selectType(this, 'paper')">
<img src="https://assets.papermc.io/brand/papermc_logo.min.svg" alt="Paper"> <img src="https://docs.papermc.io/assets/images/papermc-logomark-512-f125384f3367cd4d9291ca983fcb7334.png" alt="Paper">
<b>Paper</b> <b>Paper</b>
<br> <br>
<p>Zoptymalizowany serwer Minecraft oferujący lepszą wydajność.</p> <p>Zoptymalizowany serwer Minecraft oferujący lepszą wydajność.</p>