287 lines
10 KiB
JavaScript
287 lines
10 KiB
JavaScript
(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();
|
|
});
|
|
})();
|