diff options
| author | Christian Cleberg <[email protected]> | 2026-07-17 10:25:53 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-07-17 10:57:30 -0500 |
| commit | d62480233819184db2e55741b05375818ebf3881 (patch) | |
| tree | bc35d414be25c2ababe4c803f88eaaaba6fa46f7 /DomainDig | |
| parent | 6f79345f8dc078b65d976079332cab3ddf7ec98b (diff) | |
| download | domain-dig-d62480233819184db2e55741b05375818ebf3881.tar.gz domain-dig-d62480233819184db2e55741b05375818ebf3881.tar.bz2 domain-dig-d62480233819184db2e55741b05375818ebf3881.zip | |
v4.7.0: Add domain-vs-domain comparison
- DiffService.compare(domainA:domainB:) reuses the existing section-diff
builders to compare two distinct domains' latest reports, returning a new
DomainComparisonResult (parallel to the time-based DomainDiff).
- DomainCompareView: pick two tracked domains and render the comparison with
the existing DomainDiffView section renderer.
- Entry point: "Compare Domains" in the Watchlist toolbar menu, shown once at
least two domains are tracked.
Diffstat (limited to 'DomainDig')
| -rw-r--r-- | DomainDig/DiffService.swift | 51 | ||||
| -rw-r--r-- | DomainDig/DomainCompareView.swift | 82 | ||||
| -rw-r--r-- | DomainDig/WatchlistView.swift | 6 |
3 files changed, 139 insertions, 0 deletions
diff --git a/DomainDig/DiffService.swift b/DomainDig/DiffService.swift index 211a16e..cf25b6f 100644 --- a/DomainDig/DiffService.swift +++ b/DomainDig/DiffService.swift @@ -105,6 +105,32 @@ struct DomainDiff: Identifiable, Equatable, Codable { typealias DomainDiffItem = DiffItem typealias DomainDiffSection = DiffSection +/// Result of comparing two distinct domains' latest reports side by side, as +/// opposed to `DomainDiff`, which compares the same domain across time. +struct DomainComparisonResult: Identifiable, Equatable { + let domainA: String + let domainB: String + let generatedAt: Date + let sections: [DiffSection] + let contextNote: String? + + var id: String { + "\(domainA)-\(domainB)-\(generatedAt.timeIntervalSince1970)" + } + + var changedSections: [DiffSection] { + sections.filter(\.hasChanges) + } + + var changeCount: Int { + sections.reduce(0) { $0 + $1.changeCount } + } + + var severity: ChangeSeverity { + sections.map(\.severity).max() ?? .low + } +} + enum DiffService { static func compare(from oldReport: DomainReport, to newReport: DomainReport) -> DomainDiff { let sections = [ @@ -181,6 +207,31 @@ enum DiffService { ) } + /// Compares two distinct domains' reports section by section. Reuses the same + /// field-level diff logic as time-based comparison; the two reports are simply + /// unrelated domains rather than the same domain at different times. + static func compare(domainA: DomainReport, domainB: DomainReport) -> DomainComparisonResult { + let sections = [ + availabilitySection(from: domainA, to: domainB), + ownershipSection(from: domainA, to: domainB), + dnsSection(from: domainA, to: domainB), + webSection(from: domainA, to: domainB), + emailSection(from: domainA, to: domainB), + networkSection(from: domainA, to: domainB), + subdomainsSection(from: domainA, to: domainB), + intelligenceSection(from: domainA, to: domainB), + riskSection(from: domainA, to: domainB) + ] + + return DomainComparisonResult( + domainA: domainA.domain, + domainB: domainB.domain, + generatedAt: Date(), + sections: sections, + contextNote: comparisonContextNote(from: domainA, to: domainB) + ) + } + static func comparisonContextNote(from oldReport: DomainReport, to newReport: DomainReport) -> String? { var notes: [String] = [] if oldReport.resolverURLString != newReport.resolverURLString { diff --git a/DomainDig/DomainCompareView.swift b/DomainDig/DomainCompareView.swift new file mode 100644 index 0000000..96341c2 --- /dev/null +++ b/DomainDig/DomainCompareView.swift @@ -0,0 +1,82 @@ +import SwiftUI + +/// Side-by-side comparison of two tracked domains' latest reports, reusing the +/// same section-diff rendering (`DomainDiffView`) that time-based history diffs +/// use, via `DiffService.compare(domainA:domainB:)`. +struct DomainCompareView: View { + @Bindable var viewModel: DomainViewModel + @State private var domainAID: UUID? + @State private var domainBID: UUID? + + private var states: [PortfolioDomainStatus] { + viewModel.portfolioDashboardData.domainStates + } + + private var stateA: PortfolioDomainStatus? { + states.first { $0.trackedDomain.id == domainAID } + } + + private var stateB: PortfolioDomainStatus? { + states.first { $0.trackedDomain.id == domainBID } + } + + private var result: DomainComparisonResult? { + guard let stateA, let stateB, stateA.trackedDomain.id != stateB.trackedDomain.id else { + return nil + } + return DiffService.compare(domainA: stateA.report, domainB: stateB.report) + } + + var body: some View { + List { + Section("Domains") { + domainPicker("Domain A", selection: $domainAID) + domainPicker("Domain B", selection: $domainBID) + } + + if domainAID != nil, domainAID == domainBID { + Section { + Text("Choose two different domains to compare.") + .font(.callout) + .foregroundStyle(.secondary) + } + } + + if let result { + Section { + DomainDiffView( + title: "\(result.domainA) vs \(result.domainB)", + sections: result.sections, + contextNote: result.contextNote, + showsUnchanged: true, + highlightedSectionID: nil + ) + } + .listRowBackground(Color.clear) + } else if domainAID == nil || domainBID == nil { + Section { + EmptyStateCardView( + title: "Pick Two Domains", + message: "Select a domain in each slot to see a side-by-side comparison across DNS, TLS, ownership, and risk.", + suggestion: "Both domains must already be tracked.", + systemImage: "arrow.left.arrow.right", + showsCardBackground: false + ) + } + .listRowBackground(Color(.systemGray6).opacity(0.5)) + } + } + .scrollContentBackground(.hidden) + .background(Color.black) + .navigationTitle("Compare Domains") + } + + private func domainPicker(_ label: String, selection: Binding<UUID?>) -> some View { + Picker(label, selection: selection) { + Text("Select a domain").tag(UUID?.none) + ForEach(states) { state in + Text(state.trackedDomain.domain).tag(Optional(state.trackedDomain.id)) + } + } + } +} diff --git a/DomainDig/WatchlistView.swift b/DomainDig/WatchlistView.swift index d91dd5d..43dc66f 100644 --- a/DomainDig/WatchlistView.swift +++ b/DomainDig/WatchlistView.swift @@ -120,6 +120,12 @@ struct WatchlistView: View { showWorkflowAddSheet = true } + if viewModel.trackedDomains.count >= 2 { + NavigationLink("Compare Domains") { + DomainCompareView(viewModel: viewModel) + } + } + Button("Export TXT") { shareTrackedDomains(format: .text) } |
