68 Commits
Author SHA1 Message Date
ScarriffleandClaude Opus 5 e5b3e549e8 Catch multi-day events that do not end on a day boundary
Requiring both ends to sit exactly on midnight was too strict. A holiday stored
as the 16th at 00:00 through the 21st at 01:00 slipped through and rendered as
a one-hour slot. Starting on a day boundary and running into a later day is
enough to call it all-day.

The agenda row now names the weekday on both sides when an event crosses a day.
Showing only "00:00 – 01:00" for something that runs from Wednesday to Monday
is true and useless.

The timezone handling above this is deliberately untouched: timed events show
the right time, so the UTC assumption for timestamps without an offset is not
what produced the odd hour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 14:52:52 +02:00
ScarriffleandClaude Opus 5 465c465af2 Treat a whole-day span as all-day, whatever the payload claims
The previous rule only caught date-only strings. A multi-day event sent with
full timestamps and no allDay flag still arrived as a timed event, so a holiday
running from the 16th to the 21st rendered as a one-hour slot at midnight.

Now an event that starts and ends on a day boundary and covers at least one
whole day is all-day regardless. That is a statement about what the data means,
not about how any one client formats it, which is why it lives in the parser
and not in a view.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 14:49:57 +02:00
ScarriffleandClaude Opus 5 6e91eaaaa2 Read all-day events correctly, and make the date circle say a date
Three things, one of which is not a watch problem at all.

parseDate already treated a date-only string as all-day, but isAllDay came from
the JSON flag alone. An event sent as "2026-09-12" with no flag therefore
landed at midnight with isAllDay false, and every surface that formats a time
put "00:00" in front of it — the widgets as much as the watch. Now derived from
both.

The date complication showed the day number and nothing else, because
.accessoryCircularCapacity renders only currentValueLabel and the month sat in
the label closure, where it is never drawn. It now stacks weekday over day
number, which is what makes it read as a date rather than a number.

The ring only appears while an appointment is running, and then shows how far
through it we are. An empty track around an idle day said nothing, so there is
no Gauge at all in that case. All-day events do not count as running: one would
hold the ring at an arbitrary fraction for twenty-four hours.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 14:10:21 +02:00
ScarriffleandClaude Opus 5 b28c5a0502 Send the trimmed snapshot to the watch over WatchConnectivity
App Groups are per-device, so the watch cannot read the file the phone writes
and has to be handed the data. Three delivery paths: application context for
the latest state, a complication push only when the next appointment actually
changed (the budget is small), and a reply to the watch asking directly.

Sign-out pushes an empty signed-out snapshot. Clearing our own container is not
enough — the watch holds its own copy and would keep rendering the previous
user's calendar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 16:40:45 +02:00
ScarriffleandClaude Opus 5 ad7a405f32 Refresh the shared snapshot in the background so widgets stop going stale
publishWidgetSnapshot() only ever ran from a user action, so an event created
on the web or on Android did not reach the widgets until the app was opened
again. A BGAppRefreshTask now republishes without the foreground.

The two keys this needs are arrays, which Xcode does not expose as
INFOPLIST_KEY_ build settings — verified by building and inspecting the
result. Hence a partial Calendarr-Info.plist that generation merges into. It
sits at the project root rather than inside the target folder: that folder is
a synchronized group, which would also copy the file as a resource and collide
with the processed one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 16:39:34 +02:00
ScarriffleandClaude Opus 5 75a6a5bd79 Fix two OIDC compile errors against the current SDK and AppAuth
UIAccessibilityIsGuidedAccessEnabled() is gone in iOS 26; use the property.

AppAuth declares lastAuthorizationResponse and its request as nonnull, so the
optional chain was rejected. Behaviour is unchanged: nonce is optional either
way, which is the tolerance the comment there already describes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 16:35:23 +02:00
ScarriffleandClaude Opus 5 7b51cab693 feat(auth): SSO-Anmeldung ueber OpenID Connect (AppAuth)
Zusaetzlicher Anmeldeweg neben Benutzername/Passwort. Der Login-Screen holt
die konfigurierten Provider von GET /api/auth/oidc/providers; ohne Provider
oder gegen einen aelteren Server bleibt der Screen unveraendert.

