release
This commit is contained in:
286
static/js/logs.js
Normal file
286
static/js/logs.js
Normal file
@@ -0,0 +1,286 @@
|
||||
(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();
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user