krz/domain-dig

an ios app for DNS & SSL analysis

clone: git clone https://gitbay.org/krz/domain-dig.git

3846dc2f5dee69fc4ffbc052009a60e17d60e36b

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-04-22T04:21:19Z

feat(v2.5.0): add caching, request deduplication, and performance improvements
 DomainDig.xcodeproj/project.pbxproj       |  78 ++++----
 DomainDig/BatchResultsView.swift          |   5 +-
 DomainDig/ContentView.swift               |  50 ++++++
 DomainDig/DomainViewModel.swift           | 267 +++++++++++++++++++++------
 DomainDig/LookupRuntime.swift             | 289 ++++++++++++++++++++++++++++++
 DomainDig/Models.swift                    |  40 +++++
 DomainDig/RDAPService.swift               |  31 +---
 DomainDig/SubdomainDiscoveryService.swift |  45 +----
 DomainDigCLI.swift                        |  32 +++-
 DomainInspectionService.swift             | 285 +++++++++++++++++++++++------
 DomainReportBuilder.swift                 |   2 +
 DomainReportExporter.swift                |   3 +
 LookupSnapshot.swift                      |  12 +-
 13 files changed, 916 insertions(+), 223 deletions(-)

diff --git a/DomainDig.xcodeproj/project.pbxproj b/DomainDig.xcodeproj/project.pbxproj
index 40dadc9..fbaa03c 100644
--- a/DomainDig.xcodeproj/project.pbxproj
+++ b/DomainDig.xcodeproj/project.pbxproj
@@ -12,16 +12,20 @@
 		8BBFEF0A2F9874AE00E8E144 /* DomainReportBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBFEF042F9874AE00E8E144 /* DomainReportBuilder.swift */; };
 		8BBFEF0B2F9874AE00E8E144 /* DomainReportExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBFEF052F9874AE00E8E144 /* DomainReportExporter.swift */; };
 		8BBFEF0C2F9874AE00E8E144 /* LookupSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBFEF062F9874AE00E8E144 /* LookupSnapshot.swift */; };
+		8BBFEFCA2F987E8700E8E144 /* DomainInspectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBFEF032F9874AE00E8E144 /* DomainInspectionService.swift */; };
+		8BBFEFCB2F987E8700E8E144 /* DomainReportBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBFEF042F9874AE00E8E144 /* DomainReportBuilder.swift */; };
+		8BBFEFCC2F987E8700E8E144 /* DomainReportExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBFEF052F9874AE00E8E144 /* DomainReportExporter.swift */; };
+		8BBFEFCD2F987E8700E8E144 /* LookupSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBFEF062F9874AE00E8E144 /* LookupSnapshot.swift */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXFileReference section */
 		8B7800692F6090E300933221 /* DomainDig.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DomainDig.app; sourceTree = BUILT_PRODUCTS_DIR; };
-		8BF124F92F70000100933221 /* domaindig */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = domaindig; sourceTree = BUILT_PRODUCTS_DIR; };
 		8BBFEF022F9874AE00E8E144 /* DomainDigCLI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainDigCLI.swift; sourceTree = "<group>"; };
 		8BBFEF032F9874AE00E8E144 /* DomainInspectionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainInspectionService.swift; sourceTree = "<group>"; };
 		8BBFEF042F9874AE00E8E144 /* DomainReportBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainReportBuilder.swift; sourceTree = "<group>"; };
 		8BBFEF052F9874AE00E8E144 /* DomainReportExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainReportExporter.swift; sourceTree = "<group>"; };
 		8BBFEF062F9874AE00E8E144 /* LookupSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LookupSnapshot.swift; sourceTree = "<group>"; };
+		8BF124F92F70000100933221 /* domaindig */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = domaindig; sourceTree = BUILT_PRODUCTS_DIR; };
 /* End PBXFileReference section */
 
 /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@@ -33,11 +37,9 @@
 			);
 			target = 8B7800682F6090E300933221 /* DomainDig */;
 		};
-/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
 		8BF124FA2F70000100933221 /* Exceptions for "DomainDig" folder in "DomainDigCLI" target */ = {
 			isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
 			membershipExceptions = (
-				Assets.xcassets,
 				BatchResultsView.swift,
 				BatchSweepSummaryView.swift,
 				ContentView.swift,
@@ -45,13 +47,13 @@
 				DomainViewModel.swift,
 				ExportPresenter.swift,
 				HistoryView.swift,
-				Info.plist,
 				LocalNotificationService.swift,
 				SavedDomainsView.swift,
 				WatchlistView.swift,
 			);
 			target = 8BF124FB2F70000100933221 /* DomainDigCLI */;
 		};
+/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
 
 /* Begin PBXFileSystemSynchronizedRootGroup section */
 		8B78006B2F6090E300933221 /* DomainDig */ = {
@@ -132,7 +134,7 @@
 		};
 		8BF124FB2F70000100933221 /* DomainDigCLI */ = {
 			isa = PBXNativeTarget;
-			buildConfigurationList = 8BF125042F70000100933221 /* Build configuration list for PBXNativeTarget "DomainDigCLI" */;
+			buildConfigurationList = 8BBFF0292F987EF700E8E144 /* Build configuration list */;
 			buildPhases = (
 				8BF124FC2F70000100933221 /* Sources */,
 				8BF124FD2F70000100933221 /* Frameworks */,
@@ -201,7 +203,6 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				8BBFEF082F9874AE00E8E144 /* DomainDigCLI.swift in Sources */,
 				8BBFEF092F9874AE00E8E144 /* DomainInspectionService.swift in Sources */,
 				8BBFEF0A2F9874AE00E8E144 /* DomainReportBuilder.swift in Sources */,
 				8BBFEF0B2F9874AE00E8E144 /* DomainReportExporter.swift in Sources */,
@@ -209,6 +210,18 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		8BF124FC2F70000100933221 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				8BBFEF082F9874AE00E8E144 /* DomainDigCLI.swift in Sources */,
+				8BBFEFCA2F987E8700E8E144 /* DomainInspectionService.swift in Sources */,
+				8BBFEFCB2F987E8700E8E144 /* DomainReportBuilder.swift in Sources */,
+				8BBFEFCC2F987E8700E8E144 /* DomainReportExporter.swift in Sources */,
+				8BBFEFCD2F987E8700E8E144 /* LookupSnapshot.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 /* End PBXSourcesBuildPhase section */
 
 /* Begin XCBuildConfiguration section */
@@ -341,7 +354,7 @@
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 17;
+				CURRENT_PROJECT_VERSION = 18;
 				DEVELOPMENT_TEAM = ZCNAX3VL9D;
 				ENABLE_PREVIEWS = YES;
 				GENERATE_INFOPLIST_FILE = YES;
@@ -358,7 +371,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 2.4.0;
+				MARKETING_VERSION = 2.5.0;
 				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -377,7 +390,7 @@
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 17;
+				CURRENT_PROJECT_VERSION = 18;
 				DEVELOPMENT_TEAM = ZCNAX3VL9D;
 				ENABLE_PREVIEWS = YES;
 				GENERATE_INFOPLIST_FILE = YES;
@@ -394,7 +407,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 2.4.0;
+				MARKETING_VERSION = 2.5.0;
 				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -407,44 +420,22 @@
 			};
 			name = Release;
 		};
-		8BF125002F70000100933221 /* Debug */ = {
+		8BBFF0272F987E8700E8E144 /* Debug */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
-				CODE_SIGNING_ALLOWED = NO;
-				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 17;
-				DEVELOPMENT_TEAM = ZCNAX3VL9D;
-				ENABLE_TESTABILITY = YES;
-				GENERATE_INFOPLIST_FILE = YES;
-				MACOSX_DEPLOYMENT_TARGET = 14.0;
-				MARKETING_VERSION = 2.4.0;
-				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDigCLI;
+				COPY_PHASE_STRIP = NO;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_OPTIMIZATION_LEVEL = 0;
 				PRODUCT_NAME = domaindig;
-				SDKROOT = macosx;
-				SWIFT_APPROACHABLE_CONCURRENCY = YES;
-				SWIFT_EMIT_LOC_STRINGS = NO;
-				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
-				SWIFT_VERSION = 5.0;
 			};
 			name = Debug;
 		};
-		8BF125012F70000100933221 /* Release */ = {
+		8BBFF0282F987E8700E8E144 /* Release */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
-				CODE_SIGNING_ALLOWED = NO;
-				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 17;
-				DEVELOPMENT_TEAM = ZCNAX3VL9D;
-				GENERATE_INFOPLIST_FILE = YES;
-				MACOSX_DEPLOYMENT_TARGET = 14.0;
-				MARKETING_VERSION = 2.4.0;
-				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDigCLI;
+				COPY_PHASE_STRIP = YES;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
 				PRODUCT_NAME = domaindig;
-				SDKROOT = macosx;
-				SWIFT_APPROACHABLE_CONCURRENCY = YES;
-				SWIFT_EMIT_LOC_STRINGS = NO;
-				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
-				SWIFT_VERSION = 5.0;
 			};
 			name = Release;
 		};
