diff options
Diffstat (limited to 'DomainDig/ContentView.swift')
| -rw-r--r-- | DomainDig/ContentView.swift | 1115 |
1 files changed, 1 insertions, 1114 deletions
diff --git a/DomainDig/ContentView.swift b/DomainDig/ContentView.swift index 3b7e759..254bc99 100644 --- a/DomainDig/ContentView.swift +++ b/DomainDig/ContentView.swift @@ -1324,1119 +1324,6 @@ struct TrackedDomainDetailHeaderView: View { } } -struct DomainSectionView: View { - @Environment(\.appDensity) private var appDensity - @Binding var isCollapsed: Bool - let rows: [InfoRowViewData] - let suggestions: [DomainSuggestionViewData] - let showSuggestions: Bool - let availabilityLoading: Bool - let suggestionsLoading: Bool - let provenance: SectionProvenance? - let confidence: ConfidenceLevel? - let snapshotNote: String? - let trackedDomain: TrackedDomain? - let workflows: [DomainWorkflow] - let trackingLimitMessage: String? - let pricingLoading: Bool - let pricingError: String? - let showsPricingPlaceholder: Bool - let onTrack: () -> Void - let onTogglePinned: () -> Void - let onEditNote: (() -> Void)? - let onAddToWorkflow: (() -> Void)? - let onOpenWorkflow: ((DomainWorkflow) -> Void)? - let onRunWorkflow: ((DomainWorkflow) -> Void)? - - var body: some View { - CollapsibleSectionView(title: "Domain", isCollapsed: $isCollapsed) { - if let trackedDomain { - HStack(spacing: 8) { - // Icon-only: the header also carries Pin and Note, and the - // full "Tracked" pill compresses at larger text sizes. - // VoiceOver still hears the word via the label. - Image(systemName: "eye.fill") - .font(appDensity.font(.caption)) - .foregroundStyle(Color(.statusPositive)) - .padding(6) - .background(Color(.statusPositiveSurface), in: Circle()) - .fixedSize() - .accessibilityLabel("Tracked") - Button { - onTogglePinned() - } label: { - Image(systemName: trackedDomain.isPinned ? "pin.fill" : "pin") - } - .buttonStyle(.bordered) - .font(appDensity.font(.caption)) - .accessibilityLabel("Pin domain") - .accessibilityValue(trackedDomain.isPinned ? "Pinned" : "Not pinned") - .accessibilityAddTraits(trackedDomain.isPinned ? .isSelected : []) - if let onEditNote { - Button("Note") { - onEditNote() - } - .buttonStyle(.bordered) - .font(appDensity.font(.caption)) - // Never compress into a vertical letter column. - .fixedSize() - } - } - } else { - Button("Track") { - AppHaptics.track() - onTrack() - } - .buttonStyle(.bordered) - .font(appDensity.font(.caption)) - .fixedSize() - } - } content: { - CardView(allowsHorizontalScroll: false) { - SectionTrustMetadataView( - provenance: provenance, - confidence: confidence, - note: snapshotNote == nil ? nil : "Audit note present" - ) - ForEach(rows) { row in - LabeledValueRow(row: row) - } - if let trackedDomain { - TrackedDomainDetailHeaderView(trackedDomain: trackedDomain) - .padding(.top, 4) - } else if let trackingLimitMessage { - MessageRowView(text: trackingLimitMessage, isError: false) - .padding(.top, 4) - } - if let onAddToWorkflow { - Button { - onAddToWorkflow() - } label: { - Label("Add to workflow", systemImage: "plus.rectangle.on.folder") - .font(appDensity.font(.caption)) - } - .buttonStyle(.bordered) - .padding(.top, 4) - } - if !workflows.isEmpty { - VStack(alignment: .leading, spacing: 8) { - Text("Part of workflow") - .font(appDensity.font(.caption)) - .foregroundStyle(Color(.appTextSecondary)) - - ForEach(workflows) { workflow in - HStack { - Text(workflow.name) - .font(appDensity.font(.caption)) - .foregroundStyle(.primary) - Spacer() - if let onOpenWorkflow { - Button("Open") { - onOpenWorkflow(workflow) - } - .buttonStyle(.bordered) - .font(appDensity.font(.caption2)) - } - if let onRunWorkflow { - Button("Run") { - onRunWorkflow(workflow) - } - .buttonStyle(.bordered) - .font(appDensity.font(.caption2)) - } - } - } - } - .padding(.top, 4) - } - if availabilityLoading { - ProgressView("Checking availability…") - .appLoadingStyle() - .padding(.top, 4) - } - if showSuggestions { - Text("Suggestions") - .font(appDensity.font(.caption)) - .foregroundStyle(Color(.appTextSecondary)) - .padding(.top, 4) - if suggestionsLoading { - ProgressView("Checking alternatives…") - .appLoadingStyle() - } else if suggestions.isEmpty { - MessageRowView(text: "No suggestions", isError: false) - } else { - ForEach(suggestions) { suggestion in - HStack { - Text(suggestion.domain) - .font(appDensity.font(.caption)) - .foregroundStyle(.primary) - .textSelection(.enabled) - Spacer() - AppStatusBadgeView(model: AppStatusFactory.availability(suggestion.availabilityStatus)) - } - } - } - } - if pricingLoading { - ProgressView("Loading external pricing…") - .appLoadingStyle() - .padding(.top, 4) - } else if let pricingError { - MessageRowView(text: pricingError, isError: false) - .padding(.top, 4) - } else if showsPricingPlaceholder { - MessageRowView(text: "Pricing signals available in Pro+", isError: false) - .padding(.top, 4) - } - } - } - } -} - -struct OwnershipSectionView: View { - @Environment(\.appDensity) private var appDensity - @Binding var isCollapsed: Bool - let rows: [InfoRowViewData] - let loading: Bool - let error: String? - let provenance: SectionProvenance? - let confidence: ConfidenceLevel? - let showsHistoryPlaceholder: Bool - let history: [DomainOwnershipHistoryEvent] - let historyLoading: Bool - let historyError: String? - let historyCreditStatus: UsageCreditStatus? - let onLoadHistory: (() -> Void)? - - init( - isCollapsed: Binding<Bool>, - rows: [InfoRowViewData], - loading: Bool, - error: String?, - provenance: SectionProvenance?, - confidence: ConfidenceLevel?, - showsHistoryPlaceholder: Bool, - history: [DomainOwnershipHistoryEvent] = [], - historyLoading: Bool = false, - historyError: String? = nil, - historyCreditStatus: UsageCreditStatus? = nil, - onLoadHistory: (() -> Void)? = nil - ) { - _isCollapsed = isCollapsed - self.rows = rows - self.loading = loading - self.error = error - self.provenance = provenance - self.confidence = confidence - self.showsHistoryPlaceholder = showsHistoryPlaceholder - self.history = history - self.historyLoading = historyLoading - self.historyError = historyError - self.historyCreditStatus = historyCreditStatus - self.onLoadHistory = onLoadHistory - } - - var body: some View { - CollapsibleSectionView(title: "Ownership", isCollapsed: $isCollapsed) { - CardView(allowsHorizontalScroll: false) { - SectionTrustMetadataView(provenance: provenance, confidence: confidence) - if loading { - ProgressView("Fetching RDAP ownership…") - .appLoadingStyle() - } else { - ForEach(rows) { row in - LabeledValueRow(row: row) - } - if let error, rows.allSatisfy({ $0.value == "Unavailable" }) { - MessageRowView(text: error, isError: error != "Unavailable") - .padding(.top, 4) - } - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("History") - .font(appDensity.font(.caption)) - .foregroundStyle(Color(.appTextSecondary)) - Spacer() - if let onLoadHistory, history.isEmpty, !historyLoading, !showsHistoryPlaceholder { - Button("Load") { - onLoadHistory() - } - .buttonStyle(.bordered) - .font(appDensity.font(.caption2)) - } - } - if historyLoading { - ProgressView("Loading history…") - .appLoadingStyle() - } else if !history.isEmpty { - ForEach(history) { event in - VStack(alignment: .leading, spacing: 3) { - Text(event.date.formatted(date: .abbreviated, time: .omitted)) - .font(appDensity.font(.caption2)) - .foregroundStyle(Color(.appTextSecondary)) - Text(event.summary) - .font(appDensity.font(.caption)) - Text(event.source) - .font(appDensity.font(.caption2)) - .foregroundStyle(Color(.appTextSecondary)) - } - } - } else if let historyError { - MessageRowView(text: historyError, isError: false) - } else if showsHistoryPlaceholder { - MessageRowView(text: "Ownership history available in Pro+", isError: false) - } - } - } - } - } - } -} - -struct IntelligenceSectionView: View { - @Environment(\.appDensity) private var appDensity - @Binding var isCollapsed: Bool - let report: DomainReport - let showsPlaceholder: Bool - - var body: some View { - CollapsibleSectionView(title: "Data+ Intelligence", isCollapsed: $isCollapsed) { - CardView(allowsHorizontalScroll: false) { - if showsPlaceholder { - MessageRowView(text: "Richer intelligence history, hosting analysis, and risk signals are available in Pro+", isError: false) - } else { - if let provider = report.inferredProvider { - intelligenceBlock(title: "Infrastructure") { - LabeledValueRow(row: .init(label: "Provider", value: provider.name, tone: .primary)) - if !provider.evidence.isEmpty { - MessageRowView(text: provider.evidence.joined(separator: " • "), isError: false) - } - if !report.priorProviders.isEmpty { - LabeledValueRow(row: .init(label: "Prior", value: report.priorProviders.joined(separator: ", "), tone: .secondary)) - } - } - } - if let classification = report.domainClassification { - intelligenceBlock(title: "Classification") { - LabeledValueRow(row: .init(label: "Purpose", value: classification.kind.title, tone: .primary)) - MessageRowView(text: classification.reasons.joined(separator: " • "), isError: false) - } - } - intelligenceBlock(title: "Risk Signals") { - if report.riskSignals.isEmpty { - MessageRowView(text: "No material historical risk signals detected", isError: false) - } else { - ForEach(report.riskSignals.prefix(4)) { signal in - VStack(alignment: .leading, spacing: 3) { - Text(signal.title) - .font(appDensity.font(.caption, weight: .semibold)) - Text(signal.detail) - .font(appDensity.font(.caption2)) - .foregroundStyle(Color(.appTextSecondary)) - } - } - } - } - intelligenceBlock(title: "Ownership History") { - if report.ownershipTransitions.isEmpty { - MessageRowView(text: "No ownership transitions observed locally", isError: false) - } else { - ForEach(report.ownershipTransitions.prefix(4)) { event in - intelligenceEventRow(date: event.date, title: event.summary) - } - } - } - intelligenceBlock(title: "Hosting History") { - if report.hostingTransitions.isEmpty { - MessageRowView(text: "No hosting transitions observed locally", isError: false) - } else { - ForEach(report.hostingTransitions.prefix(4)) { event in - intelligenceEventRow(date: event.date, title: event.summary) - } - } - } - intelligenceBlock(title: "Subdomain Intelligence") { - if report.subdomainHistory.isEmpty { - MessageRowView(text: "No subdomain history available", isError: false) - } else { - ForEach(report.subdomainHistory.prefix(5)) { item in - VStack(alignment: .leading, spacing: 3) { - HStack { - Text(item.hostname) - .font(appDensity.font(.caption)) - Spacer() - if item.isEphemeral { - Text("Ephemeral") - .font(appDensity.font(.caption2)) - .foregroundStyle(Color(.statusWarning)) - } - } - Text("First \(item.firstSeen.formatted(date: .abbreviated, time: .omitted)) • Last \(item.lastSeen.formatted(date: .abbreviated, time: .omitted)) • Seen \(item.recurrenceCount)x") - .font(appDensity.font(.caption2)) - .foregroundStyle(Color(.appTextSecondary)) - } - } - } - } - intelligenceBlock(title: "Timeline") { - if report.intelligenceTimeline.isEmpty { - MessageRowView(text: "No inferred intelligence events yet", isError: false) - } else { - ForEach(report.intelligenceTimeline.prefix(5)) { event in - intelligenceEventRow(date: event.date, title: "\(event.title): \(event.detail)") - } - } - } - } - } - } - } - - @ViewBuilder - private func intelligenceBlock<Content: View>(title: String, @ViewBuilder content: () -> Content) -> some View { - VStack(alignment: .leading, spacing: 8) { - Text(title) - .font(appDensity.font(.subheadline, weight: .semibold)) - .foregroundStyle(Color(.statusInfo)) - content() - } - } - - private func intelligenceEventRow(date: Date, title: String) -> some View { - VStack(alignment: .leading, spacing: 3) { - Text(date.formatted(date: .abbreviated, time: .omitted)) - .font(appDensity.font(.caption2)) - .foregroundStyle(Color(.appTextSecondary)) - Text(title) - .font(appDensity.font(.caption)) - } - } -} - -struct SubdomainsSectionView: View { - @Environment(\.appDensity) private var appDensity - @Binding var isCollapsed: Bool - let rows: [SubdomainRowViewData] - let groups: [SubdomainGroup] - let loading: Bool - let error: String? - let provenance: SectionProvenance? - let confidence: ConfidenceLevel? - let showsExtendedPlaceholder: Bool - let extendedCount: Int - let extendedLoading: Bool - let extendedError: String? - let extendedCreditStatus: UsageCreditStatus? - let onLoadExtended: (() -> Void)? - - init( - isCollapsed: Binding<Bool>, - rows: [SubdomainRowViewData], - groups: [SubdomainGroup], - loading: Bool, - error: String?, - provenance: SectionProvenance?, - confidence: ConfidenceLevel?, - showsExtendedPlaceholder: Bool, - extendedCount: Int = 0, - extendedLoading: Bool = false, - extendedError: String? = nil, - extendedCreditStatus: UsageCreditStatus? = nil, - onLoadExtended: (() -> Void)? = nil - ) { - _isCollapsed = isCollapsed - self.rows = rows - self.groups = groups - self.loading = loading - self.error = error - self.provenance = provenance - self.confidence = confidence - self.showsExtendedPlaceholder = showsExtendedPlaceholder - self.extendedCount = extendedCount - self.extendedLoading = extendedLoading - self.extendedError = extendedError - self.extendedCreditStatus = extendedCreditStatus - self.onLoadExtended = onLoadExtended - } - - var body: some View { - CollapsibleSectionView(title: "Subdomains", isCollapsed: $isCollapsed, subtitle: "\(rows.count) found") { - CardView(allowsHorizontalScroll: false) { - SectionTrustMetadataView(provenance: provenance, confidence: confidence) - if loading { - ProgressView("Checking certificate transparency…") - .appLoadingStyle() - } else if rows.isEmpty { - MessageRowView(text: error ?? "No passive subdomains found", isError: false) - if showsExtendedPlaceholder { - MessageRowView(text: "Extended subdomain discovery available in Pro+", isError: false) - .padding(.top, 4) - } - } else { - if let onLoadExtended, extendedCount == 0, !extendedLoading, !showsExtendedPlaceholder { - Button("Load extended results") { - onLoadExtended() - } - .buttonStyle(.bordered) - .font(appDensity.font(.caption2)) - } - if !groups.isEmpty { - Text("Groups") - .font(appDensity.font(.caption2)) - .foregroundStyle(Color(.appTextSecondary)) - ForEach(groups) { group in - HStack { - Text("\(group.label).*") - .font(appDensity.font(.caption)) - .foregroundStyle(Color(.statusInfo)) - Spacer() - Text("\(group.subdomains.count)") - .font(appDensity.font(.caption2)) - .foregroundStyle(Color(.appTextSecondary)) - } - } - } - ForEach(rows) { row in - HStack(spacing: 8) { - Text(row.hostname) - .font(appDensity.font(.caption)) - .foregroundStyle(.primary) - .textSelection(.enabled) - Spacer() - if row.isInteresting { - Text("Interesting") - .font(.system(.caption2, design: .monospaced)) - .foregroundStyle(Color(.statusWarning)) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color(.statusWarningSurface)) - .clipShape(Capsule()) - } - } - } - if extendedLoading { - ProgressView("Loading extended subdomains…") - .appLoadingStyle() - .padding(.top, 4) - } else if extendedCount > 0 { - MessageRowView(text: "\(extendedCount) extended results included", isError: false) - .padding(.top, 4) - } else if let extendedError { - MessageRowView(text: extendedError, isError: false) - .padding(.top, 4) - } else if showsExtendedPlaceholder { - MessageRowView(text: "Extended subdomain discovery available in Pro+", isError: false) - .padding(.top, 4) - } - } - } - } - } -} - -struct DNSSectionView: View { - @Environment(\.appDensity) private var appDensity - @Binding var isCollapsed: Bool - let dnssecLabel: String? - let patternSummary: DNSPatternSummary? - let sections: [DNSRecordSectionViewData] - let ptrMessage: SectionMessageViewData? - let loading: Bool - let dnsProvenance: SectionProvenance? - let ptrProvenance: SectionProvenance? - let sectionError: String? - let history: [DNSHistoryEvent] - let historyLoading: Bool - let historyError: String? - let showsHistoryPlaceholder: Bool - let historyCreditStatus: UsageCreditStatus? - let onLoadHistory: (() -> Void)? - - init( - isCollapsed: Binding<Bool>, - dnssecLabel: String?, - patternSummary: DNSPatternSummary?, - sections: [DNSRecordSectionViewData], - ptrMessage: SectionMessageViewData?, - loading: Bool, - dnsProvenance: SectionProvenance?, - ptrProvenance: SectionProvenance?, - sectionError: String?, - history: [DNSHistoryEvent] = [], - historyLoading: Bool = false, - historyError: String? = nil, - showsHistoryPlaceholder: Bool = false, - historyCreditStatus: UsageCreditStatus? = nil, - onLoadHistory: (() -> Void)? = nil - ) { - _isCollapsed = isCollapsed - self.dnssecLabel = dnssecLabel - self.patternSummary = patternSummary - self.sections = sections - self.ptrMessage = ptrMessage - self.loading = loading - self.dnsProvenance = dnsProvenance - self.ptrProvenance = ptrProvenance - self.sectionError = sectionError - self.history = history - self.historyLoading = historyLoading - self.historyError = historyError - self.showsHistoryPlaceholder = showsHistoryPlaceholder - self.historyCreditStatus = historyCreditStatus - self.onLoadHistory = onLoadHistory - } - - var body: some View { - CollapsibleSectionView(title: "DNS", isCollapsed: $isCollapsed, subtitle: dnssecLabel) { - if loading { - LoadingCardView(text: "Querying DNS…") - } else if let sectionError, sections.isEmpty { - MessageCardView(text: sectionError, isError: true) - } else { - if dnsProvenance != nil { - CardView(allowsHorizontalScroll: false) { - SectionTrustMetadataView(provenance: dnsProvenance, confidence: nil) - if let patternSummary { - if !patternSummary.providers.isEmpty { - MessageRowView(text: "Providers: \(patternSummary.providers.joined(separator: ", "))", isError: false) - } - if !patternSummary.patterns.isEmpty { - ForEach(Array(patternSummary.patterns.enumerated()), id: \.offset) { _, pattern in - MessageRowView(text: pattern, isError: false) - } - } - } - } - } - ForEach(sections) { section in - CardView { - Text(section.title) - .font(.system(.subheadline, design: .monospaced)) - .fontWeight(.semibold) - .foregroundStyle(Color(.statusInfo)) - - if let message = section.message { - MessageRowView(text: message.text, isError: message.isError) - } - - ForEach(section.rows) { row in - LabeledValueRow(row: row) - } - - if let wildcardTitle = section.wildcardTitle { - Text(wildcardTitle) - .font(.system(.caption, design: .monospaced)) - .foregroundStyle(Color(.appTextSecondary)) - .padding(.top, 4) - ForEach(section.wildcardRows) { row in - LabeledValueRow(row: row) - } - } - } - } - - if let ptrMessage { - CardView { - Text("PTR") - .font(.system(.subheadline, design: .monospaced)) - .fontWeight(.semibold) - .foregroundStyle(Color(.statusInfo)) - SectionTrustMetadataView(provenance: ptrProvenance, confidence: nil) - MessageRowView(text: ptrMessage.text, isError: ptrMessage.isError) - } - } - - CardView(allowsHorizontalScroll: false) { - HStack { - Text("History") - .font(appDensity.font(.subheadline, weight: .semibold)) - .foregroundStyle(Color(.statusInfo)) - Spacer() - if let onLoadHistory, history.isEmpty, !historyLoading, !showsHistoryPlaceholder { - Button("Load") { - onLoadHistory() - } - .buttonStyle(.bordered) - .font(appDensity.font(.caption2)) - } - } - if historyLoading { - ProgressView("Loading DNS history…") - .appLoadingStyle() - } else if !history.isEmpty { - ForEach(history) { event in - VStack(alignment: .leading, spacing: 3) { - Text(event.date.formatted(date: .abbreviated, time: .omitted)) - .font(appDensity.font(.caption2)) - .foregroundStyle(Color(.appTextSecondary)) - Text(event.summary) - .font(appDensity.font(.caption)) - if !event.aRecords.isEmpty { - Text("A: \(event.aRecords.joined(separator: ", "))") - .font(appDensity.font(.caption2)) - .foregroundStyle(Color(.appTextSecondary)) - } - if !event.nameservers.isEmpty { - Text("NS: \(event.nameservers.joined(separator: ", "))") - .font(appDensity.font(.caption2)) - .foregroundStyle(Color(.appTextSecondary)) - } - } - } - } else if let historyError { - MessageRowView(text: historyError, isError: false) - } else if showsHistoryPlaceholder { - MessageRowView(text: "DNS history available in Pro+", isError: false) - } - } - } - } - } -} - -struct WebSectionView: View { - @Environment(\.appDensity) private var appDensity - @Binding var isCollapsed: Bool - let certificateRows: [InfoRowViewData] - let sslInfo: SSLCertificateInfo? - let tlsSummary: WebResultSummary? - let sslLoading: Bool - let sslError: String? - let tlsProvenance: SectionProvenance? - let responseRows: [InfoRowViewData] - let headers: [HTTPHeader] - let headersLoading: Bool - let headersError: String? - let httpProvenance: SectionProvenance? - let redirects: [RedirectHopViewData] - let redirectLoading: Bool - let redirectError: String? - let redirectProvenance: SectionProvenance? - let finalURL: String? - - var body: some View { - CollapsibleSectionView(title: "Web", isCollapsed: $isCollapsed) { - CardView { - HStack { - Text("TLS") - .font(appDensity.font(.subheadline, weight: .semibold)) - .foregroundStyle(Color(.statusInfo)) - Spacer() - if !sslLoading { - AppStatusBadgeView(model: AppStatusFactory.tls(sslInfo: sslInfo, error: sslError)) - } - } - SectionTrustMetadataView(provenance: tlsProvenance, confidence: nil) - if !sslLoading, let tlsSummary { - 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)) - } - } - if sslLoading { - ProgressView("Checking certificate…") - .appLoadingStyle() - } else if let sslError { - MessageRowView(text: sslError, isError: true) - } else { - ForEach(certificateRows) { row in - LabeledValueRow(row: row) - } - if let sslInfo, !sslInfo.subjectAltNames.isEmpty { - Text("SANs") - .font(appDensity.font(.caption2)) - .foregroundStyle(Color(.appTextSecondary)) - ForEach(sslInfo.subjectAltNames, id: \.self) { san in - HStack(alignment: .top, spacing: 8) { - Text(san) - .font(appDensity.font(.caption)) - .lineLimit(nil) - .fixedSize(horizontal: false, vertical: true) - .textSelection(.enabled) - Spacer() - AppCopyButton(value: san, label: "Copy certificate SAN") - } - } - } - } - } - - CardView { - Text("Headers") - .font(appDensity.font(.subheadline, weight: .semibold)) - .foregroundStyle(Color(.statusInfo)) - SectionTrustMetadataView(provenance: httpProvenance, confidence: nil) - if headersLoading { - ProgressView("Fetching headers…") - .appLoadingStyle() - } else if let headersError { - MessageRowView(text: headersError, isError: true) - } else { - ForEach(responseRows) { row in - LabeledValueRow(row: row) - } - if headers.isEmpty { - MessageRowView(text: "No HTTP headers returned", isError: false) - } else { - ForEach(headers) { header in - HStack(alignment: .top, spacing: 4) { - Text(header.name + ":") - .font(appDensity.font(.caption)) - .foregroundStyle(header.isSecurityHeader ? Color(.statusWarning) : Color(.statusInfo)) - Text(header.value) - .font(appDensity.font(.caption)) - .foregroundStyle(.primary) - .textSelection(.enabled) - } - } - } - } - } - - CardView { - HStack { - Text("Redirects") - .font(appDensity.font(.subheadline, weight: .semibold)) - .foregroundStyle(Color(.statusInfo)) - Spacer() - if let finalURL { - AppCopyButton(value: finalURL, label: "Copy redirect URL") - } - } - SectionTrustMetadataView(provenance: redirectProvenance, confidence: nil) - if redirectLoading { - ProgressView("Tracing redirects…") - .appLoadingStyle() - } else if let redirectError { - MessageRowView(text: redirectError, isError: true) - } else if redirects.isEmpty { - MessageRowView(text: "No redirect data available", isError: false) - } else { - if let finalURL { - LabeledValueRow(row: InfoRowViewData(label: "Final URL", value: finalURL, tone: .secondary)) - } - ForEach(redirects) { redirect in - HStack(alignment: .top, spacing: 6) { - Text(redirect.stepLabel) - .font(appDensity.font(.caption)) - .foregroundStyle(Color(.appTextSecondary)) - .frame(width: 16, alignment: .trailing) - Text(redirect.statusCode) - .font(appDensity.font(.caption)) - .foregroundStyle(Color(.statusInfo)) - .frame(width: 36, alignment: .leading) - Text(redirect.url) - .font(appDensity.font(.caption)) - .textSelection(.enabled) - AppCopyButton(value: redirect.url, label: "Copy redirect URL") - if redirect.isFinal { - Text("(final)") - .font(appDensity.font(.caption2)) - .foregroundStyle(Color(.appTextSecondary)) - } - } - } - } - } - } - } - - private func isTLSHighlightError(_ highlight: String) -> Bool { - let normalized = highlight.lowercased() - if normalized.contains("no weak tls indicators were detected") { - return false - } - return normalized.contains("expires") - || normalized.contains("weak") - || normalized.contains("tls 1.0") - || normalized.contains("tls 1.1") - } -} - -struct EmailSectionView: View { - @Environment(\.appDensity) private var appDensity - @Binding var isCollapsed: Bool - let rows: [EmailRowViewData] - let assessment: EmailSecuritySummary? - let loading: Bool - let provenance: SectionProvenance? - let confidence: ConfidenceLevel? - let error: String? - - var body: some View { - CollapsibleSectionView(title: "Email", isCollapsed: $isCollapsed) { - CardView { - SectionTrustMetadataView(provenance: provenance, confidence: confidence) - HStack { - Spacer() - AppStatusBadgeView(model: AppStatusFactory.email(nil, error: error)) - .opacity(loading ? 0 : 1) - } - if let assessment, let grade = assessment.grade { - LabeledValueRow(row: InfoRowViewData(label: "Grade", value: grade.rawValue, tone: grade.tone)) - if !assessment.reasons.isEmpty { - Text(assessment.reasons.joined(separator: " | ")) - .font(appDensity.font(.caption2)) - .foregroundStyle(Color(.appTextSecondary)) - } - } - if loading { - ProgressView("Checking email records…") - .appLoadingStyle() - } else if let error { - MessageRowView(text: error, isError: true) - } else if rows.isEmpty { - MessageRowView(text: "No email security records found", isError: false) - } else { - ForEach(rows) { row in - VStack(alignment: .leading, spacing: 4) { - HStack(spacing: 8) { - Text(row.label) - .font(appDensity.font(.caption)) - .foregroundStyle(Color(.statusInfo)) - .frame(width: 76, alignment: .leading) - AppStatusBadgeView(model: emailRowBadge(row)) - } - Text(row.detail) - .font(appDensity.font(.caption2)) - .foregroundStyle(.primary) - .textSelection(.enabled) - if let auxiliaryDetail = row.auxiliaryDetail { - Text(auxiliaryDetail) - .font(appDensity.font(.caption2)) - .foregroundStyle(Color(.appTextSecondary)) - } - } - } - } - } - } - } - - private func emailRowBadge(_ row: EmailRowViewData) -> AppStatusBadgeModel { - switch row.statusTone { - case .success: - return .init(title: row.status, systemImage: "checkmark.shield.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface)) - case .warning: - return .init(title: row.status, systemImage: "shield.lefthalf.filled", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface)) - case .failure: - return .init(title: row.status, systemImage: "minus.circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated)) - case .primary, .secondary: - return .init(title: row.status, systemImage: "circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated)) - } - } -} - -struct NetworkSectionView: View { - @Environment(\.appDensity) private var appDensity - @Binding var isCollapsed: Bool - let reachabilityRows: [ReachabilityRowViewData] - let reachabilityLoading: Bool - let reachabilityError: String? - let reachabilityProvenance: SectionProvenance? - let locationRows: [InfoRowViewData] - let geolocation: IPGeolocation? - let geolocationLoading: Bool - let geolocationError: String? - let geolocationProvenance: SectionProvenance? - let geolocationConfidence: ConfidenceLevel? - let standardPortRows: [PortScanRowViewData] - let customPortRows: [PortScanRowViewData] - let portScanLoading: Bool - let portScanError: String? - let portScanProvenance: SectionProvenance? - let customPortScanLoading: Bool - let customPortScanError: String? - let isCloudflareProxied: Bool - @Binding var customPortsExpanded: Bool - @Binding var customPortInput: String - let onScanCustomPorts: () -> Void - - var body: some View { - CollapsibleSectionView(title: "Network", isCollapsed: $isCollapsed) { - CardView { - Text("Reachability") - .font(appDensity.font(.subheadline, weight: .semibold)) - .foregroundStyle(Color(.statusInfo)) - SectionTrustMetadataView(provenance: reachabilityProvenance, confidence: nil) - if reachabilityLoading { - ProgressView("Checking ports…") - .appLoadingStyle() - } else if let reachabilityError { - MessageRowView(text: reachabilityError, isError: true) - } else { - ForEach(reachabilityRows) { row in - HStack { - Text(row.portLabel) - .font(appDensity.font(.caption)) - Spacer() - Text(row.latencyLabel) - .font(appDensity.font(.caption2)) - .foregroundStyle(Color(.appTextSecondary)) - AppStatusBadgeView(model: reachabilityBadge(row)) - } - } - } - } - - CardView(allowsHorizontalScroll: false) { - Text("Location") - .font(appDensity.font(.subheadline, weight: .semibold)) - .foregroundStyle(Color(.statusInfo)) - SectionTrustMetadataView(provenance: geolocationProvenance, confidence: geolocationConfidence) - if geolocationLoading { - ProgressView("Looking up location…") - .appLoadingStyle() - } else if let geolocationError, geolocation == nil { - MessageRowView(text: geolocationError, isError: geolocationError != "No A record available") - } else if let geolocation { - ForEach(locationRows) { row in - LabeledValueRow(row: row) - } - if let latitude = geolocation.latitude, let longitude = geolocation.longitude { - let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) - Map(initialPosition: .region(MKCoordinateRegion( - center: coordinate, - span: MKCoordinateSpan(latitudeDelta: 1, longitudeDelta: 1) - ))) { - Marker(geolocation.ip, coordinate: coordinate) - } - .mapStyle(.standard) - .frame(maxWidth: .infinity) - .frame(height: 180) - .cornerRadius(8) - } - } else { - MessageRowView(text: "No location data available", isError: false) - } - } - - CardView(allowsHorizontalScroll: false) { - Text("Port Scan") - .font(appDensity.font(.subheadline, weight: .semibold)) - .foregroundStyle(Color(.statusInfo)) - SectionTrustMetadataView(provenance: portScanProvenance, confidence: nil) - - if isCloudflareProxied { - Text("Domain is behind Cloudflare's proxy. Results reflect the edge, not the origin.") - .font(appDensity.font(.caption2)) - .foregroundStyle(Color(.statusWarning)) - .fixedSize(horizontal: false, vertical: true) - } - - if portScanLoading { - ProgressView("Scanning ports…") - .appLoadingStyle() - } else if let portScanError, standardPortRows.isEmpty { - MessageRowView(text: portScanError, isError: true) - } else { - Text("Standard Ports") - .font(.system(.caption, design: .monospaced)) - .foregroundStyle(Color(.appTextSecondary)) - PortRowsView(rows: standardPortRows) - } - - DisclosureGroup("Custom Ports", isExpanded: $customPortsExpanded) { - VStack(alignment: .leading, spacing: 10) { - TextField("8888, 9000, 27017", text: $customPortInput) - .font(appDensity.font(.caption)) - .textInputAutocapitalization(.never) - .autocorrectionDisabled() - .keyboardType(.numberPad) - .padding(10) - .background(Color(.appSurface)) - .clipShape(RoundedRectangle(cornerRadius: appDensity.metrics.cardCornerRadius)) - - Button("Scan") { - AppHaptics.refresh() - onScanCustomPorts() - } - .buttonStyle(.borderedProminent) - .tint(Color(.accentFill)) - .disabled(customPortScanLoading) - - if customPortScanLoading { - ProgressView("Scanning custom ports…") - .appLoadingStyle() - } else if let customPortScanError { - MessageRowView(text: customPortScanError, isError: true) - } else { - PortRowsView(rows: customPortRows) - } - } - .padding(.top, 8) - } - .font(.system(.caption, design: .monospaced)) - .tint(.secondary) - } - } - } - - private func reachabilityBadge(_ row: ReachabilityRowViewData) -> AppStatusBadgeModel { - switch row.statusTone { - case .success: - return .init(title: row.statusLabel, systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface)) - case .warning: - return .init(title: row.statusLabel, systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface)) - case .failure: - return .init(title: row.statusLabel, systemImage: "xmark.circle.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface)) - case .primary, .secondary: - return .init(title: row.statusLabel, systemImage: "circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated)) - } - } -} - -struct PortRowsView: View { - @Environment(\.appDensity) private var appDensity - let rows: [PortScanRowViewData] - - var body: some View { - if rows.isEmpty { - MessageRowView(text: "No results", isError: false) - } else { - ForEach(rows) { row in - VStack(alignment: .leading, spacing: appDensity.metrics.rowSpacing - 1) { - HStack { - Text(row.portLabel) - .font(appDensity.font(.caption)) - .frame(width: 52, alignment: .leading) - Text(row.service) - .font(appDensity.font(.caption)) - .foregroundStyle(.primary) - Spacer() - if let durationLabel = row.durationLabel { - Text(durationLabel) - .font(appDensity.font(.caption2)) - .foregroundStyle(Color(.appTextSecondary)) - } - AppStatusBadgeView(model: portBadge(row)) - } - if let banner = row.banner { - Text(banner) - .font(appDensity.font(.caption2)) - .foregroundStyle(Color(.appTextSecondary)) - .padding(.leading, 8) - } - } - .frame(minHeight: appDensity.metrics.rowMinHeight, alignment: .topLeading) - } - } - } - - private func portBadge(_ row: PortScanRowViewData) -> AppStatusBadgeModel { - switch row.statusTone { - case .success: - return .init(title: row.statusLabel, systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface)) - case .warning: - return .init(title: row.statusLabel, systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface)) - case .failure: - return .init(title: row.statusLabel, systemImage: "xmark.circle.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface)) - case .primary, .secondary: - return .init(title: row.statusLabel, systemImage: "circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated)) - } - } -} - struct SectionTitleView: View { @Environment(\.appDensity) private var appDensity let title: String @@ -2691,7 +1578,7 @@ extension DateFormatter { }() } -private extension View { +extension View { func appLoadingStyle() -> some View { font(.system(.caption, design: .monospaced)) } |
