9 Commits

27 changed files with 1816 additions and 419 deletions

1
.gitignore vendored
View File

@@ -18,6 +18,7 @@ ExportOptions.plist
# Misc # Misc
*.swp *.swp
*.profraw
*.zip *.zip
*.sha256 *.sha256
dist/ dist/

View File

@@ -410,7 +410,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 8; CURRENT_PROJECT_VERSION = 17;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = BusyMirror/Info.plist; INFOPLIST_FILE = BusyMirror/Info.plist;
@@ -421,7 +421,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MARKETING_VERSION = 1.2.6; MARKETING_VERSION = 1.3.9;
PRODUCT_BUNDLE_IDENTIFIER = com.cqrenet.BusyMirror; PRODUCT_BUNDLE_IDENTIFIER = com.cqrenet.BusyMirror;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES; REGISTER_APP_GROUPS = YES;
@@ -440,7 +440,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 8; CURRENT_PROJECT_VERSION = 17;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = BusyMirror/Info.plist; INFOPLIST_FILE = BusyMirror/Info.plist;
@@ -451,7 +451,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MARKETING_VERSION = 1.2.6; MARKETING_VERSION = 1.3.9;
PRODUCT_BUNDLE_IDENTIFIER = com.cqrenet.BusyMirror; PRODUCT_BUNDLE_IDENTIFIER = com.cqrenet.BusyMirror;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES; REGISTER_APP_GROUPS = YES;

View File

