diff --git a/data/config/cds.yaml b/data/config/cds.yaml
index a18b8bd..f05264f 100755
--- a/data/config/cds.yaml
+++ b/data/config/cds.yaml
@@ -2,7 +2,7 @@
- "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster
name: _acme_renewer
connect_timeout: 0.2s
- type: STATIC_DNS
+ type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: acme_renewer
diff --git a/static/clusters.js b/static/clusters.js
deleted file mode 100755
index 071a556..0000000
--- a/static/clusters.js
+++ /dev/null
@@ -1,242 +0,0 @@
-// clusters.js
-import { API_BASE_URL, configStore, cleanupConfigStore } from './global.js';
-
-// =========================================================================
-// CLUSTER UTILITIES (Unchanged)
-// =========================================================================
-
-function getClusterEndpointDetails(cluster) {
- try {
- const endpoints = cluster.load_assignment?.endpoints;
- if (!endpoints?.length) return '(No Endpoints)';
- const lbEndpoints = endpoints[0].lb_endpoints;
- if (!lbEndpoints?.length) return '(No LB Endpoints)';
-
- // Extract address and port details
- const endpointObj = lbEndpoints[0].HostIdentifier?.Endpoint || lbEndpoints[0].endpoint;
- const address = endpointObj.address.Address.SocketAddress.address;
- const port = endpointObj.address.Address.SocketAddress.PortSpecifier.PortValue;
-
- const tls = cluster.transport_socket ? 'TLS/SSL' : '';
- return `${address}:${port} ${tls}`;
- } catch {
- return '(Config Error)';
- }
-}
-
-// =========================================================================
-// CLUSTER CORE LOGIC (Unchanged)
-// =========================================================================
-
-export 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 =
- '
| Loading... |
';
-
- 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 =
- '| No clusters found. |
';
- configStore.clusters = {};
- return;
- }
- cleanupConfigStore();
-
- // Store full configs in memory by name
- configStore.clusters = allClusters.reduce((acc, c) => {
- 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 = ``;
- } else {
- // When disabled, show Enable and Remove buttons
- actionButtons = `
-
-
- `;
- }
-
- // Cluster Name Hyperlink (uses showClusterConfigModal from global.js)
- const clusterNameCell = row.insertCell();
- clusterNameCell.innerHTML =
- `${cluster.name}`;
-
- 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 = `| 🚨 Cluster Error: ${error.message} |
`;
- console.error("Cluster Fetch/Parse Error:", error);
- }
-}
-
-
-// =========================================================================
-// CLUSTER ENABLE/DISABLE/REMOVE LOGIC (Unchanged)
-// =========================================================================
-
-async function toggleClusterStatus(clusterName, action) {
- let url = (action === 'remove') ? `${API_BASE_URL}/remove-cluster` : `${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.`);
- cleanupConfigStore();
- listClusters();
- } catch (error) {
- console.error(`Failed to ${action} cluster '${clusterName}':`, error);
- alert(`Failed to ${action} cluster '${clusterName}'. Check console for details.`);
- }
-}
-
-export function disableCluster(clusterName, event) {
- event.stopPropagation();
- if (confirm(`Are you sure you want to DISABLE cluster: ${clusterName}?`)) {
- toggleClusterStatus(clusterName, 'disable');
- }
-}
-
-export function enableCluster(clusterName, event) {
- event.stopPropagation();
- if (confirm(`Are you sure you want to ENABLE cluster: ${clusterName}?`)) {
- toggleClusterStatus(clusterName, 'enable');
- }
-}
-
-export 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');
- }
-}
-
-// =========================================================================
-// ADD CLUSTER LOGIC
-// =========================================================================
-
-/**
- * Shows the modal for adding a new cluster.
- */
-export function showAddClusterModal() {
- // document.getElementById('add-cluster-modal-title').textContent =
- // `Add New Cluster`;
- document.getElementById('add-cluster-yaml-input').value = '';
- // Clear checkbox on show
- const upsertCheckbox = document.getElementById('add-cluster-upsert-flag');
- if (upsertCheckbox) {
- upsertCheckbox.checked = false;
- }
- document.getElementById('addClusterModal').style.display = 'block';
-}
-
-/**
- * Hides the modal for adding a new cluster.
- */
-export function hideAddClusterModal() {
- const modal = document.getElementById('addClusterModal');
- if (modal) {
- modal.style.display = 'none';
- document.getElementById('add-cluster-yaml-input').value = '';
- // Clear checkbox on hide
- const upsertCheckbox = document.getElementById('add-cluster-upsert-flag');
- if (upsertCheckbox) {
- upsertCheckbox.checked = false;
- }
- }
-}
-
-
-/**
- * Submits the new cluster YAML to the /add-cluster endpoint.
- *
- * MODIFIED: Now checks for an 'allow-upsert' checkbox and adds 'upsert: true' to the payload.
- */
-export async function submitNewCluster() {
- const yamlInput = document.getElementById('add-cluster-yaml-input');
- // Get the checkbox element and its state
- const upsertCheckbox = document.getElementById('add-cluster-upsert-flag');
- const clusterYaml = yamlInput.value.trim();
-
- if (!clusterYaml) {
- alert('Please paste the cluster YAML configuration.');
- return;
- }
-
- try {
- const payload = { yaml: clusterYaml };
-
- // Add upsert flag to payload if checkbox is checked
- if (upsertCheckbox && upsertCheckbox.checked) {
- payload.upsert = true;
- }
-
- const url = `${API_BASE_URL}/add-cluster`;
-
- 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 cluster successfully added.`);
- alert('Cluster successfully added! The dashboard will now refresh.');
-
- yamlInput.value = '';
- // Uncheck the box upon success/closing
- if (upsertCheckbox) {
- upsertCheckbox.checked = false;
- }
- hideAddClusterModal();
-
- cleanupConfigStore();
- listClusters();
-
- } catch (error) {
- console.error(`Failed to add new cluster:`, error);
- alert(`Failed to add new cluster. Check console for details. Error: ${error.message}`);
- }
-}
\ No newline at end of file
diff --git a/static/consistency.js b/static/consistency.js
deleted file mode 100755
index d65aa5b..0000000
--- a/static/consistency.js
+++ /dev/null
@@ -1,66 +0,0 @@
-// consistency.js
-import { API_BASE_URL, CONSISTENCY_POLL_INTERVAL, setInconsistencyData, inconsistencyData } from './global.js';
-import { hideConsistencyModal, showConsistencyModal } from './modals.js';
-import { loadAllData } from './data_loader.js'; // Will be imported later
-import { checkConsistency } from './global.js';
-
-/**
- * Core function to resolve consistency by making a POST call to a sync endpoint.
- * @param {string} action - 'flush' (Cache -> DB) or 'rollback' (DB -> Cache).
- */
-export 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;
- }
-
- const modal = document.getElementById('consistencyModal');
- if (modal) modal.style.display = 'none';
-
- const button = document.getElementById('consistency-button');
- 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();
- } catch (error) {
- alert(`Failed to sync via ${action}. Check console for details.`);
- console.error(`Sync operation (${action}) failed:`, error);
- checkConsistency();
- }
-}
-
-
-// Exported functions must be attached to 'window' if called from inline HTML attributes
-export function manualFlush() {
- resolveConsistency('flush');
-}
-
-export function manualRollback() {
- resolveConsistency('rollback');
-}
\ No newline at end of file
diff --git a/static/css/style.css b/static/css/style.css
new file mode 100755
index 0000000..b840566
--- /dev/null
+++ b/static/css/style.css
@@ -0,0 +1,1075 @@
+:root {
+ --primary-color: #0d6efd;
+ --secondary-color: #6c757d;
+ --info-color: #0dcaf0;
+ --success-color: #198754; /* New: for consistent status */
+ --danger-color: #dc3545; /* New: for conflict status */
+ --warning-color: #ffc107; /* New: for error status */
+ --border-color: #e9ecef; /* MODIFIED: Lighter border for a softer look */
+ --bg-color-light: #f8f9fa;
+ --text-color: #212529;
+ --text-color-light: #495057; /* NEW: for secondary text/headers */
+
+ /* Light Theme Code Colors */
+ --code-bg: #f8f8f8; /* Very light gray, like a parchment or fresh terminal */
+ --code-text: #333333; /* Darker text for readability */
+ --code-border: #e0e0e0;
+}
+
+/* Layout */
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ margin: 0;
+ padding: 20px;
+ background-color: var(--bg-color-light);
+ color: var(--text-color);
+}
+
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ background: #fff;
+ border: 1px solid var(--border-color);
+ padding: 30px;
+ border-radius: 12px; /* MODIFIED: Larger radius for a modern feel */
+ box-shadow: 0 6px 15px rgba(0, 0, 0, 0.05); /* MODIFIED: Softer, deeper shadow */
+ position: relative;
+}
+
+/* NEW: Consistent Heading Styles */
+h1 {
+ font-size: 2rem;
+ font-weight: 700;
+ margin-top: 0;
+ margin-bottom: 5px;
+}
+
+h2 {
+ font-size: 1.5rem;
+ font-weight: 600;
+ margin-top: 30px;
+ margin-bottom: 10px;
+ color: var(--text-color-light); /* Subdued color for section headers */
+}
+
+/* ------------------------------------------------------------- */
+/* Action Buttons (General Styles) */
+/* ------------------------------------------------------------- */
+.action-button {
+ font-size: 0.8rem; /* MODIFIED: Slightly smaller in the table for a cleaner fit */
+ padding: 4px 8px; /* MODIFIED: Tighter padding */
+ border-radius: 4px;
+ cursor: pointer;
+ border: 1px solid transparent;
+ transition: all 0.2s ease;
+ margin: 0;
+ margin-right: 5px;
+ min-width: 65px; /* NEW: Ensure uniform width in table */
+ text-align: center;
+}
+
+.action-button.disable {
+ background-color: #fff; /* MODIFIED: Ghost/Outlined style for better contrast */
+ color: var(--secondary-color);
+ border-color: #dcdcdc; /* MODIFIED: Light gray border */
+ font-weight: 500;
+}
+
+.action-button.disable:hover {
+ background-color: var(--secondary-color); /* Solid on hover */
+ color: white;
+}
+
+.action-button.enable {
+ background-color: var(--success-color);
+ color: white;
+ font-weight: 500;
+}
+
+.action-button.enable:hover {
+ background-color: #157347;
+}
+
+.action-button.remove {
+ background-color: var(--danger-color);
+ color: white;
+}
+
+.action-button.remove:hover {
+ background-color: #bb2d3b;
+}
+
+/* Add Chain Button */
+.action-button.add {
+ background-color: var(--success-color); /* MODIFIED: Using variable for consistency */
+ color: white;
+}
+
+.action-button.add:hover {
+ background-color: #157347;
+}
+
+/* Cancel button for forms (like in the Add Chain modal) */
+.action-button.cancel {
+ background-color: var(--secondary-color);
+ color: white;
+}
+.action-button.cancel:hover {
+ background-color: #5c636a;
+}
+
+/* ------------------------------------------------------------- */
+/* Consistency Status Indicator (Modernized) */
+/* ------------------------------------------------------------- */
+#consistency-status-container {
+ position: absolute;
+ top: 30px;
+ right: 30px;
+ z-index: 5;
+}
+
+#consistency-button {
+ font-size: 0.85rem; /* MODIFIED: Smaller font */
+ padding: 5px 12px; /* MODIFIED: Tighter padding */
+ border-radius: 20px; /* MODIFIED: Pill-shaped */
+ font-weight: 700;
+ cursor: default;
+ pointer-events: none;
+ transition: background-color 0.2s ease, transform 0.1s ease;
+ border: none;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* NEW: Subtle shadow */
+}
+
+#consistency-button.consistent {
+ background-color: var(--success-color);
+ color: white;
+}
+
+#consistency-button.inconsistent {
+ background-color: var(--danger-color);
+ color: white;
+ cursor: pointer;
+ pointer-events: auto;
+}
+
+#consistency-button.inconsistent:hover {
+ background-color: #bb2d3b;
+ transform: scale(1.02);
+}
+
+#consistency-button.error {
+ background-color: var(--warning-color);
+ color: var(--text-color);
+}
+
+#consistency-button:disabled {
+ opacity: 0.8;
+}
+
+/* ------------------------------------------------------------- */
+/* Toolbar (Modernized) - FIX APPLIED HERE */
+/* ------------------------------------------------------------- */
+
+.toolbar {
+ display: flex;
+ /* This pushes the .toolbar-left-group and .toolbar-right-group to opposite sides */
+ justify-content: space-between;
+ gap: 10px;
+ margin-bottom: 25px;
+ margin-top: 25px;
+ padding: 10px 0;
+ border-top: 1px solid var(--border-color);
+ border-bottom: 1px solid var(--border-color);
+}
+
+/* Grouping for Reload/Refresh buttons on the left */
+.toolbar-left-group {
+ display: flex;
+ gap: 10px;
+}
+
+/* Grouping for Add New buttons on the right */
+.toolbar-right-group {
+ display: flex;
+ gap: 10px;
+}
+
+.toolbar button, .toolbar a {
+ background-color: #fff; /* Ghost/Outlined button style for secondary actions */
+ color: var(--primary-color);
+ border: 1px solid var(--primary-color);
+ border-radius: 5px;
+ padding: 8px 14px;
+ font-size: 0.9rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ text-decoration: none;
+}
+
+.toolbar button:hover, .toolbar a:hover {
+ background-color: var(--primary-color);
+ color: white; /* Text inverts to white on hover */
+}
+
+/* Primary Action Button Styles (Targeting the 'Add New' buttons via onclick attribute) */
+.toolbar button[onclick*="showAddClusterModal"],
+.toolbar button[onclick*="showAddListenerModal"],
+.toolbar button[onclick*="showAddSecretModal"],
+.toolbar button[onclick*="showAddExtensionConfigModal"] {
+ background-color: var(--primary-color); /* Solid background for primary actions */
+ color: white;
+ border-color: var(--primary-color);
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+ /* Ensure no unwanted margin pushing */
+ margin-left: 0 !important;
+}
+
+.toolbar button[onclick*="showAddClusterModal"]:hover,
+.toolbar button[onclick*="showAddListenerModal"]:hover ,
+.toolbar button[onclick*="showAddSecretModal"]:hover ,
+.toolbar button[onclick*="showAddExtensionConfigModal"]:hover {
+ background-color: #0b5ed7;
+ border-color: #0b5ed7;
+}
+
+/* ------------------------------------------------------------- */
+/* Table (Modernized - Striped and Cleaner) */
+/* ------------------------------------------------------------- */
+.config-table {
+ width: 100%;
+ border-collapse: separate; /* MODIFIED: Allows border-radius on cells */
+ border-spacing: 0; /* NEW: Removes spacing between borders */
+ margin-top: 15px;
+ border: 1px solid var(--border-color); /* NEW: Single border around the table */
+ border-radius: 6px; /* NEW: Rounded corners on table */
+ overflow: hidden;
+}
+
+.config-table th,
+.config-table td {
+ padding: 12px 15px; /* MODIFIED: Increased vertical padding */
+ text-align: left;
+ border-bottom: none; /* MODIFIED: Removed internal horizontal lines */
+ vertical-align: middle; /* MODIFIED: Center align content vertically */
+}
+
+.config-table th {
+ background-color: var(--bg-color-light); /* MODIFIED: Lighter header background */
+ font-weight: 600;
+ color: var(--text-color-light); /* MODIFIED: Subdued header text */
+ position: sticky;
+ top: 0;
+ z-index: 2;
+ border-bottom: 1px solid var(--border-color); /* Re-add separator */
+}
+
+/* NEW: Zebra Striping */
+.config-table tbody tr:nth-child(even) {
+ background-color: #fcfcfd;
+}
+.config-table tbody tr:nth-child(odd) {
+ background-color: #fff;
+}
+
+/* Prevent whole row hover highlight (still needed) */
+.config-table tr:hover {
+ background-color: transparent;
+}
+
+/* Row-specific hover highlight (Softer blue on hover) */
+.config-table tr:hover:not(.disabled-row) {
+ background-color: #eef5ff; /* MODIFIED: Softer hover color */
+}
+
+/* Status Column Text Styling for readability */
+.config-table td:nth-child(2) {
+ font-family: monospace;
+ font-size: 0.875rem;
+ color: var(--text-color-light);
+}
+
+.listener-name,
+.domain-name-link {
+ font-weight: 600;
+ color: var(--primary-color);
+ cursor: pointer;
+ text-decoration: none; /* MODIFIED: Remove default underline */
+ transition: color 0.15s ease;
+}
+
+/* Highlight only when hovering over the link itself */
+.listener-name:hover,
+.domain-name-link:hover {
+ text-decoration: underline; /* MODIFIED: Add underline only on hover */
+ color: #0b5ed7;
+}
+
+/* Keep non-clickable text normal */
+.config-table td {
+ cursor: default;
+}
+
+.disabled-row {
+ opacity: 0.5; /* MODIFIED: Slightly more muted */
+ background-color: #fbfbfb !important;
+}
+
+.disabled-row .listener-name {
+ text-decoration: line-through;
+}
+
+/* Badges */
+.tls-badge {
+ display: inline-block;
+ background-color: var(--info-color);
+ color: white;
+ padding: 4px 8px;
+ border-radius: 12px;
+ font-size: 0.75rem;
+ margin-left: 6px;
+}
+
+.filter-badge {
+ display: inline-block;
+ padding: 3px 6px;
+ border-radius: 4px;
+ font-size: 0.7rem;
+ margin-left: 6px;
+}
+
+/* Domain Config */
+.domain-config-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-top: 6px;
+}
+
+.domain-config-item {
+ background-color: #fcfcfc;
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ padding: 6px 10px;
+ transition: background-color 0.2s ease;
+}
+
+.domain-config-item:hover {
+ background-color: #eef5ff;
+}
+
+.domain-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.domain-name-link {
+ color: var(--primary-color);
+ font-weight: 500;
+ cursor: pointer;
+}
+
+.domain-name-link:hover {
+ text-decoration: underline;
+}
+
+/* IMPROVED: Layout for Route Type and Remove Button */
+.route-type-display {
+ font-size: 0.75rem;
+ color: var(--secondary-color);
+ margin-top: 3px;
+
+ /* NEW: Use flex to align type and button on the same line */
+ display: flex;
+ justify-content: space-between; /* Push button to the right */
+ align-items: center;
+}
+
+/* NEW: Style for the "Remove Chain" button inside the domain item */
+.action-button.remove-chain-button {
+ /* Override standard action button size */
+ font-size: 0.7rem;
+ padding: 2px 6px;
+ border-radius: 3px;
+ margin: 0;
+
+ /* Make it less aggressive than the full listener remove button */
+ background-color: #ff4d4d; /* A lighter, softer red */
+ color: white;
+ border: 1px solid #ff4d4d;
+ transition: all 0.2s ease;
+}
+
+.action-button.remove-chain-button:hover {
+ background-color: var(--danger-color); /* Full red on hover */
+ border-color: var(--danger-color);
+}
+
+/* IMPROVED: Action column styling to stack buttons neatly (for the last column) */
+.config-table td:last-child {
+ display: flex;
+ flex-direction: column; /* Stack buttons vertically */
+ gap: 4px; /* Small space between stacked buttons */
+ /* Ensure padding is kept */
+ padding: 10px 12px;
+}
+
+/* Target buttons in the main Action column to ensure consistent width */
+.config-table td:last-child .action-button {
+ margin-right: 0; /* Remove horizontal margin */
+ width: 100%; /* Make them full width for that column */
+ box-sizing: border-box;
+ text-align: center;
+}
+
+
+/* ============================================================= */
+/* MODAL FIXES AND TAB STYLING (Consistent) */
+/* ============================================================= */
+
+.modal {
+ display: none;
+ position: fixed;
+ z-index: 1000;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ padding-top: 50px;
+ background-color: rgba(0, 0, 0, 0.5);
+ overflow: auto;
+}
+
+.modal-content {
+ background-color: #fff;
+ margin: 0 auto;
+ padding: 25px;
+ border-radius: 8px;
+ max-width: 900px;
+ width: 80%;
+ box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
+ position: relative;
+ max-height: 90vh;
+ overflow-y: auto;
+}
+
+/* NEW: Consistent Modal Header */
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-bottom: 10px;
+}
+
+/* Close Button Styling and Positioning Fix */
+.close-btn, /* The one used in the secondary modals */
+.modal-content > .close { /* The one used in the main configModal (Unified) */
+ color: #aaa;
+ font-size: 28px;
+ font-weight: bold;
+ line-height: 1;
+ cursor: pointer;
+ position: absolute;
+ top: 10px;
+ right: 20px;
+ z-index: 10;
+}
+
+.close-btn:hover,
+.close-btn:focus,
+.modal-content > .close:hover,
+.modal-content > .close:focus {
+ color: #000;
+ text-decoration: none;
+}
+
+.modal-content h2,
+.modal-content h3 { /* Apply to H2 and H3 for consistency */
+ padding-right: 40px;
+ margin-top: 0; /* Ensures consistent top margin on modal headers */
+}
+
+/* --- TAB CONTROL LAYOUT --- */
+.tab-controls {
+ display: flex; /* Aligns tab-selection and action-group horizontally */
+ align-items: flex-end; /* Aligns the tab buttons and action group to the bottom line */
+ margin: 0 0 0;
+ /* REMOVE: border-bottom: 2px solid var(--border-color); */
+}
+
+
+/* NEW CSS: Create a dedicated container for the gray underline */
+.tab-selection {
+ display: flex;
+ /* ADD the border-bottom here. It will only span the width of the tab buttons. */
+ border-bottom: 2px solid var(--border-color);
+}
+
+/* Wrapper for Copy/Download buttons (right side) */
+.modal-action-group {
+ display: flex;
+ gap: 10px; /* Space between Copy and Download buttons */
+ align-items: center;
+ margin-left: auto; /* Pushes the group to the right */
+ padding-bottom: 5px; /* Visual adjustment above the border */
+}
+
+/* --- TAB BUTTON STYLING (JSON/YAML) --- */
+.tab-button {
+ background: transparent;
+ border: none;
+ padding: 10px 15px;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ color: var(--secondary-color);
+ transition: all 0.2s ease;
+}
+
+.tab-button.active {
+ color: var(--primary-color);
+ /* The active blue underline is still correct here */
+ border-bottom-color: var(--primary-color);
+}
+
+/* Style for the Copy Button to look like an action button */
+#copy-config-button {
+ /* Set to look like a secondary action button */
+ background-color: #fff;
+ color: var(--primary-color);
+ border: 1px solid var(--primary-color);
+ border-radius: 4px;
+ padding: 8px 12px;
+ font-size: 0.9rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ /* Override any tab-button specific styles that might clash */
+ margin-bottom: 0 !important;
+ line-height: 1.2;
+}
+
+#copy-config-button:hover {
+ background-color: var(--primary-color);
+ color: white;
+}
+
+/* Download Button Styling (Standardized to match the outlined look of Copy to Clip) */
+.download-button {
+ /* Set to match the Copy button: outlined style */
+ background-color: #fff; /* White background */
+ color: var(--primary-color); /* Primary text color */
+ border: 1px solid var(--primary-color); /* Primary border color */
+ border-radius: 4px;
+ padding: 8px 15px;
+ font-size: 0.9rem;
+ font-weight: 500;
+}
+
+.download-button:hover {
+ /* Solid primary color on hover for consistency */
+ background-color: var(--primary-color);
+ border-color: #0b5ed7;
+ color: white; /* White text on hover */
+}
+
+/* Consistent Icon/Text Spacing for both action buttons */
+#copy-config-button,
+.download-button {
+ display: flex;
+ align-items: center;
+}
+
+/* Adjust spacing for the icons in both buttons */
+#copy-config-button .action-button-icon, /* Use a span class if you wrap the icon */
+.download-button .action-button-icon { /* Use a span class if you wrap the icon */
+ margin-right: 5px;
+}
+
+/* Since you are using unicode, this targets the first child (the emoji) */
+#copy-config-button:not([style*='Copied']) > *:first-child,
+.download-button > *:first-child {
+ margin-right: 5px; /* Small space between icon and text */
+}
+
+/* Code Block Styling */
+.code-block {
+ background: var(--code-bg);
+ color: var(--code-text);
+ font-family: monospace;
+ font-size: 0.9rem;
+ padding: 15px;
+ border-radius: 4px;
+ border: 1px solid var(--code-border);
+ max-height: 500px;
+ overflow: auto;
+ white-space: pre;
+ word-wrap: normal;
+}
+
+/* ------------------------------------------------------------- */
+/* Add Filter Chain & Add Listener Modal Styles (Unified) */
+/* ------------------------------------------------------------- */
+
+/* Styles for the larger content area within the modal */
+.modal-content.large-modal {
+ max-width: 900px;
+ width: 90%;
+}
+
+/* Styles for the YAML textarea input (Unified for all YAML inputs) */
+#add-fc-yaml-input,
+#add-listener-yaml-input, #add-cluster-yaml-input , #add-secret-yaml-input, #add-extension-config-yaml-input { /* Unified ID for consistency */
+ width: 100%;
+ font-family: monospace;
+ font-size: 0.9rem;
+ padding: 10px;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ background-color: var(--code-bg);
+ color: var(--code-text);
+ resize: vertical;
+ box-sizing: border-box;
+ min-height: 250px; /* Ensures Add Listener textarea has a good starting height */
+}
+
+/* Styles for the label above the textarea (Unified for all form labels) */
+label[for="add-fc-yaml-input"],
+label[for="add-listener-yaml-input"], label[for="add-cluster-yaml-input"], label[for="add-secret-yaml-input"]{ /* Unified ID for consistency */
+ display: block;
+ font-weight: 600;
+ margin-bottom: 5px;
+ margin-top: 15px;
+}
+
+.modal-note {
+ font-size: 0.85rem;
+ color: var(--secondary-color);
+ margin-top: 20px;
+ padding-top: 10px;
+ border-top: 1px solid var(--border-color);
+}
+
+
+/* ------------------------------------------------------------- */
+/* Consistency Modal Specific Styles (Enhanced) */
+/* ------------------------------------------------------------- */
+
+/* Modal Action buttons container (Unified Footer) */
+.modal-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 15px;
+ padding-top: 15px;
+ border-top: 1px solid var(--border-color);
+ margin-top: 20px;
+}
+
+.modal-actions button {
+ font-size: 1rem;
+ padding: 10px 20px;
+ line-height: 1.2;
+ text-align: center;
+ min-width: 150px; /* Consistent minimum width for action buttons */
+}
+
+.modal-actions small {
+ font-weight: 400;
+ display: block;
+ font-size: 0.75rem;
+ opacity: 0.9;
+}
+
+#inconsistency-details-content pre {
+ /* Ensure consistency modal PRE elements also use the light code-block styles for scrolling */
+ background: var(--code-bg);
+ color: var(--code-text);
+ padding: 10px;
+ border-radius: 4px;
+ border: 1px solid var(--code-border);
+ font-size: 0.9rem;
+ max-height: 300px;
+ overflow: auto;
+ white-space: pre;
+ word-wrap: normal;
+}
+
+/* Consistency Conflict List styling (Enhanced) */
+.conflict-list {
+ list-style: none;
+ padding: 0;
+ margin: 10px 0 20px 0;
+ border: 1px solid var(--code-border);
+ border-radius: 4px;
+ background-color: var(--code-bg);
+ max-height: 200px;
+ overflow-y: auto;
+}
+
+.conflict-list li {
+ padding: 8px 10px;
+ border-bottom: 1px solid var(--code-border);
+ font-size: 0.95rem;
+ display: flex;
+ gap: 10px;
+ align-items: center;
+}
+
+.conflict-list li:last-child {
+ border-bottom: none;
+}
+
+.conflict-list li:nth-child(even) {
+ background-color: transparent;
+}
+
+.conflict-list .no-conflicts {
+ color: var(--success-color);
+ font-weight: 500;
+ background-color: #e6f7ef;
+ border-radius: 4px;
+ padding: 10px;
+ margin: 5px;
+ border: 1px solid rgba(25, 135, 84, 0.2);
+ text-align: center;
+}
+
+.conflict-type {
+ font-weight: 600;
+ color: var(--primary-color);
+ background-color: #e9f2ff;
+ padding: 2px 6px;
+ border-radius: 4px;
+}
+
+.conflict-name {
+ font-family: monospace;
+ font-weight: 500;
+ color: var(--text-color);
+ flex-grow: 1;
+}
+/* ------------------------------------------------------------- */
+
+
+/* Responsive */
+@media (max-width: 768px) {
+ .config-table th, .config-table td {
+ font-size: 0.85rem;
+ padding: 8px;
+ }
+ .container {
+ padding: 15px;
+ }
+ .modal-content {
+ width: 95%;
+ margin: 10px auto;
+ }
+ .modal {
+ padding-top: 20px;
+ }
+ #consistency-status-container {
+ top: 15px;
+ right: 15px;
+ }
+}
+
+/* Style for the validity status labels */
+.status-label {
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-size: 0.85em;
+ font-weight: bold;
+ color: white; /* Default text color */
+ margin-left: 5px;
+ text-transform: uppercase;
+}
+
+.status-label.invalid {
+ background-color: var(--error-color, #ff4d4f); /* Red color for Invalid */
+}
+
+.status-label.valid {
+ background-color: var(--success-color, #52c41a); /* Green color for Valid */
+}
+
+/* Custom styles for the Certificate Issuer link/button (Issue New Certificate) */
+
+/* Container to ensure the heading and the button are on the same line and spaced correctly */
+.secrets-header-container {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 30px; /* Use the same top margin as H2 for proper spacing */
+ margin-bottom: 10px; /* Use the same bottom margin as H2 for proper spacing */
+}
+
+.cert-issuer-button {
+ /* Style to match existing header buttons (e.g., Add/Update Cluster) */
+ display: inline-flex;
+ align-items: center;
+
+ /* Using toolbar button styling for consistency */
+ background-color: var(--primary-color);
+ color: white;
+ border: 1px solid var(--primary-color);
+ border-radius: 5px;
+ padding: 8px 14px;
+ font-size: 0.9rem;
+ font-weight: 500;
+ cursor: pointer;
+ text-decoration: none; /* Remove underline from the anchor tag */
+ transition: all 0.2s ease;
+
+ /* Add a subtle shadow consistent with primary toolbar buttons */
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+}
+
+.cert-issuer-button:hover {
+ background-color: #0b5ed7; /* Darker primary color on hover (same as toolbar) */
+ border-color: #0b5ed7;
+ color: white;
+}
+
+/* Style for the text label within the button */
+.cert-issuer-button-text {
+ margin-right: 8px; /* Space between text and icon */
+ font-size: 0.9rem; /* Match button text size */
+}
+
+/* Style for the plus icon within the button */
+.cert-issuer-button-icon {
+ font-size: 1.1rem; /* Slightly larger plus sign for emphasis */
+ line-height: 1;
+}
+
+/* ============================================================= */
+/* Rotation Settings Modal Specific Styles */
+/* ============================================================= */
+
+/* General form group layout for modals */
+.modal-content .form-group {
+ margin-bottom: 15px;
+}
+
+/* Style for input fields inside modals (Unified for consistency) */
+.modal-content input[type="text"],
+.modal-content input[type="number"] {
+ width: 100%;
+ padding: 10px;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ box-sizing: border-box;
+ font-size: 1rem;
+ transition: border-color 0.2s ease;
+}
+
+.modal-content input[type="text"]:focus,
+.modal-content input[type="number"]:focus {
+ border-color: var(--primary-color);
+ outline: none;
+ box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
+}
+
+/* Style for labels (Unified for consistency) */
+.modal-content label {
+ display: block;
+ font-weight: 600;
+ margin-bottom: 5px;
+ color: var(--text-color);
+}
+
+/* Style for the checkbox/toggle group (Unified) */
+.modal-content .checkbox-group {
+ display: flex;
+ align-items: center;
+ padding-left: 0;
+}
+
+.modal-content .checkbox-group input[type="checkbox"] {
+ /* Style for the visual toggle or checkbox itself */
+ margin-right: 10px;
+ width: 18px;
+ height: 18px;
+ cursor: pointer;
+}
+
+.modal-content .checkbox-group label {
+ /* Adjust label positioning for checkbox */
+ font-weight: 500;
+ margin-bottom: 0;
+ cursor: pointer;
+}
+
+/* Hint text below inputs */
+.modal-content .input-hint {
+ font-size: 0.8rem;
+ color: var(--secondary-color);
+ margin-top: 5px;
+ margin-bottom: 0;
+ padding-left: 2px;
+}
+
+/* Align the submit button in the footer using the existing .modal-actions */
+#rotationSettingsModal .modal-actions #rotation-submit-btn {
+ /* Use the standard 'add' style */
+ background-color: var(--success-color);
+ color: white;
+ font-weight: 500;
+}
+
+#rotationSettingsModal .modal-actions #rotation-submit-btn:hover {
+ background-color: #157347;
+}
+
+#rotationSettingsModal .modal-actions button.disable {
+ /* Use the standard 'cancel' style */
+ background-color: var(--secondary-color);
+ color: white;
+ border-color: var(--secondary-color);
+}
+
+#rotationSettingsModal .modal-actions button.disable:hover {
+ background-color: #5c636a;
+}
+
+/* Recommended CSS to make the rotation label visible */
+.status-label {
+ /* Base styles for all status labels */
+ display: inline-block;
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-size: 0.75em;
+ font-weight: bold;
+ margin-left: 5px;
+ vertical-align: middle;
+ white-space: nowrap;
+ text-transform: uppercase;
+}
+
+.status-label.valid {
+ background-color: #28a745; /* Green */
+ color: white;
+}
+
+.status-label.invalid {
+ background-color: #dc3545; /* Red */
+ color: white;
+}
+
+.status-label.rotation-enabled {
+ /* This is the key missing style for the new label */
+ background-color: #007bff; /* A distinct blue for rotation status */
+ color: white;
+}
+
+/* --- Base Status Label Styles (from previous answers) --- */
+.status-label {
+ display: inline-block;
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-size: 0.75em;
+ font-weight: bold;
+ margin-left: 5px;
+ vertical-align: middle;
+ white-space: nowrap;
+ text-transform: uppercase;
+}
+
+.status-label.valid {
+ background-color: #28a745; /* Green */
+ color: white;
+}
+
+.status-label.invalid {
+ background-color: #dc3545; /* Red */
+ color: white;
+}
+
+.status-label.rotation-enabled {
+ background-color: #007bff; /* Blue */
+ color: white;
+ position: relative; /* Crucial for positioning the tooltip */
+ cursor: help; /* Indicates interactivity */
+}
+
+/* --- Custom Tooltip Styles --- */
+.has-tooltip::before,
+.has-tooltip::after {
+ --tooltip-bg: #333;
+ --tooltip-text-color: #fff;
+ --tooltip-border-radius: 5px;
+ --tooltip-padding: 8px 12px;
+ --tooltip-font-size: 0.8em;
+ --tooltip-max-width: 300px;
+ --tooltip-offset: 0px; /* Distance from the element */
+
+ position: absolute;
+ visibility: hidden;
+ opacity: 0;
+ transition: opacity 0.3s ease, visibility 0.3s ease;
+ z-index: 1000; /* Ensure it's above other content */
+ pointer-events: none; /* Allows clicks through tooltip if not interacting with it */
+}
+
+/* Tooltip Content */
+.has-tooltip::before {
+ content: attr(data-tooltip); /* Get content from data-tooltip attribute */
+ background: var(--tooltip-bg);
+ color: var(--tooltip-text-color);
+ border-radius: var(--tooltip-border-radius);
+ padding: var(--tooltip-padding);
+ font-size: var(--tooltip-font-size);
+ max-width: var(--tooltip-max-width);
+ white-space: pre-wrap; /* Preserve newlines and wrap text */
+ text-align: left; /* Align text within the tooltip */
+ text-transform: none; /* Don't uppercase tooltip text */
+
+ /* Positioning - centered horizontally, below the element */
+ left: 50%;
+ transform: translateX(-50%) translateY(var(--tooltip-offset));
+ bottom: calc(100% + var(--tooltip-offset)); /* Position above the element by default */
+
+ /* Ensure it doesn't get squished too thin */
+ min-width: 180px;
+}
+
+/* Tooltip Arrow */
+.has-tooltip::after {
+ content: '';
+ border-width: var(--tooltip-offset);
+ border-style: solid;
+ border-color: var(--tooltip-bg) transparent transparent transparent; /* Arrow pointing up */
+
+ /* Positioning - centered horizontally, below the content */
+ left: 50%;
+ transform: translateX(-50%) translateY(var(--tooltip-offset));
+ bottom: calc(100% + var(--tooltip-offset) - 5px); /* Position slightly below the content */
+
+ /* Adjust for arrow direction if position changes */
+ /* Example: if tooltip is below, arrow points down */
+ /* border-color: transparent transparent var(--tooltip-bg) transparent; */
+}
+
+/* Show tooltip on hover */
+.has-tooltip:hover::before,
+.has-tooltip:hover::after {
+ visibility: visible;
+ opacity: 1;
+}
+
+/* Adjust tooltip position if it's too close to the right edge (optional, more complex) */
+/* This is a basic example; for robust positioning, a JS library is best */
+/* @media (max-width: 768px) {
+ .has-tooltip::before,
+ .has-tooltip::after {
+ left: 0;
+ transform: translateY(var(--tooltip-offset));
+ right: auto;
+ bottom: calc(100% + var(--tooltip-offset));
+ }
+ .has-tooltip::after {
+ left: 10px; // Adjust arrow
+ }
+} */
\ No newline at end of file
diff --git a/static/data_fetchers.js b/static/data_fetchers.js
deleted file mode 100755
index 6a2d3ec..0000000
--- a/static/data_fetchers.js
+++ /dev/null
@@ -1,248 +0,0 @@
-// data_fetchers.js
-import { API_BASE_URL, configStore } from './global.js';
-import { showConfigModal, switchTab } from './modals.js';
-import { listListeners } from './listeners.js'; // Will be imported later
-
-// =========================================================================
-// YAML UTILITY FUNCTION
-// =========================================================================
-
-/**
- * Extracts the YAML section for a specific domain from the filterChains array.
- * NOTE: This is a complex utility that you may decide is no longer needed
- * given the new approach in showDomainConfig (generating from JSON).
- * However, since it was in the original code, we keep it here.
- * @param {string} yamlData - The full YAML string containing the listener configuration.
- * @param {string} domainName - The domain name to search for.
- * @returns {string | null} The YAML string for the matching filterChain.
- */
-export function extractFilterChainByDomain(yamlData, domainName) {
- if (typeof require === 'undefined' && typeof jsyaml === 'undefined') {
- console.error("Error: YAML parser (e.g., js-yaml) is required but not found.");
- return null;
- }
- // ... (rest of the original extractFilterChainByDomain function logic) ...
-
- let fullConfig;
- try {
- const yaml = (typeof require !== 'undefined') ? require('js-yaml') : jsyaml;
- const allDocs = yaml.loadAll(yamlData);
- // Find the main configuration object (usually the first object document)
- 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;
- }
- let matchingChain
- if (domainName === "*" && fullConfig.filterChains.length > 0) {
- matchingChain = fullConfig.filterChains.find(chain => {
- return chain.filterChainMatch == null
- });
-
- } else {
- matchingChain = fullConfig.filterChains.find(chain => {
- const serverNames = chain.filterChainMatch?.serverNames;
- return serverNames && serverNames.includes(domainName);
- });
- }
-
- if (!matchingChain) {
- console.log(`No filterChain found for domain: ${domainName}`);
- return null;
- }
-
- try {
- const yaml = (typeof require !== 'undefined') ? require('js-yaml') : jsyaml;
- const outputYaml = yaml.dump(matchingChain, {
- indent: 2,
- lineWidth: -1,
- flowLevel: -1
- });
- return outputYaml.trim();
-
- } catch (e) {
- console.error("Error dumping YAML data:", e);
- return null;
- }
-}
-
-
-// =========================================================================
-// CONFIG-SPECIFIC MODAL LAUNCHERS
-// =========================================================================
-
-/**
- * Handles showing the configuration for an individual FilterChain/Domain.
- * This function loads the JSON from memory and generates the YAML from it.
- * @param {HTMLElement} element - The DOM element that triggered the function.
- */
-export async function showDomainConfig(element) {
- const title = element.getAttribute('data-title');
- const listenerName = element.getAttribute('data-listener-name');
- const chainIndex = parseInt(element.getAttribute('data-chain-index'), 10);
-
- if (!listenerName || isNaN(chainIndex)) {
- console.error("Missing required data attributes or invalid chain index for domain config.");
- return;
- }
-
- // 1. Get the full Listener JSON config from memory
- const listenerConfig = configStore.listeners[listenerName];
- if (!listenerConfig || !listenerConfig.filterChains || !listenerConfig.filterChains[chainIndex]) {
- console.error(`Listener or FilterChain at index ${chainIndex} not found in memory for ${listenerName}.`);
- showConfigModal(`🚨 Error: Domain Config Not Found`, { name: title, error: `FilterChain index ${chainIndex} not found for listener ${listenerName}.` }, 'Error: JSON configuration missing.');
- return;
- }
-
- // The JSON data for the specific filter chain
- const jsonData = listenerConfig.filterChains[chainIndex];
-
- // 2. Fetch the full YAML for the listener if not already in memory
- let fullListenerYaml = listenerConfig.yaml || 'Loading YAML...';
- if (fullListenerYaml === 'Loading YAML...') {
- try {
- const response = await fetch(`${API_BASE_URL}/get-listener?name=${listenerName}&format=yaml`);
- if (!response.ok) {
- fullListenerYaml = `Error fetching YAML: ${response.status} ${response.statusText}`;
- } else {
- fullListenerYaml = await response.text();
- configStore.listeners[listenerName].yaml = fullListenerYaml; // Store YAML
- }
- } catch (error) {
- console.error("Failed to fetch YAML listener config:", error);
- fullListenerYaml = `Network Error fetching YAML: ${error.message}`;
- }
- }
-
- let yamlData;
- // 3. Extract the specific filterChain YAML using the utility function.
- // Use the domain name from the title as a proxy, or the first server_name from the JSON.
- const domainName = jsonData.filter_chain_match?.server_names?.[0] || "*";
-
- if (fullListenerYaml.startsWith('Error') || fullListenerYaml.startsWith('Network Error')) {
- yamlData = fullListenerYaml; // Pass the error message
- } else {
- // Use the utility function to extract the specific chain's YAML
- yamlData = extractFilterChainByDomain(fullListenerYaml, domainName);
- if (yamlData === null) {
- // As a fallback if the utility fails or doesn't find it, dump the JSON directly
- try {
- const yaml = (typeof require !== 'undefined') ? require('js-yaml') : jsyaml;
- yamlData = yaml.dump(jsonData, { indent: 2, lineWidth: -1, flowLevel: -1 }).trim();
- } catch (e) {
- yamlData = `Could not extract or dump YAML for chain: ${domainName}. Error: ${e.message}`;
- }
- }
- }
-
- // 4. Show the modal
- showConfigModal(title, jsonData, yamlData, 'json'); // Default to 'json' since it's guaranteed from memory
-}
-
-
-// export async function showClusterConfigModal(clusterName) {
-// const config = configStore.clusters[clusterName];
-// if (!config) {
-// showConfigModal(`🚨 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 {
-// 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}`;
-// }
-// }
-
-// showConfigModal(`Full Config for Cluster: ${clusterName}`, config, yamlData);
-// }
-
-// export async function showListenerConfigModal(listenerName) {
-// const config = configStore.listeners[listenerName];
-// if (!config) {
-// showConfigModal(`🚨 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 {
-// 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}`;
-// }
-// }
-
-// showConfigModal(`Full Config for Listener: ${listenerName}`, config, yamlData);
-// }
-
-// =========================================================================
-// FILTER CHAIN ADDITION LOGIC (NEW)
-// =========================================================================
-
-/**
- * Handles the submission of the new filter chain YAML.
- */
-export async function submitNewFilterChain() {
- const listenerName = document.getElementById('add-fc-listener-name').value;
- const yamlData = document.getElementById('add-fc-yaml-input').value.trim();
-
- if (!yamlData) {
- alert("Please paste the filter chain YAML configuration.");
- return;
- }
-
- if (!listenerName) {
- alert("Listener name is missing. Cannot submit.");
- return;
- }
-
- const payload = {
- listener_name: listenerName,
- yaml: yamlData
- };
-
- try {
- const response = await fetch(`${API_BASE_URL}/append-filter-chain`, {
- 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}`);
- }
-
- alert(`Successfully appended new filter chain to '${listenerName}'.`);
-
- // Close modal and refresh listener list
- document.getElementById('addFilterChainModal').style.display = 'none';
- listListeners();
- } catch (error) {
- console.error(`Failed to append filter chain to '${listenerName}':`, error);
- alert(`Failed to append filter chain. Check console for details. Error: ${error.message}`);
- }
-}
\ No newline at end of file
diff --git a/static/data_loader.js b/static/data_loader.js
deleted file mode 100755
index d266be7..0000000
--- a/static/data_loader.js
+++ /dev/null
@@ -1,51 +0,0 @@
-// data_loader.js
-import { listClusters } from './clusters.js';
-import { listListeners } from './listeners.js';
-import { listSecrets } from './secrets.js';
-import { listExtensionConfigs } from './extension_configs.js';
-import { setupModalTabs } from './modals.js';
-import {CONSISTENCY_POLL_INTERVAL, checkConsistency} from './global.js';
-
-
-// =========================================================================
-// COMBINED LOADER & POLLING
-// =========================================================================
-
-/**
- * Loads all primary data (Clusters and Listeners) from the API.
- */
-export function loadAllData() {
- listClusters();
- listListeners();
- listSecrets();
- listExtensionConfigs();
-}
-
-
-// =========================================================================
-// INITIALIZATION
-// =========================================================================
-
-window.onload = () => {
- loadAllData();
- setupModalTabs(); // Setup tab logic on load
- checkConsistency(); // Initial check
- setInterval(checkConsistency, CONSISTENCY_POLL_INTERVAL); // Start polling
-};
-
-
-// =========================================================================
-// EXPOSE GLOBAL FUNCTIONS (Required for inline HTML handlers)
-// =========================================================================
-
-// This section is necessary if your HTML uses inline onclick attributes,
-// as is common in older or simpler JS projects.
-import * as Modals from './modals.js';
-import * as Fetchers from './data_fetchers.js';
-import * as Clusters from './clusters.js';
-import * as Listeners from './listeners.js';
-import * as ExtensionConfig from './extension_configs.js';
-import * as Consistency from './consistency.js';
-
-// Attach all necessary functions to the global window object
-Object.assign(window, Modals, Fetchers, Clusters, Listeners,ExtensionConfig, Consistency);
\ No newline at end of file
diff --git a/static/extension_configs.js b/static/extension_configs.js
deleted file mode 100755
index c627a4c..0000000
--- a/static/extension_configs.js
+++ /dev/null
@@ -1,237 +0,0 @@
-// extension_configs.js
-import { API_BASE_URL, configStore, cleanupConfigStore } from './global.js';
-
-// =========================================================================
-// EXTENSION CONFIG UTILITIES
-// =========================================================================
-
-/**
- * Extracts the Type URL of the inner, configured resource from the TypedExtensionConfig.
- * @param {object} extensionConfig - The ExtensionConfig object from the API.
- * @returns {string} The inner type URL or a default message.
- */
-function getExtensionConfigTypeUrl(extensionConfig) {
- try {
- const typeUrl = extensionConfig.typed_config.type_url;
- if (typeUrl) {
- // Display only the last part of the type URL (e.g., Lua or JwtAuthn)
- return typeUrl.substring(typeUrl.lastIndexOf('/') + 1);
- }
- return '(Unknown Type)';
- } catch {
- return '(Config Error)';
- }
-}
-
-// =========================================================================
-// EXTENSION CONFIG CORE LOGIC
-// =========================================================================
-
-/**
- * Fetches and lists all enabled and disabled ExtensionConfigs.
- */
-export async function listExtensionConfigs() {
- const tableBody = document.getElementById('extensionconfig-table-body');
- if (!tableBody) {
- console.error("Could not find element with ID 'extensionconfig-table-body'.");
- return;
- }
-
- tableBody.innerHTML =
- '| Loading... |
';
-
- try {
- const response = await fetch(`${API_BASE_URL}/list-extensionconfigs`);
- if (!response.ok) throw new Error(response.statusText);
-
- const extensionConfigResponse = await response.json();
-
- const allConfigs = [
- ...(extensionConfigResponse.enabled || []).map(c => ({ ...c, status: 'Enabled', configData: c })),
- ...(extensionConfigResponse.disabled || []).map(c => ({ ...c, status: 'Disabled', configData: c }))
- ];
-
- if (!allConfigs.length) {
- tableBody.innerHTML =
- '| No ExtensionConfigs found. |
';
- configStore.extension_configs = {};
- return;
- }
- cleanupConfigStore();
-
- // Store full configs in memory by name
- configStore.extension_configs = allConfigs.reduce((acc, c) => {
- const existingYaml = acc[c.name]?.yaml;
- acc[c.name] = { ...c.configData, yaml: existingYaml };
- return acc;
- }, configStore.extension_configs);
-
-
- tableBody.innerHTML = '';
- allConfigs.forEach(config => {
- const row = tableBody.insertRow();
- if (config.status === 'Disabled') row.classList.add('disabled-row');
-
- let actionButtons = '';
- if (config.status === 'Enabled') {
- actionButtons = ``;
- } else {
- // When disabled, show Enable and Remove buttons
- actionButtons = `
-
-
- `;
- }
-
- // Config Name Hyperlink (uses showExtensionConfigModal from global.js - must be implemented there)
- const nameCell = row.insertCell();
- nameCell.innerHTML =
- `${config.name}`;
-
- row.insertCell().textContent = config.status;
- row.insertCell().innerHTML = getExtensionConfigTypeUrl(config); // Shows the inner type
- row.insertCell().innerHTML = actionButtons;
- });
- } catch (error) {
- tableBody.innerHTML = `| 🚨 ExtensionConfig Error: ${error.message} |
`;
- console.error("ExtensionConfig Fetch/Parse Error:", error);
- }
-}
-
-// =========================================================================
-// EXTENSION CONFIG ENABLE/DISABLE/REMOVE LOGIC
-// =========================================================================
-
-async function toggleExtensionConfigStatus(configName, action) {
- let url = (action === 'remove') ? `${API_BASE_URL}/remove-extensionconfig` : `${API_BASE_URL}/${action}-extensionconfig`;
- const payload = { name: configName };
-
- 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(`ExtensionConfig '${configName}' successfully ${action}d.`);
- cleanupConfigStore();
- listExtensionConfigs();
- } catch (error) {
- console.error(`Failed to ${action} ExtensionConfig '${configName}':`, error);
- alert(`Failed to ${action} ExtensionConfig '${configName}'. Check console for details.`);
- }
-}
-
-// Attach functions to the global window object so they can be called from HTML buttons
-export function disableExtensionConfig(configName, event) {
- event.stopPropagation();
- if (confirm(`Are you sure you want to DISABLE ExtensionConfig: ${configName}?`)) {
- toggleExtensionConfigStatus(configName, 'disable');
- }
-}
-
-export function enableExtensionConfig(configName, event) {
- event.stopPropagation();
- if (confirm(`Are you sure you want to ENABLE ExtensionConfig: ${configName}?`)) {
- toggleExtensionConfigStatus(configName, 'enable');
- }
-}
-
-export function removeExtensionConfig(configName, event) {
- event.stopPropagation();
- if (confirm(`⚠️ WARNING: Are you absolutely sure you want to PERMANENTLY REMOVE ExtensionConfig: ${configName}? This action cannot be undone.`)) {
- toggleExtensionConfigStatus(configName, 'remove');
- }
-}
-
-// =========================================================================
-// ADD EXTENSION CONFIG LOGIC
-// =========================================================================
-
-/**
- * Shows the modal for adding a new ExtensionConfig.
- */
-export function showAddExtensionConfigModal() {
- document.getElementById('add-extension-config-yaml-input').value = '';
- // Clear checkbox on show
- const upsertCheckbox = document.getElementById('add-extension-config-upsert-flag');
- if (upsertCheckbox) {
- upsertCheckbox.checked = false;
- }
- document.getElementById('addExtensionConfigModal').style.display = 'block';
-}
-
-/**
- * Hides the modal for adding a new ExtensionConfig.
- */
-export function hideAddExtensionConfigModal() {
- const modal = document.getElementById('addExtensionConfigModal');
- if (modal) {
- modal.style.display = 'none';
- document.getElementById('add-extension-config-yaml-input').value = '';
- // Clear checkbox on hide
- const upsertCheckbox = document.getElementById('add-extension-config-upsert-flag');
- if (upsertCheckbox) {
- upsertCheckbox.checked = false;
- }
- }
-}
-
-
-/**
- * Submits the new ExtensionConfig YAML to the /add-extensionconfig endpoint.
- */
-export async function submitNewExtensionConfig() {
- const yamlInput = document.getElementById('add-extension-config-yaml-input');
- const upsertCheckbox = document.getElementById('add-extension-config-upsert-flag');
- const configYaml = yamlInput.value.trim();
-
- if (!configYaml) {
- alert('Please paste the ExtensionConfig YAML configuration.');
- return;
- }
-
- try {
- const payload = { yaml: configYaml };
-
- // Add upsert flag to payload if checkbox is checked
- if (upsertCheckbox && upsertCheckbox.checked) {
- payload.upsert = true;
- }
-
- const url = `${API_BASE_URL}/add-extensionconfig`;
-
- 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 ExtensionConfig successfully added.`);
- alert('ExtensionConfig successfully added! The dashboard will now refresh.');
-
- yamlInput.value = '';
- // Uncheck the box upon success/closing
- if (upsertCheckbox) {
- upsertCheckbox.checked = false;
- }
- hideAddExtensionConfigModal();
-
- cleanupConfigStore();
- listExtensionConfigs();
-
- } catch (error) {
- console.error(`Failed to add new ExtensionConfig:`, error);
- alert(`Failed to add new ExtensionConfig. Check console for details. Error: ${error.message}`);
- }
-}
\ No newline at end of file
diff --git a/static/global.js b/static/global.js
deleted file mode 100755
index 3a1b447..0000000
--- a/static/global.js
+++ /dev/null
@@ -1,464 +0,0 @@
-// global.js
-// Use 'export let' to make it available for other modules
-export let API_BASE_URL = window.location.href;
-API_BASE_URL = API_BASE_URL.replace(/\/$/, "");
-// Note: When reassigning, you don't repeat 'export'
-
-// =========================================================================
-// 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.
-export const configStore = {
- clusters: {},
- listeners: {},
- secrets: {},
- extension_configs: {} // NEW: Storage for ExtensionConfig data
- // 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.
- * @param {string} defaultTab - The tab to show by default ('json' or 'yaml').
- */
-export function showConfigModal(title, jsonData, yamlData, defaultTab = 'yaml') {
- 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 the specified tab
- const modalContent = document.getElementById('configModal')?.querySelector('.modal-content');
- if (modalContent) {
- switchTab(modalContent, defaultTab);
- }
-
- document.getElementById('configModal').style.display = 'block';
-}
-
-
-export function hideModal() {
- document.getElementById('configModal').style.display = 'none';
- // document.getElementById('secretConfigModal').style.display = 'none';
-}
-
-window.addEventListener('keydown', (event) => {
- // Check for Escape key to close all modals
- if (event.key === 'Escape') {
- hideModal();
- window.hideAddFilterChainModal?.();
- window.hideAddListenerModal?.();
- window.hideAddClusterModal?.();
- window.hideAddSecretModal?.();
- window.hideAddExtensionConfigModal?.(); // NEW: Close ExtensionConfig modal
- window.hideCertificateDetailsModal?.()
- window.hideRotationSettingsModal?.();
- }
-});
-
-window.addEventListener('click', (event) => {
- // 1. Check if the clicked element has the 'modal' class (i.e., is a backdrop)
- if (event.target.classList.contains('modal')) {
- const modalId = event.target.id;
-
- // 2. Map the modal ID to its corresponding close function
- switch (modalId) {
- case 'configModal':
- // The general configuration/details modal
- hideModal();
- break;
- case 'secretConfigModal':
- // Note: The HTML provided doesn't show this ID, but it's in your original JS.
- document.getElementById('secretConfigModal').style.display = 'none';
- break;
- case 'addFilterChainModal':
- window.hideAddFilterChainModal?.();
- break;
- case 'addListenerModal':
- window.hideAddListenerModal?.();
- break;
- case 'addClusterModal':
- window.hideAddClusterModal?.();
- break;
- case 'addSecretModal':
- window.hideAddSecretModal?.();
- break;
- case 'addExtensionConfigModal': // NEW: Close ExtensionConfig modal
- window.hideAddExtensionConfigModal?.();
- break;
- case 'certificateDetailsModal':
- window.hideCertificateDetailsModal?.();
- break;
- case 'rotationSettingsModal':
- window.hideRotationSettingsModal?.();
- break;
- case 'consistencyModal':
- // Note: The HTML has an inline onclick, but for consistency, we call the global function.
- window.hideConsistencyModal?.();
- break;
- // Add any future modal IDs here
- }
- }
-});
-
-
-// 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}"]`);
-
- // Determine the content ID based on whether the modal is the regular or secret one
- const isSecret = modalContent.closest('#secretConfigModal');
- const contentIdPrefix = isSecret ? 'secret-modal' : 'modal';
- const activeContent = document.getElementById(`${contentIdPrefix}-${tabName}-content`);
-
- if (activeBtn) activeBtn.classList.add('active');
- if (activeContent) activeContent.style.display = 'block';
-}
-
-export function setupModalTabs() {
- const configModalContent = document.getElementById('configModal')?.querySelector('.modal-content');
- if (configModalContent) {
- configModalContent.querySelectorAll('.tab-button').forEach(button => {
- button.addEventListener('click', (event) => {
- const tabName = event.target.getAttribute('data-tab');
- switchTab(configModalContent, tabName);
- });
- });
- }
-
- const secretModalContent = document.getElementById('secretConfigModal')?.querySelector('.modal-content');
- if (secretModalContent) {
- secretModalContent.querySelectorAll('.tab-button').forEach(button => {
- button.addEventListener('click', (event) => {
- const tabName = event.target.getAttribute('data-tab');
- switchTab(secretModalContent, tabName);
- });
- });
- }
-}
-
-// // =========================================================================
-// // CONFIG-SPECIFIC MODAL LAUNCHERS
-// // =========================================================================
-
-// (Removed internal launcher functions to avoid redundancy)
-
-// =========================================================================
-// FILTER CHAIN ADDITION LOGIC
-// =========================================================================
-
-/**
- * Shows the modal for adding a new filter chain to a listener.
- */
-export function showAddFilterChainModal(listenerName) {
- document.getElementById('add-fc-listener-name').value = listenerName;
- document.getElementById('add-fc-modal-title').textContent =
- `Add New Filter Chain to: ${listenerName}`;
- const yamlInput = document.getElementById('add-fc-yaml-input');
- yamlInput.value = '';
- document.getElementById('addFilterChainModal').style.display = 'block';
-
- yamlInput.placeholder =
-`# Paste your new Filter Chain YAML here.
-# NOTE: The root key should be the filter chain object itself.
-filter_chain_match:
- server_names: ["new.example.com"]
-filters:
- - name: envoy.filters.network.http_connection_manager
- typed_config:
- "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
- stat_prefix: new_route_http
- route_config:
- virtual_hosts:
- - name: new_service
- domains: ["new.example.com"]
- routes:
- - match: { prefix: "/" }
- route: { cluster: "new_backend_cluster" }
- http_filters:
- - name: envoy.filters.http.router
- typed_config:
- "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
-`;
-}
-
-/**
- * Handles the submission of the new filter chain YAML.
- */
-export async function submitNewFilterChain() {
- const listenerName = document.getElementById('add-fc-listener-name').value;
- const yamlData = document.getElementById('add-fc-yaml-input').value.trim();
- const upsertCheckbox = document.getElementById('add-filter-chain-upsert-flag');
- if (!yamlData) {
- alert("Please paste the filter chain YAML configuration.");
- return;
- }
-
- if (!listenerName) {
- alert("Listener name is missing. Cannot submit.");
- return;
- }
-
- const payload = { listener_name: listenerName, yaml: yamlData };
-
- if (upsertCheckbox && upsertCheckbox.checked) {
- payload.upsert = true;
- }
-
- try {
- const response = await fetch(`${API_BASE_URL}/append-filter-chain`, {
- 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}`);
- }
-
- alert(`Successfully update new filter chain to '${listenerName}'.`);
-
- document.getElementById('addFilterChainModal').style.display = 'none';
-
- cleanupConfigStore();
- window.listListeners?.();
- } catch (error) {
- console.error(`Failed to append filter chain to '${listenerName}':`, error);
- alert(`Failed to append filter chain. Check console for details. Error: ${error.message}`);
- }
-}
-
-/**
- * Closes the Add Filter Chain modal.
- */
-export function hideAddFilterChainModal() {
- document.getElementById('addFilterChainModal').style.display = 'none';
-}
-
-
-// =========================================================================
-// CONSISTENCY LOGIC
-// =========================================================================
-
-/**
- * Cleans up the cached YAML data in configStore for all resources.
- */
-export function cleanupConfigStore() {
- for (const name in configStore.clusters) {
- if (configStore.clusters.hasOwnProperty(name)) {
- configStore.clusters[name].yaml = 'Loading YAML...';
- }
- }
-
- for (const name in configStore.listeners) {
- if (configStore.listeners.hasOwnProperty(name)) {
- configStore.listeners[name].yaml = 'Loading YAML...';
- }
- }
- for (const name in configStore.secrets) {
- if (configStore.secrets.hasOwnProperty(name)) {
- configStore.secrets[name].yaml = 'Loading YAML...';
- }
- }
- for (const name in configStore.extension_configs) { // NEW: Cleanup ExtensionConfigs
- if (configStore.extension_configs.hasOwnProperty(name)) {
- configStore.extension_configs[name].yaml = 'Loading YAML...';
- }
- }
-}
-
-
-export const CONSISTENCY_POLL_INTERVAL = 5000;
-export let inconsistencyData = null;
-
-export function setInconsistencyData(data) {
- inconsistencyData = data;
-}
-
-export function showConsistencyModal() {
- if (!inconsistencyData || inconsistencyData.inconsistent === false) return;
-
- 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';
-}
-
-export function hideConsistencyModal() {
- document.getElementById('consistencyModal').style.display = 'none';
-}
-
-export async function checkConsistency() {
- const button = document.getElementById('consistency-button');
- if (!button) return;
-
- const hasPreviousConflict = inconsistencyData !== null;
- const isCurrentlyError = button.classList.contains('error');
-
- 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;
- const stateChanged = isNewConflict !== hasPreviousConflict;
-
- button.classList.remove('loading');
-
- if (isNewConflict) {
- button.textContent = '🚨 CONFLICT';
- if (stateChanged || isCurrentlyError) {
- button.classList.remove('consistent', 'error');
- button.classList.add('inconsistent');
- button.disabled = false;
- inconsistencyData = consistencyStatus;
- }
- } else {
- button.textContent = '✅ Consistent';
- if (stateChanged || isCurrentlyError) {
- 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);
- }
-}
-
-// (Removed resolveConsistency, manualFlush, manualRollback helper functions as they were commented out)
-
-export function downloadYaml() {
- const yamlContent = document.getElementById('modal-yaml-content').textContent;
- if (!yamlContent || yamlContent.trim() === '') {
- alert("No YAML content available to download.");
- return;
- }
-
- 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);
-}
-
-// (Removed manualFlush, manualRollback window attachments as the functions are removed)
-
-// Attach exported core functions to window for inline HTML calls
-window.showConfigModal = showConfigModal;
-window.hideModal = hideModal;
-window.showConsistencyModal = showConsistencyModal;
-window.hideConsistencyModal = hideConsistencyModal;
-window.checkConsistency = checkConsistency;
-window.downloadYaml = downloadYaml;
-window.showAddFilterChainModal = showAddFilterChainModal;
-window.hideAddFilterChainModal = hideAddFilterChainModal;
-window.submitNewFilterChain = submitNewFilterChain;
-window.cleanupConfigStore = cleanupConfigStore;
-
-// IMPORTED MODULES and function attachments to window
-import { listClusters, disableCluster, enableCluster, removeCluster, showAddClusterModal, hideAddClusterModal, submitNewCluster } from './clusters.js';
-import { loadAllData } from './data_loader.js';
-import {showDomainConfig} from '/data_fetchers.js'
-import {resolveConsistency} from './consistency.js'
-import { listSecrets,showAddSecretModal ,hideAddSecretModal, disableSecret, enableSecret, submitNewSecret, removeSecret, manualRenewCertificate, hideRotationSettingsModal} from './secrets.js';
-import { showListenerConfigModal ,showClusterConfigModal,showSecretConfigModal} from './modals.js';
-import { listListeners, removeFilterChainByRef, disableListener, enableListener, removeListener, showAddListenerModal, hideAddListenerModal, submitNewListener } from './listeners.js';
-import { listExtensionConfigs, showAddExtensionConfigModal, hideAddExtensionConfigModal, submitNewExtensionConfig, disableExtensionConfig, enableExtensionConfig, removeExtensionConfig } from './extension_configs.js'; // NEW IMPORT
-
-window.listClusters = listClusters;
-window.listSecrets = listSecrets;
-window.disableCluster = disableCluster;
-window.enableCluster = enableCluster;
-window.removeCluster = removeCluster;
-window.showAddClusterModal = showAddClusterModal;
-window.showAddSecretModal = showAddSecretModal;
-window.hideAddClusterModal = hideAddClusterModal;
-window.loadAllData = loadAllData;
-window.submitNewCluster = submitNewCluster;
-window.showDomainConfig = showDomainConfig;
-window.disableSecret = disableSecret;
-window.enableSecret = enableSecret;
-window.resolveConsistency = resolveConsistency;
-window.showClusterConfigModal = showClusterConfigModal;
-window.showListenerConfigModal = showListenerConfigModal;
-window.hideAddSecretModal = hideAddSecretModal;
-window.showSecretConfigModal = showSecretConfigModal;
-window.submitNewSecret = submitNewSecret;
-window.removeSecret = removeSecret;
-window.manualRenewCertificate = manualRenewCertificate;
-window.hideRotationSettingsModal = hideRotationSettingsModal;
-
-window.listListeners = listListeners;
-window.removeFilterChainByRef = removeFilterChainByRef;
-window.disableListener = disableListener;
-window.enableListener = enableListener;
-window.removeListener = removeListener;
-window.submitNewListener = submitNewListener;
-window.showAddListenerModal = showAddListenerModal;
-window.hideAddListenerModal = hideAddListenerModal;
-
-// NEW EXTENSION CONFIG WINDOW ATTACHMENTS
-window.listExtensionConfigs = listExtensionConfigs;
-window.showAddExtensionConfigModal = showAddExtensionConfigModal;
-window.hideAddExtensionConfigModal = hideAddExtensionConfigModal;
-window.submitNewExtensionConfig = submitNewExtensionConfig;
-window.disableExtensionConfig = disableExtensionConfig;
-window.enableExtensionConfig = enableExtensionConfig;
-window.removeExtensionConfig = removeExtensionConfig;
-
-window.onload = () => {
- window.loadAllData();
- setupModalTabs();
- checkConsistency();
- setInterval(checkConsistency, CONSISTENCY_POLL_INTERVAL);
-};
\ No newline at end of file
diff --git a/static/index.html b/static/index.html
index 73b21e2..3f16cf0 100755
--- a/static/index.html
+++ b/static/index.html
@@ -5,8 +5,9 @@
Envoy Configuration Dashboard
-
+
+
@@ -38,7 +39,8 @@
Cluster
-
+
+
Existing Listeners (Click a domain/filter for details)
@@ -120,36 +122,38 @@
-
+
-