bf53b65706
When `webPassword` is null and the daemon starts headless (systemd, piped
SSH), no longer auto-generate a random password. Instead:
- Boot normally with the gate denying everything except /api/auth/setup
- Panel UI eagerly probes new /api/auth/state on load and renders a
first-run setup overlay (password + confirm) when needsSetup=true
- POST /api/auth/setup writes the chosen password and issues the auth
cookie in the same response, so the operator lands logged in
Interactive TTY behaviour (prompt at the console) is unchanged. The gate
middleware is now registered unconditionally so first-run mode is still
locked-down instead of wide-open.
395 lines
20 KiB
HTML
395 lines
20 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||
<title>Brass & Sigil -- Server Panel</title>
|
||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||
<link rel="stylesheet" href="/styles.css" />
|
||
</head>
|
||
<body>
|
||
<div class="topbar">
|
||
<div class="topbar-left">
|
||
<img class="topbar-icon" src="/favicon.png" alt="" />
|
||
<h1>BRASS & SIGIL -- SERVER</h1>
|
||
</div>
|
||
<div id="statusPill" class="status-pill"><span class="dot"></span><span id="statusText">Connecting…</span></div>
|
||
</div>
|
||
|
||
<div class="layout">
|
||
<aside>
|
||
<div class="card">
|
||
<h2>Status</h2>
|
||
<div class="stat-row"><span class="key">PID</span><span id="pid" class="val">--</span></div>
|
||
<div class="stat-row"><span class="key">Uptime</span><span id="uptime" class="val">--</span></div>
|
||
<div class="stat-row"><span class="key">Pack</span><span id="packVersion" class="val">--</span></div>
|
||
<div class="stat-row"><span class="key">Players</span><span id="playerCount" class="val">--</span></div>
|
||
<div class="stat-row"><span class="key">World</span><span id="worldSize" class="val">--</span></div>
|
||
<div class="actions" style="margin-top: 14px;">
|
||
<button id="btnStart" class="ghost-btn">Start</button>
|
||
<button id="btnStop" class="danger">Stop</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Resources</h2>
|
||
<div class="res-block">
|
||
<div class="res-label"><span>Memory</span><span id="memUsage" class="res-val">--</span></div>
|
||
<div class="res-bar"><div id="memBar"></div></div>
|
||
</div>
|
||
<div class="res-block">
|
||
<div class="res-label"><span>CPU</span><span id="cpuCurrent" class="res-val">--</span></div>
|
||
<div class="res-bar"><div id="cpuBar"></div></div>
|
||
<div class="res-sub">
|
||
<span>Peak (60s) <strong id="cpuMax">--</strong></span>
|
||
<span>Avg (60s) <strong id="cpuAvg">--</strong></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Players online</h2>
|
||
<ul id="players" class="name-list">
|
||
<li class="empty-state">No-one online</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Whitelist <span id="wlReqBadge" class="badge" hidden></span></h2>
|
||
<div id="wlRequestsBlock" hidden>
|
||
<div class="wl-req-label">Pending requests</div>
|
||
<ul id="wlRequests" class="name-list"></ul>
|
||
</div>
|
||
<ul id="whitelist" class="name-list">
|
||
<li class="empty-state">No players whitelisted yet</li>
|
||
</ul>
|
||
<div class="input-row" style="margin-top: 8px;">
|
||
<div class="input-wrap">
|
||
<input id="wlInput" type="text" placeholder="Add player by username…" autocomplete="off" maxlength="16" />
|
||
</div>
|
||
<button id="wlAdd">Add</button>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<main>
|
||
<div class="card">
|
||
<h2>Console</h2>
|
||
<div id="console" class="console-pane">Connecting to server log…</div>
|
||
<div class="input-row">
|
||
<div class="input-wrap">
|
||
<div id="cmdGhost" class="ghost"></div>
|
||
<input id="cmdInput" type="text"
|
||
placeholder="Type a server command (e.g. say hello, op alice, whitelist add bob)…"
|
||
autocomplete="off" />
|
||
<div id="cmdSuggest" class="suggest-list"></div>
|
||
</div>
|
||
<button id="cmdSend">Send</button>
|
||
</div>
|
||
<div id="cmdHint" class="hint">
|
||
<kbd>Tab</kbd> autocomplete · <kbd>↑</kbd>/<kbd>↓</kbd> history · <kbd>Esc</kbd> dismiss
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<aside class="aside-right">
|
||
<div class="card">
|
||
<h2>Account</h2>
|
||
<div class="actions">
|
||
<button id="acctChangePw" class="ghost-btn">Change password</button>
|
||
<button id="acctLogout" class="ghost-btn">Log out</button>
|
||
</div>
|
||
<div id="acctChangeForm" class="acct-form" hidden>
|
||
<div class="input-wrap" style="margin-top: 10px;">
|
||
<input id="acctCurrent" type="password" placeholder="Current password" autocomplete="current-password" />
|
||
</div>
|
||
<div class="input-wrap" style="margin-top: 8px;">
|
||
<input id="acctNew" type="password" placeholder="New password (min 8)" autocomplete="new-password" />
|
||
</div>
|
||
<div class="input-wrap" style="margin-top: 8px;">
|
||
<input id="acctConfirm" type="password" placeholder="Confirm new password" autocomplete="new-password" />
|
||
</div>
|
||
<div class="actions" style="margin-top: 10px;">
|
||
<button id="acctSubmit">Update</button>
|
||
<button id="acctCancel" class="ghost-btn">Cancel</button>
|
||
</div>
|
||
<div id="acctMsg" class="acct-msg"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card" id="updateCard" hidden>
|
||
<h2>Modpack update</h2>
|
||
<div id="updateInfo" class="update-info">
|
||
<div class="stat-row"><span class="key">Current</span><span id="updCurrent" class="val">--</span></div>
|
||
<div class="stat-row"><span class="key">Available</span><span id="updAvailable" class="val">--</span></div>
|
||
</div>
|
||
<p class="update-note">Updating restarts Minecraft. Players see a countdown banner then the server stops, syncs new mods, and starts again.</p>
|
||
<div class="input-row" style="margin-top: 8px;">
|
||
<div class="input-wrap">
|
||
<input id="updDelay" type="number" min="0" max="3600" step="30" value="300" placeholder="Warning seconds" />
|
||
</div>
|
||
<button id="updStart">Update</button>
|
||
</div>
|
||
<div id="updProgress" class="update-progress" hidden>
|
||
<div id="updPhaseLabel" class="update-phase">Idle</div>
|
||
<div class="pg-progress-bar"><div id="updProgressFill"></div></div>
|
||
<div id="updStatusText" class="update-status">--</div>
|
||
<button id="updCancel" class="ghost-btn" hidden>Cancel countdown</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Server settings</h2>
|
||
<div class="stat-row"><span class="key">MOTD</span><span id="ssMotd" class="val">--</span></div>
|
||
<div class="stat-row"><span class="key">Difficulty</span><span id="ssDifficulty" class="val">--</span></div>
|
||
<div class="stat-row"><span class="key">View / Sim</span><span id="ssDistances" class="val">--</span></div>
|
||
<div class="stat-row"><span class="key">Max players</span><span id="ssMaxPlayers" class="val">--</span></div>
|
||
<div class="stat-row"><span class="key">Whitelist</span><span id="ssWhitelist" class="val">--</span></div>
|
||
<button class="ghost-btn" style="width: 100%; margin-top: 10px;" data-open-modal="modalSettings">Edit settings</button>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>World</h2>
|
||
<div class="trigger-list">
|
||
<button class="ghost-btn" data-open-modal="modalPregen">
|
||
<span>Pre-generate</span>
|
||
<span id="pgBadge" class="badge" hidden></span>
|
||
</button>
|
||
<button class="ghost-btn" data-open-modal="modalBackup">
|
||
<span>Backups</span>
|
||
<span id="bkpBadge" class="badge"></span>
|
||
</button>
|
||
<button class="ghost-btn" data-open-modal="modalMap">
|
||
<span>Map</span>
|
||
<span id="mapBadge" class="badge" hidden></span>
|
||
</button>
|
||
<button class="ghost-btn" data-open-modal="modalWipe">
|
||
<span>Wipe world</span>
|
||
<span class="badge" style="color: var(--danger); border-color: #6a2814;">danger</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
|
||
<!-- ── Modals ─────────────────────────────────────────────────────── -->
|
||
|
||
<div class="modal" id="modalPregen" hidden>
|
||
<div class="modal-backdrop"></div>
|
||
<div class="modal-dialog">
|
||
<div class="modal-header">
|
||
<h2>Pre-generate world</h2>
|
||
<button class="modal-close" aria-label="Close">×</button>
|
||
</div>
|
||
<p style="font-size: 12px; color: var(--text-muted); margin: 0 0 12px; line-height: 1.45;">
|
||
Smooths Distant Horizons by generating chunks ahead of time.
|
||
Run once after first start; takes a while (be patient -- server keeps running).
|
||
</p>
|
||
<div class="input-row" style="margin-top: 4px;">
|
||
<div class="input-wrap">
|
||
<input id="pgRadius" type="number" min="100" max="20000" step="100" value="1000" placeholder="Radius (blocks)" />
|
||
</div>
|
||
<button id="pgStart">Start</button>
|
||
</div>
|
||
<div class="actions" style="margin-top: 8px;">
|
||
<button id="pgPause" class="ghost-btn">Pause</button>
|
||
<button id="pgContinue" class="ghost-btn">Resume</button>
|
||
<button id="pgCancel" class="danger">Cancel</button>
|
||
</div>
|
||
<div class="pg-status">
|
||
<div class="stat-row"><span class="key">State</span><span id="pgState" class="val">Idle</span></div>
|
||
<div class="pg-progress-bar"><div id="pgProgressFill"></div></div>
|
||
<div class="stat-row"><span class="key">Progress</span><span id="pgProgressText" class="val">--</span></div>
|
||
<div class="stat-row"><span class="key">Chunks</span><span id="pgChunks" class="val">--</span></div>
|
||
<div class="stat-row"><span class="key">Rate</span><span id="pgRate" class="val">--</span></div>
|
||
<div class="stat-row"><span class="key">ETA</span><span id="pgEta" class="val">--</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal" id="modalBackup" hidden>
|
||
<div class="modal-backdrop"></div>
|
||
<div class="modal-dialog">
|
||
<div class="modal-header">
|
||
<h2>Backups</h2>
|
||
<button class="modal-close" aria-label="Close">×</button>
|
||
</div>
|
||
<div class="stat-row" style="font-size: 11px;">
|
||
<span class="key">Stored at</span><span id="backupDir" class="val" style="font-size: 11px;">--</span>
|
||
</div>
|
||
<div class="stat-row" style="font-size: 11px;">
|
||
<span class="key">Schedule</span><span id="backupSchedule" class="val" style="font-size: 11px;">--</span>
|
||
</div>
|
||
<div class="stat-row" style="font-size: 11px;">
|
||
<span class="key">Next run</span><span id="backupNext" class="val" style="font-size: 11px;">--</span>
|
||
</div>
|
||
<div class="stat-row" style="font-size: 11px;">
|
||
<span class="key">Keep</span><span id="backupKeep" class="val" style="font-size: 11px;">--</span>
|
||
</div>
|
||
<div class="actions" style="margin-top: 12px;">
|
||
<button id="bkpEditSchedule" class="ghost-btn" style="flex: 1;">Edit schedule</button>
|
||
<button id="bkpCreate">Create now</button>
|
||
</div>
|
||
<div id="bkpScheduleForm" class="acct-form" hidden style="margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--card-edge);">
|
||
<div class="input-row">
|
||
<div class="input-wrap" style="flex: 1;">
|
||
<input id="bkpScheduleInput" type="text" placeholder="04:00 | 04:00,16:00 | every 6h | every 30m" />
|
||
</div>
|
||
<div class="input-wrap" style="width: 80px; flex: 0 0 80px;">
|
||
<input id="bkpKeepInput" type="number" min="1" max="365" placeholder="Keep" />
|
||
</div>
|
||
</div>
|
||
<p style="font-size: 11px; color: var(--text-muted); margin: 8px 0 0; line-height: 1.45;">
|
||
Hourly = ~24 backups/day. Each backup pauses world saves for a few seconds.
|
||
For hourly retention, raise <em>keep</em> to 48+. Empty schedule disables auto-backups.
|
||
</p>
|
||
<div class="actions" style="margin-top: 8px;">
|
||
<button id="bkpScheduleSave">Save</button>
|
||
<button id="bkpScheduleCancel" class="ghost-btn">Cancel</button>
|
||
</div>
|
||
</div>
|
||
<ul id="bkpList" class="name-list" style="margin-top: 12px;">
|
||
<li class="empty-state">No backups yet</li>
|
||
</ul>
|
||
<div id="bkpMsg" class="acct-msg"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal" id="modalSettings" hidden>
|
||
<div class="modal-backdrop"></div>
|
||
<div class="modal-dialog" style="max-width: 560px;">
|
||
<div class="modal-header">
|
||
<h2>Server settings</h2>
|
||
<button class="modal-close" aria-label="Close">×</button>
|
||
</div>
|
||
<p style="font-size: 12px; color: var(--text-muted); margin: 0 0 14px;">
|
||
These map to <code>server.properties</code>. MC reads them at startup, so changes need a server restart to take effect.
|
||
</p>
|
||
<div class="settings-grid">
|
||
<label>MOTD<input id="ssfMotd" type="text" /></label>
|
||
<label>Gamemode<select id="ssfGamemode">
|
||
<option>survival</option><option>creative</option><option>adventure</option><option>spectator</option>
|
||
</select></label>
|
||
<label>Difficulty<select id="ssfDifficulty">
|
||
<option>peaceful</option><option>easy</option><option>normal</option><option>hard</option>
|
||
</select></label>
|
||
<label>View distance<input id="ssfViewDistance" type="number" min="3" max="32" step="1" /></label>
|
||
<label>Sim distance<input id="ssfSimulationDistance" type="number" min="3" max="32" step="1" /></label>
|
||
<label>Max players<input id="ssfMaxPlayers" type="number" min="1" max="200" step="1" /></label>
|
||
<label>Spawn protection<input id="ssfSpawnProtection" type="number" min="0" max="64" step="1" /></label>
|
||
</div>
|
||
<div class="settings-checks">
|
||
<label class="danger-row"><input id="ssfPvp" type="checkbox" /><span>PvP</span></label>
|
||
<label class="danger-row"><input id="ssfHardcore" type="checkbox" /><span>Hardcore</span></label>
|
||
<label class="danger-row"><input id="ssfAllowFlight" type="checkbox" /><span>Allow flight</span></label>
|
||
<label class="danger-row"><input id="ssfWhiteList" type="checkbox" /><span>Whitelist enabled</span></label>
|
||
<label class="danger-row"><input id="ssfEnforceWhitelist" type="checkbox" /><span>Enforce whitelist</span></label>
|
||
<label class="danger-row"><input id="ssfEnableCommandBlock" type="checkbox" /><span>Enable command blocks</span></label>
|
||
</div>
|
||
<div class="actions" style="margin-top: 14px;">
|
||
<button id="ssSave" style="flex: 1;">Save</button>
|
||
<button id="ssRestart" class="ghost-btn">Save & restart</button>
|
||
</div>
|
||
<div id="ssMsg" class="acct-msg"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal" id="modalMap" hidden>
|
||
<div class="modal-backdrop"></div>
|
||
<div class="modal-dialog">
|
||
<div class="modal-header">
|
||
<h2>World map</h2>
|
||
<button class="modal-close" aria-label="Close">×</button>
|
||
</div>
|
||
<p style="font-size: 12px; color: var(--text-muted); margin: 0 0 14px; line-height: 1.45;">
|
||
Renders the world to a browsable 3D map (BlueMap). Runs as a separate process -- no impact on the live MC server.
|
||
First render of a 5000-block area takes 2-6 hours; subsequent renders are incremental and much faster.
|
||
</p>
|
||
<div class="stat-row"><span class="key">Phase</span><span id="mapPhase" class="val">Idle</span></div>
|
||
<div class="stat-row"><span class="key">Last log</span><span id="mapLastLog" class="val" style="font-size: 10px; max-width: 300px; overflow: hidden; text-overflow: ellipsis;">--</span></div>
|
||
<div class="actions" style="margin-top: 14px;">
|
||
<button id="mapRender" style="flex: 1;">Render now</button>
|
||
<button id="mapCancel" class="danger" hidden>Cancel</button>
|
||
<button id="mapOpen" class="ghost-btn">Open map ↗</button>
|
||
</div>
|
||
<div id="mapMsg" class="acct-msg"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal" id="modalWipe" hidden>
|
||
<div class="modal-backdrop"></div>
|
||
<div class="modal-dialog danger">
|
||
<div class="modal-header">
|
||
<h2>Wipe world</h2>
|
||
<button class="modal-close" aria-label="Close">×</button>
|
||
</div>
|
||
<p class="danger-note">
|
||
Wipes the world directory and restarts the server with a fresh world.
|
||
With "Back up first" ticked, the old world is archived to your backup directory before deletion.
|
||
Players see a 30-second urgent warning before the wipe begins.
|
||
</p>
|
||
<label class="danger-row">
|
||
<input id="wipeBackup" type="checkbox" checked />
|
||
<span>Back up current world before wiping</span>
|
||
</label>
|
||
|
||
<div class="danger-section">
|
||
<div class="danger-section-title">World seed</div>
|
||
<div class="danger-row" style="margin-bottom: 6px;">
|
||
<span>Current:</span>
|
||
<code id="wipeCurrentSeed" style="margin-left: 8px;">loading...</code>
|
||
</div>
|
||
<label class="danger-row">
|
||
<input type="radio" name="wipeSeedMode" value="random" checked />
|
||
<span>Random new seed (Minecraft picks one)</span>
|
||
</label>
|
||
<label class="danger-row">
|
||
<input type="radio" name="wipeSeedMode" value="keep" />
|
||
<span>Keep current seed (regenerate identical world)</span>
|
||
</label>
|
||
<label class="danger-row">
|
||
<input type="radio" name="wipeSeedMode" value="custom" />
|
||
<span>Custom seed:</span>
|
||
<input id="wipeCustomSeed" type="text" placeholder="e.g. 12345 or 'a phrase'"
|
||
style="margin-left: 8px; flex: 1;" disabled />
|
||
</label>
|
||
</div>
|
||
|
||
<button id="wipeBtn" class="danger" style="margin-top: 14px; width: 100%;">Wipe world</button>
|
||
<div id="wipeMsg" class="acct-msg"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="footer">brass-sigil-server v0.1 -- embedded panel</div>
|
||
|
||
<div id="loginOverlay" class="login-overlay" hidden>
|
||
<div class="login-box">
|
||
<h2>Brass & Sigil</h2>
|
||
<p>Sign in to manage the server.</p>
|
||
<div class="input-wrap">
|
||
<input id="loginPassword" type="password" autocomplete="current-password" placeholder="Password" />
|
||
</div>
|
||
<button id="loginSubmit">Sign in</button>
|
||
<div id="loginError" class="login-error"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="setupOverlay" class="login-overlay" hidden>
|
||
<div class="login-box">
|
||
<h2>Brass & Sigil</h2>
|
||
<p>First-run setup. Pick an admin password — this is the credential you'll use to sign in from now on.</p>
|
||
<div class="input-wrap">
|
||
<input id="setupPassword" type="password" autocomplete="new-password" placeholder="New password (min 8 chars)" />
|
||
</div>
|
||
<div class="input-wrap">
|
||
<input id="setupConfirm" type="password" autocomplete="new-password" placeholder="Confirm password" />
|
||
</div>
|
||
<button id="setupSubmit">Set password & continue</button>
|
||
<div id="setupError" class="login-error"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script type="module" src="/app.js"></script>
|
||
</body>
|
||
</html>
|