krz/domain-dig

an ios app for DNS & SSL analysis

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

7805888980f6d41bcc881c5d2e6001b2d8e69351

unsigned

author: Christian Cleberg <hello@cleberg.net> · 2026-07-25T04:52:39Z

refactor: extract monitoring surface into DomainViewModel+Monitoring (v5 step 4, 2/n)

Second slice of the DomainViewModel decomposition, stacked on the audit split.

- Moves the monitoring configuration surface — notification authorization, the
  settings mutators (enabled/scope/interval/adaptive/sensitivity/quiet-hours/
  alert filter/alerts/selection), toggleMonitoring, runMonitoringNow, the manual
  run, and the per-domain interval/status labels — plus the private
  intervalLabel helper into DomainViewModel+Monitoring.swift. Pure move.
- persistMonitoringSettings(...) and sanitizeMonitoringSelection() are promoted
  private -> internal: the tracked-domain lifecycle (add/delete/clear) on the
  main type calls them too, so they stay put and are now reachable cross-file.
  The two visibility drops are the only non-deletion edits to DomainViewModel.

DomainViewModel.swift: 4761 -> 4601 lines. App builds clean; unit suite 58/58.
No project.pbxproj change (DomainDig/ is a synchronized group).
 DomainDig/DomainViewModel+Monitoring.swift | 174 +++++++++++++++++++++++++++++
 DomainDig/DomainViewModel.swift            | 165 +--------------------------
 2 files changed, 176 insertions(+), 163 deletions(-)

