Initial scaffold — NASBackup iOS app
Full project scaffold: XcodeGen project.yml, Core models/protocols/errors,
SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2),
PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager,
all feature UIs (Login, Connect, Browse, Backup, History, Settings),
Shared components and theme. Builds clean with zero errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:05:04 +03:00
|
|
|
import Foundation
|
|
|
|
|
import BackgroundTasks
|
feat: automatic backup on launch, foreground, and photo library change
AutoBackupCoordinator (new):
- @MainActor singleton that owns all auto-backup decisions
- Phase model: idle / checking / autoBackingUp / disconnected
- idle: nothing pending or destination not set
- checking: reconciliation in progress before decision
- autoBackingUp: coordinator-triggered BackupEngine run active
- disconnected: NAS offline, pending count queued for retry
- Combine subscription on BackupStatusService.$isRefreshing fires
handleReconciliationComplete() on every reconcile completion — the
single entry point for all auto-backup decisions
- Combine subscription on LANMonitor.$nasReachable fires
handleNASReachable() which retries only when phase == .disconnected
- Guards: autoBackupEnabled, quiet hours, chargingOnlyMode + battery state
- canAutoBackup also blocks when engine.job.status.isActive to prevent
double-start from concurrent photo library change + foreground events
BackupView:
- .task + .onChange(of: scenePhase) now call coordinator.onActive() instead
of statusService.refresh() directly — coordinator decides whether to also
start a backup after reconciliation completes
- ringContentID includes coordinator phase suffix → ring animates on
checking/disconnected transitions
- ringCenterContent case 0: when engine is idle, shows:
- disconnectedRingView if phase == .disconnected and needBackup > 0
(wifi.slash icon, "Waiting for NAS", queued count)
- checkingRingView if phase == .checking (spinner, "Checking…")
- existing ringMainView otherwise
- ringColor: dim orange (0.35 opacity) when disconnected with pending items
- Pull-to-refresh reconciliation triggers auto-backup via Combine chain
ConnectionStore:
- autoBackupEnabled: Bool, default true, UserDefaults-persisted
SettingsView:
- AUTO BACKUP section at top with autoBackupEnabled toggle
BackgroundTaskManager:
- BGProcessingTask: quiet-hours guard, requiresExternalPower mirrors
chargingOnlyMode, expiration handler calls engine.cancel() for clean exit
- BGAppRefreshTask: lightweight refresh only (no upload in background refresh)
- Both tasks reschedule themselves before doing any work
AppDelegate:
- UIDevice.current.isBatteryMonitoringEnabled = true for battery checks
NASBackupApp:
- AutoBackupCoordinator.shared injected as @StateObject + environmentObject
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 17:28:11 +03:00
|
|
|
import os.log
|
|
|
|
|
|
|
|
|
|
private let log = Logger(subsystem: "com.albert.nasbackup", category: "BGTask")
|
Initial scaffold — NASBackup iOS app
Full project scaffold: XcodeGen project.yml, Core models/protocols/errors,
SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2),
PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager,
all feature UIs (Login, Connect, Browse, Backup, History, Settings),
Shared components and theme. Builds clean with zero errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:05:04 +03:00
|
|
|
|
|
|
|
|
final class BackgroundTaskManager {
|
feat: automatic backup on launch, foreground, and photo library change
AutoBackupCoordinator (new):
- @MainActor singleton that owns all auto-backup decisions
- Phase model: idle / checking / autoBackingUp / disconnected
- idle: nothing pending or destination not set
- checking: reconciliation in progress before decision
- autoBackingUp: coordinator-triggered BackupEngine run active
- disconnected: NAS offline, pending count queued for retry
- Combine subscription on BackupStatusService.$isRefreshing fires
handleReconciliationComplete() on every reconcile completion — the
single entry point for all auto-backup decisions
- Combine subscription on LANMonitor.$nasReachable fires
handleNASReachable() which retries only when phase == .disconnected
- Guards: autoBackupEnabled, quiet hours, chargingOnlyMode + battery state
- canAutoBackup also blocks when engine.job.status.isActive to prevent
double-start from concurrent photo library change + foreground events
BackupView:
- .task + .onChange(of: scenePhase) now call coordinator.onActive() instead
of statusService.refresh() directly — coordinator decides whether to also
start a backup after reconciliation completes
- ringContentID includes coordinator phase suffix → ring animates on
checking/disconnected transitions
- ringCenterContent case 0: when engine is idle, shows:
- disconnectedRingView if phase == .disconnected and needBackup > 0
(wifi.slash icon, "Waiting for NAS", queued count)
- checkingRingView if phase == .checking (spinner, "Checking…")
- existing ringMainView otherwise
- ringColor: dim orange (0.35 opacity) when disconnected with pending items
- Pull-to-refresh reconciliation triggers auto-backup via Combine chain
ConnectionStore:
- autoBackupEnabled: Bool, default true, UserDefaults-persisted
SettingsView:
- AUTO BACKUP section at top with autoBackupEnabled toggle
BackgroundTaskManager:
- BGProcessingTask: quiet-hours guard, requiresExternalPower mirrors
chargingOnlyMode, expiration handler calls engine.cancel() for clean exit
- BGAppRefreshTask: lightweight refresh only (no upload in background refresh)
- Both tasks reschedule themselves before doing any work
AppDelegate:
- UIDevice.current.isBatteryMonitoringEnabled = true for battery checks
NASBackupApp:
- AutoBackupCoordinator.shared injected as @StateObject + environmentObject
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 17:28:11 +03:00
|
|
|
static let backupTaskID = "com.albert.nasbackup.backup"
|
Initial scaffold — NASBackup iOS app
Full project scaffold: XcodeGen project.yml, Core models/protocols/errors,
SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2),
PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager,
all feature UIs (Login, Connect, Browse, Backup, History, Settings),
Shared components and theme. Builds clean with zero errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:05:04 +03:00
|
|
|
static let refreshTaskID = "com.albert.nasbackup.refresh"
|
|
|
|
|
|
|
|
|
|
static func registerTasks() {
|
|
|
|
|
BGTaskScheduler.shared.register(
|
|
|
|
|
forTaskWithIdentifier: backupTaskID,
|
|
|
|
|
using: nil
|
|
|
|
|
) { task in
|
|
|
|
|
guard let task = task as? BGProcessingTask else { return }
|
|
|
|
|
BackgroundTaskManager.handleBackupTask(task)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
BGTaskScheduler.shared.register(
|
|
|
|
|
forTaskWithIdentifier: refreshTaskID,
|
|
|
|
|
using: nil
|
|
|
|
|
) { task in
|
|
|
|
|
guard let task = task as? BGAppRefreshTask else { return }
|
|
|
|
|
BackgroundTaskManager.handleRefreshTask(task)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static func scheduleBackup(delay: TimeInterval = 30) {
|
|
|
|
|
let request = BGProcessingTaskRequest(identifier: backupTaskID)
|
|
|
|
|
request.requiresNetworkConnectivity = true
|
feat: automatic backup on launch, foreground, and photo library change
AutoBackupCoordinator (new):
- @MainActor singleton that owns all auto-backup decisions
- Phase model: idle / checking / autoBackingUp / disconnected
- idle: nothing pending or destination not set
- checking: reconciliation in progress before decision
- autoBackingUp: coordinator-triggered BackupEngine run active
- disconnected: NAS offline, pending count queued for retry
- Combine subscription on BackupStatusService.$isRefreshing fires
handleReconciliationComplete() on every reconcile completion — the
single entry point for all auto-backup decisions
- Combine subscription on LANMonitor.$nasReachable fires
handleNASReachable() which retries only when phase == .disconnected
- Guards: autoBackupEnabled, quiet hours, chargingOnlyMode + battery state
- canAutoBackup also blocks when engine.job.status.isActive to prevent
double-start from concurrent photo library change + foreground events
BackupView:
- .task + .onChange(of: scenePhase) now call coordinator.onActive() instead
of statusService.refresh() directly — coordinator decides whether to also
start a backup after reconciliation completes
- ringContentID includes coordinator phase suffix → ring animates on
checking/disconnected transitions
- ringCenterContent case 0: when engine is idle, shows:
- disconnectedRingView if phase == .disconnected and needBackup > 0
(wifi.slash icon, "Waiting for NAS", queued count)
- checkingRingView if phase == .checking (spinner, "Checking…")
- existing ringMainView otherwise
- ringColor: dim orange (0.35 opacity) when disconnected with pending items
- Pull-to-refresh reconciliation triggers auto-backup via Combine chain
ConnectionStore:
- autoBackupEnabled: Bool, default true, UserDefaults-persisted
SettingsView:
- AUTO BACKUP section at top with autoBackupEnabled toggle
BackgroundTaskManager:
- BGProcessingTask: quiet-hours guard, requiresExternalPower mirrors
chargingOnlyMode, expiration handler calls engine.cancel() for clean exit
- BGAppRefreshTask: lightweight refresh only (no upload in background refresh)
- Both tasks reschedule themselves before doing any work
AppDelegate:
- UIDevice.current.isBatteryMonitoringEnabled = true for battery checks
NASBackupApp:
- AutoBackupCoordinator.shared injected as @StateObject + environmentObject
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 17:28:11 +03:00
|
|
|
// Respect chargingOnlyMode: if set, only run when plugged in.
|
|
|
|
|
request.requiresExternalPower = ConnectionStore.shared.chargingOnlyMode
|
Initial scaffold — NASBackup iOS app
Full project scaffold: XcodeGen project.yml, Core models/protocols/errors,
SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2),
PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager,
all feature UIs (Login, Connect, Browse, Backup, History, Settings),
Shared components and theme. Builds clean with zero errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:05:04 +03:00
|
|
|
request.earliestBeginDate = Date(timeIntervalSinceNow: delay)
|
|
|
|
|
do {
|
|
|
|
|
try BGTaskScheduler.shared.submit(request)
|
|
|
|
|
} catch {
|
feat: automatic backup on launch, foreground, and photo library change
AutoBackupCoordinator (new):
- @MainActor singleton that owns all auto-backup decisions
- Phase model: idle / checking / autoBackingUp / disconnected
- idle: nothing pending or destination not set
- checking: reconciliation in progress before decision
- autoBackingUp: coordinator-triggered BackupEngine run active
- disconnected: NAS offline, pending count queued for retry
- Combine subscription on BackupStatusService.$isRefreshing fires
handleReconciliationComplete() on every reconcile completion — the
single entry point for all auto-backup decisions
- Combine subscription on LANMonitor.$nasReachable fires
handleNASReachable() which retries only when phase == .disconnected
- Guards: autoBackupEnabled, quiet hours, chargingOnlyMode + battery state
- canAutoBackup also blocks when engine.job.status.isActive to prevent
double-start from concurrent photo library change + foreground events
BackupView:
- .task + .onChange(of: scenePhase) now call coordinator.onActive() instead
of statusService.refresh() directly — coordinator decides whether to also
start a backup after reconciliation completes
- ringContentID includes coordinator phase suffix → ring animates on
checking/disconnected transitions
- ringCenterContent case 0: when engine is idle, shows:
- disconnectedRingView if phase == .disconnected and needBackup > 0
(wifi.slash icon, "Waiting for NAS", queued count)
- checkingRingView if phase == .checking (spinner, "Checking…")
- existing ringMainView otherwise
- ringColor: dim orange (0.35 opacity) when disconnected with pending items
- Pull-to-refresh reconciliation triggers auto-backup via Combine chain
ConnectionStore:
- autoBackupEnabled: Bool, default true, UserDefaults-persisted
SettingsView:
- AUTO BACKUP section at top with autoBackupEnabled toggle
BackgroundTaskManager:
- BGProcessingTask: quiet-hours guard, requiresExternalPower mirrors
chargingOnlyMode, expiration handler calls engine.cancel() for clean exit
- BGAppRefreshTask: lightweight refresh only (no upload in background refresh)
- Both tasks reschedule themselves before doing any work
AppDelegate:
- UIDevice.current.isBatteryMonitoringEnabled = true for battery checks
NASBackupApp:
- AutoBackupCoordinator.shared injected as @StateObject + environmentObject
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 17:28:11 +03:00
|
|
|
log.warning("BGProcessingTask schedule failed: \(error.localizedDescription)")
|
Initial scaffold — NASBackup iOS app
Full project scaffold: XcodeGen project.yml, Core models/protocols/errors,
SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2),
PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager,
all feature UIs (Login, Connect, Browse, Backup, History, Settings),
Shared components and theme. Builds clean with zero errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:05:04 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static func scheduleAppRefresh() {
|
|
|
|
|
let request = BGAppRefreshTaskRequest(identifier: refreshTaskID)
|
|
|
|
|
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
|
|
|
|
|
do {
|
|
|
|
|
try BGTaskScheduler.shared.submit(request)
|
|
|
|
|
} catch {
|
feat: automatic backup on launch, foreground, and photo library change
AutoBackupCoordinator (new):
- @MainActor singleton that owns all auto-backup decisions
- Phase model: idle / checking / autoBackingUp / disconnected
- idle: nothing pending or destination not set
- checking: reconciliation in progress before decision
- autoBackingUp: coordinator-triggered BackupEngine run active
- disconnected: NAS offline, pending count queued for retry
- Combine subscription on BackupStatusService.$isRefreshing fires
handleReconciliationComplete() on every reconcile completion — the
single entry point for all auto-backup decisions
- Combine subscription on LANMonitor.$nasReachable fires
handleNASReachable() which retries only when phase == .disconnected
- Guards: autoBackupEnabled, quiet hours, chargingOnlyMode + battery state
- canAutoBackup also blocks when engine.job.status.isActive to prevent
double-start from concurrent photo library change + foreground events
BackupView:
- .task + .onChange(of: scenePhase) now call coordinator.onActive() instead
of statusService.refresh() directly — coordinator decides whether to also
start a backup after reconciliation completes
- ringContentID includes coordinator phase suffix → ring animates on
checking/disconnected transitions
- ringCenterContent case 0: when engine is idle, shows:
- disconnectedRingView if phase == .disconnected and needBackup > 0
(wifi.slash icon, "Waiting for NAS", queued count)
- checkingRingView if phase == .checking (spinner, "Checking…")
- existing ringMainView otherwise
- ringColor: dim orange (0.35 opacity) when disconnected with pending items
- Pull-to-refresh reconciliation triggers auto-backup via Combine chain
ConnectionStore:
- autoBackupEnabled: Bool, default true, UserDefaults-persisted
SettingsView:
- AUTO BACKUP section at top with autoBackupEnabled toggle
BackgroundTaskManager:
- BGProcessingTask: quiet-hours guard, requiresExternalPower mirrors
chargingOnlyMode, expiration handler calls engine.cancel() for clean exit
- BGAppRefreshTask: lightweight refresh only (no upload in background refresh)
- Both tasks reschedule themselves before doing any work
AppDelegate:
- UIDevice.current.isBatteryMonitoringEnabled = true for battery checks
NASBackupApp:
- AutoBackupCoordinator.shared injected as @StateObject + environmentObject
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 17:28:11 +03:00
|
|
|
log.warning("BGAppRefreshTask schedule failed: \(error.localizedDescription)")
|
Initial scaffold — NASBackup iOS app
Full project scaffold: XcodeGen project.yml, Core models/protocols/errors,
SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2),
PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager,
all feature UIs (Login, Connect, Browse, Backup, History, Settings),
Shared components and theme. Builds clean with zero errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:05:04 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat: automatic backup on launch, foreground, and photo library change
AutoBackupCoordinator (new):
- @MainActor singleton that owns all auto-backup decisions
- Phase model: idle / checking / autoBackingUp / disconnected
- idle: nothing pending or destination not set
- checking: reconciliation in progress before decision
- autoBackingUp: coordinator-triggered BackupEngine run active
- disconnected: NAS offline, pending count queued for retry
- Combine subscription on BackupStatusService.$isRefreshing fires
handleReconciliationComplete() on every reconcile completion — the
single entry point for all auto-backup decisions
- Combine subscription on LANMonitor.$nasReachable fires
handleNASReachable() which retries only when phase == .disconnected
- Guards: autoBackupEnabled, quiet hours, chargingOnlyMode + battery state
- canAutoBackup also blocks when engine.job.status.isActive to prevent
double-start from concurrent photo library change + foreground events
BackupView:
- .task + .onChange(of: scenePhase) now call coordinator.onActive() instead
of statusService.refresh() directly — coordinator decides whether to also
start a backup after reconciliation completes
- ringContentID includes coordinator phase suffix → ring animates on
checking/disconnected transitions
- ringCenterContent case 0: when engine is idle, shows:
- disconnectedRingView if phase == .disconnected and needBackup > 0
(wifi.slash icon, "Waiting for NAS", queued count)
- checkingRingView if phase == .checking (spinner, "Checking…")
- existing ringMainView otherwise
- ringColor: dim orange (0.35 opacity) when disconnected with pending items
- Pull-to-refresh reconciliation triggers auto-backup via Combine chain
ConnectionStore:
- autoBackupEnabled: Bool, default true, UserDefaults-persisted
SettingsView:
- AUTO BACKUP section at top with autoBackupEnabled toggle
BackgroundTaskManager:
- BGProcessingTask: quiet-hours guard, requiresExternalPower mirrors
chargingOnlyMode, expiration handler calls engine.cancel() for clean exit
- BGAppRefreshTask: lightweight refresh only (no upload in background refresh)
- Both tasks reschedule themselves before doing any work
AppDelegate:
- UIDevice.current.isBatteryMonitoringEnabled = true for battery checks
NASBackupApp:
- AutoBackupCoordinator.shared injected as @StateObject + environmentObject
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 17:28:11 +03:00
|
|
|
// MARK: — BGProcessingTask: best-effort full backup
|
|
|
|
|
|
Initial scaffold — NASBackup iOS app
Full project scaffold: XcodeGen project.yml, Core models/protocols/errors,
SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2),
PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager,
all feature UIs (Login, Connect, Browse, Backup, History, Settings),
Shared components and theme. Builds clean with zero errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:05:04 +03:00
|
|
|
private static func handleBackupTask(_ task: BGProcessingTask) {
|
feat: automatic backup on launch, foreground, and photo library change
AutoBackupCoordinator (new):
- @MainActor singleton that owns all auto-backup decisions
- Phase model: idle / checking / autoBackingUp / disconnected
- idle: nothing pending or destination not set
- checking: reconciliation in progress before decision
- autoBackingUp: coordinator-triggered BackupEngine run active
- disconnected: NAS offline, pending count queued for retry
- Combine subscription on BackupStatusService.$isRefreshing fires
handleReconciliationComplete() on every reconcile completion — the
single entry point for all auto-backup decisions
- Combine subscription on LANMonitor.$nasReachable fires
handleNASReachable() which retries only when phase == .disconnected
- Guards: autoBackupEnabled, quiet hours, chargingOnlyMode + battery state
- canAutoBackup also blocks when engine.job.status.isActive to prevent
double-start from concurrent photo library change + foreground events
BackupView:
- .task + .onChange(of: scenePhase) now call coordinator.onActive() instead
of statusService.refresh() directly — coordinator decides whether to also
start a backup after reconciliation completes
- ringContentID includes coordinator phase suffix → ring animates on
checking/disconnected transitions
- ringCenterContent case 0: when engine is idle, shows:
- disconnectedRingView if phase == .disconnected and needBackup > 0
(wifi.slash icon, "Waiting for NAS", queued count)
- checkingRingView if phase == .checking (spinner, "Checking…")
- existing ringMainView otherwise
- ringColor: dim orange (0.35 opacity) when disconnected with pending items
- Pull-to-refresh reconciliation triggers auto-backup via Combine chain
ConnectionStore:
- autoBackupEnabled: Bool, default true, UserDefaults-persisted
SettingsView:
- AUTO BACKUP section at top with autoBackupEnabled toggle
BackgroundTaskManager:
- BGProcessingTask: quiet-hours guard, requiresExternalPower mirrors
chargingOnlyMode, expiration handler calls engine.cancel() for clean exit
- BGAppRefreshTask: lightweight refresh only (no upload in background refresh)
- Both tasks reschedule themselves before doing any work
AppDelegate:
- UIDevice.current.isBatteryMonitoringEnabled = true for battery checks
NASBackupApp:
- AutoBackupCoordinator.shared injected as @StateObject + environmentObject
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 17:28:11 +03:00
|
|
|
scheduleBackup() // Reschedule before doing any work so we always get a next shot
|
|
|
|
|
|
|
|
|
|
let store = ConnectionStore.shared
|
|
|
|
|
|
|
|
|
|
// Quiet hours: skip silently — next scheduled slot will try again
|
|
|
|
|
guard !store.isInQuietHours else {
|
|
|
|
|
log.info("BGBackup skipped — quiet hours")
|
|
|
|
|
task.setTaskCompleted(success: true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guard let connection = store.savedConnection, connection.remotePath != "/" else {
|
|
|
|
|
log.info("BGBackup skipped — no destination configured")
|
|
|
|
|
task.setTaskCompleted(success: false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Expiration handler: cancel current upload cleanly.
|
|
|
|
|
// BackupEngine.cancel() sets isCancelled = true; the upload loop checks this
|
|
|
|
|
// between files and exits. defer { transfer.disconnect() } in engine.run()
|
|
|
|
|
// ensures the NAS connection is always closed before iOS reclaims the slot.
|
|
|
|
|
task.expirationHandler = {
|
|
|
|
|
log.warning("BGBackup expiring — cancelling upload")
|
|
|
|
|
Task { @MainActor in BackupEngine.shared.cancel() }
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 10:55:54 +03:00
|
|
|
Task { @MainActor in
|
Initial scaffold — NASBackup iOS app
Full project scaffold: XcodeGen project.yml, Core models/protocols/errors,
SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2),
PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager,
all feature UIs (Login, Connect, Browse, Backup, History, Settings),
Shared components and theme. Builds clean with zero errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:05:04 +03:00
|
|
|
do {
|
feat: automatic backup on launch, foreground, and photo library change
AutoBackupCoordinator (new):
- @MainActor singleton that owns all auto-backup decisions
- Phase model: idle / checking / autoBackingUp / disconnected
- idle: nothing pending or destination not set
- checking: reconciliation in progress before decision
- autoBackingUp: coordinator-triggered BackupEngine run active
- disconnected: NAS offline, pending count queued for retry
- Combine subscription on BackupStatusService.$isRefreshing fires
handleReconciliationComplete() on every reconcile completion — the
single entry point for all auto-backup decisions
- Combine subscription on LANMonitor.$nasReachable fires
handleNASReachable() which retries only when phase == .disconnected
- Guards: autoBackupEnabled, quiet hours, chargingOnlyMode + battery state
- canAutoBackup also blocks when engine.job.status.isActive to prevent
double-start from concurrent photo library change + foreground events
BackupView:
- .task + .onChange(of: scenePhase) now call coordinator.onActive() instead
of statusService.refresh() directly — coordinator decides whether to also
start a backup after reconciliation completes
- ringContentID includes coordinator phase suffix → ring animates on
checking/disconnected transitions
- ringCenterContent case 0: when engine is idle, shows:
- disconnectedRingView if phase == .disconnected and needBackup > 0
(wifi.slash icon, "Waiting for NAS", queued count)
- checkingRingView if phase == .checking (spinner, "Checking…")
- existing ringMainView otherwise
- ringColor: dim orange (0.35 opacity) when disconnected with pending items
- Pull-to-refresh reconciliation triggers auto-backup via Combine chain
ConnectionStore:
- autoBackupEnabled: Bool, default true, UserDefaults-persisted
SettingsView:
- AUTO BACKUP section at top with autoBackupEnabled toggle
BackgroundTaskManager:
- BGProcessingTask: quiet-hours guard, requiresExternalPower mirrors
chargingOnlyMode, expiration handler calls engine.cancel() for clean exit
- BGAppRefreshTask: lightweight refresh only (no upload in background refresh)
- Both tasks reschedule themselves before doing any work
AppDelegate:
- UIDevice.current.isBatteryMonitoringEnabled = true for battery checks
NASBackupApp:
- AutoBackupCoordinator.shared injected as @StateObject + environmentObject
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 17:28:11 +03:00
|
|
|
log.info("BGBackup starting")
|
|
|
|
|
_ = try await BackupEngine.shared.run(
|
|
|
|
|
connection: connection,
|
|
|
|
|
filter: store.backupFilter,
|
|
|
|
|
triggeredByLAN: true
|
|
|
|
|
)
|
|
|
|
|
log.info("BGBackup completed")
|
Initial scaffold — NASBackup iOS app
Full project scaffold: XcodeGen project.yml, Core models/protocols/errors,
SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2),
PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager,
all feature UIs (Login, Connect, Browse, Backup, History, Settings),
Shared components and theme. Builds clean with zero errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:05:04 +03:00
|
|
|
task.setTaskCompleted(success: true)
|
|
|
|
|
} catch {
|
feat: automatic backup on launch, foreground, and photo library change
AutoBackupCoordinator (new):
- @MainActor singleton that owns all auto-backup decisions
- Phase model: idle / checking / autoBackingUp / disconnected
- idle: nothing pending or destination not set
- checking: reconciliation in progress before decision
- autoBackingUp: coordinator-triggered BackupEngine run active
- disconnected: NAS offline, pending count queued for retry
- Combine subscription on BackupStatusService.$isRefreshing fires
handleReconciliationComplete() on every reconcile completion — the
single entry point for all auto-backup decisions
- Combine subscription on LANMonitor.$nasReachable fires
handleNASReachable() which retries only when phase == .disconnected
- Guards: autoBackupEnabled, quiet hours, chargingOnlyMode + battery state
- canAutoBackup also blocks when engine.job.status.isActive to prevent
double-start from concurrent photo library change + foreground events
BackupView:
- .task + .onChange(of: scenePhase) now call coordinator.onActive() instead
of statusService.refresh() directly — coordinator decides whether to also
start a backup after reconciliation completes
- ringContentID includes coordinator phase suffix → ring animates on
checking/disconnected transitions
- ringCenterContent case 0: when engine is idle, shows:
- disconnectedRingView if phase == .disconnected and needBackup > 0
(wifi.slash icon, "Waiting for NAS", queued count)
- checkingRingView if phase == .checking (spinner, "Checking…")
- existing ringMainView otherwise
- ringColor: dim orange (0.35 opacity) when disconnected with pending items
- Pull-to-refresh reconciliation triggers auto-backup via Combine chain
ConnectionStore:
- autoBackupEnabled: Bool, default true, UserDefaults-persisted
SettingsView:
- AUTO BACKUP section at top with autoBackupEnabled toggle
BackgroundTaskManager:
- BGProcessingTask: quiet-hours guard, requiresExternalPower mirrors
chargingOnlyMode, expiration handler calls engine.cancel() for clean exit
- BGAppRefreshTask: lightweight refresh only (no upload in background refresh)
- Both tasks reschedule themselves before doing any work
AppDelegate:
- UIDevice.current.isBatteryMonitoringEnabled = true for battery checks
NASBackupApp:
- AutoBackupCoordinator.shared injected as @StateObject + environmentObject
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 17:28:11 +03:00
|
|
|
log.error("BGBackup failed: \(error.localizedDescription)")
|
Initial scaffold — NASBackup iOS app
Full project scaffold: XcodeGen project.yml, Core models/protocols/errors,
SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2),
PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager,
all feature UIs (Login, Connect, Browse, Backup, History, Settings),
Shared components and theme. Builds clean with zero errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:05:04 +03:00
|
|
|
task.setTaskCompleted(success: false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat: automatic backup on launch, foreground, and photo library change
AutoBackupCoordinator (new):
- @MainActor singleton that owns all auto-backup decisions
- Phase model: idle / checking / autoBackingUp / disconnected
- idle: nothing pending or destination not set
- checking: reconciliation in progress before decision
- autoBackingUp: coordinator-triggered BackupEngine run active
- disconnected: NAS offline, pending count queued for retry
- Combine subscription on BackupStatusService.$isRefreshing fires
handleReconciliationComplete() on every reconcile completion — the
single entry point for all auto-backup decisions
- Combine subscription on LANMonitor.$nasReachable fires
handleNASReachable() which retries only when phase == .disconnected
- Guards: autoBackupEnabled, quiet hours, chargingOnlyMode + battery state
- canAutoBackup also blocks when engine.job.status.isActive to prevent
double-start from concurrent photo library change + foreground events
BackupView:
- .task + .onChange(of: scenePhase) now call coordinator.onActive() instead
of statusService.refresh() directly — coordinator decides whether to also
start a backup after reconciliation completes
- ringContentID includes coordinator phase suffix → ring animates on
checking/disconnected transitions
- ringCenterContent case 0: when engine is idle, shows:
- disconnectedRingView if phase == .disconnected and needBackup > 0
(wifi.slash icon, "Waiting for NAS", queued count)
- checkingRingView if phase == .checking (spinner, "Checking…")
- existing ringMainView otherwise
- ringColor: dim orange (0.35 opacity) when disconnected with pending items
- Pull-to-refresh reconciliation triggers auto-backup via Combine chain
ConnectionStore:
- autoBackupEnabled: Bool, default true, UserDefaults-persisted
SettingsView:
- AUTO BACKUP section at top with autoBackupEnabled toggle
BackgroundTaskManager:
- BGProcessingTask: quiet-hours guard, requiresExternalPower mirrors
chargingOnlyMode, expiration handler calls engine.cancel() for clean exit
- BGAppRefreshTask: lightweight refresh only (no upload in background refresh)
- Both tasks reschedule themselves before doing any work
AppDelegate:
- UIDevice.current.isBatteryMonitoringEnabled = true for battery checks
NASBackupApp:
- AutoBackupCoordinator.shared injected as @StateObject + environmentObject
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 17:28:11 +03:00
|
|
|
// MARK: — BGAppRefreshTask: lightweight status check only
|
|
|
|
|
|
Initial scaffold — NASBackup iOS app
Full project scaffold: XcodeGen project.yml, Core models/protocols/errors,
SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2),
PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager,
all feature UIs (Login, Connect, Browse, Backup, History, Settings),
Shared components and theme. Builds clean with zero errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:05:04 +03:00
|
|
|
private static func handleRefreshTask(_ task: BGAppRefreshTask) {
|
feat: automatic backup on launch, foreground, and photo library change
AutoBackupCoordinator (new):
- @MainActor singleton that owns all auto-backup decisions
- Phase model: idle / checking / autoBackingUp / disconnected
- idle: nothing pending or destination not set
- checking: reconciliation in progress before decision
- autoBackingUp: coordinator-triggered BackupEngine run active
- disconnected: NAS offline, pending count queued for retry
- Combine subscription on BackupStatusService.$isRefreshing fires
handleReconciliationComplete() on every reconcile completion — the
single entry point for all auto-backup decisions
- Combine subscription on LANMonitor.$nasReachable fires
handleNASReachable() which retries only when phase == .disconnected
- Guards: autoBackupEnabled, quiet hours, chargingOnlyMode + battery state
- canAutoBackup also blocks when engine.job.status.isActive to prevent
double-start from concurrent photo library change + foreground events
BackupView:
- .task + .onChange(of: scenePhase) now call coordinator.onActive() instead
of statusService.refresh() directly — coordinator decides whether to also
start a backup after reconciliation completes
- ringContentID includes coordinator phase suffix → ring animates on
checking/disconnected transitions
- ringCenterContent case 0: when engine is idle, shows:
- disconnectedRingView if phase == .disconnected and needBackup > 0
(wifi.slash icon, "Waiting for NAS", queued count)
- checkingRingView if phase == .checking (spinner, "Checking…")
- existing ringMainView otherwise
- ringColor: dim orange (0.35 opacity) when disconnected with pending items
- Pull-to-refresh reconciliation triggers auto-backup via Combine chain
ConnectionStore:
- autoBackupEnabled: Bool, default true, UserDefaults-persisted
SettingsView:
- AUTO BACKUP section at top with autoBackupEnabled toggle
BackgroundTaskManager:
- BGProcessingTask: quiet-hours guard, requiresExternalPower mirrors
chargingOnlyMode, expiration handler calls engine.cancel() for clean exit
- BGAppRefreshTask: lightweight refresh only (no upload in background refresh)
- Both tasks reschedule themselves before doing any work
AppDelegate:
- UIDevice.current.isBatteryMonitoringEnabled = true for battery checks
NASBackupApp:
- AutoBackupCoordinator.shared injected as @StateObject + environmentObject
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 17:28:11 +03:00
|
|
|
scheduleAppRefresh() // Always reschedule
|
|
|
|
|
|
|
|
|
|
task.expirationHandler = {
|
|
|
|
|
task.setTaskCompleted(success: false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Task { @MainActor in
|
|
|
|
|
// Light metadata-only refresh — no NAS upload, just phone count update.
|
|
|
|
|
// This keeps the cached status fresh so the dashboard shows accurate data
|
|
|
|
|
// when the user opens the app.
|
|
|
|
|
BackupStatusService.shared.refresh(force: false)
|
|
|
|
|
task.setTaskCompleted(success: true)
|
|
|
|
|
}
|
Initial scaffold — NASBackup iOS app
Full project scaffold: XcodeGen project.yml, Core models/protocols/errors,
SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2),
PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager,
all feature UIs (Login, Connect, Browse, Backup, History, Settings),
Shared components and theme. Builds clean with zero errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:05:04 +03:00
|
|
|
}
|
|
|
|
|
}
|