// --- JWT Constants --- const JWT_TOKEN_KEY = 'metrics_jwt_token'; // --- 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 --- const viewMetricsBtn = document.getElementById('view-metrics-btn'); const passwordModal = document.getElementById('password-modal'); const passwordInput = document.getElementById('metrics-password'); const submitPasswordBtn = document.getElementById('submit-password'); const closeModalBtn = document.querySelector('.close-button'); const passwordError = document.getElementById('password-error'); const metricsSection = document.getElementById('metrics-section'); const metricsControls = document.getElementById('metrics-controls'); // Container for buttons 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; } 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(); } } 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', // New 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) { const metricGrid = document.querySelector('.metric-grid'); if (!metricGrid) { return; } 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); } } // Service Status section (not displayed as a chart - we already added it to server stats) // We skip adding charts for service_* metrics } function createMetricCard(container, metricName, definition) { const displayName = definition.label || metricName; const metricCard = document.createElement('div'); metricCard.className = 'metric-card'; metricCard.innerHTML = `
${displayName}
`; container.appendChild(metricCard); } 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.'); } }); } // --- Initial Load Logic --- document.addEventListener('DOMContentLoaded', function() { // Start server stats check immediately and then interval getServerStats(); setInterval(getServerStats, 30000); // Style the submit button if (submitPasswordBtn) submitPasswordBtn.classList.add('submit-button'); // Event listeners for authentication if (viewMetricsBtn) { viewMetricsBtn.addEventListener('click', () => { // Authenticate and show metrics const token = getToken(); if (token) { // Try to initialize/show metrics, fetchWithAuth will handle invalid tokens showMetrics(); } else { openPasswordModal(); } }); } if (submitPasswordBtn) { submitPasswordBtn.addEventListener('click', login); } if (passwordInput) { passwordInput.addEventListener('keyup', (event) => { if (event.key === 'Enter') login(); }); } if (closeModalBtn) { closeModalBtn.addEventListener('click', closePasswordModal); } window.addEventListener('click', (event) => { if (event.target === passwordModal) closePasswordModal(); }); // Check token on load - attempt to show metrics if token exists // initializeMetricsUI() will handle token validation via fetchWithAuth if (getToken()) { showMetrics(); } else { // Ensure metrics are hidden if no token hideMetrics(); } });