Files
calchat/CLAUDE.md
Linus Waldowsky 43d40b46d7 feat: add theme system with light/dark mode support
- Add ThemeStore (Zustand) for reactive theme switching
- Add Themes.tsx with THEMES object (defaultLight, defaultDark)
- Add Settings screen with theme switcher and logout button
- Add BaseButton component for reusable themed buttons
- Migrate all components from static currentTheme to useThemeStore()
- Add shadowColor to theme (iOS only, Android uses elevation)
- All text elements now use theme colors (textPrimary, textSecondary, etc.)
- Update tab navigation to include Settings tab
- Move logout from Header to Settings screen
2026-01-24 16:57:33 +01:00

24 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
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)
│   ├── event/
│   │   └── [id].tsx            # Event detail screen (dynamic route)
│   └── note/
│       └── [id].tsx            # Note editor for event (dynamic route)
├── components/
│   ├── 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)
│   ├── ChatBubble.tsx          # Reusable chat bubble component (used by ChatMessage & TypingIndicator)
│   ├── TypingIndicator.tsx     # Animated typing indicator (. .. ...) shown while waiting for AI response
│   ├── EventCardBase.tsx       # Shared event card layout with icons (used by EventCard & ProposedEventCard)
│   ├── 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 buttons)
├── 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
│   ├── AuthService.ts          # login(), register(), logout() - calls API and updates AuthStore
│   ├── EventService.ts         # getAll(), getById(), getByDateRange(), create(), update(), delete()
│   └── ChatService.ts          # sendMessage(), confirmEvent(), rejectEvent(), getConversations(), getConversation()
└── 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()
    └── ThemeStore.ts           # theme, setTheme() - reactive theme switching with Zustand

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.

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.

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(), rejectEvent(), getConversations(), getConversation()
│   ├── EventController.ts    # create(), getById(), getAll(), getByDateRange(), update(), delete()
│   ├── 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)
├── services/                 # Business logic
│   ├── interfaces/           # DB-agnostic interfaces (for dependency injection)
│   │   ├── AIProvider.ts     # processMessage()
│   │   ├── UserRepository.ts # findById, findByEmail, findByUserName, create + CreateUserData
│   │   ├── EventRepository.ts
│   │   └── ChatRepository.ts
│   ├── AuthService.ts
│   ├── ChatService.ts
│   └── EventService.ts
├── 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
│       ├── MongoUserRepository.ts  # findById, findByEmail, findByUserName, create
│       ├── MongoEventRepository.ts
│       └── MongoChatRepository.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 # formatExistingEvents() for system prompt
│       ├── systemPrompt.ts   # buildSystemPrompt() - German calendar assistant prompt
│       ├── toolDefinitions.ts # TOOL_DEFINITIONS - provider-agnostic tool specs
│       └── toolExecutor.ts   # executeToolCall() - handles getDay, proposeCreate/Update/Delete, searchEvents
├── 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)
  • 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)
  • 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
│   ├── 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

Key Types:

  • User: id, email, userName, passwordHash?, createdAt?, updatedAt?
  • CalendarEvent: id, userId, title, description?, startTime, endTime, note?, isRecurring?, recurrenceRule?
  • 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?
    • Each proposal has unique id (e.g., "proposal-0") for individual confirm/reject
    • respondedAction tracks user response per proposal (not per message)
  • 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
  • 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"

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]"
  • existingEvents"[3 events]"
  • 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

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)

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)
    • EventController: Full CRUD (create, getById, getAll, getByDateRange, update, delete)
    • EventService: Full CRUD with recurring event expansion via recurrenceExpander
    • 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)
    • ChatRepository interface: updateMessage() and updateProposalResponse() for per-proposal respondedAction 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, eventFormatter)
    • ai/utils/systemPrompt: Includes RRULE documentation - AI knows to create separate events when times differ by day
    • utils/recurrenceExpander: Handles RRULE parsing, strips RRULE: prefix if present (AI may include it)
    • logging/: Structured logging with pino, pino-http middleware, @Logged decorator
    • All repositories and GPTAdapter decorated with @Logged for automatic method logging
    • 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, and date utilities defined and exported.

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
    • Login screen: Supports email OR userName login
    • Register screen: Email validation, checks for existing email/userName
    • AuthButton: Reusable button component with themed shadow
    • Header: Themed header component (logout moved to Settings)
    • index.tsx: Auth redirect - checks stored user on app start
  • 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)
    • 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
    • Uses useFocusEffect for automatic reload on tab focus
  • 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 and loaded on mount
    • Tracks conversationId for message continuity across sessions
    • ChatStore with addMessages() for bulk loading, chatMessageToMessageData() helper
    • KeyboardAvoidingView for proper keyboard handling (iOS padding, Android height)
    • Auto-scroll to end on new messages and keyboard show
    • keyboardDismissMode="interactive" and keyboardShouldPersistTaps="handled"
  • EventService: getAll(), getById(), getByDateRange(), create(), update(), delete() - fully implemented
  • ChatService: sendMessage(), confirmEvent(), rejectEvent(), getConversations(), getConversation() - fully implemented with cursor pagination
  • EventCardBase: Shared base component with event layout (header, date/time/recurring icons, description) - used by both EventCard and ProposedEventCard
  • EventCard: Uses EventCardBase + edit/delete buttons for calendar display
  • ProposedEventCard: Uses EventCardBase + confirm/reject buttons for chat proposals (supports create/update/delete actions)
  • 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[]
  • 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

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