summaryrefslogtreecommitdiff
path: root/DomainDig/DomainViewModel+Monitoring.swift
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 /DomainDig/DomainViewModel+Monitoring.swift
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).
Diffstat (limited to 'DomainDig/DomainViewModel+Monitoring.swift')
-rw-r--r--DomainDig/DomainViewModel+Monitoring.swift174
1 files changed, 174 insertions, 0 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"
+ }
+}