import { API_BASE_URL, configStore, cleanupConfigStore } from './global.js';
/**
* Extracts a concise description of the secret type (e.g., TlsCertificate).
* @param {object} secret - The secret configuration object.
* @returns {string} A string describing the secret type.
*/
function getSecretTypeDetails(secret) {
try {
const secretType = secret.Type;
if (!secretType) return '<span style="color: gray;">(Unknown Type)</span>';
// Find the key that is not 'name' or 'Type' itself
const typeKeys = Object.keys(secretType);
const typeName = typeKeys.find(key => key !== 'name');
let typeDetails = '';
if (typeName) {
// Convert 'TlsCertificate' to 'TLS Certificate'
typeDetails = typeName.replace(/([A-Z])/g, ' $1').trim();
} else {
typeDetails = '<span style="color: gray;">(Generic)</span>';
}
// ADD: Append validity status for TLS Certificates
if (typeName === 'TlsCertificate' && secret.validityStatus) {
const statusText = secret.validityStatus; // 'Valid' or 'Invalid'
const statusClass = statusText.toLowerCase(); // 'valid' or 'invalid'
typeDetails += ` <span class="status-label ${statusClass}">${statusText}</span>`;
}
// NEW: Append rotation status for TLS Certificates with HOVER DETAILS
if (typeName === 'TlsCertificate' && secret.rotationStatus === 'Enabled') {
const rotationInfo = secret.rotationDetails;
let tooltipContent = 'Auto Rotation Enabled.'; // Renamed from tooltipText to tooltipContent
if (rotationInfo) {
// 1. Convert renew_before to display format
let renewBeforeDisplay = rotationInfo.renew_before || 'N/A';
const hoursMatch = renewBeforeDisplay.match(/(\d+)h/);
if (hoursMatch) {
const hours = parseInt(hoursMatch[1], 10);
const days = Math.round(hours / 24);
renewBeforeDisplay = `${days}d (${hours}h)`;
}
const rotationStatusText = rotationInfo.rotation_enabled ? 'Active' : 'Configured (Disabled)';
const expiresAtText = rotationInfo.expires_at || 'N/A';
// 2. Assemble the full tooltip content with actual newlines
// This content will be used by our CSS tooltip.
tooltipContent = `
Rotation Status: ${rotationStatusText}
Domain: ${rotationInfo.domain}
Secret Name: ${rotationInfo.secret_name}
Renew Before: ${renewBeforeDisplay}
Expires At: ${expiresAtText}
`.trim();
}
// --- IMPORTANT CHANGE HERE ---
// Remove the 'title' attribute and use 'data-tooltip' instead.
// The 'title' attribute can be kept as a fallback for accessibility,
// but the CSS tooltip will take precedence.
typeDetails += `<span class="status-label rotation-enabled has-tooltip" data-tooltip="${tooltipContent}">Auto Rotation Enabled</span>`;
}
return typeDetails;
} catch {
return '<span style="color: gray;">(Config Error)';
}
}
/**
* Checks if a secret is a TLS Certificate.
* @param {object} secret - The secret configuration object.
* @returns {boolean} True if the secret is a TLS Certificate.
*/
function isTlsCertificate(secret) {
try {
const typeKeys = Object.keys(secret.Type);
return typeKeys.some(key => key === 'TlsCertificate');
} catch {
return false;
}
}
// =========================================================================
// CERTIFICATE LOGIC (API & Modal) (Modified/New)
// =========================================================================
/**
* Calls the backend API to parse and retrieve certificate details.
* @param {string} certificatePem - The PEM encoded certificate string.
* @returns {Promise<object>} The parsed certificate details as a JSON object.
*/
async function getCertificateDetails(certificatePem) {
const url = `${API_BASE_URL}/parse-certificate`;
const payload = { certificate_pem: certificatePem };
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`HTTP Error ${response.status}: ${errorBody}`);
}
return response.json();
}
/**
* Calls the backend API to check the validity of a certificate.
* @param {string} certificatePem - The PEM encoded certificate string.
* @returns {Promise<boolean>} True if the certificate is valid, false otherwise.
*/
async function checkCertificateValidity(certificatePem) {
const url = `${API_BASE_URL}/check-certificate-validity`;
const payload = { certificate_pem: certificatePem };
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
// Treat an un-ok response as invalid or uncheckable, but don't crash the UI.
console.warn(`Validity check failed with status ${response.status} for a certificate.`);
return false;
}
const result = await response.json();
// The API response structure is { "valid": true/false }
return result.valid === true;
} catch (error) {
console.error("Error during certificate validity check:", error);
return false; // Assume invalid on API failure
}
}
/**
* Shows a modal with the detailed certificate information.
* @param {string} secretName - The name of the secret.
*/
export async function showCertificateDetailsModal(secretName) {
const modal = document.getElementById('certificateDetailsModal');
const content = document.getElementById('certificate-details-content');
if (!modal || !content) {
console.error("Certificate modal elements not found. Ensure 'certificateDetailsModal' and 'certificate-details-content' exist in your HTML.");
alert("Certificate details modal is not properly configured in HTML.");
return;
}
// Get the full secret config, which should contain the 'certificate_pem'
const secretConfig = configStore.secrets[secretName];
// Drill down to the PEM string
const certPem = secretConfig?.Type?.TlsCertificate?.certificate_chain?.Specifier?.InlineString
if (!certPem) {
alert(`Could not find certificate PEM in the configuration for secret: ${secretName}`);
return;
}
content.innerHTML = `<p style="text-align: center;">Parsing certificate for <strong>${secretName}</strong>...</p>`;
modal.style.display = 'block';
try {
const details = await getCertificateDetails(certPem);
// Display the pretty-printed JSON response
content.innerHTML = `
<h3>TLS Certificate Details: ${secretName}</h3>
<pre style="white-space: pre-wrap; word-wrap: break-word; background: var(--input-bg); padding: 10px; border-radius: 5px;">${JSON.stringify(details, null, 2)}</pre>
`;
} catch (error) {
content.innerHTML = `
<h3>Error Parsing Certificate: ${secretName}</h3>
<p class="error">🚨 Failed to parse certificate. Error: ${error.message}</p>
`;
console.error("Certificate Parsing Error:", error);
}
}
export function hideCertificateDetailsModal() {
const modal = document.getElementById('certificateDetailsModal');
if (modal) {
modal.style.display = 'none';
// Clear the input when closing
// NOTE: Changed .value to .innerHTML as 'certificate-details-content' is a container, not an input.
const content = document.getElementById('certificate-details-content');
if (content) content.innerHTML = '';
}
}
// Expose the new function globally for inline HTML onclick handlers
window.showCertificateDetailsModal = showCertificateDetailsModal;
window.hideCertificateDetailsModal = hideCertificateDetailsModal;
// -------------------------------------------------------------------------
// NEW ROTATION SETTINGS LOGIC
// -------------------------------------------------------------------------
/**
* Calls the backend API to fetch the current certificate rotation status.
* @param {string} secretName - The name of the secret/certificate.
* @returns {Promise<object>} The rotation status as a JSON object.
*/
async function getCertificateRotationStatus(secretName) {
// NOTE: Using a GET request with query parameter, similar to the user's curl example
const url = `${API_BASE_URL}/get-certificate?secret_name=${encodeURIComponent(secretName)}`;
try {
const response = await fetch(url, { method: 'GET' });
if (!response.ok) {
const errorText = await response.text();
let errorMessage = `HTTP Error ${response.status}: ${response.statusText}`;
try {
errorMessage = JSON.parse(errorText).error || JSON.parse(errorText).message || errorText;
} catch {
errorMessage = errorText || errorMessage;
}
throw new Error(errorMessage);
}
return response.json();
} catch (error) {
console.error(`Failed to fetch rotation status for '${secretName}':`, error);
throw new Error(`Failed to load rotation status. Error: ${error.message}`);
}
}
/**
* Calls the backend API to enable certificate rotation.
* @param {string} secretName - The name of the secret/certificate.
* @param {string} domain - The domain name associated with the certificate.
* @param {string} renewBefore - The Go duration string (e.g., "720h").
*/
async function enableCertificateRotation(secretName, domain, renewBefore) {
const url = `${API_BASE_URL}/enable-certificate-rotation`;
const payload = {
domain: domain.trim(),
secret_name: secretName,
renew_before: renewBefore.trim()
};
console.log(`Attempting to enable rotation for secret: ${secretName}`, payload);
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorText = await response.text();
let errorMessage = `HTTP Error ${response.status}: ${response.statusText}`;
try {
// Try to extract a clean error message from the JSON response
errorMessage = JSON.parse(errorText).error || JSON.parse(errorText).message || errorText;
} catch {
errorMessage = errorText || errorMessage;
}
throw new Error(errorMessage);
}
const responseBody = await response.json();
console.log(`Rotation successfully enabled for '${domain}':`, responseBody);
alert(`Certificate rotation successfully enabled for ${domain}! The list will now refresh.`);
hideRotationSettingsModal();
cleanupConfigStore();
listSecrets();
} catch (error) {
console.error(`Failed to enable rotation for '${domain}':`, error);
alert(`🚨 Failed to enable rotation for ${domain}. Error: ${error.message}`);
}
}
/**
* Calls the backend API to disable certificate rotation.
* @param {string} secretName - The name of the secret/certificate.
*/
async function disableCertificateRotation(secretName, domain) {
const url = `${API_BASE_URL}/disable-certificate-rotation`;
const payload = { secret_name: secretName , domain: domain.trim()};
console.log(`Attempting to disable rotation for secret: ${secretName}`);
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorText = await response.text();
let errorMessage = `HTTP Error ${response.status}: ${response.statusText}`;
try {
errorMessage = JSON.parse(errorText).error || JSON.parse(errorText).message || errorText;
} catch {
errorMessage = errorText || errorMessage;
}
throw new Error(errorMessage);
}
console.log(`Rotation successfully disabled for secret: ${secretName}`);
alert(`Certificate rotation successfully disabled for ${secretName}! The list will now refresh.`);
hideRotationSettingsModal();
cleanupConfigStore();
listSecrets();
} catch (error) {
console.error(`Failed to disable rotation for secret: ${secretName}`, error);
alert(`🚨 Failed to disable rotation for ${secretName}. Error: ${error.message}`);
}
}
/**
* Shows the modal for certificate rotation settings.
* @param {string} secretName - The name of the secret.
*/
export async function showRotationSettingsModal(secretName) {
const modal = document.getElementById('rotationSettingsModal');
const secretNameEl = document.getElementById('rotation-secret-name');
const statusContainer = document.getElementById('rotation-current-status-container');
const domainInput = document.getElementById('rotation-domain-input');
const renewBeforeInput = document.getElementById('rotation-renew-before-input');
const rotationToggle = document.getElementById('rotation-enable-toggle');
const submitBtn = document.getElementById('rotation-submit-btn');
const manualRenewBtn = document.getElementById('rotation-manual-renew-btn');
if (!modal || !secretNameEl || !domainInput || !renewBeforeInput || !rotationToggle || !submitBtn || !manualRenewBtn) {
console.error("Rotation settings modal elements not found.");
alert("Rotation settings modal is not properly configured in HTML.");
return;
}
// Set secret name and show loading state
secretNameEl.textContent = secretName;
const loadingHtml = '<p style="text-align: center;">⏳ Loading current rotation status...</p>';
if (!statusContainer) {
alert("HTML is missing the required 'rotation-current-status-container' element.");
return;
}
statusContainer.innerHTML = loadingHtml;
// Hide the form elements until data is loaded
document.getElementById('rotation-settings-form').style.display = 'none';
modal.style.display = 'block';
try {
const status = await getCertificateRotationStatus(secretName);
// --- Display Current Status ---
const isEnabled = status.EnableRotation;
const statusClass = isEnabled ? 'valid' : 'invalid';
const statusText = isEnabled ? 'ENABLED' : 'DISABLED';
// Get RenewBefore, which is now an integer nanosecond timestamp
const renewBeforeNanos = status.RenewBefore || 0;
let days = 0;
let renewBeforeDisplay = 'N/A';
// CORRECTED LOGIC: Convert nanoseconds to days for display and pre-fill
if (typeof renewBeforeNanos === 'number' && renewBeforeNanos > 0) {
// 1 day = 24 hours * 60 minutes * 60 seconds * 1,000,000,000 nanoseconds
const NANOS_PER_DAY = 86400000000000;
// Calculate days (use Math.round for setting the input value)
days = Math.round(renewBeforeNanos / NANOS_PER_DAY);
// For display, format it nicely
const hours = Math.round(renewBeforeNanos / 3600000000000); // 1 hour = 3.6e12 nanos
renewBeforeDisplay = `${days} days (${hours}h)`;
}
// Fallback for pre-fill if conversion results in 0 or N/A, use default 30 days
const renewBeforeDaysForInput = days > 0 ? days : 30;
// --- Build Status HTML ---
statusContainer.innerHTML = `
<div class="status-block ${statusClass}" style="margin-bottom: 20px;">
<p><strong>Rotation Status:</strong> <span class="status-label ${statusClass}">${statusText}</span></p>
${isEnabled ?
`<p><strong>Domain:</strong> ${status.Domain}</p>
<p><strong>Renew Before:</strong> ${renewBeforeDisplay}</p>
<p><strong>Issuer:</strong> ${status.IssuerType}</p>`
: '<p style="color: var(--secondary-color);">Rotation is not currently configured for this secret.</p>'
}
</div>
<hr style="margin-top: 20px; margin-bottom: 20px;" />
<h3>Change Rotation Settings</h3>
`;
// --- Pre-fill & Configure Form ---
// Domain: Use fetched domain if available, otherwise suggest a domain
domainInput.value = status.Domain || secretName.replace(/_/g, '.');
// ADDED: Disable the domain input if rotation is already enabled (unchangeable)
domainInput.disabled = true; // Always disable domain input to prevent changes after initial setup
// Renew Before: Use calculated days or default
renewBeforeInput.value = renewBeforeDaysForInput;
// Toggle: Set based on fetched status
rotationToggle.checked = isEnabled;
// Initial visibility of renew-before group
document.getElementById('renew-before-group').style.display = rotationToggle.checked ? 'block' : 'none';
// Re-show form
document.getElementById('rotation-settings-form').style.display = 'block';
// Update submit button action to call the correct enable/disable function
submitBtn.onclick = () => {
if (rotationToggle.checked) {
// --- ENABLE LOGIC ---
// Domain validation (simple check)
if (!domainInput.value.trim()) {
alert("Please enter the associated Domain Name.");
domainInput.focus();
return;
}
const inputDays = parseInt(renewBeforeInput.value, 10);
// Validation: Check if renewBefore is within the 1 to 80 day range
if (isNaN(inputDays) || inputDays < 1 || inputDays > 80) {
alert("Please enter a 'Renew Before' value between 1 and 80 days.");
renewBeforeInput.focus();
return;
}
// Convert input days back to Go duration string (e.g., 30 days -> 720h)
const hours = inputDays * 24;
const renewBeforeDuration = `${hours}h`;
// Call the API function
enableCertificateRotation(secretName, domainInput.value, renewBeforeDuration);
} else {
// --- DISABLE LOGIC ---
disableCertificateRotation(secretName ,domainInput.value,);
}
};
// Ensure manual renew button uses the secretName from the closure
manualRenewBtn.onclick = () => manualRenewCertificate(secretName);
} catch (error) {
statusContainer.innerHTML = `
<p class="error" style="text-align: center;">🚨 Could not load rotation status. Error: ${error.message}</p>
`;
// Hide form on failure
document.getElementById('rotation-settings-form').style.display = 'none';
console.error("Rotation Status Load Error:", error);
}
}
/**
* Hides the rotation settings modal.
*/
export function hideRotationSettingsModal() {
const modal = document.getElementById('rotationSettingsModal');
if (modal) {
modal.style.display = 'none';
// Clear status and hide form when closing
const statusContainer = document.getElementById('rotation-current-status-container');
if (statusContainer) statusContainer.innerHTML = '';
document.getElementById('rotation-settings-form').style.display = 'none';
}
}
// Expose the new functions globally for inline HTML onclick handlers
window.showRotationSettingsModal = showRotationSettingsModal;
window.hideRotationSettingsModal = hideRotationSettingsModal;
/**
* Calls the backend API to trigger an immediate manual certificate renewal.
* NOTE: Updated to accept secretName as an argument or fallback to the element text.
* @param {string} [secretNameArg] - The name of the secret/certificate to renew (optional).
*/
export async function manualRenewCertificate(secretNameArg) {
// Prefer argument, fallback to element text if available
const secretName = secretNameArg || document.getElementById("rotation-secret-name").textContent;
if (!secretName) {
alert("Error: Could not determine the secret name for manual renewal.");
return;
}
const url = `${API_BASE_URL}/manual-renew-certificate`;
const payload = {
secret_name: secretName
};
if (!confirm(`⚠️ WARNING: Are you sure you want to manually renew certificate for secret: ${secretName}? This will attempt to refresh the certificate immediately.`)) {
return;
}
// Hide the modal while processing
hideRotationSettingsModal();
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorText = await response.text();
let errorMessage = `HTTP Error ${response.status}: ${response.statusText}`;
try {
errorMessage = JSON.parse(errorText).error || JSON.parse(errorText).message || errorText;
} catch {
errorMessage = errorText || errorMessage;
}
throw new Error(errorMessage);
}
console.log(`Manual renewal successfully triggered for secret: ${secretName}`);
alert(`✅ Manual certificate renewal triggered for ${secretName}. Refreshing list to check status...`);
// Refresh the secrets list to show the new status
// NOTE: listSecrets is imported from global.js now
listSecrets();
} catch (error) {
console.error(`Failed to trigger manual renewal for '${secretName}':`, error);
alert(`🚨 Failed to trigger manual renewal for ${secretName}. Error: ${error.message}`);
}
}
// =========================================================================
// NEW: Rotation List Logic (Updated for full details)
// =========================================================================
/**
* Calls the backend API to get a list of all secrets with rotation enabled,
* and returns them as a Map of secret names to rotation details for quick lookup.
* @returns {Promise<Map<string, object>>} A Map containing the rotation details for enabled secrets.
*/
async function getRotatingCertificatesMap() {
const url = `${API_BASE_URL}/list-rotating-certificates`;
try {
const response = await fetch(url, { method: 'GET' });
if (!response.ok) {
console.warn(`Failed to fetch rotating certificate list with status ${response.status}. Assuming none are rotating.`);
return new Map();
}
const rotationList = await response.json();
if (!Array.isArray(rotationList)) {
console.error("API returned an unexpected format for rotation list:", rotationList);
return new Map();
}
// Create a Map of secret_name -> { domain, renew_before, ... } for O(1) lookups
return rotationList.reduce((map, cert) => {
if (cert.rotation_enabled && cert.secret_name) {
map.set(cert.secret_name, cert);
}
return map;
}, new Map());
} catch (error) {
console.error("Error fetching rotating certificate list:", error);
return new Map(); // Assume no rotation enabled on failure
}
}
// =========================================================================
// SECRET CORE LOGIC (listSecrets) (Modified)
// =========================================================================
// Assuming refreshSecretsList is an alias for listSecrets or a similar function in global.js
// If refreshSecretsList is not defined in global.js, replace it with listSecrets() in all calls.
export async function listSecrets() {
const tableBody = document.getElementById('secret-table-body');
if (!tableBody) {
console.error("Could not find element with ID 'secret-table-body'.");
return;
}
tableBody.innerHTML =
'<tr><td colspan="4" style="text-align: center; padding: 20px;">Loading...</td></tr>';
try {
// Run three parallel fetches: secrets list, certificate validity checks, and rotation list
const [secretsResponse, rotationMap] = await Promise.all([
fetch(`${API_BASE_URL}/list-secrets`),
getRotatingCertificatesMap() // Fetch the Map of rotating secrets
]);
if (!secretsResponse.ok) throw new Error(secretsResponse.statusText);
const secretResponse = await secretsResponse.json();
// Combine enabled and disabled secrets for display
let allSecrets = [
...(secretResponse.enabled || []).map(s => ({ ...s, status: 'Enabled', configData: s })),
...(secretResponse.disabled || []).map(s => ({ ...s, status: 'Disabled', configData: s }))
];
if (!allSecrets.length) {
tableBody.innerHTML =
'<tr><td colspan="4" style="text-align: center; color: var(--secondary-color);">No secrets found.</td></tr>';
configStore.secrets = {};
return;
}
cleanupConfigStore();
// ----------------------------------------------------------------------
// NEW: Check validity for all TLS certificates concurrently AND set rotation status
const validityChecks = allSecrets.map(async secret => {
if (isTlsCertificate(secret)) {
// 1. Check/Set Rotation Status and Details
const rotationDetails = rotationMap.get(secret.name);
if (rotationDetails) {
secret.rotationStatus = 'Enabled';
secret.rotationDetails = rotationDetails; // Attach the full details
}
// 2. Check Validity Status
// Drill down to the PEM string
const certPem = secret.Type?.TlsCertificate?.certificate_chain?.Specifier?.InlineString;
if (certPem) {
const isValid = await checkCertificateValidity(certPem);
secret.validityStatus = isValid ? 'Valid' : 'Invalid';
} else {
secret.validityStatus = 'Invalid'; // Treat missing PEM as invalid
}
}
return secret;
});
// Wait for all checks to complete
allSecrets = await Promise.all(validityChecks);
// ----------------------------------------------------------------------
// Store full configs in memory by name
configStore.secrets = allSecrets.reduce((acc, s) => {
const existingYaml = acc[s.name]?.yaml;
// Also store the validityStatus and rotationStatus in the configStore entry for potential future use
acc[s.name] = {
...s.configData,
yaml: existingYaml,
validityStatus: s.validityStatus,
rotationStatus: s.rotationStatus,
rotationDetails: s.rotationDetails // Store rotation details
};
return acc;
}, configStore.secrets);
tableBody.innerHTML = '';
allSecrets.forEach(secret => {
const row = tableBody.insertRow();
if (secret.status === 'Disabled') row.classList.add('disabled-row');
let actionButtons = '';
// NOTE: Assuming the API supports enable/disable for secrets like clusters
if (secret.status === 'Enabled') {
actionButtons = `<button class="action-button disable" onclick="window.disableSecret('${secret.name}', event)">Disable</button>`;
} else {
actionButtons = `
<button class="action-button enable" onclick="window.enableSecret('${secret.name}', event)">Enable</button>
<button class="action-button remove" onclick="window.removeSecret('${secret.name}', event)">Remove</button>
`;
}
// Add 'View Details' and 'Rotation Setting' buttons for TLS Certificates
if (isTlsCertificate(secret)) {
actionButtons += `
<button class="action-button view-cert" onclick="window.showCertificateDetailsModal('${secret.name}')">View Details</button>
<button class="action-button renew-cert" onclick="window.showRotationSettingsModal('${secret.name}')">Rotation Setting</button>
`;
}
// Secret Name Hyperlink (uses showSecretConfigModal, which must be imported from global.js or defined globally)
const secretNameCell = row.insertCell();
secretNameCell.innerHTML =
`<a href="#" onclick="event.preventDefault(); window.showSecretConfigModal('${secret.name}')"><span class="secret-name">${secret.name}</span></a>`;
row.insertCell().textContent = secret.status;
// The validity label and NEW rotation label are included in the result of getSecretTypeDetails
row.insertCell().innerHTML = getSecretTypeDetails(secret);
row.insertCell().innerHTML = actionButtons;
});
} catch (error) {
tableBody.innerHTML = `<tr><td colspan="4" class="error" style="text-align: center;">🚨 Secret Error: ${error.message}</td></tr>`;
console.error("Secret Fetch/Parse Error:", error);
}
}
// =========================================================================
// SECRET ENABLE/DISABLE/REMOVE LOGIC (toggleSecretStatus) (Unchanged)
// =========================================================================
async function toggleSecretStatus(secretName, action) {
// API endpoints are assumed to be /remove-secret, /enable-secret, /disable-secret
let url = (action === 'remove') ? `${API_BASE_URL}/remove-secret` : `${API_BASE_URL}/${action}-secret`;
const payload = { name: secretName };
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`HTTP Error ${response.status}: ${errorBody}`);
}
console.log(`Secret '${secretName}' successfully ${action}d.`);
cleanupConfigStore();
listSecrets();
} catch (error) {
console.error(`Failed to ${action} secret '${secretName}':`, error);
alert(`Failed to ${action} secret '${secretName}'. Check console for details.`);
}
}
// Expose these functions globally for inline HTML onclick handlers
export function disableSecret(secretName, event) {
event.stopPropagation();
if (confirm(`Are you sure you want to DISABLE secret: ${secretName}?`)) {
toggleSecretStatus(secretName, 'disable');
}
}
export function enableSecret(secretName, event) {
event.stopPropagation();
if (confirm(`Are you sure you want to ENABLE secret: ${secretName}?`)) {
toggleSecretStatus(secretName, 'enable');
}
}
export function removeSecret(secretName, event) {
event.stopPropagation();
if (confirm(`⚠️ WARNING: Are you absolutely sure you want to PERMANENTLY REMOVE secret: ${secretName}? This action cannot be undone.`)) {
toggleSecretStatus(secretName, 'remove');
}
}
// =========================================================================
// ADD SECRET LOGIC (showAddSecretModal, hideAddSecretModal, submitNewSecret) (Unchanged)
// =========================================================================
/**
* Shows the modal for adding a new secret.
* MODIFIED: Clears upsert checkbox on show.
*/
export function showAddSecretModal() {
document.getElementById('add-secret-yaml-input').value = '';
const upsertCheckbox = document.getElementById('add-secret-upsert-flag');
if (upsertCheckbox) {
upsertCheckbox.checked = false;
}
document.getElementById('addSecretModal').style.display = 'block';
}
/**
* Hides the modal for adding a new secret.
* MODIFIED: Clears upsert checkbox on hide.
*/
export function hideAddSecretModal() {
const modal = document.getElementById('addSecretModal');
if (modal) {
modal.style.display = 'none';
document.getElementById('add-secret-yaml-input').value = '';
const upsertCheckbox = document.getElementById('add-secret-upsert-flag');
if (upsertCheckbox) {
upsertCheckbox.checked = false;
}
}
}
/**
* Submits the new secret YAML to the /add-secret endpoint.
* MODIFIED: Now checks for an 'allow-upsert' checkbox and adds 'upsert: true' to the payload.
*/
export async function submitNewSecret() {
const yamlInput = document.getElementById('add-secret-yaml-input');
// Get the checkbox element and its state
const upsertCheckbox = document.getElementById('add-secret-upsert-flag');
const secretYaml = yamlInput.value.trim();
if (!secretYaml) {
alert('Please paste the secret YAML configuration.');
return;
}
try {
// The /add-secret endpoint expects a JSON body with a 'YAML' key containing the stringified YAML.
const payload = { YAML: secretYaml };
// Add upsert flag to payload if checkbox is checked
if (upsertCheckbox && upsertCheckbox.checked) {
// Note: Assuming your API expects the 'upsert' flag as a separate top-level key like the YAML key.
// If the API expects { yaml: ..., upsert: true } use: payload.upsert = true;
// Based on your Listener/Cluster pattern, it's safer to use a top-level `upsert` key if the API supports it.
// Assuming payload: { YAML: "...", upsert: true }
payload.upsert = true;
}
const url = `${API_BASE_URL}/add-secret`;
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`HTTP Error ${response.status}: ${errorBody}`);
}
console.log(`New secret successfully added.`);
alert('Secret successfully added! The dashboard will now refresh.');
yamlInput.value = '';
// Uncheck the box upon success/closing
if (upsertCheckbox) {
upsertCheckbox.checked = false;
}
hideAddSecretModal();
cleanupConfigStore();
listSecrets();
} catch (error) {
console.error(`Failed to add new secret:`, error);
alert(`Failed to add new secret. Check console for details. Error: ${error.message}`);
}
}