···77/// Arguments for listing events
88pub struct ListEventsArgs {
99 pub source: Option<String>,
1010- pub date_from: Option<String>,
1111- pub date_until: Option<String>,
1010+ pub datetime_from: Option<String>,
1111+ pub datetime_until: Option<String>,
1212 pub format: OutputFormat,
1313}
14141515/// Executes the events list command
1616pub fn execute_list(repository: &dyn CalendarRepository, args: ListEventsArgs) -> Result<()> {
1717 // Parse date range
1818- let range = parse_date_range(&args.date_from, &args.date_until)?;
1818+ let range = parse_datetime_range(&args.datetime_from, &args.datetime_until)?;
19192020 // Get events
2121 let mut events = if let Some(source_uid) = &args.source {
···3333 Ok(())
3434}
35353636-/// Parses date range from string arguments
3737-fn parse_date_range(date_from: &Option<String>, date_until: &Option<String>) -> Result<DateRange> {
3838- let start = if let Some(date_str) = date_from {
3939- parse_date_to_timestamp(date_str)?
3636+/// Parses datetime range from string arguments
3737+fn parse_datetime_range(
3838+ datetime_from: &Option<String>,
3939+ datetime_until: &Option<String>,
4040+) -> Result<DateRange> {
4141+ let start = if let Some(datetime_str) = datetime_from {
4242+ parse_datetime_to_timestamp(datetime_str)?
4043 } else {
4144 Local::now()
4245 };
43464444- let end = if let Some(date_str) = date_until {
4545- parse_date_to_timestamp(date_str)?
4747+ let end = if let Some(datetime_str) = datetime_until {
4848+ parse_datetime_to_timestamp(datetime_str)?
4649 } else {
4750 start + chrono::Duration::days(60)
4851 };
···5053 DateRange::new(start, end)
5154}
52555353-/// Parses a date string (YYYY-MM-DD) to a DateTime
5454-fn parse_date_to_timestamp(date_str: &str) -> Result<chrono::DateTime<Local>> {
5555- let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d")?;
5656+/// Parses a datetime string (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS) to a DateTime
5757+fn parse_datetime_to_timestamp(datetime_str: &str) -> Result<chrono::DateTime<Local>> {
5858+ // Try parsing as full datetime first
5959+ if let Ok(datetime) = chrono::NaiveDateTime::parse_from_str(datetime_str, "%Y-%m-%dT%H:%M:%S") {
6060+ return datetime.and_local_timezone(Local).single().ok_or_else(|| {
6161+ alarma_core::AlarmaError::InvalidDateTime {
6262+ input: datetime_str.to_string(),
6363+ expected_format: Some("YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS".to_string()),
6464+ }
6565+ });
6666+ }
6767+6868+ // Fall back to date only (with time 00:00:00)
6969+ let date = NaiveDate::parse_from_str(datetime_str, "%Y-%m-%d")?;
5670 let datetime =
5771 date.and_hms_opt(0, 0, 0)
5872 .ok_or_else(|| alarma_core::AlarmaError::InvalidDateTime {
5959- input: date_str.to_string(),
6060- expected_format: Some("YYYY-MM-DD".to_string()),
7373+ input: datetime_str.to_string(),
7474+ expected_format: Some("YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS".to_string()),
6175 })?;
62766377 datetime.and_local_timezone(Local).single().ok_or_else(|| {
6478 alarma_core::AlarmaError::InvalidDateTime {
6565- input: date_str.to_string(),
6666- expected_format: Some("YYYY-MM-DD".to_string()),
7979+ input: datetime_str.to_string(),
8080+ expected_format: Some("YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS".to_string()),
6781 }
6882 })
6983}
···74887589 #[test]
7690 fn test_parse_date() {
7777- let result = parse_date_to_timestamp("2026-01-15");
9191+ let result = parse_datetime_to_timestamp("2026-01-15");
9292+ assert!(result.is_ok());
9393+ }
9494+9595+ #[test]
9696+ fn test_parse_datetime() {
9797+ let result = parse_datetime_to_timestamp("2026-01-15T14:30:00");
7898 assert!(result.is_ok());
7999 }
8010081101 #[test]
82102 fn test_parse_invalid_date() {
8383- let result = parse_date_to_timestamp("invalid-date");
103103+ let result = parse_datetime_to_timestamp("invalid-date");
84104 assert!(result.is_err());
85105 }
86106}
+6-6
crates/alarma-cli/src/main.rs
···6666 #[arg(short, long)]
6767 source: Option<String>,
68686969- /// Start date (YYYY-MM-DD format). Defaults to now
6969+ /// Start datetime (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS format). Defaults to now
7070 #[arg(long)]
7171- date_from: Option<String>,
7171+ datetime_from: Option<String>,
72727373- /// End date (YYYY-MM-DD format). Defaults to 60 days from now
7373+ /// End datetime (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS format). Defaults to 60 days from now
7474 #[arg(long)]
7575- date_until: Option<String>,
7575+ datetime_until: Option<String>,
76767777 /// Output format (table, json, simple)
7878 #[arg(short, long, default_value = "table")]
···102102 let format = parse_output_format(&args.format)?;
103103 let cmd_args = commands::events::ListEventsArgs {
104104 source: args.source,
105105- date_from: args.date_from,
106106- date_until: args.date_until,
105105+ datetime_from: args.datetime_from,
106106+ datetime_until: args.datetime_until,
107107 format,
108108 };
109109 commands::events::execute_list(repository.as_ref(), cmd_args)?;
+10-1
crates/alarma-eds/src/repository.rs
···9898 // Query events
9999 let components = client.list_events(range.start_timestamp(), range.end_timestamp())?;
100100101101- // Convert to domain events
101101+ // Convert to domain events and filter by actual start time
102102+ // EDS may return recurring event masters that are outside the range,
103103+ // so we need to filter by the actual event start time
104104+ let range_start = range.start_timestamp();
105105+ let range_end = range.end_timestamp();
106106+102107 Ok(components
103108 .iter()
104109 .filter_map(|comp| Self::ical_to_event(comp, source_uid.to_string()))
110110+ .filter(|event| {
111111+ let event_start = event.start.timestamp();
112112+ event_start >= range_start && event_start < range_end
113113+ })
105114 .collect())
106115 }
107116}