krz/domain-dig

an ios app for DNS & SSL analysis

clone: git clone https://gitbay.org/krz/domain-dig.git

v5.0.0: DomainDig/DomainViewModel+Monitoring.swift · raw

  1import Foundation
  2import SwiftUI
  3import UserNotifications
  4
  5/// Monitoring configuration surface of `DomainViewModel`: notification
  6/// authorization, the monitoring settings mutators (scope, interval, adaptivity,
  7/// sensitivity, quiet hours, alert filter/toggle, per-domain selection), the
  8/// manual run, and the per-domain status labels.
  9///
 10/// Settings mutations persist through `persistMonitoringSettings(...)` and
 11/// `sanitizeMonitoringSelection()`, which remain on the main type because the
 12/// tracked-domain lifecycle (add/delete/clear) calls them too.
 13extension DomainViewModel {
 14    func refreshMonitoringAuthorizationStatus() async {
 15        let settings = await UNUserNotificationCenter.current().notificationSettings()
 16        monitoringNotificationStatus = settings.authorizationStatus
 17    }
 18
 19    func setMonitoringEnabled(_ isEnabled: Bool) {
 20        guard !isEnabled || FeatureAccessService.hasAccess(to: .automatedMonitoring) else {
 21            monitoringSettings.isEnabled = false
 22            upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring)
 23            return
 24        }
 25
 26        monitoringSettings.isEnabled = isEnabled
 27        persistMonitoringSettings(localActivationConfirmed: isEnabled)
 28        monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule()
 29    }
 30
 31    func setMonitoringScope(_ scope: MonitoringScope) {
 32        monitoringSettings.scope = scope
 33        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
 34    }
 35
 36    func setMonitoringBaseInterval(_ baseInterval: MonitoringBaseInterval) {
 37        guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else {
 38            upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring)
 39            return
 40        }
 41        monitoringSettings.baseInterval = baseInterval.interval
 42        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
 43        monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule()
 44    }
 45
 46    func setMonitoringAdaptiveEnabled(_ isEnabled: Bool) {
 47        guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else {
 48            upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring)
 49            return
 50        }
 51        monitoringSettings.adaptiveEnabled = isEnabled
 52        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
 53        monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule()
 54    }
 55
 56    func setMonitoringSensitivity(_ sensitivity: MonitoringSensitivity) {
 57        guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else {
 58            upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring)
 59            return
 60        }
 61        monitoringSettings.sensitivity = sensitivity
 62        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
 63        monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule()
 64    }
 65
 66    func setMonitoringQuietHours(startHour: Int, endHour: Int, isEnabled: Bool) {
 67        guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else {
 68            upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring)
 69            return
 70        }
 71        monitoringSettings.quietHours = isEnabled ? QuietHours(startHour: startHour, endHour: endHour) : nil
 72        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
 73    }
 74
 75    func setMonitoringAlertFilter(_ filter: MonitoringAlertFilter) {
 76        guard FeatureAccessService.hasAccess(to: .localAlerts) else {
 77            monitoringSettings.alertsEnabled = false
 78            upgradePrompt = FeatureAccessService.upgradePrompt(for: .localAlerts)
 79            return
 80        }
 81        monitoringSettings.alertFilter = filter
 82        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
 83    }
 84
 85    func setMonitoringAlertsEnabled(_ isEnabled: Bool) {
 86        guard !isEnabled || FeatureAccessService.hasAccess(to: .localAlerts) else {
 87            monitoringSettings.alertsEnabled = false
 88            upgradePrompt = FeatureAccessService.upgradePrompt(for: .localAlerts)
 89            return
 90        }
 91        monitoringSettings.alertsEnabled = isEnabled
 92        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
 93    }
 94
 95    func setMonitoringSelection(for trackedDomain: TrackedDomain, isSelected: Bool) {
 96        if isSelected {
 97            if !monitoringSettings.selectedDomainIDs.contains(trackedDomain.id) {
 98                monitoringSettings.selectedDomainIDs.append(trackedDomain.id)
 99            }
100        } else {
101            monitoringSettings.selectedDomainIDs.removeAll { $0 == trackedDomain.id }
102        }
103        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
104    }
105
106    func toggleMonitoring(for trackedDomain: TrackedDomain) {
107        guard canEdit(trackedDomain) else { return }
108        guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else {
109            upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring)
110            return
111        }
112        guard let index = trackedDomains.firstIndex(where: { $0.id == trackedDomain.id }) else { return }
113        trackedDomains[index].monitoringEnabled.toggle()
114        trackedDomains[index].updatedAt = Date()
115        MonitoringStorage.saveTrackedDomains(trackedDomains)
116        CloudSyncService.shared.scheduleSyncIfNeeded()
117        sanitizeMonitoringSelection()
118    }
119
120    func requestMonitoringNotificationAuthorization() async {
121        let granted = await LocalNotificationService.shared.requestAuthorizationIfNeeded()
122        monitoringSettings.alertsEnabled = granted
123        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
124        await refreshMonitoringAuthorizationStatus()
125    }
126
127    func runMonitoringNow() {
128        guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else {
129            upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring)
130            return
131        }
132        guard !monitoringRunInProgress else { return }
133
134        monitoringRunInProgress = true
135        monitoringStatusMessage = nil
136
137        Task { [weak self] in
138            guard let self else { return }
139            let outcome = await DomainMonitoringService.shared.performMonitoring(
140                trigger: .manual,
141                requireEnabledSetting: false
142            )
143            await MainActor.run {
144                self.refreshMonitoringState()
145                self.monitoringRunInProgress = false
146                self.monitoringStatusMessage = outcome.message
147            }
148        }
149    }
150
151    func monitoringIntervalLabel(for trackedDomain: TrackedDomain) -> String {
152        let state = DomainMonitoringScheduler.normalizedMonitoringState(for: trackedDomain, settings: monitoringSettings)
153        return Self.intervalLabel(for: state.currentInterval)
154    }
155
156    func monitoringStatusLabel(for trackedDomain: TrackedDomain) -> String {
157        let state = trackedDomain.monitoringState
158        if let lastChangeDate = state.lastChangeDate,
159           Date().timeIntervalSince(lastChangeDate) <= 6 * 60 * 60 {
160            return "Recently Changed"
161        }
162        if state.consecutiveStableChecks >= 2 {
163            return "Stable"
164        }
165        return trackedDomain.monitoringEnabled ? "Active" : "Paused"
166    }
167
168    private static func intervalLabel(for interval: TimeInterval) -> String {
169        let formatter = DateComponentsFormatter()
170        formatter.allowedUnits = interval < 3600 ? [.minute] : [.hour, .minute]
171        formatter.unitsStyle = .full
172        return formatter.string(from: interval) ?? "Unknown"
173    }
174}