// --- Global State Variables --- let chartInstancesScada = {}; // Separate instances for SCADA let chartInstancesDrawing = {}; // Separate instances for Drawing let currentProjectData = {}; // Stores the LATEST full data received from SSE { project: {status, commit, progress}, ... } let selectedProjectName = null; // Track the currently selected project let detailsModalInstance = null; let currentVisibleSection = 'scada'; // Track visible section: 'scada', 'drawing', 'conflicts' // --- Chart Configurations --- const scadaChartLabels = ['Found in SCADA', 'Not Found in SCADA']; const scadaChartColors = ['rgb(13, 110, 253)', 'rgb(220, 53, 69)']; const drawingChartLabels = ['Found in Drawing', 'Not Found in Drawing']; const drawingChartColors = ['rgb(25, 135, 84)', 'rgb(220, 53, 69)']; // Map backend list keys for modal clicks (can be combined or kept separate if needed) const scadaListKeysMap = { found: ['found_both_list', 'found_scada_only_list'], notFound: ['found_drawing_only_list', 'missing_list'] }; const drawingListKeysMap = { found: ['found_both_list', 'found_drawing_only_list'], notFound: ['found_scada_only_list', 'missing_list'] }; // --- Debounce Utility (Only need one) --- function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // --- NEW: Helper Functions for Processing State --- // Checks if the status message indicates ongoing processing function isProcessing(statusMsg) { if (!statusMsg) return false; // Handle undefined/null status const lowerStatus = statusMsg.toLowerCase(); // Keywords indicating processing (adjust as needed based on app.py status messages) return lowerStatus.includes('initializ') || // initializing, initial... lowerStatus.includes('cloning') || lowerStatus.includes('fetching') || lowerStatus.includes('pulling') || lowerStatus.includes('checking') || // checking repo, checking scada, checking drawings lowerStatus.includes('reading manifest') || lowerStatus.includes('calculating') || lowerStatus.includes('extracting') || // If PDF extraction status is sent lowerStatus.includes('loading data'); // From handleProjectChange initial state } // Displays a loading indicator in a container element function showLoadingIndicator(containerElement, message = "Processing project data...") { if (!containerElement) return; // Hide existing content for (const child of containerElement.children) { if (!child.classList.contains('processing-indicator')) { child.classList.add('content-hidden-by-loader'); } } let indicatorDiv = containerElement.querySelector('.processing-indicator'); if (!indicatorDiv) { indicatorDiv = document.createElement('div'); indicatorDiv.className = 'text-center p-4 processing-indicator'; indicatorDiv.innerHTML = `
Loading...

