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:
mARTin 2026-02-24 00:26:04 +01:00
parent 1f202eb8c9
commit 866f514f10
5 changed files with 43 additions and 18 deletions

View File

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

View File

@ -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({

View File

@ -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"
>

View File

@ -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
});

View File

@ -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);