fix(tasks): work around Stalwart not returning Task objects via CalendarEvent/query

Stalwart's CalendarEvent/query ignores Task-type objects and does not
support the 'types' filter (returns unsupportedFilter error).  This
caused tasks  both locally created and from external clients like
Thunderbird  to disappear on reload.

Root causes:
- CalendarEvent/query only returns @type:'Event' objects on Stalwart,
  so tasks were invisible to the query endpoint.
- CALENDAR_EVENT_PROPERTIES lacked Task-specific fields (due, progress,
  progressUpdated, priority), causing garbled data when tasks were
  fetched with Event properties (e.g. utcStart:'32548-12-04T15:30:07Z').

Changes:
- Add CALENDAR_TASK_PROPERTIES with Task-specific fields (due, progress,
  progressUpdated, priority).
- Rewrite getCalendarTasks() to first try CalendarEvent/query with
  types:['Task'] filter, then fall back to CalendarEvent/get ids:null
  which returns all calendar objects regardless of @type per JMAP spec.
- Rewrite createCalendarTask() to fetch back created tasks using
  CALENDAR_TASK_PROPERTIES instead of piggybacking on createCalendarEvent.
- Add comprehensive debug logging throughout the task fetch/create flow
  (TaskStore, JMAP client) visible when Debug Mode is enabled.
- Add 'types' field to CalendarEventFilter interface.
This commit is contained in:
Linus Rath
2026-03-28 14:17:54 +01:00
parent 308adf0101
commit 098127148e
3 changed files with 262 additions and 24 deletions
+13 -1
View File
@@ -1,6 +1,7 @@
import { create } from 'zustand';
import type { CalendarTask } from '@/lib/jmap/types';
import type { IJMAPClient } from '@/lib/jmap/client-interface';
import { debug } from '@/lib/debug';
export type TaskViewFilter = 'all' | 'pending' | 'completed' | 'overdue';
@@ -36,18 +37,29 @@ export const useTaskStore = create<TaskStore>((set, get) => ({
setShowCompleted: (show) => set({ showCompleted: show }),
fetchTasks: async (client, calendarIds) => {
debug.log('TaskStore/fetchTasks start', { calendarIds: calendarIds || 'all' });
set({ isLoading: true, error: null });
try {
const tasks = await client.getCalendarTasks(calendarIds);
debug.log('TaskStore/fetchTasks received', tasks.length, 'tasks');
tasks.forEach((t, i) => {
debug.log(`TaskStore/fetchTasks [${i}]`, {
id: t.id, uid: t.uid, '@type': t['@type'],
title: t.title, due: t.due, progress: t.progress,
showWithoutTime: t.showWithoutTime, calendarIds: t.calendarIds,
});
});
set({ tasks, isLoading: false });
} catch (error) {
console.error('Failed to fetch tasks:', error);
debug.error('TaskStore/fetchTasks failed', error);
set({ isLoading: false, error: 'Failed to fetch tasks' });
}
},
createTask: async (client, task) => {
debug.log('TaskStore/createTask', task);
const created = await client.createCalendarTask(task);
debug.log('TaskStore/createTask result', { id: created.id, uid: created.uid, title: created.title });
set({ tasks: [...get().tasks, created] });
return created;
},