Newer
Older
EnvoyControlPlane / static / script.js
// Step 1: Initialize the variable with the current full URL
let API_BASE_URL = window.location.href;

// Step 2: Trim the last slash if it exists and reassign to the variable
API_BASE_URL = API_BASE_URL.replace(/\/$/, "");

// =========================================================================
// GLOBAL IN-MEMORY STORE
// =========================================================================
// This object will hold the full configuration data in JavaScript memory.
// It stores JSON configs and caches the raw YAML string under the 'yaml' key.
const configStore = {
    clusters: {},
    listeners: {}
    // listener objects will now have a 'filterChains' array to store domain configs
};

// =========================================================================
// MODAL HANDLERS
// =========================================================================

/**
 * Displays the modal with JSON and YAML content.
 * @param {string} title - The modal title.
 * @param {object} jsonData - The configuration data object (used for JSON tab).
 * @param {string} yamlData - The configuration data as a YAML string.
 */
function showModal(title, jsonData, yamlData) {
    document.getElementById('modal-title').textContent = title;
    
    // Populate JSON content
    document.getElementById('modal-json-content').textContent =
        JSON.stringify(jsonData, null, 2);

    // Populate YAML content
    document.getElementById('modal-yaml-content').textContent = yamlData;
    
    // Default to YAML tab
    const modalContent = document.getElementById('configModal')?.querySelector('.modal-content');
    if (modalContent) {
        switchTab(modalContent, 'yaml'); 
    }

    document.getElementById('configModal').style.display = 'block';
}

function hideModal() {
    document.getElementById('configModal').style.display = 'none';
}

window.addEventListener('keydown', (event) => {
    if (event.key === 'Escape') hideModal();
});

// Close modal when clicking outside of the content (on the backdrop)
window.addEventListener('click', (event) => {
    const modal = document.getElementById('configModal');
    if (event.target === modal) {
        hideModal();
    }
});


// Helper function that MUST be in your HTML/JS setup for the tabs to work
function switchTab(modalContent, tabName) {
    // Deactivate all buttons and hide all content
    modalContent.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
    modalContent.querySelectorAll('.code-block').forEach(content => content.style.display = 'none');

    // Activate the selected button and show the corresponding content
    const activeBtn = modalContent.querySelector(`.tab-button[data-tab="${tabName}"]`);
    const activeContent = document.getElementById(`modal-${tabName}-content`);

    if (activeBtn) activeBtn.classList.add('active');
    if (activeContent) activeContent.style.display = 'block';
}

function setupModalTabs() {
    const modalContent = document.getElementById('configModal')?.querySelector('.modal-content');
    if (!modalContent) return;

    modalContent.querySelectorAll('.tab-button').forEach(button => {
        button.addEventListener('click', (event) => {
            const tabName = event.target.getAttribute('data-tab');
            switchTab(modalContent, tabName);
        });
    });
}

// =========================================================================
// CONFIG-SPECIFIC MODAL LAUNCHERS (UPDATED)
// =========================================================================

/**
 * Handles showing the configuration for an individual FilterChain/Domain.
 * This function now loads the JSON from memory and fetches YAML via API.
 */
