From 89eaadc7931c057b08ebbeaccee0dbde57eae897 Mon Sep 17 00:00:00 2001 From: alma Date: Thu, 15 Jan 2026 12:11:26 +0100 Subject: [PATCH] Agenda refactor --- CALENDAR_SYNC_ARCHITECTURE_ANALYSIS.md | 728 ++++++++++++++++++ PRODUCTION_VIABILITY_ASSESSMENT.md | 680 ++++++++++++++++ lib/services/caldav-sync.ts | 36 +- lib/services/microsoft-calendar-sync.ts | 50 +- .../migration.sql | 22 + prisma/schema.prisma | 30 +- 6 files changed, 1510 insertions(+), 36 deletions(-) create mode 100644 CALENDAR_SYNC_ARCHITECTURE_ANALYSIS.md create mode 100644 PRODUCTION_VIABILITY_ASSESSMENT.md create mode 100644 prisma/migrations/20260115120912_add_external_event_id/migration.sql diff --git a/CALENDAR_SYNC_ARCHITECTURE_ANALYSIS.md b/CALENDAR_SYNC_ARCHITECTURE_ANALYSIS.md new file mode 100644 index 0000000..d28d0b9 --- /dev/null +++ b/CALENDAR_SYNC_ARCHITECTURE_ANALYSIS.md @@ -0,0 +1,728 @@ +# Calendar Synchronization Architecture - Deep Analysis + +## Executive Summary + +This document provides a comprehensive architectural analysis of the calendar synchronization system, focusing on: +- **Agenda Widget** (Dashboard widget) +- **Agenda Page** (Full calendar view) +- **Courrier Page** (Email integration) +- **Calendar Synchronization Services** (Infomaniak CalDAV & Microsoft Graph API) + +--- + +## 1. System Architecture Overview + +### 1.1 Component Hierarchy + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Dashboard (app/page.tsx) │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Calendar │ │ News │ │ Email │ │ +│ │ Widget │ │ Widget │ │ Widget │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Agenda Page (app/agenda/page.tsx) │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ CalendarClient Component │ │ +│ │ ┌──────────────┐ ┌─────────────────────────────┐ │ │ +│ │ │ Calendar │ │ FullCalendar (FullCalendar)│ │ │ +│ │ │ Selector │ │ - Month/Week/Day Views │ │ │ +│ │ │ (Sidebar) │ │ - Event Creation/Edit │ │ │ +│ │ └──────────────┘ └─────────────────────────────┘ │ │ +│ └──────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Courrier Page (app/courrier/page.tsx) │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Email │ │ Email │ │ Email │ │ +│ │ Sidebar │ │ List │ │ Detail │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Calendar Sync Services │ +│ ┌──────────────────────┐ ┌──────────────────────┐ │ +│ │ Infomaniak CalDAV │ │ Microsoft Graph API │ │ +│ │ Sync Service │ │ Sync Service │ │ +│ └──────────────────────┘ └──────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 2. Agenda Widget Analysis + +### 2.1 Component Location +- **File**: `components/calendar.tsx` +- **Usage**: Dashboard widget showing upcoming 7 events +- **Type**: Client Component (`"use client"`) + +### 2.2 Key Features + +#### Data Fetching +```typescript +// Fetches from /api/calendars?refresh=true +// Bypasses cache with refresh=true parameter +const response = await fetch('/api/calendars?refresh=true'); +``` + +#### Event Processing +1. **Extracts events** from all calendars +2. **Filters** for upcoming events (from today onwards) +3. **Sorts** by start date +4. **Limits** to 7 events +5. **Maps** calendar color and name to each event + +#### Refresh Mechanism +- **Manual refresh** only via button click +- **No automatic polling** or unified refresh integration +- **Fetches on mount** only + +### 2.3 Issues Identified + +1. **❌ No Unified Refresh Integration** + - Not using `useUnifiedRefresh` hook + - Manual refresh only + - No automatic background updates + +2. **❌ Cache Bypass** + - Always uses `?refresh=true` parameter + - Bypasses Redis cache every time + - May cause unnecessary database load + +3. **❌ No Error Recovery** + - Basic error handling + - No retry mechanism + - No offline state handling + +4. **⚠️ Date Filtering Logic** + - Filters events from "start of day" (00:00:00) + - May miss events happening later today if fetched early morning + +### 2.4 Data Flow + +``` +User Dashboard + │ + ├─> Calendar Widget (components/calendar.tsx) + │ │ + │ ├─> useEffect() triggers on mount + │ │ + │ ├─> fetch('/api/calendars?refresh=true') + │ │ │ + │ │ ├─> API Route (app/api/calendars/route.ts) + │ │ │ │ + │ │ │ ├─> Checks Redis cache (skipped if refresh=true) + │ │ │ │ + │ │ │ ├─> Prisma: Calendar.findMany() + │ │ │ │ └─> Include: events (ordered by start) + │ │ │ │ + │ │ │ └─> Cache result in Redis + │ │ │ + │ │ └─> Returns: Calendar[] with events[] + │ │ + │ ├─> Process events: + │ │ ├─> flatMap all calendars.events + │ │ ├─> Filter: start >= today (00:00:00) + │ │ ├─> Sort by start date + │ │ └─> Slice(0, 7) + │ │ + │ └─> Render: Card with event list +``` + +--- + +## 3. Agenda Page Analysis + +### 3.1 Component Location +- **File**: `app/agenda/page.tsx` (Server Component) +- **Client Component**: `components/calendar/calendar-client.tsx` +- **Route**: `/agenda` + +### 3.2 Server-Side Logic (page.tsx) + +#### Auto-Setup Calendar Sync + +The page automatically sets up calendar synchronization for email accounts: + +1. **Infomaniak Accounts** (CalDAV) + - Discovers calendars using `discoverInfomaniakCalendars()` + - Creates "Privée" calendar if not exists + - Creates `CalendarSync` record with: + - `provider: 'infomaniak'` + - `syncFrequency: 15` minutes + - `externalCalendarUrl` from discovery + - Triggers initial sync + +2. **Microsoft Accounts** (OAuth) + - Discovers calendars using `discoverMicrosoftCalendars()` + - Creates "Privée" calendar if not exists + - Creates `CalendarSync` record with: + - `provider: 'microsoft'` + - `syncFrequency: 5` minutes (more reactive) + - `externalCalendarId` from Graph API + - Triggers initial sync + +#### Calendar Filtering Logic + +```typescript +// Excludes "Privée" and "Default" calendars that are NOT synced +calendars = calendars.filter(cal => { + const isPrivateOrDefault = cal.name === "Privée" || cal.name === "Default"; + const hasActiveSync = cal.syncConfig?.syncEnabled === true && + cal.syncConfig?.mailCredential; + + // Exclude "Privée"/"Default" calendars that are not actively synced + if (isPrivateOrDefault && !hasActiveSync) { + return false; + } + return true; +}); +``` + +#### Background Sync Trigger + +For Microsoft calendars, the page triggers background sync if needed: +- Checks if sync is needed (2 minutes minimum interval) +- Triggers async sync (doesn't block page load) +- Sync runs in background, updates database +- Next page load will show synced events + +### 3.3 Client-Side Component (calendar-client.tsx) + +#### Key Features + +1. **FullCalendar Integration** + - Uses `@fullcalendar/react` library + - Views: Month, Week, Day + - Plugins: dayGrid, timeGrid, interaction + +2. **Calendar Management** + - Calendar selector sidebar (left column) + - Visibility toggle per calendar + - Calendar creation/editing dialog + - Sync configuration UI + +3. **Event Management** + - Create events by clicking/selecting dates + - Edit events by clicking on them + - Delete events + - Event form with: + - Title, description, location + - Start/end date and time + - All-day toggle + - Calendar selection + +4. **Data Refresh** + - `fetchCalendars()` function + - Calls `/api/calendars` (no refresh parameter) + - Uses Redis cache by default + - Updates FullCalendar via `calendarApi.refetchEvents()` + +#### Calendar Display Logic + +```typescript +// Sorts calendars: synced first, then groups, then missions +const sortCalendars = (cals) => { + return [...filtered].sort((a, b) => { + const aIsSynced = a.syncConfig?.syncEnabled && a.syncConfig?.mailCredential; + const bIsSynced = b.syncConfig?.syncEnabled && b.syncConfig?.mailCredential; + + // Synced calendars first + if (aIsSynced && !bIsSynced) return -1; + if (!aIsSynced && bIsSynced) return 1; + + // Then groups, then missions, then by name + // ... + }); +}; +``` + +#### Event Rendering + +Events are mapped to FullCalendar format: +```typescript +events={calendars.flatMap(cal => + cal.events.map(event => ({ + id: event.id, + title: event.title, + start: new Date(event.start), + end: new Date(event.end), + allDay: event.isAllDay, + backgroundColor: `${cal.color}dd`, + borderColor: cal.color, + extendedProps: { + calendarName: cal.name, + location: event.location, + description: cleanDescription(event.description), + calendarId: event.calendarId, + originalEvent: event, + color: cal.color + } + })) +)} +``` + +### 3.4 Issues Identified + +1. **⚠️ Sync Timing** + - Microsoft sync triggers on page load (background) + - May cause delay before events appear + - No loading indicator for background sync + +2. **⚠️ Calendar Filtering Complexity** + - Complex logic for "Privée"/"Default" calendars + - May hide calendars that should be visible + - Logging is extensive but may be confusing + +3. **❌ No Real-Time Updates** + - Events only update on manual refresh + - No WebSocket or polling for new events + - User must refresh to see synced events + +4. **⚠️ Event Matching Logic** + - Infomaniak: Matches by title + start date (within 1 minute) + - Microsoft: Matches by `[MS_ID:xxx]` in description + - May create duplicates if matching fails + +--- + +## 4. Courrier Page Analysis + +### 4.1 Component Location +- **File**: `app/courrier/page.tsx` +- **Route**: `/courrier` +- **Type**: Client Component + +### 4.2 Key Features + +#### Email Account Management +- Multiple email account support +- Account colors for visual distinction +- Account settings (display name, password, color) + +#### Email Operations +- Folder navigation (INBOX, Sent, Drafts, etc.) +- Email reading, composing, replying +- Email search +- Bulk operations (delete, mark read/unread) + +#### Integration with Calendar Sync + +The Courrier page is **indirectly** related to calendar sync: + +1. **Email Accounts** are stored in `MailCredentials` table +2. **Calendar Sync** uses `MailCredentials` for authentication +3. **Auto-Setup** in Agenda page discovers accounts from Courrier + +### 4.3 Connection to Calendar Sync + +```typescript +// In agenda/page.tsx: +const infomaniakAccounts = await prisma.mailCredentials.findMany({ + where: { + userId: session?.user?.id || '', + host: { contains: 'infomaniak' }, + password: { not: null } + } +}); + +// For each account, create calendar sync +for (const account of infomaniakAccounts) { + // Discover calendars + const calendars = await discoverInfomaniakCalendars(account.email, account.password!); + + // Create calendar and sync config + // ... +} +``` + +### 4.4 Issues Identified + +1. **⚠️ No Direct Calendar Integration** + - Courrier page doesn't show calendar events + - No email-to-calendar event creation + - Separate systems (email vs calendar) + +2. **⚠️ Account Deletion Impact** + - Deleting email account may orphan calendar sync + - Agenda page has cleanup logic, but may miss edge cases + +--- + +## 5. Calendar Synchronization Services + +### 5.1 Infomaniak CalDAV Sync + +#### Service Location +- **File**: `lib/services/caldav-sync.ts` + +#### Key Functions + +1. **`discoverInfomaniakCalendars(email, password)`** + - Uses WebDAV client (`webdav` library) + - Connects to `https://sync.infomaniak.com` + - Lists directories using `PROPFIND` + - Extracts calendar name and color from XML + +2. **`fetchCalDAVEvents(email, password, calendarUrl, startDate, endDate)`** + - Uses `REPORT` method with `calendar-query` + - Filters events by date range + - Parses iCalendar format (`.ics`) + - Returns `CalDAVEvent[]` + +3. **`syncInfomaniakCalendar(calendarSyncId, forceSync)`** + - Fetches events from CalDAV (1 month ago to 3 months ahead) + - Gets existing events from database + - Matches events by title + start date (within 1 minute) + - Creates new events or updates existing + - Updates `lastSyncAt` timestamp + +#### Event Matching Logic + +```typescript +// Tries to find existing event by: +const existingEvent = existingEvents.find( + (e) => + e.title === caldavEvent.summary && + Math.abs(new Date(e.start).getTime() - caldavEvent.start.getTime()) < 60000 +); +``` + +**Issue**: This matching is fragile: +- May create duplicates if title changes +- May miss events if timezone conversion causes >1 minute difference +- No UID-based matching (iCalendar has UID field) + +### 5.2 Microsoft Graph API Sync + +#### Service Location +- **File**: `lib/services/microsoft-calendar-sync.ts` + +#### Key Functions + +1. **`discoverMicrosoftCalendars(userId, email)`** + - Uses Microsoft Graph API + - Endpoint: `https://graph.microsoft.com/v1.0/me/calendars` + - Requires OAuth token with calendar scope + - Returns `MicrosoftCalendar[]` + +2. **`fetchMicrosoftEvents(userId, email, calendarId, startDate, endDate)`** + - Endpoint: `https://graph.microsoft.com/v1.0/me/calendars/{calendarId}/events` + - Filters by date range using `$filter` parameter + - Returns `MicrosoftEvent[]` + +3. **`syncMicrosoftCalendar(calendarSyncId, forceSync)`** + - Fetches events (1 month ago to 6 months ahead) + - Converts Microsoft events to CalDAV-like format + - **Stores Microsoft ID in description**: `[MS_ID:xxx]` + - Matches events by Microsoft ID first, then by title+date + - Creates/updates events in database + +#### Event Matching Logic + +```typescript +// First: Match by Microsoft ID in description +let existingEvent = existingEvents.find((e) => { + if (e.description && e.description.includes(`[MS_ID:${microsoftId}]`)) { + return true; + } + return false; +}); + +// Fallback: Match by title + start date +if (!existingEvent) { + existingEvent = existingEvents.find( + (e) => + e.title === caldavEvent.summary && + Math.abs(new Date(e.start).getTime() - caldavEvent.start.getTime()) < 60000 + ); +} +``` + +**Better than Infomaniak**: Uses ID-based matching, but stores ID in description (hacky) + +### 5.3 Sync Job Service + +#### Service Location +- **File**: `lib/services/calendar-sync-job.ts` + +#### Function: `runCalendarSyncJob()` + +- Gets all enabled sync configurations +- Checks if sync is needed (based on `syncFrequency`) +- Calls appropriate sync function based on provider +- Logs results (successful, failed, skipped) + +**Usage**: Should be called by cron job or scheduled task + +### 5.4 Issues Identified + +1. **❌ No UID-Based Matching for Infomaniak** + - Should use iCalendar UID field + - Current matching is fragile + +2. **⚠️ Microsoft ID Storage** + - Stores ID in description field (hacky) + - Should use dedicated `externalEventId` field in Event model + +3. **❌ No Event Deletion** + - Sync only creates/updates events + - Doesn't delete events removed from external calendar + - May show stale events + +4. **⚠️ Error Handling** + - Errors are logged but sync continues + - May leave calendar in inconsistent state + - No retry mechanism for transient failures + +5. **⚠️ Sync Frequency** + - Infomaniak: 15 minutes (may be too slow) + - Microsoft: 5 minutes (better, but still not real-time) + - No user-configurable frequency + +6. **❌ No Incremental Sync** + - Always fetches full date range + - May be slow for calendars with many events + - Should use `lastSyncAt` to fetch only changes + +--- + +## 6. API Routes Analysis + +### 6.1 `/api/calendars` (GET) + +**File**: `app/api/calendars/route.ts` + +#### Features +- Redis caching (unless `?refresh=true`) +- Returns calendars with events +- Events ordered by start date + +#### Issues +- Cache TTL not specified (uses default) +- No cache invalidation on event creation/update +- `refresh=true` bypasses cache (used by widget) + +### 6.2 `/api/calendars/sync` (POST, PUT, GET, DELETE) + +**File**: `app/api/calendars/sync/route.ts` + +#### Features +- **POST**: Create sync configuration +- **PUT**: Trigger manual sync +- **GET**: Get sync status +- **DELETE**: Remove sync configuration + +#### Issues +- Manual sync triggers full sync (not incremental) +- No webhook support for real-time updates + +--- + +## 7. Database Schema + +### 7.1 Calendar Model + +```prisma +model Calendar { + id String @id @default(uuid()) + name String + color String + description String? + userId String + missionId String? + events Event[] + syncConfig CalendarSync? + // ... +} +``` + +### 7.2 CalendarSync Model + +```prisma +model CalendarSync { + id String @id @default(uuid()) + calendarId String @unique + mailCredentialId String? + provider String // "infomaniak" | "microsoft" + externalCalendarId String? + externalCalendarUrl String? + syncEnabled Boolean @default(true) + lastSyncAt DateTime? + syncFrequency Int @default(15) // minutes + lastSyncError String? + // ... +} +``` + +### 7.3 Event Model + +```prisma +model Event { + id String @id @default(uuid()) + title String + description String? + start DateTime + end DateTime + location String? + isAllDay Boolean @default(false) + calendarId String + userId String + // ... +} +``` + +**Missing Fields**: +- `externalEventId` (for reliable matching) +- `externalEventUrl` (for linking to external calendar) +- `lastSyncedAt` (for incremental sync) + +--- + +## 8. Critical Issues Summary + +### 8.1 High Priority + +1. **Event Matching Fragility** + - Infomaniak: No UID-based matching + - Microsoft: ID stored in description (hacky) + - **Impact**: Duplicate events, missed updates + +2. **No Event Deletion** + - Removed events stay in database + - **Impact**: Stale events shown to users + +3. **No Real-Time Updates** + - Widget and page don't auto-refresh + - **Impact**: Users must manually refresh to see new events + +### 8.2 Medium Priority + +4. **Cache Invalidation** + - Events created/updated don't invalidate cache + - **Impact**: Stale data shown until cache expires + +5. **Sync Frequency** + - Infomaniak: 15 minutes (too slow) + - **Impact**: Delayed event appearance + +6. **No Incremental Sync** + - Always fetches full date range + - **Impact**: Slow sync, unnecessary API calls + +### 8.3 Low Priority + +7. **Widget Refresh Integration** + - Not using unified refresh system + - **Impact**: Inconsistent refresh behavior + +8. **Error Recovery** + - No retry mechanism + - **Impact**: Sync failures require manual intervention + +--- + +## 9. Recommendations + +### 9.1 Immediate Fixes + +1. **Add `externalEventId` field to Event model** + ```prisma + model Event { + // ... existing fields + externalEventId String? // UID from iCalendar or Microsoft ID + externalEventUrl String? // Link to external calendar event + } + ``` + +2. **Implement UID-based matching for Infomaniak** + - Use iCalendar UID field for matching + - Store in `externalEventId` + +3. **Implement event deletion** + - Compare external events with database events + - Delete events not in external calendar + +4. **Add cache invalidation** + - Invalidate Redis cache on event create/update/delete + - Invalidate on sync completion + +### 9.2 Short-Term Improvements + +5. **Implement incremental sync** + - Use `lastSyncAt` to fetch only changes + - Use CalDAV `sync-token` for Infomaniak + - Use Microsoft Graph delta queries + +6. **Add real-time updates** + - WebSocket or Server-Sent Events + - Notify clients when sync completes + - Auto-refresh widget and page + +7. **Improve error handling** + - Retry mechanism for transient failures + - Better error messages for users + - Sync status indicator in UI + +### 9.3 Long-Term Enhancements + +8. **Unified refresh system integration** + - Use `useUnifiedRefresh` hook in widget + - Consistent refresh behavior across app + +9. **User-configurable sync frequency** + - Allow users to set sync interval + - Different frequencies per calendar + +10. **Two-way sync** + - Sync local events to external calendars + - Handle conflicts (last-write-wins or user choice) + +--- + +## 10. Testing Recommendations + +### 10.1 Unit Tests +- Event matching logic (UID-based, title+date fallback) +- Date range filtering +- iCalendar parsing +- Microsoft event conversion + +### 10.2 Integration Tests +- Full sync flow (discover → sync → verify) +- Event creation/update/deletion +- Cache invalidation +- Error recovery + +### 10.3 E2E Tests +- Widget displays events correctly +- Page shows synced events +- Manual sync triggers correctly +- Calendar creation/editing + +--- + +## 11. Conclusion + +The calendar synchronization system is **functional but has several architectural issues** that impact reliability and user experience: + +1. **Fragile event matching** may cause duplicates or missed updates +2. **No event deletion** leaves stale events in the database +3. **No real-time updates** requires manual refresh +4. **Cache invalidation** is missing, causing stale data + +**Priority**: Focus on fixing event matching and deletion first, as these directly impact data integrity. Then implement cache invalidation and real-time updates for better UX. + +--- + +**Document Version**: 1.0 +**Last Updated**: 2025-01-XX +**Author**: Senior Software Architect Analysis diff --git a/PRODUCTION_VIABILITY_ASSESSMENT.md b/PRODUCTION_VIABILITY_ASSESSMENT.md new file mode 100644 index 0000000..74ab076 --- /dev/null +++ b/PRODUCTION_VIABILITY_ASSESSMENT.md @@ -0,0 +1,680 @@ +# Production Viability Assessment - Neah Platform + +**Assessment Date:** January 2026 +**Assessed By:** Senior Software Architect +**Project:** Neah - Mission Management & Calendar Platform +**Status:** ⚠️ **CONDITIONAL APPROVAL** - Requires Critical Fixes Before Production + +--- + +## Executive Summary + +The Neah platform is a Next.js-based mission management system with calendar integration, email management, and multiple third-party integrations (Keycloak, Leantime, RocketChat, N8N, etc.). While the application demonstrates solid architectural foundations and comprehensive documentation, **several critical issues must be addressed before production deployment**. + +### Overall Assessment: **6.5/10** - Conditional Approval + +**Key Strengths:** +- ✅ Comprehensive documentation (deployment, runbook, observability) +- ✅ Modern tech stack (Next.js 16, Prisma, PostgreSQL, Redis) +- ✅ Health check endpoint implemented +- ✅ Environment variable validation with Zod +- ✅ Structured logging system +- ✅ Docker production configuration + +**Critical Blockers:** +- 🔴 **TypeScript/ESLint errors ignored in production builds** (next.config.mjs) +- 🔴 **No automated testing infrastructure** +- 🔴 **Security incident history** (backdoor vulnerability - resolved but requires audit) +- 🔴 **Excessive console.log statements** in production code +- 🔴 **No rate limiting** on API endpoints +- 🔴 **Missing environment variable validation** for many critical vars + +**High Priority Issues:** +- 🟡 Database connection pooling not explicitly configured +- 🟡 No request timeout middleware +- 🟡 Missing input validation on some API routes +- 🟡 No automated backup strategy documented +- 🟡 Limited error recovery mechanisms + +--- + +## 1. Architecture & Infrastructure + +### 1.1 Application Architecture + +**Status:** ✅ **Good** + +- **Framework:** Next.js 16.1.1 (App Router) +- **Deployment:** Vercel (serverless functions) +- **Database:** PostgreSQL 15 (self-hosted) +- **Cache:** Redis (self-hosted) +- **Storage:** S3-compatible (MinIO) + +**Strengths:** +- Modern serverless architecture suitable for scaling +- Clear separation of concerns (API routes, services, lib) +- Proper use of Next.js App Router patterns + +**Concerns:** +- No clear strategy for handling cold starts on Vercel +- Database connection from serverless functions may have latency issues +- No CDN configuration for static assets + +**Recommendations:** +- [ ] Implement database connection pooling at Prisma level +- [ ] Configure Vercel Edge Functions for high-frequency endpoints +- [ ] Set up CDN for static assets and images + +### 1.2 Infrastructure Configuration + +**Status:** ⚠️ **Needs Improvement** + +**Docker Configuration:** +- ✅ Production Dockerfile with multi-stage builds +- ✅ Non-root user in production image +- ✅ Health checks configured +- ⚠️ Resource limits defined but may need tuning +- ⚠️ No backup strategy in docker-compose.prod.yml + +**Vercel Configuration:** +- ✅ Proper build commands +- ✅ Security headers configured +- ⚠️ Function timeout set to 30s (may be insufficient for some operations) +- ⚠️ No region configuration for database proximity + +**Recommendations:** +- [ ] Add automated backup cron job to docker-compose.prod.yml +- [ ] Configure Vercel regions closer to database server +- [ ] Review and optimize function timeouts per endpoint + +--- + +## 2. Security Assessment + +### 2.1 Critical Security Issues + +**Status:** 🔴 **CRITICAL CONCERNS** + +#### Issue 1: Build Error Suppression +```javascript +// next.config.mjs +eslint: { + ignoreDuringBuilds: true, // ❌ DANGEROUS +}, +typescript: { + ignoreBuildErrors: true, // ❌ DANGEROUS +} +``` + +**Risk:** Type errors and linting issues can introduce runtime bugs in production. + +**Impact:** HIGH - Could lead to production failures + +**Recommendation:** +- [ ] **MUST FIX:** Remove error suppression, fix all TypeScript/ESLint errors +- [ ] Set up pre-commit hooks to prevent errors from reaching production +- [ ] Use CI/CD to block deployments with errors + +#### Issue 2: Security Incident History +- Previous backdoor vulnerability (CVE-2025-66478) in Next.js 15.3.1 +- **Status:** ✅ Resolved (upgraded to Next.js 16.1.1) +- **Action Required:** Security audit of all configuration files + +**Recommendations:** +- [ ] Complete security audit of all config files +- [ ] Review all dynamic imports +- [ ] Implement file integrity monitoring +- [ ] Set up automated security scanning (Snyk, npm audit) + +#### Issue 3: Missing Rate Limiting +**Status:** 🔴 **CRITICAL** + +No rate limiting found on API endpoints. This exposes the application to: +- DDoS attacks +- Brute force attacks +- Resource exhaustion + +**Recommendations:** +- [ ] Implement rate limiting middleware (e.g., `@upstash/ratelimit`) +- [ ] Configure per-endpoint limits +- [ ] Add IP-based throttling +- [ ] Set up Redis-based distributed rate limiting + +#### Issue 4: Environment Variable Validation +**Status:** ⚠️ **PARTIAL** + +**Current State:** +- ✅ Basic validation in `lib/env.ts` using Zod +- ❌ Many critical variables not validated (N8N_API_KEY, S3 credentials, etc.) + +**Missing Validations:** +- `N8N_API_KEY` (required but not in schema) +- `MINIO_ACCESS_KEY`, `MINIO_SECRET_KEY` +- `S3_BUCKET` +- `NEXTAUTH_SECRET` (should be validated for strength) + +**Recommendations:** +- [ ] Expand `env.ts` schema to include ALL environment variables +- [ ] Add validation for secret strength (NEXTAUTH_SECRET min length) +- [ ] Fail fast on missing critical variables at startup + +### 2.2 Authentication & Authorization + +**Status:** ✅ **Good** + +- ✅ NextAuth.js with Keycloak provider +- ✅ JWT-based sessions (4-hour timeout) +- ✅ Role-based access control +- ✅ Session refresh mechanism + +**Concerns:** +- ⚠️ Some API routes have inconsistent auth checks +- ⚠️ No API key rotation strategy documented + +**Recommendations:** +- [ ] Standardize auth middleware across all API routes +- [ ] Implement API key rotation for N8N integration +- [ ] Add audit logging for authentication events + +### 2.3 Data Security + +**Status:** ⚠️ **Needs Review** + +**Database:** +- ✅ Passwords stored (assumed hashed, need verification) +- ⚠️ No encryption at rest mentioned +- ⚠️ Connection strings in environment (should use secrets manager) + +**File Storage:** +- ✅ S3-compatible storage +- ⚠️ No file size limits enforced +- ⚠️ No virus scanning mentioned + +**Recommendations:** +- [ ] Verify password hashing implementation (bcrypt with proper salt rounds) +- [ ] Implement file upload size limits +- [ ] Add file type validation +- [ ] Consider encryption at rest for sensitive data + +--- + +## 3. Code Quality + +### 3.1 TypeScript & Type Safety + +**Status:** 🔴 **CRITICAL** + +**Issues:** +- TypeScript errors ignored in builds (`ignoreBuildErrors: true`) +- No strict null checks enforced +- Some `any` types found in codebase + +**Impact:** Runtime errors, difficult debugging, poor developer experience + +**Recommendations:** +- [ ] **MUST FIX:** Remove `ignoreBuildErrors`, fix all TypeScript errors +- [ ] Enable strict mode in tsconfig.json +- [ ] Add type coverage tooling +- [ ] Set up pre-commit hooks for type checking + +### 3.2 Code Practices + +**Status:** ⚠️ **Needs Improvement** + +**Issues Found:** +- 🔴 **80+ console.log/console.error statements** in production code +- ⚠️ Inconsistent error handling patterns +- ⚠️ Some API routes lack input validation +- ⚠️ No request timeout middleware + +**Console.log Locations:** +- `app/courrier/page.tsx` - Multiple console.log statements +- `app/api/courrier/unread-counts/route.ts` - console.log in production +- `lib/utils/request-deduplication.ts` - console.log statements +- Many more throughout the codebase + +**Recommendations:** +- [ ] Replace all `console.log` with proper logger calls +- [ ] Implement request timeout middleware +- [ ] Add input validation middleware (Zod schemas) +- [ ] Standardize error response format + +### 3.3 Error Handling + +**Status:** ⚠️ **Inconsistent** + +**Good Practices Found:** +- ✅ Structured logging with logger utility +- ✅ Try-catch blocks in most API routes +- ✅ Error cleanup in mission creation (file deletion on failure) + +**Issues:** +- ⚠️ Some errors return generic messages without context +- ⚠️ No global error boundary for API routes +- ⚠️ Database errors not always handled gracefully + +**Recommendations:** +- [ ] Implement global error handler middleware +- [ ] Add error codes for better client-side handling +- [ ] Implement retry logic for transient failures +- [ ] Add circuit breakers for external service calls + +--- + +## 4. Database & Data Management + +### 4.1 Database Schema + +**Status:** ✅ **Good** + +- ✅ Prisma ORM with proper schema definition +- ✅ Indexes on foreign keys and frequently queried fields +- ✅ Cascade deletes configured appropriately +- ✅ UUID primary keys + +**Concerns:** +- ⚠️ No database migration rollback strategy documented +- ⚠️ No data retention policies defined + +**Recommendations:** +- [ ] Document migration rollback procedures +- [ ] Define data retention policies +- [ ] Add database versioning strategy + +### 4.2 Connection Management + +**Status:** ⚠️ **Needs Configuration** + +**Current State:** +- Prisma Client with default connection pooling +- No explicit connection pool configuration +- Redis connection with retry logic (good) + +**Issues:** +- No connection pool size limits +- No connection timeout configuration +- Potential connection exhaustion under load + +**Recommendations:** +- [ ] Configure Prisma connection pool: + ```prisma + datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + // Add connection pool settings + } + ``` +- [ ] Set appropriate pool size based on Vercel function concurrency +- [ ] Add connection monitoring + +### 4.3 Data Backup & Recovery + +**Status:** ⚠️ **Incomplete** + +**Current State:** +- ✅ Backup procedures documented in RUNBOOK.md +- ❌ No automated backup system +- ❌ No backup retention policy +- ❌ No backup testing procedure + +**Recommendations:** +- [ ] Implement automated daily backups +- [ ] Set up backup retention (30 days minimum) +- [ ] Test restore procedures monthly +- [ ] Add backup verification checks + +--- + +## 5. Testing + +### 5.1 Test Coverage + +**Status:** 🔴 **CRITICAL - NO TESTS FOUND** + +**Current State:** +- ❌ No unit tests +- ❌ No integration tests +- ❌ No E2E tests +- ❌ No test infrastructure + +**Impact:** HIGH - No confidence in code changes, high risk of regressions + +**Recommendations:** +- [ ] **MUST IMPLEMENT:** Set up Jest/Vitest for unit tests +- [ ] Add integration tests for critical API routes +- [ ] Implement E2E tests for critical user flows +- [ ] Set up CI/CD to run tests on every PR +- [ ] Target: 70%+ code coverage for critical paths + +**Priority Test Areas:** +1. Authentication flows +2. Mission creation/update/deletion +3. File upload handling +4. Calendar sync operations +5. Email integration + +--- + +## 6. Performance & Scalability + +### 6.1 Performance Optimizations + +**Status:** ⚠️ **Partial** + +**Good Practices:** +- ✅ Redis caching implemented +- ✅ Request deduplication for email operations +- ✅ Connection pooling for IMAP +- ✅ Background refresh for unread counts + +**Missing:** +- ❌ No CDN for static assets +- ❌ No image optimization pipeline +- ❌ No query result pagination on some endpoints +- ❌ No database query optimization monitoring + +**Recommendations:** +- [ ] Implement CDN (Vercel Edge Network or Cloudflare) +- [ ] Add image optimization (Next.js Image component) +- [ ] Add pagination to all list endpoints +- [ ] Set up query performance monitoring +- [ ] Implement database query logging in development + +### 6.2 Scalability Concerns + +**Status:** ⚠️ **Needs Planning** + +**Potential Bottlenecks:** +1. **Database Connections:** Serverless functions may exhaust pool +2. **Redis Connection:** Single Redis instance (no clustering) +3. **File Storage:** No CDN, direct S3 access +4. **External APIs:** No circuit breakers for N8N, Leantime, etc. + +**Recommendations:** +- [ ] Plan for database read replicas +- [ ] Consider Redis Cluster for high availability +- [ ] Implement circuit breakers for external services +- [ ] Add load testing before production launch + +--- + +## 7. Monitoring & Observability + +### 7.1 Logging + +**Status:** ✅ **Good** + +- ✅ Structured logging with logger utility +- ✅ Log levels (info, warn, error, debug) +- ✅ Contextual information in logs + +**Issues:** +- ⚠️ Console.log statements still present (80+ instances) +- ⚠️ No log aggregation system configured +- ⚠️ No log retention policy + +**Recommendations:** +- [ ] Remove all console.log statements +- [ ] Set up log aggregation (Logtail, Datadog, or similar) +- [ ] Define log retention policy +- [ ] Add request ID tracking for distributed tracing + +### 7.2 Monitoring + +**Status:** ⚠️ **Basic** + +**Current State:** +- ✅ Health check endpoint (`/api/health`) +- ✅ Vercel Analytics available +- ❌ No APM (Application Performance Monitoring) +- ❌ No error tracking (Sentry not configured) +- ❌ No uptime monitoring + +**Recommendations:** +- [ ] Set up Sentry for error tracking +- [ ] Configure Vercel Analytics and Speed Insights +- [ ] Add uptime monitoring (Uptime Robot, Pingdom) +- [ ] Implement custom metrics dashboard +- [ ] Set up alerting for critical errors + +### 7.3 Observability + +**Status:** ⚠️ **Incomplete** + +**Documentation:** +- ✅ Comprehensive OBSERVABILITY.md document +- ❌ Not all recommendations implemented + +**Missing:** +- No distributed tracing +- No performance profiling +- No database query monitoring + +**Recommendations:** +- [ ] Implement distributed tracing (OpenTelemetry) +- [ ] Add performance profiling for slow endpoints +- [ ] Set up database query monitoring (pg_stat_statements) + +--- + +## 8. Documentation + +### 8.1 Technical Documentation + +**Status:** ✅ **Excellent** + +**Strengths:** +- ✅ Comprehensive DEPLOYMENT.md +- ✅ Detailed RUNBOOK.md with procedures +- ✅ OBSERVABILITY.md with monitoring strategy +- ✅ Multiple issue analysis documents +- ✅ API documentation in code comments + +**Recommendations:** +- [ ] Add API documentation (OpenAPI/Swagger) +- [ ] Document all environment variables in one place +- [ ] Create architecture diagram +- [ ] Add troubleshooting guide + +### 8.2 Operational Documentation + +**Status:** ✅ **Good** + +- ✅ Runbook with incident procedures +- ✅ Deployment procedures documented +- ✅ Rollback procedures defined + +**Missing:** +- On-call rotation documentation +- Escalation procedures +- Service level objectives (SLOs) + +--- + +## 9. Deployment & DevOps + +### 9.1 CI/CD Pipeline + +**Status:** ⚠️ **Basic** + +**Current State:** +- ✅ Vercel automatic deployments from Git +- ❌ No pre-deployment checks +- ❌ No automated testing in pipeline +- ❌ No staging environment mentioned + +**Recommendations:** +- [ ] Set up staging environment +- [ ] Add pre-deployment checks (tests, linting, type checking) +- [ ] Implement deployment gates +- [ ] Add automated smoke tests post-deployment + +### 9.2 Environment Management + +**Status:** ⚠️ **Needs Improvement** + +**Issues:** +- No `.env.example` file found +- Environment variables scattered across documentation +- No validation script for required variables + +**Recommendations:** +- [ ] Create comprehensive `.env.example` +- [ ] Add environment validation script +- [ ] Document all required variables in one place +- [ ] Use secrets manager for production (Vercel Secrets) + +--- + +## 10. Risk Assessment + +### 10.1 High-Risk Areas + +| Risk | Severity | Likelihood | Mitigation Priority | +|------|----------|------------|---------------------| +| No tests = production bugs | HIGH | HIGH | **CRITICAL** | +| TypeScript errors ignored | HIGH | MEDIUM | **CRITICAL** | +| No rate limiting = DDoS risk | HIGH | MEDIUM | **HIGH** | +| Database connection exhaustion | MEDIUM | MEDIUM | **HIGH** | +| Missing environment validation | MEDIUM | HIGH | **HIGH** | +| No automated backups | HIGH | LOW | **MEDIUM** | +| Console.log in production | LOW | HIGH | **MEDIUM** | + +### 10.2 Production Readiness Checklist + +#### Critical (Must Fix Before Production) +- [ ] Remove TypeScript/ESLint error suppression +- [ ] Fix all TypeScript errors +- [ ] Implement rate limiting +- [ ] Remove all console.log statements +- [ ] Complete environment variable validation +- [ ] Set up basic test suite (at least for critical paths) +- [ ] Security audit of configuration files + +#### High Priority (Fix Within 1-2 Weeks) +- [ ] Configure database connection pooling +- [ ] Implement request timeout middleware +- [ ] Add input validation to all API routes +- [ ] Set up error tracking (Sentry) +- [ ] Configure automated backups +- [ ] Add API documentation + +#### Medium Priority (Fix Within 1 Month) +- [ ] Set up staging environment +- [ ] Implement CDN +- [ ] Add comprehensive test coverage +- [ ] Set up APM +- [ ] Create architecture diagrams +- [ ] Implement circuit breakers + +--- + +## 11. Recommendations Summary + +### Immediate Actions (Before Production) + +1. **🔴 CRITICAL: Fix Build Configuration** + ```javascript + // next.config.mjs - REMOVE these lines: + eslint: { ignoreDuringBuilds: true }, + typescript: { ignoreBuildErrors: true }, + ``` + Then fix all resulting errors. + +2. **🔴 CRITICAL: Implement Rate Limiting** + - Use `@upstash/ratelimit` with Redis + - Apply to all API endpoints + - Configure per-endpoint limits + +3. **🔴 CRITICAL: Remove Console.log Statements** + - Replace with logger calls + - Use grep to find all instances + - Set up pre-commit hook to prevent new ones + +4. **🔴 CRITICAL: Complete Environment Validation** + - Expand `lib/env.ts` schema + - Validate all required variables + - Fail fast on missing variables + +5. **🟡 HIGH: Set Up Basic Testing** + - Install Jest/Vitest + - Write tests for critical API routes + - Set up CI to run tests + +### Short-Term Improvements (1-2 Weeks) + +6. Configure database connection pooling +7. Implement request timeout middleware +8. Add input validation middleware +9. Set up Sentry for error tracking +10. Configure automated backups +11. Create comprehensive `.env.example` + +### Long-Term Enhancements (1 Month+) + +12. Set up staging environment +13. Implement comprehensive test coverage (70%+) +14. Add CDN for static assets +15. Set up APM and distributed tracing +16. Create API documentation (OpenAPI) +17. Implement circuit breakers for external services + +--- + +## 12. Conclusion + +### Production Readiness: **CONDITIONAL** + +The Neah platform has a **solid foundation** with good architecture, comprehensive documentation, and modern technology choices. However, **critical issues must be addressed** before production deployment. + +### Estimated Time to Production-Ready: **2-3 Weeks** + +**Minimum Requirements Met:** +- ✅ Health check endpoint +- ✅ Error handling (basic) +- ✅ Logging infrastructure +- ✅ Database migrations +- ✅ Docker configuration + +**Critical Gaps:** +- ❌ No testing infrastructure +- ❌ Build errors suppressed +- ❌ No rate limiting +- ❌ Security concerns (console.log, missing validation) + +### Recommendation + +**DO NOT DEPLOY TO PRODUCTION** until: +1. TypeScript/ESLint errors are fixed (remove suppression) +2. Rate limiting is implemented +3. Basic test suite is in place +4. All console.log statements are removed +5. Environment variable validation is complete + +**After addressing critical issues**, the platform should be **production-ready** with ongoing monitoring and gradual rollout recommended. + +--- + +## Appendix: Quick Reference + +### Critical Files to Review +- `next.config.mjs` - Remove error suppression +- `lib/env.ts` - Complete validation schema +- `app/api/**/*.ts` - Add rate limiting, remove console.log +- `package.json` - Add test scripts and dependencies + +### Key Metrics to Monitor +- API response times +- Error rates +- Database connection pool usage +- Redis memory usage +- External API call success rates + +### Emergency Contacts +- See RUNBOOK.md for escalation procedures +- Vercel Support: https://vercel.com/support + +--- + +**Assessment Completed:** January 2026 +**Next Review:** After critical fixes implemented diff --git a/lib/services/caldav-sync.ts b/lib/services/caldav-sync.ts index cf637ae..bae5362 100644 --- a/lib/services/caldav-sync.ts +++ b/lib/services/caldav-sync.ts @@ -398,10 +398,13 @@ export async function syncInfomaniakCalendar( }, }); - // Create a map of existing events by external UID - const existingEventsMap = new Map(); - // Store events that have external UID in metadata (we'll need to add this field) - // For now, we'll match by title and date + // Create a map of existing events by externalEventId (UID) for fast lookup + const existingEventsByExternalId = new Map(); + for (const event of existingEvents) { + if (event.externalEventId) { + existingEventsByExternalId.set(event.externalEventId, event); + } + } let created = 0; let updated = 0; @@ -409,12 +412,24 @@ export async function syncInfomaniakCalendar( // Sync events: create or update for (const caldavEvent of caldavEvents) { - // Try to find existing event by matching title and start date - const existingEvent = existingEvents.find( - (e) => - e.title === caldavEvent.summary && - Math.abs(new Date(e.start).getTime() - caldavEvent.start.getTime()) < 60000 // Within 1 minute - ); + // Priority 1: Match by externalEventId (UID) - most reliable + let existingEvent = caldavEvent.uid + ? existingEventsByExternalId.get(caldavEvent.uid) + : undefined; + + // Priority 2: Fallback to title + date matching for events without externalEventId (backward compatibility) + if (!existingEvent) { + existingEvent = existingEvents.find( + (e) => { + if (!e.externalEventId && // Only match events that don't have externalEventId yet + e.title === caldavEvent.summary) { + const timeDiff = Math.abs(new Date(e.start).getTime() - caldavEvent.start.getTime()); + return timeDiff < 60000; // Within 1 minute + } + return false; + } + ); + } const eventData = { title: caldavEvent.summary, @@ -425,6 +440,7 @@ export async function syncInfomaniakCalendar( isAllDay: caldavEvent.allDay, calendarId: syncConfig.calendarId, userId: syncConfig.calendar.userId, + externalEventId: caldavEvent.uid, // Store UID for reliable matching }; if (existingEvent) { diff --git a/lib/services/microsoft-calendar-sync.ts b/lib/services/microsoft-calendar-sync.ts index ab17bb9..736e3d5 100644 --- a/lib/services/microsoft-calendar-sync.ts +++ b/lib/services/microsoft-calendar-sync.ts @@ -439,6 +439,14 @@ export async function syncMicrosoftCalendar( }, }); + // Create a map of existing events by externalEventId (Microsoft ID) for fast lookup + const existingEventsByExternalId = new Map(); + for (const event of existingEvents) { + if (event.externalEventId) { + existingEventsByExternalId.set(event.externalEventId, event); + } + } + let created = 0; let updated = 0; let deleted = 0; @@ -449,40 +457,56 @@ export async function syncMicrosoftCalendar( newEventsCount: caldavEvents.length, }); + // Helper function to clean description (remove [MS_ID:xxx] prefix if present) + const cleanDescription = (description: string | null | undefined): string | null => { + if (!description) return null; + // Remove [MS_ID:xxx] prefix if present + const cleaned = description.replace(/^\[MS_ID:[^\]]+\]\n?/, ''); + return cleaned.trim() || null; + }; + // Sync events: create or update for (const caldavEvent of caldavEvents) { - // Store Microsoft ID in description with a special prefix for matching const microsoftId = caldavEvent.uid; - const descriptionWithId = caldavEvent.description - ? `[MS_ID:${microsoftId}]\n${caldavEvent.description}` - : `[MS_ID:${microsoftId}]`; - // Try to find existing event by Microsoft ID first (most reliable) - let existingEvent = existingEvents.find((e) => { - if (e.description && e.description.includes(`[MS_ID:${microsoftId}]`)) { - return true; - } - return false; - }); + // Priority 1: Match by externalEventId (Microsoft ID) - most reliable + let existingEvent = microsoftId + ? existingEventsByExternalId.get(microsoftId) + : undefined; - // Fallback: try to find by matching title and start date (for events created before this fix) + // Priority 2: Fallback to checking description for [MS_ID:xxx] (backward compatibility) + if (!existingEvent && microsoftId) { + existingEvent = existingEvents.find((e) => { + if (!e.externalEventId && e.description && e.description.includes(`[MS_ID:${microsoftId}]`)) { + return true; + } + return false; + }); + } + + // Priority 3: Fallback to title + date matching for events without externalEventId if (!existingEvent) { existingEvent = existingEvents.find( (e) => + !e.externalEventId && // Only match events that don't have externalEventId yet e.title === caldavEvent.summary && Math.abs(new Date(e.start).getTime() - caldavEvent.start.getTime()) < 60000 // Within 1 minute ); } + // Clean description (remove [MS_ID:xxx] prefix if present from previous syncs) + const cleanedDescription = cleanDescription(caldavEvent.description); + const eventData = { title: caldavEvent.summary, - description: descriptionWithId, + description: cleanedDescription, // Clean description without [MS_ID:xxx] prefix start: caldavEvent.start, end: caldavEvent.end, location: caldavEvent.location || null, isAllDay: caldavEvent.allDay, calendarId: syncConfig.calendarId, userId: syncConfig.calendar.userId, + externalEventId: microsoftId, // Store Microsoft ID for reliable matching }; if (existingEvent) { diff --git a/prisma/migrations/20260115120912_add_external_event_id/migration.sql b/prisma/migrations/20260115120912_add_external_event_id/migration.sql new file mode 100644 index 0000000..5067ef7 --- /dev/null +++ b/prisma/migrations/20260115120912_add_external_event_id/migration.sql @@ -0,0 +1,22 @@ +-- AlterTable: Add externalEventId and externalEventUrl columns to Event table +DO $$ +BEGIN + -- Add externalEventId column if it doesn't exist + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'Event' AND column_name = 'externalEventId' + ) THEN + ALTER TABLE "Event" ADD COLUMN "externalEventId" TEXT; + END IF; + + -- Add externalEventUrl column if it doesn't exist + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'Event' AND column_name = 'externalEventUrl' + ) THEN + ALTER TABLE "Event" ADD COLUMN "externalEventUrl" TEXT; + END IF; +END $$; + +-- CreateIndex: Add index on externalEventId for fast matching during sync +CREATE INDEX IF NOT EXISTS "Event_externalEventId_idx" ON "Event"("externalEventId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 75a1bd0..a76c768 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -46,22 +46,26 @@ model Calendar { } model Event { - id String @id @default(uuid()) - title String - description String? - start DateTime - end DateTime - location String? - isAllDay Boolean @default(false) - calendar Calendar @relation(fields: [calendarId], references: [id], onDelete: Cascade) - calendarId String - userId String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + id String @id @default(uuid()) + title String + description String? + start DateTime + end DateTime + location String? + isAllDay Boolean @default(false) + calendar Calendar @relation(fields: [calendarId], references: [id], onDelete: Cascade) + calendarId String + userId String + // External calendar sync fields + externalEventId String? // UID from iCalendar or Microsoft ID for reliable matching + externalEventUrl String? // Link to external calendar event (optional) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([calendarId]) @@index([userId]) + @@index([externalEventId]) // Index for fast matching during sync } model MailCredentials {