Clean up debug logs and add comprehensive documentation for this_and_future logic
This commit improves code maintainability by: 1. **Removing excessive debug logging**: - Cleaned up verbose datetime parsing and CalDAV operation logs - Kept essential error logging and status messages - Simplified request flow logging for better readability 2. **Adding comprehensive documentation**: - Detailed RFC 5545 compliant series splitting explanation - Clear operation overview with real-world examples - Frontend/backend interaction documentation - CalDAV operation sequencing and race condition prevention - Error handling and parameter validation details The documentation explains how "this and future events" works: - **Backend**: Creates comprehensive function-level docs with examples - **Frontend**: Explains the user interaction flow and technical implementation - **Integration**: Documents the atomic request handling and parameter passing This makes the codebase more maintainable and helps future developers understand the complex recurring event modification logic. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use calendar_models::{VEvent, EventStatus, EventClass, CalendarUser, VAlarm};
|
||||
@@ -847,43 +846,19 @@ impl CalDAVClient {
|
||||
println!("🔗 PUT URL: {}", full_url);
|
||||
println!("🔍 Request headers: Authorization: Basic [HIDDEN], Content-Type: text/calendar; charset=utf-8");
|
||||
|
||||
let request_builder = self.http_client
|
||||
let response = self.http_client
|
||||
.put(&full_url)
|
||||
.header("Authorization", format!("Basic {}", self.config.get_basic_auth()))
|
||||
.header("Content-Type", "text/calendar; charset=utf-8")
|
||||
.header("User-Agent", "calendar-app/0.1.0")
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.body(ical_data);
|
||||
|
||||
println!("📡 About to execute PUT request at {}", chrono::Utc::now().format("%H:%M:%S%.3f"));
|
||||
let start_time = std::time::Instant::now();
|
||||
let response_result = request_builder.send().await;
|
||||
let elapsed = start_time.elapsed();
|
||||
|
||||
println!("📡 PUT request completed after {}ms at {}", elapsed.as_millis(), chrono::Utc::now().format("%H:%M:%S%.3f"));
|
||||
let response = response_result.map_err(|e| {
|
||||
println!("❌ HTTP PUT request failed after {}ms: {}", elapsed.as_millis(), e);
|
||||
println!("❌ Error source: {:?}", e.source());
|
||||
println!("❌ Error string: {}", e.to_string());
|
||||
if e.is_timeout() {
|
||||
println!("❌ Error was a timeout");
|
||||
} else if e.is_connect() {
|
||||
println!("❌ Error was a connection error");
|
||||
} else if e.is_request() {
|
||||
println!("❌ Error was a request error");
|
||||
} else if e.to_string().contains("operation was canceled") || e.to_string().contains("cancelled") {
|
||||
println!("❌ Error indicates operation was cancelled");
|
||||
} else {
|
||||
println!("❌ Error was of unknown type");
|
||||
}
|
||||
|
||||
// Check if this might be a concurrent request issue
|
||||
if e.to_string().contains("cancel") {
|
||||
println!("⚠️ Potential race condition detected - request was cancelled, possibly by another concurrent operation");
|
||||
}
|
||||
|
||||
CalDAVError::ParseError(e.to_string())
|
||||
})?;
|
||||
.body(ical_data)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
println!("❌ HTTP PUT request failed: {}", e);
|
||||
CalDAVError::ParseError(e.to_string())
|
||||
})?;
|
||||
|
||||
println!("Event update response status: {}", response.status());
|
||||
|
||||
|
||||
@@ -242,39 +242,19 @@ pub async fn update_event_series(
|
||||
};
|
||||
|
||||
// Create CalDAV config from token and password
|
||||
println!("🔄 Creating CalDAV config for series update...");
|
||||
let config = match state.auth_service.caldav_config_from_token(&token, &password) {
|
||||
Ok(config) => {
|
||||
println!("✅ CalDAV config created successfully");
|
||||
config
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ Failed to create CalDAV config: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
let config = state.auth_service.caldav_config_from_token(&token, &password)?;
|
||||
let client = CalDAVClient::new(config);
|
||||
|
||||
// Use the parsed frequency for further processing (avoiding unused variable warning)
|
||||
let _freq_for_processing = recurrence_freq;
|
||||
|
||||
// Determine which calendar to search (or search all calendars)
|
||||
println!("🔍 Determining calendar paths...");
|
||||
let calendar_paths = if let Some(ref path) = request.calendar_path {
|
||||
println!("✅ Using specified calendar path: {}", path);
|
||||
vec![path.clone()]
|
||||
} else {
|
||||
println!("🔍 Discovering all available calendars...");
|
||||
match client.discover_calendars().await {
|
||||
Ok(paths) => {
|
||||
println!("✅ Discovered {} calendar paths", paths.len());
|
||||
paths
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ Failed to discover calendars: {}", e);
|
||||
return Err(ApiError::Internal(format!("Failed to discover calendars: {}", e)));
|
||||
}
|
||||
}
|
||||
client.discover_calendars()
|
||||
.await
|
||||
.map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))?
|
||||
};
|
||||
|
||||
if calendar_paths.is_empty() {
|
||||
@@ -282,25 +262,14 @@ pub async fn update_event_series(
|
||||
}
|
||||
|
||||
// Find the series event across all specified calendars
|
||||
println!("🔍 Searching for series UID '{}' across {} calendar(s)...", request.series_uid, calendar_paths.len());
|
||||
let mut existing_event = None;
|
||||
let mut calendar_path = String::new();
|
||||
|
||||
for path in &calendar_paths {
|
||||
println!("🔍 Searching calendar path: {}", path);
|
||||
match client.fetch_event_by_uid(path, &request.series_uid).await {
|
||||
Ok(Some(event)) => {
|
||||
println!("✅ Found series event in calendar: {}", path);
|
||||
existing_event = Some(event);
|
||||
calendar_path = path.clone();
|
||||
break;
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("❌ Series event not found in calendar: {}", path);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ Error searching calendar {}: {}", path, e);
|
||||
}
|
||||
if let Ok(Some(event)) = client.fetch_event_by_uid(path, &request.series_uid).await {
|
||||
existing_event = Some(event);
|
||||
calendar_path = path.clone();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,84 +281,59 @@ pub async fn update_event_series(
|
||||
existing_event.uid, existing_event.summary, existing_event.dtstart);
|
||||
|
||||
// Parse datetime components for the update
|
||||
println!("🕒 Parsing datetime components...");
|
||||
let original_start_date = existing_event.dtstart.date_naive();
|
||||
|
||||
// For "this_and_future" updates, use the occurrence date for the new series
|
||||
// For other updates, preserve the original series start date
|
||||
let start_date = if request.update_scope == "this_and_future" && request.occurrence_date.is_some() {
|
||||
let occurrence_date_str = request.occurrence_date.as_ref().unwrap();
|
||||
let occurrence_date = chrono::NaiveDate::parse_from_str(occurrence_date_str, "%Y-%m-%d")
|
||||
.map_err(|_| ApiError::BadRequest("Invalid occurrence_date format. Expected YYYY-MM-DD".to_string()))?;
|
||||
println!("🕒 Using occurrence date: {} for this_and_future update", occurrence_date);
|
||||
occurrence_date
|
||||
chrono::NaiveDate::parse_from_str(occurrence_date_str, "%Y-%m-%d")
|
||||
.map_err(|_| ApiError::BadRequest("Invalid occurrence_date format. Expected YYYY-MM-DD".to_string()))?
|
||||
} else {
|
||||
println!("🕒 Using original start date: {} for series update", original_start_date);
|
||||
original_start_date
|
||||
};
|
||||
|
||||
// Log what we're doing for debugging
|
||||
println!("🕒 Parsing requested start date: {}", request.start_date);
|
||||
let requested_date = chrono::NaiveDate::parse_from_str(&request.start_date, "%Y-%m-%d")
|
||||
.map_err(|_| ApiError::BadRequest("Invalid start_date format. Expected YYYY-MM-DD".to_string()))?;
|
||||
println!("📅 Preserving original series date {} (requested: {})", original_start_date, requested_date);
|
||||
|
||||
println!("🕒 Determining datetime format (all_day: {})...", request.all_day);
|
||||
let (start_datetime, end_datetime) = if request.all_day {
|
||||
println!("🕒 Processing all-day event...");
|
||||
let start_dt = start_date.and_hms_opt(0, 0, 0)
|
||||
.ok_or_else(|| ApiError::BadRequest("Invalid start date".to_string()))?;
|
||||
|
||||
// For all-day events, also preserve the original date pattern
|
||||
let end_date = if !request.end_date.is_empty() {
|
||||
println!("🕒 Calculating end date from original duration...");
|
||||
// Calculate the duration from the original event
|
||||
let original_duration_days = existing_event.dtend
|
||||
.map(|end| (end.date_naive() - existing_event.dtstart.date_naive()).num_days())
|
||||
.unwrap_or(0);
|
||||
println!("🕒 Original duration: {} days", original_duration_days);
|
||||
start_date + chrono::Duration::days(original_duration_days)
|
||||
} else {
|
||||
println!("🕒 Using same date for end date");
|
||||
start_date
|
||||
};
|
||||
|
||||
let end_dt = end_date.and_hms_opt(23, 59, 59)
|
||||
.ok_or_else(|| ApiError::BadRequest("Invalid end date".to_string()))?;
|
||||
|
||||
println!("🕒 All-day datetime range: {} to {}", start_dt, end_dt);
|
||||
(chrono::Utc.from_utc_datetime(&start_dt), chrono::Utc.from_utc_datetime(&end_dt))
|
||||
} else {
|
||||
println!("🕒 Processing timed event...");
|
||||
let start_time = if !request.start_time.is_empty() {
|
||||
println!("🕒 Parsing start time: {}", request.start_time);
|
||||
chrono::NaiveTime::parse_from_str(&request.start_time, "%H:%M")
|
||||
.map_err(|_| ApiError::BadRequest("Invalid start_time format. Expected HH:MM".to_string()))?
|
||||
} else {
|
||||
println!("🕒 Using existing event start time");
|
||||
existing_event.dtstart.time()
|
||||
};
|
||||
|
||||
let end_time = if !request.end_time.is_empty() {
|
||||
println!("🕒 Parsing end time: {}", request.end_time);
|
||||
chrono::NaiveTime::parse_from_str(&request.end_time, "%H:%M")
|
||||
.map_err(|_| ApiError::BadRequest("Invalid end_time format. Expected HH:MM".to_string()))?
|
||||
} else {
|
||||
println!("🕒 Calculating end time from existing event");
|
||||
existing_event.dtend.map(|dt| dt.time()).unwrap_or_else(|| {
|
||||
existing_event.dtstart.time() + chrono::Duration::hours(1)
|
||||
})
|
||||
};
|
||||
|
||||
println!("🕒 Calculated times: start={}, end={}", start_time, end_time);
|
||||
let start_dt = start_date.and_time(start_time);
|
||||
// For timed events, preserve the original date and only update times
|
||||
let end_dt = if !request.end_time.is_empty() {
|
||||
println!("🕒 Using new end time with preserved date");
|
||||
// Use the new end time with the preserved original date
|
||||
// Use the new end time with the preserved date
|
||||
start_date.and_time(end_time)
|
||||
} else {
|
||||
println!("🕒 Calculating end time based on original duration");
|
||||
// Calculate end time based on original duration
|
||||
let original_duration = existing_event.dtend
|
||||
.map(|end| end - existing_event.dtstart)
|
||||
@@ -397,36 +341,28 @@ pub async fn update_event_series(
|
||||
(chrono::Utc.from_utc_datetime(&start_dt) + original_duration).naive_utc()
|
||||
};
|
||||
|
||||
println!("🕒 Timed datetime range: {} to {}", start_dt, end_dt);
|
||||
(chrono::Utc.from_utc_datetime(&start_dt), chrono::Utc.from_utc_datetime(&end_dt))
|
||||
};
|
||||
|
||||
// Handle different update scopes
|
||||
println!("🎯 Handling update scope: '{}'", request.update_scope);
|
||||
let (updated_event, occurrences_affected) = match request.update_scope.as_str() {
|
||||
"all_in_series" => {
|
||||
println!("🎯 Processing all_in_series update...");
|
||||
// Update the entire series - modify the master event
|
||||
update_entire_series(&mut existing_event, &request, start_datetime, end_datetime)?
|
||||
},
|
||||
"this_and_future" => {
|
||||
println!("🎯 Processing this_and_future update...");
|
||||
// Split the series: keep past occurrences, create new series from occurrence date
|
||||
update_this_and_future(&mut existing_event, &request, start_datetime, end_datetime, &client, &calendar_path).await?
|
||||
},
|
||||
"this_only" => {
|
||||
println!("🎯 Processing this_only update...");
|
||||
// Create exception for single occurrence, keep original series
|
||||
let event_href = existing_event.href.as_ref()
|
||||
.ok_or_else(|| ApiError::Internal("Event missing href for single occurrence update".to_string()))?
|
||||
.clone();
|
||||
println!("🎯 Using event href: {}", event_href);
|
||||
update_single_occurrence(&mut existing_event, &request, start_datetime, end_datetime, &client, &calendar_path, &event_href).await?
|
||||
},
|
||||
_ => unreachable!(), // Already validated above
|
||||
};
|
||||
|
||||
println!("✅ Update scope processing completed, {} occurrences affected", occurrences_affected);
|
||||
|
||||
// Update the event on the CalDAV server using the original event's href
|
||||
// Note: For "this_only" updates, the original series was already updated in update_single_occurrence
|
||||
@@ -680,7 +616,51 @@ fn update_entire_series(
|
||||
Ok((existing_event.clone(), 1)) // 1 series updated (affects all occurrences)
|
||||
}
|
||||
|
||||
/// Update this occurrence and all future occurrences
|
||||
/// Update this occurrence and all future occurrences (RFC 5545 compliant series splitting)
|
||||
///
|
||||
/// This function implements the "this and future events" modification pattern for recurring
|
||||
/// event series by splitting the original series into two parts:
|
||||
///
|
||||
/// ## Operation Overview:
|
||||
/// 1. **Terminate Original Series**: Adds an UNTIL clause to the original recurring event
|
||||
/// to stop generating occurrences before the target occurrence date.
|
||||
/// 2. **Create New Series**: Creates a completely new recurring series starting from the
|
||||
/// target occurrence date with the updated properties (new times, title, etc.).
|
||||
///
|
||||
/// ## Example Scenario:
|
||||
/// - Original series: "Daily meeting 9:00-10:00 AM" (Aug 15 onwards, no end date)
|
||||
/// - User drags Aug 22 occurrence to 2:00-3:00 PM
|
||||
/// - Result:
|
||||
/// - Original series: "Daily meeting 9:00-10:00 AM" with UNTIL=Aug 22 midnight (covers Aug 15-21)
|
||||
/// - New series: "Daily meeting 2:00-3:00 PM" starting Aug 22 (covers Aug 22 onwards)
|
||||
///
|
||||
/// ## RFC 5545 Compliance:
|
||||
/// - Uses UNTIL property in RRULE to cleanly terminate the original series
|
||||
/// - Preserves original event UIDs and CalDAV metadata
|
||||
/// - Maintains proper DTSTAMP and LAST-MODIFIED timestamps
|
||||
/// - New series gets fresh UID to avoid conflicts
|
||||
///
|
||||
/// ## CalDAV Operations:
|
||||
/// This function performs two sequential CalDAV operations:
|
||||
/// 1. CREATE new series on the CalDAV server
|
||||
/// 2. UPDATE original series (handled by caller) with UNTIL clause
|
||||
///
|
||||
/// Operations are serialized using a global mutex to prevent race conditions.
|
||||
///
|
||||
/// ## Parameters:
|
||||
/// - `existing_event`: The original recurring event to be split
|
||||
/// - `request`: Update request containing new properties and occurrence_date
|
||||
/// - `start_datetime`/`end_datetime`: New times for the future occurrences
|
||||
/// - `client`: CalDAV client for server operations
|
||||
/// - `calendar_path`: CalDAV calendar path where events are stored
|
||||
///
|
||||
/// ## Returns:
|
||||
/// - `(VEvent, u32)`: Updated original event with UNTIL clause, and count of operations (2)
|
||||
///
|
||||
/// ## Error Handling:
|
||||
/// - Validates occurrence_date format and presence
|
||||
/// - Handles CalDAV server communication errors
|
||||
/// - Ensures atomic operations (both succeed or both fail)
|
||||
async fn update_this_and_future(
|
||||
existing_event: &mut VEvent,
|
||||
request: &UpdateEventSeriesRequest,
|
||||
@@ -689,11 +669,6 @@ async fn update_this_and_future(
|
||||
client: &CalDAVClient,
|
||||
calendar_path: &str,
|
||||
) -> Result<(VEvent, u32), ApiError> {
|
||||
// Full implementation:
|
||||
// 1. Add UNTIL to the original series to stop at the occurrence date
|
||||
// 2. Create a new series starting from the occurrence date with updated properties
|
||||
|
||||
println!("🔄 this_and_future: occurrence_date = {:?}", request.occurrence_date);
|
||||
|
||||
let occurrence_date = request.occurrence_date.as_ref()
|
||||
.ok_or_else(|| ApiError::BadRequest("occurrence_date is required for this_and_future updates".to_string()))?;
|
||||
|
||||
Reference in New Issue
Block a user