Files
autoban/static/js/logs.js
2026-01-01 02:13:34 +01:00

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();
});
})();