// // EmailLogView.swift // oAI // // Email handler activity log viewer // // SPDX-License-Identifier: AGPL-3.0-or-later // Copyright (C) 2026 Rune Olsen // // This file is part of oAI. // // oAI is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // oAI is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General // Public License for more details. // // You should have received a copy of the GNU Affero General Public // License along with oAI. If not, see . import SwiftUI struct EmailLogView: View { @Environment(\.dismiss) var dismiss @Bindable private var settings = SettingsService.shared private let emailLogService = EmailLogService.shared @State private var logs: [EmailLog] = [] @State private var searchText = "" @State private var showClearConfirmation = false var body: some View { VStack(spacing: 0) { // Title and controls header Divider() // Statistics statistics Divider() // Search bar searchBar // Logs list logsList Divider() // Bottom actions bottomActions } .frame(width: 800, height: 600) .onAppear { loadLogs() } } // MARK: - Header private var header: some View { HStack { Text("Email Activity Log") .font(.system(size: 18, weight: .bold)) Spacer() Button("Done") { dismiss() } .keyboardShortcut(.defaultAction) } .padding() } // MARK: - Statistics private var statistics: some View { let stats = emailLogService.getStatistics() return HStack(spacing: 30) { EmailStatItem(title: "Total", value: "\(stats.total)", color: .blue) EmailStatItem(title: "Successful", value: "\(stats.successful)", color: .green) EmailStatItem(title: "Errors", value: "\(stats.errors)", color: .red) if stats.total > 0 { EmailStatItem( title: "Success Rate", value: String(format: "%.1f%%", stats.successRate * 100), color: .green ) } if let avgTime = stats.averageResponseTime { EmailStatItem( title: "Avg Response Time", value: String(format: "%.1fs", avgTime), color: .orange ) } if stats.totalCost > 0 { EmailStatItem( title: "Total Cost", value: String(format: "$%.4f", stats.totalCost), color: .purple ) } } .padding() } // MARK: - Search Bar private var searchBar: some View { HStack { Image(systemName: "magnifyingglass") .foregroundColor(.secondary) TextField("Search by sender, subject, or content...", text: $searchText) .textFieldStyle(.plain) .onChange(of: searchText) { filterLogs() } if !searchText.isEmpty { Button(action: { searchText = "" loadLogs() }) { Image(systemName: "xmark.circle.fill") .foregroundColor(.secondary) } .buttonStyle(.plain) } } .padding(8) .background(Color.secondary.opacity(0.1)) .cornerRadius(8) .padding(.horizontal) .padding(.vertical, 8) } // MARK: - Logs List private var logsList: some View { ScrollView { LazyVStack(spacing: 8) { if logs.isEmpty { emptyState } else { ForEach(logs) { log in EmailLogRow(log: log) } } } .padding() } } private var emptyState: some View { VStack(spacing: 16) { Image(systemName: "tray") .font(.system(size: 48)) .foregroundColor(.secondary) Text("No email activity yet") .font(.system(size: 16, weight: .medium)) .foregroundColor(.secondary) if !settings.emailHandlerEnabled { Text("Enable email handler in Settings to start monitoring emails") .font(.system(size: 14)) .foregroundColor(.secondary) .multilineTextAlignment(.center) } } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding() } // MARK: - Bottom Actions private var bottomActions: some View { HStack { Text("\(logs.count) \(logs.count == 1 ? "entry" : "entries")") .font(.system(size: 13)) .foregroundColor(.secondary) Spacer() Button(action: { showClearConfirmation = true }) { HStack(spacing: 4) { Image(systemName: "trash") Text("Clear All") } } .disabled(logs.isEmpty) .alert("Clear Email Log?", isPresented: $showClearConfirmation) { Button("Cancel", role: .cancel) {} Button("Clear All", role: .destructive) { emailLogService.clearAllLogs() loadLogs() } } message: { Text("This will permanently delete all email activity logs. This action cannot be undone.") } } .padding() } // MARK: - Data Operations private func loadLogs() { logs = emailLogService.loadLogs(limit: 500) } private func filterLogs() { if searchText.isEmpty { loadLogs() } else { let allLogs = emailLogService.loadLogs(limit: 500) logs = allLogs.filter { log in log.sender.localizedCaseInsensitiveContains(searchText) || log.subject.localizedCaseInsensitiveContains(searchText) || log.emailContent.localizedCaseInsensitiveContains(searchText) || (log.aiResponse?.localizedCaseInsensitiveContains(searchText) ?? false) } } } } // MARK: - Email Log Row struct EmailLogRow: View { let log: EmailLog var body: some View { VStack(alignment: .leading, spacing: 8) { // Header: status, sender, time HStack { Image(systemName: log.status.iconName) .foregroundColor(statusColor) Text(log.sender) .font(.system(size: 14, weight: .medium)) Spacer() Text(log.timestamp.formatted(date: .abbreviated, time: .shortened)) .font(.system(size: 12)) .foregroundColor(.secondary) } // Subject Text(log.subject) .font(.system(size: 13)) .foregroundColor(.primary) .lineLimit(1) // Content preview if log.status == .success, let response = log.aiResponse { Text(response) .font(.system(size: 12)) .foregroundColor(.secondary) .lineLimit(2) } else if log.status == .error, let error = log.errorMessage { Text("Error: \(error)") .font(.system(size: 12)) .foregroundColor(.red) .lineLimit(2) } // Metadata HStack(spacing: 12) { if let model = log.modelId { Label(model, systemImage: "cpu") .font(.system(size: 11)) .foregroundColor(.secondary) } if let tokens = log.tokens { Label("\(tokens) tokens", systemImage: "number") .font(.system(size: 11)) .foregroundColor(.secondary) } if let cost = log.cost, cost > 0 { Label(String(format: "$%.4f", cost), systemImage: "dollarsign.circle") .font(.system(size: 11)) .foregroundColor(.secondary) } if let responseTime = log.responseTime { Label(String(format: "%.1fs", responseTime), systemImage: "clock") .font(.system(size: 11)) .foregroundColor(.secondary) } } } .padding(12) .background(Color.secondary.opacity(0.05)) .cornerRadius(8) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(statusColor.opacity(0.3), lineWidth: 1) ) } private var statusColor: Color { switch log.status { case .success: return .green case .error: return .red } } } // MARK: - Stat Item struct EmailStatItem: View { let title: String let value: String let color: Color var body: some View { VStack(spacing: 4) { Text(value) .font(.system(size: 20, weight: .bold)) .foregroundColor(color) Text(title) .font(.system(size: 11)) .foregroundColor(.secondary) } } } #Preview { EmailLogView() }