Fix recurring event series modification via drag and drop operations
This commit resolves the "Failed to fetch" errors when updating recurring event series through drag operations by implementing proper request sequencing and fixing time parameter handling. Key fixes: - Eliminate HTTP request cancellation by sequencing operations properly - Add global mutex to prevent CalDAV HTTP race conditions - Implement complete RFC 5545-compliant series splitting for "this_and_future" - Fix frontend to pass dragged times instead of original times - Add comprehensive error handling and request timing logs - Backend now handles both UPDATE (add UNTIL) and CREATE (new series) in single request Technical changes: - Frontend: Remove concurrent CREATE request, pass dragged times to backend - Backend: Implement full this_and_future logic with sequential operations - CalDAV: Add mutex serialization and detailed error tracking - Series: Create new series with occurrence date + dragged times 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,7 @@ regex = "1.0"
|
||||
dotenvy = "0.15"
|
||||
base64 = "0.21"
|
||||
thiserror = "1.0"
|
||||
lazy_static = "1.4"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.0", features = ["macros", "rt"] }
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
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};
|
||||
|
||||
// Global mutex to serialize CalDAV HTTP requests to prevent race conditions
|
||||
lazy_static::lazy_static! {
|
||||
static ref CALDAV_HTTP_MUTEX: Arc<Mutex<()>> = Arc::new(Mutex::new(()));
|
||||
}
|
||||
|
||||
/// Type alias for shared VEvent (for backward compatibility during migration)
|
||||
pub type CalendarEvent = VEvent;
|
||||
|
||||
@@ -105,9 +113,15 @@ pub struct CalDAVClient {
|
||||
impl CalDAVClient {
|
||||
/// Create a new CalDAV client with the given configuration
|
||||
pub fn new(config: crate::config::CalDAVConfig) -> Self {
|
||||
// Create HTTP client with global timeout to prevent hanging requests
|
||||
let http_client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(60)) // 60 second global timeout
|
||||
.build()
|
||||
.expect("Failed to create HTTP client");
|
||||
|
||||
Self {
|
||||
config,
|
||||
http_client: reqwest::Client::new(),
|
||||
http_client,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -773,6 +787,10 @@ impl CalDAVClient {
|
||||
println!("Creating event at: {}", full_url);
|
||||
println!("iCal data: {}", ical_data);
|
||||
|
||||
println!("📡 Acquiring CalDAV HTTP lock for CREATE request...");
|
||||
let _lock = CALDAV_HTTP_MUTEX.lock().await;
|
||||
println!("📡 Lock acquired, sending CREATE request to CalDAV server...");
|
||||
|
||||
let response = self.http_client
|
||||
.put(&full_url)
|
||||
.header("Authorization", format!("Basic {}", self.config.get_basic_auth()))
|
||||
@@ -823,15 +841,49 @@ impl CalDAVClient {
|
||||
println!("📝 Updated iCal data: {}", ical_data);
|
||||
println!("📝 Event has {} exception dates", event.exdate.len());
|
||||
|
||||
let response = self.http_client
|
||||
println!("📡 Acquiring CalDAV HTTP lock for PUT request...");
|
||||
let _lock = CALDAV_HTTP_MUTEX.lock().await;
|
||||
println!("📡 Lock acquired, sending PUT request to CalDAV server...");
|
||||
println!("🔗 PUT URL: {}", full_url);
|
||||
println!("🔍 Request headers: Authorization: Basic [HIDDEN], Content-Type: text/calendar; charset=utf-8");
|
||||
|
||||
let request_builder = 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")
|
||||
.body(ical_data)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| CalDAVError::ParseError(e.to_string()))?;
|
||||
.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())
|
||||
})?;
|
||||
|
||||
println!("Event update response status: {}", response.status());
|
||||
|
||||
@@ -1020,6 +1072,10 @@ impl CalDAVClient {
|
||||
|
||||
println!("Deleting event at: {}", full_url);
|
||||
|
||||
println!("📡 Acquiring CalDAV HTTP lock for DELETE request...");
|
||||
let _lock = CALDAV_HTTP_MUTEX.lock().await;
|
||||
println!("📡 Lock acquired, sending DELETE request to CalDAV server...");
|
||||
|
||||
let response = self.http_client
|
||||
.delete(&full_url)
|
||||
.header("Authorization", format!("Basic {}", self.config.get_basic_auth()))
|
||||
|
||||
@@ -242,19 +242,39 @@ pub async fn update_event_series(
|
||||
};
|
||||
|
||||
// Create CalDAV config from token and password
|
||||
let config = state.auth_service.caldav_config_from_token(&token, &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 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 {
|
||||
client.discover_calendars()
|
||||
.await
|
||||
.map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))?
|
||||
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)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if calendar_paths.is_empty() {
|
||||
@@ -262,14 +282,25 @@ 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 {
|
||||
if let Ok(Some(event)) = client.fetch_event_by_uid(path, &request.series_uid).await {
|
||||
existing_event = Some(event);
|
||||
calendar_path = path.clone();
|
||||
break;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,60 +308,88 @@ pub async fn update_event_series(
|
||||
.ok_or_else(|| ApiError::NotFound(format!("Event series with UID '{}' not found", request.series_uid)))?;
|
||||
|
||||
println!("📅 Found series event in calendar: {}", calendar_path);
|
||||
println!("📅 Event details: UID={}, summary={:?}, dtstart={}",
|
||||
existing_event.uid, existing_event.summary, existing_event.dtstart);
|
||||
|
||||
// Parse datetime components for the update
|
||||
// For recurring events, preserve the original series start date and only update the time
|
||||
// to prevent the entire series from shifting to a different date
|
||||
println!("🕒 Parsing datetime components...");
|
||||
let original_start_date = existing_event.dtstart.date_naive();
|
||||
let start_date = original_start_date; // Always use original series date
|
||||
|
||||
// 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
|
||||
} 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
|
||||
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)
|
||||
@@ -338,37 +397,57 @@ 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)?
|
||||
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
|
||||
if request.update_scope != "this_only" {
|
||||
println!("📤 Updating event on CalDAV server...");
|
||||
let event_href = existing_event.href.as_ref()
|
||||
.ok_or_else(|| ApiError::Internal("Event missing href for update".to_string()))?;
|
||||
client.update_event(&calendar_path, &updated_event, event_href)
|
||||
.await
|
||||
.map_err(|e| ApiError::Internal(format!("Failed to update event series: {}", e)))?;
|
||||
println!("📤 Using event href: {}", event_href);
|
||||
println!("📤 Calendar path: {}", calendar_path);
|
||||
|
||||
match client.update_event(&calendar_path, &updated_event, event_href).await {
|
||||
Ok(_) => {
|
||||
println!("✅ CalDAV update completed successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ CalDAV update failed: {}", e);
|
||||
return Err(ApiError::Internal(format!("Failed to update event series: {}", e)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("📤 Skipping CalDAV update (already handled in this_only scope)");
|
||||
}
|
||||
|
||||
println!("✅ Event series updated successfully with UID: {}", request.series_uid);
|
||||
@@ -602,43 +681,92 @@ fn update_entire_series(
|
||||
}
|
||||
|
||||
/// Update this occurrence and all future occurrences
|
||||
fn update_this_and_future(
|
||||
async fn update_this_and_future(
|
||||
existing_event: &mut VEvent,
|
||||
request: &UpdateEventSeriesRequest,
|
||||
start_datetime: chrono::DateTime<chrono::Utc>,
|
||||
end_datetime: chrono::DateTime<chrono::Utc>,
|
||||
client: &CalDAVClient,
|
||||
calendar_path: &str,
|
||||
) -> Result<(VEvent, u32), ApiError> {
|
||||
// For now, treat this the same as update_entire_series
|
||||
// In a full implementation, this would:
|
||||
// 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
|
||||
|
||||
// For simplicity, we'll modify the original series with an UNTIL date if occurrence_date is provided
|
||||
if let Some(occurrence_date) = &request.occurrence_date {
|
||||
// Parse occurrence date and set as UNTIL for the original series
|
||||
match chrono::NaiveDate::parse_from_str(occurrence_date, "%Y-%m-%d") {
|
||||
Ok(date) => {
|
||||
let until_datetime = date.and_hms_opt(0, 0, 0)
|
||||
.ok_or_else(|| ApiError::BadRequest("Invalid occurrence date".to_string()))?;
|
||||
let utc_until = chrono::Utc.from_utc_datetime(&until_datetime);
|
||||
|
||||
// Create modified RRULE with UNTIL clause
|
||||
let mut rrule = existing_event.rrule.clone().unwrap_or_else(|| "FREQ=WEEKLY".to_string());
|
||||
|
||||
// Remove existing UNTIL or COUNT if present
|
||||
let parts: Vec<&str> = rrule.split(';').filter(|part| {
|
||||
!part.starts_with("UNTIL=") && !part.starts_with("COUNT=")
|
||||
}).collect();
|
||||
|
||||
rrule = format!("{};UNTIL={}", parts.join(";"), utc_until.format("%Y%m%dT%H%M%SZ"));
|
||||
existing_event.rrule = Some(rrule);
|
||||
},
|
||||
Err(_) => return Err(ApiError::BadRequest("Invalid occurrence date format".to_string())),
|
||||
}
|
||||
}
|
||||
println!("🔄 this_and_future: occurrence_date = {:?}", request.occurrence_date);
|
||||
|
||||
// Then apply the same updates as all_in_series for the rest of the properties
|
||||
update_entire_series(existing_event, request, start_datetime, end_datetime)
|
||||
let occurrence_date = request.occurrence_date.as_ref()
|
||||
.ok_or_else(|| ApiError::BadRequest("occurrence_date is required for this_and_future updates".to_string()))?;
|
||||
|
||||
// Parse occurrence date
|
||||
let occurrence_date_parsed = chrono::NaiveDate::parse_from_str(occurrence_date, "%Y-%m-%d")
|
||||
.map_err(|_| ApiError::BadRequest("Invalid occurrence date format".to_string()))?;
|
||||
|
||||
// Step 1: Add UNTIL to the original series to stop before the occurrence date
|
||||
let until_datetime = occurrence_date_parsed.and_hms_opt(0, 0, 0)
|
||||
.ok_or_else(|| ApiError::BadRequest("Invalid occurrence date".to_string()))?;
|
||||
let utc_until = chrono::Utc.from_utc_datetime(&until_datetime);
|
||||
|
||||
// Create modified RRULE with UNTIL clause for the original series
|
||||
let original_rrule = existing_event.rrule.clone().unwrap_or_else(|| "FREQ=WEEKLY".to_string());
|
||||
let parts: Vec<&str> = original_rrule.split(';').filter(|part| {
|
||||
!part.starts_with("UNTIL=") && !part.starts_with("COUNT=")
|
||||
}).collect();
|
||||
|
||||
existing_event.rrule = Some(format!("{};UNTIL={}", parts.join(";"), utc_until.format("%Y%m%dT%H%M%SZ")));
|
||||
println!("🔄 this_and_future: Updated original series RRULE: {:?}", existing_event.rrule);
|
||||
|
||||
// Step 2: Create a new series starting from the occurrence date with updated properties
|
||||
let new_series_uid = format!("series-{}", uuid::Uuid::new_v4());
|
||||
let mut new_series = existing_event.clone();
|
||||
|
||||
// Update the new series with new properties
|
||||
new_series.uid = new_series_uid.clone();
|
||||
new_series.dtstart = start_datetime;
|
||||
new_series.dtend = Some(end_datetime);
|
||||
new_series.summary = if request.title.trim().is_empty() { None } else { Some(request.title.clone()) };
|
||||
new_series.description = if request.description.trim().is_empty() { None } else { Some(request.description.clone()) };
|
||||
new_series.location = if request.location.trim().is_empty() { None } else { Some(request.location.clone()) };
|
||||
|
||||
new_series.status = Some(match request.status.to_lowercase().as_str() {
|
||||
"tentative" => EventStatus::Tentative,
|
||||
"cancelled" => EventStatus::Cancelled,
|
||||
_ => EventStatus::Confirmed,
|
||||
});
|
||||
|
||||
new_series.class = Some(match request.class.to_lowercase().as_str() {
|
||||
"private" => EventClass::Private,
|
||||
"confidential" => EventClass::Confidential,
|
||||
_ => EventClass::Public,
|
||||
});
|
||||
|
||||
new_series.priority = request.priority;
|
||||
|
||||
// Reset the RRULE for the new series (remove UNTIL)
|
||||
let new_rrule_parts: Vec<&str> = original_rrule.split(';').filter(|part| {
|
||||
!part.starts_with("UNTIL=") && !part.starts_with("COUNT=")
|
||||
}).collect();
|
||||
new_series.rrule = Some(new_rrule_parts.join(";"));
|
||||
|
||||
// Update timestamps
|
||||
let now = chrono::Utc::now();
|
||||
new_series.dtstamp = now;
|
||||
new_series.created = Some(now);
|
||||
new_series.last_modified = Some(now);
|
||||
new_series.href = None; // Will be set when created
|
||||
|
||||
println!("🔄 this_and_future: Creating new series with UID: {}", new_series_uid);
|
||||
println!("🔄 this_and_future: New series RRULE: {:?}", new_series.rrule);
|
||||
|
||||
// Create the new series on CalDAV server
|
||||
client.create_event(calendar_path, &new_series)
|
||||
.await
|
||||
.map_err(|e| ApiError::Internal(format!("Failed to create new series: {}", e)))?;
|
||||
|
||||
println!("✅ this_and_future: Created new series successfully");
|
||||
|
||||
// Return the original event (with UNTIL added) - it will be updated by the main handler
|
||||
Ok((existing_event.clone(), 2)) // 2 operations: updated original + created new series
|
||||
}
|
||||
|
||||
/// Update only a single occurrence (create an exception)
|
||||
|
||||
Reference in New Issue
Block a user