summaryrefslogtreecommitdiff
path: root/DomainDig/WatchlistView.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-21 21:53:16 -0500
committerChristian Cleberg <[email protected]>2026-04-21 21:53:16 -0500
commit1f58092dcb67cded75a850db364f3b46d86181f3 (patch)
tree662731251ed1022b4a1585ec74379f233ca6705a /DomainDig/WatchlistView.swift
parent2e90ee4ceca73d5126118920b486149cb227c4e9 (diff)
downloaddomain-dig-1f58092dcb67cded75a850db364f3b46d86181f3.tar.gz
domain-dig-1f58092dcb67cded75a850db364f3b46d86181f3.tar.bz2
domain-dig-1f58092dcb67cded75a850db364f3b46d86181f3.zip
feat(v2.2.0): add foreground monitoring, notifications, and smarter diffs
- implement “Check All” sweep for tracked domains - add local notifications for changes and certificate expiration - introduce change severity filtering (low/medium/high) - enhance change summaries and diff readability - add certificate expiration tracking and alerts - improve watchlist indicators for changes - add sweep summary screen - optimize performance for larger watchlists
Diffstat (limited to 'DomainDig/WatchlistView.swift')
-rw-r--r--DomainDig/WatchlistView.swift68
1 files changed, 63 insertions, 5 deletions
diff --git a/DomainDig/WatchlistView.swift b/DomainDig/WatchlistView.swift
index b01b8f9..1f03a0d 100644
--- a/DomainDig/WatchlistView.swift
+++ b/DomainDig/WatchlistView.swift
@@ -9,12 +9,20 @@ struct WatchlistView: View {
if viewModel.batchLookupSource == .watchlistRefresh, (!viewModel.batchResults.isEmpty || viewModel.batchLookupRunning) {
Section("Refresh Progress") {
VStack(alignment: .leading, spacing: 8) {
- if viewModel.batchLookupRunning {
- ProgressView(value: Double(viewModel.batchCompletedCount), total: Double(max(viewModel.batchTotalCount, 1)))
- .tint(.cyan)
+ ProgressView(value: Double(viewModel.batchCompletedCount), total: Double(max(viewModel.batchTotalCount, 1)))
+ .tint(.cyan)
+ HStack {
Text(viewModel.batchProgressLabel)
.font(.system(.caption, design: .monospaced))
.foregroundStyle(.secondary)
+ Spacer()
+ if viewModel.batchLookupRunning {
+ Button("Cancel") {
+ viewModel.cancelBatchLookup()
+ }
+ .buttonStyle(.bordered)
+ .font(.system(.caption2, design: .monospaced))
+ }
}
ForEach(viewModel.batchResults.prefix(5)) { result in
@@ -129,9 +137,10 @@ struct WatchlistView: View {
}
}
- Button("Refresh All") {
+ Button(viewModel.batchLookupRunning ? "Check All Running" : "Check All") {
viewModel.refreshAllTrackedDomains()
}
+ .disabled(viewModel.batchLookupRunning)
Button("Export TXT") {
shareTrackedDomains(asCSV: false)
@@ -151,9 +160,19 @@ struct WatchlistView: View {
.onChange(of: viewModel.rerunNavigationToken) { _, _ in
dismiss()
}
+ .sheet(item: batchSummaryBinding) { summary in
+ BatchSweepSummaryView(viewModel: viewModel, summary: summary)
+ }
.preferredColorScheme(.dark)
}
+ private var batchSummaryBinding: Binding<BatchSweepSummary?> {
+ Binding(
+ get: { viewModel.latestBatchSweepSummary },
+ set: { viewModel.latestBatchSweepSummary = $0 }
+ )
+ }
+
private func deleteFilteredTrackedDomains(at offsets: IndexSet) {
let domains = offsets.map { viewModel.filteredTrackedDomains[$0] }
domains.forEach(viewModel.deleteTrackedDomain)
@@ -197,13 +216,15 @@ struct WatchlistRowView: View {
.font(.system(.caption2, design: .monospaced))
.foregroundStyle(.secondary)
+ indicatorRow
+
if let note = trackedDomain.note?.trimmingCharacters(in: .whitespacesAndNewlines), !note.isEmpty {
Text(note)
.font(.system(.caption, design: .monospaced))
.foregroundStyle(.secondary)
.lineLimit(2)
} else if let summary = trackedDomain.lastChangeSummary {
- Text(summary.changedSections.isEmpty ? "No meaningful changes detected." : summary.changedSections.joined(separator: " • "))
+ Text(summary.message)
.font(.system(.caption, design: .monospaced))
.foregroundStyle(.secondary)
.lineLimit(2)
@@ -270,6 +291,43 @@ struct WatchlistRowView: View {
return Color(.systemGray5).opacity(0.6)
}
}
+
+ @ViewBuilder
+ private var indicatorRow: some View {
+ HStack(spacing: 8) {
+ if let severity = trackedDomain.lastChangeSeverity, severity >= .medium {
+ Label(severity.title, systemImage: severity == .high ? "exclamationmark.circle.fill" : "circle.fill")
+ .font(.system(.caption2, design: .monospaced))
+ .foregroundStyle(severity == .high ? .red : .yellow)
+ } else {
+ Label("Stable", systemImage: "circle.fill")
+ .font(.system(.caption2, design: .monospaced))
+ .foregroundStyle(.secondary)
+ }
+
+ if trackedDomain.certificateWarningLevel != .none {
+ Text(certificateLabel)
+ .font(.system(.caption2, design: .monospaced))
+ .foregroundStyle(trackedDomain.certificateWarningLevel == .critical ? .red : .yellow)
+ .padding(.horizontal, 8)
+ .padding(.vertical, 4)
+ .background((trackedDomain.certificateWarningLevel == .critical ? Color.red : Color.yellow).opacity(0.16))
+ .clipShape(Capsule())
+ }
+ }
+ }
+
+ private var certificateLabel: String {
+ let days = trackedDomain.certificateDaysRemaining.map { "\($0)d" } ?? "Soon"
+ switch trackedDomain.certificateWarningLevel {
+ case .critical:
+ return "Cert \(days)"
+ case .warning:
+ return "Warn \(days)"
+ case .none:
+ return ""
+ }
+ }
}
struct TrackedDomainDetailView: View {