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 domain::alert::AlertStatus, DisasterConfig, DisasterType, Priority, ScanZone, TriageStatus,
20 ZoneBounds, ZoneStatus,
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!("{} Starting survivor scan...", "[MAT]".bright_cyan().bold());
456 println!();
457
458 println!("{}", "Configuration:".bold());
460 println!(" {} {:?}", "Disaster Type:".dimmed(), args.disaster_type);
461 println!(" {} {:.1}", "Sensitivity:".dimmed(), args.sensitivity);
462 println!(" {} {:.1}m", "Max Depth:".dimmed(), args.max_depth);
463 println!(
464 " {} {}",
465 "Continuous:".dimmed(),
466 if args.continuous { "Yes" } else { "No" }
467 );
468 if args.continuous {
469 println!(" {} {}ms", "Interval:".dimmed(), args.interval);
470 }
471 if let Some(ref zone) = args.zone {
472 println!(" {} {}", "Zone:".dimmed(), zone);
473 }
474 println!();
475
476 if args.simulate {
477 println!(
478 "{} Running in simulation mode",
479 "[SIMULATION]".yellow().bold()
480 );
481 println!();
482
483 simulate_scan_output().await?;
485 } else {
486 let config = DisasterConfig::builder()
488 .disaster_type(args.disaster_type.into())
489 .sensitivity(args.sensitivity)
490 .max_depth(args.max_depth)
491 .continuous_monitoring(args.continuous)
492 .scan_interval_ms(args.interval)
493 .build();
494
495 println!(
496 "{} Initializing detection pipeline with config: {:?}",
497 "[INFO]".blue(),
498 config.disaster_type
499 );
500 println!("{} Waiting for hardware connection...", "[INFO]".blue());
501 println!();
502 println!(
503 "{} No hardware detected. Use --simulate for demo mode.",
504 "[WARN]".yellow()
505 );
506 }
507
508 Ok(())
509}
510
511async fn simulate_scan_output() -> Result<()> {
513 use indicatif::{ProgressBar, ProgressStyle};
514 use std::time::Duration;
515
516 let pb = ProgressBar::new(100);
517 pb.set_style(
518 ProgressStyle::default_bar()
519 .template(
520 "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})",
521 )?
522 .progress_chars("#>-"),
523 );
524
525 for i in 0..100 {
526 pb.set_position(i);
527 tokio::time::sleep(Duration::from_millis(50)).await;
528
529 if i == 25 {
531 pb.suspend(|| {
532 println!();
533 print_detection(
534 "SURV-001",
535 "Zone A",
536 TriageStatus::Immediate,
537 0.92,
538 Some((12.5, 8.3, -2.1)),
539 );
540 });
541 }
542 if i == 55 {
543 pb.suspend(|| {
544 print_detection(
545 "SURV-002",
546 "Zone A",
547 TriageStatus::Delayed,
548 0.78,
549 Some((15.2, 10.1, -1.5)),
550 );
551 });
552 }
553 if i == 80 {
554 pb.suspend(|| {
555 print_detection(
556 "SURV-003",
557 "Zone B",
558 TriageStatus::Minor,
559 0.85,
560 Some((8.7, 22.4, -0.8)),
561 );
562 });
563 }
564 }
565
566 pb.finish_with_message("Scan complete");
567 println!();
568 println!(
569 "{} Scan complete. Detected {} survivors.",
570 "[MAT]".bright_cyan().bold(),
571 "3".green().bold()
572 );
573 println!(
574 " {} 1 {} 1 {} 1",
575 "IMMEDIATE:".red().bold(),
576 "DELAYED:".yellow().bold(),
577 "MINOR:".green().bold(),
578 );
579
580 Ok(())
581}
582
583fn print_detection(
585 id: &str,
586 zone: &str,
587 triage: TriageStatus,
588 confidence: f64,
589 location: Option<(f64, f64, f64)>,
590) {
591 let triage_str = format_triage(&triage);
592 let location_str = location
593 .map(|(x, y, z)| format!("({:.1}, {:.1}, {:.1})", x, y, z))
594 .unwrap_or_else(|| "Unknown".to_string());
595
596 println!(
597 "{} {} detected in {} - {} | Confidence: {:.0}% | Location: {}",
598 format!("[{}]", triage_str).bold(),
599 id.cyan(),
600 zone,
601 triage_str,
602 confidence * 100.0,
603 location_str.dimmed()
604 );
605}
606
607async fn execute_status(args: StatusArgs) -> Result<()> {
609 let status = SystemStatus {
611 scanning: false,
612 active_zones: 0,
613 total_zones: 0,
614 survivors_detected: 0,
615 critical_survivors: 0,
616 pending_alerts: 0,
617 disaster_type: "Not configured".to_string(),
618 uptime: "N/A".to_string(),
619 };
620
621 match args.format {
622 OutputFormat::Json => {
623 println!("{}", serde_json::to_string_pretty(&status)?);
624 }
625 OutputFormat::Compact => {
626 println!(
627 "scanning={} zones={}/{} survivors={} critical={} alerts={}",
628 status.scanning,
629 status.active_zones,
630 status.total_zones,
631 status.survivors_detected,
632 status.critical_survivors,
633 status.pending_alerts
634 );
635 }
636 OutputFormat::Table => {
637 println!("{}", "MAT System Status".bold().cyan());
638 println!("{}", "=".repeat(50));
639 println!(
640 " {} {}",
641 "Scanning:".dimmed(),
642 if status.scanning {
643 "Active".green()
644 } else {
645 "Inactive".red()
646 }
647 );
648 println!(
649 " {} {}/{}",
650 "Zones:".dimmed(),
651 status.active_zones,
652 status.total_zones
653 );
654 println!(" {} {}", "Disaster Type:".dimmed(), status.disaster_type);
655 println!(
656 " {} {}",
657 "Survivors Detected:".dimmed(),
658 status.survivors_detected
659 );
660 println!(
661 " {} {}",
662 "Critical (Immediate):".dimmed(),
663 if status.critical_survivors > 0 {
664 status.critical_survivors.to_string().red().bold()
665 } else {
666 status.critical_survivors.to_string().normal()
667 }
668 );
669 println!(
670 " {} {}",
671 "Pending Alerts:".dimmed(),
672 if status.pending_alerts > 0 {
673 status.pending_alerts.to_string().yellow().bold()
674 } else {
675 status.pending_alerts.to_string().normal()
676 }
677 );
678 println!(" {} {}", "Uptime:".dimmed(), status.uptime);
679 println!();
680
681 if !status.scanning {
682 println!(
683 "{} No active scan. Run '{}' to start.",
684 "[INFO]".blue(),
685 "wifi-densepose mat scan".green()
686 );
687 }
688 }
689 }
690
691 Ok(())
692}
693
694async fn execute_zones(args: ZonesArgs) -> Result<()> {
696 match args.command {
697 ZonesCommand::List { active } => {
698 println!("{}", "Scan Zones".bold().cyan());
699 println!("{}", "=".repeat(80));
700
701 let zones = vec![
703 ZoneRow {
704 id: "zone-001".to_string(),
705 name: "Building A - North Wing".to_string(),
706 status: format_zone_status(&ZoneStatus::Active),
707 area: "1500.0".to_string(),
708 scan_count: 42,
709 detections: 3,
710 last_scan: "2 min ago".to_string(),
711 },
712 ZoneRow {
713 id: "zone-002".to_string(),
714 name: "Building A - South Wing".to_string(),
715 status: format_zone_status(&ZoneStatus::Paused),
716 area: "1200.0".to_string(),
717 scan_count: 28,
718 detections: 1,
719 last_scan: "15 min ago".to_string(),
720 },
721 ];
722
723 let filtered: Vec<_> = if active {
724 zones
725 .into_iter()
726 .filter(|z| z.status.contains("Active"))
727 .collect()
728 } else {
729 zones
730 };
731
732 if filtered.is_empty() {
733 println!("No zones configured. Use 'wifi-densepose mat zones add' to create one.");
734 } else {
735 let table = Table::new(filtered).with(Style::rounded()).to_string();
736 println!("{}", table);
737 }
738 }
739 ZonesCommand::Add {
740 name,
741 zone_type,
742 bounds,
743 sensitivity,
744 } => {
745 let bounds_parsed: Result<ZoneBounds, _> = parse_bounds(&zone_type, &bounds);
747 match bounds_parsed {
748 Ok(zone_bounds) => {
749 let zone = if let Some(sens) = sensitivity {
750 let params = wifi_densepose_mat::ScanParameters {
751 sensitivity: sens,
752 ..Default::default()
753 };
754 ScanZone::with_parameters(&name, zone_bounds, params)
755 } else {
756 ScanZone::new(&name, zone_bounds)
757 };
758
759 println!(
760 "{} Zone '{}' created with ID: {}",
761 "[OK]".green().bold(),
762 name.cyan(),
763 zone.id()
764 );
765 println!(" Area: {:.1} m2", zone.area());
766 }
767 Err(e) => {
768 eprintln!("{} Failed to parse bounds: {}", "[ERROR]".red().bold(), e);
769 eprintln!(" Expected format for rectangle: min_x,min_y,max_x,max_y");
770 eprintln!(" Expected format for circle: center_x,center_y,radius");
771 return Err(e);
772 }
773 }
774 }
775 ZonesCommand::Remove { zone, force } => {
776 if !force {
777 println!(
778 "{} This will remove zone '{}' and stop any active scans.",
779 "[WARN]".yellow().bold(),
780 zone
781 );
782 println!("Use --force to confirm.");
783 } else {
784 println!("{} Zone '{}' removed.", "[OK]".green().bold(), zone.cyan());
785 }
786 }
787 ZonesCommand::Pause { zone } => {
788 println!("{} Zone '{}' paused.", "[OK]".green().bold(), zone.cyan());
789 }
790 ZonesCommand::Resume { zone } => {
791 println!("{} Zone '{}' resumed.", "[OK]".green().bold(), zone.cyan());
792 }
793 }
794
795 Ok(())
796}
797
798fn parse_bounds(zone_type: &ZoneType, bounds: &str) -> Result<ZoneBounds> {
800 let parts: Vec<f64> = bounds
801 .split(',')
802 .map(|s| s.trim().parse::<f64>())
803 .collect::<std::result::Result<Vec<_>, _>>()
804 .context("Failed to parse bounds values as numbers")?;
805
806 match zone_type {
807 ZoneType::Rectangle => {
808 if parts.len() != 4 {
809 anyhow::bail!(
810 "Rectangle requires 4 values: min_x,min_y,max_x,max_y (got {})",
811 parts.len()
812 );
813 }
814 Ok(ZoneBounds::rectangle(
815 parts[0], parts[1], parts[2], parts[3],
816 ))
817 }
818 ZoneType::Circle => {
819 if parts.len() != 3 {
820 anyhow::bail!(
821 "Circle requires 3 values: center_x,center_y,radius (got {})",
822 parts.len()
823 );
824 }
825 Ok(ZoneBounds::circle(parts[0], parts[1], parts[2]))
826 }
827 }
828}
829
830async fn execute_survivors(args: SurvivorsArgs) -> Result<()> {
832 let survivors = vec![
834 SurvivorRow {
835 id: "SURV-001".to_string(),
836 zone: "Zone A".to_string(),
837 triage: format_triage(&TriageStatus::Immediate),
838 status: "Active".green().to_string(),
839 confidence: "92%".to_string(),
840 location: "(12.5, 8.3, -2.1)".to_string(),
841 last_update: "30s ago".to_string(),
842 },
843 SurvivorRow {
844 id: "SURV-002".to_string(),
845 zone: "Zone A".to_string(),
846 triage: format_triage(&TriageStatus::Delayed),
847 status: "Active".green().to_string(),
848 confidence: "78%".to_string(),
849 location: "(15.2, 10.1, -1.5)".to_string(),
850 last_update: "1m ago".to_string(),
851 },
852 SurvivorRow {
853 id: "SURV-003".to_string(),
854 zone: "Zone B".to_string(),
855 triage: format_triage(&TriageStatus::Minor),
856 status: "Active".green().to_string(),
857 confidence: "85%".to_string(),
858 location: "(8.7, 22.4, -0.8)".to_string(),
859 last_update: "2m ago".to_string(),
860 },
861 ];
862
863 let mut filtered = survivors;
865
866 if let Some(ref triage_filter) = args.triage {
867 let status: TriageStatus = triage_filter.clone().into();
868 let status_str = format_triage(&status);
869 filtered.retain(|s| s.triage == status_str);
870 }
871
872 if let Some(ref zone) = args.zone {
873 filtered.retain(|s| s.zone.contains(zone));
874 }
875
876 if let Some(limit) = args.limit {
877 filtered.truncate(limit);
878 }
879
880 match args.format {
881 OutputFormat::Json => {
882 println!("{}", serde_json::to_string_pretty(&filtered)?);
883 }
884 OutputFormat::Compact => {
885 for s in &filtered {
886 println!(
887 "{}\t{}\t{}\t{}\t{}",
888 s.id, s.zone, s.triage, s.confidence, s.location
889 );
890 }
891 }
892 OutputFormat::Table => {
893 println!("{}", "Detected Survivors".bold().cyan());
894 println!("{}", "=".repeat(100));
895
896 if filtered.is_empty() {
897 println!("No survivors detected matching criteria.");
898 } else {
899 let immediate = filtered
901 .iter()
902 .filter(|s| s.triage.contains("IMMEDIATE"))
903 .count();
904 let delayed = filtered
905 .iter()
906 .filter(|s| s.triage.contains("DELAYED"))
907 .count();
908 let minor = filtered
909 .iter()
910 .filter(|s| s.triage.contains("MINOR"))
911 .count();
912
913 println!(
914 "Total: {} | {} {} | {} {} | {} {}",
915 filtered.len().to_string().bold(),
916 "IMMEDIATE:".red().bold(),
917 immediate,
918 "DELAYED:".yellow().bold(),
919 delayed,
920 "MINOR:".green().bold(),
921 minor
922 );
923 println!();
924
925 let table = Table::new(filtered).with(Style::rounded()).to_string();
926 println!("{}", table);
927 }
928 }
929 }
930
931 Ok(())
932}
933
934async fn execute_alerts(args: AlertsArgs) -> Result<()> {
936 match args.command {
937 Some(AlertsCommand::Ack { alert_id, by }) => {
938 println!(
939 "{} Alert {} acknowledged by {}",
940 "[OK]".green().bold(),
941 alert_id.cyan(),
942 by
943 );
944 }
945 Some(AlertsCommand::Resolve {
946 alert_id,
947 resolution,
948 notes,
949 }) => {
950 println!(
951 "{} Alert {} resolved as {:?}",
952 "[OK]".green().bold(),
953 alert_id.cyan(),
954 resolution
955 );
956 if let Some(notes) = notes {
957 println!(" Notes: {}", notes);
958 }
959 }
960 Some(AlertsCommand::Escalate { alert_id }) => {
961 println!(
962 "{} Alert {} escalated to higher priority",
963 "[OK]".green().bold(),
964 alert_id.cyan()
965 );
966 }
967 Some(AlertsCommand::List) | None => {
968 let alerts = vec![
970 AlertRow {
971 id: "ALRT-001".to_string(),
972 priority: format_priority(Priority::Critical),
973 status: format_alert_status(&AlertStatus::Pending),
974 survivor_id: "SURV-001".to_string(),
975 title: "Immediate: Survivor detected".to_string(),
976 age: "5m".to_string(),
977 },
978 AlertRow {
979 id: "ALRT-002".to_string(),
980 priority: format_priority(Priority::High),
981 status: format_alert_status(&AlertStatus::Acknowledged),
982 survivor_id: "SURV-002".to_string(),
983 title: "Delayed: Survivor needs attention".to_string(),
984 age: "12m".to_string(),
985 },
986 ];
987
988 let mut filtered = alerts;
989
990 if args.pending {
991 filtered.retain(|a| a.status.contains("Pending"));
992 }
993
994 if let Some(limit) = args.limit {
995 filtered.truncate(limit);
996 }
997
998 println!("{}", "Alerts".bold().cyan());
999 println!("{}", "=".repeat(100));
1000
1001 if filtered.is_empty() {
1002 println!("No alerts.");
1003 } else {
1004 let pending = filtered
1005 .iter()
1006 .filter(|a| a.status.contains("Pending"))
1007 .count();
1008 if pending > 0 {
1009 println!(
1010 "{} {} pending alert(s) require attention!",
1011 "[ALERT]".red().bold(),
1012 pending
1013 );
1014 println!();
1015 }
1016
1017 let table = Table::new(filtered).with(Style::rounded()).to_string();
1018 println!("{}", table);
1019 }
1020 }
1021 }
1022
1023 Ok(())
1024}
1025
1026async fn execute_export(args: ExportArgs) -> Result<()> {
1028 println!(
1029 "{} Exporting data to {}...",
1030 "[INFO]".blue(),
1031 args.output.display()
1032 );
1033
1034 #[derive(Serialize)]
1036 struct ExportData {
1037 exported_at: DateTime<Utc>,
1038 survivors: Vec<SurvivorExport>,
1039 zones: Vec<ZoneExport>,
1040 alerts: Vec<AlertExport>,
1041 }
1042
1043 #[derive(Serialize)]
1044 struct SurvivorExport {
1045 id: String,
1046 zone_id: String,
1047 triage_status: String,
1048 confidence: f64,
1049 location: Option<(f64, f64, f64)>,
1050 first_detected: DateTime<Utc>,
1051 last_updated: DateTime<Utc>,
1052 }
1053
1054 #[derive(Serialize)]
1055 struct ZoneExport {
1056 id: String,
1057 name: String,
1058 status: String,
1059 area: f64,
1060 scan_count: u32,
1061 }
1062
1063 #[derive(Serialize)]
1064 struct AlertExport {
1065 id: String,
1066 priority: String,
1067 status: String,
1068 survivor_id: String,
1069 created_at: DateTime<Utc>,
1070 }
1071
1072 let data = ExportData {
1073 exported_at: Utc::now(),
1074 survivors: vec![SurvivorExport {
1075 id: "SURV-001".to_string(),
1076 zone_id: "zone-001".to_string(),
1077 triage_status: "Immediate".to_string(),
1078 confidence: 0.92,
1079 location: Some((12.5, 8.3, -2.1)),
1080 first_detected: Utc::now() - chrono::Duration::minutes(15),
1081 last_updated: Utc::now() - chrono::Duration::seconds(30),
1082 }],
1083 zones: vec![ZoneExport {
1084 id: "zone-001".to_string(),
1085 name: "Building A - North Wing".to_string(),
1086 status: "Active".to_string(),
1087 area: 1500.0,
1088 scan_count: 42,
1089 }],
1090 alerts: vec![AlertExport {
1091 id: "ALRT-001".to_string(),
1092 priority: "Critical".to_string(),
1093 status: "Pending".to_string(),
1094 survivor_id: "SURV-001".to_string(),
1095 created_at: Utc::now() - chrono::Duration::minutes(5),
1096 }],
1097 };
1098
1099 match args.format {
1100 ExportFormat::Json => {
1101 let json = serde_json::to_string_pretty(&data)?;
1102 std::fs::write(&args.output, json)?;
1103 }
1104 ExportFormat::Csv => {
1105 let mut wtr = csv::Writer::from_path(&args.output)?;
1106 for survivor in &data.survivors {
1107 wtr.serialize(survivor)?;
1108 }
1109 wtr.flush()?;
1110 }
1111 }
1112
1113 println!(
1114 "{} Export complete: {}",
1115 "[OK]".green().bold(),
1116 args.output.display()
1117 );
1118
1119 Ok(())
1120}
1121
1122fn format_triage(status: &TriageStatus) -> String {
1128 match status {
1129 TriageStatus::Immediate => "IMMEDIATE (Red)".red().bold().to_string(),
1130 TriageStatus::Delayed => "DELAYED (Yellow)".yellow().bold().to_string(),
1131 TriageStatus::Minor => "MINOR (Green)".green().bold().to_string(),
1132 TriageStatus::Deceased => "DECEASED (Black)".dimmed().to_string(),
1133 TriageStatus::Unknown => "UNKNOWN".dimmed().to_string(),
1134 }
1135}
1136
1137fn format_zone_status(status: &ZoneStatus) -> String {
1139 match status {
1140 ZoneStatus::Active => "Active".green().to_string(),
1141 ZoneStatus::Paused => "Paused".yellow().to_string(),
1142 ZoneStatus::Complete => "Complete".blue().to_string(),
1143 ZoneStatus::Inaccessible => "Inaccessible".red().to_string(),
1144 ZoneStatus::Deactivated => "Deactivated".dimmed().to_string(),
1145 }
1146}
1147
1148fn format_priority(priority: Priority) -> String {
1150 match priority {
1151 Priority::Critical => "CRITICAL".red().bold().to_string(),
1152 Priority::High => "HIGH".bright_red().to_string(),
1153 Priority::Medium => "MEDIUM".yellow().to_string(),
1154 Priority::Low => "LOW".blue().to_string(),
1155 }
1156}
1157
1158fn format_alert_status(status: &AlertStatus) -> String {
1160 match status {
1161 AlertStatus::Pending => "Pending".red().to_string(),
1162 AlertStatus::Acknowledged => "Acknowledged".yellow().to_string(),
1163 AlertStatus::InProgress => "In Progress".blue().to_string(),
1164 AlertStatus::Resolved => "Resolved".green().to_string(),
1165 AlertStatus::Cancelled => "Cancelled".dimmed().to_string(),
1166 AlertStatus::Expired => "Expired".dimmed().to_string(),
1167 }
1168}
1169
1170#[cfg(test)]
1171mod tests {
1172 use super::*;
1173
1174 #[test]
1175 fn test_parse_rectangle_bounds() {
1176 let result = parse_bounds(&ZoneType::Rectangle, "0,0,10,20");
1177 assert!(result.is_ok());
1178 }
1179
1180 #[test]
1181 fn test_parse_circle_bounds() {
1182 let result = parse_bounds(&ZoneType::Circle, "5,5,10");
1183 assert!(result.is_ok());
1184 }
1185
1186 #[test]
1187 fn test_parse_invalid_bounds() {
1188 let result = parse_bounds(&ZoneType::Rectangle, "invalid");
1189 assert!(result.is_err());
1190 }
1191
1192 #[test]
1193 fn test_disaster_type_conversion() {
1194 let dt: DisasterType = DisasterTypeArg::Earthquake.into();
1195 assert!(matches!(dt, DisasterType::Earthquake));
1196 }
1197
1198 #[test]
1199 fn test_triage_filter_conversion() {
1200 let ts: TriageStatus = TriageFilter::Immediate.into();
1201 assert!(matches!(ts, TriageStatus::Immediate));
1202 }
1203}