`toLocaleString()` was given the German locale but no time zone, so it printed
the time in whatever zone the browser of the visitor is set to. The events
happen in Berlin, so this is only correct for visitors who are in Berlin.
Somebody reading the start page from Sydney was told the Plenum of Tuesday
20:00 takes place on Wednesday at 04:00.
Format in Europe/Berlin explicitly. For an event that names a point in time
only the printing was wrong, picking and sorting work on absolute points in
time and were not affected.
An all day event carries a date, and a date has neither a time nor a zone: its
digits are the day itself. `toJSDate()` reads them as midnight in the zone of
the browser, so the point in time that is then printed in Berlin time is off by
the offset between the two, which puts a bogus time of day on the entry and,
far enough east or west, the wrong day. An event on 30.08. was announced as:
Berlin Sonntag, 30.08., 00:00 Uhr
Los Angeles Sonntag, 30.08., 09:00 Uhr
Tokio Samstag, 29.08., 17:00 Uhr
Keep the digits of the date and read them as UTC, which no browser setting
moves, and print an all day event in UTC and without a time of day. Berlin,
Tokyo, Los Angeles and Kiritimati all say "Sonntag, 30.08." now, while a timed
event keeps saying "Sonntag, 30.08., 19:00 Uhr" everywhere. Sorting improves
with it, an all day event no longer changes its place in the list depending on
where the visitor sits.
The published calendar has no all day events today, they can be created in the
CalDAV calendar the export comes from.
Drop the two replacements around the formatted date while touching it. The
first removes the comma after the weekday and the second puts it back, so they
cancel each other out:
"Samstag, 22.08., 17:00" -> "Samstag 22.08., 17:00" -> "Samstag, 22.08., 17:00"
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>
An occurrence of a recurring event that was changed on its own carries a
RECURRENCE-ID and is stored as an additional VEVENT next to the event it
belongs to. `getAllSubcomponents("vevent")` returns those components as well,
and since they have no RRULE of their own they were handled as separate single
events. The occurrence therefore ended up in the list twice: once from
expanding the recurring event, which resolves the modified time through the
exception, and once more from the extra VEVENT.
To make it worse the two rows disagreed, because name and URL were taken from
the recurring event while the time came from the modification, so the first row
showed the new time under the old name.
Skip components that are a recurrence exception, they are already covered by
the event they modify, and take name and URL from the occurrence details, which
point at the modification where there is one and at the event itself otherwise.
Events whose RECURRENCE-ID refers to an event that is not in the file are
dropped by this, which cannot happen in a full calendar export.
With a recurring Plenum whose 25.08. occurrence is moved two hours earlier and
renamed:
before: 25.08. 18:00 CCCB Plenum
25.08. 18:00 CCCB Plenum (verschoben)
after: 25.08. 18:00 CCCB Plenum (verschoben)
Leaning on the resolution ical.js does here needs two more things to be right.
The first is which modifications an event is asked to resolve. Unless it is
told, `ICAL.Event` relates every VEVENT with a RECURRENCE-ID in the file to
every recurring event and keys them by the recurrence id alone; the UID is only
compared with `strictExceptions`, which then throws instead of skipping. A
modification would therefore also override the occurrence another series holds
at the same instant, so the wrong entry is shown and the modified one twice
over. Group the modifications by the UID of the event they belong to and hand
each event its own, which also turns the relating off for events that have
none. Recognise a modification on the component instead of on the event,
because relating exceptions to an exception throws. With two unrelated weekly
series that both meet on Thursday at 19:00, of which only A has its occurrence
of 03.09. moved to 17:00:
before: 27.08. 19:00 Serie A after: 27.08. 19:00 Serie A
27.08. 19:00 Serie B 27.08. 19:00 Serie B
03.09. 17:00 Serie A (versch.) 03.09. 17:00 Serie A (versch.)
03.09. 17:00 Serie A (versch.) 03.09. 19:00 Serie B
The second is how far the expansion has to run. It walks the unmodified
recurrence times and stopped at the end of the window, but the occurrence
handed to `getOccurrenceDetails()` may have been moved somewhere else entirely.
Both directions were wrong: an occurrence pulled forward from beyond the window
was never reached, because the walk had already stopped at its original time,
and one pushed out of the window was still listed, because only its original
time was ever compared against the window. Iterate far enough that the largest
move towards the past can still reach the window, and decide by the time the
occurrence really takes place at. The stop condition keeps using the raw
recurrence time, which stays monotonic, so the iteration still terminates. With
a weekly series whose occurrence of 05.10. is pulled forward to 25.08. and
whose occurrence of 31.08. is pushed to 02.11., seen from 23.08. through the 20
day window:
before: 24.08. Serie after: 24.08. Serie
31.08. Serie 25.08. Serie (vorgezogen)
07.09. Serie 07.09. Serie
Reading the URL becomes a function of its own while name and URL move to the
occurrence details, and it now looks at what it reads. The calendar is exported
from a CalDAV server, so whoever may write to it decides what ends up in the
href of the link, and `URL:javascript:alert(1)` on an event would run that
script when a visitor clicks the entry. Pass on nothing but http and https; a
value that is not a URL at all no longer reaches the href either.
The published calendar contains no RECURRENCE-ID at all today, so nothing about
the modifications changes for it. It is exported from a CalDAV server though,
and moving a single Club Discordia or Plenum out of the way of a holiday is
exactly what creates such a modification. Checked against
https://berlin.ccc.de/calendars/all.ics: the list and the links of the 30
events that carry a URL are unchanged.
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>
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>
The "Nächste Veranstaltungen" table read the event URL via `ICAL.Event.url`,
but ical.js does not expose a `url` getter on `ICAL.Event` (it only has uid,
summary, description, color, location, sequence, the dates, organizer and
attendees). `event.url` was therefore always `undefined` and the `?? ""`
fallback turned it into an empty string, so every row rendered as
`<a href="">`, a dead link that just reloads the start page.
Read the URL from the VEVENT component instead. This also picks up the
`URL;VALUE=URI:` form used by most events in the published calendar, which is
exported from a CalDAV client and does not use a bare `URL:` property.
Not every event has a URL, so only wrap the name in a link when one is
present and emit plain text otherwise.
Checked against https://berlin.ccc.de/calendars/all.ics (31 events, 1 of them
without a URL):
before: <td><a href="">CCCB Plenum</a></td>
after: <td><a href="https://wiki.berlin.ccc.de/Plenum">CCCB Plenum</a></td>
after: <td>Aktionstag gegen Überwachung im Chaos Computer Club Berlin</td>
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>
`build.sh` no longer post-processes anything: since the switch to ICS files it
just deletes `public/` and runs `hugo`. The `CALENDAR` placeholder, the Python
dependency on `icalendar`, `python-dateutil` and `pytz`, and the `de_DE.UTF-8`
locale requirement are all gone, and nothing in `packages.nix`, `devShells.nix`
or `flake.nix` pulls in Python any more. Drop those claims.
Describe instead where the two calendar views actually come from: the
"Nächste Veranstaltungen" table and the calendar page are rendered in the
browser from `/calendars/all.ics`, which is published next to the site and is
not built from this repository. That also replaces the old advice to preview
them via `./build.sh` plus a local HTTP server, which no longer helps.
Without that file the two of them stay empty. Hugo serves everything below
`static/` from the root of the site, so it can simply be put there, and the
command that downloads it is worth writing down. Checked from an empty state:
afterwards the file is served under `/calendars/all.ics` by `hugo serve` and the
table on the start page fills with the upcoming events. It is a copy of
published data and has no business in the repository, so ignore it.
Mention the per-section `.ics` feeds Hugo does generate, so the ICS templates
under `layouts/` are not mistaken for the source of `all.ics`.
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>
Der DI.Day findet am 5. Juli 2026 nicht im Chaos Computer Club Berlin
statt. Der neue Blog-Eintrag informiert darüber und verweist auf die
alternativen Berliner DI.Day-Veranstaltungen (c-base, Stadtschloss
Moabit).
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
Der Digital Independence Day findet ab Juli 2026 nicht mehr im
zweimonatlichen Turnus statt. Mit UNTIL in der RRULE endet die
Wiederholung nach dem letzten Termin am 3. Mai 2026, sodass keine
künftigen Instanzen mehr in den Kalendern (all.ics,
veranstaltungen/index.ics) und in der Terminübersicht erscheinen.
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
gen_upcoming.py normalisiert dtstart auf naive lokale Zeit, reichte die
laut RFC 5545 in UTC anzugebende UNTIL-Zeit der RRULE aber tz-aware an
dateutil weiter. Die Mischung aus naivem dtstart und tz-awarer UNTIL
lässt dateutil.rrule mit "RRULE UNTIL values must be specified in UTC
when DTSTART is timezone-aware" abbrechen -- der Build schlägt fehl,
sobald ein wiederkehrender Termin ein UNTIL enthält.
Mit ignoretz=True wird die UNTIL-Zeit ebenfalls naiv behandelt,
konsistent zur restlichen Verarbeitung. Für Termine ohne UNTIL ist das
ein No-Op.
Fixes: 2e3a02af0c ("refacture all the things!")
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
Die layouts/_default/*.calendar.html-Vorlagen werden in Hugo
≥0.158 fälschlich für die HTML-Ausgabe ausgewählt, sodass alle
Sektions- und Einzelseiten VCALENDAR- statt HTML-Inhalt
enthielten. Die Vorlagen waren ohnehin nie funktionsfähig
(Warnung „found no layout file for calendar"); die ICS-Feeds
liefern die abschnittsspezifischen Vorlagen unter
layouts/{veranstaltungen,datengarten,page}/.
list.xml.html bekommt aus demselben Grund die korrekte Endung
.xml.
tools/gen_upcoming.py vergleicht Datumsangaben jetzt
zeitzonenneutral, damit Events mit Z-Suffix keinen TypeError
auslösen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>