summaryrefslogtreecommitdiff
path: root/DomainDig
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-17 10:25:53 -0500
committerChristian Cleberg <[email protected]>2026-07-17 10:57:30 -0500
commitd62480233819184db2e55741b05375818ebf3881 (patch)
treebc35d414be25c2ababe4c803f88eaaaba6fa46f7 /DomainDig
parent6f79345f8dc078b65d976079332cab3ddf7ec98b (diff)
downloaddomain-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.swift51
-rw-r--r--DomainDig/DomainCompareView.swift82
-rw-r--r--DomainDig/WatchlistView.swift6
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)
}