Die App laeuft als Public Client: Authorization Code Flow mit PKCE in einer
ASWebAuthenticationSession (nie ein WKWebView), ohne Client Secret. Das
ID-Token wird an POST /api/auth/oidc/exchange gegen ein Calendarr-Token
getauscht; die Antwort hat dieselbe Form wie der Passwort-Login, sodass der
bestehende Session-Pfad unveraendert weiterlaeuft.

- OIDCAuthenticator kapselt Discovery, PKCE-Request, Code-Tausch und die
  stille Erneuerung. Der AuthState mit dem Refresh-Token liegt in der
  Keychain (kSecAttrAccessibleAfterFirstUnlock) und wird beim Logout aus
  beiden Zugriffsgruppen entfernt.
- OIDCSessionRefresher erneuert die Sitzung beim Start, wenn das
  Calendarr-Token binnen 24 h ablaeuft — ohne Browser, ueber den
  Refresh-Token des Providers.
- Die angefragten Scopes kommen vom Server (mobile_scopes); ein Scope, den
  der Provider nicht kennt, wuerde die ganze Anfrage scheitern lassen.
- Bei aktiviertem gefuehrten Zugriff weicht AppAuth auf SFSafariViewController
  aus, dessen Redirect nur ueber ein registriertes URL-Schema zurueckkaeme.
  Da das App-Target seine Info.plist generiert und CFBundleURLTypes dort
  nicht setzbar ist, wird der Fall vorab sauber abgelehnt statt endlos auf
  einen Callback zu warten, der nicht kommen kann.
- Abbrueche werden mit Fehlerdomaene geprueft (user- und programCanceled) und
  still behandelt; alle uebrigen Fehler tragen Domain, Code und OAuth-Fehler
  ins DEBUG-Log, statt spurlos zu verschwinden.

project.pbxproj: AppAuth-iOS 1.7.5 als SPM-Abhaengigkeit, nur am App-Target.
OTHER_LDFLAGS bekommt -ObjC, weil OIDAuthState+IOS.m ausschliesslich eine
Kategorie enthaelt und ohne Symbol sonst wegoptimiert werden koennte.

Achtung: mangels macOS/Xcode ist dieser Stand nicht kompiliert. Beim ersten
Oeffnen loest Xcode das Paket auf und legt Package.resolved an; die bitte
mit committen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 18:33:59 +02:00
ScarriffleandClaude Opus 5 d1da8de06a Read and write the widget snapshot through CalendarrCore
The snapshot format was an internal detail shared between the app and its own
widget extension. A second Mac app needs the same data, and the way to give it
that is a documented contract rather than a format it reverse-engineers and then
drifts from. Both targets now link CalendarrCore and go through SnapshotStore.

Shared/WidgetData.swift becomes a thin facade. The typealiases and the flat
colour accessors exist so the ~70 existing call sites across the app and the
widget views compile unchanged; they are a migration convenience, not a design.

The snapshot now carries what a reader outside this app actually needs:

coverageStart / coverageEnd, because the published window is ~7 days back and
~42 ahead. Outside it the snapshot holds no information, which is not the same
as holding no events — and only the writer knows where that edge is. Without it
a consumer renders a convincingly empty March and is simply wrong.

isLoggedIn plus a session record, so a reader can say "sign in to Calendarr"
rather than "open Calendarr once". The events are deleted on sign-out, so the
absence of a cache alone cannot tell those two apart.

writerVersion, purely so a mismatch between the two apps is diagnosable.

The coverage constants move to SnapshotCoverage, so the writer and the code that
reconstructs the window for older files can no longer disagree about it.

CalendarStore needs an explicit `import CalendarrCore` because the app target
builds with SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY, which requires the
defining module to be imported directly rather than picked up transitively.

Verified: builds for iOS and Mac Catalyst; CalendarrKit's 11 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 17:42:00 +02:00
ScarriffleandClaude Opus 5 0d2ad6e021 Give Mac Catalyst its own App Group prefix and sandbox entitlements
The App Group identifier registered in the portal does not change, but the
string the runtime expects does: macOS and Mac Catalyst require the Team ID
prefix, iOS forbids it. CalendarrAppGroup now resolves the right one per
platform. Getting this wrong is the worst failure mode in the whole port —
containerURL() returns nil, every snapshot read and write quietly no-ops, and
the widgets show placeholder content forever with no error anywhere. A DEBUG
assertion now makes that loud during development.

