use crate::log_processor::{LogProcessingRequest, LogProcessor, ProcessedLogEntry};
use crate::models::{
ExecutionResultMsg, JobExecution, LogFilterLevel, StepExecution, Workflow, WorkflowExecution,
WorkflowStatus,
};
use chrono::Local;
use crossterm::event::KeyCode;
use ratatui::widgets::{ListState, TableState};
use std::sync::mpsc;
use std::time::{Duration, Instant};
use wrkflw_executor::{JobStatus, RuntimeType, StepStatus};
pub struct App {
pub workflows: Vec<Workflow>,
pub workflow_list_state: ListState,
pub selected_tab: usize,
pub running: bool,
pub show_help: bool,
pub runtime_type: RuntimeType,
pub validation_mode: bool,
pub preserve_containers_on_failure: bool,
pub execution_queue: Vec<usize>, pub current_execution: Option<usize>,
pub logs: Vec<String>, pub log_scroll: usize, pub job_list_state: ListState, pub detailed_view: bool, pub step_list_state: ListState, pub step_table_state: TableState, pub last_tick: Instant, pub tick_rate: Duration, pub tx: mpsc::Sender<ExecutionResultMsg>, pub status_message: Option<String>, pub status_message_time: Option<Instant>,
pub log_search_query: String, pub log_search_active: bool, pub log_filter_level: Option<LogFilterLevel>, pub log_search_matches: Vec<usize>, pub log_search_match_idx: usize,
pub help_scroll: usize,
pub log_processor: LogProcessor,
pub processed_logs: Vec<ProcessedLogEntry>,
pub logs_need_update: bool, pub last_system_logs_count: usize, }
impl App {
pub fn new(
runtime_type: RuntimeType,
tx: mpsc::Sender<ExecutionResultMsg>,
preserve_containers_on_failure: bool,
) -> App {
let mut workflow_list_state = ListState::default();
workflow_list_state.select(Some(0));
let mut job_list_state = ListState::default();
job_list_state.select(Some(0));
let mut step_list_state = ListState::default();
step_list_state.select(Some(0));
let mut step_table_state = TableState::default();
step_table_state.select(Some(0));
let mut initial_logs = Vec::new();
let runtime_type = match runtime_type {
RuntimeType::Docker => {
let is_docker_available = match std::panic::catch_unwind(|| {
let result = std::thread::scope(|s| {
let handle = s.spawn(|| {
wrkflw_utils::fd::with_stderr_to_null(
wrkflw_executor::docker::is_available,
)
.unwrap_or(false)
});
let start = std::time::Instant::now();
let timeout = std::time::Duration::from_secs(1);
while start.elapsed() < timeout {
if handle.is_finished() {
return handle.join().unwrap_or(false);
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
wrkflw_logging::warning(
"Docker availability check timed out, falling back to emulation mode",
);
false
});
result
}) {
Ok(result) => result,
Err(_) => {
wrkflw_logging::warning("Docker availability check failed with panic, falling back to emulation mode");
false
}
};
if !is_docker_available {
initial_logs.push(
"Docker is not available or unresponsive. Using emulation mode instead."
.to_string(),
);
wrkflw_logging::warning(
"Docker is not available or unresponsive. Using emulation mode instead.",
);
RuntimeType::Emulation
} else {
wrkflw_logging::info("Docker is available, using Docker runtime");
RuntimeType::Docker
}
}
RuntimeType::Podman => {
let is_podman_available = match std::panic::catch_unwind(|| {
let result = std::thread::scope(|s| {
let handle = s.spawn(|| {
wrkflw_utils::fd::with_stderr_to_null(
wrkflw_executor::podman::is_available,
)
.unwrap_or(false)
});
let start = std::time::Instant::now();
let timeout = std::time::Duration::from_secs(1);
while start.elapsed() < timeout {
if handle.is_finished() {
return handle.join().unwrap_or(false);
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
wrkflw_logging::warning(
"Podman availability check timed out, falling back to emulation mode",
);
false
});
result
}) {
Ok(result) => result,
Err(_) => {
wrkflw_logging::warning("Podman availability check failed with panic, falling back to emulation mode");
false
}
};
if !is_podman_available {
initial_logs.push(
"Podman is not available or unresponsive. Using emulation mode instead."
.to_string(),
);
wrkflw_logging::warning(
"Podman is not available or unresponsive. Using emulation mode instead.",
);
RuntimeType::Emulation
} else {
wrkflw_logging::info("Podman is available, using Podman runtime");
RuntimeType::Podman
}
}
RuntimeType::Emulation => RuntimeType::Emulation,
RuntimeType::SecureEmulation => RuntimeType::SecureEmulation,
};
App {
workflows: Vec::new(),
workflow_list_state,
selected_tab: 0,
running: false,
show_help: false,
runtime_type,
validation_mode: false,
preserve_containers_on_failure,
execution_queue: Vec::new(),
current_execution: None,
logs: initial_logs,
log_scroll: 0,
job_list_state,
detailed_view: false,
step_list_state,
step_table_state,
last_tick: Instant::now(),
tick_rate: Duration::from_millis(250), tx,
status_message: None,
status_message_time: None,
log_search_query: String::new(),
log_search_active: false,
log_filter_level: Some(LogFilterLevel::All),
log_search_matches: Vec::new(),
log_search_match_idx: 0,
help_scroll: 0,
log_processor: LogProcessor::new(),
processed_logs: Vec::new(),
logs_need_update: true,
last_system_logs_count: 0,
}
}
pub fn toggle_selected(&mut self) {
if let Some(idx) = self.workflow_list_state.selected() {
if idx < self.workflows.len() {
self.workflows[idx].selected = !self.workflows[idx].selected;
}
}
}
pub fn toggle_emulation_mode(&mut self) {
self.runtime_type = match self.runtime_type {
RuntimeType::Docker => RuntimeType::Podman,
RuntimeType::Podman => RuntimeType::SecureEmulation,
RuntimeType::SecureEmulation => RuntimeType::Emulation,
RuntimeType::Emulation => RuntimeType::Docker,
};
self.logs
.push(format!("Switched to {} mode", self.runtime_type_name()));
}
pub fn toggle_validation_mode(&mut self) {
self.validation_mode = !self.validation_mode;
let mode = if self.validation_mode {
"validation"
} else {
"normal"
};
let timestamp = Local::now().format("%H:%M:%S").to_string();
self.logs
.push(format!("[{}] Switched to {} mode", timestamp, mode));
wrkflw_logging::info(&format!("Switched to {} mode", mode));
}
pub fn runtime_type_name(&self) -> &str {
match self.runtime_type {
RuntimeType::Docker => "Docker",
RuntimeType::Podman => "Podman",
RuntimeType::SecureEmulation => "Secure Emulation",
RuntimeType::Emulation => "Emulation (Unsafe)",
}
}
pub fn previous_workflow(&mut self) {
if self.workflows.is_empty() {
return;
}
let i = match self.workflow_list_state.selected() {
Some(i) => {
if i == 0 {
self.workflows.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.workflow_list_state.select(Some(i));
}
pub fn next_workflow(&mut self) {
if self.workflows.is_empty() {
return;
}
let i = match self.workflow_list_state.selected() {
Some(i) => {
if i >= self.workflows.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.workflow_list_state.select(Some(i));
}
pub fn previous_job(&mut self) {
let current_workflow_idx = self
.current_execution
.or_else(|| self.workflow_list_state.selected());
if let Some(workflow_idx) = current_workflow_idx {
if workflow_idx >= self.workflows.len() {
return;
}
if let Some(execution) = &self.workflows[workflow_idx].execution_details {
if execution.jobs.is_empty() {
return;
}
let i = match self.job_list_state.selected() {
Some(i) => {
if i == 0 {
execution.jobs.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.job_list_state.select(Some(i));
self.step_list_state.select(Some(0));
}
}
}
pub fn next_job(&mut self) {
let current_workflow_idx = self
.current_execution
.or_else(|| self.workflow_list_state.selected())
.filter(|&idx| idx < self.workflows.len());
if let Some(workflow_idx) = current_workflow_idx {
if workflow_idx >= self.workflows.len() {
return;
}
if let Some(execution) = &self.workflows[workflow_idx].execution_details {
if execution.jobs.is_empty() {
return;
}
let i = match self.job_list_state.selected() {
Some(i) => {
if i >= execution.jobs.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.job_list_state.select(Some(i));
self.step_list_state.select(Some(0));
}
}
}
pub fn previous_step(&mut self) {
let current_workflow_idx = self
.current_execution
.or_else(|| self.workflow_list_state.selected())
.filter(|&idx| idx < self.workflows.len());
if let Some(workflow_idx) = current_workflow_idx {
if let Some(execution) = &self.workflows[workflow_idx].execution_details {
if let Some(job_idx) = self.job_list_state.selected() {
if job_idx < execution.jobs.len() {
let steps = &execution.jobs[job_idx].steps;
if steps.is_empty() {
return;
}
let i = match self.step_list_state.selected() {
Some(i) => {
if i == 0 {
steps.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.step_list_state.select(Some(i));
self.step_table_state.select(Some(i));
}
}
}
}
}
pub fn next_step(&mut self) {
let current_workflow_idx = self
.current_execution
.or_else(|| self.workflow_list_state.selected())
.filter(|&idx| idx < self.workflows.len());
if let Some(workflow_idx) = current_workflow_idx {
if let Some(execution) = &self.workflows[workflow_idx].execution_details {
if let Some(job_idx) = self.job_list_state.selected() {
if job_idx < execution.jobs.len() {
let steps = &execution.jobs[job_idx].steps;
if steps.is_empty() {
return;
}
let i = match self.step_list_state.selected() {
Some(i) => {
if i >= steps.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.step_list_state.select(Some(i));
self.step_table_state.select(Some(i));
}
}
}
}
}
pub fn switch_tab(&mut self, tab: usize) {
self.selected_tab = tab;
}
pub fn queue_selected_for_execution(&mut self) {
if let Some(idx) = self.workflow_list_state.selected() {
if idx < self.workflows.len() && !self.execution_queue.contains(&idx) {
self.execution_queue.push(idx);
self.add_timestamped_log(&format!(
"Added '{}' to execution queue. Press 'Enter' to start.",
self.workflows[idx].name
));
}
}
}
pub fn start_execution(&mut self) {
if !self.execution_queue.is_empty() && self.current_execution.is_none() {
self.running = true;
let timestamp = Local::now().format("%H:%M:%S").to_string();
self.logs
.push(format!("[{}] Starting workflow execution...", timestamp));
wrkflw_logging::info("Starting workflow execution...");
}
}
pub fn process_execution_result(
&mut self,
workflow_idx: usize,
result: Result<(Vec<wrkflw_executor::JobResult>, ()), String>,
) {
if workflow_idx >= self.workflows.len() {
let timestamp = Local::now().format("%H:%M:%S").to_string();
self.logs.push(format!(
"[{}] Error: Invalid workflow index received",
timestamp
));
wrkflw_logging::error("Invalid workflow index received in process_execution_result");
return;
}
let workflow = &mut self.workflows[workflow_idx];
if workflow.execution_details.is_none() {
workflow.execution_details = Some(WorkflowExecution {
jobs: Vec::new(),
start_time: Local::now(),
end_time: Some(Local::now()),
logs: Vec::new(),
progress: 1.0,
});
}
if let Some(execution_details) = &mut workflow.execution_details {
execution_details.end_time = Some(Local::now());
match &result {
Ok((jobs, _)) => {
let timestamp = Local::now().format("%H:%M:%S").to_string();
execution_details
.logs
.push(format!("[{}] Operation completed successfully.", timestamp));
execution_details.progress = 1.0;
execution_details.jobs = jobs
.iter()
.map(|job_result| JobExecution {
name: job_result.name.clone(),
status: match job_result.status {
wrkflw_executor::JobStatus::Success => JobStatus::Success,
wrkflw_executor::JobStatus::Failure => JobStatus::Failure,
wrkflw_executor::JobStatus::Skipped => JobStatus::Skipped,
},
steps: job_result
.steps
.iter()
.map(|step_result| StepExecution {
name: step_result.name.clone(),
status: match step_result.status {
wrkflw_executor::StepStatus::Success => StepStatus::Success,
wrkflw_executor::StepStatus::Failure => StepStatus::Failure,
wrkflw_executor::StepStatus::Skipped => StepStatus::Skipped,
},
output: step_result.output.clone(),
})
.collect::<Vec<StepExecution>>(),
logs: vec![job_result.logs.clone()],
})
.collect::<Vec<JobExecution>>();
}
Err(e) => {
let timestamp = Local::now().format("%H:%M:%S").to_string();
execution_details
.logs
.push(format!("[{}] Error: {}", timestamp, e));
execution_details.progress = 1.0;
execution_details.jobs = vec![JobExecution {
name: "Workflow Execution".to_string(),
status: JobStatus::Failure,
steps: vec![StepExecution {
name: "Execution Error".to_string(),
status: StepStatus::Failure,
output: format!("Error: {}\n\nThis error prevented the workflow from executing properly.", e),
}],
logs: vec![format!("Workflow execution error: {}", e)],
}];
}
}
}
match result {
Ok(_) => {
workflow.status = WorkflowStatus::Success;
let timestamp = Local::now().format("%H:%M:%S").to_string();
self.logs.push(format!(
"[{}] Workflow '{}' completed successfully!",
timestamp, workflow.name
));
wrkflw_logging::info(&format!(
"[{}] Workflow '{}' completed successfully!",
timestamp, workflow.name
));
}
Err(e) => {
workflow.status = WorkflowStatus::Failed;
let timestamp = Local::now().format("%H:%M:%S").to_string();
self.logs.push(format!(
"[{}] Workflow '{}' failed: {}",
timestamp, workflow.name, e
));
wrkflw_logging::error(&format!(
"[{}] Workflow '{}' failed: {}",
timestamp, workflow.name, e
));
}
}
if let Some(current_idx) = self.current_execution {
if current_idx == workflow_idx {
self.current_execution = None;
}
}
}
pub fn get_next_workflow_to_execute(&mut self) -> Option<usize> {
if self.execution_queue.is_empty() {
return None;
}
let next = self.execution_queue.remove(0);
self.workflows[next].status = WorkflowStatus::Running;
self.current_execution = Some(next);
self.logs
.push(format!("Executing workflow: {}", self.workflows[next].name));
wrkflw_logging::info(&format!(
"Executing workflow: {}",
self.workflows[next].name
));
self.workflows[next].execution_details = Some(WorkflowExecution {
jobs: Vec::new(),
start_time: Local::now(),
end_time: None,
logs: vec!["Execution started".to_string()],
progress: 0.0, });
Some(next)
}
pub fn toggle_detailed_view(&mut self) {
self.detailed_view = !self.detailed_view;
if self.detailed_view {
if let Some(step_idx) = self.step_list_state.selected() {
self.step_table_state.select(Some(step_idx));
} else {
self.step_list_state.select(Some(0));
self.step_table_state.select(Some(0));
}
if self.job_list_state.selected().is_none() {
self.job_list_state.select(Some(0));
}
}
}
pub fn handle_log_search_input(&mut self, key: KeyCode) {
match key {
KeyCode::Esc => {
self.log_search_active = false;
self.log_search_query.clear();
self.log_search_matches.clear();
self.mark_logs_for_update();
}
KeyCode::Backspace => {
self.log_search_query.pop();
self.mark_logs_for_update();
}
KeyCode::Enter => {
self.log_search_active = false;
}
KeyCode::Char(c) => {
self.log_search_query.push(c);
self.mark_logs_for_update();
}
_ => {}
}
}
pub fn toggle_log_search(&mut self) {
self.log_search_active = !self.log_search_active;
if !self.log_search_active {
} else {
self.mark_logs_for_update();
}
}
pub fn toggle_log_filter(&mut self) {
self.log_filter_level = match &self.log_filter_level {
None => Some(LogFilterLevel::Info),
Some(level) => Some(level.next()),
};
self.mark_logs_for_update();
}
pub fn clear_log_search_and_filter(&mut self) {
self.log_search_query.clear();
self.log_filter_level = None;
self.log_search_matches.clear();
self.log_search_match_idx = 0;
self.mark_logs_for_update();
}
pub fn update_log_search_matches(&mut self) {
self.log_search_matches.clear();
self.log_search_match_idx = 0;
let mut all_logs = Vec::new();
for log in &self.logs {
all_logs.push(log.clone());
}
for log in wrkflw_logging::get_logs() {
all_logs.push(log.clone());
}
for (idx, log) in all_logs.iter().enumerate() {
let passes_filter = match &self.log_filter_level {
None => true,
Some(level) => level.matches(log),
};
let matches_search = if self.log_search_query.is_empty() {
true
} else {
log.to_lowercase()
.contains(&self.log_search_query.to_lowercase())
};
if passes_filter && matches_search {
self.log_search_matches.push(idx);
}
}
if !self.log_search_matches.is_empty() {
if let Some(&idx) = self.log_search_matches.first() {
self.log_scroll = idx;
if !self.log_search_query.is_empty() {
self.set_status_message(format!(
"Found {} matches for '{}'",
self.log_search_matches.len(),
self.log_search_query
));
}
}
} else if !self.log_search_query.is_empty() {
self.set_status_message(format!("No matches found for '{}'", self.log_search_query));
}
}
pub fn next_search_match(&mut self) {
if !self.log_search_matches.is_empty() {
self.log_search_match_idx =
(self.log_search_match_idx + 1) % self.log_search_matches.len();
if let Some(&idx) = self.log_search_matches.get(self.log_search_match_idx) {
self.log_scroll = idx;
self.set_status_message(format!(
"Search match {}/{} for '{}'",
self.log_search_match_idx + 1,
self.log_search_matches.len(),
self.log_search_query
));
}
}
}
pub fn previous_search_match(&mut self) {
if !self.log_search_matches.is_empty() {
self.log_search_match_idx = if self.log_search_match_idx == 0 {
self.log_search_matches.len() - 1
} else {
self.log_search_match_idx - 1
};
if let Some(&idx) = self.log_search_matches.get(self.log_search_match_idx) {
self.log_scroll = idx;
self.set_status_message(format!(
"Search match {}/{} for '{}'",
self.log_search_match_idx + 1,
self.log_search_matches.len(),
self.log_search_query
));
}
}
}
pub fn scroll_logs_up(&mut self) {
self.log_scroll = self.log_scroll.saturating_sub(1);
}
pub fn scroll_logs_down(&mut self) {
let total_logs = self.logs.len() + wrkflw_logging::get_logs().len();
if total_logs > 0 {
self.log_scroll = (self.log_scroll + 1).min(total_logs - 1);
}
}
pub fn scroll_help_up(&mut self) {
self.help_scroll = self.help_scroll.saturating_sub(1);
}
pub fn scroll_help_down(&mut self) {
const MAX_HELP_SCROLL: usize = 30; self.help_scroll = (self.help_scroll + 1).min(MAX_HELP_SCROLL);
}
pub fn update_running_workflow_progress(&mut self) {
if let Some(idx) = self.current_execution {
if let Some(execution) = &mut self.workflows[idx].execution_details {
if execution.end_time.is_none() {
execution.progress = (execution.progress + 0.01).min(0.95);
}
}
}
}
pub fn set_status_message(&mut self, message: String) {
self.status_message = Some(message);
self.status_message_time = Some(Instant::now());
}
pub fn tick(&mut self) -> bool {
let now = Instant::now();
if let Some(message_time) = self.status_message_time {
if now.duration_since(message_time).as_secs() >= 3 {
self.status_message = None;
self.status_message_time = None;
}
}
if now.duration_since(self.last_tick) >= self.tick_rate {
self.last_tick = now;
true
} else {
false
}
}
pub fn trigger_selected_workflow(&mut self) {
if let Some(selected_idx) = self.workflow_list_state.selected() {
if selected_idx < self.workflows.len() {
let workflow = &self.workflows[selected_idx];
if workflow.name.is_empty() {
let timestamp = Local::now().format("%H:%M:%S").to_string();
self.logs
.push(format!("[{}] Error: Invalid workflow selection", timestamp));
wrkflw_logging::error(
"Invalid workflow selection in trigger_selected_workflow",
);
return;
}
let timestamp = Local::now().format("%H:%M:%S").to_string();
self.logs.push(format!(
"[{}] Triggering workflow: {}",
timestamp, workflow.name
));
wrkflw_logging::info(&format!("Triggering workflow: {}", workflow.name));
let workflow_name = workflow.name.clone();
let tx_clone = self.tx.clone();
self.current_execution = Some(selected_idx);
self.selected_tab = 1;
std::thread::spawn(move || {
let rt = match tokio::runtime::Runtime::new() {
Ok(runtime) => runtime,
Err(e) => {
let _ = tx_clone.send((
selected_idx,
Err(format!("Failed to create Tokio runtime: {}", e)),
));
return;
}
};
let result = rt.block_on(async {
crate::handlers::workflow::execute_curl_trigger(&workflow_name, None).await
});
if let Err(e) = tx_clone.send((selected_idx, result)) {
wrkflw_logging::error(&format!("Error sending trigger result: {}", e));
}
});
} else {
let timestamp = Local::now().format("%H:%M:%S").to_string();
self.logs
.push(format!("[{}] No workflow selected to trigger", timestamp));
wrkflw_logging::warning("No workflow selected to trigger");
}
} else {
self.logs
.push("No workflow selected to trigger".to_string());
wrkflw_logging::warning("No workflow selected to trigger");
}
}
pub fn reset_workflow_status(&mut self) {
if self.workflow_list_state.selected().is_none() {
let timestamp = Local::now().format("%H:%M:%S").to_string();
self.logs.push(format!(
"[{}] Debug: No workflow selected for reset",
timestamp
));
wrkflw_logging::warning("No workflow selected for reset");
return;
}
if let Some(idx) = self.workflow_list_state.selected() {
if idx < self.workflows.len() {
let workflow = &mut self.workflows[idx];
let timestamp = Local::now().format("%H:%M:%S").to_string();
self.logs.push(format!(
"[{}] Debug: Attempting to reset workflow '{}' from {:?} state",
timestamp, workflow.name, workflow.status
));
let old_status = match workflow.status {
WorkflowStatus::Success => "Success",
WorkflowStatus::Failed => "Failed",
WorkflowStatus::Skipped => "Skipped",
WorkflowStatus::NotStarted => "NotStarted",
WorkflowStatus::Running => "Running",
};
let workflow_name = workflow.name.clone();
workflow.status = WorkflowStatus::NotStarted;
workflow.execution_details = None;
let timestamp = Local::now().format("%H:%M:%S").to_string();
self.logs.push(format!(
"[{}] Reset workflow '{}' from {} state to NotStarted - status is now {:?}",
timestamp, workflow.name, old_status, workflow.status
));
wrkflw_logging::info(&format!(
"Reset workflow '{}' from {} state to NotStarted - status is now {:?}",
workflow.name, old_status, workflow.status
));
self.set_status_message(format!("✅ Workflow '{}' has been reset!", workflow_name));
}
}
}
pub fn request_log_processing_update(&mut self) {
let request = LogProcessingRequest {
search_query: self.log_search_query.clone(),
filter_level: self.log_filter_level.clone(),
app_logs: self.logs.clone(),
app_logs_count: self.logs.len(),
system_logs_count: wrkflw_logging::get_logs().len(),
};
if self.log_processor.request_update(request).is_err() {
self.log_processor = LogProcessor::new();
self.logs_need_update = true;
}
}
pub fn check_log_processing_updates(&mut self) {
let current_system_logs_count = wrkflw_logging::get_logs().len();
if current_system_logs_count != self.last_system_logs_count {
self.last_system_logs_count = current_system_logs_count;
self.mark_logs_for_update();
}
if let Some(response) = self.log_processor.try_get_update() {
self.processed_logs = response.processed_logs;
self.log_search_matches = response.search_matches;
if !self.log_search_matches.is_empty() && !self.log_search_query.is_empty() {
self.log_search_match_idx = 0;
if let Some(&idx) = self.log_search_matches.first() {
self.log_scroll = idx;
}
}
self.logs_need_update = false;
}
}
pub fn mark_logs_for_update(&mut self) {
self.logs_need_update = true;
self.request_log_processing_update();
}
pub fn get_combined_logs(&self) -> Vec<String> {
let mut all_logs = Vec::new();
for log in &self.logs {
all_logs.push(log.clone());
}
for log in wrkflw_logging::get_logs() {
all_logs.push(log.clone());
}
all_logs
}
pub fn add_log(&mut self, message: String) {
self.logs.push(message);
self.mark_logs_for_update();
}
pub fn add_timestamped_log(&mut self, message: &str) {
let timestamp = Local::now().format("%H:%M:%S").to_string();
let formatted_message = format!("[{}] {}", timestamp, message);
self.add_log(formatted_message);
}
}