Commit Graph
24 Commits
Author SHA1 Message Date
ScarriffleandClaude Opus 5 f9bc1965a5 Add the watch app and its eight complications
The watch receives the phone's payload over WatchConnectivity and writes it
into its own App Group container through the same SnapshotStore, so there is
one wire format and one reader. The agenda, the detail screen and the filter
all read that; the complications read what the app last wrote, since an
extension cannot run a WCSession of its own.

Complication timelines land on event boundaries rather than a schedule, so the
next appointment flips in the exact minute it changes without spending refresh
budget.

CalendarrWatchShared holds what both watch targets need, attached to each the
way Shared/ is attached to the app and the widgets.

recommendations() is implemented because watchOS requires it — iOS has a
default, the watch does not, since the complication gallery has to be
populated. Found by building, not by reading.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 17:08:27 +02:00
ScarriffleandClaude Opus 5 9394a80ea2 Add the watchOS app and complication targets
The companion dropdown in Xcode's target template came up empty, which left
both bundle identifiers starting with a bare dot and
WKCompanionAppBundleIdentifier as an empty string. Both are set explicitly
here, along with the App Group entitlements and watchOS 26 as the floor.

The iOS app had no Embed Watch Content phase, so the watch app would have built
and never shipped. Added, with the matching target dependency: the product now
lands at Calendarr iOS.app/Watch/CalendarrWatch.app with the complications
inside it.

Folders follow what they contain: CalendarrWatch is the app,
CalendarrWatchComplications the extension. Xcode had them the other way round.

Package.resolved is now tracked. It pins AppAuth, whose version drift is what
produced the two compile errors fixed in 75a6a5b.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 16:56:26 +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 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 84f5c00516 Keep iOS iPhone-only; give the iPad family to Mac Catalyst alone
Enabling Mac Catalyst pulled in the iPad device family, which made App Store
Connect reject the iOS build with error 90474: an iPad-capable bundle must
declare all four interface orientations to support iPad multitasking, and this
app declared two.

Catalyst genuinely needs the iPad idiom, but only for its own build. A per-SDK
condition gives macOS family 2 while iOS keeps family 1 — verified to resolve
correctly for both destinations, and Catalyst still builds with family 2 alone.
The shipped iOS bundle is back to UIDeviceFamily [1], exactly as before the
port started.

The iPad orientation key is removed rather than extended. With an iPhone-only
iOS build it is ignored, and macOS ignores orientation keys entirely, so it was
dead configuration that only served to trip the validator.

Supporting iPad properly is a deliberate piece of work — the layout is
portrait-only with no size-class handling — and is better done on purpose than
inherited as a side effect of the Mac port.

Verified: builds for both Mac Catalyst and iOS Simulator; built bundle reports
UIDeviceFamily [1] and portrait-only with no iPad orientation key.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 17:19:55 +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 7fa8dbdbdf Enable Mac Catalyst on both targets; make the widget intent parameter optional
Supported Destinations now includes Mac (Mac Catalyst) on the app and the
widget extension. Catalyst requires the iPad device family, so both targets
move to TARGETED_DEVICE_FAMILY "1,2".

The first Catalyst build failed on CalendarSelectionIntent: WidgetConfigurationIntent
requires every @Parameter to be optional, and the macOS SDK enforces that where
the iOS one lets a bare array through. selectedCalendars is now optional.

Behaviour is unchanged — the timeline provider already treated an empty
selection as "show all calendars", so nil folds into the same path.

Verified: builds for both Mac Catalyst and iOS Simulator.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 16:57:10 +02:00
ScarriffleandClaude Opus 5 c9bd42aae5 Unify app and widget version numbers; raise widget to iOS 26
The app shipped 3.5 (6) while the widget shipped 1.0.1 (2). Apple requires an
embedded appex to match its host's CFBundleShortVersionString and CFBundleVersion,
so this was a latent App Store validation failure that only surfaces at upload —
and it would surface on the Mac submission too. Both keys now live at project
level, where they cannot drift apart again.

The widget also targeted iOS 17 while the container app requires 26.0, so the
extension could never have been installed on 17 anyway. Raising it to 26.0
removes the availability skew that Mac Catalyst would otherwise inherit as
macOS 14 vs macOS 26.

Verified: app and appex Info.plists now both report 3.5 / 7 / 26.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 16:37:35 +02:00
ScarriffleandClaude Opus 4.8 871fb833ba chore: sync Xcode project structure (Shared group + widget embedding)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 17:18:34 +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 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 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
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 544e0d9265 iOS: bump marketing version to 2.7
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 20:52:04 +02:00
ScarriffleandClaude Opus 4.8 69a32121df iOS: show more events in the "Next 7 days" widget
The per-day cap of 3 made busy days collapse to "+N" so only ~6-7 events
showed total. Raise it (25) and let the total-row cap govern; extraLarge
fills up to 40 rows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 21:32:59 +02:00
Scarriffle 13d80981c6 Liquid Glass ICON 2026-06-08 19:20:10 +02:00
ScarriffleandClaude Opus 4.8 a62b200dfa chore: Marketing-Version auf 2.0 (Sharing/Gruppen/Import-Export-Release)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 21:33:12 +02:00
Scarriffle b1e0cf1fdc WIP: Widget-, Sync- & Event-Editor-Änderungen
Zwischenstand vor den Sharing/Gruppen/Import-Export-Features (gesichert,
damit die neuen Features sauber darauf aufbauen).
2026-05-31 19:22:12 +02:00
ScarriffleandClaude Opus 4.7 4125bfc728 Settings sync, calendar visibility sync, event refresh & week-view fixes
- 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>
2026-05-27 20:44:14 +02:00
Scarriffle 6c506770ba Widget anpassung vorbereitung 2026-05-25 11:53:02 +02:00
Scarriffle d1004a9111 Fix month scroll event disappearance by improving visible month detection and cache refresh rules 2026-05-25 10:33:31 +02:00
ScarriffleandClaude Opus 4.7 8b3cc11e25 Add localization (DE/EN), vertical-scroll month view, context menus, custom colors
- 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>
2026-05-19 22:00:49 +02:00
Scarriffle e5529ca653 Initial Commit 2026-05-17 08:32:34 +02:00