Add CalDAV sync (v0.3.0)#102
Conversation
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.
|
Thanks for submitting this, could you rebase this branch? |
|
Started the rebase and hit a wall — the upstream A
|
|
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.
|
Adds two-way CalDAV sync (Nextcloud, Radicale, SOGo, Fastmail, Apple iCloud, …) and a batch of related polish.
Refs #6, #92.
Highlights
VTODOcalendars, push-on-edit, and a 60-second background sync loop.YYYY-MM-DD).Bug fixes uncovered while testing
SetListshort-circuited on unchanged id; newMessage::ReloadTasks).Completehandler routeddetails::updatedirectly and dropped its outputs — now goes throughMessage::Details(..)so the in-memory task and sidebar refresh.invalid SecondaryMap key usedpanic when a slotmap was rebuilt under in-flight messages — all hot-path indexing switched to.get()with bail-out.VALUE=DATEfor all-day RFC encoding.Url::join("uid.ics")overwrite the last segment.Interop / RFC compliance
icalendar::Todo::get_due()so all RFC 5545 forms (DATE,DATE-TIMEUTC / floating / TZID) are accepted.DTSTAMP— some servers refuse VTODOs without one.DUE;VALUE=DATE:…instead ofDUE:…T000000Z.Removed
sync_passwordfield onTasksConfig(passwords are in the keyring now).sqlxdependency andError::Sqlxvariant — meaningful build-graph reduction.caldav:URLdescription marker — replaced byList::remote_url; legacy lists migrate on first sync.unsafe impl Send for List— unnecessary,PathBufis alreadySend.Release housekeeping
finish-argsgain--share=networkand--talk-name=org.freedesktop.secrets;<internet>flipped fromoffline-onlytoalways.README.mdgets a CalDAV section; newCHANGELOG.md(Keep-a-Changelog).CARGO_PKG_VERSION.Tests
12 unit tests cover legacy-marker parsing,
remote_urlprecedence, marker stripping, ISO-8601 (UTC, offset, extended), garbage rejection, and the all-dayVALUE=DATEround-trip.Notes
generated-sources.jsonwas deliberately not regenerated in this PR — happy to add a follow-up commit if you'd prefer to ship that here.