/* Pi-hole: A black hole for Internet advertisements * (c) 2017 Pi-hole, LLC (https://pi-hole.net) * Network-wide ad blocking via your own hardware. * * This file is copyright under the latest version of the EUPL. * Please see LICENSE file for your rights under this license. */ /* global moment: false, apiFailure: false, utils: false, REFRESH_INTERVAL: false */ "use strict"; let nextID = 0; let lastPID = -1; // Maximum number of lines to display const maxlines = 5000; // Fade in new lines const fadeIn = true; // Mark new lines with a red line above them const markUpdates = true; // Format a line of the dnsmasq log function formatDnsmasq(line) { // Remove dnsmasq + PID let txt = line.replaceAll(/ dnsmasq\[\d*]/g, ""); if (line.includes("denied") || line.includes("gravity blocked")) { // Red bold text for blocked domains txt = `${txt}`; } else if (line.includes("query[A") || line.includes("query[DHCP")) { // Bold text for initial query lines txt = `${txt}`; } else { // Grey text for all other lines txt = `${txt}`; } return txt; } function formatFTL(line, prio) { // Colorize priority let prioClass = ""; switch (prio) { case "INFO": { prioClass = "text-success"; break; } case "WARNING": { prioClass = "text-warning"; break; } case "ERR": case "ERROR": case "EMERG": case "ALERT": case "CRIT": { prioClass = "text-danger"; break; } default: prioClass = prio.startsWith("DEBUG") ? "text-info" : "text-muted"; } // Return formatted line return `${utils.escapeHtml(prio)} ${line}`; } let gAutoScrolling; // Function that asks the API for new data function getData() { // Only update when spinner is spinning if (!$("#feed-icon").hasClass("fa-play")) { utils.setTimer(getData, REFRESH_INTERVAL.logs); return; } const GETDict = utils.parseQueryString(); if (!("file" in GETDict)) { globalThis.location.href += "?file=dnsmasq"; return; } $.ajax({ url: document.body.dataset.apiurl + "/logs/" + GETDict.file + "?nextID=" + nextID, timeout: 5000, method: "GET", }) .done(data => { // Check if we have a new PID -> FTL was restarted if (lastPID !== data.pid) { if (lastPID !== -1) { $("#output").append("
*** FTL restarted ***
"); } // Remember PID lastPID = data.pid; // Reset nextID nextID = 0; getData(); return; } // Set placeholder text if log file is empty and we have no new lines if (data.log.length === 0) { if (nextID === 0) { $("#output").html("
*** Log file is empty ***
"); } utils.setTimer(getData, REFRESH_INTERVAL.logs); return; } // We have new lines if (markUpdates && nextID > 0) { // Add red fading out background to new lines $("#output").append('
').children(":last").fadeOut(2000); } for (const line of data.log) { // Escape HTML line.message = utils.escapeHtml(line.message); // Format line if applicable if (GETDict.file === "dnsmasq") line.message = formatDnsmasq(line.message); else if (GETDict.file === "ftl") line.message = formatFTL(line.message, line.prio); // Add new line to output $("#output").append( '
' + moment(1000 * line.timestamp).format("YYYY-MM-DD HH:mm:ss.SSS") + " " + line.message + "
" ); if (fadeIn) { //$(".left-line:last").fadeOut(2000); $("#output").children(":last").hide().fadeIn("fast"); } } // Limit output to lines const lines = $("#output").val().split("\n"); if (lines.length > maxlines) { lines.splice(0, lines.length - maxlines); $("#output").val(lines.join("\n")); } // Scroll to bottom of output if we are already at the bottom if (gAutoScrolling) { // Auto-scrolling is enabled $("#output").scrollTop($("#output")[0].scrollHeight); } // Update nextID nextID = data.nextID; // Set filename $("#filename").text(data.file); utils.setTimer(getData, REFRESH_INTERVAL.logs); }) .fail(data => { apiFailure(data); utils.setTimer(getData, 5 * REFRESH_INTERVAL.logs); }); } gAutoScrolling = true; $("#output").on("scroll", () => { // Check if we are at the bottom of the output // // - $("#output")[0].scrollHeight: This gets the entire height of the content // of the "output" element, including the part that is not visible due to // scrolling. // - $("#output").innerHeight(): This gets the inner height of the "output" // element, which is the visible part of the content. // - $("#output").scrollTop(): This gets the number of pixels that the content // of the "output" element is scrolled vertically from the top. // // By subtracting the inner height and the scroll top from the scroll height, // you get the distance from the bottom of the scrollable area. const bottom = $("#output")[0].scrollHeight - $("#output").innerHeight() - $("#output").scrollTop(); // Add a tolerance of four line heights const tolerance = 4 * Number.parseFloat($("#output").css("line-height")); if (bottom <= tolerance) { // Auto-scrolling is enabled gAutoScrolling = true; $("#autoscrolling").addClass("fa-check"); $("#autoscrolling").removeClass("fa-xmark"); } else { // Auto-scrolling is disabled gAutoScrolling = false; $("#autoscrolling").addClass("fa-xmark"); $("#autoscrolling").removeClass("fa-check"); } }); $(() => { getData(); // Clicking on the element with class "fa-spinner" will toggle the play/pause state $("#live-feed").on("click", function () { if ($("#feed-icon").hasClass("fa-play")) { // Toggle button color $("#feed-icon").addClass("fa-pause"); $("#feed-icon").removeClass("fa-fade"); $("#feed-icon").removeClass("fa-play"); $(this).addClass("btn-danger"); $(this).removeClass("btn-success"); $("#title").text("Paused"); } else { // Toggle button color $("#feed-icon").addClass("fa-play"); $("#feed-icon").addClass("fa-fade"); $("#feed-icon").removeClass("fa-pause"); $(this).addClass("btn-success"); $(this).removeClass("btn-danger"); $("#title").text("Live"); } }); });