You are about to delete the certificate for: ${certs[0].domain}
` - ); - $("#confirmDeleteCertBtn").data("cert-name", certs[0].domain); - } else { - const certList = certs - .map((cert) => `You are about to delete these certificates:
-You are about to delete the certificate for: ` + + `${certs[0].domain}
` + ); + $("#confirmDeleteCertBtn").data("cert-name", + certs[0].domain); } else { - $("#letsencrypt").DataTable().ajax.reload(); + debugLog(`Configuring modal for multiple certificates: ${ + certs.map(c => c.domain).join(", ")}`); + + const certList = certs + .map((cert) => `You are about to delete these certificates:
+Error deleting certificate ${certName}:
${response.message || "Unknown error"}
` + + debugLog("Modal configuration completed"); + }; + + // Display error modal with specified title and message for user + // feedback during certificate management operations + const showErrorModal = (title, message) => { + debugLog(`Showing error modal: ${title} - ${message}`); + + $("#errorModalLabel").text(title); + $("#errorModalContent").text(message); + const errorModal = new bootstrap.Modal( + document.getElementById("errorModal") ); - if (callback) callback(); - else $("#letsencrypt").DataTable().ajax.reload(); - } - }, - error: function (xhr, status, error) { - console.error("Error deleting certificate:", error, xhr); - - // Create a more detailed error message - let errorMessage = `Failed to delete certificate ${certName}:
`; - - if (xhr.responseJSON && xhr.responseJSON.message) { - errorMessage += `${xhr.responseJSON.message}
`; - } else if (xhr.responseText) { - try { - const parsedError = JSON.parse(xhr.responseText); - errorMessage += `${parsedError.message || error}
`; - } catch (e) { - // If can't parse JSON, use the raw response text if not too large - if (xhr.responseText.length < 200) { - errorMessage += `${xhr.responseText}
`; - } else { - errorMessage += `${error || "Unknown error"}
`; - } + errorModal.show(); + }; + + // Handle delete button click events for certificate deletion + // confirmation modal + $("#confirmDeleteCertBtn").on("click", function () { + const certName = $(this).data("cert-name"); + const certNames = $(this).data("cert-names"); + + debugLog(`Delete button clicked: certName=${certName}, ` + + `certNames=${certNames}`); + + if (certName) { + deleteCertificate(certName); + } else if (certNames && Array.isArray(certNames)) { + // Delete multiple certificates sequentially + const deleteNext = (index) => { + if (index < certNames.length) { + deleteCertificate(certNames[index], () => { + deleteNext(index + 1); + }); + } else { + $("#deleteCertModal").modal("hide"); + $("#letsencrypt").DataTable().ajax.reload(); + } + }; + deleteNext(0); } - } else { - errorMessage += `${error || "Unknown error"}
`; - } - showErrorModal("Certificate Deletion Failed", errorMessage); + $("#deleteCertModal").modal("hide"); + }); - if (callback) callback(); - else $("#letsencrypt").DataTable().ajax.reload(); - }, - }); - } + // Delete a single certificate via AJAX request with optional callback + // for sequential deletion operations + function deleteCertificate(certName, callback) { + debugLog("Starting certificate deletion process:"); + debugLog(`- Certificate name: ${certName}`); + debugLog(`- Has callback: ${!!callback}`); + debugLog(`- Request URL: ${ + window.location.pathname}/delete`); + + const requestData = { cert_name: certName }; + const csrfToken = $("#csrf_token").val(); + + debugLog(`Request payload: ${JSON.stringify(requestData)}`); + debugLog(`CSRF token: ${csrfToken ? "present" : "missing"}`); + + $.ajax({ + url: `${window.location.pathname}/delete`, + type: "POST", + contentType: "application/json", + data: JSON.stringify(requestData), + headers: { + "X-CSRFToken": csrfToken, + }, + beforeSend: function(xhr) { + debugLog(`AJAX request starting for: ${certName}`); + debugLog(`Request headers: ${ + xhr.getAllResponseHeaders()}`); + }, + success: function (response) { + debugLog("Delete response received:"); + debugLog(`- Status: ${response.status}`); + debugLog(`- Message: ${response.message}`); + debugLog(`- Full response: ${ + JSON.stringify(response)}`); + + if (response.status === "ok") { + debugLog(`Certificate deletion successful: ${ + certName}`); + + if (callback) { + debugLog("Executing callback function"); + callback(); + } else { + debugLog("Reloading DataTable data"); + $("#letsencrypt").DataTable().ajax.reload(); + } + } else { + debugLog(`Certificate deletion failed: ${ + response.message}`); + + showErrorModal( + "Certificate Deletion Error", + `Error deleting certificate ${certName}: ${ + response.message || "Unknown error"}` + ); + if (callback) callback(); + else $("#letsencrypt").DataTable().ajax.reload(); + } + }, + error: function (xhr, status, error) { + debugLog("AJAX error details:"); + debugLog(`- XHR status: ${xhr.status}`); + debugLog(`- Status text: ${status}`); + debugLog(`- Error: ${error}`); + debugLog(`- Response text: ${xhr.responseText}`); + debugLog(`- Response JSON: ${ + JSON.stringify(xhr.responseJSON)}`); + + console.error("Error deleting certificate:", error, xhr); + + let errorMessage = `Failed to delete certificate ` + + `${certName}: `; + + if (xhr.responseJSON && xhr.responseJSON.message) { + errorMessage += xhr.responseJSON.message; + } else if (xhr.responseText) { + try { + const parsedError = JSON.parse(xhr.responseText); + errorMessage += + parsedError.message || error; + } catch (e) { + debugLog(`Failed to parse error response: ${e}`); + if (xhr.responseText.length < 200) { + errorMessage += xhr.responseText; + } else { + errorMessage += error || "Unknown error"; + } + } + } else { + errorMessage += error || "Unknown error"; + } + + showErrorModal("Certificate Deletion Failed", + errorMessage); + + if (callback) callback(); + else $("#letsencrypt").DataTable().ajax.reload(); + }, + complete: function(xhr, status) { + debugLog("AJAX request completed:"); + debugLog(`- Final status: ${status}`); + debugLog(`- Certificate: ${certName}`); + } + }); + } - // DataTable Layout and Buttons - const layout = { - top1: { - searchPanes: { - viewTotal: true, - cascadePanes: true, - collapse: false, - columns: [2, 5, 6, 7], // Issuer, Preferred Profile, Challenge and Key Type - }, - }, - topStart: {}, - topEnd: { - search: true, - buttons: [ - { - extend: "auto_refresh", - className: - "btn btn-sm btn-outline-primary d-flex align-items-center", - }, - { - extend: "toggle_filters", - className: "btn btn-sm btn-outline-primary toggle-filters", - }, - ], - }, - bottomStart: { - pageLength: { - menu: [10, 25, 50, 100, { label: "All", value: -1 }], - }, - info: true, - }, - }; - - layout.topStart.buttons = [ - { - extend: "colvis", - columns: "th:not(:nth-child(-n+3)):not(:last-child)", - text: `${t( - "button.columns", - "Columns" - )}`, - className: "btn btn-sm btn-outline-primary rounded-start", - columnText: function (dt, idx, title) { - return `${idx + 1}. ${title}`; - }, - }, - { - extend: "colvisRestore", - text: `${t( - "button.reset_columns", - "Reset columns" - )}`, - className: "btn btn-sm btn-outline-primary d-none d-md-inline", - }, - { - extend: "collection", - text: `${t( - "button.export", - "Export" - )}`, - className: "btn btn-sm btn-outline-primary", - buttons: [ - { - extend: "copy", - text: `${t( - "button.copy_visible", - "Copy visible" - )}`, - exportOptions: { - columns: ":visible:not(:nth-child(-n+2)):not(:last-child)", + // DataTable Layout and Button configuration + const layout = { + top1: { + searchPanes: { + viewTotal: true, + cascadePanes: true, + collapse: false, + // Issuer, Preferred Profile, Challenge, Key Type, and OCSP + columns: [2, 5, 6, 7, 8], + }, }, - }, - { - extend: "csv", - text: `CSV`, - bom: true, - filename: "bw_certificates", - exportOptions: { - modifier: { search: "none" }, - columns: ":not(:nth-child(-n+2)):not(:last-child)", + topStart: {}, + topEnd: { + search: true, + buttons: [ + { + extend: "auto_refresh", + className: ( + "btn btn-sm btn-outline-primary " + + "d-flex align-items-center" + ), + }, + { + extend: "toggle_filters", + className: "btn btn-sm btn-outline-primary " + + "toggle-filters", + }, + ], }, - }, - { - extend: "excel", - text: `Excel`, - filename: "bw_certificates", - exportOptions: { - modifier: { search: "none" }, - columns: ":not(:nth-child(-n+2)):not(:last-child)", + bottomStart: { + pageLength: { + menu: [10, 25, 50, 100, + { label: "All", value: -1 }], + }, + info: true, }, - }, - ], - }, - { - extend: "collection", - text: `${t( - "button.actions", - "Actions" - )}`, - className: "btn btn-sm btn-outline-primary action-button disabled", - buttons: [{ extend: "delete_cert", className: "text-danger" }], - }, - ]; - - let autoRefresh = false; - let autoRefreshInterval = null; - const sessionAutoRefresh = sessionStorage.getItem("letsencryptAutoRefresh"); - - function toggleAutoRefresh() { - autoRefresh = !autoRefresh; - sessionStorage.setItem("letsencryptAutoRefresh", autoRefresh); - if (autoRefresh) { - $(".bx-loader") - .addClass("bx-spin") - .closest(".btn") - .removeClass("btn-outline-primary") - .addClass("btn-primary"); - if (autoRefreshInterval) clearInterval(autoRefreshInterval); - autoRefreshInterval = setInterval(() => { - if (!autoRefresh) { - clearInterval(autoRefreshInterval); - autoRefreshInterval = null; - } else { - $("#letsencrypt").DataTable().ajax.reload(null, false); - } - }, 10000); // 10 seconds - } else { - $(".bx-loader") - .removeClass("bx-spin") - .closest(".btn") - .removeClass("btn-primary") - .addClass("btn-outline-primary"); - if (autoRefreshInterval) { - clearInterval(autoRefreshInterval); - autoRefreshInterval = null; - } - } - } + }; - if (sessionAutoRefresh === "true") { - toggleAutoRefresh(); - } + debugLog("DataTable layout configuration:"); + debugLog(`- Search panes columns: ${ + layout.top1.searchPanes.columns.join(", ")}`); + debugLog(`- Page length options: ${ + JSON.stringify(layout.bottomStart.pageLength.menu)}`); + debugLog(`- Layout structure: ${JSON.stringify(layout)}`); + debugLog(`- Headers count: ${headers.length}`); + + layout.topStart.buttons = [ + { + extend: "colvis", + columns: "th:not(:nth-child(-n+3)):not(:last-child)", + text: ( + `${t( + "button.columns", + "Columns" + )}` + ), + className: "btn btn-sm btn-outline-primary rounded-start", + columnText: function (dt, idx, title) { + return `${idx + 1}. ${title}`; + }, + }, + { + extend: "colvisRestore", + text: ( + `${t( + "button.reset_columns", + "Reset columns" + )}` + ), + className: "btn btn-sm btn-outline-primary d-none d-md-inline", + }, + { + extend: "collection", + text: ( + `${t( + "button.export", + "Export" + )}` + ), + className: "btn btn-sm btn-outline-primary", + buttons: [ + { + extend: "copy", + text: ( + `${t( + "button.copy_visible", + "Copy visible" + )}` + ), + exportOptions: { + columns: ( + ":visible:not(:nth-child(-n+2)):" + + "not(:last-child)" + ), + }, + }, + { + extend: "csv", + text: ( + `CSV` + ), + bom: true, + filename: "bw_certificates", + exportOptions: { + modifier: { search: "none" }, + columns: ( + ":not(:nth-child(-n+2)):not(:last-child)" + ), + }, + }, + { + extend: "excel", + text: ( + `Excel` + ), + filename: "bw_certificates", + exportOptions: { + modifier: { search: "none" }, + columns: ( + ":not(:nth-child(-n+2)):not(:last-child)" + ), + }, + }, + ], + }, + { + extend: "collection", + text: ( + `${t( + "button.actions", + "Actions" + )}` + ), + className: ( + "btn btn-sm btn-outline-primary action-button disabled" + ), + buttons: [ + { extend: "delete_cert", className: "text-danger" } + ], + }, + ]; + + let autoRefresh = false; + let autoRefreshInterval = null; + const sessionAutoRefresh = + sessionStorage.getItem("letsencryptAutoRefresh"); + + // Toggle auto-refresh functionality for DataTable data with + // visual feedback and interval management + function toggleAutoRefresh() { + autoRefresh = !autoRefresh; + sessionStorage.setItem("letsencryptAutoRefresh", autoRefresh); + + debugLog(`Auto-refresh toggled: ${autoRefresh}`); + + if (autoRefresh) { + $(".bx-loader") + .addClass("bx-spin") + .closest(".btn") + .removeClass("btn-outline-primary") + .addClass("btn-primary"); + + if (autoRefreshInterval) clearInterval(autoRefreshInterval); + + autoRefreshInterval = setInterval(() => { + if (!autoRefresh) { + clearInterval(autoRefreshInterval); + autoRefreshInterval = null; + } else { + $("#letsencrypt").DataTable().ajax.reload(null, + false); + } + }, 10000); + } else { + $(".bx-loader") + .removeClass("bx-spin") + .closest(".btn") + .removeClass("btn-primary") + .addClass("btn-outline-primary"); + + if (autoRefreshInterval) { + clearInterval(autoRefreshInterval); + autoRefreshInterval = null; + } + } + } - const getSelectedCertificates = () => { - const certs = []; - $("tr.selected").each(function () { - const $row = $(this); - const domain = $row.find("td:eq(2)").text().trim(); - certs.push({ - domain: domain, - }); - }); - return certs; - }; - - $.fn.dataTable.ext.buttons.auto_refresh = { - text: ' Auto refresh', - action: (e, dt, node, config) => { - toggleAutoRefresh(); - }, - }; - - $.fn.dataTable.ext.buttons.delete_cert = { - text: `Delete certificate`, - action: function (e, dt, node, config) { - if (isReadOnly) { - alert( - t( - "alert.readonly_mode", - "This action is not allowed in read-only mode." - ) - ); - return; + if (sessionAutoRefresh === "true") { + toggleAutoRefresh(); } - if (actionLock) return; - actionLock = true; - $(".dt-button-background").click(); - - const certs = getSelectedCertificates(); - if (certs.length === 0) { - actionLock = false; - return; + + // Extract currently selected certificates from DataTable rows + // and return their domain information for bulk operations + const getSelectedCertificates = () => { + const certs = []; + $("tr.selected").each(function () { + const $row = $(this); + const domain = $row.find("td:eq(2)").text().trim(); + certs.push({ domain: domain }); + }); + + debugLog(`Selected certificates: ${ + certs.map(c => c.domain).join(", ")}`); + + return certs; + }; + + // Custom DataTable button for auto-refresh functionality + $.fn.dataTable.ext.buttons.auto_refresh = { + text: ( + '' + + ' ' + + 'Auto refresh' + ), + action: (e, dt, node, config) => { + toggleAutoRefresh(); + }, + }; + + // Custom DataTable button for certificate deletion with + // read-only mode checks and selection validation + $.fn.dataTable.ext.buttons.delete_cert = { + text: ( + `` + + `Delete certificate` + ), + action: function (e, dt, node, config) { + if (isReadOnly) { + alert( + t( + "alert.readonly_mode", + "This action is not allowed in read-only mode." + ) + ); + return; + } + + if (actionLock) return; + actionLock = true; + $(".dt-button-background").click(); + + const certs = getSelectedCertificates(); + if (certs.length === 0) { + actionLock = false; + return; + } + + setupDeleteCertModal(certs); + + const deleteModal = new bootstrap.Modal( + document.getElementById("deleteCertModal") + ); + deleteModal.show(); + + actionLock = false; + }, + }; + + // Build column definitions for DataTable configuration with + // responsive controls, selection, and search pane settings + function buildColumnDefs() { + return [ + { + orderable: false, + className: "dtr-control", + targets: 0 + }, + { + orderable: false, + render: DataTable.render.select(), + targets: 1 + }, + { type: "string", targets: 2 }, + { orderable: true, targets: -1 }, + { + targets: [5, 6], + render: function (data, type, row) { + if (type === "display" || type === "filter") { + const date = new Date(data); + if (!isNaN(date.getTime())) { + return date.toLocaleString(); + } + } + return data; + }, + }, + { + searchPanes: { + show: true, + combiner: "or", + header: t("searchpane.issuer", "Issuer"), + }, + targets: 2, + }, + { + searchPanes: { + show: true, + header: t("searchpane.preferred_profile", + "Preferred Profile"), + combiner: "or", + }, + targets: 5, + }, + { + searchPanes: { + show: true, + header: t("searchpane.challenge", "Challenge"), + combiner: "or", + }, + targets: 6, + }, + { + searchPanes: { + show: true, + header: t("searchpane.key_type", "Key Type"), + combiner: "or", + }, + targets: 7, + }, + { + searchPanes: { + show: true, + header: t("searchpane.ocsp", "OCSP Support"), + combiner: "or", + }, + targets: 8, + }, + ]; } - setupDeleteCertModal(certs); - - // Show the modal - const deleteModal = new bootstrap.Modal( - document.getElementById("deleteCertModal") - ); - deleteModal.show(); - - actionLock = false; - }, - }; - - // Create columns configuration - function buildColumnDefs() { - return [ - { orderable: false, className: "dtr-control", targets: 0 }, - { orderable: false, render: DataTable.render.select(), targets: 1 }, - { type: "string", targets: 2 }, // domain - { orderable: true, targets: -1 }, - { - targets: [5, 6], - render: function (data, type, row) { - if (type === "display" || type === "filter") { - const date = new Date(data); - if (!isNaN(date.getTime())) { - return date.toLocaleString(); - } - } - return data; - }, - }, - { - searchPanes: { - show: true, - combiner: "or", - header: t("searchpane.issuer", "Issuer"), - }, - targets: 2, // Issuer column - }, - { - searchPanes: { - show: true, - header: t("searchpane.preferred_profile", "Preferred Profile"), - combiner: "or", - }, - targets: 5, // Preferred Profile column - }, - { - searchPanes: { - show: true, - header: t("searchpane.challenge", "Challenge"), - combiner: "or", - }, - targets: 6, // Challenge column - }, - { - searchPanes: { - show: true, - header: t("searchpane.key_type", "Key Type"), - combiner: "or", - }, - targets: 7, // Key Type column - }, - ]; - } - // Define the columns for the DataTable - function buildColumns() { - return [ - { - data: null, - defaultContent: "", - orderable: false, - className: "dtr-control", - }, - { data: null, defaultContent: "", orderable: false }, - { - data: "domain", - title: "Domain", - }, - { - data: "common_name", - title: "Common Name", - }, - { - data: "issuer", - title: "Issuer", - }, - { - data: "valid_from", - title: "Valid From", - }, - { - data: "valid_to", - title: "Valid To", - }, - { - data: "preferred_profile", - title: "Preferred Profile", - }, - { - data: "challenge", - title: "Challenge", - }, - { - data: "key_type", - title: "Key Type", - }, - { - data: "serial_number", - title: "Serial Number", - }, - { - data: "fingerprint", - title: "Fingerprint", - }, - { - data: "version", - title: "Version", - }, - ]; - } + // Define the columns for the DataTable with data mappings + // and display configurations for certificate information + function buildColumns() { + return [ + { + data: null, + defaultContent: "", + orderable: false, + className: "dtr-control", + }, + { data: null, defaultContent: "", orderable: false }, + { data: "domain", title: "Domain" }, + { data: "common_name", title: "Common Name" }, + { data: "issuer", title: "Issuer" }, + { data: "valid_from", title: "Valid From" }, + { data: "valid_to", title: "Valid To" }, + { data: "preferred_profile", title: "Preferred Profile" }, + { data: "challenge", title: "Challenge" }, + { data: "key_type", title: "Key Type" }, + { data: "ocsp_support", title: "OCSP" }, + { data: "serial_number", title: "Serial Number" }, + { data: "fingerprint", title: "Fingerprint" }, + { data: "version", title: "Version" }, + ]; + } - // Utility function to manage header tooltips - function updateHeaderTooltips(selector, headers) { - $(selector) - .find("th") - .each((index, element) => { - const $th = $(element); - const tooltip = headers[index] ? headers[index].tooltip : ""; - if (!tooltip) return; - - $th.attr({ - "data-bs-toggle": "tooltip", - "data-bs-placement": "bottom", - title: tooltip, - }); - }); + // Manage header tooltips for DataTable columns by applying + // Bootstrap tooltip attributes to table headers + function updateHeaderTooltips(selector, headers) { + $(selector) + .find("th") + .each((index, element) => { + const $th = $(element); + const tooltip = headers[index] ? + headers[index].tooltip : ""; + if (!tooltip) return; + + $th.attr({ + "data-bs-toggle": "tooltip", + "data-bs-placement": "bottom", + title: tooltip, + }); + }); + + $('[data-bs-toggle="tooltip"]').tooltip("dispose").tooltip(); + } - $('[data-bs-toggle="tooltip"]').tooltip("dispose").tooltip(); - } + // Initialize the DataTable with complete configuration including + // server-side processing, AJAX data loading, and UI components + const letsencrypt_config = { + tableSelector: "#letsencrypt", + tableName: "letsencrypt", + columnVisibilityCondition: (column) => column > 2 && column < 14, + dataTableOptions: { + columnDefs: buildColumnDefs(), + order: [[2, "asc"]], + autoFill: false, + responsive: true, + select: { + style: "multi+shift", + selector: "td:nth-child(2)", + headerCheckbox: true, + }, + layout: layout, + processing: true, + serverSide: true, + ajax: { + url: `${window.location.pathname}/fetch`, + type: "POST", + data: function (d) { + debugLog(`DataTable AJAX request data: ${ + JSON.stringify(d)}`); + debugLog("Request parameters:"); + debugLog(`- Draw: ${d.draw}`); + debugLog(`- Start: ${d.start}`); + debugLog(`- Length: ${d.length}`); + debugLog(`- Search value: ${d.search?.value}`); + + d.csrf_token = $("#csrf_token").val(); + return d; + }, + error: function (jqXHR, textStatus, errorThrown) { + debugLog("DataTable AJAX error details:"); + debugLog(`- Status: ${jqXHR.status}`); + debugLog(`- Status text: ${textStatus}`); + debugLog(`- Error: ${errorThrown}`); + debugLog(`- Response text: ${jqXHR.responseText}`); + debugLog(`- Response headers: ${ + jqXHR.getAllResponseHeaders()}`); + + console.error("DataTables AJAX error:", + textStatus, errorThrown); + + $("#letsencrypt").addClass("d-none"); + $("#letsencrypt-waiting") + .removeClass("d-none") + .text("Error loading certificates. " + + "Please try refreshing the page.") + .addClass("text-danger"); + + $(".dataTables_processing").hide(); + }, + success: function(data, textStatus, jqXHR) { + debugLog("DataTable AJAX success:"); + debugLog(`- Records total: ${data.recordsTotal}`); + debugLog(`- Records filtered: ${ + data.recordsFiltered}`); + debugLog(`- Data length: ${data.data?.length}`); + debugLog(`- Draw number: ${data.draw}`); + } + }, + columns: buildColumns(), + initComplete: function (settings, json) { + debugLog(`DataTable initialized with settings: ${ + JSON.stringify(settings)}`); + + $("#letsencrypt_wrapper .btn-secondary") + .removeClass("btn-secondary"); + + $("#letsencrypt-waiting").addClass("d-none"); + $("#letsencrypt").removeClass("d-none"); + + if (isReadOnly) { + const titleKey = userReadOnly + ? "tooltip.readonly_user_action_disabled" + : "tooltip.readonly_db_action_disabled"; + const defaultTitle = userReadOnly + ? "Your account is readonly, action disabled." + : "The database is in readonly, action disabled."; + } + }, + headerCallback: function (thead) { + updateHeaderTooltips(thead, headers); + }, + }, + }; - // Initialize the DataTable with columns and configuration - const letsencrypt_config = { - tableSelector: "#letsencrypt", - tableName: "letsencrypt", - columnVisibilityCondition: (column) => column > 2 && column < 13, - dataTableOptions: { - columnDefs: buildColumnDefs(), - order: [[2, "asc"]], // Sort by domain name - autoFill: false, - responsive: true, - select: { - style: "multi+shift", - selector: "td:nth-child(2)", - headerCheckbox: true, - }, - layout: layout, - processing: true, - serverSide: true, - ajax: { - url: `${window.location.pathname}/fetch`, - type: "POST", - data: function (d) { - d.csrf_token = $("#csrf_token").val(); - return d; - }, - // Add error handling for ajax requests - error: function (jqXHR, textStatus, errorThrown) { - console.error("DataTables AJAX error:", textStatus, errorThrown); - $("#letsencrypt").addClass("d-none"); - $("#letsencrypt-waiting") - .removeClass("d-none") - .text( - "Error loading certificates. Please try refreshing the page." - ) - .addClass("text-danger"); - // Remove any loading indicators - $(".dataTables_processing").hide(); - }, - }, - columns: buildColumns(), - initComplete: function (settings, json) { - $("#letsencrypt_wrapper .btn-secondary").removeClass("btn-secondary"); - - // Hide loading message and show table - $("#letsencrypt-waiting").addClass("d-none"); - $("#letsencrypt").removeClass("d-none"); - - if (isReadOnly) { - const titleKey = userReadOnly - ? "tooltip.readonly_user_action_disabled" - : "tooltip.readonly_db_action_disabled"; - const defaultTitle = userReadOnly - ? "Your account is readonly, action disabled." - : "The database is in readonly, action disabled."; - } - }, - headerCallback: function (thead) { - updateHeaderTooltips(thead, headers); - }, - }, - }; - - const dt = initializeDataTable(letsencrypt_config); - dt.on("draw.dt", function () { - updateHeaderTooltips(dt.table().header(), headers); - $(".tooltip").remove(); - }); - dt.on("column-visibility.dt", function (e, settings, column, state) { - updateHeaderTooltips(dt.table().header(), headers); - $(".tooltip").remove(); - }); + const dt = initializeDataTable(letsencrypt_config); + + dt.on("draw.dt", function () { + updateHeaderTooltips(dt.table().header(), headers); + $(".tooltip").remove(); + }); + + dt.on("column-visibility.dt", function (e, settings, column, state) { + updateHeaderTooltips(dt.table().header(), headers); + $(".tooltip").remove(); + }); - // Add selection event handler for toggle action button - dt.on("select.dt deselect.dt", function () { - const count = dt.rows({ selected: true }).count(); - $(".action-button").toggleClass("disabled", count === 0); + // Toggle action button based on row selection state + dt.on("select.dt deselect.dt", function () { + const count = dt.rows({ selected: true }).count(); + $(".action-button").toggleClass("disabled", count === 0); + + debugLog(`Selection changed, count: ${count}`); + }); }); - }); -})(); +})(); \ No newline at end of file diff --git a/src/common/core/letsencrypt/ui/hooks.py b/src/common/core/letsencrypt/ui/hooks.py index 0ca87d78b6..79e5ddee1d 100644 --- a/src/common/core/letsencrypt/ui/hooks.py +++ b/src/common/core/letsencrypt/ui/hooks.py @@ -1,30 +1,87 @@ +from logging import getLogger +from os import getenv from flask import request -# Default column visibility settings for letsencrypt tables +# Default column visibility settings for Let's Encrypt certificate tables +# Key represents column index, value indicates if column is visible by default COLUMNS_PREFERENCES_DEFAULTS = { - "3": True, - "4": True, - "5": True, - "6": True, - "7": True, - "8": True, - "9": False, - "10": False, - "11": True, + "3": True, # Common Name + "4": True, # Issuer + "5": True, # Valid From + "6": True, # Valid To + "7": True, # Preferred Profile + "8": True, # Challenge + "9": True, # Key Type + "10": True, # OCSP Support + "11": False, # Serial Number (hidden by default) + "12": False, # Fingerprint (hidden by default) + "13": True, # Version } +def debug_log(logger, message): + # Log debug messages only when LOG_LEVEL environment variable is set to + # "debug" + if getenv("LOG_LEVEL") == "debug": + logger.debug(f"[DEBUG] {message}") + + def context_processor(): - """ - Flask context processor to inject variables into templates. - - This adds: - - Column preference defaults for tables - - Extra pages visibility based on user permissions - """ - if request.path.startswith(("/check", "/setup", "/loading", "/login", "/totp", "/logout")): + # Flask context processor to inject variables into templates. + # + # Provides template context data for the Let's Encrypt certificate + # management interface. Injects column preferences and other UI + # configuration data that templates need for proper rendering. + logger = getLogger("UI") + is_debug = getenv("LOG_LEVEL") == "debug" + + debug_log(logger, "Context processor called") + debug_log(logger, f"Request path: {request.path}") + debug_log(logger, f"Request method: {request.method}") + debug_log(logger, + f"Request endpoint: {getattr(request, 'endpoint', 'unknown')}") + + # Skip context processing for system/auth pages that don't need it + excluded_paths = [ + "/check", "/setup", "/loading", + "/login", "/totp", "/logout" + ] + + # Check if current path should be excluded + path_excluded = request.path.startswith(tuple(excluded_paths)) + + if path_excluded: + debug_log(logger, + f"Path {request.path} is excluded from context processing") + for excluded_path in excluded_paths: + if request.path.startswith(excluded_path): + debug_log(logger, + f" Matched exclusion pattern: {excluded_path}") + break return None - data = {"columns_preferences_defaults_letsencrypt": COLUMNS_PREFERENCES_DEFAULTS} + debug_log(logger, f"Processing context for path: {request.path}") + debug_log(logger, "Column preferences to inject:") + column_names = { + "3": "Common Name", "4": "Issuer", "5": "Valid From", + "6": "Valid To", "7": "Preferred Profile", "8": "Challenge", + "9": "Key Type", "10": "OCSP Support", "11": "Serial Number", + "12": "Fingerprint", "13": "Version" + } + for col_id, visible in COLUMNS_PREFERENCES_DEFAULTS.items(): + col_name = column_names.get(col_id, f"Column {col_id}") + debug_log(logger, + f" {col_name} (#{col_id}): {'visible' if visible else 'hidden'}") + + # Prepare context data for templates + data = { + "columns_preferences_defaults_letsencrypt": COLUMNS_PREFERENCES_DEFAULTS + } + + debug_log(logger, f"Context processor returning {len(data)} variables") + debug_log(logger, f"Context data keys: {list(data.keys())}") + debug_log(logger, + f"Let's Encrypt preferences: {len(COLUMNS_PREFERENCES_DEFAULTS)} " + f"columns configured") - return data + return data \ No newline at end of file