diff --git a/DomainDig/DomainViewModel+Monitoring.swift b/DomainDig/DomainViewModel+Monitoring.swift
new file mode 100644
index 0000000..57f297b
--- /dev/null
+++ b/DomainDig/DomainViewModel+Monitoring.swift
@@ -0,0 +1,174 @@
+import Foundation
+import SwiftUI
+import UserNotifications
+
+/// Monitoring configuration surface of `DomainViewModel`: notification
+/// authorization, the monitoring settings mutators (scope, interval, adaptivity,
+/// sensitivity, quiet hours, alert filter/toggle, per-domain selection), the
+/// manual run, and the per-domain status labels.
+///
+/// Settings mutations persist through `persistMonitoringSettings(...)` and
+/// `sanitizeMonitoringSelection()`, which remain on the main type because the
+/// tracked-domain lifecycle (add/delete/clear) calls them too.
+extension DomainViewModel {
+    func refreshMonitoringAuthorizationStatus() async {
+        let settings = await UNUserNotificationCenter.current().notificationSettings()
+        monitoringNotificationStatus = settings.authorizationStatus
+    }
+
+    func setMonitoringEnabled(_ isEnabled: Bool) {
+        guard !isEnabled || FeatureAccessService.hasAccess(to: .automatedMonitoring) else {
+            monitoringSettings.isEnabled = false
+            upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring)
+            return
+        }
+
+        monitoringSettings.isEnabled = isEnabled
+        persistMonitoringSettings(localActivationConfirmed: isEnabled)
+        monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule()
+    }
+
+    func setMonitoringScope(_ scope: MonitoringScope) {
+        monitoringSettings.scope = scope
+        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
+    }
+
+    func setMonitoringBaseInterval(_ baseInterval: MonitoringBaseInterval) {
+        guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else {
+            upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring)
+            return
+        }
+        monitoringSettings.baseInterval = baseInterval.interval
+        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
+        monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule()
+    }
+
+    func setMonitoringAdaptiveEnabled(_ isEnabled: Bool) {
+        guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else {
+            upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring)
+            return
+        }
+        monitoringSettings.adaptiveEnabled = isEnabled
+        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
+        monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule()
+    }
+
+    func setMonitoringSensitivity(_ sensitivity: MonitoringSensitivity) {
+        guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else {
+            upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring)
+            return
+        }
+        monitoringSettings.sensitivity = sensitivity
+        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
+        monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule()
+    }
+
+    func setMonitoringQuietHours(startHour: Int, endHour: Int, isEnabled: Bool) {
+        guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else {
+            upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring)
+            return
+        }
+        monitoringSettings.quietHours = isEnabled ? QuietHours(startHour: startHour, endHour: endHour) : nil
+        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
+    }
+
+    func setMonitoringAlertFilter(_ filter: MonitoringAlertFilter) {
+        guard FeatureAccessService.hasAccess(to: .localAlerts) else {
+            monitoringSettings.alertsEnabled = false
+            upgradePrompt = FeatureAccessService.upgradePrompt(for: .localAlerts)
+            return
+        }
+        monitoringSettings.alertFilter = filter
+        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
+    }
+
+    func setMonitoringAlertsEnabled(_ isEnabled: Bool) {
+        guard !isEnabled || FeatureAccessService.hasAccess(to: .localAlerts) else {
+            monitoringSettings.alertsEnabled = false
+            upgradePrompt = FeatureAccessService.upgradePrompt(for: .localAlerts)
+            return
+        }
+        monitoringSettings.alertsEnabled = isEnabled
+        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
+    }
+
+    func setMonitoringSelection(for trackedDomain: TrackedDomain, isSelected: Bool) {
+        if isSelected {
+            if !monitoringSettings.selectedDomainIDs.contains(trackedDomain.id) {
+                monitoringSettings.selectedDomainIDs.append(trackedDomain.id)
+            }
+        } else {
+            monitoringSettings.selectedDomainIDs.removeAll { $0 == trackedDomain.id }
+        }
+        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
+    }
+
+    func toggleMonitoring(for trackedDomain: TrackedDomain) {
+        guard canEdit(trackedDomain) else { return }
+        guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else {
+            upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring)
+            return
+        }
+        guard let index = trackedDomains.firstIndex(where: { $0.id == trackedDomain.id }) else { return }
+        trackedDomains[index].monitoringEnabled.toggle()
+        trackedDomains[index].updatedAt = Date()
+        MonitoringStorage.saveTrackedDomains(trackedDomains)
+        CloudSyncService.shared.scheduleSyncIfNeeded()
+        sanitizeMonitoringSelection()
+    }
+
+    func requestMonitoringNotificationAuthorization() async {
+        let granted = await LocalNotificationService.shared.requestAuthorizationIfNeeded()
+        monitoringSettings.alertsEnabled = granted
+        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
+        await refreshMonitoringAuthorizationStatus()
+    }
+
+    func runMonitoringNow() {
+        guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else {
+            upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring)
+            return
+        }
+        guard !monitoringRunInProgress else { return }
+
+        monitoringRunInProgress = true
+        monitoringStatusMessage = nil
+
+        Task { [weak self] in
+            guard let self else { return }
+            let outcome = await DomainMonitoringService.shared.performMonitoring(
+                trigger: .manual,
+                requireEnabledSetting: false
+            )
+            await MainActor.run {
+                self.refreshMonitoringState()
+                self.monitoringRunInProgress = false
+                self.monitoringStatusMessage = outcome.message
+            }
+        }
+    }
+
+    func monitoringIntervalLabel(for trackedDomain: TrackedDomain) -> String {
+        let state = DomainMonitoringScheduler.normalizedMonitoringState(for: trackedDomain, settings: monitoringSettings)
+        return Self.intervalLabel(for: state.currentInterval)
+    }
+
+    func monitoringStatusLabel(for trackedDomain: TrackedDomain) -> String {
+        let state = trackedDomain.monitoringState
+        if let lastChangeDate = state.lastChangeDate,
+           Date().timeIntervalSince(lastChangeDate) <= 6 * 60 * 60 {
+            return "Recently Changed"
+        }
+        if state.consecutiveStableChecks >= 2 {
+            return "Stable"
+        }
+        return trackedDomain.monitoringEnabled ? "Active" : "Paused"
+    }
+
+    private static func intervalLabel(for interval: TimeInterval) -> String {
+        let formatter = DateComponentsFormatter()
+        formatter.allowedUnits = interval < 3600 ? [.minute] : [.hour, .minute]
+        formatter.unitsStyle = .full
+        return formatter.string(from: interval) ?? "Unknown"
+    }
+}
diff --git a/DomainDig/DomainViewModel.swift b/DomainDig/DomainViewModel.swift
index fdf6b84..3814704 100644
--- a/DomainDig/DomainViewModel.swift
+++ b/DomainDig/DomainViewModel.swift
@@ -1100,167 +1100,6 @@ final class DomainViewModel {
         await refreshMonitoringAuthorizationStatus()
     }
 
