- Add CalendarToolbar component between header and weekdays in calendar.tsx - Sync button with CalDAV sync, spinner during sync, green checkmark on success, red X on error (3s feedback) - Sync button disabled/greyed out when no CalDAV config present - Logout button with redirect to login screen - Buttons styled with border and shadow - Update CLAUDE.md with CalendarToolbar documentation
687 lines
39 KiB
Markdown
687 lines
39 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## Project Overview
|
|
|
|
**CalChat** is a calendar mobile app with AI support. The core concept is managing calendar events through a chat interface with an AI chatbot. Users can add, edit, and delete events via natural language conversation.
|
|
|
|
This is a fullstack TypeScript monorepo with npm workspaces.
|
|
|
|
## Commands
|
|
|
|
### Root (monorepo)
|
|
```bash
|
|
npm install # Install all dependencies for all workspaces
|
|
npm run format # Format all TypeScript files with Prettier
|
|
```
|
|
|
|
### Client (apps/client) - Expo React Native app
|
|
```bash
|
|
npm run start -w @calchat/client # Start Expo dev server
|
|
npm run android -w @calchat/client # Start on Android
|
|
npm run ios -w @calchat/client # Start on iOS
|
|
npm run web -w @calchat/client # Start web version
|
|
npm run lint -w @calchat/client # Run ESLint
|
|
npm run build:apk -w @calchat/client # Build APK locally with EAS
|
|
```
|
|
|
|
### Server (apps/server) - Express.js backend
|
|
```bash
|
|
npm run dev -w @calchat/server # Start dev server with hot reload (tsx watch)
|
|
npm run build -w @calchat/server # Compile TypeScript
|
|
npm run start -w @calchat/server # Run compiled server (port 3000)
|
|
```
|
|
|
|
## Technology Stack
|
|
|
|
| Area | Technology | Purpose |
|
|
|------|------------|---------|
|
|
| Frontend | React Native | Mobile UI Framework |
|
|
| | Expo | Development platform |
|
|
| | Expo-Router | File-based routing |
|
|
| | NativeWind | Tailwind CSS for React Native |
|
|
| | Zustand | State management |
|
|
| | FlashList | High-performance lists |
|
|
| | EAS Build | Local APK/IPA builds |
|
|
| Backend | Express.js | Web framework |
|
|
| | MongoDB | Database |
|
|
| | Mongoose | ODM |
|
|
| | GPT (OpenAI) | AI/LLM for chat |
|
|
| | X-User-Id Header | Authentication |
|
|
| | 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
|
|
|
|
### Workspace Structure
|
|
```
|
|
apps/client - @calchat/client - Expo React Native app
|
|
apps/server - @calchat/server - Express.js backend
|
|
packages/shared - @calchat/shared - Shared TypeScript types and models
|
|
```
|
|
|
|
### Frontend Architecture (apps/client)
|
|
|
|
```
|
|
src/
|
|
├── app/ # Expo-Router file-based routing
|
|
│ ├── _layout.tsx # Root Stack layout
|
|
│ ├── index.tsx # Entry redirect
|
|
│ ├── login.tsx # Login screen
|
|
│ ├── register.tsx # Registration screen
|
|
│ ├── (tabs)/ # Tab navigation group
|
|
│ │ ├── _layout.tsx # Tab bar configuration (themed)
|
|
│ │ ├── chat.tsx # Chat screen (AI conversation)
|
|
│ │ ├── calendar.tsx # Calendar overview (with CalendarToolbar: sync + logout)
|
|
│ │ └── settings.tsx # Settings screen (theme switcher, logout, CalDAV config with feedback)
|
|
│ ├── editEvent.tsx # Event edit screen (dual-mode: calendar/chat)
|
|
│ ├── event/
|
|
│ │ └── [id].tsx # Event detail screen (dynamic route)
|
|
│ └── note/
|
|
│ └── [id].tsx # Note editor for event (dynamic route)
|
|
├── components/
|
|
│ ├── AuthGuard.tsx # Auth wrapper: loads user, preloads app data (events + CalDAV config), CalDAV sync, shows loading, redirects if unauthenticated. Exports preloadAppData()
|
|
│ ├── BaseBackground.tsx # Common screen wrapper (themed)
|
|
│ ├── BaseButton.tsx # Reusable button component (themed, supports children)
|
|
│ ├── Header.tsx # Header component (themed)
|
|
│ ├── AuthButton.tsx # Reusable button for auth screens (themed, with shadow)
|
|
│ ├── CardBase.tsx # Reusable card component (header + content + optional footer)
|
|
│ ├── ModalBase.tsx # Reusable modal with backdrop (uses CardBase, click-outside-to-close)
|
|
│ ├── ChatBubble.tsx # Reusable chat bubble component (used by ChatMessage & TypingIndicator)
|
|
│ ├── TypingIndicator.tsx # Animated typing indicator (. .. ...) shown while waiting for AI response
|
|
│ ├── EventCardBase.tsx # Event card layout with icons (uses CardBase)
|
|
│ ├── EventCard.tsx # Calendar event card (uses EventCardBase + edit/delete buttons)
|
|
│ ├── 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 login, register, CaldavSettings, editEvent)
|
|
│ ├── DateTimePicker.tsx # Date and time picker components
|
|
│ └── ScrollableDropdown.tsx # Scrollable dropdown component
|
|
├── Themes.tsx # Theme definitions: THEMES object with defaultLight/defaultDark, Theme type
|
|
├── logging/
|
|
│ ├── index.ts # Re-exports
|
|
│ └── logger.ts # react-native-logs config (apiLogger, storeLogger)
|
|
├── services/
|
|
│ ├── index.ts # Re-exports all services
|
|
│ ├── 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()
|
|
│ └── 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()
|
|
│ │ # Uses expo-secure-store (native) / localStorage (web)
|
|
│ ├── ChatStore.ts # messages[], isWaitingForResponse, addMessage(), addMessages(), updateMessage(), clearMessages(), setWaitingForResponse(), chatMessageToMessageData()
|
|
│ ├── EventsStore.ts # events[], setEvents(), addEvent(), updateEvent(), deleteEvent()
|
|
│ ├── CaldavConfigStore.ts # config (CaldavConfig | null), setConfig() - cached CalDAV config
|
|
│ └── ThemeStore.ts # theme, setTheme() - reactive theme switching with Zustand
|
|
└── hooks/
|
|
└── useDropdownPosition.ts # Hook for positioning dropdowns relative to trigger element
|
|
```
|
|
|
|
**Routing:** Tab-based navigation with Chat, Calendar, and Settings as main screens. Auth screens (login, register) outside tabs. Dynamic routes for event detail and note editing.
|
|
|
|
**Authentication Flow:**
|
|
- `AuthGuard` component wraps the tab layout in `(tabs)/_layout.tsx`
|
|
- On app start, `AuthGuard` calls `loadStoredUser()` and shows loading indicator
|
|
- After auth, `preloadAppData()` loads events (current month) + CalDAV config into stores before dismissing spinner
|
|
- If not authenticated, redirects to `/login`
|
|
- `login.tsx` also calls `preloadAppData()` after successful login (spinner stays visible during preload)
|
|
- `index.tsx` simply redirects to `/(tabs)/chat` - AuthGuard handles the rest
|
|
- This pattern handles Expo Router's navigation state caching (avoids race conditions)
|
|
- Preloading prevents empty screens when navigating to Calendar or Settings tabs for the first time
|
|
|
|
### Theme System
|
|
|
|
The app supports multiple themes (light/dark) via a reactive Zustand store.
|
|
|
|
**Theme Structure (`Themes.tsx`):**
|
|
```typescript
|
|
export type Theme = {
|
|
chatBot, primeFg, primeBg, secondaryBg, messageBorderBg, placeholderBg,
|
|
calenderBg, confirmButton, rejectButton, disabledButton, buttonText,
|
|
textPrimary, textSecondary, textMuted, eventIndicator, borderPrimary, shadowColor
|
|
};
|
|
|
|
export const THEMES = {
|
|
defaultLight: { ... },
|
|
defaultDark: { ... }
|
|
} as const satisfies Record<string, Theme>;
|
|
```
|
|
|
|
**Usage in Components:**
|
|
```typescript
|
|
import { useThemeStore } from "../stores/ThemeStore";
|
|
|
|
const MyComponent = () => {
|
|
const { theme } = useThemeStore();
|
|
return <View style={{ backgroundColor: theme.primeBg }} />;
|
|
};
|
|
```
|
|
|
|
**Theme Switching:**
|
|
```typescript
|
|
const { setTheme } = useThemeStore();
|
|
setTheme("defaultDark"); // or "defaultLight"
|
|
```
|
|
|
|
**Note:** `shadowColor` only works on iOS. Android uses `elevation` with system-defined shadow colors.
|
|
|
|
### Base Components (CardBase & ModalBase)
|
|
|
|
Reusable base components for cards and modals with consistent styling.
|
|
|
|
**CardBase** - Card structure with header, content, and optional footer:
|
|
```typescript
|
|
<CardBase
|
|
title="Title"
|
|
subtitle="Optional subtitle"
|
|
footer={{ label: "Button", onPress: () => {} }}
|
|
// Styling props (all optional):
|
|
headerPadding={4} // p-{n}, default: px-3 py-2
|
|
contentPadding={4} // p-{n}, default: px-3 py-2
|
|
headerTextSize="text-lg" // "text-sm" | "text-base" | "text-lg" | "text-xl"
|
|
borderWidth={2} // outer border, default: 2
|
|
headerBorderWidth={3} // header bottom border, default: borderWidth
|
|
contentBg={theme.primeBg} // content background color, default: theme.secondaryBg
|
|
scrollable={true} // wrap content in ScrollView
|
|
maxContentHeight={400} // for scrollable content
|
|
>
|
|
{children}
|
|
</CardBase>
|
|
```
|
|
|
|
**ModalBase** - Modal with backdrop using CardBase internally:
|
|
```typescript
|
|
<ModalBase
|
|
visible={isVisible}
|
|
onClose={() => setVisible(false)}
|
|
title="Modal Title"
|
|
subtitle="Optional"
|
|
footer={{ label: "Close", onPress: onClose }}
|
|
scrollable={true}
|
|
maxContentHeight={400}
|
|
>
|
|
{children}
|
|
</ModalBase>
|
|
```
|
|
|
|
ModalBase provides: transparent Modal + backdrop (click-outside-to-close) + Android back button support.
|
|
|
|
**ModalBase Architecture Note:** Uses absolute-positioned backdrop behind the card content (not nested Pressables). This approach:
|
|
- Fixes modal stacking issues on web (React Native Web renders modals as DOM portals)
|
|
- Allows proper scrolling on Android (no touch event conflicts)
|
|
- Card naturally blocks touches from reaching backdrop due to z-order
|
|
|
|
**Component Hierarchy:**
|
|
```
|
|
CardBase
|
|
├── ModalBase (uses CardBase)
|
|
│ ├── DeleteEventModal
|
|
│ └── EventOverlay (in calendar.tsx)
|
|
└── EventCardBase (uses CardBase)
|
|
├── EventCard
|
|
└── ProposedEventCard
|
|
```
|
|
|
|
### Backend Architecture (apps/server)
|
|
|
|
```
|
|
src/
|
|
├── app.ts # Entry point, DI setup, Express config
|
|
├── controllers/ # Request handlers + middleware (per architecture diagram)
|
|
│ ├── AuthController.ts # login(), register()
|
|
│ ├── 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/
|
|
│ ├── index.ts # Re-exports
|
|
│ ├── logger.ts # pino config with redact for sensitive data
|
|
│ └── Logged.ts # @Logged() class decorator for automatic method logging
|
|
├── routes/ # API endpoint definitions
|
|
│ ├── index.ts # Combines all routes under /api
|
|
│ ├── auth.routes.ts # /api/auth/*
|
|
│ ├── chat.routes.ts # /api/chat/* (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
|
|
│ │ └── CaldavRepository.ts
|
|
│ ├── AuthService.ts
|
|
│ ├── ChatService.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
|
|
│ ├── models/ # Mongoose schemas
|
|
│ │ ├── types.ts # Shared types (IdVirtual interface)
|
|
│ │ ├── UserModel.ts
|
|
│ │ ├── EventModel.ts
|
|
│ │ ├── ChatModel.ts
|
|
│ │ └── CaldavConfigModel.ts
|
|
│ ├── MongoUserRepository.ts # findById, findByEmail, findByUserName, create
|
|
│ ├── MongoEventRepository.ts
|
|
│ ├── MongoChatRepository.ts
|
|
│ └── MongoCaldavRepository.ts
|
|
├── ai/
|
|
│ ├── GPTAdapter.ts # Implements AIProvider using OpenAI GPT
|
|
│ ├── index.ts # Re-exports GPTAdapter
|
|
│ └── utils/ # Shared AI utilities (provider-agnostic)
|
|
│ ├── index.ts # Re-exports
|
|
│ ├── eventFormatter.ts # Re-exports formatDate/Time/DateTime from shared
|
|
│ ├── systemPrompt.ts # buildSystemPrompt() - German calendar assistant prompt
|
|
│ ├── toolDefinitions.ts # TOOL_DEFINITIONS - provider-agnostic tool specs
|
|
│ └── toolExecutor.ts # executeToolCall() - handles getDay, proposeCreate/Update/Delete, searchEvents, getEventsInRange
|
|
├── utils/
|
|
│ ├── password.ts # hash(), compare() using bcrypt
|
|
│ ├── eventFormatters.ts # getWeeksOverview(), getMonthOverview() - formatted event listings
|
|
│ └── recurrenceExpander.ts # expandRecurringEvents() - expand recurring events into occurrences
|
|
└── scripts/
|
|
└── hash-password.js # Utility to hash passwords for manual DB updates
|
|
```
|
|
|
|
**API Endpoints:**
|
|
- `POST /api/auth/login` - User login
|
|
- `POST /api/auth/register` - User registration
|
|
- `GET /api/events` - Get all events (protected)
|
|
- `GET /api/events/range` - Get events by date range (protected)
|
|
- `GET /api/events/:id` - Get single event (protected)
|
|
- `POST /api/events` - Create event (protected)
|
|
- `PUT /api/events/:id` - Update event (protected)
|
|
- `DELETE /api/events/:id` - Delete event (protected, query params: mode, occurrenceDate for recurring)
|
|
- `POST /api/chat/message` - Send message to AI (protected)
|
|
- `POST /api/chat/confirm/:conversationId/:messageId` - Confirm proposed event (protected)
|
|
- `POST /api/chat/reject/:conversationId/:messageId` - Reject proposed event (protected)
|
|
- `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)
|
|
|
|
### Shared Package (packages/shared)
|
|
|
|
```
|
|
src/
|
|
├── index.ts
|
|
├── models/
|
|
│ ├── index.ts
|
|
│ ├── User.ts # User, CreateUserDTO, LoginDTO, AuthResponse
|
|
│ ├── CalendarEvent.ts # CalendarEvent, CreateEventDTO, UpdateEventDTO, ExpandedEvent, CaldavSyncStatus
|
|
│ ├── CaldavConfig.ts # CaldavConfig
|
|
│ ├── ChatMessage.ts # ChatMessage, Conversation, SendMessageDTO, CreateMessageDTO,
|
|
│ │ # GetMessagesOptions, ChatResponse, ConversationSummary,
|
|
│ │ # ProposedEventChange, EventAction, RespondedAction, UpdateMessageDTO
|
|
│ └── Constants.ts # DAYS, MONTHS, Day, Month, DAY_INDEX, DAY_INDEX_TO_DAY,
|
|
│ # DAY_TO_GERMAN, DAY_TO_GERMAN_SHORT, MONTH_TO_GERMAN
|
|
└── utils/
|
|
├── index.ts
|
|
├── dateHelpers.ts # getDay() - get date for specific weekday relative to today
|
|
├── formatters.ts # formatDate(), formatTime(), formatDateTime(), formatDateWithWeekday() - German locale
|
|
└── rruleHelpers.ts # parseRRule(), buildRRule(), formatRecurrenceRule() - RRULE parsing, building, and German formatting
|
|
```
|
|
|
|
**Key Types:**
|
|
- `User`: id, email, userName, passwordHash?, createdAt?, updatedAt?
|
|
- `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?
|
|
- Each proposal has unique `id` (e.g., "proposal-0") for individual confirm/reject
|
|
- `respondedAction` tracks user response per proposal (not per message)
|
|
- `deleteMode` ('single' | 'future' | 'all') and `occurrenceDate` for recurring event deletion
|
|
- `conflictingEvents` contains events that overlap with the proposed time (for conflict warnings)
|
|
- `ConflictingEvent`: title, startTime, endTime - simplified event info for conflict display
|
|
- `RecurringDeleteMode`: 'single' | 'future' | 'all' - delete modes for recurring events
|
|
- `DeleteRecurringEventDTO`: mode, occurrenceDate? - DTO for recurring event deletion
|
|
- `Conversation`: id, userId, createdAt?, updatedAt? (messages loaded separately via lazy loading)
|
|
- `CreateUserDTO`: email, userName, password (for registration)
|
|
- `LoginDTO`: identifier (email OR userName), password
|
|
- `CreateEventDTO`: Used for creating events AND for AI-proposed events, includes optional `exceptionDates` for proposals
|
|
- `GetMessagesOptions`: Cursor-based pagination with `before?: string` and `limit?: number`
|
|
- `ConversationSummary`: id, lastMessage?, createdAt? (for conversation list)
|
|
- `UpdateMessageDTO`: proposalId?, respondedAction? (for marking individual proposals as confirmed/rejected)
|
|
- `RespondedAction`: 'confirm' | 'reject' (tracks user response to proposed events)
|
|
- `Day`: "Monday" | "Tuesday" | ... | "Sunday"
|
|
- `Month`: "January" | "February" | ... | "December"
|
|
|
|
### AI Context Architecture
|
|
|
|
The AI assistant fetches calendar data on-demand rather than receiving pre-loaded events. This reduces token usage significantly.
|
|
|
|
**AIContext Interface:**
|
|
```typescript
|
|
interface AIContext {
|
|
userId: string;
|
|
conversationHistory: ChatMessage[]; // Last 20 messages for context
|
|
currentDate: Date;
|
|
// Callbacks for on-demand data fetching:
|
|
fetchEventsInRange: (start: Date, end: Date) => Promise<ExpandedEvent[]>;
|
|
searchEvents: (query: string) => Promise<CalendarEvent[]>;
|
|
fetchEventById: (eventId: string) => Promise<CalendarEvent | null>;
|
|
}
|
|
```
|
|
|
|
**Available AI Tools:**
|
|
- `getDay` - Calculate relative dates (e.g., "next Friday")
|
|
- `getCurrentDateTime` - Get current timestamp
|
|
- `proposeCreateEvent` - Propose new event (includes automatic conflict detection)
|
|
- `proposeUpdateEvent` - Propose event modification
|
|
- `proposeDeleteEvent` - Propose event deletion (supports recurring delete modes)
|
|
- `searchEvents` - Search events by title (returns IDs for update/delete)
|
|
- `getEventsInRange` - Load events for a date range (for "what's today?" queries)
|
|
|
|
**Conflict Detection:**
|
|
When creating events, `toolExecutor` automatically:
|
|
1. Fetches events for the target day via `fetchEventsInRange`
|
|
2. Checks for time overlaps using `occurrenceStart/occurrenceEnd` (important for recurring events)
|
|
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`): Events load instantly from DB on focus (`loadEvents`), CalDAV sync runs in background (`syncAndReload`) and reloads events after. Repeats every 10s via `setInterval`
|
|
- **Sync button** (`settings.tsx`): Manual trigger in CaldavSettings
|
|
|
|
**Lazy sync (server-side in ChatService):**
|
|
- AI data access callbacks (`fetchEventsInRange`, `searchEvents`, `fetchEventById`) trigger `syncOnce()` before the first DB query
|
|
- Uses `CaldavService.sync()` which checks config internally (silent no-op without config)
|
|
|
|
**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 on `EventService` and `CaldavService` (lazy CalDAV sync on AI data access)
|
|
- `EventController` and `ChatController` both receive `CaldavService` for CalDAV push on mutations
|
|
|
|
### Database Abstraction
|
|
|
|
The repository pattern allows swapping databases:
|
|
- **Interfaces** (`services/interfaces/`) are DB-agnostic
|
|
- **Implementations** (`repositories/mongo/`) are DB-specific
|
|
- To add MySQL: create `repositories/mysql/` with TypeORM entities
|
|
|
|
### Mongoose Model Pattern
|
|
|
|
All Mongoose models use a consistent pattern for TypeScript-safe `id` virtuals:
|
|
|
|
```typescript
|
|
import { IdVirtual } from './types';
|
|
|
|
const Schema = new Schema<Doc, Model<Doc, {}, {}, IdVirtual>, {}, {}, IdVirtual>(
|
|
{ /* fields */ },
|
|
{
|
|
virtuals: {
|
|
id: {
|
|
get() { return this._id.toString(); }
|
|
}
|
|
},
|
|
toJSON: {
|
|
virtuals: true,
|
|
transform: (_, ret) => {
|
|
delete ret._id;
|
|
delete ret.__v;
|
|
return ret;
|
|
}
|
|
}
|
|
}
|
|
);
|
|
```
|
|
|
|
Repositories use `doc.toJSON() as unknown as Type` casting (required because Mongoose's TypeScript types don't reflect virtual fields in toJSON output).
|
|
|
|
### Logging
|
|
|
|
Structured logging with pino (server) and react-native-logs (client).
|
|
|
|
**Server Logging:**
|
|
- `pino` with `pino-pretty` for development, JSON in production
|
|
- `pino-http` middleware logs all HTTP requests (method, path, status, duration)
|
|
- `@Logged()` class decorator for automatic method logging on repositories and services
|
|
- Sensitive data (password, token, etc.) automatically redacted via pino's `redact` config
|
|
|
|
**@Logged Decorator Pattern:**
|
|
```typescript
|
|
@Logged("MongoEventRepository")
|
|
export class MongoEventRepository implements EventRepository { ... }
|
|
|
|
@Logged("GPTAdapter")
|
|
export class GPTAdapter implements AIProvider { ... }
|
|
```
|
|
|
|
The decorator uses a Proxy to intercept method calls lazily, preserves sync/async nature, and logs start/completion/failure with duration.
|
|
|
|
**Log Summarization:**
|
|
The `@Logged` decorator automatically summarizes large arguments to keep logs readable:
|
|
- `conversationHistory` → `"[5 messages]"`
|
|
- `proposedChanges` → logged in full (for debugging AI issues)
|
|
- Long strings (>100 chars) → truncated
|
|
- Arrays → `"[Array(n)]"`
|
|
|
|
**Client Logging:**
|
|
- `react-native-logs` with namespaced loggers (apiLogger, storeLogger)
|
|
- ApiClient logs all requests with method, endpoint, status, duration
|
|
- Log level: debug in __DEV__, warn in production
|
|
|
|
## MVP Feature Scope
|
|
|
|
### Must-Have
|
|
- Chat interface with AI assistant (text input) for event management
|
|
- Calendar overview
|
|
- Manual event CRUD (without AI)
|
|
- View completed events
|
|
- Simple reminders
|
|
- One note per event
|
|
- Recurring events
|
|
|
|
### Nice-to-Have
|
|
- iCalendar import/export
|
|
- Multiple calendars
|
|
- ~~CalDAV synchronization with external services~~ (implemented)
|
|
|
|
## Development Environment
|
|
|
|
### MongoDB (Docker)
|
|
```bash
|
|
cd apps/server/docker/mongo
|
|
docker compose up -d # Start MongoDB + Mongo Express
|
|
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/`:
|
|
```
|
|
MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin
|
|
OPENAI_API_KEY=sk-proj-...
|
|
USE_TEST_RESPONSES=false # true = static test responses, false = real GPT AI
|
|
LOG_LEVEL=debug # debug | info | warn | error | fatal
|
|
NODE_ENV=development # development = pretty logs, production = JSON
|
|
```
|
|
|
|
## Current Implementation Status
|
|
|
|
**Backend:**
|
|
- **Implemented:**
|
|
- `AuthController`: login(), register() with error handling
|
|
- `AuthService`: login() supports email OR userName, register() checks for existing email AND userName
|
|
- `AuthMiddleware`: Validates X-User-Id header for protected routes
|
|
- `MongoUserRepository`: findById(), findByEmail(), findByUserName(), create()
|
|
- `utils/password`: hash(), compare() using bcrypt
|
|
- `scripts/hash-password.js`: Utility for manual password resets
|
|
- `dotenv` integration for environment variables
|
|
- `ChatController`: sendMessage(), confirmEvent(), rejectEvent()
|
|
- `ChatService`: processMessage() with test responses (create, update, delete actions), confirmEvent() handles all CRUD actions
|
|
- `MongoEventRepository`: Full CRUD implemented (findById, findByUserId, findByDateRange, create, update, delete, addExceptionDate)
|
|
- `EventController`: Full CRUD (create, getById, getAll, getByDateRange, update, delete)
|
|
- `EventService`: Full CRUD with recurring event expansion via recurrenceExpander, deleteRecurring() with three modes (single/future/all)
|
|
- `utils/eventFormatters`: getWeeksOverview(), getMonthOverview() with German localization
|
|
- `utils/recurrenceExpander`: expandRecurringEvents() using rrule library for RRULE parsing
|
|
- `ChatController`: getConversations(), getConversation() with cursor-based pagination support
|
|
- `ChatService`: getConversations(), getConversation(), processMessage() uses real AI or test responses (via USE_TEST_RESPONSES), confirmEvent()/rejectEvent() update respondedAction and persist response messages
|
|
- `MongoChatRepository`: Full CRUD implemented (getConversationsByUser, createConversation, getMessages with cursor pagination, createMessage, updateMessage, updateProposalResponse, updateProposalEvent)
|
|
- `ChatRepository` interface: updateMessage(), updateProposalResponse(), updateProposalEvent() for per-proposal tracking
|
|
- `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 `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, sync, getConfig, saveConfig, deleteConfig). `sync()` checks config internally and is a silent no-op without config.
|
|
- `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`: Uses EventService + CaldavService (lazy sync on AI data access via syncOnce pattern)
|
|
- `EventService`: Extended with searchByTitle(), findByCaldavUUID()
|
|
- `utils/eventFormatters`: Refactored to use EventService instead of EventRepository
|
|
- CORS configured to allow X-User-Id header
|
|
|
|
**Shared:**
|
|
- 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. `buildRRule()` builds RRULE from RepeatType + interval. `formatRecurrenceRule()` formats RRULE into German description (e.g., "Jede Woche", "Alle 2 Monate"). Exports `REPEAT_TYPE_LABELS` and `RepeatType`.
|
|
- `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:**
|
|
- **Authentication fully implemented:**
|
|
- `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, preloads app data (events + CalDAV config) into stores before dismissing spinner, triggers CalDAV sync, shows loading, redirects if unauthenticated. Exports `preloadAppData()` (also called by `login.tsx`)
|
|
- Login screen: Supports email OR userName login, uses CustomTextInput with focus border, preloads app data + triggers CalDAV sync after successful login
|
|
- Register screen: Email validation, checks for existing email/userName, uses CustomTextInput with focus border
|
|
- `AuthButton`: Reusable button component with themed shadow
|
|
- `Header`: Themed header component (logout moved to Settings)
|
|
- `(tabs)/_layout.tsx`: Wraps tabs with AuthGuard for protected access
|
|
- `index.tsx`: Simple redirect to chat (AuthGuard handles auth)
|
|
- **Theme system fully implemented:**
|
|
- `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) and CalDAV configuration (url, username, password with save/sync buttons, loads existing config on mount). Save/Sync buttons show independent feedback via `FeedbackRow` component: spinner + loading text during request, then success (green) or error (red) message that auto-clears after 3s. Both feedbacks can be visible simultaneously.
|
|
- `BaseButton`: Reusable themed button component
|
|
- Tab navigation (Chat, Calendar, Settings) implemented with themed UI
|
|
- Calendar screen fully functional:
|
|
- Month navigation with grid display and Ionicons (chevron-back/forward)
|
|
- MonthSelector dropdown with infinite scroll (dynamically loads months, lazy-loaded when modal opens, cleared on close for memory efficiency)
|
|
- Events loaded from API via EventService.getByDateRange()
|
|
- 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
|
|
- Events load instantly from local DB on tab focus, CalDAV sync runs non-blocking in background (`syncAndReload`) with 10s interval
|
|
- 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
|
|
- **Multiple event proposals**: AI can propose multiple events in one response
|
|
- Arrow navigation between proposals with "Event X von Y" counter
|
|
- Each proposal individually confirmable/rejectable
|
|
- **Typing indicator**: Animated dots (. .. ...) shown after 500ms delay while waiting for AI response
|
|
- Messages persisted to database via ChatService, loaded via `useFocusEffect` when screen gains focus
|
|
- Tracks conversationId for message continuity across sessions
|
|
- ChatStore with addMessages() for bulk loading, chatMessageToMessageData() helper
|
|
- KeyboardAvoidingView for proper keyboard handling (iOS padding, Android height)
|
|
- Auto-scroll to end on new messages and keyboard show; initial load uses `onContentSizeChange` with `animated: false` to start at bottom without visible scrolling
|
|
- 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 with focus border highlight. Props: `text`, `onValueChange`, `placeholder`, `placeholderTextColor`, `secureTextEntry`, `autoCapitalize`, `keyboardType`, `className`, `multiline`. No default padding — callers must set padding via `className` (e.g., `px-3 py-2` or `p-4`). When not focused, cursor is reset to start (`selection={{ start: 0 }}`) to avoid text appearing scrolled to the end.
|
|
- `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. Accepts `recurrenceRule` string (not boolean) and displays German-formatted recurrence via `formatRecurrenceRule()`
|
|
- `EventCard`: Uses EventCardBase + edit/delete buttons (TouchableOpacity with delayPressIn for scroll-friendly touch handling)
|
|
- `ProposedEventCard`: Uses EventCardBase + confirm/reject/edit buttons for chat proposals, displays green highlighted text for new changes ("Neue Ausnahme: [date]" for single delete, "Neues Ende: [date]" for UNTIL updates), shows yellow conflict warnings when proposed time overlaps with existing events. Edit button allows modifying proposals before confirming.
|
|
- `DeleteEventModal`: Delete confirmation modal using ModalBase - shows three options for recurring events (single/future/all), simple confirm for non-recurring
|
|
- `CalendarToolbar` (in calendar.tsx): Toolbar between header and weekdays with Sync button (CalDAV sync with spinner/green checkmark/red X feedback, disabled without config) and Logout button
|
|
- `EventOverlay` (in calendar.tsx): Day events overlay using ModalBase - shows EventCards for selected day
|
|
- `Themes.tsx`: Theme definitions with THEMES object (defaultLight, defaultDark) including all color tokens (textPrimary, borderPrimary, eventIndicator, secondaryBg, shadowColor, etc.)
|
|
- `EventsStore`: Zustand store with setEvents(), addEvent(), updateEvent(), deleteEvent() - stores ExpandedEvent[], preloaded by AuthGuard
|
|
- `CaldavConfigStore`: Zustand store with config (CaldavConfig | null), setConfig() - cached CalDAV config, preloaded by AuthGuard, used by Settings to avoid API call on mount
|
|
- `ChatStore`: Zustand store with addMessage(), addMessages(), updateMessage(), clearMessages(), isWaitingForResponse/setWaitingForResponse() for typing indicator - loads from server on mount and persists across tab switches
|
|
- `ThemeStore`: Zustand store with theme/setTheme() for reactive theme switching across all components
|
|
- `ChatBubble`: Reusable chat bubble component with Tailwind styling, used by ChatMessage and TypingIndicator
|
|
- `TypingIndicator`: Animated typing indicator component showing `. → .. → ...` loop while waiting for AI response
|
|
- Event Detail and Note screens exist as skeletons
|
|
- `editEvent.tsx`: Dual-mode event editor screen
|
|
- **Calendar mode**: Edit existing events, create new events - calls EventService API
|
|
- **Chat mode**: Edit AI-proposed events before confirming - updates ChatStore locally and persists to server via ChatService.updateProposalEvent()
|
|
- Route params: `mode` ('calendar' | 'chat'), `id?`, `date?`, `eventData?` (JSON), `proposalContext?` (JSON with messageId, proposalId, conversationId)
|
|
- Supports recurring events with RRULE configuration (daily/weekly/monthly/yearly)
|
|
|
|
## Building
|
|
|
|
### Local APK Build with EAS
|
|
```bash
|
|
npm run build:apk -w @calchat/client
|
|
```
|
|
|
|
This uses the `preview` profile from `eas.json` which builds an APK with:
|
|
- `arm64-v8a` architecture only (smaller APK size)
|
|
- No credentials required (`withoutCredentials: true`)
|
|
- Internal distribution
|
|
|
|
**Requirements:** Android SDK and Java must be installed locally.
|
|
|
|
**EAS Configuration:** `apps/client/eas.json` contains build profiles:
|
|
- `development`: Development client with internal distribution
|
|
- `preview`: APK build for testing (used by `build:apk`)
|
|
- `production`: Production build with auto-increment versioning
|
|
|
|
**App Identity:**
|
|
- Package name: `com.gilmour109.calchat`
|
|
- EAS Project ID: `b722dde6-7d89-48ff-9095-e007e7c7da87`
|
|
|
|
## Documentation
|
|
|
|
Detailed architecture diagrams are in `docs/`:
|
|
- `api-routes.md` - API endpoint overview (German)
|
|
- `technisches_brainstorm.tex` - Technical concept document (German)
|
|
- `architecture-class-diagram.puml` - Backend class diagram
|
|
- `frontend-class-diagram.puml` - Frontend class diagram
|
|
- `component-diagram.puml` - System component overview
|