git / moonbite-network / reactor-app/ui/reactor-app.js · raw
/* MoonBite Reactor — controller. Drives the verbatim god-reactor carriers from a real live-chain mining loop. Mining = POST /api/mine {address} (the local launcher proxies to the live explorer, so there is no CORS problem and no RPC secret in the client). */(function () { "use strict"; // ---- carriers the god engine reads every frame ---- var orb = document.getElementById("miningOrb"); var elBlocks = document.getElementById("blocksMined"); var elTotal = document.getElementById("totalBlocks"); var elFill = document.getElementById("progressBarFill"); var elHeight = document.getElementById("currentHeight"); var elElapsed = document.getElementById("elapsedTime"); // ---- console UI ---- var form = document.getElementById("rxForm"); var input = document.getElementById("rxAddress"); var hint = document.getElementById("rxAddrHint"); var btnStart = document.getElementById("rxStart"); var btnStop = document.getElementById("rxStop"); var statBlocks = document.getElementById("rxStatBlocks"); var statHeight = document.getElementById("rxStatHeight"); var statUptime = document.getElementById("rxStatUptime"); var logfeed = document.getElementById("rxLog"); var connWrap = document.querySelector(".rx-conn"); var connText = document.getElementById("rxConnText"); var ADDR_KEY = "moonbite.reactor.address"; // ---- state ---- var mining = false; var blocks = 0; var startedAt = 0; var lastHeight = null; var workTarget = 0; // 0..100 work-meter, snaps on a found block var workNow = 0; var uptimeTimer = null; var meterRaf = null; // ---- helpers ---- // Log lines are plain text, never markup: the message often carries the // node's `error` string straight from the explorer response, and that // must not be able to inject HTML into the local app window. function log(msg, cls) { appendLogLine([msg], cls); } // Same, but `parts` may contain {b: "text"} for the few lines that want a // bold span. Each part is still inserted as text, never parsed. function appendLogLine(parts, cls) { var line = document.createElement("span"); line.className = "rx-logline" + (cls ? " " + cls : ""); parts.forEach(function (p) { if (p && typeof p === "object" && "b" in p) { var b = document.createElement("b"); b.textContent = String(p.b); line.appendChild(b); } else { line.appendChild(document.createTextNode(String(p))); } }); logfeed.appendChild(line); while (logfeed.childNodes.length > 60) logfeed.removeChild(logfeed.firstChild); logfeed.scrollTop = logfeed.scrollHeight; } function setConn(state, text) { connWrap.classList.remove("is-online", "is-offline"); if (state) connWrap.classList.add(state); connText.textContent = text; } // Basic client-side sanity only — the node is the real validator. function looksLikeAddress(a) { a = (a || "").trim(); if (a.length < 20 || a.length > 120) return false; if (/^(moon1|tmoon1|rmoon1)/.test(a)) return true; // bech32 if (/^[M23][1-9A-HJ-NP-Za-km-z]{20,}$/.test(a)) return true; // base58 P2PKH/P2SH return false; } function fmtElapsed(sec) { var m = Math.floor(sec / 60), s = sec % 60; return m > 0 ? m + "m " + s + "s" : s + "s"; } // ---- work meter: smooth ramp toward workTarget, mirrored into the beam ---- function meterLoop() { workNow += (workTarget - workNow) * 0.08; if (Math.abs(workTarget - workNow) < 0.1) workNow = workTarget; elFill.style.width = workNow.toFixed(1) + "%"; // while mining and not at a snap, keep creeping so it feels alive if (mining && workTarget < 92) workTarget = Math.min(92, workTarget + 0.25); meterRaf = requestAnimationFrame(meterLoop); } // ---- the mining loop ---- function mineRound() { if (!mining) return; fetch("/api/mine", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ address: input.value.trim() }) }).then(function (r) { return r.json().then(function (body) { return { ok: r.ok, body: body }; }); }).then(function (res) { if (!mining) return; setConn("is-online", "online"); var b = res.body || {}; if (b.error) { var errText = String(b.error); log("node: " + errText, "is-bad"); // A bad address / disabled node will not self-heal — stop cleanly. if (/invalid|disabled|demo/i.test(errText)) { stop("node rejected: " + errText); return; } setTimeout(mineRound, 1500); return; } if (b.found) { blocks += 1; workTarget = 100; workNow = 100; // snap the beam setTimeout(function () { if (mining) { workTarget = 0; workNow = 0; } }, 220); var h = (b.height != null) ? b.height : (lastHeight != null ? lastHeight + 1 : "?"); lastHeight = (typeof h === "number") ? h : lastHeight; var hash = (b.hashes && b.hashes[0]) ? b.hashes[0] : ""; var shortHash = hash ? (hash.slice(0, 10) + "…" + hash.slice(-6)) : ""; // update carriers (god engine flashes burst on blocksMined++) elBlocks.textContent = blocks; elHeight.textContent = h; elTotal.textContent = blocks; statBlocks.textContent = blocks; statHeight.textContent = h; appendLogLine(["+ block ", { b: "#" + h }, " mined" + (shortHash ? " " + shortHash : "")], "is-hit"); } else { log("… no block this round, retrying"); } setTimeout(mineRound, 250); }).catch(function (err) { if (!mining) return; setConn("is-offline", "offline"); log("connection lost — retrying (" + (err && err.message || "error") + ")", "is-bad"); setTimeout(mineRound, 2500); }); } // ---- start / stop ---- function start() { var addr = input.value.trim(); if (!looksLikeAddress(addr)) { input.classList.add("is-bad"); hint.classList.add("is-bad"); hint.textContent = "That doesn't look like a MoonBite address (moon1… or M…)."; input.focus(); return; } input.classList.remove("is-bad"); hint.classList.remove("is-bad"); hint.textContent = "Blocks you mine pay their reward straight to this address."; try { localStorage.setItem(ADDR_KEY, addr); } catch (e) {} mining = true; blocks = 0; lastHeight = null; workTarget = 0; workNow = 0; startedAt = Date.now(); elBlocks.textContent = "0"; elTotal.textContent = "0"; statBlocks.textContent = "0"; orb.classList.remove("idle"); // → god engine goes REACTOR ONLINE btnStart.hidden = true; btnStop.hidden = false; input.disabled = true; setConn("is-online", "reactor online"); appendLogLine(["⚡ reactor ignition — mining to ", { b: addr.slice(0, 6) + "…" + addr.slice(-4) }], "is-good"); uptimeTimer = setInterval(function () { var sec = Math.floor((Date.now() - startedAt) / 1000); var t = fmtElapsed(sec); statUptime.textContent = t; elElapsed.textContent = t; }, 500); meterLoop(); mineRound(); } function stop(reason) { if (!mining) return; mining = false; orb.classList.add("idle"); // → god engine returns to STANDBY if (uptimeTimer) { clearInterval(uptimeTimer); uptimeTimer = null; } if (meterRaf) { cancelAnimationFrame(meterRaf); meterRaf = null; } workTarget = 0; workNow = 0; elFill.style.width = "0%"; btnStart.hidden = false; btnStop.hidden = true; input.disabled = false; setConn("", "idle"); log(reason ? "■ powered down — " + reason : "■ reactor powered down. " + blocks + " block(s) this session.", "is-bad"); } // ---- wiring ---- form.addEventListener("submit", function (e) { e.preventDefault(); start(); }); btnStop.addEventListener("click", function () { stop(); }); input.addEventListener("input", function () { input.classList.remove("is-bad"); hint.classList.remove("is-bad"); }); btnStart.addEventListener("mousemove", function (e) { var rect = btnStart.getBoundingClientRect(); btnStart.style.setProperty("--mx", (e.clientX - rect.left) + "px"); }); // restore saved address try { var saved = localStorage.getItem(ADDR_KEY); if (saved) input.value = saved; } catch (e) {} // reachability ping (best-effort; the launcher exposes /api/health) fetch("/api/health").then(function (r) { return r.json(); }).then(function (h) { setConn("is-online", "explorer reachable"); if (h && h.height != null) { statHeight.textContent = h.height; elHeight.textContent = h.height; } }).catch(function () { setConn("is-offline", "explorer offline"); }); log("MoonBite Reactor ready. Enter your address and fire the core.");})();