krz/domain-dig
an ios app for DNS & SSL analysis
clone: git clone https://gitbay.org/krz/domain-dig.git
v4.8.2: DomainDig/MonitoringView.swift · raw
1import SwiftUI
2
3struct MonitoringView: View {
4 @Environment(\.appDensity) private var appDensity
5 @Bindable var viewModel: DomainViewModel
6
7 private var monitoredDomainsCount: Int {
8 MonitoringStorage.monitoredDomains(
9 settings: viewModel.monitoringSettings,
10 trackedDomains: viewModel.trackedDomains
11 ).count
12 }
13
14 var body: some View {
15 List {
16 Section("Overview") {
17 VStack(alignment: .leading, spacing: 8) {
18 LabeledContent("Status", value: viewModel.monitoringSettings.isEnabled ? "Scheduled" : "Manual only")
19 LabeledContent("Domains", value: "\(monitoredDomainsCount)")
20 LabeledContent("Base Interval", value: MonitoringBaseInterval.nearest(to: viewModel.monitoringSettings.baseInterval).title)
21 LabeledContent("Adaptive", value: viewModel.monitoringSettings.adaptiveEnabled ? viewModel.monitoringSettings.sensitivity.title : "Off")
22 LabeledContent(
23 "Quiet Hours",
24 value: viewModel.monitoringSettings.quietHours.map {
25 "\(Self.hourLabel($0.startHour)) to \(Self.hourLabel($0.endHour))"
26 } ?? "Off"
27 )
28 LabeledContent("Alerts", value: viewModel.monitoringSettings.alertsEnabled ? viewModel.monitoringSettings.alertFilter.title : "Off")
29
30 if let monitoringStatusMessage = viewModel.monitoringStatusMessage,
31 !monitoringStatusMessage.isEmpty {
32 Text(monitoringStatusMessage)
33 .font(appDensity.font(.caption))
34 .foregroundStyle(.secondary)
35 }
36
37 Button(viewModel.monitoringRunInProgress ? "Monitoring…" : "Run Now") {
38 viewModel.runMonitoringNow()
39 }
40 .disabled(viewModel.monitoringRunInProgress)
41 }
42 .padding(.vertical, 4)
43 }
44 .listRowBackground(Color(.systemGray6).opacity(0.5))
45
46 if viewModel.monitoringLogs.isEmpty {
47 Section {
48 EmptyStateCardView(
49 title: "No Monitoring Runs Yet",
50 message: "Monitoring history appears here after manual or background runs finish.",
51 suggestion: "Enable monitoring in Settings or run a manual monitoring sweep.",
52 systemImage: "waveform.path.ecg",
53 showsCardBackground: false
54 )
55 }
56 .listRowBackground(Color(.systemGray6).opacity(0.5))
57 } else {
58 Section("Recent Runs") {
59 ForEach(viewModel.monitoringLogs) { log in
60 NavigationLink {
61 MonitoringLogDetailView(viewModel: viewModel, log: log)
62 } label: {
63 VStack(alignment: .leading, spacing: 6) {
64 HStack {
65 Text(log.trigger.title)
66 .font(appDensity.font(.callout))
67 .foregroundStyle(.primary)
68 Spacer()
69 Text(log.timestamp.formatted(date: .abbreviated, time: .shortened))
70 .font(appDensity.font(.caption2))
71 .foregroundStyle(.secondary)
72 }
73
74 Text(log.summary)
75 .font(appDensity.font(.caption))
76 .foregroundStyle(.secondary)
77
78 HStack(spacing: 8) {
79 metricBadge(title: "\(log.domainsChecked) checked")
80 if log.changesFound > 0 {
81 metricBadge(title: "\(log.changesFound) changed", tint: .orange)
82 }
83 if log.alertsTriggered > 0 {
84 metricBadge(title: "\(log.alertsTriggered) alerts", tint: .red)
85 }
86 }
87 }
88 .padding(.vertical, 4)
89 }
90 }
91 }
92 .listRowBackground(Color(.systemGray6).opacity(0.5))
93 }
94 }
95 .scrollContentBackground(.hidden)
96 .background(Color.black)
97 .navigationTitle("Monitoring")
98 .preferredColorScheme(.dark)
99 .onAppear {
100 viewModel.refreshMonitoringState()
101 }
102 }
103
104 private func metricBadge(title: String, tint: Color = .cyan) -> some View {
105 Text(title)
106 .font(appDensity.font(.caption2))
107 .foregroundStyle(tint)
108 .padding(.horizontal, 8)
109 .padding(.vertical, 4)
110 .background(tint.opacity(0.16))
111 .clipShape(Capsule())
112 }
113
114 private static func hourLabel(_ hour: Int) -> String {
115 let formatter = DateFormatter()
116 formatter.dateFormat = "h a"
117 let components = DateComponents(calendar: .current, hour: hour)
118 return components.date.map(formatter.string(from:)) ?? "\(hour):00"
119 }
120}
121
122struct MonitoringLogDetailView: View {
123 @Environment(\.appDensity) private var appDensity
124 @Bindable var viewModel: DomainViewModel
125 let log: MonitoringLog
126
127 var body: some View {
128 List {
129 Section("Summary") {
130 LabeledContent("Trigger", value: log.trigger.title)
131 LabeledContent("Checked", value: "\(log.domainsChecked)")
132 LabeledContent("Changes", value: "\(log.changesFound)")
133 LabeledContent("Alerts", value: "\(log.alertsTriggered)")
134 LabeledContent("Timestamp", value: log.timestamp.formatted(date: .abbreviated, time: .shortened))
135 }
136 .listRowBackground(Color(.systemGray6).opacity(0.5))
137
138 Section("Domains") {
139 ForEach(log.checkedDomains) { result in
140 VStack(alignment: .leading, spacing: 6) {
141 HStack {
142 Text(result.domain)
143 .font(appDensity.font(.callout))
144 .foregroundStyle(.primary)
145 Spacer()
146 if let alertSeverity = result.alertSeverity {
147 Text(alertSeverity.title.uppercased())
148 .font(appDensity.font(.caption2))
149 .foregroundStyle(color(for: alertSeverity))
150 }
151 }
152
153 Text(result.summaryMessage)
154 .font(appDensity.font(.caption))
155 .foregroundStyle(.secondary)
156
157 HStack(spacing: 10) {
158 Text(result.resultSource.label)
159 Text(result.didChange ? "Changed" : "No change")
160 if result.certificateWarningLevel != .none {
161 Text(result.certificateWarningLevel.title)
162 }
163 }
164 .font(appDensity.font(.caption2))
165 .foregroundStyle(.secondary)
166
167 if let errorMessage = result.errorMessage {
168 Text(errorMessage)
169 .font(appDensity.font(.caption2))
170 .foregroundStyle(.yellow)
171 }
172
173 if let historyEntryID = result.historyEntryID,
174 let entry = viewModel.history.first(where: { $0.id == historyEntryID }) {
175 NavigationLink("Open Snapshot") {
176 HistoryDetailView(viewModel: viewModel, entry: entry)
177 }
178 .font(appDensity.font(.caption))
179 }
180 }
181 .padding(.vertical, 4)
182 }
183 }
184 .listRowBackground(Color(.systemGray6).opacity(0.5))
185
186 if !log.errors.isEmpty {
187 Section("Errors") {
188 ForEach(log.errors, id: \.self) { error in
189 Text(error)
190 .font(appDensity.font(.caption))
191 .foregroundStyle(.secondary)
192 }
193 }
194 .listRowBackground(Color(.systemGray6).opacity(0.5))
195 }
196 }
197 .scrollContentBackground(.hidden)
198 .background(Color.black)
199 .navigationTitle("Run Details")
200 .preferredColorScheme(.dark)
201 }
202
203 private func color(for severity: MonitoringAlertSeverity) -> Color {
204 switch severity {
205 case .info:
206 return .secondary
207 case .warning:
208 return .yellow
209 case .critical:
210 return .red
211 }
212 }
213}