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",
"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": {

View File

@ -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,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) {
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 storedIds = calendars.map((c: any) => c.id);
const staleIds = storedIds.filter((id: string) => !freshIds.has(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 = calendars.filter((c: any) => freshIds.has(c.id));
if (pruned.length < calendars.length) {
calendars = pruned;
const pruned = storedCalendars.filter((c: any) => freshIds.has(c.id));
await prisma.calendarConnection.update({
where: { id: conn.id },
where: { id: connId },
data: { calendars: pruned },
});
}
@ -67,6 +66,7 @@ export async function GET(request: NextRequest) {
} 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) {

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[]> => {
const service = google.tasks({ version: 'v1', auth: client });
try {
const allTasks: GoogleTask[] = [];
let pageToken: string | undefined;
do {
const params: any = {
tasklist: taskListId,
showCompleted: true,
showHidden: true,
maxResults: 100,
};
if (updatedMin) {
params.updatedMin = updatedMin;
}
if (updatedMin) params.updatedMin = updatedMin;
if (pageToken) params.pageToken = pageToken;
const response = await service.tasks.list(params);
return (response.data.items || []).map(item => ({
const items = (response.data.items || []).map(item => ({
id: item.id!,
title: item.title!,
notes: item.notes || undefined,
@ -174,6 +177,11 @@ export const fetchGoogleTasksForSync = async (client: OAuth2Client, taskListId:
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) {

View File

@ -127,22 +127,22 @@ export const fetchMsTodoTasksForSync = async (
listId: string,
modifiedSince?: string
): Promise<MicrosoftTodoTask[]> => {
const params = new URLSearchParams({
'$top': '100'
});
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();
@ -151,7 +151,11 @@ export const fetchMsTodoTasksForSync = async (
}
const data = await response.json();
return (data.value || []) as MicrosoftTodoTask[];
allTasks.push(...((data.value || []) as MicrosoftTodoTask[]));
url = data['@odata.nextLink'] || null;
}
return allTasks;
};
/**