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 @@
497 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 497 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
498 CODE_SIGN_ENTITLEMENTS = DomainDig/DomainDig.entitlements; 498 CODE_SIGN_ENTITLEMENTS = DomainDig/DomainDig.entitlements;
499 CODE_SIGN_STYLE = Automatic; 499 CODE_SIGN_STYLE = Automatic;
500 CURRENT_PROJECT_VERSION = 42; 500 CURRENT_PROJECT_VERSION = 43;
501 DEVELOPMENT_TEAM = ZCNAX3VL9D; 501 DEVELOPMENT_TEAM = ZCNAX3VL9D;
502 ENABLE_PREVIEWS = YES; 502 ENABLE_PREVIEWS = YES;
503 GENERATE_INFOPLIST_FILE = YES; 503 GENERATE_INFOPLIST_FILE = YES;
@@ -514,7 +514,7 @@
514 "$(inherited)", 514 "$(inherited)",
515 "@executable_path/Frameworks", 515 "@executable_path/Frameworks",
516 ); 516 );
517 MARKETING_VERSION = 4.8.2; 517 MARKETING_VERSION = 4.8.3;
518 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig; 518 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig;
519 PRODUCT_NAME = "$(TARGET_NAME)"; 519 PRODUCT_NAME = "$(TARGET_NAME)";
520 STRING_CATALOG_GENERATE_SYMBOLS = YES; 520 STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -534,7 +534,7 @@
534 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 534 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
535 CODE_SIGN_ENTITLEMENTS = DomainDig/DomainDig.entitlements; 535 CODE_SIGN_ENTITLEMENTS = DomainDig/DomainDig.entitlements;
536 CODE_SIGN_STYLE = Automatic; 536 CODE_SIGN_STYLE = Automatic;
537 CURRENT_PROJECT_VERSION = 42; 537 CURRENT_PROJECT_VERSION = 43;
538 DEVELOPMENT_TEAM = ZCNAX3VL9D; 538 DEVELOPMENT_TEAM = ZCNAX3VL9D;
539 ENABLE_PREVIEWS = YES; 539 ENABLE_PREVIEWS = YES;
540 GENERATE_INFOPLIST_FILE = YES; 540 GENERATE_INFOPLIST_FILE = YES;
@@ -551,7 +551,7 @@
551 "$(inherited)", 551 "$(inherited)",
552 "@executable_path/Frameworks", 552 "@executable_path/Frameworks",
553 ); 553 );
554 MARKETING_VERSION = 4.8.2; 554 MARKETING_VERSION = 4.8.3;
555 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig; 555 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig;
556 PRODUCT_NAME = "$(TARGET_NAME)"; 556 PRODUCT_NAME = "$(TARGET_NAME)";
557 STRING_CATALOG_GENERATE_SYMBOLS = YES; 557 STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -570,7 +570,7 @@
570 APPLICATION_EXTENSION_API_ONLY = YES; 570 APPLICATION_EXTENSION_API_ONLY = YES;
571 CODE_SIGN_ENTITLEMENTS = DomainDigWidget/DomainDigWidget.entitlements; 571 CODE_SIGN_ENTITLEMENTS = DomainDigWidget/DomainDigWidget.entitlements;
572 CODE_SIGN_STYLE = Automatic; 572 CODE_SIGN_STYLE = Automatic;
573 CURRENT_PROJECT_VERSION = 42; 573 CURRENT_PROJECT_VERSION = 43;
574 DEVELOPMENT_TEAM = ZCNAX3VL9D; 574 DEVELOPMENT_TEAM = ZCNAX3VL9D;
575 GENERATE_INFOPLIST_FILE = NO; 575 GENERATE_INFOPLIST_FILE = NO;
576 INFOPLIST_FILE = DomainDigWidget/Info.plist; 576 INFOPLIST_FILE = DomainDigWidget/Info.plist;
@@ -581,7 +581,7 @@
581 "@executable_path/Frameworks", 581 "@executable_path/Frameworks",
582 "@executable_path/../../Frameworks", 582 "@executable_path/../../Frameworks",
583 ); 583 );
584 MARKETING_VERSION = 4.8.2; 584 MARKETING_VERSION = 4.8.3;
585 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig.DomainDigWidget; 585 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig.DomainDigWidget;
586 PRODUCT_NAME = "$(TARGET_NAME)"; 586 PRODUCT_NAME = "$(TARGET_NAME)";
587 SKIP_INSTALL = YES; 587 SKIP_INSTALL = YES;
@@ -599,7 +599,7 @@
599 APPLICATION_EXTENSION_API_ONLY = YES; 599 APPLICATION_EXTENSION_API_ONLY = YES;
600 CODE_SIGN_ENTITLEMENTS = DomainDigWidget/DomainDigWidget.entitlements; 600 CODE_SIGN_ENTITLEMENTS = DomainDigWidget/DomainDigWidget.entitlements;
601 CODE_SIGN_STYLE = Automatic; 601 CODE_SIGN_STYLE = Automatic;
602 CURRENT_PROJECT_VERSION = 42; 602 CURRENT_PROJECT_VERSION = 43;
603 DEVELOPMENT_TEAM = ZCNAX3VL9D; 603 DEVELOPMENT_TEAM = ZCNAX3VL9D;
604 GENERATE_INFOPLIST_FILE = NO; 604 GENERATE_INFOPLIST_FILE = NO;
605 INFOPLIST_FILE = DomainDigWidget/Info.plist; 605 INFOPLIST_FILE = DomainDigWidget/Info.plist;
@@ -610,7 +610,7 @@
610 "@executable_path/Frameworks", 610 "@executable_path/Frameworks",
611 "@executable_path/../../Frameworks", 611 "@executable_path/../../Frameworks",
612 ); 612 );
613 MARKETING_VERSION = 4.8.2; 613 MARKETING_VERSION = 4.8.3;
614 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig.DomainDigWidget; 614 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig.DomainDigWidget;
615 PRODUCT_NAME = "$(TARGET_NAME)"; 615 PRODUCT_NAME = "$(TARGET_NAME)";
616 SKIP_INSTALL = YES; 616 SKIP_INSTALL = YES;
@@ -628,7 +628,7 @@
628 APPLICATION_EXTENSION_API_ONLY = YES; 628 APPLICATION_EXTENSION_API_ONLY = YES;
629 CODE_SIGN_ENTITLEMENTS = DomainDigShareExtension/DomainDigShareExtension.entitlements; 629 CODE_SIGN_ENTITLEMENTS = DomainDigShareExtension/DomainDigShareExtension.entitlements;
630 CODE_SIGN_STYLE = Automatic; 630 CODE_SIGN_STYLE = Automatic;
631 CURRENT_PROJECT_VERSION = 42; 631 CURRENT_PROJECT_VERSION = 43;
632 DEVELOPMENT_TEAM = ZCNAX3VL9D; 632 DEVELOPMENT_TEAM = ZCNAX3VL9D;
633 GENERATE_INFOPLIST_FILE = NO; 633 GENERATE_INFOPLIST_FILE = NO;
634 INFOPLIST_FILE = DomainDigShareExtension/Info.plist; 634 INFOPLIST_FILE = DomainDigShareExtension/Info.plist;
@@ -639,7 +639,7 @@
639 "@executable_path/Frameworks", 639 "@executable_path/Frameworks",
640 "@executable_path/../../Frameworks", 640 "@executable_path/../../Frameworks",
641 ); 641 );
642 MARKETING_VERSION = 4.8.2; 642 MARKETING_VERSION = 4.8.3;
643 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig.DomainDigShareExtension; 643 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig.DomainDigShareExtension;
644 PRODUCT_NAME = "$(TARGET_NAME)"; 644 PRODUCT_NAME = "$(TARGET_NAME)";
645 SKIP_INSTALL = YES; 645 SKIP_INSTALL = YES;
@@ -657,7 +657,7 @@
657 APPLICATION_EXTENSION_API_ONLY = YES; 657 APPLICATION_EXTENSION_API_ONLY = YES;
658 CODE_SIGN_ENTITLEMENTS = DomainDigShareExtension/DomainDigShareExtension.entitlements; 658 CODE_SIGN_ENTITLEMENTS = DomainDigShareExtension/DomainDigShareExtension.entitlements;
659 CODE_SIGN_STYLE = Automatic; 659 CODE_SIGN_STYLE = Automatic;
660 CURRENT_PROJECT_VERSION = 42; 660 CURRENT_PROJECT_VERSION = 43;
661 DEVELOPMENT_TEAM = ZCNAX3VL9D; 661 DEVELOPMENT_TEAM = ZCNAX3VL9D;
662 GENERATE_INFOPLIST_FILE = NO; 662 GENERATE_INFOPLIST_FILE = NO;
663 INFOPLIST_FILE = DomainDigShareExtension/Info.plist; 663 INFOPLIST_FILE = DomainDigShareExtension/Info.plist;
@@ -668,7 +668,7 @@
668 "@executable_path/Frameworks", 668 "@executable_path/Frameworks",
669 "@executable_path/../../Frameworks", 669 "@executable_path/../../Frameworks",
670 ); 670 );
671 MARKETING_VERSION = 4.8.2; 671 MARKETING_VERSION = 4.8.3;
672 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig.DomainDigShareExtension; 672 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig.DomainDigShareExtension;
673 PRODUCT_NAME = "$(TARGET_NAME)"; 673 PRODUCT_NAME = "$(TARGET_NAME)";
674 SKIP_INSTALL = YES; 674 SKIP_INSTALL = YES;
DomainDig/AppVersion.swift +1 −1
@@ -2,6 +2,6 @@ import Foundation
2 2
3enum AppVersion { 3enum AppVersion {
4 nonisolated static var current: String { 4 nonisolated static var current: String {
5 "4.8.2" 5 "4.8.3"
6 } 6 }
7} 7}
DomainDig/AuditModeView.swift +1 −1
@@ -57,7 +57,7 @@ struct AuditModeView: View {
57 } 57 }
58 } 58 }
59 .alert("Audit Export", isPresented: Binding(get: { exportMessage != nil }, set: { if !$0 { exportMessage = nil } })) { 59 .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. */ }
61 } message: { 61 } message: {
62 Text(exportMessage ?? "") 62 Text(exportMessage ?? "")
63 } 63 }
DomainDig/BatchResultsView.swift +1 −1
@@ -104,7 +104,7 @@ struct BatchResultRowView: View {
104 if let changeClassification = result.changeClassification { 104 if let changeClassification = result.changeClassification {
105 Text("Impact: \(changeClassification.title)") 105 Text("Impact: \(changeClassification.title)")
106 .font(appDensity.font(.caption2)) 106 .font(appDensity.font(.caption2))
107 .foregroundStyle(changeClassification == .critical ? .red : (changeClassification == .warning ? .yellow : .secondary)) 107 .foregroundStyle(changeClassification.color)
108 } 108 }
109 109
110 if let errorMessage = result.errorMessage { 110 if let errorMessage = result.errorMessage {
DomainDig/CloudSharingLifecycle.swift +4 −4
@@ -3,9 +3,9 @@ import UIKit
3 3
4final class DomainDigAppDelegate: NSObject, UIApplicationDelegate { 4final class DomainDigAppDelegate: NSObject, UIApplicationDelegate {
5 func application( 5 func application(
6 _ application: UIApplication, 6 _: UIApplication,
7 configurationForConnecting connectingSceneSession: UISceneSession, 7 configurationForConnecting connectingSceneSession: UISceneSession,
8 options: UIScene.ConnectionOptions 8 options _: UIScene.ConnectionOptions
9 ) -> UISceneConfiguration { 9 ) -> UISceneConfiguration {
10 let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) 10 let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
11 configuration.delegateClass = DomainDigSceneDelegate.self 11 configuration.delegateClass = DomainDigSceneDelegate.self
@@ -14,12 +14,12 @@ final class DomainDigAppDelegate: NSObject, UIApplicationDelegate {
14} 14}
15 15
16final class DomainDigSceneDelegate: NSObject, UIWindowSceneDelegate { 16final 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) {
18 guard let metadata = connectionOptions.cloudKitShareMetadata else { return } 18 guard let metadata = connectionOptions.cloudKitShareMetadata else { return }
19 accept(metadata: metadata) 19 accept(metadata: metadata)
20 } 20 }
21 21
22 func windowScene(_ windowScene: UIWindowScene, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) { 22 func windowScene(_: UIWindowScene, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {
23 accept(metadata: cloudKitShareMetadata) 23 accept(metadata: cloudKitShareMetadata)
24 } 24 }
25 25
DomainDig/CloudSharingSheet.swift +2 −2
@@ -13,7 +13,7 @@ struct CloudSharingSheet: UIViewControllerRepresentable {
13 Coordinator(dismiss: dismiss, title: title) 13 Coordinator(dismiss: dismiss, title: title)
14 } 14 }
15 15
16 func makeUIViewController(context: Context) -> UIViewController { 16 func makeUIViewController(context _: Context) -> UIViewController {
17 UIViewController() 17 UIViewController()
18 } 18 }
19 19
@@ -77,7 +77,7 @@ struct CloudSharingSheet: UIViewControllerRepresentable {
77 } 77 }
78 } 78 }
79 79
80 func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { 80 func presentationControllerDidDismiss(_: UIPresentationController) {
81 dismissAction() 81 dismissAction()
82 } 82 }
83 } 83 }
DomainDig/CloudSyncService.swift +2 −2
@@ -40,7 +40,7 @@ enum CloudSyncTrigger: String {
40 case launch 40 case launch
41 case automatic 41 case automatic
42 case manual 42 case manual
43 case `import` 43 case imported = "import"
44} 44}
45 45
46enum ShareableEntity: Identifiable, Hashable { 46enum ShareableEntity: Identifiable, Hashable {
@@ -1186,7 +1186,7 @@ final class CloudSyncService {
1186 } 1186 }
1187 1187
1188 private func collaborationMetadata( 1188 private func collaborationMetadata(
1189 for record: CKRecord, 1189 for _: CKRecord,
1190 share: CKShare?, 1190 share: CKShare?,
1191 scope: SyncDatabaseScope 1191 scope: SyncDatabaseScope
1192 ) -> CollaborationMetadata { 1192 ) -> CollaborationMetadata {
DomainDig/ContentView.swift +56 −31
@@ -569,13 +569,13 @@ struct ContentView: View {
569 shareSingleResults(format: .pdf) 569 shareSingleResults(format: .pdf)
570 } 570 }
571 } else { 571 } else {
572 Button("CSV Export • Available in Pro") {} 572 Button("CSV Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
573 .disabled(true) 573 .disabled(true)
574 Button("JSON Export • Available in Pro") {} 574 Button("JSON Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
575 .disabled(true) 575 .disabled(true)
576 Button("Markdown Export • Available in Pro") {} 576 Button("Markdown Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
577 .disabled(true) 577 .disabled(true)
578 Button("PDF Export • Available in Pro") {} 578 Button("PDF Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
579 .disabled(true) 579 .disabled(true)
580 } 580 }
581 } label: { 581 } label: {
@@ -621,13 +621,13 @@ struct ContentView: View {
621 shareBatchResults(format: .pdf) 621 shareBatchResults(format: .pdf)
622 } 622 }
623 } else { 623 } else {
624 Button("Batch CSV • Available in Pro") {} 624 Button("Batch CSV • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
625 .disabled(true) 625 .disabled(true)
626 Button("Batch JSON • Available in Pro") {} 626 Button("Batch JSON • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
627 .disabled(true) 627 .disabled(true)
628 Button("Batch Markdown • Available in Pro") {} 628 Button("Batch Markdown • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
629 .disabled(true) 629 .disabled(true)
630 Button("Batch PDF • Available in Pro") {} 630 Button("Batch PDF • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
631 .disabled(true) 631 .disabled(true)
632 } 632 }
633 } label: { 633 } label: {
@@ -1040,10 +1040,10 @@ struct DomainChangeSummaryView: View {
1040 .clipShape(Capsule()) 1040 .clipShape(Capsule())
1041 Text(summary.impactClassification.title.uppercased()) 1041 Text(summary.impactClassification.title.uppercased())
1042 .font(appDensity.font(.caption2)) 1042 .font(appDensity.font(.caption2))
1043 .foregroundStyle(impactColor(summary.impactClassification)) 1043 .foregroundStyle(summary.impactClassification.color)
1044 .padding(.horizontal, 8) 1044 .padding(.horizontal, 8)
1045 .padding(.vertical, 4) 1045 .padding(.vertical, 4)
1046 .background(impactColor(summary.impactClassification).opacity(0.16)) 1046 .background(summary.impactClassification.color.opacity(0.16))
1047 .clipShape(Capsule()) 1047 .clipShape(Capsule())
1048 Text(summary.generatedAt, style: .time) 1048 Text(summary.generatedAt, style: .time)
1049 .font(appDensity.font(.caption2)) 1049 .font(appDensity.font(.caption2))
@@ -1107,16 +1107,6 @@ struct DomainChangeSummaryView: View {
1107 } 1107 }
1108 } 1108 }
1109 1109
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 }
1120} 1110}
1121 1111
1122struct DomainDiffView: View { 1112struct DomainDiffView: View {
@@ -2008,7 +1998,7 @@ struct WebSectionView: View {
2008 } 1998 }
2009 SectionTrustMetadataView(provenance: tlsProvenance, confidence: nil) 1999 SectionTrustMetadataView(provenance: tlsProvenance, confidence: nil)
2010 if !sslLoading, let tlsSummary { 2000 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))
2012 ForEach(Array(tlsSummary.tlsHighlights.enumerated()), id: \.offset) { _, highlight in 2002 ForEach(Array(tlsSummary.tlsHighlights.enumerated()), id: \.offset) { _, highlight in
2013 MessageRowView(text: highlight, isError: isTLSHighlightError(highlight)) 2003 MessageRowView(text: highlight, isError: isTLSHighlightError(highlight))
2014 } 2004 }
@@ -2153,7 +2143,7 @@ struct EmailSectionView: View {
2153 .opacity(loading ? 0 : 1) 2143 .opacity(loading ? 0 : 1)
2154 } 2144 }
2155 if let assessment, let grade = assessment.grade { 2145 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))
2157 if !assessment.reasons.isEmpty { 2147 if !assessment.reasons.isEmpty {
2158 Text(assessment.reasons.joined(separator: " | ")) 2148 Text(assessment.reasons.joined(separator: " | "))
2159 .font(appDensity.font(.caption2)) 2149 .font(appDensity.font(.caption2))
@@ -2897,6 +2887,11 @@ private struct LocalAPISettingsView: View {
2897 @State private var localAPIService = LocalAPIService.shared 2887 @State private var localAPIService = LocalAPIService.shared
2898 @State private var portText = "" 2888 @State private var portText = ""
2899 2889
2890 private var statusText: String {
2891 if localAPIService.isRunning { return "Running" }
2892 return localAPIService.config.isEnabled ? "Stopped" : "Disabled"
2893 }
2894
2900 var body: some View { 2895 var body: some View {
2901 Form { 2896 Form {
2902 Section("Local API") { 2897 Section("Local API") {
@@ -2923,7 +2918,7 @@ private struct LocalAPISettingsView: View {
2923 .keyboardType(.numberPad) 2918 .keyboardType(.numberPad)
2924 2919
2925 LabeledContent("Address", value: localAPIService.address) 2920 LabeledContent("Address", value: localAPIService.address)
2926 LabeledContent("Status", value: localAPIService.isRunning ? "Running" : (localAPIService.config.isEnabled ? "Stopped" : "Disabled")) 2921 LabeledContent("Status", value: statusText)
2927 LabeledContent("Token", value: localAPIService.maskedToken) 2922 LabeledContent("Token", value: localAPIService.maskedToken)
2928 2923
2929 if let statusMessage = localAPIService.statusMessage { 2924 if let statusMessage = localAPIService.statusMessage {
@@ -3339,7 +3334,7 @@ private struct DataPortabilitySettingsView: View {
3339 Button("Replace", role: .destructive) { 3334 Button("Replace", role: .destructive) {
3340 applyPendingImport() 3335 applyPendingImport()
3341 } 3336 }
3342 Button("Cancel", role: .cancel) {} 3337 Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
3343 } message: { 3338 } message: {
3344 Text("Replace mode overwrites local data covered by the imported file and may remove items that are only on this device.") 3339 Text("Replace mode overwrites local data covered by the imported file and may remove items that are only on this device.")
3345 } 3340 }
@@ -3347,7 +3342,7 @@ private struct DataPortabilitySettingsView: View {
3347 get: { pendingImportError != nil }, 3342 get: { pendingImportError != nil },
3348 set: { if !$0 { pendingImportError = nil } } 3343 set: { if !$0 { pendingImportError = nil } }
3349 )) { 3344 )) {
3350 Button("OK", role: .cancel) {} 3345 Button("OK", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
3351 } message: { 3346 } message: {
3352 Text(pendingImportError ?? "The import could not be completed.") 3347 Text(pendingImportError ?? "The import could not be completed.")
3353 } 3348 }
@@ -3585,7 +3580,7 @@ private struct DataManagementSettingsView: View {
3585 Button("Clear", role: .destructive) { 3580 Button("Clear", role: .destructive) {
3586 viewModel.clearHistory() 3581 viewModel.clearHistory()
3587 } 3582 }
3588 Button("Cancel", role: .cancel) {} 3583 Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
3589 } message: { 3584 } message: {
3590 Text("This removes saved lookup snapshots and clears monitoring run history on this device.") 3585 Text("This removes saved lookup snapshots and clears monitoring run history on this device.")
3591 } 3586 }
@@ -3593,7 +3588,7 @@ private struct DataManagementSettingsView: View {
3593 Button("Clear", role: .destructive) { 3588 Button("Clear", role: .destructive) {
3594 viewModel.clearLookupCache() 3589 viewModel.clearLookupCache()
3595 } 3590 }
3596 Button("Cancel", role: .cancel) {} 3591 Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
3597 } message: { 3592 } message: {
3598 Text("This clears the in-memory lookup cache and cancels any cached in-flight work.") 3593 Text("This clears the in-memory lookup cache and cancels any cached in-flight work.")
3599 } 3594 }
@@ -3601,7 +3596,7 @@ private struct DataManagementSettingsView: View {
3601 Button("Clear", role: .destructive) { 3596 Button("Clear", role: .destructive) {
3602 viewModel.clearWorkflows() 3597 viewModel.clearWorkflows()
3603 } 3598 }
3604 Button("Cancel", role: .cancel) {} 3599 Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
3605 } message: { 3600 } message: {
3606 Text("This removes saved workflows only. History, tracked domains, and saved reports stay intact.") 3601 Text("This removes saved workflows only. History, tracked domains, and saved reports stay intact.")
3607 } 3602 }
@@ -3609,12 +3604,12 @@ private struct DataManagementSettingsView: View {
3609 Button("Clear", role: .destructive) { 3604 Button("Clear", role: .destructive) {
3610 viewModel.clearTrackedDomains() 3605 viewModel.clearTrackedDomains()
3611 } 3606 }
3612 Button("Cancel", role: .cancel) {} 3607 Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
3613 } message: { 3608 } message: {
3614 Text("This removes the watchlist and clears monitoring run history. History and workflows stay intact.") 3609 Text("This removes the watchlist and clears monitoring run history. History and workflows stay intact.")
3615 } 3610 }
3616 .alert("Delete All Data?", isPresented: $showDeleteAllConfirmation) { 3611 .alert("Delete All Data?", isPresented: $showDeleteAllConfirmation) {
3617 Button("Cancel", role: .cancel) {} 3612 Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
3618 Button("Delete All Data", role: .destructive) { 3613 Button("Delete All Data", role: .destructive) {
3619 deleteAllData() 3614 deleteAllData()
3620 } 3615 }
@@ -3625,7 +3620,7 @@ private struct DataManagementSettingsView: View {
3625 get: { deleteAllErrorMessage != nil }, 3620 get: { deleteAllErrorMessage != nil },
3626 set: { if !$0 { deleteAllErrorMessage = nil } } 3621 set: { if !$0 { deleteAllErrorMessage = nil } }
3627 )) { 3622 )) {
3628 Button("OK", role: .cancel) {} 3623 Button("OK", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
3629 } message: { 3624 } message: {
3630 Text(deleteAllErrorMessage ?? "The local data reset could not be completed.") 3625 Text(deleteAllErrorMessage ?? "The local data reset could not be completed.")
3631 } 3626 }
@@ -3745,6 +3740,36 @@ private struct DataImportPreviewSheet: View {
3745 } 3740 }
3746} 3741}
3747 3742
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
3748#Preview { 3773#Preview {
3749 ContentView(viewModel: DomainViewModel()) 3774 ContentView(viewModel: DomainViewModel())
3750} 3775}
DomainDig/DashboardView.swift +5 −1
@@ -239,22 +239,26 @@ struct DashboardView: View {
239 let warningCount = states.filter { $0.health == .warning }.count 239 let warningCount = states.filter { $0.health == .warning }.count
240 let title: String 240 let title: String
241 let color: Color 241 let color: Color
242 let systemImage: String
242 243
243 if criticalCount > 0 { 244 if criticalCount > 0 {
244 title = "\(criticalCount) critical" 245 title = "\(criticalCount) critical"
245 color = .red 246 color = .red
247 systemImage = "exclamationmark.octagon.fill"
246 } else if warningCount > 0 { 248 } else if warningCount > 0 {
247 title = "\(warningCount) warning" 249 title = "\(warningCount) warning"
248 color = .yellow 250 color = .yellow
251 systemImage = "exclamationmark.triangle.fill"
249 } else { 252 } else {
250 title = "Healthy" 253 title = "Healthy"
251 color = .green 254 color = .green
255 systemImage = "checkmark.circle.fill"
252 } 256 }
253 257
254 return AppStatusBadgeView( 258 return AppStatusBadgeView(
255 model: .init( 259 model: .init(
256 title: title, 260 title: title,
257 systemImage: criticalCount > 0 ? "exclamationmark.octagon.fill" : (warningCount > 0 ? "exclamationmark.triangle.fill" : "checkmark.circle.fill"), 261 systemImage: systemImage,
258 foregroundColor: color, 262 foregroundColor: color,
259 backgroundColor: color.opacity(0.16) 263 backgroundColor: color.opacity(0.16)
260 ) 264 )
DomainDig/DomainAvailabilityService.swift +1
@@ -47,6 +47,7 @@ struct DomainAvailabilityService {
47 return .registered 47 return .registered
48 } 48 }
49 } catch { 49 } catch {
50 // No A record (or the lookup failed); fall through to the NS probe.
50 } 51 }
51 52
52 do { 53 do {
DomainDig/DomainDigIntents.swift +1 −1
@@ -149,7 +149,7 @@ struct RunSweepIntent: AppIntent {
149final class DomainDigIntentRouter { 149final class DomainDigIntentRouter {
150 static let shared = DomainDigIntentRouter() 150 static let shared = DomainDigIntentRouter()
151 var pendingAction: DomainDigDeepLink.Action? 151 var pendingAction: DomainDigDeepLink.Action?
152 private init() {} 152 private init() { /* Singleton; use the shared instance. */ }
153} 153}
154 154
155/// Exposes DomainDig intents to Spotlight and Siri with invocation phrases. 155/// Exposes DomainDig intents to Spotlight and Siri with invocation phrases.
DomainDig/DomainInsightEngine.swift +9 −2
@@ -412,7 +412,7 @@ enum DomainInsightEngine {
412 if hasMXRecords(snapshot), snapshot.emailSecurity == nil { 412 if hasMXRecords(snapshot), snapshot.emailSecurity == nil {
413 patterns.append("MX records exist without corresponding email security records") 413 patterns.append("MX records exist without corresponding email security records")
414 } 414 }
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 {
416 patterns.append("TLS is active but no CAA record was found") 416 patterns.append("TLS is active but no CAA record was found")
417 } 417 }
418 418
@@ -427,9 +427,16 @@ enum DomainInsightEngine {
427 let dmarcFound = result.dmarc.found 427 let dmarcFound = result.dmarc.found
428 let dmarcStrict = isStrictDMARC(result.dmarc.value) 428 let dmarcStrict = isStrictDMARC(result.dmarc.value)
429 429
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
430 let reasons = [ 437 let reasons = [
431 spfFound ? "SPF present" : "SPF missing", 438 spfFound ? "SPF present" : "SPF missing",
432 dmarcFound ? (dmarcStrict ? "DMARC policy is strict" : "DMARC policy is not strict") : "DMARC missing", 439 dmarcReason,
433 dkimFound ? "DKIM present" : "DKIM not detected" 440 dkimFound ? "DKIM present" : "DKIM not detected"
434 ] 441 ]
435 442
DomainDig/DomainMonitoringService.swift +3 −3
@@ -85,7 +85,7 @@ final class DomainMonitoringScheduler {
85 85
86 private var isRegistered = false 86 private var isRegistered = false
87 87
88 private init() {} 88 private init() { /* Singleton; use the shared instance. */ }
89 89
90 func registerBackgroundTask() { 90 func registerBackgroundTask() {
91 #if canImport(BackgroundTasks) 91 #if canImport(BackgroundTasks)
@@ -613,8 +613,8 @@ final class DomainMonitoringService {
613 613
614 private func processAdaptiveMonitoringState( 614 private func processAdaptiveMonitoringState(
615 trackedDomain: TrackedDomain, 615 trackedDomain: TrackedDomain,
616 snapshot: LookupSnapshot, 616 snapshot _: LookupSnapshot,
617 previousSnapshot: LookupSnapshot?, 617 previousSnapshot _: LookupSnapshot?,
618 alertDescriptor: MonitoringAlertDescriptor?, 618 alertDescriptor: MonitoringAlertDescriptor?,
619 settings: MonitoringSettings, 619 settings: MonitoringSettings,
620 notificationsAuthorized: Bool, 620 notificationsAuthorized: Bool,
DomainDig/DomainViewModel.swift +17 −81
@@ -1280,19 +1280,7 @@ final class DomainViewModel {
1280 } 1280 }
1281 1281
1282 func clearPresentedResults() { 1282 func clearPresentedResults() {
1283 lookupTask?.cancel() 1283 reset()
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()
1296 } 1284 }
1297 1285
1298 func run() { 1286 func run() {
@@ -1857,7 +1845,7 @@ final class DomainViewModel {
1857 portabilityStatusMessage = result.summary 1845 portabilityStatusMessage = result.summary
1858 CloudSyncService.shared.markAppSettingsChanged() 1846 CloudSyncService.shared.markAppSettingsChanged()
1859 CloudSyncService.shared.markMonitoringSettingsChanged(localActivationConfirmed: monitoringSettings.isEnabled) 1847 CloudSyncService.shared.markMonitoringSettingsChanged(localActivationConfirmed: monitoringSettings.isEnabled)
1860 CloudSyncService.shared.scheduleSyncIfNeeded(trigger: .import) 1848 CloudSyncService.shared.scheduleSyncIfNeeded(trigger: .imported)
1861 return result 1849 return result
1862 } 1850 }
1863 1851
@@ -2174,10 +2162,7 @@ final class DomainViewModel {
2174 case let .success(sections): 2162 case let .success(sections):
2175 dnsSections = sections 2163 dnsSections = sections
2176 dnsError = nil 2164 dnsError = nil
2177 case let .empty(message): 2165 case let .empty(message), let .error(message):
2178 dnsSections = []
2179 dnsError = message
2180 case let .error(message):
2181 dnsSections = [] 2166 dnsSections = []
2182 dnsError = message 2167 dnsError = message
2183 } 2168 }
@@ -2199,10 +2184,7 @@ final class DomainViewModel {
2199 case let .success(info): 2184 case let .success(info):
2200 sslInfo = info 2185 sslInfo = info
2201 sslError = nil 2186 sslError = nil
2202 case let .empty(message): 2187 case let .empty(message), let .error(message):
2203 sslInfo = nil
2204 sslError = message
2205 case let .error(message):
2206 sslInfo = nil 2188 sslInfo = nil
2207 sslError = message 2189 sslError = message
2208 } 2190 }
@@ -2228,15 +2210,7 @@ final class DomainViewModel {
2228 httpProtocol = headersResult.httpProtocol 2210 httpProtocol = headersResult.httpProtocol
2229 http3Advertised = headersResult.http3Advertised 2211 http3Advertised = headersResult.http3Advertised
2230 httpHeadersError = nil 2212 httpHeadersError = nil
2231 case let .empty(message): 2213 case let .empty(message), let .error(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):
2240 httpHeaders = [] 2214 httpHeaders = []
2241 httpSecurityGrade = nil 2215 httpSecurityGrade = nil
2242 httpStatusCode = nil 2216 httpStatusCode = nil
@@ -2255,10 +2229,7 @@ final class DomainViewModel {
2255 case let .success(results): 2229 case let .success(results):
2256 reachabilityResults = results 2230 reachabilityResults = results
2257 reachabilityError = nil 2231 reachabilityError = nil
2258 case let .empty(message): 2232 case let .empty(message), let .error(message):
2259 reachabilityResults = []
2260 reachabilityError = message
2261 case let .error(message):
2262 reachabilityResults = [] 2233 reachabilityResults = []
2263 reachabilityError = message 2234 reachabilityError = message
2264 } 2235 }
@@ -2272,10 +2243,7 @@ final class DomainViewModel {
2272 case let .success(emailResult): 2243 case let .success(emailResult):
2273 emailSecurity = emailResult 2244 emailSecurity = emailResult
2274 emailSecurityError = nil 2245 emailSecurityError = nil
2275 case let .empty(message): 2246 case let .empty(message), let .error(message):
2276 emailSecurity = nil
2277 emailSecurityError = message
2278 case let .error(message):
2279 emailSecurity = nil 2247 emailSecurity = nil
2280 emailSecurityError = message 2248 emailSecurityError = message
2281 } 2249 }
@@ -2289,10 +2257,7 @@ final class DomainViewModel {
2289 case let .success(ownership): 2257 case let .success(ownership):
2290 ownershipResult = ownership 2258 ownershipResult = ownership
2291 ownershipError = nil 2259 ownershipError = nil
2292 case let .empty(message): 2260 case let .empty(message), let .error(message):
2293 ownershipResult = nil
2294 ownershipError = message
2295 case let .error(message):
2296 ownershipResult = nil 2261 ownershipResult = nil
2297 ownershipError = message 2262 ownershipError = message
2298 } 2263 }
@@ -2306,10 +2271,7 @@ final class DomainViewModel {
2306 case let .success(record): 2271 case let .success(record):
2307 ptrRecord = record 2272 ptrRecord = record
2308 ptrError = nil 2273 ptrError = nil
2309 case let .empty(message): 2274 case let .empty(message), let .error(message):
2310 ptrRecord = nil
2311 ptrError = message
2312 case let .error(message):
2313 ptrRecord = nil 2275 ptrRecord = nil
2314 ptrError = message 2276 ptrError = message
2315 } 2277 }
@@ -2323,10 +2285,7 @@ final class DomainViewModel {
2323 case let .success(hops): 2285 case let .success(hops):
2324 redirectChain = hops 2286 redirectChain = hops
2325 redirectChainError = nil 2287 redirectChainError = nil
2326 case let .empty(message): 2288 case let .empty(message), let .error(message):
2327 redirectChain = []
2328 redirectChainError = message
2329 case let .error(message):
2330 redirectChain = [] 2289 redirectChain = []
2331 redirectChainError = message 2290 redirectChainError = message
2332 } 2291 }
@@ -2340,10 +2299,7 @@ final class DomainViewModel {
2340 case let .success(results): 2299 case let .success(results):
2341 subdomains = results 2300 subdomains = results
2342 subdomainsError = nil 2301 subdomainsError = nil
2343 case let .empty(message): 2302 case let .empty(message), let .error(message):
2344 subdomains = []
2345 subdomainsError = message
2346 case let .error(message):
2347 subdomains = [] 2303 subdomains = []
2348 subdomainsError = message 2304 subdomainsError = message
2349 } 2305 }
@@ -2358,11 +2314,7 @@ final class DomainViewModel {
2358 guard !Task.isCancelled, isCurrentLookup(lookupID) else { return } 2314 guard !Task.isCancelled, isCurrentLookup(lookupID) else { return }
2359 portScanResults = enrichedResults 2315 portScanResults = enrichedResults
2360 portScanError = nil 2316 portScanError = nil
2361 case let .empty(message): 2317 case let .empty(message), let .error(message):
2362 guard !Task.isCancelled, isCurrentLookup(lookupID) else { return }
2363 portScanResults = []
2364 portScanError = message
2365 case let .error(message):
2366 guard !Task.isCancelled, isCurrentLookup(lookupID) else { return } 2318 guard !Task.isCancelled, isCurrentLookup(lookupID) else { return }
2367 portScanResults = [] 2319 portScanResults = []
2368 portScanError = message 2320 portScanError = message
@@ -2377,10 +2329,7 @@ final class DomainViewModel {
2377 case let .success(geolocation): 2329 case let .success(geolocation):
2378 ipGeolocation = geolocation 2330 ipGeolocation = geolocation
2379 ipGeolocationError = nil 2331 ipGeolocationError = nil
2380 case let .empty(message): 2332 case let .empty(message), let .error(message):
2381 ipGeolocation = nil
2382 ipGeolocationError = message
2383 case let .error(message):
2384 ipGeolocation = nil 2333 ipGeolocation = nil
2385 ipGeolocationError = message 2334 ipGeolocationError = message
2386 } 2335 }
@@ -2408,10 +2357,7 @@ final class DomainViewModel {
2408 customPortResults = results 2357 customPortResults = results
2409 customPortScanError = nil 2358 customPortScanError = nil
2410 _ = saveHistoryEntry(replaceLatest: true) 2359 _ = saveHistoryEntry(replaceLatest: true)
2411 case let .empty(message): 2360 case let .empty(message), let .error(message):
2412 customPortResults = []
2413 customPortScanError = message
2414 case let .error(message):
2415 customPortResults = [] 2361 customPortResults = []
2416 customPortScanError = message 2362 customPortScanError = message
2417 } 2363 }
@@ -4024,7 +3970,7 @@ final class DomainViewModel {
4024 3970
4025 static func summaryFields(from snapshot: LookupSnapshot) -> [SummaryFieldViewData] { 3971 static func summaryFields(from snapshot: LookupSnapshot) -> [SummaryFieldViewData] {
4026 [ 3972 [
4027 SummaryFieldViewData(label: "Domain", value: snapshot.domain.nonEmpty ?? "Unavailable", tone: .primary), 3973 SummaryFieldViewData(label: "Domain", value: snapshot.domain.nilIfEmpty ?? "Unavailable", tone: .primary),
4028 SummaryFieldViewData(label: "Observed IP", value: primaryIPAddress(from: snapshot) ?? "Unavailable", tone: .primary), 3974 SummaryFieldViewData(label: "Observed IP", value: primaryIPAddress(from: snapshot) ?? "Unavailable", tone: .primary),
4029 SummaryFieldViewData(label: "Observed Redirect", value: finalRedirectTarget(from: snapshot) ?? "Unavailable", tone: .secondary), 3975 SummaryFieldViewData(label: "Observed Redirect", value: finalRedirectTarget(from: snapshot) ?? "Unavailable", tone: .secondary),
4030 SummaryFieldViewData(label: "Inference", value: availabilityInference(from: snapshot), tone: availabilityTone(snapshot.availabilityResult?.status)), 3976 SummaryFieldViewData(label: "Inference", value: availabilityInference(from: snapshot), tone: availabilityTone(snapshot.availabilityResult?.status)),
@@ -4780,10 +4726,7 @@ final class DomainViewModel {
4780 case let .success(pricing): 4726 case let .success(pricing):
4781 domainPricing = pricing 4727 domainPricing = pricing
4782 domainPricingError = nil 4728 domainPricingError = nil
4783 case let .empty(message): 4729 case let .empty(message), let .error(message):
4784 domainPricing = nil
4785 domainPricingError = conciseExternalMessage(message, fallback: "External pricing unavailable")
4786 case let .error(message):
4787 domainPricing = nil 4730 domainPricing = nil
4788 domainPricingError = conciseExternalMessage(message, fallback: "External pricing unavailable") 4731 domainPricingError = conciseExternalMessage(message, fallback: "External pricing unavailable")
4789 } 4732 }
@@ -4803,10 +4746,7 @@ final class DomainViewModel {
4803 case let .success(result): 4746 case let .success(result):
4804 reputation = result 4747 reputation = result
4805 reputationError = nil 4748 reputationError = nil
4806 case let .empty(message): 4749 case let .empty(message), let .error(message):
4807 reputation = nil
4808 reputationError = conciseExternalMessage(message, fallback: "Reputation check unavailable")
4809 case let .error(message):
4810 reputation = nil 4750 reputation = nil
4811 reputationError = conciseExternalMessage(message, fallback: "Reputation check unavailable") 4751 reputationError = conciseExternalMessage(message, fallback: "Reputation check unavailable")
4812 } 4752 }
@@ -4865,10 +4805,6 @@ final class DomainViewModel {
4865} 4805}
4866 4806
4867private extension String { 4807private extension String {
4868 var nonEmpty: String? {
4869 isEmpty ? nil : self
4870 }
4871
4872 var nilIfEmpty: String? { 4808 var nilIfEmpty: String? {
4873 isEmpty ? nil : self 4809 isEmpty ? nil : self
4874 } 4810 }
DomainDig/HistoryView.swift +3 −3
@@ -115,7 +115,7 @@ struct HistoryView: View {
115 Button("Clear All", role: .destructive) { 115 Button("Clear All", role: .destructive) {
116 viewModel.clearHistory() 116 viewModel.clearHistory()
117 } 117 }
118 Button("Cancel", role: .cancel) {} 118 Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
119 } message: { 119 } message: {
120 Text("This will delete all saved history entries. This cannot be undone.") 120 Text("This will delete all saved history entries. This cannot be undone.")
121 } 121 }
@@ -314,7 +314,7 @@ struct HistoryDetailView: View {
314 isCloudflareProxied: snapshot.httpHeaders.contains(where: { $0.name.lowercased() == "cf-ray" }), 314 isCloudflareProxied: snapshot.httpHeaders.contains(where: { $0.name.lowercased() == "cf-ray" }),
315 customPortsExpanded: .constant(false), 315 customPortsExpanded: .constant(false),
316 customPortInput: .constant(""), 316 customPortInput: .constant(""),
317 onScanCustomPorts: {} 317 onScanCustomPorts: { /* Historical snapshot; re-scanning is not offered here. */ }
318 ) 318 )
319 .padding(.top, appDensity.metrics.sectionSpacing) 319 .padding(.top, appDensity.metrics.sectionSpacing)
320 } 320 }
@@ -344,7 +344,7 @@ struct HistoryDetailView: View {
344 Button("Run with Snapshot Resolver") { 344 Button("Run with Snapshot Resolver") {
345 viewModel.rerunLookup(from: entry, useSnapshotResolver: true) 345 viewModel.rerunLookup(from: entry, useSnapshotResolver: true)
346 } 346 }
347 Button("Cancel", role: .cancel) {} 347 Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
348 } message: { 348 } message: {
349 Text(viewModel.resolverMismatchNote(for: entry) ?? "Choose how to reproduce this snapshot.") 349 Text(viewModel.resolverMismatchNote(for: entry) ?? "Choose how to reproduce this snapshot.")
350 } 350 }
DomainDig/IntegrationService.swift +7 −3
@@ -768,9 +768,7 @@ private final class SMTPChannel {
768 connection.stateUpdateHandler = { [weak self] state in 768 connection.stateUpdateHandler = { [weak self] state in
769 switch state { 769 switch state {
770 case .ready: 770 case .ready:
771 DispatchQueue.global(qos: .utility).async { 771 self?.scheduleReceiveLoop()
772 self?.startReceiveLoop()
773 }
774 continuation.resume() 772 continuation.resume()
775 case .failed(let error): 773 case .failed(let error):
776 continuation.resume(throwing: error) 774 continuation.resume(throwing: error)
@@ -782,6 +780,12 @@ private final class SMTPChannel {
782 } 780 }
783 } 781 }
784 782
783 private func scheduleReceiveLoop() {
784 DispatchQueue.global(qos: .utility).async { [weak self] in
785 self?.startReceiveLoop()
786 }
787 }
788
785 func cancel() { 789 func cancel() {
786 connection.cancel() 790 connection.cancel()
787 } 791 }
DomainDig/LocalNotificationService.swift +6 −7
@@ -5,7 +5,7 @@ import UserNotifications
5final class LocalNotificationService { 5final class LocalNotificationService {
6 static let shared = LocalNotificationService() 6 static let shared = LocalNotificationService()
7 7
8 private init() {} 8 private init() { /* Singleton; use the shared instance. */ }
9 9
10 static let domainUserInfoKey = "domain" 10 static let domainUserInfoKey = "domain"
11 static let domainCategoryIdentifier = "domain-event" 11 static let domainCategoryIdentifier = "domain-event"
@@ -188,14 +188,14 @@ private final class NotificationCenterDelegate: NSObject, UNUserNotificationCent
188 static let shared = NotificationCenterDelegate() 188 static let shared = NotificationCenterDelegate()
189 189
190 func userNotificationCenter( 190 func userNotificationCenter(
191 _ center: UNUserNotificationCenter, 191 _: UNUserNotificationCenter,
192 willPresent notification: UNNotification 192 willPresent _: UNNotification
193 ) async -> UNNotificationPresentationOptions { 193 ) async -> UNNotificationPresentationOptions {
194 [.banner, .list, .sound] 194 [.banner, .list, .sound]
195 } 195 }
196 196
197 func userNotificationCenter( 197 func userNotificationCenter(
198 _ center: UNUserNotificationCenter, 198 _: UNUserNotificationCenter,
199 didReceive response: UNNotificationResponse 199 didReceive response: UNNotificationResponse
200 ) async { 200 ) async {
201 let userInfo = response.notification.request.content.userInfo 201 let userInfo = response.notification.request.content.userInfo
@@ -204,10 +204,9 @@ private final class NotificationCenterDelegate: NSObject, UNUserNotificationCent
204 else { return } 204 else { return }
205 205
206 let action: DomainDigDeepLink.Action 206 let action: DomainDigDeepLink.Action
207 switch response.actionIdentifier { 207 if response.actionIdentifier == LocalNotificationService.reinspectActionIdentifier {
208 case LocalNotificationService.reinspectActionIdentifier:
209 action = .inspect(domain) 208 action = .inspect(domain)
210 default: 209 } else {
211 // Default tap: open the tracked domain's detail. 210 // Default tap: open the tracked domain's detail.
212 action = .detail(domain) 211 action = .detail(domain)
213 } 212 }
DomainDig/Models.swift +9 −5
@@ -244,8 +244,14 @@ struct DomainChangeSummary: Codable, Equatable {
244 changedSections = try container.decodeIfPresent([String].self, forKey: .changedSections) ?? [] 244 changedSections = try container.decodeIfPresent([String].self, forKey: .changedSections) ?? []
245 generatedAt = try container.decode(Date.self, forKey: .generatedAt) 245 generatedAt = try container.decode(Date.self, forKey: .generatedAt)
246 severity = try container.decodeIfPresent(ChangeSeverity.self, forKey: .severity) ?? (hasChanges ? .medium : .low) 246 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 }
247 impactClassification = try container.decodeIfPresent(ChangeImpactClassification.self, forKey: .impactClassification) 253 impactClassification = try container.decodeIfPresent(ChangeImpactClassification.self, forKey: .impactClassification)
248 ?? (severity == .high ? .critical : (hasChanges ? .warning : .informational)) 254 ?? defaultImpact
249 message = try container.decodeIfPresent(String.self, forKey: .message) 255 message = try container.decodeIfPresent(String.self, forKey: .message)
250 ?? (changedSections.isEmpty ? "No meaningful changes" : changedSections.joined(separator: " • ")) 256 ?? (changedSections.isEmpty ? "No meaningful changes" : changedSections.joined(separator: " • "))
251 observedFacts = try container.decodeIfPresent([String].self, forKey: .observedFacts) ?? [] 257 observedFacts = try container.decodeIfPresent([String].self, forKey: .observedFacts) ?? []
@@ -562,12 +568,10 @@ enum DomainClassificationKind: String, Codable, CaseIterable, Sendable {
562 case unknown 568 case unknown
563 569
564 var title: String { 570 var title: String {
565 switch self { 571 if self == .staticSite {
566 case .staticSite:
567 return "Static" 572 return "Static"
568 default:
569 return rawValue.capitalized
570 } 573 }
574 return rawValue.capitalized
571 } 575 }
572} 576}
573 577
DomainDig/PortScanService.swift +21 −18
@@ -89,24 +89,7 @@ struct PortScanService {
89 switch state { 89 switch state {
90 case .ready: 90 case .ready:
91 connection.receive(minimumIncompleteLength: 1, maximumLength: 256) { data, _, _, error in 91 connection.receive(minimumIncompleteLength: 1, maximumLength: 256) { data, _, _, error in
92 guard error == nil, 92 context.finish(with: printableBanner(from: data, error: error))
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)
110 } 93 }
111 case .failed, .cancelled: 94 case .failed, .cancelled:
112 context.finish(with: nil) 95 context.finish(with: nil)
@@ -123,6 +106,26 @@ struct PortScanService {
123 } 106 }
124 } 107 }
125 108
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
126 private static func probe(domain: String, port: UInt16) async -> PortProbeResult { 129 private static func probe(domain: String, port: UInt16) async -> PortProbeResult {
127 await probe(domain: domain, port: port, timeout: 1.5) 130 await probe(domain: domain, port: port, timeout: 1.5)
128 } 131 }
DomainDig/PurchaseService.swift +2 −4
@@ -288,12 +288,10 @@ final class PurchaseService {
288 288
289 private func storeMessage(for error: Error, fallback: String) -> String { 289 private func storeMessage(for error: Error, fallback: String) -> String {
290 if let storeKitError = error as? StoreKitError { 290 if let storeKitError = error as? StoreKitError {
291 switch storeKitError { 291 if case .networkError = storeKitError {
292 case .networkError:
293 return "The App Store is offline right now." 292 return "The App Store is offline right now."
294 default:
295 return fallback
296 } 293 }
294 return fallback
297 } 295 }
298 296
299 let message = error.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines) 297 let message = error.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines)
DomainDig/SSLCheckService.swift +28 −32
@@ -156,10 +156,9 @@ private enum DERCertificateParser {
156 var offset = tbsContent.contentStart 156 var offset = tbsContent.contentStart
157 157
158 // Skip version (explicit tag [0]) if present 158 // Skip version (explicit tag [0]) if present
159 if offset < bytes.count && (bytes[offset] & 0xE0) == 0xA0 { 159 if offset < bytes.count, (bytes[offset] & 0xE0) == 0xA0,
160 if let tagLen = readTagAndLength(bytes, offset: offset) { 160 let tagLen = readTagAndLength(bytes, offset: offset) {
161 offset = tagLen.contentStart + tagLen.length 161 offset = tagLen.contentStart + tagLen.length
162 }
163 } 162 }
164 163
165 // Skip serialNumber 164 // Skip serialNumber
@@ -199,11 +198,10 @@ private enum DERCertificateParser {
199 // Extensions are in an explicit tag [3] 198 // Extensions are in an explicit tag [3]
200 while offset < tbsContent.contentStart + tbsContent.length { 199 while offset < tbsContent.contentStart + tbsContent.length {
201 if bytes[offset] == 0xA3 { 200 if bytes[offset] == 0xA3 {
202 if let extWrapper = readTagAndLength(bytes, offset: offset) { 201 // Inside the wrapper is a SEQUENCE of SEQUENCE extensions
203 // Inside is a SEQUENCE of SEQUENCE extensions 202 if let extWrapper = readTagAndLength(bytes, offset: offset),
204 if let extsSeq = readTagAndLength(bytes, offset: extWrapper.contentStart) { 203 let extsSeq = readTagAndLength(bytes, offset: extWrapper.contentStart) {
205 result.subjectAltNames = extractSANs(bytes, sequenceStart: extsSeq.contentStart, length: extsSeq.length) 204 result.subjectAltNames = extractSANs(bytes, sequenceStart: extsSeq.contentStart, length: extsSeq.length)
206 }
207 } 205 }
208 break 206 break
209 } 207 }
@@ -267,27 +265,25 @@ private enum DERCertificateParser {
267 if oidBytes == sanOID { 265 if oidBytes == sanOID {
268 var valuePos = oidTL.contentStart + oidTL.length 266 var valuePos = oidTL.contentStart + oidTL.length
269 // Skip optional critical BOOLEAN 267 // Skip optional critical BOOLEAN
270 if valuePos < extEnd && bytes[valuePos] == 0x01 { 268 if valuePos < extEnd, bytes[valuePos] == 0x01,
271 if let boolTL = readTagAndLength(bytes, offset: valuePos) { 269 let boolTL = readTagAndLength(bytes, offset: valuePos) {
272 valuePos = boolTL.contentStart + boolTL.length 270 valuePos = boolTL.contentStart + boolTL.length
273 }
274 } 271 }
275 // The value is an OCTET STRING wrapping a SEQUENCE of GeneralNames 272 // The value is an OCTET STRING wrapping a SEQUENCE of GeneralNames
276 if let octetTL = readTagAndLength(bytes, offset: valuePos) { 273 if let octetTL = readTagAndLength(bytes, offset: valuePos),
277 if let sanSeq = readTagAndLength(bytes, offset: octetTL.contentStart) { 274 let sanSeq = readTagAndLength(bytes, offset: octetTL.contentStart) {
278 let sanEnd = sanSeq.contentStart + sanSeq.length 275 let sanEnd = sanSeq.contentStart + sanSeq.length
279 var sanPos = sanSeq.contentStart 276 var sanPos = sanSeq.contentStart
280 while sanPos < sanEnd { 277 while sanPos < sanEnd {
281 guard let nameTL = readTagAndLength(bytes, offset: sanPos) else { break } 278 guard let nameTL = readTagAndLength(bytes, offset: sanPos) else { break }
282 // Context tag [2] = dNSName (IA5String) 279 // Context tag [2] = dNSName (IA5String)
283 if (bytes[sanPos] & 0x1F) == 2 { 280 if (bytes[sanPos] & 0x1F) == 2 {
284 let nameBytes = bytes[nameTL.contentStart..<nameTL.contentStart + nameTL.length] 281 let nameBytes = bytes[nameTL.contentStart..<nameTL.contentStart + nameTL.length]
285 if let name = String(bytes: nameBytes, encoding: .ascii) { 282 if let name = String(bytes: nameBytes, encoding: .ascii) {
286 sans.append(name) 283 sans.append(name)
287 }
288 } 284 }
289 sanPos = nameTL.contentStart + nameTL.length
290 } 285 }
286 sanPos = nameTL.contentStart + nameTL.length
291 } 287 }
292 } 288 }
293 } 289 }
@@ -386,19 +382,19 @@ enum SSLError: LocalizedError {
386 382
387final class SSLSessionDelegate: NSObject, URLSessionDelegate, @unchecked Sendable { 383final class SSLSessionDelegate: NSObject, URLSessionDelegate, @unchecked Sendable {
388 private let lock = NSLock() 384 private let lock = NSLock()
389 private var _serverTrust: SecTrust? 385 private var storedServerTrust: SecTrust?
390 private var _tlsMetadata: TLSMetadata? 386 private var storedTLSMetadata: TLSMetadata?
391 387
392 var serverTrust: SecTrust? { 388 var serverTrust: SecTrust? {
393 lock.lock() 389 lock.lock()
394 defer { lock.unlock() } 390 defer { lock.unlock() }
395 return _serverTrust 391 return storedServerTrust
396 } 392 }
397 393
398 fileprivate var tlsMetadata: TLSMetadata? { 394 fileprivate var tlsMetadata: TLSMetadata? {
399 lock.lock() 395 lock.lock()
400 defer { lock.unlock() } 396 defer { lock.unlock() }
401 return _tlsMetadata 397 return storedTLSMetadata
402 } 398 }
403 399
404 func urlSession( 400 func urlSession(
@@ -413,7 +409,7 @@ final class SSLSessionDelegate: NSObject, URLSessionDelegate, @unchecked Sendabl
413 } 409 }
414 410
415 lock.lock() 411 lock.lock()
416 _serverTrust = trust 412 storedServerTrust = trust
417 lock.unlock() 413 lock.unlock()
418 414
419 let credential = URLCredential(trust: trust) 415 let credential = URLCredential(trust: trust)
@@ -439,7 +435,7 @@ extension SSLSessionDelegate: URLSessionTaskDelegate {
439 } 435 }
440 436
441 lock.lock() 437 lock.lock()
442 _tlsMetadata = TLSMetadata(tlsVersion: tlsVersion, cipherSuite: cipherSuite) 438 storedTLSMetadata = TLSMetadata(tlsVersion: tlsVersion, cipherSuite: cipherSuite)
443 lock.unlock() 439 lock.unlock()
444 } 440 }
445 441
DomainDig/ScheduledReportService.swift +3 −3
@@ -106,10 +106,10 @@ struct ScheduledReportOutcome {
106final class ScheduledReportService { 106final class ScheduledReportService {
107 static let shared = ScheduledReportService() 107 static let shared = ScheduledReportService()
108 108
109 private init() {} 109 private init() { /* Singleton; use the shared instance. */ }
110 110
111 @discardableResult 111 @discardableResult
112 func generateReport(trigger: MonitoringRunTrigger, requireEnabledSetting: Bool) async -> ScheduledReportOutcome { 112 func generateReport(trigger _: MonitoringRunTrigger, requireEnabledSetting: Bool) async -> ScheduledReportOutcome {
113 var settings = ScheduledReportStorage.loadSettings() 113 var settings = ScheduledReportStorage.loadSettings()
114 114
115 guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else { 115 guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else {
@@ -191,7 +191,7 @@ final class ScheduledReportScheduler {
191 191
192 private var isRegistered = false 192 private var isRegistered = false
193 193
194 private init() {} 194 private init() { /* Singleton; use the shared instance. */ }
195 195
196 func registerBackgroundTask() { 196 func registerBackgroundTask() {
197 #if canImport(BackgroundTasks) 197 #if canImport(BackgroundTasks)
DomainDig/SweepActivityController.swift +1 −1
@@ -8,7 +8,7 @@ final class SweepActivityController {
8 8
9 private var activity: Activity<SweepActivityAttributes>? 9 private var activity: Activity<SweepActivityAttributes>?
10 10
11 private init() {} 11 private init() { /* Singleton; use the shared instance. */ }
12 12
13 func begin(title: String, total: Int) { 13 func begin(title: String, total: Int) {
14 guard ActivityAuthorizationInfo().areActivitiesEnabled else { return } 14 guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
DomainDig/WatchlistView.swift +6 −6
@@ -169,13 +169,13 @@ struct WatchlistView: View {
169 shareTrackedDomains(format: .pdf) 169 shareTrackedDomains(format: .pdf)
170 } 170 }
171 } else { 171 } else {
172 Button("CSV Export • Available in Pro") {} 172 Button("CSV Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
173 .disabled(true) 173 .disabled(true)
174 Button("JSON Export • Available in Pro") {} 174 Button("JSON Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
175 .disabled(true) 175 .disabled(true)
176 Button("Markdown Export • Available in Pro") {} 176 Button("Markdown Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
177 .disabled(true) 177 .disabled(true)
178 Button("PDF Export • Available in Pro") {} 178 Button("PDF Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
179 .disabled(true) 179 .disabled(true)
180 } 180 }
181 } label: { 181 } label: {
@@ -250,7 +250,7 @@ struct WatchlistView: View {
250 Button("Save") { 250 Button("Save") {
251 viewModel.saveCurrentWatchlistView(name: newSavedViewName) 251 viewModel.saveCurrentWatchlistView(name: newSavedViewName)
252 } 252 }
253 Button("Cancel", role: .cancel) {} 253 Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
254 } message: { 254 } message: {
255 Text("Saves the current tag, filter, and sort as a reusable preset.") 255 Text("Saves the current tag, filter, and sort as a reusable preset.")
256 } 256 }
@@ -736,7 +736,7 @@ struct TrackedDomainDetailView: View {
736 viewModel.rerunInspection(for: liveTrackedDomain, useSnapshotResolver: true) 736 viewModel.rerunInspection(for: liveTrackedDomain, useSnapshotResolver: true)
737 } 737 }
738 } 738 }
739 Button("Cancel", role: .cancel) {} 739 Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
740 } message: { 740 } message: {
741 Text(viewModel.resolverMismatchNote(for: liveTrackedDomain) ?? "Choose how to reproduce the most recent snapshot.") 741 Text(viewModel.resolverMismatchNote(for: liveTrackedDomain) ?? "Choose how to reproduce the most recent snapshot.")
742 } 742 }
DomainDig/WorkflowsView.swift +8 −8
@@ -253,13 +253,13 @@ struct WorkflowDetailView: View {
253 shareWorkflowResults(format: .pdf) 253 shareWorkflowResults(format: .pdf)
254 } 254 }
255 } else { 255 } else {
256 Button("CSV Export • Available in Pro") {} 256 Button("CSV Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
257 .disabled(true) 257 .disabled(true)
258 Button("JSON Export • Available in Pro") {} 258 Button("JSON Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
259 .disabled(true) 259 .disabled(true)
260 Button("Markdown Export • Available in Pro") {} 260 Button("Markdown Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
261 .disabled(true) 261 .disabled(true)
262 Button("PDF Export • Available in Pro") {} 262 Button("PDF Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
263 .disabled(true) 263 .disabled(true)
264 } 264 }
265 } label: { 265 } label: {
@@ -554,13 +554,13 @@ struct WorkflowRunSummaryView: View {
554 share(format: .pdf) 554 share(format: .pdf)
555 } 555 }
556 } else { 556 } else {
557 Button("CSV Export • Available in Pro") {} 557 Button("CSV Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
558 .disabled(true) 558 .disabled(true)
559 Button("JSON Export • Available in Pro") {} 559 Button("JSON Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
560 .disabled(true) 560 .disabled(true)
561 Button("Markdown Export • Available in Pro") {} 561 Button("Markdown Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
562 .disabled(true) 562 .disabled(true)
563 Button("PDF Export • Available in Pro") {} 563 Button("PDF Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ }
564 .disabled(true) 564 .disabled(true)
565 } 565 }
566 } label: { 566 } label: {
DomainDigWidget/DomainDigPortfolioWidget.swift +4 −5
@@ -7,7 +7,7 @@ struct DomainDigEntry: TimelineEntry {
7} 7}
8 8
9struct DomainDigProvider: TimelineProvider { 9struct DomainDigProvider: TimelineProvider {
10 func placeholder(in context: Context) -> DomainDigEntry { 10 func placeholder(in _: Context) -> DomainDigEntry {
11 DomainDigEntry(date: Date(), data: .placeholder) 11 DomainDigEntry(date: Date(), data: .placeholder)
12 } 12 }
13 13
@@ -16,7 +16,7 @@ struct DomainDigProvider: TimelineProvider {
16 completion(DomainDigEntry(date: Date(), data: data)) 16 completion(DomainDigEntry(date: Date(), data: data))
17 } 17 }
18 18
19 func getTimeline(in context: Context, completion: @escaping (Timeline<DomainDigEntry>) -> Void) { 19 func getTimeline(in _: Context, completion: @escaping (Timeline<DomainDigEntry>) -> Void) {
20 let data = DomainDigWidgetStore.read() ?? .empty 20 let data = DomainDigWidgetStore.read() ?? .empty
21 let entry = DomainDigEntry(date: Date(), data: data) 21 let entry = DomainDigEntry(date: Date(), data: data)
22 // The app reloads timelines on foreground and on watchlist changes; this 22 // The app reloads timelines on foreground and on watchlist changes; this
@@ -49,10 +49,9 @@ struct DomainDigWidgetView: View {
49 if data.totalDomains == 0 { 49 if data.totalDomains == 0 {
50 emptyState 50 emptyState
51 } else { 51 } else {
52 switch family { 52 if family == .systemSmall {
53 case .systemSmall:
54 smallView 53 smallView
55 default: 54 } else {
56 mediumOrLargeView 55 mediumOrLargeView
57 } 56 }
58 } 57 }
DomainInspectionService.swift +12 −12
@@ -295,10 +295,10 @@ struct DomainInspectionService {
295 let ptrRecord = mapOptionalServiceResult(ptrOutcome.map { normalizeErrors(in: $0.value) }, missingMessage: "No A record available") 295 let ptrRecord = mapOptionalServiceResult(ptrOutcome.map { normalizeErrors(in: $0.value) }, missingMessage: "No A record available")
296 let geolocation = mapOptionalServiceResult(geoOutcome.map { normalizeErrors(in: $0.value) }, missingMessage: "No A record available") 296 let geolocation = mapOptionalServiceResult(geoOutcome.map { normalizeErrors(in: $0.value) }, missingMessage: "No A record available")
297 let availabilityConfidence = confidenceForAvailability(result: availability.value, provenance: provenanceBySection[.availability]) 297 let availabilityConfidence = confidenceForAvailability(result: availability.value, provenance: provenanceBySection[.availability])
298 let ownershipConfidence = confidenceForOwnership(result: ownership.value, error: ownership.message) 298 let ownershipConfidence = confidenceForOwnership(result: ownership.value)
299 let subdomainConfidence = confidenceForSubdomains(results: subdomains.value, error: subdomains.message) 299 let subdomainConfidence = confidenceForSubdomains(results: subdomains.value)
300 let emailConfidence = confidenceForEmail(result: emailSecurity.value, error: emailSecurity.message) 300 let emailConfidence = confidenceForEmail(result: emailSecurity.value)
301 let geolocationConfidence = confidenceForGeolocation(result: geolocation.value, error: geolocation.message) 301 let geolocationConfidence = confidenceForGeolocation(result: geolocation.value)
302 let validationIssues = validationIssues(for: normalizedDomain, snapshotTimestamp: startedAt, availability: availability.value, dnsSections: dnsSections.value, provenanceBySection: provenanceBySection) 302 let validationIssues = validationIssues(for: normalizedDomain, snapshotTimestamp: startedAt, availability: availability.value, dnsSections: dnsSections.value, provenanceBySection: provenanceBySection)
303 303
304 let snapshot = LookupSnapshot( 304 let snapshot = LookupSnapshot(
@@ -580,21 +580,21 @@ struct DomainInspectionService {
580 return .low 580 return .low
581 } 581 }
582 582
583 private func confidenceForOwnership(result: DomainOwnership?, error: String?) -> ConfidenceLevel { 583 private func confidenceForOwnership(result: DomainOwnership?) -> ConfidenceLevel {
584 guard let result else { return error == nil ? .low : .low } 584 guard let result else { return .low }
585 let hasDirectRegistrationData = result.registrar != nil || result.createdDate != nil || result.expirationDate != nil 585 let hasDirectRegistrationData = result.registrar != nil || result.createdDate != nil || result.expirationDate != nil
586 return hasDirectRegistrationData ? .high : .medium 586 return hasDirectRegistrationData ? .high : .medium
587 } 587 }
588 588
589 private func confidenceForSubdomains(results: [DiscoveredSubdomain], error: String?) -> ConfidenceLevel { 589 private func confidenceForSubdomains(results: [DiscoveredSubdomain]) -> ConfidenceLevel {
590 if !results.isEmpty { 590 if !results.isEmpty {
591 return .medium 591 return .medium
592 } 592 }
593 return error == nil ? .low : .low 593 return .low
594 } 594 }
595 595
596 private func confidenceForEmail(result: EmailSecurityResult?, error: String?) -> ConfidenceLevel { 596 private func confidenceForEmail(result: EmailSecurityResult?) -> ConfidenceLevel {
597 guard let result else { return error == nil ? .low : .low } 597 guard let result else { return .low }
598 let foundCount = [result.spf.found, result.dmarc.found, result.dkim.found, result.bimi.found, result.mtaSts?.txtFound == true] 598 let foundCount = [result.spf.found, result.dmarc.found, result.dkim.found, result.bimi.found, result.mtaSts?.txtFound == true]
599 .filter { $0 } 599 .filter { $0 }
600 .count 600 .count
@@ -607,8 +607,8 @@ struct DomainInspectionService {
607 return .low 607 return .low
608 } 608 }
609 609
610 private func confidenceForGeolocation(result: IPGeolocation?, error: String?) -> ConfidenceLevel { 610 private func confidenceForGeolocation(result: IPGeolocation?) -> ConfidenceLevel {
611 guard let result else { return error == nil ? .low : .low } 611 guard let result else { return .low }
612 if result.city != nil && result.country_name != nil && result.latitude != nil && result.longitude != nil { 612 if result.city != nil && result.country_name != nil && result.latitude != nil && result.longitude != nil {
613 return .high 613 return .high
614 } 614 }
DomainReportExporter.swift +1 −1
@@ -11,8 +11,8 @@ enum DomainExportFormat: String, CaseIterable, Identifiable, Codable {
11 case markdown = "md" 11 case markdown = "md"
12 case pdf = "pdf" 12 case pdf = "pdf"
13 13
14 var id: String { rawValue }
15 var fileExtension: String { rawValue } 14 var fileExtension: String { rawValue }
15 var id: String { fileExtension }
16 16
17 var title: String { 17 var title: String {
18 switch self { 18 switch self {
RELEASE_ROADMAP.md +32 −1
@@ -6,7 +6,7 @@ ports, geolocation, subdomains, availability). The next several releases invest
6in *reach and surfacing* — getting that data onto more iOS surfaces and into more 6in *reach and surfacing* — getting that data onto more iOS surfaces and into more
7workflows — rather than adding raw protocol checks. 7workflows — rather than adding raw protocol checks.
8 8
9Current version: `v4.8.2`. 9Current version: `v4.8.3`.
10 10
11## v4.4.1 Patch: Release Readiness — ✅ shipped 11## v4.4.1 Patch: Release Readiness — ✅ shipped
12 12
@@ -154,6 +154,37 @@ Goal: close the UAT follow-ups and make failures legible instead of silent.
154 synced against App Store Connect. Registered in the project without target 154 synced against App Store Connect. Registered in the project without target
155 membership so it is not bundled into shipping builds. 155 membership so it is not bundled into shipping builds.
156 156
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
157## v5.0.0 Major: Contract Stabilization & Engineering Health 188## v5.0.0 Major: Contract Stabilization & Engineering Health
158 189
159Goal: earn long-term compatibility promises — and pay down the debt that the 190Goal: earn long-term compatibility promises — and pay down the debt that the