summaryrefslogtreecommitdiff
path: root/DomainDig/Models.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-24 15:04:54 -0500
committerChristian Cleberg <[email protected]>2026-04-24 15:04:54 -0500
commit3f0b7746b1bee4c2f626eb805e3fd13c5f2f5d22 (patch)
tree17c710faf6191ac86ea5b6cab4df240cf2cefef9 /DomainDig/Models.swift
parentd0b3b7a15b59266b4ae4262fdb0364245092c17c (diff)
downloaddomain-dig-3f0b7746b1bee4c2f626eb805e3fd13c5f2f5d22.tar.gz
domain-dig-3f0b7746b1bee4c2f626eb805e3fd13c5f2f5d22.tar.bz2
domain-dig-3f0b7746b1bee4c2f626eb805e3fd13c5f2f5d22.zip
DomainDig v3.8.0 — Smart Monitoring
* Stable domain intervals * Frequent changes decrease intervals * Duplicate states do not trigger alerts * Quiet hours suppress alerts * Quiet hours across midnight work correctly * Sensitivity levels alter behavior as expected
Diffstat (limited to 'DomainDig/Models.swift')
-rw-r--r--DomainDig/Models.swift292
1 files changed, 261 insertions, 31 deletions
diff --git a/DomainDig/Models.swift b/DomainDig/Models.swift
index 51273b4..f872aab 100644
--- a/DomainDig/Models.swift
+++ b/DomainDig/Models.swift
@@ -728,6 +728,179 @@ struct CollaborationMetadata: Codable, Equatable {
}
}
+enum MonitoringSensitivity: String, Codable, CaseIterable, Identifiable, Sendable {
+ case low
+ case medium
+ case high
+
+ var id: String { rawValue }
+
+ var title: String {
+ rawValue.capitalized
+ }
+
+ var minimumInterval: TimeInterval {
+ switch self {
+ case .low:
+ return 30 * 60
+ case .medium:
+ return 15 * 60
+ case .high:
+ return 5 * 60
+ }
+ }
+
+ var intervalMultiplier: Double {
+ switch self {
+ case .low:
+ return 1.5
+ case .medium:
+ return 1.25
+ case .high:
+ return 1.1
+ }
+ }
+}
+
+struct QuietHours: Codable, Equatable, Sendable {
+ var startHour: Int
+ var endHour: Int
+
+ init(startHour: Int, endHour: Int) {
+ self.startHour = max(0, min(23, startHour))
+ self.endHour = max(0, min(23, endHour))
+ }
+
+ func contains(_ date: Date, calendar: Calendar = .current) -> Bool {
+ let hour = calendar.component(.hour, from: date)
+ if startHour <= endHour {
+ return hour >= startHour && hour < endHour
+ }
+ return hour >= startHour || hour < endHour
+ }
+}
+
+enum MonitoringBaseInterval: String, Codable, CaseIterable, Identifiable {
+ case thirtyMinutes
+ case hourly
+ case sixHours
+ case twelveHours
+ case daily
+
+ var id: String { rawValue }
+
+ var title: String {
+ switch self {
+ case .thirtyMinutes:
+ return "Every 30 Minutes"
+ case .hourly:
+ return "Hourly"
+ case .sixHours:
+ return "Every 6 Hours"
+ case .twelveHours:
+ return "Every 12 Hours"
+ case .daily:
+ return "Daily"
+ }
+ }
+
+ var interval: TimeInterval {
+ switch self {
+ case .thirtyMinutes:
+ return 30 * 60
+ case .hourly:
+ return 60 * 60
+ case .sixHours:
+ return 6 * 60 * 60
+ case .twelveHours:
+ return 12 * 60 * 60
+ case .daily:
+ return 24 * 60 * 60
+ }
+ }
+
+ static func nearest(to interval: TimeInterval) -> MonitoringBaseInterval {
+ allCases.min { abs($0.interval - interval) < abs($1.interval - interval) } ?? .twelveHours
+ }
+}
+
+struct MonitoringConfig: Codable, Equatable, Sendable {
+ var isEnabled: Bool
+ var baseInterval: TimeInterval
+ var adaptiveEnabled: Bool
+ var sensitivity: MonitoringSensitivity
+ var quietHours: QuietHours?
+
+ init(
+ isEnabled: Bool = false,
+ baseInterval: TimeInterval = MonitoringBaseInterval.twelveHours.interval,
+ adaptiveEnabled: Bool = true,
+ sensitivity: MonitoringSensitivity = .medium,
+ quietHours: QuietHours? = nil
+ ) {
+ self.isEnabled = isEnabled
+ self.baseInterval = baseInterval
+ self.adaptiveEnabled = adaptiveEnabled
+ self.sensitivity = sensitivity
+ self.quietHours = quietHours
+ }
+
+ var maxInterval: TimeInterval {
+ 24 * 60 * 60
+ }
+
+ var sanitizedBaseInterval: TimeInterval {
+ max(5 * 60, min(baseInterval, maxInterval))
+ }
+}
+
+struct MonitoringState: Codable, Equatable, Sendable {
+ var lastCheck: Date?
+ var lastChangeHash: String?
+ var lastAlertDate: Date?
+ var consecutiveStableChecks: Int
+ var currentInterval: TimeInterval
+ var lastChangeDate: Date?
+
+ init(
+ lastCheck: Date? = nil,
+ lastChangeHash: String? = nil,
+ lastAlertDate: Date? = nil,
+ consecutiveStableChecks: Int = 0,
+ currentInterval: TimeInterval = 0,
+ lastChangeDate: Date? = nil
+ ) {
+ self.lastCheck = lastCheck
+ self.lastChangeHash = lastChangeHash
+ self.lastAlertDate = lastAlertDate
+ self.consecutiveStableChecks = consecutiveStableChecks
+ self.currentInterval = currentInterval
+ self.lastChangeDate = lastChangeDate
+ }
+}
+
+struct MonitoringPendingAlert: Codable, Identifiable, Equatable, Sendable {
+ let id: UUID
+ var detectedAt: Date
+ var message: String
+ var severity: MonitoringAlertSeverity
+ var changeHash: String
+
+ init(
+ id: UUID = UUID(),
+ detectedAt: Date,
+ message: String,
+ severity: MonitoringAlertSeverity,
+ changeHash: String
+ ) {
+ self.id = id
+ self.detectedAt = detectedAt
+ self.message = message
+ self.severity = severity
+ self.changeHash = changeHash
+ }
+}
+
struct TrackedDomain: Codable, Identifiable, Equatable {
let id: UUID
var domain: String
@@ -744,6 +917,8 @@ struct TrackedDomain: Codable, Identifiable, Equatable {
var certificateDaysRemaining: Int?
var lastMonitoredAt: Date?
var lastAlertAt: Date?
+ var monitoringState: MonitoringState
+ var pendingMonitoringAlerts: [MonitoringPendingAlert]
var collaboration: CollaborationMetadata?
init(
@@ -762,6 +937,8 @@ struct TrackedDomain: Codable, Identifiable, Equatable {
certificateDaysRemaining: Int? = nil,
lastMonitoredAt: Date? = nil,
lastAlertAt: Date? = nil,
+ monitoringState: MonitoringState = MonitoringState(),
+ pendingMonitoringAlerts: [MonitoringPendingAlert] = [],
collaboration: CollaborationMetadata? = nil
) {
self.id = id
@@ -779,6 +956,8 @@ struct TrackedDomain: Codable, Identifiable, Equatable {
self.certificateDaysRemaining = certificateDaysRemaining
self.lastMonitoredAt = lastMonitoredAt
self.lastAlertAt = lastAlertAt
+ self.monitoringState = monitoringState
+ self.pendingMonitoringAlerts = pendingMonitoringAlerts
self.collaboration = collaboration
}
@@ -799,35 +978,12 @@ struct TrackedDomain: Codable, Identifiable, Equatable {
certificateDaysRemaining = try container.decodeIfPresent(Int.self, forKey: .certificateDaysRemaining)
lastMonitoredAt = try container.decodeIfPresent(Date.self, forKey: .lastMonitoredAt)
lastAlertAt = try container.decodeIfPresent(Date.self, forKey: .lastAlertAt)
+ monitoringState = try container.decodeIfPresent(MonitoringState.self, forKey: .monitoringState) ?? MonitoringState()
+ pendingMonitoringAlerts = try container.decodeIfPresent([MonitoringPendingAlert].self, forKey: .pendingMonitoringAlerts) ?? []
collaboration = try container.decodeIfPresent(CollaborationMetadata.self, forKey: .collaboration)
}
}
-enum MonitoringFrequency: String, Codable, CaseIterable, Identifiable {
- case daily
- case twiceDaily
-
- var id: String { rawValue }
-
- var title: String {
- switch self {
- case .daily:
- return "Daily"
- case .twiceDaily:
- return "Twice Daily"
- }
- }
-
- var schedulingInterval: TimeInterval {
- switch self {
- case .daily:
- return 24 * 60 * 60
- case .twiceDaily:
- return 12 * 60 * 60
- }
- }
-}
-
enum MonitoringScope: String, Codable, CaseIterable, Identifiable {
case allTracked
case selectedOnly
@@ -913,28 +1069,102 @@ enum MonitoringRunTrigger: String, Codable {
}
struct MonitoringSettings: Codable, Equatable {
- var isEnabled: Bool
- var frequency: MonitoringFrequency
+ private enum CodingKeys: String, CodingKey {
+ case config
+ case isEnabled
+ case frequency
+ case scope
+ case selectedDomainIDs
+ case alertFilter
+ case alertsEnabled
+ }
+
+ var config: MonitoringConfig
var scope: MonitoringScope
var selectedDomainIDs: [UUID]
var alertFilter: MonitoringAlertFilter
var alertsEnabled: Bool
init(
- isEnabled: Bool = false,
- frequency: MonitoringFrequency = .daily,
+ config: MonitoringConfig = MonitoringConfig(),
scope: MonitoringScope = .allTracked,
selectedDomainIDs: [UUID] = [],
alertFilter: MonitoringAlertFilter = .criticalAndWarnings,
alertsEnabled: Bool = false
) {
- self.isEnabled = isEnabled
- self.frequency = frequency
+ self.config = config
self.scope = scope
self.selectedDomainIDs = selectedDomainIDs
self.alertFilter = alertFilter
self.alertsEnabled = alertsEnabled
}
+
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ let legacyEnabled = try container.decodeIfPresent(Bool.self, forKey: .isEnabled) ?? false
+ let legacyFrequencyRaw = try container.decodeIfPresent(String.self, forKey: .frequency)
+
+ if let decodedConfig = try container.decodeIfPresent(MonitoringConfig.self, forKey: .config) {
+ config = decodedConfig
+ } else {
+ let mappedInterval: TimeInterval
+ switch legacyFrequencyRaw {
+ case "daily":
+ mappedInterval = MonitoringBaseInterval.daily.interval
+ case "twiceDaily":
+ mappedInterval = MonitoringBaseInterval.twelveHours.interval
+ default:
+ mappedInterval = MonitoringBaseInterval.twelveHours.interval
+ }
+
+ config = MonitoringConfig(
+ isEnabled: legacyEnabled,
+ baseInterval: mappedInterval,
+ adaptiveEnabled: true,
+ sensitivity: .medium,
+ quietHours: nil
+ )
+ }
+
+ scope = try container.decodeIfPresent(MonitoringScope.self, forKey: .scope) ?? .allTracked
+ selectedDomainIDs = try container.decodeIfPresent([UUID].self, forKey: .selectedDomainIDs) ?? []
+ alertFilter = try container.decodeIfPresent(MonitoringAlertFilter.self, forKey: .alertFilter) ?? .criticalAndWarnings
+ alertsEnabled = try container.decodeIfPresent(Bool.self, forKey: .alertsEnabled) ?? false
+ }
+
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ try container.encode(config, forKey: .config)
+ try container.encode(scope, forKey: .scope)
+ try container.encode(selectedDomainIDs, forKey: .selectedDomainIDs)
+ try container.encode(alertFilter, forKey: .alertFilter)
+ try container.encode(alertsEnabled, forKey: .alertsEnabled)
+ }
+
+ var isEnabled: Bool {
+ get { config.isEnabled }
+ set { config.isEnabled = newValue }
+ }
+
+ var baseInterval: TimeInterval {
+ get { config.baseInterval }
+ set { config.baseInterval = newValue }
+ }
+
+ var adaptiveEnabled: Bool {
+ get { config.adaptiveEnabled }
+ set { config.adaptiveEnabled = newValue }
+ }
+
+ var sensitivity: MonitoringSensitivity {
+ get { config.sensitivity }
+ set { config.sensitivity = newValue }
+ }
+
+ var quietHours: QuietHours? {
+ get { config.quietHours }
+ set { config.quietHours = newValue }
+ }
}
struct MonitoringDomainResult: Codable, Identifiable, Equatable {