425 lines
19 KiB
JavaScript

// --- Event Listener Setup and Handlers ---
// Note: Relies on MANY globally accessible functions and variables:
// - State: setSelectedProject, selectedProject, selectedProjectName, currentProjectData
// - State helpers: isProcessing
// - UI: uiUpdateStatusBar, uiShowProcessingState, uiClearProcessingState, uiClearProjectDisplay,
// uiUpdateProjectNameDisplay, uiShowSection, uiShowStatusMessage (via specific wrappers like uiShowUploadStatus),
// uiClearManageFilesStatusMessages
// - API: apiAddProject, apiUploadPdfs, apiTriggerAnalysis, apiDeletePdf, apiDeleteProject, apiUploadManifest
// - Charting: chartManagerDestroyAll
// - Modals: modalManagerHide, modalManagerUpdateManageFilesTitle
// - Other Core Logic: handleLoadAndDisplayPdfs, updateUIScadaCore, updateUIDrawingCore, updateUIConflictsCore
// --- Core Handler Functions ---
/**
* Handles the change event for the project selector dropdown.
* Updates the global state and triggers a full UI refresh for the selected project.
*/
function handleProjectChange() {
// selectedProjectName should have been updated by the calling event listener
console.log(`[Events] Project selection changed to: ${selectedProjectName}`);
chartManagerDestroyAll(); // Clear charts immediately
const projectData = currentProjectData[selectedProjectName]; // Access global state
if (projectData) {
console.log(`[Events] Data found for ${selectedProjectName}. Status: ${projectData.status}`);
uiUpdateStatusBar(selectedProjectName, projectData.status, projectData.last_commit);
if (isProcessing(projectData.status)) {
console.log(`[Events] Project processing. Showing loading state.`);
uiShowProcessingState(projectData.status);
} else {
console.log(`[Events] Triggering redraw for newly selected project: ${selectedProjectName}`);
uiClearProcessingState(); // Clear any potential global loaders
// Draw immediately instead of using setTimeout
const latestData = currentProjectData[selectedProjectName];
if (latestData && !isProcessing(latestData.status)) {
updateUIScadaCore(latestData);
updateUIDrawingCore(latestData);
updateUIConflictsCore(latestData);
} else {
// If status is *still* processing (e.g., changed between checks), show loading
console.warn("[Events:ProjectChange] State indicates processing on immediate draw attempt.");
uiShowProcessingState(latestData?.status || "Processing...");
}
}
} else {
if (selectedProjectName) {
console.log(`[Events] No data found yet for selected project: ${selectedProjectName}. Showing loading.`);
uiUpdateStatusBar(selectedProjectName, 'Loading data...', 'N/A');
uiShowProcessingState('Loading data...');
} else {
console.log(`[Events] No project selected.`);
uiUpdateStatusBar('...', 'No project selected', 'N/A');
uiUpdateProjectNameDisplay('No Projects');
uiClearProjectDisplay();
}
}
// Manage Files button state is handled by the selector's listener directly
}
// --- Add Project Form Handler ---
async function handleAddProjectSubmit(event) {
event.preventDefault();
const form = event.target;
const statusDiv = document.getElementById('addProjectStatus');
const submitButton = form.querySelector('button[type="submit"]');
const modalElement = form.closest('.modal');
// UI update: Show submitting status
statusDiv.style.display = 'none'; // Use direct style manipulation or a UI function
statusDiv.textContent = '';
statusDiv.className = 'mt-3 alert alert-info';
statusDiv.textContent = 'Submitting project...';
statusDiv.style.display = 'block';
submitButton.disabled = true;
// Validation
const projectNameInput = document.getElementById('projectName');
const projectName = projectNameInput.value.trim();
const manifestFile = document.getElementById('manifestFile').files[0];
const pdfFiles = document.getElementById('pdfFiles').files;
let validationError = null;
if (!/^[a-zA-Z0-9_-]+$/.test(projectName)) validationError = 'Invalid Project Name. Use only letters, numbers, underscores, or hyphens.';
else if (!manifestFile) validationError = 'Manifest CSV file is required.';
else if (pdfFiles.length === 0) validationError = 'At least one Drawing PDF file is required.';
if (validationError) {
statusDiv.className = 'mt-3 alert alert-danger';
statusDiv.textContent = validationError;
submitButton.disabled = false;
return;
}
// Call API
try {
const formData = new FormData(form);
const result = await apiAddProject(formData); // Use API module
statusDiv.className = 'mt-3 alert alert-success';
statusDiv.textContent = result.message || 'Project added successfully.';
form.reset();
modalManagerHide(modalElement); // Use Modal module
setTimeout(() => { submitButton.disabled = false; }, 500);
} catch (error) {
statusDiv.className = 'mt-3 alert alert-danger';
statusDiv.textContent = `Error: ${error.message || 'Could not add project.'}`;
submitButton.disabled = false;
}
}
// --- Manage Files Modal Action Handlers ---
async function handlePdfDeleteAction(projectName, filename) {
if (!confirm(`Are you sure you want to delete the file: ${filename}? This cannot be undone.`)) {
return;
}
console.log(`[Events] Requesting deletion of ${filename} from project ${projectName}`);
uiClearManageFilesStatusMessages();
uiShowManageFilesStatus(`Deleting ${filename}...`, 'info');
try {
const data = await apiDeletePdf(projectName, filename); // Use API module
uiShowManageFilesStatus(data.message || `Successfully deleted ${filename}.`, 'success');
await handleLoadAndDisplayPdfs(projectName); // Refresh list (function in script.js)
uiShowAnalysisTriggerStatus('File deleted. Trigger analysis to update progress.', 'info');
} catch (error) {
console.error('[Events] Error deleting PDF:', error);
uiShowManageFilesStatus(`Error deleting file: ${error.message}`, 'danger');
}
}
async function handlePdfUploadAction(form) {
// selectedProject accessed globally
if (!selectedProject) return;
const formData = new FormData(form);
const fileInput = document.getElementById('newPdfFiles');
if (!fileInput || !fileInput.files || fileInput.files.length === 0) {
uiShowUploadStatus('Please select at least one PDF file to upload.', 'warning');
return;
}
console.log(`[Events] Uploading ${fileInput.files.length} file(s) to project ${selectedProject}`);
uiClearManageFilesStatusMessages();
uiShowUploadStatus('Uploading files...', 'info', false);
try {
const data = await apiUploadPdfs(selectedProject, formData); // Use API module
uiShowUploadStatus(data.message || `Successfully uploaded files.`, 'success');
form.reset();
await handleLoadAndDisplayPdfs(selectedProject); // Refresh list (function in script.js)
uiShowAnalysisTriggerStatus('Files uploaded. Trigger analysis to update progress.', 'info');
} catch (error) {
console.error('[Events] Error uploading PDFs:', error);
uiShowUploadStatus(`Upload failed: ${error.message}`, 'danger');
}
// Can't easily clear the 'uploading' message here if error occurs before final state
// Consider removing the direct DOM check from the original function
}
async function handleTriggerAnalysisAction() {
// selectedProject accessed globally
if (!selectedProject) return;
console.log(`[Events] Requesting manual analysis trigger for project ${selectedProject}`);
uiClearManageFilesStatusMessages();
// --- Set global flag to true ---
isAnalysisGloballyActive = true;
console.log(`[State] isAnalysisGloballyActive set to: ${isAnalysisGloballyActive}`);
// Show status in modal immediately
uiShowAnalysisTriggerStatus('Triggering analysis...', 'info', false);
// Also update main UI immediately to show processing state
const statusMsg = "Analysis triggered...";
const currentCommit = currentProjectData[selectedProject]?.last_commit || 'N/A'; // Get current commit if available
uiUpdateStatusBar(selectedProject, statusMsg, currentCommit);
uiShowProcessingState(statusMsg);
try {
const data = await apiTriggerAnalysis(selectedProject); // Use API module
// Update modal status on success, main UI is already showing processing
uiShowAnalysisTriggerStatus(data.message || 'Analysis triggered successfully. Monitor status bar for updates.', 'success', false);
} catch (error) {
console.error('[Events] Error triggering analysis:', error);
uiShowAnalysisTriggerStatus(`Error: ${error.message}`, 'danger');
// If trigger fails, reset global flag and clear processing state? Maybe.
isAnalysisGloballyActive = false; // Reset flag on error
uiClearProcessingState();
// Restore status bar to previous state? Or show error?
uiUpdateStatusBar(selectedProject, `Error triggering: ${error.message}`, currentCommit);
}
}
async function handleDeleteProjectAction() {
// selectedProject accessed globally
if (!selectedProject) {
uiShowDeleteProjectStatus('No project selected to delete.', 'warning');
return;
}
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) {
uiShowDeleteProjectStatus('Project deletion cancelled or confirmation mismatch.', 'info');
return;
}
console.log(`[Events] Requesting deletion of project ${selectedProject}`);
uiClearManageFilesStatusMessages();
uiShowDeleteProjectStatus(`Deleting project '${selectedProject}'...`, 'info', false);
const deleteBtn = document.getElementById('deleteProjectBtn'); // Direct access ok for disabling?
if (deleteBtn) deleteBtn.disabled = true;
try {
const data = await apiDeleteProject(selectedProject); // Use API module
uiShowDeleteProjectStatus(`Project '${selectedProject}' deleted successfully.`, 'success', false);
// Remove setTimeout for immediate UI update
// setTimeout(() => {
const manageModalElem = document.getElementById('manageFilesModal');
modalManagerHide(manageModalElem); // Use Modal module
// Update project selector UI
const selector = document.getElementById('projectSelector');
let newSelectedIndex = -1;
for (let i = 0; i < selector.options.length; i++) {
if (selector.options[i].value === selectedProject) {
newSelectedIndex = (i > 0) ? i - 1 : 0;
selector.remove(i);
break;
}
}
// Update state and trigger UI refresh
if (selector.options.length > 0) {
selector.selectedIndex = newSelectedIndex >= 0 ? newSelectedIndex : 0;
setSelectedProject(selector.value);
handleProjectChange(); // Trigger full refresh
} else {
selector.innerHTML = '<option value="" disabled selected>No projects found</option>';
setSelectedProject(null);
uiUpdateProjectNameDisplay('No Projects');
uiUpdateStatusBar('...', 'No projects available', 'N/A');
uiClearProjectDisplay();
const manageFilesBtn = document.getElementById('manageFilesBtn');
if(manageFilesBtn) manageFilesBtn.disabled = true;
}
if (deleteBtn) deleteBtn.disabled = false; // Re-enable after operation
// }, 1500);
} catch (error) {
console.error('[Events] Error deleting project:', error);
uiShowDeleteProjectStatus(`Error: ${error.message}`, 'danger', false);
if (deleteBtn) deleteBtn.disabled = false; // Re-enable on error
}
}
async function handleManifestUploadAction(form) {
// selectedProject accessed globally
if (!selectedProject) return;
const formData = new FormData(form);
const fileInput = document.getElementById('newManifestFile');
// Validation
if (!fileInput || !fileInput.files || fileInput.files.length === 0) {
uiShowUploadManifestStatus('Please select a manifest CSV file to upload.', 'warning');
return;
}
const manifestFile = fileInput.files[0];
if (!manifestFile.name.toLowerCase().endsWith('.csv')) {
uiShowUploadManifestStatus('Invalid file type. Please select a .csv file.', 'warning');
return;
}
console.log(`[Events] Uploading new manifest for project ${selectedProject}`);
uiClearManageFilesStatusMessages();
uiShowUploadManifestStatus('Uploading manifest...', 'info', false);
try {
const data = await apiUploadManifest(selectedProject, formData); // Use API module
uiShowUploadManifestStatus(data.message || 'Manifest updated successfully.', 'success');
form.reset();
uiShowAnalysisTriggerStatus('Manifest updated. Trigger analysis to see changes.', 'info');
} catch (error) {
console.error('[Events] Error uploading manifest:', error);
uiShowUploadManifestStatus(`Upload failed: ${error.message}`, 'danger');
}
}
// --- Initialization Function ---
/**
* Initializes the application state and attaches all necessary event listeners.
*/
function initializeEventListeners() {
console.log("[Events] Initializing event listeners and initial UI state...");
// --- Get DOM element references ---
const projectSelector = document.getElementById('projectSelector');
const manageFilesBtn = document.getElementById('manageFilesBtn');
const addProjectForm = document.getElementById('addProjectForm');
const manageFilesModalElement = document.getElementById('manageFilesModal');
const uploadPdfsForm = document.getElementById('uploadPdfsForm');
const triggerAnalysisBtn = document.getElementById('triggerAnalysisBtn');
const deleteProjectBtn = document.getElementById('deleteProjectBtn');
const uploadManifestForm = document.getElementById('uploadManifestForm');
const existingPdfListDiv = document.getElementById('existingPdfList');
// --- Setup Initial State & UI ---
// initialServerData is expected to be globally available from the HTML template
initialStatuses = initialServerData.status || {}; // Store initial statuses globally
if (projectSelector && projectSelector.options.length > 0 && projectSelector.value) {
setSelectedProject(projectSelector.value);
console.log("[Events] Initial project selected:", selectedProject);
if (manageFilesBtn) manageFilesBtn.disabled = !selectedProject;
// Set initial status bar text using potentially embedded initial status
const initialStatus = initialStatuses[selectedProject] || 'Initializing...';
uiUpdateStatusBar(selectedProject, initialStatus, 'N/A');
// Perform initial UI setup (will show loading if status is processing)
handleProjectChange();
} else {
console.log("[Events] No projects found initially or selector empty.");
setSelectedProject(null);
if (manageFilesBtn) manageFilesBtn.disabled = true;
uiUpdateProjectNameDisplay('No Projects');
uiUpdateStatusBar('...', 'No projects discovered.', 'N/A');
uiClearProjectDisplay();
}
// Show initial section (SCADA by default) after potentially setting initial state
uiShowSection('scada');
// --- Attach Event Listeners ---
// Project Selector
if (projectSelector) {
projectSelector.addEventListener('change', (event) => {
setSelectedProject(event.target.value);
handleProjectChange(); // Trigger full update
if (manageFilesBtn) manageFilesBtn.disabled = !selectedProject;
});
}
// Navigation Tabs
document.querySelectorAll('.nav-link[data-view]').forEach(link => {
link.addEventListener('click', (event) => {
event.preventDefault();
const targetSection = link.getAttribute('data-view');
uiShowSection(targetSection);
});
});
// Add Project Form
if (addProjectForm) {
addProjectForm.addEventListener('submit', handleAddProjectSubmit);
}
// Manage Files Modal: Show
if (manageFilesModalElement && manageFilesBtn) {
manageFilesModalElement.addEventListener('show.bs.modal', async () => {
// selectedProject accessed globally
if (!selectedProject) return;
modalManagerUpdateManageFilesTitle(selectedProject);
uiClearManageFilesStatusMessages();
await handleLoadAndDisplayPdfs(selectedProject); // Calls function in script.js
});
}
// Manage Files Modal: PDF Upload Form
if (uploadPdfsForm) {
uploadPdfsForm.addEventListener('submit', (event) => {
event.preventDefault();
handlePdfUploadAction(event.target);
});
}
// Manage Files Modal: Trigger Analysis Button
if (triggerAnalysisBtn) {
triggerAnalysisBtn.addEventListener('click', handleTriggerAnalysisAction);
}
// Manage Files Modal: Delete PDF Button (Event Delegation)
if (existingPdfListDiv) {
existingPdfListDiv.addEventListener('click', (event) => {
if (event.target.classList.contains('pdf-delete-btn')) {
const button = event.target;
const filename = button.dataset.filename;
const projectName = button.dataset.project;
if (filename && projectName) {
handlePdfDeleteAction(projectName, filename);
} else {
console.error("[Events] Missing filename or project name on delete button.", button);
}
}
});
}
// Manage Files Modal: Delete Project Button
if (deleteProjectBtn) {
deleteProjectBtn.addEventListener('click', handleDeleteProjectAction);
}
// Manage Files Modal: Manifest Upload Form
if (uploadManifestForm) {
uploadManifestForm.addEventListener('submit', (event) => {
event.preventDefault();
handleManifestUploadAction(event.target);
});
}
console.log("[Events] Event listeners attached.");
}
// --- Export (if using modules) ---
// export { initializeEventListeners };