Because the two platforms need different values, they need different
entitlements files — listing both strings in one file breaks iOS provisioning
on the unregistered prefixed value. Selected via CODE_SIGN_ENTITLEMENTS[sdk=macosx*],
verified to resolve correctly for both destinations.

The Catalyst entitlements are written App Store grade from the start, so one
configuration serves both the Mac App Store and a notarized DMG: sandbox,
network client, Contacts (birthday import), user-selected files (.ics import
and export), the prefixed App Group, and keychain sharing. Deliberately absent:
files.downloads, network.server, device.*, temporary-exception.* — nothing needs
them and each is App Review friction. ENABLE_HARDENED_RUNTIME is required for
notarization and ignored by the App Store, so it is safe to set unconditionally.

Verified: builds for both Mac Catalyst and iOS Simulator.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 17:03:33 +02:00
ScarriffleandClaude Opus 5 781ccd1752 Make Keychain and shared-container state correct before the Mac port
Two failures here are silent rather than loud, which is why they have gone
unnoticed on iOS and would have been much harder to diagnose on a Mac.

The old `enum Keychain` discarded all four OSStatus results. On Mac Catalyst a
missing `keychain-access-groups` entitlement makes SecItem calls fail with
errSecMissingEntitlement, and because nothing checked, that was indistinguishable
from "no token stored" — the user would be signed out on every launch, with no
error anywhere. KeychainStore now checks every status, distinguishes
errSecItemNotFound from real failures, and sets kSecUseDataProtectionKeychain so
macOS selects the modern entitlement-gated keychain instead of the legacy login
keychain.

Adding the entitlement moves the default access group to the first array entry,
so that entry is deliberately the app's own group: existing tokens keep
resolving with an unqualified query and nobody is signed out. loadToken() then
migrates forward in three steps — shared group, own default group, and the
pre-Keychain UserDefaults copy.

If the entitlement is not provisioned yet, KeychainStore falls back to the
default group rather than throwing. A hard failure would make the app unusable
for everyone whose provisioning lags; the fallback asserts in DEBUG instead, so
a misconfiguration is loud in development and survivable in production.

Separately, logout() left widget-cache.json in the App Group container, so
widgets kept rendering the signed-out user's events indefinitely. That is an
existing iOS bug, and it would have leaked the same data to any other app
reading the container. WidgetStore.clear() now removes both cache files and
reloads the timelines.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 16:31:06 +02:00
ScarriffleandClaude Opus 4.8 554ad425b1 Week view: render multi-day all-day events as continuous spanning bars
The all-day strip repeated a multi-day event once per day; now events span their
start→end columns and pack into lanes (like the month view) via a GeometryReader
layout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 11:20:21 +02:00
ScarriffleandClaude Opus 4.8 7981ced5ba iOS drawer: declutter controls, bigger header, more top spacing
- remove the show-all / hide-all buttons (rarely needed with a few calendars;
  the web has none). The calendar section now shows a 'Calendars' header + a
  slim Sort toggle.
- larger header (avatar, name, action icons) with more padding
- more vertical breathing room between header / view switcher / groups / list

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 18:06:56 +02:00
ScarriffleandClaude Opus 4.8 6881c68935 iOS drawer: flat reorderable calendars, long-press menu, left-swipe close, header actions
Testing feedback:
- calendars are now one flat, drag-reorderable list (no per-source grouping);
  order is device-local (CalendarStore.calendarOrder, mirrors the web cal_order).
  Reordering is done via a 'Sort' toggle (native edit mode with drag handles).
- banish + reminder-mute moved from row swipe actions to a long-press context
  menu, which frees the horizontal swipe: swiping the drawer left closes it.
- the hamburger moved to the LEFT (drawer opens left); new device-local setting
  'Hide menu button' (open via edge-swipe only), not synced.
- the settings entry moved from the footer into the header as an icon, next to a
  restored manual-sync icon and the close button; the footer button is gone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:19:29 +02:00
