feat: add CalDAV synchronization with automatic sync
- Add CaldavService with tsdav/ical.js for CalDAV server communication - Add CaldavController, CaldavRepository, and caldav routes - Add client-side CaldavConfigService with sync(), config CRUD - Add CalDAV settings UI with config load/save in settings screen - Sync on login, auto-login (AuthGuard), periodic timer (calendar), and sync button - Push single events to CalDAV on server-side create/update/delete - Push all events to CalDAV after chat event confirmation - Refactor ChatService to use EventService instead of direct EventRepository - Rename CalDav/calDav to Caldav/caldav for consistent naming - Add Radicale Docker setup for local CalDAV testing - Update PlantUML diagrams and CLAUDE.md with CalDAV architecture
This commit is contained in:
100
CLAUDE.md
100
CLAUDE.md
@@ -51,6 +51,8 @@ npm run start -w @calchat/server # Run compiled server (port 3000)
|
||||
| | X-User-Id Header | Authentication (simple, no JWT yet) |
|
||||
| | pino / pino-http | Structured logging |
|
||||
| | react-native-logs | Client-side logging |
|
||||
| | tsdav | CalDAV client library |
|
||||
| | ical.js | iCalendar parsing/generation |
|
||||
| Planned | iCalendar | Event export/import |
|
||||
|
||||
## Architecture
|
||||
@@ -82,7 +84,7 @@ src/
|
||||
│ └── note/
|
||||
│ └── [id].tsx # Note editor for event (dynamic route)
|
||||
├── components/
|
||||
│ ├── AuthGuard.tsx # Auth wrapper: loads user, shows loading, redirects if unauthenticated
|
||||
│ ├── AuthGuard.tsx # Auth wrapper: loads user, CalDAV sync on auto-login, shows loading, redirects if unauthenticated
|
||||
│ ├── BaseBackground.tsx # Common screen wrapper (themed)
|
||||
│ ├── BaseButton.tsx # Reusable button component (themed, supports children)
|
||||
│ ├── Header.tsx # Header component (themed)
|
||||
@@ -96,6 +98,7 @@ src/
|
||||
│ ├── EventConfirmDialog.tsx # AI-proposed event confirmation modal (skeleton)
|
||||
│ ├── ProposedEventCard.tsx # Chat event proposal (uses EventCardBase + confirm/reject/edit buttons)
|
||||
│ ├── DeleteEventModal.tsx # Delete confirmation modal (uses ModalBase)
|
||||
│ ├── CustomTextInput.tsx # Themed text input with focus border (used in CaldavSettings)
|
||||
│ ├── DateTimePicker.tsx # Date and time picker components
|
||||
│ └── ScrollableDropdown.tsx # Scrollable dropdown component
|
||||
├── Themes.tsx # Theme definitions: THEMES object with defaultLight/defaultDark, Theme type
|
||||
@@ -107,7 +110,8 @@ src/
|
||||
│ ├── ApiClient.ts # HTTP client with X-User-Id header injection, request logging, handles empty responses (204)
|
||||
│ ├── AuthService.ts # login(), register(), logout() - calls API and updates AuthStore
|
||||
│ ├── EventService.ts # getAll(), getById(), getByDateRange(), create(), update(), delete(mode, occurrenceDate)
|
||||
│ └── ChatService.ts # sendMessage(), confirmEvent(deleteMode, occurrenceDate), rejectEvent(), getConversations(), getConversation(), updateProposalEvent()
|
||||
│ ├── ChatService.ts # sendMessage(), confirmEvent(deleteMode, occurrenceDate), rejectEvent(), getConversations(), getConversation(), updateProposalEvent()
|
||||
│ └── CaldavConfigService.ts # saveConfig(), getConfig(), deleteConfig(), pull(), pushAll(), sync()
|
||||
├── stores/ # Zustand state management
|
||||
│ ├── index.ts # Re-exports all stores
|
||||
│ ├── AuthStore.ts # user, isAuthenticated, isLoading, login(), logout(), loadStoredUser()
|
||||
@@ -228,8 +232,9 @@ src/
|
||||
├── app.ts # Entry point, DI setup, Express config
|
||||
├── controllers/ # Request handlers + middleware (per architecture diagram)
|
||||
│ ├── AuthController.ts # login(), register(), refresh(), logout()
|
||||
│ ├── ChatController.ts # sendMessage(), confirmEvent(), rejectEvent(), getConversations(), getConversation(), updateProposalEvent()
|
||||
│ ├── EventController.ts # create(), getById(), getAll(), getByDateRange(), update(), delete()
|
||||
│ ├── ChatController.ts # sendMessage(), confirmEvent() + CalDAV push, rejectEvent(), getConversations(), getConversation(), updateProposalEvent()
|
||||
│ ├── EventController.ts # create(), getById(), getAll(), getByDateRange(), update(), delete() - pushes/deletes to CalDAV on mutations
|
||||
│ ├── CaldavController.ts # saveConfig(), loadConfig(), deleteConfig(), pullEvents(), pushEvents(), pushEvent()
|
||||
│ ├── AuthMiddleware.ts # authenticate() - X-User-Id header validation
|
||||
│ └── LoggingMiddleware.ts # httpLogger - pino-http request logging
|
||||
├── logging/
|
||||
@@ -240,16 +245,19 @@ src/
|
||||
│ ├── index.ts # Combines all routes under /api
|
||||
│ ├── auth.routes.ts # /api/auth/*
|
||||
│ ├── chat.routes.ts # /api/chat/* (protected)
|
||||
│ └── event.routes.ts # /api/events/* (protected)
|
||||
│ ├── event.routes.ts # /api/events/* (protected)
|
||||
│ └── caldav.routes.ts # /api/caldav/* (protected)
|
||||
├── services/ # Business logic
|
||||
│ ├── interfaces/ # DB-agnostic interfaces (for dependency injection)
|
||||
│ │ ├── AIProvider.ts # processMessage()
|
||||
│ │ ├── UserRepository.ts # findById, findByEmail, findByUserName, create + CreateUserData
|
||||
│ │ ├── EventRepository.ts
|
||||
│ │ └── ChatRepository.ts
|
||||
│ │ ├── ChatRepository.ts
|
||||
│ │ └── CaldavRepository.ts
|
||||
│ ├── AuthService.ts
|
||||
│ ├── ChatService.ts
|
||||
│ └── EventService.ts
|
||||
│ ├── EventService.ts
|
||||
│ └── CaldavService.ts # connect(), pullEvents(), pushEvent(), pushAll(), deleteEvent(), sync logic
|
||||
├── repositories/ # Data access (DB-specific implementations)
|
||||
│ ├── index.ts # Re-exports from ./mongo
|
||||
│ └── mongo/ # MongoDB implementation
|
||||
@@ -257,10 +265,12 @@ src/
|
||||
│ │ ├── types.ts # Shared types (IdVirtual interface)
|
||||
│ │ ├── UserModel.ts
|
||||
│ │ ├── EventModel.ts
|
||||
│ │ └── ChatModel.ts
|
||||
│ │ ├── ChatModel.ts
|
||||
│ │ └── CaldavConfigModel.ts
|
||||
│ ├── MongoUserRepository.ts # findById, findByEmail, findByUserName, create
|
||||
│ ├── MongoEventRepository.ts
|
||||
│ └── MongoChatRepository.ts
|
||||
│ ├── MongoChatRepository.ts
|
||||
│ └── MongoCaldavRepository.ts
|
||||
├── ai/
|
||||
│ ├── GPTAdapter.ts # Implements AIProvider using OpenAI GPT
|
||||
│ ├── index.ts # Re-exports GPTAdapter
|
||||
@@ -296,6 +306,12 @@ src/
|
||||
- `GET /api/chat/conversations` - Get all conversations (protected)
|
||||
- `GET /api/chat/conversations/:id` - Get messages of a conversation with cursor-based pagination (protected)
|
||||
- `PUT /api/chat/messages/:messageId/proposal` - Update proposal event data before confirming (protected)
|
||||
- `PUT /api/caldav/config` - Save CalDAV config (protected)
|
||||
- `GET /api/caldav/config` - Load CalDAV config (protected)
|
||||
- `DELETE /api/caldav/config` - Delete CalDAV config (protected)
|
||||
- `POST /api/caldav/pull` - Pull events from CalDAV server (protected)
|
||||
- `POST /api/caldav/pushAll` - Push all unsynced events (protected)
|
||||
- `POST /api/caldav/push/:caldavUUID` - Push single event (protected)
|
||||
- `GET /health` - Health check
|
||||
- `POST /api/ai/test` - AI test endpoint (development only)
|
||||
|
||||
@@ -307,7 +323,8 @@ src/
|
||||
├── models/
|
||||
│ ├── index.ts
|
||||
│ ├── User.ts # User, CreateUserDTO, LoginDTO, AuthResponse
|
||||
│ ├── CalendarEvent.ts # CalendarEvent, CreateEventDTO, UpdateEventDTO, ExpandedEvent
|
||||
│ ├── CalendarEvent.ts # CalendarEvent, CreateEventDTO, UpdateEventDTO, ExpandedEvent, CaldavSyncStatus
|
||||
│ ├── CaldavConfig.ts # CaldavConfig
|
||||
│ ├── ChatMessage.ts # ChatMessage, Conversation, SendMessageDTO, CreateMessageDTO,
|
||||
│ │ # GetMessagesOptions, ChatResponse, ConversationSummary,
|
||||
│ │ # ProposedEventChange, EventAction, RespondedAction, UpdateMessageDTO
|
||||
@@ -322,7 +339,9 @@ src/
|
||||
|
||||
**Key Types:**
|
||||
- `User`: id, email, userName, passwordHash?, createdAt?, updatedAt?
|
||||
- `CalendarEvent`: id, userId, title, description?, startTime, endTime, note?, isRecurring?, recurrenceRule?, exceptionDates?
|
||||
- `CalendarEvent`: id, userId, caldavUUID?, etag?, title, description?, startTime, endTime, note?, recurrenceRule?, exceptionDates?, caldavSyncStatus?
|
||||
- `CaldavConfig`: userId, serverUrl, username, password, syncIntervalSeconds?
|
||||
- `CaldavSyncStatus`: 'synced' | 'error'
|
||||
- `ExpandedEvent`: Extends CalendarEvent with occurrenceStart, occurrenceEnd (for recurring event instances)
|
||||
- `ChatMessage`: id, conversationId, sender ('user' | 'assistant'), content, proposedChanges?
|
||||
- `ProposedEventChange`: id, action ('create' | 'update' | 'delete'), eventId?, event?, updates?, respondedAction?, deleteMode?, occurrenceDate?, conflictingEvents?
|
||||
@@ -377,6 +396,32 @@ When creating events, `toolExecutor` automatically:
|
||||
3. Returns `conflictingEvents` array in the proposal for UI display
|
||||
4. Adds ⚠️ warning to tool result so AI can inform user
|
||||
|
||||
### CalDAV Synchronization
|
||||
|
||||
CalDAV sync with external calendar servers (e.g., Radicale) using `tsdav` and `ical.js`.
|
||||
|
||||
**Naming Convention:** All CalDAV-related identifiers use `Caldav` (PascalCase) / `caldav` (camelCase), NOT `CalDav`. The only exception is the protocol name "CalDAV" in comments and log messages.
|
||||
|
||||
**Sync Triggers (client-side via `CaldavConfigService.sync()`):**
|
||||
- **Login** (`login.tsx`): After successful authentication
|
||||
- **Auto-login** (`AuthGuard.tsx`): After `loadStoredUser()` if authenticated
|
||||
- **Calendar timer** (`calendar.tsx`): Every 10s while Calendar tab is focused, via `setInterval` in `useFocusEffect`
|
||||
- **Sync button** (`settings.tsx`): Manual trigger in CaldavSettings
|
||||
|
||||
**Single-event sync (server-side in controllers):**
|
||||
- `EventController`: `pushToCaldav()` after create/update, `deleteFromCaldav()` after delete
|
||||
- `ChatController`: `pushAll()` after confirming an event proposal
|
||||
|
||||
**Sync Flow:**
|
||||
1. `sync()` calls `pushAll` (push unsynced local events) then `pull` (fetch remote events)
|
||||
2. `pullEvents`: Compares etags to skip unchanged events, creates/updates local events, deletes locally if removed remotely
|
||||
3. `pushEvent`: Creates or updates remote event, fetches new etag after push
|
||||
|
||||
**Architecture:**
|
||||
- `CaldavService` depends on `CaldavRepository` (config storage) and `EventService` (event CRUD)
|
||||
- `ChatService` depends only on `EventService` (not EventRepository) for all event operations
|
||||
- `EventController` and `ChatController` both receive `CaldavService` for CalDAV push on mutations
|
||||
|
||||
### Database Abstraction
|
||||
|
||||
The repository pattern allows swapping databases:
|
||||
@@ -460,7 +505,7 @@ The `@Logged` decorator automatically summarizes large arguments to keep logs re
|
||||
### Nice-to-Have
|
||||
- iCalendar import/export
|
||||
- Multiple calendars
|
||||
- CalDAV synchronization with external services
|
||||
- ~~CalDAV synchronization with external services~~ (implemented)
|
||||
|
||||
## Development Environment
|
||||
|
||||
@@ -473,6 +518,13 @@ docker compose down # Stop services
|
||||
- MongoDB: `localhost:27017` (root/mongoose)
|
||||
- Mongo Express UI: `localhost:8083` (admin/admin)
|
||||
|
||||
### Radicale CalDAV Server (Docker)
|
||||
```bash
|
||||
cd apps/server/docker/radicale
|
||||
docker compose up -d # Start Radicale CalDAV server
|
||||
```
|
||||
- Radicale: `localhost:5232`
|
||||
|
||||
### Environment Variables
|
||||
Server requires `.env` file in `apps/server/`:
|
||||
```
|
||||
@@ -510,12 +562,20 @@ NODE_ENV=development # development = pretty logs, production = JSON
|
||||
- `GPTAdapter`: Full implementation with OpenAI GPT (gpt-4o-mini model), function calling for calendar operations, collects multiple proposals per response
|
||||
- `ai/utils/`: Provider-agnostic shared utilities (systemPrompt, toolDefinitions, toolExecutor)
|
||||
- `ai/utils/systemPrompt`: AI fetches events on-demand (no pre-loaded context), includes RRULE documentation, warns AI not to put RRULE in description field, instructs AI not to show event IDs to users
|
||||
- `ai/utils/toolDefinitions`: proposeUpdateEvent supports `isRecurring` and `recurrenceRule` parameters, getEventsInRange tool for on-demand event loading
|
||||
- `ai/utils/toolDefinitions`: proposeUpdateEvent supports `recurrenceRule` parameter, getEventsInRange tool for on-demand event loading
|
||||
- `ai/utils/toolExecutor`: Async execution, conflict detection uses `occurrenceStart/occurrenceEnd` for recurring events, returns `conflictingEvents` in proposals
|
||||
- `MongoEventRepository`: Includes `searchByTitle()` for case-insensitive title search
|
||||
- `utils/recurrenceExpander`: Handles RRULE parsing, strips `RRULE:` prefix if present (AI may include it), filters out exceptionDates
|
||||
- `logging/`: Structured logging with pino, pino-http middleware, @Logged decorator
|
||||
- All repositories and GPTAdapter decorated with @Logged for automatic method logging
|
||||
- `CaldavService`: Full CalDAV sync (connect, pullEvents, pushEvent, pushAll, deleteEvent, getConfig, saveConfig, deleteConfig)
|
||||
- `CaldavController`: REST endpoints for config CRUD, pull, push
|
||||
- `MongoCaldavRepository`: Config persistence with createOrUpdate, findByUserId, deleteByUserId
|
||||
- `EventController`: CalDAV push on create/update, CalDAV delete on delete (via pushToCaldav/deleteFromCaldav helpers)
|
||||
- `ChatController`: CalDAV pushAll after confirmEvent (ensures chat-created events sync)
|
||||
- `ChatService`: Refactored to use only EventService (no direct EventRepository dependency)
|
||||
- `EventService`: Extended with searchByTitle(), findByCaldavUUID()
|
||||
- `utils/eventFormatters`: Refactored to use EventService instead of EventRepository
|
||||
- CORS configured to allow X-User-Id header
|
||||
- **Stubbed (TODO):**
|
||||
- `AuthController`: refresh(), logout()
|
||||
@@ -523,9 +583,9 @@ NODE_ENV=development # development = pretty logs, production = JSON
|
||||
- JWT authentication (currently using simple X-User-Id header)
|
||||
|
||||
**Shared:**
|
||||
- Types, DTOs, constants (Day, Month with German translations), ExpandedEvent type defined and exported
|
||||
- Types, DTOs, constants (Day, Month with German translations), ExpandedEvent type, CaldavConfig, CaldavSyncStatus defined and exported
|
||||
- `rruleHelpers.ts`: `parseRRule()` parses RRULE strings using rrule library, returns `ParsedRRule` with freq, until, count, interval, byDay
|
||||
- `formatters.ts`: German date/time formatters (`formatDate`, `formatTime`, `formatDateTime`, `formatDateWithWeekday`) used by both client and server
|
||||
- `formatters.ts`: German date/time formatters (`formatDate`, `formatTime`, `formatDateTime`, `formatDateWithWeekday`, `formatDateKey`) used by both client and server
|
||||
- rrule library added as dependency for RRULE parsing
|
||||
|
||||
**Frontend:**
|
||||
@@ -533,8 +593,8 @@ NODE_ENV=development # development = pretty logs, production = JSON
|
||||
- `AuthStore`: Manages user state with expo-secure-store (native) / localStorage (web)
|
||||
- `AuthService`: login(), register(), logout() - calls backend API
|
||||
- `ApiClient`: Automatically injects X-User-Id header for authenticated requests, handles empty responses (204)
|
||||
- `AuthGuard`: Reusable component that wraps protected routes - loads user, shows loading, redirects if unauthenticated
|
||||
- Login screen: Supports email OR userName login
|
||||
- `AuthGuard`: Reusable component that wraps protected routes - loads user, triggers CalDAV sync on auto-login, shows loading, redirects if unauthenticated
|
||||
- Login screen: Supports email OR userName login, triggers CalDAV sync after successful login
|
||||
- Register screen: Email validation, checks for existing email/userName
|
||||
- `AuthButton`: Reusable button component with themed shadow
|
||||
- `Header`: Themed header component (logout moved to Settings)
|
||||
@@ -544,7 +604,7 @@ NODE_ENV=development # development = pretty logs, production = JSON
|
||||
- `ThemeStore`: Zustand store with theme state and setTheme()
|
||||
- `Themes.tsx`: THEMES object with defaultLight/defaultDark variants
|
||||
- All components use `useThemeStore()` for reactive theme colors
|
||||
- Settings screen with theme switcher (light/dark)
|
||||
- Settings screen with theme switcher (light/dark) and CalDAV configuration (url, username, password with save/sync buttons, loads existing config on mount)
|
||||
- `BaseButton`: Reusable themed button component
|
||||
- Tab navigation (Chat, Calendar, Settings) implemented with themed UI
|
||||
- Calendar screen fully functional:
|
||||
@@ -554,7 +614,7 @@ NODE_ENV=development # development = pretty logs, production = JSON
|
||||
- Orange dot indicator for days with events
|
||||
- Tap-to-open modal overlay showing EventCards for selected day
|
||||
- Supports events from adjacent months visible in grid
|
||||
- Uses `useFocusEffect` for automatic reload on tab focus
|
||||
- Uses `useFocusEffect` for automatic reload on tab focus with periodic CalDAV sync (10s interval while focused)
|
||||
- DeleteEventModal integration for recurring event deletion with three modes
|
||||
- EventOverlay hides when DeleteEventModal is open (fixes modal stacking on web)
|
||||
- Chat screen fully functional with FlashList, message sending, and event confirm/reject
|
||||
@@ -570,6 +630,8 @@ NODE_ENV=development # development = pretty logs, production = JSON
|
||||
- keyboardDismissMode="interactive" and keyboardShouldPersistTaps="handled"
|
||||
- `EventService`: getAll(), getById(), getByDateRange(), create(), update(), delete(mode, occurrenceDate) - fully implemented with recurring delete modes
|
||||
- `ChatService`: sendMessage(), confirmEvent(deleteMode, occurrenceDate), rejectEvent(), getConversations(), getConversation(), updateProposalEvent() - fully implemented with cursor pagination, recurring delete support, and proposal editing
|
||||
- `CaldavConfigService`: saveConfig(), getConfig(), deleteConfig(), pull(), pushAll(), sync() - CalDAV config management and sync trigger
|
||||
- `CustomTextInput`: Themed text input component with focus border highlight, supports controlled value via `text` prop
|
||||
- `CardBase`: Reusable card component with header (title/subtitle), content area, and optional footer button - configurable padding, border, text size via props, ScrollView uses `nestedScrollEnabled` for Android
|
||||
- `ModalBase`: Reusable modal wrapper with backdrop (absolute-positioned behind card), uses CardBase internally - provides click-outside-to-close, Android back button support, and proper scrolling on Android
|
||||
- `EventCardBase`: Event card with date/time/recurring icons - uses CardBase for structure
|
||||
|
||||
Reference in New Issue
Block a user