Completing a task in a CalDAV client wrote through to the markdown
correctly, then the client silently un-completed it moments later.
`to_ics` stamped DTSTAMP and LAST-MODIFIED with the wall clock on every
serialization, and Radicale derives an item's ETag by hashing that
serialization. So an unchanged task minted a fresh ETag on every read, at
one-second granularity. A client that read the task, waited for the user
to tick the checkbox, then PUT with `If-Match: <the etag it holds>` always
got 412 Precondition Failed — and resolved that apparent conflict by
re-downloading the server copy, discarding the completion.
Persist a per-task `last_modified` in the sidecar instead, stamped only
when the task's content or status actually changes, and serve DTSTAMP,
LAST-MODIFIED and the Item's own last_modified from it. The ETag is now
stable while the task is, and changes exactly when the task does.
Schema goes to v2, migrated in place with ALTER TABLE: rebuilding the
database would regenerate every UID and replace clients' whole task list.
Also fixes a plain GET of an item returning 500 — Radicale asserts on
`Item.last_modified`, which was never passed. Only REPORT was covered by
tests, so nothing caught it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>