Correctness and resilience release. Fixes an OOM-triggering infinite loop in slot computation, adds explicit per-event-type timezones, makes team slot grids honour each member's personal working hours across timezones, and parallelizes per-member CalDAV syncs with per-source deduplication.
Added
- Explicit timezone on event types (issue #50) — each event type now carries its own IANA
timezonecolumn; availability rules are interpreted in that timezone rather than silently inheriting the creator's profile. New picker in the event-type form. Migration046_event_type_timezonebackfills existing rows with the current account owner's timezone, so upgrades preserve behaviour. - Per-member working hours on team events — team slot grids intersect each member's personal
user_availability_rules(converted from the member's own timezone into the event's host timezone) with the event-type's rules. Members without explicit personal hours stay unconstrained. Prevents the scenario where a team event pinned to Paris would offer 09:00 Paris bookings to a US-based member whose actual working day is 09:00 Chicago (= 16:00 Paris). - Member timezone shown on event-type priority list — the Member Priority / Required Members section now renders each member's timezone under their name. Makes mis-configured user timezones immediately visible.
Fixed
- Infinite slot loop → OOM when availability window ends near midnight —
compute_slots_from_ruleswalked its inner cursor as aNaiveTime, andNaiveTime + Durationwraps at 24h. On a rule ending at 23:00 with a 60-minute slot duration,cursor + slot_durationwrapped to 00:00 (still ≤ 23:00 as a time-of-day), producing an infinite loop until the kernel OOM-killed the process (~4-minute CPU spike, ~9 GB RAM, ~240 GB of SQLite re-reads under memory pressure). Cursor now walks asNaiveDateTimeso midnight rolls into the next day cleanly. - Dashboard Decline button no-op on pending bookings (#51) —
cancel_bookingfiltered onstatus = 'confirmed', so clicking Decline on a pending booking matched zero rows and silently redirected. Now branches on status: confirmed → cancelled (CalDAV delete + emails), pending → declined (guest decline notice only). Mirrors the email-token decline flow.
Performance
- Parallel per-member CalDAV sync on team / dynamic-group slot pages — fans out via
tokio::task::JoinSet, guarded by a per-source async mutex insidesync_if_stale: same-source concurrent calls serialize and the loser skips after re-checking staleness. At most one CalDAV fetch per source is in flight at any time across the whole process. Team booking pages no longer serialize on the slowest member's CalDAV server.
Internal
- Migration
046_event_type_timezonewith per-row backfill - New helper
normalize_event_type_tzvalidates IANA submissions - Regression tests:
get_host_tz_prefers_explicit_event_type_timezone,compute_slots_terminates_with_window_ending_at_23_00,chicago_member_is_busy_at_paris_morning,member_without_personal_rules_is_unconstrained,source_lock_identity,sync_if_stale_serializes_on_per_source_lock - 545 tests total (up from 537 in 1.6.0), all green on pre-commit
- Verified end-to-end against a copy of a production DB: the previously-OOMing team booking URL now responds in under a second with flat RSS
Full changelog: https://github.com/olivierlambert/calrs/blob/v1.7.0/CHANGELOG.md#170---2026-04-24