krz/domain-dig
an ios app for DNS & SSL analysis
clone: git clone https://gitbay.org/krz/domain-dig.git
v4.7.0: 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(.secondary)
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(.secondary)
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) {}
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}