Job status work

This commit is contained in:
Greg Shuflin 2025-02-16 01:35:49 -08:00
parent 2874d3a885
commit c138940250
4 changed files with 101 additions and 11 deletions

View File

@ -15,7 +15,7 @@ use uuid::Uuid;
use crate::feed_utils::fetch_feed; use crate::feed_utils::fetch_feed;
use crate::feeds::Feed; use crate::feeds::Feed;
use crate::jobs::{JobStatus, SharedJobStore}; use crate::jobs::{JobStatus, JobType, SharedJobStore};
use crate::user::AuthenticatedUser; use crate::user::AuthenticatedUser;
use crate::Db; use crate::Db;
@ -130,7 +130,7 @@ pub async fn import_opml(
// Create a background job // Create a background job
let job_id = { let job_id = {
let mut store = job_store.write().await; 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 // 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/<job_id>")]
pub async fn get_job_status(
job_id: Uuid,
job_store: &State<SharedJobStore>,
user: AuthenticatedUser,
) -> Result<Json<JobStatusResponse>, 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))
}

View File

@ -3,18 +3,30 @@ use std::sync::Arc;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use uuid::Uuid; use uuid::Uuid;
#[derive(Debug, Clone)]
pub enum JobType {
OpmlImport,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum JobStatus { pub enum JobStatus {
InProgress { completed: usize, total: usize }, InProgress {
Completed { success_count: usize }, completed: usize,
total: usize,
},
Completed {
success_count: usize,
},
#[allow(dead_code)]
Failed(String), Failed(String),
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Job { pub struct Job {
pub id: Uuid, pub id: Uuid,
pub job_type: String, pub job_type: JobType,
pub status: JobStatus, pub status: JobStatus,
pub user_id: Uuid,
} }
#[derive(Debug, Default)] #[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(); let job_id = Uuid::new_v4();
self.jobs.insert( self.jobs.insert(
job_id, job_id,
@ -40,6 +52,7 @@ impl JobStore {
completed: 0, completed: 0,
total: 0, total: 0,
}, },
user_id,
}, },
); );
job_id job_id
@ -51,8 +64,14 @@ impl JobStore {
} }
} }
pub fn get_job_status(&self, job_id: Uuid) -> Option<JobStatus> { pub fn get_job_status(&self, job_id: Uuid, user_id: Uuid) -> Option<JobStatus> {
self.jobs.get(&job_id).map(|job| job.status.clone()) self.jobs.get(&job_id).and_then(|job| {
if job.user_id == user_id {
Some(job.status.clone())
} else {
None
}
})
} }
} }

View File

@ -177,6 +177,7 @@ fn rocket() -> _ {
poll::poll_feed, poll::poll_feed,
poll::update_entry_state, poll::update_entry_state,
import::import_opml, import::import_opml,
import::get_job_status,
], ],
) )
.mount("/static", FileServer::from("static")) .mount("/static", FileServer::from("static"))

View File

@ -88,9 +88,9 @@ document.addEventListener('DOMContentLoaded', function() {
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
showStatusModal(result.message, false); showStatusModal(result.message, false);
// TODO: Poll job status endpoint when implemented if (result.job_id) {
// For now, just refresh the feed list after a delay pollJobStatus(result.job_id);
setTimeout(handleFeeds, 5000); }
} else { } else {
showStatusModal('OPML import failed: ' + result.message, true); showStatusModal('OPML import failed: ' + result.message, true);
} }
@ -106,6 +106,40 @@ document.addEventListener('DOMContentLoaded', function() {
e.target.value = ''; 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 // User menu handlers
userMenuButton.addEventListener('click', (e) => { userMenuButton.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();