summaryrefslogtreecommitdiff
path: root/DomainDig/ContentView.swift
diff options
context:
space:
mode:
Diffstat (limited to 'DomainDig/ContentView.swift')
-rw-r--r--DomainDig/ContentView.swift1115
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))
}