Calendar Tab: moved recurrence exception is hidden at its original time #1333

Closed
opened 2026-10-07 06:48:19 +00:00 by kayg · 8 comments
Owner

Steps:

  1. Connect a CalDAV Calendar that contains a weekly master (DTSTART 2026-10-10 09:00Z, COUNT=3) and a detached component with RECURRENCE-ID 2026-10-17 09:00Z moved to 12:00Z.
  2. Edit the master with khal and sync the object back with an ETag-guarded PUT.
  3. Open the production Calendar range for 2026-10-17 in UTC.

Expected: the detached Event appears at 12:00, and the original 09:00 occurrence is replaced.

Actual: the provider still stores the RECURRENCE-ID component after the edit, but /api/v1/calendar/range returns only the edited master at 09:00. The moved exception is missing and the original occurrence remains visible.

Evidence: artifacts/bash-calendar/round2-caldav/external-recurrence-range.json records the retained provider component and exact production API response.

Severity: major. A valid moved recurrence exception is displayed at the wrong time and the user cannot see the Event at its actual time.

Steps: 1. Connect a CalDAV Calendar that contains a weekly master (DTSTART 2026-10-10 09:00Z, COUNT=3) and a detached component with RECURRENCE-ID 2026-10-17 09:00Z moved to 12:00Z. 2. Edit the master with khal and sync the object back with an ETag-guarded PUT. 3. Open the production Calendar range for 2026-10-17 in UTC. Expected: the detached Event appears at 12:00, and the original 09:00 occurrence is replaced. Actual: the provider still stores the RECURRENCE-ID component after the edit, but `/api/v1/calendar/range` returns only the edited master at 09:00. The moved exception is missing and the original occurrence remains visible. Evidence: `artifacts/bash-calendar/round2-caldav/external-recurrence-range.json` records the retained provider component and exact production API response. Severity: major. A valid moved recurrence exception is displayed at the wrong time and the user cannot see the Event at its actual time.
Author
Owner

Correction: I added a focused same-day RECURRENCE-ID case to the Calendar expansion helper, and the helper returned the moved override at 12:00 with the master occurrence replaced. That means the earlier production API result does not isolate a defect in recurrence::expand_event. The report remains open because the account-backed production range returned only the master while the fake provider still stored both components; the sync/cache boundary needs a fresh, non-stalled reproduction before assigning a code owner or making a change.

Correction: I added a focused same-day RECURRENCE-ID case to the Calendar expansion helper, and the helper returned the moved override at 12:00 with the master occurrence replaced. That means the earlier production API result does not isolate a defect in `recurrence::expand_event`. The report remains open because the account-backed production range returned only the master while the fake provider still stored both components; the sync/cache boundary needs a fresh, non-stalled reproduction before assigning a code owner or making a change.
Author
Owner

Starting #1333 on branch job/exmove-1333, based on d395b6e25da20d8c67b8b9fc1a593b813939c8a7 (origin/dev). The existing helper-level same-day override check passes, so I am tracing the production CalDAV sync/cache path and will add a UI-to-range regression for the reported provider update.

Starting #1333 on branch `job/exmove-1333`, based on `d395b6e25da20d8c67b8b9fc1a593b813939c8a7` (`origin/dev`). The existing helper-level same-day override check passes, so I am tracing the production CalDAV sync/cache path and will add a UI-to-range regression for the reported provider update.
Author
Owner

Starting #1333 on branch job/exmove-1333, based on d395b6e25da20d8c67b8b9fc1a593b813939c8a7 (origin/dev). The existing helper-level same-day override check passes, so I am tracing the production CalDAV sync/cache path and will add a UI-to-range regression for the reported provider update.

Starting #1333 on branch `job/exmove-1333`, based on `d395b6e25da20d8c67b8b9fc1a593b813939c8a7` (`origin/dev`). The existing helper-level same-day override check passes, so I am tracing the production CalDAV sync/cache path and will add a UI-to-range regression for the reported provider update.
Author
Owner

Reproduction on the real account-backed path: the browser connected to a local CalDAV provider, updated the master with If-Match, then posted /api/v1/calendar/refresh so sync-collection refreshed the cache. With the master and RECURRENCE-ID component in separate hrefs but sharing a UID, /api/v1/calendar/range returned both the master at 09:00 and the moved exception at 12:00. The Calendar currently expands each cached href independently, so it does not apply the detached component to the master occurrence. I am fixing the shared recurrence projection and will cover the response plus the visible Calendar item.