ScarriffleandClaude Opus 4.8 daf0345120 iOS drawer: drop the grouped-section card behind show/hide-all
listRowBackground(.clear) so only the two bordered button pills show, without
the surrounding grouped-list capsule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 06:46:32 +02:00
ScarriffleandClaude Opus 4.8 108503ad53 iOS: month-switching setting as a named dropdown (parity with web)
Replace the boolean "month view as pages" toggle with a Monatswechsel dropdown:
Continuous scroll vs. Page by page — same names/format as the web client.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 06:45:22 +02:00
ScarriffleandClaude Opus 4.8 e008d5d98a iOS drawer: compact show/hide-all, single menu button, corner-safe footer
Testing feedback:
- show-all / hide-all are now side by side (bordered), not two big stacked rows
- the busy quick-access strip becomes a single "Einstellungen" button that
  opens the full menu (which already holds accounts/profile/groups/server/sync/
  logout)
- extra horizontal + bottom padding so the footer button is not clipped by the
  iPhone's rounded corners / home indicator

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 06:09:38 +02:00
ScarriffleandClaude Opus 4.8 d90a1a552c iOS: left side drawer (calendars + groups + nav) and opt-in top-bar colour
Replace the top-bar menu popup with an off-canvas left drawer, opened by the
hamburger or a narrow left-edge swipe (kept off the content area so month/week
swipe stay free). The drawer hosts the calendar visibility list, a view
switcher, group switching, and quick access to Settings/Accounts/Profile/
Groups/Server plus sync and logout.

- extract the filter list into reusable CalendarFilterContent (shared by the
  drawer and the still-present CalendarFilterSheet); the sheet is now a thin
  wrapper
- new CalendarDrawer view; CalendarHostView hosts it as a ZStack overlay with a
  scrim and edge-swipe gesture, and presents drawer destinations as sheets
- opt-in surface colour: surfaceColor (device-local) tints the flat top bar;
  empty keeps the translucent .bar material. Settings row with an Auto reset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 22:24:24 +02:00
ScarriffleandClaude Opus 4.8 9a50f25f57 iOS: per-setting sync table with sync icons, dropdowns, colour resets
Generalise the two-tier SettingsSync into a server-driven flag map: each
syncable key has an account-wide flag (fetched from the server); pull applies
only on-flagged keys, push (read-modify-write) sends only on-flagged keys plus
the flag map. Per-row link icon toggles a setting's sync; a global switch flips
all. Enabling a flag adopts this device's current value as the shared one.

- AppSettings: fix bg_color mapping (was background_color), add cache_months,
  month_view_paged, sync_flags
- SettingsView: uniform rows (sync icon | name | value), ContrastSelector and
  the button pickers replaced by menu dropdowns, every colour gets a reset to a
  canonical default, global "sync all" toggle; account settings (privacy,
  group calendar, hide profile) kept in their own block; language and text/line
  contrast kept device-local (iOS-specific)
- unify default line colour to #3A3A52 across views

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 21:31:16 +02:00
ScarriffleandClaude Opus 4.8 d4fd7beaf7 Birthdays: explicit calendar creation, sync never auto-creates (iOS)
- Contacts sync fills only an EXISTING birthday calendar (birthdayCalendar),
  never auto-creates it — deleting it no longer resurrects it on sync
- Accounts + menu gains "Geburtstagskalender" (only when none exists) to create
  the single birthday calendar explicitly
- the Birthdays-from-Contacts section shows a "create one first" hint when there
  is no birthday calendar; enabling sync only requests Contacts access

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 21:25:06 +02:00
ScarriffleandClaude Opus 4.8 69ed8bf67d Birthdays: single calendar model + sync on server-sync (iOS)
- one birthday calendar per user; BirthdaysImporter auto-creates/targets it
  (no picker) and reconciles per-device (external_uid contact:<deviceId>:<id>)
  so multiple devices don't clash
- Contacts birthday sync now runs on every "Sync with server" (syncFromServer),
  not just app launch and the manual button
- report the device after each sync (POST /api/birthdays/sync-report) for the
  web device list
- Accounts: Birthdays-from-Contacts section drops the target picker, keeps the
  reminder picker; removed the birthday toggle from the generic new-calendar sheet
