1540 lines
72 KiB
JavaScript

// --- 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 allProjectData = {}; // Holds combined status, progress, commit for all projects
let activeCharts = {}; // Store chart instances to prevent duplicates
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 = `
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 fst-italic"></p>
`;
// 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, "&lt;").replace(/>/g, "&gt;");
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 = '<p class="text-center fst-italic">No panel data available yet.</p>';
} 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 = `<tr><th>Alias</th><th>Panel</th><th>SCADA Status</th><th>Drawing Status</th><th>Equipment Type</th><th>Type of Conveyor</th></tr>`;
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 = '<span class="status-yes">Yes</span>';
row.insertCell().innerHTML = '<span class="status-no">No</span>';
row.insertCell().textContent = item.equipment_type || 'N/A';
row.insertCell().textContent = item.conveyor_type || 'N/A';
});
panelsContainer.appendChild(table);
}
});
if (panelsWithConflicts === 0) {
panelsContainer.innerHTML = '<p class="text-center fst-italic">No conflicts found across all panels.</p>';
}
}
// 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 = '<p class="text-center fst-italic">No panel data available yet.</p>';
}
}
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} <span class="badge bg-secondary ms-2">${combinedDataList.length}</span>`;
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 ? '<span class="status-yes">Yes</span>' : '<span class="status-no">No</span>';
const drawingCell = row.insertCell(); drawingCell.innerHTML = item.found_drawing ? '<span class="status-yes">Yes</span>' : '<span class="status-no">No</span>';
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
console.log("Initial project selected:", selectedProject);
if (manageFilesBtn) manageFilesBtn.disabled = !selectedProject; // Enable button if a project is initially selected
updateUIForSelectedProject(); // Update based on initially selected project
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;
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 || '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;
pdfListDiv.innerHTML = '<div class="list-group-item text-muted">Loading files...</div>'; // Show loading state within list-group
statusDiv.style.display = 'none'; // Hide general status
try {
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}`;
deleteButton.onclick = () => handleDeletePdfClick(projectName, filename);
listItem.appendChild(nameSpan);
listItem.appendChild(deleteButton);
pdfListDiv.appendChild(listItem);
});
} else {
pdfListDiv.innerHTML = '<div class="list-group-item text-muted">No PDF files found for this project.</div>';
}
} catch (error) {
console.error('Error loading PDF list:', error);
pdfListDiv.innerHTML = '<div class="list-group-item text-danger">Error loading files.</div>';
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() {
// Clear Overall Charts & Text
updateOverallChart('overall-scada', 0, 0, 0); // Assumes updateOverallChart exists and handles zero data
updateOverallChart('overall-drawing', 0, 0, 0); // Assumes updateOverallChart exists and handles zero data
const overallScadaText = document.getElementById('overall-scada-text');
if (overallScadaText) overallScadaText.textContent = 'Found in SCADA: 0/0 (0%)';
const overallDrawingText = document.getElementById('overall-drawing-text');
if (overallDrawingText) overallDrawingText.textContent = 'Found in Drawing: 0/0 (0%)';
// Clear Panel Sections
document.getElementById('scada-panels-progress').innerHTML = '<p>Select a project to view data.</p>';
document.getElementById('drawing-panels-progress').innerHTML = '<p>Select a project to view data.</p>';
document.getElementById('panels-conflicts').innerHTML = '<p>Select a project to view data.</p>';
const conflictCount = document.getElementById('conflict-count');
if (conflictCount) conflictCount.textContent = '0';
// Clear charts in case they were drawn
destroyAllCharts();
}
function destroyAllCharts() {
Object.keys(activeCharts).forEach(key => {
if (activeCharts[key]) {
activeCharts[key].destroy();
delete activeCharts[key];
}
});
// console.log("Destroyed all active charts."); // Optional log
}
// Add a helper to update overall chart
function updateOverallChart(chartId, foundScada, foundDrawing, total) {
const chartCanvas = document.getElementById(chartId);
if (chartCanvas) {
const ctx = chartCanvas.getContext('2d');
const chart = new Chart(ctx, {
type: 'bar',
data: {
labels: ['Found in SCADA', 'Found in Drawing', 'Total'],
datasets: [{
label: 'Match Count',
data: [foundScada, foundDrawing, total],
backgroundColor: ['rgba(13, 110, 253, 0.5)', 'rgba(25, 135, 84, 0.5)', 'rgba(75, 192, 192, 0.5)'],
borderColor: ['rgb(13, 110, 253)', 'rgb(25, 135, 84)', 'rgb(75, 192, 192)'],
borderWidth: 1
}]
},
options: {
scales: {
y: {
beginAtZero: true
}
}
}
});
}
}
// Add a helper to create panel card
function createPanelCard(panelName, foundScada, foundDrawing, total) {
const card = document.createElement('div');
card.className = 'panel-card';
card.innerHTML = `
<h5>${panelName}</h5>
<p>Found in SCADA: ${foundScada}</p>
<p>Found in Drawing: ${foundDrawing}</p>
<p>Total: ${total}</p>
`;
return card;
}
// Add a helper to populate details modal
function populateDetailsModal(projectName, identifier, categoryType, context) {
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} <span class="badge bg-secondary ms-2">${combinedDataList.length}</span>`;
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 ? '<span class="status-yes">Yes</span>' : '<span class="status-no">No</span>';
const drawingCell = row.insertCell(); drawingCell.innerHTML = item.found_drawing ? '<span class="status-yes">Yes</span>' : '<span class="status-no">No</span>';
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();
}
// Add a helper to update UI for selected project
function updateUIForSelectedProject() {
// Implement the logic to update the UI for the selected project
console.log("Updating UI for selected project:", selectedProject);
}
// 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 = '<option value="" disabled>No projects found</option>';
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);
}
}