Files
brass-and-sigil/server/wwwroot/index.html
T
Matt Sijbers a1331212cb Initial commit: Brass & Sigil monorepo
Self-hosted Minecraft modpack distribution + administration system.

- launcher/  Avalonia 12 desktop client; single-file win-x64 publish.
             Microsoft auth via XboxAuthNet, manifest+SHA-1 mod sync,
             portable install path, sidecar settings.
- server/    brass-sigil-server daemon (.NET 8, linux-x64). Wraps the
             MC subprocess, embedded Kestrel admin panel with cookie
             auth + rate limiting, RCON bridge, scheduled backups,
             BlueMap CLI integration with player markers + skin proxy,
             friend-side whitelist request flow, world wipe with seed
             selection (keep current / random / custom).
- pack/      pack.lock.json (Modrinth + manual CurseForge entries),
             data-only tweak source under tweaks/, build outputs in
             overrides/ (gitignored).
- scripts/   Build-Pack / Build-Tweaks / Update-Pack / Check-Updates
             plus Deploy-Brass.ps1 unified one-shot deploy with
             version-bump pre-flight and daemon-state detection.
2026-05-05 00:19:05 +01:00

380 lines
19 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Brass &amp; 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 &amp; 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 &amp; 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 &amp; 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>
<script type="module" src="/app.js"></script>
</body>
</html>