Commit bb18197860

bb18197860ffdb491f010648ec903003b6e1bb65

parent: 083dfa7237

Unsigned

cmc <hello@cleberg.net> · 2026-07-20 21:22 UTC

v4.8.3: Clear SonarCloud new-code issues

Fixes the 4 reported bugs and ~97 code smells flagged in the new-code
period. No behavior changes.

Bugs (swift:S3923) — DomainInspectionService's confidenceFor* helpers each
returned `error == nil ? .low : .low`, an inert conditional. Simplified to
`return .low` and dropped the now-unused `error` parameter.

Smells:
- Merged 14 identical `.empty`/`.error` switch branches in DomainViewModel
- Consolidated duplicate implementations (clearPresentedResults/reset,
  String.nonEmpty/nilIfEmpty, ExportFormat.id/fileExtension)
- Extracted nested ternaries into TLSGrade.tone, EmailSecurityGrade.tone,
  and ChangeImpactClassification.color; removed ContentView.impactColor
  and the duplicate mapping in BatchResultsView
- Documented empty closures and singleton inits
- Marked unused protocol-conformance parameters `_`
- Renamed CloudSyncTrigger.`import` to `imported` (raw value preserved)
  and SSLSessionDelegate's _serverTrust/_tlsMetadata
- Merged nested ifs in the DER parser; flattened closure nesting in
  PortScanService and IntegrationService
- Replaced two-case switches with if/else

