// ============================================================================ // Collapsible Section Toggle // ============================================================================ function toggleSection(headerElement) { const contentElement = headerElement.nextElementSibling; if (headerElement.classList.contains('collapsed')) { // Expand headerElement.classList.remove('collapsed'); contentElement.classList.remove('collapsed'); } else { // Collapse headerElement.classList.add('collapsed'); contentElement.classList.add('collapsed'); } } // ============================================================================ // Constants and Utilities // ============================================================================ // Constants for log message types const LOG_TYPE = { SUCCESS: 'success', WARNING: 'warning', ERROR: 'error', INFO: 'info', DEBUG: 'debug' }; // Helper function to convert provider name to camelCase for ID lookup // e.g., "dw_leds" -> "DwLeds", "wled" -> "Wled", "none" -> "None" function providerToCamelCase(provider) { return provider.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1) ).join(''); } // Constants for cache const CACHE_KEYS = { CONNECTION_STATUS: 'connection_status', LAST_UPDATE: 'last_status_update' }; const CACHE_DURATION = 5000; // 5 seconds cache duration // Function to log messages function logMessage(message, type = LOG_TYPE.DEBUG) { console.log(`[${type}] ${message}`); } // Function to get cached connection status function getCachedConnectionStatus() { const cachedData = localStorage.getItem(CACHE_KEYS.CONNECTION_STATUS); const lastUpdate = localStorage.getItem(CACHE_KEYS.LAST_UPDATE); if (cachedData && lastUpdate) { const now = Date.now(); const cacheAge = now - parseInt(lastUpdate); if (cacheAge < CACHE_DURATION) { return JSON.parse(cachedData); } } return null; } // Function to set cached connection status function setCachedConnectionStatus(data) { localStorage.setItem(CACHE_KEYS.CONNECTION_STATUS, JSON.stringify(data)); localStorage.setItem(CACHE_KEYS.LAST_UPDATE, Date.now().toString()); } // Function to update serial connection status async function updateSerialStatus(forceUpdate = false) { try { // Check cache first unless force update is requested if (!forceUpdate) { const cachedData = getCachedConnectionStatus(); if (cachedData) { updateConnectionUI(cachedData); return; } } const response = await fetch('/serial_status'); if (response.ok) { const data = await response.json(); setCachedConnectionStatus(data); updateConnectionUI(data); } } catch (error) { logMessage(`Error checking serial status: ${error.message}`, LOG_TYPE.ERROR); } } // Function to update UI based on connection status function updateConnectionUI(data) { const statusElement = document.getElementById('serialStatus'); const iconElement = document.querySelector('.material-icons.text-3xl'); const disconnectButton = document.getElementById('disconnectButton'); const portSelectionDiv = document.getElementById('portSelectionDiv'); if (statusElement && iconElement) { if (data.connected) { statusElement.textContent = `Connected to ${data.port || 'unknown port'}`; statusElement.className = 'text-green-500 text-sm font-medium leading-normal'; iconElement.textContent = 'usb'; if (disconnectButton) { disconnectButton.hidden = false; } if (portSelectionDiv) { portSelectionDiv.hidden = true; } } else { statusElement.textContent = 'Disconnected'; statusElement.className = 'text-red-500 text-sm font-medium leading-normal'; iconElement.textContent = 'usb_off'; if (disconnectButton) { disconnectButton.hidden = true; } if (portSelectionDiv) { portSelectionDiv.hidden = false; } } } } // Function to update available serial ports async function updateSerialPorts() { try { const response = await fetch('/list_serial_ports'); if (response.ok) { const ports = await response.json(); const portsElement = document.getElementById('availablePorts'); const portSelect = document.getElementById('portSelect'); const preferredPortSelect = document.getElementById('preferredPortSelect'); if (portsElement) { portsElement.textContent = ports.length > 0 ? ports.join(', ') : 'No ports available'; } if (portSelect) { // Clear existing options except the first one while (portSelect.options.length > 1) { portSelect.remove(1); } // Add new options ports.forEach(port => { const option = document.createElement('option'); option.value = port; option.textContent = port; portSelect.appendChild(option); }); // If there's exactly one port available, select and connect to it if (ports.length === 1) { portSelect.value = ports[0]; // Trigger connect button click const connectButton = document.getElementById('connectButton'); if (connectButton) { connectButton.click(); } } } // Also update the preferred port select dropdown if (preferredPortSelect) { // Store current selection const currentPreferred = preferredPortSelect.value; // Clear existing options except the first one (no preference) while (preferredPortSelect.options.length > 1) { preferredPortSelect.remove(1); } // Add all available ports ports.forEach(port => { const option = document.createElement('option'); option.value = port; option.textContent = port; preferredPortSelect.appendChild(option); }); // Restore selection if it's still available if (currentPreferred && ports.includes(currentPreferred)) { preferredPortSelect.value = currentPreferred; } } } } catch (error) { logMessage(`Error fetching serial ports: ${error.message}`, LOG_TYPE.ERROR); } } // Function to load and display preferred port setting async function loadPreferredPort() { try { const response = await fetch('/api/preferred-port'); if (response.ok) { const data = await response.json(); const preferredPortSelect = document.getElementById('preferredPortSelect'); const currentPreferredPort = document.getElementById('currentPreferredPort'); const preferredPortDisplay = document.getElementById('preferredPortDisplay'); if (preferredPortSelect && data.preferred_port) { // Check if the preferred port is in the options const optionExists = Array.from(preferredPortSelect.options).some( opt => opt.value === data.preferred_port ); if (optionExists) { preferredPortSelect.value = data.preferred_port; } else { // Add the preferred port as an option (it might not be currently available) const option = document.createElement('option'); option.value = data.preferred_port; option.textContent = `${data.preferred_port} (not currently available)`; preferredPortSelect.appendChild(option); preferredPortSelect.value = data.preferred_port; } } // Show current preferred port indicator if (currentPreferredPort && preferredPortDisplay && data.preferred_port) { preferredPortDisplay.textContent = `Currently set to: ${data.preferred_port}`; currentPreferredPort.classList.remove('hidden'); } else if (currentPreferredPort) { currentPreferredPort.classList.add('hidden'); } } } catch (error) { logMessage(`Error loading preferred port: ${error.message}`, LOG_TYPE.ERROR); } } // Function to save preferred port setting async function savePreferredPort() { const preferredPortSelect = document.getElementById('preferredPortSelect'); if (!preferredPortSelect) return; const preferredPort = preferredPortSelect.value || null; try { const response = await fetch('/api/settings', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ connection: { preferred_port: preferredPort } }) }); if (response.ok) { await response.json(); const currentPreferredPort = document.getElementById('currentPreferredPort'); const preferredPortDisplay = document.getElementById('preferredPortDisplay'); if (preferredPort) { showStatusMessage(`Preferred port set to: ${preferredPort}`, 'success'); if (currentPreferredPort && preferredPortDisplay) { preferredPortDisplay.textContent = `Currently set to: ${preferredPort}`; currentPreferredPort.classList.remove('hidden'); } } else { showStatusMessage('Preferred port cleared - will auto-detect on startup', 'success'); if (currentPreferredPort) { currentPreferredPort.classList.add('hidden'); } } } else { throw new Error('Failed to save preferred port'); } } catch (error) { showStatusMessage(`Failed to save preferred port: ${error.message}`, 'error'); } } function setWledButtonState(isSet) { const saveWledConfig = document.getElementById('saveWledConfig'); if (!saveWledConfig) return; if (isSet) { saveWledConfig.className = 'flex items-center justify-center gap-2 min-w-[100px] max-w-[480px] cursor-pointer rounded-lg h-10 px-4 bg-red-600 hover:bg-red-700 text-white text-sm font-medium leading-normal tracking-[0.015em] transition-colors'; saveWledConfig.innerHTML = 'closeClear WLED IP'; } else { saveWledConfig.className = 'flex items-center justify-center gap-2 min-w-[100px] max-w-[480px] cursor-pointer rounded-lg h-10 px-4 bg-sky-600 hover:bg-sky-700 text-white text-sm font-medium leading-normal tracking-[0.015em] transition-colors'; saveWledConfig.innerHTML = 'saveSave Configuration'; } } // Handle LED provider selection and show/hide appropriate config sections function updateLedProviderUI() { const provider = document.querySelector('input[name="ledProvider"]:checked')?.value || 'none'; const wledConfig = document.getElementById('wledConfig'); const dwLedsConfig = document.getElementById('dwLedsConfig'); if (wledConfig && dwLedsConfig) { if (provider === 'wled') { wledConfig.classList.remove('hidden'); dwLedsConfig.classList.add('hidden'); } else if (provider === 'dw_leds') { wledConfig.classList.add('hidden'); dwLedsConfig.classList.remove('hidden'); } else { wledConfig.classList.add('hidden'); dwLedsConfig.classList.add('hidden'); } } } // Load LED configuration from server async function loadLedConfig() { try { const response = await fetch('/get_led_config'); if (response.ok) { const data = await response.json(); // Set provider radio button const providerRadio = document.getElementById(`ledProvider${providerToCamelCase(data.provider)}`); if (providerRadio) { providerRadio.checked = true; } else { document.getElementById('ledProviderNone').checked = true; } // Set WLED IP if configured if (data.wled_ip) { const wledIpInput = document.getElementById('wledIpInput'); if (wledIpInput) { wledIpInput.value = data.wled_ip; } } // Set DW LED configuration if configured if (data.dw_led_num_leds) { const numLedsInput = document.getElementById('dwLedNumLeds'); if (numLedsInput) { numLedsInput.value = data.dw_led_num_leds; } } if (data.dw_led_gpio_pin) { const gpioPinInput = document.getElementById('dwLedGpioPin'); if (gpioPinInput) { gpioPinInput.value = data.dw_led_gpio_pin; } } if (data.dw_led_pixel_order) { const pixelOrderInput = document.getElementById('dwLedPixelOrder'); if (pixelOrderInput) { pixelOrderInput.value = data.dw_led_pixel_order; } } // Update UI to show correct config section updateLedProviderUI(); } } catch (error) { logMessage(`Error loading LED config: ${error.message}`, LOG_TYPE.ERROR); } } // Initialize settings page document.addEventListener('DOMContentLoaded', async () => { // Initialize UI with default disconnected state updateConnectionUI({ connected: false }); // Handle scroll to section if hash is present in URL if (window.location.hash) { setTimeout(() => { const targetSection = document.querySelector(window.location.hash); if (targetSection) { targetSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); // Add a subtle highlight animation targetSection.style.transition = 'background-color 0.5s ease'; const originalBg = targetSection.style.backgroundColor; targetSection.style.backgroundColor = 'rgba(14, 165, 233, 0.1)'; setTimeout(() => { targetSection.style.backgroundColor = originalBg; }, 2000); } }, 300); // Delay to ensure page is fully loaded } // Load all data asynchronously using unified settings endpoint Promise.all([ // Unified settings endpoint (replaces multiple individual fetches) fetch('/api/settings').then(response => response.json()).catch(() => ({})), // Non-settings operational endpoints (kept separate) fetch('/serial_status').then(response => response.json()).catch(() => ({ connected: false })), fetch('/api/version').then(response => response.json()).catch(() => ({ current: '1.0.0', latest: '1.0.0', update_available: false })), fetch('/list_serial_ports').then(response => response.json()).catch(() => []), getCachedPatternFiles().catch(() => []) ]).then(([settings, statusData, updateData, ports, patterns]) => { // Map unified settings to legacy variable names for backward compatibility with existing UI code const ledConfigData = { provider: settings.led?.provider || 'none', wled_ip: settings.led?.wled_ip || null, dw_led_num_leds: settings.led?.dw_led?.num_leds, dw_led_gpio_pin: settings.led?.dw_led?.gpio_pin, dw_led_pixel_order: settings.led?.dw_led?.pixel_order }; const clearPatterns = { custom_clear_from_in: settings.patterns?.custom_clear_from_in, custom_clear_from_out: settings.patterns?.custom_clear_from_out }; const clearSpeedData = { clear_pattern_speed: settings.patterns?.clear_pattern_speed, effective_speed: settings.patterns?.clear_pattern_speed // Will be handled by UI }; const appNameData = { app_name: settings.app?.name || 'Dune Weaver' }; const scheduledPauseData = settings.scheduled_pause || { enabled: false, time_slots: [] }; const preferredPortData = { preferred_port: settings.connection?.preferred_port }; // Store full settings for other initialization functions window.unifiedSettings = settings; // Update connection status setCachedConnectionStatus(statusData); updateConnectionUI(statusData); // Update LED configuration const providerRadio = document.getElementById(`ledProvider${providerToCamelCase(ledConfigData.provider)}`); if (providerRadio) { providerRadio.checked = true; } else { document.getElementById('ledProviderNone').checked = true; } if (ledConfigData.wled_ip) { const wledIpInput = document.getElementById('wledIpInput'); if (wledIpInput) wledIpInput.value = ledConfigData.wled_ip; } // Load DW LED settings if (ledConfigData.dw_led_num_leds) { const numLedsInput = document.getElementById('dwLedNumLeds'); if (numLedsInput) numLedsInput.value = ledConfigData.dw_led_num_leds; } if (ledConfigData.dw_led_gpio_pin) { const gpioPinInput = document.getElementById('dwLedGpioPin'); if (gpioPinInput) gpioPinInput.value = ledConfigData.dw_led_gpio_pin; } if (ledConfigData.dw_led_pixel_order) { const pixelOrderInput = document.getElementById('dwLedPixelOrder'); if (pixelOrderInput) pixelOrderInput.value = ledConfigData.dw_led_pixel_order; } updateLedProviderUI() // Update version display const currentVersionText = document.getElementById('currentVersionText'); const latestVersionText = document.getElementById('latestVersionText'); const updateButton = document.getElementById('updateSoftware'); const updateIcon = document.getElementById('updateIcon'); const updateText = document.getElementById('updateText'); if (currentVersionText) { currentVersionText.textContent = updateData.current; } if (latestVersionText) { if (updateData.error) { latestVersionText.textContent = 'Error checking updates'; latestVersionText.className = 'text-red-500 text-sm font-normal leading-normal'; } else { latestVersionText.textContent = updateData.latest; latestVersionText.className = 'text-slate-500 text-sm font-normal leading-normal'; } } // Update button state if (updateButton && updateIcon && updateText) { if (updateData.update_available) { updateButton.disabled = false; updateButton.className = 'flex items-center justify-center gap-1.5 min-w-[84px] cursor-pointer rounded-lg h-9 px-3 bg-emerald-500 hover:bg-emerald-600 text-white text-xs font-medium leading-normal tracking-[0.015em] transition-colors'; updateIcon.textContent = 'download'; updateText.textContent = 'Update'; } else { updateButton.disabled = true; updateButton.className = 'flex items-center justify-center gap-1.5 min-w-[84px] cursor-pointer rounded-lg h-9 px-3 bg-gray-400 text-white text-xs font-medium leading-normal tracking-[0.015em] transition-colors disabled:opacity-50 disabled:cursor-not-allowed'; updateIcon.textContent = 'check'; updateText.textContent = 'Up to date'; } } // Update port selection const portSelect = document.getElementById('portSelect'); if (portSelect) { // Clear existing options except the first one while (portSelect.options.length > 1) { portSelect.remove(1); } // Add new options ports.forEach(port => { const option = document.createElement('option'); option.value = port; option.textContent = port; portSelect.appendChild(option); }); // If there's exactly one port available, select it if (ports.length === 1) { portSelect.value = ports[0]; } } // Update preferred port selection const preferredPortSelect = document.getElementById('preferredPortSelect'); const currentPreferredPort = document.getElementById('currentPreferredPort'); const preferredPortDisplay = document.getElementById('preferredPortDisplay'); if (preferredPortSelect) { // Clear existing options except the first one (no preference) while (preferredPortSelect.options.length > 1) { preferredPortSelect.remove(1); } // Add all available ports ports.forEach(port => { const option = document.createElement('option'); option.value = port; option.textContent = port; preferredPortSelect.appendChild(option); }); // Set the current preferred port value if (preferredPortData && preferredPortData.preferred_port) { // Check if the preferred port is in the available ports const isAvailable = ports.includes(preferredPortData.preferred_port); if (isAvailable) { preferredPortSelect.value = preferredPortData.preferred_port; } else { // Add the preferred port as an option (it might not be currently available) const option = document.createElement('option'); option.value = preferredPortData.preferred_port; option.textContent = `${preferredPortData.preferred_port} (not currently available)`; preferredPortSelect.appendChild(option); preferredPortSelect.value = preferredPortData.preferred_port; } // Show the current preferred port indicator if (currentPreferredPort && preferredPortDisplay) { preferredPortDisplay.textContent = `Currently set to: ${preferredPortData.preferred_port}`; currentPreferredPort.classList.remove('hidden'); } } } // Initialize autocomplete for clear patterns const clearFromInInput = document.getElementById('customClearFromInInput'); const clearFromOutInput = document.getElementById('customClearFromOutInput'); if (clearFromInInput && clearFromOutInput && patterns && Array.isArray(patterns)) { // Store patterns globally for autocomplete window.availablePatterns = patterns; // Set current values if they exist if (clearPatterns && clearPatterns.custom_clear_from_in) { clearFromInInput.value = clearPatterns.custom_clear_from_in; } if (clearPatterns && clearPatterns.custom_clear_from_out) { clearFromOutInput.value = clearPatterns.custom_clear_from_out; } // Initialize autocomplete for both inputs initializeAutocomplete('customClearFromInInput', 'clearFromInSuggestions', 'clearFromInClear', patterns); initializeAutocomplete('customClearFromOutInput', 'clearFromOutSuggestions', 'clearFromOutClear', patterns); console.log('Autocomplete initialized with', patterns.length, 'patterns'); } // Set clear pattern speed const clearPatternSpeedInput = document.getElementById('clearPatternSpeedInput'); const effectiveClearSpeed = document.getElementById('effectiveClearSpeed'); if (clearPatternSpeedInput && clearSpeedData) { // Only set value if clear_pattern_speed is not null if (clearSpeedData.clear_pattern_speed !== null && clearSpeedData.clear_pattern_speed !== undefined) { clearPatternSpeedInput.value = clearSpeedData.clear_pattern_speed; if (effectiveClearSpeed) { effectiveClearSpeed.textContent = `Current: ${clearSpeedData.clear_pattern_speed} steps/min`; } } else { // Leave empty to show placeholder for default clearPatternSpeedInput.value = ''; if (effectiveClearSpeed && clearSpeedData.effective_speed) { effectiveClearSpeed.textContent = `Using default pattern speed: ${clearSpeedData.effective_speed} steps/min`; } } } // Update app name const appNameInput = document.getElementById('appNameInput'); if (appNameInput && appNameData.app_name) { appNameInput.value = appNameData.app_name; } // Store Still Sands data for later initialization window.initialStillSandsData = scheduledPauseData; }).catch(error => { logMessage(`Error initializing settings page: ${error.message}`, LOG_TYPE.ERROR); }); // Set up event listeners setupEventListeners(); }); // Setup event listeners function setupEventListeners() { // Save App Name const saveAppNameButton = document.getElementById('saveAppName'); const appNameInput = document.getElementById('appNameInput'); if (saveAppNameButton && appNameInput) { saveAppNameButton.addEventListener('click', async () => { const appName = appNameInput.value.trim() || 'Dune Weaver'; try { const response = await fetch('/api/settings', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ app: { name: appName } }) }); if (response.ok) { await response.json(); showStatusMessage('Application name updated successfully. Refresh the page to see changes.', 'success'); // Update the page title and header immediately document.title = `Settings - ${appName}`; const headerTitle = document.querySelector('h1.text-gray-800'); if (headerTitle) { // Update just the text content, preserving the connection status dot const textNode = headerTitle.childNodes[0]; if (textNode && textNode.nodeType === Node.TEXT_NODE) { textNode.textContent = data.app_name; } } } else { throw new Error('Failed to save application name'); } } catch (error) { showStatusMessage(`Failed to save application name: ${error.message}`, 'error'); } }); // Handle Enter key in app name input appNameInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { saveAppNameButton.click(); } }); } // LED provider selection change handlers const ledProviderRadios = document.querySelectorAll('input[name="ledProvider"]'); ledProviderRadios.forEach(radio => { radio.addEventListener('change', updateLedProviderUI); }); // Save LED configuration const saveLedConfig = document.getElementById('saveLedConfig'); if (saveLedConfig) { saveLedConfig.addEventListener('click', async () => { const provider = document.querySelector('input[name="ledProvider"]:checked')?.value || 'none'; let requestBody = { provider }; if (provider === 'wled') { const wledIp = document.getElementById('wledIpInput')?.value; if (!wledIp) { showStatusMessage('Please enter a WLED IP address', 'error'); return; } requestBody.ip_address = wledIp; } else if (provider === 'dw_leds') { const numLeds = parseInt(document.getElementById('dwLedNumLeds')?.value) || 60; const gpioPin = parseInt(document.getElementById('dwLedGpioPin')?.value) || 12; const pixelOrder = document.getElementById('dwLedPixelOrder')?.value || 'GRB'; requestBody.num_leds = numLeds; requestBody.gpio_pin = gpioPin; requestBody.pixel_order = pixelOrder; } try { const response = await fetch('/set_led_config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (response.ok) { const data = await response.json(); if (provider === 'wled' && data.wled_ip) { localStorage.setItem('wled_ip', data.wled_ip); showStatusMessage('WLED configured successfully', 'success'); } else if (provider === 'dw_leds') { // Check if there's a warning (hardware not available but settings saved) if (data.warning) { showStatusMessage( `Settings saved for testing. Hardware issue: ${data.warning}`, 'warning' ); } else { showStatusMessage( `DW LEDs configured: ${data.dw_led_num_leds} LEDs on GPIO${data.dw_led_gpio_pin}`, 'success' ); } } else if (provider === 'none') { localStorage.removeItem('wled_ip'); showStatusMessage('LED controller disabled', 'success'); } } else { // Extract error detail from response const errorData = await response.json().catch(() => ({})); const errorMessage = errorData.detail || 'Failed to save LED configuration'; showStatusMessage(errorMessage, 'error'); } } catch (error) { showStatusMessage(`Failed to save LED configuration: ${error.message}`, 'error'); } }); } // Update software const updateSoftware = document.getElementById('updateSoftware'); if (updateSoftware) { updateSoftware.addEventListener('click', async () => { if (updateSoftware.disabled) { return; } try { const response = await fetch('/api/update', { method: 'POST' }); const data = await response.json(); if (data.success) { showStatusMessage('Software update started successfully', 'success'); } else if (data.manual_update_url) { // Show modal with manual update instructions, but use wiki link const wikiData = { ...data, manual_update_url: 'https://github.com/tuanchris/dune-weaver/wiki/Updating-software' }; showUpdateInstructionsModal(wikiData); } else { showStatusMessage(data.message || 'No updates available', 'info'); } } catch (error) { logMessage(`Error updating software: ${error.message}`, LOG_TYPE.ERROR); showStatusMessage('Failed to check for updates', 'error'); } }); } // Connect button const connectButton = document.getElementById('connectButton'); if (connectButton) { connectButton.addEventListener('click', async () => { const portSelect = document.getElementById('portSelect'); if (!portSelect || !portSelect.value) { logMessage('Please select a port first', LOG_TYPE.WARNING); return; } try { const response = await fetch('/connect', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ port: portSelect.value }) }); if (response.ok) { logMessage('Connected successfully', LOG_TYPE.SUCCESS); await updateSerialStatus(true); // Force update after connecting } else { throw new Error('Failed to connect'); } } catch (error) { logMessage(`Error connecting to device: ${error.message}`, LOG_TYPE.ERROR); } }); } // Disconnect button const disconnectButton = document.getElementById('disconnectButton'); if (disconnectButton) { disconnectButton.addEventListener('click', async () => { try { const response = await fetch('/disconnect', { method: 'POST' }); if (response.ok) { logMessage('Device disconnected successfully', LOG_TYPE.SUCCESS); await updateSerialStatus(true); // Force update after disconnecting } else { throw new Error('Failed to disconnect device'); } } catch (error) { logMessage(`Error disconnecting device: ${error.message}`, LOG_TYPE.ERROR); } }); } // Save preferred port button const savePreferredPortButton = document.getElementById('savePreferredPort'); if (savePreferredPortButton) { savePreferredPortButton.addEventListener('click', savePreferredPort); } // Save custom clear patterns button const saveClearPatterns = document.getElementById('saveClearPatterns'); if (saveClearPatterns) { saveClearPatterns.addEventListener('click', async () => { const clearFromInInput = document.getElementById('customClearFromInInput'); const clearFromOutInput = document.getElementById('customClearFromOutInput'); if (!clearFromInInput || !clearFromOutInput) { return; } // Validate that the entered patterns exist (if not empty) const inValue = clearFromInInput.value.trim(); const outValue = clearFromOutInput.value.trim(); if (inValue && window.availablePatterns && !window.availablePatterns.includes(inValue)) { showStatusMessage(`Pattern not found: ${inValue}`, 'error'); return; } if (outValue && window.availablePatterns && !window.availablePatterns.includes(outValue)) { showStatusMessage(`Pattern not found: ${outValue}`, 'error'); return; } try { const response = await fetch('/api/settings', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ patterns: { custom_clear_from_in: inValue || null, custom_clear_from_out: outValue || null } }) }); if (response.ok) { showStatusMessage('Clear patterns saved successfully', 'success'); } else { const error = await response.json(); throw new Error(error.detail || 'Failed to save clear patterns'); } } catch (error) { showStatusMessage(`Failed to save clear patterns: ${error.message}`, 'error'); } }); } // Logo upload functionality const logoFileInput = document.getElementById('logoFileInput'); const resetLogoBtn = document.getElementById('resetLogoBtn'); const logoPreview = document.getElementById('logoPreview'); const logoUploadStatus = document.getElementById('logoUploadStatus'); if (logoFileInput) { logoFileInput.addEventListener('change', async (event) => { const file = event.target.files[0]; if (!file) return; // Show uploading status if (logoUploadStatus) { logoUploadStatus.textContent = 'Uploading...'; logoUploadStatus.className = 'text-xs text-slate-500'; } const formData = new FormData(); formData.append('file', file); try { const response = await fetch('/api/upload-logo', { method: 'POST', body: formData }); if (response.ok) { const data = await response.json(); // Update preview image with cache-busting query param if (logoPreview) { logoPreview.src = data.url + '?t=' + Date.now(); } // Show reset button if (resetLogoBtn) { resetLogoBtn.classList.remove('hidden'); } // Show success status if (logoUploadStatus) { logoUploadStatus.textContent = 'Logo uploaded successfully!'; logoUploadStatus.className = 'text-xs text-green-600'; setTimeout(() => { logoUploadStatus.textContent = ''; }, 3000); } showStatusMessage('Logo uploaded successfully. Refresh the page to see all changes.', 'success'); } else { const error = await response.json(); throw new Error(error.detail || 'Failed to upload logo'); } } catch (error) { if (logoUploadStatus) { logoUploadStatus.textContent = `Error: ${error.message}`; logoUploadStatus.className = 'text-xs text-red-600'; } showStatusMessage(`Failed to upload logo: ${error.message}`, 'error'); } // Reset file input so the same file can be re-selected logoFileInput.value = ''; }); } if (resetLogoBtn) { resetLogoBtn.addEventListener('click', async () => { try { const response = await fetch('/api/custom-logo', { method: 'DELETE' }); if (response.ok) { // Reset preview to default logo if (logoPreview) { logoPreview.src = '/static/apple-touch-icon.png?t=' + Date.now(); } // Hide reset button resetLogoBtn.classList.add('hidden'); showStatusMessage('Logo reset to default. Refresh the page to see all changes.', 'success'); } else { const error = await response.json(); throw new Error(error.detail || 'Failed to reset logo'); } } catch (error) { showStatusMessage(`Failed to reset logo: ${error.message}`, 'error'); } }); } // Save clear pattern speed button const saveClearSpeed = document.getElementById('saveClearSpeed'); if (saveClearSpeed) { saveClearSpeed.addEventListener('click', async () => { const clearPatternSpeedInput = document.getElementById('clearPatternSpeedInput'); if (!clearPatternSpeedInput) { return; } let speed; if (clearPatternSpeedInput.value === '' || clearPatternSpeedInput.value === null) { // Empty value means use default (None) speed = null; } else { speed = parseInt(clearPatternSpeedInput.value); // Validate speed only if it's not null if (isNaN(speed) || speed < 50 || speed > 2000) { showStatusMessage('Clear pattern speed must be between 50 and 2000, or leave empty for default', 'error'); return; } } try { const response = await fetch('/api/settings', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ patterns: { clear_pattern_speed: speed } }) }); if (response.ok) { await response.json(); if (speed === null) { showStatusMessage('Clear pattern speed set to default', 'success'); } else { showStatusMessage(`Clear pattern speed set to ${speed} steps/min`, 'success'); } // Update the effective speed display const effectiveClearSpeed = document.getElementById('effectiveClearSpeed'); if (effectiveClearSpeed) { if (speed === null) { effectiveClearSpeed.textContent = `Using default pattern speed: ${data.effective_speed} steps/min`; } else { effectiveClearSpeed.textContent = `Current: ${speed} steps/min`; } } } else { const error = await response.json(); throw new Error(error.detail || 'Failed to save clear pattern speed'); } } catch (error) { showStatusMessage(`Failed to save clear pattern speed: ${error.message}`, 'error'); } }); } } // Button click handlers document.addEventListener('DOMContentLoaded', function() { // Home button const homeButton = document.getElementById('homeButton'); if (homeButton) { homeButton.addEventListener('click', async () => { try { const response = await fetch('/send_home', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.success) { updateStatus('Moving to home position...'); } } catch (error) { console.error('Error sending home command:', error); updateStatus('Error: Failed to move to home position'); } }); } // Stop button const stopButton = document.getElementById('stopButton'); if (stopButton) { stopButton.addEventListener('click', async () => { try { const response = await fetch('/stop_execution', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.success) { updateStatus('Execution stopped'); } } catch (error) { console.error('Error stopping execution:', error); updateStatus('Error: Failed to stop execution'); } }); } // Move to Center button const centerButton = document.getElementById('centerButton'); if (centerButton) { centerButton.addEventListener('click', async () => { try { const response = await fetch('/move_to_center', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.success) { updateStatus('Moving to center position...'); } } catch (error) { console.error('Error moving to center:', error); updateStatus('Error: Failed to move to center'); } }); } // Move to Perimeter button const perimeterButton = document.getElementById('perimeterButton'); if (perimeterButton) { perimeterButton.addEventListener('click', async () => { try { const response = await fetch('/move_to_perimeter', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.success) { updateStatus('Moving to perimeter position...'); } } catch (error) { console.error('Error moving to perimeter:', error); updateStatus('Error: Failed to move to perimeter'); } }); } }); // Function to update status function updateStatus(message) { const statusElement = document.querySelector('.text-slate-800.text-base.font-medium.leading-normal'); if (statusElement) { statusElement.textContent = message; // Reset status after 3 seconds if it's a temporary message if (message.includes('Moving') || message.includes('Execution')) { setTimeout(() => { statusElement.textContent = 'Status'; }, 3000); } } } // Function to show status messages (using existing base.js showStatusMessage if available) function showStatusMessage(message, type) { if (typeof window.showStatusMessage === 'function') { window.showStatusMessage(message, type); } else { // Fallback to console logging console.log(`[${type}] ${message}`); } } // Function to show update instructions modal function showUpdateInstructionsModal(data) { // Create modal HTML const modal = document.createElement('div'); modal.id = 'updateInstructionsModal'; modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4'; modal.innerHTML = `

