Mirror "Maybe" (tentative RSVP) events instead of dropping them.
Previously, with "Mirror accepted events only" on, events you'd replied
tentative to were skipped entirely. They're now mirrored with a "Maybe: "
marker in the title (after the normal prefix) and written with tentative
availability so Calendar shows them hatched. The marker is applied for any
tentative RSVP regardless of the accepted-only setting; an accepted<->
tentative flip re-syncs the title. Not carried on the merge path
(mergeGapMin > 0), which already discards per-event titles.
Block gains a `tentative` flag (in ==/hash); accepted-only gate lets
.tentative through alongside .accepted.
All unit tests pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix Sync Now toolbar button showing icon-only, no text label. Confirmed
via screenshot that 1.9.1's sidebar/toolbar fixes worked; this was the
remaining clarity issue -- forced .labelStyle(.titleAndIcon) so the label
always shows.
All unit tests pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix sidebar not clickable: List(SidebarSection.allCases, selection:) ties
selection to Identifiable's id (String) while the binding was typed
SidebarSection? -- rebuilt with the standard List(selection:) { ForEach {
.tag(section) } } pattern instead, plus .listStyle(.sidebar).
Fix "Write to calendars" hard to find: still present as the Dry Run/Write
toolbar segmented control, but likely lost to toolbar overflow alongside a
status readout, Sync Now, and the overflow menu all in one
ToolbarItemGroup. Moved status out to .navigationSubtitle (native, never
overflows), toolbar now just Dry Run/Write, Sync Now/Cancel, overflow menu.
All unit tests pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Sidebar-navigation redesign: NavigationSplitView with Routes / Schedule /
Activity Log replaces the 2x2 panel-card grid. Primary actions moved to
the toolbar (dry-run/write, sync status, Sync Now); Export/Import/Reveal
Log/Cleanup/Refresh/Recheck Permission moved to a toolbar overflow menu.
- Routes are now a collapsed-by-default list -- click a row to expand its
editor instead of always showing every field for every route.
- Schedule view leads with live auto-sync status; the manual launchd
schedule is demoted to an explicit optional section underneath.
- Activity Log is readable rows (status icon + text) with a search filter
and a Clear button, instead of a monospaced text dump.
- Menu bar dropdown got icons and real Cmd+,/Cmd+Q shortcuts.
- Explored as a design mockup first (published as an Artifact, light+dark),
direction confirmed before writing any SwiftUI.
Fixed along the way: routes were being silently dropped on most launches.
reloadCalendars() pruned routes against a freshly-created EKEventStore's
calendar fetch right after a permission grant, which can under-report
calendars for a moment (especially remote/Exchange accounts) before they
finish loading -- wrongly concluding a route's calendar was gone. Only
survived because of a routes.v1 legacy backup key that itself never got
updated after its first write, so any real route edit made in an affected
session would have been silently reverted on the next launch. Fixed by
skipping the prune specifically on the post-grant reload
(reloadCalendars(pruneRoutes: false)) and keeping the legacy backup current
on every save instead of frozen.
All unit tests pass. Verified on a real install: routes stopped
disappearing across repeated launches (checked via --status and reading
the UserDefaults plist directly).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix Preferences missing from the standard app menu: 1.8.1's workaround for
LSUIElement (plain Window + openWindow) was never swapped back for a real
Settings{} scene after 1.8.2 removed LSUIElement, so there was still no
Preferences item in the app's own menu or Cmd+, binding, only a button in
the main window and a menu-bar-dropdown item. Restored Settings{} now that
the app menu exists to host it; menu bar dropdown and main window button
both use the standard openSettings()/SettingsLink instead of a custom
openWindow(id:). Net simplification -- removes the now-redundant window ID
and controller method.
All unit tests pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>