// --- 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 = `
`;
}
}
// --- 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) {
}
});