Reproduction on the real account-backed path: the browser connected to a local CalDAV provider, updated the master with `If-Match`, then posted `/api/v1/calendar/refresh` so `sync-collection` refreshed the cache. With the master and RECURRENCE-ID component in separate hrefs but sharing a UID, `/api/v1/calendar/range` returned both the master at 09:00 and the moved exception at 12:00. The Calendar currently expands each cached href independently, so it does not apply the detached component to the master occurrence. I am fixing the shared recurrence projection and will cover the response plus the visible Calendar item.
Author
Owner

Finding and fix for #1333: the account-backed CalDAV regression reproduced after an ETag-conditional master PUT and sync-collection refresh. The master and detached exception use separate hrefs; the range had returned both the original 09:00Z occurrence and the moved 12:00Z exception.

The shared Calendar projection now groups visible resources by calendar and UID before expansion, combines their VEVENT and VTIMEZONE component trees, and retains each component's source resource identity. A UID index supports sibling lookup. I added a focused recurrence unit test and a production-browser regression covering the real sync and Calendar UI. Calendar clippy passes; the crate test build is still running.

Finding and fix for #1333: the account-backed CalDAV regression reproduced after an ETag-conditional master PUT and sync-collection refresh. The master and detached exception use separate hrefs; the range had returned both the original 09:00Z occurrence and the moved 12:00Z exception. The shared Calendar projection now groups visible resources by calendar and UID before expansion, combines their VEVENT and VTIMEZONE component trees, and retains each component's source resource identity. A UID index supports sibling lookup. I added a focused recurrence unit test and a production-browser regression covering the real sync and Calendar UI. Calendar clippy passes; the crate test build is still running.
Author
Owner

Completed

Fixed #1333 at the shared Calendar recurrence owner. Calendar range and the Events API now load cached CalDAV resources that share a UID, combine detached components into one recurrence set, and preserve each Event's source resource identity. A moved override now replaces the old master occurrence even when its href is separate.

The query uses the (owner_id, calendar_id, uid) index. Multi-resource series parsing has a 4 MiB aggregate byte budget, charged before parsing so malformed resources cannot evade it. Work is bounded by the 5,000-resource query cap, the per-series byte cap, and the caller's occurrence limit.

The browser regression edits the master with its observed CalDAV ETag, retains the separate override, syncs, and checks the signed-in Calendar API and screen. It also checks Europe/Berlin across the autumn DST change and Asia/Kolkata. The new malformed-body regression was verified both ways: with old accounting it failed at assert!(expanded.instances.is_empty()); with the fix it passed.

UX gaps closed

  • The stale master occurrence no longer appears beside its moved detached Event. API and screen agree on one occurrence at the moved time.
  • The real Calendar screen was checked after a pointer click and after ⌘I at 390, 820 and 1440 px in light and dark themes. The browser used macOS platform emulation. The 12 actual-size screenshots are attached below.

UX gaps left: none found in this data-projection change. The screenshots use macOS platform emulation in Chromium; no separate Mac VM GUI run was needed.

Gates and browser evidence

  • cargo fmt --check: exit 0, no output.
  • cargo clippy -p calternal-plugin-calendar --all-targets -- -D warnings: Finished dev profile [unoptimized + debuginfo] target(s) in 31.36s.
  • cargo test -p calternal-plugin-calendar:
    test result: ok. 106 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 16.04s
    test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.42s
    test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.17s
    
  • cargo clippy -p calternal-server --all-targets -- -D warnings: Finished dev profile [unoptimized + debuginfo] target(s) in 3m 12s.
  • cargo test -p calternal-server: 271 passed, 1 failed, 12 ignored. The only failure was the load-sensitive Calendar write profile:
    search_reconcile_calendar_write_profile files=20000 writes=32 p50_ms=13534 p95_ms=13724 p95_budget_ms=10000
    Calendar write p95 13.724269222s exceeded 10s
    
    No test expectation was changed. Treat this as a host-load performance finding; it did not reproduce in the recurrence regression.
  • bun run check exited 0:
    perf-lint: PASS; 0 violations; 22542 scoped exceptions
    svelte-check found 0 errors and 4 warnings in 2 files
    
    The four warnings are the existing unused CSS selectors in MailView and the money page.
  • bun run test --maxWorkers=2:
     Test Files  297 passed (297)
          Tests  2120 passed (2120)
    
  • Real-server browser regression:
    Calendar recurrence regression passed: separate CalDAV override, ETag sync, UI, Europe/Berlin DST and Asia/Kolkata. Screenshots: /home/kayg/Developer/calternal-wt/exmove-1333/artifacts/calendar-recurrence-1333
    

Performance

