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}