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}