summaryrefslogtreecommitdiff
path: root/DomainDig
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-25 00:10:46 -0500
committerChristian Cleberg <[email protected]>2026-07-25 00:11:47 -0500
commit321a10f85435fdcbfb54f109ee53df895a550a42 (patch)
treee8931efbad04ca639329d19c3417c82ccdc49cad /DomainDig
parent9092ab60f82152cc4ae38677c3af4a76013f4e54 (diff)
downloaddomain-dig-321a10f85435fdcbfb54f109ee53df895a550a42.tar.gz
domain-dig-321a10f85435fdcbfb54f109ee53df895a550a42.tar.bz2
domain-dig-321a10f85435fdcbfb54f109ee53df895a550a42.zip
refactor: extract history query surface into DomainViewModel+History (v5 step 4, 5/n)
Fifth slice of the DomainViewModel decomposition. - Moves the history query/compare surface into DomainViewModel+History.swift: per-domain snapshot reads (historyEntries/historyEntry/previousHistoryEntry), timeline grouping (timelineEntries/timelineSections), the two-snapshot selection model (toggle/clear/selectedSnapshots), diff generation and navigation (generateDiff*/moveTo*DiffChange/currentDiffTargetSectionID), and the resolver-mismatch notes. Pure move. - The inspection *pipeline* that produces history — performLookup, applySnapshot, saveHistoryEntry, persistHistory, snapshot-metadata bookkeeping — stays on the main type with the inspection code. This slice needs zero visibility changes: every dependency (the history array, diff state, latestSnapshot) was already internal. The main-file diff is pure deletion. DomainViewModel.swift: 4306 -> 4171 lines (4864 at the start of step 4; -693 total). App builds clean; unit suite 58/58. No project.pbxproj change.
Diffstat (limited to 'DomainDig')
-rw-r--r--DomainDig/DomainViewModel+History.swift147
-rw-r--r--DomainDig/DomainViewModel.swift136
2 files changed, 147 insertions, 136 deletions
diff --git a/DomainDig/DomainViewModel+History.swift b/DomainDig/DomainViewModel+History.swift
new file mode 100644
index 0000000..7b6f80e
--- /dev/null
+++ b/DomainDig/DomainViewModel+History.swift
@@ -0,0 +1,147 @@
+import Foundation
+
+/// History query surface of `DomainViewModel`: reading snapshots for a domain,
+/// grouping them into a timeline, the two-snapshot selection model, and
+/// generating/navigating diffs between snapshots.
+///
+/// This is the read/compare side of history. The pipeline that *produces*
+/// history — `performLookup`, `applySnapshot`, `saveHistoryEntry`,
+/// `persistHistory`, and the snapshot-metadata bookkeeping — stays on the main
+/// type with the inspection code.
+extension DomainViewModel {
+ func resolverMismatchNote(for entry: HistoryEntry) -> String? {
+ guard entry.resolverURLString != resolverURLString else { return nil }
+ return "Current resolver differs from this snapshot. Re-running may produce different evidence."
+ }
+
+ func resolverMismatchNote(for trackedDomain: TrackedDomain) -> String? {
+ guard let snapshot = latestSnapshot(for: trackedDomain), snapshot.resolverURLString != resolverURLString else {
+ return nil
+ }
+ return "Current resolver differs from the latest snapshot for this tracked domain."
+ }
+
+ func comparisonSnapshot(for entry: HistoryEntry) -> LookupSnapshot? {
+ previousHistoryEntry(for: entry)?.snapshot
+ }
+
+ func timelineEntries(for domain: String) -> [SnapshotSummary] {
+ historyEntries(for: domain).map(\.snapshotSummary)
+ }
+
+ func timelineSections(for domain: String, grouping: TimelineGroupingOption? = nil) -> [TimelineSection] {
+ let entries = timelineEntries(for: domain)
+ let grouping = grouping ?? timelineGrouping
+
+ guard grouping == .relativeDay else {
+ return entries.isEmpty ? [] : [TimelineSection(id: "all", title: "All Snapshots", entries: entries)]
+ }
+
+ let calendar = Calendar.current
+ let today = Date()
+ let yesterday = calendar.date(byAdding: .day, value: -1, to: today) ?? today
+ let grouped = Dictionary(grouping: entries) { entry -> String in
+ if calendar.isDate(entry.timestamp, inSameDayAs: today) {
+ return "Today"
+ }
+ if calendar.isDate(entry.timestamp, inSameDayAs: yesterday) {
+ return "Yesterday"
+ }
+ return "Older"
+ }
+
+ return ["Today", "Yesterday", "Older"].compactMap { title in
+ guard let items = grouped[title], !items.isEmpty else { return nil }
+ return TimelineSection(id: title.lowercased(), title: title, entries: items)
+ }
+ }
+
+ func historyEntries(for domain: String) -> [HistoryEntry] {
+ history
+ .filter { $0.domain.caseInsensitiveCompare(domain) == .orderedSame }
+ .sorted { $0.timestamp > $1.timestamp }
+ }
+
+ func historyEntry(withID id: UUID) -> HistoryEntry? {
+ history.first(where: { $0.id == id })
+ }
+
+ func previousHistoryEntry(for entry: HistoryEntry) -> HistoryEntry? {
+ let siblings = historyEntries(for: entry.domain)
+ guard let index = siblings.firstIndex(where: { $0.id == entry.id }) else { return nil }
+ let nextIndex = index + 1
+ guard siblings.indices.contains(nextIndex) else { return nil }
+ return siblings[nextIndex]
+ }
+
+ func toggleSnapshotSelection(_ entry: HistoryEntry) {
+ if selectedSnapshotIDs.contains(entry.id) {
+ selectedSnapshotIDs.remove(entry.id)
+ } else if selectedSnapshotIDs.count < 2 {
+ selectedSnapshotIDs.insert(entry.id)
+ } else if let oldest = selectedSnapshotIDs.first {
+ selectedSnapshotIDs.remove(oldest)
+ selectedSnapshotIDs.insert(entry.id)
+ }
+ }
+
+ func clearSnapshotSelection() {
+ selectedSnapshotIDs.removeAll()
+ }
+
+ var selectedSnapshots: [HistoryEntry] {
+ selectedSnapshotIDs.compactMap(historyEntry(withID:)).sorted { $0.timestamp < $1.timestamp }
+ }
+
+ @discardableResult
+ func generateDiffForSelectedSnapshots(focusSectionID: String? = nil) -> DomainDiff? {
+ guard selectedSnapshots.count == 2 else {
+ activeDomainDiff = nil
+ activeDiffChangeIndex = 0
+ return nil
+ }
+
+ let diff = DiffService.compare(from: selectedSnapshots[0].snapshot, to: selectedSnapshots[1].snapshot)
+ activeDomainDiff = diff
+ if let focusSectionID, let index = diff.changedSectionIDs.firstIndex(of: focusSectionID) {
+ activeDiffChangeIndex = index
+ } else {
+ activeDiffChangeIndex = 0
+ }
+ return diff
+ }
+
+ func generateDiff(from olderEntry: HistoryEntry, to newerEntry: HistoryEntry, focusSectionID: String? = nil) -> DomainDiff {
+ selectedSnapshotIDs = [olderEntry.id, newerEntry.id]
+ let diff = DiffService.compare(from: olderEntry.snapshot, to: newerEntry.snapshot)
+ activeDomainDiff = diff
+ if let focusSectionID, let index = diff.changedSectionIDs.firstIndex(of: focusSectionID) {
+ activeDiffChangeIndex = index
+ } else {
+ activeDiffChangeIndex = 0
+ }
+ return diff
+ }
+
+ func historyEntry(for batchResult: BatchLookupResult) -> HistoryEntry? {
+ guard let historyEntryID = batchResult.historyEntryID else { return nil }
+ return history.first(where: { $0.id == historyEntryID })
+ }
+
+ var currentDiffTargetSectionID: String? {
+ guard let activeDomainDiff, activeDomainDiff.changedSectionIDs.indices.contains(activeDiffChangeIndex) else {
+ return nil
+ }
+ return activeDomainDiff.changedSectionIDs[activeDiffChangeIndex]
+ }
+
+ func moveToNextDiffChange() {
+ guard let activeDomainDiff, !activeDomainDiff.changedSectionIDs.isEmpty else { return }
+ activeDiffChangeIndex = min(activeDiffChangeIndex + 1, activeDomainDiff.changedSectionIDs.count - 1)
+ }
+
+ func moveToPreviousDiffChange() {
+ guard activeDomainDiff != nil else { return }
+ activeDiffChangeIndex = max(activeDiffChangeIndex - 1, 0)
+ }
+}
diff --git a/DomainDig/DomainViewModel.swift b/DomainDig/DomainViewModel.swift
index e0f649b..7246112 100644
--- a/DomainDig/DomainViewModel.swift
+++ b/DomainDig/DomainViewModel.swift
@@ -3047,142 +3047,6 @@ final class DomainViewModel {
}
}
- func resolverMismatchNote(for entry: HistoryEntry) -> String? {
- guard entry.resolverURLString != resolverURLString else { return nil }
- return "Current resolver differs from this snapshot. Re-running may produce different evidence."
- }
-
- func resolverMismatchNote(for trackedDomain: TrackedDomain) -> String? {
- guard let snapshot = latestSnapshot(for: trackedDomain), snapshot.resolverURLString != resolverURLString else {
- return nil
- }
- return "Current resolver differs from the latest snapshot for this tracked domain."
- }
-
- func comparisonSnapshot(for entry: HistoryEntry) -> LookupSnapshot? {
- previousHistoryEntry(for: entry)?.snapshot
- }
-
- func timelineEntries(for domain: String) -> [SnapshotSummary] {
- historyEntries(for: domain).map(\.snapshotSummary)
- }
-
- func timelineSections(for domain: String, grouping: TimelineGroupingOption? = nil) -> [TimelineSection] {
- let entries = timelineEntries(for: domain)
- let grouping = grouping ?? timelineGrouping
-
- guard grouping == .relativeDay else {
- return entries.isEmpty ? [] : [TimelineSection(id: "all", title: "All Snapshots", entries: entries)]
- }
-
- let calendar = Calendar.current
- let today = Date()
- let yesterday = calendar.date(byAdding: .day, value: -1, to: today) ?? today
- let grouped = Dictionary(grouping: entries) { entry -> String in
- if calendar.isDate(entry.timestamp, inSameDayAs: today) {
- return "Today"
- }
- if calendar.isDate(entry.timestamp, inSameDayAs: yesterday) {
- return "Yesterday"
- }
- return "Older"
- }
-
- return ["Today", "Yesterday", "Older"].compactMap { title in
- guard let items = grouped[title], !items.isEmpty else { return nil }
- return TimelineSection(id: title.lowercased(), title: title, entries: items)
- }
- }
-
- func historyEntries(for domain: String) -> [HistoryEntry] {
- history
- .filter { $0.domain.caseInsensitiveCompare(domain) == .orderedSame }
- .sorted { $0.timestamp > $1.timestamp }
- }
-
- func historyEntry(withID id: UUID) -> HistoryEntry? {
- history.first(where: { $0.id == id })
- }
-
- func previousHistoryEntry(for entry: HistoryEntry) -> HistoryEntry? {
- let siblings = historyEntries(for: entry.domain)
- guard let index = siblings.firstIndex(where: { $0.id == entry.id }) else { return nil }
- let nextIndex = index + 1
- guard siblings.indices.contains(nextIndex) else { return nil }
- return siblings[nextIndex]
- }
-
- func toggleSnapshotSelection(_ entry: HistoryEntry) {
- if selectedSnapshotIDs.contains(entry.id) {
- selectedSnapshotIDs.remove(entry.id)
- } else if selectedSnapshotIDs.count < 2 {
- selectedSnapshotIDs.insert(entry.id)
- } else if let oldest = selectedSnapshotIDs.first {
- selectedSnapshotIDs.remove(oldest)
- selectedSnapshotIDs.insert(entry.id)
- }
- }
-
- func clearSnapshotSelection() {
- selectedSnapshotIDs.removeAll()
- }
-
- var selectedSnapshots: [HistoryEntry] {
- selectedSnapshotIDs.compactMap(historyEntry(withID:)).sorted { $0.timestamp < $1.timestamp }
- }
-
- @discardableResult
- func generateDiffForSelectedSnapshots(focusSectionID: String? = nil) -> DomainDiff? {
- guard selectedSnapshots.count == 2 else {
- activeDomainDiff = nil
- activeDiffChangeIndex = 0
- return nil
- }
-
- let diff = DiffService.compare(from: selectedSnapshots[0].snapshot, to: selectedSnapshots[1].snapshot)
- activeDomainDiff = diff
- if let focusSectionID, let index = diff.changedSectionIDs.firstIndex(of: focusSectionID) {
- activeDiffChangeIndex = index
- } else {
- activeDiffChangeIndex = 0
- }
- return diff
- }
-
- func generateDiff(from olderEntry: HistoryEntry, to newerEntry: HistoryEntry, focusSectionID: String? = nil) -> DomainDiff {
- selectedSnapshotIDs = [olderEntry.id, newerEntry.id]
- let diff = DiffService.compare(from: olderEntry.snapshot, to: newerEntry.snapshot)
- activeDomainDiff = diff
- if let focusSectionID, let index = diff.changedSectionIDs.firstIndex(of: focusSectionID) {
- activeDiffChangeIndex = index
- } else {
- activeDiffChangeIndex = 0
- }
- return diff
- }
-
- func historyEntry(for batchResult: BatchLookupResult) -> HistoryEntry? {
- guard let historyEntryID = batchResult.historyEntryID else { return nil }
- return history.first(where: { $0.id == historyEntryID })
- }
-
- var currentDiffTargetSectionID: String? {
- guard let activeDomainDiff, activeDomainDiff.changedSectionIDs.indices.contains(activeDiffChangeIndex) else {
- return nil
- }
- return activeDomainDiff.changedSectionIDs[activeDiffChangeIndex]
- }
-
- func moveToNextDiffChange() {
- guard let activeDomainDiff, !activeDomainDiff.changedSectionIDs.isEmpty else { return }
- activeDiffChangeIndex = min(activeDiffChangeIndex + 1, activeDomainDiff.changedSectionIDs.count - 1)
- }
-
- func moveToPreviousDiffChange() {
- guard activeDomainDiff != nil else { return }
- activeDiffChangeIndex = max(activeDiffChangeIndex - 1, 0)
- }
-
private func exportSnapshots(for domains: [TrackedDomain]) -> [LookupSnapshot] {
let latestEntries = latestSnapshots(for: domains)