Commit bb18197860
Unsigned
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 | ||
| 3 | enum AppVersion { | 3 | enum 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 | ||
| 4 | final class DomainDigAppDelegate: NSObject, UIApplicationDelegate { | 4 | final 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 | ||
| 16 | final class DomainDigSceneDelegate: NSObject, UIWindowSceneDelegate { | 16 | final 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 | ||
| 46 | enum ShareableEntity: Identifiable, Hashable { | 46 | enum 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 | ||
| 1122 | struct DomainDiffView: View { | 1112 | struct 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 | ||
| 3743 | extension 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 | |||
| 3753 | extension 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 | |||
| 3763 | extension 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 { | |||
| 149 | final class DomainDigIntentRouter { | 149 | final 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 | ||
| 4867 | private extension String { | 4807 | private 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 | |||
| 5 | final class LocalNotificationService { | 5 | final 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 | ||
| 387 | final class SSLSessionDelegate: NSObject, URLSessionDelegate, @unchecked Sendable { | 383 | final 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 { | |||
| 106 | final class ScheduledReportService { | 106 | final 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 | ||
| 9 | struct DomainDigProvider: TimelineProvider { | 9 | struct 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 | |||
| 6 | in *reach and surfacing* — getting that data onto more iOS surfaces and into more | 6 | in *reach and surfacing* — getting that data onto more iOS surfaces and into more |
| 7 | workflows — rather than adding raw protocol checks. | 7 | workflows — rather than adding raw protocol checks. |
| 8 | 8 | ||
| 9 | Current version: `v4.8.2`. | 9 | Current 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 | |||
| 159 | Goal: 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 | |||
| 181 | Left open deliberately: `swift:S107` (initializer parameter counts on model | ||
| 182 | memberwise inits), `swift:S115` (constants mirroring DoH/ipapi JSON keys), | ||
| 183 | `swift:S1075` (false positives on `https://` literals), and two `swift:S117` | ||
| 184 | hits on SwiftUI `$binding` shorthand in `AuditModeView`, which cannot be | ||
| 185 | renamed. These want a *Won't Fix* / *Safe* resolution in SonarCloud rather than | ||
| 186 | a code change. | ||
| 187 | |||
| 157 | ## v5.0.0 Major: Contract Stabilization & Engineering Health | 188 | ## v5.0.0 Major: Contract Stabilization & Engineering Health |
| 158 | 189 | ||
| 159 | Goal: earn long-term compatibility promises — and pay down the debt that the | 190 | Goal: earn long-term compatibility promises — and pay down the debt that the |