2025-04-10 04:08:55 +04:00

877 lines
43 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'
// --- 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
});
// --- Connect to SSE stream (Single connection) ---
console.log("Initializing SSE connection...");
const eventSource = new EventSource("/stream");
eventSource.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
debouncedProcessUpdate(data); // Call the single debounced processor
} catch (error) {
console.error("Error parsing SSE data:", error);
document.getElementById('status-message').textContent = 'Error processing update from server.';
}
};
eventSource.onerror = function(err) {
console.error("EventSource failed:", err);
document.getElementById('status-message').textContent = 'Connection to server lost. Retrying...';
};
console.log("SSE handler set up.");
// --- Project Selector Change Handler ---
function handleProjectChange() {
selectedProjectName = document.getElementById('projectSelector').value;
console.log(`Project selection changed to: ${selectedProjectName}`);
// Immediately update status bar for the selected project using stored data
const projectData = currentProjectData[selectedProjectName];
if (projectData) {
console.log(`[Project Change] Data found for ${selectedProjectName}. Status: ${projectData.status}`);
updateStatusBar(selectedProjectName, projectData.status, projectData.last_commit);
// Update UI based on the current state of the selected project
if (isProcessing(projectData.status)) {
showProcessingStateUI(projectData);
} else {
// Trigger a UI redraw using the stored data for the newly selected project
console.log(`[Project Change] Triggering redraw for newly selected project: ${selectedProjectName}`);
showReadyStateUI(projectData); // Use the new function
}
} else {
// Handle case where data might not be available yet for the selected project
const loadingStatus = 'Loading data...';
console.log(`[Project Change] No data found yet for selected project: ${selectedProjectName}. Showing loading state.`);
updateStatusBar(selectedProjectName, loadingStatus, 'N/A');
// Show processing/loading indicators in all sections
showProcessingStateUI(null); // Pass null to show generic loading message
}
}
// --- Initialize Add Project Form ---
function setupAddProjectForm() {
const form = document.getElementById('addProjectForm');
const statusDiv = document.getElementById('addProjectStatus');
const submitButton = form.querySelector('button[type="submit"]');
if (!form) {
console.log("Add Project form not found on this page.");
return; // Exit if the form isn't present
}
form.addEventListener('submit', async (event) => {
event.preventDefault(); // Prevent default HTML form submission
statusDiv.style.display = 'none';
statusDiv.textContent = '';
statusDiv.className = 'mt-3 alert'; // Reset classes
submitButton.disabled = true;
statusDiv.classList.add('alert-info');
statusDiv.textContent = 'Uploading project data...';
statusDiv.style.display = 'block';
const formData = new FormData(form);
const projectNameInput = document.getElementById('projectName');
// Basic validation for project name (allow letters, numbers, underscore, hyphen)
const projectName = projectNameInput.value.trim();
if (!/^[a-zA-Z0-9_-]+$/.test(projectName)) {
statusDiv.classList.remove('alert-info');
statusDiv.classList.add('alert-danger');
statusDiv.textContent = 'Invalid Project Name. Use only letters, numbers, underscores, or hyphens.';
statusDiv.style.display = 'block';
submitButton.disabled = false;
return;
}
// Additional Client-Side Validation (Optional but recommended)
const manifestFile = document.getElementById('manifestFile').files[0];
const pdfFiles = document.getElementById('pdfFiles').files;
if (!manifestFile) {
statusDiv.classList.remove('alert-info');
statusDiv.classList.add('alert-danger');
statusDiv.textContent = 'Manifest CSV file is required.';
statusDiv.style.display = 'block';
submitButton.disabled = false;
return;
}
if (pdfFiles.length === 0) {
statusDiv.classList.remove('alert-info');
statusDiv.classList.add('alert-danger');
statusDiv.textContent = 'At least one Drawing PDF file is required.';
statusDiv.style.display = 'block';
submitButton.disabled = false;
return;
}
try {
const response = await fetch('/add_project', {
method: 'POST',
body: formData // FormData handles multipart/form-data encoding
});
const result = await response.json();
statusDiv.classList.remove('alert-info');
if (response.ok && result.success) {
statusDiv.classList.add('alert-success');
statusDiv.textContent = result.message + ' Please restart the server for the new project to appear.';
form.reset(); // Clear the form on success
// Keep button disabled after successful submission
} else {
statusDiv.classList.add('alert-danger');
statusDiv.textContent = 'Error: ' + (result.message || 'Unknown error occurred.');
submitButton.disabled = false; // Re-enable button on error
}
} catch (error) {
console.error('Error submitting add project form:', error);
statusDiv.classList.remove('alert-info');
statusDiv.classList.add('alert-danger');
statusDiv.textContent = 'Network error or server unavailable. Please try again.';
submitButton.disabled = false; // Re-enable button on network error
}
statusDiv.style.display = 'block'; // Ensure status is visible
});
}