summaryrefslogtreecommitdiff
path: root/DomainDig/HistoryView.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-22 09:47:24 -0500
committerChristian Cleberg <[email protected]>2026-04-22 09:47:24 -0500
commit7b41195620eadd54d25fa98dcd36735cba906ba3 (patch)
tree929a791a4c2fd3278f98c17c0ae5476940a01312 /DomainDig/HistoryView.swift
parent36ea668d7fc9070770973b0bc329f4319bf43cab (diff)
downloaddomain-dig-7b41195620eadd54d25fa98dcd36735cba906ba3.tar.gz
domain-dig-7b41195620eadd54d25fa98dcd36735cba906ba3.tar.bz2
domain-dig-7b41195620eadd54d25fa98dcd36735cba906ba3.zip
feat(v2.7.0): add provenance, confidence, and reproducibility metadata
* add result provenance across major sections * introduce confidence levels for ambiguous outputs * distinguish observed facts from inferred summaries * expand snapshot metadata for reproducibility * improve error classification and partial snapshot handling * include provenance and confidence in export * add local notes for tracked domains and history
Diffstat (limited to 'DomainDig/HistoryView.swift')
-rw-r--r--DomainDig/HistoryView.swift112
1 files changed, 101 insertions, 11 deletions
diff --git a/DomainDig/HistoryView.swift b/DomainDig/HistoryView.swift
index 1a5c0d0..9505c12 100644
--- a/DomainDig/HistoryView.swift
+++ b/DomainDig/HistoryView.swift
@@ -39,6 +39,9 @@ struct HistoryView: View {
HStack(spacing: 8) {
AppStatusBadgeView(model: AppStatusFactory.availability(entry.availabilityResult?.status))
AppStatusBadgeView(model: AppStatusFactory.tls(sslInfo: entry.sslInfo, error: entry.sslError))
+ if entry.isPartialSnapshot {
+ AppStatusBadgeView(model: .init(title: "Partial", systemImage: "exclamationmark.triangle.fill", foregroundColor: .yellow, backgroundColor: .yellow.opacity(0.16)))
+ }
}
HStack(spacing: 8) {
@@ -51,6 +54,13 @@ struct HistoryView: View {
}
.font(appDensity.font(.caption2))
.foregroundStyle(.secondary)
+
+ if let note = entry.note, !note.isEmpty {
+ Text(note)
+ .font(appDensity.font(.caption2))
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ }
}
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
@@ -128,6 +138,9 @@ struct HistoryDetailView: View {
@Bindable var viewModel: DomainViewModel
let entry: HistoryEntry
@Environment(\.dismiss) private var dismiss
+ @State private var noteDraft = ""
+ @State private var isEditingNote = false
+ @State private var showRerunOptions = false
private let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
@@ -153,6 +166,9 @@ struct HistoryDetailView: View {
showSuggestions: entry.availabilityResult?.status == .registered && !entry.suggestions.isEmpty,
availabilityLoading: false,
suggestionsLoading: false,
+ provenance: snapshot.provenanceBySection[.availability],
+ confidence: snapshot.availabilityConfidence,
+ snapshotNote: entry.note,
trackedDomain: viewModel.trackedDomains.first(where: { $0.domain.lowercased() == entry.domain.lowercased() }),
trackingLimitMessage: nil,
onTrack: {
@@ -170,6 +186,8 @@ struct HistoryDetailView: View {
rows: DomainViewModel.ownershipRows(from: snapshot),
loading: false,
error: snapshot.ownershipError,
+ provenance: snapshot.provenanceBySection[.ownership],
+ confidence: snapshot.ownershipConfidence,
showsHistoryPlaceholder: !DataAccessService.hasAccess(to: .ownershipHistory)
)
.padding(.top, appDensity.metrics.sectionSpacing)
@@ -178,6 +196,8 @@ struct HistoryDetailView: View {
rows: DomainViewModel.subdomainRows(from: snapshot),
loading: false,
error: snapshot.subdomainsError,
+ provenance: snapshot.provenanceBySection[.subdomains],
+ confidence: snapshot.subdomainConfidence,
showsExtendedPlaceholder: !DataAccessService.hasAccess(to: .extendedSubdomains)
)
.padding(.top, appDensity.metrics.sectionSpacing)
@@ -189,6 +209,7 @@ struct HistoryDetailView: View {
DomainDiffView(
title: "Compared With Previous Snapshot",
sections: DomainDiffService.diff(from: comparisonSnapshot, to: snapshot),
+ contextNote: DomainDiffService.comparisonContextNote(from: comparisonSnapshot, to: snapshot),
showsUnchanged: false
)
.padding(.top, appDensity.metrics.sectionSpacing)
@@ -199,6 +220,8 @@ struct HistoryDetailView: View {
sections: DomainViewModel.dnsRows(from: snapshot),
ptrMessage: DomainViewModel.ptrMessage(from: snapshot),
loading: false,
+ dnsProvenance: snapshot.provenanceBySection[.dns],
+ ptrProvenance: snapshot.provenanceBySection[.ptr],
sectionError: snapshot.dnsError
)
.padding(.top, appDensity.metrics.sectionSpacing)
@@ -208,13 +231,16 @@ struct HistoryDetailView: View {
sslInfo: snapshot.sslInfo,
sslLoading: false,
sslError: snapshot.sslError,
+ tlsProvenance: snapshot.provenanceBySection[.ssl],
responseRows: DomainViewModel.webResponseRows(from: snapshot),
headers: snapshot.httpHeaders,
headersLoading: false,
headersError: snapshot.httpHeadersError,
+ httpProvenance: snapshot.provenanceBySection[.httpHeaders],
redirects: DomainViewModel.redirectRows(from: snapshot),
redirectLoading: false,
redirectError: snapshot.redirectChainError,
+ redirectProvenance: snapshot.provenanceBySection[.redirectChain],
finalURL: snapshot.redirectChain.last?.url
)
.padding(.top, appDensity.metrics.sectionSpacing)
@@ -222,6 +248,8 @@ struct HistoryDetailView: View {
isCollapsed: .constant(false),
rows: DomainViewModel.emailRows(from: snapshot),
loading: false,
+ provenance: snapshot.provenanceBySection[.emailSecurity],
+ confidence: snapshot.emailSecurityConfidence,
error: snapshot.emailSecurityError
)
.padding(.top, appDensity.metrics.sectionSpacing)
@@ -230,14 +258,18 @@ struct HistoryDetailView: View {
reachabilityRows: DomainViewModel.reachabilityRows(from: snapshot),
reachabilityLoading: false,
reachabilityError: snapshot.reachabilityError,
+ reachabilityProvenance: snapshot.provenanceBySection[.reachability],
locationRows: DomainViewModel.locationRows(from: snapshot),
geolocation: snapshot.ipGeolocation,
geolocationLoading: false,
geolocationError: snapshot.ipGeolocationError,
+ geolocationProvenance: snapshot.provenanceBySection[.ipGeolocation],
+ geolocationConfidence: snapshot.geolocationConfidence,
standardPortRows: DomainViewModel.portRows(from: snapshot, kind: .standard),
customPortRows: DomainViewModel.portRows(from: snapshot, kind: .custom),
portScanLoading: false,
portScanError: snapshot.portScanError,
+ portScanProvenance: snapshot.provenanceBySection[.portScan],
customPortScanLoading: false,
customPortScanError: nil,
isCloudflareProxied: snapshot.httpHeaders.contains(where: { $0.name.lowercased() == "cf-ray" }),
@@ -253,26 +285,84 @@ struct HistoryDetailView: View {
.background(Color.black)
.navigationTitle(entry.domain)
.toolbar {
- Button("Re-run") {
- viewModel.rerunLookup(from: entry)
+ ToolbarItemGroup(placement: .topBarTrailing) {
+ Button("Note") {
+ noteDraft = entry.note ?? ""
+ isEditingNote = true
+ }
+ Button("Re-run") {
+ showRerunOptions = true
+ }
}
}
.onChange(of: viewModel.rerunNavigationToken) { _, _ in
dismiss()
}
+ .confirmationDialog("Re-run lookup", isPresented: $showRerunOptions) {
+ Button("Run with Current Settings") {
+ viewModel.rerunLookup(from: entry, useSnapshotResolver: false)
+ }
+ Button("Run with Snapshot Resolver") {
+ viewModel.rerunLookup(from: entry, useSnapshotResolver: true)
+ }
+ Button("Cancel", role: .cancel) {}
+ } message: {
+ Text(viewModel.resolverMismatchNote(for: entry) ?? "Choose how to reproduce this snapshot.")
+ }
+ .sheet(isPresented: $isEditingNote) {
+ NavigationStack {
+ Form {
+ Section("Audit Note") {
+ TextField("Optional note", text: $noteDraft, axis: .vertical)
+ .lineLimit(3...6)
+ }
+ }
+ .navigationTitle(entry.domain)
+ .toolbar {
+ ToolbarItem(placement: .cancellationAction) {
+ Button("Cancel") {
+ isEditingNote = false
+ }
+ }
+ ToolbarItem(placement: .confirmationAction) {
+ Button("Save") {
+ viewModel.updateHistoryNote(noteDraft, for: entry)
+ isEditingNote = false
+ }
+ }
+ }
+ }
+ }
.preferredColorScheme(.dark)
}
private var snapshotBanner: some View {
- HStack(spacing: 8) {
- Image(systemName: "archivebox")
- .font(.caption)
- Text("Snapshot from \(dateFormatter.string(from: entry.timestamp))")
- .font(appDensity.font(.caption))
- Spacer()
- Text("Live re-run available")
- .font(appDensity.font(.caption2))
- .foregroundStyle(.secondary)
+ VStack(alignment: .leading, spacing: 8) {
+ HStack(spacing: 8) {
+ Image(systemName: "archivebox")
+ .font(.caption)
+ Text("Snapshot from \(dateFormatter.string(from: entry.timestamp))")
+ .font(appDensity.font(.caption))
+ Spacer()
+ Text("Live re-run available")
+ .font(appDensity.font(.caption2))
+ .foregroundStyle(.secondary)
+ }
+ if let mismatchNote = viewModel.resolverMismatchNote(for: entry) {
+ Text(mismatchNote)
+ .font(appDensity.font(.caption2))
+ .foregroundStyle(.orange)
+ }
+ if entry.isPartialSnapshot {
+ Text("Partial snapshot: \(entry.validationIssues.joined(separator: " | "))")
+ .font(appDensity.font(.caption2))
+ .foregroundStyle(.yellow)
+ }
+ if let note = entry.note, !note.isEmpty {
+ Text(note)
+ .font(appDensity.font(.caption2))
+ .foregroundStyle(.secondary)
+ }
}
.foregroundStyle(.secondary)
.padding(8)