diff options
| author | Christian Cleberg <[email protected]> | 2026-04-22 09:47:24 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-22 09:47:24 -0500 |
| commit | 7b41195620eadd54d25fa98dcd36735cba906ba3 (patch) | |
| tree | 929a791a4c2fd3278f98c17c0ae5476940a01312 /DomainDig/HistoryView.swift | |
| parent | 36ea668d7fc9070770973b0bc329f4319bf43cab (diff) | |
| download | domain-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.swift | 112 |
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) |