-    func refreshMonitoringAuthorizationStatus() async {
-        let settings = await UNUserNotificationCenter.current().notificationSettings()
-        monitoringNotificationStatus = settings.authorizationStatus
-    }
-
-    func setMonitoringEnabled(_ isEnabled: Bool) {
-        guard !isEnabled || FeatureAccessService.hasAccess(to: .automatedMonitoring) else {
-            monitoringSettings.isEnabled = false
-            upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring)
-            return
-        }
-
-        monitoringSettings.isEnabled = isEnabled
-        persistMonitoringSettings(localActivationConfirmed: isEnabled)
-        monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule()
-    }
-
-    func setMonitoringScope(_ scope: MonitoringScope) {
-        monitoringSettings.scope = scope
-        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
-    }
-
-    func setMonitoringBaseInterval(_ baseInterval: MonitoringBaseInterval) {
-        guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else {
-            upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring)
-            return
-        }
-        monitoringSettings.baseInterval = baseInterval.interval
-        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
-        monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule()
-    }
-
-    func setMonitoringAdaptiveEnabled(_ isEnabled: Bool) {
-        guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else {
-            upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring)
-            return
-        }
-        monitoringSettings.adaptiveEnabled = isEnabled
-        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
-        monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule()
-    }
-
-    func setMonitoringSensitivity(_ sensitivity: MonitoringSensitivity) {
-        guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else {
-            upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring)
-            return
-        }
-        monitoringSettings.sensitivity = sensitivity
-        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
-        monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule()
-    }
-
-    func setMonitoringQuietHours(startHour: Int, endHour: Int, isEnabled: Bool) {
-        guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else {
-            upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring)
-            return
-        }
-        monitoringSettings.quietHours = isEnabled ? QuietHours(startHour: startHour, endHour: endHour) : nil
-        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
-    }
-
-    func setMonitoringAlertFilter(_ filter: MonitoringAlertFilter) {
-        guard FeatureAccessService.hasAccess(to: .localAlerts) else {
-            monitoringSettings.alertsEnabled = false
-            upgradePrompt = FeatureAccessService.upgradePrompt(for: .localAlerts)
-            return
-        }
-        monitoringSettings.alertFilter = filter
-        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
-    }
-
-    func setMonitoringAlertsEnabled(_ isEnabled: Bool) {
-        guard !isEnabled || FeatureAccessService.hasAccess(to: .localAlerts) else {
-            monitoringSettings.alertsEnabled = false
-            upgradePrompt = FeatureAccessService.upgradePrompt(for: .localAlerts)
-            return
-        }
-        monitoringSettings.alertsEnabled = isEnabled
-        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
-    }
-
-    func setMonitoringSelection(for trackedDomain: TrackedDomain, isSelected: Bool) {
-        if isSelected {
-            if !monitoringSettings.selectedDomainIDs.contains(trackedDomain.id) {
-                monitoringSettings.selectedDomainIDs.append(trackedDomain.id)
-            }
-        } else {
-            monitoringSettings.selectedDomainIDs.removeAll { $0 == trackedDomain.id }
-        }
-        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
-    }
-
-    func toggleMonitoring(for trackedDomain: TrackedDomain) {
-        guard canEdit(trackedDomain) else { return }
-        guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else {
-            upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring)
-            return
-        }
-        guard let index = trackedDomains.firstIndex(where: { $0.id == trackedDomain.id }) else { return }
-        trackedDomains[index].monitoringEnabled.toggle()
-        trackedDomains[index].updatedAt = Date()
-        MonitoringStorage.saveTrackedDomains(trackedDomains)
-        CloudSyncService.shared.scheduleSyncIfNeeded()
-        sanitizeMonitoringSelection()
-    }
-
-    func requestMonitoringNotificationAuthorization() async {
-        let granted = await LocalNotificationService.shared.requestAuthorizationIfNeeded()
-        monitoringSettings.alertsEnabled = granted
-        persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled)
-        await refreshMonitoringAuthorizationStatus()
-    }
-
-    func runMonitoringNow() {
-        guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else {
-            upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring)
-            return
-        }
-        guard !monitoringRunInProgress else { return }
-
-        monitoringRunInProgress = true
-        monitoringStatusMessage = nil
-
-        Task { [weak self] in
-            guard let self else { return }
-            let outcome = await DomainMonitoringService.shared.performMonitoring(
-                trigger: .manual,
-                requireEnabledSetting: false
-            )
-            await MainActor.run {
-                self.refreshMonitoringState()
-                self.monitoringRunInProgress = false
-                self.monitoringStatusMessage = outcome.message
-            }
-        }
-    }
-
-    func monitoringIntervalLabel(for trackedDomain: TrackedDomain) -> String {
-        let state = DomainMonitoringScheduler.normalizedMonitoringState(for: trackedDomain, settings: monitoringSettings)
-        return Self.intervalLabel(for: state.currentInterval)
-    }
-
-    func monitoringStatusLabel(for trackedDomain: TrackedDomain) -> String {
-        let state = trackedDomain.monitoringState
-        if let lastChangeDate = state.lastChangeDate,
-           Date().timeIntervalSince(lastChangeDate) <= 6 * 60 * 60 {
-            return "Recently Changed"
-        }
-        if state.consecutiveStableChecks >= 2 {
-            return "Stable"
-        }
-        return trackedDomain.monitoringEnabled ? "Active" : "Paused"
-    }
-
-    private static func intervalLabel(for interval: TimeInterval) -> String {
-        let formatter = DateComponentsFormatter()
-        formatter.allowedUnits = interval < 3600 ? [.minute] : [.hour, .minute]
-        formatter.unitsStyle = .full
-        return formatter.string(from: interval) ?? "Unknown"
-    }
-
     func rerunLookup(from entry: HistoryEntry, useSnapshotResolver: Bool) {
         if useSnapshotResolver {
             UserDefaults.standard.set(entry.resolverURLString, forKey: DNSResolverOption.userDefaultsKey)
@@ -2590,13 +2429,13 @@ final class DomainViewModel {
         return existingMax + 1
     }
 
-    private func persistMonitoringSettings(localActivationConfirmed: Bool = false) {
+    func persistMonitoringSettings(localActivationConfirmed: Bool = false) {
         monitoringSettings = MonitoringStorage.sanitizeSettings(monitoringSettings, trackedDomains: trackedDomains)
         MonitoringStorage.saveSettings(monitoringSettings)
         CloudSyncService.shared.markMonitoringSettingsChanged(localActivationConfirmed: localActivationConfirmed)
     }
 
-    private func sanitizeMonitoringSelection() {
+    func sanitizeMonitoringSelection() {
         monitoringSettings = MonitoringStorage.sanitizeSettings(monitoringSettings, trackedDomains: trackedDomains)
         MonitoringStorage.saveSettings(monitoringSettings)
         CloudSyncService.shared.markMonitoringSettingsChanged(localActivationConfirmed: false)