- New-birthday sheet targets the single calendar, offers activation if none
- exclude the birthday calendar from the group-visible ("shared calendar") picker

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 19:07:46 +02:00
ScarriffleandClaude Opus 4.8 cf990e4279 Add birthday feature (iOS)
- 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>
2026-07-12 21:58:31 +02:00
ScarriffleandClaude Opus 4.8 e55e6c7c92 feat(ios): toggle month view between scroll feed and paged swipe
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>
2026-07-08 05:15:58 +02:00
ScarriffleandClaude Opus 4.8 c4195243c0 feat(sharing): let share recipients recolour a shared calendar
The colour dot for a shared (non-owned) local calendar was disabled and the
colour write hit the owner-only PUT /calendars/{id} (404 for recipients). The
dot is now always editable and colour changes go through the dedicated
PUT /calendars/{id}/color endpoint, storing a recipient's own per-user colour
server-side (synced across devices) without granting write or rename. Renaming
stays owner-only. Parity with Android + server.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 20:57:53 +02:00
ScarriffleandClaude Opus 4.8 32cb8e1689 feat(sharing): hidden-profile toggle + hide non-sharing group members
- 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>
2026-07-06 18:45:49 +02:00
ScarriffleandClaude Opus 4.8 3f5de5cdfa fix(security): store auth token in Keychain and restrict ATS to local networking
- Move the bearer token from UserDefaults to the Keychain (accessible after
  first unlock), with a one-time migration so existing logins survive.
- Replace NSAllowsArbitraryLoads=YES with NSAllowsLocalNetworking=YES so ATS
  still permits cleartext to LAN/self-hosted servers but enforces TLS for
  public hosts (no arbitrary cleartext/MITM).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 14:32:01 +02:00
ScarriffleandClaude Opus 4.8 9db04361cc feat(sharing): show shared calendars read-only (parity with server/Android)
- 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>
2026-07-04 18:56:32 +02:00
ScarriffleandClaude Sonnet 4.6 325f357357 fix(visibility): auto-reconcile server sidebar_hidden on launch/resume/sync
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>
2026-07-03 22:04:58 +02:00
ScarriffleandClaude Sonnet 4.6 61237134f0 fix(cache): preserve whole source on account-level sync errors (HA/Google tokens)
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>
2026-07-03 21:31:05 +02:00
ScarriffleandClaude Sonnet 4.6 fb22aeb090 fix(cache): retry failed-calendar ranges instead of marking them as cached
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>
2026-07-03 20:55:57 +02:00
ScarriffleandClaude Sonnet 4.6 1cbab3f3ec fix: preserve cached events when a calendar has a per-source sync error
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>
2026-07-03 20:06:11 +02:00
Scarriffle 8ed474663e feat: surface per-calendar sync errors from /api/caldav/events
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.
2026-07-02 18:25:47 +02:00
ScarriffleandClaude Sonnet 4.6 4e9ae83299 feat(widget): calendar filter config + fix group-view data leak
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>
2026-06-17 18:41:43 +02:00
ScarriffleandClaude Sonnet 4.6 52040fff53 feat: show relative time in notifications + move default duration to profile settings
Notifications now show "in 30 Min. · 10:00" instead of just the time,
giving users immediate context about how soon the event is.

Default event duration picker moved from the View section to right below
the privacy setting, grouped under the calendar/profile settings area.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 19:38:20 +02:00
ScarriffleandClaude Opus 4.8 cc3d16ddce feat: custom reminder picker, muted-calendar hint, synced default duration
- 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>
2026-06-15 10:03:24 +02:00
ScarriffleandClaude Sonnet 4.6 1dbe405689 iOS: replace chevron icons with clip-path shape, fix +N overlap in month view
- DayPreviewAllDayBar: use custom ChevronBarShape (polygon clip) instead of
  chevron SF Symbols; bars have tapered pointed ends for continuation
- MonthView rowHeight: increase bottom padding from +4 to +16 so the
  overflow "+N" count no longer overlaps the last event bar lane

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 20:34:58 +02:00
ScarriffleandClaude Sonnet 4.6 17ebf788ce iOS: widget padding, week event limit, long-press day preview
- CalendarDayWidget: add 6pt top padding to header so content isn't flush
  against the widget edge
- ThisWeekWidget: increase per-day event cap from 6 to 8 to prevent "+1"
  overflow when there is vertical space available
