- LocalCalendar/CalEvent gain is_birthday + birthday fields; API sends
rrule/external_uid/birth_year and can create/update birthday calendars
- BirthdaysImporter: mirrors Contacts birthdays into a chosen birthday
calendar, reconciling by external_uid (leaves manual entries untouched)
- AccountsView: "birthday calendar" toggle + notify-days when creating a
calendar, and a "Birthdays from Contacts" section (enable + target + sync now)
- FAB long-press menu -> "New birthday" opens a minimal name+date mask
(BirthdayEditorSheet) targeting birthday calendars, with year-unknown option
- Event bars render display_title (age) and a cake icon via EventLabel
- NSContactsUsageDescription added to both app build configs
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New device-local setting "Monatsansicht seitenweise wischen" (@AppStorage, no
server sync). Off keeps the continuous vertical scroll feed; on switches the
month view to a paged TabView — one month per screen, six height-filling week
rows, swipe left/right to change month (bounded ±monthsBack/Ahead range to keep
the TabView light). WeekRow gains a fillHeight flag so the same row renders at a
fixed height (scroll) or fills the page evenly (paged); its inner cells/divider
now size to the actual geometry. Paged selection is two-way bound to the store's
current date so prev/next/today and the title stay in sync.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- UserProfile decodes directory_hidden; updateProfile() sends it; Settings has
a "hide my profile" toggle (excludes you from share/group pickers).
- GroupMember decodes shares_calendar; the group filter list now only shows
members who actually share a calendar (no phantom empty rows).
- Robust custom decoders (decodeIfPresent) for the new fields.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Parse the server's new read_only flag on events.
- Filter sheet: a calendar shared with me is listed under the owner's name
(sharedBy) with a lock icon when I only have read access.
- loadWritableCalendars: exclude read-only shared calendars so they no longer
appear in the event editor (a save would 403).
The group-title prefix removal is server-side and needs no iOS change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hiding or showing a calendar on the web sets enabled=false + sidebar_hidden
server-side, so the server stops returning that calendar's events entirely.
The app only reconciled these per-calendar flags when the filter sheet or the
accounts screen happened to be opened — never on launch, resume or the
periodic pull. So a calendar re-enabled on the web stayed invisible (still
banished locally, and its events weren't in the cache) until a manual sync or
relaunch — the "hide it on the web and the app won't bring it back" bug.
Add CalendarStore.reconcileCalendarVisibility(api:) which pulls the account
lists, updates the banished set from the server's sidebar_hidden flags, and
returns whether anything changed. CalendarHostView now runs it (via
syncFromServer) on launch, on scenePhase .active, in the periodic loop, and on
manual sync — forcing a refetch when the set changed so a re-enabled calendar's
events actually reappear without user intervention.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The per-calendar preservation added in the previous fix only kicks in when a
sync error carries a calendar_id. But account-level failures — most commonly
a Home Assistant or Google token-refresh failure — abort the entire account
fetch and arrive WITHOUT a calendar_id. Those events were still being evicted,
so HA/Google calendars kept vanishing on a transient token hiccup.
failedCalendarKeys() now returns both per-calendar keys and whole sources:
an error without a calendar_id protects every cached calendar of that source.
mergeIntoCache() keeps events whose source is in keepSourcesInRange, and the
cached range is only extended when neither set is populated (auto-retry).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mergeIntoCache() previously extended cachedStart/End even when sync errors
occurred — so isCached() returned true on the next call and the failed
calendar was silently left empty (especially bad on first launch or after
forceReload when there's nothing to preserve).
Now the cached range is only extended on a fully clean fetch. When
keepKeysInRange is non-empty (sync errors present), we leave cachedStart/End
unchanged, forcing isCached() to return false and triggering an automatic
retry on the next loadEvents call.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: mergeIntoCache() unconditionally evicted every event in the
fetched date range, even for calendars whose sync failed. Those calendars'
events were removed and never restored, so the calendar appeared to vanish.
Fix:
- SyncError now decodes calendarId from the server's "calendar_id" field
(added server-side in e0ea16f but not yet read by iOS).
- mergeIntoCache() gains a keepKeysInRange parameter: events from failed
calendars are retained (not evicted) even within the fetch window.
- loadEvents() and prefetchBackground() compute the failed keys from the
syncErrors list and pass them to mergeIntoCache.
Result: when CalDAV / Google / HA sync fails for a specific calendar, the
user sees stale-but-correct events alongside the existing error banner,
instead of a completely empty calendar with no explanation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The server now reports per-calendar sync failures (e.g. expired
credentials) alongside an otherwise-successful events response, via an
"errors" array. Previously such failures were silently swallowed, so a
calendar could appear enabled while showing zero events with no
explanation.
- Decode the new "errors" field into SyncError (source/name/message),
following the existing JSONSerialization + from(json:) convention.
- CalendarStore.syncErrors captures these on successful personal
fetches (loadEvents, prefetchBackground); left untouched on a hard
fetch failure so it isn't conflated with lastError. Group overlay
fetches never populate it (the endpoint doesn't apply there).
- CalendarHostView shows a second red warning banner alongside the
existing lastError banner, reusing the same visual style.
Bug fix: publishWidgetSnapshot() now guards against activeGroup != nil,
so group view events/colors never contaminate the widget cache.
Feature: widgets can now be configured via long-press → Edit Widget.
- WidgetCalendar struct + writeCalendars/readCalendars in WidgetData.swift
- calendarKey added to WidgetEvent (backward-compatible decoder)
- CalendarIntent.swift: CalendarAppEntity + CalendarEntityQuery + CalendarSelectionIntent
- CalendarrTimelineProvider migrated from TimelineProvider to AppIntentTimelineProvider
- All 13 StaticConfiguration widgets changed to AppIntentConfiguration
- publishWidgetSnapshot() builds + writes the calendar list for the intent
An empty selection (default) shows all calendars; selecting specific
calendars filters the widget events accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reminder editor: presets + custom number+unit (minutes/hours/days/weeks)
- Grey out + footer hint when the selected calendar's reminders are muted;
reminders are kept, scheduler already skips them
- New synced setting defaultEventDurationMinutes for new events
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop 10 min / 2 h / 2 days, add 1 week, and use full-word "… vorher" /
"… before" labels so the choices match the web client.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
C1 — Localization: route the remaining hardcoded German strings through
L10n (LoginView, ServerSetupView, SettingsView email, EventDetailSheet) so
"System Default" + English device language shows fully English text.
C2 — Per-calendar reminders: parse the new reminders_enabled flag on every
calendar type; CalendarStore persists a reminderDisabledKeys set and passes
it to NotificationScheduler, which skips events of muted calendars (default
and per-event reminders). Filter sheet gains a per-calendar reminder toggle
(leading swipe + bell.slash indicator), reconciled from the server and
synced back via PUT.
C3 — Widgets:
- Shared WidgetTime.range helper; Today / Today & Tomorrow / Three Days /
Up Next now show start–end instead of only the start time.
- This Week: show up to 6 events per day (was 3) to use the height.
- Two Weeks: mini event-title pills instead of bare dots.
- Two Months: weeks expand to fill the column (no more empty lower third).
- Day & Events: smaller header/strip/rows so content stops clipping.
- Next 5 days → Next 7 days (range + labels), higher row cap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per-event reminders (multiple, local calendars only) in the editor, prefilled
from a new "default reminder" setting that applies to all events otherwise.
CalEvent gains `reminders`; AppSettings/SettingsSync sync default_reminder_minutes
(always group). New NotificationScheduler requests permission and schedules the
soonest ≤60 upcoming reminders via UNUserNotificationCenter, rescheduling on
load/sync/edit and when the default changes (skipped in group overlay).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The system NavigationStack toolbar title would not refresh on a plain @State
change (title kept disappearing on iPhone). Moved visibleMonth into the
@Observable CalendarStore so the toolbar's read is tracked with @Observable's
fine-grained observation and refreshes on month change. Reverted the @State/.id
workaround. Real system glass bar retained.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The calendar filter, when a group overlay is active, now lists the group's
members (+ the shared group calendar) and lets you hide each one individually
(Outlook-style). Filtering is client-side via CalendarStore.hiddenGroupKeys
(per-member gm:<id> / group-calendar gc keys), reset when switching groups.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CalEvent parses display_title; the combined view uses it (group icon + owner
prefix from the server) instead of client-side decoration, with a fallback for
older servers. Raw title kept for editing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Liquid Glass: the calendar content now scrolls underneath a translucent
safeAreaInset bar (real glass look restored) while the inline title stays
reliable — toggling Liquid Glass is visibly different again.
- Group events are prefixed with the group's own emoji icon (from group
settings) instead of a generic people glyph, so they're recognisable.
- Week view: today's column header now uses the configured "today" colour
instead of the accent colour (matches the current-time line).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Der Gruppen-Modus ersetzte events nur mit dem schmalen aktuellen Fetch, statt
denselben Cache/Prefetch-Pfad wie die Normalansicht zu nutzen -> Termine
erschienen erst nach Scrollen. Jetzt fetchForMode (personal/group) läuft durch
loadEvents + prefetchBackground + refreshFromCache; Moduswechsel lädt breit neu.
In der Gruppenansicht greift der "ausgeblendet"-Filter nicht.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gruppen sind nicht mehr nur im Menü versteckt: im Top-Bar gibt es einen
Gruppen-Umschalter (Persönlich / <Gruppe>). Beim Wählen einer Gruppe zeigt
der echte Monats-/Wochen-/Tagesansicht die kombinierte Überlagerung
(GET /groups/{id}/combined) mit server-definierten Farben und
Besitzer-Präfix; ein Banner "Gruppenansicht: <Name>" mit "Verlassen".
CalendarStore.activeGroup steuert den Modus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ein doppelter Dictionary-Key liess die App beim Start abstürzen
("Dictionary literal contains duplicate keys"). Meine Dublette entfernt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Kalenderverwaltung: pro lokalem Kalender ein Menü mit Teilen (SharingView:
Benutzer aus Verzeichnis, read/read_write, entfernbar), Importieren
(.ics File Picker) und Exportieren (Share Sheet). Geteilte Kalender mit
"geteilt von"-Badge; Gruppenkalender markiert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neue Settings-Sektionen: Anzeigename + E-Mail ändern (Login-Name read-only),
Private-Termine-Sichtbarkeit (busy/hidden) und Auswahl des für Gruppen
sichtbaren Kalenders. Gezielte API-PUTs, damit nicht die ganze AppSettings
überschrieben wird.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add two-way settings sync (SettingsSync) with toggle, app-start/foreground/
10-min pull and debounced push; server wins; view/week-start/dim-past always
sync. Wire previously-ignored settings (hour height, contrasts, week start,
default view, dim past) into the actual UI.
- Make AppSettings decoding resilient (decodeIfPresent) so getSettings no longer
fails on iOS-only fields the server omits; keep text/bg/line colors local-only;
month divider/label colors now sync.
- Auto-refresh after create/edit (cache-busting) and optimistic removal on
delete; switch delete confirm to a centered alert. Add HA event deletion.
- Calendar visibility: fix inverted hide/show toggle; normalize calendar keys so
local filtering works for all sources; sync banish with server sidebar_hidden
(CalDAV/Google/HA), refetch on un-banish.
- Manual "sync with server" button in the menu.
- Upcoming widget shows next 5 days (renamed).
- Week/Day view: route multi-day timed events to the all-day strip so they no
longer render as a full-height block.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Vertical-scroll month view with multi-day event spans, zig-zag month
divider, CW number per week, on-demand event loading while scrolling
- Top bar redesign: icon-only view picker on right, month title centered
- Long-press context menus on day cells (month) and hour slots (week/day)
for "New event", "Open in week view", "Open in day view", "Open in month view"
- Localization system with system/de/en switch covering top bar, view picker,
settings, menu, profile, server, accounts, event editor, agenda
- Three new color pickers (text/background/line) + today-marker color
applied in calendar views; current-time line now uses today color
- App icon: removed alpha channel, accent color set to icon green (#20A050)
- TestFlight: ITSAppUsesNonExemptEncryption=NO baked into Info.plist keys
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>