// --- 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' let eventSource = null; let selectedProject = null; // Store the currently selected project name let initialStatuses = initialServerData.status || {}; // Use embedded status initially const projectSelector = document.getElementById('projectSelector'); const manageFilesBtn = document.getElementById('manageFilesBtn'); // Get manage files button // --- 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 const incomingProjects = fullData.projects || []; // Ensure it's an array incomingProjects.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: {} } }; }); // --- NEW: Update Project Selector Dropdown --- const projectSelector = document.getElementById('projectSelector'); const currentOptions = Array.from(projectSelector.options).map(opt => opt.value); const hasChanged = incomingProjects.length !== currentOptions.length || incomingProjects.some(p => !currentOptions.includes(p)) || currentOptions.some(p => !incomingProjects.includes(p)); if (hasChanged) { console.log("Project list changed. Rebuilding project selector."); const previouslySelected = projectSelector.value; // Store current selection projectSelector.innerHTML = ''; // Clear existing options if (incomingProjects.length > 0) { incomingProjects.sort().forEach(projName => { const option = document.createElement('option'); option.value = projName; option.textContent = projName; projectSelector.appendChild(option); }); // Try to re-select the previous project if it still exists, otherwise select the first if (incomingProjects.includes(previouslySelected)) { projectSelector.value = previouslySelected; } else { projectSelector.selectedIndex = 0; // Select the first one // Manually trigger change handler if selection had to be reset handleProjectChange(); } } else { // Handle empty project list const option = document.createElement('option'); option.value = ''; option.textContent = 'No projects found'; option.disabled = true; projectSelector.appendChild(option); selectedProjectName = null; // Clear selection selectedProject = null; // Clear other selection var too clearAllProjectSpecificUI(); // Clear the main UI updateStatusBar('...', 'No projects available', 'N/A'); } // Update button state based on selection manageFilesBtn.disabled = !projectSelector.value; } // --- End Project Selector Update --- // Get current selection AFTER storing data and potentially updating selector selectedProjectName = projectSelector.value; // Update from potentially modified selector selectedProject = selectedProjectName; // Keep both synced for now console.log(`Selected project (post-update): ${selectedProjectName}`); // Enable/Disable Manage Files button based on whether a project is selected manageFilesBtn.disabled = !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 if selector isn't empty but data is missing if (projectSelector.options.length > 0 && projectSelector.value) { updateStatusBar(selectedProjectName, 'Waiting for data...', 'N/A'); showProcessingStateUI(null); // Show generic loading } else { // Handled by the selector update logic above if projects list is empty } return; } // Extract data for the selected project const projectData = currentProjectData[selectedProjectName]; // 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 // Event Listener for Project Selector Change projectSelector.addEventListener('change', (event) => { selectedProject = event.target.value; console.log("Project selected:", selectedProject); updateUIForSelectedProject(); // Enable manage files button if a project is selected, disable if no project manageFilesBtn.disabled = !selectedProject; // If no project selected (e.g., placeholder), update display accordingly if (!selectedProject) { document.querySelectorAll('.project-name-display').forEach(span => span.textContent = 'No Project Selected'); document.getElementById('status-message').textContent = 'N/A'; document.getElementById('last-commit').textContent = 'N/A'; document.getElementById('selected-project-status-name').textContent = '...'; // Optionally clear charts/tables or show a placeholder message clearAllProjectSpecificUI(); // Assuming a function to clear UI elements } }); // Event Listener for Add Project Form const addProjectForm = document.getElementById('addProjectForm'); if (addProjectForm) { setupAddProjectForm(); // Call the setup function for the new form } // --- NEW: Event Listeners for Manage Files Modal --- const manageFilesModalElement = document.getElementById('manageFilesModal'); const uploadPdfsForm = document.getElementById('uploadPdfsForm'); const triggerAnalysisBtn = document.getElementById('triggerAnalysisBtn'); if (manageFilesBtn && manageFilesModalElement && uploadPdfsForm && triggerAnalysisBtn) { // When the Manage Files modal is shown, fetch the PDF list manageFilesModalElement.addEventListener('show.bs.modal', async () => { if (!selectedProject) return; // Should not happen if button is enabled correctly // Update modal title *before* loading manageFilesModalElement.querySelectorAll('.project-name-display').forEach(span => { span.textContent = selectedProject; }); // Clear previous statuses immediately clearManageFilesStatusMessages(); // Load files await loadAndDisplayPdfs(selectedProject); }); // Handle PDF Upload Form Submission uploadPdfsForm.addEventListener('submit', handlePdfUploadSubmit); // Handle Trigger Analysis Button Click triggerAnalysisBtn.addEventListener('click', handleTriggerAnalysisClick); // --- NEW: Handle Delete Project Button Click --- const deleteProjectBtn = document.getElementById('deleteProjectBtn'); if (deleteProjectBtn) { deleteProjectBtn.addEventListener('click', handleDeleteProjectClick); } else { console.warn("Delete Project button (deleteProjectBtn) not found."); } // --- NEW: Handle Manifest Upload Form Submit --- const uploadManifestForm = document.getElementById('uploadManifestForm'); if (uploadManifestForm) { uploadManifestForm.addEventListener('submit', handleManifestUploadSubmit); } else { console.warn("Upload Manifest form (uploadManifestForm) not found."); } } else { console.warn("Manage Files Modal elements (button, modal, form, trigger btn) not all found. File management disabled."); if(manageFilesBtn) manageFilesBtn.style.display = 'none'; // Hide button if modal isn't functional } // --- Adjust Initial State Setting --- const initialProjects = initialServerData.projects || []; if (projectSelector.options.length > 0 && projectSelector.value) { selectedProject = projectSelector.value; // Get initial value from selector selectedProjectName = selectedProject; // Sync both console.log("Initial project selected:", selectedProject); if (manageFilesBtn) manageFilesBtn.disabled = !selectedProject; // Enable button if a project is initially selected handleProjectChange(); // Call the main handler to set initial state correctly connectEventSource(); // Connect to SSE } else { console.log("No projects found initially or selector empty."); if (manageFilesBtn) manageFilesBtn.disabled = true; // Ensure button is disabled document.querySelectorAll('.project-name-display').forEach(span => span.textContent = 'No Projects'); updateStatusDisplay('No projects discovered.', '...', 'N/A'); clearAllProjectSpecificUI(); } // Initialize details modal instance (if it exists) const detailsModalElement = document.getElementById('detailsModal'); if (detailsModalElement) { detailsModalInstance = new bootstrap.Modal(detailsModalElement); } // Add event listeners for navigation tabs document.querySelectorAll('#viewTabs .nav-link').forEach(tab => { tab.addEventListener('click', (event) => { event.preventDefault(); const newView = event.target.getAttribute('data-view'); switchView(newView); }); }); }); // --- Connect to SSE stream (Single connection) --- console.log("Initializing SSE connection..."); 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; selectedProject = selectedProjectName; // Keep synced console.log(`Project selection changed to: ${selectedProjectName}`); // --- NEW: Explicitly destroy existing charts immediately --- destroyAllCharts(); // --- End Explicit destroy --- // 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)) { console.log(`[Project Change] Project processing. Showing loading state.`); 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}`); // Call showReadyStateUI AFTER destroying charts showReadyStateUI(projectData); } } else { // Handle case where data might not be available yet for the selected project OR no project selected if (selectedProjectName) { // A project is selected, but data not yet available 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 } else { // No project is selected (e.g., after deletion and list is empty) console.log(`[Project Change] No project selected.`); updateStatusBar('...', 'No project selected', 'N/A'); clearAllProjectSpecificUI(); // Ensure UI is fully cleared } } // Update manage files button state manageFilesBtn.disabled = !selectedProjectName; } // --- 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 || 'Project added successfully.'; form.reset(); // Clear the form on success // Hide the modal on success const modalElement = form.closest('.modal'); if (modalElement) { const modalInstance = bootstrap.Modal.getInstance(modalElement); if (modalInstance) { modalInstance.hide(); } else { console.warn('Could not get modal instance to hide it.'); } } // Re-enable button shortly after modal starts closing (optional, could leave disabled) setTimeout(() => { submitButton.disabled = false; }, 500); } 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 }); } // --- NEW: File Management Functions --- async function loadAndDisplayPdfs(projectName) { const pdfListDiv = document.getElementById('existingPdfList'); const statusDiv = document.getElementById('manageFilesStatus'); if (!pdfListDiv || !statusDiv) return; // --- NEW: Robustness Check --- if (!projectName || !currentProjectData[projectName]) { console.warn(`loadAndDisplayPdfs called for invalid or non-existent project: ${projectName}. Aborting.`); pdfListDiv.innerHTML = '
Error: Invalid project selected.
'; return; // Don't proceed if the project isn't valid in our current state } // --- End Robustness Check --- pdfListDiv.innerHTML = '
Loading files...
'; // Show loading state within list-group statusDiv.style.display = 'none'; // Hide general status try { // Use the validated projectName const response = await fetch(`/list_pdfs/${encodeURIComponent(projectName)}`); const data = await response.json(); pdfListDiv.innerHTML = ''; // Clear loading state if (!response.ok || !data.success) { throw new Error(data.message || `Failed to list PDF files (HTTP ${response.status})`); } if (data.files && data.files.length > 0) { data.files.forEach(filename => { const listItem = document.createElement('div'); listItem.className = 'list-group-item d-flex justify-content-between align-items-center'; const nameSpan = document.createElement('span'); nameSpan.textContent = filename; nameSpan.title = filename; // Show full name on hover if needed nameSpan.style.overflow = 'hidden'; nameSpan.style.textOverflow = 'ellipsis'; nameSpan.style.whiteSpace = 'nowrap'; nameSpan.style.marginRight = '10px'; const deleteButton = document.createElement('button'); deleteButton.className = 'btn btn-danger btn-sm flex-shrink-0'; // Prevent button shrinking deleteButton.textContent = 'Delete'; deleteButton.title = `Delete ${filename}`; // Pass the validated projectName to the delete handler deleteButton.onclick = () => handleDeletePdfClick(projectName, filename); listItem.appendChild(nameSpan); listItem.appendChild(deleteButton); pdfListDiv.appendChild(listItem); }); } else { pdfListDiv.innerHTML = '
No PDF files found for this project.
'; } } catch (error) { console.error('Error loading PDF list:', error); pdfListDiv.innerHTML = '
Error loading files.
'; showManageFilesStatus(`Error loading PDF list: ${error.message}`, 'danger'); } } async function handleDeletePdfClick(projectName, filename) { if (!confirm(`Are you sure you want to delete the file: ${filename}? This cannot be undone.`)) { return; } console.log(`Requesting deletion of ${filename} from project ${projectName}`); clearManageFilesStatusMessages(); // Clear previous messages showManageFilesStatus(`Deleting ${filename}...`, 'info'); // Show deleting status try { const response = await fetch(`/delete_pdf/${encodeURIComponent(projectName)}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ filename: filename }) }); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.message || `Failed to delete PDF file (HTTP ${response.status})`); } showManageFilesStatus(data.message || `Successfully deleted ${filename}.`, 'success'); // Refresh the list after deletion await loadAndDisplayPdfs(projectName); // Recommend triggering analysis showAnalysisTriggerStatus('File deleted. Trigger analysis to update progress.', 'info'); } catch (error) { console.error('Error deleting PDF:', error); showManageFilesStatus(`Error deleting file: ${error.message}`, 'danger'); } } async function handlePdfUploadSubmit(event) { event.preventDefault(); if (!selectedProject) return; const form = event.target; const formData = new FormData(form); const fileInput = document.getElementById('newPdfFiles'); const uploadStatusDiv = document.getElementById('uploadStatus'); if (!fileInput || !fileInput.files || fileInput.files.length === 0) { showUploadStatus('Please select at least one PDF file to upload.', 'warning'); return; } console.log(`Uploading ${fileInput.files.length} file(s) to project ${selectedProject}`); clearManageFilesStatusMessages(); showUploadStatus('Uploading files...', 'info', false); // Show persistent uploading message try { const response = await fetch(`/upload_pdfs/${encodeURIComponent(selectedProject)}`, { method: 'POST', body: formData // FormData handles multipart/form-data automatically }); // Try to parse JSON regardless of status for potential error messages let data = {}; try { data = await response.json(); } catch(e) { console.warn("Could not parse JSON response from upload endpoint."); // If JSON parsing fails on error, create a basic error object if (!response.ok) { data = { success: false, message: `Upload failed with status ${response.status}. No error details available.` }; } } if (!response.ok || !data.success) { throw new Error(data.message || `File upload failed (HTTP ${response.status})`); } showUploadStatus(data.message || `Successfully uploaded files.`, 'success'); form.reset(); // Clear the file input await loadAndDisplayPdfs(selectedProject); // Refresh the list // Recommend triggering analysis showAnalysisTriggerStatus('Files uploaded. Trigger analysis to update progress.', 'info'); } catch (error) { console.error('Error uploading PDFs:', error); showUploadStatus(`Upload failed: ${error.message}`, 'danger'); } finally { // Ensure the 'Uploading files...' message is cleared if it wasn't replaced by success/error if (uploadStatusDiv && uploadStatusDiv.textContent === 'Uploading files...') { uploadStatusDiv.style.display = 'none'; } } } async function handleTriggerAnalysisClick() { if (!selectedProject) return; console.log(`Requesting manual analysis trigger for project ${selectedProject}`); clearManageFilesStatusMessages(); showAnalysisTriggerStatus('Triggering analysis...', 'info', false); try { const response = await fetch(`/trigger_analysis/${encodeURIComponent(selectedProject)}`, { method: 'POST' }); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.message || `Failed to trigger analysis (HTTP ${response.status})`); } showAnalysisTriggerStatus(data.message || 'Analysis triggered successfully. Monitor status bar for updates.', 'success', false); // Keep success message visible } catch (error) { console.error('Error triggering analysis:', error); showAnalysisTriggerStatus(`Error: ${error.message}`, 'danger'); } } function showManageFilesStatus(message, type = 'info') { const statusDiv = document.getElementById('manageFilesStatus'); if (!statusDiv) return; statusDiv.className = `mt-3 alert alert-${type}`; statusDiv.textContent = message; statusDiv.style.display = 'block'; } function showUploadStatus(message, type = 'info', autoClear = true) { const statusDiv = document.getElementById('uploadStatus'); if (!statusDiv) return; statusDiv.className = `mt-2 text-${type}`; if (type === 'danger' || type === 'warning' || type === 'success') { statusDiv.className += ' fw-bold'; } statusDiv.textContent = message; statusDiv.style.display = 'block'; // Clear previous timeouts if any if (statusDiv.timeoutId) clearTimeout(statusDiv.timeoutId); if (autoClear) { statusDiv.timeoutId = setTimeout(() => { statusDiv.style.display = 'none'; statusDiv.timeoutId = null; }, 5000); // Hide after 5 seconds } } function showAnalysisTriggerStatus(message, type = 'info', autoClear = true) { const statusDiv = document.getElementById('analysisTriggerStatus'); if (!statusDiv) return; statusDiv.className = `mt-2 text-${type}`; if (type === 'danger' || type === 'warning' || type === 'success') { statusDiv.className += ' fw-bold'; } statusDiv.textContent = message; statusDiv.style.display = 'block'; // Clear previous timeouts if any if (statusDiv.timeoutId) clearTimeout(statusDiv.timeoutId); if (autoClear) { statusDiv.timeoutId = setTimeout(() => { statusDiv.style.display = 'none'; statusDiv.timeoutId = null; }, 8000); // Hide after 8 seconds (longer for analysis trigger) } } function clearManageFilesStatusMessages() { const manageStatus = document.getElementById('manageFilesStatus'); const uploadStatus = document.getElementById('uploadStatus'); const analysisStatus = document.getElementById('analysisTriggerStatus'); const deleteStatus = document.getElementById('deleteProjectStatus'); const manifestStatus = document.getElementById('uploadManifestStatus'); // Added if(manageStatus) manageStatus.style.display = 'none'; if(uploadStatus) { uploadStatus.style.display = 'none'; if (uploadStatus.timeoutId) clearTimeout(uploadStatus.timeoutId); uploadStatus.timeoutId = null; } if(analysisStatus) { analysisStatus.style.display = 'none'; if (analysisStatus.timeoutId) clearTimeout(analysisStatus.timeoutId); analysisStatus.timeoutId = null; } if(deleteStatus) { deleteStatus.style.display = 'none'; if (deleteStatus.timeoutId) clearTimeout(deleteStatus.timeoutId); deleteStatus.timeoutId = null; } // Clear timeout for delete too if(manifestStatus) { manifestStatus.style.display = 'none'; if (manifestStatus.timeoutId) clearTimeout(manifestStatus.timeoutId); manifestStatus.timeoutId = null; } // Added } function clearAllProjectSpecificUI() { console.log("[UI Clear] Clearing all project-specific UI elements."); // Clear Overall Charts & Text destroyAllCharts(); // Clear canvas elements and text placeholders const overallScadaCanvas = document.getElementById('overall-scada-chart-canvas'); const overallDrawingCanvas = document.getElementById('overall-drawing-chart-canvas'); if (overallScadaCanvas) { const ctx = overallScadaCanvas.getContext('2d'); ctx.clearRect(0, 0, overallScadaCanvas.width, overallScadaCanvas.height); // Clear canvas visually } if (overallDrawingCanvas) { const ctx = overallDrawingCanvas.getContext('2d'); ctx.clearRect(0, 0, overallDrawingCanvas.width, overallDrawingCanvas.height); // Clear canvas visually } const overallScadaText = document.getElementById('overall-scada-text'); if (overallScadaText) overallScadaText.textContent = 'Found in SCADA: N/A'; const overallDrawingText = document.getElementById('overall-drawing-text'); if (overallDrawingText) overallDrawingText.textContent = 'Found in Drawing: N/A'; // Clear Panel Sections const scadaPanelsContainer = document.getElementById('scada-panels-progress'); const drawingPanelsContainer = document.getElementById('drawing-panels-progress'); const conflictsContainer = document.getElementById('panels-conflicts'); const defaultPanelMsg = '

