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",
|
||||
"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",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@ -47,9 +47,9 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
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) {
|
||||
const refreshWork = Promise.allSettled(
|
||||
const doRefresh = () => Promise.allSettled(
|
||||
staleConnections.map(conn => {
|
||||
const rc: RefreshableConnection = {
|
||||
dbId: conn.id,
|
||||
@ -64,7 +64,23 @@ export async function POST(request: NextRequest) {
|
||||
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({
|
||||
|
||||
@ -857,7 +857,7 @@ export default function WeeklyView() {
|
||||
const workingHoursEnd = endHour;
|
||||
|
||||
// Fetch calendar events
|
||||
const fetchCalendarEvents = useCallback(async () => {
|
||||
const fetchCalendarEvents = useCallback(async (forceRefresh = false) => {
|
||||
setIsSyncing(true);
|
||||
try {
|
||||
const response = await fetch("/api/calendar/sync", {
|
||||
@ -868,6 +868,7 @@ export default function WeeklyView() {
|
||||
timeMax: new Date(
|
||||
currentWeekStart.getTime() + 7 * 24 * 60 * 60 * 1000,
|
||||
).toISOString(),
|
||||
forceRefresh,
|
||||
}),
|
||||
});
|
||||
|
||||
@ -3394,7 +3395,7 @@ export default function WeeklyView() {
|
||||
<div className="weekly-spinner" title="Syncing..."></div>
|
||||
) : (
|
||||
<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"
|
||||
title="Refresh Calendar & Tasks"
|
||||
>
|
||||
|
||||
@ -145,6 +145,8 @@ export const getUpcomingEvents = async (
|
||||
const exceptionDates = new Set<string>();
|
||||
|
||||
// First, process exceptions that fall in our range
|
||||
const isAllDayRecurring = event.startDate.isDate === true;
|
||||
|
||||
exceptionVevents.forEach((exVevent: any) => {
|
||||
const exEvent = new ICAL.Event(exVevent);
|
||||
const recId = exVevent.getFirstPropertyValue('recurrence-id');
|
||||
@ -154,13 +156,14 @@ export const getUpcomingEvents = async (
|
||||
|
||||
const exStart = exEvent.startDate.toJSDate();
|
||||
const exEnd = exEvent.endDate.toJSDate();
|
||||
const exIsAllDay = exEvent.startDate.isDate === true;
|
||||
|
||||
if (exEnd.getTime() >= minTime && exStart.getTime() <= maxTime) {
|
||||
parsedEvents.push({
|
||||
id: `${exEvent.uid}-${exStart.toISOString()}`,
|
||||
title: exEvent.summary || 'Untitled Event',
|
||||
startDate: exStart.toISOString(),
|
||||
endDate: exEnd.toISOString(),
|
||||
startDate: exIsAllDay ? exStart.toISOString().slice(0, 10) : exStart.toISOString(),
|
||||
endDate: exIsAllDay ? exEnd.toISOString().slice(0, 10) : exEnd.toISOString(),
|
||||
description: exEvent.description,
|
||||
location: exEvent.location
|
||||
});
|
||||
@ -191,8 +194,8 @@ export const getUpcomingEvents = async (
|
||||
parsedEvents.push({
|
||||
id: `${event.uid}-${occStart.toISOString()}`,
|
||||
title: event.summary || 'Untitled Event',
|
||||
startDate: occStart.toISOString(),
|
||||
endDate: occEnd.toISOString(),
|
||||
startDate: isAllDayRecurring ? occStart.toISOString().slice(0, 10) : occStart.toISOString(),
|
||||
endDate: isAllDayRecurring ? occEnd.toISOString().slice(0, 10) : occEnd.toISOString(),
|
||||
description: event.description,
|
||||
location: event.location
|
||||
});
|
||||
@ -204,14 +207,15 @@ export const getUpcomingEvents = async (
|
||||
// Simple non-recurring event
|
||||
const start = event.startDate.toJSDate();
|
||||
const end = event.endDate.toJSDate();
|
||||
const isAllDay = event.startDate.isDate === true;
|
||||
|
||||
if (end.getTime() < minTime || start.getTime() > maxTime) return;
|
||||
|
||||
parsedEvents.push({
|
||||
id: event.uid || eventObj.url,
|
||||
title: event.summary || 'Untitled Event',
|
||||
startDate: start.toISOString(),
|
||||
endDate: end.toISOString(),
|
||||
startDate: isAllDay ? start.toISOString().slice(0, 10) : start.toISOString(),
|
||||
endDate: isAllDay ? end.toISOString().slice(0, 10) : end.toISOString(),
|
||||
description: event.description,
|
||||
location: event.location
|
||||
});
|
||||
|
||||
@ -301,24 +301,28 @@ export const getCalendarEvents = async (
|
||||
|
||||
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,
|
||||
title: event.title,
|
||||
description: event.description,
|
||||
start: {
|
||||
dateTime: event.startDate,
|
||||
date: undefined // Apple usually returns date-time
|
||||
dateTime: startIsAllDay ? undefined : event.startDate,
|
||||
date: startIsAllDay ? event.startDate : undefined,
|
||||
},
|
||||
end: {
|
||||
dateTime: event.endDate,
|
||||
date: undefined
|
||||
dateTime: startIsAllDay ? undefined : event.endDate,
|
||||
date: startIsAllDay ? event.endDate : undefined,
|
||||
},
|
||||
location: event.location,
|
||||
source: 'apple' as const,
|
||||
calendarId,
|
||||
calendarTitle: calendars.find(c => c.id === calendarId)?.title || 'Apple Calendar',
|
||||
backgroundColor: calendars.find(c => c.id === calendarId)?.color || '#FF3B30'
|
||||
})));
|
||||
};}));
|
||||
}
|
||||
} else if (connection.provider === 'outlook') {
|
||||
console.log('[CALENDAR] Processing Outlook connection:', connection.id);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user