Keep running events in the upcoming list until they end

The "Nächste Veranstaltungen" table selected events with `start > now`, so an
event disappeared from the list the moment it began. Someone looking at the
start page at 20:30 no longer saw the Plenum that had started at 20:00 and ran
until 22:00, which is exactly when that information is most useful.

Select on the end of the event instead, so an event stays listed for as long as
it is still running. A currently running event sorts first, because the list is
ordered by start time.

For recurring events the end of the individual occurrence is needed, not the
end of the series, so go through `getOccurrenceDetails()`. That also resolves
occurrences overridden via RECURRENCE-ID. The chronological break out of the
iteration keeps using the raw occurrence time, which stays monotonic even when
an override moves a single occurrence. `ICAL.Event.endDate` falls back to
DURATION and, for all-day events, to the following day, so events without an
explicit DTEND keep working.

The `maxDays` window still applies to the start of an event, so a long running
event does not extend the window.

This is not a regression from the commit below, the Python generator it
replaced filtered on `dtstart >= start` in the same way.

Checked against https://berlin.ccc.de/calendars/all.ics: the Plenum (20:00 to
22:00) is now listed at 20:30 and 21:59 and gone at 22:01, and the multi-day
Amateurfunk trip (30.10. 12:00 to 01.11. 18:00) stays listed throughout.

Fixes: c28f04c6e8 ("switch to ics files; make calendars work; fix some minor issues")
Assisted-by: Claude:claude-opus-5
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
This commit is contained in:
Hauke Mehrtens 2026-08-22 19:43:35 +02:00
commit 8b692a2e88

View file

@ -4,7 +4,7 @@ import ICAL from "https://unpkg.com/ical.js/dist/ical.min.js";
* Parse an ICS calendar and return upcoming event occurrences. * Parse an ICS calendar and return upcoming event occurrences.
* *
* @param {string} icsText The contents of the .ics file * @param {string} icsText The contents of the .ics file
* @param {Date} now Events must start after this date * @param {Date} now Events must still be running at this date
* @param {number} maxEvents Maximum number of events to return * @param {number} maxEvents Maximum number of events to return
* @param {number} maxDays Maximum number of days into the future * @param {number} maxDays Maximum number of days into the future
* @returns {{start: Date, name: string, url: string}[]} url is empty when the event has no URL * @returns {{start: Date, name: string, url: string}[]} url is empty when the event has no URL
@ -38,17 +38,19 @@ function getUpcomingEvents(icsText, now, maxEvents, maxDays) {
break; break;
} }
const start = occurrence.toJSDate();
// Recurrences are chronological, so we're done // Recurrences are chronological, so we're done
// once we pass the end of our search window. // once we pass the end of our search window.
if (start > end) { if (occurrence.toJSDate() > end) {
break; break;
} }
if (start > now) { // Details resolve the times of occurrences overridden by RECURRENCE-ID.
const details = event.getOccurrenceDetails(occurrence);
// A running event stays listed until it is over, so filter on its end.
if (details.endDate.toJSDate() > now) {
events.push({ events.push({
start, start: details.startDate.toJSDate(),
name: event.summary ?? "", name: event.summary ?? "",
url, url,
}); });
@ -57,7 +59,7 @@ function getUpcomingEvents(icsText, now, maxEvents, maxDays) {
} else { } else {
const start = event.startDate.toJSDate(); const start = event.startDate.toJSDate();
if (start > now && start <= end) { if (start <= end && event.endDate.toJSDate() > now) {
events.push({ events.push({
start, start,
name: event.summary ?? "", name: event.summary ?? "",