// ButtonTask v2 — web configurator client logic const $ = (sel) => document.querySelector(sel); const $$ = (sel) => Array.from(document.querySelectorAll(sel)); async function api(method, path, body) { const opts = { method, headers: {} }; if (body !== undefined) { opts.headers["Content-Type"] = "application/json"; opts.body = JSON.stringify(body); } const r = await fetch(path, opts); if (r.status === 401) { window.location.href = "/login?next=" + encodeURIComponent(window.location.pathname); throw new Error("auth required"); } if (!r.ok) { let msg = `${method} ${path} -> ${r.status}`; try { const j = await r.json(); if (j.error) msg = j.error; } catch (e) {} throw new Error(msg); } return r.headers.get("content-type")?.includes("application/json") ? r.json() : r.text(); } function flash(text, isError) { const t = document.createElement("div"); t.className = "toast" + (isError ? " error" : ""); t.textContent = text; document.body.appendChild(t); setTimeout(() => t.remove(), 2800); } // ---------- Tabs ---------- $$(".tab").forEach(tab => { tab.addEventListener("click", () => { $$(".tab").forEach(t => t.classList.remove("active")); $$(".panel").forEach(p => p.classList.remove("active")); tab.classList.add("active"); $("#tab-" + tab.dataset.tab).classList.add("active"); if (tab.dataset.tab === "network") loadNetwork(); if (tab.dataset.tab === "update") loadUpdateStatus(); }); }); // ---------- Layout ---------- $("#save-layout")?.addEventListener("click", async () => { try { await api("PUT", "/api/layout", { mode: $("#layout-mode").value, columns: parseInt($("#layout-columns").value, 10), spacing: parseInt($("#layout-spacing").value, 10), showLabels: $("#layout-show-labels").checked, }); flash("Расположение сохранено"); } catch (e) { flash(e.message, true); } }); // ---------- Background ---------- $("#save-bg")?.addEventListener("click", async () => { try { await api("PUT", "/api/background", { type: $("#bg-type").value, color1: $("#bg-c1").value, color2: $("#bg-c2").value, animated: $("#bg-animated").checked, imagePath: $("#bg-image").value, }); flash("Фон сохранён"); } catch (e) { flash(e.message, true); } }); // ---------- Feedback (borders) ---------- function syncFeedbackPreview() { const ok = $("#fb-preview-ok"), err = $("#fb-preview-error"); if (!ok) return; const okColor = $("#fb-ok-color").value; const errColor = $("#fb-error-color").value; const okW = $("#fb-ok-width").value; const errW = $("#fb-error-width").value; const glow = $("#fb-glow").value; $("#fb-ok-width-val").textContent = okW; $("#fb-error-width-val").textContent = errW; $("#fb-glow-val").textContent = glow; ok.style.boxShadow = `0 0 ${glow}px ${Math.round(glow/2)}px ${okColor}`; ok.style.border = `${okW}px solid ${okColor}`; err.style.boxShadow = `0 0 ${glow}px ${Math.round(glow/2)}px ${errColor}`; err.style.border = `${errW}px solid ${errColor}`; } ["fb-ok-color", "fb-error-color", "fb-ok-width", "fb-error-width", "fb-glow"] .forEach(id => $("#" + id)?.addEventListener("input", syncFeedbackPreview)); syncFeedbackPreview(); $("#save-feedback")?.addEventListener("click", async () => { try { await api("PUT", "/api/feedback", { okColor: $("#fb-ok-color").value, errorColor: $("#fb-error-color").value, okWidth: parseInt($("#fb-ok-width").value, 10), errorWidth: parseInt($("#fb-error-width").value, 10), glowRadius: parseInt($("#fb-glow").value, 10), }); flash("Обводка сохранена"); } catch (e) { flash(e.message, true); } }); // ---------- Icons ---------- $("#upload-icon")?.addEventListener("click", async () => { const file = $("#icon-file").files[0]; if (!file) { flash("Выберите файл", true); return; } const fd = new FormData(); fd.append("file", file); const r = await fetch("/api/icons", { method: "POST", body: fd }); if (!r.ok) { flash("Ошибка загрузки", true); return; } location.reload(); }); $$(".icon-tile .del").forEach(btn => { btn.addEventListener("click", async (e) => { const name = e.target.dataset.name; if (!confirm(`Удалить ${name}?`)) return; await api("DELETE", `/api/icons/${encodeURIComponent(name)}`); location.reload(); }); }); // ---------- Password ---------- $("#save-password")?.addEventListener("click", async () => { const pwd = $("#new-password").value; if (!pwd) { flash("Введите пароль", true); return; } await api("PUT", "/api/settings", { password: pwd }); flash("Пароль сохранён. Действует со следующего входа."); $("#new-password").value = ""; }); // ---------- Buttons drag-drop ---------- const list = $("#buttons-list"); if (list && window.Sortable) { new Sortable(list, { handle: ".handle", animation: 150, onEnd: async () => { const order = Array.from(list.children).map(li => li.dataset.id); await api("POST", "/api/buttons/reorder", { order }); flash("Порядок сохранён"); }, }); } // ---------- Buttons CRUD ---------- const dlg = $("#btn-dialog"); const form = $("#btn-form"); function openDialogFor(btn) { $("#btn-dialog-title").textContent = btn ? "Редактирование" : "Новая кнопка"; $("#b-id").value = btn?.id || ""; $("#b-label").value = btn?.label || ""; $("#b-icon").value = btn?.iconPath || ""; $("#b-type").value = btn?.action?.type || "http_get"; $("#b-url").value = btn?.action?.url || ""; $("#b-success").value = btn?.feedback?.successText || ""; $("#b-error").value = btn?.feedback?.errorText || "Ошибка"; $("#b-color").value = btn?.color || "#7b007b"; dlg.showModal(); } $("#add-btn")?.addEventListener("click", () => openDialogFor(null)); $$(".buttons-list .edit").forEach(btn => { btn.addEventListener("click", async (e) => { const id = e.target.dataset.id; const cfg = await api("GET", "/api/config"); const b = cfg.buttons.find(x => x.id === id); if (b) openDialogFor(b); }); }); $$(".buttons-list .del").forEach(btn => { btn.addEventListener("click", async (e) => { const id = e.target.dataset.id; if (!confirm("Удалить кнопку?")) return; await api("DELETE", `/api/buttons/${encodeURIComponent(id)}`); location.reload(); }); }); form?.addEventListener("submit", async (e) => { if (e.submitter && e.submitter.value === "cancel") return; e.preventDefault(); const id = $("#b-id").value; const payload = { label: $("#b-label").value, iconPath: $("#b-icon").value, actionType: $("#b-type").value, url: $("#b-url").value, successText: $("#b-success").value, errorText: $("#b-error").value, color: $("#b-color").value, }; if (id) await api("PUT", `/api/buttons/${encodeURIComponent(id)}`, payload); else await api("POST", "/api/buttons", payload); dlg.close(); location.reload(); }); // ---------- Network ---------- function toggleStatic() { const wrap = $("#net-mode")?.closest(".grid-form"); if (!wrap) return; wrap.classList.toggle("show-static", $("#net-mode").value === "static"); } $("#net-mode")?.addEventListener("change", toggleStatic); async function loadNetwork() { const statusEl = $("#net-status"); const ifaceSel = $("#net-iface"); if (!statusEl) return; statusEl.textContent = "Загрузка состояния…"; try { const data = await api("GET", "/api/network"); const devs = data.devices || []; if (!devs.length) { statusEl.innerHTML = "
Ethernet-интерфейсы не найдены (или nmcli недоступен).
"; } else { statusEl.innerHTML = devs.map(d => { const up = (d.state || "").includes("connected") && !(d.state || "").includes("dis"); const badge = up ? "up" : "down"; const addr = (d.addresses || []).join(", ") || "—"; return `${e.message}
`; } } $("#net-refresh")?.addEventListener("click", loadNetwork); $("#net-apply")?.addEventListener("click", async () => { const mode = $("#net-mode").value; const payload = { iface: $("#net-iface").value, mode }; if (mode === "static") { payload.address = $("#net-address").value.trim(); payload.prefix = parseInt($("#net-prefix").value, 10); payload.gateway = $("#net-gateway").value.trim(); payload.dns = $("#net-dns").value.trim(); } if (!payload.iface) { flash("Выберите интерфейс", true); return; } if (!confirm("Применить сетевые настройки? Соединение может прерваться.")) return; try { await api("PUT", "/api/network", payload); flash("Сетевые настройки применены"); setTimeout(loadNetwork, 1500); } catch (e) { flash(e.message, true); } }); // ---------- Update / OTA ---------- let updatePollTimer = null; function renderUpdateStatus(s) { const el = $("#update-status"); if (!el) return; const st = (s.status && s.status.state) || "idle"; const map = { idle: ["Статус: готово к обновлению", ""], staging: ["Распаковка пакета…", "busy"], switching: ["Переключение версии…", "busy"], health_check: ["Проверка работоспособности…", "busy"], success: ["Обновление успешно применено", "ok"], rolledback: ["Запуск новой версии не удался — выполнен откат", "fail"], rollback_done: ["Откат выполнен", "ok"], error: ["Ошибка обновления", "fail"], }; const [text, cls] = map[st] || [`Статус: ${st}`, ""]; el.className = "update-status " + cls; el.textContent = (s.status && s.status.message) ? `${text} — ${s.status.message}` : text; if (s.current) { $("#update-version").textContent = s.current.version || "—"; $("#update-path").textContent = s.current.path || "—"; } const busy = ["staging", "switching", "health_check"].includes(st); if (busy && !updatePollTimer) { updatePollTimer = setInterval(loadUpdateStatus, 2000); } else if (!busy && updatePollTimer) { clearInterval(updatePollTimer); updatePollTimer = null; } } async function loadUpdateStatus() { try { const s = await api("GET", "/api/update/status"); renderUpdateStatus(s); } catch (e) { const el = $("#update-status"); if (el) { el.className = "update-status fail"; el.textContent = e.message; } } } $("#update-upload")?.addEventListener("click", async () => { const file = $("#update-file").files[0]; if (!file) { flash("Выберите пакет .tar.gz", true); return; } if (!confirm("Загрузить и применить обновление? Сервис будет перезапущен.")) return; const fd = new FormData(); fd.append("file", file); try { const r = await fetch("/api/update", { method: "POST", body: fd }); if (!r.ok) { let msg = "Ошибка загрузки"; try { const j = await r.json(); if (j.error) msg = j.error; } catch (e) {} throw new Error(msg); } flash("Обновление запущено"); setTimeout(loadUpdateStatus, 2000); } catch (e) { flash(e.message, true); } }); $("#update-rollback")?.addEventListener("click", async () => { if (!confirm("Откатить на предыдущую версию?")) return; try { await api("POST", "/api/update/rollback"); flash("Откат запущен"); setTimeout(loadUpdateStatus, 2000); } catch (e) { flash(e.message, true); } }); toggleStatic();