Manual Update Required

${data.message}

${data.instructions}

`; document.body.appendChild(modal); // Add event listeners const openGitHubButton = modal.querySelector('#openGitHubRelease'); const closeButton = modal.querySelector('#closeUpdateModal'); openGitHubButton.addEventListener('click', () => { window.open(data.manual_update_url, '_blank'); document.body.removeChild(modal); }); closeButton.addEventListener('click', () => { document.body.removeChild(modal); }); // Close on outside click modal.addEventListener('click', (e) => { if (e.target === modal) { document.body.removeChild(modal); } }); } // Autocomplete functionality function initializeAutocomplete(inputId, suggestionsId, clearButtonId, patterns) { const input = document.getElementById(inputId); const suggestionsDiv = document.getElementById(suggestionsId); const clearButton = document.getElementById(clearButtonId); let selectedIndex = -1; if (!input || !suggestionsDiv) return; // Function to update clear button visibility function updateClearButton() { if (clearButton) { if (input.value.trim()) { clearButton.classList.remove('hidden'); } else { clearButton.classList.add('hidden'); } } } // Format pattern name for display function formatPatternName(pattern) { return pattern.replace('.thr', '').replace(/_/g, ' '); } // Filter patterns based on input function filterPatterns(searchTerm) { if (!searchTerm) return patterns.slice(0, 20); // Show first 20 when empty const term = searchTerm.toLowerCase(); return patterns.filter(pattern => { const name = pattern.toLowerCase(); return name.includes(term); }).sort((a, b) => { // Prioritize patterns that start with the search term const aStarts = a.toLowerCase().startsWith(term); const bStarts = b.toLowerCase().startsWith(term); if (aStarts && !bStarts) return -1; if (!aStarts && bStarts) return 1; return a.localeCompare(b); }).slice(0, 20); // Limit to 20 results } // Highlight matching text function highlightMatch(text, searchTerm) { if (!searchTerm) return text; const regex = new RegExp(`(${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'); return text.replace(regex, '$1'); } // Show suggestions function showSuggestions(searchTerm) { const filtered = filterPatterns(searchTerm); if (filtered.length === 0 && searchTerm) { suggestionsDiv.innerHTML = '
No patterns found
'; suggestionsDiv.classList.remove('hidden'); return; } suggestionsDiv.innerHTML = filtered.map((pattern, index) => { const displayName = formatPatternName(pattern); const highlighted = highlightMatch(displayName, searchTerm); return `
${highlighted}
`; }).join(''); suggestionsDiv.classList.remove('hidden'); selectedIndex = -1; } // Hide suggestions function hideSuggestions() { setTimeout(() => { suggestionsDiv.classList.add('hidden'); selectedIndex = -1; }, 200); } // Select suggestion function selectSuggestion(value) { input.value = value; hideSuggestions(); updateClearButton(); } // Handle keyboard navigation function handleKeyboard(e) { const items = suggestionsDiv.querySelectorAll('.suggestion-item[data-value]'); if (e.key === 'ArrowDown') { e.preventDefault(); selectedIndex = Math.min(selectedIndex + 1, items.length - 1); updateSelection(items); } else if (e.key === 'ArrowUp') { e.preventDefault(); selectedIndex = Math.max(selectedIndex - 1, -1); updateSelection(items); } else if (e.key === 'Enter') { e.preventDefault(); if (selectedIndex >= 0 && items[selectedIndex]) { selectSuggestion(items[selectedIndex].dataset.value); } else if (items.length === 1) { selectSuggestion(items[0].dataset.value); } } else if (e.key === 'Escape') { hideSuggestions(); } } // Update visual selection function updateSelection(items) { items.forEach((item, index) => { if (index === selectedIndex) { item.classList.add('selected'); item.scrollIntoView({ block: 'nearest' }); } else { item.classList.remove('selected'); } }); } // Event listeners input.addEventListener('input', (e) => { const value = e.target.value.trim(); updateClearButton(); if (value.length > 0 || e.target === document.activeElement) { showSuggestions(value); } else { hideSuggestions(); } }); input.addEventListener('focus', () => { const value = input.value.trim(); showSuggestions(value); }); input.addEventListener('blur', hideSuggestions); input.addEventListener('keydown', handleKeyboard); // Click handler for suggestions suggestionsDiv.addEventListener('click', (e) => { const item = e.target.closest('.suggestion-item[data-value]'); if (item) { selectSuggestion(item.dataset.value); } }); // Mouse hover handler suggestionsDiv.addEventListener('mouseover', (e) => { const item = e.target.closest('.suggestion-item[data-value]'); if (item) { selectedIndex = parseInt(item.dataset.index); const items = suggestionsDiv.querySelectorAll('.suggestion-item[data-value]'); updateSelection(items); } }); // Clear button handler if (clearButton) { clearButton.addEventListener('click', () => { input.value = ''; updateClearButton(); hideSuggestions(); input.focus(); }); } // Initialize clear button visibility updateClearButton(); } // auto_play Mode Functions async function initializeauto_playMode() { const auto_playToggle = document.getElementById('auto_playModeToggle'); const auto_playSettings = document.getElementById('auto_playSettings'); const auto_playPlaylistSelect = document.getElementById('auto_playPlaylistSelect'); const auto_playRunModeSelect = document.getElementById('auto_playRunModeSelect'); const auto_playPauseTimeInput = document.getElementById('auto_playPauseTimeInput'); const auto_playClearPatternSelect = document.getElementById('auto_playClearPatternSelect'); const auto_playShuffleToggle = document.getElementById('auto_playShuffleToggle'); // Load current auto_play settings try { const response = await fetch('/api/auto_play-mode'); const data = await response.json(); auto_playToggle.checked = data.enabled; if (data.enabled) { auto_playSettings.style.display = 'block'; } // Set current values auto_playRunModeSelect.value = data.run_mode || 'loop'; auto_playPauseTimeInput.value = data.pause_time || 5.0; auto_playClearPatternSelect.value = data.clear_pattern || 'adaptive'; auto_playShuffleToggle.checked = data.shuffle || false; // Load playlists for selection const playlistsResponse = await fetch('/list_all_playlists'); const playlists = await playlistsResponse.json(); // Clear and populate playlist select auto_playPlaylistSelect.innerHTML = ''; playlists.forEach(playlist => { const option = document.createElement('option'); option.value = playlist; option.textContent = playlist; if (playlist === data.playlist) { option.selected = true; } auto_playPlaylistSelect.appendChild(option); }); } catch (error) { logMessage(`Error loading auto_play settings: ${error.message}`, LOG_TYPE.ERROR); } // Get save button const saveAutoPlayButton = document.getElementById('saveAutoPlaySettings'); // Function to save settings async function saveSettings(showFeedback = false) { const originalButtonHTML = saveAutoPlayButton ? saveAutoPlayButton.innerHTML : ''; if (showFeedback && saveAutoPlayButton) { saveAutoPlayButton.disabled = true; saveAutoPlayButton.innerHTML = 'refreshSaving...'; } try { const response = await fetch('/api/auto_play-mode', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: auto_playToggle.checked, playlist: auto_playPlaylistSelect.value || null, run_mode: auto_playRunModeSelect.value, pause_time: parseFloat(auto_playPauseTimeInput.value) || 0, clear_pattern: auto_playClearPatternSelect.value, shuffle: auto_playShuffleToggle.checked }) }); if (!response.ok) { throw new Error('Failed to save settings'); } if (showFeedback && saveAutoPlayButton) { saveAutoPlayButton.innerHTML = 'checkSaved!'; showStatusMessage('Auto-play settings saved successfully', 'success'); setTimeout(() => { saveAutoPlayButton.innerHTML = originalButtonHTML; saveAutoPlayButton.disabled = false; }, 2000); } } catch (error) { logMessage(`Error saving auto_play settings: ${error.message}`, LOG_TYPE.ERROR); if (showFeedback && saveAutoPlayButton) { showStatusMessage(`Failed to save settings: ${error.message}`, 'error'); saveAutoPlayButton.innerHTML = originalButtonHTML; saveAutoPlayButton.disabled = false; } } } // Toggle auto_play settings visibility and save auto_playToggle.addEventListener('change', async () => { auto_playSettings.style.display = auto_playToggle.checked ? 'block' : 'none'; await saveSettings(false); // Auto-save toggle state without full feedback const statusText = auto_playToggle.checked ? 'enabled' : 'disabled'; showStatusMessage(`Auto-play ${statusText}`, 'success'); }); // Save button click handler if (saveAutoPlayButton) { saveAutoPlayButton.addEventListener('click', () => saveSettings(true)); } } // Initialize auto_play mode when DOM is ready document.addEventListener('DOMContentLoaded', function() { initializeauto_playMode(); initializeStillSandsMode(); initializeHomingConfig(); }); // Still Sands Mode Functions async function initializeStillSandsMode() { logMessage('Initializing Still Sands mode', LOG_TYPE.INFO); const stillSandsToggle = document.getElementById('scheduledPauseToggle'); const stillSandsSettings = document.getElementById('scheduledPauseSettings'); const addTimeSlotButton = document.getElementById('addTimeSlotButton'); const saveStillSandsButton = document.getElementById('savePauseSettings'); const timeSlotsContainer = document.getElementById('timeSlotsContainer'); const wledControlToggle = document.getElementById('stillSandsWledControl'); const finishPatternToggle = document.getElementById('stillSandsFinishPattern'); const timezoneSelect = document.getElementById('stillSandsTimezone'); // Check if elements exist if (!stillSandsToggle || !stillSandsSettings || !addTimeSlotButton || !saveStillSandsButton || !timeSlotsContainer) { logMessage('Still Sands elements not found, skipping initialization', LOG_TYPE.WARNING); logMessage(`Found elements: toggle=${!!stillSandsToggle}, settings=${!!stillSandsSettings}, addBtn=${!!addTimeSlotButton}, saveBtn=${!!saveStillSandsButton}, container=${!!timeSlotsContainer}`, LOG_TYPE.WARNING); return; } logMessage('All Still Sands elements found successfully', LOG_TYPE.INFO); // Track time slots let timeSlots = []; let slotIdCounter = 0; // Load current Still Sands settings from initial data try { // Use the data loaded during page initialization, fallback to API if not available let data; if (window.initialStillSandsData) { data = window.initialStillSandsData; // Clear the global variable after use delete window.initialStillSandsData; } else { // Fallback to API call if initial data not available const response = await fetch('/api/scheduled-pause'); data = await response.json(); } stillSandsToggle.checked = data.enabled || false; if (data.enabled) { stillSandsSettings.style.display = 'block'; } // Load WLED control setting if (wledControlToggle) { wledControlToggle.checked = data.control_wled || false; } // Load finish pattern setting if (finishPatternToggle) { finishPatternToggle.checked = data.finish_pattern || false; } // Load timezone setting if (timezoneSelect) { timezoneSelect.value = data.timezone || ''; } // Load existing time slots timeSlots = data.time_slots || []; // Assign IDs to loaded slots BEFORE rendering if (timeSlots.length > 0) { slotIdCounter = 0; timeSlots.forEach(slot => { slot.id = ++slotIdCounter; }); } renderTimeSlots(); } catch (error) { logMessage(`Error loading Still Sands settings: ${error.message}`, LOG_TYPE.ERROR); // Initialize with empty settings if load fails timeSlots = []; renderTimeSlots(); } // Function to validate time format (HH:MM) function isValidTime(timeString) { const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/; return timeRegex.test(timeString); } // Function to create a new time slot element function createTimeSlotElement(slot) { const slotDiv = document.createElement('div'); slotDiv.className = 'time-slot-item'; slotDiv.dataset.slotId = slot.id; slotDiv.innerHTML = `
${['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'].map(day => ` `).join('')}
`; // Add event listeners for this slot const startTimeInput = slotDiv.querySelector('.start-time'); const endTimeInput = slotDiv.querySelector('.end-time'); const daysSelect = slotDiv.querySelector('.days-select'); const customDaysContainer = slotDiv.querySelector('.custom-days-container'); const removeButton = slotDiv.querySelector('.remove-slot-btn'); // Show/hide custom days based on selection daysSelect.addEventListener('change', () => { customDaysContainer.style.display = daysSelect.value === 'custom' ? 'block' : 'none'; updateTimeSlot(slot.id); }); // Update slot data when inputs change startTimeInput.addEventListener('change', () => updateTimeSlot(slot.id)); endTimeInput.addEventListener('change', () => updateTimeSlot(slot.id)); // Handle custom day checkboxes customDaysContainer.addEventListener('change', () => updateTimeSlot(slot.id)); // Remove slot button removeButton.addEventListener('click', () => { removeTimeSlot(slot.id); }); return slotDiv; } // Function to render all time slots function renderTimeSlots() { timeSlotsContainer.innerHTML = ''; if (timeSlots.length === 0) { timeSlotsContainer.innerHTML = `
schedule

No time slots configured

Click "Add Time Slot" to create a pause schedule

`; return; } timeSlots.forEach(slot => { const slotElement = createTimeSlotElement(slot); timeSlotsContainer.appendChild(slotElement); }); } // Function to add a new time slot function addTimeSlot() { const newSlot = { id: ++slotIdCounter, start_time: '22:00', end_time: '08:00', days: 'daily', custom_days: [] }; timeSlots.push(newSlot); renderTimeSlots(); } // Function to remove a time slot function removeTimeSlot(slotId) { timeSlots = timeSlots.filter(slot => slot.id !== slotId); renderTimeSlots(); } // Function to update a time slot's data function updateTimeSlot(slotId) { const slotElement = timeSlotsContainer.querySelector(`[data-slot-id="${slotId}"]`); if (!slotElement) return; const slot = timeSlots.find(s => s.id === slotId); if (!slot) return; // Update slot data from inputs slot.start_time = slotElement.querySelector('.start-time').value; slot.end_time = slotElement.querySelector('.end-time').value; slot.days = slotElement.querySelector('.days-select').value; // Update custom days if applicable if (slot.days === 'custom') { const checkedDays = Array.from(slotElement.querySelectorAll(`input[name="custom-days-${slotId}"]:checked`)) .map(cb => cb.value); slot.custom_days = checkedDays; } else { slot.custom_days = []; } } // Function to validate all time slots function validateTimeSlots() { const errors = []; timeSlots.forEach((slot, index) => { if (!slot.start_time || !isValidTime(slot.start_time)) { errors.push(`Time slot ${index + 1}: Invalid start time`); } if (!slot.end_time || !isValidTime(slot.end_time)) { errors.push(`Time slot ${index + 1}: Invalid end time`); } if (slot.days === 'custom' && (!slot.custom_days || slot.custom_days.length === 0)) { errors.push(`Time slot ${index + 1}: Please select at least one day for custom schedule`); } }); return errors; } // Function to save settings async function saveStillSandsSettings() { // Update all slots from current form values timeSlots.forEach(slot => updateTimeSlot(slot.id)); // Validate time slots const validationErrors = validateTimeSlots(); if (validationErrors.length > 0) { showStatusMessage(`Validation errors: ${validationErrors.join(', ')}`, 'error'); return; } // Update button UI to show loading state const originalButtonHTML = saveStillSandsButton.innerHTML; saveStillSandsButton.disabled = true; saveStillSandsButton.innerHTML = 'refreshSaving...'; try { const response = await fetch('/api/scheduled-pause', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: stillSandsToggle.checked, control_wled: wledControlToggle ? wledControlToggle.checked : false, finish_pattern: finishPatternToggle ? finishPatternToggle.checked : false, timezone: timezoneSelect ? (timezoneSelect.value || null) : null, time_slots: timeSlots.map(slot => ({ start_time: slot.start_time, end_time: slot.end_time, days: slot.days, custom_days: slot.custom_days })) }) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.detail || 'Failed to save Still Sands settings'); } // Show success state temporarily saveStillSandsButton.innerHTML = 'checkSaved!'; showStatusMessage('Still Sands settings saved successfully', 'success'); // Restore button after 2 seconds setTimeout(() => { saveStillSandsButton.innerHTML = originalButtonHTML; saveStillSandsButton.disabled = false; }, 2000); } catch (error) { logMessage(`Error saving Still Sands settings: ${error.message}`, LOG_TYPE.ERROR); showStatusMessage(`Failed to save settings: ${error.message}`, 'error'); // Restore button immediately on error saveStillSandsButton.innerHTML = originalButtonHTML; saveStillSandsButton.disabled = false; } } // Note: Slot IDs are now assigned during initialization above, before first render // Event listeners stillSandsToggle.addEventListener('change', async () => { logMessage(`Still Sands toggle changed: ${stillSandsToggle.checked}`, LOG_TYPE.INFO); stillSandsSettings.style.display = stillSandsToggle.checked ? 'block' : 'none'; logMessage(`Settings display set to: ${stillSandsSettings.style.display}`, LOG_TYPE.INFO); // Auto-save when toggle changes try { await saveStillSandsSettings(); const statusText = stillSandsToggle.checked ? 'enabled' : 'disabled'; showStatusMessage(`Still Sands ${statusText} successfully`, 'success'); } catch (error) { logMessage(`Error saving Still Sands toggle: ${error.message}`, LOG_TYPE.ERROR); showStatusMessage(`Failed to save Still Sands setting: ${error.message}`, 'error'); } }); addTimeSlotButton.addEventListener('click', addTimeSlot); saveStillSandsButton.addEventListener('click', saveStillSandsSettings); // Add listener for WLED control toggle if (wledControlToggle) { wledControlToggle.addEventListener('change', async () => { logMessage(`WLED control toggle changed: ${wledControlToggle.checked}`, LOG_TYPE.INFO); // Auto-save when WLED control changes await saveStillSandsSettings(); }); } // Add listener for finish pattern toggle if (finishPatternToggle) { finishPatternToggle.addEventListener('change', async () => { logMessage(`Finish pattern toggle changed: ${finishPatternToggle.checked}`, LOG_TYPE.INFO); // Auto-save when finish pattern setting changes await saveStillSandsSettings(); }); } // Add listener for timezone select if (timezoneSelect) { timezoneSelect.addEventListener('change', async () => { logMessage(`Timezone changed: ${timezoneSelect.value || 'System Default'}`, LOG_TYPE.INFO); // Auto-save when timezone changes await saveStillSandsSettings(); }); } } // Homing Configuration async function initializeHomingConfig() { logMessage('Initializing homing configuration', LOG_TYPE.INFO); const homingModeCrash = document.getElementById('homingModeCrash'); const homingModeSensor = document.getElementById('homingModeSensor'); const angularOffsetInput = document.getElementById('angularOffsetInput'); const compassOffsetContainer = document.getElementById('compassOffsetContainer'); const saveHomingConfigButton = document.getElementById('saveHomingConfig'); const homingInfoContent = document.getElementById('homingInfoContent'); const autoHomeEnabledToggle = document.getElementById('autoHomeEnabledToggle'); const autoHomeSettings = document.getElementById('autoHomeSettings'); const autoHomeAfterPatternsInput = document.getElementById('autoHomeAfterPatternsInput'); // Check if elements exist if (!homingModeCrash || !homingModeSensor || !angularOffsetInput || !saveHomingConfigButton || !homingInfoContent || !compassOffsetContainer) { logMessage('Homing configuration elements not found, skipping initialization', LOG_TYPE.WARNING); return; } logMessage('Homing configuration elements found successfully', LOG_TYPE.INFO); // Function to get selected homing mode function getSelectedMode() { return homingModeCrash.checked ? 0 : 1; } // Function to update info box and visibility based on selected mode function updateHomingInfo() { const mode = getSelectedMode(); // Show/hide compass offset based on mode if (mode === 0) { compassOffsetContainer.style.display = 'none'; homingInfoContent.innerHTML = `

Crash Homing Mode:

`; } else { compassOffsetContainer.style.display = 'block'; homingInfoContent.innerHTML = `

Sensor Homing Mode:

`; } } // Load current homing configuration try { const response = await fetch('/api/homing-config'); const data = await response.json(); // Set radio button based on mode if (data.homing_mode === 1) { homingModeSensor.checked = true; } else { homingModeCrash.checked = true; } angularOffsetInput.value = data.angular_homing_offset_degrees || 0; // Load auto-home settings if (autoHomeEnabledToggle) { autoHomeEnabledToggle.checked = data.auto_home_enabled || false; if (autoHomeSettings) { autoHomeSettings.style.display = data.auto_home_enabled ? 'block' : 'none'; } } if (autoHomeAfterPatternsInput) { autoHomeAfterPatternsInput.value = data.auto_home_after_patterns || 5; } updateHomingInfo(); logMessage(`Loaded homing config: mode=${data.homing_mode}, offset=${data.angular_homing_offset_degrees}°, auto_home=${data.auto_home_enabled}, after=${data.auto_home_after_patterns}`, LOG_TYPE.INFO); } catch (error) { logMessage(`Error loading homing configuration: ${error.message}`, LOG_TYPE.ERROR); // Initialize with defaults if load fails homingModeCrash.checked = true; angularOffsetInput.value = 0; if (autoHomeEnabledToggle) autoHomeEnabledToggle.checked = false; if (autoHomeAfterPatternsInput) autoHomeAfterPatternsInput.value = 5; updateHomingInfo(); } // Function to save homing configuration async function saveHomingConfig() { // Update button UI to show loading state const originalButtonHTML = saveHomingConfigButton.innerHTML; saveHomingConfigButton.disabled = true; saveHomingConfigButton.innerHTML = 'refreshSaving...'; try { const requestBody = { homing_mode: getSelectedMode(), angular_homing_offset_degrees: parseFloat(angularOffsetInput.value) || 0 }; // Include auto-home settings if elements exist if (autoHomeEnabledToggle) { requestBody.auto_home_enabled = autoHomeEnabledToggle.checked; } if (autoHomeAfterPatternsInput) { const afterPatterns = parseInt(autoHomeAfterPatternsInput.value); if (!isNaN(afterPatterns) && afterPatterns >= 1) { requestBody.auto_home_after_patterns = afterPatterns; } } const response = await fetch('/api/homing-config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.detail || 'Failed to save homing configuration'); } // Show success state temporarily saveHomingConfigButton.innerHTML = 'checkSaved!'; showStatusMessage('Homing configuration saved successfully', 'success'); // Restore button after 2 seconds setTimeout(() => { saveHomingConfigButton.innerHTML = originalButtonHTML; saveHomingConfigButton.disabled = false; }, 2000); } catch (error) { logMessage(`Error saving homing configuration: ${error.message}`, LOG_TYPE.ERROR); showStatusMessage(`Failed to save homing configuration: ${error.message}`, 'error'); // Restore button immediately on error saveHomingConfigButton.innerHTML = originalButtonHTML; saveHomingConfigButton.disabled = false; } } // Event listeners homingModeCrash.addEventListener('change', updateHomingInfo); homingModeSensor.addEventListener('change', updateHomingInfo); saveHomingConfigButton.addEventListener('click', saveHomingConfig); // Auto-home toggle event listener if (autoHomeEnabledToggle && autoHomeSettings) { autoHomeEnabledToggle.addEventListener('change', () => { autoHomeSettings.style.display = autoHomeEnabledToggle.checked ? 'block' : 'none'; }); } } // Toggle password visibility helper function togglePasswordVisibility(inputId, button) { const input = document.getElementById(inputId); if (!input || !button) return; const icon = button.querySelector('.material-icons'); if (input.type === 'password') { input.type = 'text'; if (icon) icon.textContent = 'visibility'; } else { input.type = 'password'; if (icon) icon.textContent = 'visibility_off'; } } // MQTT Configuration async function initializeMqttConfig() { logMessage('Initializing MQTT configuration', LOG_TYPE.INFO); const mqttEnableToggle = document.getElementById('mqttEnableToggle'); const mqttSettings = document.getElementById('mqttSettings'); const mqttStatusBanner = document.getElementById('mqttStatusBanner'); const mqttConnectedBanner = document.getElementById('mqttConnectedBanner'); const mqttDisconnectedBanner = document.getElementById('mqttDisconnectedBanner'); const mqttBrokerInput = document.getElementById('mqttBrokerInput'); const mqttPortInput = document.getElementById('mqttPortInput'); const mqttUsernameInput = document.getElementById('mqttUsernameInput'); const mqttPasswordInput = document.getElementById('mqttPasswordInput'); const mqttDeviceNameInput = document.getElementById('mqttDeviceNameInput'); const mqttDeviceIdInput = document.getElementById('mqttDeviceIdInput'); const mqttClientIdInput = document.getElementById('mqttClientIdInput'); const mqttDiscoveryPrefixInput = document.getElementById('mqttDiscoveryPrefixInput'); const testMqttButton = document.getElementById('testMqttConnection'); const mqttTestResult = document.getElementById('mqttTestResult'); const saveMqttButton = document.getElementById('saveMqttConfig'); const mqttRestartNotice = document.getElementById('mqttRestartNotice'); // Check if elements exist if (!mqttEnableToggle || !mqttSettings || !saveMqttButton) { logMessage('MQTT configuration elements not found, skipping initialization', LOG_TYPE.WARNING); return; } logMessage('MQTT configuration elements found successfully', LOG_TYPE.INFO); // Track if settings have changed (to show restart notice) let originalConfig = null; let configChanged = false; // Function to update UI based on enabled state function updateMqttSettingsVisibility() { mqttSettings.style.display = mqttEnableToggle.checked ? 'block' : 'none'; if (mqttStatusBanner) { mqttStatusBanner.classList.toggle('hidden', !mqttEnableToggle.checked); } } // Function to update connection status banners function updateConnectionStatus(connected) { if (mqttConnectedBanner && mqttDisconnectedBanner) { if (connected) { mqttConnectedBanner.classList.remove('hidden'); mqttDisconnectedBanner.classList.add('hidden'); } else { mqttConnectedBanner.classList.add('hidden'); mqttDisconnectedBanner.classList.remove('hidden'); } } } // Function to check if config has changed function checkConfigChanged() { if (!originalConfig) return false; const currentConfig = { enabled: mqttEnableToggle.checked, broker: mqttBrokerInput.value, port: parseInt(mqttPortInput.value) || 1883, username: mqttUsernameInput.value, password: mqttPasswordInput.value, device_name: mqttDeviceNameInput.value, device_id: mqttDeviceIdInput.value, client_id: mqttClientIdInput.value, discovery_prefix: mqttDiscoveryPrefixInput.value }; return JSON.stringify(currentConfig) !== JSON.stringify(originalConfig); } // Function to show/hide restart notice function updateRestartNotice() { configChanged = checkConfigChanged(); if (mqttRestartNotice) { mqttRestartNotice.classList.toggle('hidden', !configChanged); } } // Load current MQTT configuration try { const response = await fetch('/api/mqtt-config'); const data = await response.json(); mqttEnableToggle.checked = data.enabled || false; mqttBrokerInput.value = data.broker || ''; mqttPortInput.value = data.port || 1883; mqttUsernameInput.value = data.username || ''; // Note: Password is not returned from API for security mqttDeviceNameInput.value = data.device_name || 'Dune Weaver'; mqttDeviceIdInput.value = data.device_id || 'dune_weaver'; mqttClientIdInput.value = data.client_id || 'dune_weaver'; mqttDiscoveryPrefixInput.value = data.discovery_prefix || 'homeassistant'; // Store original config for change detection originalConfig = { enabled: data.enabled || false, broker: data.broker || '', port: data.port || 1883, username: data.username || '', password: '', // We don't have the original password device_name: data.device_name || 'Dune Weaver', device_id: data.device_id || 'dune_weaver', client_id: data.client_id || 'dune_weaver', discovery_prefix: data.discovery_prefix || 'homeassistant' }; updateMqttSettingsVisibility(); // Update connection status if MQTT is enabled if (data.enabled) { updateConnectionStatus(data.connected || false); } logMessage(`Loaded MQTT config: enabled=${data.enabled}, broker=${data.broker}`, LOG_TYPE.INFO); } catch (error) { logMessage(`Error loading MQTT configuration: ${error.message}`, LOG_TYPE.ERROR); // Initialize with defaults if load fails mqttEnableToggle.checked = false; updateMqttSettingsVisibility(); } // Function to save MQTT configuration async function saveMqttConfig() { // Validate required fields if MQTT is enabled if (mqttEnableToggle.checked && !mqttBrokerInput.value.trim()) { showStatusMessage('MQTT broker address is required when MQTT is enabled', 'error'); mqttBrokerInput.focus(); return; } // Update button UI to show loading state const originalButtonHTML = saveMqttButton.innerHTML; saveMqttButton.disabled = true; saveMqttButton.innerHTML = 'refreshSaving...'; try { const requestBody = { enabled: mqttEnableToggle.checked, broker: mqttBrokerInput.value.trim(), port: parseInt(mqttPortInput.value) || 1883, username: mqttUsernameInput.value.trim() || null, device_name: mqttDeviceNameInput.value.trim() || 'Dune Weaver', device_id: mqttDeviceIdInput.value.trim() || 'dune_weaver', client_id: mqttClientIdInput.value.trim() || 'dune_weaver', discovery_prefix: mqttDiscoveryPrefixInput.value.trim() || 'homeassistant' }; // Only include password if it was changed (not empty) if (mqttPasswordInput.value) { requestBody.password = mqttPasswordInput.value; } const response = await fetch('/api/mqtt-config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.detail || 'Failed to save MQTT configuration'); } const data = await response.json(); // Update original config for change detection originalConfig = { enabled: requestBody.enabled, broker: requestBody.broker, port: requestBody.port, username: requestBody.username || '', password: '', // Reset password tracking device_name: requestBody.device_name, device_id: requestBody.device_id, client_id: requestBody.client_id, discovery_prefix: requestBody.discovery_prefix }; // Clear password field after save mqttPasswordInput.value = ''; // Show success state temporarily saveMqttButton.innerHTML = 'checkSaved!'; showStatusMessage('MQTT configuration saved successfully. Restart the application to apply changes.', 'success'); // Show restart notice if (mqttRestartNotice) { mqttRestartNotice.classList.remove('hidden'); } // Restore button after 2 seconds setTimeout(() => { saveMqttButton.innerHTML = originalButtonHTML; saveMqttButton.disabled = false; }, 2000); } catch (error) { logMessage(`Error saving MQTT configuration: ${error.message}`, LOG_TYPE.ERROR); showStatusMessage(`Failed to save MQTT configuration: ${error.message}`, 'error'); // Restore button immediately on error saveMqttButton.innerHTML = originalButtonHTML; saveMqttButton.disabled = false; } } // Function to test MQTT connection async function testMqttConnection() { // Validate broker address if (!mqttBrokerInput.value.trim()) { showStatusMessage('Please enter a broker address to test', 'error'); mqttBrokerInput.focus(); return; } // Update button UI to show loading state const originalButtonHTML = testMqttButton.innerHTML; testMqttButton.disabled = true; testMqttButton.innerHTML = 'refreshTesting...'; // Clear previous result if (mqttTestResult) { mqttTestResult.innerHTML = ''; } try { const requestBody = { broker: mqttBrokerInput.value.trim(), port: parseInt(mqttPortInput.value) || 1883, username: mqttUsernameInput.value.trim() || null, password: mqttPasswordInput.value || null }; const response = await fetch('/api/mqtt-test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); const data = await response.json(); if (data.success) { if (mqttTestResult) { mqttTestResult.innerHTML = 'check_circleConnection successful!'; } showStatusMessage('MQTT connection test successful', 'success'); } else { if (mqttTestResult) { mqttTestResult.innerHTML = `error${data.error || 'Connection failed'}`; } showStatusMessage(`MQTT test failed: ${data.error || 'Connection failed'}`, 'error'); } } catch (error) { logMessage(`Error testing MQTT connection: ${error.message}`, LOG_TYPE.ERROR); if (mqttTestResult) { mqttTestResult.innerHTML = `errorTest failed: ${error.message}`; } showStatusMessage(`MQTT test failed: ${error.message}`, 'error'); } finally { // Restore button testMqttButton.innerHTML = originalButtonHTML; testMqttButton.disabled = false; } } // Event listeners mqttEnableToggle.addEventListener('change', () => { updateMqttSettingsVisibility(); updateRestartNotice(); }); // Track changes to show restart notice [mqttBrokerInput, mqttPortInput, mqttUsernameInput, mqttPasswordInput, mqttDeviceNameInput, mqttDeviceIdInput, mqttClientIdInput, mqttDiscoveryPrefixInput].forEach(input => { if (input) { input.addEventListener('input', updateRestartNotice); } }); testMqttButton.addEventListener('click', testMqttConnection); saveMqttButton.addEventListener('click', saveMqttConfig); } // Initialize MQTT config when DOM is ready document.addEventListener('DOMContentLoaded', function() { initializeMqttConfig(); initializeTableTypeConfig(); }); // ============================================================================ // Table Type Configuration // ============================================================================ function initializeTableTypeConfig() { const tableTypeSelect = document.getElementById('tableTypeSelect'); const saveTableTypeButton = document.getElementById('saveTableType'); const detectedTableType = document.getElementById('detectedTableType'); if (!tableTypeSelect || !saveTableTypeButton) { logMessage('Table type elements not found', LOG_TYPE.WARNING); return; } // Load current settings loadTableTypeSettings(); // Save button click handler saveTableTypeButton.addEventListener('click', saveTableTypeConfig); async function loadTableTypeSettings() { try { const response = await fetch('/api/settings'); if (!response.ok) throw new Error('Failed to fetch settings'); const settings = await response.json(); const machine = settings.machine || {}; // Populate dropdown with available table types tableTypeSelect.innerHTML = ''; if (machine.available_table_types) { machine.available_table_types.forEach(type => { const option = document.createElement('option'); option.value = type.value; option.textContent = type.label; tableTypeSelect.appendChild(option); }); } // Set current override value if (machine.table_type_override) { tableTypeSelect.value = machine.table_type_override; } else { tableTypeSelect.value = ''; } // Update detected type display if (detectedTableType) { const detected = machine.detected_table_type; if (detected) { // Find the label for the detected type const typeInfo = machine.available_table_types?.find(t => t.value === detected); detectedTableType.textContent = typeInfo ? typeInfo.label : detected; } else { detectedTableType.textContent = 'Not connected'; } } logMessage('Table type settings loaded', LOG_TYPE.DEBUG); } catch (error) { logMessage(`Error loading table type settings: ${error.message}`, LOG_TYPE.ERROR); } } async function saveTableTypeConfig() { const originalButtonHTML = saveTableTypeButton.innerHTML; saveTableTypeButton.disabled = true; saveTableTypeButton.innerHTML = 'refreshSaving...'; try { const response = await fetch('/api/settings', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ machine: { table_type_override: tableTypeSelect.value || '' } }) }); if (!response.ok) throw new Error('Failed to save settings'); const result = await response.json(); if (result.success) { showStatusMessage('Table type settings saved. Changes will take effect on next connection.', 'success'); // Reload to show updated effective type await loadTableTypeSettings(); } else { throw new Error('Save failed'); } } catch (error) { logMessage(`Error saving table type: ${error.message}`, LOG_TYPE.ERROR); showStatusMessage(`Failed to save table type: ${error.message}`, 'error'); } finally { saveTableTypeButton.innerHTML = originalButtonHTML; saveTableTypeButton.disabled = false; } } } // ============================================================================ // Application Logs Modal // ============================================================================ let logsWebSocket = null; let logsEntries = []; const MAX_LOG_ENTRIES = 500; // Color classes for different log levels const LOG_LEVEL_COLORS = { DEBUG: 'text-gray-500 dark:text-gray-400', INFO: 'text-blue-600 dark:text-blue-400', WARNING: 'text-amber-600 dark:text-amber-400', ERROR: 'text-red-600 dark:text-red-400', CRITICAL: 'text-red-700 dark:text-red-300 font-bold' }; const LOG_LEVEL_BG = { DEBUG: 'bg-gray-100 dark:bg-gray-700', INFO: 'bg-blue-50 dark:bg-blue-900/30', WARNING: 'bg-amber-50 dark:bg-amber-900/30', ERROR: 'bg-red-50 dark:bg-red-900/30', CRITICAL: 'bg-red-100 dark:bg-red-900/50' }; function openLogsModal() { const modal = document.getElementById('logsModal'); modal.classList.remove('hidden'); // Load initial logs loadInitialLogs(); // Connect to WebSocket for real-time updates connectLogsWebSocket(); // Close on overlay click modal.addEventListener('click', (e) => { if (e.target === modal) { closeLogsModal(); } }); // Close on Escape key document.addEventListener('keydown', handleLogsEscapeKey); } function closeLogsModal() { const modal = document.getElementById('logsModal'); modal.classList.add('hidden'); // Disconnect WebSocket if (logsWebSocket) { logsWebSocket.close(); logsWebSocket = null; } // Remove event listener document.removeEventListener('keydown', handleLogsEscapeKey); } function handleLogsEscapeKey(e) { if (e.key === 'Escape') { closeLogsModal(); } } async function loadInitialLogs() { const logsContent = document.getElementById('logsContent'); logsContent.innerHTML = '
Loading logs...
'; try { const response = await fetch('/api/logs?limit=500'); const data = await response.json(); logsEntries = data.logs || []; renderLogs(); updateLogsCount(); // Scroll to bottom const container = document.getElementById('logsContainer'); container.scrollTop = container.scrollHeight; } catch (error) { console.error('Failed to load logs:', error); logsContent.innerHTML = '
Failed to load logs
'; } } function connectLogsWebSocket() { const statusEl = document.getElementById('logsConnectionStatus'); const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/ws/logs`; try { logsWebSocket = new WebSocket(wsUrl); logsWebSocket.onopen = () => { statusEl.textContent = 'Live'; statusEl.className = 'text-xs px-2 py-1 rounded-full bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300'; }; logsWebSocket.onmessage = (event) => { const message = JSON.parse(event.data); if (message.type === 'log_entry') { addLogEntry(message.data); } // Ignore heartbeat messages }; logsWebSocket.onclose = () => { statusEl.textContent = 'Disconnected'; statusEl.className = 'text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-600 text-gray-600 dark:text-gray-300'; }; logsWebSocket.onerror = (error) => { console.error('Logs WebSocket error:', error); statusEl.textContent = 'Error'; statusEl.className = 'text-xs px-2 py-1 rounded-full bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300'; }; } catch (error) { console.error('Failed to connect to logs WebSocket:', error); statusEl.textContent = 'Failed'; statusEl.className = 'text-xs px-2 py-1 rounded-full bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300'; } } function addLogEntry(entry) { // Add to beginning (newest first in memory, but we render oldest first in UI) logsEntries.unshift(entry); // Trim if exceeds max if (logsEntries.length > MAX_LOG_ENTRIES) { logsEntries.pop(); } // Add to UI const logsContent = document.getElementById('logsContent'); const entryEl = createLogEntryElement(entry); // Insert at the end (bottom) for newest logsContent.appendChild(entryEl); // Auto-scroll if enabled const autoScroll = document.getElementById('logsAutoScroll').checked; if (autoScroll) { const container = document.getElementById('logsContainer'); container.scrollTop = container.scrollHeight; } // Apply filter if active const levelFilter = document.getElementById('logLevelFilter').value; if (levelFilter && entry.level !== levelFilter) { entryEl.classList.add('hidden'); } updateLogsCount(); } function createLogEntryElement(entry) { const div = document.createElement('div'); div.className = `log-entry flex gap-2 py-1 px-2 rounded ${LOG_LEVEL_BG[entry.level] || 'bg-gray-50 dark:bg-gray-800'}`; div.dataset.level = entry.level; // Format timestamp (show only time part for brevity) const timestamp = new Date(entry.timestamp); const timeStr = timestamp.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }); const msStr = timestamp.getMilliseconds().toString().padStart(3, '0'); div.innerHTML = ` ${timeStr}.${msStr} ${entry.level} ${entry.module}:${entry.line} ${escapeHtml(entry.message)} `; return div; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function renderLogs() { const logsContent = document.getElementById('logsContent'); logsContent.innerHTML = ''; // Render in reverse order (oldest first, newest at bottom) const reversedLogs = [...logsEntries].reverse(); const levelFilter = document.getElementById('logLevelFilter').value; for (const entry of reversedLogs) { const entryEl = createLogEntryElement(entry); if (levelFilter && entry.level !== levelFilter) { entryEl.classList.add('hidden'); } logsContent.appendChild(entryEl); } } function filterLogs() { const levelFilter = document.getElementById('logLevelFilter').value; const entries = document.querySelectorAll('#logsContent .log-entry'); let visibleCount = 0; entries.forEach(entry => { if (!levelFilter || entry.dataset.level === levelFilter) { entry.classList.remove('hidden'); visibleCount++; } else { entry.classList.add('hidden'); } }); // Update count to show filtered amount const countEl = document.getElementById('logsCount'); if (levelFilter) { countEl.textContent = `${visibleCount} of ${logsEntries.length} entries (filtered)`; } else { countEl.textContent = `${logsEntries.length} entries`; } } function updateLogsCount() { const countEl = document.getElementById('logsCount'); const levelFilter = document.getElementById('logLevelFilter').value; if (levelFilter) { const filteredCount = logsEntries.filter(e => e.level === levelFilter).length; countEl.textContent = `${filteredCount} of ${logsEntries.length} entries (filtered)`; } else { countEl.textContent = `${logsEntries.length} entries`; } } async function clearLogs() { try { await fetch('/api/logs', { method: 'DELETE' }); logsEntries = []; document.getElementById('logsContent').innerHTML = '
Logs cleared
'; updateLogsCount(); } catch (error) { console.error('Failed to clear logs:', error); } }