- Show spinner + loading text while request is in progress - Display success (green) or error (red) message, auto-clears after 3s - Save and Sync have independent feedback rows (both visible at once) - Fix CaldavTextInput theming and add secureTextEntry for password - Reset CustomTextInput cursor to start when unfocused
40 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, CalDAV config with feedback)
│ ├── editEvent.tsx # Event edit screen (dual-mode: calendar/chat)
│ ├── event/
│ │ └── [id].tsx # Event detail screen (dynamic route)
│ └── note/
│ └── [id].tsx # Note editor for event (dynamic route)
├── components/
│ ├── AuthGuard.tsx # Auth wrapper: loads user, preloads app data (events + CalDAV config), CalDAV sync, shows loading, redirects if unauthenticated. Exports preloadAppData()
│ ├── BaseBackground.tsx # Common screen wrapper (themed)
│ ├── BaseButton.tsx # Reusable button component (themed, supports children)
│ ├── Header.tsx # Header component (themed)
│ ├── AuthButton.tsx # Reusable button for auth screens (themed, with shadow)
│ ├── CardBase.tsx # Reusable card component (header + content + optional footer)
│ ├── ModalBase.tsx # Reusable modal with backdrop (uses CardBase, click-outside-to-close)
│ ├── ChatBubble.tsx # Reusable chat bubble component (used by ChatMessage & TypingIndicator)
│ ├── TypingIndicator.tsx # Animated typing indicator (. .. ...) shown while waiting for AI response
│ ├── EventCardBase.tsx # Event card layout with icons (uses CardBase)
│ ├── EventCard.tsx # Calendar event card (uses EventCardBase + edit/delete buttons)
│ ├── EventConfirmDialog.tsx # AI-proposed event confirmation modal (skeleton)
│ ├── ProposedEventCard.tsx # Chat event proposal (uses EventCardBase + confirm/reject/edit buttons)
│ ├── DeleteEventModal.tsx # Delete confirmation modal (uses ModalBase)
│ ├── CustomTextInput.tsx # Themed text input with focus border (used in login, register, CaldavSettings, editEvent)
│ ├── DateTimePicker.tsx # Date and time picker components
│ └── ScrollableDropdown.tsx # Scrollable dropdown component
├── Themes.tsx # Theme definitions: THEMES object with defaultLight/defaultDark, Theme type
├── logging/
│ ├── index.ts # Re-exports
│ └── logger.ts # react-native-logs config (apiLogger, storeLogger)
├── services/
│ ├── index.ts # Re-exports all services
│ ├── ApiClient.ts # HTTP client with X-User-Id header injection, request logging, handles empty responses (204)
│ ├── AuthService.ts # login(), register(), logout() - calls API and updates AuthStore
│ ├── EventService.ts # getAll(), getById(), getByDateRange(), create(), update(), delete(mode, occurrenceDate)
│ ├── ChatService.ts # sendMessage(), confirmEvent(deleteMode, occurrenceDate), rejectEvent(), getConversations(), getConversation(), updateProposalEvent()
│ └── CaldavConfigService.ts # saveConfig(), getConfig(), deleteConfig(), pull(), pushAll(), sync()
├── stores/ # Zustand state management
│ ├── index.ts # Re-exports all stores
│ ├── AuthStore.ts # user, isAuthenticated, isLoading, login(), logout(), loadStoredUser()
│ │ # Uses expo-secure-store (native) / localStorage (web)
│ ├── ChatStore.ts # messages[], isWaitingForResponse, addMessage(), addMessages(), updateMessage(), clearMessages(), setWaitingForResponse(), chatMessageToMessageData()
│ ├── EventsStore.ts # events[], setEvents(), addEvent(), updateEvent(), deleteEvent()
│ ├── CaldavConfigStore.ts # config (CaldavConfig | null), setConfig() - cached CalDAV config
│ └── ThemeStore.ts # theme, setTheme() - reactive theme switching with Zustand
└── hooks/
└── useDropdownPosition.ts # Hook for positioning dropdowns relative to trigger element
Routing: Tab-based navigation with Chat, Calendar, and Settings as main screens. Auth screens (login, register) outside tabs. Dynamic routes for event detail and note editing.
Authentication Flow:
AuthGuardcomponent wraps the tab layout in(tabs)/_layout.tsx- On app start,
AuthGuardcallsloadStoredUser()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.tsxalso callspreloadAppData()after successful login (spinner stays visible during preload)index.tsxsimply 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 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, 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 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, 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 respondedActiontracks user response per proposal (not per message)deleteMode('single' | 'future' | 'all') andoccurrenceDatefor recurring event deletionconflictingEventscontains events that overlap with the proposed time (for conflict warnings)
- Each proposal has unique
ConflictingEvent: title, startTime, endTime - simplified event info for conflict displayRecurringDeleteMode: 'single' | 'future' | 'all' - delete modes for recurring eventsDeleteRecurringEventDTO: mode, occurrenceDate? - DTO for recurring event deletionConversation: 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 events, includes optionalexceptionDatesfor proposalsGetMessagesOptions: 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"
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 timestampproposeCreateEvent- Propose new event (includes automatic conflict detection)proposeUpdateEvent- Propose event modificationproposeDeleteEvent- 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:
- Fetches events for the target day via
fetchEventsInRange - Checks for time overlaps using
occurrenceStart/occurrenceEnd(important for recurring events) - Returns
conflictingEventsarray in the proposal for UI display - 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): AfterloadStoredUser()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 viasetInterval - Sync button (
settings.tsx): Manual trigger in CaldavSettings
Lazy sync (server-side in ChatService):
- AI data access callbacks (
fetchEventsInRange,searchEvents,fetchEventById) triggersyncOnce()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 deleteChatController:pushAll()after confirming an event proposal
Sync Flow:
sync()callspushAll(push unsynced local events) thenpull(fetch remote events)pullEvents: Compares etags to skip unchanged events, creates/updates local events, deletes locally if removed remotelypushEvent: Creates or updates remote event, fetches new etag after push
Architecture:
CaldavServicedepends onCaldavRepository(config storage) andEventService(event CRUD)ChatServicedepends onEventServiceandCaldavService(lazy CalDAV sync on AI data access)EventControllerandChatControllerboth receiveCaldavServicefor 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:
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]"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(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 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, 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 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, updateProposalEvent)ChatRepositoryinterface: updateMessage(), updateProposalResponse(), updateProposalEvent() for per-proposal 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)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 usersai/utils/toolDefinitions: proposeUpdateEvent supportsrecurrenceRuleparameter, getEventsInRange tool for on-demand event loadingai/utils/toolExecutor: Async execution, conflict detection usesoccurrenceStart/occurrenceEndfor recurring events, returnsconflictingEventsin proposalsMongoEventRepository: IncludessearchByTitle()for case-insensitive title searchutils/recurrenceExpander: Handles RRULE parsing, stripsRRULE:prefix if present (AI may include it), filters out exceptionDateslogging/: 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, pushMongoCaldavRepository: Config persistence with createOrUpdate, findByUserId, deleteByUserIdEventController: 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, returnsParsedRRulewith freq, until, count, interval, byDay.buildRRule()builds RRULE from RepeatType + interval.formatRecurrenceRule()formats RRULE into German description (e.g., "Jede Woche", "Alle 2 Monate"). ExportsREPEAT_TYPE_LABELSandRepeatType.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 APIApiClient: 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. ExportspreloadAppData()(also called bylogin.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 shadowHeader: Themed header component (logout moved to Settings)(tabs)/_layout.tsx: Wraps tabs with AuthGuard for protected accessindex.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
FeedbackRowcomponent: 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
useFocusEffectwhen 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
onContentSizeChangewithanimated: falseto 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 modesChatService: sendMessage(), confirmEvent(deleteMode, occurrenceDate), rejectEvent(), getConversations(), getConversation(), updateProposalEvent() - fully implemented with cursor pagination, recurring delete support, and proposal editingCaldavConfigService: saveConfig(), getConfig(), deleteConfig(), pull(), pushAll(), sync() - CalDAV config management and sync triggerCustomTextInput: Themed text input with focus border highlight. Props:text,onValueChange,placeholder,placeholderTextColor,secureTextEntry,autoCapitalize,keyboardType,className,multiline. No default padding — callers must set padding viaclassName(e.g.,px-3 py-2orp-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 usesnestedScrollEnabledfor AndroidModalBase: 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 AndroidEventCardBase: Event card with date/time/recurring icons - uses CardBase for structure. AcceptsrecurrenceRulestring (not boolean) and displays German-formatted recurrence viaformatRecurrenceRule()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-recurringEventOverlay(in calendar.tsx): Day events overlay using ModalBase - shows EventCards for selected dayThemes.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 AuthGuardCaldavConfigStore: Zustand store with config (CaldavConfig | null), setConfig() - cached CalDAV config, preloaded by AuthGuard, used by Settings to avoid API call on mountChatStore: 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
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-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