Standard app instead of menu-bar-only: removed LSUIElement from Info.plist.
Dock icon, Cmd+Tab, and the standard app menu (Cmd+Q) are back. The
accessory-app design was making the app hard to quit reliably -- no Dock
icon and no accessible app menu meant the only way to quit was finding the
menu bar dropdown's "Quit BusyMirror" item, which was blocking updates
(old process holds the bundle open, "app in use" on replace). Menu bar
extra stays as a secondary quick-access point.
Verified via `lsappinfo` (type="Foreground", was "UIElement") since this is
an app-type change a screenshot wouldn't show either way.
All unit tests pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix Preferences window being unreachable in 1.8.0: BusyMirror is LSUIElement
(accessory), so it never gets the standard app menu that the automatic
Settings scene's Cmd+, / SettingsLink depend on. Replaced Settings{} with a
plain Window opened via openWindow(id:) -- the same mechanism already used
for the main window -- and added a "Preferences..." item to the menu bar
dropdown as a second, more discoverable entry point.
All unit tests pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- 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>