(() => { const LEVELS = ["Verbose", "Debug", "Information", "Warning", "Error", "Fatal"]; const HIDDEN_PROP_KEYS = new Set(["ActionId", "RequestId", "ConnectionId"]); const state = { page: 1, pageSize: 25, total: 0, levels: new Set(), search: "", from: "", to: "", source: "mongo", autoRefresh: false, }; const $ = (id) => document.getElementById(id); const levelChips = $("levelChips"); const list = $("list"); const statusText = $("statusText"); const pageInfo = $("pageInfo"); const warningBox = $("warning"); const modalOverlay = $("modalOverlay"); const modalJson = $("modalJson"); let refreshTimer = null; function buildChips() { levelChips.innerHTML = ""; for (const lvl of LEVELS) { const chip = document.createElement("button"); chip.className = "chip"; chip.type = "button"; chip.textContent = lvl; chip.dataset.level = lvl; chip.addEventListener("click", () => { if (state.levels.has(lvl)) state.levels.delete(lvl); else state.levels.add(lvl); chip.classList.toggle("active"); applyFilters(); }); levelChips.appendChild(chip); } } function escapeHtml(str) { return String(str) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">"); } function formatTime(iso) { const d = new Date(iso); return d.toLocaleString(undefined, { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", }); } function syntaxHighlightJson(json) { const escaped = escapeHtml(json); return escaped.replace( /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false)\b|\bnull\b|-?\d+(\.\d+)?([eE][+-]?\d+)?)/g, (match) => { let cls = "jv-number"; if (/^"/.test(match)) { cls = /:$/.test(match) ? "jv-key" : "jv-string"; } else if (/true|false/.test(match)) { cls = "jv-bool"; } else if (/null/.test(match)) { cls = "jv-null"; } return `${match}`; } ); } function openModal(entry) { $("modalTitle").textContent = `${entry.level} · ${formatTime(entry.timestamp)}`; modalJson.innerHTML = syntaxHighlightJson(entry.rawJson || "{}"); modalOverlay.classList.remove("hidden"); } function closeModal() { modalOverlay.classList.add("hidden"); } function renderRow(entry) { const row = document.createElement("div"); row.className = "log-row"; row.setAttribute("role", "listitem"); row.dataset.level = entry.level; const time = document.createElement("div"); time.className = "log-time"; time.textContent = formatTime(entry.timestamp); const level = document.createElement("div"); level.className = "log-level"; level.dataset.level = entry.level; level.textContent = entry.level; const body = document.createElement("div"); const msg = document.createElement("div"); msg.className = "log-message"; msg.textContent = entry.message || entry.messageTemplate || "(geen bericht)"; body.appendChild(msg); if (entry.exception) { const ex = document.createElement("div"); ex.className = "log-exception"; ex.textContent = entry.exception.split("\n").slice(0, 2).join("\n"); body.appendChild(ex); } const propKeys = Object.keys(entry.properties || {}); if (propKeys.length) { const visibleKeys = propKeys.filter((k) => !HIDDEN_PROP_KEYS.has(k)).slice(0, 6); const hiddenKeys = propKeys.filter((k) => HIDDEN_PROP_KEYS.has(k)); const propsWrap = document.createElement("div"); propsWrap.className = "log-props"; const makeTag = (key) => { const tag = document.createElement("span"); tag.className = "prop-tag"; tag.innerHTML = `${escapeHtml(key)}=${escapeHtml(entry.properties[key])}`; return tag; }; for (const key of visibleKeys) propsWrap.appendChild(makeTag(key)); if (hiddenKeys.length) { const hiddenWrap = document.createElement("div"); hiddenWrap.className = "log-props hidden-props hidden"; for (const key of hiddenKeys) hiddenWrap.appendChild(makeTag(key)); const toggle = document.createElement("button"); toggle.type = "button"; toggle.className = "prop-toggle"; toggle.textContent = `+${hiddenKeys.length} technisch`; toggle.addEventListener("click", (e) => { e.stopPropagation(); const isHidden = hiddenWrap.classList.toggle("hidden"); toggle.textContent = isHidden ? `+${hiddenKeys.length} technisch` : "− verbergen"; }); propsWrap.appendChild(toggle); body.appendChild(propsWrap); body.appendChild(hiddenWrap); } else { body.appendChild(propsWrap); } } row.appendChild(time); row.appendChild(level); row.appendChild(body); row.addEventListener("click", () => openModal(entry)); return row; } function buildQuery() { const params = new URLSearchParams(); params.set("page", state.page); params.set("pageSize", state.pageSize); params.set("source", state.source); if (state.levels.size) params.set("level", [...state.levels].join(",")); if (state.search) params.set("search", state.search); if (state.from) params.set("from", new Date(state.from).toISOString()); if (state.to) params.set("to", new Date(state.to).toISOString()); return params.toString(); } async function load() { statusText.textContent = "Laden..."; try { const res = await fetch(`/api/logs?${buildQuery()}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); state.total = data.total; if (data.warning) { warningBox.textContent = data.warning; warningBox.classList.remove("hidden"); } else { warningBox.classList.add("hidden"); } list.innerHTML = ""; if (!data.items.length) { const empty = document.createElement("div"); empty.className = "empty"; empty.textContent = "Geen logregels gevonden voor deze filters."; list.appendChild(empty); } else { for (const entry of data.items) list.appendChild(renderRow(entry)); } const totalPages = Math.max(1, Math.ceil(data.total / state.pageSize)); pageInfo.textContent = `Pagina ${data.page} / ${totalPages} · ${data.total} regels (${data.source})`; statusText.textContent = `${data.total} logregels · bron: ${data.source}`; $("prevBtn").disabled = data.page <= 1; $("nextBtn").disabled = data.page >= totalPages; } catch (err) { statusText.textContent = `Fout bij laden: ${err.message}`; list.innerHTML = `