(() => { // ========================= // Utils / status // ========================= const elGlobalStatus = document.getElementById("globalStatus"); const setStatus = (txt) => { if (!elGlobalStatus) return; elGlobalStatus.textContent = txt || ""; if (txt) setTimeout(() => (elGlobalStatus.textContent = ""), 1200); }; const debounceMap = new Map(); const debounce = (key, fn, ms) => { if (debounceMap.has(key)) clearTimeout(debounceMap.get(key)); debounceMap.set(key, setTimeout(fn, ms)); }; // Base URL from current page const HTTP_BASE = `${location.protocol}//${location.host}`; const WS_URL = (() => { const proto = location.protocol === "https:" ? "wss" : "ws"; return `${proto}://${location.host}/ws`; })(); // Helpful logs (можешь потом удалить) console.log("Page:", location.href); console.log("HTTP_BASE:", HTTP_BASE); console.log("WS_URL:", WS_URL); // ========================= // HTTP helpers // ========================= async function postJson(path, payload) { const r = await fetch(`${HTTP_BASE}${path}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload || {}), }); return r; } async function sendSetting(id, value) { try { const r = await postJson("/api/setting", { id, value }); if (!r.ok) { setStatus("Ошибка применения"); return; } setStatus("Сохранено"); } catch (e) { setStatus("Сервер недоступен"); } } async function sendCoins(amount) { const coinsStatus = document.getElementById("coinsStatus"); try { if (coinsStatus) coinsStatus.textContent = ""; const r = await postJson("/api/coins", { amount }); if (!r.ok) { if (coinsStatus) coinsStatus.textContent = "Ошибка запроса"; return; } if (coinsStatus) { coinsStatus.textContent = "Ок"; setTimeout(() => (coinsStatus.textContent = ""), 1200); } } catch (e) { if (coinsStatus) coinsStatus.textContent = "Сервер недоступен"; } } async function sendAction(name, value) { // value — опционально (для новой схемы: action + payload) try { const r = await postJson("/api/action", value !== undefined ? { name, value } : { name }); const t = await r.text(); if (!r.ok) { setStatus("Ошибка действия"); try { const j = JSON.parse(t); if (j && j.message) setStatus(j.message); } catch (_) {} return false; } setStatus("Ок"); return true; } catch (e) { setStatus("Сервер недоступен"); return false; } } // ========================= // Bind UI events (existing page) // ========================= // Settings controls by data-setting-id document.querySelectorAll("[data-setting-id]").forEach((el) => { const id = el.getAttribute("data-setting-id"); if (!id) return; if (el.type === "checkbox") { el.addEventListener("change", () => sendSetting(id, el.checked ? "on" : "off")); return; } if (el.type === "range") { const valueSpanId = el.getAttribute("data-value-span"); const valueSpan = valueSpanId ? document.getElementById(valueSpanId) : null; const apply = () => { if (valueSpan) valueSpan.textContent = el.value; debounce("rng_" + id, () => sendSetting(id, el.value), 120); }; el.addEventListener("input", apply); el.addEventListener("change", apply); return; } if (el.type === "radio") { el.addEventListener("change", () => { if (el.checked) sendSetting(id, el.value); }); return; } // number/text el.addEventListener("change", () => sendSetting(id, el.value)); }); // Coins: input + button const coinsAmount = document.getElementById("coinsAmount"); const addCoinsBtn = document.getElementById("addCoinsBtn"); if (addCoinsBtn) { addCoinsBtn.addEventListener("click", async () => { const v = parseInt((coinsAmount && coinsAmount.value) || "0", 10); if (!Number.isFinite(v) || v <= 0) { const coinsStatus = document.getElementById("coinsStatus"); if (coinsStatus) coinsStatus.textContent = "Введи число > 0"; return; } await sendCoins(v); }); } // Actions (buttons) document.querySelectorAll("[data-action-name]").forEach((btn) => { btn.addEventListener("click", async () => { const name = btn.getAttribute("data-action-name") || ""; if (!name) return; // Если кнопка "Сохранить" в карточках цен/валют — берём значение инпута рядом // (не влияет на обычные action-btn) let value; const card = btn.closest(".price-card, .currency-card"); if (card) { const input = card.querySelector("input"); if (input) value = input.value; } try { btn.disabled = true; await sendAction(name, value); } finally { setTimeout(() => (btn.disabled = false), 200); } }); }); // ========================= // WebSocket (preparing for new scheme) // ========================= let ws = null; let wsTimer = null; let wsBackoffMs = 500; function wsConnect() { if (ws) { try { ws.close(); } catch (_) {} ws = null; } if (wsTimer) { clearTimeout(wsTimer); wsTimer = null; } console.log("WS connecting:", WS_URL); try { ws = new WebSocket(WS_URL); } catch (e) { console.log("WS create failed:", e); scheduleReconnect(); return; } ws.addEventListener("open", () => { console.log("WS open"); wsBackoffMs = 500; // На будущее: можно отправлять hello/subscribe // ws.send(JSON.stringify({ op: "hello", page: "settings" })); }); ws.addEventListener("message", (ev) => { // Сервер дальше будет слать JSON snapshot/patch try { const msg = JSON.parse(ev.data); handleWsMessage(msg); } catch (e) { console.log("WS message not json:", ev.data); } }); ws.addEventListener("close", (e) => { console.log("WS close", e.code, e.reason); scheduleReconnect(); }); ws.addEventListener("error", (e) => { console.log("WS error", e); // Обычно после error будет close, но подстрахуемся scheduleReconnect(); }); } function scheduleReconnect() { if (wsTimer) return; const delay = Math.min(wsBackoffMs, 5000); wsBackoffMs = Math.min(wsBackoffMs * 1.6, 5000); wsTimer = setTimeout(() => { wsTimer = null; wsConnect(); }, delay); } // ===== WS message protocol (заготовка) ===== // Ожидаем: // { op:"snapshot", data:{ counters:{}, hints:{}, characterPrizes:[] } } // { op:"patch", data:{ counters:{...} } } function handleWsMessage(msg) { if (!msg || !msg.op) return; if (msg.op === "snapshot" || msg.op === "patch") { const data = msg.data || {}; if (data.counters) applyCounters(data.counters); if (data.hints) applyHints(data.hints); if (data.characterPrizes) applyCharacterPrizes(data.characterPrizes); if (data.passwordStatus) applyPasswordStatus(data.passwordStatus); return; } if (msg.op === "toast") { if (msg.text) setStatus(String(msg.text)); return; } } // ========================= // Apply data to DOM (no visual changes) // ========================= // counters: { key: value } // Важно: сейчас твои счетчики — это .form-group.counter где внутри label + .status // Мы ищем по тексту label (как fallback), но лучше позже добавить data-counter-key в html. function applyCounters(counters) { // 1) special ids if exist if (counters.impulsesResetCurrent !== undefined) { const el = document.getElementById("impulsesResetCurrent"); if (el) el.textContent = String(counters.impulsesResetCurrent); } // 2) generic: match label text -> value document.querySelectorAll(".form-group.counter").forEach((fg) => { const label = fg.querySelector("label"); const st = fg.querySelector(".status"); if (!label || !st) return; const key = label.textContent.trim(); if (Object.prototype.hasOwnProperty.call(counters, key)) { st.textContent = String(counters[key]); } }); } function applyHints(hints) { // пример: { impulsesResetHint: "..." } if (hints.impulsesResetHint !== undefined) { const el = document.getElementById("impulsesResetHint"); if (el) el.textContent = String(hints.impulsesResetHint); } } // characterPrizes: [{ character:"...", prize:"..." }, ...] или массив строк function applyCharacterPrizes(list) { const root = document.getElementById("characterPrizes"); const empty = document.getElementById("characterPrizesEmpty"); if (!root) return; root.innerHTML = ""; if (!list || !list.length) { if (empty) empty.style.display = "block"; return; } if (empty) empty.style.display = "none"; // вывод без смены стиля: просто строки в .status list.forEach((row) => { const line = document.createElement("div"); line.className = "status"; if (typeof row === "string") { line.textContent = row; } else if (row && typeof row === "object") { const c = row.character ?? row.name ?? ""; const p = row.prize ?? row.value ?? ""; line.textContent = `${c}: ${p}`.trim(); } else { line.textContent = String(row); } root.appendChild(line); }); } function applyPasswordStatus(statusText) { const el = document.getElementById("settingsPasswordStatus"); if (el && statusText !== undefined) el.textContent = String(statusText); } // Start WS wsConnect(); })();