summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-24 23:52:39 -0500
committerChristian Cleberg <[email protected]>2026-07-25 00:00:29 -0500
commit7805888980f6d41bcc881c5d2e6001b2d8e69351 (patch)
tree40710d217106a500428ad8f26c3bddd803d17bdb
parente597cdf5b7084634e300af39b622c8552c9cc5e8 (diff)
downloaddomain-dig-7805888980f6d41bcc881c5d2e6001b2d8e69351.tar.gz
domain-dig-7805888980f6d41bcc881c5d2e6001b2d8e69351.tar.bz2
domain-dig-7805888980f6d41bcc881c5d2e6001b2d8e69351.zip
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).
-rw-r--r--DomainDig/DomainViewModel+Monitoring.swift174
-rw-r--r--DomainDig/DomainViewModel.swift165
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)