Left open: S107 (init parameter counts), S115 (constants mirroring DoH and
ipapi JSON keys), S1075 (false positives on https:// literals), and two
S117 hits on SwiftUI $binding shorthand. These want a Won't Fix resolution
in SonarCloud, not a code change.

Layout: unified · split

DomainDig.xcodeproj/project.pbxproj +12 −12
@@ -497,7 +497,7 @@
497497 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
498498 CODE_SIGN_ENTITLEMENTS = DomainDig/DomainDig.entitlements;
499499 CODE_SIGN_STYLE = Automatic;
500 CURRENT_PROJECT_VERSION = 42;
500 CURRENT_PROJECT_VERSION = 43;
501501 DEVELOPMENT_TEAM = ZCNAX3VL9D;
502502 ENABLE_PREVIEWS = YES;
503503 GENERATE_INFOPLIST_FILE = YES;
@@ -514,7 +514,7 @@
514514 "$(inherited)",
515515 "@executable_path/Frameworks",
516516 );
517 MARKETING_VERSION = 4.8.2;
517 MARKETING_VERSION = 4.8.3;
518518 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig;
519519 PRODUCT_NAME = "$(TARGET_NAME)";
520520 STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -534,7 +534,7 @@
534534 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
535535 CODE_SIGN_ENTITLEMENTS = DomainDig/DomainDig.entitlements;
536536 CODE_SIGN_STYLE = Automatic;
537 CURRENT_PROJECT_VERSION = 42;
537 CURRENT_PROJECT_VERSION = 43;
538538 DEVELOPMENT_TEAM = ZCNAX3VL9D;
539539 ENABLE_PREVIEWS = YES;
540540 GENERATE_INFOPLIST_FILE = YES;
@@ -551,7 +551,7 @@
551551 "$(inherited)",
552552 "@executable_path/Frameworks",
553553 );
554 MARKETING_VERSION = 4.8.2;
554 MARKETING_VERSION = 4.8.3;
555555 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig;
556556 PRODUCT_NAME = "$(TARGET_NAME)";
557557 STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -570,7 +570,7 @@
570570 APPLICATION_EXTENSION_API_ONLY = YES;
571571 CODE_SIGN_ENTITLEMENTS = DomainDigWidget/DomainDigWidget.entitlements;
572572 CODE_SIGN_STYLE = Automatic;
573 CURRENT_PROJECT_VERSION = 42;
573 CURRENT_PROJECT_VERSION = 43;
574574 DEVELOPMENT_TEAM = ZCNAX3VL9D;
575575 GENERATE_INFOPLIST_FILE = NO;
576576 INFOPLIST_FILE = DomainDigWidget/Info.plist;
@@ -581,7 +581,7 @@
581581 "@executable_path/Frameworks",
582582 "@executable_path/../../Frameworks",
583583 );
584 MARKETING_VERSION = 4.8.2;
584 MARKETING_VERSION = 4.8.3;
585585 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig.DomainDigWidget;
586586 PRODUCT_NAME = "$(TARGET_NAME)";
587587 SKIP_INSTALL = YES;
@@ -599,7 +599,7 @@
599599 APPLICATION_EXTENSION_API_ONLY = YES;
600600 CODE_SIGN_ENTITLEMENTS = DomainDigWidget/DomainDigWidget.entitlements;
601601 CODE_SIGN_STYLE = Automatic;
602 CURRENT_PROJECT_VERSION = 42;
602 CURRENT_PROJECT_VERSION = 43;
603603 DEVELOPMENT_TEAM = ZCNAX3VL9D;
604604 GENERATE_INFOPLIST_FILE = NO;
605605 INFOPLIST_FILE = DomainDigWidget/Info.plist;
@@ -610,7 +610,7 @@
610610 "@executable_path/Frameworks",
611611 "@executable_path/../../Frameworks",
612612 );
613 MARKETING_VERSION = 4.8.2;
613 MARKETING_VERSION = 4.8.3;
614614 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig.DomainDigWidget;
615615 PRODUCT_NAME = "$(TARGET_NAME)";
616616 SKIP_INSTALL = YES;
@@ -628,7 +628,7 @@
628628 APPLICATION_EXTENSION_API_ONLY = YES;
629629 CODE_SIGN_ENTITLEMENTS = DomainDigShareExtension/DomainDigShareExtension.entitlements;
630630 CODE_SIGN_STYLE = Automatic;
631 CURRENT_PROJECT_VERSION = 42;
631 CURRENT_PROJECT_VERSION = 43;
632632 DEVELOPMENT_TEAM = ZCNAX3VL9D;
633633 GENERATE_INFOPLIST_FILE = NO;
634634 INFOPLIST_FILE = DomainDigShareExtension/Info.plist;
@@ -639,7 +639,7 @@
639639 "@executable_path/Frameworks",
640640 "@executable_path/../../Frameworks",
641641 );
642 MARKETING_VERSION = 4.8.2;
642 MARKETING_VERSION = 4.8.3;
643643 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig.DomainDigShareExtension;
644644 PRODUCT_NAME = "$(TARGET_NAME)";
645645 SKIP_INSTALL = YES;
@@ -657,7 +657,7 @@
657657 APPLICATION_EXTENSION_API_ONLY = YES;
658658 CODE_SIGN_ENTITLEMENTS = DomainDigShareExtension/DomainDigShareExtension.entitlements;
659659 CODE_SIGN_STYLE = Automatic;
660 CURRENT_PROJECT_VERSION = 42;
660 CURRENT_PROJECT_VERSION = 43;
661661 DEVELOPMENT_TEAM = ZCNAX3VL9D;
662662 GENERATE_INFOPLIST_FILE = NO;
663663 INFOPLIST_FILE = DomainDigShareExtension/Info.plist;
@@ -668,7 +668,7 @@
668668 "@executable_path/Frameworks",
669669 "@executable_path/../../Frameworks",
670670 );
671 MARKETING_VERSION = 4.8.2;
671 MARKETING_VERSION = 4.8.3;
672672 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig.DomainDigShareExtension;
673673 PRODUCT_NAME = "$(TARGET_NAME)";
674674 SKIP_INSTALL = YES;
DomainDig/AppVersion.swift +1 −1
@@ -2,6 +2,6 @@ import Foundation
22
33enum AppVersion {
44 nonisolated static var current: String {
5 "4.8.2"
5 "4.8.3"
66 }
77}
DomainDig/AuditModeView.swift +1 −1
@@ -57,7 +57,7 @@ struct AuditModeView: View {
5757 }
5858 }
5959 .alert("Audit Export", isPresented: Binding(get: { exportMessage != nil }, set: { if !$0 { exportMessage = nil } })) {
60 Button("OK", role: .cancel) {}
60 Button("OK", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
6161 } message: {
6262 Text(exportMessage ?? "")
6363 }
DomainDig/BatchResultsView.swift +1 −1
@@ -104,7 +104,7 @@ struct BatchResultRowView: View {
104104 if let changeClassification = result.changeClassification {
105105 Text("Impact: \(changeClassification.title)")
106106 .font(appDensity.font(.caption2))
107 .foregroundStyle(changeClassification == .critical ? .red : (changeClassification == .warning ? .yellow : .secondary))
107 .foregroundStyle(changeClassification.color)
108108 }
109109
110110 if let errorMessage = result.errorMessage {
DomainDig/CloudSharingLifecycle.swift +4 −4
@@ -3,9 +3,9 @@ import UIKit
33
44final class DomainDigAppDelegate: NSObject, UIApplicationDelegate {
55 func application(
6 _ application: UIApplication,
6 _: UIApplication,
77 configurationForConnecting connectingSceneSession: UISceneSession,
8 options: UIScene.ConnectionOptions
8 options _: UIScene.ConnectionOptions
99 ) -> UISceneConfiguration {
1010 let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
1111 configuration.delegateClass = DomainDigSceneDelegate.self
@@ -14,12 +14,12 @@ final class DomainDigAppDelegate: NSObject, UIApplicationDelegate {
1414}
1515
1616final class DomainDigSceneDelegate: NSObject, UIWindowSceneDelegate {
17 func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
17 func scene(_: UIScene, willConnectTo _: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
1818 guard let metadata = connectionOptions.cloudKitShareMetadata else { return }
1919 accept(metadata: metadata)
2020 }
2121
22 func windowScene(_ windowScene: UIWindowScene, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {
22 func windowScene(_: UIWindowScene, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {
2323 accept(metadata: cloudKitShareMetadata)
2424 }
2525
DomainDig/CloudSharingSheet.swift +2 −2
@@ -13,7 +13,7 @@ struct CloudSharingSheet: UIViewControllerRepresentable {
1313 Coordinator(dismiss: dismiss, title: title)
1414 }
1515
16 func makeUIViewController(context: Context) -> UIViewController {
16 func makeUIViewController(context _: Context) -> UIViewController {
1717 UIViewController()
1818 }
1919
@@ -77,7 +77,7 @@ struct CloudSharingSheet: UIViewControllerRepresentable {
7777 }
7878 }
7979
80 func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
80 func presentationControllerDidDismiss(_: UIPresentationController) {
8181 dismissAction()
8282 }
8383 }
DomainDig/CloudSyncService.swift +2 −2
@@ -40,7 +40,7 @@ enum CloudSyncTrigger: String {
4040 case launch
4141 case automatic
4242 case manual
43 case `import`
43 case imported = "import"
4444}
4545
4646enum ShareableEntity: Identifiable, Hashable {
@@ -1186,7 +1186,7 @@ final class CloudSyncService {
11861186 }
11871187
11881188 private func collaborationMetadata(
1189 for record: CKRecord,
1189 for _: CKRecord,
11901190 share: CKShare?,
11911191 scope: SyncDatabaseScope
11921192 ) -> CollaborationMetadata {
DomainDig/ContentView.swift +56 −31
@@ -569,13 +569,13 @@ struct ContentView: View {
569569 shareSingleResults(format: .pdf)
570570 }
571571 } else {
572 Button("CSV Export • Available in Pro") {}
572 Button("CSV Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
573573 .disabled(true)
574 Button("JSON Export • Available in Pro") {}
574 Button("JSON Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
575575 .disabled(true)
576 Button("Markdown Export • Available in Pro") {}
576 Button("Markdown Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
577577 .disabled(true)
578 Button("PDF Export • Available in Pro") {}
578 Button("PDF Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
579579 .disabled(true)
580580 }
581581 } label: {
@@ -621,13 +621,13 @@ struct ContentView: View {
621621 shareBatchResults(format: .pdf)
622622 }
623623 } else {
624 Button("Batch CSV • Available in Pro") {}
624 Button("Batch CSV • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
625625 .disabled(true)
626 Button("Batch JSON • Available in Pro") {}
626 Button("Batch JSON • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
627627 .disabled(true)
628 Button("Batch Markdown • Available in Pro") {}
628 Button("Batch Markdown • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
629629 .disabled(true)
630 Button("Batch PDF • Available in Pro") {}
630 Button("Batch PDF • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
631631 .disabled(true)
632632 }
633633 } label: {
@@ -1040,10 +1040,10 @@ struct DomainChangeSummaryView: View {
10401040 .clipShape(Capsule())
10411041 Text(summary.impactClassification.title.uppercased())
10421042 .font(appDensity.font(.caption2))
1043 .foregroundStyle(impactColor(summary.impactClassification))
1043 .foregroundStyle(summary.impactClassification.color)
10441044 .padding(.horizontal, 8)
10451045 .padding(.vertical, 4)
1046 .background(impactColor(summary.impactClassification).opacity(0.16))
1046 .background(summary.impactClassification.color.opacity(0.16))
10471047 .clipShape(Capsule())
10481048 Text(summary.generatedAt, style: .time)
10491049 .font(appDensity.font(.caption2))
@@ -1107,16 +1107,6 @@ struct DomainChangeSummaryView: View {
11071107 }
11081108 }
11091109
1110 private func impactColor(_ impact: ChangeImpactClassification) -> Color {
1111 switch impact {
1112 case .informational:
1113 return .secondary
1114 case .warning:
1115 return .yellow
1116 case .critical:
1117 return .red
1118 }
1119 }
11201110}
11211111
11221112struct DomainDiffView: View {
@@ -2008,7 +1998,7 @@ struct WebSectionView: View {
20081998 }
20091999 SectionTrustMetadataView(provenance: tlsProvenance, confidence: nil)
20102000 if !sslLoading, let tlsSummary {
2011 LabeledValueRow(row: InfoRowViewData(label: "TLS Grade", value: tlsSummary.tlsGrade.rawValue, tone: tlsSummary.tlsGrade == .a ? .success : (tlsSummary.tlsGrade == .f ? .failure : .warning)))
2001 LabeledValueRow(row: InfoRowViewData(label: "TLS Grade", value: tlsSummary.tlsGrade.rawValue, tone: tlsSummary.tlsGrade.tone))
20122002 ForEach(Array(tlsSummary.tlsHighlights.enumerated()), id: \.offset) { _, highlight in
20132003 MessageRowView(text: highlight, isError: isTLSHighlightError(highlight))
20142004 }
@@ -2153,7 +2143,7 @@ struct EmailSectionView: View {
21532143 .opacity(loading ? 0 : 1)
21542144 }
21552145 if let assessment, let grade = assessment.grade {
2156 LabeledValueRow(row: InfoRowViewData(label: "Grade", value: grade.rawValue, tone: grade == .a ? .success : (grade == .f ? .failure : .warning)))
2146 LabeledValueRow(row: InfoRowViewData(label: "Grade", value: grade.rawValue, tone: grade.tone))
21572147 if !assessment.reasons.isEmpty {
21582148 Text(assessment.reasons.joined(separator: " | "))
21592149 .font(appDensity.font(.caption2))
@@ -2897,6 +2887,11 @@ private struct LocalAPISettingsView: View {
28972887 @State private var localAPIService = LocalAPIService.shared
28982888 @State private var portText = ""
28992889
2890 private var statusText: String {
2891 if localAPIService.isRunning { return "Running" }
2892 return localAPIService.config.isEnabled ? "Stopped" : "Disabled"
2893 }
2894
29002895 var body: some View {
29012896 Form {
29022897 Section("Local API") {
@@ -2923,7 +2918,7 @@ private struct LocalAPISettingsView: View {
29232918 .keyboardType(.numberPad)
29242919
29252920 LabeledContent("Address", value: localAPIService.address)
2926 LabeledContent("Status", value: localAPIService.isRunning ? "Running" : (localAPIService.config.isEnabled ? "Stopped" : "Disabled"))
2921 LabeledContent("Status", value: statusText)
29272922 LabeledContent("Token", value: localAPIService.maskedToken)
29282923
29292924 if let statusMessage = localAPIService.statusMessage {
@@ -3339,7 +3334,7 @@ private struct DataPortabilitySettingsView: View {
33393334 Button("Replace", role: .destructive) {
33403335 applyPendingImport()
33413336 }
3342 Button("Cancel", role: .cancel) {}
3337 Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
33433338 } message: {
33443339 Text("Replace mode overwrites local data covered by the imported file and may remove items that are only on this device.")
33453340 }
@@ -3347,7 +3342,7 @@ private struct DataPortabilitySettingsView: View {
33473342 get: { pendingImportError != nil },
33483343 set: { if !$0 { pendingImportError = nil } }
33493344 )) {
3350 Button("OK", role: .cancel) {}
3345 Button("OK", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
33513346 } message: {
33523347 Text(pendingImportError ?? "The import could not be completed.")
33533348 }
@@ -3585,7 +3580,7 @@ private struct DataManagementSettingsView: View {
35853580 Button("Clear", role: .destructive) {
35863581 viewModel.clearHistory()
35873582 }
3588 Button("Cancel", role: .cancel) {}
3583 Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
35893584 } message: {
35903585 Text("This removes saved lookup snapshots and clears monitoring run history on this device.")
35913586 }
@@ -3593,7 +3588,7 @@ private struct DataManagementSettingsView: View {
35933588 Button("Clear", role: .destructive) {
35943589 viewModel.clearLookupCache()
35953590 }
3596 Button("Cancel", role: .cancel) {}
3591 Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
35973592 } message: {
35983593 Text("This clears the in-memory lookup cache and cancels any cached in-flight work.")
35993594 }
@@ -3601,7 +3596,7 @@ private struct DataManagementSettingsView: View {
36013596 Button("Clear", role: .destructive) {
36023597 viewModel.clearWorkflows()
36033598 }
3604 Button("Cancel", role: .cancel) {}
3599 Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
36053600 } message: {
36063601 Text("This removes saved workflows only. History, tracked domains, and saved reports stay intact.")
36073602 }
@@ -3609,12 +3604,12 @@ private struct DataManagementSettingsView: View {
36093604 Button("Clear", role: .destructive) {
36103605 viewModel.clearTrackedDomains()
36113606 }
3612 Button("Cancel", role: .cancel) {}
3607 Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
36133608 } message: {
36143609 Text("This removes the watchlist and clears monitoring run history. History and workflows stay intact.")
36153610 }
36163611 .alert("Delete All Data?", isPresented: $showDeleteAllConfirmation) {
3617 Button("Cancel", role: .cancel) {}
3612 Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
36183613 Button("Delete All Data", role: .destructive) {
36193614 deleteAllData()
36203615 }
@@ -3625,7 +3620,7 @@ private struct DataManagementSettingsView: View {
36253620 get: { deleteAllErrorMessage != nil },
36263621 set: { if !$0 { deleteAllErrorMessage = nil } }
36273622 )) {
3628 Button("OK", role: .cancel) {}
3623 Button("OK", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
36293624 } message: {
36303625 Text(deleteAllErrorMessage ?? "The local data reset could not be completed.")
36313626 }
@@ -3745,6 +3740,36 @@ private struct DataImportPreviewSheet: View {
37453740 }
37463741}
37473742
3743extension ChangeImpactClassification {
3744 var color: Color {
3745 switch self {
3746 case .informational: return .secondary
3747 case .warning: return .yellow
3748 case .critical: return .red
3749 }
3750 }
3751}
3752
3753extension TLSGrade {
3754 var tone: ResultTone {
3755 switch self {
3756 case .a: return .success
3757 case .f: return .failure
3758 default: return .warning
3759 }
3760 }
3761}
3762
3763extension EmailSecurityGrade {
3764 var tone: ResultTone {
3765 switch self {
3766 case .a: return .success
3767 case .f: return .failure
3768 default: return .warning
3769 }
3770 }
3771}
3772
37483773#Preview {
37493774 ContentView(viewModel: DomainViewModel())
37503775}
DomainDig/DashboardView.swift +5 −1
@@ -239,22 +239,26 @@ struct DashboardView: View {
239239 let warningCount = states.filter { $0.health == .warning }.count
240240 let title: String
241241 let color: Color
242 let systemImage: String
242243
243244 if criticalCount > 0 {
244245 title = "\(criticalCount) critical"
245246 color = .red
247 systemImage = "exclamationmark.octagon.fill"
246248 } else if warningCount > 0 {
247249 title = "\(warningCount) warning"
248250 color = .yellow
251 systemImage = "exclamationmark.triangle.fill"
249252 } else {
250253 title = "Healthy"
251254 color = .green
255 systemImage = "checkmark.circle.fill"
252256 }
253257
254258 return AppStatusBadgeView(
255259 model: .init(
256260 title: title,
257 systemImage: criticalCount > 0 ? "exclamationmark.octagon.fill" : (warningCount > 0 ? "exclamationmark.triangle.fill" : "checkmark.circle.fill"),
261 systemImage: systemImage,
258262 foregroundColor: color,
259263 backgroundColor: color.opacity(0.16)
260264 )
DomainDig/DomainAvailabilityService.swift +1
@@ -47,6 +47,7 @@ struct DomainAvailabilityService {
4747 return .registered
4848 }
4949 } catch {
50 // No A record (or the lookup failed); fall through to the NS probe.
5051 }
5152
5253 do {
DomainDig/DomainDigIntents.swift +1 −1
@@ -149,7 +149,7 @@ struct RunSweepIntent: AppIntent {
149149final class DomainDigIntentRouter {
150150 static let shared = DomainDigIntentRouter()
151151 var pendingAction: DomainDigDeepLink.Action?
152 private init() {}
152 private init() { /* Singleton; use the shared instance. */ }
153153}
154154
155155/// Exposes DomainDig intents to Spotlight and Siri with invocation phrases.
DomainDig/DomainInsightEngine.swift +9 −2
@@ -412,7 +412,7 @@ enum DomainInsightEngine {
412412 if hasMXRecords(snapshot), snapshot.emailSecurity == nil {
413413 patterns.append("MX records exist without corresponding email security records")
414414 }
415 if snapshot.sslInfo != nil && !(snapshot.dnsSections.first(where: { $0.recordType == .CAA })?.records.isEmpty == false) {
415 if snapshot.sslInfo != nil, snapshot.dnsSections.first(where: { $0.recordType == .CAA })?.records.isEmpty != false {
416416 patterns.append("TLS is active but no CAA record was found")
417417 }
418418
@@ -427,9 +427,16 @@ enum DomainInsightEngine {
427427 let dmarcFound = result.dmarc.found
428428 let dmarcStrict = isStrictDMARC(result.dmarc.value)
429429
430 let dmarcReason: String
431 if !dmarcFound {
432 dmarcReason = "DMARC missing"
433 } else {
434 dmarcReason = dmarcStrict ? "DMARC policy is strict" : "DMARC policy is not strict"
435 }
436
430437 let reasons = [
431438 spfFound ? "SPF present" : "SPF missing",
432 dmarcFound ? (dmarcStrict ? "DMARC policy is strict" : "DMARC policy is not strict") : "DMARC missing",
439 dmarcReason,
433440 dkimFound ? "DKIM present" : "DKIM not detected"
434441 ]
435442
DomainDig/DomainMonitoringService.swift +3 −3
@@ -85,7 +85,7 @@ final class DomainMonitoringScheduler {
8585
8686 private var isRegistered = false
8787
88 private init() {}
88 private init() { /* Singleton; use the shared instance. */ }
8989
9090 func registerBackgroundTask() {
9191 #if canImport(BackgroundTasks)
@@ -613,8 +613,8 @@ final class DomainMonitoringService {
613613
614614 private func processAdaptiveMonitoringState(
615615 trackedDomain: TrackedDomain,
616 snapshot: LookupSnapshot,
617 previousSnapshot: LookupSnapshot?,
616 snapshot _: LookupSnapshot,
617 previousSnapshot _: LookupSnapshot?,
618618 alertDescriptor: MonitoringAlertDescriptor?,
619619 settings: MonitoringSettings,
620620 notificationsAuthorized: Bool,
DomainDig/DomainViewModel.swift +17 −81
@@ -1280,19 +1280,7 @@ final class DomainViewModel {
12801280 }
12811281
12821282 func clearPresentedResults() {
1283 lookupTask?.cancel()
1284 customPortScanTask?.cancel()
1285 batchTask?.cancel()
1286 hasRun = false
1287 searchedDomain = ""
1288 lastLookupDurationMs = nil
1289 currentDiffSections = []
1290 currentChangeSummary = nil
1291 ownershipDiff = []
1292 currentReport = nil
1293 refreshingTrackedDomainID = nil
1294 clearBatchState()
1295 clearLookupState()
1283 reset()
12961284 }
12971285
12981286 func run() {
@@ -1857,7 +1845,7 @@ final class DomainViewModel {
18571845 portabilityStatusMessage = result.summary
18581846 CloudSyncService.shared.markAppSettingsChanged()
18591847 CloudSyncService.shared.markMonitoringSettingsChanged(localActivationConfirmed: monitoringSettings.isEnabled)
1860 CloudSyncService.shared.scheduleSyncIfNeeded(trigger: .import)
1848 CloudSyncService.shared.scheduleSyncIfNeeded(trigger: .imported)
18611849 return result
18621850 }
18631851
@@ -2174,10 +2162,7 @@ final class DomainViewModel {
21742162 case let .success(sections):
21752163 dnsSections = sections
21762164 dnsError = nil
2177 case let .empty(message):
2178 dnsSections = []
2179 dnsError = message
2180 case let .error(message):
2165 case let .empty(message), let .error(message):
21812166 dnsSections = []
21822167 dnsError = message
21832168 }
@@ -2199,10 +2184,7 @@ final class DomainViewModel {
21992184 case let .success(info):
22002185 sslInfo = info
22012186 sslError = nil
2202 case let .empty(message):
2203 sslInfo = nil
2204 sslError = message
2205 case let .error(message):
2187 case let .empty(message), let .error(message):
22062188 sslInfo = nil
22072189 sslError = message
22082190 }
@@ -2228,15 +2210,7 @@ final class DomainViewModel {
22282210 httpProtocol = headersResult.httpProtocol
22292211 http3Advertised = headersResult.http3Advertised
22302212 httpHeadersError = nil
2231 case let .empty(message):
2232 httpHeaders = []
2233 httpSecurityGrade = nil
2234 httpStatusCode = nil
2235 httpResponseTimeMs = nil
2236 httpProtocol = nil
2237 http3Advertised = false
2238 httpHeadersError = message
2239 case let .error(message):
2213 case let .empty(message), let .error(message):
22402214 httpHeaders = []
22412215 httpSecurityGrade = nil
22422216 httpStatusCode = nil
@@ -2255,10 +2229,7 @@ final class DomainViewModel {
22552229 case let .success(results):
22562230 reachabilityResults = results
22572231 reachabilityError = nil
2258 case let .empty(message):
2259 reachabilityResults = []
2260 reachabilityError = message
2261 case let .error(message):
2232 case let .empty(message), let .error(message):
22622233 reachabilityResults = []
22632234 reachabilityError = message
22642235 }
@@ -2272,10 +2243,7 @@ final class DomainViewModel {
22722243 case let .success(emailResult):
22732244 emailSecurity = emailResult
22742245 emailSecurityError = nil
2275 case let .empty(message):
2276 emailSecurity = nil
2277 emailSecurityError = message
2278 case let .error(message):
2246 case let .empty(message), let .error(message):
22792247 emailSecurity = nil
22802248 emailSecurityError = message
22812249 }
@@ -2289,10 +2257,7 @@ final class DomainViewModel {
22892257 case let .success(ownership):
22902258 ownershipResult = ownership
22912259 ownershipError = nil
2292 case let .empty(message):
2293 ownershipResult = nil
2294 ownershipError = message
2295 case let .error(message):
2260 case let .empty(message), let .error(message):
22962261 ownershipResult = nil
22972262 ownershipError = message
22982263 }
@@ -2306,10 +2271,7 @@ final class DomainViewModel {
23062271 case let .success(record):
23072272 ptrRecord = record
23082273 ptrError = nil
2309 case let .empty(message):
2310 ptrRecord = nil
2311 ptrError = message
2312 case let .error(message):
2274 case let .empty(message), let .error(message):
23132275 ptrRecord = nil
23142276 ptrError = message
23152277 }
@@ -2323,10 +2285,7 @@ final class DomainViewModel {
23232285 case let .success(hops):
23242286 redirectChain = hops
23252287 redirectChainError = nil
2326 case let .empty(message):
2327 redirectChain = []
2328 redirectChainError = message
2329 case let .error(message):
2288 case let .empty(message), let .error(message):
23302289 redirectChain = []
23312290 redirectChainError = message
23322291 }
@@ -2340,10 +2299,7 @@ final class DomainViewModel {
23402299 case let .success(results):
23412300 subdomains = results
23422301 subdomainsError = nil
2343 case let .empty(message):
2344 subdomains = []
2345 subdomainsError = message
2346 case let .error(message):
2302 case let .empty(message), let .error(message):
23472303 subdomains = []
23482304 subdomainsError = message
23492305 }
@@ -2358,11 +2314,7 @@ final class DomainViewModel {
23582314 guard !Task.isCancelled, isCurrentLookup(lookupID) else { return }
23592315 portScanResults = enrichedResults
23602316 portScanError = nil
2361 case let .empty(message):
2362 guard !Task.isCancelled, isCurrentLookup(lookupID) else { return }
2363 portScanResults = []
2364 portScanError = message
2365 case let .error(message):
2317 case let .empty(message), let .error(message):
23662318 guard !Task.isCancelled, isCurrentLookup(lookupID) else { return }
23672319 portScanResults = []
23682320 portScanError = message
@@ -2377,10 +2329,7 @@ final class DomainViewModel {
23772329 case let .success(geolocation):
23782330 ipGeolocation = geolocation
23792331 ipGeolocationError = nil
2380 case let .empty(message):
2381 ipGeolocation = nil
2382 ipGeolocationError = message
2383 case let .error(message):
2332 case let .empty(message), let .error(message):
23842333 ipGeolocation = nil
23852334 ipGeolocationError = message
23862335 }
@@ -2408,10 +2357,7 @@ final class DomainViewModel {
24082357 customPortResults = results
24092358 customPortScanError = nil
24102359 _ = saveHistoryEntry(replaceLatest: true)
2411 case let .empty(message):
2412 customPortResults = []
2413 customPortScanError = message
2414 case let .error(message):
2360 case let .empty(message), let .error(message):
24152361 customPortResults = []
24162362 customPortScanError = message
24172363 }
@@ -4024,7 +3970,7 @@ final class DomainViewModel {
40243970
40253971 static func summaryFields(from snapshot: LookupSnapshot) -> [SummaryFieldViewData] {
40263972 [
4027 SummaryFieldViewData(label: "Domain", value: snapshot.domain.nonEmpty ?? "Unavailable", tone: .primary),
3973 SummaryFieldViewData(label: "Domain", value: snapshot.domain.nilIfEmpty ?? "Unavailable", tone: .primary),
40283974 SummaryFieldViewData(label: "Observed IP", value: primaryIPAddress(from: snapshot) ?? "Unavailable", tone: .primary),
40293975 SummaryFieldViewData(label: "Observed Redirect", value: finalRedirectTarget(from: snapshot) ?? "Unavailable", tone: .secondary),
40303976 SummaryFieldViewData(label: "Inference", value: availabilityInference(from: snapshot), tone: availabilityTone(snapshot.availabilityResult?.status)),
@@ -4780,10 +4726,7 @@ final class DomainViewModel {
47804726 case let .success(pricing):
47814727 domainPricing = pricing
47824728 domainPricingError = nil
4783 case let .empty(message):
4784 domainPricing = nil
4785 domainPricingError = conciseExternalMessage(message, fallback: "External pricing unavailable")
4786 case let .error(message):
4729 case let .empty(message), let .error(message):
47874730 domainPricing = nil
47884731 domainPricingError = conciseExternalMessage(message, fallback: "External pricing unavailable")
47894732 }
@@ -4803,10 +4746,7 @@ final class DomainViewModel {
48034746 case let .success(result):
48044747 reputation = result
48054748 reputationError = nil
4806 case let .empty(message):
4807 reputation = nil
4808 reputationError = conciseExternalMessage(message, fallback: "Reputation check unavailable")
4809 case let .error(message):
4749 case let .empty(message), let .error(message):
48104750 reputation = nil
48114751 reputationError = conciseExternalMessage(message, fallback: "Reputation check unavailable")
48124752 }
@@ -4865,10 +4805,6 @@ final class DomainViewModel {
48654805}
48664806
48674807private extension String {
4868 var nonEmpty: String? {
4869 isEmpty ? nil : self
4870 }
4871
48724808 var nilIfEmpty: String? {
48734809 isEmpty ? nil : self
48744810 }
DomainDig/HistoryView.swift +3 −3
@@ -115,7 +115,7 @@ struct HistoryView: View {
115115 Button("Clear All", role: .destructive) {
116116 viewModel.clearHistory()
117117 }
118 Button("Cancel", role: .cancel) {}
118 Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
119119 } message: {
120120 Text("This will delete all saved history entries. This cannot be undone.")
121121 }
@@ -314,7 +314,7 @@ struct HistoryDetailView: View {
314314 isCloudflareProxied: snapshot.httpHeaders.contains(where: { $0.name.lowercased() == "cf-ray" }),
315315 customPortsExpanded: .constant(false),
316316 customPortInput: .constant(""),
317 onScanCustomPorts: {}
317 onScanCustomPorts: { /* Historical snapshot; re-scanning is not offered here. */ }
318318 )
319319 .padding(.top, appDensity.metrics.sectionSpacing)
320320 }
@@ -344,7 +344,7 @@ struct HistoryDetailView: View {
344344 Button("Run with Snapshot Resolver") {
345345 viewModel.rerunLookup(from: entry, useSnapshotResolver: true)
346346 }
347 Button("Cancel", role: .cancel) {}
347 Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
348348 } message: {
349349 Text(viewModel.resolverMismatchNote(for: entry) ?? "Choose how to reproduce this snapshot.")
350350 }
DomainDig/IntegrationService.swift +7 −3
@@ -768,9 +768,7 @@ private final class SMTPChannel {
768768 connection.stateUpdateHandler = { [weak self] state in
769769 switch state {
770770 case .ready:
771 DispatchQueue.global(qos: .utility).async {
772 self?.startReceiveLoop()
773 }
771 self?.scheduleReceiveLoop()
774772 continuation.resume()
775773 case .failed(let error):
776774 continuation.resume(throwing: error)
@@ -782,6 +780,12 @@ private final class SMTPChannel {
782780 }
783781 }
784782
783 private func scheduleReceiveLoop() {
784 DispatchQueue.global(qos: .utility).async { [weak self] in
785 self?.startReceiveLoop()
786 }
787 }
788
785789 func cancel() {
786790 connection.cancel()
787791 }
DomainDig/LocalNotificationService.swift +6 −7
@@ -5,7 +5,7 @@ import UserNotifications
55final class LocalNotificationService {
66 static let shared = LocalNotificationService()
77
8 private init() {}
8 private init() { /* Singleton; use the shared instance. */ }
99
1010 static let domainUserInfoKey = "domain"
1111 static let domainCategoryIdentifier = "domain-event"
@@ -188,14 +188,14 @@ private final class NotificationCenterDelegate: NSObject, UNUserNotificationCent
188188 static let shared = NotificationCenterDelegate()
189189
190190 func userNotificationCenter(
191 _ center: UNUserNotificationCenter,
192 willPresent notification: UNNotification
191 _: UNUserNotificationCenter,
192 willPresent _: UNNotification
193193 ) async -> UNNotificationPresentationOptions {
194194 [.banner, .list, .sound]
195195 }
196196
197197 func userNotificationCenter(
198 _ center: UNUserNotificationCenter,
198 _: UNUserNotificationCenter,
199199 didReceive response: UNNotificationResponse
200200 ) async {
201201 let userInfo = response.notification.request.content.userInfo
@@ -204,10 +204,9 @@ private final class NotificationCenterDelegate: NSObject, UNUserNotificationCent
204204 else { return }
205205
206206 let action: DomainDigDeepLink.Action
207 switch response.actionIdentifier {
208 case LocalNotificationService.reinspectActionIdentifier:
207 if response.actionIdentifier == LocalNotificationService.reinspectActionIdentifier {
209208 action = .inspect(domain)
210 default:
209 } else {
211210 // Default tap: open the tracked domain's detail.
212211 action = .detail(domain)
213212 }
DomainDig/Models.swift +9 −5
@@ -244,8 +244,14 @@ struct DomainChangeSummary: Codable, Equatable {
244244 changedSections = try container.decodeIfPresent([String].self, forKey: .changedSections) ?? []
245245 generatedAt = try container.decode(Date.self, forKey: .generatedAt)
246246 severity = try container.decodeIfPresent(ChangeSeverity.self, forKey: .severity) ?? (hasChanges ? .medium : .low)
247 let defaultImpact: ChangeImpactClassification
248 if severity == .high {
249 defaultImpact = .critical
250 } else {
251 defaultImpact = hasChanges ? .warning : .informational
252 }
247253 impactClassification = try container.decodeIfPresent(ChangeImpactClassification.self, forKey: .impactClassification)
248 ?? (severity == .high ? .critical : (hasChanges ? .warning : .informational))
254 ?? defaultImpact
249255 message = try container.decodeIfPresent(String.self, forKey: .message)
250256 ?? (changedSections.isEmpty ? "No meaningful changes" : changedSections.joined(separator: " • "))
251257 observedFacts = try container.decodeIfPresent([String].self, forKey: .observedFacts) ?? []
@@ -562,12 +568,10 @@ enum DomainClassificationKind: String, Codable, CaseIterable, Sendable {
562568 case unknown
563569
564570 var title: String {
565 switch self {
566 case .staticSite:
571 if self == .staticSite {
567572 return "Static"
568 default:
569 return rawValue.capitalized
570573 }
574 return rawValue.capitalized
571575 }
572576}
573577
DomainDig/PortScanService.swift +21 −18
@@ -89,24 +89,7 @@ struct PortScanService {
8989 switch state {
9090 case .ready:
9191 connection.receive(minimumIncompleteLength: 1, maximumLength: 256) { data, _, _, error in
92 guard error == nil,
93 let data,
94 !data.isEmpty,
95 let rawBanner = String(data: data, encoding: .utf8) else {
96 context.finish(with: nil)
97 return
98 }
99
100 let printableBanner = rawBanner.filter { character in
101 guard let scalar = character.unicodeScalars.first,
102 character.unicodeScalars.count == 1 else {
103 return false
104 }
105 return (32...126).contains(scalar.value)
106 }
107
108 let banner = String(printableBanner.prefix(80))
109 context.finish(with: banner.isEmpty ? nil : banner)
92 context.finish(with: printableBanner(from: data, error: error))
11093 }
11194 case .failed, .cancelled:
11295 context.finish(with: nil)
@@ -123,6 +106,26 @@ struct PortScanService {
123106 }
124107 }
125108
109 private static func printableBanner(from data: Data?, error: Error?) -> String? {
110 guard error == nil,
111 let data,
112 !data.isEmpty,
113 let rawBanner = String(data: data, encoding: .utf8) else {
114 return nil
115 }
116
117 let printable = rawBanner.filter { character in
118 guard let scalar = character.unicodeScalars.first,
119 character.unicodeScalars.count == 1 else {
120 return false
121 }
122 return (32...126).contains(scalar.value)
123 }
124
125 let banner = String(printable.prefix(80))
126 return banner.isEmpty ? nil : banner
127 }
128
126129 private static func probe(domain: String, port: UInt16) async -> PortProbeResult {
127130 await probe(domain: domain, port: port, timeout: 1.5)
128131 }
DomainDig/PurchaseService.swift +2 −4
@@ -288,12 +288,10 @@ final class PurchaseService {
288288
289289 private func storeMessage(for error: Error, fallback: String) -> String {
290290 if let storeKitError = error as? StoreKitError {
291 switch storeKitError {
292 case .networkError:
291 if case .networkError = storeKitError {
293292 return "The App Store is offline right now."
294 default:
295 return fallback
296293 }
294 return fallback
297295 }
298296
299297 let message = error.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines)
DomainDig/SSLCheckService.swift +28 −32
@@ -156,10 +156,9 @@ private enum DERCertificateParser {
156156 var offset = tbsContent.contentStart
157157
158158 // Skip version (explicit tag [0]) if present
159 if offset < bytes.count && (bytes[offset] & 0xE0) == 0xA0 {
160 if let tagLen = readTagAndLength(bytes, offset: offset) {
161 offset = tagLen.contentStart + tagLen.length
162 }
159 if offset < bytes.count, (bytes[offset] & 0xE0) == 0xA0,
160 let tagLen = readTagAndLength(bytes, offset: offset) {
161 offset = tagLen.contentStart + tagLen.length
163162 }
164163
165164 // Skip serialNumber
@@ -199,11 +198,10 @@ private enum DERCertificateParser {
199198 // Extensions are in an explicit tag [3]
200199 while offset < tbsContent.contentStart + tbsContent.length {
201200 if bytes[offset] == 0xA3 {
202 if let extWrapper = readTagAndLength(bytes, offset: offset) {
203 // Inside is a SEQUENCE of SEQUENCE extensions
204 if let extsSeq = readTagAndLength(bytes, offset: extWrapper.contentStart) {
205 result.subjectAltNames = extractSANs(bytes, sequenceStart: extsSeq.contentStart, length: extsSeq.length)
206 }
201 // Inside the wrapper is a SEQUENCE of SEQUENCE extensions
202 if let extWrapper = readTagAndLength(bytes, offset: offset),
203 let extsSeq = readTagAndLength(bytes, offset: extWrapper.contentStart) {
204 result.subjectAltNames = extractSANs(bytes, sequenceStart: extsSeq.contentStart, length: extsSeq.length)
207205 }
208206 break
209207 }
@@ -267,27 +265,25 @@ private enum DERCertificateParser {
267265 if oidBytes == sanOID {
268266 var valuePos = oidTL.contentStart + oidTL.length
269267 // Skip optional critical BOOLEAN
270 if valuePos < extEnd && bytes[valuePos] == 0x01 {
271 if let boolTL = readTagAndLength(bytes, offset: valuePos) {
272 valuePos = boolTL.contentStart + boolTL.length
273 }
268 if valuePos < extEnd, bytes[valuePos] == 0x01,
269 let boolTL = readTagAndLength(bytes, offset: valuePos) {
270 valuePos = boolTL.contentStart + boolTL.length
274271 }
275272 // The value is an OCTET STRING wrapping a SEQUENCE of GeneralNames
276 if let octetTL = readTagAndLength(bytes, offset: valuePos) {
277 if let sanSeq = readTagAndLength(bytes, offset: octetTL.contentStart) {
278 let sanEnd = sanSeq.contentStart + sanSeq.length
279 var sanPos = sanSeq.contentStart
280 while sanPos < sanEnd {
281 guard let nameTL = readTagAndLength(bytes, offset: sanPos) else { break }
282 // Context tag [2] = dNSName (IA5String)
283 if (bytes[sanPos] & 0x1F) == 2 {
284 let nameBytes = bytes[nameTL.contentStart..<nameTL.contentStart + nameTL.length]
285 if let name = String(bytes: nameBytes, encoding: .ascii) {
286 sans.append(name)
287 }
273 if let octetTL = readTagAndLength(bytes, offset: valuePos),
274 let sanSeq = readTagAndLength(bytes, offset: octetTL.contentStart) {
275 let sanEnd = sanSeq.contentStart + sanSeq.length
276 var sanPos = sanSeq.contentStart
277 while sanPos < sanEnd {
278 guard let nameTL = readTagAndLength(bytes, offset: sanPos) else { break }
279 // Context tag [2] = dNSName (IA5String)
280 if (bytes[sanPos] & 0x1F) == 2 {
281 let nameBytes = bytes[nameTL.contentStart..<nameTL.contentStart + nameTL.length]
282 if let name = String(bytes: nameBytes, encoding: .ascii) {
283 sans.append(name)
288284 }
289 sanPos = nameTL.contentStart + nameTL.length
290285 }
286 sanPos = nameTL.contentStart + nameTL.length
291287 }
292288 }
293289 }
@@ -386,19 +382,19 @@ enum SSLError: LocalizedError {
386382
387383final class SSLSessionDelegate: NSObject, URLSessionDelegate, @unchecked Sendable {
388384 private let lock = NSLock()
389 private var _serverTrust: SecTrust?
390 private var _tlsMetadata: TLSMetadata?
385 private var storedServerTrust: SecTrust?
386 private var storedTLSMetadata: TLSMetadata?
391387
392388 var serverTrust: SecTrust? {
393389 lock.lock()
394390 defer { lock.unlock() }
395 return _serverTrust
391 return storedServerTrust
396392 }
397393
398394 fileprivate var tlsMetadata: TLSMetadata? {
399395 lock.lock()
400396 defer { lock.unlock() }
401 return _tlsMetadata
397 return storedTLSMetadata
402398 }
403399
404400 func urlSession(
@@ -413,7 +409,7 @@ final class SSLSessionDelegate: NSObject, URLSessionDelegate, @unchecked Sendabl
413409 }
414410
415411 lock.lock()
416 _serverTrust = trust
412 storedServerTrust = trust
417413 lock.unlock()
418414
419415 let credential = URLCredential(trust: trust)
@@ -439,7 +435,7 @@ extension SSLSessionDelegate: URLSessionTaskDelegate {
439435 }
440436
441437 lock.lock()
442 _tlsMetadata = TLSMetadata(tlsVersion: tlsVersion, cipherSuite: cipherSuite)
438 storedTLSMetadata = TLSMetadata(tlsVersion: tlsVersion, cipherSuite: cipherSuite)
443439 lock.unlock()
444440 }
445441
DomainDig/ScheduledReportService.swift +3 −3
@@ -106,10 +106,10 @@ struct ScheduledReportOutcome {
106106final class ScheduledReportService {
107107 static let shared = ScheduledReportService()
108108
109 private init() {}
109 private init() { /* Singleton; use the shared instance. */ }
110110
111111 @discardableResult
112 func generateReport(trigger: MonitoringRunTrigger, requireEnabledSetting: Bool) async -> ScheduledReportOutcome {
112 func generateReport(trigger _: MonitoringRunTrigger, requireEnabledSetting: Bool) async -> ScheduledReportOutcome {
113113 var settings = ScheduledReportStorage.loadSettings()
114114
115115 guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else {
@@ -191,7 +191,7 @@ final class ScheduledReportScheduler {
191191
192192 private var isRegistered = false
193193
194 private init() {}
194 private init() { /* Singleton; use the shared instance. */ }
195195
196196 func registerBackgroundTask() {
197197 #if canImport(BackgroundTasks)
DomainDig/SweepActivityController.swift +1 −1
@@ -8,7 +8,7 @@ final class SweepActivityController {
88
99 private var activity: Activity<SweepActivityAttributes>?
1010
11 private init() {}
11 private init() { /* Singleton; use the shared instance. */ }
1212
1313 func begin(title: String, total: Int) {
1414 guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
DomainDig/WatchlistView.swift +6 −6
@@ -169,13 +169,13 @@ struct WatchlistView: View {
169169 shareTrackedDomains(format: .pdf)
170170 }
171171 } else {
172 Button("CSV Export • Available in Pro") {}
172 Button("CSV Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
173173 .disabled(true)
174 Button("JSON Export • Available in Pro") {}
174 Button("JSON Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
175175 .disabled(true)
176 Button("Markdown Export • Available in Pro") {}
176 Button("Markdown Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
177177 .disabled(true)
178 Button("PDF Export • Available in Pro") {}
178 Button("PDF Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
179179 .disabled(true)
180180 }
181181 } label: {
@@ -250,7 +250,7 @@ struct WatchlistView: View {
250250 Button("Save") {
251251 viewModel.saveCurrentWatchlistView(name: newSavedViewName)
252252 }
253 Button("Cancel", role: .cancel) {}
253 Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
254254 } message: {
255255 Text("Saves the current tag, filter, and sort as a reusable preset.")
256256 }
@@ -736,7 +736,7 @@ struct TrackedDomainDetailView: View {
736736 viewModel.rerunInspection(for: liveTrackedDomain, useSnapshotResolver: true)
737737 }
738738 }
739 Button("Cancel", role: .cancel) {}
739 Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
740740 } message: {
741741 Text(viewModel.resolverMismatchNote(for: liveTrackedDomain) ?? "Choose how to reproduce the most recent snapshot.")
742742 }
DomainDig/WorkflowsView.swift +8 −8
@@ -253,13 +253,13 @@ struct WorkflowDetailView: View {
253253 shareWorkflowResults(format: .pdf)
254254 }
255255 } else {
256 Button("CSV Export • Available in Pro") {}
256 Button("CSV Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
257257 .disabled(true)
258 Button("JSON Export • Available in Pro") {}
258 Button("JSON Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
259259 .disabled(true)
260 Button("Markdown Export • Available in Pro") {}
260 Button("Markdown Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
261261 .disabled(true)
262 Button("PDF Export • Available in Pro") {}
262 Button("PDF Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
263263 .disabled(true)
264264 }
265265 } label: {
@@ -554,13 +554,13 @@ struct WorkflowRunSummaryView: View {
554554 share(format: .pdf)
555555 }
556556 } else {
557 Button("CSV Export • Available in Pro") {}
557 Button("CSV Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
558558 .disabled(true)
559 Button("JSON Export • Available in Pro") {}
559 Button("JSON Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
560560 .disabled(true)
561 Button("Markdown Export • Available in Pro") {}
561 Button("Markdown Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
562562 .disabled(true)
563 Button("PDF Export • Available in Pro") {}
563 Button("PDF Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
564564 .disabled(true)
565565 }
566566 } label: {
DomainDigWidget/DomainDigPortfolioWidget.swift +4 −5
@@ -7,7 +7,7 @@ struct DomainDigEntry: TimelineEntry {
77}
88
99struct DomainDigProvider: TimelineProvider {
10 func placeholder(in context: Context) -> DomainDigEntry {
10 func placeholder(in _: Context) -> DomainDigEntry {
1111 DomainDigEntry(date: Date(), data: .placeholder)
1212 }
1313
@@ -16,7 +16,7 @@ struct DomainDigProvider: TimelineProvider {
1616 completion(DomainDigEntry(date: Date(), data: data))
1717 }
1818
19 func getTimeline(in context: Context, completion: @escaping (Timeline<DomainDigEntry>) -> Void) {
19 func getTimeline(in _: Context, completion: @escaping (Timeline<DomainDigEntry>) -> Void) {
2020 let data = DomainDigWidgetStore.read() ?? .empty
2121 let entry = DomainDigEntry(date: Date(), data: data)
2222 // The app reloads timelines on foreground and on watchlist changes; this
@@ -49,10 +49,9 @@ struct DomainDigWidgetView: View {
4949 if data.totalDomains == 0 {
5050 emptyState
5151 } else {
52 switch family {
53 case .systemSmall:
52 if family == .systemSmall {
5453 smallView
55 default:
54 } else {
5655 mediumOrLargeView
5756 }
5857 }
DomainInspectionService.swift +12 −12
@@ -295,10 +295,10 @@ struct DomainInspectionService {
295295 let ptrRecord = mapOptionalServiceResult(ptrOutcome.map { normalizeErrors(in: $0.value) }, missingMessage: "No A record available")
296296 let geolocation = mapOptionalServiceResult(geoOutcome.map { normalizeErrors(in: $0.value) }, missingMessage: "No A record available")
297297 let availabilityConfidence = confidenceForAvailability(result: availability.value, provenance: provenanceBySection[.availability])
298 let ownershipConfidence = confidenceForOwnership(result: ownership.value, error: ownership.message)
299 let subdomainConfidence = confidenceForSubdomains(results: subdomains.value, error: subdomains.message)
300 let emailConfidence = confidenceForEmail(result: emailSecurity.value, error: emailSecurity.message)
301 let geolocationConfidence = confidenceForGeolocation(result: geolocation.value, error: geolocation.message)
298 let ownershipConfidence = confidenceForOwnership(result: ownership.value)
299 let subdomainConfidence = confidenceForSubdomains(results: subdomains.value)
300 let emailConfidence = confidenceForEmail(result: emailSecurity.value)
301 let geolocationConfidence = confidenceForGeolocation(result: geolocation.value)
302302 let validationIssues = validationIssues(for: normalizedDomain, snapshotTimestamp: startedAt, availability: availability.value, dnsSections: dnsSections.value, provenanceBySection: provenanceBySection)
303303
304304 let snapshot = LookupSnapshot(
@@ -580,21 +580,21 @@ struct DomainInspectionService {
580580 return .low
581581 }
582582
583 private func confidenceForOwnership(result: DomainOwnership?, error: String?) -> ConfidenceLevel {
584 guard let result else { return error == nil ? .low : .low }
583 private func confidenceForOwnership(result: DomainOwnership?) -> ConfidenceLevel {
584 guard let result else { return .low }
585585 let hasDirectRegistrationData = result.registrar != nil || result.createdDate != nil || result.expirationDate != nil
586586 return hasDirectRegistrationData ? .high : .medium
587587 }
588588
589 private func confidenceForSubdomains(results: [DiscoveredSubdomain], error: String?) -> ConfidenceLevel {
589 private func confidenceForSubdomains(results: [DiscoveredSubdomain]) -> ConfidenceLevel {
590590 if !results.isEmpty {
591591 return .medium
592592 }
593 return error == nil ? .low : .low
593 return .low
594594 }
595595
596 private func confidenceForEmail(result: EmailSecurityResult?, error: String?) -> ConfidenceLevel {
597 guard let result else { return error == nil ? .low : .low }
596 private func confidenceForEmail(result: EmailSecurityResult?) -> ConfidenceLevel {
597 guard let result else { return .low }
598598 let foundCount = [result.spf.found, result.dmarc.found, result.dkim.found, result.bimi.found, result.mtaSts?.txtFound == true]
599599 .filter { $0 }
600600 .count
@@ -607,8 +607,8 @@ struct DomainInspectionService {
607607 return .low
608608 }
609609
610 private func confidenceForGeolocation(result: IPGeolocation?, error: String?) -> ConfidenceLevel {
611 guard let result else { return error == nil ? .low : .low }
610 private func confidenceForGeolocation(result: IPGeolocation?) -> ConfidenceLevel {
611 guard let result else { return .low }
612612 if result.city != nil && result.country_name != nil && result.latitude != nil && result.longitude != nil {
613613 return .high
614614 }
DomainReportExporter.swift +1 −1
@@ -11,8 +11,8 @@ enum DomainExportFormat: String, CaseIterable, Identifiable, Codable {
1111 case markdown = "md"
1212 case pdf = "pdf"
1313
14 var id: String { rawValue }
1514 var fileExtension: String { rawValue }
15 var id: String { fileExtension }
1616
1717 var title: String {
1818 switch self {
RELEASE_ROADMAP.md +32 −1
@@ -6,7 +6,7 @@ ports, geolocation, subdomains, availability). The next several releases invest
66in *reach and surfacing* — getting that data onto more iOS surfaces and into more
77workflows — rather than adding raw protocol checks.
88
9Current version: `v4.8.2`.
9Current version: `v4.8.3`.
1010
1111## v4.4.1 Patch: Release Readiness — ✅ shipped
1212
@@ -154,6 +154,37 @@ Goal: close the UAT follow-ups and make failures legible instead of silent.
154154 synced against App Store Connect. Registered in the project without target
155155 membership so it is not bundled into shipping builds.
156156
157## v4.8.3 Patch: Static Analysis Cleanup — ✅ shipped
158
159Goal: clear the SonarCloud new-code backlog without changing behavior.
160
161- **Dead confidence conditionals fixed** (4 bugs) —
162 `DomainInspectionService`'s `confidenceFor*` helpers each returned
163 `error == nil ? .low : .low`. The conditional was inert, so the unused `error`
164 parameter was dropped alongside it.
165- **Identical switch branches merged** — 14 sites in `DomainViewModel` handled
166 `.empty(message)` and `.error(message)` with byte-identical bodies; they now
167 share one `case let .empty(message), let .error(message):`.
168- **Duplicate implementations consolidated** — `clearPresentedResults()` now
169 delegates to `reset()`, `String.nonEmpty` was folded into `nilIfEmpty`, and
170 `ExportFormat.id` derives from `fileExtension`.
171- **Nested ternaries extracted** — grade-to-tone and impact-to-color mappings
172 became `TLSGrade.tone`, `EmailSecurityGrade.tone`, and
173 `ChangeImpactClassification.color`, replacing `ContentView`'s private
174 `impactColor` and the duplicate mapping in `BatchResultsView`.
175- **Remaining smells** — empty closures and singleton inits documented, unused
176 protocol-conformance parameters marked `_`, `CloudSyncTrigger.import` renamed
177 to `imported` (raw value preserved), `_serverTrust`/`_tlsMetadata` renamed,
178 nested `if`s merged in the DER parser, and deep closure nesting flattened in
179 `PortScanService` and `IntegrationService`.
180
181Left open deliberately: `swift:S107` (initializer parameter counts on model
182memberwise inits), `swift:S115` (constants mirroring DoH/ipapi JSON keys),
183`swift:S1075` (false positives on `https://` literals), and two `swift:S117`
184hits on SwiftUI `$binding` shorthand in `AuditModeView`, which cannot be
185renamed. These want a *Won't Fix* / *Safe* resolution in SonarCloud rather than
186a code change.
187
157188## v5.0.0 Major: Contract Stabilization & Engineering Health
158189
159190Goal: earn long-term compatibility promises — and pay down the debt that the