@@ -469,6 +460,15 @@
 			defaultConfigurationIsVisible = 0;
 			defaultConfigurationName = Release;
 		};
+		8BBFF0292F987EF700E8E144 /* Build configuration list */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				8BBFF0272F987E8700E8E144 /* Debug */,
+				8BBFF0282F987E8700E8E144 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
 /* End XCConfigurationList section */
 	};
 	rootObject = 8B7800612F6090E300933221 /* Project object */;
diff --git a/DomainDig/BatchResultsView.swift b/DomainDig/BatchResultsView.swift
index 6bf18c0..1e0333d 100644
--- a/DomainDig/BatchResultsView.swift
+++ b/DomainDig/BatchResultsView.swift
@@ -58,6 +58,9 @@ struct BatchResultRowView: View {
                     .foregroundStyle(.primary)
                     .lineLimit(1)
                 Spacer(minLength: 8)
+                Text(result.resultSource.label.lowercased())
+                    .font(.system(.caption2, design: .monospaced))
+                    .foregroundStyle(.secondary)
                 Text(result.quickStatus)
                     .font(.system(.caption2, design: .monospaced))
                     .foregroundStyle(quickStatusColor)
@@ -84,7 +87,7 @@ struct BatchResultRowView: View {
             if let errorMessage = result.errorMessage {
                 Text(errorMessage)
                     .font(.system(.caption2, design: .monospaced))
-                    .foregroundStyle(.red)
+                    .foregroundStyle(result.status == .failed ? .red : .secondary)
             }
         }
         .frame(maxWidth: .infinity, alignment: .leading)
diff --git a/DomainDig/ContentView.swift b/DomainDig/ContentView.swift
index c5fc1fa..7a3a42d 100644
--- a/DomainDig/ContentView.swift
+++ b/DomainDig/ContentView.swift
@@ -30,6 +30,10 @@ struct ContentView: View {
                     }
                     if viewModel.hasRun {
                         actionButtons
+                        if let statusMessage = viewModel.currentStatusMessage ?? (viewModel.currentResultSource != .live ? viewModel.currentResultSource.label : nil) {
+                            LookupStatusBannerView(message: statusMessage, resultSource: viewModel.currentResultSource)
+                                .padding(.top, 8)
+                        }
                         SummaryView(fields: viewModel.summaryFields)
                             .padding(.top, 8)
                         if let changeSummary = viewModel.currentChangeSummary {
@@ -494,6 +498,52 @@ struct SummaryView: View {
     }
 }
 
+struct LookupStatusBannerView: View {
+    let message: String
+    let resultSource: LookupResultSource
+
+    var body: some View {
+        HStack(spacing: 8) {
+            Image(systemName: iconName)
+                .font(.caption)
+            Text(message)
+                .font(.system(.caption, design: .monospaced))
+            Spacer()
+        }
+        .foregroundStyle(color)
+        .padding(8)
+        .frame(maxWidth: .infinity, alignment: .leading)
+        .background(color.opacity(0.12))
+        .clipShape(RoundedRectangle(cornerRadius: 8))
+    }
+
+    private var color: Color {
+        switch resultSource {
+        case .live:
+            return .green
+        case .cached:
+            return .secondary
+        case .mixed:
+            return .yellow
+        case .snapshot:
+            return .orange
+        }
+    }
+
+    private var iconName: String {
+        switch resultSource {
+        case .live:
+            return "bolt.horizontal"
+        case .cached:
+            return "clock.arrow.trianglehead.counterclockwise.rotate.90"
+        case .mixed:
+            return "arrow.triangle.branch"
+        case .snapshot:
+            return "archivebox"
+        }
+    }
+}
+
 struct DomainChangeSummaryView: View {
     let summary: DomainChangeSummary
 
diff --git a/DomainDig/DomainViewModel.swift b/DomainDig/DomainViewModel.swift
index 71209dc..9fdf1fc 100644
--- a/DomainDig/DomainViewModel.swift
+++ b/DomainDig/DomainViewModel.swift
@@ -184,6 +184,12 @@ final class DomainViewModel {
     private var activeBatchDomains: [String] = []
     private var lastBatchStartedAt: Date?
     private let reportBuilder = DomainReportBuilder()
+    private let inspectionService = DomainInspectionService()
+    private(set) var currentResultSource: LookupResultSource = .live
+    private(set) var currentCachedSections: [LookupSectionKind] = []
+    private(set) var currentStatusMessage: String?
+    private(set) var currentSnapshotTimestamp = Date()
+    private(set) var currentHistoryEntryID: UUID?
 
     private static let recentSearchesKey = "recentSearches"
     private static let maxRecent = 20
@@ -355,9 +361,9 @@ final class DomainViewModel {
 
     var currentSnapshot: LookupSnapshot {
         LookupSnapshot(
-            historyEntryID: nil,
+            historyEntryID: currentHistoryEntryID,
             domain: searchedDomain,
-            timestamp: Date(),
+            timestamp: currentSnapshotTimestamp,
             trackedDomainID: currentTrackedDomain?.id,
             resolverDisplayName: resolverDisplayName,
             resolverURLString: resolverURLString,
@@ -393,7 +399,9 @@ final class DomainViewModel {
             portScanResults: allPortScanResults,
             portScanError: combinedPortScanError,
             changeSummary: currentChangeSummary,
-            isLive: true
+            resultSource: currentResultSource,
+            cachedSections: currentCachedSections,
+            statusMessage: currentStatusMessage
         )
     }
 
@@ -746,51 +754,163 @@ final class DomainViewModel {
     }
 
     private func performLookup(domain: String, lookupID: UUID) async -> HistoryEntry? {
-        await withTaskGroup(of: Void.self) { group in
-            group.addTask { await self.runDNS(domain: domain, lookupID: lookupID) }
-            group.addTask { await self.runAvailability(domain: domain, lookupID: lookupID) }
-            group.addTask { await self.runSSL(domain: domain, lookupID: lookupID) }
-            group.addTask { await self.runHSTSPreload(domain: domain, lookupID: lookupID) }
-            group.addTask { await self.runOwnership(domain: domain, lookupID: lookupID) }
-            group.addTask { await self.runSubdomains(domain: domain, lookupID: lookupID) }
-        }
+        let previous = previousSnapshot(
+            for: domain,
+            trackedDomainID: currentTrackedDomain?.id,
+            replacingLatest: false
+        )
+        let inspectedSnapshot = await inspectionService.inspectSnapshot(domain: domain, previousSnapshot: previous)
+        guard !Task.isCancelled, isCurrentLookup(lookupID) else { return nil }
 
-        await withTaskGroup(of: Void.self) { group in
-            group.addTask { await self.runHTTPHeaders(domain: domain, lookupID: lookupID) }
-            group.addTask { await self.runReachability(domain: domain, lookupID: lookupID) }
-            group.addTask { await self.runRedirectChain(domain: domain, lookupID: lookupID) }
-            group.addTask { await self.runPortScan(domain: domain, lookupID: lookupID) }
+        let snapshot = Self.resolvedSnapshotAfterFallback(inspectedSnapshot, previousSnapshot: previous)
+        applySnapshot(snapshot)
+        lastLookupDurationMs = snapshot.totalLookupDurationMs
+        refreshingTrackedDomainID = nil
+
+        guard snapshot.statusMessage == nil else {
+            return history.first(where: { $0.id == snapshot.historyEntryID })
         }
 
-        guard !Task.isCancelled, isCurrentLookup(lookupID) else { return nil }
+        return saveHistoryEntry(replaceLatest: false)
+    }
 
-        let txtRecords = dnsSections.first(where: { $0.recordType == .TXT })?.records ?? []
-        let primaryIP = primaryIPAddress(from: dnsSections)
+    private func applySnapshot(_ snapshot: LookupSnapshot) {
+        currentHistoryEntryID = snapshot.historyEntryID
+        currentSnapshotTimestamp = snapshot.timestamp
+        currentResultSource = snapshot.resultSource
+        currentCachedSections = snapshot.cachedSections
+        currentStatusMessage = snapshot.statusMessage
+        currentChangeSummary = snapshot.changeSummary
+        currentDiffSections = []
+        ownershipDiff = []
 
-        await withTaskGroup(of: Void.self) { group in
-            group.addTask { await self.runEmailSecurity(domain: domain, txtRecords: txtRecords, lookupID: lookupID) }
-            if let primaryIP {
-                group.addTask { await self.runReverseDNS(ip: primaryIP, lookupID: lookupID) }
-                group.addTask { await self.runIPGeolocation(ip: primaryIP, lookupID: lookupID) }
-            } else {
-                group.addTask { await self.finishDependentWithoutPrimaryIP(lookupID: lookupID) }
-            }
-        }
+        dnsSections = snapshot.dnsSections
+        dnsError = snapshot.dnsError
+        availabilityResult = snapshot.availabilityResult
+        suggestions = snapshot.suggestions
+        sslInfo = snapshot.sslInfo
+        sslError = snapshot.sslError
+        hstsPreloaded = snapshot.hstsPreloaded
+        httpHeaders = snapshot.httpHeaders
+        httpSecurityGrade = snapshot.httpSecurityGrade
+        httpStatusCode = snapshot.httpStatusCode
+        httpResponseTimeMs = snapshot.httpResponseTimeMs
+        httpProtocol = snapshot.httpProtocol
+        http3Advertised = snapshot.http3Advertised
+        httpHeadersError = snapshot.httpHeadersError
+        reachabilityResults = snapshot.reachabilityResults
+        reachabilityError = snapshot.reachabilityError
+        ipGeolocation = snapshot.ipGeolocation
+        ipGeolocationError = snapshot.ipGeolocationError
+        emailSecurity = snapshot.emailSecurity
+        emailSecurityError = snapshot.emailSecurityError
+        ownershipResult = snapshot.ownership
+        ownershipError = snapshot.ownershipError
+        ptrRecord = snapshot.ptrRecord
+        ptrError = snapshot.ptrError
+        redirectChain = snapshot.redirectChain
+        redirectChainError = snapshot.redirectChainError
+        subdomains = snapshot.subdomains
+        subdomainsError = snapshot.subdomainsError
+        portScanResults = snapshot.portScanResults.filter { $0.kind == .standard }
+        customPortResults = snapshot.portScanResults.filter { $0.kind == .custom }
+        portScanError = snapshot.portScanError
+        customPortScanError = nil
 
-        guard !Task.isCancelled, isCurrentLookup(lookupID) else { return nil }
+        dnsLoading = false
+        availabilityLoading = false
+        suggestionsLoading = false
+        sslLoading = false
+        hstsLoading = false
+        httpHeadersLoading = false
+        reachabilityLoading = false
+        ipGeolocationLoading = false
+        emailSecurityLoading = false
+        ownershipLoading = false
+        ptrLoading = false
+        redirectChainLoading = false
+        subdomainsLoading = false
+        portScanLoading = false
+        customPortScanLoading = false
+    }
 
-        if availabilityResult?.status == .registered {
-            await runSuggestions(domain: domain, lookupID: lookupID)
-        } else {
-            suggestions = []
-            suggestionsLoading = false
+    private static func resolvedSnapshotAfterFallback(
+        _ snapshot: LookupSnapshot,
+        previousSnapshot: LookupSnapshot?
+    ) -> LookupSnapshot {
+        guard shouldFallbackToSnapshot(snapshot), let previousSnapshot else {
+            return snapshot
+        }
+
+        return LookupSnapshot(
+            historyEntryID: previousSnapshot.historyEntryID,
+            domain: previousSnapshot.domain,
+            timestamp: previousSnapshot.timestamp,
+            trackedDomainID: previousSnapshot.trackedDomainID,
+            resolverDisplayName: previousSnapshot.resolverDisplayName,
+            resolverURLString: previousSnapshot.resolverURLString,
+            totalLookupDurationMs: previousSnapshot.totalLookupDurationMs,
+            dnsSections: previousSnapshot.dnsSections,
+            dnsError: previousSnapshot.dnsError,
+            availabilityResult: previousSnapshot.availabilityResult,
+            suggestions: previousSnapshot.suggestions,
+            sslInfo: previousSnapshot.sslInfo,
+            sslError: previousSnapshot.sslError,
+            hstsPreloaded: previousSnapshot.hstsPreloaded,
+            httpHeaders: previousSnapshot.httpHeaders,
+            httpSecurityGrade: previousSnapshot.httpSecurityGrade,
+            httpStatusCode: previousSnapshot.httpStatusCode,
+            httpResponseTimeMs: previousSnapshot.httpResponseTimeMs,
+            httpProtocol: previousSnapshot.httpProtocol,
+            http3Advertised: previousSnapshot.http3Advertised,
+            httpHeadersError: previousSnapshot.httpHeadersError,
+            reachabilityResults: previousSnapshot.reachabilityResults,
+            reachabilityError: previousSnapshot.reachabilityError,
+            ipGeolocation: previousSnapshot.ipGeolocation,
+            ipGeolocationError: previousSnapshot.ipGeolocationError,
+            emailSecurity: previousSnapshot.emailSecurity,
+            emailSecurityError: previousSnapshot.emailSecurityError,
+            ownership: previousSnapshot.ownership,
+            ownershipError: previousSnapshot.ownershipError,
+            ptrRecord: previousSnapshot.ptrRecord,
+            ptrError: previousSnapshot.ptrError,
+            redirectChain: previousSnapshot.redirectChain,
+            redirectChainError: previousSnapshot.redirectChainError,
+            subdomains: previousSnapshot.subdomains,
+            subdomainsError: previousSnapshot.subdomainsError,
+            portScanResults: previousSnapshot.portScanResults,
+            portScanError: previousSnapshot.portScanError,
+            changeSummary: previousSnapshot.changeSummary,
+            resultSource: .snapshot,
+            cachedSections: [],
+            statusMessage: "Last known result • \(previousSnapshot.timestamp.formatted(date: .abbreviated, time: .shortened))"
+        )
+    }
+
+    private static func shouldFallbackToSnapshot(_ snapshot: LookupSnapshot) -> Bool {
+        let candidateMessages = [
+            snapshot.dnsError,
+            snapshot.httpHeadersError,
+            snapshot.sslError,
+            snapshot.ownershipError,
+            snapshot.subdomainsError,
+            snapshot.redirectChainError,
+            snapshot.ipGeolocationError
+        ]
+        .compactMap { $0?.lowercased() }
+
+        guard !candidateMessages.isEmpty else { return false }
+        let failedDueToConnectivity = candidateMessages.allSatisfy { message in
+            message.hasPrefix("network error:") || message.hasPrefix("timeout:") || message.hasPrefix("rate limit:")
         }
 
-        guard !Task.isCancelled, isCurrentLookup(lookupID) else { return nil }
-        lastLookupDurationMs = lookupStartedAt.map { Int(Date().timeIntervalSince($0) * 1000) }
-        let entry = saveHistoryEntry(replaceLatest: false)
-        refreshingTrackedDomainID = nil
-        return entry
+        let hasMaterialData = !snapshot.dnsSections.isEmpty
+            || !snapshot.httpHeaders.isEmpty
+            || snapshot.sslInfo != nil
+            || snapshot.ownership != nil
+            || !snapshot.subdomains.isEmpty
+
+        return failedDueToConnectivity && !hasMaterialData
     }
 
     private func runDNS(domain: String, lookupID: UUID) async {
@@ -1044,11 +1164,12 @@ final class DomainViewModel {
         customPortScanLoading = false
     }
 
-    private static func performBatchLookup(domain: String) async -> BatchLookupPayload? {
+    private static func performBatchLookup(domain: String, previousSnapshot: LookupSnapshot?) async -> BatchLookupPayload? {
         guard !Task.isCancelled else { return nil }
-        let snapshot = await DomainInspectionService().inspectSnapshot(domain: domain)
+        let inspectionService = DomainInspectionService()
+        let snapshot = await inspectionService.inspectSnapshot(domain: domain, previousSnapshot: previousSnapshot)
         guard !Task.isCancelled else { return nil }
-        return BatchLookupPayload(snapshot: snapshot)
+        return BatchLookupPayload(snapshot: resolvedSnapshotAfterFallback(snapshot, previousSnapshot: previousSnapshot))
     }
 
     private static func enrichOpenPortBanners(_ results: [PortScanResult], domain: String) async -> [PortScanResult] {
@@ -1141,6 +1262,10 @@ final class DomainViewModel {
             portScanError: snapshot.portScanError
         )
 
+        if updateCurrentState {
+            currentHistoryEntryID = entry.id
+        }
+
         if replaceLatest, !history.isEmpty, history[0].domain.caseInsensitiveCompare(snapshot.domain) == .orderedSame {
             history[0] = entry
         } else {
@@ -1318,6 +1443,11 @@ final class DomainViewModel {
         addRecentSearch(target)
         searchedDomain = target
         hasRun = true
+        currentHistoryEntryID = nil
+        currentSnapshotTimestamp = Date()
+        currentResultSource = .live
+        currentCachedSections = []
+        currentStatusMessage = nil
         currentDiffSections = []
         currentChangeSummary = nil
         ownershipDiff = []
@@ -1424,9 +1554,10 @@ final class DomainViewModel {
             refreshingTrackedDomainID = trackedDomain(for: domain)?.id
         }
         updateBatchResult(domain: domain, status: .running, quickStatus: "Running", entry: nil, errorMessage: nil)
+        let previousSnapshot = previousSnapshot(for: domain, trackedDomainID: trackedDomain(for: domain)?.id, replacingLatest: false)
 
-        group.addTask { [domain] in
-            let payload = await Self.performBatchLookup(domain: domain)
+        group.addTask { [domain, previousSnapshot] in
+            let payload = await Self.performBatchLookup(domain: domain, previousSnapshot: previousSnapshot)
             return (domain, payload)
         }
     }
@@ -1441,13 +1572,21 @@ final class DomainViewModel {
                 status: .failed,
                 quickStatus: "Failed",
                 entry: nil,
+                resultSource: .live,
                 errorMessage: "Lookup cancelled"
             )
             batchCompletedCount += 1
             return
         }
 
-        let entry = saveHistoryEntry(from: payload.snapshot, replaceLatest: false, updateCurrentState: false)
+        let entry: HistoryEntry?
+        if payload.snapshot.statusMessage == nil {
+            entry = saveHistoryEntry(from: payload.snapshot, replaceLatest: false, updateCurrentState: false)
+        } else {
+            entry = payload.snapshot.historyEntryID.flatMap { id in
+                history.first(where: { $0.id == id })
+            }
+        }
         let certificateWarningLevel = DomainDiffService.certificateWarningLevel(for: payload.snapshot)
         let quickStatus: String
         if entry?.changeSummary?.hasChanges == true {
@@ -1463,7 +1602,8 @@ final class DomainViewModel {
             status: .completed,
             quickStatus: quickStatus,
             entry: entry,
-            errorMessage: nil
+            resultSource: payload.snapshot.resultSource,
+            errorMessage: payload.snapshot.statusMessage
         )
         batchCompletedCount += 1
     }
@@ -1503,6 +1643,7 @@ final class DomainViewModel {
         status: BatchLookupStatus,
         quickStatus: String,
         entry: HistoryEntry?,
+        resultSource: LookupResultSource = .live,
         errorMessage: String?
     ) {
         guard let index = batchResults.firstIndex(where: { $0.domain.caseInsensitiveCompare(domain) == .orderedSame }) else {
@@ -1513,6 +1654,7 @@ final class DomainViewModel {
             id: batchResults[index].id,
             domain: domain,
             historyEntryID: entry?.id,
+            resultSource: resultSource,
             availability: entry?.availabilityResult?.status,
             primaryIP: entry?.primaryIP,
             quickStatus: quickStatus,
@@ -1573,6 +1715,11 @@ final class DomainViewModel {
         customPortResults = []
         customPortScanError = nil
         customPortScanLoading = false
+        currentHistoryEntryID = nil
+        currentSnapshotTimestamp = Date()
+        currentResultSource = .live
+        currentCachedSections = []
+        currentStatusMessage = nil
     }
 
     private func setAllLoadingStates(_ loading: Bool) {
@@ -1715,7 +1862,9 @@ final class DomainViewModel {
             portScanResults: [],
             portScanError: nil,
             changeSummary: trackedDomain.lastChangeSummary,
-            isLive: false
+            resultSource: .snapshot,
+            cachedSections: [],
+            statusMessage: nil
         )
     }
 
@@ -1801,7 +1950,8 @@ final class DomainViewModel {
             SummaryFieldViewData(label: "Primary IP", value: primaryIPAddress(from: snapshot) ?? "Unavailable", tone: .primary),
             SummaryFieldViewData(label: "HTTPS", value: httpsSummary(from: snapshot), tone: httpsSummaryTone(from: snapshot)),
             SummaryFieldViewData(label: "Certificate", value: certificateStatusLabel(from: snapshot), tone: certificateStatusTone(from: snapshot)),
-            SummaryFieldViewData(label: "Redirect", value: finalRedirectTarget(from: snapshot) ?? "Unavailable", tone: .secondary)
+            SummaryFieldViewData(label: "Redirect", value: finalRedirectTarget(from: snapshot) ?? "Unavailable", tone: .secondary),
+            SummaryFieldViewData(label: "Source", value: snapshot.statusMessage ?? snapshot.resultSource.label, tone: sourceTone(for: snapshot))
         ]
     }
 
@@ -1809,7 +1959,7 @@ final class DomainViewModel {
         var rows = [
             InfoRowViewData(label: "Domain", value: snapshot.domain, tone: .primary),
             InfoRowViewData(label: "Resolver", value: snapshot.resolverDisplayName, tone: .secondary),
-            InfoRowViewData(label: snapshot.isLive ? "Result" : "Snapshot", value: snapshot.isLive ? "Live" : "Snapshot", tone: snapshot.isLive ? .success : .warning),
+            InfoRowViewData(label: snapshot.statusMessage == nil ? "Result" : "Snapshot", value: snapshot.statusMessage ?? snapshot.resultSource.label, tone: sourceTone(for: snapshot)),
             InfoRowViewData(label: "Lookup Duration", value: durationLabel(snapshot.totalLookupDurationMs), tone: .secondary)
         ]
         rows.insert(
@@ -2083,7 +2233,7 @@ final class DomainViewModel {
             "DomainDig Export",
             "Domain: \(snapshot.domain)",
             "Date: \(exportDateFormatter.string(from: snapshot.timestamp))",
-            "Mode: \(snapshot.isLive ? "Live" : "Snapshot")",
+            "Mode: \(snapshot.statusMessage ?? snapshot.resultSource.label)",
             "Resolver: \(snapshot.resolverDisplayName)",
             "Lookup Duration: \(durationLabel(snapshot.totalLookupDurationMs))",
             "Tracked: \(trackedDomain == nil ? "No" : "Yes")"
@@ -2417,6 +2567,23 @@ final class DomainViewModel {
         }
     }
 
+    private static func sourceTone(for snapshot: LookupSnapshot) -> ResultTone {
+        if snapshot.statusMessage != nil {
+            return .warning
+        }
+
+        switch snapshot.resultSource {
+        case .live:
+            return .success
+        case .cached:
+            return .secondary
+        case .mixed:
+            return .warning
+        case .snapshot:
+            return .warning
+        }
+    }
+
     private static func securityGradeTone(_ grade: String) -> ResultTone {
         switch grade {
         case "A", "B":
diff --git a/DomainDig/LookupRuntime.swift b/DomainDig/LookupRuntime.swift
new file mode 100644
index 0000000..b70a134
--- /dev/null
+++ b/DomainDig/LookupRuntime.swift
@@ -0,0 +1,289 @@
+import Foundation
+
+struct CachedLookupResult<Value> {
+    let value: Value
+    let source: LookupResultSource
+}
+
+actor LookupRuntime {
+    static let shared = LookupRuntime()
+
+    private let ttl: TimeInterval = 300
+
+    private enum RequestKey: Hashable {
+        case domain(String, LookupSectionKind)
+        case subject(String, LookupSectionKind)
+    }
+
+    private enum RateLimitBucket: Hashable {
+        case crtsh
+        case rdap
+        case ipGeolocation
+
+        var minimumSpacing: TimeInterval {
+            switch self {
+            case .crtsh:
+                return 1.0
+            case .rdap:
+                return 0.75
+            case .ipGeolocation:
+                return 0.75
+            }
+        }
+    }
+
+    private enum CachedPayload {
+        case dns(ServiceResult<[DNSSection]>)
+        case availability(DomainAvailabilityResult)
+        case ssl(ServiceResult<SSLCertificateInfo>)
+        case hsts(Bool?)
+        case http(ServiceResult<HTTPHeadersResult>)
+        case reachability(ServiceResult<[PortReachability]>)
+        case ownership(ServiceResult<DomainOwnership>)
+        case redirect(ServiceResult<[RedirectHop]>)
+        case subdomains(ServiceResult<[DiscoveredSubdomain]>)
+        case portScan(ServiceResult<[PortScanResult]>)
+        case email(ServiceResult<EmailSecurityResult>)
+        case ptr(ServiceResult<String>)
+        case ipGeolocation(ServiceResult<IPGeolocation>)
+        case suggestions([DomainSuggestionResult])
+    }
+
+    private struct CacheEntry {
+        let payload: CachedPayload
+        let expiresAt: Date
+    }
+
+    private var cache: [RequestKey: CacheEntry] = [:]
+    private var inFlight: [RequestKey: Task<CachedPayload, Never>] = [:]
+    private var nextAllowedAt: [RateLimitBucket: Date] = [:]
+
+    func dns(domain: String) async -> CachedLookupResult<ServiceResult<[DNSSection]>> {
+        await execute(
+            key: .domain(domain, .dns),
+            extract: { payload in
+                guard case let .dns(result) = payload else { return nil }
+                return result
+            },
+            operation: {
+                .dns(await DNSLookupService.lookupAll(domain: domain))
+            }
+        )
+    }
+
+    func availability(domain: String) async -> CachedLookupResult<DomainAvailabilityResult> {
+        await execute(
+            key: .domain(domain, .availability),
+            extract: { payload in
+                guard case let .availability(result) = payload else { return nil }
+                return result
+            },
+            operation: {
+                .availability(await DomainAvailabilityService.check(domain: domain))
+            }
+        )
+    }
+
+    func ssl(domain: String) async -> CachedLookupResult<ServiceResult<SSLCertificateInfo>> {
+        await execute(
+            key: .domain(domain, .ssl),
+            extract: { payload in
+                guard case let .ssl(result) = payload else { return nil }
+                return result
+            },
+            operation: {
+                .ssl(await SSLCheckService.check(domain: domain))
+            }
+        )
+    }
+
+    func hsts(domain: String) async -> CachedLookupResult<Bool?> {
+        await execute(
+            key: .domain(domain, .hsts),
+            extract: { payload in
+                guard case let .hsts(result) = payload else { return nil }
+                return result
+            },
+            operation: {
+                .hsts(await SSLCheckService.checkHSTSPreload(domain: domain))
+            }
+        )
+    }
+
+    func http(domain: String) async -> CachedLookupResult<ServiceResult<HTTPHeadersResult>> {
+        await execute(
+            key: .domain(domain, .httpHeaders),
+            extract: { payload in
+                guard case let .http(result) = payload else { return nil }
+                return result
+            },
+            operation: {
+                .http(await HTTPHeadersService.fetch(domain: domain))
+            }
+        )
+    }
+
+    func reachability(domain: String) async -> CachedLookupResult<ServiceResult<[PortReachability]>> {
+        await execute(
+            key: .domain(domain, .reachability),
+            extract: { payload in
+                guard case let .reachability(result) = payload else { return nil }
+                return result
+            },
+            operation: {
+                .reachability(await ReachabilityService.checkAll(domain: domain))
+            }
+        )
+    }
+
+    func ownership(domain: String) async -> CachedLookupResult<ServiceResult<DomainOwnership>> {
+        await execute(
+            key: .domain(domain, .ownership),
+            rateLimitBucket: .rdap,
+            extract: { payload in
+                guard case let .ownership(result) = payload else { return nil }
+                return result
+            },
+            operation: {
+                .ownership(await DomainOwnershipService.lookup(domain: domain))
+            }
+        )
+    }
+
+    func redirectChain(domain: String) async -> CachedLookupResult<ServiceResult<[RedirectHop]>> {
+        await execute(
+            key: .domain(domain, .redirectChain),
+            extract: { payload in
+                guard case let .redirect(result) = payload else { return nil }
+                return result
+            },
+            operation: {
+                .redirect(await RedirectChainService.trace(domain: domain))
+            }
+        )
+    }
+
+    func subdomains(domain: String) async -> CachedLookupResult<ServiceResult<[DiscoveredSubdomain]>> {
+        await execute(
+            key: .domain(domain, .subdomains),
+            rateLimitBucket: .crtsh,
+            extract: { payload in
+                guard case let .subdomains(result) = payload else { return nil }
+                return result
+            },
+            operation: {
+                .subdomains(await SubdomainDiscoveryService.discover(for: domain))
+            }
+        )
+    }
+
+    func portScan(domain: String) async -> CachedLookupResult<ServiceResult<[PortScanResult]>> {
+        await execute(
+            key: .domain(domain, .portScan),
+            extract: { payload in
+                guard case let .portScan(result) = payload else { return nil }
+                return result
+            },
+            operation: {
+                .portScan(await PortScanService.scanAll(domain: domain))
+            }
+        )
+    }
+
+    func email(domain: String, txtRecords: [DNSRecord]) async -> CachedLookupResult<ServiceResult<EmailSecurityResult>> {
+        await execute(
+            key: .domain(domain, .emailSecurity),
+            extract: { payload in
+                guard case let .email(result) = payload else { return nil }
+                return result
+            },
+            operation: {
+                .email(await EmailSecurityService.analyze(domain: domain, txtRecords: txtRecords))
+            }
+        )
+    }
+
+    func ptr(ip: String, resolverURLString: String) async -> CachedLookupResult<ServiceResult<String>> {
+        await execute(
+            key: .subject("\(resolverURLString)|\(ip)", .ptr),
+            extract: { payload in
+                guard case let .ptr(result) = payload else { return nil }
+                return result
+            },
+            operation: {
+                .ptr(await ReverseDNSService.lookup(ip: ip, resolverURLString: resolverURLString))
+            }
+        )
+    }
+
+    func ipGeolocation(ip: String) async -> CachedLookupResult<ServiceResult<IPGeolocation>> {
+        await execute(
+            key: .subject(ip, .ipGeolocation),
+            rateLimitBucket: .ipGeolocation,
+            extract: { payload in
+                guard case let .ipGeolocation(result) = payload else { return nil }
+                return result
+            },
+            operation: {
+                .ipGeolocation(await IPGeolocationService.lookup(ip: ip))
+            }
+        )
+    }
+
+    func suggestions(domain: String) async -> CachedLookupResult<[DomainSuggestionResult]> {
+        await execute(
+            key: .domain(domain, .suggestions),
+            extract: { payload in
+                guard case let .suggestions(result) = payload else { return nil }
+                return result
+            },
+            operation: {
+                .suggestions(await DomainAvailabilityService.suggestions(for: domain))
+            }
+        )
+    }
+
+    private func execute<T>(
+        key: RequestKey,
+        rateLimitBucket: RateLimitBucket? = nil,
+        extract: @escaping (CachedPayload) -> T?,
+        operation: @escaping @Sendable () async -> CachedPayload
+    ) async -> CachedLookupResult<T> {
+        if let cachedEntry = cache[key], cachedEntry.expiresAt > Date(), let value = extract(cachedEntry.payload) {
+            return CachedLookupResult(value: value, source: .cached)
+        }
+
+        if let task = inFlight[key], let value = extract(await task.value) {
+            return CachedLookupResult(value: value, source: .mixed)
+        }
+
+        let task = Task<CachedPayload, Never> {
+            if let rateLimitBucket {
+                await self.enforceRateLimit(for: rateLimitBucket)
+            }
+            return await operation()
+        }
+        inFlight[key] = task
+
+        let payload = await task.value
+        cache[key] = CacheEntry(payload: payload, expiresAt: Date().addingTimeInterval(ttl))
+        inFlight[key] = nil
+
+        guard let value = extract(payload) else {
+            fatalError("LookupRuntime payload extraction mismatch")
+        }
+
+        return CachedLookupResult(value: value, source: .live)
+    }
+
+    private func enforceRateLimit(for bucket: RateLimitBucket) async {
+        let now = Date()
+        if let nextAllowed = nextAllowedAt[bucket], nextAllowed > now {
+            let delay = nextAllowed.timeIntervalSince(now)
+            if delay > 0 {
+                try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
+            }
+        }
+        nextAllowedAt[bucket] = Date().addingTimeInterval(bucket.minimumSpacing)
+    }
+}
diff --git a/DomainDig/Models.swift b/DomainDig/Models.swift
index b1e5432..85136d2 100644
--- a/DomainDig/Models.swift
+++ b/DomainDig/Models.swift
@@ -6,6 +6,43 @@ enum ServiceResult<Value> {
     case error(String)
 }
 
+enum LookupResultSource: String, Codable {
+    case live
+    case cached
+    case mixed
+    case snapshot
+
+    var label: String {
+        switch self {
+        case .live:
+            return "Live"
+        case .cached:
+            return "Cached"
+        case .mixed:
+            return "Mixed"
+        case .snapshot:
+            return "Snapshot"
+        }
+    }
+}
+
+enum LookupSectionKind: String, Codable, CaseIterable {
+    case dns
+    case availability
+    case ssl
+    case hsts
+    case httpHeaders
+    case reachability
+    case ipGeolocation
+    case emailSecurity
+    case ownership
+    case ptr
+    case redirectChain
+    case subdomains
+    case portScan
+    case suggestions
+}
+
 enum DomainAvailabilityStatus: String, Codable {
     case available
     case registered
@@ -134,6 +171,7 @@ struct BatchLookupResult: Identifiable, Codable, Equatable {
     let id: UUID
     let domain: String
     let historyEntryID: UUID?
+    let resultSource: LookupResultSource
     let availability: DomainAvailabilityStatus?
     let primaryIP: String?
     let quickStatus: String
@@ -148,6 +186,7 @@ struct BatchLookupResult: Identifiable, Codable, Equatable {
         id: UUID = UUID(),
         domain: String,
         historyEntryID: UUID?,
+        resultSource: LookupResultSource = .live,
         availability: DomainAvailabilityStatus?,
         primaryIP: String?,
         quickStatus: String,
@@ -161,6 +200,7 @@ struct BatchLookupResult: Identifiable, Codable, Equatable {
         self.id = id
         self.domain = domain
         self.historyEntryID = historyEntryID
+        self.resultSource = resultSource
         self.availability = availability
         self.primaryIP = primaryIP
         self.quickStatus = quickStatus
diff --git a/DomainDig/RDAPService.swift b/DomainDig/RDAPService.swift
index c38a140..3556624 100644
--- a/DomainDig/RDAPService.swift
+++ b/DomainDig/RDAPService.swift
@@ -5,7 +5,7 @@ enum RDAPService {
         let normalizedDomain = normalize(domain)
         guard !normalizedDomain.isEmpty else { return nil }
 
-        switch await cache.response(for: normalizedDomain) {
+        switch await fetchRDAPResponse(for: normalizedDomain) {
         case let .success(response):
             return response.isDomainRecord ? .registered : nil
         case .empty:
@@ -21,7 +21,7 @@ enum RDAPService {
             return .empty("Unavailable")
         }
 
-        switch await cache.response(for: normalizedDomain) {
+        switch await fetchRDAPResponse(for: normalizedDomain) {
         case let .success(response):
             let ownership = DomainOwnership(
                 registrar: response.registrarName,
@@ -39,8 +39,6 @@ enum RDAPService {
         }
     }
 
-    private static let cache = RDAPCache()
-
     private static func normalize(_ domain: String) -> String {
         domain
             .trimmingCharacters(in: .whitespacesAndNewlines)
@@ -48,31 +46,6 @@ enum RDAPService {
     }
 }
 
-private actor RDAPCache {
-    private var cachedResponses: [String: ServiceResult<RDAPDomainResponse>] = [:]
-    private var inFlightTasks: [String: Task<ServiceResult<RDAPDomainResponse>, Never>] = [:]
-
-    func response(for domain: String) async -> ServiceResult<RDAPDomainResponse> {
-        if let cachedResponse = cachedResponses[domain] {
-            return cachedResponse
-        }
-
-        if let inFlightTask = inFlightTasks[domain] {
-            return await inFlightTask.value
-        }
-
-        let task = Task<ServiceResult<RDAPDomainResponse>, Never> {
-            await fetchRDAPResponse(for: domain)
-        }
-        inFlightTasks[domain] = task
-
-        let result = await task.value
-        cachedResponses[domain] = result
-        inFlightTasks[domain] = nil
-        return result
-    }
-}
-
 private func fetchRDAPResponse(for domain: String) async -> ServiceResult<RDAPDomainResponse> {
     guard let url = URL(string: "https://rdap.org/domain/\(domain)") else {
         return .error("Unavailable")
diff --git a/DomainDig/SubdomainDiscoveryService.swift b/DomainDig/SubdomainDiscoveryService.swift
index 57db25d..4e25cf0 100644
--- a/DomainDig/SubdomainDiscoveryService.swift
+++ b/DomainDig/SubdomainDiscoveryService.swift
@@ -7,55 +7,16 @@ enum SubdomainDiscoveryService {
             return .empty("No passive subdomains found")
         }
 
-        return await cache.subdomains(for: normalizedDomain, limit: limit)
+        return await fetchSubdomains(for: normalizedDomain, limit: limit)
     }
 
-    private static let cache = SubdomainDiscoveryCache()
-
     private static func normalize(_ domain: String) -> String {
         domain
             .trimmingCharacters(in: .whitespacesAndNewlines)
             .lowercased()
     }
-}
-
-private actor SubdomainDiscoveryCache {
-    private var cachedResults: [String: ServiceResult<[DiscoveredSubdomain]>] = [:]
-    private var inFlightTasks: [String: Task<ServiceResult<[DiscoveredSubdomain]>, Never>] = [:]
-    private var lastRequestAt: Date?
-
-    func subdomains(for domain: String, limit: Int) async -> ServiceResult<[DiscoveredSubdomain]> {
-        if let cachedResult = cachedResults[domain] {
-            return cachedResult
-        }
-
-        if let inFlightTask = inFlightTasks[domain] {
-            return await inFlightTask.value
-        }
-
-        let task = Task<ServiceResult<[DiscoveredSubdomain]>, Never> {
-            await enforceRateLimit()
-            return await fetchSubdomains(for: domain, limit: limit)
-        }
-        inFlightTasks[domain] = task
-
-        let result = await task.value
-        cachedResults[domain] = result
-        inFlightTasks[domain] = nil
-        return result
-    }
-
-    private func enforceRateLimit() async {
-        if let lastRequestAt {
-            let delay = max(0, 0.75 - Date().timeIntervalSince(lastRequestAt))
-            if delay > 0 {
-                try? await Task.sleep(for: .seconds(delay))
-            }
-        }
-        lastRequestAt = Date()
-    }
 
-    private func fetchSubdomains(for domain: String, limit: Int) async -> ServiceResult<[DiscoveredSubdomain]> {
+    private static func fetchSubdomains(for domain: String, limit: Int) async -> ServiceResult<[DiscoveredSubdomain]> {
         var components = URLComponents(string: "https://crt.sh/")!
         components.queryItems = [
             URLQueryItem(name: "q", value: "%.\(domain)"),
@@ -81,7 +42,7 @@ private actor SubdomainDiscoveryCache {
         }
     }
 
-    private func parseSubdomains(from entries: [CRTShEntry], domain: String, limit: Int) -> [DiscoveredSubdomain] {
+    private static func parseSubdomains(from entries: [CRTShEntry], domain: String, limit: Int) -> [DiscoveredSubdomain] {
         var seen = Set<String>()
         var results: [DiscoveredSubdomain] = []
 
diff --git a/DomainDigCLI.swift b/DomainDigCLI.swift
index 73f7028..d0a4d86 100644
--- a/DomainDigCLI.swift
+++ b/DomainDigCLI.swift
@@ -14,19 +14,39 @@ struct DomainDigCLI {
         let wantsJSON = arguments.contains("--json") || arguments.contains("-j")
         let domains = arguments.filter { !$0.hasPrefix("-") }
 
-        guard let domain = domains.first, !domain.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
+        let requestedDomains = domains
+            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
+            .filter { !$0.isEmpty }
+
+        guard !requestedDomains.isEmpty else {
             fputs("usage: domaindig <domain> [--json]\n", stderr)
             Foundation.exit(1)
         }
 
         let inspectionService = DomainInspectionService()
-        let report = await inspectionService.inspect(domain: domain)
+        var reports: [DomainReport] = []
+        var seen = Set<String>()
+
+        for domain in requestedDomains {
+            let normalizedDomain = domain.lowercased()
+            guard seen.insert(normalizedDomain).inserted else { continue }
+            reports.append(await inspectionService.inspect(domain: domain))
+        }
 
         do {
-            let data = try DomainReportExporter.data(
-                for: report,
-                format: wantsJSON ? .json : .text
-            )
+            let data: Data
+            if reports.count == 1, let report = reports.first {
+                data = try DomainReportExporter.data(
+                    for: report,
+                    format: wantsJSON ? .json : .text
+                )
+            } else {
+                data = try DomainReportExporter.data(
+                    for: reports,
+                    format: wantsJSON ? .json : .text,
+                    title: "DomainDig Batch Report"
+                )
+            }
             FileHandle.standardOutput.write(data)
             if data.last != 0x0A {
                 FileHandle.standardOutput.write(Data([0x0A]))
diff --git a/DomainInspectionService.swift b/DomainInspectionService.swift
index 3f11290..cb9350b 100644
--- a/DomainInspectionService.swift
+++ b/DomainInspectionService.swift
@@ -2,89 +2,155 @@ import Foundation
 
 struct DomainInspectionService {
     private let reportBuilder = DomainReportBuilder()
+    private let runtime: LookupRuntime
 
-    func inspect(domain: String) async -> DomainReport {
-        let snapshot = await inspectSnapshot(domain: domain)
+    init(runtime: LookupRuntime = .shared) {
+        self.runtime = runtime
+    }
+
+    func inspect(domain: String, previousSnapshot: LookupSnapshot? = nil) async -> DomainReport {
+        let snapshot = await inspectSnapshot(domain: domain, previousSnapshot: previousSnapshot)
         return reportBuilder.build(from: snapshot)
     }
 
-    func inspectSnapshot(domain: String) async -> LookupSnapshot {
+    func inspectSnapshot(domain: String, previousSnapshot: LookupSnapshot? = nil) async -> LookupSnapshot {
         let normalizedDomain = normalize(domain)
         let startedAt = Date()
         let resolverDisplayName = DNSLookupService.currentResolverDisplayName()
         let resolverURLString = DNSLookupService.currentResolverURLString()
+        var cachedSections = Set<LookupSectionKind>()
+        var sectionSources: [LookupResultSource] = []
+
+        async let dnsFetch = runtime.dns(domain: normalizedDomain)
+        async let availabilityFetch = runtime.availability(domain: normalizedDomain)
+        async let sslFetch = runtime.ssl(domain: normalizedDomain)
+        async let hstsFetch = runtime.hsts(domain: normalizedDomain)
+        async let httpFetch = runtime.http(domain: normalizedDomain)
+        async let reachabilityFetch = runtime.reachability(domain: normalizedDomain)
+        async let ownershipFetch = runtime.ownership(domain: normalizedDomain)
+        async let redirectFetch = runtime.redirectChain(domain: normalizedDomain)
+        async let subdomainFetch = runtime.subdomains(domain: normalizedDomain)
+        async let portScanFetch = runtime.portScan(domain: normalizedDomain)
+
+        let resolvedDNS = await dnsFetch
+        let dnsResult = normalizeErrors(in: resolvedDNS.value)
+        track(.dns, source: resolvedDNS.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+
+        let availability = await availabilityFetch
+        track(.availability, source: availability.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+
+        let resolvedSSL = await sslFetch
+        let sslResult = normalizeErrors(in: resolvedSSL.value)
+        track(.ssl, source: resolvedSSL.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+
+        let hsts = await hstsFetch
+        track(.hsts, source: hsts.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
 
-        async let dnsResult = DNSLookupService.lookupAll(domain: normalizedDomain)
-        async let availabilityResult = DomainAvailabilityService.check(domain: normalizedDomain)
-        async let sslResult = SSLCheckService.check(domain: normalizedDomain)
-        async let hstsResult = SSLCheckService.checkHSTSPreload(domain: normalizedDomain)
-        async let httpResult = HTTPHeadersService.fetch(domain: normalizedDomain)
-        async let reachabilityResult = ReachabilityService.checkAll(domain: normalizedDomain)
-        async let ownershipResult = DomainOwnershipService.lookup(domain: normalizedDomain)
-        async let redirectResult = RedirectChainService.trace(domain: normalizedDomain)
-        async let subdomainResult = SubdomainDiscoveryService.discover(for: normalizedDomain)
-        async let portScanResult = PortScanService.scanAll(domain: normalizedDomain)
-
-        let resolvedDNS = await dnsResult
-        let availability = await availabilityResult
-        let resolvedSSL = await sslResult
-        let hsts = await hstsResult
-        let http = await httpResult
-        let reachability = await reachabilityResult
-        let resolvedOwnership = await ownershipResult
-        let redirects = await redirectResult
-        let resolvedSubdomains = await subdomainResult
-        let ports = await portScanResult
-
-        let dnsSections = mapServiceResult(resolvedDNS, emptyValue: [])
-        let sslInfo = mapOptionalValueServiceResult(resolvedSSL)
-        let httpHeadersResult = mapHTTPResult(http)
-        let reachabilityResultValue = mapServiceResult(reachability, emptyValue: [])
-        let redirectChain = mapServiceResult(redirects, emptyValue: [])
-        let ownership = mapOptionalValueServiceResult(resolvedOwnership)
-        let subdomains = mapServiceResult(resolvedSubdomains, emptyValue: [])
-        let portScanResults = await mapPortScanResult(ports, domain: normalizedDomain)
+        let http = await httpFetch
+        let httpResult = normalizeErrors(in: http.value)
+        track(.httpHeaders, source: http.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+
+        let reachability = await reachabilityFetch
+        let reachabilityResult = normalizeErrors(in: reachability.value)
+        track(.reachability, source: reachability.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+
+        let resolvedOwnership = await ownershipFetch
+        let ownershipResult = normalizeErrors(in: resolvedOwnership.value)
+        track(.ownership, source: resolvedOwnership.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+
+        let redirects = await redirectFetch
+        let redirectResult = normalizeErrors(in: redirects.value)
+        track(.redirectChain, source: redirects.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+
+        let resolvedSubdomains = await subdomainFetch
+        let subdomainResult = normalizeErrors(in: resolvedSubdomains.value)
+        track(.subdomains, source: resolvedSubdomains.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+
+        let ports = await portScanFetch
+        let portScanResult = normalizeErrors(in: ports.value)
+        track(.portScan, source: ports.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+
+        let dnsSections = mapServiceResult(dnsResult, emptyValue: [])
+        let sslInfo = mapOptionalValueServiceResult(sslResult)
+        let httpHeadersResult = mapHTTPResult(httpResult)
+        let reachabilityResultValue = mapServiceResult(reachabilityResult, emptyValue: [])
+        let redirectChain = mapServiceResult(redirectResult, emptyValue: [])
+        let ownership = mapOptionalValueServiceResult(ownershipResult)
+        let subdomains = mapServiceResult(subdomainResult, emptyValue: [])
+        let portScanResults = await mapPortScanResult(portScanResult, domain: normalizedDomain)
 
         let txtRecords = dnsSections.value.first(where: { $0.recordType == .TXT })?.records ?? []
         let primaryIP = dnsSections.value.first(where: { $0.recordType == .A })?.records.first?.value
+        let canReuseDNSDependents = canReuseDependentSections(from: previousSnapshot, dnsSections: dnsSections.value)
+        let canReuseIPDependents = canReuseIPBasedSections(from: previousSnapshot, primaryIP: primaryIP)
 
-        async let emailResult = EmailSecurityService.analyze(domain: normalizedDomain, txtRecords: txtRecords)
-        async let suggestions = availability.status == .registered
-            ? DomainAvailabilityService.suggestions(for: normalizedDomain)
-            : []
+        let emailOutcome: CachedLookupResult<ServiceResult<EmailSecurityResult>>
+        if canReuseDNSDependents, let previousSnapshot, let emailSecurity = previousSnapshot.emailSecurity {
+            emailOutcome = CachedLookupResult(value: .success(emailSecurity), source: .cached)
+        } else if canReuseDNSDependents, let previousSnapshot, let error = previousSnapshot.emailSecurityError {
+            emailOutcome = CachedLookupResult(value: .error(error), source: .cached)
+        } else {
+            emailOutcome = await runtime.email(domain: normalizedDomain, txtRecords: txtRecords)
+        }
+        track(.emailSecurity, source: emailOutcome.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
 
-        let resolvedEmail = await emailResult
-        let resolvedSuggestions = await suggestions
+        let suggestionsOutcome: CachedLookupResult<[DomainSuggestionResult]>
+        if availability.value.status == .registered {
+            suggestionsOutcome = await runtime.suggestions(domain: normalizedDomain)
+            track(.suggestions, source: suggestionsOutcome.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+        } else {
+            suggestionsOutcome = CachedLookupResult(value: [], source: .live)
+        }
 
-        let ptrResult: ServiceResult<String>?
-        let geoResult: ServiceResult<IPGeolocation>?
+        let ptrOutcome: CachedLookupResult<ServiceResult<String>>?
+        let geoOutcome: CachedLookupResult<ServiceResult<IPGeolocation>>?
         if let primaryIP {
-            ptrResult = await ReverseDNSService.lookup(ip: primaryIP, resolverURLString: resolverURLString)
-            geoResult = await IPGeolocationService.lookup(ip: primaryIP)
+            if canReuseIPDependents, let previousSnapshot, let ptrRecord = previousSnapshot.ptrRecord {
+                ptrOutcome = CachedLookupResult(value: .success(ptrRecord), source: .cached)
+            } else if canReuseIPDependents, let previousSnapshot, let ptrError = previousSnapshot.ptrError {
+                ptrOutcome = CachedLookupResult(value: .error(ptrError), source: .cached)
+            } else {
+                ptrOutcome = await runtime.ptr(ip: primaryIP, resolverURLString: resolverURLString)
+            }
+
+            if canReuseIPDependents, let previousSnapshot, let ipGeolocation = previousSnapshot.ipGeolocation {
+                geoOutcome = CachedLookupResult(value: .success(ipGeolocation), source: .cached)
+            } else if canReuseIPDependents, let previousSnapshot, let ipGeolocationError = previousSnapshot.ipGeolocationError {
+                geoOutcome = CachedLookupResult(value: .error(ipGeolocationError), source: .cached)
+            } else {
+                geoOutcome = await runtime.ipGeolocation(ip: primaryIP)
+            }
+
+            if let ptrOutcome {
+                track(.ptr, source: ptrOutcome.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+            }
+            if let geoOutcome {
+                track(.ipGeolocation, source: geoOutcome.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+            }
         } else {
-            ptrResult = nil
-            geoResult = nil
+            ptrOutcome = nil
+            geoOutcome = nil
         }
 
-        let emailSecurity = mapOptionalValueServiceResult(resolvedEmail)
-        let ptrRecord = mapOptionalServiceResult(ptrResult, missingMessage: "No A record available")
-        let geolocation = mapOptionalServiceResult(geoResult, missingMessage: "No A record available")
+        let emailSecurity = mapOptionalValueServiceResult(normalizeErrors(in: emailOutcome.value))
+        let ptrRecord = mapOptionalServiceResult(ptrOutcome.map { normalizeErrors(in: $0.value) }, missingMessage: "No A record available")
+        let geolocation = mapOptionalServiceResult(geoOutcome.map { normalizeErrors(in: $0.value) }, missingMessage: "No A record available")
 
         return LookupSnapshot(
             historyEntryID: nil,
-            domain: availability.domain,
+            domain: availability.value.domain,
             timestamp: Date(),
-            trackedDomainID: nil,
+            trackedDomainID: previousSnapshot?.trackedDomainID,
             resolverDisplayName: resolverDisplayName,
             resolverURLString: resolverURLString,
             totalLookupDurationMs: Int(Date().timeIntervalSince(startedAt) * 1000),
             dnsSections: dnsSections.value,
             dnsError: dnsSections.message,
-            availabilityResult: availability,
-            suggestions: resolvedSuggestions,
+            availabilityResult: availability.value,
+            suggestions: suggestionsOutcome.value,
             sslInfo: sslInfo.value,
             sslError: sslInfo.message,
-            hstsPreloaded: hsts,
+            hstsPreloaded: hsts.value,
             httpHeaders: httpHeadersResult.headers,
             httpSecurityGrade: httpHeadersResult.securityGrade,
             httpStatusCode: httpHeadersResult.statusCode,
@@ -109,7 +175,9 @@ struct DomainInspectionService {
             portScanResults: portScanResults.value,
             portScanError: portScanResults.message,
             changeSummary: nil,
-            isLive: false
+            resultSource: aggregateSource(sectionSources),
+            cachedSections: Array(cachedSections).sorted { $0.rawValue < $1.rawValue },
+            statusMessage: nil
         )
     }
 
@@ -122,6 +190,115 @@ struct DomainInspectionService {
             .lowercased() ?? domain.lowercased()
     }
 
+    private func track(
+        _ section: LookupSectionKind,
+        source: LookupResultSource,
+        cachedSections: inout Set<LookupSectionKind>,
+        sectionSources: inout [LookupResultSource]
+    ) {
+        sectionSources.append(source)
+        if source != .live {
+            cachedSections.insert(section)
+        }
+    }
+
+    private func aggregateSource(_ sectionSources: [LookupResultSource]) -> LookupResultSource {
+        let normalizedSources = sectionSources.map { source -> LookupResultSource in
+            source == .mixed ? .cached : source
+        }
+
+        let hasLive = normalizedSources.contains(.live)
+        let hasCached = normalizedSources.contains(.cached)
+
+        switch (hasLive, hasCached) {
+        case (true, true):
+            return .mixed
+        case (false, true):
+            return .cached
+        default:
+            return .live
+        }
+    }
+
+    private func canReuseDependentSections(from previousSnapshot: LookupSnapshot?, dnsSections: [DNSSection]) -> Bool {
+        guard let previousSnapshot else { return false }
+        return dnsSignature(for: previousSnapshot.dnsSections) == dnsSignature(for: dnsSections)
+    }
+
+    private func canReuseIPBasedSections(from previousSnapshot: LookupSnapshot?, primaryIP: String?) -> Bool {
+        guard let previousSnapshot else { return false }
+        let previousIP = previousSnapshot.dnsSections.first(where: { $0.recordType == .A })?.records.first?.value
+        return primaryIP == previousIP
+    }
+
+    private func dnsSignature(for sections: [DNSSection]) -> String {
+        sections
+            .sorted { $0.recordType.rawValue < $1.recordType.rawValue }
+            .map { section in
+                let records = section.records
+                    .sorted { $0.value < $1.value }
+                    .map { "\($0.value)|\($0.ttl)" }
+                    .joined(separator: ",")
+                let wildcardRecords = section.wildcardRecords
+                    .sorted { $0.value < $1.value }
+                    .map { "\($0.value)|\($0.ttl)" }
+                    .joined(separator: ",")
+                return [
+                    section.recordType.rawValue,
+                    records,
+                    wildcardRecords,
+                    section.dnssecSigned.map { $0 ? "signed" : "unsigned" } ?? "unknown",
+                    section.error ?? ""
+                ].joined(separator: "#")
+            }
+            .joined(separator: "||")
+    }
+
+    private func normalizeErrors<Value>(in result: ServiceResult<Value>) -> ServiceResult<Value> {
+        switch result {
+        case let .success(value):
+            return .success(value)
+        case let .empty(message):
+            return .empty(message)
+        case let .error(message):
+            return .error(classifiedMessage(from: message))
+        }
+    }
+
+    private func classifiedMessage(from message: String) -> String {
+        let normalizedMessage = message.trimmingCharacters(in: .whitespacesAndNewlines)
+        let lowercasedMessage = normalizedMessage.lowercased()
+
+        if lowercasedMessage.hasPrefix("network error:")
+            || lowercasedMessage.hasPrefix("timeout:")
+            || lowercasedMessage.hasPrefix("rate limit:")
+            || lowercasedMessage.hasPrefix("parsing error:") {
+            return normalizedMessage
+        }
+
+        if lowercasedMessage.contains("timed out") {
+            return "Timeout: Request timed out"
+        }
+        if lowercasedMessage.contains("429")
+            || lowercasedMessage.contains("too many requests")
+            || lowercasedMessage.contains("rate limit") {
+            return "Rate limit: Try again shortly"
+        }
+        if lowercasedMessage.contains("cannot parse")
+            || lowercasedMessage.contains("decoding")
+            || lowercasedMessage.contains("json") {
+            return "Parsing error: Invalid server response"
+        }
+        if lowercasedMessage.contains("offline")
+            || lowercasedMessage.contains("internet connection")
+            || lowercasedMessage.contains("not connected")
+            || lowercasedMessage.contains("network connection") {
+            return "Network error: Offline"
+        }
+
+        return "Network error: \(normalizedMessage)"
+    }
+
     private func mapServiceResult<Value>(_ result: ServiceResult<Value>, emptyValue: Value) -> (value: Value, message: String?) {
         switch result {
         case let .success(value):
diff --git a/DomainReportBuilder.swift b/DomainReportBuilder.swift
index 8cb5c82..7c5a7c0 100644
--- a/DomainReportBuilder.swift
+++ b/DomainReportBuilder.swift
@@ -3,6 +3,7 @@ import Foundation
 struct DomainReport: Codable {
     let domain: String
     let timestamp: Date
+    let resultSource: LookupResultSource
     let availability: DomainAvailabilityStatus
     let ownership: DomainOwnership?
     let dns: DNSResultSummary
@@ -70,6 +71,7 @@ struct DomainReportBuilder {
         return DomainReport(
             domain: snapshot.domain,
             timestamp: snapshot.timestamp,
+            resultSource: snapshot.resultSource,
             availability: snapshot.availabilityResult?.status ?? .unknown,
             ownership: snapshot.ownership,
             dns: DNSResultSummary(
diff --git a/DomainReportExporter.swift b/DomainReportExporter.swift
index 19271fa..ecfdbd4 100644
--- a/DomainReportExporter.swift
+++ b/DomainReportExporter.swift
@@ -36,6 +36,7 @@ enum DomainReportExporter {
             "DomainDig Report",
             "Domain: \(report.domain)",
             "Timestamp: \(textDateFormatter.string(from: report.timestamp))",
+            "Source: \(report.resultSource.label)",
             "Availability: \(availabilityLabel(report.availability))"
         ]
 
@@ -212,6 +213,7 @@ enum DomainReportExporter {
         let headers = [
             "domain",
             "timestamp",
+            "result_source",
             "availability",
             "registrar",
             "ownership_expires",
@@ -247,6 +249,7 @@ enum DomainReportExporter {
             return [
                 report.domain,
                 csvDateFormatter.string(from: report.timestamp),
+                report.resultSource.rawValue,
                 availabilityLabel(report.availability),
                 report.ownership?.registrar ?? "",
                 expirationDate,
diff --git a/LookupSnapshot.swift b/LookupSnapshot.swift
index 3a35eff..dd1e5f4 100644
--- a/LookupSnapshot.swift
+++ b/LookupSnapshot.swift
@@ -39,7 +39,13 @@ struct LookupSnapshot {
     let portScanResults: [PortScanResult]
     let portScanError: String?
     let changeSummary: DomainChangeSummary?
-    let isLive: Bool
+    let resultSource: LookupResultSource
+    let cachedSections: [LookupSectionKind]
+    let statusMessage: String?
+
+    var isLive: Bool {
+        resultSource == .live
+    }
 }
 
 extension HistoryEntry {
@@ -83,7 +89,9 @@ extension HistoryEntry {
             portScanResults: portScanResults,
             portScanError: portScanError,
             changeSummary: changeSummary,
-            isLive: false
+            resultSource: .snapshot,
+            cachedSections: [],
+            statusMessage: nil
         )
     }
 }