(function () { const $ = (sel, root = document) => root.querySelector(sel); const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel)); const logContainer = $("#logContainer"); if (!logContainer) return; const displayLevelForm = $("#displayLevelForm"); const searchForm = $("#searchForm"); const queryInput = $("#query"); const levelSelect = $("#level"); const logScroll = $("#logScroll"); // toolbar const btnLive = $("#btn-live"); const btnAuto = $("#btn-autoscroll"); const btnWrap = $("#btn-wrap"); const fontSelect = $("#font-size"); const btnCopy = $("#btn-copy"); const btnDownload = $("#btn-download"); // konfiguracja const POLL_MS = 1000; const ORDER_NEWEST_FIRST = true; // najnowsze na GÓRZE (backend już odwraca) const TOL = 40; // tolerancja (px) od krawędzi, aby uznać, że jesteśmy „przy krawędzi” // state let selectedLevel = levelSelect ? levelSelect.value : "INFO"; let pollTimer = null; let live = true; // autoscroll: ON domyślnie let autoScroll = true; // gdy użytkownik ręcznie wyłączy — blokujemy automatyczne włączanie let autoLockByUser = false; // gdy autoscroll wyłączył się przez przewinięcie — możemy go automatycznie przywracać let autoDisabledBy = null; // null | 'scroll' | 'user' let wrapped = false; let lastPayload = ""; let debounceTimer; function setUrlParam(key, val) { const url = new URL(window.location.href); if (val === "" || val == null) url.searchParams.delete(key); else url.searchParams.set(key, val); window.history.replaceState(null, "", url.toString()); } function highlight() { if (window.hljs && typeof window.hljs.highlightElement === "function") { window.hljs.highlightElement(logContainer); } } function setEmptyState(on) { $("#logEmpty")?.classList.toggle("d-none", !on); logContainer.classList.toggle("d-none", on); } function refreshBtnLive() { if (!btnLive) return; btnLive.classList.toggle("btn-outline-light", live); btnLive.classList.toggle("btn-danger", !live); btnLive.textContent = live ? (btnLive.dataset.onText || "Live ON") : (btnLive.dataset.offText || "Live OFF"); } function refreshBtnAuto() { if (!btnAuto) return; btnAuto.classList.toggle("btn-outline-secondary", !autoScroll); btnAuto.classList.toggle("btn-light", autoScroll); btnAuto.textContent = autoScroll ? "Auto-scroll" : "Auto-scroll (OFF)"; } function scrollToEdgeIfNeeded() { if (!autoScroll) return; if (ORDER_NEWEST_FIRST) { // najnowsze na górze -> krawędź to top logScroll.scrollTop = 0; } else { // klasyczny tail -> krawędź to dół logScroll.scrollTop = logScroll.scrollHeight; } } function applyText(content) { lastPayload = content || ""; setEmptyState(!lastPayload.length); logContainer.textContent = lastPayload; highlight(); // tylko gdy autoscroll jest aktywny, przeskakujemy do krawędzi scrollToEdgeIfNeeded(); } async function copyToClipboard(text) { try { if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") { await navigator.clipboard.writeText(text); return true; } } catch (_) { /* fallback below */ } // Fallback dla HTTP/nieobsługiwanych środowisk const ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; ta.style.top = "-1000px"; ta.style.left = "-1000px"; ta.setAttribute("readonly", ""); document.body.appendChild(ta); ta.select(); ta.setSelectionRange(0, ta.value.length); let ok = false; try { ok = document.execCommand("copy"); } catch (_) { ok = false; } document.body.removeChild(ta); return ok; } function loadLogs({ silent = false } = {}) { const query = queryInput ? queryInput.value : ""; const url = `/logs-data?level=${encodeURIComponent(selectedLevel)}&query=${encodeURIComponent(query)}`; return fetch(url) .then(res => res.ok ? res.json() : Promise.reject(res.status)) .then(data => { // backend już zwraca newest-first; nie odwracaj kolejności const lines = Array.isArray(data.logs) ? data.logs.slice() : []; const text = lines.join("\n"); applyText(text); }) .catch(() => { if (!silent) window.showToast?.({ text: "Błąd ładowania logów", variant: "danger" }); }); } function startPolling() { stopPolling(); if (!live) return; pollTimer = setInterval(() => loadLogs({ silent: true }), POLL_MS); } function stopPolling() { if (pollTimer) clearInterval(pollTimer); pollTimer = null; } // wyszukiwarka — enter lub debounce 400ms searchForm?.addEventListener("submit", (e) => { e.preventDefault(); setUrlParam("query", queryInput.value || ""); loadLogs(); }); queryInput?.addEventListener("input", () => { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { setUrlParam("query", queryInput.value || ""); loadLogs({ silent: true }); }, 400); }); // --- TOOLBAR --- btnLive?.addEventListener("click", () => { live = !live; refreshBtnLive(); window.showToast?.({ text: live ? "Live tail: włączony" : "Live tail: wyłączony", variant: live ? "dark" : "warning" }); live ? startPolling() : stopPolling(); }); refreshBtnLive(); btnAuto?.addEventListener("click", () => { // ręczne przełączenie blokuje/odblokowuje auto włączanie autoScroll = !autoScroll; autoLockByUser = !autoScroll; // jeśli wyłączasz przyciskiem — zablokuj auto re-enable autoDisabledBy = autoScroll ? null : "user"; refreshBtnAuto(); window.showToast?.({ text: autoScroll ? "Auto-scroll: ON" : "Auto-scroll: OFF (ręcznie)", variant: "info" }); if (autoScroll) { // po włączeniu — przeskocz do krawędzi scrollToEdgeIfNeeded(); } }); refreshBtnAuto(); btnWrap?.addEventListener("click", () => { wrapped = !wrapped; btnWrap.classList.toggle("btn-outline-secondary", !wrapped); btnWrap.classList.toggle("btn-light", wrapped); logContainer.style.whiteSpace = wrapped ? "pre-wrap" : "pre"; }); fontSelect?.addEventListener("change", () => { logContainer.style.fontSize = fontSelect.value; }); btnCopy?.addEventListener("click", async () => { const ok = await copyToClipboard(lastPayload || ""); window.showToast?.({ text: ok ? "Skopiowano log do schowka." : "Nie udało się skopiować.", variant: ok ? "success" : "danger" }); }); btnDownload?.addEventListener("click", () => { const blob = new Blob([lastPayload || ""], { type: "text/plain;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `app.log.view.txt`; a.click(); URL.revokeObjectURL(url); }); // --- SMART AUTOSCROLL GUARD --- // 1) auto-wyłączenie gdy oddalimy się od krawędzi // 2) auto-włączenie gdy wrócimy do krawędzi, ale tylko jeśli nie zablokowano przyciskiem let scrollTicking = false; // micro-raf guard logScroll.addEventListener("scroll", () => { if (scrollTicking) return; scrollTicking = true; requestAnimationFrame(() => { scrollTicking = false; if (ORDER_NEWEST_FIRST) { // krawędź to góra (top) const atTop = logScroll.scrollTop <= TOL; const farFromTop = logScroll.scrollTop > TOL; // 1) gdy uciekasz od góry — wyłącz auto, jeśli było włączone if (farFromTop && autoScroll) { autoScroll = false; autoDisabledBy = "scroll"; refreshBtnAuto(); window.showToast?.({ text: "Auto-scroll: OFF (przewijanie)", variant: "info" }); } // 2) gdy wracasz na samą górę — automatycznie włącz, jeśli nie ma blokady usera if (atTop && !autoScroll && !autoLockByUser && autoDisabledBy === "scroll") { autoScroll = true; autoDisabledBy = null; refreshBtnAuto(); // upewnij się, że trzymamy top logScroll.scrollTop = 0; window.showToast?.({ text: "Auto-scroll: ON (powrót na górę)", variant: "info" }); } } else { // klasyczny tail — krawędź to dół const distanceFromBottom = logScroll.scrollHeight - logScroll.clientHeight - logScroll.scrollTop; const atBottom = distanceFromBottom <= TOL; const farFromBottom = distanceFromBottom > TOL; if (farFromBottom && autoScroll) { autoScroll = false; autoDisabledBy = "scroll"; refreshBtnAuto(); window.showToast?.({ text: "Auto-scroll: OFF (przewijanie)", variant: "info" }); } if (atBottom && !autoScroll && !autoLockByUser && autoDisabledBy === "scroll") { autoScroll = true; autoDisabledBy = null; refreshBtnAuto(); logScroll.scrollTop = logScroll.scrollHeight; window.showToast?.({ text: "Auto-scroll: ON (powrót na dół)", variant: "info" }); } } }); }, { passive: true }); // Start loadLogs().then(() => startPolling()); // sprzątanie window.addEventListener("beforeunload", () => { stopPolling(); }); })();