- 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>
5.9 KiB
5.9 KiB
BusyMirror Roadmap
Shipped (highlights)
- Route-driven mirroring (multi-source)
- Accepted-only filter (mirror your accepted meetings)
- Persistent settings with autosave/restore; Import/Export JSON
- Overlap modes (allow, skipCovered, fillGaps) and merge-gap
- Work Hours filter and title-based skip filters
- Privacy: placeholders with prefix + customizable title
- 1.3.0: Mark Private option (global + per-route)
- 1.3.4: persistent file logging, stale-calendar pruning on refresh, clickable top-bar mode toggle
- 1.3.6: in-app scheduling via
launchdwith hourly/daily/weekday modes - 1.3.6: generated macOS app icon set and packaged release assets
- 1.4.0: unit-test suite (45 tests), Cancel button, progress indicator, sandbox LaunchAgent fix, mirror URL fix, engine refactor into
MirrorConfig - CLI diagnostics:
--help,--list-calendars [--json],--status [--json], real exit codes (2 = no access, 3 = no saved routes), last-run tracking (time/ok/summary) - 1.7.0: Event-driven background sync, replacing the hourly
launchd StartIntervalpoll. Auto-sync logic lives inBusyMirrorAppController(an app-lifetime object, not tied toContentView's window lifecycle — closing the main window used to tear down theEKEventStoreChangedobserver along with it, which would have made auto-sync a no-op whenever the window was closed). Once saved routes exist: registers as a login item (SMAppService.mainApp), removes any oldlaunchdschedule, watchesEKEventStoreChanged(debounced ~3s) andNSWorkspace.didWakeNotification, plus a 30-min fallback timer as a safety net. Auto-sync always writes (writeEnabled: true), independent of the interactive dry-run toggle. - 1.8.0: V2 UI polish.
ContentView.swiftsplit from ~2000 lines into focused view files —CalendarsSectionView,RoutesSectionView,ScheduleSectionView,LogSectionView(state stays owned byContentView/@AppStorage; these are view-layer extractions, not a full MVVM rewrite — the settings-persistence model didn't need touching and touching it is exactly how the 1.6.0/1.6.1 data-loss bug happened). A preferences window hosting the pure@AppStorage-backed defaults (time window, privacy/mirroring defaults, work hours, skip filters) that used to live in the main window; this surfaced a latent bug — moving fields out ofContentViewmeant they stopped re-triggeringsaveSettingsToDefaults(), so on next launchapplySnapshotwould have silently reverted a preference changed in the new window using the stale blob. Fixed by splitting launch-time restore (restoreLaunchState, routes/selections only, since @AppStorage fields already self-restore) from Import's full restore (applySnapshot, unchanged). Menu bar icon now reflects idle/syncing/error state, and the dropdown shows last-sync time/result and whether auto-sync is armed. - 1.8.1: the preferences window (originally a
Settings{}scene) didn't actually open —LSUIElementapps get no standard app menu, so Cmd+,/SettingsLinkhad no menu to hook into. Replaced with a plainWindowopened viaopenWindow(id:), plus a "Preferences…" menu bar item as a second entry point. - 1.8.2: standard app, not menu-bar-only. Removed
LSUIElement— Dock icon, Cmd+Tab, standard app menu (Cmd+Q) are back. The accessory-app design (no Dock icon) turned out to make the app hard to quit reliably, which blocked replacing the bundle during updates. Menu bar extra stays as a secondary quick-access point. - 1.8.3: restored the real
Settings{}scene for Preferences now that the app menu exists to host it (1.8.1'sWindow+openWindowworkaround was never swapped back after 1.8.2 removedLSUIElement) — Cmd+, and the app's own "Preferences…" menu item work again, not just a button and a menu-bar-dropdown item. - 1.9.0: sidebar-navigation redesign (Routes / Schedule / Activity Log via
NavigationSplitView, toolbar actions, collapsed-by-default route list, structured log view, refreshed menu bar) — explored as a mockup first, direction confirmed before implementing. Also fixed a real bug the redesign work surfaced: routes were being silently dropped on most launches (a freshly-createdEKEventStoreright after a permission grant can under-report calendars for a moment, especially remote accounts, so pruning against it wrongly deleted routes referencing calendars that were actually still there) — only ever recovered by a legacy backup key that was itself frozen at its first-ever value. Fixed the prune timing and made the backup stay current.
Next
- MCP server (thin external wrapper, not embedded in the app). So agents driving BusyMirror don't have to shell out to the CLI and regex-parse log lines. A small standalone stdio-transport script (Node/Python) maps MCP tools 1:1 onto the CLI's
--jsonoutput:list_calendars,list_routes,run_route,run_saved_routes,get_status. Deliberately kept out of the Swift app itself — no MCP SDK dependency in the signed binary (AGENTS.md's zero-external-packages rule stays intact), and MCP hosts spawn server processes on demand anyway, so there's no need for the app to run one persistently. - Better server-side privacy mapping (per-provider heuristics).
Later
- Signed/notarized binaries and release pipeline.
- Smarter cleanup & conflict resolution.
- iOS/iPadOS helper (Shortcuts integration).
- Profiles & MDM/Managed Config support.
Decided against
- Direct Google/CalDAV/Exchange API integration (OAuth flows, token storage, per-provider clients so BusyMirror can mirror a non-local calendar without it being added to macOS). EventKit already surfaces any calendar added via System Settings → Internet Accounts — macOS does the sync, auth, and refresh. Building a parallel integration would duplicate the OS for the narrow case of an account that can't or won't be added system-wide (e.g. MDM-restricted work accounts). Not worth the OAuth/Keychain/per-provider-quirk surface for that.