krz/domain-dig

an ios app for DNS & SSL analysis

clone: git clone https://gitbay.org/krz/domain-dig.git

v4.4.1: 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}