a1331212cb
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.
380 lines
19 KiB
HTML
380 lines
19 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>
|
||
|
||
<script type="module" src="/app.js"></script>
|
||
</body>
|
||
</html>
|