1use anyhow::{Context, Result};
11use chrono::{DateTime, Utc};
12use clap::{Args, Subcommand, ValueEnum};
13use colored::Colorize;
14use serde::{Deserialize, Serialize};
15use std::path::PathBuf;
16use tabled::{settings::Style, Table, Tabled};
17
18use wifi_densepose_mat::{
19 DisasterConfig, DisasterType, Priority, ScanZone, TriageStatus, ZoneBounds,
20 ZoneStatus, domain::alert::AlertStatus,
21};
22
23#[derive(Subcommand, Debug)]
25pub enum MatCommand {
26 Scan(ScanArgs),
28
29 Status(StatusArgs),
31
32 Zones(ZonesArgs),
34
35 Survivors(SurvivorsArgs),
37
38 Alerts(AlertsArgs),
40
41 Export(ExportArgs),
43}
44
45#[derive(Args, Debug)]
47pub struct ScanArgs {
48 #[arg(short, long)]
50 pub zone: Option<String>,
51
52 #[arg(short, long, value_enum, default_value = "earthquake")]
54 pub disaster_type: DisasterTypeArg,
55
56 #[arg(short, long, default_value = "0.8")]
58 pub sensitivity: f64,
59
60 #[arg(short = 'd', long, default_value = "5.0")]
62 pub max_depth: f64,
63
64 #[arg(short, long)]
66 pub continuous: bool,
67
68 #[arg(short, long, default_value = "500")]
70 pub interval: u64,
71
72 #[arg(long)]
74 pub simulate: bool,
75}
76
77#[derive(ValueEnum, Clone, Debug)]
79pub enum DisasterTypeArg {
80 Earthquake,
81 BuildingCollapse,
82 Avalanche,
83 Flood,
84 MineCollapse,
85 Unknown,
86}
87
88impl From<DisasterTypeArg> for DisasterType {
89 fn from(val: DisasterTypeArg) -> Self {
90 match val {
91 DisasterTypeArg::Earthquake => DisasterType::Earthquake,
92 DisasterTypeArg::BuildingCollapse => DisasterType::BuildingCollapse,
93 DisasterTypeArg::Avalanche => DisasterType::Avalanche,
94 DisasterTypeArg::Flood => DisasterType::Flood,
95 DisasterTypeArg::MineCollapse => DisasterType::MineCollapse,
96 DisasterTypeArg::Unknown => DisasterType::Unknown,
97 }
98 }
99}
100
101#[derive(Args, Debug)]
103pub struct StatusArgs {
104 #[arg(short, long)]
106 pub verbose: bool,
107
108 #[arg(short, long, value_enum, default_value = "table")]
110 pub format: OutputFormat,
111
112 #[arg(short, long)]
114 pub watch: bool,
115}
116
117#[derive(Args, Debug)]
119pub struct ZonesArgs {
120 #[command(subcommand)]
122 pub command: ZonesCommand,
123}
124
125#[derive(Subcommand, Debug)]
127pub enum ZonesCommand {
128 List {
130 #[arg(short, long)]
132 active: bool,
133 },
134
135 Add {
137 #[arg(short, long)]
139 name: String,
140
141 #[arg(short = 't', long, value_enum, default_value = "rectangle")]
143 zone_type: ZoneType,
144
145 #[arg(short, long)]
147 bounds: String,
148
149 #[arg(short, long)]
151 sensitivity: Option<f64>,
152 },
153
154 Remove {
156 zone: String,
158
159 #[arg(short, long)]
161 force: bool,
162 },
163
164 Pause {
166 zone: String,
168 },
169
170 Resume {
172 zone: String,
174 },
175}
176
177#[derive(ValueEnum, Clone, Debug)]
179pub enum ZoneType {
180 Rectangle,
181 Circle,
182}
183
184#[derive(Args, Debug)]
186pub struct SurvivorsArgs {
187 #[arg(short, long, value_enum)]
189 pub triage: Option<TriageFilter>,
190
191 #[arg(short, long)]
193 pub zone: Option<String>,
194
195 #[arg(short, long, value_enum, default_value = "triage")]
197 pub sort_by: SortOrder,
198
199 #[arg(short, long, value_enum, default_value = "table")]
201 pub format: OutputFormat,
202
203 #[arg(short, long)]
205 pub active: bool,
206
207 #[arg(short = 'n', long)]
209 pub limit: Option<usize>,
210}
211
212#[derive(ValueEnum, Clone, Debug)]
214pub enum TriageFilter {
215 Immediate,
216 Delayed,
217 Minor,
218 Deceased,
219 Unknown,
220}
221
222impl From<TriageFilter> for TriageStatus {
223 fn from(val: TriageFilter) -> Self {
224 match val {
225 TriageFilter::Immediate => TriageStatus::Immediate,
226 TriageFilter::Delayed => TriageStatus::Delayed,
227 TriageFilter::Minor => TriageStatus::Minor,
228 TriageFilter::Deceased => TriageStatus::Deceased,
229 TriageFilter::Unknown => TriageStatus::Unknown,
230 }
231 }
232}
233
234#[derive(ValueEnum, Clone, Debug)]
236pub enum SortOrder {
237 Triage,
239 Time,
241 Zone,
243 Confidence,
245}
246
247#[derive(ValueEnum, Clone, Debug, Default)]
249pub enum OutputFormat {
250 #[default]
252 Table,
253 Json,
255 Compact,
257}
258
259#[derive(Args, Debug)]
261pub struct AlertsArgs {
262 #[command(subcommand)]
264 pub command: Option<AlertsCommand>,
265
266 #[arg(short, long, value_enum)]
268 pub priority: Option<PriorityFilter>,
269
270 #[arg(long)]
272 pub pending: bool,
273
274 #[arg(short = 'n', long)]
276 pub limit: Option<usize>,
277}
278
279#[derive(Subcommand, Debug)]
281pub enum AlertsCommand {
282 List,
284
285 Ack {
287 alert_id: String,
289
290 #[arg(short, long)]
292 by: String,
293 },
294
295 Resolve {
297 alert_id: String,
299
300 #[arg(short, long, value_enum)]
302 resolution: ResolutionType,
303
304 #[arg(short, long)]
306 notes: Option<String>,
307 },
308
309 Escalate {
311 alert_id: String,
313 },
314}
315
316#[derive(ValueEnum, Clone, Debug)]
318pub enum PriorityFilter {
319 Critical,
320 High,
321 Medium,
322 Low,
323}
324
325#[derive(ValueEnum, Clone, Debug)]
327pub enum ResolutionType {
328 Rescued,
329 FalsePositive,
330 Deceased,
331 Other,
332}
333
334#[derive(Args, Debug)]
336pub struct ExportArgs {
337 #[arg(short, long)]
339 pub output: PathBuf,
340
341 #[arg(short, long, value_enum, default_value = "json")]
343 pub format: ExportFormat,
344
345 #[arg(long)]
347 pub include_history: bool,
348
349 #[arg(short, long, value_enum)]
351 pub triage: Option<TriageFilter>,
352
353 #[arg(short = 'z', long)]
355 pub zone: Option<String>,
356}
357
358#[derive(ValueEnum, Clone, Debug)]
360pub enum ExportFormat {
361 Json,
362 Csv,
363}
364
365#[derive(Tabled, Serialize, Deserialize)]
371struct SurvivorRow {
372 #[tabled(rename = "ID")]
373 id: String,
374 #[tabled(rename = "Zone")]
375 zone: String,
376 #[tabled(rename = "Triage")]
377 triage: String,
378 #[tabled(rename = "Status")]
379 status: String,
380 #[tabled(rename = "Confidence")]
381 confidence: String,
382 #[tabled(rename = "Location")]
383 location: String,
384 #[tabled(rename = "Last Update")]
385 last_update: String,
386}
387
388#[derive(Tabled, Serialize, Deserialize)]
390struct ZoneRow {
391 #[tabled(rename = "ID")]
392 id: String,
393 #[tabled(rename = "Name")]
394 name: String,
395 #[tabled(rename = "Status")]
396 status: String,
397 #[tabled(rename = "Area (m2)")]
398 area: String,
399 #[tabled(rename = "Scans")]
400 scan_count: u32,
401 #[tabled(rename = "Detections")]
402 detections: u32,
403 #[tabled(rename = "Last Scan")]
404 last_scan: String,
405}
406
407#[derive(Tabled, Serialize, Deserialize)]
409struct AlertRow {
410 #[tabled(rename = "ID")]
411 id: String,
412 #[tabled(rename = "Priority")]
413 priority: String,
414 #[tabled(rename = "Status")]
415 status: String,
416 #[tabled(rename = "Survivor")]
417 survivor_id: String,
418 #[tabled(rename = "Title")]
419 title: String,
420 #[tabled(rename = "Age")]
421 age: String,
422}
423
424#[derive(Serialize, Deserialize)]
426struct SystemStatus {
427 scanning: bool,
428 active_zones: usize,
429 total_zones: usize,
430 survivors_detected: usize,
431 critical_survivors: usize,
432 pending_alerts: usize,
433 disaster_type: String,
434 uptime: String,
435}
436
437pub async fn execute(command: MatCommand) -> Result<()> {
443 match command {
444 MatCommand::Scan(args) => execute_scan(args).await,
445 MatCommand::Status(args) => execute_status(args).await,
446 MatCommand::Zones(args) => execute_zones(args).await,
447 MatCommand::Survivors(args) => execute_survivors(args).await,
448 MatCommand::Alerts(args) => execute_alerts(args).await,
449 MatCommand::Export(args) => execute_export(args).await,
450 }
451}
452
453async fn execute_scan(args: ScanArgs) -> Result<()> {
455 println!(
456 "{} Starting survivor scan...",
457 "[MAT]".bright_cyan().bold()
458 );
459 println!();
460
461 println!("{}", "Configuration:".bold());
463 println!(
464 " {} {:?}",
465 "Disaster Type:".dimmed(),
466 args.disaster_type
467 );
468 println!(
469 " {} {:.1}",
470 "Sensitivity:".dimmed(),
471 args.sensitivity
472 );
473 println!(
474 " {} {:.1}m",
475 "Max Depth:".dimmed(),
476 args.max_depth
477 );
478 println!(
479 " {} {}",
480 "Continuous:".dimmed(),
481 if args.continuous { "Yes" } else { "No" }
482 );
483 if args.continuous {
484 println!(
485 " {} {}ms",
486 "Interval:".dimmed(),
487 args.interval
488 );
489 }
490 if let Some(ref zone) = args.zone {
491 println!(" {} {}", "Zone:".dimmed(), zone);
492 }
493 println!();
494
495 if args.simulate {
496 println!(
497 "{} Running in simulation mode",
498 "[SIMULATION]".yellow().bold()
499 );
500 println!();
501
502 simulate_scan_output().await?;
504 } else {
505 let config = DisasterConfig::builder()
507 .disaster_type(args.disaster_type.into())
508 .sensitivity(args.sensitivity)
509 .max_depth(args.max_depth)
510 .continuous_monitoring(args.continuous)
511 .scan_interval_ms(args.interval)
512 .build();
513
514 println!(
515 "{} Initializing detection pipeline with config: {:?}",
516 "[INFO]".blue(),
517 config.disaster_type
518 );
519 println!(
520 "{} Waiting for hardware connection...",
521 "[INFO]".blue()
522 );
523 println!();
524 println!(
525 "{} No hardware detected. Use --simulate for demo mode.",
526 "[WARN]".yellow()
527 );
528 }
529
530 Ok(())
531}
532
533async fn simulate_scan_output() -> Result<()> {
535 use indicatif::{ProgressBar, ProgressStyle};
536 use std::time::Duration;
537
538 let pb = ProgressBar::new(100);
539 pb.set_style(
540 ProgressStyle::default_bar()
541 .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})")?
542 .progress_chars("#>-"),
543 );
544
545 for i in 0..100 {
546 pb.set_position(i);
547 tokio::time::sleep(Duration::from_millis(50)).await;
548
549 if i == 25 {
551 pb.suspend(|| {
552 println!();
553 print_detection(
554 "SURV-001",
555 "Zone A",
556 TriageStatus::Immediate,
557 0.92,
558 Some((12.5, 8.3, -2.1)),
559 );
560 });
561 }
562 if i == 55 {
563 pb.suspend(|| {
564 print_detection(
565 "SURV-002",
566 "Zone A",
567 TriageStatus::Delayed,
568 0.78,
569 Some((15.2, 10.1, -1.5)),
570 );
571 });
572 }
573 if i == 80 {
574 pb.suspend(|| {
575 print_detection(
576 "SURV-003",
577 "Zone B",
578 TriageStatus::Minor,
579 0.85,
580 Some((8.7, 22.4, -0.8)),
581 );
582 });
583 }
584 }
585
586 pb.finish_with_message("Scan complete");
587 println!();
588 println!(
589 "{} Scan complete. Detected {} survivors.",
590 "[MAT]".bright_cyan().bold(),
591 "3".green().bold()
592 );
593 println!(
594 " {} {} {} {} {} {}",
595 "IMMEDIATE:".red().bold(),
596 "1",
597 "DELAYED:".yellow().bold(),
598 "1",
599 "MINOR:".green().bold(),
600 "1"
601 );
602
603 Ok(())
604}
605
606fn print_detection(
608 id: &str,
609 zone: &str,
610 triage: TriageStatus,
611 confidence: f64,
612 location: Option<(f64, f64, f64)>,
613) {
614 let triage_str = format_triage(&triage);
615 let location_str = location
616 .map(|(x, y, z)| format!("({:.1}, {:.1}, {:.1})", x, y, z))
617 .unwrap_or_else(|| "Unknown".to_string());
618
619 println!(
620 "{} {} detected in {} - {} | Confidence: {:.0}% | Location: {}",
621 format!("[{}]", triage_str).bold(),
622 id.cyan(),
623 zone,
624 triage_str,
625 confidence * 100.0,
626 location_str.dimmed()
627 );
628}
629
630async fn execute_status(args: StatusArgs) -> Result<()> {
632 let status = SystemStatus {
634 scanning: false,
635 active_zones: 0,
636 total_zones: 0,
637 survivors_detected: 0,
638 critical_survivors: 0,
639 pending_alerts: 0,
640 disaster_type: "Not configured".to_string(),
641 uptime: "N/A".to_string(),
642 };
643
644 match args.format {
645 OutputFormat::Json => {
646 println!("{}", serde_json::to_string_pretty(&status)?);
647 }
648 OutputFormat::Compact => {
649 println!(
650 "scanning={} zones={}/{} survivors={} critical={} alerts={}",
651 status.scanning,
652 status.active_zones,
653 status.total_zones,
654 status.survivors_detected,
655 status.critical_survivors,
656 status.pending_alerts
657 );
658 }
659 OutputFormat::Table => {
660 println!("{}", "MAT System Status".bold().cyan());
661 println!("{}", "=".repeat(50));
662 println!(
663 " {} {}",
664 "Scanning:".dimmed(),
665 if status.scanning {
666 "Active".green()
667 } else {
668 "Inactive".red()
669 }
670 );
671 println!(
672 " {} {}/{}",
673 "Zones:".dimmed(),
674 status.active_zones,
675 status.total_zones
676 );
677 println!(
678 " {} {}",
679 "Disaster Type:".dimmed(),
680 status.disaster_type
681 );
682 println!(
683 " {} {}",
684 "Survivors Detected:".dimmed(),
685 status.survivors_detected
686 );
687 println!(
688 " {} {}",
689 "Critical (Immediate):".dimmed(),
690 if status.critical_survivors > 0 {
691 status.critical_survivors.to_string().red().bold()
692 } else {
693 status.critical_survivors.to_string().normal()
694 }
695 );
696 println!(
697 " {} {}",
698 "Pending Alerts:".dimmed(),
699 if status.pending_alerts > 0 {
700 status.pending_alerts.to_string().yellow().bold()
701 } else {
702 status.pending_alerts.to_string().normal()
703 }
704 );
705 println!(" {} {}", "Uptime:".dimmed(), status.uptime);
706 println!();
707
708 if !status.scanning {
709 println!(
710 "{} No active scan. Run '{}' to start.",
711 "[INFO]".blue(),
712 "wifi-densepose mat scan".green()
713 );
714 }
715 }
716 }
717
718 Ok(())
719}
720
721async fn execute_zones(args: ZonesArgs) -> Result<()> {
723 match args.command {
724 ZonesCommand::List { active } => {
725 println!("{}", "Scan Zones".bold().cyan());
726 println!("{}", "=".repeat(80));
727
728 let zones = vec![
730 ZoneRow {
731 id: "zone-001".to_string(),
732 name: "Building A - North Wing".to_string(),
733 status: format_zone_status(&ZoneStatus::Active),
734 area: "1500.0".to_string(),
735 scan_count: 42,
736 detections: 3,
737 last_scan: "2 min ago".to_string(),
738 },
739 ZoneRow {
740 id: "zone-002".to_string(),
741 name: "Building A - South Wing".to_string(),
742 status: format_zone_status(&ZoneStatus::Paused),
743 area: "1200.0".to_string(),
744 scan_count: 28,
745 detections: 1,
746 last_scan: "15 min ago".to_string(),
747 },
748 ];
749
750 let filtered: Vec<_> = if active {
751 zones
752 .into_iter()
753 .filter(|z| z.status.contains("Active"))
754 .collect()
755 } else {
756 zones
757 };
758
759 if filtered.is_empty() {
760 println!("No zones configured. Use 'wifi-densepose mat zones add' to create one.");
761 } else {
762 let table = Table::new(filtered).with(Style::rounded()).to_string();
763 println!("{}", table);
764 }
765 }
766 ZonesCommand::Add {
767 name,
768 zone_type,
769 bounds,
770 sensitivity,
771 } => {
772 let bounds_parsed: Result<ZoneBounds, _> = parse_bounds(&zone_type, &bounds);
774 match bounds_parsed {
775 Ok(zone_bounds) => {
776 let zone = if let Some(sens) = sensitivity {
777 let mut params = wifi_densepose_mat::ScanParameters::default();
778 params.sensitivity = sens;
779 ScanZone::with_parameters(&name, zone_bounds, params)
780 } else {
781 ScanZone::new(&name, zone_bounds)
782 };
783
784 println!(
785 "{} Zone '{}' created with ID: {}",
786 "[OK]".green().bold(),
787 name.cyan(),
788 zone.id()
789 );
790 println!(" Area: {:.1} m2", zone.area());
791 }
792 Err(e) => {
793 eprintln!("{} Failed to parse bounds: {}", "[ERROR]".red().bold(), e);
794 eprintln!(" Expected format for rectangle: min_x,min_y,max_x,max_y");
795 eprintln!(" Expected format for circle: center_x,center_y,radius");
796 return Err(e);
797 }
798 }
799 }
800 ZonesCommand::Remove { zone, force } => {
801 if !force {
802 println!(
803 "{} This will remove zone '{}' and stop any active scans.",
804 "[WARN]".yellow().bold(),
805 zone
806 );
807 println!("Use --force to confirm.");
808 } else {
809 println!(
810 "{} Zone '{}' removed.",
811 "[OK]".green().bold(),
812 zone.cyan()
813 );
814 }
815 }
816 ZonesCommand::Pause { zone } => {
817 println!(
818 "{} Zone '{}' paused.",
819 "[OK]".green().bold(),
820 zone.cyan()
821 );
822 }
823 ZonesCommand::Resume { zone } => {
824 println!(
825 "{} Zone '{}' resumed.",
826 "[OK]".green().bold(),
827 zone.cyan()
828 );
829 }
830 }
831
832 Ok(())
833}
834
835fn parse_bounds(zone_type: &ZoneType, bounds: &str) -> Result<ZoneBounds> {
837 let parts: Vec<f64> = bounds
838 .split(',')
839 .map(|s| s.trim().parse::<f64>())
840 .collect::<std::result::Result<Vec<_>, _>>()
841 .context("Failed to parse bounds values as numbers")?;
842
843 match zone_type {
844 ZoneType::Rectangle => {
845 if parts.len() != 4 {
846 anyhow::bail!(
847 "Rectangle requires 4 values: min_x,min_y,max_x,max_y (got {})",
848 parts.len()
849 );
850 }
851 Ok(ZoneBounds::rectangle(parts[0], parts[1], parts[2], parts[3]))
852 }
853 ZoneType::Circle => {
854 if parts.len() != 3 {
855 anyhow::bail!(
856 "Circle requires 3 values: center_x,center_y,radius (got {})",
857 parts.len()
858 );
859 }
860 Ok(ZoneBounds::circle(parts[0], parts[1], parts[2]))
861 }
862 }
863}
864
865async fn execute_survivors(args: SurvivorsArgs) -> Result<()> {
867 let survivors = vec![
869 SurvivorRow {
870 id: "SURV-001".to_string(),
871 zone: "Zone A".to_string(),
872 triage: format_triage(&TriageStatus::Immediate),
873 status: "Active".green().to_string(),
874 confidence: "92%".to_string(),
875 location: "(12.5, 8.3, -2.1)".to_string(),
876 last_update: "30s ago".to_string(),
877 },
878 SurvivorRow {
879 id: "SURV-002".to_string(),
880 zone: "Zone A".to_string(),
881 triage: format_triage(&TriageStatus::Delayed),
882 status: "Active".green().to_string(),
883 confidence: "78%".to_string(),
884 location: "(15.2, 10.1, -1.5)".to_string(),
885 last_update: "1m ago".to_string(),
886 },
887 SurvivorRow {
888 id: "SURV-003".to_string(),
889 zone: "Zone B".to_string(),
890 triage: format_triage(&TriageStatus::Minor),
891 status: "Active".green().to_string(),
892 confidence: "85%".to_string(),
893 location: "(8.7, 22.4, -0.8)".to_string(),
894 last_update: "2m ago".to_string(),
895 },
896 ];
897
898 let mut filtered = survivors;
900
901 if let Some(ref triage_filter) = args.triage {
902 let status: TriageStatus = triage_filter.clone().into();
903 let status_str = format_triage(&status);
904 filtered.retain(|s| s.triage == status_str);
905 }
906
907 if let Some(ref zone) = args.zone {
908 filtered.retain(|s| s.zone.contains(zone));
909 }
910
911 if let Some(limit) = args.limit {
912 filtered.truncate(limit);
913 }
914
915 match args.format {
916 OutputFormat::Json => {
917 println!("{}", serde_json::to_string_pretty(&filtered)?);
918 }
919 OutputFormat::Compact => {
920 for s in &filtered {
921 println!(
922 "{}\t{}\t{}\t{}\t{}",
923 s.id, s.zone, s.triage, s.confidence, s.location
924 );
925 }
926 }
927 OutputFormat::Table => {
928 println!("{}", "Detected Survivors".bold().cyan());
929 println!("{}", "=".repeat(100));
930
931 if filtered.is_empty() {
932 println!("No survivors detected matching criteria.");
933 } else {
934 let immediate = filtered
936 .iter()
937 .filter(|s| s.triage.contains("IMMEDIATE"))
938 .count();
939 let delayed = filtered
940 .iter()
941 .filter(|s| s.triage.contains("DELAYED"))
942 .count();
943 let minor = filtered
944 .iter()
945 .filter(|s| s.triage.contains("MINOR"))
946 .count();
947
948 println!(
949 "Total: {} | {} {} | {} {} | {} {}",
950 filtered.len().to_string().bold(),
951 "IMMEDIATE:".red().bold(),
952 immediate,
953 "DELAYED:".yellow().bold(),
954 delayed,
955 "MINOR:".green().bold(),
956 minor
957 );
958 println!();
959
960 let table = Table::new(filtered).with(Style::rounded()).to_string();
961 println!("{}", table);
962 }
963 }
964 }
965
966 Ok(())
967}
968
969async fn execute_alerts(args: AlertsArgs) -> Result<()> {
971 match args.command {
972 Some(AlertsCommand::Ack { alert_id, by }) => {
973 println!(
974 "{} Alert {} acknowledged by {}",
975 "[OK]".green().bold(),
976 alert_id.cyan(),
977 by
978 );
979 }
980 Some(AlertsCommand::Resolve {
981 alert_id,
982 resolution,
983 notes,
984 }) => {
985 println!(
986 "{} Alert {} resolved as {:?}",
987 "[OK]".green().bold(),
988 alert_id.cyan(),
989 resolution
990 );
991 if let Some(notes) = notes {
992 println!(" Notes: {}", notes);
993 }
994 }
995 Some(AlertsCommand::Escalate { alert_id }) => {
996 println!(
997 "{} Alert {} escalated to higher priority",
998 "[OK]".green().bold(),
999 alert_id.cyan()
1000 );
1001 }
1002 Some(AlertsCommand::List) | None => {
1003 let alerts = vec![
1005 AlertRow {
1006 id: "ALRT-001".to_string(),
1007 priority: format_priority(Priority::Critical),
1008 status: format_alert_status(&AlertStatus::Pending),
1009 survivor_id: "SURV-001".to_string(),
1010 title: "Immediate: Survivor detected".to_string(),
1011 age: "5m".to_string(),
1012 },
1013 AlertRow {
1014 id: "ALRT-002".to_string(),
1015 priority: format_priority(Priority::High),
1016 status: format_alert_status(&AlertStatus::Acknowledged),
1017 survivor_id: "SURV-002".to_string(),
1018 title: "Delayed: Survivor needs attention".to_string(),
1019 age: "12m".to_string(),
1020 },
1021 ];
1022
1023 let mut filtered = alerts;
1024
1025 if args.pending {
1026 filtered.retain(|a| a.status.contains("Pending"));
1027 }
1028
1029 if let Some(limit) = args.limit {
1030 filtered.truncate(limit);
1031 }
1032
1033 println!("{}", "Alerts".bold().cyan());
1034 println!("{}", "=".repeat(100));
1035
1036 if filtered.is_empty() {
1037 println!("No alerts.");
1038 } else {
1039 let pending = filtered.iter().filter(|a| a.status.contains("Pending")).count();
1040 if pending > 0 {
1041 println!(
1042 "{} {} pending alert(s) require attention!",
1043 "[ALERT]".red().bold(),
1044 pending
1045 );
1046 println!();
1047 }
1048
1049 let table = Table::new(filtered).with(Style::rounded()).to_string();
1050 println!("{}", table);
1051 }
1052 }
1053 }
1054
1055 Ok(())
1056}
1057
1058async fn execute_export(args: ExportArgs) -> Result<()> {
1060 println!(
1061 "{} Exporting data to {}...",
1062 "[INFO]".blue(),
1063 args.output.display()
1064 );
1065
1066 #[derive(Serialize)]
1068 struct ExportData {
1069 exported_at: DateTime<Utc>,
1070 survivors: Vec<SurvivorExport>,
1071 zones: Vec<ZoneExport>,
1072 alerts: Vec<AlertExport>,
1073 }
1074
1075 #[derive(Serialize)]
1076 struct SurvivorExport {
1077 id: String,
1078 zone_id: String,
1079 triage_status: String,
1080 confidence: f64,
1081 location: Option<(f64, f64, f64)>,
1082 first_detected: DateTime<Utc>,
1083 last_updated: DateTime<Utc>,
1084 }
1085
1086 #[derive(Serialize)]
1087 struct ZoneExport {
1088 id: String,
1089 name: String,
1090 status: String,
1091 area: f64,
1092 scan_count: u32,
1093 }
1094
1095 #[derive(Serialize)]
1096 struct AlertExport {
1097 id: String,
1098 priority: String,
1099 status: String,
1100 survivor_id: String,
1101 created_at: DateTime<Utc>,
1102 }
1103
1104 let data = ExportData {
1105 exported_at: Utc::now(),
1106 survivors: vec![SurvivorExport {
1107 id: "SURV-001".to_string(),
1108 zone_id: "zone-001".to_string(),
1109 triage_status: "Immediate".to_string(),
1110 confidence: 0.92,
1111 location: Some((12.5, 8.3, -2.1)),
1112 first_detected: Utc::now() - chrono::Duration::minutes(15),
1113 last_updated: Utc::now() - chrono::Duration::seconds(30),
1114 }],
1115 zones: vec![ZoneExport {
1116 id: "zone-001".to_string(),
1117 name: "Building A - North Wing".to_string(),
1118 status: "Active".to_string(),
1119 area: 1500.0,
1120 scan_count: 42,
1121 }],
1122 alerts: vec![AlertExport {
1123 id: "ALRT-001".to_string(),
1124 priority: "Critical".to_string(),
1125 status: "Pending".to_string(),
1126 survivor_id: "SURV-001".to_string(),
1127 created_at: Utc::now() - chrono::Duration::minutes(5),
1128 }],
1129 };
1130
1131 match args.format {
1132 ExportFormat::Json => {
1133 let json = serde_json::to_string_pretty(&data)?;
1134 std::fs::write(&args.output, json)?;
1135 }
1136 ExportFormat::Csv => {
1137 let mut wtr = csv::Writer::from_path(&args.output)?;
1138 for survivor in &data.survivors {
1139 wtr.serialize(survivor)?;
1140 }
1141 wtr.flush()?;
1142 }
1143 }
1144
1145 println!(
1146 "{} Export complete: {}",
1147 "[OK]".green().bold(),
1148 args.output.display()
1149 );
1150
1151 Ok(())
1152}
1153
1154fn format_triage(status: &TriageStatus) -> String {
1160 match status {
1161 TriageStatus::Immediate => "IMMEDIATE (Red)".red().bold().to_string(),
1162 TriageStatus::Delayed => "DELAYED (Yellow)".yellow().bold().to_string(),
1163 TriageStatus::Minor => "MINOR (Green)".green().bold().to_string(),
1164 TriageStatus::Deceased => "DECEASED (Black)".dimmed().to_string(),
1165 TriageStatus::Unknown => "UNKNOWN".dimmed().to_string(),
1166 }
1167}
1168
1169fn format_zone_status(status: &ZoneStatus) -> String {
1171 match status {
1172 ZoneStatus::Active => "Active".green().to_string(),
1173 ZoneStatus::Paused => "Paused".yellow().to_string(),
1174 ZoneStatus::Complete => "Complete".blue().to_string(),
1175 ZoneStatus::Inaccessible => "Inaccessible".red().to_string(),
1176 ZoneStatus::Deactivated => "Deactivated".dimmed().to_string(),
1177 }
1178}
1179
1180fn format_priority(priority: Priority) -> String {
1182 match priority {
1183 Priority::Critical => "CRITICAL".red().bold().to_string(),
1184 Priority::High => "HIGH".bright_red().to_string(),
1185 Priority::Medium => "MEDIUM".yellow().to_string(),
1186 Priority::Low => "LOW".blue().to_string(),
1187 }
1188}
1189
1190fn format_alert_status(status: &AlertStatus) -> String {
1192 match status {
1193 AlertStatus::Pending => "Pending".red().to_string(),
1194 AlertStatus::Acknowledged => "Acknowledged".yellow().to_string(),
1195 AlertStatus::InProgress => "In Progress".blue().to_string(),
1196 AlertStatus::Resolved => "Resolved".green().to_string(),
1197 AlertStatus::Cancelled => "Cancelled".dimmed().to_string(),
1198 AlertStatus::Expired => "Expired".dimmed().to_string(),
1199 }
1200}
1201
1202#[cfg(test)]
1203mod tests {
1204 use super::*;
1205
1206 #[test]
1207 fn test_parse_rectangle_bounds() {
1208 let result = parse_bounds(&ZoneType::Rectangle, "0,0,10,20");
1209 assert!(result.is_ok());
1210 }
1211
1212 #[test]
1213 fn test_parse_circle_bounds() {
1214 let result = parse_bounds(&ZoneType::Circle, "5,5,10");
1215 assert!(result.is_ok());
1216 }
1217
1218 #[test]
1219 fn test_parse_invalid_bounds() {
1220 let result = parse_bounds(&ZoneType::Rectangle, "invalid");
1221 assert!(result.is_err());
1222 }
1223
1224 #[test]
1225 fn test_disaster_type_conversion() {
1226 let dt: DisasterType = DisasterTypeArg::Earthquake.into();
1227 assert!(matches!(dt, DisasterType::Earthquake));
1228 }
1229
1230 #[test]
1231 fn test_triage_filter_conversion() {
1232 let ts: TriageStatus = TriageFilter::Immediate.into();
1233 assert!(matches!(ts, TriageStatus::Immediate));
1234 }
1235}