diff options
Diffstat (limited to 'static/js/main.js')
-rw-r--r-- | static/js/main.js | 750 |
1 files changed, 750 insertions, 0 deletions
diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..74cd9c7 --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,750 @@ +// --- 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 = ` + <div class="service-item"> + <div class="status-light ${httpClass}"></div> + <span>HTTP: ${httpStatus}</span> + </div> + <div class="service-item"> + <div class="status-light ${sshClass}"></div> + <span>SSH: ${sshStatus}</span> + </div> + `; + } +} + +// --- 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 = `<h5>${displayName}</h5><div class="chart-container"><canvas id="${metricName}Chart"></canvas></div>`; + 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(); + } +});
\ No newline at end of file |