Local measurements on calternal-dev; load average was 41.88/40.61/39.57 before and 39.96/40.15/39.50 after. The baseline Calendar range profile is p50 1.7 ms / p95 4 ms (50 successful samples). With 17 resources, this profile measured p50 39.92 ms / p95 74.08 ms; mean server RSS was 262,455,052 B. The 513-resource, 32-concurrent-request profile measured p50 1,935.60 ms / p95 3,626.50 ms; peak RSS was 226,922,496 B. The baseline workload is smaller, so these numbers are not a like-for-like regression comparison. Full profile: artifacts/calendar-recurrence-1333/profile.json (ignored).

Files

  • crates/plugins/calendar/src/cache/store.rs, crates/plugins/calendar/src/recurrence.rs, crates/plugins/calendar/src/routes.rs, crates/plugins/calendar/src/view.rs, crates/plugins/calendar/src/feeds/subscriptions.rs
  • crates/plugins/calendar/migrations/0006_event_series.sql
  • crates/calternal-server/src/upgrade_tests.rs
  • apps/web/e2e/calendar-recurrence-1333.mjs, apps/web/e2e/harness.mjs
  • bench/calendar-recurrence-1333.mjs
  • contracts/perf/exceptions.json, contracts/perf/ratchet.json, contracts/perf/registry.json

Decisions not specified by DESIGN

  • Group resources by Calendar and UID; keep resources without a UID separate. Use an indexed EXISTS lookup so any matching detached resource fetches its sibling resources.
  • Cap combined multi-resource parsing at 4 MiB and charge rejected input before parsing. Keep the existing 5,000-resource and occurrence limits.
  • Keep occurrence identity rooted in the actual source Event resource, including detached overrides.

For the merge round

Run the prescribed one-round adversarial API matrices, including tests/adversarial/run.sh, for XUser, authorization and robustness coverage.

Screenshots

Head: 13a4e59a24682cb1523829794e6873cdbff8ea4d.

