1540 lines
72 KiB
JavaScript
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, "<").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
|
|
|
|
// 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);
|
|
}
|
|
}
|