// --- JWT Constants --- const JWT_TOKEN_KEY = 'metrics_jwt_token'; // --- Global DOM Elements --- // These will be properly initialized in DOMContentLoaded let chartModal = null; let chartModalTitle = null; let closeChartModalBtn = null; let closeChartBtn = null; let expandedChart = null; let metricsSection = null; let viewMetricsBtn = null; let passwordModal = null; let passwordInput = null; let submitPasswordBtn = null; let closeModalBtn = null; let passwordError = null; let metricsControls = null; // --- DOM Ready Flag --- let domReady = false; // --- Server Stats Logic --- async function getServerStats() { const statusElement = document.getElementById('server-status'); const uptimeElement = document.getElementById('uptime'); if (!statusElement || !uptimeElement) { return; } try { const response = await fetch('/stat/.server-stats.json'); if (!response || !response.ok) { throw new Error(`Could not fetch server stats (status: ${response?.status})`); } const data = await response.json(); let status = "green"; const load1 = parseFloat(data.load_1min); const cores = parseInt(data.cores, 10) || 1; if (!isNaN(load1)) { if (load1 > (cores * 0.7)) status = "yellow"; if (load1 > (cores * 0.9)) status = "red"; } else { status = "yellow"; } statusElement.className = `status-light ${status}`; // More direct class setting statusElement.title = status === "green" ? "Server running normally" : status === "yellow" ? "Server under medium load" : status === "red" ? "Server under heavy load" : "Unknown status"; uptimeElement.textContent = `Uptime: ${data.uptime || 'Unknown'} (Load: ${data.load_1min || '?'}, ${data.load_5min || '?'}, ${data.load_15min || '?'})`; // Store service status in a global variable for later use when metrics are displayed window.serviceStatusData = { http_status: data.service_http_status, ssh_status: data.service_ssh_status }; // Update the service status if it's currently visible updateServiceStatusDisplay(); } catch (error) { statusElement.className = 'status-light yellow'; statusElement.title = "Status check failed - server may still be operational"; uptimeElement.textContent = 'Status: Available (Stats unavailable)'; // Clear service status data on error window.serviceStatusData = null; } } // Function to create/update service status display function updateServiceStatusDisplay() { // Only update if metrics section is visible if (metricsSection && metricsSection.style.display === 'block' && window.serviceStatusData) { let serviceStatusElement = document.getElementById('service-status'); // Create the element if it doesn't exist if (!serviceStatusElement) { serviceStatusElement = document.createElement('div'); serviceStatusElement.id = 'service-status'; serviceStatusElement.className = 'service-status'; // Insert at the beginning of metrics section for better visibility if (metricsSection.firstChild) { metricsSection.insertBefore(serviceStatusElement, metricsSection.firstChild); } else { metricsSection.appendChild(serviceStatusElement); } } // Update the content const httpStatus = window.serviceStatusData.http_status === 1 ? "Up" : "Down"; const sshStatus = window.serviceStatusData.ssh_status === 1 ? "Up" : "Down"; const httpClass = window.serviceStatusData.http_status === 1 ? "green" : "red"; const sshClass = window.serviceStatusData.ssh_status === 1 ? "green" : "red"; serviceStatusElement.innerHTML = `
HTTP: ${httpStatus}
SSH: ${sshStatus}
`; } } // --- Metrics Authentication & Display Logic --- // Constants and variables will now be initialized when DOM is ready // Functions stay decoupled from global DOM element references function getToken() { return localStorage.getItem(JWT_TOKEN_KEY); } function setToken(token) { localStorage.setItem(JWT_TOKEN_KEY, token); } function removeToken() { localStorage.removeItem(JWT_TOKEN_KEY); } function showMetrics() { if (!metricsSection) { return; } try { metricsSection.style.display = 'block'; createOrShowHideButton(); // Display the service status when metrics are shown updateServiceStatusDisplay(); // Initialize metrics UI if not already initialized if (!chartsInitialized) { initializeMetricsUI(); } else { startDataUpdates(); } } catch (error) { alert("There was an error displaying the metrics. Please try refreshing the page."); } } function hideMetrics() { if (!metricsSection) return; metricsSection.style.display = 'none'; removeHideButton(); // Remove the service status element when metrics are hidden const serviceStatusElement = document.getElementById('service-status'); if (serviceStatusElement) { serviceStatusElement.remove(); } if (typeof stopDataUpdates === 'function') { stopDataUpdates(); } } function createOrShowHideButton() { if (!metricsControls) return; let hideBtn = document.getElementById('hide-metrics-btn'); if (!hideBtn) { hideBtn = document.createElement('button'); hideBtn.id = 'hide-metrics-btn'; hideBtn.className = 'button'; hideBtn.textContent = 'Hide Server Metrics'; hideBtn.style.marginLeft = '10px'; hideBtn.addEventListener('click', () => { removeToken(); // Optionally clear token on hide hideMetrics(); }); metricsControls.appendChild(hideBtn); } hideBtn.style.display = 'inline-block'; // Ensure it's visible } function removeHideButton() { const hideBtn = document.getElementById('hide-metrics-btn'); if (hideBtn) { hideBtn.remove(); } } function openPasswordModal() { if (!passwordModal || !passwordInput || !passwordError) return; passwordInput.value = ''; passwordError.textContent = ''; passwordError.style.opacity = 0; passwordModal.style.display = 'block'; setTimeout(() => { passwordModal.classList.add('show'); passwordInput.focus(); }, 10); } function closePasswordModal() { if (!passwordModal) return; passwordModal.classList.remove('show'); setTimeout(() => { passwordModal.style.display = 'none'; }, 300); } async function login() { if (!passwordInput || !passwordError) return; const password = passwordInput.value; if (!password) { showPasswordError('Please enter a password'); return; } try { const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: password }) }); if (response.ok) { const data = await response.json(); if (data.token) { setToken(data.token); // Store JWT closePasswordModal(); showMetrics(); } else { showPasswordError('Login failed: No token received.'); } } else { const errorData = await response.json().catch(() => ({ error: 'Unknown login error' })); showPasswordError(`Login failed: ${errorData.error || response.statusText}`); passwordInput.value = ''; } } catch (error) { showPasswordError('Login request error. Check connection.'); } } function showPasswordError(message) { if (!passwordError || !passwordInput) return; passwordError.textContent = message; passwordError.style.opacity = 1; passwordInput.classList.add('shake'); setTimeout(() => { passwordInput.classList.remove('shake'); }, 500); } // --- SNMP Charts Logic --- const API_ENDPOINT = '/api/metrics'; const UPDATE_INTERVAL = 10000; const CHART_HISTORY = 60; let charts = {}; let chartsInitialized = false; let updateIntervalId; let metricsDefinitions = {}; const chartColors = { networkIn: '#88B7B5', networkOut: '#FDCFF3', cpu: '#88B7B5', memory: '#FDCFF3', system: '#88B7B5', generic: '#88B7B5', // Colors for application performance metrics appResponse: '#4CAF50', // Green for response time appError: '#F44336', // Red for error rate appRequests: '#2196F3', // Blue for request count serviceStatus: '#FF9800' // Orange for service status }; // formatBytes, formatTime, calculateRates - Keep these utility functions as they were function formatBytes(bytes, decimals = 2) { if (bytes === undefined || bytes === null || bytes === 0) return '0 Bytes'; const k = 1024, dm = decimals < 0 ? 0 : decimals, sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } function formatTime(centiseconds) { if (!centiseconds || centiseconds < 0) return '0s'; const totalSeconds = Math.floor(centiseconds / 100); const days = Math.floor(totalSeconds / 86400), hours = Math.floor((totalSeconds % 86400) / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60), seconds = totalSeconds % 60; if (days > 0) return `${days}d ${hours}h ${minutes}m`; if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`; if (minutes > 0) return `${minutes}m ${seconds}s`; return `${seconds}s`; } function calculateRates(data) { if (!data || data.length < 2) { return []; } const rates = []; for (let i = 1; i < data.length; i++) { const timeDiff = (data[i].timestamp - data[i-1].timestamp) / 1000; const valueDiff = data[i].value - data[i-1].value; if (timeDiff < 0.001) continue; // Detect counter wrap/reset (when a counter resets to 0) const rate = valueDiff >= 0 ? valueDiff / timeDiff : (4294967295 + valueDiff) / timeDiff; rates.push({ timestamp: data[i].timestamp, value: rate }); } return rates; } function createChartContainers(metrics) { if (!domReady) { setTimeout(() => createChartContainers(metrics), 300); return; } if (!metricsSection) { metricsSection = document.getElementById('metrics-section'); if (!metricsSection) { return; } } const metricGrid = metricsSection.querySelector('.metric-grid'); if (!metricGrid) { return; } try { metricGrid.innerHTML = ''; // Clear previous charts = {}; // Clear chart objects // Group the metrics into two sections const networkAndSystemMetrics = {}; const appPerformanceMetrics = {}; const serviceStatusMetrics = {}; // First categorize the metrics for (const [metricName, definition] of Object.entries(metrics)) { // Skip system_uptime and memory metrics (as requested) if (metricName === 'system_uptime' || metricName.includes('memory_total') || metricName === 'memory_size') { continue; } // Categorize based on metric name if (metricName.startsWith('app_')) { appPerformanceMetrics[metricName] = definition; } else if (metricName.startsWith('service_')) { serviceStatusMetrics[metricName] = definition; } else { networkAndSystemMetrics[metricName] = definition; } } // Create section headers and containers if (Object.keys(networkAndSystemMetrics).length > 0) { const sectionHeader = document.createElement('h4'); sectionHeader.textContent = 'Network & System Metrics'; sectionHeader.className = 'metric-section-header'; metricGrid.appendChild(sectionHeader); // Create containers for network & system metrics for (const [metricName, definition] of Object.entries(networkAndSystemMetrics)) { createMetricCard(metricGrid, metricName, definition); } } // Application Performance section if (Object.keys(appPerformanceMetrics).length > 0) { const sectionHeader = document.createElement('h4'); sectionHeader.textContent = 'Application Performance'; sectionHeader.className = 'metric-section-header'; metricGrid.appendChild(sectionHeader); // Create containers for app performance metrics for (const [metricName, definition] of Object.entries(appPerformanceMetrics)) { createMetricCard(metricGrid, metricName, definition); } } } catch (error) { } } function createMetricCard(container, metricName, definition) { if (!container) return; try { const displayName = definition.label || metricName; const metricCard = document.createElement('div'); metricCard.className = 'metric-card'; metricCard.innerHTML = `
${displayName}
`; container.appendChild(metricCard); // Add click event for chart expansion const chartContainer = metricCard.querySelector('.chart-container'); if (chartContainer) { chartContainer.addEventListener('click', function() { try { const metricAttr = this.getAttribute('data-metric'); const labelAttr = this.getAttribute('data-label'); if (metricAttr && labelAttr && typeof expandChart === 'function') { // Wait until DOM is ready before trying to expand if (domReady) { expandChart(metricAttr, labelAttr); } else { setTimeout(() => { if (typeof expandChart === 'function') { expandChart(metricAttr, labelAttr); } }, 500); } } } catch (error) { alert(`Could not expand the ${displayName} chart. Please try again later.`); } }); } } catch (error) { } } function startDataUpdates() { if (updateIntervalId) { return; // Don't start if already running } updateCharts(); updateIntervalId = setInterval(updateCharts, UPDATE_INTERVAL); } function stopDataUpdates() { if (updateIntervalId) { clearInterval(updateIntervalId); updateIntervalId = null; } } async function fetchWithAuth(url, options = {}) { const token = getToken(); const headers = { ...options.headers }; if (token) { headers['Authorization'] = `Bearer ${token}`; } try { const response = await fetch(url, { ...options, headers }); if (response.status === 401) { // Unauthorized - Token is invalid or expired removeToken(); hideMetrics(); // Hide metrics section openPasswordModal(); // Prompt for login throw new Error('Unauthorized'); // Prevent further processing } return response; } catch (error) { throw error; } } function initCharts(metrics) { // Common options are reused from previous logic const commonOptions = { responsive: true, maintainAspectRatio: false, animation: { duration: 300 }, scales: { x: { type: 'time', time: { unit: 'minute', tooltipFormat: 'HH:mm:ss' }, grid: { color: 'rgba(255, 255, 255, 0.1)' }, ticks: { color: '#E8F1F2', maxTicksLimit: 5 } }, y: { beginAtZero: true, grid: { color: 'rgba(255, 255, 255, 0.1)' }, ticks: { color: '#E8F1F2' } } }, plugins: { legend: { display: true, labels: { color: '#E8F1F2', font: { family: "'IBM Plex Sans', sans-serif" } } }, tooltip: { enabled: true, mode: 'index', intersect: false, backgroundColor: 'rgba(17, 17, 17, 0.8)', titleColor: '#FDCFF3', bodyColor: '#E8F1F2', borderColor: '#333', borderWidth: 1 } } }; for (const [metricName, definition] of Object.entries(metrics)) { // Skip system_uptime and memory metrics (as requested) if (metricName === 'system_uptime' || metricName.includes('memory_total') || metricName === 'memory_size') { continue; } // Skip service status metrics - they're shown in the server stats if (metricName.startsWith('service_')) { continue; } const canvas = document.getElementById(`${metricName}Chart`); if (!canvas) { continue; } const ctx = canvas.getContext('2d'); if (!ctx) { continue; } let chartOptions = JSON.parse(JSON.stringify(commonOptions)); // Deep clone options let datasets = []; let label = definition.label || metricName; let color = chartColors.generic; // Configure chart based on metric type if (metricName === 'network_in' || metricName === 'network_out') { chartOptions.scales.y.title = { display: true, text: 'Bytes/sec', color: '#E8F1F2' }; chartOptions.scales.y.ticks.callback = function(value) { return formatBytes(value, 0); }; label = metricName === 'network_in' ? 'In' : 'Out'; color = metricName === 'network_in' ? chartColors.networkIn : chartColors.networkOut; } else if (metricName === 'cpu_load') { chartOptions.scales.y.title = { display: true, text: 'Load/Usage', color: '#E8F1F2' }; label = 'CPU'; color = chartColors.cpu; } else if (metricName === 'system_processes') { chartOptions.scales.y.title = { display: true, text: 'Count', color: '#E8F1F2' }; label = 'Processes'; color = chartColors.system; } // New application performance metrics else if (metricName === 'app_response_time_avg') { chartOptions.scales.y.title = { display: true, text: 'Time (ms)', color: '#E8F1F2' }; label = 'Response Time'; color = chartColors.appResponse; } else if (metricName === 'app_error_rate') { chartOptions.scales.y.title = { display: true, text: 'Error Rate (%)', color: '#E8F1F2' }; label = 'Error Rate'; color = chartColors.appError; } else if (metricName === 'app_request_count') { chartOptions.scales.y.title = { display: true, text: 'Requests', color: '#E8F1F2' }; label = 'Request Count'; color = chartColors.appRequests; } else { chartOptions.scales.y.title = { display: true, text: 'Value', color: '#E8F1F2' }; } datasets = [{ label: label, borderColor: color, backgroundColor: `${color}33`, borderWidth: 2, data: [], pointRadius: 0, fill: true }]; // Destroy existing chart if it exists to prevent memory leaks if (charts[metricName]) { try { charts[metricName].destroy(); } catch (e) { // Silent error handling } } try { charts[metricName] = new Chart(ctx, { type: 'line', data: { datasets }, options: chartOptions }); } catch (error) { // Silent error handling } } chartsInitialized = true; } // Modified to handle sparse data better async function updateCharts() { // Only run if metrics section is visible if (!metricsSection || metricsSection.style.display === 'none') return; try { const response = await fetchWithAuth(API_ENDPOINT); if (!response.ok) { return; } const data = await response.json(); // Combine metrics from both regular metrics and app performance metrics const allMetrics = { ...data.metrics }; // Update chart data for each metric for (const [metricName, metricData] of Object.entries(allMetrics)) { // Skip metrics we're not displaying if (!charts[metricName]) { continue; } if (!metricData || !Array.isArray(metricData) || metricData.length === 0) { continue; } // Process the data let chartData; // For network metrics, try to calculate rates, but fall back to actual values if needed if (metricName === 'network_in' || metricName === 'network_out') { chartData = calculateRates(metricData); // If rate calculation failed due to insufficient data, use the raw values directly if (!chartData || chartData.length === 0) { chartData = [...metricData]; } } else { // For other metrics, use the data directly chartData = [...metricData]; } if (!chartData || !Array.isArray(chartData) || chartData.length === 0) { continue; } // Map the data to chart format const formattedData = chartData.map(point => { if (!point || typeof point !== 'object' || point.timestamp === undefined || point.value === undefined) { return null; } let value = point.value; if (typeof value === 'string') { const match = value.match(/[\d.]+/); // Allow decimals value = match ? parseFloat(match[0]) : 0; } else if (typeof value !== 'number') { value = 0; } return { x: point.timestamp, y: value }; }).filter(point => point !== null); if (formattedData.length === 0) { continue; } // Update the chart - wrap in try/catch to prevent errors from breaking all charts try { charts[metricName].data.datasets[0].data = formattedData; charts[metricName].update('none'); // Use 'none' mode for better performance } catch (e) { // Silent error handling } } } catch (error) { if (error.message !== 'Unauthorized') { // Silent error handling } } } // Modified initializeMetricsUI to better handle edge cases function initializeMetricsUI() { if (chartsInitialized) { startDataUpdates(); return; } // Ensure we are authenticated before trying to fetch if (!getToken()) { openPasswordModal(); return; } // Fetch metrics data fetchWithAuth(API_ENDPOINT) .then(response => { if (!response.ok) { return Promise.reject(new Error(`HTTP error: ${response.status}`)); } return response.json(); }) .then(data => { if (!data || typeof data !== 'object') { return Promise.reject(new Error("Invalid metrics data format")); } if (!data.metrics || !data.definitions) { // Create empty objects if missing rather than failing data.metrics = data.metrics || {}; data.definitions = data.definitions || {}; } // Store definitions metricsDefinitions = data.definitions || {}; // Check specifically for app performance metrics const appMetrics = Object.keys(metricsDefinitions) .filter(key => key.startsWith('app_')); if (appMetrics.length === 0) { // Add placeholder definitions if they don't exist if (!metricsDefinitions['app_response_time_avg']) { metricsDefinitions['app_response_time_avg'] = { label: 'Avg Response Time (ms)', type: 'calculated' }; } if (!metricsDefinitions['app_error_rate']) { metricsDefinitions['app_error_rate'] = { label: 'Error Rate (%)', type: 'calculated' }; } if (!metricsDefinitions['app_request_count']) { metricsDefinitions['app_request_count'] = { label: 'Request Count', type: 'calculated' }; } } // Create UI createChartContainers(metricsDefinitions); initCharts(metricsDefinitions); // Start updates startDataUpdates(); }) .catch(error => { if (error.message === 'Unauthorized') { openPasswordModal(); } else { alert('Error loading metrics. Please try again.'); } }); } // Function to handle metrics visibility changes function handleMetricsVisibilityChange() { if (!metricsSection) return; try { if (metricsSection.offsetParent !== null) { // Metrics section is visible if (!chartsInitialized) { initializeMetricsUI(); } else { startDataUpdates(); } } else { // Metrics section is hidden stopDataUpdates(); } } catch (error) { // Don't show alert here since this might be called repeatedly // Just log the error to console } } // --- Chart expansion functionality --- function expandChart(metricName, displayName) { // First ensure the chart modal exists (create it if needed) ensureChartModalExists(); // After ensuring the chart modal exists, get fresh references const chartModal = document.getElementById('chart-modal'); const chartModalTitle = document.getElementById('chart-modal-title'); const expandedChartCanvas = document.getElementById('expandedChart'); // Check that we have all required elements if (!chartModal || !chartModalTitle || !expandedChartCanvas) { alert("Unable to display expanded chart. Please refresh the page and try again."); return; } // Set the modal title chartModalTitle.textContent = displayName; // Show the modal chartModal.style.display = 'block'; setTimeout(() => chartModal.classList.add('show'), 10); const ctx = expandedChartCanvas.getContext('2d'); if (!ctx) { return; } // If there's already an expanded chart, destroy it first if (window.expandedChart) { try { window.expandedChart.destroy(); } catch (error) { } } // Clone the options and data from the original chart const originalChart = charts[metricName]; if (!originalChart) { return; } try { const chartOptions = JSON.parse(JSON.stringify(originalChart.options)); // Adjust options for the expanded view chartOptions.maintainAspectRatio = false; if (chartOptions.scales && chartOptions.scales.y) { chartOptions.scales.y.ticks.maxTicksLimit = 10; // More ticks for expanded view } // Create the expanded chart with cloned data const chartData = { datasets: originalChart.data.datasets.map(dataset => ({ ...dataset, data: [...dataset.data], pointRadius: 3 // Show points in expanded view })) }; window.expandedChart = new Chart(ctx, { type: 'line', data: chartData, options: chartOptions }); } catch (error) { } } // Close modal events function closeChartModal(modalElement) { // If called with a specific modal element, use that // Otherwise try to get it from the DOM const chartModal = modalElement || document.getElementById('chart-modal'); if (!chartModal) { return; } try { chartModal.classList.remove('show'); setTimeout(() => { chartModal.style.display = 'none'; }, 300); } catch (error) { } } // Ensure chart modal exists in the DOM function ensureChartModalExists() { let chartModalEl = document.getElementById('chart-modal'); // If the chart modal doesn't exist, create it programmatically if (!chartModalEl) { ensureModalStyles(); // Create the modal elements chartModalEl = document.createElement('div'); chartModalEl.id = 'chart-modal'; chartModalEl.className = 'modal'; const modalContent = document.createElement('div'); modalContent.className = 'modal-content chart-modal-content'; const closeButton = document.createElement('span'); closeButton.className = 'close-button'; closeButton.innerHTML = '×'; const modalTitle = document.createElement('h3'); modalTitle.id = 'chart-modal-title'; modalTitle.textContent = 'Chart Details'; const chartContainer = document.createElement('div'); chartContainer.className = 'expanded-chart-container'; const canvas = document.createElement('canvas'); canvas.id = 'expandedChart'; // Assemble the modal chartContainer.appendChild(canvas); modalContent.appendChild(closeButton); modalContent.appendChild(modalTitle); modalContent.appendChild(chartContainer); chartModalEl.appendChild(modalContent); // Add to document body document.body.appendChild(chartModalEl); // Set up event listeners closeButton.addEventListener('click', () => closeChartModal(chartModalEl)); chartModalEl.addEventListener('click', (event) => { if (event.target === chartModalEl) { closeChartModal(chartModalEl); } }); // Update global references chartModal = chartModalEl; chartModalTitle = modalTitle; closeChartModalBtn = closeButton; closeChartBtn = null; // No longer used return true; } return false; } // Ensure modal styles are added to the page function ensureModalStyles() { // Check if we already have a style tag with our styles if (document.getElementById('dynamic-modal-styles')) { return; } // Create a style element const styleEl = document.createElement('style'); styleEl.id = 'dynamic-modal-styles'; // Define CSS for modals const css = ` .modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); z-index: 1000; opacity: 0; transition: opacity 0.3s ease; overflow-y: auto; } .modal.show { opacity: 1; } .modal-content { background-color: #111; margin: 10% auto; padding: 20px; border: 1px solid #333; width: 80%; max-width: 500px; border-radius: 8px; box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); transform: translateY(-20px); transition: transform 0.3s ease; animation: modalFadeIn 0.3s forwards; position: relative; /* Add positioning context */ } .modal.show .modal-content { transform: translateY(0); } @keyframes modalFadeIn { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } } .close-button { position: absolute; top: 10px; right: 15px; color: #aaa; font-size: 28px; font-weight: bold; cursor: pointer; transition: color 0.3s ease; line-height: 20px; margin: 0; padding: 0; z-index: 10; } .close-button:hover { color: #FDCFF3; text-decoration: none; } .chart-modal-content { max-width: 800px; width: 90%; } .expanded-chart-container { width: 100%; height: 400px; margin: 20px 0; } /* Add more space at the top of modal content for close button */ .chart-modal-content h3 { margin-top: 10px; padding-right: 30px; /* Make room for the close button */ } `; // Add the styles to the page styleEl.textContent = css; document.head.appendChild(styleEl); } // --- Initialize everything when DOM is loaded --- document.addEventListener('DOMContentLoaded', function() { // Set DOM ready flag domReady = true; // Try to find chart modal elements first chartModal = document.getElementById('chart-modal'); chartModalTitle = document.getElementById('chart-modal-title'); closeChartModalBtn = document.querySelector('#chart-modal .close-button'); // Bottom close button has been removed closeChartBtn = null; // If chart modal doesn't exist, create it if (!chartModal || !chartModalTitle || !closeChartModalBtn) { const created = ensureChartModalExists(); if (created) { } } // Initialize other DOM elements metricsSection = document.getElementById('metrics-section'); viewMetricsBtn = document.getElementById('view-metrics-btn'); passwordModal = document.getElementById('password-modal'); passwordInput = document.getElementById('metrics-password'); submitPasswordBtn = document.getElementById('submit-password'); passwordError = document.getElementById('password-error'); metricsControls = document.getElementById('metrics-controls'); // For debugging - log all initialized elements console.log("DOM elements initialized:", { chartModal: !!chartModal, chartModalTitle: !!chartModalTitle, closeChartModalBtn: !!closeChartModalBtn, metricsSection: !!metricsSection, viewMetricsBtn: !!viewMetricsBtn, passwordModal: !!passwordModal }); // Dispatch a custom event to indicate DOM is ready document.dispatchEvent(new Event('dom-fully-ready')); // Start server stats check getServerStats(); setInterval(getServerStats, 30000); // Apply styles to submit button if (submitPasswordBtn) submitPasswordBtn.classList.add('submit-button'); // Set up password input event listener if (passwordInput) { passwordInput.addEventListener('keyup', function(event) { if (event.key === 'Enter') { submitPasswordBtn.click(); } }); } // Set up view metrics button if (viewMetricsBtn) { viewMetricsBtn.addEventListener('click', function() { if (getToken()) { showMetrics(); } else { openPasswordModal(); } }); } // Set up password modal submit button if (submitPasswordBtn) { submitPasswordBtn.addEventListener('click', login); } // Set up password modal close button const passwordCloseBtn = document.querySelector('#password-modal .close-button'); if (passwordCloseBtn) { passwordCloseBtn.addEventListener('click', closePasswordModal); } // Close password modal when clicking outside if (passwordModal) { passwordModal.addEventListener('click', function(event) { if (event.target === passwordModal) { closePasswordModal(); } }); } // Check if already authenticated and show metrics if so if (getToken()) showMetrics(); // Observer for metrics section visibility if (metricsSection) { const observer = new MutationObserver(handleMetricsVisibilityChange); observer.observe(metricsSection, { attributes: true, attributeFilter: ['style'] }); } // Close modals with Escape key document.addEventListener('keydown', function(event) { if (event.key === 'Escape') { const chartModal = document.getElementById('chart-modal'); if (chartModal && chartModal.classList.contains('show')) { closeChartModal(chartModal); } else if (passwordModal && passwordModal.style.display === 'block') { closePasswordModal(); } } }); }); // Ensure chart elements are initialized when DOM is fully ready document.addEventListener('dom-fully-ready', function() { // Ensure the chart modal exists const created = ensureChartModalExists(); if (created) { } // Get fresh references to chart elements const chartModalEl = document.getElementById('chart-modal'); const chartModalTitleEl = document.getElementById('chart-modal-title'); const closeChartModalBtnEl = document.querySelector('#chart-modal .close-button'); // Store in global variables for access in other functions if (chartModalEl) chartModal = chartModalEl; if (chartModalTitleEl) chartModalTitle = chartModalTitleEl; if (closeChartModalBtnEl) closeChartModalBtn = closeChartModalBtnEl; // Verify modal elements were found const allElementsFound = !!chartModal && !!chartModalTitle && !!closeChartModalBtn; console.log("Chart elements initialized on dom-fully-ready event:", { chartModal: !!chartModal, chartModalTitle: !!chartModalTitle, closeChartModalBtn: !!closeChartModalBtn }); if (!allElementsFound) { } });