## Completed Fixed #1333 at the shared Calendar recurrence owner. Calendar range and the Events API now load cached CalDAV resources that share a UID, combine detached components into one recurrence set, and preserve each Event's source resource identity. A moved override now replaces the old master occurrence even when its href is separate. The query uses the `(owner_id, calendar_id, uid)` index. Multi-resource series parsing has a 4 MiB aggregate byte budget, charged before parsing so malformed resources cannot evade it. Work is bounded by the 5,000-resource query cap, the per-series byte cap, and the caller's occurrence limit. The browser regression edits the master with its observed CalDAV ETag, retains the separate override, syncs, and checks the signed-in Calendar API and screen. It also checks Europe/Berlin across the autumn DST change and Asia/Kolkata. The new malformed-body regression was verified both ways: with old accounting it failed at `assert!(expanded.instances.is_empty())`; with the fix it passed. ## UX gaps closed - The stale master occurrence no longer appears beside its moved detached Event. API and screen agree on one occurrence at the moved time. - The real Calendar screen was checked after a pointer click and after ⌘I at 390, 820 and 1440 px in light and dark themes. The browser used macOS platform emulation. The 12 actual-size screenshots are attached below. UX gaps left: none found in this data-projection change. The screenshots use macOS platform emulation in Chromium; no separate Mac VM GUI run was needed. ## Gates and browser evidence - `cargo fmt --check`: exit 0, no output. - `cargo clippy -p calternal-plugin-calendar --all-targets -- -D warnings`: `Finished `dev` profile [unoptimized + debuginfo] target(s) in 31.36s`. - `cargo test -p calternal-plugin-calendar`: ``` test result: ok. 106 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 16.04s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.42s test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.17s ``` - `cargo clippy -p calternal-server --all-targets -- -D warnings`: `Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 12s`. - `cargo test -p calternal-server`: 271 passed, 1 failed, 12 ignored. The only failure was the load-sensitive Calendar write profile: ``` search_reconcile_calendar_write_profile files=20000 writes=32 p50_ms=13534 p95_ms=13724 p95_budget_ms=10000 Calendar write p95 13.724269222s exceeded 10s ``` No test expectation was changed. Treat this as a host-load performance finding; it did not reproduce in the recurrence regression. - `bun run check` exited 0: ``` perf-lint: PASS; 0 violations; 22542 scoped exceptions svelte-check found 0 errors and 4 warnings in 2 files ``` The four warnings are the existing unused CSS selectors in MailView and the money page. - `bun run test --maxWorkers=2`: ``` Test Files 297 passed (297) Tests 2120 passed (2120) ``` - Real-server browser regression: ``` Calendar recurrence regression passed: separate CalDAV override, ETag sync, UI, Europe/Berlin DST and Asia/Kolkata. Screenshots: /home/kayg/Developer/calternal-wt/exmove-1333/artifacts/calendar-recurrence-1333 ``` ## Performance Local measurements on `calternal-dev`; load average was 41.88/40.61/39.57 before and 39.96/40.15/39.50 after. The baseline Calendar range profile is p50 1.7 ms / p95 4 ms (50 successful samples). With 17 resources, this profile measured p50 39.92 ms / p95 74.08 ms; mean server RSS was 262,455,052 B. The 513-resource, 32-concurrent-request profile measured p50 1,935.60 ms / p95 3,626.50 ms; peak RSS was 226,922,496 B. The baseline workload is smaller, so these numbers are not a like-for-like regression comparison. Full profile: `artifacts/calendar-recurrence-1333/profile.json` (ignored). ## Files - `crates/plugins/calendar/src/cache/store.rs`, `crates/plugins/calendar/src/recurrence.rs`, `crates/plugins/calendar/src/routes.rs`, `crates/plugins/calendar/src/view.rs`, `crates/plugins/calendar/src/feeds/subscriptions.rs` - `crates/plugins/calendar/migrations/0006_event_series.sql` - `crates/calternal-server/src/upgrade_tests.rs` - `apps/web/e2e/calendar-recurrence-1333.mjs`, `apps/web/e2e/harness.mjs` - `bench/calendar-recurrence-1333.mjs` - `contracts/perf/exceptions.json`, `contracts/perf/ratchet.json`, `contracts/perf/registry.json` ## Decisions not specified by DESIGN - Group resources by Calendar and UID; keep resources without a UID separate. Use an indexed `EXISTS` lookup so any matching detached resource fetches its sibling resources. - Cap combined multi-resource parsing at 4 MiB and charge rejected input before parsing. Keep the existing 5,000-resource and occurrence limits. - Keep occurrence identity rooted in the actual source Event resource, including detached overrides. ## For the merge round Run the prescribed one-round adversarial API matrices, including `tests/adversarial/run.sh`, for XUser, authorization and robustness coverage. ## Screenshots - 390 px: [light · click](https://git.kayg.org/attachments/76e93294-cfcd-4d93-b649-6d16f13d979f) · [light · ⌘I](https://git.kayg.org/attachments/afc37529-424e-4bc7-903c-1d4b7a30b3ac) · [dark · click](https://git.kayg.org/attachments/e73b74f9-30e2-46b8-ad0b-4ec640c776f0) · [dark · ⌘I](https://git.kayg.org/attachments/f8fc9e82-270f-4568-8459-286b12b78ec5) - 820 px: [light · click](https://git.kayg.org/attachments/fa288383-4d9b-4141-ab5f-27fe09a81a74) · [light · ⌘I](https://git.kayg.org/attachments/6692ea7a-61e3-4d72-8f97-3820c9473ac4) · [dark · click](https://git.kayg.org/attachments/99da4526-4c16-4a3b-88aa-bceded2186ad) · [dark · ⌘I](https://git.kayg.org/attachments/72251e6d-4614-4cd6-8a0a-51ba8c931163) - 1440 px: [light · click](https://git.kayg.org/attachments/79d13504-ef19-4bd6-ae60-adc84352009b) · [light · ⌘I](https://git.kayg.org/attachments/9d157670-e614-4b98-96c0-b19e59fcd5b9) · [dark · click](https://git.kayg.org/attachments/53ef61f9-f156-4c32-a375-4a2569bd3c60) · [dark · ⌘I](https://git.kayg.org/attachments/6e8f9089-96f9-4c94-a68d-5d3c26c2b48c) Head: `13a4e59a24682cb1523829794e6873cdbff8ea4d`.
Author
Owner

Correction to my final report: crates/calternal-server/src/upgrade_tests.rs intentionally updates the migration receipt fixture and count to include Calendar migration 0006. The sentence “No test expectation was changed” was inaccurate. No Calendar API behavior expectation or performance threshold was changed. The server test's 10-second Calendar write profile budget remains unchanged.

The Calendar clippy output was:

Finished `dev` profile [unoptimized + debuginfo] target(s) in 31.36s
Correction to my final report: `crates/calternal-server/src/upgrade_tests.rs` intentionally updates the migration receipt fixture and count to include Calendar migration 0006. The sentence “No test expectation was changed” was inaccurate. No Calendar API behavior expectation or performance threshold was changed. The server test's 10-second Calendar write profile budget remains unchanged. The Calendar clippy output was: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 31.36s ```
kayg referenced this issue from a commit 2026-10-08 19:43:32 +00:00
Author
Owner

Deployed to calternal.cloud in merge round 35b (build 9dc65437e, live 2026-10-09 ~11:07 IST). Closing.

Deployed to calternal.cloud in merge round 35b (build 9dc65437e, live 2026-10-09 ~11:07 IST). Closing.
kayg closed this issue 2026-10-09 05:36:42 +00:00
Commenting is not possible because the repository is archived.
No labels
No milestone
No project
No assignees
1 participant
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal-v0#1333
No description provided.