Skip to content

fix(calendar): keep the named date for all-day events - #6291

Open
JulianFriedle wants to merge 1 commit into
odysseus-dev:devfrom
JulianFriedle:fix/all-day-date-shift
Open

JulianFriedle wants to merge 1 commit into
odysseus-dev:devfrom
JulianFriedle:fix/all-day-date-shift

Conversation

@JulianFriedle

@JulianFriedle JulianFriedle commented Sep 14, 2026 •

Copy link
Copy Markdown

Summary

All-day events were stored shifted by the user's UTC offset and flagged as
local time, so they read back — and reached CalDAV — one day early. At UTC+2,
an all-day event created for 2026-10-01 landed on 2026-09-30 in Google
Calendar. This parses all-day values as the calendar date they name instead
of converting them to a UTC instant, resolves the all_day flag before the
datetimes in both update paths, and reuses the existing
_ensure_positive_duration() so an all-day event sent with
dtstart == dtend still spans a day.

Target branch

  • This PR targets dev, not main.

Linked Issue

Fixes #6290

Type of Change

  • Bug fix (non-breaking — fixes a confirmed issue)

Checklist

  • I searched open issues and open PRs — this is not a duplicate.
  • This PR targets dev
  • My changes are limited to the scope described above — no unrelated refactors or whitespace changes mixed in.
  • I actually ran the app (docker compose up or uvicorn app:app) and verified the change works end-to-end. Type-checks and unit tests are not enough.
  • I did not run the app/runtime validation and stated that gap in How to Test.

How to Test

Unit tests:

python -m pytest tests/test_calendar_all_day_date_shift.py -v

Four cases: a bare date, the local-midnight-with-offset form the agent path
actually emits, a zero-duration all-day event, and flipping a timed event to
all-day in a single update_event call. All four fail on dev with
'2026-09-30' == '2026-10-01' and pass with this change.

Neighbouring suites that touch the same code paths, all green:

python -m pytest tests/test_calendar_update_event_tz.py \
                 tests/test_calendar_import_zero_duration.py \
                 tests/test_calendar_recurrence.py \
                 tests/test_caldav_writeback.py -q

End to end in the running app, user timezone UTC+2, Google calendar over
CalDAV:

  1. docker compose up -d --build
  2. Ask the agent to create an all-day event on 1 October in the CalDAV
    calendar.
  3. Check the stored row and the Google web UI.

Before — the agent-created event next to one synced down from Google on
the same date:

'Halde'                  2026-10-01 00:00 -> 2026-10-05 00:00  is_utc=0   (from Google)
'Tesla Roadster Reveal'  2026-09-30 22:00 -> 2026-10-01 22:00  is_utc=0   (from the agent)

Google Calendar showed the event on 30 September.

After — same request, same timezone:

'Tesla Roadster Reveal'  2026-10-01 00:00 -> 2026-10-02 00:00  is_utc=0

Google Calendar shows 1 October.

Visual / UI changes — REQUIRED if you touched anything that renders

Not applicable — this changes routes/calendar_routes.py and
src/tools/calendar.py only. Nothing that renders is touched.

  • I am not an LLM agent submitting a bulk PR.

Note for the reviewer

_parse_date_only() falls back to _parse_dt() for non-ISO input, and that
helper does its own UTC conversion. In practice all-day values never reach
the fallback, because parse_due_for_user() normalises to ISO first — but it
is worth knowing if natural-language all-day input is ever wired up.

All-day events are date-only in RFC 5545, but both create paths parsed
dtstart with _parse_dt_pair — which normalises tz-aware input to UTC —
and then stored the row with is_utc forced to False. For a user east of
UTC that turned local midnight into 22:00 the previous day, so the event
read back one day early and was pushed to CalDAV as DTSTART;VALUE=DATE
for the wrong date.

Parse all-day values with a new _parse_date_only() that keeps the named
calendar date, and resolve the effective all_day flag before parsing in
both update paths, where dtstart was parsed first. Reuse the existing
_ensure_positive_duration() so an all-day event sent with
dtstart == dtend still spans a day instead of being dropped by the
list_events overlap filter.

Fixes odysseus-dev#6290
@github-actions github-actions Bot added needs work PR description incomplete — please update before review ready for review Description complete — ready for maintainer review and removed needs work PR description incomplete — please update before review labels Sep 14, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready for review Description complete — ready for maintainer review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

All-day events are stored and pushed one day early for users east of UTC

1 participant