Release 1.5.1

Bug fixes and code quality improvements:

- Fix mirror index dirtied on every sync (MirrorRecord.updatedAt in equality)
- Fix mirror URL corruption: encode calendar/source IDs before joining with ';'
  and use percentEncodedPath to prevent double-encoding
- Fix cleanup route mutating UI calendar picker selection unnecessarily
- Fix --exit flag redundancy (isCLIRun no longer implies termination)
- Remove dead SKIP_ALL_DAY_DEFAULT constant
- Replace deprecated FileHandle(forWritingAtPath:) with throwing variant
- Add EKEventStoreChanged observer for live calendar list refresh
- Extract AppLogStore into its own file (AppLogStore.swift)
- Add Block.span(start🔚) factory; replace verbose nil-field constructions
- Remove redundant MainActor.run{} wrappers inside @MainActor MirrorEngine
- Fix SettingsPayload indentation inside ContentView

All 45 unit tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 15:48:08 +02:00
parent 2c319808c2
commit ad6ae396da
11 changed files with 220 additions and 120 deletions
+33 -19
View File
@@ -13,6 +13,29 @@ struct MirrorRecord: Hashable, Codable {
var lastKnownEndTimestamp: TimeInterval
var updatedAt: Date = Date()
// updatedAt is intentionally excluded from equality and hashing: it is a
// bookkeeping timestamp that changes on every write and should not cause
// the mirror index to be marked dirty when the meaningful fields are equal.
static func == (lhs: MirrorRecord, rhs: MirrorRecord) -> Bool {
lhs.targetCalendarID == rhs.targetCalendarID &&
lhs.sourceCalendarID == rhs.sourceCalendarID &&
lhs.sourceStableID == rhs.sourceStableID &&
lhs.occurrenceTimestamp == rhs.occurrenceTimestamp &&
lhs.targetEventIdentifier == rhs.targetEventIdentifier &&
lhs.lastKnownStartTimestamp == rhs.lastKnownStartTimestamp &&
lhs.lastKnownEndTimestamp == rhs.lastKnownEndTimestamp
}
func hash(into hasher: inout Hasher) {
hasher.combine(targetCalendarID)
hasher.combine(sourceCalendarID)
hasher.combine(sourceStableID)
hasher.combine(occurrenceTimestamp)
hasher.combine(targetEventIdentifier)
hasher.combine(lastKnownStartTimestamp)
hasher.combine(lastKnownEndTimestamp)
}
var sourceKey: String {
sourceOccurrenceKey(
sourceCalID: sourceCalendarID,
@@ -229,7 +252,7 @@ final class MirrorEngine {
}
}
}
occupied.append(Block(start: ts, end: te, srcStableID: nil, label: nil, notes: nil, occurrence: nil))
occupied.append(Block.span(start: ts, end: te))
}
}
occupied = coalesce(occupied)
@@ -360,12 +383,10 @@ final class MirrorEngine {
existing.notes = notes
existing.url = desiredURL
do {
try await MainActor.run {
try store.save(existing, span: .thisEvent, commit: true)
}
try store.save(existing, span: .thisEvent, commit: true)
log("✓ UPDATED [\(srcName) -> \(tgtName)]\(byTimeSuffix) \(blk.start) -> \(blk.end)")
rememberMirrorEvent(existing, for: blk)
occupied = coalesce(occupied + [Block(start: blk.start, end: blk.end, srcStableID: nil, label: nil, notes: nil, occurrence: nil)])
occupied = coalesce(occupied + [Block.span(start: blk.start, end: blk.end)])
sessionGuard.insert(gKey)
updated += 1
} catch {
@@ -414,13 +435,11 @@ final class MirrorEngine {
newEv.url = desiredURL
newEv.availability = .busy
do {
try await MainActor.run {
try store.save(newEv, span: .thisEvent, commit: true)
}
try store.save(newEv, span: .thisEvent, commit: true)
created += 1
log("✓ CREATED [\(srcName) -> \(tgtName)] \(blk.start) -> \(blk.end)")
rememberMirrorEvent(newEv, for: blk)
occupied = coalesce(occupied + [Block(start: blk.start, end: blk.end, srcStableID: nil, label: nil, notes: nil, occurrence: nil)])
occupied = coalesce(occupied + [Block.span(start: blk.start, end: blk.end)])
sessionGuard.insert(gKey)
} catch {
log("Save failed: \(error.localizedDescription)")
@@ -479,9 +498,7 @@ final class MirrorEngine {
log("~ WOULD DELETE (missing source) [\(srcName) -> \(tgtName)] \(candidate.startDate ?? windowStart) -> \(candidate.endDate ?? windowEnd)")
} else {
do {
try await MainActor.run {
try store.remove(candidate, span: .thisEvent, commit: true)
}
try store.remove(candidate, span: .thisEvent, commit: true)
removed += 1
} catch {
log("Delete failed: \(error.localizedDescription)")
@@ -543,9 +560,7 @@ final class MirrorEngine {
log("~ WOULD DELETE (missing source) [\(srcName) -> \(tgtName)] \(ev.startDate ?? windowStart) -> \(ev.endDate ?? windowEnd)")
} else {
do {
try await MainActor.run {
try store.remove(ev, span: .thisEvent, commit: true)
}
try store.remove(ev, span: .thisEvent, commit: true)
removed += 1
} catch {
log("Delete failed: \(error.localizedDescription)")
@@ -598,12 +613,11 @@ final class MirrorEngine {
log("~ WOULD DELETE [\(tgt.title)] \(ev.startDate ?? todayStart) -> \(ev.endDate ?? todayStart)")
} else {
do {
try await MainActor.run {
try store.remove(ev, span: .thisEvent, commit: true)
}
try store.remove(ev, span: .thisEvent, commit: true)
delCount += 1
} catch {
log("Delete failed: \(error.localizedDescription)")
}
catch { log("Delete failed: \(error.localizedDescription)") }
}
}
log("[Cleanup \(tgt.title)] deleted=\(delCount)")