Skip to content

Add CalDAV sync (v0.3.0)#102

Open
atayozcan wants to merge 5 commits into
cosmic-utils:mainfrom
atayozcan:feat/caldav-sync
Open

Add CalDAV sync (v0.3.0)#102
atayozcan wants to merge 5 commits into
cosmic-utils:mainfrom
atayozcan:feat/caldav-sync

Conversation

@atayozcan
Copy link
Copy Markdown

@atayozcan atayozcan commented Apr 29, 2026

Adds two-way CalDAV sync (Nextcloud, Radicale, SOGo, Fastmail, Apple iCloud, …) and a batch of related polish.

Refs #6, #92.

Highlights

  • CalDAV sync, with auto-discovery of VTODO calendars, push-on-edit, and a 60-second background sync loop.
  • Account credentials in the system keyring (Secret Service / cosmic-keyring), never on disk.
  • Account settings panel with a status row, helper text, last-synced relative timestamp, and a destructive Sign-out button.
  • Sync triggers everywhere: header bar icon, View menu, per-list right-click menu.
  • Due-date badge on each task row ("Today" / "Tomorrow" / weekday / YYYY-MM-DD).
  • Sort by due date; completed tasks always sink to the bottom.

Bug fixes uncovered while testing

  • Pulled VTODOs now appear immediately in the active list (SetList short-circuited on unchanged id; new Message::ReloadTasks).
  • Date dialog Complete handler routed details::update directly and dropped its outputs — now goes through Message::Details(..) so the in-memory task and sidebar refresh.
  • invalid SecondaryMap key used panic when a slotmap was rebuilt under in-flight messages — all hot-path indexing switched to .get() with bail-out.
  • Date picker stored UTC midnight; in negative-offset timezones that lands on the previous day. Now stores local-midnight and emits VALUE=DATE for all-day RFC encoding.
  • Rename / Set-Icon dialogs targeted the active list instead of the entity passed by the nav context menu.
  • CalDAV calendar URLs without a trailing slash had Url::join("uid.ics") overwrite the last segment.

Interop / RFC compliance

  • Uses icalendar::Todo::get_due() so all RFC 5545 forms (DATE, DATE-TIME UTC / floating / TZID) are accepted.
  • Textual fallback parser also accepts ISO-8601 extended forms (with separators, with offset).
  • Always emits DTSTAMP — some servers refuse VTODOs without one.
  • All-day tasks emit DUE;VALUE=DATE:… instead of DUE:…T000000Z.

Removed

  • sync_password field on TasksConfig (passwords are in the keyring now).
  • Dead sqlx dependency and Error::Sqlx variant — meaningful build-graph reduction.
  • caldav:URL description marker — replaced by List::remote_url; legacy lists migrate on first sync.
  • unsafe impl Send for List — unnecessary, PathBuf is already Send.

Release housekeeping

  • 0.3.0 metainfo entry; flatpak finish-args gain --share=network and --talk-name=org.freedesktop.secrets; <internet> flipped from offline-only to always.
  • README.md gets a CalDAV section; new CHANGELOG.md (Keep-a-Changelog).
  • About dialog reads version from CARGO_PKG_VERSION.

Tests

12 unit tests cover legacy-marker parsing, remote_url precedence, marker stripping, ISO-8601 (UTC, offset, extended), garbage rejection, and the all-day VALUE=DATE round-trip.

Notes

  • Flatpak generated-sources.json was deliberately not regenerated in this PR — happy to add a follow-up commit if you'd prefer to ship that here.
  • Google Tasks / Microsoft To Do aren't covered (no CalDAV); only what providers expose via RFC 4791.

Adds a Sync section to settings with server URL, username, password,
Test Connection and Sync Now actions. New sync module implements a
minimal CalDAV client (PROPFIND principal/home, REPORT VTODOs, PUT)
and a sync engine that pulls remote VTODO calendars as lists and
syncs tasks both ways using last-modified.

Tested against the build; runtime sync needs a CalDAV server (e.g.
Stalwart) to validate end-to-end.
Stalwart (and others) return <calendar-data> as CDATA so the iCal
payload survives XML escaping. quick-xml fires that as Event::CData,
which the multistatus parser ignored, causing fetch_todos to return
zero items and sync to appear no-op.

Listen for CData and accumulate text chunks across events.
- LocalStorage::update_task now bumps last_modified_date_time so the
  sync engine sees local edits as newer than remote.
- Add LocalStorage::replace_task that preserves LMD; sync engine uses
  it on pull to avoid ping-pong.
- Pages emit Output::Mutated on save-class events (add/complete/
  delete/title submit/expand/sub-task ops; details: title/favorite/
  priority/due-date). Keystroke-grade writes (TitleUpdate, Editor)
  are intentionally skipped to avoid spam — periodic sync picks them
  up.
- App handles Mutated by dispatching SyncNow if configured and not
  already syncing.
- Add 60s subscription emitting SyncTick to drive periodic sync.
Three fixes for CalDAV sync.

- parse_ical_datetime: chrono's DateTime::parse_from_str rejects a
  literal 'Z' as a timezone specifier, so VTODO timestamps like
  20260405T170617Z always failed to parse and fell back to Utc::now().
  This made every local task's last_modified equal to the most recent
  pull time, which then equaled (or trailed) the remote's reparsed
  "now" in the push comparison, so push never fired. Strip trailing Z
  and parse via NaiveDateTime in UTC. Tests cover Zulu / floating /
  date-only forms.
