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:
Connor Johnstone
2025-08-30 20:23:48 -04:00
parent 783e13eb10
commit 9536158f58
3 changed files with 106 additions and 123 deletions

View File

@@ -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());

View File

@@ -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()))?;

View File

@@ -160,8 +160,28 @@ pub fn week_view(props: &WeekViewProps) -> Html {
}
},
RecurringEditAction::FutureEvents => {
// Split series and modify future events
// 1. Update original series to set UNTIL to end before this occurrence
// RFC 5545 Compliant Series Splitting: "This and Future Events"
//
// When a user chooses to modify "this and future events" for a recurring series,
// we implement a series split operation that:
//
// 1. **Terminates Original Series**: The existing series is updated with an UNTIL
// clause to stop before the occurrence being modified
// 2. **Creates New Series**: A new recurring series is created starting from the
// occurrence date with the user's modifications (new time, title, etc.)
//
// Example: User drags Aug 22 occurrence of "Daily 9AM meeting" to 2PM:
// - Original: "Daily 9AM meeting" → ends Aug 21 (UNTIL=Aug22T000000Z)
// - New: "Daily 2PM meeting" → starts Aug 22, continues indefinitely
//
// This approach ensures:
// - Past occurrences remain unchanged (preserves user's historical data)
// - Future occurrences reflect the new modifications
// - CalDAV compatibility through proper RRULE manipulation
// - No conflicts with existing calendar applications
//
// The backend handles both operations atomically within a single API call
// to prevent race conditions and ensure data consistency.
if let Some(update_callback) = &on_event_update {
// Find the original series event (not the occurrence)
// UIDs like "uuid-timestamp" need to split on the last hyphen, not the first
@@ -216,17 +236,30 @@ pub fn week_view(props: &WeekViewProps) -> Html {
until_utc.format("%Y-%m-%d %H:%M:%S UTC"),
edit.event.dtstart.format("%Y-%m-%d %H:%M:%S UTC")).into());
// Use the dragged times for the new series (not the original series times)
let new_start = edit.new_start; // The dragged start time
// Critical: Use the dragged times (new_start/new_end) not the original series times
// This ensures the new series reflects the user's drag operation
let new_start = edit.new_start; // The dragged start time
let new_end = edit.new_end; // The dragged end time
// Send until_date to backend instead of modifying RRULE on frontend
// Extract occurrence date from the dragged event for backend processing
// Format: YYYY-MM-DD (e.g., "2025-08-22")
// This tells the backend which specific occurrence is being modified
let occurrence_date = edit.event.dtstart.format("%Y-%m-%d").to_string();
update_callback.emit((original_series, new_start, new_end, true, Some(until_utc), Some("this_and_future".to_string()), Some(occurrence_date))); // preserve_rrule = true, backend will add UNTIL
// Send single request to backend with "this_and_future" scope
// Backend will atomically:
// 1. Add UNTIL clause to original series (stops before occurrence_date)
// 2. Create new series starting from occurrence_date with dragged times
update_callback.emit((
original_series, // Original event to terminate
new_start, // Dragged start time for new series
new_end, // Dragged end time for new series
true, // preserve_rrule = true
Some(until_utc), // UNTIL date for original series
Some("this_and_future".to_string()), // Update scope
Some(occurrence_date) // Date of occurrence being modified
));
}
// The backend will handle creating the new series as part of the this_and_future update
web_sys::console::log_1(&format!("✅ this_and_future update request sent - backend will handle both UPDATE (add UNTIL) and CREATE (new series) operations").into());
},
RecurringEditAction::AllEvents => {
// Modify the entire series