Files
calchat/CLAUDE.md
Linus Waldowsky 3ad4a77951 fix: chat starts scrolled to bottom instead of visibly scrolling down
- Use onContentSizeChange to scroll after FlashList renders content
- Scroll without animation on initial load via needsInitialScroll ref
- Remove unreliable 100ms timeout scrollToEnd from message loading
2026-02-09 19:23:45 +01:00

39 KiB

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)

npm install          # Install all dependencies for all workspaces
npm run format       # Format all TypeScript files with Prettier

Client (apps/client) - Expo React Native app

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

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

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:

import { useThemeStore } from "../stores/ThemeStore";

const MyComponent = () => {
  const { theme } = useThemeStore();
  return <View style={{ backgroundColor: theme.primeBg }} />;
};

Theme Switching:

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:

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

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

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:

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:

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

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)

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, 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)
    • 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)
  • 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

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