krz/domain-dig
an ios app for DNS & SSL analysis
clone: git clone https://gitbay.org/krz/domain-dig.git
v5.0.1: DomainDig/DomainViewModel+History.swift · raw
1import Foundation
2
3/// History query surface of `DomainViewModel`: reading snapshots for a domain,
4/// grouping them into a timeline, the two-snapshot selection model, and
5/// generating/navigating diffs between snapshots.
6///
7/// This is the read/compare side of history. The pipeline that *produces*
8/// history — `performLookup`, `applySnapshot`, `saveHistoryEntry`,
9/// `persistHistory`, and the snapshot-metadata bookkeeping — stays on the main
10/// type with the inspection code.
11extension DomainViewModel {
12 func resolverMismatchNote(for entry: HistoryEntry) -> String? {
13 guard entry.resolverURLString != resolverURLString else { return nil }
14 return "Current resolver differs from this snapshot. Re-running may produce different evidence."
15 }
16
17 func resolverMismatchNote(for trackedDomain: TrackedDomain) -> String? {
18 guard let snapshot = latestSnapshot(for: trackedDomain), snapshot.resolverURLString != resolverURLString else {
19 return nil
20 }
21 return "Current resolver differs from the latest snapshot for this tracked domain."
22 }
23
24 func comparisonSnapshot(for entry: HistoryEntry) -> LookupSnapshot? {
25 previousHistoryEntry(for: entry)?.snapshot
26 }
27
28 func timelineEntries(for domain: String) -> [SnapshotSummary] {
29 historyEntries(for: domain).map(\.snapshotSummary)
30 }
31
32 func timelineSections(for domain: String, grouping: TimelineGroupingOption? = nil) -> [TimelineSection] {
33 let entries = timelineEntries(for: domain)
34 let grouping = grouping ?? timelineGrouping
35
36 guard grouping == .relativeDay else {
37 return entries.isEmpty ? [] : [TimelineSection(id: "all", title: "All Snapshots", entries: entries)]
38 }
39
40 let calendar = Calendar.current
41 let today = Date()
42 let yesterday = calendar.date(byAdding: .day, value: -1, to: today) ?? today
43 let grouped = Dictionary(grouping: entries) { entry -> String in
44 if calendar.isDate(entry.timestamp, inSameDayAs: today) {
45 return "Today"
46 }
47 if calendar.isDate(entry.timestamp, inSameDayAs: yesterday) {
48 return "Yesterday"
49 }
50 return "Older"
51 }
52
53 return ["Today", "Yesterday", "Older"].compactMap { title in
54 guard let items = grouped[title], !items.isEmpty else { return nil }
55 return TimelineSection(id: title.lowercased(), title: title, entries: items)
56 }
57 }
58
59 func historyEntries(for domain: String) -> [HistoryEntry] {
60 history
61 .filter { $0.domain.caseInsensitiveCompare(domain) == .orderedSame }
62 .sorted { $0.timestamp > $1.timestamp }
63 }
64
65 func historyEntry(withID id: UUID) -> HistoryEntry? {
66 history.first(where: { $0.id == id })
67 }
68
69 func previousHistoryEntry(for entry: HistoryEntry) -> HistoryEntry? {
70 let siblings = historyEntries(for: entry.domain)
71 guard let index = siblings.firstIndex(where: { $0.id == entry.id }) else { return nil }
72 let nextIndex = index + 1
73 guard siblings.indices.contains(nextIndex) else { return nil }
74 return siblings[nextIndex]
75 }
76
77 func toggleSnapshotSelection(_ entry: HistoryEntry) {
78 if selectedSnapshotIDs.contains(entry.id) {
79 selectedSnapshotIDs.remove(entry.id)
80 } else if selectedSnapshotIDs.count < 2 {
81 selectedSnapshotIDs.insert(entry.id)
82 } else if let oldest = selectedSnapshotIDs.first {
83 selectedSnapshotIDs.remove(oldest)
84 selectedSnapshotIDs.insert(entry.id)
85 }
86 }
87
88 func clearSnapshotSelection() {
89 selectedSnapshotIDs.removeAll()
90 }
91
92 var selectedSnapshots: [HistoryEntry] {
93 selectedSnapshotIDs.compactMap(historyEntry(withID:)).sorted { $0.timestamp < $1.timestamp }
94 }
95
96 @discardableResult
97 func generateDiffForSelectedSnapshots(focusSectionID: String? = nil) -> DomainDiff? {
98 guard selectedSnapshots.count == 2 else {
99 activeDomainDiff = nil
100 activeDiffChangeIndex = 0
101 return nil
102 }
103
104 let diff = DiffService.compare(from: selectedSnapshots[0].snapshot, to: selectedSnapshots[1].snapshot)
105 activeDomainDiff = diff
106 if let focusSectionID, let index = diff.changedSectionIDs.firstIndex(of: focusSectionID) {
107 activeDiffChangeIndex = index
108 } else {
109 activeDiffChangeIndex = 0
110 }
111 return diff
112 }
113
114 func generateDiff(from olderEntry: HistoryEntry, to newerEntry: HistoryEntry, focusSectionID: String? = nil) -> DomainDiff {
115 selectedSnapshotIDs = [olderEntry.id, newerEntry.id]
116 let diff = DiffService.compare(from: olderEntry.snapshot, to: newerEntry.snapshot)
117 activeDomainDiff = diff
118 if let focusSectionID, let index = diff.changedSectionIDs.firstIndex(of: focusSectionID) {
119 activeDiffChangeIndex = index
120 } else {
121 activeDiffChangeIndex = 0
122 }
123 return diff
124 }
125
126 func historyEntry(for batchResult: BatchLookupResult) -> HistoryEntry? {
127 guard let historyEntryID = batchResult.historyEntryID else { return nil }
128 return history.first(where: { $0.id == historyEntryID })
129 }
130
131 var currentDiffTargetSectionID: String? {
132 guard let activeDomainDiff, activeDomainDiff.changedSectionIDs.indices.contains(activeDiffChangeIndex) else {
133 return nil
134 }
135 return activeDomainDiff.changedSectionIDs[activeDiffChangeIndex]
136 }
137
138 func moveToNextDiffChange() {
139 guard let activeDomainDiff, !activeDomainDiff.changedSectionIDs.isEmpty else { return }
140 activeDiffChangeIndex = min(activeDiffChangeIndex + 1, activeDomainDiff.changedSectionIDs.count - 1)
141 }
142
143 func moveToPreviousDiffChange() {
144 guard activeDomainDiff != nil else { return }
145 activeDiffChangeIndex = max(activeDiffChangeIndex - 1, 0)
146 }
147}