// --- Server-Sent Events (SSE) Management --- // Note: Relies on several functions and variables being globally accessible: // - Global state: currentProjectData, selectedProjectName, eventSource // - State helpers: isProcessing, setSelectedProject // - UI functions: uiUpdateStatusBar, uiShowProcessingState, uiClearProcessingState, uiUpdateProjectSelector, uiClearProjectDisplay // - Core update functions: handleProjectChange, updateUIScadaCore, updateUIDrawingCore, updateUIConflictsCore // - Utility: debounce /** * Processes the full data update received from the SSE stream. * Updates the internal state and triggers UI updates based on the data. * @param {object} fullData - The complete data object from the server. */ function sseProcessUpdate(fullData) { console.log("[SSE] Received Full Data:", fullData); // Store the latest full data globally currentProjectData = {}; // Reset first const incomingProjects = fullData.projects || []; incomingProjects.forEach(projName => { currentProjectData[projName] = { status: fullData.status ? fullData.status[projName] : 'Unknown', last_commit: fullData.last_commit ? fullData.last_commit[projName] : 'N/A', // Ensure progress exists and has expected structure progress: (fullData.progress && fullData.progress[projName]) ? fullData.progress[projName] : { overall: {}, panels: {} } }; }); // --- Update Project Selector Dropdown --- const selectionChanged = uiUpdateProjectSelector(incomingProjects); if (selectionChanged) { // If the selection was forced (e.g., previous deleted, list empty) // handleProjectChange will update the UI for the new/empty selection console.log("[SSE] Project selection changed by dropdown update. Calling handleProjectChange."); handleProjectChange(); // Needs global access return; // Stop further processing here } // --- Process data for the currently selected project --- const currentSelection = document.getElementById('projectSelector')?.value; // Check selector exists // Update global state if needed (should normally match after selector update) if (selectedProjectName !== currentSelection) { setSelectedProject(currentSelection); // Needs global access } if (!selectedProjectName || !currentProjectData[selectedProjectName]) { console.log("[SSE] No project selected or no data available for selected project."); // Update UI to reflect lack of selection/data const projectSelector = document.getElementById('projectSelector'); if (projectSelector && projectSelector.options.length > 0 && projectSelector.value) { uiUpdateStatusBar(selectedProjectName, 'Waiting for data...', 'N/A'); uiShowProcessingState('Waiting for data...'); } else { uiUpdateStatusBar('...', 'No projects available', 'N/A'); uiClearProjectDisplay(); } return; } const projectData = currentProjectData[selectedProjectName]; // Update status bar immediately uiUpdateStatusBar(selectedProjectName, projectData.status, projectData.last_commit); // Update main UI based on processing state const projectIsCurrentlyProcessing = isProcessing(projectData.status); console.log(`[SSE] Status Check: Project '${selectedProjectName}', Status: '${projectData.status}', isProcessing: ${projectIsCurrentlyProcessing}`); if (projectIsCurrentlyProcessing) { // If *any* project is processing, the global flag should remain true isAnalysisGloballyActive = true; console.log(`[State] isAnalysisGloballyActive set/kept true due to processing status: "${projectData.status}"`); uiShowProcessingState(projectData.status); } else { // --- Update Global Flag: Check if ANY project is still processing --- let anyProjectStillProcessing = false; const projectNames = Object.keys(currentProjectData); for (const projName of projectNames) { if (currentProjectData[projName] && isProcessing(currentProjectData[projName].status)) { anyProjectStillProcessing = true; console.log(`[State] Project ${projName} is still processing. Keeping isAnalysisGloballyActive true.`); break; // Found one, no need to check others } } if (!anyProjectStillProcessing) { isAnalysisGloballyActive = false; console.log("[State] No projects are processing. isAnalysisGloballyActive set to false."); } // --- End Global Flag Update --- // Added Debugging: Log before clearing and redrawing console.log(`[SSE] Clearing processing state and triggering redraw for ${selectedProjectName}`); uiClearProcessingState(); // Call core redraws immediately if(projectData && !isProcessing(projectData.status)) { console.log(`[SSE] Confirmed non-processing state for ${selectedProjectName}. Calling core redraw functions.`); updateUIScadaCore(projectData); // Needs global access updateUIDrawingCore(projectData); // Needs global access updateUIConflictsCore(projectData); // Needs global access } else { // This case handles if the status somehow flipped back to processing // between the initial check and this point (very unlikely but safe). const latestStatus = projectData?.status || "Processing..."; console.warn(`[SSE] State unexpectedly indicates processing just before redraw for ${selectedProjectName}. Status: '${latestStatus}'. Showing processing state again.`); uiShowProcessingState(latestStatus); } } } // Create a debounced version of the process update function const debouncedProcessUpdate = debounce(sseProcessUpdate, 250); /** * Initializes the Server-Sent Events connection and sets up message handlers. */ function sseInitialize() { console.log("[SSE] Initializing connection..."); // Use global eventSource variable from state.js if (eventSource) { console.log("[SSE] Closing existing connection."); eventSource.close(); } try { eventSource = new EventSource("/stream"); eventSource.onmessage = function(event) { try { const data = JSON.parse(event.data); // Call the debounced processor debouncedProcessUpdate(data); } catch (error) { console.error("[SSE] Error parsing data:", error, "Data:", event.data); // Update UI to show error uiUpdateStatusBar(selectedProjectName, 'Error processing server update.', 'N/A'); } }; eventSource.onerror = function(err) { console.error("[SSE] Connection error:", err); // Update UI to show error uiUpdateStatusBar(selectedProjectName, 'Connection lost. Retrying...', 'N/A'); // Simple retry mechanism: close and re-initialize after a delay if (eventSource) { eventSource.close(); eventSource = null; // Nullify to allow re-creation } setTimeout(sseInitialize, 5000); // Attempt to reconnect after 5 seconds }; eventSource.onopen = function() { console.log("[SSE] Connection opened."); // Optionally update UI to show connected status uiUpdateStatusBar(selectedProjectName, "Connected. Waiting for updates...", currentProjectData[selectedProjectName]?.last_commit); }; console.log("[SSE] Event handlers set up."); } catch (error) { console.error("[SSE] Failed to create EventSource:", error); uiUpdateStatusBar(selectedProjectName, 'Failed to connect to server.', 'N/A'); } } // --- Export (if using modules) --- // export { sseInitialize };