/* VoxIP Kiosk Manager — progressive enhancements. Every page works without JS. */ (function () { "use strict"; const csrf = () => document.querySelector('meta[name="csrf-token"]').content; const $ = (sel, root = document) => root.querySelector(sel); const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel)); // Mobile navigation const toggle = $("[data-nav-toggle]"); if (toggle) { toggle.addEventListener("click", () => { const nav = $("#mainnav"); const open = nav.classList.toggle("open"); toggle.setAttribute("aria-expanded", String(open)); }); } // Confirmation dialog for dangerous actions (forms with data-confirm) const dialog = $("#confirm-dialog"); $$("form[data-confirm]").forEach((form) => { form.addEventListener("submit", (event) => { if (form.dataset.confirmed === "1" || !dialog || typeof dialog.showModal !== "function") return; event.preventDefault(); $("[data-confirm-title]", dialog).textContent = form.dataset.confirmTitle || "Weet je het zeker?"; $("[data-confirm-text]", dialog).textContent = form.dataset.confirm; dialog.returnValue = ""; dialog.showModal(); dialog.addEventListener("close", function onClose() { dialog.removeEventListener("close", onClose); if (dialog.returnValue === "ok") { form.dataset.confirmed = "1"; form.requestSubmit ? form.requestSubmit() : form.submit(); } }); }); }); // Disable buttons while a form submits, to prevent double actions $$("form").forEach((form) => { form.addEventListener("submit", (event) => { if (event.defaultPrevented || form.method.toLowerCase() !== "post") return; setTimeout(() => $$("button[type=submit], button:not([type])", form).forEach((b) => (b.disabled = true)), 0); }); }); // Meters (width set here because the CSP forbids inline styles) function setMeter(el, pct) { const value = Math.max(0, Math.min(100, Number(pct) || 0)); el.style.width = value + "%"; el.classList.toggle("is-high", value >= 90); el.classList.toggle("is-mid", value >= 75 && value < 90); } $$("[data-meter]").forEach((el) => setMeter(el, el.dataset.pct)); // Local time for epoch timestamps $$("time[data-epoch]").forEach((el) => { const d = new Date(Number(el.dataset.epoch) * 1000); el.dateTime = d.toISOString(); el.textContent = d.toLocaleString("nl-NL", { dateStyle: "short", timeStyle: "medium" }); }); // Live dashboard const live = $("[data-live-status]"); if (live) { const get = (obj, path) => path.split(".").reduce((o, k) => (o == null ? o : o[k]), obj); const refresh = async () => { try { const res = await fetch(live.dataset.liveStatus, { headers: { Accept: "application/json" }, cache: "no-store" }); if (res.status === 401) return location.reload(); if (!res.ok) return; const data = await res.json(); $$("[data-field]").forEach((el) => { const value = get(data, el.dataset.field); if (value !== undefined && value !== null) el.textContent = value; }); $$("[data-meter]").forEach((el) => setMeter(el, data[el.dataset.meter])); $$("[data-badge]").forEach((el) => { const key = el.dataset.badge; let status, label; if (key === "temperature") { status = data.temperature_status; label = data.temperature; } else if (data[key]) { status = data[key].status; label = data[key].label; } if (!status) return; el.className = "badge badge-" + status; $("[data-badge-label]", el).textContent = label; }); } catch (e) { /* device rebooting or offline: try again next round */ } }; setInterval(refresh, 5000); } // Screenshot const shotBtn = $("[data-screenshot-btn]"); if (shotBtn) { const box = $("[data-screenshot]"); const img = $("img", box); const msg = $("[data-screenshot-msg]", box); shotBtn.addEventListener("click", async () => { shotBtn.disabled = true; msg.textContent = "Screenshot maken…"; msg.hidden = false; try { const res = await fetch(box.dataset.screenshot + "?t=" + Date.now(), { cache: "no-store" }); if (!res.ok) throw new Error(await res.text()); const blob = await res.blob(); if (img.src) URL.revokeObjectURL(img.src); img.src = URL.createObjectURL(blob); img.hidden = false; msg.hidden = true; } catch (e) { img.hidden = true; msg.textContent = e.message || "Screenshot niet beschikbaar."; } finally { shotBtn.disabled = false; } }); } // Static IP fields only when "Statisch" is selected $$("form[data-ipform]").forEach((form) => { const fields = $("[data-static-fields]", form); const update = () => { const method = $('input[name="method"]:checked', form); fields.hidden = !method || method.value !== "static"; }; $$('input[name="method"]', form).forEach((r) => r.addEventListener("change", update)); update(); }); // "Scherm altijd aan" hides the timeout field $$("[data-toggle-target]").forEach((box) => { const target = $(box.dataset.toggleTarget); const update = () => { if (target) target.hidden = box.checked; }; box.addEventListener("change", update); update(); }); // Wifi scan const scanBtn = $("[data-wifi-scan]"); if (scanBtn) { const form = scanBtn.closest("form"); const list = $("[data-scan-results]", form); const datalist = $("#wifi-networks"); scanBtn.addEventListener("click", async () => { scanBtn.disabled = true; scanBtn.textContent = "Scannen…"; list.hidden = false; list.replaceChildren(Object.assign(document.createElement("li"), { textContent: "Netwerken zoeken…", className: "muted" })); try { const res = await fetch(scanBtn.dataset.wifiScan, { method: "POST", headers: { "X-CSRF-Token": csrf(), Accept: "application/json" } }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "Scannen mislukt"); list.replaceChildren(); datalist.replaceChildren(); if (!data.networks.length) list.append(Object.assign(document.createElement("li"), { textContent: "Geen netwerken gevonden.", className: "muted" })); data.networks.forEach((n) => { const li = document.createElement("li"); const b = document.createElement("button"); b.type = "button"; const name = document.createElement("span"); name.textContent = n.ssid + (n.in_use ? " (verbonden)" : ""); const meta = document.createElement("span"); meta.className = "muted"; meta.textContent = n.signal + "% · " + (n.security === "open" ? "open" : n.security); b.append(name, meta); b.addEventListener("click", () => { $('input[name="ssid"]', form).value = n.ssid; $('input[name="open"]', form).checked = n.security === "open"; list.hidden = true; $('input[name="psk"]', form).focus(); }); li.append(b); list.append(li); datalist.append(Object.assign(document.createElement("option"), { value: n.ssid })); }); } catch (e) { list.replaceChildren(Object.assign(document.createElement("li"), { textContent: e.message, className: "muted" })); } finally { scanBtn.disabled = false; scanBtn.textContent = "Scannen"; } }); } // Pending network change countdown const pending = $("[data-pending]"); if (pending) { let remaining = Number(pending.dataset.remaining) || 0; const counter = $("[data-countdown]", pending); const tick = setInterval(() => { remaining = Math.max(0, remaining - 1); counter.textContent = remaining; if (remaining === 0) { clearInterval(tick); setTimeout(() => location.reload(), 5000); } }, 1000); } // Redirect after the admin interface moved const moved = $("[data-redirect]"); if (moved) setTimeout(() => (location.href = moved.dataset.redirect), Number(moved.dataset.redirectAfter) * 1000); // Scroll log views to the newest line $$("[data-scroll-bottom]").forEach((el) => (el.scrollTop = el.scrollHeight)); })();