1#[cfg(all(feature = "cloud-engine-host", feature = "subnet-catalog-host"))]
8use super::{CloudEngineListReport, CloudEngineOperatorLookupStatus};
9use super::{CloudEngineOperatorReport, CloudEnginePricesReport, CloudEngineReportContext};
10use crate::{
11 human_quantity::decimal_cycle_count_text,
12 table::{ColumnAlign, render_table},
13 text_value::{optional_text, sanitize_text, yes_no},
14};
15
16#[cfg(all(feature = "cloud-engine-host", feature = "subnet-catalog-host"))]
18#[must_use]
19pub fn cloud_engine_list_report_text(report: &CloudEngineListReport) -> String {
20 let mut lines = list_context_lines(report);
21 lines.push(String::new());
22 lines.push("CloudEngine subnets".to_string());
23 if report.cloud_engines.is_empty() {
24 lines.push("none".to_string());
25 return lines.join("\n");
26 }
27 lines.push(list_table(report));
28 append_lookup_failures(report, &mut lines);
29 lines.join("\n")
30}
31
32#[cfg(all(feature = "cloud-engine-host", feature = "subnet-catalog-host"))]
33fn list_context_lines(report: &CloudEngineListReport) -> Vec<String> {
34 vec![
35 format!("network: {}", sanitize_text(&report.network)),
36 format!(
37 "registry_authority: {}",
38 sanitize_text(&report.registry_authority)
39 ),
40 format!("registry_canister_id: {}", report.registry_canister_id),
41 format!("registry_version: {}", report.registry_version),
42 format!("registry_assurance: {}", report.registry_assurance.as_str()),
43 format!(
44 "registry_source_endpoints: {}",
45 report
46 .registry_source_endpoints
47 .iter()
48 .map(|endpoint| sanitize_text(endpoint))
49 .collect::<Vec<_>>()
50 .join(", ")
51 ),
52 format!(
53 "catalog_fetched_at: {}",
54 sanitize_text(&report.catalog_fetched_at)
55 ),
56 format!(
57 "catalog_cache_disposition: {}",
58 report.catalog_cache_disposition.as_str()
59 ),
60 format!("catalog_stale: {}", yes_no(report.catalog_stale)),
61 String::new(),
62 format!(
63 "control_plane_authority: {}",
64 sanitize_text(&report.control_plane_authority)
65 ),
66 format!(
67 "control_plane_canister_id: {}",
68 report.control_plane_canister_id
69 ),
70 format!(
71 "control_plane_source_endpoint: {}",
72 sanitize_text(&report.control_plane_source_endpoint)
73 ),
74 format!(
75 "control_plane_fetched_at: {}",
76 sanitize_text(&report.control_plane_fetched_at)
77 ),
78 format!(
79 "control_plane_certified: {}",
80 yes_no(report.control_plane_certified)
81 ),
82 format!(
83 "control_plane_point_in_time_guaranteed: {}",
84 yes_no(report.control_plane_point_in_time_guaranteed)
85 ),
86 format!(
87 "control_plane_lookup_attempt_count: {}",
88 report.control_plane_lookup_attempt_count
89 ),
90 format!(
91 "operator_bindings: {} resolved, {} absent, {} failed",
92 report.operator_binding_count,
93 report.missing_operator_binding_count,
94 report.operator_lookup_failure_count
95 ),
96 ]
97}
98
99#[cfg(all(feature = "cloud-engine-host", feature = "subnet-catalog-host"))]
100fn list_table(report: &CloudEngineListReport) -> String {
101 let headers = ["Label", "Subnet", "Nodes", "Binding", "Operator"];
102 let alignments = [
103 ColumnAlign::Left,
104 ColumnAlign::Left,
105 ColumnAlign::Right,
106 ColumnAlign::Left,
107 ColumnAlign::Left,
108 ];
109 let rows = report
110 .cloud_engines
111 .iter()
112 .map(|row| {
113 [
114 sanitize_text(&row.subnet_label),
115 row.subnet_id.clone(),
116 row.node_count
117 .map_or_else(|| "unknown".to_string(), |count| count.to_string()),
118 row.operator_lookup_status.as_str().to_string(),
119 row.operator_canister_id
120 .clone()
121 .unwrap_or_else(|| "-".to_string()),
122 ]
123 })
124 .collect::<Vec<_>>();
125 render_table(&headers, &rows, &alignments)
126}
127
128#[cfg(all(feature = "cloud-engine-host", feature = "subnet-catalog-host"))]
129fn append_lookup_failures(report: &CloudEngineListReport, lines: &mut Vec<String>) {
130 let failures = report
131 .cloud_engines
132 .iter()
133 .filter(|row| row.operator_lookup_status == CloudEngineOperatorLookupStatus::Failed)
134 .collect::<Vec<_>>();
135 if !failures.is_empty() {
136 lines.push(String::new());
137 lines.push("Operator lookup failures".to_string());
138 lines.extend(failures.into_iter().map(|row| {
139 format!(
140 " {}: {}",
141 row.subnet_id,
142 sanitize_text(
143 row.operator_lookup_error
144 .as_deref()
145 .unwrap_or("unspecified lookup failure")
146 )
147 )
148 }));
149 }
150}
151
152#[must_use]
154pub fn cloud_engine_operator_report_text(report: &CloudEngineOperatorReport) -> String {
155 let mut lines = context_lines(&report.context);
156 lines.extend([
157 format!("subnet_id: {}", report.subnet_id),
158 format!(
159 "operator_binding_present: {}",
160 yes_no(report.operator_binding_present)
161 ),
162 format!(
163 "operator_canister_id: {}",
164 optional_text(report.operator_canister_id.as_ref())
165 ),
166 format!(
167 "engine_owner: {}",
168 optional_text(report.engine_owner.as_ref())
169 ),
170 format!(
171 "platform_admin: {}",
172 optional_text(report.platform_admin.as_ref())
173 ),
174 format!(
175 "caffeine_enabled: {}",
176 report
177 .caffeine_enabled
178 .map_or_else(|| "-".to_string(), |enabled| yes_no(enabled).to_string())
179 ),
180 format!(
181 "claimed_domain_count: {}",
182 report
183 .claimed_domain_count
184 .map_or_else(|| "-".to_string(), |count| count.to_string())
185 ),
186 ]);
187
188 if let Some(domains) = report.claimed_domains.as_ref()
189 && !domains.is_empty()
190 {
191 lines.push(String::new());
192 lines.push("Claimed domains".to_string());
193 lines.extend(
194 domains
195 .iter()
196 .map(|domain| format!(" {}", sanitize_text(domain))),
197 );
198 }
199 lines.join("\n")
200}
201
202#[must_use]
204pub fn cloud_engine_prices_report_text(report: &CloudEnginePricesReport) -> String {
205 let mut lines = context_lines(&report.context);
206 lines.extend([
207 format!("network_fee: {}", report.network_fee),
208 format!("price_count: {}", report.price_count),
209 ]);
210
211 if !report.prices.is_empty() {
212 let headers = [
213 "Key",
214 "Node type",
215 "Data center",
216 "Provider",
217 "Net/month",
218 "Gross/month",
219 "Updated (ns)",
220 ];
221 let alignments = [
222 ColumnAlign::Left,
223 ColumnAlign::Left,
224 ColumnAlign::Left,
225 ColumnAlign::Left,
226 ColumnAlign::Right,
227 ColumnAlign::Right,
228 ColumnAlign::Right,
229 ];
230 let rows = report
231 .prices
232 .iter()
233 .map(|row| {
234 [
235 row.key.clone(),
236 row.node_type.to_string(),
237 row.data_center_id
238 .as_deref()
239 .map_or_else(|| "-".to_string(), sanitize_text),
240 row.provider_id.clone().unwrap_or_else(|| "-".to_string()),
241 decimal_cycle_count_text(&row.net_cycles_per_month),
242 decimal_cycle_count_text(&row.gross_cycles_per_month),
243 row.updated_at_unix_nanos.to_string(),
244 ]
245 })
246 .collect::<Vec<_>>();
247 lines.push(String::new());
248 lines.push("Marketplace prices".to_string());
249 lines.push(render_table(&headers, &rows, &alignments));
250 }
251 lines.join("\n")
252}
253
254fn context_lines(context: &CloudEngineReportContext) -> Vec<String> {
255 vec![
256 format!("network: {}", sanitize_text(&context.network)),
257 format!("authority: {}", sanitize_text(&context.authority)),
258 format!("engine_canister_id: {}", context.engine_canister_id),
259 format!("fetched_at: {}", sanitize_text(&context.fetched_at)),
260 format!(
261 "source_endpoint: {}",
262 sanitize_text(&context.source_endpoint)
263 ),
264 format!("fetched_by: {}", sanitize_text(&context.fetched_by)),
265 format!("certified: {}", yes_no(context.certified)),
266 format!(
267 "point_in_time_guaranteed: {}",
268 yes_no(context.point_in_time_guaranteed)
269 ),
270 format!("query_call_count: {}", context.query_call_count),
271 ]
272}