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