@@ -1,55 +1,15 @@
{ {
"images" : [ "images" : [
{ { "filename" : "icon_16x16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" },
"idiom" : "mac", { "filename" : "icon_32x32.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" },
"scale" : "1x", { "filename" : "icon_32x32.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" },
"size" : "16x16" { "filename" : "icon_64x64.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" },
}, { "filename" : "icon_128x128.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" },
{ { "filename" : "icon_256x256.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" },
"idiom" : "mac", { "filename" : "icon_256x256.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" },
"scale" : "2x", { "filename" : "icon_512x512.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" },
"size" : "16x16" { "filename" : "icon_512x512.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" },
}, { "filename" : "icon_1024x1024.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" }
{
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
], ],
"info" : { "info" : {
"author" : "xcode", "author" : "xcode",

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -2,10 +2,19 @@ import SwiftUI
@main @main
struct BusyMirrorApp: App { struct BusyMirrorApp: App {
@StateObject private var appController = BusyMirrorAppController()
var body: some Scene { var body: some Scene {
WindowGroup { Window("BusyMirror", id: BusyMirrorSceneID.mainWindow) {
ContentView() ContentView()
.environmentObject(appController)
.frame(minWidth: 720, minHeight: 520) .frame(minWidth: 720, minHeight: 520)
} }
.defaultSize(width: 1120, height: 760)
MenuBarExtra("BusyMirror", systemImage: appController.isSyncing ? "arrow.triangle.2.circlepath.circle.fill" : "calendar.badge.clock") {
BusyMirrorMenuBarView()
.environmentObject(appController)
}
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>LSUIElement</key>
<true/>
<key>NSCalendarsFullAccessUsageDescription</key> <key>NSCalendarsFullAccessUsageDescription</key>
<string>BusyMirror needs access to your calendars to create busy placeholders.</string> <string>BusyMirror needs access to your calendars to create busy placeholders.</string>
<key>NSRemindersFullAccessUsageDescription</key> <key>NSRemindersFullAccessUsageDescription</key>

View File

@@ -0,0 +1,75 @@
import SwiftUI
import AppKit
enum BusyMirrorSceneID {
static let mainWindow = "main-window"
}
@MainActor
final class BusyMirrorAppController: ObservableObject {
@Published private(set) var isSyncing = false
@Published private(set) var hasPendingSyncRequest = false
@Published private(set) var syncRequestToken = UUID()
@Published private(set) var isMainWindowVisible = false
func requestSync() {
hasPendingSyncRequest = true
syncRequestToken = UUID()
}
func clearPendingSyncRequest() {
hasPendingSyncRequest = false
}
func setSyncing(_ syncing: Bool) {
isSyncing = syncing
}
func setMainWindowVisible(_ visible: Bool) {
isMainWindowVisible = visible
}
func openMainWindow(using openWindow: OpenWindowAction) {
NSApp.activate(ignoringOtherApps: true)
openWindow(id: BusyMirrorSceneID.mainWindow)
}
}
struct BusyMirrorMenuBarView: View {
@Environment(\.openWindow) private var openWindow
@EnvironmentObject private var appController: BusyMirrorAppController
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text("BusyMirror")
.font(.headline)
Text(appController.isSyncing ? "Sync in progress." : "Use your saved routes or current selection.")
.font(.subheadline)
.foregroundStyle(.secondary)
Divider()
Button(appController.isSyncing ? "Syncing…" : "Sync Now") {
let shouldOpenWindow = !appController.isMainWindowVisible
appController.requestSync()
if shouldOpenWindow {
appController.openMainWindow(using: openWindow)
}
}
.disabled(appController.isSyncing)
Button("Open BusyMirror") {
appController.openMainWindow(using: openWindow)
}
Divider()
Button("Quit BusyMirror") {
NSApp.terminate(nil)
}
}
.padding(12)
.frame(width: 240, alignment: .leading)
}
}

View File

@@ -1,11 +1,52 @@
# Changelog # Changelog
# Changelog
All notable changes to BusyMirror will be documented in this file. All notable changes to BusyMirror will be documented in this file.
## [1.3.9] - 2026-04-09
- New: add a macOS menu bar extra with `Sync Now`, `Open BusyMirror`, and `Quit 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.app` at 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 runs` controls to install or remove a user `launchd` LaunchAgent from BusyMirror itself.
- Scheduling: support `Hourly`, `Daily`, and `Weekdays` schedules; hourly mode runs saved routes via `StartInterval`.
- 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 `EKEventStore` for a hard reload.
- UX: the top bar `DRY RUN` / `WRITE` status 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.log` with simple rotation to `BusyMirror.previous.log`.
- CLI: add `--run-saved-routes` so scheduled `launchd` runs 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 ## [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. - 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, acceptedonly filter, settings autosave/restore, Mirror Now enablement.
## [1.2.3] - 2025-10-10 ## [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. - 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. - UX: persist Source and Target selections; rebuild indices on launch so UI matches saved IDs.

View File

@@ -8,7 +8,7 @@ DEST := platform=macOS
# Extract marketing version from project settings # Extract marketing version from project settings
VERSION := $(shell sed -n 's/.*MARKETING_VERSION = \([0-9.]*\);.*/\1/p' $(PROJECT)/project.pbxproj | head -n1) VERSION := $(shell sed -n 's/.*MARKETING_VERSION = \([0-9.]*\);.*/\1/p' $(PROJECT)/project.pbxproj | head -n1)
.PHONY: all clean build-debug build-release open app package .PHONY: all clean build-debug build-release sign-app open app package
all: build-release all: build-release
@@ -31,16 +31,25 @@ open: app
# Path to built app (Release) # Path to built app (Release)
APP_PATH := $(DERIVED)/Build/Products/Release/BusyMirror.app APP_PATH := $(DERIVED)/Build/Products/Release/BusyMirror.app
SIGNED_APP_PATH := build/ReleaseSigned/BusyMirror.app
app: build-release sign-app: build-release
@echo "Preparing signed release app…"
@rm -rf "$(SIGNED_APP_PATH)"
@mkdir -p "$(dir $(SIGNED_APP_PATH))"
@ditto "$(APP_PATH)" "$(SIGNED_APP_PATH)"
@xattr -rc "$(SIGNED_APP_PATH)"
@codesign --force --deep --sign - "$(SIGNED_APP_PATH)"
@codesign --verify --deep --strict --verbose=2 "$(SIGNED_APP_PATH)"
app: sign-app
@# Ensure the app exists @# Ensure the app exists
@test -d "$(APP_PATH)" && echo "Built: $(APP_PATH)" || (echo "App not found at $(APP_PATH)" && exit 1) @test -d "$(SIGNED_APP_PATH)" && echo "Built: $(SIGNED_APP_PATH)" || (echo "App not found at $(SIGNED_APP_PATH)" && exit 1)
@echo "Version: $(VERSION)" @echo "Version: $(VERSION)"
@echo "OK" @echo "OK"
package: app package: app
@echo "Packaging BusyMirror $(VERSION)" @echo "Packaging BusyMirror $(VERSION)"
@zip -qry "BusyMirror-$(VERSION)-macOS.zip" "$(APP_PATH)" @ditto --norsrc -c -k --keepParent "$(SIGNED_APP_PATH)" "BusyMirror-$(VERSION)-macOS.zip"
@shasum -a 256 "BusyMirror-$(VERSION)-macOS.zip" | awk '{print $$1}' > "BusyMirror-$(VERSION)-macOS.zip.sha256" @shasum -a 256 "BusyMirror-$(VERSION)-macOS.zip" | awk '{print $$1}' > "BusyMirror-$(VERSION)-macOS.zip.sha256"
@echo "Created BusyMirror-$(VERSION)-macOS.zip and .sha256" @echo "Created BusyMirror-$(VERSION)-macOS.zip and .sha256"

View File

@@ -2,13 +2,26 @@
BusyMirror mirrors meetings between your calendars so your availability stays consistent across accounts/devices. BusyMirror mirrors meetings between your calendars so your availability stays consistent across accounts/devices.
## What it does (current checkpoint) On macOS, BusyMirror now runs as a menu bar app. Use the menu bar icon to sync manually or open the main window; it no longer appears in the Dock.
- Manual “Run” to mirror events across selected routes (Source → Targets).
- DRY-RUN mode shows what would happen. ## What it does (current)
- Prefix-based tagging of mirrored events. - Route-driven mirroring (multi-source): define Source → Targets routes and run them in one go.
- Cleanup of placeholders (with confirmation). - Manual selection mirroring: pick a source and targets in the UI and run.
- Loop/duplicate guards so mirrors dont replicate themselves. - Two privacy modes:
- Time window and merge-gap settings. - Private (hide details): mirrors placeholders with prefix + placeholder title (e.g., "🪞 Busy").
- Mark Private: mirrors prefix + real title, but marks events Private on supported servers (best-effort).
- DRY-RUN mode: see what would be created/updated/deleted without writing.
- Activity Log in the app plus persistent file logging on disk.
- In-app scheduling: install or remove a `launchd` LaunchAgent from the `Scheduled runs` section.
- Menu bar controls: trigger `Sync Now`, open the main window, or quit without keeping a Dock icon around.
- Overlap modes: `allow`, `skipCovered`, `fillGaps`.
- Merge adjacent events with a configurable gap.
- Time window controls (days back/forward) and Work Hours filter.
- Accepted-only filter (mirror your accepted meetings only).
- Cleanup of placeholders, including auto-delete of mirrors whose source disappeared.
- Refresh Calendars prunes stale saved calendars and routes when calendars are removed from the system.
- Prefix-based tagging and loop guards to prevent re-mirroring mirrors.
- Settings: autosave/restore, Import/Export JSON, saved routes for scheduled/headless runs.
## Why ## Why
Use one calendars confirmed meetings to block time in other calendars (e.g., corporate iPad vs. personal devices). Use one calendars confirmed meetings to block time in other calendars (e.g., corporate iPad vs. personal devices).
@@ -27,6 +40,33 @@ Option B — Makefile (reproducible)
See `CHANGELOG.md` for notable changes. See `CHANGELOG.md` for notable changes.
## CLI (optional)
- Run from Terminal with `--routes` to mirror without the UI. Example:
- `BusyMirror.app/Contents/MacOS/BusyMirror --routes "1->2,3; 4->5" --write 1 --days-forward 7 --mode allow --exit`
- Run the routes already saved in the app settings:
- `BusyMirror.app/Contents/MacOS/BusyMirror --run-saved-routes --write 1 --exit`
- Flags exist for privacy, all-day, merge gap, days window, overlap mode, cleanup, and filters.
- Filters:
- `--exclude-titles "token1, token2"`
- `--exclude-organizers "alice@example.com, Example Org"`
- Tokens are comma or newline separated; matching is case-insensitive.
## Logs
- BusyMirror now writes a persistent log file to `~/Library/Logs/BusyMirror/BusyMirror.log`.
- When the file grows large, the previous file is rotated to `~/Library/Logs/BusyMirror/BusyMirror.previous.log`.
- `launchd` stdout/stderr for scheduled runs are also written in the same folder.
- In the UI, use `Reveal Log File` to open the current log directly in Finder.
## Scheduling
- BusyMirror can create its own schedule from the app UI in `Scheduled runs`.
- Choose `Hourly`, `Daily`, or `Weekdays`, then click `Install Schedule`.
- The installed LaunchAgent runs:
- `/Applications/BusyMirror.app/Contents/MacOS/BusyMirror --run-saved-routes --write 1 --exit`
- This is more stable than index-based `--routes`, because it uses the routes and per-route options you already configured in the UI.
- Hourly schedules use `launchd` `StartInterval`; daily and weekday schedules use `StartCalendarInterval`.
- You can remove the job from the same UI with `Remove Schedule`, and inspect the generated plist with `Reveal LaunchAgent`.
- Note: scheduled headless runs depend on Calendar permission being granted to the installed app. Because these local builds are unsigned, macOS may require re-granting permission after replacing the app bundle with a new build.
## Roadmap ## Roadmap
See [ROADMAP.md](ROADMAP.md) See [ROADMAP.md](ROADMAP.md)

View File

@@ -1,17 +1,29 @@
# BusyMirror Roadmap # 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 `launchd` with hourly/daily/weekday modes
- 1.3.6: generated macOS app icon set and packaged release assets
## Next ## Next
- Source filters (name patterns like `[HOLD]`, `#nomirror`) - Auto-refresh calendars on `EKEventStoreChanged` (live refresh button-less)
- Mirror only **Accepted** meetings (exclude tentative/declined) - Better scheduled-run diagnostics in the UI (last run / last error / next run)
- Persistent settings (routes, window, prefix) - Better server-side privacy mapping (per-provider heuristics)
- Import/Export settings (.busymirror.json)
## Then ## Then
- iOS/iPadOS app (Run Now, Shortcuts, iCloud sync) - Signed/notarized binaries and release pipeline
- UI: route editor & clearer toggles - CLI quality: friendlier `--routes` parsing and help flag
- “Dry-run by default” preference - “Dry-run by default” preference
## Later ## Later
- Background monitoring (macOS) - Background monitoring (macOS)
- Smarter cleanup & conflict resolution - Smarter cleanup & conflict resolution
- iOS/iPadOS helper (Shortcuts integration)
- Profiles & MDM/Managed Config support - Profiles & MDM/Managed Config support

17
ReleaseNotes-1.3.0.md Normal file
View File

@@ -0,0 +1,17 @@
# BusyMirror 1.3.0 — 2025-10-10
New
- Mark Private option: mirror events with your prefix + real title while marking them Private on supported servers (e.g., Exchange). Coworkers see the time block but not the details.
- Per-route and global toggles for Mark Private; persists in settings and export/import.
Fixes & improvements
- More reliable calendar loading after permission grant (reinit EKEventStore).
- Concurrency: `@MainActor` on permission/refresh methods.
- Acceptedonly filter via current user attendee `participantStatus`.
- Settings autosave and restore (including source/target selections by IDs).
- Mirror Now enabled when calendars available; routes or manual selection used as appropriate.
Build
- `make build-release`
- `make package` → BusyMirror-1.3.0-macOS.zip and .sha256

6
ReleaseNotes-1.3.1.md Normal file
View File

@@ -0,0 +1,6 @@
BusyMirror 1.3.1 — Bugfix Release
- Fix: Auto-delete mirrored placeholders when the source event is removed.
- Triggers even if no source instances remain in the selected window.
- Also cleans legacy mirrors without mirror URLs by matching exact times.

11
ReleaseNotes-1.3.2.md Normal file
View File

@@ -0,0 +1,11 @@
BusyMirror 1.3.2 — 2025-10-13
Changes
- Organizer filters: skip mirroring events whose organizer matches a name, email, or URL token. Case-insensitive. Configure in Options.
- CLI flags: `--exclude-organizers` and `--exclude-titles` accept comma/newline separated tokens. Example:
- `--routes "1->2" --write 1 --exclude-organizers "alice@example.com, Example Org" --exit`
Notes
- Export/Import settings now includes organizer filters (backwards compatible).
- No changes to event URL format; feature is fully optional.

9
ReleaseNotes-1.3.3.md Normal file
View File

@@ -0,0 +1,9 @@
BusyMirror 1.3.3 — 2025-10-13
Changes
- UI: Options panel is scrollable to ensure new filters are always visible on smaller windows.
- Organizer filter: skip by organizer name/email/URL; settings persisted; usable via CLI with `--exclude-organizers`.
Build
- Version bump to 1.3.3 (build stays 11).

11
ReleaseNotes-1.3.4.md Normal file
View File

@@ -0,0 +1,11 @@
BusyMirror 1.3.4 - 2026-03-13
Changes
- Fix multi-route cleanup so one source route no longer deletes mirrored placeholders created by another route.
- Persist activity logs to `~/Library/Logs/BusyMirror/BusyMirror.log` and expose a `Reveal Log File` action in the app.
- Add `--run-saved-routes` for headless runs using the routes configured in the UI, which makes `launchd` scheduling practical.
- Improve calendar refresh by pruning stale saved identifiers and recreating the EventKit store.
- Keep the left column from stretching to match the routes/log column on desktop layouts.
Build
- Version bump to 1.3.4 (build 12).

9
ReleaseNotes-1.3.6.md Normal file
View File

@@ -0,0 +1,9 @@
BusyMirror 1.3.6 - 2026-03-13
Changes
- Add in-app scheduling controls so BusyMirror can install and remove its own `launchd` LaunchAgent.
- Support hourly saved-route runs in addition to daily and weekday schedules.
- Ship a generated macOS app icon set for the app bundle and exported releases.
Build
- Version bump to 1.3.6 (build 14).

9
ReleaseNotes-1.3.7.md Normal file
View File

@@ -0,0 +1,9 @@
BusyMirror 1.3.7 - 2026-03-24
Changes
- Fix mirrored event tracking on providers that do not preserve BusyMirror's custom event URL metadata.
- Track source events using stable EventKit identifiers and a local mirror index so moved and deleted source events update target calendars reliably.
- Detect title and notes changes during reconciliation instead of only updating mirrors when times change.
Build
- Version bump to 1.3.7 (build 15).

9
ReleaseNotes-1.3.8.md Normal file
View File

@@ -0,0 +1,9 @@
BusyMirror 1.3.8 - 2026-04-08
Changes
- Fix release packaging so the ZIP contains `BusyMirror.app` at the archive root.
- Apply an ad-hoc bundle signature before packaging so the distributed app bundle verifies correctly after unzip.
- Strip resource fork sidecars from release archives to avoid malformed download contents.
Build
- Version bump to 1.3.8 (build 16).

9
ReleaseNotes-1.3.9.md Normal file
View File

@@ -0,0 +1,9 @@
BusyMirror 1.3.9 - 2026-04-09
Changes
- Add a menu bar extra with `Sync Now`, `Open BusyMirror`, and `Quit BusyMirror`.
- Route menu bar sync requests through the same mirroring flow as the main window, opening the window automatically when needed.
- Run BusyMirror as a menu bar-only app so it no longer appears in the Dock.
Build
- Version bump to 1.3.9 (build 17).