# 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 ``` ### 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 | | Deployment | Docker | Server containerization (multi-stage build) | | | Drone CI | CI/CD pipelines (build, test, format check) | | 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; ``` **Usage in Components:** ```typescript import { useThemeStore } from "../stores/ThemeStore"; const MyComponent = () => { const { theme } = useThemeStore(); return ; }; ``` **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 {} }} // 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} ``` **ModalBase** - Modal with backdrop using CardBase internally: ```typescript setVisible(false)} title="Modal Title" subtitle="Optional" footer={{ label: "Close", onPress: onClose }} scrollable={true} maxContentHeight={400} > {children} ``` 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; searchEvents: (query: string) => Promise; fetchEventById: (eventId: string) => Promise; } ``` **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, {}, {}, 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 (clones from Gitea, no local context needed): docker build -f apps/server/docker/Dockerfile -t calchat-server . # Build from a specific branch: docker build -f apps/server/docker/Dockerfile --build-arg BRANCH=feature-x -t calchat-server . docker run -p 3000:3000 --env-file apps/server/.env calchat-server ``` Multi-stage build: clones the repo from Gitea (`git clone --depth 1`) in the build stage using a `BRANCH` build arg (default: `main`), compiles shared + server, then copies only `dist/` and production dependencies to the runtime stage. No local source context required — the image can be built on any machine with Docker. ### 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` ## CI/CD (Drone) The project uses Drone CI (`.drone.yml`) with two pipelines: 1. **`server_build_and_test`**: Builds the server (`npm ci` + `npm run build`) and runs Jest tests (`npm run test`) 2. **`check_for_formatting`**: Checks Prettier formatting across all workspaces (`npm run check_format`) ## Testing Server uses 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) ## 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