Skip to main content

llm_manager/tui/app/
async_ops.rs

1use super::types::App;
2use std::path::PathBuf;
3use std::sync::Arc;
4use std::sync::atomic::{AtomicBool, AtomicU8};
5
6impl App {
7    pub async fn start_pending_download(&mut self) {
8        if let Some((model_id, filename, download_url, file_size, model_id_for_subdir)) =
9            self.pending.pending_download.take()
10        {
11            let models_dirs = &self.config.models_dirs;
12            // Use the first directory as the download destination, stored under model_id subdirectory
13            let models_dir = models_dirs.first().cloned().unwrap_or_default();
14            let dest_dir = models_dir.join(&model_id_for_subdir);
15            let basename = std::path::Path::new(&filename)
16                .file_name()
17                .unwrap_or_default();
18            let dest = dest_dir.join(basename);
19            // Create the model_id subdirectory if it doesn't exist
20            tokio::fs::create_dir_all(&dest_dir).await.ok();
21            let free_space = crate::backend::hub::get_free_space_bytes(&models_dir);
22            if file_size > free_space {
23                self.add_log(
24                    format!(
25                        "Not enough disk space to download {}: need {} but only {} available",
26                        filename,
27                        crate::tui::format_size(file_size),
28                        crate::tui::format_size(free_space)
29                    ),
30                    crate::config::LogLevel::Warning,
31                );
32                return;
33            }
34            let model_id_clone = model_id.clone();
35            let filename_clone = filename.clone();
36            let url_clone = download_url.clone();
37            let cancelled = Arc::new(AtomicBool::new(false));
38            let cancelled_clone = cancelled.clone();
39            self.add_log(
40                format!("Downloading {}...", model_id),
41                crate::config::LogLevel::Info,
42            );
43            let tx = self.ensure_download_channel();
44            let tx_clone = tx.clone();
45            let cancelled_for_state = cancelled_clone.clone();
46            let download_state = Arc::new(AtomicU8::new(1));
47            let download_state_clone = download_state.clone();
48            let dest_path = dest.clone();
49            self.download.download_progress.last_mut().and_then(|d| {
50                d.dest = Some(dest_path.clone());
51                None::<()>
52            });
53
54            tokio::spawn(async move {
55                let mut state = crate::models::DownloadState::new(
56                    model_id_clone.clone(),
57                    filename_clone.clone(),
58                    0,
59                );
60                state.cancel_token = Some(cancelled_for_state);
61                state.download_state = 1;
62                state.dest = Some(dest_path);
63                state.download_state_arc = Some(download_state_clone.clone());
64                let result = crate::backend::hub::download_file(
65                    &model_id_clone,
66                    &filename_clone,
67                    &url_clone,
68                    &dest,
69                    &mut state,
70                    download_state_clone,
71                    tx_clone,
72                )
73                .await;
74                if let Err(e) = result {
75                    state.status = crate::models::DownloadStatus::Error(e.to_string());
76                    let _ = tx.send(state);
77                }
78            });
79            self.download.downloading = true;
80            self.cancelled = Some(cancelled);
81            self.download.download_scroll_state.select(Some(0));
82            self.ui.needs_redraw = true;
83        }
84    }
85
86    pub async fn start_pending_deletion(&mut self, path: PathBuf) {
87        let path_clone = path.clone();
88        tokio::spawn(async move {
89            if let Err(e) = tokio::fs::remove_file(&path_clone).await {
90                tracing::warn!("Failed to delete file: {}", e);
91            }
92        });
93        let model_key = path
94            .file_name()
95            .map(|n| n.to_string_lossy().to_string())
96            .unwrap_or_default();
97        self.config.model_overrides.delete(&model_key);
98        if let Err(e) = self.config.save() {
99            self.add_log(
100                format!("Failed to save config after deletion: {}", e),
101                crate::config::LogLevel::Error,
102            );
103        }
104        self.models.retain(|m| m.path != path);
105        if let Some(idx) = self.selected_model_idx {
106            if idx >= self.models.len() && !self.models.is_empty() {
107                self.selected_model_idx = Some(self.models.len() - 1);
108                self.on_model_selection_change();
109            } else if self.models.is_empty() {
110                self.selected_model_idx = None;
111                self.on_model_selection_change();
112            } else {
113                self.on_model_selection_change();
114            }
115        }
116        self.add_log(
117            format!("Model deleted: {:?}", path.file_name().unwrap_or_default()),
118            crate::config::LogLevel::Info,
119        );
120        self.ui.needs_redraw = true;
121    }
122
123    pub fn start_pending_backend_deletion(&mut self, backend: crate::models::Backend, tag: String) {
124        let bin_dir = crate::backend::hub::get_backend_dir(backend, &tag);
125        if bin_dir.exists() {
126            if let Err(e) = std::fs::remove_dir_all(&bin_dir) {
127                self.add_log(
128                    format!("Failed to delete backend: {}", e),
129                    crate::config::LogLevel::Error,
130                );
131            } else {
132                self.add_log(
133                    format!("Deleted backend {} ({})", backend, tag),
134                    crate::config::LogLevel::Info,
135                );
136                let new_entries = self.fetch_backend_picker_entries();
137                if let super::types::GlobalMode::BackendPicker { entries, selected } =
138                    &mut self.ui.global_mode
139                {
140                    *entries = new_entries;
141                    if *selected >= entries.len() {
142                        *selected = entries.len().saturating_sub(1);
143                    }
144                }
145                self.ui.needs_redraw = true;
146            }
147        }
148    }
149
150    pub async fn poll_backend_resolution(&mut self) {
151        if let Some(handle) = &self.pending.backend_resolve_handle
152            && handle.is_finished()
153                && let Some(handle) = self.pending.backend_resolve_handle.take() {
154                    match handle.await {
155                        Ok(Ok(path)) => {
156                            self.add_log(
157                                format!("Backend ready: {}", path.display()),
158                                crate::config::LogLevel::Info,
159                            );
160                        }
161                        Ok(Err(e)) => {
162                            self.add_log(
163                                format!("Backend installation failed: {}", e),
164                                crate::config::LogLevel::Error,
165                            );
166                        }
167                        Err(e) => {
168                            self.add_log(
169                                format!("Backend task panicked: {}", e),
170                                crate::config::LogLevel::Error,
171                            );
172                        }
173                    }
174                    self.pending.backend_resolving = false;
175                    self.ui.needs_redraw = true;
176                }
177    }
178
179    pub fn poll_download_progress(&mut self) {
180        let mut redraw = false;
181        let mut download_logs = Vec::new();
182        if let Some(rx) = &mut self.download.download_rx {
183            while let Ok(state) = rx.try_recv() {
184                if let Some(idx) = self
185                    .download
186                    .download_progress
187                    .iter()
188                    .position(|d| d.model_id == state.model_id && d.filename == state.filename)
189                {
190                    if state.total_bytes > 0 {
191                        let old_pct = (self.download.download_progress[idx].downloaded_bytes as f32
192                            / self.download.download_progress[idx].total_bytes as f32
193                            * 100.0) as u32;
194                        let new_pct = (state.downloaded_bytes as f32 / state.total_bytes as f32
195                            * 100.0) as u32;
196                        if new_pct / 5 > old_pct / 5 && new_pct < 100 {
197                            let speed_mib = state.bytes_per_second / (1024.0 * 1024.0);
198                            let total_mib = state.total_bytes as f64 / (1024.0 * 1024.0);
199                            let name = if state.model_id == "llama-server" {
200                                "backend"
201                            } else {
202                                &state.filename
203                            };
204                            download_logs.push(format!(
205                                "Downloading {}: {}% of {:.1} MiB ({:.2} MiB/s)...",
206                                name, new_pct, total_mib, speed_mib
207                            ));
208                        }
209                    }
210                    self.download.download_progress[idx] = state;
211                } else {
212                    if state.model_id == "llama-server" {
213                        download_logs.push("Starting backend download...".to_string());
214                    } else {
215                        download_logs.push(format!("Starting download: {}...", state.filename));
216                    }
217                    self.download.download_progress.push(state);
218                }
219                redraw = true;
220            }
221        }
222        for log in download_logs {
223            self.add_log(log, crate::config::LogLevel::Info);
224        }
225        if redraw {
226            self.ui.needs_redraw = true;
227        }
228    }
229
230    pub fn poll_bench_tune_progress(&mut self) {
231        if let Some(mut rx) = self.bench_tune.bench_tune_rx.take() {
232            while let Ok(status) = rx.try_recv() {
233                self.bench_tune.bench_tune_progress =
234                    crate::models::BenchTuneProgress::from_status(&status);
235            }
236            self.bench_tune.bench_tune_rx = Some(rx);
237            self.ui.needs_redraw = true;
238        }
239    }
240
241    pub fn process_completed_downloads(&mut self) {
242        let completed: Vec<crate::models::DownloadState> = self
243            .download
244            .download_progress
245            .iter()
246            .filter(|d| {
247                matches!(
248                    d.status,
249                    crate::models::DownloadStatus::Complete
250                        | crate::models::DownloadStatus::Error(_)
251                        | crate::models::DownloadStatus::Cancelled
252                )
253            })
254            .cloned()
255            .collect();
256        if !completed.is_empty() {
257            for state in &completed {
258                match &state.status {
259                    crate::models::DownloadStatus::Complete => {
260                        if state.model_id == "llama-server" {
261                            self.add_log(
262                                "Backend download complete",
263                                crate::config::LogLevel::Info,
264                            );
265                        } else {
266                            self.add_log(
267                                format!("Download complete: {}", state.filename),
268                                crate::config::LogLevel::Info,
269                            );
270                            self.models = Self::discover_models(&self.config.models_dirs);
271                        }
272                    }
273                    crate::models::DownloadStatus::Error(e) => {
274                        let name = if state.model_id == "llama-server" {
275                            "Backend"
276                        } else {
277                            &state.filename
278                        };
279                        self.add_log(
280                            format!("Download failed ({}): {}", name, e),
281                            crate::config::LogLevel::Error,
282                        );
283                    }
284                    crate::models::DownloadStatus::Cancelled => {
285                        let name = if state.model_id == "llama-server" {
286                            "Backend"
287                        } else {
288                            &state.filename
289                        };
290                        self.add_log(
291                            format!("Download cancelled: {}", name),
292                            crate::config::LogLevel::Info,
293                        );
294                        if let Some(ref dest) = state.dest
295                            && dest.exists()
296                                && let Err(e) = std::fs::remove_file(dest) {
297                                    self.add_log(
298                                        format!(
299                                            "Failed to remove temp file {}: {}",
300                                            dest.display(),
301                                            e
302                                        ),
303                                        crate::config::LogLevel::Warning,
304                                    );
305                                }
306                    }
307                    _ => {}
308                }
309            }
310            self.download.download_progress.retain(|d| {
311                !matches!(
312                    d.status,
313                    crate::models::DownloadStatus::Complete
314                        | crate::models::DownloadStatus::Error(_)
315                        | crate::models::DownloadStatus::Cancelled
316                )
317            });
318              self.download.downloading = !self.download.download_progress.is_empty();
319            if !self.download.downloading {
320                self.download.download_scroll_state.select(None);
321            } else if let Some(idx) = self.download.download_scroll_state.selected()
322                && idx >= self.download.download_progress.len()
323            {
324                self.download
325                    .download_scroll_state
326                    .select(Some(self.download.download_progress.len() - 1));
327            }
328            self.ui.needs_redraw = true;
329        }
330    }
331
332    pub fn poll_server_logs(&mut self) {
333        let mut server_logs = Vec::new();
334        if let Some(rx) = &mut self.server.server_log_rx {
335            while let Ok(line) = rx.try_recv() {
336                if line.contains("n_tokens =")
337                    && let Some(tokens_part) = line.split("n_tokens =").last()
338                {
339                    let val_str = tokens_part.split(',').next().unwrap_or(tokens_part).trim();
340                    if let Ok(tokens) = val_str.parse::<u32>() {
341                        self.metrics.ctx_used = tokens;
342                    }
343                }
344                if line.contains("n_decoded =")
345                    && let Some(decoded_part) = line.split("n_decoded =").last()
346                {
347                    let val_str = decoded_part.split(',').next().unwrap_or(decoded_part).trim();
348                    if let Ok(tokens) = val_str.parse::<u64>() {
349                        self.metrics.decoded_tokens = tokens;
350                    }
351                }
352                if line.contains("tg =")
353                    && let Some(tg_part) = line.split("tg =").last()
354                {
355                    let val_str = tg_part.trim().split(' ').next().unwrap_or(tg_part).trim();
356                    if let Ok(tg) = val_str.parse::<f64>() {
357                        self.metrics.gen_tps = tg;
358                    }
359                }
360                server_logs.push(line);
361                if server_logs.len() > 100 {
362                    break;
363                }
364            }
365        }
366        if !server_logs.is_empty() {
367            for line in server_logs {
368                self.add_log(line, crate::config::LogLevel::Info);
369            }
370            self.ui.needs_redraw = true;
371        }
372    }
373
374    pub fn poll_sync(&mut self) {
375        let mut sync_updated = false;
376        if let Some(rx) = &mut self.server.sync_rx {
377            while let Ok(models) = rx.try_recv() {
378                if let Some(handle) = &self.server.server_handle {
379                    let port = handle.port;
380                    let pid = handle.pid;
381                    for (id, status, path) in models {
382                        let status_lower = status.to_lowercase();
383                        let is_active = status_lower == "loaded"
384                            || status_lower == "loading"
385                            || status_lower == "ready";
386                        let mut matched = false;
387                        for model in &self.models {
388                            let path_match = path
389                                .as_ref()
390                                .map(|p| p == &model.path.to_string_lossy())
391                                .unwrap_or(false);
392                            let id_match = id == model.display_name || id == model.name;
393                            let filename_match = path
394                                .as_ref()
395                                .and_then(|p| {
396                                    std::path::Path::new(p)
397                                        .file_name()
398                                        .map(|f| f.to_string_lossy().to_string())
399                                })
400                                .map(|f| f == model.name)
401                                .unwrap_or(false);
402                            let id_filename_match = std::path::Path::new(&id)
403                                .file_name()
404                                .map(|f| f.to_string_lossy().to_string())
405                                .map(|f| f == model.name || f == model.display_name)
406                                .unwrap_or(false);
407                            if path_match || id_match || filename_match || id_filename_match {
408                                if is_active {
409                                    if status_lower == "loading" {
410                                        self.model_states.insert(
411                                            model.display_name.clone(),
412                                            crate::models::ModelState::Loading,
413                                        );
414                                    } else {
415                                        let mut loaded_names =
416                                            self.server.loaded_model_names.lock().unwrap();
417                                        if !loaded_names.contains(&model.display_name) {
418                                            loaded_names.push(model.display_name.clone());
419                                        }
420                                        self.model_states.insert(
421                                            model.display_name.clone(),
422                                            crate::models::ModelState::Loaded { port, pid },
423                                        );
424                                    }
425                                }
426                                matched = true;
427                            }
428                        }
429                        if !matched {
430                            let possible_names = vec![id.clone(), format!("{}.gguf", id)];
431                            for name in possible_names {
432                                for model in &self.models {
433                                    if model.display_name == name || model.name == name {
434                                        if is_active {
435                                            let mut loaded_names =
436                                                self.server.loaded_model_names.lock().unwrap();
437                                            if !loaded_names.contains(&model.display_name) {
438                                                loaded_names.push(model.display_name.clone());
439                                            }
440                                            self.model_states.insert(
441                                                model.display_name.clone(),
442                                                crate::models::ModelState::Loaded { port, pid },
443                                            );
444                                        }
445                                        matched = true;
446                                        break;
447                                    }
448                                }
449                                if matched {
450                                    break;
451                                }
452                            }
453                        }
454                    }
455                    sync_updated = true;
456                }
457            }
458        }
459        if sync_updated {
460            self.ui.needs_redraw = true;
461        }
462    }
463
464    pub fn poll_metrics(&mut self) {
465        if let Some(rx) = &mut self.server.metrics_rx {
466            while let Ok(mut m) = rx.try_recv() {
467                // ctx_max uses the effective context length (context_length * rope_scale).
468                if self.server.spawned_context_length > 0 {
469                    m.ctx_max = self.server.spawned_context_length;
470                }
471
472                // Only carry over values that are technically "stateful" or slow to update
473                // but don't force them to stick if the API is clearly reporting something else.
474                if m.gpu_mem_used == 0 && self.metrics.gpu_mem_used > 0 {
475                    m.gpu_mem_used = self.metrics.gpu_mem_used;
476                    if m.gpu_mem_total == 0 {
477                        m.gpu_mem_total = self.metrics.gpu_mem_total;
478                    }
479                }
480
481                if m.tps > 0.0 {
482                    m.latency_per_token_ms = 1000.0 / m.tps;
483                }
484                if m.prompt_tps > 0.0 {
485                    m.prompt_latency_ms = 1000.0 / m.prompt_tps;
486                }
487
488                // If log parsing gave us a value but API didn't (or hasn't yet), use the log value.
489                if m.ctx_used == 0 && self.metrics.ctx_used > 0 {
490                    m.ctx_used = self.metrics.ctx_used;
491                }
492                if m.decoded_tokens == 0 && self.metrics.decoded_tokens > 0 {
493                    m.decoded_tokens = self.metrics.decoded_tokens;
494                }
495                if m.gen_tps == 0.0 && self.metrics.gen_tps > 0.0 {
496                    m.gen_tps = self.metrics.gen_tps;
497                }
498                if m.cpu_usage == 0.0 && self.metrics.cpu_usage > 0.0 {
499                    m.cpu_usage = self.metrics.cpu_usage;
500                }
501
502                // Only redraw if metrics actually changed
503                let any_changed = m.loaded != self.metrics.loaded
504                    || m.tps != self.metrics.tps
505                    || m.prompt_tps != self.metrics.prompt_tps
506                    || (m.cpu_usage - self.metrics.cpu_usage).abs() > 0.01
507                    || m.gpu_mem_used != self.metrics.gpu_mem_used
508                    || m.ram_used != self.metrics.ram_used
509                    || m.ctx_used != self.metrics.ctx_used
510                    || m.decoded_tokens != self.metrics.decoded_tokens
511                    || (m.gen_tps - self.metrics.gen_tps).abs() > 0.01;
512
513                self.metrics = m;
514
515                if any_changed {
516                    self.ui.needs_redraw = true;
517                }
518            }
519        }
520    }
521
522    pub async fn poll_loading_completion(&mut self) {
523        use super::types::LoadingPhase;
524
525        if self
526            .loading
527            .loading_phases
528            .contains(&LoadingPhase::Complete)
529        {
530            return;
531        }
532
533        if !self
534            .loading
535            .loading_phases
536            .contains(&LoadingPhase::ServerListening)
537        {
538            return;
539        }
540
541        if self.loading.health_poll_handle.is_some() {
542            if let Some(rx) = &mut self.loading.loading_completion_rx {
543                let mut got_completion = false;
544                while let Ok(()) = rx.try_recv() {
545                    got_completion = true;
546                }
547                if got_completion {
548                    // Clear all previous loading phases (Starting, Meta, Tensors) once complete
549                    self.loading.loading_phases.clear();
550                    self.loading.loading_phases.insert(LoadingPhase::Complete);
551                    self.loading.last_active_phase = Some(LoadingPhase::Complete);
552                    self.loading.loading_progress = 1.0;
553                    if let Some(h) = self.loading.health_poll_handle.take() {
554                        h.abort();
555                    }
556                    self.loading.loading_completion_rx = None;
557                    self.server.spawned_model_state = Some("loaded".to_string());
558                    self.loading.progress_target = 1.0;
559                    self.ui.needs_full_redraw = true;
560                    self.ui.needs_redraw = true;
561
562                    if let Some(handle) = &self.server.server_handle {
563                        let port = handle.port;
564                        let pid = handle.pid;
565
566                        // Cleanup stale "Loading" entries (like "llama-server") before updating
567                        let to_update: Vec<String> = self
568                            .model_states
569                            .iter()
570                            .filter(|(_, s)| matches!(s, crate::models::ModelState::Loading))
571                            .map(|(n, _)| n.clone())
572                            .collect();
573
574                        // Explicitly remove all Loading entries first to ensure no duplicates or stale names persist
575                        self.model_states
576                            .retain(|_, s| !matches!(s, crate::models::ModelState::Loading));
577
578                        for name in to_update {
579                            // If it's a real model name (not a generic server process name), mark as Loaded
580                            if name != "llama-server" && name != "Router" {
581                                self.model_states.insert(
582                                    name.clone(),
583                                    crate::models::ModelState::Loaded { port, pid },
584                                );
585                                let mut loaded = self
586                                    .server
587                                    .loaded_model_names
588                                    .lock()
589                                    .unwrap_or_else(|e| e.into_inner());
590                                if !loaded.contains(&name) {
591                                    loaded.push(name);
592                                }
593                            }
594                        }
595                    }
596
597                    self.metrics.ctx_used = 0;
598                }
599            }
600            return;
601        }
602
603        if let Some(handle) = &self.server.server_handle {
604            let host = handle.host.clone();
605            let port = handle.port;
606            let (tx, rx) = tokio::sync::mpsc::channel(1);
607            self.loading.loading_completion_rx = Some(rx);
608
609            // Abort any previous health poll task to prevent leaked tasks
610            if let Some(prev) = self.loading.health_poll_handle.take() {
611                prev.abort();
612            }
613
614            let handle = tokio::spawn(async move {
615                let client = reqwest::Client::new();
616                let url = format!("http://{}:{}/health", host, port);
617
618                loop {
619                    tokio::time::sleep(std::time::Duration::from_millis(500)).await;
620                    match client.get(&url).send().await {
621                        Ok(resp) if resp.status().is_success() => {
622                            if let Ok(json) = resp.json::<serde_json::Value>().await {
623                                let status_ok =
624                                    json.get("status").and_then(|v| v.as_str()) == Some("ok");
625                                let slots_ready = json
626                                    .get("slots")
627                                    .and_then(|v| v.as_array())
628                                    .map(|a| !a.is_empty())
629                                    .unwrap_or(false);
630
631                                if slots_ready || status_ok {
632                                    let _ = tx.send(()).await;
633                                    return;
634                                }
635                            }
636                        }
637                        _ => {}
638                    }
639                }
640            });
641            self.loading.health_poll_handle = Some(handle);
642        }
643    }
644
645    pub async fn start_pending_spawn(&mut self) {
646        if let Some((model_opt, settings)) = self.pending.pending_spawn.take() {
647            let (tx, rx) = tokio::sync::mpsc::channel(100);
648            self.server.server_log_rx = Some(rx);
649            let (exit_tx, exit_rx) = tokio::sync::mpsc::channel(1);
650            self.server.server_exit_tx = Some(exit_tx.clone());
651            self.server.server_exit_rx = Some(exit_rx);
652            let config_clone = self.config.clone();
653            let model_clone = model_opt.clone();
654            let settings_clone = settings.clone();
655            let tx_clone = tx.clone();
656            let server_mode_clone = self.server_mode;
657            let router_max_models_clone = self.router_max_models;
658            let download_tx_clone = Some(self.ensure_download_channel());
659            let display_name = model_opt
660                .as_ref()
661                .map(|m| m.display_name.clone())
662                .unwrap_or_else(|| "Router".to_string());
663            if let Some(m) = &model_opt {
664                let state = if server_mode_clone == crate::models::ServerMode::Bench
665                    || server_mode_clone == crate::models::ServerMode::BenchTune
666                {
667                    crate::models::ModelState::Benchmarking
668                } else {
669                    crate::models::ModelState::Loading
670                };
671                self.model_states.insert(m.display_name.clone(), state);
672            }
673            self.add_log(
674                format!("Loading {}...", display_name),
675                crate::config::LogLevel::Info,
676            );
677            if server_mode_clone == crate::models::ServerMode::BenchTune {
678                let model = match model_opt {
679                    Some(m) => m,
680                    None => {
681                        self.add_log(
682                            "Error: Benchmark tuning requires a selected model.",
683                            crate::config::LogLevel::Error,
684                        );
685                        return;
686                    }
687                };
688                let bench_tune_config =
689                    self.bench_tune.bench_tune_config.take().unwrap_or_else(|| {
690                        crate::models::BenchTuneConfig::new(
691                            model.path.clone(),
692                            3,
693                            crate::models::BENCHMARK_PROMPT.to_string(),
694                        )
695                    });
696                let (tx_tune, rx_tune) = tokio::sync::mpsc::channel(100);
697                self.bench_tune.bench_tune_tx = Some(tx_tune.clone());
698                self.bench_tune.bench_tune_config = Some(bench_tune_config.clone());
699                self.bench_tune.bench_tune_running = true;
700                self.bench_tune.bench_tune_results.clear();
701                self.bench_tune.bench_tune_result_row = 0;
702                self.models_mode = super::types::ModelsMode::BenchTune;
703
704                // Create cancellation channel for benchmark tuning
705                let (cancel_tx, mut cancel_rx) = tokio::sync::watch::channel(false);
706                self.bench_tune.bench_tune_cancel_tx = Some(cancel_tx);
707
708                let bench_tune_config_clone = bench_tune_config.clone();
709                let settings_clone = settings_clone.clone();
710                let model_clone = model.clone();
711                let tx_tune_clone = tx_tune.clone();
712                let spawn_log_tx_clone = tx.clone();
713                let handle = tokio::spawn(async move {
714                    let results = crate::backend::benchmark::run_bench_tune(
715                        crate::backend::benchmark::BenchTuneRequest {
716                            main_config: &config_clone,
717                            config: &bench_tune_config_clone,
718                            model: &model_clone,
719                            settings: &settings_clone,
720                            progress_tx: tx_tune_clone,
721                            log_tx: spawn_log_tx_clone,
722                            cancel_rx: &mut cancel_rx,
723                        },
724                    )
725                    .await
726                    .map_err(|e| e.to_string());
727                    (results, display_name, bench_tune_config_clone)
728                });
729                self.server.bench_tune_task_handle = Some(handle);
730                self.server.spawn_log_tx = Some(tx);
731                self.bench_tune.bench_tune_rx = Some(rx_tune);
732            } else {
733                let settings_for_result = settings_clone.clone();
734                let exit_tx_clone = exit_tx.clone();
735                let handle = tokio::spawn(async move {
736                    crate::backend::server::spawn_server(crate::backend::server::SpawnServerRequest {
737                        config: &config_clone,
738                        model: model_clone.as_ref(),
739                        settings: &settings_clone,
740                        log_tx: tx_clone,
741                        progress_tx: download_tx_clone,
742                        server_mode: server_mode_clone,
743                        router_max_models: router_max_models_clone,
744                        exit_tx: exit_tx_clone,
745                    })
746                    .await
747                    .map(|(handle, cmd)| (display_name, handle, cmd, settings_for_result))
748                });
749                self.server.spawn_task_handle = Some(handle);
750                self.server.spawn_log_tx = Some(tx);
751                self.ui.needs_redraw = true;
752            }
753        }
754    }
755
756    pub async fn poll_spawn_result(&mut self) {
757        if let Some(handle) = &self.server.spawn_task_handle
758            && handle.is_finished()
759            && let Some(handle) = self.server.spawn_task_handle.take()
760        {
761            match handle.await {
762                Ok(Ok((server_display_name, server_handle, cmd, spawned_settings))) => {
763                    let port = server_handle.port;
764                    let pid = server_handle.pid;
765                    let host = server_handle.host.clone();
766                    self.add_log(
767                        format!("Server started on port {port} (pid={pid})"),
768                        crate::config::LogLevel::Info,
769                    );
770                    self.server.server_handle = Some(server_handle);
771                    self.server.cmd_display = Some(cmd);
772                    self.server.spawned_settings = Some(spawned_settings.clone());
773                    self.server.spawned_model_name = Some(server_display_name.clone());
774                    self.server.spawned_model_state = Some("loading".to_string());
775                    self.server.spawned_context_length = (spawned_settings.context_length as f32
776                        * spawned_settings.rope_scale)
777                        as u32;
778                    // API endpoint (proxy) is managed by update_api_endpoint(), which
779                    // runs every loop iteration and (re)starts the proxy as needed
780                    // when a new model becomes available.
781                    self.loading.loading_phases =
782                        std::iter::once(super::types::LoadingPhase::ServerListening).collect();
783                    self.loading.last_active_phase =
784                        Some(super::types::LoadingPhase::ServerListening);
785                    self.server.spawned_model_state = Some("loading".to_string());
786                    self.loading.progress_target = 1.0;
787                    let (metrics_tx, metrics_rx) = tokio::sync::mpsc::channel(10);
788                    self.server.metrics_rx = Some(metrics_rx);
789                    let task_host = host.clone();
790                    let task_port = port;
791                    let task_pid = pid;
792                    let metrics_model_name = self.server.metrics_model_name.clone();
793                    self.add_log("Starting metrics polling...", crate::config::LogLevel::Info);
794                    let _task_handle = tokio::spawn(Self::metrics_polling_task(
795                        task_host,
796                        task_port,
797                        task_pid,
798                        metrics_model_name,
799                        metrics_tx,
800                    ));
801                    self.server.metrics_task_handle = Some(_task_handle);
802                    let sync_host = host.clone();
803                    let sync_port = port;
804                    let (sync_tx, sync_rx) = tokio::sync::mpsc::channel(1);
805                    let _sync_task_handle =
806                        tokio::spawn(Self::sync_polling_task(sync_host, sync_port, sync_tx));
807                    self.server.sync_rx = Some(sync_rx);
808                    self.server.sync_task_handle = Some(_sync_task_handle);
809                    self.ui.needs_redraw = true;
810                }
811                Ok(Err(e)) => {
812                    self.loading.progress_target = 1.0;
813                    self.add_log(
814                        format!("ERROR: Server failed: {}", e),
815                        crate::config::LogLevel::Error,
816                    );
817                    if let Some(mut rx) = self.server.server_log_rx.take() {
818                        while let Ok(line) = rx.try_recv() {
819                            self.add_log(line, crate::config::LogLevel::Info);
820                        }
821                    }
822                    self.ui.last_error_message = Some(e);
823                    self.reset_loading_state(true);
824                    self.ui.needs_redraw = true;
825                }
826                Err(e) => {
827                    self.loading.progress_target = 1.0;
828                    self.add_log(
829                        format!("ERROR: Spawn task panicked: {}", e),
830                        crate::config::LogLevel::Error,
831                    );
832                    self.ui.needs_redraw = true;
833                }
834            }
835        }
836    }
837
838    async fn metrics_polling_task(
839        host: String,
840        port: u16,
841        pid: u32,
842        metrics_model_name: Arc<std::sync::Mutex<Option<String>>>,
843        metrics_tx: tokio::sync::mpsc::Sender<crate::models::ServerMetrics>,
844    ) {
845        let mut consecutive_failures: u32 = 0;
846        let max_failures: u32 = 15;
847        let mut prev_model_name: Option<String> = None;
848        loop {
849            let mut m = match crate::backend::server::get_metrics(&host, port, None, Some(pid))
850                .await
851            {
852                Ok(metrics) => {
853                    consecutive_failures = 0;
854                    metrics
855                }
856                Err(_) => {
857                    consecutive_failures += 1;
858                    if consecutive_failures >= max_failures {
859                        tracing::warn!(
860                            "Metrics polling aborted after {} consecutive failures (server likely dead)",
861                            max_failures
862                        );
863                        break;
864                    }
865                    if consecutive_failures % 5 == 1 {
866                        tracing::warn!(
867                            "Metrics polling: server unreachable (attempt {}/{})",
868                            consecutive_failures,
869                            max_failures
870                        );
871                    }
872                    tokio::time::sleep(std::time::Duration::from_secs(2)).await;
873                    continue;
874                }
875            };
876            m.total_vram_used = m.gpu_mem_used;
877            let current_model = {
878                let lock = metrics_model_name.lock().unwrap();
879                lock.clone()
880            };
881            if let Some(name) = current_model
882                && let Ok(model_metrics) =
883                    crate::backend::server::get_metrics(&host, port, Some(&name), Some(pid)).await
884            {
885                let stotal = m.gpu_mem_total;
886                let should_use_model_vram = if stotal > 0 {
887                    model_metrics.gpu_mem_used >= stotal / 4
888                } else {
889                    true
890                };
891                // Reset ctx_used when model changes to avoid showing stale cumulative values.
892                if prev_model_name.as_deref() != Some(&name) {
893                    prev_model_name = Some(name.clone());
894                    m.ctx_used = 0;
895                }
896                m.ctx_used = model_metrics.ctx_used;
897                if model_metrics.ctx_max > 0 {
898                    m.ctx_max = model_metrics.ctx_max;
899                }
900                if model_metrics.tps > 0.0 {
901                    m.tps = model_metrics.tps;
902                }
903                if should_use_model_vram {
904                    m.gpu_mem_used = model_metrics.gpu_mem_used;
905                }
906            }
907            if metrics_tx.send(m).await.is_err() {
908                break;
909            }
910            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
911        }
912    }
913
914    async fn sync_polling_task(
915        host: String,
916        port: u16,
917        sync_tx: tokio::sync::mpsc::Sender<Vec<(String, String, Option<String>)>>,
918    ) {
919        loop {
920            if let Ok(models) = crate::backend::server::list_models(&host, port).await
921                && sync_tx.send(models).await.is_err()
922            {
923                break;
924            }
925            tokio::time::sleep(std::time::Duration::from_secs(10)).await;
926        }
927    }
928
929    pub async fn poll_bench_tune_result(&mut self) {
930        if let Some(handle) = &self.server.bench_tune_task_handle
931            && handle.is_finished()
932            && let Some(handle) = self.server.bench_tune_task_handle.take()
933        {
934            match handle.await {
935                Ok((results, display_name, bench_config)) => match results {
936                    Ok(bench_results) => {
937                        self.add_log(
938                            format!(
939                                "Benchmark tuning completed for {} with {} results",
940                                display_name,
941                                bench_results.len()
942                            ),
943                            crate::config::LogLevel::Info,
944                        );
945                        if bench_results.is_empty() {
946                            self.add_log("No successful benchmark results were obtained. Check the Log (F6) for details on test failures.", crate::config::LogLevel::Warning);
947                        } else {
948                            let output_dir = crate::config::Config::config_path()
949                                .parent()
950                                .unwrap()
951                                .join("benchmarks");
952                            match crate::backend::benchmark::save_results(
953                                &bench_results,
954                                &output_dir,
955                                &bench_config,
956                            )
957                            .await
958                            {
959                                Ok(()) => self.add_log(
960                                    format!("Results saved to {}/", output_dir.display()),
961                                    crate::config::LogLevel::Info,
962                                ),
963                                Err(e) => self.add_log(
964                                    format!("Failed to save benchmark results: {}", e),
965                                    crate::config::LogLevel::Error,
966                                ),
967                            }
968                        }
969                        let mut sorted_results = bench_results;
970                        sorted_results.sort_by(|a, b| {
971                            b.metrics
972                                .generation_tps
973                                .partial_cmp(&a.metrics.generation_tps)
974                                .unwrap_or(std::cmp::Ordering::Equal)
975                        });
976                        self.bench_tune.bench_tune_results = sorted_results;
977                        self.bench_tune.bench_tune_running = false;
978
979                        let model_display_name = self
980                            .selected_model()
981                            .map(|m| m.display_name.clone());
982
983                        if let Some(model_display_name) = model_display_name {
984                            self.model_states
985                                .insert(model_display_name.clone(), crate::models::ModelState::Available);
986                        }
987
988                        if let Some(handle) = &self.server.server_handle {
989                            if let Some(model) = self.selected_model() {
990                                let host = handle.host.clone();
991                                let port = handle.port;
992                                let model_name = model.display_name.clone();
993                                let model_path_str = model.path.to_str().map(|s| s.to_string());
994                                let task_name = format!("bench_unload_{}", model.display_name);
995                                let task_handle = tokio::spawn(async move {
996                                    let _ = crate::backend::server::unload_model(
997                                        &host,
998                                        port,
999                                        &model_name,
1000                                        model_path_str.as_deref(),
1001                                    )
1002                                    .await;
1003                                });
1004                                self.background_tasks.insert(task_name, task_handle);
1005                            }
1006                        }
1007
1008                        self.ui.needs_redraw = true;
1009                    }
1010                    Err(e) => {
1011                        self.add_log(
1012                            format!("Benchmark tuning failed: {}", e),
1013                            crate::config::LogLevel::Error,
1014                        );
1015                        self.bench_tune.bench_tune_running = false;
1016                        if let Some(model) = self.selected_model() {
1017                            self.model_states.insert(
1018                                model.display_name.clone(),
1019                                crate::models::ModelState::Failed {
1020                                    error: e.to_string(),
1021                                },
1022                            );
1023                        }
1024                        self.ui.needs_redraw = true;
1025                    }
1026                },
1027                Err(e) => {
1028                    self.add_log(
1029                        format!("Benchmark task panicked: {:?}", e),
1030                        crate::config::LogLevel::Error,
1031                    );
1032                    self.bench_tune.bench_tune_running = false;
1033                    self.ui.needs_redraw = true;
1034                }
1035            }
1036        }
1037    }
1038
1039    pub fn handle_pending_api_load(&mut self) {
1040        if let Some((model_name, model_path)) = self.pending.pending_api_load.clone() {
1041            if let Some(handle) = &self.server.server_handle {
1042                if self
1043                    .loading
1044                    .loading_phases
1045                    .contains(&super::types::LoadingPhase::Complete)
1046                    || self
1047                        .loading
1048                        .loading_phases
1049                        .contains(&super::types::LoadingPhase::ServerListening)
1050                {
1051                    let host = handle.host.clone();
1052                    let port = handle.port;
1053                    let model_name_clone = model_name.clone();
1054                    let model_path_clone = model_path.clone();
1055                    self.pending.pending_api_load = None;
1056                    self.add_log(
1057                        format!("Sending load request for {}...", model_name_clone),
1058                        crate::config::LogLevel::Info,
1059                    );
1060                    {
1061                        let mut lock = self.server.metrics_model_name.lock().unwrap();
1062                        *lock = Some(model_name_clone.clone());
1063                    }
1064                    let log_tx = self.server.spawn_log_tx.clone();
1065                    let model_name_err = model_name_clone.clone();
1066                    self.metrics.ctx_used = 0;
1067                    tokio::spawn(async move {
1068                        if let Err(e) = crate::backend::server::load_model(
1069                            &host,
1070                            port,
1071                            &model_name_clone,
1072                            model_path_clone.as_deref(),
1073                        )
1074                        .await
1075                        {
1076                            let err_msg =
1077                                format!("ERROR: Failed to load model {}: {}", model_name_err, e);
1078                            if let Some(tx) = log_tx {
1079                                let _ = tx.send(err_msg.clone()).await;
1080                            } else {
1081                                tracing::error!("{}", err_msg);
1082                            }
1083                        }
1084                    });
1085                    self.model_states
1086                        .insert(model_name, crate::models::ModelState::Loading);
1087                    self.ui.needs_redraw = true;
1088                }
1089            } else if self.server.spawn_task_handle.is_none()
1090                && self.pending.pending_spawn.is_none()
1091            {
1092                self.pending.pending_api_load = None;
1093            }
1094        }
1095    }
1096
1097    pub fn handle_pending_api_unload(&mut self) {
1098        if !matches!(
1099            self.ui.global_mode,
1100            super::types::GlobalMode::Confirmation { .. }
1101        )
1102            && let Some((model_name, model_path)) = self.pending.pending_api_unload.take()
1103                && let Some(handle) = &self.server.server_handle
1104            {
1105                let server_mode = self.server_mode;
1106                let handle_clone = handle.clone();
1107                {
1108                    let mut lock = self.server.metrics_model_name.lock().unwrap();
1109                    if lock.as_deref() == Some(&model_name) {
1110                        *lock = None;
1111                    }
1112                }
1113                let host = handle.host.clone();
1114                let port = handle.port;
1115                let model_name_clone = model_name.clone();
1116                let model_path_clone = model_path.clone();
1117                if server_mode == crate::models::ServerMode::Normal {
1118                    self.add_log(
1119                        format!("Unloading {} (killing server)...", model_name_clone),
1120                        crate::config::LogLevel::Info,
1121                    );
1122                    self.pending.pending_kill = Some(handle_clone);
1123                } else {
1124                    self.add_log(
1125                        format!("Sending unload request for {}...", model_name_clone),
1126                        crate::config::LogLevel::Info,
1127                    );
1128                    let kill_tx = self.server.spawn_log_tx.clone();
1129                    let kill_tx2 = kill_tx.clone();
1130                    let server_clone = self.server.server_handle.clone();
1131                    let host_clone = host.clone();
1132                    let port_clone = port;
1133                    let model_name_task = model_name_clone.clone();
1134                    let loaded_names_clone = self.server.loaded_model_names.clone();
1135                    self.background_tasks.insert(
1136                        format!("api_unload_{}", model_name_task),
1137                        tokio::spawn(async move {
1138                            if let Err(e) = crate::backend::server::unload_model(
1139                                &host,
1140                                port,
1141                                &model_name_clone,
1142                                model_path_clone.as_deref(),
1143                            )
1144                            .await
1145                            {
1146                                if let Some(tx) = kill_tx {
1147                                    let _ = tx
1148                                        .send(format!("Failed to unload model via API: {}", e))
1149                                        .await;
1150                                }
1151                                return;
1152                            }
1153                            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
1154
1155                            let mut should_stop = false;
1156
1157                            if let Ok(loaded) =
1158                                crate::backend::server::list_models(&host_clone, port_clone).await
1159                            {
1160                                if loaded.is_empty() {
1161                                    should_stop = true;
1162                                } else {
1163                                    if let Some(tx) = kill_tx.clone() {
1164                                        let _ = tx
1165                                            .send(format!(
1166                                                "{} models still loaded on server",
1167                                                loaded.len()
1168                                            ))
1169                                            .await;
1170                                    }
1171                                }
1172                            }
1173
1174                            if !should_stop {
1175                                let loaded_names =
1176                                    loaded_names_clone.lock().unwrap_or_else(|e| e.into_inner());
1177                                if loaded_names.is_empty() {
1178                                    should_stop = true;
1179                                }
1180                            }
1181
1182                            if should_stop {
1183                                if let Some(tx) = kill_tx {
1184                                    let _ = tx
1185                                        .send("No models left, stopping router...".to_string())
1186                                        .await;
1187                                }
1188                                if let Some(server) = server_clone {
1189                                    let _ = crate::backend::server::kill_server(server).await;
1190                                    if let Some(tx) = kill_tx2 {
1191                                        let _ = tx.send("Server stopped".to_string()).await;
1192                                    }
1193                                }
1194                            }
1195                        }),
1196                    );
1197                }
1198                self.server
1199                    .loaded_model_names
1200                    .lock()
1201                    .unwrap()
1202                    .retain(|n| n != &model_name);
1203                self.metrics.ctx_used = 0;
1204                self.model_states
1205                    .insert(model_name, crate::models::ModelState::Available);
1206                self.ui.needs_redraw = true;
1207            }
1208    }
1209
1210    pub async fn start_pending_kill(&mut self) {
1211        if let Some(handle) = self.pending.pending_kill.take() {
1212            match crate::backend::server::kill_server(handle).await {
1213                Ok(()) => {
1214                    self.add_log("Server stopped", crate::config::LogLevel::Info);
1215                    self.server.server_handle = None;
1216                    self.server.metrics_rx = None;
1217                    self.metrics = Default::default();
1218                    if let Some(task) = self.server.metrics_task_handle.take() {
1219                        task.abort();
1220                    }
1221                    if let Some(task) = self.server.sync_task_handle.take() {
1222                        task.abort();
1223                    }
1224                    self.server.sync_rx = None;
1225                    if let Some(tx) = self.server.api_shutdown_tx.take() {
1226                        let _ = tx.send(true);
1227                    }
1228                    if let Some(proxy) = self.server.api_proxy_handle.take() {
1229                        proxy.abort();
1230                    }
1231                    let mut names_to_reset = Vec::new();
1232                    for (name, state) in &self.model_states {
1233                        if !matches!(state, crate::models::ModelState::Available)
1234                            && !matches!(state, crate::models::ModelState::Failed { .. })
1235                        {
1236                            names_to_reset.push(name.clone());
1237                        }
1238                    }
1239                    for name in names_to_reset {
1240                        let n: String = name.clone();
1241                        self.model_states
1242                            .insert(n, crate::models::ModelState::Available);
1243                    }
1244                    self.server.loaded_model_names.lock().unwrap().clear();
1245                    self.loading.loading_phases = std::collections::HashSet::new();
1246                    self.loading.loading_progress = 0.0;
1247                    self.loading.progress_target = 0.0;
1248                    self.ui.needs_full_redraw = true;
1249                    self.ui.needs_redraw = true;
1250                }
1251                Err(e) => {
1252                    self.add_log(
1253                        format!("Failed to stop server: {}", e),
1254                        crate::config::LogLevel::Error,
1255                    );
1256                }
1257            }
1258        }
1259    }
1260
1261    pub async fn handle_pending_search(&mut self) {
1262        if self.search.search_loading {
1263            if let Some((query, offset)) = self.search.pending_search_load.take() {
1264                let is_append = offset > 0;
1265                let query_clone = query.clone();
1266                let offset_clone = offset;
1267                let search_limit = self.config.search_limit;
1268                self.add_log(
1269                    format!(
1270                        "Searching with limit={} offset={}...",
1271                        search_limit, offset_clone
1272                    ),
1273                    crate::config::LogLevel::Info,
1274                );
1275                let search_handle = tokio::spawn(async move {
1276                    crate::backend::hub::search_models(&query_clone, search_limit, offset_clone)
1277                        .await
1278                });
1279                match search_handle.await {
1280                    Ok(Ok((res, _, raw_ids))) => {
1281                        let query_str = &query;
1282                        let mut buf =
1283                            format!("Search complete: {} results for '{}'", res.len(), query_str);
1284                        buf.push_str(&format!("\n  RAW API returned: {}", raw_ids.join(", ")));
1285                        for r in &res {
1286                            let gguf_tags: Vec<String> = r
1287                                .tags
1288                                .iter()
1289                                .filter(|t| t.starts_with("gguf:"))
1290                                .cloned()
1291                                .collect();
1292                            buf.push_str(&format!(
1293                                "\n  {} quant={} tags={} params={} cap={} ctx={}",
1294                                r.model_id,
1295                                r.quantization.as_deref().unwrap_or("-"),
1296                                gguf_tags.join(","),
1297                                r.parameters.as_deref().unwrap_or("none"),
1298                                r.capabilities.join(","),
1299                                r.context_length.unwrap_or(0)
1300                            ));
1301                        }
1302                        let raw_len = raw_ids.len();
1303                        if is_append {
1304                            if let super::types::ModelsMode::Search {
1305                                results,
1306                                has_more,
1307                                loading,
1308                                ..
1309                            } = &mut self.models_mode
1310                            {
1311                                let models = self.models.clone();
1312                                for r in res {
1313                                    let downloaded =
1314                                        super::sync_ops::model_is_downloaded(&models, &r.model_id);
1315                                    results.push(crate::models::SearchResult { downloaded, ..r });
1316                                }
1317                                if raw_len < self.config.search_limit as usize {
1318                                    *has_more = false;
1319                                }
1320                                *loading = false;
1321                            }
1322                        } else {
1323                            if let super::types::ModelsMode::Search {
1324                                results,
1325                                loading,
1326                                has_more,
1327                                ..
1328                            } = &mut self.models_mode
1329                            {
1330                                let models = self.models.clone();
1331                                *results = res
1332                                    .into_iter()
1333                                    .map(|r| {
1334                                        let downloaded = super::sync_ops::model_is_downloaded(
1335                                            &models,
1336                                            &r.model_id,
1337                                        );
1338                                        crate::models::SearchResult { downloaded, ..r }
1339                                    })
1340                                    .collect();
1341                                if !results.is_empty() {
1342                                    self.search.search_results_idx = Some(0);
1343                                } else {
1344                                    self.search.search_results_idx = None;
1345                                }
1346                                *has_more = raw_len >= self.config.search_limit as usize;
1347                                *loading = false;
1348                            }
1349                        }
1350                        self.add_log(buf, crate::config::LogLevel::Info);
1351                    }
1352                    Ok(Err(e)) => {
1353                        self.add_log(
1354                            format!("Search failed: {}", e),
1355                            crate::config::LogLevel::Error,
1356                        );
1357                        if let super::types::ModelsMode::Search { loading, .. } =
1358                            &mut self.models_mode
1359                        {
1360                            *loading = false;
1361                        }
1362                    }
1363                    Err(e) => {
1364                        self.add_log(
1365                            format!("Search task error: {}", e),
1366                            crate::config::LogLevel::Error,
1367                        );
1368                        if let super::types::ModelsMode::Search { loading, .. } =
1369                            &mut self.models_mode
1370                        {
1371                            *loading = false;
1372                        }
1373                    }
1374                }
1375            }
1376            self.search.search_loading = false;
1377        }
1378    }
1379
1380    pub fn update_metrics_model_name(&mut self) {
1381        let active_loaded_model = if let Some(model) = self.selected_model() {
1382            if self.is_model_loaded(&model.display_name) {
1383                Some(model.display_name.clone())
1384            } else {
1385                // Fallback to the first actually loaded model
1386                let lock = self
1387                    .server
1388                    .loaded_model_names
1389                    .lock()
1390                    .unwrap_or_else(|e| e.into_inner());
1391                lock.first().cloned()
1392            }
1393        } else {
1394            // No selection, fallback to the first actually loaded model
1395            let lock = self
1396                .server
1397                .loaded_model_names
1398                .lock()
1399                .unwrap_or_else(|e| e.into_inner());
1400            lock.first().cloned()
1401        };
1402        let mut lock = self
1403            .server
1404            .metrics_model_name
1405            .lock()
1406            .unwrap_or_else(|e| e.into_inner());
1407        *lock = active_loaded_model;
1408    }
1409
1410    pub fn ensure_download_channel(
1411        &mut self,
1412    ) -> tokio::sync::broadcast::Sender<crate::models::DownloadState> {
1413        if self.download.download_rx.is_none() {
1414            let (tx, rx) = tokio::sync::broadcast::channel(10);
1415            self.download.download_tx = Some(tx);
1416            self.download.download_rx = Some(rx);
1417        }
1418        self.download.download_tx.as_ref().unwrap().clone()
1419    }
1420
1421    pub async fn update_ws_server(&mut self) {
1422        let enabled = self.config.default.ws_server_enabled;
1423        let port = self.config.default.ws_server_port;
1424        let auth_key = self.config.default.ws_server_auth_key.clone();
1425        let tls_enabled = self.config.default.ws_server_tls_enabled;
1426        let tls_cert = self.config.default.ws_server_tls_cert.clone();
1427        let tls_key = self.config.default.ws_server_tls_key.clone();
1428
1429        // Load TLS config only if paths changed since last load, or if not yet cached.
1430        let tls_cfg = if tls_enabled {
1431            let needs_reload = match (&tls_cert, &tls_key) {
1432                (Some(cert), Some(key)) => {
1433                    Some(cert.as_str()) != self.server.running_ws_tls_cert_path.as_deref()
1434                        || Some(key.as_str()) != self.server.running_ws_tls_key_path.as_deref()
1435                }
1436                _ => {
1437                    // Auto-generated certs: only reload if we haven't cached
1438                    // the auto-generated paths yet.
1439                    self.server.running_ws_tls_cert_path.is_none()
1440                        || self.server.running_ws_tls_key_path.is_none()
1441                }
1442            };
1443            if needs_reload {
1444                if let (Some(cert), Some(key)) = (&tls_cert, &tls_key) {
1445                    crate::backend::tls::load_tls_config(cert, key).await.ok()
1446                } else {
1447                    self.add_log(
1448                        "Auto-generating TLS certificate and key",
1449                        crate::config::LogLevel::Info,
1450                    );
1451                    match crate::backend::tls::ensure_tls_certs() {
1452                        Ok((cert, key)) => {
1453                            self.config.default.ws_server_tls_cert = Some(cert.to_string_lossy().to_string());
1454                            self.config.default.ws_server_tls_key = Some(key.to_string_lossy().to_string());
1455                            self.server.running_ws_tls_cert_path =
1456                                Some(cert.to_string_lossy().to_string());
1457                            self.server.running_ws_tls_key_path =
1458                                Some(key.to_string_lossy().to_string());
1459                            crate::backend::tls::load_tls_config(
1460                                cert.to_string_lossy().as_ref(),
1461                                key.to_string_lossy().as_ref(),
1462                            )
1463                            .await
1464                            .ok()
1465                        }
1466                        Err(_) => None,
1467                    }
1468                }
1469            } else {
1470                self.server.running_ws_tls_cfg.clone()
1471            }
1472        } else {
1473            self.server.running_ws_tls_cfg = None;
1474            self.server.running_ws_tls_cert_path = None;
1475            self.server.running_ws_tls_key_path = None;
1476            None
1477        };
1478
1479        // Cache the TLS config and the paths used to load it.
1480        if let (Some(cert), Some(key)) = (&tls_cert, &tls_key) {
1481            self.server.running_ws_tls_cert_path = Some(cert.clone());
1482            self.server.running_ws_tls_key_path = Some(key.clone());
1483        }
1484        self.server.running_ws_tls_cfg = tls_cfg.clone();
1485
1486        // Check if settings have changed since last start.
1487        // When the user sets cert paths, compare configured against running.
1488        // When TLS is auto-generated (user didn't set paths), the effective
1489        // paths are already stored in running_ws_tls_cert_path/key_path, so
1490        // just skip the TLS path comparison — the paths haven't changed
1491        // unless the user explicitly changed their config.
1492        let tls_paths_changed = match (&tls_cert, &tls_key) {
1493            (Some(cert), Some(key)) => {
1494                Some(cert.as_str()) != self.server.running_ws_tls_cert_path.as_deref()
1495                    || Some(key.as_str()) != self.server.running_ws_tls_key_path.as_deref()
1496            }
1497            _ => {
1498                // Auto-generated: paths are stored in running_ws_tls_*_path,
1499                // which is set once after generation. Since the user didn't
1500                // configure specific paths, just check the TLS toggle itself.
1501                false
1502            }
1503        };
1504        let settings_changed = self.server.running_ws_port != Some(port)
1505            || self.server.running_ws_auth != auth_key
1506            || self.server.running_ws_tls != Some(tls_enabled)
1507            || tls_paths_changed;
1508
1509        if self.ws_server_handle.is_some() && (!enabled || settings_changed) {
1510            let handle = self.ws_server_handle.take().unwrap();
1511            crate::backend::ws_server::stop_ws_server(handle);
1512            self.server.running_ws_port = None;
1513            self.server.running_ws_auth = None;
1514            self.server.running_ws_tls = None;
1515            if !enabled {
1516                self.add_log("Dashboard disabled", crate::config::LogLevel::Info);
1517            }
1518        }
1519
1520        if enabled && self.ws_server_handle.is_none() {
1521            let (tx, rx) = tokio::sync::broadcast::channel(64);
1522            let ws_rx = std::sync::Arc::new(rx);
1523            let _host = self.settings.host.clone();
1524            match crate::backend::ws_server::start_ws_server(
1525                port,
1526                ws_rx,
1527                auth_key.clone(),
1528                tls_cfg,
1529                _host,
1530            )
1531            .await
1532            {
1533                Ok(handle) => {
1534                    self.server.metrics_tx = Some(tx);
1535                    self.ws_server_handle = Some(handle);
1536                    self.server.running_ws_port = Some(port);
1537                    self.server.running_ws_auth = auth_key.clone();
1538                    self.server.running_ws_tls = Some(tls_enabled);
1539                    let protocol = if tls_enabled { "https" } else { "http" };
1540                    let auth_param = match &auth_key {
1541                        Some(a) => format!("?auth={}", urlencoding::encode(a)),
1542                        None => String::new(),
1543                    };
1544                    self.add_log(
1545                        format!(
1546                            "Dashboard enabled: {protocol}://{}:{}/dashboard{}",
1547                            self.settings.host, port, auth_param
1548                        ),
1549                        crate::config::LogLevel::Info,
1550                    );
1551                }
1552                Err(e) => {
1553                    // Bind failed (port in use, invalid address, etc.). Surface the
1554                    // error to the user and flip the toggle back so the loop does
1555                    // not busy-retry every iteration.
1556                    self.add_log(
1557                        format!("Dashboard failed to start on port {}: {}", port, e),
1558                        crate::config::LogLevel::Error,
1559                    );
1560                    self.config.default.ws_server_enabled = false;
1561                    if let Err(e) = self.config.save() {
1562                        self.add_log(
1563                            format!("Failed to persist dashboard-disabled state: {}", e),
1564                            crate::config::LogLevel::Error,
1565                        );
1566                    }
1567                }
1568            }
1569        }
1570    }
1571
1572    /// Start/stop the API endpoint proxy based on settings.
1573    ///
1574    /// The proxy can run before any model is loaded (it will accept connections
1575    /// for `/api/status` and serve a proxy that returns errors until a model is
1576    /// loaded). When a model is loaded later, or the loaded model changes, the
1577    /// proxy is restarted so it points at the right llama-server port.
1578    pub async fn update_api_endpoint(&mut self) {
1579        let enabled = self.settings.api_endpoint_enabled;
1580        let port = self.settings.api_endpoint_port;
1581        let host = self.settings.host.clone();
1582        let server_port = self
1583            .server
1584            .server_handle
1585            .as_ref()
1586            .map(|h| h.port)
1587            .unwrap_or(0);
1588        let pid = self
1589            .server
1590            .server_handle
1591            .as_ref()
1592            .map(|h| h.pid)
1593            .unwrap_or(0);
1594        let model_name = self.server.spawned_model_name.clone().unwrap_or_default();
1595
1596        // No backend server and API proxy is not running — nothing to do.
1597        // This prevents a busy loop where settings_changed is always true
1598        // because running_api_server_port holds the old port while server_port is 0.
1599        if self.server.server_handle.is_none() && self.server.api_proxy_handle.is_none() {
1600            return;
1601        }
1602
1603        let settings_changed = self.server.running_api_port != Some(port)
1604            || self.server.running_api_server_port != Some(server_port)
1605            || self.server.running_api_model.as_deref() != Some(model_name.as_str());
1606
1607        // Stop if disabled or settings/model changed.
1608        if self.server.api_proxy_handle.is_some() && (!enabled || settings_changed) {
1609            if let Some(tx) = self.server.api_shutdown_tx.take() {
1610                let _ = tx.send(true);
1611            }
1612            if let Some(handle) = self.server.api_proxy_handle.take() {
1613                handle.abort();
1614            }
1615            self.server.running_api_port = None;
1616            self.server.running_api_server_port = None;
1617            self.server.running_api_model = None;
1618            if !enabled {
1619                self.add_log("API endpoint disabled", crate::config::LogLevel::Info);
1620            }
1621        }
1622
1623        // Start if enabled and not running.
1624        if enabled && self.server.api_proxy_handle.is_none() {
1625            let addr: std::net::SocketAddr = match format!("{}:{}", host, port).parse() {
1626                Ok(a) => a,
1627                Err(e) => {
1628                    self.add_log(
1629                        format!(
1630                            "API endpoint failed to start: invalid address {}:{}: {}",
1631                            host, port, e
1632                        ),
1633                        crate::config::LogLevel::Error,
1634                    );
1635                    self.settings.api_endpoint_enabled = false;
1636                    self.config.default.api_endpoint_enabled = false;
1637                    let _ = self.config.save();
1638                    return;
1639                }
1640            };
1641
1642            // Pre-bind to detect port-in-use before spawning.
1643            match tokio::net::TcpListener::bind(addr).await {
1644                Ok(listener) => drop(listener),
1645                Err(e) => {
1646                    self.add_log(
1647                        format!("API endpoint failed to start on {}:{}: {}", host, port, e),
1648                        crate::config::LogLevel::Error,
1649                    );
1650                    self.settings.api_endpoint_enabled = false;
1651                    self.config.default.api_endpoint_enabled = false;
1652                    let _ = self.config.save();
1653                    return;
1654                }
1655            }
1656
1657            let (api_shutdown_tx, api_shutdown_rx) = tokio::sync::watch::channel(false);
1658            self.server.api_shutdown_tx = Some(api_shutdown_tx);
1659            let host_clone = host.clone();
1660            let model_name_clone = model_name.clone();
1661            let handle = tokio::spawn(async move {
1662                let _ = crate::serve_api::start_api_server(
1663                    addr,
1664                    None,
1665                    server_port,
1666                    model_name_clone,
1667                    pid,
1668                    api_shutdown_rx,
1669                    host_clone,
1670                    None,
1671                )
1672                .await;
1673            });
1674            self.server.api_proxy_handle = Some(handle);
1675            self.server.running_api_port = Some(port);
1676            self.server.running_api_server_port = Some(server_port);
1677            self.server.running_api_model = Some(model_name);
1678            let status = if server_port == 0 {
1679                " (no model loaded yet)"
1680            } else {
1681                ""
1682            };
1683            self.add_log(
1684                format!("API endpoint started on {}:{}{}", host, port, status),
1685                crate::config::LogLevel::Info,
1686            );
1687        }
1688    }
1689}