Commit Graph
8 Commits
Author SHA1 Message Date
ScarriffleandClaude Opus 5 25f015a15b Let each person decide whether a shared calendar reminds them
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>
2026-10-24 17:20:14 +02:00
ScarriffleandClaude Opus 5 b885e731ee feat(anhaenge): attachment_count im Termin-JSON
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>
2026-09-14 10:11:30 +02:00
ScarriffleandClaude Opus 4.8 ca09538971 Add birthday calendar backend support
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>
2026-07-12 21:38:37 +02:00
ScarriffleandClaude Opus 4.8 f844ded57d fix(web): hide edit/delete for others' events; persistent hide for shared calendars
Bug 1 — a calendar shared with me stayed visible after unchecking it: the hide
was a one-shot cache filter the server undid on refetch. Add a per-device
hidden set (localStorage 'hiddenLocalCalendars'), honoured in filterEvents
(normal view) and used to drive the checkbox state, so it survives refetch/reload.

Bug 2 — in the group combined view, other members' events showed edit/delete and
403'd on save. The combined endpoint now emits read_only (editable = the group
calendar OR my own events), via a read_only param threaded through
build_local_event_dict/expand_recurring_local. The event popup and edit modal now
treat read_only events as read-only (copy still allowed). Test added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 10:11:46 +02:00
ScarriffleandClaude Opus 4.8 bff9a244e7 feat: per-event reminders + default reminder setting (server)
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>
2026-06-06 16:07:19 +02:00
ScarriffleandClaude Opus 4.8 6869a15bb8 fix(security): stop private-event leak in merge read + harden busy masking, uploads, profile
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>
2026-06-01 17:49:56 +02:00
ScarriffleandClaude Opus 4.8 f9923b022e feat: Login-Name vs. Anzeigename (Server)
- Neue Spalte users.display_name (Original-Schreibweise); username bleibt der
  lowercase Login-Name. Setup/Create setzen display_name aus der Eingabe.
- Login bleibt case-insensitive (Anzeigename eingeben funktioniert -> wird
  lowercased -> trifft den Login-Namen).
- Profil: PUT /api/profile/ kann display_name UND username (Login-Name) aendern;
  bei Login-Namen-Wechsel kommt ein frischer Token zurueck (JWT sub haengt am
  Namen). Stabile interne ID (Integer-PK) traegt alle Verweise -> Umbenennen
  bricht Shares/Gruppen/creator_id nicht.
- display_name ueberall ausgeliefert/genutzt (me, profile, users, directory,
  shares, Gruppen-Mitglieder, creator/owner, ORGANIZER-Export).
- Migration + Backfill (display_name = username). Tests ergaenzt (17 gruen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 17:40:38 +02:00
ScarriffleandClaude Opus 4.8 32268a18b2 feat: Kalender-Sharing, Gruppen, iCal Import/Export & Ersteller (Server)
Kollaborations-Features ausschliesslich fuer lokale Kalender:

- Sharing: calendar_shares-Tabelle, GET/POST/DELETE /api/local/calendars/{id}/shares
  (nur Besitzer), GET /api/users/directory, geteilte Kalender in
  GET /api/local/calendars (shared_by/permission/owned) und im Merge-Read.
- Gruppen: groups/group_members/group_calendars + /api/groups-Router inkl.
  kombinierter Ansicht /api/groups/{id}/combined (owner + is_group_event).
- Ersteller: local_events.creator_id (serverseitig gesetzt) + creator_name_external
  aus ORGANIZER; creator-Feld in allen lokalen Event-Responses.
- Private-Flag: local_events.is_private + user_settings.private_event_visibility
  (hidden|busy), Filterung in der Gruppenansicht.
- iCal Import/Export: ical_io.py, POST /api/local/calendars/{id}/import,
  POST /api/local/import, GET /api/local/calendars/{id}/export.
- Zentraler Berechtigungs-Helper (permissions.py) und gemeinsamer Event-Dict-
  Builder (local_events_util.py) ersetzen die Nur-Besitzer-Filter.
- pytest-Suite (12 Tests) fuer Sharing, Gruppen, Parser, Private-Filterung.

Additiv & rueckwaertskompatibel; Migrationen in main.py._migrate().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 16:05:18 +02:00