- 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
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 loginPOST /api/auth/register- User registrationPOST /api/auth/refresh- Refresh JWT tokenPOST /api/auth/logout- User logoutGET /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 checkPOST /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 respondedActiontracks user response per proposal (not per message)
- Each proposal has unique
Conversation: id, userId, createdAt?, updatedAt? (messages loaded separately via lazy loading)CreateUserDTO: email, userName, password (for registration)LoginDTO: identifier (email OR userName), passwordCreateEventDTO: Used for creating events AND for AI-proposed eventsGetMessagesOptions: Cursor-based pagination withbefore?: stringandlimit?: numberConversationSummary: 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:
pinowithpino-prettyfor development, JSON in productionpino-httpmiddleware 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
redactconfig
@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-logswith 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 handlingAuthService: login() supports email OR userName, register() checks for existing email AND userNameAuthMiddleware: Validates X-User-Id header for protected routesMongoUserRepository: findById(), findByEmail(), findByUserName(), create()utils/password: hash(), compare() using bcryptscripts/hash-password.js: Utility for manual password resetsdotenvintegration for environment variablesChatController: sendMessage(), confirmEvent(), rejectEvent()ChatService: processMessage() with test responses (create, update, delete actions), confirmEvent() handles all CRUD actionsMongoEventRepository: 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 recurrenceExpanderutils/eventFormatters: getWeeksOverview(), getMonthOverview() with German localizationutils/recurrenceExpander: expandRecurringEvents() using rrule library for RRULE parsingChatController: getConversations(), getConversation() with cursor-based pagination supportChatService: getConversations(), getConversation(), processMessage() uses real AI or test responses (via USE_TEST_RESPONSES), confirmEvent()/rejectEvent() update respondedAction and persist response messagesMongoChatRepository: Full CRUD implemented (getConversationsByUser, createConversation, getMessages with cursor pagination, createMessage, updateMessage, updateProposalResponse)ChatRepositoryinterface: updateMessage() and updateProposalResponse() for per-proposal respondedAction trackingGPTAdapter: Full implementation with OpenAI GPT (gpt-4o-mini model), function calling for calendar operations, collects multiple proposals per responseai/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 dayutils/recurrenceExpander: Handles RRULE parsing, stripsRRULE: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 APIApiClient: 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 shadowHeader: 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
useFocusEffectfor 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 implementedChatService: sendMessage(), confirmEvent(), rejectEvent(), getConversations(), getConversation() - fully implemented with cursor paginationEventCardBase: Shared base component with event layout (header, date/time/recurring icons, description) - used by both EventCard and ProposedEventCardEventCard: Uses EventCardBase + edit/delete buttons for calendar displayProposedEventCard: 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 switchesThemeStore: Zustand store with theme/setTheme() for reactive theme switching across all componentsChatBubble: Reusable chat bubble component with Tailwind styling, used by ChatMessage and TypingIndicatorTypingIndicator: 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-v8aarchitecture 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 distributionpreview: APK build for testing (used bybuild: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 diagramfrontend-class-diagram.puml- Frontend class diagramcomponent-diagram.puml- System component overview