//! Job management types for processing queue use serde::{Deserialize, Serialize}; use crate::nmf::NmfConfig; /// Status of a processing job #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JobStatus { /// Unique job identifier pub id: String, /// Slide being processed pub slide_id: String, /// Current job state pub state: JobState, /// Progress percentage (0-100) pub progress: u8, /// Current phase description pub phase: String, /// Unix timestamp when job was queued pub queued_at: i64, /// Unix timestamp when processing started pub started_at: Option, /// Unix timestamp when processing completed pub completed_at: Option, /// Error message if failed pub error: Option, } impl JobStatus { /// Create a new queued job pub fn new_queued(id: String, slide_id: String) -> Self { Self { id, slide_id, state: JobState::Queued, progress: 0, phase: "Queued".to_string(), queued_at: chrono_timestamp(), started_at: None, completed_at: None, error: None, } } /// Update to running state pub fn start(&mut self) { self.state = JobState::Running; self.started_at = Some(chrono_timestamp()); } /// Update progress pub fn update_progress(&mut self, progress: u8, phase: &str) { self.progress = progress.min(100); self.phase = phase.to_string(); } /// Mark as completed pub fn complete(&mut self) { self.state = JobState::Completed; self.progress = 100; self.phase = "Completed".to_string(); self.completed_at = Some(chrono_timestamp()); } /// Mark as failed pub fn fail(&mut self, error: &str) { self.state = JobState::Failed; self.phase = "Failed".to_string(); self.error = Some(error.to_string()); self.completed_at = Some(chrono_timestamp()); } } /// Job execution state #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] #[derive(Default)] pub enum JobState { /// Job is waiting in queue #[default] Queued, /// Job is currently processing Running, /// Job completed successfully Completed, /// Job failed Failed, } /// Progress update for real-time tracking #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JobProgress { /// Job ID pub job_id: String, /// Progress percentage (0-100) pub percent: u8, /// Current phase pub phase: ProcessingPhase, /// Optional message pub message: Option, } /// Processing phases for NMF workflow #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] #[derive(Default)] pub enum ProcessingPhase { /// Loading image data #[default] Loading, /// Converting to optical density ConvertingOd, /// Estimating stain vectors EstimatingStains, /// Running NMF iterations Processing, /// Generating output images GeneratingOutput, /// Saving results Saving, /// Completed Complete, } impl ProcessingPhase { /// Get typical progress percentage for this phase pub fn typical_progress(&self) -> u8 { match self { Self::Loading => 5, Self::ConvertingOd => 10, Self::EstimatingStains => 15, Self::Processing => 50, // Main processing 15-85% Self::GeneratingOutput => 90, Self::Saving => 95, Self::Complete => 100, } } /// Get human-readable description pub fn description(&self) -> &'static str { match self { Self::Loading => "Loading image data", Self::ConvertingOd => "Converting to optical density", Self::EstimatingStains => "Estimating stain vectors", Self::Processing => "Running NMF algorithm", Self::GeneratingOutput => "Generating stain maps", Self::Saving => "Saving results", Self::Complete => "Processing complete", } } } /// Job request to queue processing #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProcessingRequest { /// Slide to process pub slide_id: String, /// NMF configuration pub config: NmfConfig, /// Priority (higher = processed sooner) pub priority: u8, } impl ProcessingRequest { /// Create a new processing request with default priority pub fn new(slide_id: String, config: NmfConfig) -> Self { Self { slide_id, config, priority: 5, // Default middle priority } } /// Create a high-priority request pub fn high_priority(slide_id: String, config: NmfConfig) -> Self { Self { slide_id, config, priority: 10, } } } /// Get current Unix timestamp fn chrono_timestamp() -> i64 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_secs() as i64) .unwrap_or(0) } #[cfg(test)] mod tests { use super::*; #[test] fn test_job_status_lifecycle() { let mut job = JobStatus::new_queued("job1".to_string(), "slide1".to_string()); assert_eq!(job.state, JobState::Queued); assert_eq!(job.progress, 0); job.start(); assert_eq!(job.state, JobState::Running); assert!(job.started_at.is_some()); job.update_progress(50, "Processing"); assert_eq!(job.progress, 50); job.complete(); assert_eq!(job.state, JobState::Completed); assert_eq!(job.progress, 100); } #[test] fn test_job_status_failure() { let mut job = JobStatus::new_queued("job1".to_string(), "slide1".to_string()); job.start(); job.fail("Out of memory"); assert_eq!(job.state, JobState::Failed); assert_eq!(job.error, Some("Out of memory".to_string())); } #[test] fn test_processing_phase_serialization() { let phase = ProcessingPhase::Processing; let json = serde_json::to_string(&phase).unwrap(); assert_eq!(json, "\"processing\""); } #[test] fn test_processing_request() { let request = ProcessingRequest::new("slide1".to_string(), NmfConfig::default()); assert_eq!(request.priority, 5); let high = ProcessingRequest::high_priority("slide2".to_string(), NmfConfig::default()); assert_eq!(high.priority, 10); } }