/* 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);
})();