# 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
npm run check_format # Check formatting without modifying files (used in CI)
```

### 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
npm run test:e2e -w @calchat/client     # Run E2E tests (requires Appium server running)
```

### Shared (packages/shared)
```bash
npm run build -w @calchat/shared        # Compile shared types to dist/
```

### Server (apps/server) - Express.js backend
```bash
npm run dev -w @calchat/server          # Build shared + start dev server with hot reload (tsx watch)
npm run build -w @calchat/server        # Build shared + compile TypeScript
npm run start -w @calchat/server        # Run compiled server (port 3000)
npm run test -w @calchat/server         # Run Jest unit tests
```

## 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 |
| Testing | Jest / ts-jest | Server unit tests |
| | WebdriverIO + Appium | E2E tests (Android) |
| | UiAutomator2 | Android UI automation driver |
| Deployment | Docker | Server containerization (multi-stage build) |
| | Drone CI | CI/CD pipelines (build, test, format check, deploy, E2E) |
| | OpenTofu | Infrastructure as Code for ephemeral E2E VMs (Proxmox) |
| | Kubernetes (k3s) | Test backend deployments for E2E |
| 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
scripts/         - CI/E2E helper scripts
tofu/e2e/        - OpenTofu config for ephemeral E2E VMs (Proxmox)
kubernetes/      - k3s manifest templates for test deployments
```

### 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
e2e/                            # E2E tests (WebdriverIO + Appium)
├── jest.config.ts              # Jest config (ts-jest, 120s timeout)
├── tsconfig.json               # TypeScript config (ES2020, CommonJS)
├── .env                        # Test credentials, device name, Appium host/port
├── config/
│   └── capabilities.ts         # Appium capabilities (Dev Client vs APK mode)
├── helpers/
│   ├── driver.ts               # WebdriverIO driver singleton (init, get, quit)
│   ├── selectors.ts            # testID constants for all screens
│   └── utils.ts                # Helpers (waitForTestId, performLogin, ensureLoggedIn, ensureOnLoginScreen)
└── tests/
    ├── 01-app-launch.test.ts   # App startup & screen detection
    └── 02-login.test.ts        # Login flow (empty fields, invalid creds, success)
```

**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)

The shared package is compiled to `dist/` (CommonJS). All imports must use `@calchat/shared` (NOT `@calchat/shared/src/...`). Server `dev` and `build` scripts automatically build shared first.

```
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`

### Server Docker Image
```bash
# Build (requires local build context):
docker build -f apps/server/docker/Dockerfile -t calchat-server .
docker run -p 3001:3001 --env-file apps/server/.env calchat-server
```
Multi-stage COPY-based build: copies `package.json` files first for layer caching, then source code. Compiles shared + server, then copies only `dist/` and production dependencies to the runtime stage. Exposes port 3001. In CI, the `plugins/docker` Drone plugin builds and pushes the image automatically.

### 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)
- **E2E testing infrastructure:**
  - WebdriverIO + Appium with UiAutomator2 driver for Android
  - testID props added to key components (`AuthButton`, `BaseButton`, `ChatBubble`, `CustomTextInput`, `ProposedEventCard`)
  - testIDs applied to login screen, chat screen, tab bar, settings logout button, and event proposal buttons
  - `app.json`: `usesCleartextTraffic: true` for Android (allows HTTP connections needed by Appium)
  - Two execution modes: Dev Client (local) and APK (CI)
  - Tests: app launch detection, login flow validation (empty fields, invalid creds, success)

## 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
- Non-interactive mode with fixed output path (`calchat.apk`) for CI compatibility

**Requirements:** Android SDK and Java must be installed locally. In CI, the `eas-build` Docker image (`gitea.gilmour109.de/gilmour109/eas-build:latest`) provides the build environment with `EXPO_TOKEN` for authentication.

