diff --git a/src/import.rs b/src/import.rs index de36d9c..07e696e 100644 --- a/src/import.rs +++ b/src/import.rs @@ -15,7 +15,7 @@ use uuid::Uuid; use crate::feed_utils::fetch_feed; use crate::feeds::Feed; -use crate::jobs::{JobStatus, SharedJobStore}; +use crate::jobs::{JobStatus, JobType, SharedJobStore}; use crate::user::AuthenticatedUser; use crate::Db; @@ -130,7 +130,7 @@ pub async fn import_opml( // Create a background job let job_id = { let mut store = job_store.write().await; - store.create_job("opml_import".to_string()) + store.create_job(JobType::OpmlImport, user.user_id) }; // Launch background job @@ -252,3 +252,39 @@ fn extract_feeds( } } } + +#[derive(Debug, Serialize)] +#[serde(crate = "rocket::serde")] +pub struct JobStatusResponse { + status: String, + completed: usize, + total: usize, +} + +#[get("/jobs/")] +pub async fn get_job_status( + job_id: Uuid, + job_store: &State, + user: AuthenticatedUser, +) -> Result, Status> { + let store = job_store.read().await; + let status = store + .get_job_status(job_id, user.user_id) + .ok_or(Status::NotFound)?; + + let response = match status { + JobStatus::InProgress { completed, total } => JobStatusResponse { + status: "in_progress".to_string(), + completed, + total, + }, + JobStatus::Completed { success_count } => JobStatusResponse { + status: "completed".to_string(), + completed: success_count, + total: success_count, + }, + JobStatus::Failed(_) => return Err(Status::InternalServerError), + }; + + Ok(Json(response)) +} diff --git a/src/jobs.rs b/src/jobs.rs index 94e6f12..5308427 100644 --- a/src/jobs.rs +++ b/src/jobs.rs @@ -3,18 +3,30 @@ use std::sync::Arc; use tokio::sync::RwLock; use uuid::Uuid; +#[derive(Debug, Clone)] +pub enum JobType { + OpmlImport, +} + #[derive(Debug, Clone)] pub enum JobStatus { - InProgress { completed: usize, total: usize }, - Completed { success_count: usize }, + InProgress { + completed: usize, + total: usize, + }, + Completed { + success_count: usize, + }, + #[allow(dead_code)] Failed(String), } #[derive(Debug, Clone)] pub struct Job { pub id: Uuid, - pub job_type: String, + pub job_type: JobType, pub status: JobStatus, + pub user_id: Uuid, } #[derive(Debug, Default)] @@ -29,7 +41,7 @@ impl JobStore { } } - pub fn create_job(&mut self, job_type: String) -> Uuid { + pub fn create_job(&mut self, job_type: JobType, user_id: Uuid) -> Uuid { let job_id = Uuid::new_v4(); self.jobs.insert( job_id, @@ -40,6 +52,7 @@ impl JobStore { completed: 0, total: 0, }, + user_id, }, ); job_id @@ -51,8 +64,14 @@ impl JobStore { } } - pub fn get_job_status(&self, job_id: Uuid) -> Option { - self.jobs.get(&job_id).map(|job| job.status.clone()) + pub fn get_job_status(&self, job_id: Uuid, user_id: Uuid) -> Option { + self.jobs.get(&job_id).and_then(|job| { + if job.user_id == user_id { + Some(job.status.clone()) + } else { + None + } + }) } } diff --git a/src/main.rs b/src/main.rs index 9292065..dd0f1fb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -177,6 +177,7 @@ fn rocket() -> _ { poll::poll_feed, poll::update_entry_state, import::import_opml, + import::get_job_status, ], ) .mount("/static", FileServer::from("static")) diff --git a/static/js/app.js b/static/js/app.js index fd1eb74..6bb97e1 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -88,9 +88,9 @@ document.addEventListener('DOMContentLoaded', function() { const result = await response.json(); if (result.success) { showStatusModal(result.message, false); - // TODO: Poll job status endpoint when implemented - // For now, just refresh the feed list after a delay - setTimeout(handleFeeds, 5000); + if (result.job_id) { + pollJobStatus(result.job_id); + } } else { showStatusModal('OPML import failed: ' + result.message, true); } @@ -106,6 +106,40 @@ document.addEventListener('DOMContentLoaded', function() { e.target.value = ''; }); + async function pollJobStatus(jobId) { + const maxAttempts = 30; // 30 attempts * 2 second delay = 1 minute maximum + let attempts = 0; + + const poll = async () => { + try { + const response = await fetch(`/jobs/${jobId}`); + if (response.ok) { + const status = await response.json(); + if (status.status === 'completed') { + showStatusModal(`Import completed. Successfully imported ${status.completed} feeds.`, false); + handleFeeds(); + return; + } else if (status.status === 'in_progress') { + showStatusModal(`Importing feeds... ${status.completed}/${status.total} completed`, false); + if (attempts++ < maxAttempts) { + setTimeout(poll, 2000); // Poll every 2 seconds + } else { + showStatusModal('Import taking longer than expected. Check feeds list in a few minutes.', false); + setTimeout(handleFeeds, 5000); + } + } + } else { + throw new Error('Failed to fetch job status'); + } + } catch (error) { + console.error('Failed to poll job status:', error); + showStatusModal('Failed to check import status. Please refresh the page.', true); + } + }; + + poll(); + } + // User menu handlers userMenuButton.addEventListener('click', (e) => { e.stopPropagation();