170 lines
7.9 KiB
JavaScript
170 lines
7.9 KiB
JavaScript
// --- 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 };
|