- MonthView: add DayContextPreviewView to the long-press context menu using
  .contextMenu(menuItems:preview:); shows all-day events as colored bars
  with chevron continuation arrows, timed events as dot+time+title rows

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 20:15:53 +02:00
ScarriffleandClaude Opus 4.8 864eb31072 iOS: align reminder options with web (5/15/30 min, 1 h, 1 day, 1 week)
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>
2026-06-09 20:32:34 +02:00
ScarriffleandClaude Opus 4.8 c0edca338e iOS: localization fixes, per-calendar reminders, widget polish
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>
2026-06-09 20:14:39 +02:00
Scarriffle 13d80981c6 Liquid Glass ICON 2026-06-08 19:20:10 +02:00
ScarriffleandClaude Opus 4.8 59a879ea23 fix: move Today button to the right so the month title sits centered (iOS)
Left side now holds only the prev/next chevrons; "Heute" moved next to the
burger menu on the right (both flat and Liquid Glass bars), so the month title
is centered again instead of pushed right by a heavy left cluster.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 10:14:08 +02:00
ScarriffleandClaude Opus 4.8 f480b438cb feat: top bar declutter — view/filter/groups/sync into one burger popup (iOS)
The top bar now shows only nav + title + a single burger. Tapping it opens a
compact menu: View (with a fixed icon, no longer per-view), Filter, Groups
(if any), Sync, and an "Einstellungen" entry that opens the existing full menu.
Removed the separate group/view/filter icons from both the flat bar and the
Liquid Glass toolbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 16:23:56 +02:00
ScarriffleandClaude Opus 4.8 587a0e65fa feat: event reminders + default reminder setting + local notifications (iOS)
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>
2026-06-06 16:21:08 +02:00
ScarriffleandClaude Opus 4.8 e7d8effb47 fix: Liquid Glass month title as inline content strip (not the system toolbar)
The NavigationStack toolbar title never refreshes on month change on iOS 26
(4 approaches tried: principal Text, navigationTitle, @Observable store). The
title now renders as a normal inline Text in a top safe-area inset just below
the system glass toolbar — the same mechanism as the flat variant, which does
update. The system toolbar keeps the buttons + the real Liquid Glass look.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 21:30:07 +02:00
ScarriffleandClaude Opus 4.8 68349d36e5 feat: non-emoji group icons (SF Symbols) for consistent cross-platform look
Group icons are now semantic keys (people/home/heart/work/school/sports/party/
pet/travel/music/food/star) rendered as SF Symbols in the picker, group list,
switcher, banner and filter — instead of OS emoji that looked different on every
platform. Legacy emoji values still render as a fallback. GroupCombinedView uses
the server display_title.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 19:20:32 +02:00
ScarriffleandClaude Opus 4.8 451d3d4d6b fix: Liquid Glass month title updates via @Observable store (visibleMonth)
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>
2026-06-01 19:06:53 +02:00
ScarriffleandClaude Opus 4.8 51218b9aa3 fix: restore real Liquid Glass bar; drive title via @State so it refreshes
The custom safeAreaInset bar removed the actual iOS-26 Liquid Glass look
("no glass even though enabled"). Restored the system NavigationStack glass
toolbar and instead fixed the disappearing month title by driving it from a
@State (navTitle) updated via onChange(of: titleString) and keyed with .id,
which forces the system bar to refresh on month change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 18:22:16 +02:00
ScarriffleandClaude Opus 4.8 b61a90d960 feat: hide individual member calendars in the group view (iOS)
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>
2026-06-01 18:00:30 +02:00
ScarriffleandClaude Opus 4.8 b9547c15f9 feat: render server display_title for group events (consistent across clients)
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>
2026-06-01 17:54:23 +02:00
ScarriffleandClaude Opus 4.8 8521a28520 fix: visible Liquid Glass again, group icon on group events, week today colour
- 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>
2026-06-01 17:22:02 +02:00
ScarriffleandClaude Opus 4.8 7f76df2600 fix: Liquid Glass month title now updates reliably (custom glass bar)
navigationTitle/principal items in the NavigationStack toolbar silently fail to
refresh on scroll-driven state changes (visibleMonth), so the month label
vanished on month change and only returned after an unrelated rebuild. The glass
variant now uses a custom top bar with the same inline Text title as the flat
variant (proven to update), styled with glassEffect (iOS 26) / ultraThinMaterial.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 09:00:48 +02:00