async function showDomainConfig(element) {
    const title = element.getAttribute('data-title');
    const listenerName = element.getAttribute('data-listener-name');
    // We now use domainName for the API call
    const domainName = element.getAttribute('data-domain-name'); 
    // We now use index to retrieve the JSON from memory
    const chainIndex = element.getAttribute('data-chain-index'); 

    if (!listenerName || domainName === null || chainIndex === null) {
        console.error("Missing required data attributes for domain config.");
        return;
    }
    
    // 1. Get JSON data from memory
    const listener = configStore.listeners[listenerName];
    const jsonData = listener?.filterChains?.[parseInt(chainIndex)];

    if (!jsonData) {
        const errorMsg = 'Filter Chain configuration not found in memory.';
        console.error(errorMsg);
        showModal(`🚨 Error: ${title}`, { error: errorMsg }, errorMsg);
        return;
    }

    let yamlData = 'Loading YAML from API...';
    
    try {
        // 2. CALL NEW API FOR DOMAIN YAML using the domain name
        // MODIFICATION: Use /get-listener API instead of the non-existent /get-domain
        const response = await fetch(`${API_BASE_URL}/get-listener?name=${encodeURIComponent(listenerName)}&format=yaml`);
        if (!response.ok) {
            yamlData = `Error fetching Listener YAML (fallback): ${response.status} ${response.statusText}`;
        } else {
            // Include a note that this is the full listener config
            yamlData = await response.text();
            yamlData = `---\n# Full Listener Config (Fallback for Domain)\n# This is not the specific filter chain config.\n---\n\n` + yamlData;
        }
    } catch (error) {
        console.error("Failed to fetch YAML listener config:", error);
        yamlData = `Network Error fetching YAML (Fallback): ${error.message}`;
    }
    
    // 3. Fallback if API call failed
    if (yamlData.includes('Error fetching') || yamlData.includes('Network Error')) {
         const yamlApproximation = JSON.stringify(jsonData, null, 2)
            .replace(/[{}]/g, '')
            .replace(/"(\w+)":\s*/g, '$1: ')
            .replace(/,\n\s*/g, '\n')
            .replace(/\[\n\s*(\s*)/g, '\n$1  - ')
            .replace(/,\n\s*(\s*)/g, '\n$1- ')
            .replace(/:\s*"/g, ': ')
            .replace(/"/g, '');
            
        yamlData = yamlApproximation + `\n\n--- WARNING: YAML is an approximation because the /get-listener API call failed. ---\n\n${yamlData}`;
        
        // Ensure the JSON tab is active by default when the YAML is a failed approximation
        const modalContent = document.getElementById('configModal')?.querySelector('.modal-content');
        if (modalContent) {
            switchTab(modalContent, 'json'); 
        }
    }
    yamlData = extractFilterChainByDomain(yamlData, domainName) || yamlData;
    showModal(title, jsonData, yamlData);
}


/**
 * Extracts the YAML section for a specific domain from the filterChains array.
 * @param {string} yamlData - The full YAML string containing the listener configuration.
 * @param {string} domainName - The domain name to search for (e.g., 'docker.jerxie.com').
 * @returns {string | null} The YAML string for the matching filterChain, or null if not found.
 */
function extractFilterChainByDomain(yamlData, domainName) {
    // 1. Check for YAML library availability
    if (typeof require === 'undefined' && typeof jsyaml === 'undefined') {
        console.error("Error: YAML parser (e.g., js-yaml) is required but not found.");
        return null;
    }

    let fullConfig;
    try {
        const yaml = (typeof require !== 'undefined') ? require('js-yaml') : jsyaml;
        
        // Use yaml.loadAll() instead of yaml.load() to handle multi-document streams (separated by ---).
        const allDocs = yaml.loadAll(yamlData);
        
        // Assuming the main configuration object is the first or only document in the stream
        fullConfig = allDocs.find(doc => doc && typeof doc === 'object');
        
    } catch (e) {
        console.error("Error parsing YAML data:", e);
        return null;
    }

    if (!fullConfig || !Array.isArray(fullConfig.filterChains)) {
        console.warn("Input YAML does not contain a 'filterChains' array, or the main document was not found.");
        return null;
    }

    // 2. Find the matching filter chain object
    const matchingChain = fullConfig.filterChains.find(chain => {
        const serverNames = chain.filterChainMatch?.serverNames;
        // Check if serverNames exists and includes the domainName
        return serverNames && serverNames.includes(domainName);
    });

    if (!matchingChain) {
        console.log(`No filterChain found for domain: ${domainName}`);
        return null;
    }

    // 3. Serialize the found object back to a YAML string
    try {
        const yaml = (typeof require !== 'undefined') ? require('js-yaml') : jsyaml;
        
        // FIX: Dump the matchingChain object directly, not as an array element.
        // This will make 'filterChainMatch' the root key of the resulting YAML string.
        const outputYaml = yaml.dump(matchingChain, {
            // Optional: Customize indentation and flow style for cleaner output
            indent: 2, 
            lineWidth: -1, // Do not wrap lines
            flowLevel: -1  // Use block style for collections
        });

        // No need to remove the initial '- ' anymore as it won't be there.
        return outputYaml.trim();

    } catch (e) {
        console.error("Error dumping YAML data:", e);
        return null;
    }
}


async function showClusterConfigModal(clusterName) {
    const config = configStore.clusters[clusterName];
    if (!config) {
        showModal(`🚨 Error: Cluster Not Found`, { name: clusterName, error: 'Configuration data missing from memory.' }, 'Error: Cluster not in memory.');
        return;
    }

    let yamlData = configStore.clusters[clusterName]?.yaml || 'Loading YAML...';
    
    if (yamlData === 'Loading YAML...') {
        try {
            // CALL API FOR YAML
            const response = await fetch(`${API_BASE_URL}/get-cluster?name=${clusterName}&format=yaml`);
            if (!response.ok) {
                yamlData = `Error fetching YAML: ${response.status} ${response.statusText}`;
            } else {
                yamlData = await response.text();
                configStore.clusters[clusterName].yaml = yamlData; // Store YAML
            }
        } catch (error) {
            console.error("Failed to fetch YAML cluster config:", error);
            yamlData = `Network Error fetching YAML: ${error.message}`;
        }
    }

    // Pass JSON object from memory and authoritative YAML from API/memory
    showModal(`Full Config for Cluster: ${clusterName}`, config, yamlData);
}

async function showListenerConfigModal(listenerName) {
    const config = configStore.listeners[listenerName];
    if (!config) {
        showModal(`🚨 Error: Listener Not Found`, { name: listenerName, error: 'Configuration data missing from memory.' }, 'Error: Listener not in memory.');
        return;
    }

    let yamlData = configStore.listeners[listenerName]?.yaml || 'Loading YAML...';
    
    if (yamlData === 'Loading YAML...') {
        try {
            // CALL API FOR YAML
            const response = await fetch(`${API_BASE_URL}/get-listener?name=${listenerName}&format=yaml`);
            if (!response.ok) {
                yamlData = `Error fetching YAML: ${response.status} ${response.statusText}`;
            } else {
                yamlData = await response.text();
                configStore.listeners[listenerName].yaml = yamlData; // Store YAML
            }
        } catch (error) {
            console.error("Failed to fetch YAML listener config:", error);
            yamlData = `Network Error fetching YAML: ${error.message}`;
        }
    }

    // Pass JSON object from memory and authoritative YAML from API/memory
    showModal(`Full Config for Listener: ${listenerName}`, config, yamlData);
}

// =========================================================================
// CLUSTER ENABLE/DISABLE/REMOVE LOGIC
// =========================================================================
async function toggleClusterStatus(clusterName, action) {
    let url = '';
    // Determine the API endpoint based on the action
    if (action === 'remove') {
        url = `${API_BASE_URL}/remove-cluster`;
    } else {
        // 'enable-cluster' or 'disable-cluster'
        url = `${API_BASE_URL}/${action}-cluster`; 
    }
    
    const payload = { name: clusterName };

    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(`Cluster '${clusterName}' successfully ${action}d.`);
        listClusters();
    } catch (error) {
        console.error(`Failed to ${action} cluster '${clusterName}':`, error);
        alert(`Failed to ${action} cluster '${clusterName}'. Check console for details.`);
    }
}

function disableCluster(clusterName, event) {
    event.stopPropagation();
    if (confirm(`Are you sure you want to DISABLE cluster: ${clusterName}?`)) {
        toggleClusterStatus(clusterName, 'disable');
    }
}

function enableCluster(clusterName, event) {
    event.stopPropagation();
    if (confirm(`Are you sure you want to ENABLE cluster: ${clusterName}?`)) {
        toggleClusterStatus(clusterName, 'enable');
    }
}

// Function to handle cluster removal
function removeCluster(clusterName, event) {
    event.stopPropagation();
    if (confirm(`⚠️ WARNING: Are you absolutely sure you want to PERMANENTLY REMOVE cluster: ${clusterName}? This action cannot be undone.`)) {
        toggleClusterStatus(clusterName, 'remove');
    }
}

// =========================================================================
// LISTENER ENABLE/DISABLE/REMOVE LOGIC
// =========================================================================
async function toggleListenerStatus(listenerName, action) {
    let url = '';
    // Determine the API endpoint based on the action
    if (action === 'remove') {
        url = `${API_BASE_URL}/remove-listener`;
    } else {
        // 'enable-listener' or 'disable-listener'
        url = `${API_BASE_URL}/${action}-listener`;
    }
    
    const payload = { name: listenerName };

    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(`Listener '${listenerName}' successfully ${action}d.`);
        listListeners();
    } catch (error) {
        console.error(`Failed to ${action} listener '${listenerName}':`, error);
        alert(`Failed to ${action} listener '${listenerName}'. Check console for details.`);
    }
}

function disableListener(listenerName, event) {
    event.stopPropagation();
    if (confirm(`Are you sure you want to DISABLE listener: ${listenerName}?`)) {
        toggleListenerStatus(listenerName, 'disable');
    }
}

function enableListener(listenerName, event) {
    event.stopPropagation();
    if (confirm(`Are you sure you want to ENABLE listener: ${listenerName}?`)) {
        toggleListenerStatus(listenerName, 'enable');
    }
}

// Function to handle listener removal
function removeListener(listenerName, event) {
    event.stopPropagation();
    if (confirm(`⚠️ WARNING: Are you absolutely sure you want to PERMANENTLY REMOVE listener: ${listenerName}? This action cannot be undone.`)) {
        toggleListenerStatus(listenerName, 'remove');
    }
}

// =========================================================================
// CLUSTER LOGIC
// =========================================================================
function getClusterEndpointDetails(cluster) {
    try {
        const endpoints = cluster.load_assignment?.endpoints;
        if (!endpoints?.length) return '<span style="color: gray;">(No Endpoints)</span>';
        const lbEndpoints = endpoints[0].lb_endpoints;
        if (!lbEndpoints?.length) return '<span style="color: gray;">(No LB Endpoints)</span>';

        const endpointObj = lbEndpoints[0].HostIdentifier.Endpoint;
        const address = endpointObj.address.Address.SocketAddress.address;
        const port = endpointObj.address.Address.SocketAddress.PortSpecifier.PortValue;

        const tls = cluster.transport_socket ? '<span class="tls-badge">TLS/SSL</span>' : '';
        return `${address}:${port} ${tls}`;
    } catch {
        return '<span style="color: gray;">(Config Error)</span>';
    }
}

async function listClusters() {
    const tableBody = document.getElementById('cluster-table-body');
    if (!tableBody) {
        console.error("Could not find element with ID 'cluster-table-body'.");
        return; 
    }
    
    tableBody.innerHTML =
        '<tr><td colspan="5" style="text-align: center; padding: 20px;">Loading...</td></tr>';

    try {
        const response = await fetch(`${API_BASE_URL}/list-clusters`);
        if (!response.ok) throw new Error(response.statusText);

        const clusterResponse = await response.json();

        const allClusters = [
            ...(clusterResponse.enabled || []).map(c => ({ ...c, status: 'Enabled', configData: c })),
            ...(clusterResponse.disabled || []).map(c => ({ ...c, status: 'Disabled', configData: c }))
        ];

        if (!allClusters.length) {
            tableBody.innerHTML =
                '<tr><td colspan="5" style="text-align: center; color: var(--secondary-color);">No clusters found.</td></tr>';
            // Clear store if no clusters are found
            configStore.clusters = {}; 
            return;
        }

        // Store full configs in memory by name
        configStore.clusters = allClusters.reduce((acc, c) => {
            // Keep existing YAML if it exists, otherwise initialize
            const existingYaml = acc[c.name]?.yaml; 
            acc[c.name] = { ...c.configData, yaml: existingYaml };
            return acc;
        }, configStore.clusters);


        tableBody.innerHTML = '';
        allClusters.forEach(cluster => {
            const row = tableBody.insertRow();
            if (cluster.status === 'Disabled') row.classList.add('disabled-row');

            let actionButtons = '';
            if (cluster.status === 'Enabled') {
                actionButtons = `<button class="action-button disable" onclick="disableCluster('${cluster.name}', event)">Disable</button>`;
            } else {
                // When disabled, show Enable and Remove buttons
                actionButtons = `
                    <button class="action-button enable" onclick="enableCluster('${cluster.name}', event)">Enable</button>
                    <button class="action-button remove" onclick="removeCluster('${cluster.name}', event)">Remove</button>
                `;
            }

            // Cluster Name Hyperlink (Updated to use new modal function)
            const clusterNameCell = row.insertCell();
            clusterNameCell.innerHTML = 
                `<a href="#" onclick="event.preventDefault(); showClusterConfigModal('${cluster.name}')"><span class="cluster-name">${cluster.name}</span></a>`;
            
            row.insertCell().textContent = cluster.status;
            row.insertCell().innerHTML = getClusterEndpointDetails(cluster);
            row.insertCell().textContent =
                `${cluster.connect_timeout?.seconds || 0}.${(cluster.connect_timeout?.nanos / 1e6 || 0).toFixed(0).padStart(3, '0')}s`;
            row.insertCell().innerHTML = actionButtons;
        });
    } catch (error) {
        tableBody.innerHTML = `<tr><td colspan="5" class="error" style="text-align: center;">🚨 Cluster Error: ${error.message}</td></tr>`;
        console.error("Cluster Fetch/Parse Error:", error);
    }
}

// =========================================================================
// LISTENER LOGIC
// =========================================================================

// This helper is no longer needed as the server will handle the lookup by domain name
// function createFilterChainId(chain, index) { ... } 


function getDomainRouteTable(filterChains, listenerName) {
    if (!filterChains || filterChains.length === 0) return 'N/A';
    
    // Store filter chains in memory for robust retrieval in showDomainConfig
    configStore.listeners[listenerName].filterChains = filterChains;

    const domainConfigs = filterChains.map((chain, index) => {
        const domains = chain.filter_chain_match?.server_names || ["(default)"];
        const filters = chain.filters?.map(f => f.name.replace(/^envoy\.filters\./, '')) || [];
        const routeType = filters.some(f => f.includes('http_connection_manager')) ? 'HTTP' : 'TCP';
        
        // Use the first domain name for the API call, or '(default)'
        const primaryDomainName = domains[0]; 

        const allDomainsTitle = domains.join(', ');
        const modalTitle = `Filter Chain for Domains: ${allDomainsTitle} (${listenerName})`;

        return `
            <div class="domain-config-item">
                <div class="domain-header">
                    <div class="domain-name-link"
                        data-title="${modalTitle}" 
                        data-listener-name="${listenerName}"
                        data-chain-index="${index}" data-domain-name="${primaryDomainName}" onclick="showDomainConfig(this)">
                        ${allDomainsTitle}
                    </div>
                    <div class="filter-list">
                        ${filters.map(f => `<span class="filter-badge">${f}</span>`).join('')}
                    </div>
                </div>
                <div class="route-type-display">${routeType} Route</div>
            </div>
        `;
    });

    return `<div class="domain-config-list">${domainConfigs.join('')}</div>`;
}

function getListenerRowData(listener) {
    const socketAddr = listener.address?.Address?.SocketAddress;
    const address = socketAddr?.address || 'N/A';
    const port = socketAddr?.PortSpecifier?.PortValue || 'N/A';
    const addressString = `${address}:${port}`;
    const isTlsInspector = listener.listener_filters?.some(f => f.name.includes('tls_inspector'));
    const isTlsInChain = listener.filter_chains?.some(c => c.transport_socket);
    const tlsIndicator = (isTlsInspector || isTlsInChain)
        ? '<span class="tls-badge">TLS</span>'
        : '';

    return {
        name: listener.name,
        address: `${addressString} ${tlsIndicator}`,
        domains: getDomainRouteTable(listener.filter_chains, listener.name),
        status: listener.status || 'Enabled',
        rawData: listener
    };
}

async function listListeners() {
    const tableBody = document.getElementById('listener-table-body');
    if (!tableBody) {
        console.error("Could not find element with ID 'listener-table-body'.");
        return; 
    }
    
    tableBody.innerHTML =
        '<tr><td colspan="5" style="text-align: center; padding: 20px;">Loading...</td></tr>';

    try {
        const response = await fetch(`${API_BASE_URL}/list-listeners`);
        if (!response.ok) throw new Error(response.statusText);

        const listenerResponse = await response.json();
        const allListeners = [
            ...(listenerResponse.enabled || []).map(l => ({ ...l, status: 'Enabled', configData: l })),
            ...(listenerResponse.disabled || []).map(l => ({ ...l, status: 'Disabled', configData: l }))
        ];

        if (!allListeners.length) {
            tableBody.innerHTML =
                '<tr><td colspan="5" style="text-align: center; color: var(--secondary-color);">No listeners found.</td></tr>';
            // Clear store if no listeners are found
            configStore.listeners = {};
            return;
        }

        // Store full configs in memory by name
        configStore.listeners = allListeners.reduce((acc, l) => {
            // Preserve existing YAML, and initialize filterChains
            const existing = acc[l.name] || {};
            acc[l.name] = { 
                ...l.configData, 
                yaml: existing.yaml,
                filterChains: existing.filterChains // Reset or keep old list temporarily
            };
            return acc;
        }, configStore.listeners); 

        tableBody.innerHTML = '';
        allListeners.forEach(listener => {
            // Call getDomainRouteTable, which now stores filterChains in memory
            const rowData = getListenerRowData(listener); 
            
            const row = tableBody.insertRow();
            if (rowData.status === 'Disabled') row.classList.add('disabled-row');

            let actionButtons = '';
            if (rowData.status === 'Enabled') {
                actionButtons = `<button class="action-button disable" onclick="disableListener('${listener.name}', event)">Disable</button>`;
            } else {
                // When disabled, show Enable and Remove buttons
                actionButtons = `
                    <button class="action-button enable" onclick="enableListener('${listener.name}', event)">Enable</button>
                    <button class="action-button remove" onclick="removeListener('${listener.name}', event)">Remove</button>
                `;
            }

            // Listener Name Hyperlink (Updated to use new modal function)
            const listenerNameCell = row.insertCell();
            listenerNameCell.innerHTML = 
                `<a href="#" onclick="event.preventDefault(); showListenerConfigModal('${listener.name}')"><span class="listener-name">${rowData.name}</span></a>`;
                
            row.insertCell().textContent = rowData.status;
            row.insertCell().innerHTML = rowData.address;
            row.insertCell().innerHTML = rowData.domains;
            row.insertCell().innerHTML = actionButtons;
        });
    } catch (error) {
        tableBody.innerHTML = `<tr><td colspan="5" class="error" style="text-align: center;">🚨 Listener Error: ${error.message}</td></tr>`;
        console.error("Listener Fetch/Parse Error:", error);
    }
}

// =========================================================================
// CONSISTENCY LOGIC
// =========================================================================

const CONSISTENCY_POLL_INTERVAL = 5000; // 5 seconds
let inconsistencyData = null; // Store the last fetched inconsistency data

function showConsistencyModal() {
    if (!inconsistencyData || inconsistencyData.inconsistent === false) return;

    // Populate modal content
    const cacheOnly = inconsistencyData['cache-only'] || {};
    const dbOnly = inconsistencyData['db-only'] || {};
    
    document.getElementById('cache-only-count').textContent = 
        Object.keys(cacheOnly).length;
    document.getElementById('cache-only-data').textContent = 
        JSON.stringify(cacheOnly, null, 2);

    document.getElementById('db-only-count').textContent = 
        Object.keys(dbOnly).length;
    document.getElementById('db-only-data').textContent = 
        JSON.stringify(dbOnly, null, 2);
    
    document.getElementById('consistencyModal').style.display = 'block';
}

function hideConsistencyModal() {
    document.getElementById('consistencyModal').style.display = 'none';
}

async function checkConsistency() {
    const button = document.getElementById('consistency-button');
    if (!button) return;

    // Capture current state before fetch
    const hasPreviousConflict = inconsistencyData !== null;
    const isCurrentlyError = button.classList.contains('error');

    // Temporarily update the status unless it's currently an error (best to move this after fetch, or ensure it's always reset)
    // For now, keep it simple and ensure it is reset at the end.
    button.textContent = 'Checking...';
    button.classList.add('loading');
    button.classList.remove('consistent', 'inconsistent', 'error');

    try {
        const response = await fetch(`${API_BASE_URL}/is-consistent`);
        if (!response.ok) throw new Error(response.statusText);

        const data = await response.json();
        const consistencyStatus = data.consistent;
        const isNewConflict = consistencyStatus.inconsistent === true;

        // Determine if the *inconsistent* state has changed
        const stateChanged = isNewConflict !== hasPreviousConflict;

        // Set the final button text and remove the loading indicator
        button.classList.remove('loading');

        if (isNewConflict) {
            button.textContent = '🚨 CONFLICT';
            if (stateChanged || isCurrentlyError) {
                // Update DOM only if state flipped OR if recovering from error
                button.classList.remove('consistent', 'error');
                button.classList.add('inconsistent');
                button.disabled = false;
                inconsistencyData = consistencyStatus;
            }
        } else {
            button.textContent = '✅ Consistent';
            if (stateChanged || isCurrentlyError) {
                // Update DOM only if state flipped OR if recovering from error
                button.classList.remove('inconsistent', 'error');
                button.classList.add('consistent');
                button.disabled = true;
                inconsistencyData = null;
                hideConsistencyModal();
            }
        }

    } catch (error) {
        button.classList.remove('loading', 'consistent', 'inconsistent');
        button.classList.add('error');
        button.textContent = '❌ Error';
        button.disabled = true;
        inconsistencyData = null;
        console.error("Consistency check failed:", error);
    }
}

/**
 * Core function to resolve consistency by making a POST call to a sync endpoint.
 * @param {string} action - 'flush' (Cache -> DB) or 'rollback' (DB -> Cache).
 */
async function resolveConsistency(action) {
    let url = '';
    let message = '';

    if (action === 'flush') {
        url = `${API_BASE_URL}/flush-to-db`;
        message = 'Flushing cache to DB...';
    } else if (action === 'rollback') {
        url = `${API_BASE_URL}/load-from-db`;
        message = 'Rolling back cache from DB...';
    } else {
        return;
    }

    if (!confirm(`Are you sure you want to perform the action: ${action.toUpperCase()}? This will overwrite the target configuration.`)) {
        return;
    }

    // Attempt to hide the modal if it's open (it's safe if it's closed)
    const modal = document.getElementById('consistencyModal');
    if (modal) modal.style.display = 'none'; 

    const button = document.getElementById('consistency-button');
    // Temporarily update the status indicator during sync
    if (button) {
        button.textContent = message;
        button.classList.remove('consistent', 'inconsistent', 'error');
        button.classList.add('loading');
        button.disabled = true;
    }


    try {
        const response = await fetch(url, { method: 'POST' });

        if (!response.ok) {
            const errorBody = await response.text();
            throw new Error(`HTTP Error ${response.status}: ${errorBody}`);
        }

        alert(`Sync successful via ${action.toUpperCase()}. Reloading data.`);
        loadAllData();
        checkConsistency(); // Rerun check immediately to update status
    } catch (error) {
        alert(`Failed to sync via ${action}. Check console for details.`);
        console.error(`Sync operation (${action}) failed:`, error);
        checkConsistency(); // Restore status indicator
    }
}


function downloadYaml() {
    const yamlContent = document.getElementById('modal-yaml-content').textContent;
    if (!yamlContent || yamlContent.trim() === '') {
        alert("No YAML content available to download.");
        return;
    }

    // Use modal title as filename fallback
    const title = document.getElementById('modal-title').textContent
        .replace(/\s+/g, '_')
        .replace(/[^\w\-]/g, '');
    
    const blob = new Blob([yamlContent], { type: 'text/yaml' });
    const url = URL.createObjectURL(blob);

    const a = document.createElement('a');
    a.href = url;
    a.download = title ? `${title}.yaml` : 'config.yaml';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
}


// =========================================================================
// MANUAL SYNC HANDLERS
// =========================================================================

function manualFlush() {
    resolveConsistency('flush');
}

function manualRollback() {
    resolveConsistency('rollback');
}


// =========================================================================
// COMBINED LOADER & POLLING
// =========================================================================
function loadAllData() {
    listClusters();
    listListeners();
}

window.onload = () => {
    loadAllData();
    setupModalTabs(); // Setup tab logic on load
    checkConsistency(); // Initial check
    setInterval(checkConsistency, CONSISTENCY_POLL_INTERVAL); // Start polling
};