`; // Prepend to ensure it's visible even if container had position: relative children containerElement.prepend(indicatorDiv); } // Update message const messageElement = indicatorDiv.querySelector('p'); if (messageElement) { // Sanitize message slightly before displaying const safeMessage = message.replace(//g, ">"); messageElement.textContent = safeMessage; } indicatorDiv.style.display = 'block'; // Ensure indicator is visible } // Removes the loading indicator from a container function clearLoadingIndicator(containerElement) { if (!containerElement) return; // Remove indicator const indicatorDiv = containerElement.querySelector('.processing-indicator'); if (indicatorDiv) { // indicatorDiv.remove(); // Or hide it if preferred indicatorDiv.style.display = 'none'; } // Show original content for (const child of containerElement.children) { child.classList.remove('content-hidden-by-loader'); } // Remove default "Loading panel data..." placeholders if they still exist const placeholderP = Array.from(containerElement.querySelectorAll('p.fst-italic')).find(p => p.textContent.toLowerCase().includes('loading') && !p.closest('.processing-indicator') ); if (placeholderP) { placeholderP.remove(); } } // Sets the entire UI to reflect a processing state function showProcessingStateUI(projectData) { const statusMsg = projectData ? projectData.status : "Loading data..."; const containers = [ document.getElementById('overall-scada-progress'), document.getElementById('scada-panels-progress'), document.getElementById('overall-drawing-progress'), document.getElementById('drawing-panels-progress'), document.getElementById('panels-conflicts') ]; console.log(`[UI State] Setting processing state: "${statusMsg}"`); // Destroy all existing charts immediately to prevent rendering issues Object.values(chartInstancesScada).forEach(chart => chart?.destroy()); chartInstancesScada = {}; Object.values(chartInstancesDrawing).forEach(chart => chart?.destroy()); chartInstancesDrawing = {}; // Show loading indicator in all content containers containers.forEach(container => { if (container) { showLoadingIndicator(container, statusMsg); // Show the actual status message } }); // Clear text content that is updated directly (like overall percentages) const overallScadaText = document.getElementById('overall-scada-text'); if (overallScadaText) overallScadaText.textContent = ''; const overallDrawingText = document.getElementById('overall-drawing-text'); if (overallDrawingText) overallDrawingText.textContent = ''; const conflictCount = document.getElementById('conflict-count'); if (conflictCount) { conflictCount.textContent = '...'; conflictCount.style.display = 'inline-block'; // Show it while loading } // Ensure the correct section's container is visible if it was hidden // (showSection handles visibility, but this prevents blank screens if called directly) const scadaContent = document.getElementById('scada-content'); const drawingsContent = document.getElementById('drawings-content'); const conflictsContent = document.getElementById('conflicts-content'); if (scadaContent && currentVisibleSection === 'scada') scadaContent.style.display = 'block'; if (drawingsContent && currentVisibleSection === 'drawings') drawingsContent.style.display = 'block'; if (conflictsContent && currentVisibleSection === 'conflicts') conflictsContent.style.display = 'block'; } // Clears loading indicators and triggers the actual UI rendering function showReadyStateUI(projectData) { console.log(`[UI State] Setting ready state for project: ${selectedProjectName}`); const containers = [ document.getElementById('overall-scada-progress'), document.getElementById('scada-panels-progress'), document.getElementById('overall-drawing-progress'), document.getElementById('drawing-panels-progress'), document.getElementById('panels-conflicts') ]; // Clear loading indicators from all containers containers.forEach(container => { if(container) clearLoadingIndicator(container); }); // Call core update functions (wrapped in setTimeout for smooth rendering) console.log(`Project state is ready. Queueing core redraw.`); setTimeout(() => { console.log(`Passing project data to UI updates:`, projectData); updateUIScadaCore(projectData); updateUIDrawingCore(projectData); updateUIConflictsCore(projectData); }, 0); } // --- Chart Click Handler (Needs PROJECT CONTEXT) --- function handleChartClick(event, elements, chart, context) { if (elements.length > 0 && selectedProjectName) { // Check if a project is selected const clickedElementIndex = elements[0].index; const isOverallChart = chart.canvas.id.startsWith('overall-'); const identifier = isOverallChart ? '__overall__' : chart.canvas.id.replace(`chart-${context}-`, ''); const categoryType = clickedElementIndex === 0 ? 'found' : 'notFound'; // Pass selectedProjectName to the modal function showDetailsModal(selectedProjectName, identifier, categoryType, context); } } // --- Core UI Update Functions (Need selected project data) --- function updateUIScadaCore(projectData) { // Accepts data for the selected project console.log(`Running core SCADA UI redraw logic for project: ${selectedProjectName}`); const progressDetails = (projectData && projectData.progress) ? projectData.progress : { overall: {}, panels: {} }; const overallData = progressDetails.overall || {}; const overallTotal = overallData.total_csv || 0; const overallFoundScada = (overallData.found_both || 0) + (overallData.found_scada_only || 0); const overallNotFoundScada = (overallData.found_drawing_only || 0) + (overallData.missing_both || 0); const overallPercentageFound = overallTotal > 0 ? ((overallFoundScada / overallTotal) * 100).toFixed(1) : 0; const overallChartCounts = [overallFoundScada, overallNotFoundScada]; // Update project name display in headers document.querySelectorAll('.project-name-display').forEach(el => el.textContent = selectedProjectName || '...'); const overallScadaTextElement = document.getElementById('overall-scada-text'); if (overallScadaTextElement) { overallScadaTextElement.textContent = `Found in SCADA: ${overallFoundScada}/${overallTotal} (${overallPercentageFound}%)`; } else { console.warn("Element with ID 'overall-scada-text' not found when trying to update SCADA text."); } const isSectionVisible = (currentVisibleSection === 'scada'); if (isSectionVisible) { const overallScadaCanvas = document.getElementById('overall-scada-chart-canvas'); if (chartInstancesScada['overall']) { if (JSON.stringify(chartInstancesScada['overall'].data.datasets[0].data) !== JSON.stringify(overallChartCounts)) { chartInstancesScada['overall'].data.datasets[0].data = overallChartCounts; chartInstancesScada['overall'].update('none'); } } else if (overallScadaCanvas) { console.log("Creating overall SCADA chart (visible)."); const ctxOverall = overallScadaCanvas.getContext('2d'); // Pass selectedProjectName to identify data context for clicks/tooltips chartInstancesScada['overall'] = new Chart(ctxOverall, createChartConfig(overallChartCounts, overallTotal, 'scada', 'overall', selectedProjectName)); } } else { if (chartInstancesScada['overall']) { console.log("Destroying hidden overall SCADA chart."); chartInstancesScada['overall'].destroy(); delete chartInstancesScada['overall']; } } const panelsContainer = document.getElementById('scada-panels-progress'); const panelsData = progressDetails.panels || {}; updatePanelCharts(panelsContainer, panelsData, chartInstancesScada, 'scada'); console.log("Finished SCADA UI core redraw."); } function updateUIDrawingCore(projectData) { // Accepts data for the selected project console.log(`Running core Drawing UI redraw logic for project: ${selectedProjectName}`); const progressDetails = (projectData && projectData.progress) ? projectData.progress : { overall: {}, panels: {} }; const overallData = progressDetails.overall || {}; const overallTotal = overallData.total_csv || 0; const overallFoundDrawing = (overallData.found_both || 0) + (overallData.found_drawing_only || 0); const overallNotFoundDrawing = (overallData.found_scada_only || 0) + (overallData.missing_both || 0); const overallPercentageFound = overallTotal > 0 ? ((overallFoundDrawing / overallTotal) * 100).toFixed(1) : 0; const overallChartCounts = [overallFoundDrawing, overallNotFoundDrawing]; document.querySelectorAll('.project-name-display').forEach(el => el.textContent = selectedProjectName || '...'); const overallDrawingTextElement = document.getElementById('overall-drawing-text'); if (overallDrawingTextElement) { overallDrawingTextElement.textContent = `Found in Drawing: ${overallFoundDrawing}/${overallTotal} (${overallPercentageFound}%)`; } else { console.warn("Element with ID 'overall-drawing-text' not found when trying to update Drawing text."); } const isSectionVisible = (currentVisibleSection === 'drawings'); if (isSectionVisible) { const overallDrawingCanvas = document.getElementById('overall-drawing-chart-canvas'); if (chartInstancesDrawing['overall']) { if (JSON.stringify(chartInstancesDrawing['overall'].data.datasets[0].data) !== JSON.stringify(overallChartCounts)) { chartInstancesDrawing['overall'].data.datasets[0].data = overallChartCounts; chartInstancesDrawing['overall'].update('none'); } } else if (overallDrawingCanvas) { console.log("Creating overall drawing chart (visible)."); const ctxOverall = overallDrawingCanvas.getContext('2d'); chartInstancesDrawing['overall'] = new Chart(ctxOverall, createChartConfig(overallChartCounts, overallTotal, 'drawing', 'overall', selectedProjectName)); } } else { if (chartInstancesDrawing['overall']) { console.log("Destroying hidden overall Drawing chart."); chartInstancesDrawing['overall'].destroy(); delete chartInstancesDrawing['overall']; } } const panelsContainer = document.getElementById('drawing-panels-progress'); const panelsData = progressDetails.panels || {}; updatePanelCharts(panelsContainer, panelsData, chartInstancesDrawing, 'drawings'); console.log("Finished Drawing UI core redraw."); } function updateUIConflictsCore(projectData) { // Accepts data for the selected project console.log(`Running core Conflicts UI redraw logic for project: ${selectedProjectName}`); const progressDetails = (projectData && projectData.progress) ? projectData.progress : { overall: {}, panels: {} }; const panelsContainer = document.getElementById('panels-conflicts'); panelsContainer.innerHTML = ''; document.querySelectorAll('.project-name-display').forEach(el => el.textContent = selectedProjectName || '...'); const panelsData = progressDetails.panels || {}; let totalConflicts = 0; let panelsWithConflicts = 0; if (!panelsData || Object.keys(panelsData).length === 0) { panelsContainer.innerHTML = '

No panel data available yet.

'; } else { const sortedPanels = Object.keys(panelsData).sort(); sortedPanels.forEach(panelName => { const panel = panelsData[panelName]; const conflictsList = panel.found_scada_only_list || []; if (conflictsList.length > 0) { panelsWithConflicts++; totalConflicts += conflictsList.length; // ... (Create header and table as in conflicts.html) ... const panelHeader = document.createElement('h4'); panelHeader.className = 'mt-4 mb-2'; panelHeader.textContent = `${panelName} (${conflictsList.length} conflicts)`; panelsContainer.appendChild(panelHeader); const table = document.createElement('table'); table.className = 'table table-sm table-striped table-hover table-bordered'; const thead = table.createTHead(); thead.innerHTML = `AliasPanelSCADA StatusDrawing StatusEquipment TypeType of Conveyor`; const tbody = table.createTBody(); conflictsList.sort((a, b) => a.alias.localeCompare(b.alias)).forEach(item => { const row = tbody.insertRow(); row.classList.add('table-warning'); row.insertCell().textContent = item.alias; row.insertCell().textContent = item.control_panel; row.insertCell().innerHTML = 'Yes'; row.insertCell().innerHTML = 'No'; row.insertCell().textContent = item.equipment_type || 'N/A'; row.insertCell().textContent = item.conveyor_type || 'N/A'; }); panelsContainer.appendChild(table); } }); if (panelsWithConflicts === 0) { panelsContainer.innerHTML = '

No conflicts found across all panels.

'; } } // Update total count badge const countBadge = document.getElementById('conflict-count'); if (countBadge) { countBadge.textContent = totalConflicts; countBadge.style.display = totalConflicts > 0 ? 'inline-block' : 'none'; } console.log("Finished Conflicts UI core redraw."); } // --- Generic Panel Chart Update Logic --- function updatePanelCharts(panelsContainer, panelsData, chartInstances, context) { // context: 'scada' or 'drawing' const incomingPanelNames = new Set(Object.keys(panelsData).sort()); const existingInstanceNames = new Set(Object.keys(chartInstances).filter(k => k !== 'overall')); // --- Check if the context matches the currently visible section --- const isSectionVisible = (context === currentVisibleSection); if (!isSectionVisible) { // If section is not visible, destroy existing panel chart instances for this context console.log(`Destroying hidden panel charts for context: ${context}`); existingInstanceNames.forEach(panelName => { if (chartInstances[panelName]) { chartInstances[panelName].destroy(); delete chartInstances[panelName]; } }); // Don't proceed further if the section is hidden return; } if (incomingPanelNames.size > 0) { const loadingMsg = panelsContainer.querySelector('p'); if (loadingMsg) { loadingMsg.remove(); } incomingPanelNames.forEach(panelName => { const panel = panelsData[panelName]; const panelTotal = (panel && panel.total) || 0; let panelChartCounts = [0, 0]; // Default to [0, 0] if (panel) { // Only calculate if panel data exists if (context === 'scada') { panelChartCounts = [(panel.found_both || 0) + (panel.found_scada_only || 0), (panel.found_drawing_only || 0) + (panel.missing_both || 0)]; } else { // drawing panelChartCounts = [(panel.found_both || 0) + (panel.found_drawing_only || 0), (panel.found_scada_only || 0) + (panel.missing_both || 0)]; } } // --- Only update/create chart if section is visible --- if (isSectionVisible) { if (chartInstances[panelName]) { if (JSON.stringify(chartInstances[panelName].data.datasets[0].data) !== JSON.stringify(panelChartCounts)) { chartInstances[panelName].data.datasets[0].data = panelChartCounts; chartInstances[panelName].update('none'); } } else { let canvas = document.getElementById(`chart-${context}-${panelName}`); // Use context in ID if (canvas) { console.log(`Recreating ${context} chart instance for panel (visible): ${panelName}`); const ctx = canvas.getContext('2d'); chartInstances[panelName] = new Chart(ctx, createChartConfig(panelChartCounts, panelTotal, context, panelName, selectedProjectName)); } else { console.log(`Creating new ${context} panel elements and chart (visible) for: ${panelName}`); const chartContainer = document.createElement('div'); chartContainer.id = `chart-container-${context}-${panelName}`; // Use context in ID chartContainer.className = 'chart-container'; const label = document.createElement('span'); label.className = 'chart-label'; label.textContent = panelName; canvas = document.createElement('canvas'); // Reassign canvas variable canvas.id = `chart-${context}-${panelName}`; // Use context in ID canvas.className = 'panel-chart-canvas'; chartContainer.appendChild(label); chartContainer.appendChild(canvas); // Added Log before append console.log(`[updatePanelCharts] Appending chartContainer (${chartContainer.id}) to panelsContainer (${panelsContainer ? panelsContainer.id : 'null'})`); panelsContainer.appendChild(chartContainer); // Append to the main panels progress div const ctx = canvas.getContext('2d'); chartInstances[panelName] = new Chart(ctx, createChartConfig(panelChartCounts, panelTotal, context, panelName, selectedProjectName)); } } } // --- End visibility check --- }); } else { if (!panelsContainer.querySelector('p')) { panelsContainer.innerHTML = '

No panel data available yet.

'; } } existingInstanceNames.forEach(panelName => { if (!incomingPanelNames.has(panelName)) { console.log(`Removing ${context} panel elements and chart for: ${panelName}`); // Ensure chart is destroyed before removing element if (chartInstances[panelName]) { chartInstances[panelName].destroy(); delete chartInstances[panelName]; } const chartElement = document.getElementById(`chart-container-${context}-${panelName}`); // Use context if (chartElement) { chartElement.remove(); } } }); } // --- Generic Helper to create chart config --- Needs PROJECT context --- function createChartConfig(chartCounts, total, context, identifier, projectName) { // Added projectName const labels = context === 'scada' ? scadaChartLabels : drawingChartLabels; const colors = context === 'scada' ? scadaChartColors : drawingChartColors; const datasetLabel = context === 'scada' ? 'SCADA Match' : 'Drawing Match'; // Retrieve the correct project's progress data for tooltip calculation const projectProgress = (currentProjectData[projectName] && currentProjectData[projectName].progress) ? currentProjectData[projectName].progress : {}; return { type: 'pie', data: { labels: labels, datasets: [{ label: datasetLabel, data: chartCounts, backgroundColor: colors, hoverOffset: 4 }] }, options: { responsive: true, maintainAspectRatio: false, onClick: (event, elements, chart) => handleChartClick(event, elements, chart, context), // Pass context plugins: { legend: { display: false }, tooltip: { callbacks: { label: function(ctxTooltip) { let label = ctxTooltip.label || ''; if (label) label += ': '; const value = ctxTooltip.parsed; if (value !== null) label += value; // Workaround: Use total passed to function for panel charts, access stored data for overall const chartTotal = (identifier === 'overall' && projectProgress.overall) ? projectProgress.overall.total_csv : total; // Use the 'total' passed in for panel charts if (chartTotal && chartTotal > 0 && value !== null) { // Add null check for value label += ` (${((value / chartTotal) * 100).toFixed(1)}%)`; } return label; } } } } } }; } // --- Process Update: Extracts data for selected project --- function processUpdate(fullData) { console.log("SSE Received Full Data:", fullData); // Log the raw data // Store the latest full data currentProjectData = {}; // Reset first fullData.projects.forEach(projName => { currentProjectData[projName] = { status: fullData.status ? fullData.status[projName] : 'Unknown', last_commit: fullData.last_commit ? fullData.last_commit[projName] : 'N/A', progress: fullData.progress ? fullData.progress[projName] : { overall: {}, panels: {} } }; }); // Get current selection AFTER storing data selectedProjectName = document.getElementById('projectSelector').value; console.log(`Selected project: ${selectedProjectName}`); if (!selectedProjectName || !currentProjectData[selectedProjectName]) { console.log("No project selected or no data for selected project. UI updates skipped."); // Optionally clear the UI or show a message updateStatusBar('N/A', 'No project selected or no data'); return; } // Extract data for the selected project const projectData = currentProjectData[selectedProjectName]; const currentCommit = projectData.last_commit; // Update status bar immediately for the selected project updateStatusBar(selectedProjectName, projectData.status, projectData.last_commit); // Check the processing state and update UI accordingly if (isProcessing(projectData.status)) { console.log(`Project ${selectedProjectName} is processing. Showing loading state.`); showProcessingStateUI(projectData); } else { console.log(`Project ${selectedProjectName} is ready/error. Showing final state.`); // TODO: Add check here if commit hash changed? Or just always update? // For now, always update the UI if the state is not 'processing'. showReadyStateUI(projectData); } } // --- Debounced version of the processing function --- const debouncedProcessUpdate = debounce(processUpdate, 250); // Single debouncer // --- Modal Display Function (Needs PROJECT context) --- function showDetailsModal(projectName, identifier, categoryType, context) { // Added projectName let sourceData = null; let panelNameDisplay = ""; const listKeysMap = context === 'scada' ? scadaListKeysMap : drawingListKeysMap; const listTypeLabel = categoryType === 'found' ? (context === 'scada' ? 'Found in SCADA' : 'Found in Drawing') : (context === 'scada' ? 'Not Found in SCADA' : 'Not Found in Drawing'); // Get the specific project's progress data const projectProgress = (currentProjectData[projectName] && currentProjectData[projectName].progress) ? currentProjectData[projectName].progress : {}; if (identifier === '__overall__') { sourceData = projectProgress.overall || null; panelNameDisplay = `Overall (${projectName})`; } else { sourceData = (projectProgress.panels) ? projectProgress.panels[identifier] : null; panelNameDisplay = `${identifier} (${projectName})`; } if (!sourceData) { console.error(`Could not find source data for modal. Project: ${projectName}, Identifier: ${identifier}, Context: ${context}`); alert("Error: Could not load details data."); return; } const backendListKeys = listKeysMap[categoryType]; if (!backendListKeys) { /* ... error handling ... */ return; } let combinedDataList = []; backendListKeys.forEach(key => { if (sourceData[key]) { combinedDataList = combinedDataList.concat(sourceData[key]); } }); if (combinedDataList.length === 0) { /* ... alert handling ... */ return; } const modalTitleElement = document.getElementById('detailsModalLabel'); const modalTableBody = document.querySelector('#detailsModal .modal-body tbody'); modalTitleElement.innerHTML = `${listTypeLabel} Items for ${panelNameDisplay} ${combinedDataList.length}`; modalTableBody.innerHTML = ''; combinedDataList.sort((a, b) => a.alias.localeCompare(b.alias)).forEach(item => { const row = document.createElement('tr'); row.insertCell().textContent = item.alias; row.insertCell().textContent = item.control_panel; const scadaCell = row.insertCell(); scadaCell.innerHTML = item.found_scada ? 'Yes' : 'No'; const drawingCell = row.insertCell(); drawingCell.innerHTML = item.found_drawing ? 'Yes' : 'No'; row.insertCell().textContent = item.equipment_type || 'N/A'; row.insertCell().textContent = item.conveyor_type || 'N/A'; if (item.found_scada && !item.found_drawing) { row.classList.add('table-warning'); } modalTableBody.appendChild(row); }); if (!detailsModalInstance) { detailsModalInstance = new bootstrap.Modal(document.getElementById('detailsModal')); } detailsModalInstance.show(); } // --- Update Status Bar Helper --- function updateStatusBar(projectName, statusMsg, commitHash) { document.getElementById('selected-project-status-name').textContent = projectName || '...'; document.getElementById('status-message').textContent = statusMsg || 'N/A'; document.getElementById('last-commit').textContent = commitHash || 'N/A'; } // --- Navigation Handling --- function showSection(sectionId) { console.log("Showing section:", sectionId); document.getElementById('scada-content').style.display = 'none'; document.getElementById('drawings-content').style.display = 'none'; document.getElementById('conflicts-content').style.display = 'none'; const elementToShow = document.getElementById(`${sectionId}-content`); if (elementToShow) { elementToShow.style.display = 'block'; currentVisibleSection = sectionId; // --- Update content based on current project state --- const projectData = currentProjectData[selectedProjectName]; if (projectData) { const statusMsg = projectData.status; console.log(`[ShowSection] Updating visible section ${sectionId} for project ${selectedProjectName}. Status: ${statusMsg}`); if (isProcessing(statusMsg)) { // Project is processing, ensure loading indicator is shown in the relevant containers for this section console.log(`[ShowSection] Project processing, showing loading indicator for ${sectionId}.`); if (sectionId === 'scada') { showLoadingIndicator(document.getElementById('overall-scada-progress'), statusMsg); showLoadingIndicator(document.getElementById('scada-panels-progress'), statusMsg); } else if (sectionId === 'drawings') { showLoadingIndicator(document.getElementById('overall-drawing-progress'), statusMsg); showLoadingIndicator(document.getElementById('drawing-panels-progress'), statusMsg); } else if (sectionId === 'conflicts') { showLoadingIndicator(document.getElementById('panels-conflicts'), statusMsg); } // Destroy any charts that might have been left over (belt and braces) if (sectionId === 'scada') { Object.values(chartInstancesScada).forEach(chart => chart?.destroy()); chartInstancesScada = {}; } else if (sectionId === 'drawings') { Object.values(chartInstancesDrawing).forEach(chart => chart?.destroy()); chartInstancesDrawing = {}; } } else { // Project is ready, trigger the specific update function for the visible section console.log(`[ShowSection] Project ready, calling update function for ${sectionId}.`); // Use setTimeout to ensure DOM update (display: block) is processed first setTimeout(() => { // Re-fetch projectData in case it changed slightly between checks const currentData = currentProjectData[selectedProjectName]; if (currentData && !isProcessing(currentData.status)) { // Double check status if (sectionId === 'scada') { updateUIScadaCore(currentData); } else if (sectionId === 'drawings') { updateUIDrawingCore(currentData); } else if (sectionId === 'conflicts') { updateUIConflictsCore(currentData); } } else { console.log(`[ShowSection] Status changed to processing before UI update could run for ${sectionId}.`) // If it became processing again, show indicator showProcessingStateUI(currentData); } }, 0); // Delay slightly } } else { console.log(`[ShowSection] Section ${sectionId} shown, but no data currently available for project ${selectedProjectName}.`); // Show loading indicator in the visible section as data is missing const msg = "Loading data..."; if (sectionId === 'scada') { showLoadingIndicator(document.getElementById('overall-scada-progress'), msg); showLoadingIndicator(document.getElementById('scada-panels-progress'), msg); } else if (sectionId === 'drawings') { showLoadingIndicator(document.getElementById('overall-drawing-progress'), msg); showLoadingIndicator(document.getElementById('drawing-panels-progress'), msg); } else if (sectionId === 'conflicts') { showLoadingIndicator(document.getElementById('panels-conflicts'), msg); } } // --- End section update trigger --- } else { console.error("Attempted to show unknown section:", sectionId); document.getElementById('scada-content').style.display = 'block'; // Default back to SCADA currentVisibleSection = 'scada'; } // Update active nav link document.querySelectorAll('.nav-link').forEach(link => { link.classList.remove('active'); // Match link's data-view attribute to sectionId if (link.getAttribute('data-view') === sectionId) { link.classList.add('active'); } }); } document.addEventListener('DOMContentLoaded', () => { console.log("DOM Loaded, setting up navigation and project selector..."); const projectSelector = document.getElementById('projectSelector'); if(projectSelector) { // Set initial selection based on first option (or potentially embedded initial data) selectedProjectName = projectSelector.value; projectSelector.addEventListener('change', handleProjectChange); console.log(`Initial project selected: ${selectedProjectName}`); // Update initial status bar text const initialStatus = (initialServerData && initialServerData.status && initialServerData.status[selectedProjectName]) ? initialServerData.status[selectedProjectName] : 'Initializing...'; updateStatusBar(selectedProjectName, initialStatus, 'N/A'); } document.querySelectorAll('.nav-link').forEach(link => { // Get the target section directly from the data-view attribute const targetSection = link.getAttribute('data-view'); if (targetSection) { // Ensure the attribute exists link.addEventListener('click', (event) => { event.preventDefault(); // Prevent page reload // Use the targetSection directly when calling showSection showSection(targetSection); }); } else { console.warn("Nav link found without data-view attribute:", link); } }); // Show initial section (SCADA by default) showSection('scada'); setupAddProjectForm(); // Call the setup function for the new form }); // --- Connect to SSE stream (Single connection) --- console.log("Initializing SSE connection..."); const eventSource = new EventSource("/stream"); eventSource.onmessage = function(event) { try { const data = JSON.parse(event.data); debouncedProcessUpdate(data); // Call the single debounced processor } catch (error) { console.error("Error parsing SSE data:", error); document.getElementById('status-message').textContent = 'Error processing update from server.'; } }; eventSource.onerror = function(err) { console.error("EventSource failed:", err); document.getElementById('status-message').textContent = 'Connection to server lost. Retrying...'; }; console.log("SSE handler set up."); // --- Project Selector Change Handler --- function handleProjectChange() { selectedProjectName = document.getElementById('projectSelector').value; console.log(`Project selection changed to: ${selectedProjectName}`); // Immediately update status bar for the selected project using stored data const projectData = currentProjectData[selectedProjectName]; if (projectData) { console.log(`[Project Change] Data found for ${selectedProjectName}. Status: ${projectData.status}`); updateStatusBar(selectedProjectName, projectData.status, projectData.last_commit); // Update UI based on the current state of the selected project if (isProcessing(projectData.status)) { showProcessingStateUI(projectData); } else { // Trigger a UI redraw using the stored data for the newly selected project console.log(`[Project Change] Triggering redraw for newly selected project: ${selectedProjectName}`); showReadyStateUI(projectData); // Use the new function } } else { // Handle case where data might not be available yet for the selected project const loadingStatus = 'Loading data...'; console.log(`[Project Change] No data found yet for selected project: ${selectedProjectName}. Showing loading state.`); updateStatusBar(selectedProjectName, loadingStatus, 'N/A'); // Show processing/loading indicators in all sections showProcessingStateUI(null); // Pass null to show generic loading message } } // --- Initialize Add Project Form --- function setupAddProjectForm() { const form = document.getElementById('addProjectForm'); const statusDiv = document.getElementById('addProjectStatus'); const submitButton = form.querySelector('button[type="submit"]'); if (!form) { console.log("Add Project form not found on this page."); return; // Exit if the form isn't present } form.addEventListener('submit', async (event) => { event.preventDefault(); // Prevent default HTML form submission statusDiv.style.display = 'none'; statusDiv.textContent = ''; statusDiv.className = 'mt-3 alert'; // Reset classes submitButton.disabled = true; statusDiv.classList.add('alert-info'); statusDiv.textContent = 'Uploading project data...'; statusDiv.style.display = 'block'; const formData = new FormData(form); const projectNameInput = document.getElementById('projectName'); // Basic validation for project name (allow letters, numbers, underscore, hyphen) const projectName = projectNameInput.value.trim(); if (!/^[a-zA-Z0-9_-]+$/.test(projectName)) { statusDiv.classList.remove('alert-info'); statusDiv.classList.add('alert-danger'); statusDiv.textContent = 'Invalid Project Name. Use only letters, numbers, underscores, or hyphens.'; statusDiv.style.display = 'block'; submitButton.disabled = false; return; } // Additional Client-Side Validation (Optional but recommended) const manifestFile = document.getElementById('manifestFile').files[0]; const pdfFiles = document.getElementById('pdfFiles').files; if (!manifestFile) { statusDiv.classList.remove('alert-info'); statusDiv.classList.add('alert-danger'); statusDiv.textContent = 'Manifest CSV file is required.'; statusDiv.style.display = 'block'; submitButton.disabled = false; return; } if (pdfFiles.length === 0) { statusDiv.classList.remove('alert-info'); statusDiv.classList.add('alert-danger'); statusDiv.textContent = 'At least one Drawing PDF file is required.'; statusDiv.style.display = 'block'; submitButton.disabled = false; return; } try { const response = await fetch('/add_project', { method: 'POST', body: formData // FormData handles multipart/form-data encoding }); const result = await response.json(); statusDiv.classList.remove('alert-info'); if (response.ok && result.success) { statusDiv.classList.add('alert-success'); statusDiv.textContent = result.message + ' Please restart the server for the new project to appear.'; form.reset(); // Clear the form on success // Keep button disabled after successful submission } else { statusDiv.classList.add('alert-danger'); statusDiv.textContent = 'Error: ' + (result.message || 'Unknown error occurred.'); submitButton.disabled = false; // Re-enable button on error } } catch (error) { console.error('Error submitting add project form:', error); statusDiv.classList.remove('alert-info'); statusDiv.classList.add('alert-danger'); statusDiv.textContent = 'Network error or server unavailable. Please try again.'; submitButton.disabled = false; // Re-enable button on network error } statusDiv.style.display = 'block'; // Ensure status is visible }); }