1use crate::error::{CoreError, CoreResult};
58use std::collections::{HashMap, VecDeque};
59use std::sync::{Arc, Mutex, RwLock};
60use std::time::{Duration, Instant, SystemTime};
61
62use serde::{Deserialize, Serialize};
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct DashboardConfig {
67 pub title: String,
69 pub refresh_interval: Duration,
71 pub retention_period: Duration,
73 pub max_data_points: usize,
75 pub enable_web_interface: bool,
77 pub web_port: u16,
79 pub enable_rest_api: bool,
81 pub api_token: Option<String>,
83 pub enable_alerts: bool,
85 pub theme: DashboardTheme,
87 pub auto_save_interval: Duration,
89}
90
91impl Default for DashboardConfig {
92 fn default() -> Self {
93 Self {
94 title: "Performance Dashboard".to_string(),
95 refresh_interval: Duration::from_secs(5),
96 retention_period: Duration::from_secs(7 * 24 * 60 * 60), max_data_points: 1000,
98 enable_web_interface: true,
99 web_port: 8080,
100 enable_rest_api: true,
101 api_token: None,
102 enable_alerts: true,
103 theme: DashboardTheme::Dark,
104 auto_save_interval: Duration::from_secs(60),
105 }
106 }
107}
108
109impl DashboardConfig {
110 pub fn with_title(mut self, title: &str) -> Self {
112 self.title = title.to_string();
113 self
114 }
115
116 pub fn with_refresh_interval(mut self, interval: Duration) -> Self {
118 self.refresh_interval = interval;
119 self
120 }
121
122 pub fn with_retention_period(mut self, period: Duration) -> Self {
124 self.retention_period = period;
125 self
126 }
127
128 pub fn with_web_interface(mut self, port: u16) -> Self {
130 self.enable_web_interface = true;
131 self.web_port = port;
132 self
133 }
134
135 pub fn with_api_token(mut self, token: &str) -> Self {
137 self.api_token = Some(token.to_string());
138 self
139 }
140
141 pub fn with_theme(mut self, theme: DashboardTheme) -> Self {
143 self.theme = theme;
144 self
145 }
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
150pub enum DashboardTheme {
151 Light,
153 Dark,
155 HighContrast,
157 Custom,
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
163pub enum ChartType {
164 LineChart,
166 AreaChart,
168 BarChart,
170 GaugeChart,
172 PieChart,
174 Heatmap,
176 ScatterPlot,
178 Histogram,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184pub enum MetricSource {
185 SystemCpu,
187 SystemMemory,
189 NetworkIO,
191 DiskIO,
193 Application(String),
195 Custom(String),
197 Database(String),
199 Cache(String),
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct Widget {
206 pub id: String,
208 pub title: String,
210 pub chart_type: ChartType,
212 pub metric_source: MetricSource,
214 pub layout: WidgetLayout,
216 pub alert_config: Option<AlertConfig>,
218 pub refresh_interval: Option<Duration>,
220 pub color_scheme: Vec<String>,
222 pub display_options: DisplayOptions,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct WidgetLayout {
229 pub x: u32,
231 pub y: u32,
233 pub width: u32,
235 pub height: u32,
237}
238
239impl Default for WidgetLayout {
240 fn default() -> Self {
241 Self {
242 x: 0,
243 y: 0,
244 width: 4,
245 height: 3,
246 }
247 }
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct AlertConfig {
253 pub threshold: f64,
255 pub condition: AlertCondition,
257 pub severity: AlertSeverity,
259 pub notification_channels: Vec<NotificationChannel>,
261 pub cooldown_period: Duration,
263}
264
265#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
267pub enum AlertCondition {
268 GreaterThan,
270 LessThan,
272 EqualTo,
274 NotEqualTo,
276 RateOfChange,
278}
279
280#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
282pub enum AlertSeverity {
283 Info,
285 Warning,
287 Error,
289 Critical,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize)]
295pub enum NotificationChannel {
296 Email(String),
298 Slack(String),
300 Webhook(String),
302 Console,
304 File(String),
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct DisplayOptions {
311 pub show_labels: bool,
313 pub show_grid: bool,
315 pub show_legend: bool,
317 pub enable_animation: bool,
319 pub number_format: NumberFormat,
321 pub time_format: String,
323}
324
325impl Default for DisplayOptions {
326 fn default() -> Self {
327 Self {
328 show_labels: true,
329 show_grid: true,
330 show_legend: true,
331 enable_animation: true,
332 number_format: NumberFormat::Auto,
333 time_format: "%H:%M:%S".to_string(),
334 }
335 }
336}
337
338#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
340pub enum NumberFormat {
341 Auto,
343 Integer,
345 Decimal(u8),
347 Percentage,
349 Scientific,
351 Bytes,
353}
354
355impl Widget {
356 pub fn new() -> Self {
358 Self {
359 id: uuid::Uuid::new_v4().to_string(),
360 title: "New Widget".to_string(),
361 chart_type: ChartType::LineChart,
362 metric_source: MetricSource::SystemCpu,
363 layout: WidgetLayout::default(),
364 alert_config: None,
365 refresh_interval: None,
366 color_scheme: vec![
367 "#007acc".to_string(),
368 "#ff6b35".to_string(),
369 "#00b894".to_string(),
370 "#fdcb6e".to_string(),
371 "#e84393".to_string(),
372 ],
373 display_options: DisplayOptions::default(),
374 }
375 }
376
377 pub fn with_title(mut self, title: &str) -> Self {
379 self.title = title.to_string();
380 self
381 }
382
383 pub fn with_chart_type(mut self, charttype: ChartType) -> Self {
385 self.chart_type = charttype;
386 self
387 }
388
389 pub fn with_metric_source(mut self, source: MetricSource) -> Self {
391 self.metric_source = source;
392 self
393 }
394
395 pub const fn with_layout(mut self, x: u32, y: u32, width: u32, height: u32) -> Self {
397 self.layout = WidgetLayout {
398 x,
399 y,
400 width,
401 height,
402 };
403 self
404 }
405
406 pub fn with_alert_threshold(mut self, threshold: f64) -> Self {
408 self.alert_config = Some(AlertConfig {
409 threshold,
410 condition: AlertCondition::GreaterThan,
411 severity: AlertSeverity::Warning,
412 notification_channels: vec![NotificationChannel::Console],
413 cooldown_period: Duration::from_secs(300), });
415 self
416 }
417
418 pub fn with_refresh_interval(mut self, interval: Duration) -> Self {
420 self.refresh_interval = Some(interval);
421 self
422 }
423
424 pub fn with_colors(mut self, colors: Vec<&str>) -> Self {
426 self.color_scheme = colors.into_iter().map(|s| s.to_string()).collect();
427 self
428 }
429}
430
431impl Default for Widget {
432 fn default() -> Self {
433 Self::new()
434 }
435}
436
437#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct MetricDataPoint {
440 pub timestamp: SystemTime,
442 pub value: f64,
444 pub metadata: HashMap<String, String>,
446}
447
448#[derive(Debug, Clone, Serialize, Deserialize)]
450pub struct MetricTimeSeries {
451 pub name: String,
453 pub data_points: VecDeque<MetricDataPoint>,
455 pub last_update: SystemTime,
457}
458
459impl MetricTimeSeries {
460 pub fn new(name: &str) -> Self {
462 Self {
463 name: name.to_string(),
464 data_points: VecDeque::new(),
465 last_update: SystemTime::now(),
466 }
467 }
468
469 pub fn add_point(&mut self, value: f64, metadata: Option<HashMap<String, String>>) {
471 let point = MetricDataPoint {
472 timestamp: SystemTime::now(),
473 value,
474 metadata: metadata.unwrap_or_default(),
475 };
476
477 self.data_points.push_back(point);
478 self.last_update = SystemTime::now();
479 }
480
481 pub fn latest_value(&self) -> Option<f64> {
483 self.data_points.back().map(|p| p.value)
484 }
485
486 pub fn average_value(&self, duration: Duration) -> Option<f64> {
488 let cutoff = SystemTime::now() - duration;
489 let values: Vec<f64> = self
490 .data_points
491 .iter()
492 .filter(|p| p.timestamp >= cutoff)
493 .map(|p| p.value)
494 .collect();
495
496 if values.is_empty() {
497 None
498 } else {
499 Some(values.iter().sum::<f64>() / values.len() as f64)
500 }
501 }
502
503 pub fn cleanup(&mut self, retention_period: Duration, maxpoints: usize) {
505 let cutoff = SystemTime::now() - retention_period;
506
507 while let Some(front) = self.data_points.front() {
509 if front.timestamp < cutoff {
510 self.data_points.pop_front();
511 } else {
512 break;
513 }
514 }
515
516 while self.data_points.len() > maxpoints {
518 self.data_points.pop_front();
519 }
520 }
521}
522
523#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct DashboardAlert {
526 pub id: String,
528 pub widget_id: String,
530 pub message: String,
532 pub severity: AlertSeverity,
534 pub triggered_at: SystemTime,
536 pub current_value: f64,
538 pub threshold_value: f64,
540 pub status: AlertStatus,
542}
543
544#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
546pub enum AlertStatus {
547 Active,
549 Acknowledged,
551 Resolved,
553}
554
555pub struct PerformanceDashboard {
557 config: DashboardConfig,
559 widgets: HashMap<String, Widget>,
561 metrics: Arc<RwLock<HashMap<String, MetricTimeSeries>>>,
563 alerts: Arc<Mutex<HashMap<String, DashboardAlert>>>,
565 state: DashboardState,
567 last_update: Instant,
569 web_server_handle: Option<WebServerHandle>,
571}
572
573#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
575pub enum DashboardState {
576 Stopped,
578 Running,
580 Paused,
582 Error,
584}
585
586pub struct WebServerHandle {
588 pub address: String,
590 pub port: u16,
592 pub running: Arc<Mutex<bool>>,
594}
595
596impl PerformanceDashboard {
597 pub fn new(config: DashboardConfig) -> CoreResult<Self> {
599 Ok(Self {
600 config,
601 widgets: HashMap::new(),
602 metrics: Arc::new(RwLock::new(HashMap::new())),
603 alerts: Arc::new(Mutex::new(HashMap::new())),
604 state: DashboardState::Stopped,
605 last_update: Instant::now(),
606 web_server_handle: None,
607 })
608 }
609
610 pub fn add_widget(&mut self, widget: Widget) -> CoreResult<String> {
612 let widget_id = widget.id.clone();
613 self.widgets.insert(widget_id.clone(), widget);
614
615 let metrics_name = format!("widget_{widget_id}");
617 if let Ok(mut metrics) = self.metrics.write() {
618 metrics.insert(metrics_name, MetricTimeSeries::new(&widget_id));
619 }
620
621 Ok(widget_id)
622 }
623
624 pub fn remove_widget(&mut self, widgetid: &str) -> CoreResult<()> {
626 self.widgets.remove(widgetid);
627
628 let metrics_name = format!("widget_{widgetid}");
630 if let Ok(mut metrics) = self.metrics.write() {
631 metrics.remove(&metrics_name);
632 }
633
634 Ok(())
635 }
636
637 pub fn start(&mut self) -> CoreResult<()> {
639 if self.state == DashboardState::Running {
640 return Ok(());
641 }
642
643 if self.config.enable_web_interface {
645 self.start_web_server()?;
646 }
647
648 self.state = DashboardState::Running;
649 self.last_update = Instant::now();
650
651 Ok(())
652 }
653
654 pub fn stop(&mut self) -> CoreResult<()> {
656 if let Some(ref handle) = self.web_server_handle {
658 if let Ok(mut running) = handle.running.lock() {
659 *running = false;
660 }
661 }
662
663 self.state = DashboardState::Stopped;
664 Ok(())
665 }
666
667 pub fn update_metric(&mut self, source: &MetricSource, value: f64) -> CoreResult<()> {
669 let metricname = self.metric_source_to_name(source);
670
671 if let Ok(mut metrics) = self.metrics.write() {
672 let time_series = metrics
673 .entry(metricname.clone())
674 .or_insert_with(|| MetricTimeSeries::new(&metricname));
675
676 time_series.add_point(value, None);
677
678 time_series.cleanup(self.config.retention_period, self.config.max_data_points);
680 }
681
682 self.check_alerts(&metricname, value)?;
684
685 self.last_update = Instant::now();
686 Ok(())
687 }
688
689 pub fn get_metrics(&self) -> CoreResult<HashMap<String, MetricTimeSeries>> {
691 self.metrics
692 .read()
693 .map(|metrics| metrics.clone())
694 .map_err(|_| CoreError::from(std::io::Error::other("Failed to read metrics")))
695 }
696
697 pub fn get_statistics(&self) -> DashboardStatistics {
699 let metrics = self.metrics.read().expect("Operation failed");
700 let alerts = self.alerts.lock().expect("Operation failed");
701
702 DashboardStatistics {
703 total_widgets: self.widgets.len(),
704 total_metrics: metrics.len(),
705 active_alerts: alerts
706 .values()
707 .filter(|a| a.status == AlertStatus::Active)
708 .count(),
709 last_update: self.last_update,
710 uptime: self.last_update.elapsed(),
711 state: self.state,
712 }
713 }
714
715 pub fn export_config(&self) -> CoreResult<String> {
717 {
718 let export_data = DashboardExport {
719 config: self.config.clone(),
720 widgets: self.widgets.values().cloned().collect(),
721 created_at: SystemTime::now(),
722 };
723
724 serde_json::to_string_pretty(&export_data).map_err(|e| {
725 CoreError::from(std::io::Error::other(format!(
726 "Failed to serialize dashboard config: {e}"
727 )))
728 })
729 }
730 #[cfg(not(feature = "serde"))]
731 {
732 Ok(format!("title: {}", self.config.title))
733 }
734 }
735
736 pub fn import_configuration(&mut self, configjson: &str) -> CoreResult<()> {
738 {
739 let import_data: DashboardExport = serde_json::from_str(configjson).map_err(|e| {
740 CoreError::from(std::io::Error::other(format!(
741 "Failed to parse dashboard config: {e}"
742 )))
743 })?;
744
745 self.config = import_data.config;
746 self.widgets.clear();
747
748 for widget in import_data.widgets {
749 self.widgets.insert(widget.id.clone(), widget);
750 }
751
752 Ok(())
753 }
754 #[cfg(not(feature = "serde"))]
755 {
756 let _ = config_json; Err(CoreError::from(std::io::Error::other(
758 "Serde feature not enabled for configuration import",
759 )))
760 }
761 }
762
763 fn check_alerts(&self, metricname: &str, value: f64) -> CoreResult<()> {
765 for widget in self.widgets.values() {
766 if let Some(ref alert_config) = widget.alert_config {
767 let widget_metric = self.metric_source_to_name(&widget.metric_source);
768
769 if widget_metric == metricname {
770 let triggered = match alert_config.condition {
771 AlertCondition::GreaterThan => value > alert_config.threshold,
772 AlertCondition::LessThan => value < alert_config.threshold,
773 AlertCondition::EqualTo => {
774 (value - alert_config.threshold).abs() < f64::EPSILON
775 }
776 AlertCondition::NotEqualTo => {
777 (value - alert_config.threshold).abs() > f64::EPSILON
778 }
779 AlertCondition::RateOfChange => {
780 false
782 }
783 };
784
785 if triggered {
786 let alert = DashboardAlert {
787 id: uuid::Uuid::new_v4().to_string(),
788 widget_id: widget.id.clone(),
789 message: format!(
790 "Alert triggered for '{}': value {:.2} {} threshold {:.2}",
791 widget.title,
792 value,
793 match alert_config.condition {
794 AlertCondition::GreaterThan => "exceeds",
795 AlertCondition::LessThan => "below",
796 _ => "meets",
797 },
798 alert_config.threshold
799 ),
800 severity: alert_config.severity,
801 triggered_at: SystemTime::now(),
802 current_value: value,
803 threshold_value: alert_config.threshold,
804 status: AlertStatus::Active,
805 };
806
807 if let Ok(mut alerts) = self.alerts.lock() {
808 alerts.insert(alert.id.clone(), alert.clone());
809 }
810
811 self.send_alert_notifications(&alert, alert_config)?;
813 }
814 }
815 }
816 }
817
818 Ok(())
819 }
820
821 fn send_alert_notifications(
823 &self,
824 alert: &DashboardAlert,
825 config: &AlertConfig,
826 ) -> CoreResult<()> {
827 for channel in &config.notification_channels {
828 match channel {
829 NotificationChannel::Console => {
830 println!("[DASHBOARD ALERT] {message}", message = alert.message);
831 }
832 NotificationChannel::File(path) => {
833 use std::fs::OpenOptions;
834 use std::io::Write;
835
836 if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
837 writeln!(
838 file,
839 "[{}] {}",
840 chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC"),
841 alert.message
842 )
843 .ok();
844 }
845 }
846 #[cfg(feature = "observability_http")]
847 NotificationChannel::Webhook(url) => {
848 let _ = url; }
851 #[cfg(not(feature = "observability_http"))]
852 NotificationChannel::Webhook(_) => {
853 }
855 NotificationChannel::Email(_) | NotificationChannel::Slack(_) => {
856 }
858 }
859 }
860
861 Ok(())
862 }
863
864 fn metric_source_to_name(&self, source: &MetricSource) -> String {
866 match source {
867 MetricSource::SystemCpu => "system.cpu".to_string(),
868 MetricSource::SystemMemory => "system.memory".to_string(),
869 MetricSource::NetworkIO => "system.network_io".to_string(),
870 MetricSource::DiskIO => "system.disk_io".to_string(),
871 MetricSource::Application(name) => format!("app.{name}"),
872 MetricSource::Custom(name) => format!("custom.{name}"),
873 MetricSource::Database(name) => format!("db.{name}"),
874 MetricSource::Cache(name) => format!("cache.{name}"),
875 }
876 }
877
878 fn start_web_server(&mut self) -> CoreResult<()> {
880 let handle = WebServerHandle {
884 address: "0.0.0.0".to_string(),
885 port: self.config.web_port,
886 running: Arc::new(Mutex::new(true)),
887 };
888
889 self.web_server_handle = Some(handle);
890
891 println!(
892 "Dashboard web interface started at http://localhost:{}/dashboard",
893 self.config.web_port
894 );
895
896 Ok(())
897 }
898}
899
900#[derive(Debug, Clone, Serialize, Deserialize)]
902pub struct DashboardExport {
903 pub config: DashboardConfig,
905 pub widgets: Vec<Widget>,
907 pub created_at: SystemTime,
909}
910
911#[derive(Debug, Clone)]
913#[cfg_attr(feature = "serde", derive(Serialize))]
914pub struct DashboardStatistics {
915 pub total_widgets: usize,
917 pub total_metrics: usize,
919 pub active_alerts: usize,
921 #[cfg_attr(feature = "serde", serde(skip))]
923 pub last_update: Instant,
924 pub uptime: Duration,
926 pub state: DashboardState,
928}
929
930impl Default for DashboardStatistics {
931 fn default() -> Self {
932 Self {
933 total_widgets: 0,
934 total_metrics: 0,
935 active_alerts: 0,
936 last_update: Instant::now(),
937 uptime: Duration::from_secs(0),
938 state: DashboardState::Stopped,
939 }
940 }
941}
942
943#[cfg(test)]
944mod tests {
945 use super::*;
946
947 #[test]
948 fn test_dashboard_creation() {
949 let config = DashboardConfig::default()
950 .with_title("Test Dashboard")
951 .with_refresh_interval(Duration::from_secs(10));
952
953 let dashboard = PerformanceDashboard::new(config);
954 assert!(dashboard.is_ok());
955
956 let dashboard = dashboard.expect("Operation failed");
957 assert_eq!(dashboard.config.title, "Test Dashboard");
958 assert_eq!(dashboard.config.refresh_interval, Duration::from_secs(10));
959 }
960
961 #[test]
962 fn test_widget_creation_and_addition() {
963 let config = DashboardConfig::default();
964 let mut dashboard = PerformanceDashboard::new(config).expect("Operation failed");
965
966 let widget = Widget::new()
967 .with_title("CPU Usage")
968 .with_chart_type(ChartType::LineChart)
969 .with_metric_source(MetricSource::SystemCpu)
970 .with_alert_threshold(80.0);
971
972 let widget_id = dashboard.add_widget(widget).expect("Operation failed");
973 assert!(!widget_id.is_empty());
974 assert_eq!(dashboard.widgets.len(), 1);
975 }
976
977 #[test]
978 fn test_metric_updates() {
979 let config = DashboardConfig::default();
980 let mut dashboard = PerformanceDashboard::new(config).expect("Operation failed");
981
982 let widget = Widget::new().with_metric_source(MetricSource::SystemCpu);
984
985 dashboard.add_widget(widget).expect("Operation failed");
986
987 let result = dashboard.update_metric(&MetricSource::SystemCpu, 75.5);
989 assert!(result.is_ok());
990
991 let metrics = dashboard.get_metrics().expect("Operation failed");
993 let cpu_metric = metrics.get("system.cpu");
994 assert!(cpu_metric.is_some());
995
996 let cpu_metric = cpu_metric.expect("Operation failed");
997 assert_eq!(cpu_metric.latest_value(), Some(75.5));
998 }
999
1000 #[test]
1001 fn test_metric_time_series() {
1002 let mut ts = MetricTimeSeries::new("test_metric");
1003
1004 ts.add_point(10.0, None);
1005 ts.add_point(20.0, None);
1006 ts.add_point(30.0, None);
1007
1008 assert_eq!(ts.latest_value(), Some(30.0));
1009 assert_eq!(ts.data_points.len(), 3);
1010
1011 ts.cleanup(Duration::from_secs(1), 2);
1013 assert_eq!(ts.data_points.len(), 2);
1014 }
1015
1016 #[test]
1017 fn test_alert_configuration() {
1018 let widget = Widget::new()
1019 .with_title("Memory Usage")
1020 .with_alert_threshold(90.0);
1021
1022 assert!(widget.alert_config.is_some());
1023
1024 let alert_config = widget.alert_config.expect("Operation failed");
1025 assert_eq!(alert_config.threshold, 90.0);
1026 assert_eq!(alert_config.condition, AlertCondition::GreaterThan);
1027 assert_eq!(alert_config.severity, AlertSeverity::Warning);
1028 }
1029
1030 #[test]
1031 fn test_dashboard_statistics() {
1032 let config = DashboardConfig::default();
1033 let mut dashboard = PerformanceDashboard::new(config).expect("Operation failed");
1034
1035 for i in 0..3 {
1037 let widget = Widget::new().with_title(&format!("Widget {i}"));
1038 dashboard.add_widget(widget).expect("Operation failed");
1039 }
1040
1041 let stats = dashboard.get_statistics();
1042 assert_eq!(stats.total_widgets, 3);
1043 assert_eq!(stats.active_alerts, 0);
1044 assert_eq!(stats.state, DashboardState::Stopped);
1045 }
1046
1047 #[test]
1048 fn test_dashboard_config_builder() {
1049 let config = DashboardConfig::default()
1050 .with_title("Custom Dashboard")
1051 .with_refresh_interval(Duration::from_secs(30))
1052 .with_retention_period(Duration::from_secs(14 * 24 * 60 * 60)) .with_web_interface(9090)
1054 .with_api_token("test-token")
1055 .with_theme(DashboardTheme::Light);
1056
1057 assert_eq!(config.title, "Custom Dashboard");
1058 assert_eq!(config.refresh_interval, Duration::from_secs(30));
1059 assert_eq!(
1060 config.retention_period,
1061 Duration::from_secs(14 * 24 * 60 * 60)
1062 ); assert_eq!(config.web_port, 9090);
1064 assert_eq!(config.api_token, Some("test-token".to_string()));
1065 assert_eq!(config.theme, DashboardTheme::Light);
1066 }
1067
1068 #[test]
1069 fn test_widget_layout() {
1070 let widget = Widget::new().with_layout(2, 3, 6, 4);
1071
1072 assert_eq!(widget.layout.x, 2);
1073 assert_eq!(widget.layout.y, 3);
1074 assert_eq!(widget.layout.width, 6);
1075 assert_eq!(widget.layout.height, 4);
1076 }
1077}