Fix external calendar creation and Outlook compatibility issues

- Fix external calendar form validation by replacing node refs with controlled state inputs
- Add multiple user-agent fallback approach for better external calendar compatibility
- Enhance HTTP client configuration with proper redirect handling and timeouts
- Add detailed error logging and Outlook-specific error detection
- Improve request headers for better calendar server compatibility

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Connor Johnstone
2025-09-11 17:58:53 -04:00
parent 927cd7d2bb
commit 91be4436a9
2 changed files with 118 additions and 31 deletions

View File

@@ -78,16 +78,74 @@ pub async fn fetch_external_calendar_events(
// If not fetched from cache, get from external URL // If not fetched from cache, get from external URL
if !fetched_from_cache { if !fetched_from_cache {
let client = Client::new(); // Log the URL being fetched for debugging
let response = client println!("🌍 Fetching calendar URL: {}", calendar.url);
.get(&calendar.url)
.send()
.await
.map_err(|e| ApiError::Internal(format!("Failed to fetch calendar: {}", e)))?;
if !response.status().is_success() { let user_agents = vec![
return Err(ApiError::Internal(format!("Calendar server returned: {}", response.status()))); "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Mozilla/5.0 (compatible; Runway Calendar/1.0)",
"Outlook-iOS/709.2226530.prod.iphone (3.24.1)"
];
let mut response = None;
let mut last_error = None;
// Try different user agents
for (i, ua) in user_agents.iter().enumerate() {
println!("🔄 Attempt {} with User-Agent: {}", i + 1, ua);
let client = Client::builder()
.redirect(reqwest::redirect::Policy::limited(10))
.timeout(std::time::Duration::from_secs(30))
.user_agent(*ua)
.build()
.map_err(|e| ApiError::Internal(format!("Failed to create HTTP client: {}", e)))?;
let result = client
.get(&calendar.url)
.header("Accept", "text/calendar,application/calendar+xml,text/plain,*/*")
.header("Accept-Charset", "utf-8")
.header("Cache-Control", "no-cache")
.send()
.await;
match result {
Ok(resp) => {
let status = resp.status();
println!("📡 Response status: {}", status);
if status.is_success() {
response = Some(resp);
break;
} else if status == 400 {
// Check if this is an Outlook auth error
let error_body = resp.text().await.unwrap_or_default();
if error_body.contains("OwaPage") || error_body.contains("Outlook") {
println!("🚫 Outlook authentication error detected, trying next approach...");
last_error = Some(format!("Outlook auth error: {}", error_body.chars().take(100).collect::<String>()));
continue;
} }
last_error = Some(format!("Bad Request: {}", error_body.chars().take(100).collect::<String>()));
} else {
last_error = Some(format!("HTTP {}", status));
}
}
Err(e) => {
println!("❌ Request failed: {}", e);
last_error = Some(format!("Request error: {}", e));
}
}
}
let response = response.ok_or_else(|| {
ApiError::Internal(format!(
"Failed to fetch calendar after trying {} different approaches. Last error: {}",
user_agents.len(),
last_error.unwrap_or("Unknown error".to_string())
))
})?;
// Response is guaranteed to be successful here since we checked in the loop
println!("✅ Successfully fetched calendar data");
ics_content = response ics_content = response
.text() .text()

View File

@@ -30,8 +30,8 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {
let is_creating = use_state(|| false); let is_creating = use_state(|| false);
// External Calendar state // External Calendar state
let external_name_ref = use_node_ref(); let external_name = use_state(|| String::new());
let external_url_ref = use_node_ref(); let external_url = use_state(|| String::new());
let external_selected_color = use_state(|| Some("#4285f4".to_string())); let external_selected_color = use_state(|| Some("#4285f4".to_string()));
let external_is_loading = use_state(|| false); let external_is_loading = use_state(|| false);
let external_error_message = use_state(|| None::<String>); let external_error_message = use_state(|| None::<String>);
@@ -43,6 +43,8 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {
let selected_color = selected_color.clone(); let selected_color = selected_color.clone();
let create_error_message = create_error_message.clone(); let create_error_message = create_error_message.clone();
let is_creating = is_creating.clone(); let is_creating = is_creating.clone();
let external_name = external_name.clone();
let external_url = external_url.clone();
let external_is_loading = external_is_loading.clone(); let external_is_loading = external_is_loading.clone();
let external_error_message = external_error_message.clone(); let external_error_message = external_error_message.clone();
let external_selected_color = external_selected_color.clone(); let external_selected_color = external_selected_color.clone();
@@ -56,6 +58,8 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {
selected_color.set(None); selected_color.set(None);
create_error_message.set(None); create_error_message.set(None);
is_creating.set(false); is_creating.set(false);
external_name.set(String::new());
external_url.set(String::new());
external_is_loading.set(false); external_is_loading.set(false);
external_error_message.set(None); external_error_message.set(None);
external_selected_color.set(Some("#4285f4".to_string())); external_selected_color.set(Some("#4285f4".to_string()));
@@ -146,8 +150,8 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {
// External Calendar handlers // External Calendar handlers
let on_external_submit = { let on_external_submit = {
let external_name_ref = external_name_ref.clone(); let external_name = external_name.clone();
let external_url_ref = external_url_ref.clone(); let external_url = external_url.clone();
let external_selected_color = external_selected_color.clone(); let external_selected_color = external_selected_color.clone();
let external_is_loading = external_is_loading.clone(); let external_is_loading = external_is_loading.clone();
let external_error_message = external_error_message.clone(); let external_error_message = external_error_message.clone();
@@ -157,24 +161,28 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {
Callback::from(move |e: SubmitEvent| { Callback::from(move |e: SubmitEvent| {
e.prevent_default(); e.prevent_default();
let name = external_name_ref let name = (*external_name).trim().to_string();
.cast::<HtmlInputElement>() let url = (*external_url).trim().to_string();
.map(|input| input.value())
.unwrap_or_default()
.trim()
.to_string();
let url = external_url_ref
.cast::<HtmlInputElement>()
.map(|input| input.value())
.unwrap_or_default()
.trim()
.to_string();
let color = (*external_selected_color).clone().unwrap_or_else(|| "#4285f4".to_string()); let color = (*external_selected_color).clone().unwrap_or_else(|| "#4285f4".to_string());
if name.is_empty() || url.is_empty() { // Debug logging to understand the issue
external_error_message.set(Some("Name and URL are required".to_string())); web_sys::console::log_1(&format!("External calendar form submission - Name: '{}', URL: '{}'", name, url).into());
if name.is_empty() {
external_error_message.set(Some("Calendar name is required".to_string()));
web_sys::console::log_1(&"Validation failed: empty name".into());
return;
}
if url.is_empty() {
external_error_message.set(Some("Calendar URL is required".to_string()));
web_sys::console::log_1(&"Validation failed: empty URL".into());
return;
}
// Basic URL validation
if !url.starts_with("http://") && !url.starts_with("https://") {
external_error_message.set(Some("Please enter a valid HTTP or HTTPS URL".to_string()));
return; return;
} }
@@ -204,6 +212,25 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {
}) })
}; };
// External input change handlers
let on_external_name_change = {
let external_name = external_name.clone();
Callback::from(move |e: Event| {
if let Some(input) = e.target_dyn_into::<HtmlInputElement>() {
external_name.set(input.value());
}
})
};
let on_external_url_change = {
let external_url = external_url.clone();
Callback::from(move |e: Event| {
if let Some(input) = e.target_dyn_into::<HtmlInputElement>() {
external_url.set(input.value());
}
})
};
if !props.is_open { if !props.is_open {
return html! {}; return html! {};
} }
@@ -333,7 +360,8 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {
<input <input
type="text" type="text"
id="external-name" id="external-name"
ref={external_name_ref.clone()} value={(*external_name).clone()}
onchange={on_external_name_change}
placeholder="Enter calendar name" placeholder="Enter calendar name"
disabled={*external_is_loading} disabled={*external_is_loading}
/> />
@@ -344,7 +372,8 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {
<input <input
type="url" type="url"
id="external-url" id="external-url"
ref={external_url_ref.clone()} value={(*external_url).clone()}
onchange={on_external_url_change}
placeholder="https://example.com/calendar.ics" placeholder="https://example.com/calendar.ics"
disabled={*external_is_loading} disabled={*external_is_loading}
/> />