fix: detect iCloud all-day events and bypass cache on manual refresh
- Apple calendar parser now checks ICAL.js isDate flag and emits date-only strings (YYYY-MM-DD) for all-day events instead of full ISO datetime — fixes all-day detection in the frontend - Calendar events mapper routes date-only Apple events to the date field instead of always using dateTime - Manual refresh button now sends forceRefresh=true which makes the sync endpoint wait for live data instead of returning stale cache - Automatic background syncs still use fire-and-forget for performance v1.2.2 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1f202eb8c9
commit
866f514f10
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "my-weekly-todo-list",
|
"name": "my-weekly-todo-list",
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"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": {
|
||||||
|
|||||||
@ -47,9 +47,9 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
const staleConnections = staleChecks.filter(c => c.stale).map(c => c.conn);
|
const staleConnections = staleChecks.filter(c => c.stale).map(c => c.conn);
|
||||||
|
|
||||||
// BACKGROUND REFRESH: fire-and-forget for stale connections
|
// Refresh stale connections
|
||||||
if (staleConnections.length > 0) {
|
if (staleConnections.length > 0) {
|
||||||
const refreshWork = Promise.allSettled(
|
const doRefresh = () => Promise.allSettled(
|
||||||
staleConnections.map(conn => {
|
staleConnections.map(conn => {
|
||||||
const rc: RefreshableConnection = {
|
const rc: RefreshableConnection = {
|
||||||
dbId: conn.id,
|
dbId: conn.id,
|
||||||
@ -64,7 +64,23 @@ export async function POST(request: NextRequest) {
|
|||||||
return refreshConnectionCache(rc, timeMinDate, timeMaxDate);
|
return refreshConnectionCache(rc, timeMinDate, timeMaxDate);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
refreshWork.catch(e => console.error('[CACHE] Background refresh error:', e));
|
|
||||||
|
if (forceRefresh) {
|
||||||
|
// BLOCKING: wait for fresh data when user explicitly requests refresh
|
||||||
|
await doRefresh();
|
||||||
|
const freshEvents = await readCachedEvents(user.id, timeMinDate, timeMaxDate);
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
events: freshEvents,
|
||||||
|
count: freshEvents.length,
|
||||||
|
fromCache: false,
|
||||||
|
staleConnectionCount: 0,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// BACKGROUND: fire-and-forget for automatic refresh
|
||||||
|
const refreshWork = doRefresh();
|
||||||
|
refreshWork.catch(e => console.error('[CACHE] Background refresh error:', e));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
|
|||||||
@ -857,7 +857,7 @@ export default function WeeklyView() {
|
|||||||
const workingHoursEnd = endHour;
|
const workingHoursEnd = endHour;
|
||||||
|
|
||||||
// Fetch calendar events
|
// Fetch calendar events
|
||||||
const fetchCalendarEvents = useCallback(async () => {
|
const fetchCalendarEvents = useCallback(async (forceRefresh = false) => {
|
||||||
setIsSyncing(true);
|
setIsSyncing(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/calendar/sync", {
|
const response = await fetch("/api/calendar/sync", {
|
||||||
@ -868,6 +868,7 @@ export default function WeeklyView() {
|
|||||||
timeMax: new Date(
|
timeMax: new Date(
|
||||||
currentWeekStart.getTime() + 7 * 24 * 60 * 60 * 1000,
|
currentWeekStart.getTime() + 7 * 24 * 60 * 60 * 1000,
|
||||||
).toISOString(),
|
).toISOString(),
|
||||||
|
forceRefresh,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -3394,7 +3395,7 @@ export default function WeeklyView() {
|
|||||||
<div className="weekly-spinner" title="Syncing..."></div>
|
<div className="weekly-spinner" title="Syncing..."></div>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => { fetchCalendarEvents(); fetchTasks(); }}
|
onClick={() => { fetchCalendarEvents(true); fetchTasks(); }}
|
||||||
className="p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-all text-gray-400 hover:text-gray-600 opacity-0 group-hover:opacity-100"
|
className="p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-all text-gray-400 hover:text-gray-600 opacity-0 group-hover:opacity-100"
|
||||||
title="Refresh Calendar & Tasks"
|
title="Refresh Calendar & Tasks"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -145,6 +145,8 @@ export const getUpcomingEvents = async (
|
|||||||
const exceptionDates = new Set<string>();
|
const exceptionDates = new Set<string>();
|
||||||
|
|
||||||
// First, process exceptions that fall in our range
|
// First, process exceptions that fall in our range
|
||||||
|
const isAllDayRecurring = event.startDate.isDate === true;
|
||||||
|
|
||||||
exceptionVevents.forEach((exVevent: any) => {
|
exceptionVevents.forEach((exVevent: any) => {
|
||||||
const exEvent = new ICAL.Event(exVevent);
|
const exEvent = new ICAL.Event(exVevent);
|
||||||
const recId = exVevent.getFirstPropertyValue('recurrence-id');
|
const recId = exVevent.getFirstPropertyValue('recurrence-id');
|
||||||
@ -154,13 +156,14 @@ export const getUpcomingEvents = async (
|
|||||||
|
|
||||||
const exStart = exEvent.startDate.toJSDate();
|
const exStart = exEvent.startDate.toJSDate();
|
||||||
const exEnd = exEvent.endDate.toJSDate();
|
const exEnd = exEvent.endDate.toJSDate();
|
||||||
|
const exIsAllDay = exEvent.startDate.isDate === true;
|
||||||
|
|
||||||
if (exEnd.getTime() >= minTime && exStart.getTime() <= maxTime) {
|
if (exEnd.getTime() >= minTime && exStart.getTime() <= maxTime) {
|
||||||
parsedEvents.push({
|
parsedEvents.push({
|
||||||
id: `${exEvent.uid}-${exStart.toISOString()}`,
|
id: `${exEvent.uid}-${exStart.toISOString()}`,
|
||||||
title: exEvent.summary || 'Untitled Event',
|
title: exEvent.summary || 'Untitled Event',
|
||||||
startDate: exStart.toISOString(),
|
startDate: exIsAllDay ? exStart.toISOString().slice(0, 10) : exStart.toISOString(),
|
||||||
endDate: exEnd.toISOString(),
|
endDate: exIsAllDay ? exEnd.toISOString().slice(0, 10) : exEnd.toISOString(),
|
||||||
description: exEvent.description,
|
description: exEvent.description,
|
||||||
location: exEvent.location
|
location: exEvent.location
|
||||||
});
|
});
|
||||||
@ -191,8 +194,8 @@ export const getUpcomingEvents = async (
|
|||||||
parsedEvents.push({
|
parsedEvents.push({
|
||||||
id: `${event.uid}-${occStart.toISOString()}`,
|
id: `${event.uid}-${occStart.toISOString()}`,
|
||||||
title: event.summary || 'Untitled Event',
|
title: event.summary || 'Untitled Event',
|
||||||
startDate: occStart.toISOString(),
|
startDate: isAllDayRecurring ? occStart.toISOString().slice(0, 10) : occStart.toISOString(),
|
||||||
endDate: occEnd.toISOString(),
|
endDate: isAllDayRecurring ? occEnd.toISOString().slice(0, 10) : occEnd.toISOString(),
|
||||||
description: event.description,
|
description: event.description,
|
||||||
location: event.location
|
location: event.location
|
||||||
});
|
});
|
||||||
@ -204,14 +207,15 @@ export const getUpcomingEvents = async (
|
|||||||
// Simple non-recurring event
|
// Simple non-recurring event
|
||||||
const start = event.startDate.toJSDate();
|
const start = event.startDate.toJSDate();
|
||||||
const end = event.endDate.toJSDate();
|
const end = event.endDate.toJSDate();
|
||||||
|
const isAllDay = event.startDate.isDate === true;
|
||||||
|
|
||||||
if (end.getTime() < minTime || start.getTime() > maxTime) return;
|
if (end.getTime() < minTime || start.getTime() > maxTime) return;
|
||||||
|
|
||||||
parsedEvents.push({
|
parsedEvents.push({
|
||||||
id: event.uid || eventObj.url,
|
id: event.uid || eventObj.url,
|
||||||
title: event.summary || 'Untitled Event',
|
title: event.summary || 'Untitled Event',
|
||||||
startDate: start.toISOString(),
|
startDate: isAllDay ? start.toISOString().slice(0, 10) : start.toISOString(),
|
||||||
endDate: end.toISOString(),
|
endDate: isAllDay ? end.toISOString().slice(0, 10) : end.toISOString(),
|
||||||
description: event.description,
|
description: event.description,
|
||||||
location: event.location
|
location: event.location
|
||||||
});
|
});
|
||||||
|
|||||||
@ -301,24 +301,28 @@ export const getCalendarEvents = async (
|
|||||||
|
|
||||||
console.log(`[CALENDAR] Fetched ${calendarEvents.length} events from calendar ${calendarId}`);
|
console.log(`[CALENDAR] Fetched ${calendarEvents.length} events from calendar ${calendarId}`);
|
||||||
|
|
||||||
events = events.concat(calendarEvents.map((event: any) => ({
|
events = events.concat(calendarEvents.map((event: any) => {
|
||||||
|
// Date-only strings (YYYY-MM-DD) indicate all-day events
|
||||||
|
const isDateOnly = (s: string) => s && !s.includes('T');
|
||||||
|
const startIsAllDay = isDateOnly(event.startDate);
|
||||||
|
return {
|
||||||
id: event.id,
|
id: event.id,
|
||||||
title: event.title,
|
title: event.title,
|
||||||
description: event.description,
|
description: event.description,
|
||||||
start: {
|
start: {
|
||||||
dateTime: event.startDate,
|
dateTime: startIsAllDay ? undefined : event.startDate,
|
||||||
date: undefined // Apple usually returns date-time
|
date: startIsAllDay ? event.startDate : undefined,
|
||||||
},
|
},
|
||||||
end: {
|
end: {
|
||||||
dateTime: event.endDate,
|
dateTime: startIsAllDay ? undefined : event.endDate,
|
||||||
date: undefined
|
date: startIsAllDay ? event.endDate : undefined,
|
||||||
},
|
},
|
||||||
location: event.location,
|
location: event.location,
|
||||||
source: 'apple' as const,
|
source: 'apple' as const,
|
||||||
calendarId,
|
calendarId,
|
||||||
calendarTitle: calendars.find(c => c.id === calendarId)?.title || 'Apple Calendar',
|
calendarTitle: calendars.find(c => c.id === calendarId)?.title || 'Apple Calendar',
|
||||||
backgroundColor: calendars.find(c => c.id === calendarId)?.color || '#FF3B30'
|
backgroundColor: calendars.find(c => c.id === calendarId)?.color || '#FF3B30'
|
||||||
})));
|
};}));
|
||||||
}
|
}
|
||||||
} else if (connection.provider === 'outlook') {
|
} else if (connection.provider === 'outlook') {
|
||||||
console.log('[CALENDAR] Processing Outlook connection:', connection.id);
|
console.log('[CALENDAR] Processing Outlook connection:', connection.id);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user