Agenda refactor
This commit is contained in:
parent
d77bbf3c16
commit
89eaadc793
728
CALENDAR_SYNC_ARCHITECTURE_ANALYSIS.md
Normal file
728
CALENDAR_SYNC_ARCHITECTURE_ANALYSIS.md
Normal file
@ -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
|
||||||
680
PRODUCTION_VIABILITY_ASSESSMENT.md
Normal file
680
PRODUCTION_VIABILITY_ASSESSMENT.md
Normal file
@ -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
|
||||||
@ -398,10 +398,13 @@ export async function syncInfomaniakCalendar(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create a map of existing events by external UID
|
// Create a map of existing events by externalEventId (UID) for fast lookup
|
||||||
const existingEventsMap = new Map<string, typeof existingEvents[0]>();
|
const existingEventsByExternalId = new Map<string, typeof existingEvents[0]>();
|
||||||
// Store events that have external UID in metadata (we'll need to add this field)
|
for (const event of existingEvents) {
|
||||||
// For now, we'll match by title and date
|
if (event.externalEventId) {
|
||||||
|
existingEventsByExternalId.set(event.externalEventId, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let created = 0;
|
let created = 0;
|
||||||
let updated = 0;
|
let updated = 0;
|
||||||
@ -409,12 +412,24 @@ export async function syncInfomaniakCalendar(
|
|||||||
|
|
||||||
// Sync events: create or update
|
// Sync events: create or update
|
||||||
for (const caldavEvent of caldavEvents) {
|
for (const caldavEvent of caldavEvents) {
|
||||||
// Try to find existing event by matching title and start date
|
// Priority 1: Match by externalEventId (UID) - most reliable
|
||||||
const existingEvent = existingEvents.find(
|
let existingEvent = caldavEvent.uid
|
||||||
(e) =>
|
? existingEventsByExternalId.get(caldavEvent.uid)
|
||||||
e.title === caldavEvent.summary &&
|
: undefined;
|
||||||
Math.abs(new Date(e.start).getTime() - caldavEvent.start.getTime()) < 60000 // Within 1 minute
|
|
||||||
);
|
// 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 = {
|
const eventData = {
|
||||||
title: caldavEvent.summary,
|
title: caldavEvent.summary,
|
||||||
@ -425,6 +440,7 @@ export async function syncInfomaniakCalendar(
|
|||||||
isAllDay: caldavEvent.allDay,
|
isAllDay: caldavEvent.allDay,
|
||||||
calendarId: syncConfig.calendarId,
|
calendarId: syncConfig.calendarId,
|
||||||
userId: syncConfig.calendar.userId,
|
userId: syncConfig.calendar.userId,
|
||||||
|
externalEventId: caldavEvent.uid, // Store UID for reliable matching
|
||||||
};
|
};
|
||||||
|
|
||||||
if (existingEvent) {
|
if (existingEvent) {
|
||||||
|
|||||||
@ -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<string, typeof existingEvents[0]>();
|
||||||
|
for (const event of existingEvents) {
|
||||||
|
if (event.externalEventId) {
|
||||||
|
existingEventsByExternalId.set(event.externalEventId, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let created = 0;
|
let created = 0;
|
||||||
let updated = 0;
|
let updated = 0;
|
||||||
let deleted = 0;
|
let deleted = 0;
|
||||||
@ -449,40 +457,56 @@ export async function syncMicrosoftCalendar(
|
|||||||
newEventsCount: caldavEvents.length,
|
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
|
// Sync events: create or update
|
||||||
for (const caldavEvent of caldavEvents) {
|
for (const caldavEvent of caldavEvents) {
|
||||||
// Store Microsoft ID in description with a special prefix for matching
|
|
||||||
const microsoftId = caldavEvent.uid;
|
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)
|
// Priority 1: Match by externalEventId (Microsoft ID) - most reliable
|
||||||
let existingEvent = existingEvents.find((e) => {
|
let existingEvent = microsoftId
|
||||||
if (e.description && e.description.includes(`[MS_ID:${microsoftId}]`)) {
|
? existingEventsByExternalId.get(microsoftId)
|
||||||
return true;
|
: undefined;
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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) {
|
if (!existingEvent) {
|
||||||
existingEvent = existingEvents.find(
|
existingEvent = existingEvents.find(
|
||||||
(e) =>
|
(e) =>
|
||||||
|
!e.externalEventId && // Only match events that don't have externalEventId yet
|
||||||
e.title === caldavEvent.summary &&
|
e.title === caldavEvent.summary &&
|
||||||
Math.abs(new Date(e.start).getTime() - caldavEvent.start.getTime()) < 60000 // Within 1 minute
|
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 = {
|
const eventData = {
|
||||||
title: caldavEvent.summary,
|
title: caldavEvent.summary,
|
||||||
description: descriptionWithId,
|
description: cleanedDescription, // Clean description without [MS_ID:xxx] prefix
|
||||||
start: caldavEvent.start,
|
start: caldavEvent.start,
|
||||||
end: caldavEvent.end,
|
end: caldavEvent.end,
|
||||||
location: caldavEvent.location || null,
|
location: caldavEvent.location || null,
|
||||||
isAllDay: caldavEvent.allDay,
|
isAllDay: caldavEvent.allDay,
|
||||||
calendarId: syncConfig.calendarId,
|
calendarId: syncConfig.calendarId,
|
||||||
userId: syncConfig.calendar.userId,
|
userId: syncConfig.calendar.userId,
|
||||||
|
externalEventId: microsoftId, // Store Microsoft ID for reliable matching
|
||||||
};
|
};
|
||||||
|
|
||||||
if (existingEvent) {
|
if (existingEvent) {
|
||||||
|
|||||||
@ -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");
|
||||||
@ -46,22 +46,26 @@ model Calendar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Event {
|
model Event {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
title String
|
title String
|
||||||
description String?
|
description String?
|
||||||
start DateTime
|
start DateTime
|
||||||
end DateTime
|
end DateTime
|
||||||
location String?
|
location String?
|
||||||
isAllDay Boolean @default(false)
|
isAllDay Boolean @default(false)
|
||||||
calendar Calendar @relation(fields: [calendarId], references: [id], onDelete: Cascade)
|
calendar Calendar @relation(fields: [calendarId], references: [id], onDelete: Cascade)
|
||||||
calendarId String
|
calendarId String
|
||||||
userId String
|
userId String
|
||||||
createdAt DateTime @default(now())
|
// External calendar sync fields
|
||||||
updatedAt DateTime @updatedAt
|
externalEventId String? // UID from iCalendar or Microsoft ID for reliable matching
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
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([calendarId])
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
|
@@index([externalEventId]) // Index for fast matching during sync
|
||||||
}
|
}
|
||||||
|
|
||||||
model MailCredentials {
|
model MailCredentials {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user