1use std::sync::Arc;
12
13use anyhow::Result;
14use colored::Colorize;
15use serde::Serialize;
16use tokio::time::Duration;
17use tracing::info;
18
19use crate::api::{search_library, create_http_client, run_with_retry};
20use crate::i18n::{t, Message};
21use crate::output::{emit_ndjson, print_health_line, health_symbol};
22use crate::storage::load_api_keys;
23
24#[derive(Debug, Serialize)]
28pub struct HealthReport {
29 pub config_ok: bool,
31 pub keys_count: usize,
33 pub api_reachable: bool,
35 pub api_details: Option<String>,
37}
38
39#[must_use]
45pub async fn run_health(json: bool) -> Result<i32> {
46 info!("Running health check");
47
48 if !json {
49 print_health_line(&t(Message::HealthRunning).cyan().to_string());
50 }
51
52 let client = match create_http_client() {
54 Ok(c) => {
55 if !json {
56 print_health_line(&format!(
57 "{} {}",
58 health_symbol(true),
59 t(Message::HealthConfigOk).green()
60 ));
61 }
62 c
63 }
64 Err(err) => {
65 let detail = err.to_string();
66 if json {
67 let report = HealthReport {
68 config_ok: false,
69 keys_count: 0,
70 api_reachable: false,
71 api_details: Some(detail.clone()),
72 };
73 emit_ndjson("health", &report);
74 } else {
75 print_health_line(&format!(
76 "{} {} — {}",
77 health_symbol(false),
78 t(Message::HealthConfigFailed).red(),
79 detail
80 ));
81 }
82 return Ok(74);
83 }
84 };
85
86 let keys = match load_api_keys() {
88 Ok(c) if !c.is_empty() => {
89 if !json {
90 print_health_line(&format!(
91 "{} {} {}",
92 health_symbol(true),
93 t(Message::HealthKeysOk).green(),
94 c.len().to_string().bold()
95 ));
96 }
97 c
98 }
99 _ => {
100 if json {
101 let report = HealthReport {
102 config_ok: true,
103 keys_count: 0,
104 api_reachable: false,
105 api_details: None,
106 };
107 emit_ndjson("health", &report);
108 } else {
109 print_health_line(&format!(
110 "{} {}",
111 health_symbol(false),
112 t(Message::HealthKeysMissing).yellow()
113 ));
114 }
115 return Ok(66);
116 }
117 };
118
119 let client_arc = Arc::new(client);
121 let probe_result = tokio::time::timeout(
122 Duration::from_secs(10),
123 run_with_retry(&keys, move |key| {
124 let c = Arc::clone(&client_arc);
125 async move { search_library(&c, &key, "react", "health probe").await }
126 }),
127 )
128 .await;
129
130 let api_reachable = matches!(probe_result, Ok(Ok(_)));
131 let api_details = match &probe_result {
132 Err(_) => Some("timeout after 10s".to_string()),
133 Ok(Err(e)) => Some(e.to_string()),
134 Ok(Ok(_)) => None,
135 };
136
137 if json {
138 let report = HealthReport {
139 config_ok: true,
140 keys_count: keys.len(),
141 api_reachable,
142 api_details: api_details.clone(),
143 };
144 emit_ndjson("health", &report);
145 } else if api_reachable {
146 print_health_line(&format!(
147 "{} {}",
148 health_symbol(true),
149 t(Message::HealthApiOk).green()
150 ));
151 } else {
152 let detail = api_details.as_deref().unwrap_or("");
153 print_health_line(&format!(
154 "{} {} — {}",
155 health_symbol(false),
156 t(Message::HealthApiOffline).red(),
157 detail
158 ));
159 }
160
161 if api_reachable {
162 Ok(0)
163 } else {
164 Ok(69)
165 }
166}