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:
parent
a5295c5dd7
commit
a009a4953a
@ -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": {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user