Fix all-day event parsing with VALUE=DATE format support

- Add special handling for date-only format (%Y%m%d) in parse_datetime_with_tz
- Convert NaiveDate to midnight NaiveDateTime for all-day events
- Reorder all-day detection to occur before datetime parsing
- All-day events now properly display in calendar views

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Connor Johnstone
2025-09-18 15:51:00 -04:00
parent ac1164fd81
commit 5854ad291d

View File

@@ -167,8 +167,6 @@ impl CalDAVClient {
}; };
let basic_auth = self.config.get_basic_auth(); let basic_auth = self.config.get_basic_auth();
println!("🔑 REPORT Basic Auth: Basic {}", basic_auth);
println!("🌐 REPORT URL: {}", url);
let response = self let response = self
.http_client .http_client
@@ -349,6 +347,13 @@ impl CalDAVClient {
} }
} }
full_prop.push_str(&format!(":{}", prop_value)); full_prop.push_str(&format!(":{}", prop_value));
// Debug logging for DTSTART properties specifically
if prop_name == "DTSTART" {
println!("🔍 Raw DTSTART property: name='{}', value='{}', params={:?}, full_prop='{}'",
property.name, prop_value, property.params, full_prop);
}
full_properties.insert(prop_name, full_prop); full_properties.insert(prop_name, full_prop);
} }
@@ -358,6 +363,21 @@ impl CalDAVClient {
.ok_or_else(|| CalDAVError::ParseError("Missing UID field".to_string()))? .ok_or_else(|| CalDAVError::ParseError("Missing UID field".to_string()))?
.clone(); .clone();
// Determine if it's an all-day event FIRST by checking for VALUE=DATE parameter per RFC 5545
let empty_string = String::new();
let dtstart_raw = full_properties.get("DTSTART").unwrap_or(&empty_string);
let dtstart_value = properties.get("DTSTART").unwrap_or(&empty_string);
let all_day = dtstart_raw.contains("VALUE=DATE") || (!dtstart_value.contains("T") && dtstart_value.len() == 8);
// Debug logging for DTSTART parsing
if all_day {
println!("✅ ALL-DAY EVENT FOUND: DTSTART='{}' -> all_day={}", dtstart_raw, all_day);
} else {
println!("🔍 Backend: DTSTART='{}' (len={}, has_T={}, has_VALUE_DATE={}) -> all_day={}",
dtstart_raw, dtstart_raw.len(), dtstart_raw.contains("T"),
dtstart_raw.contains("VALUE=DATE"), all_day);
}
// Parse start time (required) // Parse start time (required)
let start_prop = properties let start_prop = properties
.get("DTSTART") .get("DTSTART")
@@ -375,11 +395,6 @@ impl CalDAVClient {
(None, None) (None, None)
}; };
// Determine if it's an all-day event by checking for VALUE=DATE parameter
let empty_string = String::new();
let dtstart_raw = properties.get("DTSTART").unwrap_or(&empty_string);
let all_day = dtstart_raw.contains("VALUE=DATE") || (!dtstart_raw.contains("T") && dtstart_raw.len() == 8);
// Parse status // Parse status
let status = properties let status = properties
.get("STATUS") .get("STATUS")
@@ -471,6 +486,14 @@ impl CalDAVClient {
vevent.exdate = exdate.into_iter().map(|dt| dt.naive_utc()).collect(); vevent.exdate = exdate.into_iter().map(|dt| dt.naive_utc()).collect();
vevent.exdate_tzid = None; // TODO: Parse timezone info for EXDATE vevent.exdate_tzid = None; // TODO: Parse timezone info for EXDATE
vevent.all_day = all_day; vevent.all_day = all_day;
if all_day {
println!("📅 Backend: Successfully created all-day VEvent '{}' with start={}, end={:?}, all_day={}",
vevent.summary.as_ref().unwrap_or(&"Untitled".to_string()),
vevent.dtstart,
vevent.dtend,
vevent.all_day
);
}
// Parse alarms // Parse alarms
vevent.alarms = self.parse_valarms(&event)?; vevent.alarms = self.parse_valarms(&event)?;
@@ -582,11 +605,9 @@ impl CalDAVClient {
pub async fn discover_calendars(&self) -> Result<Vec<String>, CalDAVError> { pub async fn discover_calendars(&self) -> Result<Vec<String>, CalDAVError> {
// First, try to discover user calendars if we have a calendar path in config // First, try to discover user calendars if we have a calendar path in config
if let Some(calendar_path) = &self.config.calendar_path { if let Some(calendar_path) = &self.config.calendar_path {
println!("Using configured calendar path: {}", calendar_path);
return Ok(vec![calendar_path.clone()]); return Ok(vec![calendar_path.clone()]);
} }
println!("No calendar path configured, discovering calendars...");
// Try different common CalDAV discovery paths // Try different common CalDAV discovery paths
// Note: paths should be relative to the server URL base // Note: paths should be relative to the server URL base
@@ -599,20 +620,16 @@ impl CalDAVClient {
let mut has_valid_caldav_response = false; let mut has_valid_caldav_response = false;
for path in discovery_paths { for path in discovery_paths {
println!("Trying discovery path: {}", path);
match self.discover_calendars_at_path(&path).await { match self.discover_calendars_at_path(&path).await {
Ok(calendars) => { Ok(calendars) => {
println!("Found {} calendar(s) at {}", calendars.len(), path);
has_valid_caldav_response = true; has_valid_caldav_response = true;
all_calendars.extend(calendars); all_calendars.extend(calendars);
} }
Err(CalDAVError::ServerError(status)) => { Err(CalDAVError::ServerError(status)) => {
// HTTP error - this might be expected for some paths, continue trying // HTTP error - this might be expected for some paths, continue trying
println!("Discovery path {} returned HTTP {}, trying next path", path, status);
} }
Err(e) => { Err(e) => {
// Network or other error - this suggests the server isn't reachable or isn't CalDAV // Network or other error - this suggests the server isn't reachable or isn't CalDAV
println!("Discovery failed for path {}: {:?}", path, e);
return Err(e); return Err(e);
} }
} }
@@ -786,14 +803,24 @@ impl CalDAVClient {
return Ok((dt.naive_utc(), Some("UTC".to_string()))); return Ok((dt.naive_utc(), Some("UTC".to_string())));
} }
// Try parsing as naive datetime // Special handling for date-only format (all-day events)
if let Ok(naive_dt) = chrono::NaiveDateTime::parse_from_str(datetime_part, format) { if *format == "%Y%m%d" {
// Per RFC 5545: if no TZID parameter is provided, treat as UTC if let Ok(date) = chrono::NaiveDate::parse_from_str(datetime_part, format) {
let tz = timezone_id.unwrap_or_else(|| "UTC".to_string()); // Convert date to midnight datetime for all-day events
let naive_dt = date.and_hms_opt(0, 0, 0).unwrap();
// If it's UTC, the naive time is already correct let tz = timezone_id.unwrap_or_else(|| "UTC".to_string());
// If it's a local timezone, we store the naive time and the timezone ID return Ok((naive_dt, Some(tz)));
return Ok((naive_dt, Some(tz))); }
} else {
// Try parsing as naive datetime for time-based formats
if let Ok(naive_dt) = chrono::NaiveDateTime::parse_from_str(datetime_part, format) {
// Per RFC 5545: if no TZID parameter is provided, treat as UTC
let tz = timezone_id.unwrap_or_else(|| "UTC".to_string());
// If it's UTC, the naive time is already correct
// If it's a local timezone, we store the naive time and the timezone ID
return Ok((naive_dt, Some(tz)));
}
} }
} }