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>
17 KiB
17 KiB
Changelog
All notable changes to BusyMirror will be documented in this file.
[1.8.2] - 2026-08-27
Changed
- Standard app instead of menu-bar-only. Removed
LSUIElementfromInfo.plist: BusyMirror now shows a Dock icon, appears in Cmd+Tab, and gets the standard app menu (Cmd+Q to quit, among others). The menu bar extra stays as a secondary quick-access point. This also fixes the practical problem it was causing: with no Dock icon and no accessible app menu, there was no reliable way to quit the app to let an update replace the bundle — you had to know the menu bar dropdown's "Quit BusyMirror" existed and use exactly that. Verified vialsappinfo(type="Foreground", previouslyUIElement) since this app type change isn't something a screenshot would catch either.
[1.8.1] - 2026-08-26
Fixed
- Preferences window unreachable: 1.8.0's Settings scene (⌘,) and its
SettingsLinkbutton didn't work because BusyMirror is anLSUIElement(accessory) app — those don't get the standard app menu, so there's no menu for the automatic "Settings…" command or Cmd+, to live in. Replaced theSettings { }scene with a plainWindow(id: "preferences-window"), opened viaopenWindow(id:)— the same mechanism that already reliably opens the main window from the menu bar. Also added a "Preferences…" item to the menu bar dropdown itself, so there are two direct routes in instead of one that depended on OS menu plumbing this app type doesn't get. (BusyMirrorApp.swift, MenuBarSupport.swift, ContentView.swift)
[1.8.0] - 2026-08-26
Added
- Real Settings (⌘,) window:
PreferencesViewnow hosts the pure-defaults controls (time window, privacy/mirroring defaults, title prefix/placeholder, work hours, skip filters) that used to live in the main window's "General Settings" panel. They're@AppStorage-backed so both windows always see the same live values. Main window keeps everything with session/live state: routes, calendar picker, dry-run toggle, Export/Import, manual scheduling, cleanup. - Menu bar icon reflects state: idle (
calendar.badge.clock), syncing (arrow.triangle.2.circlepath.circle.fill), or last run failed (exclamationmark.triangle.fill). - Menu bar dropdown diagnostics: last-sync time and result, and whether auto-sync is currently armed — sourced from the same
lastRunAtISO/lastRunOK/lastRunSummarykeys--statusreads, so CLI/interactive/auto-sync runs all feed the same indicator. ContentView.swiftsplit from ~2000 lines intoCalendarsSectionView,RoutesSectionView,ScheduleSectionView,LogSectionView(plus a small sharedCalendarDisplay.swift). View-layer extraction — state ownership and settings persistence were deliberately left untouched, since that's exactly the area the 1.6.0/1.6.1 release had to fix a real data-loss bug in.
Fixed
- Latent revert-on-relaunch bug, caught while building the Settings window: moving the
@AppStorage-backed preference controls out ofContentViewmeant changing them no longer re-triggeredsaveSettingsToDefaults(), so thesettings.v2snapshot blob could go stale relative to the individual UserDefaults keys. SinceloadSettingsFromDefaults()used the same fullapplySnapshotas Import, the next launch would silently revert a preference you'd just changed in Preferences back to whatever the stale blob had. Fixed by splitting launch-time restore into a narrowrestoreLaunchState(routes + manual selection only — the only state that isn't already self-restoring via@AppStorage) separate fromapplySnapshot(unchanged, still used by Import where overwriting everything is the point). (ContentView.swift)
[1.7.0] - 2026-08-26
Added
- Event-driven background sync, replacing the hourly
launchd StartIntervalpoll (which only fired if the Mac happened to be awake at that instant, and got throttled/coalesced by macOS). Once saved routes exist, the app registers as a login item (SMAppService.mainApp), removes any previously-installedlaunchdschedule, and reacts toEKEventStoreChanged(debounced ~3s),NSWorkspace.didWakeNotification(resync after sleep), and a 30-minute fallback timer as a safety net for a missed notification. Auto-sync always writes (writeEnabled: true) regardless of the interactive dry-run toggle, matching what the old scheduled--write 1runs did. - This logic lives in
BusyMirrorAppController, notContentView— the controller is owned by theAppstruct for the whole process lifetime, whereasContentView's ownEKEventStoreChangedobserver is torn down in.onDisappearwhen the main window closes (confirmed:MenuBarSupport.swift's "Sync Now" already reopens the window before syncing, which only makes sense if the window's state is discarded on close). Auto-sync needs to work with the window closed, so it can't live there. (MenuBarSupport.swift)
[1.6.1] - 2026-08-26
Added
- Sync reminders: new "Sync reminders when mirroring" option (global and per-route) copies source event alarms/relative offsets into mirrored placeholders. This lets calendars that are synced to a phone ring for mirrored events. When merging is enabled, only the first event's alarms are preserved for a merged block. (ContentView.swift, MirrorEngine.swift, BlockMath.swift)
- CLI flag
--sync-remindersto enable reminder syncing from scripted/headless runs. - CLI polish:
--help/-h,--list-calendars, and--status(all support--jsonfor machine-readable output). Scheduled/headless runs now record last-run time, success/failure, and a summary so--statuscan report real diagnostics instead of just log-file grepping. Failure paths (no calendar access,no saved routes) now exit with distinct nonzero codes instead of always exiting 0. (ContentView.swift)
Fixed
- Settings silently wiped on upgrade:
SettingsPayloadused fully auto-synthesizedCodable, so decoding a settings blob written by an older build (missing a field added since, e.g.syncReminders) threwkeyNotFoundand failed the entire decode — not just that one field. The app then ran with in-code defaults (empty routes, filters, etc.), and the next autosave persisted that empty state back over the real data.SettingsPayloadnow has a custominit(from:)that reads every field added after the first release withdecodeIfPresent+ its existing default, matching the patternRoutealready used.loadSettingsFromDefaults()also now recovers routes from the legacyroutes.v1backup key ifsettings.v2decodes successfully but with an emptyroutesarray — repairing installs that already hit this bug before upgrading. (ContentView.swift, SettingsPayloadTests.swift)
[1.5.1] - 2026-05-27
Fixed
- Mirror index dirtied on every sync:
MirrorRecordused synthesizedEquatablewhich includedupdatedAt: Date = Date(). BecauseupdatedAtis set to the current time whenever a record is constructed, the comparison used to detect changes always returned "not equal", causingUserDefaultsto be written on every sync run even when nothing changed. A custom==/hash(into:)now excludesupdatedAt. (MirrorEngine.swift) - Mirror URL corruption with special characters in calendar IDs:
buildMirrorURLplaced raw calendar and source IDs into the URL path without percent-encoding them. If any ID contained the;separator character the resulting URL would be mis-parsed on the next sync.mirrorURLComponentEncode(which already existed and was tested) is now called on all ID fields before they are joined. The path is set viapercentEncodedPathto preventURLComponentsfrom double-encoding the already-encoded values. (MirrorUtils.swift) - Dead constant: removed unused
SKIP_ALL_DAY_DEFAULT = truefromContentView.swift. - Deprecated
FileHandleAPI: replacedFileHandle(forWritingAtPath:)+handle.closeFile()with the modern throwingFileHandle(forWritingTo:),handle.seekToEnd(), andhandle.write(contentsOf:)inAppLogStore. (AppLogStore.swift) - Cleanup jumps calendar picker:
runCleanupForRoutewas mutatingsourceIndex,sourceID, andtargetIDsduring route cleanup, visibly shifting the picker in the UI. Cleanup does not need to update the UI selection; those mutations are removed. --exitflag redundancy:NSApp.terminatewas called wheneverisCLIRunwas true, making--exita no-op. The app now exits only when--exitis explicitly passed, so--routes/--run-saved-routescan be used without forcing termination.
Added
- Live calendar refresh: the calendar list now updates automatically when the system calendar database changes (
EKEventStoreChangednotification), removing the need to press "Refresh Calendars" after adding or removing a calendar. The observer is unregistered on view disappear and re-registered when theEKEventStoreis recreated. (ContentView.swift)
Changed
AppLogStoreextracted: moved from an inline private enum inContentView.swiftto its own fileAppLogStore.swiftfor easier navigation. (AppLogStore.swift)Block.spanfactory: addedBlock.span(start:end:)to replace the repetitiveBlock(start:end:srcStableID:nil:label:nil:notes:nil:occurrence:nil)construction pattern throughoutBlockMath.swiftandMirrorEngine.swift. (BlockMath.swift)- Removed redundant
MainActor.runwrappers:MirrorEngineis@MainActor; wrappingstore.save/store.removeintry await MainActor.run { }was unnecessary and added overhead. (MirrorEngine.swift) SettingsPayloadindentation: the nested struct was de-dented to column 0 insideContentView, making it look like a top-level type. Indentation is now consistent with the surrounding members.
Build
- Bump version to 1.5.1 (build 20).
[1.5.0] - 2026-05-27
Removed
- Mark Private feature: removed the non-functional server-side "Private" flagging for mirrored events. The Objective-C runtime hack (
setPrivate:, KVC onsensitivity/classification) never worked reliably and would have blocked App Store review. This simplifies the UI and removes a private-API liability.
Changed
- Extracted mirror engine: the ~500-line
runMirrorandrunCleanuplogic has been moved fromContentView.swiftinto a newMirrorEngine.swiftclass.ContentViewnow delegates to the engine viamakeEngine(). MirrorRecord, mirror index persistence, andSAME_TIME_TOL_MINnow live in the engine module.calLabelmoved toMirrorUtils.swiftso it can be shared between UI and engine.
Build
- Bump version to 1.5.0 (build 19).
[1.4.0] - 2026-05-27
Fixed
- Sandbox LaunchAgent: added
temporary-exceptionentitlement so scheduled runs work in the sandboxed app. - Mirror URL generation:
buildMirrorURLwas silently broken —URL(string:)rejects raw|characters on current macOS, so mirror metadata URLs were alwaysnil. Rebuilt withURLComponentsusing;separator and backward-compatible parser. - Crash on Cleanup:
runCleanup()no longer crashes if the selected source calendar was removed. - State corruption in multi-route runs:
runConfiguredRoutesno longer mutates global@Statesettings and restores them at the end of each loop; instead it passes aMirrorConfigstruct into the engine. - KVC safety: removed misleading
do-catcharoundsetValue:forKey:insetEventPrivateIfSupported()(Objective-C exceptions are uncatchable in Swift). - Log memory leak: in-memory log now caps at 2,000 lines.
- CLI race:
tryRunCLIIfPresent()now preloads calendars when access is already granted, eliminating the 10-second timeout race. - launchCtl output: stdout and stderr now use separate pipes instead of interleaving into one.
Added
- Cancel button: long-running mirrors now show a Cancel button; loops check
Task.isCancelledfor responsive cancellation. - Progress indicator: multi-route runs display
"Route X of Y"in the status area. - Unit tests: 45 tests across
BlockMathTests,MirrorUtilsTests, andEventFiltersTests. - Extracted modules:
BlockMath.swift,MirrorUtils.swift,EventFilters.swift, andMirrorConfig.swiftseparate pure logic from the UI monolith. - Target event cache: target calendars shared across routes are fetched only once per run session.
Changed
mergeGapMinis now a computed property instead of redundant@State.- Log editor is now read-only (still selectable/copyable).
SettingsPayload.excludedOrganizerFiltersis now non-optional for consistency.
Build
- Bump minimum macOS version to
15.5inInfo.plist. - Bump version to 1.4.0 (build 18).
[1.3.9] - 2026-04-09
- New: add a macOS menu bar extra with
Sync Now,Open BusyMirror, andQuit BusyMirror. - UX: menu bar sync requests reuse the existing mirror flow and can open the main window automatically when needed.
- UX: BusyMirror now runs as a menu bar-only app and no longer appears in the Dock.
- Build: bump version to 1.3.9 (build 17).
[1.3.8] - 2026-04-08
- Fix: release ZIPs now package
BusyMirror.appat the archive root instead of embedding the full build path. - Fix: release builds now apply an ad-hoc bundle signature before packaging so downloaded artifacts pass
codesign --verify --deep --strict. - Build: suppress resource fork sidecars in release ZIPs via
ditto --norsrc --keepParent. - Build: bump version to 1.3.8 (build 16).
[1.3.7] - 2026-03-24
- Fix: mirror reconciliation now survives target providers that strip BusyMirror's custom event URL metadata.
- Fix: moved and deleted source events are tracked via stable EventKit identifiers and a persisted local mirror index, so target placeholders update reliably.
- Fix: mirror updates now detect title and notes changes, not just start/end time changes.
- Build: bump version to 1.3.7 (build 15).
[1.3.6] - 2026-03-13
- Scheduling: add in-app
Scheduled runscontrols to install or remove a userlaunchdLaunchAgent from BusyMirror itself. - Scheduling: support
Hourly,Daily, andWeekdaysschedules; hourly mode runs saved routes viaStartInterval. - UX: generate and ship a proper macOS app icon set for BusyMirror.
- Build: bump version to 1.3.6 (build 14).
[1.3.4] - 2026-03-13
- Fix: route-scoped cleanup no longer deletes placeholders created by other source routes during the same multi-route run.
- Fix: stale calendars are pruned from saved selections and routes during refresh, and refresh now recreates
EKEventStorefor a hard reload. - UX: the top bar
DRY RUN/WRITEstatus pill is clickable, the left column keeps its own height on desktop, and the app can reveal its log file from the UI. - Logging: mirror activity is persisted to
~/Library/Logs/BusyMirror/BusyMirror.logwith simple rotation toBusyMirror.previous.log. - CLI: add
--run-saved-routesso scheduledlaunchdruns can use the saved UI routes instead of fragile index-based route definitions.
[1.3.1] - 2025-10-13
- Fix: auto-delete of mirrored placeholders when the source is removed now works even if no source instances remain in the window. Also cleans legacy mirrors without URLs by matching exact times.
[1.3.2] - 2025-10-13
- New: Organizer filters — skip events by organizer (name/email/URL). UI under Options and persisted in settings.
- CLI: add
--exclude-organizers(and--exclude-titles) flags to control filters when running headless.
[1.2.4] - 2025-10-10
- Fix: enable “Mirror Now” when Routes are defined even if no Source/Targets are checked in the main window. Button now enables if either routes exist or a manual selection is present.
[1.3.0] - 2025-10-10
- New: Mark Private option to mirror with prefix + real title and set event privacy on supported servers; available globally and per-route; persisted.
- Misc: calendar access fixes, concurrency annotations, accepted‑only filter, settings autosave/restore, Mirror Now enablement.
[1.2.3] - 2025-10-10
- Fix: reliably save and restore settings between runs via autosave of key options and restoration of source/target selections by persistent IDs.
- UX: persist Source and Target selections; rebuild indices on launch so UI matches saved IDs.
- Build: bump version to 1.2.3 (build 5).
[1.2.1] - 2025-10-10
- Fix: reinitialize EKEventStore after permission grant to avoid “Loaded 0 calendars” right after approval.
- Fix: attendee status filter uses current user’s attendee
participantStatus == .acceptedinstead of unavailable APIs. - Concurrency: mark
requestAccess()andreloadCalendars()as@MainActorto satisfy strict concurrency checks. - Dev: add Makefile with
build-debug,build-release, andpackagetargets; produce versioned ZIP + SHA-256.
[1.2.0] - 2024-09-29
- Feature: multi-route mirroring, overlap modes, merge gaps, work hours filter, CLI support, export/import settings.