- Real Settings (Cmd+,) window via PreferencesView, hosting the pure
@AppStorage-backed defaults that used to live in the main window's
"General Settings" panel. Both windows share the same live values since
they're backed by the same UserDefaults keys.
- Menu bar icon reflects idle/syncing/error state; dropdown shows last-sync
time/result and whether auto-sync is armed, sourced from the same
lastRunAtISO/lastRunOK/lastRunSummary keys --status reads.
- ContentView.swift split from ~2000 lines into CalendarsSectionView,
RoutesSectionView, ScheduleSectionView, LogSectionView + a small shared
CalendarDisplay.swift. View-layer extraction only — state ownership and
settings persistence deliberately left alone.
- Fixed a latent revert-on-relaunch bug caught while building the Settings
window: moving preference controls out of ContentView meant they no longer
re-triggered saveSettingsToDefaults(), so settings.v2 could go stale and
the next launch would silently revert a just-changed preference via the
shared applySnapshot path. Split launch-time restore (routes + selection
only) from Import's full restore.
All 47 unit tests pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Event-driven background sync, replacing the hourly launchd StartInterval
poll (only fired if the Mac happened to be awake at that instant, and got
throttled/coalesced by macOS). Once saved routes exist: registers as a
login item (SMAppService.mainApp), removes any old launchd schedule, and
reacts to EKEventStoreChanged (debounced ~3s), NSWorkspace.didWakeNotification,
plus a 30-min fallback timer as a safety net. Auto-sync always writes,
matching what the old scheduled runs did.
- This lives in BusyMirrorAppController (app-lifetime), not ContentView —
ContentView's own EKEventStoreChanged observer is torn down when the main
window closes, which would make auto-sync a no-op exactly when it needs to
keep working (window closed, menu-bar-only). Confirmed via MenuBarSupport's
existing "Sync Now" handler, which already has to reopen the window before
it can run anything.
All 47 unit tests pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Sync reminders: new "Sync reminders when mirroring" option (global and
per-route) copies source event alarms into mirrored placeholders. CLI flag
--sync-reminders.
- CLI polish: --help, --list-calendars, --status (all support --json).
Scheduled/headless runs record last-run time/result/summary. Failure paths
now exit with distinct nonzero codes (2 = no calendar access, 3 = no saved
routes) instead of always exiting 0.
- Fix settings silently wiped on upgrade: SettingsPayload's auto-synthesized
Codable threw on any settings blob missing a field added since (e.g. the
new syncReminders key), failing the whole decode and letting the next
autosave persist an empty state over real routes/filters. Custom decode
now falls back per-field like Route already did, and loadSettingsFromDefaults
recovers routes from the legacy routes.v1 key if settings.v2 comes back
empty, repairing installs already hit by this.
- Roadmap: event-driven background sync (SMAppService + EKEventStoreChanged +
wake notification) and an external MCP wrapper around the CLI queued next;
decided against direct Google/CalDAV API integration since EventKit already
covers it via System Settings accounts.
All 47 unit tests pass (45 existing + 2 new SettingsPayloadTests).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>