fix: paginate task sync and unblock connections endpoint

- Google Tasks and Microsoft To-Do sync now paginate through all
  results instead of capping at 100 tasks per list.
- Synology calendar pruning in GET /connections is now fire-and-forget
  so a slow/unreachable NAS doesn't block the entire response.

v1.57.12

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
mARTin 2026-03-23 01:11:23 +01:00
parent a5295c5dd7
commit a009a4953a
4 changed files with 73 additions and 61 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "my-weekly-todo-list", "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", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -32,7 +32,7 @@ export async function GET(request: NextRequest) {
// Return calendar connections (without sensitive tokens) // Return calendar connections (without sensitive tokens)
// Prune deleted calendars from providers that support live listing // 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; let calendars = conn.calendars as any[] | null;
// For Apple connections, filter out VTODO/Reminders collections // For Apple connections, filter out VTODO/Reminders collections
@ -42,24 +42,23 @@ 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) { if (conn.provider === 'synology' && Array.isArray(calendars) && calendars.length > 0) {
const connId = conn.id;
const storedCalendars = calendars;
(async () => {
try { try {
const [username, password] = conn.accessToken.split(':'); const [username, password] = conn.accessToken.split(':');
const serverUrl = conn.refreshToken; const serverUrl = conn.refreshToken;
if (username && password && serverUrl) { if (username && password && serverUrl) {
const freshCalendars = await getSynologyCalendars(serverUrl, username, password); const freshCalendars = await getSynologyCalendars(serverUrl, username, password);
const freshIds = new Set(freshCalendars.map(c => c.id)); const freshIds = new Set(freshCalendars.map(c => c.id));
const storedIds = calendars.map((c: any) => c.id); const staleIds = storedCalendars.filter((c: any) => !freshIds.has(c.id)).map((c: any) => c.id);
const staleIds = storedIds.filter((id: string) => !freshIds.has(id));
if (staleIds.length > 0) { if (staleIds.length > 0) {
console.log('[CONNECTIONS] Synology stale calendars to prune:', staleIds); console.log('[CONNECTIONS] Synology stale calendars to prune:', staleIds);
} const pruned = storedCalendars.filter((c: any) => freshIds.has(c.id));
const pruned = calendars.filter((c: any) => freshIds.has(c.id));
if (pruned.length < calendars.length) {
calendars = pruned;
await prisma.calendarConnection.update({ await prisma.calendarConnection.update({
where: { id: conn.id }, where: { id: connId },
data: { calendars: pruned }, data: { calendars: pruned },
}); });
} }
@ -67,6 +66,7 @@ export async function GET(request: NextRequest) {
} catch (err) { } catch (err) {
console.error('[CONNECTIONS] Synology calendar pruning failed:', err); console.error('[CONNECTIONS] Synology calendar pruning failed:', err);
} }
})();
} }
return { return {
@ -76,7 +76,7 @@ export async function GET(request: NextRequest) {
createdAt: conn.createdAt, createdAt: conn.createdAt,
expiresAt: conn.expiresAt, expiresAt: conn.expiresAt,
}; };
})); });
return NextResponse.json({ connections }); return NextResponse.json({ connections });
} catch (error) { } catch (error) {

View File

@ -153,19 +153,22 @@ export const deleteGoogleTask = async (client: OAuth2Client, taskListId: string,
export const fetchGoogleTasksForSync = async (client: OAuth2Client, taskListId: string, updatedMin?: string): Promise<GoogleTask[]> => { export const fetchGoogleTasksForSync = async (client: OAuth2Client, taskListId: string, updatedMin?: string): Promise<GoogleTask[]> => {
const service = google.tasks({ version: 'v1', auth: client }); const service = google.tasks({ version: 'v1', auth: client });
try { try {
const allTasks: GoogleTask[] = [];
let pageToken: string | undefined;
do {
const params: any = { const params: any = {
tasklist: taskListId, tasklist: taskListId,
showCompleted: true, showCompleted: true,
showHidden: true, showHidden: true,
maxResults: 100, maxResults: 100,
}; };
if (updatedMin) { if (updatedMin) params.updatedMin = updatedMin;
params.updatedMin = updatedMin; if (pageToken) params.pageToken = pageToken;
}
const response = await service.tasks.list(params); const response = await service.tasks.list(params);
return (response.data.items || []).map(item => ({ const items = (response.data.items || []).map(item => ({
id: item.id!, id: item.id!,
title: item.title!, title: item.title!,
notes: item.notes || undefined, notes: item.notes || undefined,
@ -174,6 +177,11 @@ export const fetchGoogleTasksForSync = async (client: OAuth2Client, taskListId:
updated: item.updated!, updated: item.updated!,
parent: (item as any).parent || undefined, parent: (item as any).parent || undefined,
})); }));
allTasks.push(...items);
pageToken = response.data.nextPageToken || undefined;
} while (pageToken);
return allTasks;
} catch (error: any) { } catch (error: any) {
// On quota exceeded (429), return empty array instead of crashing // On quota exceeded (429), return empty array instead of crashing
if (error?.code === 429 || error?.status === 429) { if (error?.code === 429 || error?.status === 429) {

View File

@ -127,22 +127,22 @@ export const fetchMsTodoTasksForSync = async (
listId: string, listId: string,
modifiedSince?: string modifiedSince?: string
): Promise<MicrosoftTodoTask[]> => { ): Promise<MicrosoftTodoTask[]> => {
const params = new URLSearchParams({ const allTasks: MicrosoftTodoTask[] = [];
'$top': '100' let url: string | null = (() => {
}); const params = new URLSearchParams({ '$top': '100' });
if (modifiedSince) { if (modifiedSince) {
params.set('$filter', `lastModifiedDateTime gt ${modifiedSince}`); params.set('$filter', `lastModifiedDateTime gt ${modifiedSince}`);
} }
return `${GRAPH_ENDPOINT}/me/todo/lists/${encodeURIComponent(listId)}/tasks?${params.toString()}`;
})();
const response = await fetch( while (url) {
`${GRAPH_ENDPOINT}/me/todo/lists/${encodeURIComponent(listId)}/tasks?${params.toString()}`, const response = await fetch(url, {
{
headers: { headers: {
'Authorization': `Bearer ${accessToken}`, 'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
} });
);
if (!response.ok) { if (!response.ok) {
const err = await response.text(); const err = await response.text();
@ -151,7 +151,11 @@ export const fetchMsTodoTasksForSync = async (
} }
const data = await response.json(); const data = await response.json();
return (data.value || []) as MicrosoftTodoTask[]; allTasks.push(...((data.value || []) as MicrosoftTodoTask[]));
url = data['@odata.nextLink'] || null;
}
return allTasks;
}; };
/** /**