Skip to content

Add an optional start time to calendar entries - #268

Open
MeisterAdebar wants to merge 2 commits into
developeregrem:masterfrom
MeisterAdebar:feature/calendar-entry-time
Open

Add an optional start time to calendar entries#268
MeisterAdebar wants to merge 2 commits into
developeregrem:masterfrom
MeisterAdebar:feature/calendar-entry-time

Conversation

@MeisterAdebar

Copy link
Copy Markdown
Contributor

Calendar entries can now carry an optional start time. Left empty they stay
all-day entries, which is what every entry was before and still is by default.

Storage

The time lives in its own nullable TIME column rather than widening date
to DATETIME. Everything downstream keys days by Y-m-d - sync
reconciliation, the reservation table decorations, the reminder query - and
each of them would otherwise have to strip a time component it never asked
for. null already says "all day", and that is exactly what existing rows
say without touching them.

Migration Version20260729140000 adds the column, down() drops it again.

Calendar sync

CalendarEntrySyncService reads the start time from DTSTART:

  • All-day events are written as a bare date (DTSTART;VALUE=DATE:20260801).
    The parser drops the parameters, so the plain Ymd shape of the value is
    what identifies them - the same signal resolveDates() already relies on.
  • On a multi-day event only the day it starts on carries the time; the days it
    runs through are whole days by definition. Tracked per date rather than per
    event, so two VEVENTs merging under one UID keep their own.

Times are compared by wall clock, not by object identity - otherwise every
re-sync would report every timed entry as "updated".

Known limitation: time zones

The wall-clock digits are taken as they stand. A feed sending
DTSTART;TZID=Europe/Berlin:20260801T140000 loses the zone during parsing but
keeps the 14:00 it meant, which is the time that should be displayed. An
explicit UTC value (trailing Z) is read as UTC, and the application runs in
UTC, so that is what gets shown.

For a feed in a different zone than the one the instance runs in, the
displayed time will therefore be off. Carrying the zone through would mean
teaching the ICS parser to keep DTSTART parameters, which is a larger change
than this one - worth doing if it turns out to matter in practice, but the
common case here is a feed from the same zone.

Display and ordering

The popover and the reminder list show the time next to the title when there
is one. Within a calendar and day, all-day entries sort first (null time),
followed by the timed ones in clock order.

In the form the time input sits with the start date, not with the date range,
mirroring that only the first day of a range carries it.

Multi-day entries

The unmapped dateTo field creates one entry per day of the range instead of
teaching the entity and every consumer about ranges - the same approach the
sync already takes for multi-day ICS events.

Wording of the existing end-date hint

The end-date field predates this branch, but its hint led with
"End date"/"Enddatum", repeating the label right above it and pushing the part
that matters - that the field may be left alone - into the middle of the
sentence. It now leads with "Optional", matching the time field this branch
adds:

time_help:     "Optional - leave empty for an all-day entry."
date_to_help:  "Optional - end date for a multi-day entry (e.g. a vacation period). ..."

Tests

Six functional tests cover the sync path: a timed event stores its time, an
all-day event stores none, a multi-day event puts the time on the first day
only, re-syncing reports unchanged, a changed time updates the entry, and an
event losing its time becomes all-day again.

Entries were always all-day: the date column held a bare date and the ICS
import zeroed the time off DTSTART. Appointments that happen at a set hour
had nowhere to put it.

Adds a nullable time column rather than widening date to DATETIME. Every
consumer keys days by Y-m-d - sync reconciliation, the reservation table
decorations, the reminder query - and each would otherwise have to strip a
time component it never asked for. Null means all-day, which is what every
existing entry already says.

The ICS import now keeps the time where the feed states one, recognising
all-day events by DTSTART's bare-date form. On a multi-day event only the
starting day carries it; the days it runs through are whole days. Times are
tracked per date so two VEVENTs merging under one UID keep their own, and
reconciliation compares them by wall clock so a re-sync does not report every
timed entry as updated. Manual entries created over a range follow the same
rule.

Shown after the title in the day popover and the reminder list, and sorted
after the all-day entries within a day.
The hint started with "End date"/"Enddatum", repeating the label right
above it and pushing the part that actually matters - that the field may
be left alone - to the middle of the sentence. The neighbouring time
field already reads "Optional - ...", so both optional fields now
announce themselves the same way.
@developeregrem developeregrem added this to the 4.11.0 milestone Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants