466 lines
12 KiB
HTML
466 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<title>Windrose Server Manager</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
font-family: sans-serif;
|
|
background: #1e1e1e;
|
|
color: #eee;
|
|
}
|
|
header {
|
|
background: #2b2b2b;
|
|
padding: 12px 20px;
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
}
|
|
#container {
|
|
display: flex;
|
|
height: calc(100vh - 50px);
|
|
}
|
|
#sidebar {
|
|
width: 240px;
|
|
background: #252525;
|
|
border-right: 1px solid #333;
|
|
padding: 10px;
|
|
}
|
|
#sidebar button {
|
|
width: 100%;
|
|
margin-bottom: 8px;
|
|
padding: 10px;
|
|
background: #333;
|
|
color: #eee;
|
|
border: none;
|
|
cursor: pointer;
|
|
}
|
|
#sidebar button:hover {
|
|
background: #444;
|
|
}
|
|
#content {
|
|
flex: 1;
|
|
padding: 20px;
|
|
overflow-y: auto;
|
|
}
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-top: 10px;
|
|
}
|
|
th, td {
|
|
padding: 8px;
|
|
border-bottom: 1px solid #444;
|
|
}
|
|
.section-title {
|
|
font-size: 22px;
|
|
margin-bottom: 10px;
|
|
}
|
|
.btn {
|
|
padding: 6px 10px;
|
|
background: #444;
|
|
color: #eee;
|
|
border: none;
|
|
cursor: pointer;
|
|
margin-right: 6px;
|
|
}
|
|
.btn:hover {
|
|
background: #555;
|
|
}
|
|
.checkbox {
|
|
margin-left: 10px;
|
|
}
|
|
pre {
|
|
background: #111;
|
|
padding: 10px;
|
|
border: 1px solid #333;
|
|
overflow-x: auto;
|
|
max-height: 70vh;
|
|
}
|
|
textarea {
|
|
background: #111;
|
|
color: #eee;
|
|
border: 1px solid #333;
|
|
padding: 10px;
|
|
width: 100%;
|
|
height: 200px;
|
|
font-family: monospace;
|
|
font-size: 14px;
|
|
}
|
|
.create-box {
|
|
border: 1px solid #444;
|
|
padding: 12px;
|
|
margin-bottom: 20px;
|
|
background: #2a2a2a;
|
|
}
|
|
.input {
|
|
width: 260px;
|
|
padding: 6px;
|
|
background: #111;
|
|
color: #eee;
|
|
border: 1px solid #333;
|
|
margin-top: 4px;
|
|
margin-bottom: 12px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header>Windrose Server Manager</header>
|
|
|
|
<div id="container">
|
|
<div id="sidebar">
|
|
<button onclick="showServers()">Servers</button>
|
|
<button onclick="showEvents()">Crash Events</button>
|
|
<button onclick="showLogs()">Logs</button>
|
|
<button onclick="showConfig()">Config</button>
|
|
</div>
|
|
|
|
<div id="content">
|
|
<!-- Dynamic content goes here -->
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
async function api(path, options = {}) {
|
|
const res = await fetch(path, {
|
|
headers: { "Content-Type": "application/json" },
|
|
...options
|
|
});
|
|
return res.json();
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
// CREATE SERVER
|
|
// ---------------------------------------------------------
|
|
|
|
async function createServer() {
|
|
const id = document.getElementById("newServerId").value.trim();
|
|
const name = document.getElementById("newServerName").value.trim();
|
|
const steamUsername = document.getElementById("steamUsername").value.trim();
|
|
const steamPassword = document.getElementById("steamPassword").value.trim();
|
|
const steamGslt = document.getElementById("steamGslt").value.trim();
|
|
|
|
const statusEl = document.getElementById("createServerStatus");
|
|
|
|
if (!id) {
|
|
statusEl.style.color = "red";
|
|
statusEl.textContent = "Server ID is required.";
|
|
return;
|
|
}
|
|
|
|
if (!steamUsername || !steamPassword) {
|
|
statusEl.style.color = "red";
|
|
statusEl.textContent = "Steam username and password are required.";
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`/servers`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
id,
|
|
name: name || null,
|
|
steamUsername,
|
|
steamPassword,
|
|
steamGslt: steamGslt || null
|
|
})
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const err = await res.text();
|
|
statusEl.style.color = "red";
|
|
statusEl.textContent = "Error: " + err;
|
|
return;
|
|
}
|
|
|
|
statusEl.style.color = "#4CAF50";
|
|
statusEl.textContent = "Server created successfully.";
|
|
|
|
document.getElementById("newServerId").value = "";
|
|
document.getElementById("newServerName").value = "";
|
|
document.getElementById("steamUsername").value = "";
|
|
document.getElementById("steamPassword").value = "";
|
|
document.getElementById("steamGslt").value = "";
|
|
|
|
showServers();
|
|
|
|
} catch (e) {
|
|
statusEl.style.color = "red";
|
|
statusEl.textContent = "Request failed: " + e.message;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
// SERVERS PAGE
|
|
// ---------------------------------------------------------
|
|
|
|
async function showServers() {
|
|
const servers = await api('/servers');
|
|
|
|
const html = `
|
|
<div class="section-title">Servers</div>
|
|
|
|
<div class="create-box">
|
|
<h3>Create New Server</h3>
|
|
|
|
<label>Server ID</label><br>
|
|
<input id="newServerId" class="input" type="text" placeholder="myserver01"><br>
|
|
|
|
<label>Server Name (optional)</label><br>
|
|
<input id="newServerName" class="input" type="text" placeholder="My Windrose Server"><br>
|
|
|
|
<label>Steam Username</label><br>
|
|
<input id="steamUsername" class="input" type="text" placeholder="steamuser123"><br>
|
|
|
|
<label>Steam Password</label><br>
|
|
<input id="steamPassword" class="input" type="password" placeholder="••••••••"><br>
|
|
|
|
<label>Steam GSLT (optional)</label><br>
|
|
<input id="steamGslt" class="input" type="text" placeholder="GSLT token"><br>
|
|
|
|
<button class="btn" onclick="createServer()">Create Server</button>
|
|
<div id="createServerStatus" style="margin-top:10px;"></div>
|
|
</div>
|
|
|
|
<table>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Name</th>
|
|
<th>Status</th>
|
|
<th>Version</th>
|
|
<th>Auto-Restart</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
${servers.map(s => `
|
|
<tr>
|
|
<td>${s.id}</td>
|
|
<td>${s.name}</td>
|
|
<td>${s.process?.status ?? 'stopped'}</td>
|
|
<td>${s.version?.buildId ?? '—'}</td>
|
|
<td>
|
|
<input type="checkbox" ${s.autoRestart ? 'checked' : ''}
|
|
onchange="toggleAutoRestart('${s.id}', this.checked)">
|
|
</td>
|
|
<td>
|
|
<button class="btn" onclick="startServer('${s.id}')">Start</button>
|
|
<button class="btn" onclick="stopServer('${s.id}')">Stop</button>
|
|
<button class="btn" onclick="restartServer('${s.id}')">Restart</button>
|
|
</td>
|
|
</tr>
|
|
`).join('')}
|
|
</table>
|
|
`;
|
|
|
|
document.getElementById('content').innerHTML = html;
|
|
}
|
|
|
|
async function startServer(id) {
|
|
await api(`/servers/${id}/start`, { method: 'POST' });
|
|
showServers();
|
|
}
|
|
async function stopServer(id) {
|
|
await api(`/servers/${id}/stop`, { method: 'POST' });
|
|
showServers();
|
|
}
|
|
async function restartServer(id) {
|
|
await api(`/servers/${id}/restart`, { method: 'POST' });
|
|
showServers();
|
|
}
|
|
async function toggleAutoRestart(id, enabled) {
|
|
await api(`/servers/${id}/auto-restart`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ enabled })
|
|
});
|
|
showServers();
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
// CRASH EVENTS
|
|
// ---------------------------------------------------------
|
|
|
|
async function showEvents() {
|
|
const servers = await api('/servers');
|
|
let html = `<div class="section-title">Crash Events</div>`;
|
|
|
|
for (const s of servers) {
|
|
const events = await api(`/servers/${s.id}/events`);
|
|
html += `
|
|
<h3>${s.name} (${s.id})</h3>
|
|
<pre>${JSON.stringify(events, null, 2)}</pre>
|
|
`;
|
|
}
|
|
|
|
document.getElementById('content').innerHTML = html;
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
// LIVE LOGS VIEWER
|
|
// ---------------------------------------------------------
|
|
|
|
let logEventSource = null;
|
|
|
|
async function showLogs() {
|
|
const servers = await api('/servers');
|
|
|
|
const html = `
|
|
<div class="section-title">Live Logs Viewer</div>
|
|
<p>Select a server and log type to stream logs in real time.</p>
|
|
|
|
<label>Server:</label>
|
|
<select id="logServerSelect" onchange="startLogStream()">
|
|
<option value="">-- choose server --</option>
|
|
${servers.map(s => `<option value="${s.id}">${s.name}</option>`).join('')}
|
|
</select>
|
|
|
|
<label class="checkbox">Log Type:</label>
|
|
<select id="logTypeSelect" onchange="startLogStream()">
|
|
<option value="stdout">stdout</option>
|
|
<option value="stderr">stderr</option>
|
|
</select>
|
|
|
|
<pre id="logOutput">(no logs yet)</pre>
|
|
`;
|
|
|
|
document.getElementById('content').innerHTML = html;
|
|
}
|
|
|
|
function startLogStream() {
|
|
const serverId = document.getElementById('logServerSelect').value;
|
|
const type = document.getElementById('logTypeSelect').value;
|
|
|
|
const output = document.getElementById('logOutput');
|
|
|
|
if (!serverId) {
|
|
output.textContent = '(no server selected)';
|
|
return;
|
|
}
|
|
|
|
if (logEventSource) {
|
|
logEventSource.close();
|
|
}
|
|
|
|
logEventSource = new EventSource(`/servers/${serverId}/logs/stream?type=${type}`);
|
|
|
|
output.textContent = '';
|
|
|
|
logEventSource.onmessage = (event) => {
|
|
output.textContent += event.data + '\n';
|
|
output.scrollTop = output.scrollHeight;
|
|
};
|
|
|
|
logEventSource.onerror = () => {
|
|
output.textContent += '\n[stream disconnected]\n';
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
// CONFIG EDITOR UI
|
|
// ---------------------------------------------------------
|
|
|
|
async function showConfig() {
|
|
const servers = await api('/servers');
|
|
|
|
const html = `
|
|
<div class="section-title">Config Editor</div>
|
|
|
|
<label>Select Server:</label>
|
|
<select id="configServerSelect" onchange="loadConfigEditor()">
|
|
<option value="">-- choose server --</option>
|
|
${servers.map(s => `<option value="${s.id}">${s.name}</option>`).join('')}
|
|
</select>
|
|
|
|
<div id="configEditor" style="margin-top:20px;"></div>
|
|
`;
|
|
|
|
document.getElementById('content').innerHTML = html;
|
|
}
|
|
|
|
async function loadConfigEditor() {
|
|
const serverId = document.getElementById("configServerSelect").value;
|
|
const editor = document.getElementById("configEditor");
|
|
|
|
if (!serverId) {
|
|
editor.innerHTML = "";
|
|
return;
|
|
}
|
|
|
|
const cfg = await api(`/servers/${serverId}/config`);
|
|
|
|
editor.innerHTML = `
|
|
<h3>Editing Config for: ${serverId}</h3>
|
|
|
|
<h4>Steam Credentials</h4>
|
|
|
|
<label>Steam Username</label><br>
|
|
<input id="cfgSteamUsername" class="input" type="text" value="${cfg.steamUsername || ''}"><br>
|
|
|
|
<label>Steam Password</label><br>
|
|
<input id="cfgSteamPassword" class="input" type="password" value="${cfg.steamPassword || ''}"><br>
|
|
|
|
<h4>server.json</h4>
|
|
<textarea id="serverCfg">${JSON.stringify(cfg.serverCfg, null, 2)}</textarea>
|
|
|
|
<h4>world.json</h4>
|
|
<textarea id="worldCfg">${JSON.stringify(cfg.worldCfg, null, 2)}</textarea>
|
|
|
|
<button class="btn" onclick="saveConfig('${serverId}')">Save Config</button>
|
|
<div id="configSaveStatus" style="margin-top:10px;"></div>
|
|
`;
|
|
}
|
|
|
|
async function saveConfig(serverId) {
|
|
const serverCfgText = document.getElementById("serverCfg").value;
|
|
const worldCfgText = document.getElementById("worldCfg").value;
|
|
const status = document.getElementById("configSaveStatus");
|
|
|
|
const steamUsername = document.getElementById("cfgSteamUsername").value.trim();
|
|
const steamPassword = document.getElementById("cfgSteamPassword").value.trim();
|
|
|
|
let serverCfg, worldCfg;
|
|
|
|
try {
|
|
serverCfg = JSON.parse(serverCfgText);
|
|
worldCfg = JSON.parse(worldCfgText);
|
|
} catch (err) {
|
|
status.style.color = "red";
|
|
status.textContent = "Invalid JSON: " + err.message;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`/servers/${serverId}/config`, {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
serverCfg,
|
|
worldCfg,
|
|
steamUsername,
|
|
steamPassword
|
|
})
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const err = await res.text();
|
|
status.style.color = "red";
|
|
status.textContent = "Error: " + err;
|
|
return;
|
|
}
|
|
|
|
status.style.color = "#4CAF50";
|
|
status.textContent = "Config saved successfully.";
|
|
|
|
} catch (err) {
|
|
status.style.color = "red";
|
|
status.textContent = "Request failed: " + err.message;
|
|
}
|
|
}
|
|
|
|
// Load servers on startup
|
|
showServers();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|