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 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 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 if self.server.spawned_context_length > 0 {
469 m.ctx_max = self.server.spawned_context_length;
470 }
471
472 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 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 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 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 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 self.model_states
576 .retain(|_, s| !matches!(s, crate::models::ModelState::Loading));
577
578 for name in to_update {
579 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}