krz/domain-dig

an ios app for DNS & SSL analysis

clone: git clone https://gitbay.org/krz/domain-dig.git

v5.0.2: DomainDig/AuditModeView.swift · raw

  1import SwiftUI
  2
  3struct AuditModeView: View {
  4    @Bindable var viewModel: DomainViewModel
  5    @State private var auditStore = AuditStore()
  6    @State private var reviewer = "Local Reviewer"
  7    @State private var selectedSession: AuditSession?
  8    @State private var exportMessage: String?
  9
 10    var body: some View {
 11        List {
 12            Section("Start Audit") {
 13                TextField("Domain", text: $viewModel.domain)
 14                    .textInputAutocapitalization(.never)
 15                    .autocorrectionDisabled(true)
 16                TextField("Reviewer", text: $reviewer)
 17                Button("Start Audit") {
 18                    let domain = viewModel.domain.trimmingCharacters(in: .whitespacesAndNewlines)
 19                    guard !domain.isEmpty else { return }
 20                    auditStore.startAudit(domain: domain, reviewer: reviewer, source: viewModel)
 21                }
 22                .disabled(viewModel.domain.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
 23            }
 24
 25            Section("Audit Timeline") {
 26                if auditStore.sessions.isEmpty {
 27                    Text("No audits yet.")
 28                        .foregroundStyle(Color(.appTextSecondary))
 29                } else {
 30                    ForEach(auditStore.sessions) { session in
 31                        Button {
 32                            selectedSession = session
 33                        } label: {
 34                            VStack(alignment: .leading) {
 35                                Text(session.domain).font(.headline)
 36                                Text("\(session.createdAt.formatted(date: .abbreviated, time: .shortened))\(session.status.title)")
 37                                    .font(.caption)
 38                                    .foregroundStyle(Color(.appTextSecondary))
 39                            }
 40                        }
 41                    }
 42                }
 43            }
 44        }
 45        .navigationTitle("Audit Mode")
 46        .sheet(item: $selectedSession) { session in
 47            AuditSessionDetailView(session: session) { updated in
 48                auditStore.update(session: updated)
 49                selectedSession = updated
 50            } onExport: { updated, format in
 51                do {
 52                    let url = try await auditStore.export(session: updated, format: format)
 53                    exportMessage = "Exported to \(url.lastPathComponent)"
 54                } catch {
 55                    exportMessage = "Export failed: \(error.localizedDescription)"
 56                }
 57            }
 58        }
 59        .alert("Audit Export", isPresented: Binding(get: { exportMessage != nil }, set: { if !$0 { exportMessage = nil } })) {
 60            Button("OK", role: .cancel) { /* Dismiss only; SwiftUI closes the alert. */ }
 61        } message: {
 62            Text(exportMessage ?? "")
 63        }
 64    }
 65}
 66
 67private struct AuditSessionDetailView: View {
 68    @Environment(\.dismiss) private var dismiss
 69    @State var session: AuditSession
 70    let onSave: (AuditSession) -> Void
 71    let onExport: (AuditSession, AuditExportFormat) async -> Void
 72
 73    var body: some View {
 74        NavigationStack {
 75            List {
 76                Section("Session") {
 77                    Text(session.domain)
 78                    Picker("Status", selection: $session.status) {
 79                        ForEach(AuditStatus.allCases, id: \.self) { Text($0.title).tag($0) }
 80                    }
 81                    TextField("Notes", text: $session.notes, axis: .vertical)
 82                }
 83                Section("Checklist") {
 84                    ForEach($session.checklist) { $item in
 85                        Toggle(item.title, isOn: $item.isComplete)
 86                    }
 87                }
 88                Section("Findings") {
 89                    ForEach($session.findings) { $finding in
 90                        VStack(alignment: .leading) {
 91                            TextField("Title", text: $finding.title)
 92                            Picker("Severity", selection: $finding.severity) {
 93                                ForEach(AuditFindingSeverity.allCases, id: \.self) { Text($0.rawValue.capitalized).tag($0) }
 94                            }
 95                            TextField("Summary", text: $finding.summary, axis: .vertical)
 96                        }
 97                    }
 98                    Button("Add Finding") {
 99                        session.findings.append(.init(title: "New finding", severity: .informational, summary: ""))
100                    }
101                }
102                Section("Export") {
103                    ForEach(AuditExportFormat.allCases) { format in
104                        Button("Export \(format.rawValue.uppercased())") {
105                            Task { await onExport(session, format) }
106                        }
107                    }
108                }
109            }
110            .navigationTitle("Audit Session")
111            .toolbar {
112                ToolbarItem(placement: .topBarLeading) {
113                    Button("Close") { dismiss() }
114                }
115                ToolbarItem(placement: .topBarTrailing) {
116                    Button("Save") {
117                        onSave(session)
118                        dismiss()
119                    }
120                }
121            }
122        }
123    }
124}