You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
✅ Replaced single current_job with active_jobs dictionary
✅ Added job_queue list for queued jobs
✅ Added max_concurrent_jobs = 3 to process 3 files simultaneously
✅ Added persistent job storage to processing_jobs.json
✅ Created load_jobs_from_disk() and save_jobs_to_disk() functions
2. app.py - Upload Endpoint
✅ Updated /admin/upload to accept multiple files via files parameter
✅ Each file creates a separate job with unique job_id
✅ Jobs are added to queue and processed in background
✅ Returns array of created jobs with their IDs
3. app.py - Background Job Processor
✅ Created start_job_processor() function
✅ Created process_job_queue() background thread
✅ Processes up to 3 jobs concurrently
✅ Automatically starts when jobs are queued
4. app.py - Status Endpoints
✅ Updated /admin/status to return all jobs (not just one)
✅ Added /admin/job/<job_id> for individual job status
✅ Returns queue length and active job count
5. processor.py - ProcessingJob Class
✅ Added job_id parameter to __init__
✅ Job ID can be provided or auto-generated
Frontend Changes (NEEDED)
1. dashboard.html - File Input
✅ Added multiple attribute to file input
2. dashboard.html - JavaScript Variables
✅ Changed selectedFile to selectedFiles array
3. dashboard.html - File Selection Handler (TODO)
// Update fileInput.addEventListener('change') around line 978fileInput.addEventListener('change',(e)=>{if(e.target.files&&e.target.files.length>0){selectedFiles=Array.from(e.target.files);displaySelectedFiles();}});functiondisplaySelectedFiles(){constfileDetails=document.getElementById('file-details');if(selectedFiles.length===0){fileDetails.innerHTML='No files selected';return;}lethtml=`<strong>${selectedFiles.length} file(s) selected:</strong><ul>`;selectedFiles.forEach(file=>{html+=`<li>${file.name} (${(file.size/1024/1024).toFixed(2)} MB)</li>`;});html+='</ul>';fileDetails.innerHTML=html;fileInfo.classList.add('show');uploadBtn.classList.add('show');}
4. dashboard.html - Upload Handler (TODO)
// Update upload button click handler around line 1270uploadBtn.addEventListener('click',async()=>{if(selectedFiles.length===0){showError('Please select at least one file');return;}// Validate form fields...constformData=newFormData();// Append all filesselectedFiles.forEach(file=>{formData.append('files',file);});// Append metadata (same for all files)formData.append('county',county);formData.append('year',year);formData.append('election_type',electionType);formData.append('election_date',electionDate);formData.append('voting_method',votingMethod);formData.append('primary_party',primaryParty);try{uploadBtn.disabled=true;uploadBtn.textContent='Uploading...';constresponse=awaitfetch('/admin/upload',{method: 'POST',body: formData,credentials: 'include'});constdata=awaitresponse.json();if(response.ok&&data.success){showSuccess(`${data.jobs.length} file(s) uploaded successfully. Processing started...`);if(data.errors.length>0){showError(`Some files failed: ${data.errors.join(', ')}`);}// Start polling for all jobsstartStatusPolling();// Reset formselectedFiles=[];fileInput.value='';fileInfo.classList.remove('show');uploadBtn.classList.remove('show');}else{showError(data.errors.join(', ')||'Upload failed');}}catch(error){showError('Upload failed. Please try again.');}finally{uploadBtn.disabled=false;uploadBtn.textContent='Upload and Process';}});
5. dashboard.html - Status Polling (TODO)
// Update startStatusPolling() around line 1330functionstartStatusPolling(){if(statusPollInterval){clearInterval(statusPollInterval);}statusPollInterval=setInterval(async()=>{try{constresponse=awaitfetch('/admin/status',{credentials: 'include'});if(!response.ok){if(response.status===401){clearInterval(statusPollInterval);showError('Session expired. Please log in again.');}return;}constdata=awaitresponse.json();// Update UI with all jobsupdateJobsDisplay(data.jobs);// Stop polling if no active jobsif(data.active_count===0&&data.queue_length===0){clearInterval(statusPollInterval);statusPollInterval=null;}}catch(error){console.error('Status poll error:',error);}},2000);// Poll every 2 seconds}functionupdateJobsDisplay(jobs){constprogressSection=document.getElementById('progress-section');if(jobs.length===0){progressSection.classList.remove('show');return;}progressSection.classList.add('show');// Create HTML for all jobslethtml='<div class="jobs-container">';jobs.forEach(job=>{conststatusClass=job.status==='completed' ? 'success' :
job.status==='failed' ? 'error' :
job.status==='running' ? 'running' : 'queued';html+=` <div class="job-card ${statusClass}"> <div class="job-header"> <strong>${job.original_filename}</strong> <span class="job-status">${job.status}</span> </div> <div class="job-info"> <span>${job.county} County - ${job.year}${job.election_type}</span> </div> <div class="progress-bar"> <div class="progress-fill" style="width: ${job.progress*100}%">${Math.round(job.progress*100)}% </div> </div> <div class="job-stats"> <span>Total: ${job.total_records}</span> <span>Processed: ${job.processed_records}</span> <span>Geocoded: ${job.geocoded_count}</span> <span>Failed: ${job.failed_count}</span> </div> </div> `;});html+='</div>';// Replace progress section contentprogressSection.innerHTML='<h2>Processing Status</h2>'+html;}