krz/domain-dig
an ios app for DNS & SSL analysis
clone: git clone https://gitbay.org/krz/domain-dig.git
v4.9.0: 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(Color(.appTextSecondary))
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(.appSurface))
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(.appSurface))
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(Color(.appTextSecondary))
72 }
73
74 Text(log.summary)
75 .font(appDensity.font(.caption))
76 .foregroundStyle(Color(.appTextSecondary))
77
78 HStack(spacing: 8) {
79 metricBadge(title: "\(log.domainsChecked) checked")
80 if log.changesFound > 0 {
81 metricBadge(title: "\(log.changesFound) changed", tone: .warning)
82 }
83 if log.alertsTriggered > 0 {
84 metricBadge(title: "\(log.alertsTriggered) alerts", tone: .critical)
85 }
86 }
87 }
88 .padding(.vertical, 4)
89 }
90 }
91 }
92 .listRowBackground(Color(.appSurface))
93 }
94 }
95 .scrollContentBackground(.hidden)
96 .background(Color(.appBackground))
97 .navigationTitle("Monitoring")
98 .onAppear {
99 viewModel.refreshMonitoringState()
100 }
101 }
102
103 private func metricBadge(title: String, tone: AppStatusTone = .info) -> some View {
104 Text(title)
105 .font(appDensity.font(.caption2))
106 .foregroundStyle(tone.foreground)
107 .padding(.horizontal, 8)
108 .padding(.vertical, 4)
109 .background(tone.surface)
110 .clipShape(Capsule())
111 }
112
113 private static func hourLabel(_ hour: Int) -> String {
114 let formatter = DateFormatter()
115 formatter.dateFormat = "h a"
116 let components = DateComponents(calendar: .current, hour: hour)
117 return components.date.map(formatter.string(from:)) ?? "\(hour):00"
118 }
119}
120
121struct MonitoringLogDetailView: View {
122 @Environment(\.appDensity) private var appDensity
123 @Bindable var viewModel: DomainViewModel
124 let log: MonitoringLog
125
126 var body: some View {
127 List {
128 Section("Summary") {
129 LabeledContent("Trigger", value: log.trigger.title)
130 LabeledContent("Checked", value: "\(log.domainsChecked)")
131 LabeledContent("Changes", value: "\(log.changesFound)")
132 LabeledContent("Alerts", value: "\(log.alertsTriggered)")
133 LabeledContent("Timestamp", value: log.timestamp.formatted(date: .abbreviated, time: .shortened))
134 }
135 .listRowBackground(Color(.appSurface))
136
137 Section("Domains") {
138 ForEach(log.checkedDomains) { result in
139 VStack(alignment: .leading, spacing: 6) {
140 HStack {
141 Text(result.domain)
142 .font(appDensity.font(.callout))
143 .foregroundStyle(.primary)
144 Spacer()
145 if let alertSeverity = result.alertSeverity {
146 Text(alertSeverity.title.uppercased())
147 .font(appDensity.font(.caption2))
148 .foregroundStyle(color(for: alertSeverity))
149 }
150 }
151
152 Text(result.summaryMessage)
153 .font(appDensity.font(.caption))
154 .foregroundStyle(Color(.appTextSecondary))
155
156 HStack(spacing: 10) {
157 Text(result.resultSource.label)
158 Text(result.didChange ? "Changed" : "No change")
159 if result.certificateWarningLevel != .none {
160 Text(result.certificateWarningLevel.title)
161 }
162 }
163 .font(appDensity.font(.caption2))
164 .foregroundStyle(Color(.appTextSecondary))
165
166 if let errorMessage = result.errorMessage {
167 Text(errorMessage)
168 .font(appDensity.font(.caption2))
169 .foregroundStyle(Color(.statusWarning))
170 }
171
172 if let historyEntryID = result.historyEntryID,
173 let entry = viewModel.history.first(where: { $0.id == historyEntryID }) {
174 NavigationLink("Open Snapshot") {
175 HistoryDetailView(viewModel: viewModel, entry: entry)
176 }
177 .font(appDensity.font(.caption))
178 }
179 }
180 .padding(.vertical, 4)
181 }
182 }
183 .listRowBackground(Color(.appSurface))
184
185 if !log.errors.isEmpty {
186 Section("Errors") {
187 ForEach(log.errors, id: \.self) { error in
188 Text(error)
189 .font(appDensity.font(.caption))
190 .foregroundStyle(Color(.appTextSecondary))
191 }
192 }
193 .listRowBackground(Color(.appSurface))
194 }
195 }
196 .scrollContentBackground(.hidden)
197 .background(Color(.appBackground))
198 .navigationTitle("Run Details")
199 }
200
201 private func color(for severity: MonitoringAlertSeverity) -> Color {
202 switch severity {
203 case .info:
204 return .secondary
205 case .warning:
206 return Color(.statusWarning)
207 case .critical:
208 return Color(.statusCritical)
209 }
210 }
211}