Major update for stability

This commit is contained in:
Mark Gregory
2026-09-08 15:22:19 +01:00
parent 3b95f53a25
commit b2f3c5aef6
24 changed files with 1059 additions and 212 deletions
@@ -23,6 +23,7 @@ final class BackgroundSyncManager: ObservableObject {
@Published private(set) var isSyncAllRunning = false
@Published private(set) var syncAllCompleted = 0
@Published private(set) var syncAllTotal = 0
@Published private(set) var lastError: String?
var activityDisplayTitle: String {
if isSyncAllRunning {
@@ -67,6 +68,7 @@ final class BackgroundSyncManager: ObservableObject {
guard !started else { return }
started = true
refreshConfiguration()
pruneHistoryIfNeeded(force: true)
checkSchedules()
timer = Timer.scheduledTimer(withTimeInterval: 20, repeats: true) { [weak self] _ in
Task { @MainActor [weak self] in
@@ -119,7 +121,7 @@ final class BackgroundSyncManager: ObservableObject {
for job in jobs where job.scheduleEnabled && job.scheduleKind != .manual && job.nextRunAt == nil {
job.nextRunAt = job.calculateNextRun(after: Date().addingTimeInterval(-1))
}
try? modelContext.save()
saveContext("refreshing automation configuration")
}
private func checkSchedules() {
@@ -136,7 +138,7 @@ final class BackgroundSyncManager: ObservableObject {
job.nextRunAt = job.calculateNextRun(after: now)
}
}
try? modelContext.save()
saveContext("updating scheduled jobs")
}
private func fileChanged(jobID: UUID, path: String) {
@@ -150,7 +152,7 @@ final class BackgroundSyncManager: ObservableObject {
guard !Task.isCancelled, let self else { return }
_ = self.enqueue(jobID: jobID, trigger: .watched)
}
try? modelContext.save()
saveContext("recording file-watch activity")
}
@discardableResult
@@ -209,12 +211,12 @@ final class BackgroundSyncManager: ObservableObject {
let command = RsyncCommandBuilder.displayCommand(job: job, dryRun: false, executable: executable)
let run = SyncRun(job: job, command: command, wasDryRun: false, trigger: trigger)
modelContext.insert(run)
try? modelContext.save()
saveContext("creating run history")
do {
let result = try await runner.run(job: job, dryRun: false)
run.output = result.stdout
run.errorOutput = result.stderr
let result = try await runner.run(job: job, dryRun: false, trigger: trigger)
run.output = SyncRun.sanitizedLog(result.stdout)
run.errorOutput = SyncRun.sanitizedLog(result.stderr)
run.exitCode = Int(result.exitCode)
run.wasCancelled = result.wasCancelled
run.finishedAt = Date()
@@ -222,13 +224,42 @@ final class BackgroundSyncManager: ObservableObject {
job.lastExitCode = Int(result.exitCode)
job.updatedAt = Date()
} catch {
run.errorOutput = error.localizedDescription
run.errorOutput = SyncRun.sanitizedLog(error.localizedDescription)
run.exitCode = -1
run.finishedAt = Date()
job.lastRunAt = run.finishedAt
job.lastExitCode = -1
}
try? modelContext.save()
saveContext("saving run history")
pruneHistoryIfNeeded()
}
private func pruneHistoryIfNeeded(force: Bool = false) {
let now = Date()
if !force, let last = UserDefaults.standard.object(forKey: "lastHistoryPruneAt") as? Date, now.timeIntervalSince(last) < 3600 { return }
let configuredDays = UserDefaults.standard.integer(forKey: "historyRetentionDays")
let days = configuredDays == 0 ? 90 : max(7, min(configuredDays, 3650))
let cutoff = now.addingTimeInterval(-TimeInterval(days) * 86_400)
do {
let runs = try modelContext.fetch(FetchDescriptor<SyncRun>())
for run in runs where run.finishedAt != nil && run.startedAt < cutoff {
modelContext.delete(run)
}
try modelContext.save()
UserDefaults.standard.set(now, forKey: "lastHistoryPruneAt")
} catch {
lastError = "Could not prune run history: \(error.localizedDescription)"
}
}
private func saveContext(_ operation: String) {
do {
try modelContext.save()
} catch {
lastError = "Database error while \(operation): \(error.localizedDescription)"
}
}
private func fetchJobs() -> [SyncJob] {