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.
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
// Modpack update controls.
|
||||
//
|
||||
// The card hides itself when there's no update available and reveals when the
|
||||
// manifest reports a newer pack version. Polls /api/update/status (every 5 s
|
||||
// when idle, every 1 s when an update is in-flight) to keep state fresh.
|
||||
"use strict";
|
||||
|
||||
import { api, apiJson } from "./api.js";
|
||||
|
||||
const els = {};
|
||||
let pollTimer = null;
|
||||
let pollInterval = 5000;
|
||||
|
||||
function setPolling(intervalMs) {
|
||||
if (intervalMs === pollInterval && pollTimer) return;
|
||||
if (pollTimer) clearInterval(pollTimer);
|
||||
pollInterval = intervalMs;
|
||||
pollTimer = setInterval(tick, intervalMs);
|
||||
}
|
||||
|
||||
async function tick() {
|
||||
let s;
|
||||
try { s = await api("/api/update/status"); }
|
||||
catch { return; }
|
||||
|
||||
els.current.textContent = s.current ?? "--";
|
||||
els.available.textContent = s.available ?? "--";
|
||||
|
||||
const card = els.card;
|
||||
if (s.needsUpdate || s.inProgress) {
|
||||
card.hidden = false;
|
||||
card.classList.toggle("has-update", s.needsUpdate && !s.inProgress);
|
||||
} else {
|
||||
card.hidden = true;
|
||||
}
|
||||
|
||||
if (s.inProgress) {
|
||||
els.progress.hidden = false;
|
||||
els.start.disabled = true;
|
||||
els.delay.disabled = true;
|
||||
els.phase.textContent = phaseLabel(s.phase);
|
||||
|
||||
const showCancel = s.phase === "countdown";
|
||||
els.cancel.hidden = !showCancel;
|
||||
|
||||
if (s.phase === "countdown" && s.countdownTotal > 0) {
|
||||
const elapsed = s.countdownTotal - s.countdownRemaining;
|
||||
const pct = (elapsed / s.countdownTotal) * 100;
|
||||
els.fill.style.width = `${pct}%`;
|
||||
els.status.textContent = `Restarting in ${formatSeconds(s.countdownRemaining)}`;
|
||||
} else {
|
||||
// Indeterminate during sync / loader install / start phases --
|
||||
// just show 100% and a phase-specific status string.
|
||||
els.fill.style.width = "100%";
|
||||
els.status.textContent = phaseStatus(s.phase);
|
||||
}
|
||||
setPolling(1000);
|
||||
} else {
|
||||
els.progress.hidden = true;
|
||||
els.start.disabled = !s.needsUpdate;
|
||||
els.delay.disabled = false;
|
||||
if (s.phase === "failed" && s.error) {
|
||||
els.progress.hidden = false;
|
||||
els.phase.textContent = "FAILED";
|
||||
els.status.textContent = s.error;
|
||||
els.fill.style.width = "0%";
|
||||
}
|
||||
setPolling(5000);
|
||||
}
|
||||
}
|
||||
|
||||
function phaseLabel(phase) {
|
||||
switch (phase) {
|
||||
case "countdown": return "COUNTDOWN";
|
||||
case "stopping": return "STOPPING";
|
||||
case "syncing": return "SYNCING MODS";
|
||||
case "installing_loader": return "INSTALLING LOADER";
|
||||
case "starting": return "STARTING";
|
||||
case "complete": return "COMPLETE";
|
||||
case "failed": return "FAILED";
|
||||
case "cancelled": return "CANCELLED";
|
||||
default: return "WORKING";
|
||||
}
|
||||
}
|
||||
|
||||
function phaseStatus(phase) {
|
||||
switch (phase) {
|
||||
case "stopping": return "Stopping Minecraft cleanly...";
|
||||
case "syncing": return "Syncing mods from manifest...";
|
||||
case "installing_loader": return "Re-running NeoForge installer...";
|
||||
case "starting": return "Starting Minecraft...";
|
||||
case "complete": return "Update complete.";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
function formatSeconds(s) {
|
||||
if (s < 60) return `${s}s`;
|
||||
const m = Math.floor(s / 60), r = s % 60;
|
||||
return `${m}m ${String(r).padStart(2, "0")}s`;
|
||||
}
|
||||
|
||||
export function setupUpdate() {
|
||||
els.card = document.getElementById("updateCard");
|
||||
els.current = document.getElementById("updCurrent");
|
||||
els.available = document.getElementById("updAvailable");
|
||||
els.delay = document.getElementById("updDelay");
|
||||
els.start = document.getElementById("updStart");
|
||||
els.progress = document.getElementById("updProgress");
|
||||
els.phase = document.getElementById("updPhaseLabel");
|
||||
els.fill = document.getElementById("updProgressFill");
|
||||
els.status = document.getElementById("updStatusText");
|
||||
els.cancel = document.getElementById("updCancel");
|
||||
|
||||
els.start.addEventListener("click", async () => {
|
||||
const delay = parseInt(els.delay.value, 10);
|
||||
if (!Number.isFinite(delay) || delay < 0) {
|
||||
alert("Enter a non-negative warning duration.");
|
||||
return;
|
||||
}
|
||||
if (!confirm(`Update modpack? Players get a ${delay}s warning, then the server restarts.`)) return;
|
||||
try {
|
||||
await apiJson("/api/update/start", { delaySeconds: delay });
|
||||
await tick();
|
||||
} catch (e) { alert(e.message); }
|
||||
});
|
||||
|
||||
els.cancel.addEventListener("click", async () => {
|
||||
if (!confirm("Cancel the countdown? Update will be aborted; server stays running.")) return;
|
||||
try {
|
||||
await apiJson("/api/update/cancel", {});
|
||||
await tick();
|
||||
} catch (e) { alert(e.message); }
|
||||
});
|
||||
|
||||
tick();
|
||||
setPolling(5000);
|
||||
}
|
||||
Reference in New Issue
Block a user