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();
println!("🔑 REPORT Basic Auth: Basic {}", basic_auth);
println!("🌐 REPORT URL: {}", url);
let response = self
.http_client
@@ -349,6 +347,13 @@ impl CalDAVClient {
}
}
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);
}
@@ -358,6 +363,21 @@ impl CalDAVClient {
.ok_or_else(|| CalDAVError::ParseError("Missing UID field".to_string()))?
.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)
let start_prop = properties
.get("DTSTART")
@@ -375,11 +395,6 @@ impl CalDAVClient {
(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
let status = properties
.get("STATUS")
@@ -471,6 +486,14 @@ impl CalDAVClient {
vevent.exdate = exdate.into_iter().map(|dt| dt.naive_utc()).collect();
vevent.exdate_tzid = None; // TODO: Parse timezone info for EXDATE
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
vevent.alarms = self.parse_valarms(&event)?;
@@ -582,11 +605,9 @@ impl CalDAVClient {
pub async fn discover_calendars(&self) -> Result<Vec<String>, CalDAVError> {
// First, try to discover user calendars if we have a calendar path in config
if let Some(calendar_path) = &self.config.calendar_path {
println!("Using configured calendar path: {}", calendar_path);
return Ok(vec![calendar_path.clone()]);
}
println!("No calendar path configured, discovering calendars...");
// Try different common CalDAV discovery paths
// Note: paths should be relative to the server URL base
@@ -599,20 +620,16 @@ impl CalDAVClient {
let mut has_valid_caldav_response = false;
for path in discovery_paths {
println!("Trying discovery path: {}", path);
match self.discover_calendars_at_path(&path).await {
Ok(calendars) => {
println!("Found {} calendar(s) at {}", calendars.len(), path);
has_valid_caldav_response = true;
all_calendars.extend(calendars);
}
Err(CalDAVError::ServerError(status)) => {
// HTTP error - this might be expected for some paths, continue trying
println!("Discovery path {} returned HTTP {}, trying next path", path, status);
}
Err(e) => {
// 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);
}
}
@@ -786,14 +803,24 @@ impl CalDAVClient {
return Ok((dt.naive_utc(), Some("UTC".to_string())));
}
// Try parsing as naive datetime
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)));
// Special handling for date-only format (all-day events)
if *format == "%Y%m%d" {
if let Ok(date) = chrono::NaiveDate::parse_from_str(datetime_part, format) {
// Convert date to midnight datetime for all-day events
let naive_dt = date.and_hms_opt(0, 0, 0).unwrap();
let tz = timezone_id.unwrap_or_else(|| "UTC".to_string());
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)));
}
}
}