routerconsole/jsp/js/stats.js

/**
 * @module stats
 * @description Handles periodic refresh and tab switching for the /stats page.
 * Fetches updated stat data via SharedWorker and replaces stat list elements.
 * @author dr|z3d
 * @license AGPL3 or later
 */

(function() {
  const infohelp = document.querySelector("#gatherstats");
  const nav = document.querySelector(".confignav");
  const tabs = document.querySelectorAll(".togglestat");
  const REFRESH_INTERVAL = 60*1000;

  const fetchWorker = new SharedWorker("/js/fetchWorker.js");
  fetchWorker.port.start();

  /**
   * Reports the page's visibility to the SharedWorker so the fetch worker can
   * suspend requests for tabs that aren't visible.
   * @function reportVisibility
   * @returns {void}
   */
  function reportVisibility() {
    fetchWorker.port.postMessage({ visibility: !document.hidden });
  }
  document.addEventListener("visibilitychange", () => {
    reportVisibility();
    if (!document.hidden) { updateStats(); }
  });
  reportVisibility();
  fetchWorker.port.onmessage = function(e) {
    const { responseText } = e.data;
    if (!responseText) return;

    const arrivedHidden = document.hidden;
    const parser = new DOMParser();
    const doc = parser.parseFromString(responseText, "text/html");

    requestAnimationFrame(() => {
      // Drop responses that arrived while the tab was hidden; rAF is suspended
      // when hidden, so they'd otherwise render stale data on regain.
      if (arrivedHidden || document.hidden) { return; }
      const info = document.getElementById("gatherstats");
      if (info) {
        const infoResponse = doc.getElementById("gatherstats");
        if (infoResponse && !Object.is(info.innerHTML, infoResponse.innerHTML)) {
          info.innerHTML = infoResponse.innerHTML;
        }
      }
      const statlistElements = document.querySelectorAll(".statlist");
      const statlistResponseElements = doc.querySelectorAll(".statlist");
      statlistResponseElements.forEach((statlistResponse, index) => {
        if (index < statlistElements.length) {
          const statlist = statlistElements[index];
          const statlistParent = statlist.parentNode;
          if (statlist.innerHTML !== statlistResponse.innerHTML) {
            const newStatlist = document.importNode(statlistResponse, true);
            statlistParent.replaceChild(newStatlist, statlist);
          }
        }
      });
      progressx.hide();
    });
  };

  function getStatsUrl() {
    const statFilter = new URLSearchParams(window.location.search).get("stat");
    return statFilter ? "/stats?stat=" + encodeURIComponent(statFilter) : "/stats";
  }

  function updateStats() {
    progressx.show(theme);
    progressx.progress(0.5);
    fetchWorker.port.postMessage({url: getStatsUrl(), force: true});
  }

  function initRefresh() {setInterval(updateStats, REFRESH_INTERVAL);}

  function initTabs() {
    for (let i = 0; i < tabs.length; i++) {tabs[i].classList.remove("tab2");}
  }

  nav.addEventListener("click", function(element) {
    if (element.target.classList.contains("togglestat")) {
      if (infohelp) {infohelp.remove();}
      updateStats();
      initTabs();
      element.target.classList.add("tab2");
      progressx.hide();
    }
  });

  document.addEventListener("DOMContentLoaded", () => {
    initRefresh();
    initTabs();
    progressx.hide();
  });
})();