]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Rework] WebUI: remove jQuery from main module
authorAlexander Moisseev <moiseev@mezonplus.ru>
Sun, 5 Jul 2026 14:31:39 +0000 (17:31 +0300)
committerAlexander Moisseev <moiseev@mezonplus.ru>
Sun, 5 Jul 2026 17:25:50 +0000 (20:25 +0300)
Migrate rspamd.js off jQuery. The /stat connect probe moves to a raw
XHR (avoiding common.query's error logging on the normal "not logged
in" path); $.ajaxSetup and $(document).ajaxStart/Complete are dropped
since no $.ajax calls remain. tabClick disables/enables the nav bar
controls via attribute+class helpers (li/a need both for Bootstrap);
:radio + .val([v]) and .data("value") become checkRadio / activeMenuValue
helpers; delegated handlers move to common.delegate; BS5 events move to
addEventListener (with {once:true} replacing self-removing .on/.off);
the popover content uses cloneNode and the outside-click dismiss uses
.contains(). Form/input re-binds become onsubmit/oninput properties.

interface/js/app/rspamd.js

index 7236ef23a62affe87c453c5f4f99241132358a2c..955655bd2732250ee1b498d00bee02d7b667f7ee 100644 (file)
@@ -5,9 +5,9 @@
 
 /* global require, Visibility */
 
-define(["jquery", "app/common", "bootstrap", "visibility",
+define(["app/common", "bootstrap", "visibility",
     "fontawesome"],
-($, common, bootstrap) => {
+(common, bootstrap) => {
     "use strict";
     const ui = {};
 
@@ -22,21 +22,17 @@ define(["jquery", "app/common", "bootstrap", "visibility",
     function ajaxSetup(ajax_timeout, setFieldValue, saveToLocalStorage) {
         const timeout = (ajax_timeout && ajax_timeout >= 0) ? ajax_timeout : defaultAjaxTimeout;
         if (saveToLocalStorage) localStorage.setItem("ajax_timeout", timeout);
-        if (setFieldValue) $(ajaxTimeoutBox).val(timeout);
+        if (setFieldValue) document.querySelector(ajaxTimeoutBox).value = timeout;
 
-        // Apply to common.query (XHR) and to the remaining direct $.ajax calls.
         common.setAjaxTimeout(timeout);
-        $.ajaxSetup({
-            timeout: timeout,
-            jsonp: false
-        });
     }
 
     function cleanCredentials() {
         sessionStorage.clear();
-        $("#statWidgets").empty();
-        $("#listMaps").children("tbody").empty();
-        $("#modalBody").empty();
+        document.getElementById("statWidgets").replaceChildren();
+        const listMapsTbody = document.querySelector("#listMaps tbody");
+        if (listMapsTbody) listMapsTbody.replaceChildren();
+        document.getElementById("modalBody").replaceChildren();
     }
 
     function stopTimers() {
@@ -98,61 +94,78 @@ define(["jquery", "app/common", "bootstrap", "visibility",
         ui.connect();
     }
 
+    // Bootstrap disables non-button nav items (li/a) via the `disabled` attribute
+    // + `.disabled` class together; buttons additionally honour the attribute.
+    function disableNav(el) {
+        el.setAttribute("disabled", "disabled");
+        el.classList.add("disabled");
+    }
+    function enableNav(el) {
+        el.removeAttribute("disabled");
+        el.classList.remove("disabled");
+    }
+    // Active dropdown item's numeric data-value (jQuery .data() coerced to number).
+    function activeMenuValue(menuClass) {
+        const el = document.querySelector(".dropdown-menu a.active." + menuClass);
+        return el ? parseInt(el.dataset.value, 10) : null;
+    }
+
     function tabClick(id) {
         let tab_id = id;
-        if ($(id).attr("disabled")) return;
-        let navBarControls = $("#selSrv, #navBar li, #navBar a, #navBar button");
-        if (id !== "#autoRefresh") navBarControls.attr("disabled", true).addClass("disabled", true);
+        if (document.querySelector(id)?.hasAttribute("disabled")) return;
+        let navBarControls = Array.from(document.querySelectorAll("#selSrv, #navBar li, #navBar a, #navBar button"));
+        if (id !== "#autoRefresh") navBarControls.forEach(disableNav);
 
         stopTimers();
 
         if (id === "#refresh" || id === "#autoRefresh") {
-            tab_id = "#" + $(".nav-link.active").attr("id");
+            tab_id = "#" + document.querySelector(".nav-link.active").id;
         }
 
-        $("#autoRefresh").addClass("invisible");
-        $("#refresh").addClass("radius-right");
+        document.getElementById("autoRefresh").classList.add("invisible");
+        document.getElementById("refresh").classList.add("radius-right");
 
         function setAutoRefresh(refreshInterval, timer, callback) {
+            const refreshBtn = document.getElementById("refresh");
             function countdown(interval) {
                 Visibility.stop(timer_id.countdown);
                 if (!interval) {
-                    $("#countdown").text("--:--");
+                    document.getElementById("countdown").textContent = "--:--";
                     return;
                 }
 
                 let timeLeft = interval;
-                $("#countdown").text("00:00");
+                document.getElementById("countdown").textContent = "00:00";
                 timer_id.countdown = Visibility.every(1000, 1000, () => {
                     timeLeft -= 1000;
-                    $("#countdown").text(new Date(timeLeft).toISOString().substr(14, 5));
+                    document.getElementById("countdown").textContent = new Date(timeLeft).toISOString().substr(14, 5);
                     if (timeLeft <= 0) Visibility.stop(timer_id.countdown);
                 });
             }
 
-            $("#refresh").removeClass("radius-right");
-            $("#autoRefresh").removeClass("invisible");
+            refreshBtn.classList.remove("radius-right");
+            document.getElementById("autoRefresh").classList.remove("invisible");
 
             countdown(refreshInterval);
             if (!refreshInterval) return;
             timer_id[timer] = Visibility.every(refreshInterval, () => {
                 countdown(refreshInterval);
-                if ($("#refresh").attr("disabled")) return;
-                $("#refresh").attr("disabled", true).addClass("disabled", true);
+                if (refreshBtn.hasAttribute("disabled")) return;
+                disableNav(refreshBtn);
                 callback();
             });
         }
 
         if (["#scan_nav", "#selectors_nav", "#disconnect"].indexOf(tab_id) !== -1) {
-            $("#refresh").addClass("invisible");
+            document.getElementById("refresh").classList.add("invisible");
         } else {
-            $("#refresh").removeClass("invisible");
+            document.getElementById("refresh").classList.remove("invisible");
         }
 
         switch (tab_id) {
             case "#status_nav":
                 require(["app/stats"], (module) => {
-                    const refreshInterval = $(".dropdown-menu a.active.preset").data("value");
+                    const refreshInterval = activeMenuValue("preset");
                     setAutoRefresh(refreshInterval, "status",
                         () => module.statWidgets(graphs, checked_server));
                     if (id !== "#autoRefresh") module.statWidgets(graphs, checked_server);
@@ -170,9 +183,9 @@ define(["jquery", "app/common", "bootstrap", "visibility",
                         week: 300000
                     };
                     let refreshInterval = step[selData] || 3600000;
-                    $("#dynamic-item").text((refreshInterval / 60000) + " min");
+                    document.getElementById("dynamic-item").textContent = (refreshInterval / 60000) + " min";
 
-                    if (!$(".dropdown-menu a.active.dynamic").data("value")) {
+                    if (!activeMenuValue("dynamic")) {
                         refreshInterval = null;
                     }
                     setAutoRefresh(refreshInterval, "throughput",
@@ -208,7 +221,7 @@ define(["jquery", "app/common", "bootstrap", "visibility",
                         module.getHistory();
                         module.getErrors();
                     }
-                    const refreshInterval = $(".dropdown-menu a.active.history").data("value");
+                    const refreshInterval = activeMenuValue("history");
                     setAutoRefresh(refreshInterval, "history",
                         () => getHistoryAndErrors());
                     if (id !== "#autoRefresh") getHistoryAndErrors();
@@ -228,9 +241,9 @@ define(["jquery", "app/common", "bootstrap", "visibility",
 
         setTimeout(() => {
             // Do not enable Refresh button until AJAX requests to all neighbours are finished
-            if (tab_id === "#history_nav") navBarControls = $(navBarControls).not("#refresh");
+            if (tab_id === "#history_nav") navBarControls = navBarControls.filter((el) => el.id !== "refresh");
 
-            navBarControls.removeAttr("disabled").removeClass("disabled");
+            navBarControls.forEach(enableNav);
         }, (id === "#autoRefresh") ? 0 : 1000);
     }
 
@@ -245,15 +258,16 @@ define(["jquery", "app/common", "bootstrap", "visibility",
 
         common.query("auth", {
             success: function (neighbours_status) {
-                $("#selSrv").empty();
-                $("#selSrv").append($('<option value="All SERVERS">All SERVERS</option>'));
+                const selSrv = document.getElementById("selSrv");
+                selSrv.replaceChildren();
+                selSrv.append(common.el("option", {value: "All SERVERS", text: "All SERVERS"}));
                 neighbours_status.forEach((e) => {
-                    $("#selSrv").append($('<option value="' + e.name + '">' + e.name + "</option>"));
-                    if (checked_server === e.name) {
-                        $('#selSrv [value="' + e.name + '"]').prop("selected", true);
-                    } else if (!e.status) {
-                        $('#selSrv [value="' + e.name + '"]').prop("disabled", true);
-                    }
+                    const selected = checked_server === e.name;
+                    const disabled = !selected && !e.status;
+                    selSrv.insertAdjacentHTML("beforeend",
+                        '<option value="' + common.escapeHTML(e.name) + '"' +
+                        (selected ? " selected" : "") + (disabled ? " disabled" : "") + ">" +
+                        common.escapeHTML(e.name) + "</option>");
                 });
             },
             complete: function () {
@@ -261,16 +275,15 @@ define(["jquery", "app/common", "bootstrap", "visibility",
 
                 if (require.defined("app/upload")) require(["app/upload"], (module) => module.getClassifiers());
 
+                document.querySelectorAll(".ro-disable").forEach((el) => { el.disabled = common.read_only; });
                 if (common.read_only) {
-                    $(".ro-disable").attr("disabled", true);
                     common.hide(".ro-hide");
                 } else {
-                    $(".ro-disable").removeAttr("disabled", true);
                     common.show(".ro-hide");
                 }
 
-                $("#preloader").addClass("d-none");
-                $("#navBar, #mainUI").removeClass("d-none");
+                document.getElementById("preloader").classList.add("d-none");
+                document.querySelectorAll("#navBar, #mainUI").forEach((el) => el.classList.remove("d-none"));
 
                 stickyTabsHandle = initStickyTabs(".nav-tabs-sticky", "#status_nav");
             },
@@ -280,6 +293,82 @@ define(["jquery", "app/common", "bootstrap", "visibility",
     }
 
 
+    // Parse JSON, returning {} for an empty or non-JSON body (the /stat probe
+    // must not throw and strand the connect flow).
+    function parseJsonSafe(text) {
+        try {
+            return text ? JSON.parse(text) : {};
+        } catch (err) {
+            return {};
+        }
+    }
+
+    // Show the connect (login) dialog and wire its form submission.
+    function showConnectDialog() {
+        const connectDialog = document.getElementById("connectDialog");
+        const connectForm = document.getElementById("connectForm");
+        const connectPassword = document.getElementById("connectPassword");
+
+        function clearFeedback() {
+            connectPassword.oninput = null;
+            connectPassword.classList.remove("is-invalid");
+            common.hide("#authInvalidCharFeedback,#authUnauthorizedFeedback");
+        }
+
+        connectDialog.addEventListener("show.bs.modal", () => clearFeedback(), {once: true});
+        connectDialog.addEventListener("shown.bs.modal", () => connectPassword.focus(), {once: true});
+        bootstrap.Modal.getOrCreateInstance(connectDialog).show();
+
+        connectForm.onsubmit = (e) => {
+            e.preventDefault();
+            const password = connectPassword.value;
+
+            function invalidFeedback(tooltip) {
+                connectPassword.classList.add("is-invalid");
+                connectPassword.oninput = () => clearFeedback();
+                common.show(tooltip);
+            }
+
+            if (!(/^[ -~]*$/).test(password)) {
+                invalidFeedback("#authInvalidCharFeedback");
+                connectPassword.focus();
+                return;
+            }
+
+            common.query("auth", {
+                headers: {
+                    Password: password
+                },
+                success: function (json) {
+                    const [{data}] = json;
+                    connectPassword.value = "";
+                    if (data.auth === "ok") {
+                        sessionStorage.setItem("read_only", data.read_only);
+                        saveCredentials(password);
+                        connectForm.onsubmit = null;
+                        bootstrap.Modal.getOrCreateInstance(connectDialog).hide();
+                        displayUI();
+                    }
+                },
+                error: function (_result, xhr, textStatus) {
+                    if (xhr.statusText === "Unauthorized") {
+                        invalidFeedback("#authUnauthorizedFeedback");
+                    } else {
+                        common.alertMessage("alert-modal alert-danger",
+                            textStatus === "timeout" ? "Request timeout" : xhr.statusText);
+                    }
+                    connectPassword.value = "";
+                    connectPassword.focus();
+                },
+                params: {
+                    global: false,
+                },
+                server: "local"
+            });
+        };
+    }
+
+
     // Public functions
 
     ui.connect = function () {
@@ -289,87 +378,37 @@ define(["jquery", "app/common", "bootstrap", "visibility",
         ajaxSetup(timeout);
 
         // Query "/stat" to check if user is already logged in or client ip matches "secure_ip"
-        $.ajax({
-            type: "GET",
-            url: "stat",
-            success: function (data) {
+        const xhr = new XMLHttpRequest();
+        xhr.open("GET", "stat", true);
+        const ajaxTimeout = common.getAjaxTimeout();
+        if (ajaxTimeout > 0) xhr.timeout = ajaxTimeout;
+        xhr.onload = () => {
+            if (xhr.status >= 200 && xhr.status < 300) {
+                const data = parseJsonSafe(xhr.responseText);
                 sessionStorage.setItem("read_only", data.read_only);
                 displayUI();
-            },
-            error: function () {
-                function clearFeedback() {
-                    $("#connectPassword").off("input").removeClass("is-invalid");
-                    common.hide("#authInvalidCharFeedback,#authUnauthorizedFeedback");
-                }
-
-                $("#connectDialog")
-                    .on("show.bs.modal", () => {
-                        $("#connectDialog").off("show.bs.modal");
-                        clearFeedback();
-                    })
-                    .on("shown.bs.modal", () => {
-                        $("#connectDialog").off("shown.bs.modal");
-                        $("#connectPassword").focus();
-                    });
-                bootstrap.Modal.getOrCreateInstance(document.getElementById("connectDialog")).show();
-
-                $("#connectForm").off("submit").on("submit", (e) => {
-                    e.preventDefault();
-                    const password = $("#connectPassword").val();
-
-                    function invalidFeedback(tooltip) {
-                        $("#connectPassword")
-                            .addClass("is-invalid")
-                            .off("input").on("input", () => clearFeedback());
-                        common.show(tooltip);
-                    }
-
-                    if (!(/^[\u0020-\u007e]*$/).test(password)) {
-                        invalidFeedback("#authInvalidCharFeedback");
-                        $("#connectPassword").focus();
-                        return;
-                    }
-
-                    common.query("auth", {
-                        headers: {
-                            Password: password
-                        },
-                        success: function (json) {
-                            const [{data}] = json;
-                            $("#connectPassword").val("");
-                            if (data.auth === "ok") {
-                                sessionStorage.setItem("read_only", data.read_only);
-                                saveCredentials(password);
-                                $("#connectForm").off("submit");
-                                bootstrap.Modal.getOrCreateInstance(document.getElementById("connectDialog")).hide();
-                                displayUI();
-                            }
-                        },
-                        error: function (jqXHR, textStatus) {
-                            if (textStatus.statusText === "Unauthorized") {
-                                invalidFeedback("#authUnauthorizedFeedback");
-                            } else {
-                                common.alertMessage("alert-modal alert-danger", textStatus.statusText);
-                            }
-                            $("#connectPassword").val("");
-                            $("#connectPassword").focus();
-                        },
-                        params: {
-                            global: false,
-                        },
-                        server: "local"
-                    });
-                });
+            } else {
+                showConnectDialog();
             }
-        });
+        };
+        xhr.onerror = () => showConnectDialog();
+        xhr.ontimeout = () => showConnectDialog();
+        xhr.send();
     };
 
     function updateThemeIcon(theme) {
-        const icon = $("#theme-icon");
-        icon.removeClass("fa-moon fa-sun fa-display");
+        const icon = document.getElementById("theme-icon");
+        icon.classList.remove("fa-moon", "fa-sun", "fa-display");
 
         const iconMap = {light: "fa-sun", dark: "fa-moon", auto: "fa-display"};
-        icon.addClass(iconMap[theme] || "fa-display");
+        icon.classList.add(iconMap[theme] || "fa-display");
+    }
+
+    // Check the radio within `selector` whose value matches.
+    function checkRadio(selector, value) {
+        document.querySelectorAll(selector).forEach((radio) => {
+            radio.checked = radio.value === value;
+        });
     }
 
     (function initSettings() {
@@ -380,8 +419,10 @@ define(["jquery", "app/common", "bootstrap", "visibility",
         const historyCountSelector = ".popover #settings-popover #settings-history-count";
 
         function validateLocale(saveToLocalStorage) {
+            const localeInput = document.querySelector(localeTextbox);
             function toggle_form_group_class(remove, add) {
-                $(localeTextbox).removeClass("is-" + remove).addClass("is-" + add);
+                localeInput.classList.remove("is-" + remove);
+                localeInput.classList.add("is-" + add);
             }
 
             const now = new Date();
@@ -400,140 +441,147 @@ define(["jquery", "app/common", "bootstrap", "visibility",
             } else {
                 if (saveToLocalStorage) localStorage.setItem("custom_locale", null);
                 common.locale = null;
-                $(localeTextbox).removeClass("is-valid is-invalid");
+                localeInput.classList.remove("is-valid", "is-invalid");
             }
 
             // Display date example
-            $(".popover #settings-popover #date-example").text(
+            document.querySelector(".popover #settings-popover #date-example").textContent =
                 (common.locale)
                     ? now.toLocaleString(common.locale)
-                    : now.toLocaleString()
-            );
+                    : now.toLocaleString();
         }
 
-        bootstrap.Popover.getOrCreateInstance(document.getElementById("settings"), {
+        const settingsBtn = document.getElementById("settings");
+        bootstrap.Popover.getOrCreateInstance(settingsBtn, {
             container: "body",
             placement: "bottom",
             html: true,
             sanitize: false,
             content: function () {
                 // Using .clone() has the side-effect of producing elements with duplicate id attributes.
-                return $("#settings-popover").clone();
+                return document.getElementById("settings-popover").cloneNode(true);
             }
         });
         // Restore the tooltip of the element that the popover is attached to.
-        $("#settings").attr("title", function () {
-            return $(this).attr("data-original-title");
-        });
-        $("#settings").on("click", (e) => {
+        const originalTitle = settingsBtn.getAttribute("data-original-title");
+        if (originalTitle !== null) settingsBtn.setAttribute("title", originalTitle);
+        settingsBtn.addEventListener("click", (e) => {
             e.preventDefault();
         });
-        $("#settings").on("inserted.bs.popover", () => {
+        settingsBtn.addEventListener("inserted.bs.popover", () => {
             selected_locale = localStorage.getItem("selected_locale") || "browser";
             custom_locale = localStorage.getItem("custom_locale") || "";
             validateLocale();
 
-            $('.popover #settings-popover input:radio[name="locale"]').val([selected_locale]);
-            $(localeTextbox).val(custom_locale);
+            checkRadio('.popover #settings-popover input[type="radio"][name="locale"]', selected_locale);
+            document.querySelector(localeTextbox).value = custom_locale;
 
             ajaxSetup(localStorage.getItem("ajax_timeout"), true);
 
-            $(historyCountSelector).val(parseInt(localStorage.getItem("historyCount"), 10) || historyCountDef);
+            document.querySelector(historyCountSelector).value =
+                parseInt(localStorage.getItem("historyCount"), 10) || historyCountDef;
 
             // Restore theme selection
             const savedTheme = localStorage.getItem("theme") || "auto";
-            $('.popover #settings-popover input:radio[name="theme"]').val([savedTheme]);
-        });
-        $(document).on("change", '.popover #settings-popover input:radio[name="locale"]', function () {
-            selected_locale = this.value;
-            localStorage.setItem("selected_locale", selected_locale);
-            validateLocale();
+            checkRadio('.popover #settings-popover input[type="radio"][name="theme"]', savedTheme);
         });
-        $(document).on("input", localeTextbox, () => {
-            custom_locale = $(localeTextbox).val();
+        common.delegate(document, "change", '.popover #settings-popover input[type="radio"][name="locale"]',
+            (event, target) => {
+                selected_locale = target.value;
+                localStorage.setItem("selected_locale", selected_locale);
+                validateLocale();
+            });
+        common.delegate(document, "input", localeTextbox, (event, target) => {
+            custom_locale = target.value;
             validateLocale(true);
         });
-        $(document).on("change", '.popover #settings-popover input:radio[name="theme"]', function () {
-            const theme = this.value;
-            if (window.rspamd && window.rspamd.theme) {
-                window.rspamd.theme.applyPreference(theme);
-            }
-            updateThemeIcon(theme || "auto");
-        });
+        common.delegate(document, "change", '.popover #settings-popover input[type="radio"][name="theme"]',
+            (event, target) => {
+                const theme = target.value;
+                if (window.rspamd && window.rspamd.theme) {
+                    window.rspamd.theme.applyPreference(theme);
+                }
+                updateThemeIcon(theme || "auto");
+            });
         updateThemeIcon(localStorage.getItem("theme") || "auto");
-        $(document).on("input", ajaxTimeoutBox, () => {
-            ajaxSetup($(ajaxTimeoutBox).val(), false, true);
+        common.delegate(document, "input", ajaxTimeoutBox, (event, target) => {
+            ajaxSetup(target.value, false, true);
         });
-        $(document).on("click", ".popover #settings-popover #ajax-timeout-restore", () => {
+        common.delegate(document, "click", ".popover #settings-popover #ajax-timeout-restore", () => {
             ajaxSetup(null, true, true);
         });
 
-        $(document).on("input", historyCountSelector, (e) => {
-            const v = parseInt($(e.currentTarget).val(), 10);
+        common.delegate(document, "input", historyCountSelector, (event, target) => {
+            const v = parseInt(target.value, 10);
             if (v > 0) {
                 localStorage.setItem("historyCount", v);
-                $(e.currentTarget).removeClass("is-invalid");
-                $("#history-count").val(v).trigger("change");
+                target.classList.remove("is-invalid");
+                const historyCount = document.getElementById("history-count");
+                historyCount.value = v;
+                historyCount.dispatchEvent(new Event("change", {bubbles: true}));
             } else {
-                $(e.currentTarget).addClass("is-invalid");
+                target.classList.add("is-invalid");
             }
         });
-        $(document).on("click", ".popover #settings-popover #settings-history-count-restore", () => {
-            localStorage.removeItem("historyCount");
-            $(historyCountSelector).val(historyCountDef);
-        });
+        common.delegate(document, "click", ".popover #settings-popover #settings-history-count-restore",
+            () => {
+                localStorage.removeItem("historyCount");
+                document.querySelector(historyCountSelector).value = historyCountDef;
+            });
 
         // Dismiss Bootstrap popover by clicking outside
-        $("body").on("click", (e) => {
-            $(".popover").each(function () {
-                if (
-                    // Popover's descendant
-                    $(this).has(e.target).length ||
-                    // Button (or icon within a button) that triggers the popover.
-                    $(e.target).closest("button").attr("aria-describedby") === this.id
-                ) return;
-                bootstrap.Popover.getOrCreateInstance(document.getElementById("settings")).hide();
+        document.body.addEventListener("click", (e) => {
+            document.querySelectorAll(".popover").forEach((popover) => {
+                const triggerBtn = e.target.closest("button");
+                if (popover.contains(e.target) ||
+                    (triggerBtn && triggerBtn.getAttribute("aria-describedby") === popover.id)) return;
+                bootstrap.Popover.getOrCreateInstance(settingsBtn).hide();
             });
         });
     }());
 
-    $("#selData").change(() => {
+    document.getElementById("selData").addEventListener("change", () => {
         tabClick("#throughput_nav");
     });
 
     function refreshSpinStart() {
-        $("#refresh > svg").addClass("fa-spin");
+        document.querySelectorAll("#refresh > svg").forEach((el) => el.classList.add("fa-spin"));
     }
     function refreshSpinStop() {
         setTimeout(() => {
-            $("#refresh > svg").removeClass("fa-spin");
+            document.querySelectorAll("#refresh > svg").forEach((el) => el.classList.remove("fa-spin"));
         }, 1000);
     }
 
-    // Drive the refresh spinner from both the remaining jQuery $.ajax calls
-    // and common.query (XHR) requests.
-    $(document).ajaxStart(refreshSpinStart);
-    $(document).ajaxComplete(refreshSpinStop);
+    // Drive the refresh spinner from common.query (XHR) requests.
     common.onAjaxStart(refreshSpinStart);
     common.onAjaxComplete(refreshSpinStop);
 
-    $('a[data-bs-toggle="tab"]').on("shown.bs.tab", function () {
-        tabClick("#" + $(this).attr("id"));
+    document.querySelectorAll('a[data-bs-toggle="tab"]').forEach((link) => {
+        link.addEventListener("shown.bs.tab", (e) => {
+            tabClick("#" + e.currentTarget.id);
+        });
     });
-    $("#refresh, #disconnect").on("click", function (e) {
-        e.preventDefault();
-        tabClick("#" + $(this).attr("id"));
+    document.querySelectorAll("#refresh, #disconnect").forEach((btn) => {
+        btn.addEventListener("click", (e) => {
+            e.preventDefault();
+            tabClick("#" + e.currentTarget.id);
+        });
     });
-    $(".dropdown-menu a").click(function (e) {
-        e.preventDefault();
-        const classList = $(this).attr("class");
-        const [menuClass] = (/\b(?:dynamic|history|preset)\b/).exec(classList);
-        $(".dropdown-menu a.active." + menuClass).removeClass("active");
-        $(this).addClass("active");
-        tabClick("#autoRefresh");
+    document.querySelectorAll(".dropdown-menu a").forEach((item) => {
+        item.addEventListener("click", (e) => {
+            e.preventDefault();
+            const match = (/\b(?:dynamic|history|preset)\b/).exec(e.currentTarget.className);
+            if (!match) return;
+            const [menuClass] = match;
+            document.querySelectorAll(".dropdown-menu a.active." + menuClass)
+                .forEach((el) => el.classList.remove("active"));
+            e.currentTarget.classList.add("active");
+            tabClick("#autoRefresh");
+        });
     });
 
-    $("#theme-toggle").on("click", (e) => {
+    document.getElementById("theme-toggle").addEventListener("click", (e) => {
         e.preventDefault();
         const currentTheme = localStorage.getItem("theme") || "auto";
         // Cycle through: light -> dark -> auto -> light
@@ -546,29 +594,28 @@ define(["jquery", "app/common", "bootstrap", "visibility",
         updateThemeIcon(newTheme);
 
         // Update radio button in settings popover if it's open
-        $('.popover #settings-popover input:radio[name="theme"]').val([newTheme]);
+        checkRadio('.popover #settings-popover input[type="radio"][name="theme"]', newTheme);
     });
 
-    $("#selSrv").change(function () {
-        checked_server = this.value;
-        $("#selSrv [value=\"" + checked_server + "\"]").prop("checked", true);
+    document.getElementById("selSrv").addEventListener("change", (e) => {
+        checked_server = e.currentTarget.value;
         if (checked_server === "All SERVERS") {
-            $("#learnServers").removeClass("invisible");
+            document.getElementById("learnServers").classList.remove("invisible");
         } else {
-            $("#learnServers").addClass("invisible");
+            document.getElementById("learnServers").classList.add("invisible");
         }
-        tabClick("#" + $("#tablist > .nav-item > .nav-link.active").attr("id"));
+        tabClick("#" + document.querySelector("#tablist > .nav-item > .nav-link.active").id);
     });
 
     // Radio buttons
-    $(document).on("click", "input:radio[name=\"clusterName\"]", function () {
-        if (!this.disabled) {
-            checked_server = this.value;
+    common.delegate(document, "click", 'input[type="radio"][name="clusterName"]', (event, target) => {
+        if (!target.disabled) {
+            checked_server = target.value;
             tabClick("#status_nav");
         }
     });
 
-    $("#loading").addClass("d-none");
+    document.getElementById("loading").classList.add("d-none");
 
     return ui;
 });