diff options
Diffstat (limited to 'DomainDig')
24 files changed, 197 insertions, 220 deletions
diff --git a/DomainDig/AppVersion.swift b/DomainDig/AppVersion.swift index e8402b8..a0956df 100644 --- a/DomainDig/AppVersion.swift +++ b/DomainDig/AppVersion.swift @@ -2,6 +2,6 @@ import Foundation enum AppVersion { nonisolated static var current: String { - "4.8.2" + "4.8.3" } } diff --git a/DomainDig/AuditModeView.swift b/DomainDig/AuditModeView.swift index fcd4083..b3e363e 100644 --- a/DomainDig/AuditModeView.swift +++ b/DomainDig/AuditModeView.swift @@ -57,7 +57,7 @@ struct AuditModeView: View { } } .alert("Audit Export", isPresented: Binding(get: { exportMessage != nil }, set: { if !$0 { exportMessage = nil } })) { - Button("OK", role: .cancel) {} + Button("OK", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ } } message: { Text(exportMessage ?? "") } diff --git a/DomainDig/BatchResultsView.swift b/DomainDig/BatchResultsView.swift index e97899e..027d074 100644 --- a/DomainDig/BatchResultsView.swift +++ b/DomainDig/BatchResultsView.swift @@ -104,7 +104,7 @@ struct BatchResultRowView: View { if let changeClassification = result.changeClassification { Text("Impact: \(changeClassification.title)") .font(appDensity.font(.caption2)) - .foregroundStyle(changeClassification == .critical ? .red : (changeClassification == .warning ? .yellow : .secondary)) + .foregroundStyle(changeClassification.color) } if let errorMessage = result.errorMessage { diff --git a/DomainDig/CloudSharingLifecycle.swift b/DomainDig/CloudSharingLifecycle.swift index fc5de80..26425e5 100644 --- a/DomainDig/CloudSharingLifecycle.swift +++ b/DomainDig/CloudSharingLifecycle.swift @@ -3,9 +3,9 @@ import UIKit final class DomainDigAppDelegate: NSObject, UIApplicationDelegate { func application( - _ application: UIApplication, + _: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, - options: UIScene.ConnectionOptions + options _: UIScene.ConnectionOptions ) -> UISceneConfiguration { let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) configuration.delegateClass = DomainDigSceneDelegate.self @@ -14,12 +14,12 @@ final class DomainDigAppDelegate: NSObject, UIApplicationDelegate { } final class DomainDigSceneDelegate: NSObject, UIWindowSceneDelegate { - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + func scene(_: UIScene, willConnectTo _: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let metadata = connectionOptions.cloudKitShareMetadata else { return } accept(metadata: metadata) } - func windowScene(_ windowScene: UIWindowScene, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) { + func windowScene(_: UIWindowScene, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) { accept(metadata: cloudKitShareMetadata) } diff --git a/DomainDig/CloudSharingSheet.swift b/DomainDig/CloudSharingSheet.swift index 3d524ef..8318e2a 100644 --- a/DomainDig/CloudSharingSheet.swift +++ b/DomainDig/CloudSharingSheet.swift @@ -13,7 +13,7 @@ struct CloudSharingSheet: UIViewControllerRepresentable { Coordinator(dismiss: dismiss, title: title) } - func makeUIViewController(context: Context) -> UIViewController { + func makeUIViewController(context _: Context) -> UIViewController { UIViewController() } @@ -77,7 +77,7 @@ struct CloudSharingSheet: UIViewControllerRepresentable { } } - func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + func presentationControllerDidDismiss(_: UIPresentationController) { dismissAction() } } diff --git a/DomainDig/CloudSyncService.swift b/DomainDig/CloudSyncService.swift index 2cb4cfe..985d2a4 100644 --- a/DomainDig/CloudSyncService.swift +++ b/DomainDig/CloudSyncService.swift @@ -40,7 +40,7 @@ enum CloudSyncTrigger: String { case launch case automatic case manual - case `import` + case imported = "import" } enum ShareableEntity: Identifiable, Hashable { @@ -1186,7 +1186,7 @@ final class CloudSyncService { } private func collaborationMetadata( - for record: CKRecord, + for _: CKRecord, share: CKShare?, scope: SyncDatabaseScope ) -> CollaborationMetadata { diff --git a/DomainDig/ContentView.swift b/DomainDig/ContentView.swift index 3e1015d..40c97d1 100644 --- a/DomainDig/ContentView.swift +++ b/DomainDig/ContentView.swift @@ -569,13 +569,13 @@ struct ContentView: View { shareSingleResults(format: .pdf) } } else { - Button("CSV Export • Available in Pro") {} + Button("CSV Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ } .disabled(true) - Button("JSON Export • Available in Pro") {} + Button("JSON Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ } .disabled(true) - Button("Markdown Export • Available in Pro") {} + Button("Markdown Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ } .disabled(true) - Button("PDF Export • Available in Pro") {} + Button("PDF Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ } .disabled(true) } } label: { @@ -621,13 +621,13 @@ struct ContentView: View { shareBatchResults(format: .pdf) } } else { - Button("Batch CSV • Available in Pro") {} + Button("Batch CSV • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ } .disabled(true) - Button("Batch JSON • Available in Pro") {} + Button("Batch JSON • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ } .disabled(true) - Button("Batch Markdown • Available in Pro") {} + Button("Batch Markdown • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ } .disabled(true) - Button("Batch PDF • Available in Pro") {} + Button("Batch PDF • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ } .disabled(true) } } label: { @@ -1040,10 +1040,10 @@ struct DomainChangeSummaryView: View { .clipShape(Capsule()) Text(summary.impactClassification.title.uppercased()) .font(appDensity.font(.caption2)) - .foregroundStyle(impactColor(summary.impactClassification)) + .foregroundStyle(summary.impactClassification.color) .padding(.horizontal, 8) .padding(.vertical, 4) - .background(impactColor(summary.impactClassification).opacity(0.16)) + .background(summary.impactClassification.color.opacity(0.16)) .clipShape(Capsule()) Text(summary.generatedAt, style: .time) .font(appDensity.font(.caption2)) @@ -1107,16 +1107,6 @@ struct DomainChangeSummaryView: View { } } - private func impactColor(_ impact: ChangeImpactClassification) -> Color { - switch impact { - case .informational: - return .secondary - case .warning: - return .yellow - case .critical: - return .red - } - } } struct DomainDiffView: View { @@ -2008,7 +1998,7 @@ struct WebSectionView: View { } SectionTrustMetadataView(provenance: tlsProvenance, confidence: nil) if !sslLoading, let tlsSummary { - LabeledValueRow(row: InfoRowViewData(label: "TLS Grade", value: tlsSummary.tlsGrade.rawValue, tone: tlsSummary.tlsGrade == .a ? .success : (tlsSummary.tlsGrade == .f ? .failure : .warning))) + LabeledValueRow(row: InfoRowViewData(label: "TLS Grade", value: tlsSummary.tlsGrade.rawValue, tone: tlsSummary.tlsGrade.tone)) ForEach(Array(tlsSummary.tlsHighlights.enumerated()), id: \.offset) { _, highlight in MessageRowView(text: highlight, isError: isTLSHighlightError(highlight)) } @@ -2153,7 +2143,7 @@ struct EmailSectionView: View { .opacity(loading ? 0 : 1) } if let assessment, let grade = assessment.grade { - LabeledValueRow(row: InfoRowViewData(label: "Grade", value: grade.rawValue, tone: grade == .a ? .success : (grade == .f ? .failure : .warning))) + LabeledValueRow(row: InfoRowViewData(label: "Grade", value: grade.rawValue, tone: grade.tone)) if !assessment.reasons.isEmpty { Text(assessment.reasons.joined(separator: " | ")) .font(appDensity.font(.caption2)) @@ -2897,6 +2887,11 @@ private struct LocalAPISettingsView: View { @State private var localAPIService = LocalAPIService.shared @State private var portText = "" + private var statusText: String { + if localAPIService.isRunning { return "Running" } + return localAPIService.config.isEnabled ? "Stopped" : "Disabled" + } + var body: some View { Form { Section("Local API") { @@ -2923,7 +2918,7 @@ private struct LocalAPISettingsView: View { .keyboardType(.numberPad) LabeledContent("Address", value: localAPIService.address) - LabeledContent("Status", value: localAPIService.isRunning ? "Running" : (localAPIService.config.isEnabled ? "Stopped" : "Disabled")) + LabeledContent("Status", value: statusText) LabeledContent("Token", value: localAPIService.maskedToken) if let statusMessage = localAPIService.statusMessage { @@ -3339,7 +3334,7 @@ private struct DataPortabilitySettingsView: View { Button("Replace", role: .destructive) { applyPendingImport() } - Button("Cancel", role: .cancel) {} + Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ } } message: { Text("Replace mode overwrites local data covered by the imported file and may remove items that are only on this device.") } @@ -3347,7 +3342,7 @@ private struct DataPortabilitySettingsView: View { get: { pendingImportError != nil }, set: { if !$0 { pendingImportError = nil } } )) { - Button("OK", role: .cancel) {} + Button("OK", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ } } message: { Text(pendingImportError ?? "The import could not be completed.") } @@ -3585,7 +3580,7 @@ private struct DataManagementSettingsView: View { Button("Clear", role: .destructive) { viewModel.clearHistory() } - Button("Cancel", role: .cancel) {} + Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ } } message: { Text("This removes saved lookup snapshots and clears monitoring run history on this device.") } @@ -3593,7 +3588,7 @@ private struct DataManagementSettingsView: View { Button("Clear", role: .destructive) { viewModel.clearLookupCache() } - Button("Cancel", role: .cancel) {} + Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ } } message: { Text("This clears the in-memory lookup cache and cancels any cached in-flight work.") } @@ -3601,7 +3596,7 @@ private struct DataManagementSettingsView: View { Button("Clear", role: .destructive) { viewModel.clearWorkflows() } - Button("Cancel", role: .cancel) {} + Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ } } message: { Text("This removes saved workflows only. History, tracked domains, and saved reports stay intact.") } @@ -3609,12 +3604,12 @@ private struct DataManagementSettingsView: View { Button("Clear", role: .destructive) { viewModel.clearTrackedDomains() } - Button("Cancel", role: .cancel) {} + Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ } } message: { Text("This removes the watchlist and clears monitoring run history. History and workflows stay intact.") } .alert("Delete All Data?", isPresented: $showDeleteAllConfirmation) { - Button("Cancel", role: .cancel) {} + Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ } Button("Delete All Data", role: .destructive) { deleteAllData() } @@ -3625,7 +3620,7 @@ private struct DataManagementSettingsView: View { get: { deleteAllErrorMessage != nil }, set: { if !$0 { deleteAllErrorMessage = nil } } )) { - Button("OK", role: .cancel) {} + Button("OK", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ } } message: { Text(deleteAllErrorMessage ?? "The local data reset could not be completed.") } @@ -3745,6 +3740,36 @@ private struct DataImportPreviewSheet: View { } } +extension ChangeImpactClassification { + var color: Color { + switch self { + case .informational: return .secondary + case .warning: return .yellow + case .critical: return .red + } + } +} + +extension TLSGrade { + var tone: ResultTone { + switch self { + case .a: return .success + case .f: return .failure + default: return .warning + } + } +} + +extension EmailSecurityGrade { + var tone: ResultTone { + switch self { + case .a: return .success + case .f: return .failure + default: return .warning + } + } +} + #Preview { ContentView(viewModel: DomainViewModel()) } diff --git a/DomainDig/DashboardView.swift b/DomainDig/DashboardView.swift index 087f080..398ff3e 100644 --- a/DomainDig/DashboardView.swift +++ b/DomainDig/DashboardView.swift @@ -239,22 +239,26 @@ struct DashboardView: View { let warningCount = states.filter { $0.health == .warning }.count let title: String let color: Color + let systemImage: String if criticalCount > 0 { title = "\(criticalCount) critical" color = .red + systemImage = "exclamationmark.octagon.fill" } else if warningCount > 0 { title = "\(warningCount) warning" color = .yellow + systemImage = "exclamationmark.triangle.fill" } else { title = "Healthy" color = .green + systemImage = "checkmark.circle.fill" } return AppStatusBadgeView( model: .init( title: title, - systemImage: criticalCount > 0 ? "exclamationmark.octagon.fill" : (warningCount > 0 ? "exclamationmark.triangle.fill" : "checkmark.circle.fill"), + systemImage: systemImage, foregroundColor: color, backgroundColor: color.opacity(0.16) ) diff --git a/DomainDig/DomainAvailabilityService.swift b/DomainDig/DomainAvailabilityService.swift index b671fbc..1d112e1 100644 --- a/DomainDig/DomainAvailabilityService.swift +++ b/DomainDig/DomainAvailabilityService.swift @@ -47,6 +47,7 @@ struct DomainAvailabilityService { return .registered } } catch { + // No A record (or the lookup failed); fall through to the NS probe. } do { diff --git a/DomainDig/DomainDigIntents.swift b/DomainDig/DomainDigIntents.swift index c0c73cc..8b46aa7 100644 --- a/DomainDig/DomainDigIntents.swift +++ b/DomainDig/DomainDigIntents.swift @@ -149,7 +149,7 @@ struct RunSweepIntent: AppIntent { final class DomainDigIntentRouter { static let shared = DomainDigIntentRouter() var pendingAction: DomainDigDeepLink.Action? - private init() {} + private init() { /* Singleton; use the shared instance. */ } } /// Exposes DomainDig intents to Spotlight and Siri with invocation phrases. diff --git a/DomainDig/DomainInsightEngine.swift b/DomainDig/DomainInsightEngine.swift index f1de9c0..6f8edeb 100644 --- a/DomainDig/DomainInsightEngine.swift +++ b/DomainDig/DomainInsightEngine.swift @@ -412,7 +412,7 @@ enum DomainInsightEngine { if hasMXRecords(snapshot), snapshot.emailSecurity == nil { patterns.append("MX records exist without corresponding email security records") } - if snapshot.sslInfo != nil && !(snapshot.dnsSections.first(where: { $0.recordType == .CAA })?.records.isEmpty == false) { + if snapshot.sslInfo != nil, snapshot.dnsSections.first(where: { $0.recordType == .CAA })?.records.isEmpty != false { patterns.append("TLS is active but no CAA record was found") } @@ -427,9 +427,16 @@ enum DomainInsightEngine { let dmarcFound = result.dmarc.found let dmarcStrict = isStrictDMARC(result.dmarc.value) + let dmarcReason: String + if !dmarcFound { + dmarcReason = "DMARC missing" + } else { + dmarcReason = dmarcStrict ? "DMARC policy is strict" : "DMARC policy is not strict" + } + let reasons = [ spfFound ? "SPF present" : "SPF missing", - dmarcFound ? (dmarcStrict ? "DMARC policy is strict" : "DMARC policy is not strict") : "DMARC missing", + dmarcReason, dkimFound ? "DKIM present" : "DKIM not detected" ] diff --git a/DomainDig/DomainMonitoringService.swift b/DomainDig/DomainMonitoringService.swift index 917ede0..eb6a581 100644 --- a/DomainDig/DomainMonitoringService.swift +++ b/DomainDig/DomainMonitoringService.swift @@ -85,7 +85,7 @@ final class DomainMonitoringScheduler { private var isRegistered = false - private init() {} + private init() { /* Singleton; use the shared instance. */ } func registerBackgroundTask() { #if canImport(BackgroundTasks) @@ -613,8 +613,8 @@ final class DomainMonitoringService { private func processAdaptiveMonitoringState( trackedDomain: TrackedDomain, - snapshot: LookupSnapshot, - previousSnapshot: LookupSnapshot?, + snapshot _: LookupSnapshot, + previousSnapshot _: LookupSnapshot?, alertDescriptor: MonitoringAlertDescriptor?, settings: MonitoringSettings, notificationsAuthorized: Bool, diff --git a/DomainDig/DomainViewModel.swift b/DomainDig/DomainViewModel.swift index 674efdf..256c1e1 100644 --- a/DomainDig/DomainViewModel.swift +++ b/DomainDig/DomainViewModel.swift @@ -1280,19 +1280,7 @@ final class DomainViewModel { } func clearPresentedResults() { - lookupTask?.cancel() - customPortScanTask?.cancel() - batchTask?.cancel() - hasRun = false - searchedDomain = "" - lastLookupDurationMs = nil - currentDiffSections = [] - currentChangeSummary = nil - ownershipDiff = [] - currentReport = nil - refreshingTrackedDomainID = nil - clearBatchState() - clearLookupState() + reset() } func run() { @@ -1857,7 +1845,7 @@ final class DomainViewModel { portabilityStatusMessage = result.summary CloudSyncService.shared.markAppSettingsChanged() CloudSyncService.shared.markMonitoringSettingsChanged(localActivationConfirmed: monitoringSettings.isEnabled) - CloudSyncService.shared.scheduleSyncIfNeeded(trigger: .import) + CloudSyncService.shared.scheduleSyncIfNeeded(trigger: .imported) return result } @@ -2174,10 +2162,7 @@ final class DomainViewModel { case let .success(sections): dnsSections = sections dnsError = nil - case let .empty(message): - dnsSections = [] - dnsError = message - case let .error(message): + case let .empty(message), let .error(message): dnsSections = [] dnsError = message } @@ -2199,10 +2184,7 @@ final class DomainViewModel { case let .success(info): sslInfo = info sslError = nil - case let .empty(message): - sslInfo = nil - sslError = message - case let .error(message): + case let .empty(message), let .error(message): sslInfo = nil sslError = message } @@ -2228,15 +2210,7 @@ final class DomainViewModel { httpProtocol = headersResult.httpProtocol http3Advertised = headersResult.http3Advertised httpHeadersError = nil - case let .empty(message): - httpHeaders = [] - httpSecurityGrade = nil - httpStatusCode = nil - httpResponseTimeMs = nil - httpProtocol = nil - http3Advertised = false - httpHeadersError = message - case let .error(message): + case let .empty(message), let .error(message): httpHeaders = [] httpSecurityGrade = nil httpStatusCode = nil @@ -2255,10 +2229,7 @@ final class DomainViewModel { case let .success(results): reachabilityResults = results reachabilityError = nil - case let .empty(message): - reachabilityResults = [] - reachabilityError = message - case let .error(message): + case let .empty(message), let .error(message): reachabilityResults = [] reachabilityError = message } @@ -2272,10 +2243,7 @@ final class DomainViewModel { case let .success(emailResult): emailSecurity = emailResult emailSecurityError = nil - case let .empty(message): - emailSecurity = nil - emailSecurityError = message - case let .error(message): + case let .empty(message), let .error(message): emailSecurity = nil emailSecurityError = message } @@ -2289,10 +2257,7 @@ final class DomainViewModel { case let .success(ownership): ownershipResult = ownership ownershipError = nil - case let .empty(message): - ownershipResult = nil - ownershipError = message - case let .error(message): + case let .empty(message), let .error(message): ownershipResult = nil ownershipError = message } @@ -2306,10 +2271,7 @@ final class DomainViewModel { case let .success(record): ptrRecord = record ptrError = nil - case let .empty(message): - ptrRecord = nil - ptrError = message - case let .error(message): + case let .empty(message), let .error(message): ptrRecord = nil ptrError = message } @@ -2323,10 +2285,7 @@ final class DomainViewModel { case let .success(hops): redirectChain = hops redirectChainError = nil - case let .empty(message): - redirectChain = [] - redirectChainError = message - case let .error(message): + case let .empty(message), let .error(message): redirectChain = [] redirectChainError = message } @@ -2340,10 +2299,7 @@ final class DomainViewModel { case let .success(results): subdomains = results subdomainsError = nil - case let .empty(message): - subdomains = [] - subdomainsError = message - case let .error(message): + case let .empty(message), let .error(message): subdomains = [] subdomainsError = message } @@ -2358,11 +2314,7 @@ final class DomainViewModel { guard !Task.isCancelled, isCurrentLookup(lookupID) else { return } portScanResults = enrichedResults portScanError = nil - case let .empty(message): - guard !Task.isCancelled, isCurrentLookup(lookupID) else { return } - portScanResults = [] - portScanError = message - case let .error(message): + case let .empty(message), let .error(message): guard !Task.isCancelled, isCurrentLookup(lookupID) else { return } portScanResults = [] portScanError = message @@ -2377,10 +2329,7 @@ final class DomainViewModel { case let .success(geolocation): ipGeolocation = geolocation ipGeolocationError = nil - case let .empty(message): - ipGeolocation = nil - ipGeolocationError = message - case let .error(message): + case let .empty(message), let .error(message): ipGeolocation = nil ipGeolocationError = message } @@ -2408,10 +2357,7 @@ final class DomainViewModel { customPortResults = results customPortScanError = nil _ = saveHistoryEntry(replaceLatest: true) - case let .empty(message): - customPortResults = [] - customPortScanError = message - case let .error(message): + case let .empty(message), let .error(message): customPortResults = [] customPortScanError = message } @@ -4024,7 +3970,7 @@ final class DomainViewModel { static func summaryFields(from snapshot: LookupSnapshot) -> [SummaryFieldViewData] { [ - SummaryFieldViewData(label: "Domain", value: snapshot.domain.nonEmpty ?? "Unavailable", tone: .primary), + SummaryFieldViewData(label: "Domain", value: snapshot.domain.nilIfEmpty ?? "Unavailable", tone: .primary), SummaryFieldViewData(label: "Observed IP", value: primaryIPAddress(from: snapshot) ?? "Unavailable", tone: .primary), SummaryFieldViewData(label: "Observed Redirect", value: finalRedirectTarget(from: snapshot) ?? "Unavailable", tone: .secondary), SummaryFieldViewData(label: "Inference", value: availabilityInference(from: snapshot), tone: availabilityTone(snapshot.availabilityResult?.status)), @@ -4780,10 +4726,7 @@ final class DomainViewModel { case let .success(pricing): domainPricing = pricing domainPricingError = nil - case let .empty(message): - domainPricing = nil - domainPricingError = conciseExternalMessage(message, fallback: "External pricing unavailable") - case let .error(message): + case let .empty(message), let .error(message): domainPricing = nil domainPricingError = conciseExternalMessage(message, fallback: "External pricing unavailable") } @@ -4803,10 +4746,7 @@ final class DomainViewModel { case let .success(result): reputation = result reputationError = nil - case let .empty(message): - reputation = nil - reputationError = conciseExternalMessage(message, fallback: "Reputation check unavailable") - case let .error(message): + case let .empty(message), let .error(message): reputation = nil reputationError = conciseExternalMessage(message, fallback: "Reputation check unavailable") } @@ -4865,10 +4805,6 @@ final class DomainViewModel { } private extension String { - var nonEmpty: String? { - isEmpty ? nil : self - } - var nilIfEmpty: String? { isEmpty ? nil : self } diff --git a/DomainDig/HistoryView.swift b/DomainDig/HistoryView.swift index 316fcce..5350168 100644 --- a/DomainDig/HistoryView.swift +++ b/DomainDig/HistoryView.swift @@ -115,7 +115,7 @@ struct HistoryView: View { Button("Clear All", role: .destructive) { viewModel.clearHistory() } - Button("Cancel", role: .cancel) {} + Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ } } message: { Text("This will delete all saved history entries. This cannot be undone.") } @@ -314,7 +314,7 @@ struct HistoryDetailView: View { isCloudflareProxied: snapshot.httpHeaders.contains(where: { $0.name.lowercased() == "cf-ray" }), customPortsExpanded: .constant(false), customPortInput: .constant(""), - onScanCustomPorts: {} + onScanCustomPorts: { /* Historical snapshot; re-scanning is not offered here. */ } ) .padding(.top, appDensity.metrics.sectionSpacing) } @@ -344,7 +344,7 @@ struct HistoryDetailView: View { Button("Run with Snapshot Resolver") { viewModel.rerunLookup(from: entry, useSnapshotResolver: true) } - Button("Cancel", role: .cancel) {} + Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ } } message: { Text(viewModel.resolverMismatchNote(for: entry) ?? "Choose how to reproduce this snapshot.") } diff --git a/DomainDig/IntegrationService.swift b/DomainDig/IntegrationService.swift index 23085d3..de24c23 100644 --- a/DomainDig/IntegrationService.swift +++ b/DomainDig/IntegrationService.swift @@ -768,9 +768,7 @@ private final class SMTPChannel { connection.stateUpdateHandler = { [weak self] state in switch state { case .ready: - DispatchQueue.global(qos: .utility).async { - self?.startReceiveLoop() - } + self?.scheduleReceiveLoop() continuation.resume() case .failed(let error): continuation.resume(throwing: error) @@ -782,6 +780,12 @@ private final class SMTPChannel { } } + private func scheduleReceiveLoop() { + DispatchQueue.global(qos: .utility).async { [weak self] in + self?.startReceiveLoop() + } + } + func cancel() { connection.cancel() } diff --git a/DomainDig/LocalNotificationService.swift b/DomainDig/LocalNotificationService.swift index d3474e3..3648cb8 100644 --- a/DomainDig/LocalNotificationService.swift +++ b/DomainDig/LocalNotificationService.swift @@ -5,7 +5,7 @@ import UserNotifications final class LocalNotificationService { static let shared = LocalNotificationService() - private init() {} + private init() { /* Singleton; use the shared instance. */ } static let domainUserInfoKey = "domain" static let domainCategoryIdentifier = "domain-event" @@ -188,14 +188,14 @@ private final class NotificationCenterDelegate: NSObject, UNUserNotificationCent static let shared = NotificationCenterDelegate() func userNotificationCenter( - _ center: UNUserNotificationCenter, - willPresent notification: UNNotification + _: UNUserNotificationCenter, + willPresent _: UNNotification ) async -> UNNotificationPresentationOptions { [.banner, .list, .sound] } func userNotificationCenter( - _ center: UNUserNotificationCenter, + _: UNUserNotificationCenter, didReceive response: UNNotificationResponse ) async { let userInfo = response.notification.request.content.userInfo @@ -204,10 +204,9 @@ private final class NotificationCenterDelegate: NSObject, UNUserNotificationCent else { return } let action: DomainDigDeepLink.Action - switch response.actionIdentifier { - case LocalNotificationService.reinspectActionIdentifier: + if response.actionIdentifier == LocalNotificationService.reinspectActionIdentifier { action = .inspect(domain) - default: + } else { // Default tap: open the tracked domain's detail. action = .detail(domain) } diff --git a/DomainDig/Models.swift b/DomainDig/Models.swift index 6989cba..d83dc79 100644 --- a/DomainDig/Models.swift +++ b/DomainDig/Models.swift @@ -244,8 +244,14 @@ struct DomainChangeSummary: Codable, Equatable { changedSections = try container.decodeIfPresent([String].self, forKey: .changedSections) ?? [] generatedAt = try container.decode(Date.self, forKey: .generatedAt) severity = try container.decodeIfPresent(ChangeSeverity.self, forKey: .severity) ?? (hasChanges ? .medium : .low) + let defaultImpact: ChangeImpactClassification + if severity == .high { + defaultImpact = .critical + } else { + defaultImpact = hasChanges ? .warning : .informational + } impactClassification = try container.decodeIfPresent(ChangeImpactClassification.self, forKey: .impactClassification) - ?? (severity == .high ? .critical : (hasChanges ? .warning : .informational)) + ?? defaultImpact message = try container.decodeIfPresent(String.self, forKey: .message) ?? (changedSections.isEmpty ? "No meaningful changes" : changedSections.joined(separator: " • ")) observedFacts = try container.decodeIfPresent([String].self, forKey: .observedFacts) ?? [] @@ -562,12 +568,10 @@ enum DomainClassificationKind: String, Codable, CaseIterable, Sendable { case unknown var title: String { - switch self { - case .staticSite: + if self == .staticSite { return "Static" - default: - return rawValue.capitalized } + return rawValue.capitalized } } diff --git a/DomainDig/PortScanService.swift b/DomainDig/PortScanService.swift index 1f3e649..808b38a 100644 --- a/DomainDig/PortScanService.swift +++ b/DomainDig/PortScanService.swift @@ -89,24 +89,7 @@ struct PortScanService { switch state { case .ready: connection.receive(minimumIncompleteLength: 1, maximumLength: 256) { data, _, _, error in - guard error == nil, - let data, - !data.isEmpty, - let rawBanner = String(data: data, encoding: .utf8) else { - context.finish(with: nil) - return - } - - let printableBanner = rawBanner.filter { character in - guard let scalar = character.unicodeScalars.first, - character.unicodeScalars.count == 1 else { - return false - } - return (32...126).contains(scalar.value) - } - - let banner = String(printableBanner.prefix(80)) - context.finish(with: banner.isEmpty ? nil : banner) + context.finish(with: printableBanner(from: data, error: error)) } case .failed, .cancelled: context.finish(with: nil) @@ -123,6 +106,26 @@ struct PortScanService { } } + private static func printableBanner(from data: Data?, error: Error?) -> String? { + guard error == nil, + let data, + !data.isEmpty, + let rawBanner = String(data: data, encoding: .utf8) else { + return nil + } + + let printable = rawBanner.filter { character in + guard let scalar = character.unicodeScalars.first, + character.unicodeScalars.count == 1 else { + return false + } + return (32...126).contains(scalar.value) + } + + let banner = String(printable.prefix(80)) + return banner.isEmpty ? nil : banner + } + private static func probe(domain: String, port: UInt16) async -> PortProbeResult { await probe(domain: domain, port: port, timeout: 1.5) } diff --git a/DomainDig/PurchaseService.swift b/DomainDig/PurchaseService.swift index d31afc7..4ab8ef4 100644 --- a/DomainDig/PurchaseService.swift +++ b/DomainDig/PurchaseService.swift @@ -288,12 +288,10 @@ final class PurchaseService { private func storeMessage(for error: Error, fallback: String) -> String { if let storeKitError = error as? StoreKitError { - switch storeKitError { - case .networkError: + if case .networkError = storeKitError { return "The App Store is offline right now." - default: - return fallback } + return fallback } let message = error.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/DomainDig/SSLCheckService.swift b/DomainDig/SSLCheckService.swift index 73c00bd..b1affa8 100644 --- a/DomainDig/SSLCheckService.swift +++ b/DomainDig/SSLCheckService.swift @@ -156,10 +156,9 @@ private enum DERCertificateParser { var offset = tbsContent.contentStart // Skip version (explicit tag [0]) if present - if offset < bytes.count && (bytes[offset] & 0xE0) == 0xA0 { - if let tagLen = readTagAndLength(bytes, offset: offset) { - offset = tagLen.contentStart + tagLen.length - } + if offset < bytes.count, (bytes[offset] & 0xE0) == 0xA0, + let tagLen = readTagAndLength(bytes, offset: offset) { + offset = tagLen.contentStart + tagLen.length } // Skip serialNumber @@ -199,11 +198,10 @@ private enum DERCertificateParser { // Extensions are in an explicit tag [3] while offset < tbsContent.contentStart + tbsContent.length { if bytes[offset] == 0xA3 { - if let extWrapper = readTagAndLength(bytes, offset: offset) { - // Inside is a SEQUENCE of SEQUENCE extensions - if let extsSeq = readTagAndLength(bytes, offset: extWrapper.contentStart) { - result.subjectAltNames = extractSANs(bytes, sequenceStart: extsSeq.contentStart, length: extsSeq.length) - } + // Inside the wrapper is a SEQUENCE of SEQUENCE extensions + if let extWrapper = readTagAndLength(bytes, offset: offset), + let extsSeq = readTagAndLength(bytes, offset: extWrapper.contentStart) { + result.subjectAltNames = extractSANs(bytes, sequenceStart: extsSeq.contentStart, length: extsSeq.length) } break } @@ -267,27 +265,25 @@ private enum DERCertificateParser { if oidBytes == sanOID { var valuePos = oidTL.contentStart + oidTL.length // Skip optional critical BOOLEAN - if valuePos < extEnd && bytes[valuePos] == 0x01 { - if let boolTL = readTagAndLength(bytes, offset: valuePos) { - valuePos = boolTL.contentStart + boolTL.length - } + if valuePos < extEnd, bytes[valuePos] == 0x01, + let boolTL = readTagAndLength(bytes, offset: valuePos) { + valuePos = boolTL.contentStart + boolTL.length } // The value is an OCTET STRING wrapping a SEQUENCE of GeneralNames - if let octetTL = readTagAndLength(bytes, offset: valuePos) { - if let sanSeq = readTagAndLength(bytes, offset: octetTL.contentStart) { - let sanEnd = sanSeq.contentStart + sanSeq.length - var sanPos = sanSeq.contentStart - while sanPos < sanEnd { - guard let nameTL = readTagAndLength(bytes, offset: sanPos) else { break } - // Context tag [2] = dNSName (IA5String) - if (bytes[sanPos] & 0x1F) == 2 { - let nameBytes = bytes[nameTL.contentStart..<nameTL.contentStart + nameTL.length] - if let name = String(bytes: nameBytes, encoding: .ascii) { - sans.append(name) - } + if let octetTL = readTagAndLength(bytes, offset: valuePos), + let sanSeq = readTagAndLength(bytes, offset: octetTL.contentStart) { + let sanEnd = sanSeq.contentStart + sanSeq.length + var sanPos = sanSeq.contentStart + while sanPos < sanEnd { + guard let nameTL = readTagAndLength(bytes, offset: sanPos) else { break } + // Context tag [2] = dNSName (IA5String) + if (bytes[sanPos] & 0x1F) == 2 { + let nameBytes = bytes[nameTL.contentStart..<nameTL.contentStart + nameTL.length] + if let name = String(bytes: nameBytes, encoding: .ascii) { + sans.append(name) } - sanPos = nameTL.contentStart + nameTL.length } + sanPos = nameTL.contentStart + nameTL.length } } } @@ -386,19 +382,19 @@ enum SSLError: LocalizedError { final class SSLSessionDelegate: NSObject, URLSessionDelegate, @unchecked Sendable { private let lock = NSLock() - private var _serverTrust: SecTrust? - private var _tlsMetadata: TLSMetadata? + private var storedServerTrust: SecTrust? + private var storedTLSMetadata: TLSMetadata? var serverTrust: SecTrust? { lock.lock() defer { lock.unlock() } - return _serverTrust + return storedServerTrust } fileprivate var tlsMetadata: TLSMetadata? { lock.lock() defer { lock.unlock() } - return _tlsMetadata + return storedTLSMetadata } func urlSession( @@ -413,7 +409,7 @@ final class SSLSessionDelegate: NSObject, URLSessionDelegate, @unchecked Sendabl } lock.lock() - _serverTrust = trust + storedServerTrust = trust lock.unlock() let credential = URLCredential(trust: trust) @@ -439,7 +435,7 @@ extension SSLSessionDelegate: URLSessionTaskDelegate { } lock.lock() - _tlsMetadata = TLSMetadata(tlsVersion: tlsVersion, cipherSuite: cipherSuite) + storedTLSMetadata = TLSMetadata(tlsVersion: tlsVersion, cipherSuite: cipherSuite) lock.unlock() } diff --git a/DomainDig/ScheduledReportService.swift b/DomainDig/ScheduledReportService.swift index c1db3de..3aee834 100644 --- a/DomainDig/ScheduledReportService.swift +++ b/DomainDig/ScheduledReportService.swift @@ -106,10 +106,10 @@ struct ScheduledReportOutcome { final class ScheduledReportService { static let shared = ScheduledReportService() - private init() {} + private init() { /* Singleton; use the shared instance. */ } @discardableResult - func generateReport(trigger: MonitoringRunTrigger, requireEnabledSetting: Bool) async -> ScheduledReportOutcome { + func generateReport(trigger _: MonitoringRunTrigger, requireEnabledSetting: Bool) async -> ScheduledReportOutcome { var settings = ScheduledReportStorage.loadSettings() guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else { @@ -191,7 +191,7 @@ final class ScheduledReportScheduler { private var isRegistered = false - private init() {} + private init() { /* Singleton; use the shared instance. */ } func registerBackgroundTask() { #if canImport(BackgroundTasks) diff --git a/DomainDig/SweepActivityController.swift b/DomainDig/SweepActivityController.swift index 26fcb2d..a00bb2f 100644 --- a/DomainDig/SweepActivityController.swift +++ b/DomainDig/SweepActivityController.swift @@ -8,7 +8,7 @@ final class SweepActivityController { private var activity: Activity<SweepActivityAttributes>? - private init() {} + private init() { /* Singleton; use the shared instance. */ } func begin(title: String, total: Int) { guard ActivityAuthorizationInfo().areActivitiesEnabled else { return } diff --git a/DomainDig/WatchlistView.swift b/DomainDig/WatchlistView.swift index 5670e5e..389ae91 100644 --- a/DomainDig/WatchlistView.swift +++ b/DomainDig/WatchlistView.swift @@ -169,13 +169,13 @@ struct WatchlistView: View { shareTrackedDomains(format: .pdf) } } else { - Button("CSV Export • Available in Pro") {} + Button("CSV Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ } .disabled(true) - Button("JSON Export • Available in Pro") {} + Button("JSON Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ } .disabled(true) - Button("Markdown Export • Available in Pro") {} + Button("Markdown Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ } .disabled(true) - Button("PDF Export • Available in Pro") {} + Button("PDF Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ } .disabled(true) } } label: { @@ -250,7 +250,7 @@ struct WatchlistView: View { Button("Save") { viewModel.saveCurrentWatchlistView(name: newSavedViewName) } - Button("Cancel", role: .cancel) {} + Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ } } message: { Text("Saves the current tag, filter, and sort as a reusable preset.") } @@ -736,7 +736,7 @@ struct TrackedDomainDetailView: View { viewModel.rerunInspection(for: liveTrackedDomain, useSnapshotResolver: true) } } - Button("Cancel", role: .cancel) {} + Button("Cancel", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ } } message: { Text(viewModel.resolverMismatchNote(for: liveTrackedDomain) ?? "Choose how to reproduce the most recent snapshot.") } diff --git a/DomainDig/WorkflowsView.swift b/DomainDig/WorkflowsView.swift index 3cfca42..ce889fb 100644 --- a/DomainDig/WorkflowsView.swift +++ b/DomainDig/WorkflowsView.swift @@ -253,13 +253,13 @@ struct WorkflowDetailView: View { shareWorkflowResults(format: .pdf) } } else { - Button("CSV Export • Available in Pro") {} + Button("CSV Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ } .disabled(true) - Button("JSON Export • Available in Pro") {} + Button("JSON Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ } .disabled(true) - Button("Markdown Export • Available in Pro") {} + Button("Markdown Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ } .disabled(true) - Button("PDF Export • Available in Pro") {} + Button("PDF Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ } .disabled(true) } } label: { @@ -554,13 +554,13 @@ struct WorkflowRunSummaryView: View { share(format: .pdf) } } else { - Button("CSV Export • Available in Pro") {} + Button("CSV Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ } .disabled(true) - Button("JSON Export • Available in Pro") {} + Button("JSON Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ } .disabled(true) - Button("Markdown Export • Available in Pro") {} + Button("Markdown Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ } .disabled(true) - Button("PDF Export • Available in Pro") {} + Button("PDF Export • Available in Pro") { /* Inert: disabled Pro upsell affordance. */ } .disabled(true) } } label: { |
