Add comprehensive reminder/alarm support to calendar events
- Backend: Parse VALARM components from CalDAV iCalendar data - Backend: Add EventReminder struct with minutes_before, action, and description - Backend: Support Display, Email, and Audio reminder types - Backend: Parse ISO 8601 duration triggers (-PT15M, -P1D, etc.) - Frontend: Add reminders field to CalendarEvent structure - Frontend: Display reminders in event modal with human-readable formatting - Frontend: Show reminder timing (15 minutes before, 1 day before) and action type - Fix: Update Trunk.toml to properly copy CSS files to dist directory 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
BIN
backend/calendar.db
Normal file
BIN
backend/calendar.db
Normal file
Binary file not shown.
@@ -53,6 +53,9 @@ pub struct CalendarEvent {
|
||||
/// All-day event flag
|
||||
pub all_day: bool,
|
||||
|
||||
/// Reminders/alarms for this event
|
||||
pub reminders: Vec<EventReminder>,
|
||||
|
||||
/// ETag from CalDAV server for conflict detection
|
||||
pub etag: Option<String>,
|
||||
|
||||
@@ -88,6 +91,27 @@ impl Default for EventClass {
|
||||
}
|
||||
}
|
||||
|
||||
/// Event reminder/alarm information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct EventReminder {
|
||||
/// How long before the event to trigger the reminder (in minutes)
|
||||
pub minutes_before: i32,
|
||||
|
||||
/// Type of reminder action
|
||||
pub action: ReminderAction,
|
||||
|
||||
/// Optional description for the reminder
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
/// Reminder action types
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ReminderAction {
|
||||
Display,
|
||||
Email,
|
||||
Audio,
|
||||
}
|
||||
|
||||
/// CalDAV client for fetching and parsing calendar events
|
||||
pub struct CalDAVClient {
|
||||
config: crate::config::CalDAVConfig,
|
||||
@@ -244,8 +268,8 @@ impl CalDAVClient {
|
||||
let mut properties: HashMap<String, String> = HashMap::new();
|
||||
|
||||
// Extract all properties from the event
|
||||
for property in event.properties {
|
||||
properties.insert(property.name.to_uppercase(), property.value.unwrap_or_default());
|
||||
for property in &event.properties {
|
||||
properties.insert(property.name.to_uppercase(), property.value.clone().unwrap_or_default());
|
||||
}
|
||||
|
||||
// Required UID field
|
||||
@@ -325,11 +349,82 @@ impl CalDAVClient {
|
||||
last_modified,
|
||||
recurrence_rule: properties.get("RRULE").cloned(),
|
||||
all_day,
|
||||
reminders: self.parse_alarms(&event)?,
|
||||
etag: None, // Set by caller
|
||||
href: None, // Set by caller
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse VALARM components from an iCal event
|
||||
fn parse_alarms(&self, event: &ical::parser::ical::component::IcalEvent) -> Result<Vec<EventReminder>, CalDAVError> {
|
||||
let mut reminders = Vec::new();
|
||||
|
||||
for alarm in &event.alarms {
|
||||
if let Ok(reminder) = self.parse_single_alarm(alarm) {
|
||||
reminders.push(reminder);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(reminders)
|
||||
}
|
||||
|
||||
/// Parse a single VALARM component into an EventReminder
|
||||
fn parse_single_alarm(&self, alarm: &ical::parser::ical::component::IcalAlarm) -> Result<EventReminder, CalDAVError> {
|
||||
let mut properties: HashMap<String, String> = HashMap::new();
|
||||
|
||||
// Extract all properties from the alarm
|
||||
for property in &alarm.properties {
|
||||
properties.insert(property.name.to_uppercase(), property.value.clone().unwrap_or_default());
|
||||
}
|
||||
|
||||
// Parse ACTION (required)
|
||||
let action = match properties.get("ACTION").map(|s| s.to_uppercase()) {
|
||||
Some(ref action_str) if action_str == "DISPLAY" => ReminderAction::Display,
|
||||
Some(ref action_str) if action_str == "EMAIL" => ReminderAction::Email,
|
||||
Some(ref action_str) if action_str == "AUDIO" => ReminderAction::Audio,
|
||||
_ => ReminderAction::Display, // Default
|
||||
};
|
||||
|
||||
// Parse TRIGGER (required)
|
||||
let minutes_before = if let Some(trigger) = properties.get("TRIGGER") {
|
||||
self.parse_trigger_duration(trigger).unwrap_or(15) // Default 15 minutes
|
||||
} else {
|
||||
15 // Default 15 minutes
|
||||
};
|
||||
|
||||
// Get description
|
||||
let description = properties.get("DESCRIPTION").cloned();
|
||||
|
||||
Ok(EventReminder {
|
||||
minutes_before,
|
||||
action,
|
||||
description,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse a TRIGGER duration string into minutes before event
|
||||
fn parse_trigger_duration(&self, trigger: &str) -> Option<i32> {
|
||||
// Basic parsing of ISO 8601 duration or relative time
|
||||
// Examples: "-PT15M" (15 minutes before), "-P1D" (1 day before)
|
||||
|
||||
if trigger.starts_with("-PT") && trigger.ends_with("M") {
|
||||
// Parse "-PT15M" format (minutes)
|
||||
let minutes_str = &trigger[3..trigger.len()-1];
|
||||
minutes_str.parse::<i32>().ok()
|
||||
} else if trigger.starts_with("-PT") && trigger.ends_with("H") {
|
||||
// Parse "-PT1H" format (hours)
|
||||
let hours_str = &trigger[3..trigger.len()-1];
|
||||
hours_str.parse::<i32>().ok().map(|h| h * 60)
|
||||
} else if trigger.starts_with("-P") && trigger.ends_with("D") {
|
||||
// Parse "-P1D" format (days)
|
||||
let days_str = &trigger[2..trigger.len()-1];
|
||||
days_str.parse::<i32>().ok().map(|d| d * 24 * 60)
|
||||
} else {
|
||||
// Try to parse as raw minutes
|
||||
trigger.parse::<i32>().ok().map(|m| m.abs())
|
||||
}
|
||||
}
|
||||
|
||||
/// Discover available calendar collections on the server
|
||||
pub async fn discover_calendars(&self) -> Result<Vec<String>, CalDAVError> {
|
||||
// First, try to discover user calendars if we have a calendar path in config
|
||||
|
||||
61
backend/src/debug_caldav.rs
Normal file
61
backend/src/debug_caldav.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use crate::calendar::CalDAVClient;
|
||||
use crate::config::CalDAVConfig;
|
||||
|
||||
pub async fn debug_caldav_fetch() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let config = CalDAVConfig::from_env()?;
|
||||
let client = CalDAVClient::new(config);
|
||||
|
||||
println!("=== DEBUG: CalDAV Fetch ===");
|
||||
|
||||
// Discover calendars
|
||||
let calendars = client.discover_calendars().await?;
|
||||
println!("Found {} calendars: {:?}", calendars.len(), calendars);
|
||||
|
||||
if let Some(calendar_path) = calendars.first() {
|
||||
println!("Fetching events from: {}", calendar_path);
|
||||
|
||||
// Make the raw REPORT request
|
||||
let report_body = r#"<?xml version="1.0" encoding="utf-8" ?>
|
||||
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
||||
<d:prop>
|
||||
<d:getetag/>
|
||||
<c:calendar-data/>
|
||||
</d:prop>
|
||||
<c:filter>
|
||||
<c:comp-filter name="VCALENDAR">
|
||||
<c:comp-filter name="VEVENT"/>
|
||||
</c:comp-filter>
|
||||
</c:filter>
|
||||
</c:calendar-query>"#;
|
||||
|
||||
let url = format!("{}{}", client.config.server_url.trim_end_matches('/'), calendar_path);
|
||||
println!("Request URL: {}", url);
|
||||
|
||||
let response = client.http_client
|
||||
.request(reqwest::Method::from_bytes(b"REPORT").unwrap(), &url)
|
||||
.header("Authorization", format!("Basic {}", client.config.get_basic_auth()))
|
||||
.header("Content-Type", "application/xml")
|
||||
.header("Depth", "1")
|
||||
.header("User-Agent", "calendar-app/0.1.0")
|
||||
.body(report_body)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
println!("Response status: {}", response.status());
|
||||
let body = response.text().await?;
|
||||
println!("Response body length: {}", body.len());
|
||||
println!("First 500 chars of response: {}", &body[..std::cmp::min(500, body.len())]);
|
||||
|
||||
// Try to parse it
|
||||
let events = client.parse_calendar_response(&body)?;
|
||||
println!("Parsed {} events", events.len());
|
||||
|
||||
for (i, event) in events.iter().enumerate() {
|
||||
println!("Event {}: {}", i+1, event.summary.as_deref().unwrap_or("No title"));
|
||||
println!(" Start: {}", event.start);
|
||||
println!(" UID: {}", event.uid);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user