Select a project to view data.

'; if(scadaPanelsContainer) scadaPanelsContainer.innerHTML = defaultPanelMsg; if(drawingPanelsContainer) drawingPanelsContainer.innerHTML = defaultPanelMsg; if(conflictsContainer) conflictsContainer.innerHTML = defaultPanelMsg; const conflictCount = document.getElementById('conflict-count'); if (conflictCount) { conflictCount.textContent = '0'; conflictCount.style.display = 'none'; // Hide badge when cleared } } function destroyAllCharts() { console.log("[Chart Destroy] Destroying all SCADA and Drawing chart instances."); // Target the correct instance objects Object.values(chartInstancesScada).forEach(chart => chart?.destroy()); chartInstancesScada = {}; // Reset the object Object.values(chartInstancesDrawing).forEach(chart => chart?.destroy()); chartInstancesDrawing = {}; // Reset the object } // Add a helper to update UI for selected project function updateUIForSelectedProject() { // This function seems redundant now. handleProjectChange covers the logic. // console.log("Updating UI for selected project:", selectedProject); // We can likely remove calls to this function as well. const data = currentProjectData[selectedProject]; if (data) { if (isProcessing(data.status)) { showProcessingStateUI(data); } else { showReadyStateUI(data); } } else if (selectedProject) { showProcessingStateUI(null); // Show loading if project selected but no data } else { clearAllProjectSpecificUI(); // Clear if no project selected } } // Add a helper to switch view function switchView(newView) { console.log("Switching to view:", newView); showSection(newView); } // Add a helper to update status display function updateStatusDisplay(statusMessage, projectName, commitHash) { const statusMsgSpan = document.getElementById('status-message'); const statusProjSpan = document.getElementById('selected-project-status-name'); const commitSpan = document.getElementById('last-commit'); if (statusMsgSpan) statusMsgSpan.textContent = statusMessage || 'N/A'; if (statusProjSpan) statusProjSpan.textContent = projectName || '...'; if (commitSpan) commitSpan.textContent = commitHash ? commitHash.substring(0, 7) : 'N/A'; // Show abbreviated hash } // --- NEW: Delete Project Function --- async function handleDeleteProjectClick() { if (!selectedProject) { showDeleteProjectStatus('No project selected to delete.', 'warning'); return; } // VERY IMPORTANT: Double confirmation with project name const confirmation = prompt( `This action is IRREVERSIBLE and will permanently delete the project '${selectedProject}' and all its data (manifest, PDFs, text files, cloned repository) from the server.\n\nType the project name '${selectedProject}' below to confirm deletion:` ); if (confirmation !== selectedProject) { showDeleteProjectStatus('Project deletion cancelled or confirmation mismatch.', 'info'); return; } console.log(`Requesting deletion of project ${selectedProject}`); clearManageFilesStatusMessages(); // Clear other messages showDeleteProjectStatus(`Deleting project '${selectedProject}'...`, 'info', false); // Show persistent status // Optionally disable buttons while deleting document.getElementById('deleteProjectBtn').disabled = true; try { const response = await fetch(`/delete_project/${encodeURIComponent(selectedProject)}`, { method: 'POST' // Backend route expects POST }); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.message || `Failed to delete project (HTTP ${response.status})`); } showDeleteProjectStatus(`Project '${selectedProject}' deleted successfully.`, 'success', false); // Give user time to read the success message before closing modal setTimeout(() => { // Close the modal const manageModalElem = document.getElementById('manageFilesModal'); const manageModalInstance = bootstrap.Modal.getInstance(manageModalElem); if (manageModalInstance) { manageModalInstance.hide(); } // Remove the project from the selector const selector = document.getElementById('projectSelector'); let newSelectedIndex = -1; for (let i = 0; i < selector.options.length; i++) { if (selector.options[i].value === selectedProject) { // Determine next index to select (previous or first) newSelectedIndex = (i > 0) ? i - 1 : 0; selector.remove(i); break; } } // Select the next/previous/first remaining project or clear UI if (selector.options.length > 0) { selector.selectedIndex = newSelectedIndex >= 0 ? newSelectedIndex : 0; // Manually trigger the change handler for the new selection handleProjectChange(); } else { // No projects left selector.innerHTML = ''; selectedProject = null; selectedProjectName = null; // Clear global state var too document.querySelectorAll('.project-name-display').forEach(span => span.textContent = 'No Projects'); updateStatusDisplay('No projects available.', '...', 'N/A'); clearAllProjectSpecificUI(); // Clear charts/tables manageFilesBtn.disabled = true; // Disable manage files button } }, 1500); // Delay closing modal } catch (error) { console.error('Error deleting project:', error); showDeleteProjectStatus(`Error: ${error.message}`, 'danger', false); // Re-enable button on error document.getElementById('deleteProjectBtn').disabled = false; } } function showDeleteProjectStatus(message, type = 'info', autoClear = true) { const statusDiv = document.getElementById('deleteProjectStatus'); if (!statusDiv) return; statusDiv.className = `mt-2 text-${type}`; if (type === 'danger' || type === 'warning' || type === 'success') { statusDiv.className += ' fw-bold'; } statusDiv.textContent = message; statusDiv.style.display = 'block'; if (autoClear) { // Clear previous timeouts if any if (statusDiv.timeoutId) clearTimeout(statusDiv.timeoutId); statusDiv.timeoutId = setTimeout(() => { statusDiv.style.display = 'none'; statusDiv.timeoutId = null; }, 6000); } } // --- NEW: Handle Manifest Upload Form Submission --- async function handleManifestUploadSubmit(event) { event.preventDefault(); if (!selectedProject) return; const form = event.target; const formData = new FormData(form); const fileInput = document.getElementById('newManifestFile'); const statusDiv = document.getElementById('uploadManifestStatus'); if (!fileInput || !fileInput.files || fileInput.files.length === 0) { showUploadManifestStatus('Please select a manifest CSV file to upload.', 'warning'); return; } const manifestFile = fileInput.files[0]; if (!manifestFile.name.toLowerCase().endsWith('.csv')) { showUploadManifestStatus('Invalid file type. Please select a .csv file.', 'warning'); return; } console.log(`Uploading new manifest for project ${selectedProject}`); clearManageFilesStatusMessages(); showUploadManifestStatus('Uploading manifest...', 'info', false); try { const response = await fetch(`/upload_manifest/${encodeURIComponent(selectedProject)}`, { method: 'POST', // Backend route expects POST body: formData }); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.message || `Failed to upload manifest (HTTP ${response.status})`); } showUploadManifestStatus(data.message || 'Manifest updated successfully.', 'success'); form.reset(); // Clear the file input // Recommend triggering analysis showAnalysisTriggerStatus('Manifest updated. Trigger analysis to see changes.', 'info'); } catch (error) { console.error('Error uploading manifest:', error); showUploadManifestStatus(`Upload failed: ${error.message}`, 'danger'); } } function showUploadManifestStatus(message, type = 'info', autoClear = true) { const statusDiv = document.getElementById('uploadManifestStatus'); if (!statusDiv) return; statusDiv.className = `mt-2 text-${type}`; if (type === 'danger' || type === 'warning' || type === 'success') { statusDiv.className += ' fw-bold'; } statusDiv.textContent = message; statusDiv.style.display = 'block'; if (autoClear) { if (statusDiv.timeoutId) clearTimeout(statusDiv.timeoutId); statusDiv.timeoutId = setTimeout(() => { statusDiv.style.display = 'none'; statusDiv.timeoutId = null; }, 5000); } }