diff --git a/package.json b/package.json index 0f1615b..02a9dc3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.57.11", + "version": "1.57.12", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "main": "index.js", "scripts": { diff --git a/src/app/api/calendar/connections/route.ts b/src/app/api/calendar/connections/route.ts index 2c8c40c..0a48753 100644 --- a/src/app/api/calendar/connections/route.ts +++ b/src/app/api/calendar/connections/route.ts @@ -32,7 +32,7 @@ export async function GET(request: NextRequest) { // Return calendar connections (without sensitive tokens) // Prune deleted calendars from providers that support live listing - const connections = await Promise.all(user.calendarConnections.map(async (conn) => { + const connections = user.calendarConnections.map((conn) => { let calendars = conn.calendars as any[] | null; // For Apple connections, filter out VTODO/Reminders collections @@ -42,31 +42,31 @@ export async function GET(request: NextRequest) { ); } - // For Synology connections, prune calendars deleted on the server + // For Synology connections, fire-and-forget prune of deleted calendars if (conn.provider === 'synology' && Array.isArray(calendars) && calendars.length > 0) { - try { - const [username, password] = conn.accessToken.split(':'); - const serverUrl = conn.refreshToken; - if (username && password && serverUrl) { - const freshCalendars = await getSynologyCalendars(serverUrl, username, password); - const freshIds = new Set(freshCalendars.map(c => c.id)); - const storedIds = calendars.map((c: any) => c.id); - const staleIds = storedIds.filter((id: string) => !freshIds.has(id)); - if (staleIds.length > 0) { - console.log('[CONNECTIONS] Synology stale calendars to prune:', staleIds); - } - const pruned = calendars.filter((c: any) => freshIds.has(c.id)); - if (pruned.length < calendars.length) { - calendars = pruned; - await prisma.calendarConnection.update({ - where: { id: conn.id }, - data: { calendars: pruned }, - }); + const connId = conn.id; + const storedCalendars = calendars; + (async () => { + try { + const [username, password] = conn.accessToken.split(':'); + const serverUrl = conn.refreshToken; + if (username && password && serverUrl) { + const freshCalendars = await getSynologyCalendars(serverUrl, username, password); + const freshIds = new Set(freshCalendars.map(c => c.id)); + const staleIds = storedCalendars.filter((c: any) => !freshIds.has(c.id)).map((c: any) => c.id); + if (staleIds.length > 0) { + console.log('[CONNECTIONS] Synology stale calendars to prune:', staleIds); + const pruned = storedCalendars.filter((c: any) => freshIds.has(c.id)); + await prisma.calendarConnection.update({ + where: { id: connId }, + data: { calendars: pruned }, + }); + } } + } catch (err) { + console.error('[CONNECTIONS] Synology calendar pruning failed:', err); } - } catch (err) { - console.error('[CONNECTIONS] Synology calendar pruning failed:', err); - } + })(); } return { @@ -76,7 +76,7 @@ export async function GET(request: NextRequest) { createdAt: conn.createdAt, expiresAt: conn.expiresAt, }; - })); + }); return NextResponse.json({ connections }); } catch (error) { diff --git a/src/lib/google-tasks.ts b/src/lib/google-tasks.ts index 8059fc9..bd7d35c 100644 --- a/src/lib/google-tasks.ts +++ b/src/lib/google-tasks.ts @@ -153,27 +153,35 @@ export const deleteGoogleTask = async (client: OAuth2Client, taskListId: string, export const fetchGoogleTasksForSync = async (client: OAuth2Client, taskListId: string, updatedMin?: string): Promise => { const service = google.tasks({ version: 'v1', auth: client }); try { - const params: any = { - tasklist: taskListId, - showCompleted: true, - showHidden: true, - maxResults: 100, - }; - if (updatedMin) { - params.updatedMin = updatedMin; - } + const allTasks: GoogleTask[] = []; + let pageToken: string | undefined; - const response = await service.tasks.list(params); + do { + const params: any = { + tasklist: taskListId, + showCompleted: true, + showHidden: true, + maxResults: 100, + }; + if (updatedMin) params.updatedMin = updatedMin; + if (pageToken) params.pageToken = pageToken; - return (response.data.items || []).map(item => ({ - id: item.id!, - title: item.title!, - notes: item.notes || undefined, - status: item.status!, - due: item.due || undefined, - updated: item.updated!, - parent: (item as any).parent || undefined, - })); + const response = await service.tasks.list(params); + + const items = (response.data.items || []).map(item => ({ + id: item.id!, + title: item.title!, + notes: item.notes || undefined, + status: item.status!, + due: item.due || undefined, + updated: item.updated!, + parent: (item as any).parent || undefined, + })); + allTasks.push(...items); + pageToken = response.data.nextPageToken || undefined; + } while (pageToken); + + return allTasks; } catch (error: any) { // On quota exceeded (429), return empty array instead of crashing if (error?.code === 429 || error?.status === 429) { diff --git a/src/lib/microsoft-todo.ts b/src/lib/microsoft-todo.ts index fabeddb..0178b81 100644 --- a/src/lib/microsoft-todo.ts +++ b/src/lib/microsoft-todo.ts @@ -127,31 +127,35 @@ export const fetchMsTodoTasksForSync = async ( listId: string, modifiedSince?: string ): Promise => { - const params = new URLSearchParams({ - '$top': '100' - }); - if (modifiedSince) { - params.set('$filter', `lastModifiedDateTime gt ${modifiedSince}`); - } + const allTasks: MicrosoftTodoTask[] = []; + let url: string | null = (() => { + const params = new URLSearchParams({ '$top': '100' }); + if (modifiedSince) { + params.set('$filter', `lastModifiedDateTime gt ${modifiedSince}`); + } + return `${GRAPH_ENDPOINT}/me/todo/lists/${encodeURIComponent(listId)}/tasks?${params.toString()}`; + })(); - const response = await fetch( - `${GRAPH_ENDPOINT}/me/todo/lists/${encodeURIComponent(listId)}/tasks?${params.toString()}`, - { + while (url) { + const response = await fetch(url, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' } - } - ); + }); - if (!response.ok) { - const err = await response.text(); - console.error(`Error fetching sync tasks from list ${listId}:`, err); - throw new Error(`Failed to fetch To-Do tasks for sync: ${response.status} ${response.statusText}`); + if (!response.ok) { + const err = await response.text(); + console.error(`Error fetching sync tasks from list ${listId}:`, err); + throw new Error(`Failed to fetch To-Do tasks for sync: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + allTasks.push(...((data.value || []) as MicrosoftTodoTask[])); + url = data['@odata.nextLink'] || null; } - const data = await response.json(); - return (data.value || []) as MicrosoftTodoTask[]; + return allTasks; }; /**