diff --git a/src/public/app.js b/src/public/app.js index 2ea942bed..8c4e682ab 100644 --- a/src/public/app.js +++ b/src/public/app.js @@ -96,7 +96,7 @@ document.addEventListener('DOMContentLoaded', function() { if (activeContent) { activeContent.classList.remove('hidden'); - // --- Handle nested tabs within 'main' --- + // --- Handle nested tabs within 'main' --- if (tabId === 'main') { // Ensure the 'Dashboard' nested tab is active by default when switching TO 'main' activateNestedTab('nested-tab-dashboard'); @@ -202,7 +202,6 @@ document.addEventListener('DOMContentLoaded', function() { }; // --- Threshold Logging State --- - let isThresholdLoggingActive = false; // Keep for potential UI state, but use activeLogSessions for logic let activeLogSessions = {}; // Object to hold multiple logging sessions { sessionId: { thresholds: {}, startTime: Date, durationMs: number, timerId: number, entries: [], element: node } } let thresholdLogEntries = []; let activeLoggingThresholds = null; // Will store the thresholds active at logging start @@ -339,6 +338,7 @@ document.addEventListener('DOMContentLoaded', function() { applyInitialFilterUI(); updateThresholdIndicator(); // Set initial indicator state + updateLogControlsVisibility(); // Check visibility on initial load // Get references to threshold row and toggle button const thresholdRow = document.getElementById('threshold-slider-row'); @@ -400,6 +400,7 @@ document.addEventListener('DOMContentLoaded', function() { updateDashboardTable(); saveFilterState(); // Save state whenever a threshold changes updateThresholdIndicator(); // Update badge count + updateLogControlsVisibility(); // Update button visibility } // Function to get formatted display text for a threshold type and value @@ -671,6 +672,7 @@ document.addEventListener('DOMContentLoaded', function() { updateDashboardTable(); // Update the main dashboard table if (searchInput) searchInput.dispatchEvent(new Event('input')); // Re-apply text filter saveFilterState(); // ---> ADDED: Save state <--- + updateLogControlsVisibility(); // Update button visibility } }); }); @@ -678,6 +680,7 @@ document.addEventListener('DOMContentLoaded', function() { if (searchInput) { searchInput.addEventListener('input', function() { updateDashboardTable(); + updateLogControlsVisibility(); // Update button visibility }); } else { console.warn('Element #dashboard-search not found - text filtering disabled.'); @@ -698,6 +701,7 @@ document.addEventListener('DOMContentLoaded', function() { updateDashboardTable(); // Re-render the table if (searchInput) searchInput.dispatchEvent(new Event('input')); // Re-apply text filter if needed saveFilterState(); // ---> ADDED: Save state <--- + updateLogControlsVisibility(); // Update button visibility } }); }); @@ -1389,17 +1393,9 @@ document.addEventListener('DOMContentLoaded', function() { refreshDashboardData(); // Ensure dashboardData is up-to-date with latest averages - // --- Define threshold prefixes used for search term filtering --- - // const thresholdPrefixes = ['cpu>=', 'memory>=', 'disk>=', 'uptime>=', 'diskread>=', 'diskwrite>=', 'netin>=', 'netout>=']; // REMOVED - // Get raw search terms, split by comma const textSearchTerms = searchInput ? searchInput.value.toLowerCase().split(',').map(term => term.trim()).filter(term => term) : []; - // Separate search terms from threshold filter tags - // const textSearchTerms = rawSearchTerms.filter(term => !thresholdPrefixes.some(prefix => term.startsWith(prefix))); // REMOVED separation - // Note: We don't actually need to *use* the threshold tags parsed from the search bar for filtering, - // because the filtering happens based on the actual thresholdState object below. - let filteredData = dashboardData.filter(guest => { // 1. Type Filter const typeMatch = filterGuestType === 'all' || (filterGuestType === 'vm' && guest.type === 'VM') || (filterGuestType === 'lxc' && guest.type === 'CT'); @@ -1711,11 +1707,6 @@ document.addEventListener('DOMContentLoaded', function() { function resetThresholds() { for (const type in thresholdState) { if (sliders[type]) { // Slider - // If logging is active while resetting, stop it. - if (isThresholdLoggingActive && toggleLogModeButton) { - toggleLogModeButton.click(); // Simulate click to stop logging cleanly - } - thresholdState[type].value = 0; const sliderElement = sliders[type]; if (sliderElement) sliderElement.value = 0; @@ -1759,6 +1750,8 @@ document.addEventListener('DOMContentLoaded', function() { updateDashboardTable(); // Update table AFTER resetting everything if (searchInput) searchInput.blur(); // Blur search input after reset + // Call visibility check AFTER resetting filters + updateLogControlsVisibility(); const backupStatusAllRadio = document.getElementById('backups-filter-status-all'); if(backupStatusAllRadio) backupStatusAllRadio.checked = true; @@ -2822,24 +2815,6 @@ document.addEventListener('DOMContentLoaded', function() { // --- START Threshold Logging Functions --- function startThresholdLogging() { - /* // REMOVED Check: Allow multiple logs - if (Object.keys(activeLogSessions).length > 0) { - console.warn("Threshold logging already active. Stop the current session first."); - // Optionally provide user feedback here (e.g., flash the button) - return; - } - */ - - // --- Get Duration --- - const durationMinutes = parseInt(logDurationSelect.value); // Use select value - if (isNaN(durationMinutes) || durationMinutes <= 0) { - alert("Please select a valid duration."); - logDurationSelect.focus(); - return; - } - const durationMs = durationMinutes * 60 * 1000; - // --- End Get Duration --- - // --- Snapshot current filter state --- const snapshottedThresholds = {}; let activeThresholdCount = 0; @@ -2922,6 +2897,11 @@ document.addEventListener('DOMContentLoaded', function() { if (tabToRemove && tabToRemove.classList.contains('active')) { activateNestedTab('nested-tab-dashboard'); } + + // Hide the entire log area if no log tabs are left + if (nestedTabsContainer && logSessionArea && nestedTabsContainer.querySelectorAll('.nested-tab[data-nested-tab^="log-session-"]').length === 0) { + logSessionArea.classList.add('hidden'); + } } }; @@ -2954,7 +2934,7 @@ document.addEventListener('DOMContentLoaded', function() { - Logging started for ${durationMinutes} min... + Logging started continuously... @@ -2976,7 +2956,6 @@ document.addEventListener('DOMContentLoaded', function() { newTab.innerHTML = ` ${startTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} - (--:--) `; newTab.addEventListener('click', () => activateNestedTab(nestedTabId)); @@ -3057,6 +3036,11 @@ document.addEventListener('DOMContentLoaded', function() { if (tabElement.classList.contains('active')) { activateNestedTab('nested-tab-dashboard'); } + + // Hide the entire log area if no log tabs are left + if (nestedTabsContainer && logSessionArea && nestedTabsContainer.querySelectorAll('.nested-tab[data-nested-tab^="log-session-"]').length === 0) { + logSessionArea.classList.add('hidden'); + } }; newTab.appendChild(tabCloseButton); // --- END Add Close Button to Tab --- @@ -3089,18 +3073,10 @@ document.addEventListener('DOMContentLoaded', function() { // } // REMOVED // --- END Append panel --- - // Start the timer to automatically stop the session - const timerId = setTimeout(() => { - // Pass 'timer' reason when stopped by timeout - stopThresholdLogging(sessionId, 'timer'); - }, durationMs); - // Store session info activeLogSessions[sessionId] = { thresholds: snapshottedThresholds, startTime: startTime, - durationMs: durationMs, - timerId: timerId, entries: [], element: panel, // Reference to the DOM element for easy removal searchTerms: snapshottedSearchTerms, // Store snapshotted search terms @@ -3112,7 +3088,7 @@ document.addEventListener('DOMContentLoaded', function() { // isThresholdLoggingActive = true; // REMOVED // logDurationSelect.disabled = true; // REMOVED - Keep select enabled - console.log(`[Log Session ${sessionId}] Started. Duration: ${durationMinutes}m. Thresholds:`, snapshottedThresholds); + console.log(`[Log Session ${sessionId}] Started. Thresholds:`, snapshottedThresholds); } function stopThresholdLogging(sessionId, reason = 'manual') { // Added reason parameter @@ -3124,20 +3100,8 @@ document.addEventListener('DOMContentLoaded', function() { console.log(`[Log Session ${sessionId}] Stopping. Reason: ${reason}`); - clearTimeout(session.timerId); // Clear the auto-stop timer regardless of reason - // --- Modifications for Persistence --- if (session.element) { - // 1. Disable the close button on the panel - /* // REMOVED THIS BLOCK - We want the 'x' to always remove the panel - const closeButton = session.element.querySelector('.log-session-header button'); - if (closeButton) { - closeButton.disabled = true; - closeButton.classList.add('opacity-50', 'cursor-not-allowed'); - closeButton.onclick = null; // Remove listener - } - */ - // 2. Add final status message to the table body (Less critical now, but keep for timer expiry case) const tableBody = session.element.querySelector(`#log-table-${sessionId} tbody`); if (tableBody) { @@ -3206,7 +3170,7 @@ document.addEventListener('DOMContentLoaded', function() { // Populate cells with comprehensive metric data - console.log(`[addLogRow] About to add row for ${entry.guestName} (${entry.guestId}) to session ${sessionId}`); // DEBUG + // console.log(`[addLogRow] About to add row for ${entry.guestName} (${entry.guestId}) to session ${sessionId}`); // DEBUG row.innerHTML = ` ${entry.timestamp.toLocaleTimeString()} ${guestDisplayHTML} @@ -3225,9 +3189,9 @@ document.addEventListener('DOMContentLoaded', function() { // --- DEBUG: Check if row is in DOM and potentially visible --- const addedRow = tableBody.firstChild; // Get the row we just prepended if (addedRow === row) { - console.log(`[addLogRow] Row for ${entry.guestName} successfully prepended to session ${sessionId}. Computed display: ${window.getComputedStyle(addedRow).display}`); + // console.log(`[addLogRow] Row for ${entry.guestName} successfully prepended to session ${sessionId}. Computed display: ${window.getComputedStyle(addedRow).display}`); } else { - console.error(`[addLogRow] Failed to prepend or find row for ${entry.guestName} in session ${sessionId}!`); + // console.error(`[addLogRow] Failed to prepend or find row for ${entry.guestName} in session ${sessionId}!`); } // --- END DEBUG --- @@ -3251,39 +3215,12 @@ function checkThresholdViolations() { // --- Get current UI filter state --- const currentFilterGuestType = filterGuestType; const currentFilterStatus = filterStatus; - // const rawSearchTerms = searchInput ? searchInput.value.toLowerCase().split(',').map(term => term.trim()).filter(term => term) : []; // REMOVED - // const currentTextSearchTerms = rawSearchTerms.filter(term => !thresholdPrefixes.some(prefix => term.startsWith(prefix))); // REMOVED // --- End Get current UI filter state --- // We assume refreshDashboardData() has been called recently by the main interval, // so dashboardData is up-to-date. dashboardData.forEach(guest => { - // --- REVERT: Remove checks for current UI filters --- - /* // REMOVED Block - // --- Apply Dashboard Filters BEFORE checking thresholds --- - // Must be running - if (guest.status !== 'running') return; - - // Type Filter - const typeMatch = currentFilterGuestType === 'all' || (currentFilterGuestType === 'vm' && guest.type === 'VM') || (currentFilterGuestType === 'lxc' && guest.type === 'CT'); - if (!typeMatch) return; // Skip if type doesn't match UI filter - - // Status Filter (Redundant if only checking running, but keep for consistency?) - const statusMatch = currentFilterStatus === 'all' || guest.status === currentFilterStatus; - if (!statusMatch) return; // Skip if status doesn't match UI filter - - // Text Search Filter - const searchMatch = currentTextSearchTerms.length === 0 || currentTextSearchTerms.some(term => - (guest.name && guest.name.toLowerCase().includes(term)) || - (guest.node && guest.node.toLowerCase().includes(term)) || - (guest.vmid && guest.vmid.toString().includes(term)) || - (guest.id && guest.id.toString().includes(term)) - ); - if (!searchMatch) return; // Skip if text search doesn't match - // --- End Apply Dashboard Filters --- - */ - // --- Keep only the check for running status --- if (guest.status !== 'running') return; // Only log for running guests // --- End Keep only check --- @@ -3292,7 +3229,7 @@ function checkThresholdViolations() { const session = activeLogSessions[sessionId]; if (!session) return; // Should not happen, but safety check - // --- Apply Snapshotted Type/Status Filters FIRST --- <<< Added + // --- Apply Snapshotted Type/Status Filters FIRST --- const sessionFilterGuestType = session.filterGuestType; const sessionFilterStatus = session.filterStatus; @@ -3305,7 +3242,7 @@ function checkThresholdViolations() { if (!statusMatch) return; // Skip guest if status doesn't match session filter // --- End Apply Snapshotted Type/Status Filters --- - // --- Apply Snapshotted Text Search Filter --- <<< Renamed section + // --- Apply Snapshotted Text Search Filter --- const snapshottedSearch = session.searchTerms || []; // Get snapshotted terms let guestMatchedSearch = false; // Flag specifically for guest match let nodeMatchedSearch = false; // Flag specifically for node match @@ -3339,7 +3276,6 @@ function checkThresholdViolations() { // --- Now check thresholds (if any were snapshotted) --- let meetsAllThresholds = true; // Start assuming guest meets all thresholds - // let shouldLogEntry = false; // Old OR logic flag let violationDetails = []; // Still useful to store which thresholds were active for context for (const type in session.thresholds) { @@ -3365,28 +3301,20 @@ function checkThresholdViolations() { } // Check for violation (guest value >= threshold limit) - // if (guestValue >= thresholdValue) { // Old OR check if (guestValue < thresholdValue) { // Check if guest FAILS this threshold - // valueFormatted = formatThresholdValue(type, guestValue); - // limitFormatted = formatThresholdValue(type, thresholdValue); - // shouldLogEntry = true; // Old OR logic flag - // violationDetails.push({ type: getReadableThresholdName(type), value: formatThresholdValue(type, guestValue), limit: formatThresholdValue(type, thresholdValue) }); meetsAllThresholds = false; // Guest failed at least one threshold // --- DEBUG LOG --- - console.log(`[Log Check FAIL] Guest: ${guest.name}, Metric: ${type}, Value: ${guestValue}, Threshold: ${thresholdValue}`); + // console.log(`[Log Check FAIL] Guest: ${guest.name}, Metric: ${type}, Value: ${guestValue}, Threshold: ${thresholdValue}`); // --- END DEBUG LOG --- break; // No need to check further thresholds for this guest } else { // --- DEBUG LOG --- - console.log(`[Log Check PASS] Guest: ${guest.name}, Metric: ${type}, Value: ${guestValue}, Threshold: ${thresholdValue}`); + // console.log(`[Log Check PASS] Guest: ${guest.name}, Metric: ${type}, Value: ${guestValue}, Threshold: ${thresholdValue}`); // --- END DEBUG LOG --- - // Optional: Store passed threshold details for context if needed - // violationDetails.push({ type: getReadableThresholdName(type), value: formatThresholdValue(type, guestValue), limit: formatThresholdValue(type, thresholdValue) }); } } // End loop through session thresholds // If the guest met ALL thresholds checked - // if (shouldLogEntry) { // Old OR check if (meetsAllThresholds) { // Gather all current metrics for the guest const metricsSnapshot = { @@ -3418,14 +3346,13 @@ function checkThresholdViolations() { activeThresholdKeys: Object.keys(session.thresholds), // Pass the keys for highlighting guestMatchedSearch: guestMatchedSearch, // <<< ADDED specific flag nodeMatchedSearch: nodeMatchedSearch // <<< ADDED specific flag - // violations: violationDetails // Removed for now, as only logging passes }; // Avoid logging the exact same violation repeatedly very quickly (debouncing) const lastEntry = session.entries.length > 0 ? session.entries[0] : null; // Check the most recent entry (inserted at top) if (!lastEntry || !(lastEntry.guestId === logEntry.guestId && - (now.getTime() - lastEntry.timestamp.getTime()) < 5000) // Simple time-based debounce per guest + (now.getTime() - lastEntry.timestamp.getTime()) < 1000) // Simple time-based debounce per guest (Reduced to 1s) ) { addLogRow(sessionId, logEntry); @@ -3435,24 +3362,6 @@ function checkThresholdViolations() { }); // End loop through guests } - // Add listener for the Start/Stop Log button - if (toggleLogModeButton) { - toggleLogModeButton.addEventListener('click', () => { - if (isThresholdLoggingActive) { - // Find the active session ID (assuming only one for now) - const activeSessionId = Object.keys(activeLogSessions)[0]; - if (activeSessionId) { - // Explicitly pass 'manual' when clicking the main button - stopThresholdLogging(activeSessionId, 'manual'); - } - } else { - startThresholdLogging(); - } - }); - } else { - console.warn('#toggle-log-mode-button not found.'); - } - // Add listener for the NEW Start Log button const startLogButton = document.getElementById('start-log-button'); if (startLogButton) { @@ -3522,16 +3431,6 @@ function checkThresholdViolations() { // --- START Reset Functions --- // Define standalone resetThresholds function function resetThresholds() { - /* // REMOVED: Don't stop active log on view reset - // If logging is active while resetting thresholds, stop it. - if (isThresholdLoggingActive) { - const activeSessionId = Object.keys(activeLogSessions)[0]; - if (activeSessionId) { - stopThresholdLogging(activeSessionId, 'manual'); // Stop manually to preserve logs - } - } - */ - for (const type in thresholdState) { if (sliders[type]) { // Slider thresholdState[type].value = 0; @@ -3580,7 +3479,6 @@ function resetThresholds() { // Extract search term like "Search: 'term'" const termMatch = search.match(/^Search: "(.+)"$/); if (termMatch) { - // parts.push(`S:'${termMatch[1].substring(0, 10)}'`); // Old format parts.push(`'${termMatch[1].substring(0, 10)}'`); // New format: just the term in quotes } } @@ -3683,45 +3581,54 @@ function resetThresholds() { } // --- END Search Bar Threshold Integration --- - // --- Global Timer Update Logic --- - function formatRemainingTime(ms) { - if (ms <= 0) return "00:00"; - const totalSeconds = Math.max(0, Math.floor(ms / 1000)); - const minutes = Math.floor(totalSeconds / 60); - const seconds = totalSeconds % 60; - return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; - } + // --- NEW Function to check filter state and toggle controls --- + function updateLogControlsVisibility() { + // console.log("[Debug] updateLogControlsVisibility called"); // Add log entry point + const startButton = document.getElementById('start-log-button'); - function updateAllLogTimers() { - const now = Date.now(); - // Select all visible timer spans first - document.querySelectorAll('.log-timer-display').forEach(timerSpan => { - const sessionId = timerSpan.dataset.sessionId; - const session = activeLogSessions[sessionId]; + if (!startButton) { + // console.error("[Debug] Start log button not found!"); + return; // Exit if start button not found + } - if (session) { - const endTime = session.startTime.getTime() + session.durationMs; - const remainingMs = endTime - now; - timerSpan.textContent = formatRemainingTime(remainingMs); + let isAnyFilterActive = false; + let activeReason = 'None'; // Store reason for debugging - // Optional: Change style when finished - if (remainingMs <= 0) { - timerSpan.classList.add('text-red-500', 'dark:text-red-400'); - } - - } else { - // If session is no longer active but span exists, ensure it shows 00:00 - // This might happen briefly if stopThresholdLogging hasn't updated it yet - if (timerSpan.textContent !== "00:00") { - timerSpan.textContent = "00:00"; - timerSpan.classList.add('text-red-500', 'dark:text-red-400'); - } + // 1. Check Thresholds + for (const type in thresholdState) { + if (thresholdState[type].value > 0) { + isAnyFilterActive = true; + activeReason = `Threshold: ${type}=${thresholdState[type].value}`; + break; } - }); - } + } + // console.log(`[Debug] After Thresholds Check: isAnyFilterActive=${isAnyFilterActive}, Reason=${activeReason}`); - // Start the global timer interval - setInterval(updateAllLogTimers, 1000); // Update every second - // --- END Global Timer Update Logic --- + // 2. Check Search Input (if thresholds not already active) + if (!isAnyFilterActive && searchInput && searchInput.value.trim() !== '') { + isAnyFilterActive = true; + activeReason = `Search: '${searchInput.value.trim()}'`; + } + // console.log(`[Debug] After Search Check: isAnyFilterActive=${isAnyFilterActive}, Reason=${activeReason}`); + + // 3. Check Type Filter (if still not active) + if (!isAnyFilterActive && filterGuestType !== 'all') { + isAnyFilterActive = true; + activeReason = `Type Filter: ${filterGuestType}`; + } + // console.log(`[Debug] After Type Filter Check: isAnyFilterActive=${isAnyFilterActive}, Reason=${activeReason}`); + + // 4. Check Status Filter (if still not active) + if (!isAnyFilterActive && filterStatus !== 'all') { + isAnyFilterActive = true; + activeReason = `Status Filter: ${filterStatus}`; + } + // console.log(`[Debug] After Status Filter Check: isAnyFilterActive=${isAnyFilterActive}, Reason=${activeReason}`); + + // Toggle button visibility + // console.log(`[Debug] Toggling button hidden state to: ${!isAnyFilterActive}`); + startButton.classList.toggle('hidden', !isAnyFilterActive); + } + // --- END NEW Function --- }); // End DOMContentLoaded