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

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

Copy link
Copy Markdown
Owner

Thanks again, @schobernoise , both for the implementation and for being open to the companion-plugin approach.

I owe you a clarification following my earlier comment about aiming to support this in v5. Having thought more carefully about the ongoing maintenance, I realise I was too optimistic about what I could take on. I don’t use CalDAV myself, and I wouldn’t be able to give a bidirectional integration the regular testing and attention it needs. I’m sorry for having raised that expectation.

My preference remains for this to live in a companion plugin, but it would need someone other than me to own and maintain it. TaskNotes already has a runtime API for companion plugins, which should cover much of what this needs.

Would maintaining it independently be something you’d be interested in? I’d be happy to link to it from the TaskNotes documentation. I also completely understand if that’s more than you intended to take on when submitting this PR.

I’m very grateful for the work you and @slipstyle have put into this and I’m sorry that I can’t follow through in the way my earlier reply may have suggested.

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.

2 participants