Files
calchat/CLAUDE.md
Linus Waldowsky 27602aee4c Add E2E testing infrastructure with WebdriverIO + Appium
Set up E2E test framework for Android using WebdriverIO, Appium, and
UiAutomator2. Add testID props to key components (AuthButton, BaseButton,
ChatBubble, CustomTextInput, ProposedEventCard) and apply testIDs to
login screen, chat screen, tab bar, and settings. Include initial tests
for app launch detection and login flow validation. Update CLAUDE.md
with E2E docs.
2026-02-26 21:37:40 +01:00

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

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

Shared (packages/shared)

npm run build -w @calchat/shared        # Compile shared types to dist/

Server (apps/server) - Express.js backend

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, APK build + Gitea release)
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
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):

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()
│   ├── 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:

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

Server Docker Image

# 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

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) with five pipelines:

On push to main:

  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)
  3. deploy_latest: Builds Docker image, pushes to Gitea Container Registry (gitea.gilmour109.de/gilmour109/calchat-server:latest), then SSHs into VPS (10.0.0.1) to pull and restart via docker compose. Builds APK via eas-build Docker image and creates a Gitea release (title "latest") with the APK. Depends on both pipelines above passing first.

On tag: 4. upload_tag: Builds Docker image tagged with the git tag (${DRONE_TAG}), pushes to registry, then deploys to k3s cluster (192.168.178.201) via SSH using envsubst with a Kubernetes manifest template. Builds APK and creates a Gitea release tagged with ${DRONE_TAG}.

On promote: 5. upload_commit: Builds Docker image tagged with short commit SHA (first 8 chars), pushes to registry, then deploys to k3s cluster (192.168.178.201) via SSH using envsubst with a Kubernetes manifest template. Builds APK and creates a Gitea release tagged with the short commit SHA.

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:

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