505 lines
20 KiB
HTML
505 lines
20 KiB
HTML
{% extends "layout.html" %}
|
|
{% block content %}
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<h2>Twój serwer Minecraft</h2>
|
|
|
|
<div class="tabs">
|
|
<button class="tab-button" onclick="showTab('controls')">Sterowanie 🎛️</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('config')">Konfiguracja 🛠️</button>
|
|
<button class="tab-button" onclick="showTab('statistics')">Statystyki 📈</button>
|
|
|
|
</div>
|
|
<div class="tabs">
|
|
<button class="tab-button" onclick="showTab('mods')">Mody/Pluginy 🧩</button>
|
|
</div>
|
|
|
|
<div class="tab-content">
|
|
<div class="tab-panel active" id="controls">
|
|
<div style="margin-bottom: 20px;">
|
|
<p><strong>IP:</strong> {{ ip }}</p>
|
|
<p><strong>Port (Serwer MC):</strong> {{ ports[0] }}</p>
|
|
<p><strong>Dodatkowe porty:</strong> {{ ports[1:] | join(", ") }}</p>
|
|
</div>
|
|
<p><strong>Status serwera:</strong> <span id="server-status">Sprawdzanie...</span></p><br>
|
|
<button class="btn btn-success" onclick="sendAction('start')">Start</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="deleteServer()">Usuń Serwer</button>
|
|
</div>
|
|
|
|
<div class="tab-panel" id="console">
|
|
<div class="console-output" id="console-log">Ładowanie logów...</div>
|
|
<div class="console-input">
|
|
<input type="text" id="console-command" placeholder="Wpisz komendę..." onkeydown="if(event.key==='Enter') sendCommand()">
|
|
<button onclick="sendCommand()">Wyślij</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tab-panel" id="files">
|
|
<h4 id="current-path">/</h4>
|
|
<div id="drop-zone">
|
|
<p>Przeciągnij pliki tutaj lub kliknij, aby przesłać</p>
|
|
<input type="file" id="file-input" multiple style="display: none;">
|
|
<div id="upload-progress" class="hidden">
|
|
<div id="progress-bar"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<br>
|
|
<h4>Lista plików:</h4>
|
|
<ul id="file-list"></ul>
|
|
</div>
|
|
|
|
<div class="tab-panel" id="config">
|
|
<form id="config-form">
|
|
<h4>Konfiguracja Serwera:</h4>
|
|
<div class="form-group">
|
|
<label for="server-type">Typ serwera</label>
|
|
<select id="server-type" class="form-control">
|
|
<option value="paper">Paper</option>
|
|
<option value="fabric">Fabric</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="server-version">Wersja</label>
|
|
<input type="text" id="server-version" class="form-control" placeholder="Np. 1.20.4">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="max-players">Maksymalna liczba graczy</label>
|
|
<input type="number" id="max-players" class="form-control" min="1" value="{{ config['max-players'] }}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="pvp">PvP</label>
|
|
<input type="checkbox" id="pvp" class="form-check-input" {% if config['pvp'] %} checked {% endif %}>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="difficulty">Trudność</label>
|
|
<select id="difficulty" class="form-control">
|
|
<option value="peaceful" {% if config['difficulty'] == 'peaceful' %}selected{% endif %}>Spokojny</option>
|
|
<option value="easy" {% if config['difficulty'] == 'easy' %}selected{% endif %}>Łatwy</option>
|
|
<option value="normal" {% if config['difficulty'] == 'normal' %}selected{% endif %}>Normalny</option>
|
|
<option value="hard" {% if config['difficulty'] == 'hard' %}selected{% endif %}>Trudny</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="online-mode">Tryb online</label>
|
|
<h6>W tym trybie mogą dołączyć tylko gracze z kupionym kontem Minecraft. Jest owiele bezpieczniejszy.
|
|
<br>Jeżeli chcesz używać serwera z wyłączonym trybem online, zainstaluj AuthMe lub podobny plugin i włącz whitelistę</h6>
|
|
<input type="checkbox" id="online-mode" class="form-check-input" {% if config['online-mode'] %} checked {% endif %}>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="spawn-monsters">Spawnowanie potworów</label>
|
|
<input type="checkbox" id="spawn-monsters" class="form-check-input" {% if config['spawn-monsters'] %} checked {% endif %}>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="spawn-animals">Spawnowanie zwierząt</label>
|
|
<input type="checkbox" id="spawn-animals" class="form-check-input" {% if config['spawn-animals'] %} checked {% endif %}>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="allow-nether">Nether Włączony</label>
|
|
<input type="checkbox" id="allow-nether" class="form-check-input" {% if config['allow-nether'] %} checked {% endif %}>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="max-build-height">Maksymalna wysokość budowania</label>
|
|
<input type="number" id="max-build-height" class="form-control" min="1" value="{{ config['max-build-height'] }}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="view-distance">Zasięg widzenia</label>
|
|
<input type="number" id="view-distance" class="form-control" min="1" value="{{ config['view-distance'] }}">
|
|
</div>
|
|
<button type="button" class="btn btn-primary" onclick="saveConfig()">Zapisz zmiany</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="tab-panel" id="statistics">
|
|
<div class="charts-row">
|
|
<div class="chart-container"><canvas id="cpuChart"></canvas></div>
|
|
<div class="chart-container"><canvas id="ramChart"></canvas></div>
|
|
<div class="chart-container"><canvas id="diskChart"></canvas></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const username = "{{ username }}";
|
|
|
|
const $ = id => document.getElementById(id);
|
|
|
|
let statsInterval = null;
|
|
let logsInterval = null;
|
|
let currentStatus = null;
|
|
|
|
function showTab(id) {
|
|
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('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 = '';
|
|
|
|
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 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() {
|
|
if (!statsInterval) {
|
|
statsInterval = setInterval(updateStats, 4000);
|
|
updateStats();
|
|
}
|
|
if (!logsInterval) {
|
|
logsInterval = setInterval(loadLogs, 5000);
|
|
loadLogs();
|
|
}
|
|
}
|
|
|
|
function stopStats() {
|
|
if (statsInterval) {
|
|
clearInterval(statsInterval);
|
|
statsInterval = null;
|
|
}
|
|
if (logsInterval) {
|
|
clearInterval(logsInterval);
|
|
logsInterval = null;
|
|
}
|
|
}
|
|
|
|
function updateStats() {
|
|
fetch(`/api/stats?username=${username}`)
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
const now = new Date().toLocaleTimeString();
|
|
const charts = [cpuChart, ramChart];
|
|
const values = [data.cpu, data.ram];
|
|
|
|
charts.forEach((chart, i) => {
|
|
if (chart.data.labels.length > 20) {
|
|
chart.data.labels.shift();
|
|
chart.data.datasets[0].data.shift();
|
|
}
|
|
chart.data.labels.push(now);
|
|
chart.data.datasets[0].data.push(values[i]);
|
|
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;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
const chartOptions = {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
x: { ticks: { color: '#aaa' }, grid: { color: '#222' } },
|
|
y: { ticks: { color: '#aaa' }, grid: { color: '#222' } }
|
|
},
|
|
plugins: {
|
|
legend: { display: false },
|
|
tooltip: {
|
|
backgroundColor: '#1a1a2a',
|
|
titleColor: '#f0f0f0',
|
|
bodyColor: '#ccc'
|
|
}
|
|
}
|
|
};
|
|
|
|
const cpuChart = new Chart($("cpuChart"), {
|
|
type: 'line',
|
|
data: {
|
|
labels: ['CPU (%)'],
|
|
datasets: [{
|
|
label: 'CPU (%)',
|
|
data: [],
|
|
borderColor: '#6c7bff',
|
|
backgroundColor: 'rgba(108, 123, 255, 0.2)',
|
|
tension: 0.3,
|
|
pointRadius: 0
|
|
}]
|
|
},
|
|
options: {
|
|
...chartOptions,
|
|
scales: { ...chartOptions.scales, y: { min: 0, max: 100, ticks: { color: '#aaa' } } }
|
|
}
|
|
});
|
|
|
|
const ramChart = new Chart($("ramChart"), {
|
|
type: 'line',
|
|
data: {
|
|
labels: ['RAM (MB)'],
|
|
datasets: [{
|
|
label: 'RAM (MB)',
|
|
data: [],
|
|
borderColor: '#2ecc71',
|
|
backgroundColor: 'rgba(46, 204, 113, 0.2)',
|
|
tension: 0.3,
|
|
pointRadius: 0
|
|
}]
|
|
},
|
|
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();
|
|
setInterval(checkServerStatus, 5000);
|
|
</script>
|
|
|
|
{% endblock %}
|