/* Handler's control panel. Sends authenticated commands to the relay and mirrors the live clock so the Handler sees exactly what the players see. */ (function () { "use strict"; const $ = (id) => document.getElementById(id); // --- control key (persisted locally) -------------------------------------- const tokenInput = $("token"); tokenInput.value = localStorage.getItem("oe-token") || ""; function saveToken() { localStorage.setItem("oe-token", tokenInput.value.trim()); flashHint("key set"); } $("saveToken").addEventListener("click", saveToken); tokenInput.addEventListener("keydown", (e) => { if (e.key === "Enter") saveToken(); }); let hintTimer = null; function flashHint(text) { const el = $("tokenHint"); el.textContent = text; clearTimeout(hintTimer); hintTimer = setTimeout(() => (el.textContent = ""), 1500); } // --- connection ----------------------------------------------------------- const model = OE.ClockModel(); const conn = OE.connect(OE.defaultWsUrl(), { onstatus(s) { $("lampLink").classList.toggle("lamp--on", s === "open"); }, onstate(state) { model.apply(state); syncRateButtons(state.rate); }, }); window.addEventListener("beforeunload", () => conn.close()); function send(obj) { const token = (localStorage.getItem("oe-token") || "").trim(); if (!token) { flashHint("no control key set"); tokenInput.focus(); return; } conn.send(Object.assign({ token }, obj)); } // --- wire fixed-command buttons (play/pause/pulse/communion) --------------- document.querySelectorAll("[data-cmd]").forEach((btn) => { btn.addEventListener("click", () => send({ cmd: btn.dataset.cmd })); }); // --- rate ----------------------------------------------------------------- document.querySelectorAll(".rate").forEach((btn) => { btn.addEventListener("click", () => send({ cmd: "rate", rate: Number(btn.dataset.rate) })); }); function syncRateButtons(rate) { document.querySelectorAll(".rate").forEach((b) => { b.classList.toggle("active", Number(b.dataset.rate) === rate); }); } // --- iterations ----------------------------------------------------------- document.querySelectorAll(".iter").forEach((btn) => { btn.addEventListener("click", () => send({ cmd: "iteration", n: Number(btn.dataset.iter) })); }); // --- timeline jumps ------------------------------------------------------- const jumps = $("jumps"); OE.EVENTS.forEach((ev) => { const b = document.createElement("button"); b.type = "button"; b.className = "btn jump" + (ev.communion ? " btn--red" : ev.pulse ? " btn--amber" : ""); b.innerHTML = "" + ev.time + "" + ev.label + ""; b.addEventListener("click", () => send({ cmd: "set", inworld: ev.t })); jumps.appendChild(b); }); // manual time -> seconds-of-day $("manualJump").addEventListener("click", () => { const parts = $("manualTime").value.split(":").map(Number); if (parts.length >= 2 && parts.every((n) => !Number.isNaN(n))) { const sec = (parts[0] || 0) * 3600 + (parts[1] || 0) * 60 + (parts[2] || 0); send({ cmd: "set", inworld: sec }); } }); // --- broadcast message ---------------------------------------------------- $("sendMsg").addEventListener("click", () => { const text = $("msg").value.trim(); if (text) send({ cmd: "msg", text }); }); $("clearMsg").addEventListener("click", () => send({ cmd: "msg", text: null })); $("msg").addEventListener("keydown", (e) => { if (e.key === "Enter") $("sendMsg").click(); }); // player-screen link points at clock.html next to this page $("clockLink").href = new URL("clock.html", location.href).href; // --- mirror loop ---------------------------------------------------------- function nextBeat(inworld) { const upcoming = OE.EVENTS.filter((e) => e.t > inworld + 0.5); return upcoming.length ? upcoming[0] : null; } function frame() { const s = model.now(); $("mClock").textContent = OE.clock(s.inworld); $("mIter").textContent = OE.ROMAN[s.iteration] || s.iteration; $("mCountdown").textContent = s.remaining <= 0 ? "00:00:00" : OE.duration(s.remaining); $("mRun").textContent = s.running ? "RUNNING" : "PAUSED"; $("mRun").classList.toggle("live", s.running); const nb = nextBeat(s.inworld); $("mNext").textContent = nb ? nb.time + " " + nb.label : "—"; requestAnimationFrame(frame); } requestAnimationFrame(frame); })();