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