Files
calchat/CLAUDE.md
Linus Waldowsky 868e1ba68d perf: preload events and CalDAV config to avoid empty screens
Add CaldavConfigStore and preloadAppData() to load events (current month)
and CalDAV config into stores before dismissing the auth loading spinner.
This prevents the brief empty flash when first navigating to Calendar or
Settings tabs. Also applies Prettier formatting across codebase.
2026-02-09 18:59:03 +01:00

695 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 (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
### 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
│ │ └── settings.tsx # Settings screen (theme switcher, logout)
│ ├── 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 CaldavSettings)
│ ├── 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(), refresh(), logout()
│ ├── 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/
│ ├── jwt.ts # signToken(), verifyToken() - NOT USED YET (no JWT)
│ ├── 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
- `POST /api/auth/refresh` - Refresh JWT token
- `POST /api/auth/logout` - User logout
- `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/`:
```
JWT_SECRET=your-secret-key
JWT_EXPIRES_IN=1h
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
- **Stubbed (TODO):**
- `AuthController`: refresh(), logout()
- `AuthService`: refreshToken()
- JWT authentication (currently using simple 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, preloads app data + 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)
- `(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)
- `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
- 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. 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
- `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