- Nav model duplicates after sync: PopulateLists appended without
  clearing the segmented_button model, so each sync re-added every
  list. Clear the model first, then restore the previously active
  list by id.
- Password storage: move CalDAV password from cosmic-config (plaintext
  on disk) to libsecret via the keyring crate. Existing config-stored
  passwords migrate into the keyring on first launch and are cleared
  from the config file. Username/server URL stay in cosmic-config.
- Surface PUT failures in the Sync status line (added `failed` count)
  and log response bodies on non-2xx PUTs.
Account / keyring
- Move CalDAV password to the system keyring (Secret Service /
  cosmic-keyring); drop the legacy plaintext-password migration and
  the now-unused sync_password field on TasksConfig.
- Replace the description-embedded "caldav:URL" marker with a proper
  List::remote_url field; legacy lists migrate on first sync.
- Account settings panel gains a status row, helper text under each
  input, last-synced relative timestamp, and a destructive Sign-out
  button that wipes config and keyring entry.

Sync triggers
- Sync icon in the header bar (configured-only, disables while
  running).
- "Sync now" entries in the View menu and per-list right-click menu.

UI polish
- Due-date badge on every task row ("Today" / "Tomorrow" /
  "Yesterday" / weekday / YYYY-MM-DD), localized.
- Sort by due date (Earliest/Latest); completed tasks always sink to
  the bottom regardless of sort.

Bug fixes
- Pulled VTODOs now appear immediately in the active list. SetList
  short-circuited when the list id was unchanged, so post-sync the
  view stayed stale until the user reselected. New
  Message::ReloadTasks is dispatched after every successful sync.
- Date dialog Complete handler called details::update directly and
  dropped RefreshTask/Mutated; routed through Message::Details(..) so
  the in-memory task and sidebar refresh and a sync is triggered.
- "invalid SecondaryMap key used" panic: ReloadTasks rebuilds the
  slotmap, so message handlers could arrive with stale DefaultKeys.
  All hot-path SecondaryMap accesses now use .get() with bail-out.
- Date picker stored UTC midnight, which shifted to the previous day
  for any UTC-negative offset. Stores local-midnight (as UTC) and
  emits VALUE=DATE for all-day RFC encoding so other clients show the
  same calendar day.
- Rename / Set-Icon dialogs now correctly target the entity passed in
  from the nav context menu instead of the active list.
- CalDAV calendar URLs without a trailing slash had Url::join()
  silently replace the last segment; trailing slashes are now
  enforced at discovery.
- Removed unsafe impl Send for List (PathBuf is already Send).
- Dropped the dead sqlx dependency and Error::Sqlx variant.

iCalendar interop
- Use icalendar::Todo::get_due() so all RFC 5545 forms (DATE,
  DATE-TIME UTC / floating / TZID) are accepted; textual fallback
  parser also accepts ISO-8601 extended forms (with separators and
  with offset).
- Always emit DTSTAMP, some servers refuse VTODOs without it.
- Use Todo::completed() for COMPLETED so it's a proper UTC date-time.

Release housekeeping
- 0.3.0 metainfo entry; flatpak finish-args gain --share=network and
  --talk-name=org.freedesktop.secrets; <internet> changed from
  offline-only to always.
- README gets a CalDAV section; new CHANGELOG.md
  (Keep-a-Changelog).
- Reorganized .gitignore.
- About dialog reads version from CARGO_PKG_VERSION.

Tests
- 12 unit tests cover legacy-marker parsing, remote_url precedence,
  marker stripping, ISO-8601 (UTC, offset, extended), garbage
  rejection, and the all-day VALUE=DATE round-trip.
@atayozcan atayozcan changed the title Feat/caldav sync Add CalDAV sync (v0.3.0) Apr 29, 2026
@edfloreshz
Copy link
Copy Markdown
Member

Thanks for submitting this, could you rebase this branch?

@atayozcan
Copy link
Copy Markdown
Author

Started the rebase and hit a wall — the upstream feat: new store system commit isn't a content change, it's a structural rewrite. On the first of 5 commits alone, ~1050 of ~1300 lines of src/app.rs ended up inside conflict markers, and most of what this PR depends on (src/app/actions.rs, src/app/dialog.rs, src/app/menu.rs, the entire src/storage/ module, chrono) has been deleted, moved into src/app/{core,dialogs,navigation,ui}/, replaced by src/services/store.rs, or swapped for jiff. Subtask handling was also removed.

A git rebase would produce conflict markers in files that no longer exist on the new tree, so I think the right move is to re-apply this PR as a port on top of the new architecture rather than force-push a rebase. Happy to do that, but wanted to check with you first:

  1. Are you open to a fresh PR built on the new services::store API (closing this one), or would you prefer a force-pushed rebase here even if it ends up being effectively a rewrite?
  2. Any guidance on where CalDAV sync should live in the new module layout (e.g. a new src/services/caldav.rs alongside store.rs)?
  3. With subtasks removed, should the CalDAV importer flatten RELATED-TO parent links, or drop tasks that have a parent?

@edfloreshz
Copy link
Copy Markdown
Member

I'd appreciate it if you could respond directly, I understand you're using AI for this PR and I'm happy to discuss the changes, but I'd prefer to hear from a human.

  1. You can create a new PR if you need to and close this one.
  2. No.
  3. Sub tasks were not removed.

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