Skip to content

Add two-way CalDAV sync for tasks (#811) - #2280

Open
schobernoise wants to merge 1 commit into
callumalpass:mainfrom
schobernoise:caldav-vtodo-sync
Open

Add two-way CalDAV sync for tasks (#811)#2280
schobernoise wants to merge 1 commit into
callumalpass:mainfrom
schobernoise:caldav-vtodo-sync

Conversation

@schobernoise

Copy link
Copy Markdown

Keeps a vault in sync with a CalDAV task list (Nextcloud, Apple Reminders, Radicale, Baikal) in both directions, as VTODOs rather than calendar events. This is a new integration alongside the Google/Microsoft export, not an extension of it: that path is one-way and models tasks as VEVENTs.

Identity lives in each note's frontmatter (caldav_uid, caldav_href, caldav_etag, caldav_account, caldav_synced_at), so disconnecting and reconnecting converges instead of duplicating. ETags detect a concurrent edit; DTSTAMP/LAST-MODIFIED against the local change time decides it.

Most of the logic is in pure modules under src/services/caldav/, registered in run-architecture-conformance.mjs so they are held to the stricter rules. VTODOs are modelled as content lines rather than a decoded struct, so properties TaskNotes does not map -- DESCRIPTION, ATTACH, X- properties -- survive a round trip untouched.

Subtasks map to RELATED-TO;RELTYPE=PARENT. TaskNotes has no parent field: a subtask is a task whose projects field points at its parent, and both sides are many-valued, so the mapping is direct. Dependencies use the RFC 9253 temporal reltypes with GAP, which TaskDependency already matches field for field. A relation whose target has no UID yet is replayed once after the rest of the pass, and one that genuinely cannot be addressed is left out rather than guessed at.

Reminders map to VALARM. TaskNotes stamps its own alarms with X-TASKNOTES-REMINDER and rewrites only those, so an alarm added on a phone is never touched.

Polling asks for the collection ctag first and stops there when nothing has changed; otherwise a VTODO-filtered calendar-query returns only tasks, and returns all of them, which is what makes deletion detection safe. Task lists commonly share a collection with far more events -- one test list held 1142 resources and a single VTODO -- so walking the resource list would mean downloading the entire calendar to find nothing.

Discovery climbs from the configured URL up to the account root on the original scheme. /.well-known/caldav is tried last: it frequently redirects to plain http behind a reverse proxy, and requestUrl follows redirects with no way to veto a scheme downgrade.

Credentials live in Obsidian's SecretStorage, never in data.json, and are refused over anything but HTTPS except to loopback.

Claude-Session: https://claude.ai/code/session_01Q8Ni1X5i6HsZaEAueYLvZd

Keeps a vault in sync with a CalDAV task list (Nextcloud, Apple Reminders,
Radicale, Baikal) in both directions, as VTODOs rather than calendar events.
This is a new integration alongside the Google/Microsoft export, not an
extension of it: that path is one-way and models tasks as VEVENTs.

Identity lives in each note's frontmatter (caldav_uid, caldav_href,
caldav_etag, caldav_account, caldav_synced_at), so disconnecting and
reconnecting converges instead of duplicating. ETags detect a concurrent
edit; DTSTAMP/LAST-MODIFIED against the local change time decides it.

Most of the logic is in pure modules under src/services/caldav/, registered
in run-architecture-conformance.mjs so they are held to the stricter rules.
VTODOs are modelled as content lines rather than a decoded struct, so
properties TaskNotes does not map -- DESCRIPTION, ATTACH, X- properties --
survive a round trip untouched.

Subtasks map to RELATED-TO;RELTYPE=PARENT. TaskNotes has no parent field: a
subtask is a task whose projects field points at its parent, and both sides
are many-valued, so the mapping is direct. Dependencies use the RFC 9253
temporal reltypes with GAP, which TaskDependency already matches field for
field. A relation whose target has no UID yet is replayed once after the
rest of the pass, and one that genuinely cannot be addressed is left out
rather than guessed at.

Reminders map to VALARM. TaskNotes stamps its own alarms with
X-TASKNOTES-REMINDER and rewrites only those, so an alarm added on a phone
is never touched.

Polling asks for the collection ctag first and stops there when nothing has
changed; otherwise a VTODO-filtered calendar-query returns only tasks, and
returns all of them, which is what makes deletion detection safe. Task
lists commonly share a collection with far more events -- one test list
held 1142 resources and a single VTODO -- so walking the resource list
would mean downloading the entire calendar to find nothing.

Discovery climbs from the configured URL up to the account root on the
original scheme. /.well-known/caldav is tried last: it frequently redirects
to plain http behind a reverse proxy, and requestUrl follows redirects with
no way to veto a scheme downgrade.

Credentials live in Obsidian's SecretStorage, never in data.json, and are
refused over anything but HTTPS except to loopback.

Claude-Session: https://claude.ai/code/session_01Q8Ni1X5i6HsZaEAueYLvZd
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant