1use chrono::Local;
2use std::collections::HashMap;
3
4use crate::blocks::{TextAlign, TextSize};
5use crate::builder::CardBuilder;
6use crate::templates::{ColorTheme, GenericCardTemplate, LanguageCode, get_translation};
7
8pub struct TaskSetProgress {
10 pub complete: u32,
12 pub total: u32,
14}
15
16pub fn network_submission_start(
17 network_set_name: &str,
18 network_type: &str,
19 group: Option<&str>,
20 prefix: Option<&str>,
21 metadata: Option<&serde_json::Value>,
22 language: LanguageCode,
23) -> GenericCardTemplate {
24 let t = |k: &str| get_translation(k, language);
25 let metadata_text = format!(
26 "**{}:** {}\n**{}:** {}",
27 t("network_set_name"),
28 network_set_name,
29 t("network_type"),
30 network_type,
31 );
32 let status_text = t("running");
33 let mut builder = CardBuilder::new()
34 .header(
35 &t("network_submission_started"),
36 Some(&status_text),
37 Some(ColorTheme::Wathet),
38 None,
39 )
40 .markdown(&metadata_text, TextAlign::Left, TextSize::Normal);
41
42 if group.is_some() || prefix.is_some() {
43 builder = builder
44 .columns()
45 .column(&t("group"), Some(group.unwrap_or("*unknown*")), "auto", 1)
46 .column(
47 &t("storage_prefix"),
48 Some(prefix.unwrap_or("*N/A*")),
49 "weighted",
50 1,
51 )
52 .end_columns();
53 }
54 if let Some(meta) = metadata {
55 let s = serde_json::to_string_pretty(meta).unwrap_or_default();
56 builder = builder.collapsible(
57 &t("metadata_overview"),
58 &format!("```json\n{s}\n```"),
59 false,
60 );
61 }
62 builder.build()
63}
64
65pub fn network_submission_complete(
66 network_set_name: &str,
67 submitted_count: Option<u32>,
68 group: Option<&str>,
69 prefix: Option<&str>,
70 duration: Option<&str>,
71 metadata: Option<&serde_json::Value>,
72 language: LanguageCode,
73) -> GenericCardTemplate {
74 let t = |k: &str| get_translation(k, language);
75 let mut lines = vec![format!(
76 "**{}:** {}",
77 t("network_set_name"),
78 network_set_name
79 )];
80 if let Some(c) = submitted_count {
81 lines.push(format!("**{}:** {c}", t("submitted_count")));
82 }
83 if let Some(d) = duration {
84 lines.push(format!("**{}:** {d}", t("duration")));
85 }
86
87 let status_text = t("success");
88 let mut builder = CardBuilder::new()
89 .header(
90 &t("network_submission_complete"),
91 Some(&status_text),
92 Some(ColorTheme::Green),
93 None,
94 )
95 .markdown(&lines.join("\n"), TextAlign::Left, TextSize::Normal);
96
97 if group.is_some() || prefix.is_some() {
98 builder = builder
99 .columns()
100 .column(&t("group"), Some(group.unwrap_or("*unknown*")), "auto", 1)
101 .column(
102 &t("storage_prefix"),
103 Some(prefix.unwrap_or("*N/A*")),
104 "weighted",
105 1,
106 )
107 .end_columns();
108 }
109 if let Some(meta) = metadata {
110 let s = serde_json::to_string_pretty(meta).unwrap_or_default();
111 builder = builder.collapsible(
112 &t("metadata_overview"),
113 &format!("```json\n{s}\n```"),
114 false,
115 );
116 }
117 builder.build()
118}
119
120pub fn network_submission_failure(
121 network_set_name: &str,
122 error_message: &str,
123 submitted_count: Option<u32>,
124 group: Option<&str>,
125 language: LanguageCode,
126) -> GenericCardTemplate {
127 let t = |k: &str| get_translation(k, language);
128 let mut lines = vec![
129 format!("**{}:** {}", t("network_set_name"), network_set_name),
130 format!("**{}:** {}", t("group"), group.unwrap_or("*unknown*")),
131 ];
132 if let Some(c) = submitted_count {
133 lines.push(format!("**{}:** {c}", t("submitted_count")));
134 }
135
136 let status_text = t("failed");
137 CardBuilder::new()
138 .header(
139 &t("network_submission_failed"),
140 Some(&status_text),
141 Some(ColorTheme::Red),
142 None,
143 )
144 .markdown(&lines.join("\n"), TextAlign::Left, TextSize::Normal)
145 .collapsible(&t("error_details"), error_message, false)
146 .build()
147}
148
149pub fn config_upload_complete(
150 config_name: &str,
151 file_count: u32,
152 labels: Option<&[&str]>,
153 desc: Option<&str>,
154 language: LanguageCode,
155) -> GenericCardTemplate {
156 let t = |k: &str| get_translation(k, language);
157 let mut lines = vec![
158 format!("**{}:** {}", t("config_name"), config_name),
159 format!("**{}:** {}", t("files_uploaded"), file_count),
160 ];
161 if let Some(d) = desc {
162 lines.push(format!("**{}:** {d}", t("description")));
163 }
164
165 let status_text = t("success");
166 let mut builder = CardBuilder::new()
167 .header(
168 &t("config_uploaded"),
169 Some(&status_text),
170 Some(ColorTheme::Green),
171 None,
172 )
173 .markdown(&lines.join("\n"), TextAlign::Left, TextSize::Normal);
174
175 if let Some(lbls) = labels
176 && !lbls.is_empty() {
177 builder = builder.collapsible(&t("uploaded_files"), &lbls.join(","), false);
178 }
179 builder.build()
180}
181
182pub fn job_submission_start(
183 job_title: &str,
184 desc: Option<&str>,
185 group: Option<&str>,
186 prefix: Option<&str>,
187 msg: Option<&str>,
188 metadata: Option<&serde_json::Value>,
189 language: LanguageCode,
190) -> GenericCardTemplate {
191 let t = |k: &str| get_translation(k, language);
192 let start_time = Local::now().format("%Y-%m-%d %H:%M").to_string();
193 let running_status = format!(
194 "<font color='wathet-400'> :StatusInFlight: {}</font>",
195 t("running")
196 );
197 let mut lines = vec![
198 format!("**{}:** {}", t("task_name"), job_title),
199 format!("**{}:** {}", t("start_time"), start_time),
200 ];
201 if let Some(d) = desc {
202 lines.push(format!("**{}:** {d}", t("task_description")));
203 }
204 lines.push(format!("**{}:** {}", t("execution_status"), running_status));
205
206 let status_text = t("running");
207 let mut builder = CardBuilder::new()
208 .header(
209 &t("task_submission_started"),
210 Some(&status_text),
211 Some(ColorTheme::Wathet),
212 None,
213 )
214 .markdown(&lines.join("\n"), TextAlign::Left, TextSize::Normal);
215
216 if group.is_some() || prefix.is_some() {
217 builder = builder
218 .columns()
219 .column(
220 &t("result_storage"),
221 Some(group.unwrap_or("*unknown*")),
222 "auto",
223 1,
224 )
225 .column(
226 &t("storage_prefix"),
227 Some(prefix.unwrap_or("*N/A*")),
228 "weighted",
229 1,
230 )
231 .end_columns();
232 }
233 if let Some(m) = msg {
234 builder = builder.collapsible(&t("running_overview"), m, false);
235 }
236 if let Some(meta) = metadata {
237 let s = serde_json::to_string_pretty(meta).unwrap_or_default();
238 builder = builder.collapsible(
239 &t("metadata_overview"),
240 &format!("```json\n{s}\n```"),
241 false,
242 );
243 }
244 builder.build()
245}
246
247#[allow(clippy::too_many_arguments)]
248pub fn job_submission_complete(
249 job_title: &str,
250 submitted_count: u32,
251 desc: Option<&str>,
252 group: Option<&str>,
253 prefix: Option<&str>,
254 duration: Option<&str>,
255 msg: Option<&str>,
256 metadata: Option<&serde_json::Value>,
257 language: LanguageCode,
258) -> GenericCardTemplate {
259 let t = |k: &str| get_translation(k, language);
260 let completion_time = Local::now().format("%Y-%m-%d %H:%M").to_string();
261 let status_text = format!(
262 "<font color='wathet-400'> :StatusInFlight: {}, {}</font>",
263 t("task_submission_complete"),
264 t("running")
265 );
266 let mut lines = vec![
267 format!("**{}:** {}", t("task_name"), job_title),
268 format!("**{}:** {}", t("completion_time"), completion_time),
269 format!("**{}:** {}", t("submitted_count"), submitted_count),
270 ];
271 if let Some(d) = desc {
272 lines.push(format!("**{}:** {d}", t("task_description")));
273 }
274 if let Some(d) = duration {
275 lines.push(format!("**{}:** {d}", t("execution_duration")));
276 }
277 lines.push(format!("**{}:** {}", t("execution_status"), status_text));
278
279 let header_status_text = t("submitted");
280 let mut builder = CardBuilder::new()
281 .header(
282 &t("task_submission_complete"),
283 Some(&header_status_text),
284 Some(ColorTheme::Wathet),
285 None,
286 )
287 .markdown(&lines.join("\n"), TextAlign::Left, TextSize::Normal);
288
289 if group.is_some() || prefix.is_some() {
290 builder = builder
291 .columns()
292 .column(
293 &t("result_storage"),
294 Some(group.unwrap_or("*unknown*")),
295 "auto",
296 1,
297 )
298 .column(
299 &t("storage_prefix"),
300 Some(prefix.unwrap_or("*N/A*")),
301 "weighted",
302 1,
303 )
304 .end_columns();
305 }
306 if let Some(m) = msg {
307 builder = builder.collapsible(&t("running_overview"), m, false);
308 }
309 if let Some(meta) = metadata {
310 let s = serde_json::to_string_pretty(meta).unwrap_or_default();
311 builder = builder.collapsible(
312 &t("metadata_overview"),
313 &format!("```json\n{s}\n```"),
314 false,
315 );
316 }
317 builder.build()
318}
319
320pub fn job_submission_failure(
321 job_title: &str,
322 error_message: &str,
323 submitted_count: Option<u32>,
324 group: Option<&str>,
325 language: LanguageCode,
326) -> GenericCardTemplate {
327 let t = |k: &str| get_translation(k, language);
328 let mut lines = vec![
329 format!("**{}:** {}", t("task_name"), job_title),
330 format!("**{}:** {}", t("group"), group.unwrap_or("*unknown*")),
331 ];
332 if let Some(c) = submitted_count {
333 lines.push(format!("**{}:** {c}", t("submitted_before_failure")));
334 }
335
336 let status_text = t("failed");
337 CardBuilder::new()
338 .header(
339 &t("task_submission_failed"),
340 Some(&status_text),
341 Some(ColorTheme::Red),
342 None,
343 )
344 .markdown(&lines.join("\n"), TextAlign::Left, TextSize::Normal)
345 .collapsible(&t("error_details"), error_message, false)
346 .build()
347}
348
349#[allow(clippy::too_many_arguments)]
350pub fn job_complete(
351 job_title: &str,
352 success: bool,
353 status: i32,
354 group: Option<&str>,
355 prefix: Option<&str>,
356 desc: Option<&str>,
357 msg: Option<&str>,
358 duration: Option<&str>,
359 title: Option<&str>,
360 language: LanguageCode,
361) -> GenericCardTemplate {
362 let t = |k: &str| get_translation(k, language);
363 let completion_time = Local::now().format("%Y-%m-%d %H:%M").to_string();
364
365 let (task_status, status_text, color, default_title) = if success {
366 (
367 format!("<font color='green'> :CheckMark: {}</font>", t("completed")),
368 t("success"),
369 ColorTheme::Green,
370 t("task_completion_notification"),
371 )
372 } else {
373 (
374 format!(
375 "<font color='red'> :CrossMark: {}: {status}</font>",
376 t("failed")
377 ),
378 t("failure"),
379 ColorTheme::Red,
380 t("task_failure_notification"),
381 )
382 };
383
384 let card_title = title.unwrap_or(&default_title);
385 let mut lines = vec![
386 format!("**{}:** {}", t("task_name"), job_title),
387 format!("**{}:** {}", t("completion_time"), completion_time),
388 ];
389 if let Some(d) = desc {
390 lines.push(format!("**{}:** {d}", t("task_description")));
391 }
392 if let Some(d) = duration {
393 lines.push(format!("**{}:** {d}", t("execution_duration")));
394 }
395 lines.push(format!("**{}:** {}", t("execution_status"), task_status));
396
397 let mut builder = CardBuilder::new()
398 .header(card_title, Some(&status_text), Some(color), None)
399 .markdown(&lines.join("\n"), TextAlign::Left, TextSize::Normal);
400
401 if group.is_some() || prefix.is_some() {
402 builder = builder
403 .columns()
404 .column(&t("group"), Some(group.unwrap_or("*unknown*")), "auto", 1)
405 .column(
406 &t("storage_prefix"),
407 Some(prefix.unwrap_or("*N/A*")),
408 "weighted",
409 1,
410 )
411 .end_columns();
412 }
413 if let Some(m) = msg {
414 builder = builder.collapsible(&t("result_overview"), m, false);
415 }
416 builder.build()
417}
418
419pub fn task_set_progress(
423 progress: &HashMap<&str, TaskSetProgress>,
424 overall_status: &str,
425 language: LanguageCode,
426) -> GenericCardTemplate {
427 let t = |k: &str| get_translation(k, language);
428 let mut table_lines = vec![
429 format!(
430 "| {} | Progress | {} | Total |",
431 t("task_set_name"),
432 t("completed")
433 ),
434 "|:---|:---|---:|---:|".to_owned(),
435 ];
436 for (name, p) in progress {
437 let pct = if p.total > 0 {
438 p.complete as f64 / p.total as f64 * 100.0
439 } else {
440 0.0
441 };
442 table_lines.push(format!(
443 "| {name} | {pct:.1}% | {} | {} |",
444 p.complete, p.total
445 ));
446 }
447
448 CardBuilder::new()
449 .header(
450 &t("task_set_progress"),
451 Some(overall_status),
452 Some(ColorTheme::Blue),
453 None,
454 )
455 .collapsible(&t("task_sets"), &table_lines.join("\n"), true)
456 .build()
457}
458
459pub fn result_collection_start(
460 task_set_names: &[&str],
461 job_title: Option<&str>,
462 group: Option<&str>,
463 msg: Option<&str>,
464 language: LanguageCode,
465) -> GenericCardTemplate {
466 let t = |k: &str| get_translation(k, language);
467 let resolved_title = job_title.map(|s| s.to_owned()).unwrap_or_else(|| {
468 if task_set_names.len() == 1 {
469 task_set_names[0].to_owned()
470 } else {
471 format!("Collection of {} Task Sets", task_set_names.len())
472 }
473 });
474 let mut lines = vec![
475 format!("**{}:** {}", t("task_name"), resolved_title),
476 format!("**{}:** {}", t("task_set_count"), task_set_names.len()),
477 ];
478 if let Some(g) = group {
479 lines.push(format!("**{}:** {g}", t("group")));
480 }
481
482 let status_text = t("running");
483 let mut builder = CardBuilder::new()
484 .header(
485 &t("result_collection_started"),
486 Some(&status_text),
487 Some(ColorTheme::Wathet),
488 None,
489 )
490 .markdown(&lines.join("\n"), TextAlign::Left, TextSize::Normal);
491
492 if let Some(m) = msg {
493 builder = builder.collapsible(&t("running_overview"), m, false);
494 }
495 builder.build()
496}
497
498pub fn result_collection_complete(
499 task_set_names: &[&str],
500 job_title: Option<&str>,
501 group: Option<&str>,
502 prefix: Option<&str>,
503 msg: Option<&str>,
504 language: LanguageCode,
505) -> GenericCardTemplate {
506 let t = |k: &str| get_translation(k, language);
507 let resolved_title = job_title.map(|s| s.to_owned()).unwrap_or_else(|| {
508 if task_set_names.len() == 1 {
509 task_set_names[0].to_owned()
510 } else {
511 format!("Collection of {} Task Sets", task_set_names.len())
512 }
513 });
514 let mut lines = vec![
515 format!("**{}:** {}", t("task_name"), resolved_title),
516 format!("**{}:** {}", t("task_set_count"), task_set_names.len()),
517 ];
518 if let Some(g) = group {
519 lines.push(format!("**{}:** {g}", t("group")));
520 }
521
522 let status_text = t("success");
523 let mut builder = CardBuilder::new()
524 .header(
525 &t("result_collection_complete"),
526 Some(&status_text),
527 Some(ColorTheme::Green),
528 None,
529 )
530 .markdown(&lines.join("\n"), TextAlign::Left, TextSize::Normal);
531
532 if group.is_some() || prefix.is_some() {
533 builder = builder
534 .columns()
535 .column(&t("group"), Some(group.unwrap_or("*unknown*")), "auto", 1)
536 .column(
537 &t("storage_prefix"),
538 Some(prefix.unwrap_or("*N/A*")),
539 "weighted",
540 1,
541 )
542 .end_columns();
543 }
544 if let Some(m) = msg {
545 builder = builder.collapsible(&t("state_overview"), m, false);
546 }
547 builder.build()
548}
549
550pub fn comparison_complete(
551 comparison_name: &str,
552 task_set_count: u32,
553 result_rows: u32,
554 result_columns: u32,
555 comparison_table: Option<&str>,
556 language: LanguageCode,
557) -> GenericCardTemplate {
558 let t = |k: &str| get_translation(k, language);
559 let metadata_text = format!(
560 "**{}:** {}\n**{}:** {}",
561 t("comparison_name"),
562 comparison_name,
563 t("task_sets_compared"),
564 task_set_count,
565 );
566
567 let status_text = t("success");
568 let mut builder = CardBuilder::new()
569 .header(
570 &t("comparison_complete"),
571 Some(&status_text),
572 Some(ColorTheme::Orange),
573 None,
574 )
575 .markdown(&metadata_text, TextAlign::Left, TextSize::Normal)
576 .columns()
577 .column(
578 &t("result_rows"),
579 Some(&result_rows.to_string()),
580 "weighted",
581 1,
582 )
583 .column(
584 &t("result_columns"),
585 Some(&result_columns.to_string()),
586 "weighted",
587 1,
588 )
589 .end_columns();
590
591 if let Some(tbl) = comparison_table {
592 builder = builder.collapsible(&t("comparison_results"), tbl, false);
593 }
594 builder.build()
595}
596
597pub fn create_custom_template(language: LanguageCode) -> CardBuilder {
598 CardBuilder::new().language(language)
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604 use crate::templates::{LanguageCode, LarkTemplate};
605 use std::collections::HashMap;
606
607 fn zh() -> LanguageCode {
608 LanguageCode::Zh
609 }
610
611 #[test]
612 fn test_network_submission_start() {
613 let t = network_submission_start("netset1", "TCP", None, None, None, zh());
614 let card = t.generate();
615 assert_eq!(card["schema"], "2.0");
616 assert_eq!(card["header"]["template"], "wathet");
617 let el0 = card["body"]["elements"][0]["content"].as_str().unwrap();
618 assert!(el0.contains("netset1"));
619 assert!(el0.contains("TCP"));
620 }
621
622 #[test]
623 fn test_network_submission_complete_green() {
624 let t = network_submission_complete("ns", Some(10), None, None, None, None, zh());
625 let card = t.generate();
626 assert_eq!(card["schema"], "2.0");
627 assert_eq!(card["header"]["template"], "green");
628 let el0 = card["body"]["elements"][0]["content"].as_str().unwrap();
629 assert!(el0.contains("ns"));
630 }
631
632 #[test]
633 fn test_network_submission_failure_red() {
634 let t = network_submission_failure("ns", "timeout", None, None, zh());
635 let card = t.generate();
636 assert_eq!(card["schema"], "2.0");
637 assert_eq!(card["header"]["template"], "red");
638 let elements = card["body"]["elements"].as_array().unwrap();
639 assert!(elements.len() >= 2); }
641
642 #[test]
643 fn test_config_upload_complete() {
644 let t = config_upload_complete("cfg1", 5, None, None, zh());
645 let card = t.generate();
646 assert_eq!(card["schema"], "2.0");
647 assert_eq!(card["header"]["template"], "green");
648 let el0 = card["body"]["elements"][0]["content"].as_str().unwrap();
649 assert!(el0.contains("cfg1"));
650 }
651
652 #[test]
653 fn test_job_submission_start() {
654 let t = job_submission_start("job1", None, None, None, None, None, zh());
655 let card = t.generate();
656 assert_eq!(card["schema"], "2.0");
657 assert_eq!(card["header"]["template"], "wathet");
658 let el0 = card["body"]["elements"][0]["content"].as_str().unwrap();
659 assert!(el0.contains("job1"));
660 }
661
662 #[test]
663 fn test_job_submission_complete() {
664 let t = job_submission_complete("job1", 42, None, None, None, None, None, None, zh());
665 let card = t.generate();
666 assert_eq!(card["schema"], "2.0");
667 assert_eq!(card["header"]["template"], "wathet");
668 let el0 = card["body"]["elements"][0]["content"].as_str().unwrap();
669 assert!(el0.contains("job1"));
670 assert!(el0.contains("42"));
671 }
672
673 #[test]
674 fn test_job_submission_failure_red() {
675 let t = job_submission_failure("job1", "oom", None, None, zh());
676 let card = t.generate();
677 assert_eq!(card["schema"], "2.0");
678 assert_eq!(card["header"]["template"], "red");
679 let elements = card["body"]["elements"].as_array().unwrap();
680 assert!(elements.len() >= 2);
681 }
682
683 #[test]
684 fn test_job_complete_success() {
685 let t = job_complete("job1", true, 0, None, None, None, None, None, None, zh());
686 let card = t.generate();
687 assert_eq!(card["schema"], "2.0");
688 assert_eq!(card["header"]["template"], "green");
689 let el0 = card["body"]["elements"][0]["content"].as_str().unwrap();
690 assert!(el0.contains("job1"));
691 }
692
693 #[test]
694 fn test_job_complete_failure() {
695 let t = job_complete("job1", false, 1, None, None, None, None, None, None, zh());
696 let card = t.generate();
697 assert_eq!(card["schema"], "2.0");
698 assert_eq!(card["header"]["template"], "red");
699 let el0 = card["body"]["elements"][0]["content"].as_str().unwrap();
700 assert!(el0.contains("1")); }
702
703 #[test]
704 fn test_task_set_progress() {
705 let mut progress = HashMap::new();
706 progress.insert(
707 "set_a",
708 TaskSetProgress {
709 complete: 5,
710 total: 10,
711 },
712 );
713 let t = task_set_progress(&progress, "running", zh());
714 let card = t.generate();
715 assert_eq!(card["schema"], "2.0");
716 assert_eq!(card["header"]["template"], "blue");
717 let elements = card["body"]["elements"].as_array().unwrap();
718 assert!(!elements.is_empty());
719 }
720
721 #[test]
722 fn test_result_collection_start() {
723 let t = result_collection_start(&["set_a", "set_b"], None, None, None, zh());
724 let card = t.generate();
725 assert_eq!(card["schema"], "2.0");
726 assert_eq!(card["header"]["template"], "wathet");
727 let el0 = card["body"]["elements"][0]["content"].as_str().unwrap();
728 assert!(el0.contains("2")); }
730
731 #[test]
732 fn test_result_collection_complete() {
733 let t = result_collection_complete(&["set_a"], Some("my_job"), None, None, None, zh());
734 let card = t.generate();
735 assert_eq!(card["schema"], "2.0");
736 assert_eq!(card["header"]["template"], "green");
737 let el0 = card["body"]["elements"][0]["content"].as_str().unwrap();
738 assert!(el0.contains("my_job"));
739 }
740
741 #[test]
742 fn test_comparison_complete_orange() {
743 let t = comparison_complete("cmp1", 3, 100, 5, None, zh());
744 let card = t.generate();
745 assert_eq!(card["schema"], "2.0");
746 assert_eq!(card["header"]["template"], "orange");
747 let el0 = card["body"]["elements"][0]["content"].as_str().unwrap();
748 assert!(el0.contains("cmp1"));
749 }
750
751 #[test]
752 fn test_task_set_progress_fields() {
753 let p = TaskSetProgress {
754 complete: 3,
755 total: 7,
756 };
757 assert_eq!(p.complete, 3);
758 assert_eq!(p.total, 7);
759 }
760
761 #[test]
762 fn test_create_custom_template() {
763 let t = create_custom_template(zh())
764 .header("Custom", Some("info"), None, None)
765 .markdown("content", TextAlign::Left, TextSize::Normal)
766 .build();
767 let card = t.generate();
768 assert_eq!(card["header"]["title"]["content"], "Custom");
769 }
770}