Exporting any calendar whose name is not pure ASCII failed. str.isalnum() is
true for "ö", so the "safe" filename kept the umlaut and went into
Content-Disposition raw — an HTTP header must be ASCII, and a reverse proxy
drops the response. The client then had no JSON to read and showed its
"unknown error" fallback, which said nothing at all. attachments_router had
already learned this and documents it in a comment; the export endpoint never
got the same treatment. Now RFC 6266, exactly as that router does it.
downloadRequest discarded a non-JSON error body entirely. That body is the only
clue when the failure happens outside the app, so it is used now, with the
status appended when there is nothing to read.
The CalDAV publish button rendered as "..." — four buttons never fitted the
240px actions column, and the cell clips instead of overflowing because the
columns are drag-resizable. Nobody could tell there was a button there, which
is why publishing a calendar to Home Assistant looked impossible: the URL, the
copy button and both hints only appear once it is switched on. The label is now
just "CalDAV" with the full action as its tooltip, which also helps anyone who
already dragged that column narrower, since saved widths beat the default.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
reminders_enabled belongs to the calendar, so it belongs to its owner: whoever
shares a calendar decides whether it notifies everyone it reaches, and a
recipient's PUT is refused with 404 by the owner-only filter. For a group
calendar that is worse than it sounds — the creator owns it, so silencing it
for themselves would silence every member.
CalendarReminderPref follows CalendarColorPref exactly, which already solves
this for colours. Only local calendars are shareable (CalendarShare,
GroupCalendar and group_visible_calendar_id all point at local_calendars.id),
so no source discrimination is needed and the other four routers are untouched.
No migration: create_all makes new tables, which is why calendar_color_prefs
has no migration entry either.
A recipient without an override gets reminders on, rather than inheriting the
owner's choice. Muting is personal and should not spread. The new endpoint
decides owner-vs-override server-side, so the existing owner-only PUT stays the
guard it already is, and GET keeps returning reminders_enabled — now carrying
the effective value, so no client model has to change.
Birthday reminders were injected without consulting the flag at all, so a muted
birthday calendar still handed the schedulers something to fire.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Jeder Anhang wird als ATTACH mit FMTTYPE, SIZE und FILENAME ausgegeben,
als URI statt inline base64 -- bei 10 MB je Datei wuerde sonst bei jeder
Synchronisation der ganze Anhang im ICS-Rumpf mitgeschleppt.
Die absolute URL kommt aus dav_util.public_base, das PUBLIC_BASE_URL und
die Forwarded-Header schon behandelt; ohne Basis-URL wird die Zeile
weggelassen, weil eine relative ATTACH-URL einem externen Client nichts
nuetzt. Abschaltbar ueber ATTACHMENT_PUBLIC_LINKS=0.
Der CalDAV-PUT ist geprueft: ein externer Client, der den Termin
zurueckschreibt, laesst die Anhaenge unberuehrt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Die Terminliste traegt nur die Anzahl, nicht die Anhaenge selbst. Der
Merge-Read laeuft pro Termin durch eine Schleife -- eine Lazy-Relationship
waere dort eine Abfrage je Termin ueber eine ganze Monatsansicht, und ein
wiederkehrender Termin wuerde dieselbe Liste an jeder Instanz wiederholen.
Stattdessen je Kalender eine gruppierte Abfrage; die Liste holen die
Clients beim Oeffnen der Detailansicht.
mask_busy_event setzt die Zahl ausdruecklich auf 0: dass ein als
"Beschaeftigt" maskierter Termin drei Dateien haette, ist auch eine
Information.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Neue Tabelle event_attachments samt Ablage-Modul. Dateien liegen unter
DATA_DIR/attachments, geshardet nach einem servergenerierten Namen -- weder
der hochgeladene Dateiname noch die Termin-UID beruehren je einen Pfad, da
die UID bei CalDAV-Terminen aus einem fremden Client stammt.
Der Dateityp wird an den Bytes bestimmt, nie am Content-Type des Clients.
SVG und HTML sind bewusst nicht erlaubt: beide fuehren Skript im Origin der
App aus, wo das Sitzungstoken liegt.
Aufgeraeumt wird an allen fuenf Loeschpfaden (Termin, Kalender, Gruppe,
Benutzer, CalDAV-DELETE), und zwar Zeilen vor dem Commit, Dateien erst
danach -- ein Rollback darf keine Bytes verlieren. Der Sweep als
Sicherheitsnetz ist implementiert, haengt aber noch an keinem Scheduler.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Theme:
- Export/import themes via UI as <date>_<time>.theme (JSON, readable keys,
link to THEME.md). Import is partial-aware: only present params are written
(server accepts partial); prompts before importing files with unknown params.
- Dynamic favicon + theme-color tinted to the primary colour on load/save.
- New themable colours: general hover-highlight, day hover/selected/bg,
today background, plus two unified sidebar action-icon colours
(inactive/active) covering bell, hide, delete and read-only icons.
- All new colours are per-setting syncable; documented in THEME.md.
UX:
- Styled confirm dialog (#modal-confirm) replaces window.confirm() for
calendar delete and account disconnect.
- Birthday/local calendars and iCal subscriptions can now be hidden from the
sidebar via Settings (new sidebar_hidden column + hide toggle).
Backend: additive nullable columns + idempotent migrations for user_settings
colours and local_calendars/ical_subscriptions.sidebar_hidden.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_cal_dict now labels shared (non-owned) calendars by the sharer/group name
(the value already computed for shared_by) instead of the owner's real calendar
name. Fixes recipients seeing the true name in calendar-management lists (and
anywhere else the name is shown); all clients pick it up automatically.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- POST /local/calendars with is_birthday returns the existing birthday calendar
if one exists (idempotent, no second created)
- PUT /local/calendars rejects (422) marking a second calendar as is_birthday
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Repeated Contacts syncs must never create duplicate birthdays. Enforce it on
the server, independent of the client's reconcile:
- POST /local/events is now idempotent on external_uid: if an event with the
same (calendar_id, external_uid) exists, update it in place instead of
inserting a new row
- startup cleanup removes existing duplicates sharing the same
(calendar_id, external_uid), keeping the earliest — auto-heals old data
- unique DB index on (calendar_id, external_uid) as a hard guarantee
external_uid is the stable "contact:<deviceId>:<contactId>", so the same contact
from the same device always maps to exactly one event.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Birthday calendars are ordinary local calendars flagged is_birthday, so they
flow to all clients via the merge read and inherit sharing/colors/reminders.
- models: LocalCalendar.is_birthday + birthday_notify_days_before;
LocalEvent.external_uid (Contacts dedup) + birth_year
- build_local_event_dict: server-computed display_title "Name (age)" per
occurrence, is_birthday flag for the client cake icon, and a reminder injected
from birthday_notify_days_before so mobile schedulers fire it
- groups combined view keeps the birthday display_title instead of overwriting it
- local_router: calendar flags + event fields on create/update, plus
GET /calendars/{id}/birthdays for importer reconcile by external_uid
- additive SQLite migrations
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The recipient colour was stored on calendar_shares, so it only worked for
DIRECT shares. A calendar made visible through a group (a co-member's
group_visible_calendar_id) has no CalendarShare row, so the colour endpoint
returned 403 and nothing was saved — the reported "colour picker opens but the
colour stays the same" for a group-shared calendar.
Replace the share-scoped colour with a general per-user override table
(calendar_color_prefs, keyed by user+calendar). PUT /calendars/{id}/color now
accepts any calendar the user can read (readable_local_calendar_ids covers
direct shares, group calendars AND group-visible), and the merge read + calendar
list apply the override for all of them. Owners still set the shared colour.
The new table is created by create_all; the old calendar_shares.color is unused.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A share recipient couldn't change anything on a shared calendar (update_calendar
is owner-only → 404), yet the clients still showed a colour picker for it.
Add a per-recipient colour: new nullable calendar_shares.color column (+ migration).
New PUT /calendars/{id}/color endpoint sets the calendar colour for the owner
(global) or, for a recipient, only their own share colour — never the name, so
recipients can recolour but not rename. The merge read and the calendar list now
prefer the recipient's share colour over the owner's (NULL = owner's colour).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Backend: co-member group_visible calendars now surface in /local/calendars
(owned=false, shared_by=owner, read-only, group_shared) and in the normal
/caldav/events merge (via readable_local_calendar_ids), deduped against
direct shares / group calendars so nothing appears twice.
- Backend: new User.directory_hidden — a user can hide from sharing/group
pickers (/users/directory), while admin user management (/users/) still lists
them. Migration + profile GET/PUT.
- Backend: /groups/{id} members carry shares_calendar so clients can drop
phantom rows for members who share nothing.
- Frontend: reachable "Teilen" button on owned local calendars; share modal is
now a checkbox multi-select of users (checked = shared). Hidden-profile toggle
in Settings → Profile. Group member filter only lists members who actually
share (phantom fix). Calendars shared with me moved to a dedicated read-only
"shared with me" section in the manage table.
- Tests: group_visible propagation, no-share absence, dedup, directory_hidden.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- dav_router: add Basic-Auth principal-discovery tree at /caldav/ (and
/.well-known/caldav) so clients can add a CalDAV account with server URL +
username + password; lists all published calendars. Token URL /dav/{token}/
still works without login. Handlers generalised over a base href.
- dav_util: derive the public origin from X-Forwarded-Proto/-Host (or
PUBLIC_BASE_URL) so published URLs are https, not internal http:8080.
- local_router: expose caldav_login_url alongside caldav_url.
- frontend/i18n: show both the no-login token URL and the login URL + hint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backend:
- LocalCalendar gains caldav_published + dav_token + dav_ctag; LocalEvent gains
etag (migrations in main.py). bump_dav() refreshes ctag/etag on every local
event write (create/update/delete/import).
- local_router: PUT /calendars/{id} accepts caldav_published (mints/revokes
token), new POST /calendars/{id}/dav-token/rotate, _cal_dict exposes
caldav_published + caldav_url.
- New dav_router mounted at root (/dav/{token}/...): a minimal two-way CalDAV
server (OPTIONS/PROPFIND/REPORT/GET/PUT/DELETE) reusing ical_io build/parse,
ctag-based change detection. Secret token = auth, no login.
Frontend:
- Settings calendar table: per-local-calendar publish toggle + subscribe URL
with copy and token-rotate; i18n (de/en) and styling.
Note: reverse proxy must allow WebDAV methods (PROPFIND/REPORT/PUT/DELETE).
VALARM/reminders are not round-tripped via CalDAV.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New boolean column on every calendar type (CalDAV, local, Google, Home
Assistant, iCal subscription), default ON so existing data is unchanged.
Lets a calendar be shown without generating reminders/notifications.
- models.py: reminders_enabled on the five calendar models
- main.py _migrate(): ALTER TABLE ADD COLUMN for each table
- caldav/google/homeassistant/local/ical routers: expose the flag in the
calendar serialization and accept it in the calendar/subscription update
endpoints
Clients map event -> (source, calendar_id) -> reminders_enabled to skip
scheduling notifications for disabled calendars.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
local_events gains a `reminders` TEXT column (comma-separated minutes-before-
start, like exdate); EventCreate/EventUpdate accept a `reminders: [int]` list
and build_local_event_dict emits it back as a list. user_settings gains
`default_reminder_minutes` (nullable int, null = off), exposed/updatable via
/api/settings (explicit null persists as off). Migrations added in _migrate().
Clients (iOS/Android) schedule the OS notifications locally from these.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Findings from the security review:
- HIGH: private local events leaked in full (title/location/description) to
anyone who could READ a shared or group calendar via GET /api/caldav/events —
the private_event_visibility rule was only enforced in /groups/{id}/combined.
Now enforced in the merge read too, via a shared helper (apply_event_privacy)
so the two paths can't drift.
- HIGH: 'busy' masking was a blacklist that still leaked creator identity,
source-calendar name, recurrence rule and per-event colour. Replaced with a
whitelist (mask_busy_event): only timing/identity/render fields survive.
- MEDIUM: .ics import had no size limit (raw = await file.read()) → memory DoS.
Now capped at 5 MB (413), read before creating any calendar.
- LOW/INFO: profile email now checked for uniqueness + basic format; display
name / username / email length-capped and control-chars stripped.
Deferred (tracked): RRULE expansion cap at the trust boundary, SQLite
PRAGMA foreign_keys + ON DELETE cascade, and JWT-by-user-id + token version.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Monatsmarker ("JUN") sitzt jetzt inline neben der Tageszahl ("1 JUN") statt
darüber -> einheitliche Zeilenhöhe; Termine der Woche rutschen nicht mehr
nach unten und überlappen nicht mehr mit "+X weitere".
- Gruppenkalender erscheint in "Meine Kalender" (mit 👥-Markierung) und kann
aus-/eingeblendet werden; Besitzer kann ihn umfärben. Recolor fremder
Kalender abgefangen (nur Besitzer). Version v36.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Sidebar: eine flache Kalenderliste statt Quellen-Gruppen; Quelle/Konto
klein-grau inline rechts neben dem Namen; per Drag&Drop sortierbar
(Reihenfolge pro Geraet in localStorage).
- Gruppenkalender serverseitig auch beim Besitzer als group:true markiert
-> erscheint nicht mehr in der "Fuer Gruppen sichtbar"-Auswahl und nicht
in der normalen Kalenderliste (nur unter Gruppen).
- Settings-URL-State: uiSettingsOpen wird beim Init aus der URL gesetzt,
bevor das erste writeUrlState() es ueberschreibt -> Reload bleibt jetzt
wirklich in den Einstellungen.
- Auswahl-Markierungen (Mitglieder/Gruppen-Sichtbar) in Akzentfarbe,
CSS-gezeichnet statt blauer Emoji. Version v28.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Import: Dedupe doppelter UIDs innerhalb der Datei (Nextcloud exportiert
wiederkehrende Termine als mehrere VEVENTs gleicher UID) -> kein
UNIQUE-constraint-500 mehr; Commit abgesichert. Test ergaenzt (15 gruen).
- Picker (Gruppen-Sichtbarkeit + Mitglieder): als <div>-Zeilen statt <label>,
damit die globale ".form-group label"-Uppercase/Grau-Regel das Layout nicht
mehr zerschiesst. Saubere .pick-row-Optik (Checkbox/Radio links, Name links).
- Einstellungen haben jetzt eigenen URL-State (#...&settings=1): Reload/Cache-
leeren bleibt in den Einstellungen statt zur Kalenderansicht zu springen.
- Version v27.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Sidebar-Sektion "Gruppen": Liste, Erstellen (Name + Mitglieder-Picker),
Verwalten (Mitglieder hinzufuegen/entfernen), Loeschen.
- Gruppenansicht: laedt /api/groups/{id}/combined fuer den sichtbaren
Bereich; Event-Titel werden mit Besitzer-Initialen bzw. Gruppen-Icon
praefixt; Banner mit "Gruppenansicht verlassen".
- Server: GET /api/local/calendars liefert nun auch Gruppenkalender
(group:true, read_write) fuer Mitglieder, damit sie im Editor waehlbar
sind. Test ergaenzt (13 gruen).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- End-Datum passt sich automatisch an wenn Start geändert wird (Duration bleibt erhalten)
- Erstellen-Button nutzt den aktuell angesehenen Tag statt immer heute
- Monatsansicht: Einzelklick = Tag auswählen, Doppelklick = Tagesansicht, Rechtsklick = Kontextmenü
- CalDAV URL-Matching robuster (Normalisierung, Path-Fallback, calendar_id Parameter)
- iCal-Abo-Termine sind nicht mehr bearbeitbar (Read-Only-Schutz)
- Wiederkehrende Termine mit RRULE-Support (täglich/wöchentlich/monatlich/jährlich/benutzerdefiniert)
Neue Features:
- Lokale Kalender erstellen mit vollem Event-CRUD (in SQLite gespeichert)
- iCal-URLs abonnieren mit Auto-Refresh und lokalem Caching
- iCal-Events sind editierbar/löschbar (Änderungen als lokale Overrides)
- Sidebar zeigt alle 3 Kalendertypen mit Farbe, Umbenennen, Löschen
- Dropdown "Kalender hinzufügen" mit 3 Optionen (Lokal, CalDAV, iCal)
Backend: models.py (4 neue Tabellen), local_router.py, ical_router.py
Frontend: Neue Modals, erweiterte Sidebar, Source-basiertes Event-Routing