**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`

## CI/CD (Drone)

The project uses Drone CI (`.drone.yml`). Note: `server_build_and_test` and `check_for_formatting` pipelines are currently commented out.

**On push to main:**
1. **`deploy_latest`**: Runs E2E testing pipeline with ephemeral infrastructure:
   - **`deploy_test_backend`**: Deploys server to k3s cluster (`192.168.178.201`) using `kubernetes/manifest.yml` with commit-based naming (`e2e<sha8>`)
   - **`create_e2e_vm`**: Provisions ephemeral Android emulator VM on Proxmox via OpenTofu (`tofu/e2e/`)
   - **`run_e2e_tests`**: SSHs into VM, runs `scripts/e2e-test.sh` (clones repo, starts emulator + Expo + Appium, executes E2E tests). VM IP derived from build number: `192.168.178.$((211 + BUILD_NUMBER % 44))`
   - **`notify_failure`**: Sends email notification on E2E failure
   - **`destroy_e2e_vm`**: Tears down VM via `tofu destroy` (runs on success and failure)
   - **`cleanup_k3s`**: Deletes test backend resources from k3s (runs on success and failure)
   - Docker image build/push and APK build/upload are currently commented out
   - Uses `e2e-tools` Docker image (`gitea.gilmour109.de/gilmour109/e2e-tools:latest`)

**On tag** (`upload_tag`) and **on promote** (`upload_commit`): Currently commented out. Previously deployed to k3s and built APK releases.

### E2E CI Infrastructure

```
scripts/
└── e2e-test.sh           # E2E test runner script (emulator + Expo + Appium)
tofu/e2e/                  # OpenTofu config for ephemeral Proxmox VMs
kubernetes/manifest.yml    # k3s manifest template for test backend (uses envsubst: $NAME, $TAG, $COMMIT)
```

**`scripts/e2e-test.sh`**: Orchestrates E2E test execution inside an ephemeral VM. Supports two modes:
- **CI mode**: Clones repo, installs deps, starts Android emulator, Expo, Appium, runs tests
- **Local mode** (`--local`): Uses existing repo checkout, optional `--api-url` override

## Testing

### Server Unit Tests

Jest with ts-jest for unit testing. Config in `apps/server/jest.config.js` ignores `/node_modules/` and `/dist/`.

**Existing tests:**
- `src/utils/password.test.ts` - Tests for bcrypt hash() and compare()
- `src/utils/recurrenceExpander.test.ts` - Tests for expandRecurringEvents() (non-recurring, weekly/daily/UNTIL recurrence, EXDATE filtering, RRULE: prefix stripping, invalid RRULE fallback, multi-day events, sorting)

### E2E Tests (Client)

WebdriverIO + Appium for Android E2E testing. Tests run sequentially (`--runInBand`) sharing a singleton Appium driver.

**Two execution modes:**
- **Dev Client mode** (local): Connects to running Expo app (`host.exp.exponent`), `noReset: true`
- **APK mode** (CI): Installs APK via `APK_PATH` env var, `noReset: false`

**Running locally:**
```bash
# Terminal 1: Start Appium server
appium
# Terminal 2: Start Expo dev server on Android emulator
npm run android -w @calchat/client
# Terminal 3: Run E2E tests
npm run test:e2e -w @calchat/client
```

**Environment variables** (`apps/client/e2e/.env`):
```
TEST_USER=test              # Login credentials for tests
TEST_PASSWORD=test
DEVICE_NAME=emulator-5554   # Android device/emulator
APPIUM_HOST=localhost
APPIUM_PORT=4723
```

**Element selection:** Uses Android UiAutomator2 with `resource-id` selectors (React Native maps `testID` → `resource-id` on Android).

**testID conventions:** Components with testID support: `AuthButton`, `BaseButton`, `ChatBubble`, `CustomTextInput`, `ProposedEventCard`. Key testIDs: `login-title`, `login-identifier-input`, `login-password-input`, `login-button`, `login-error-text`, `tab-chat`, `tab-calendar`, `tab-settings`, `chat-message-input`, `chat-send-button`, `chat-bubble-left`, `chat-bubble-right`, `settings-logout-button`, `event-accept-button`, `event-reject-button`.

**Existing E2E tests:**
- `01-app-launch.test.ts` - App startup, detects login or auto-logged-in state
- `02-login.test.ts` - Empty field validation, invalid credentials error, successful login

## 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
