Files
oai-swift/oAI/Services/EmailLogService.swift
T
rune 76b58e6fdc Rename app from oAI to Confab
"oAI" reads as easily confused with OpenAI, both visually and in
casual conversation. Renamed to "Confab" throughout: Xcode
target/scheme/bundle ID (com.oai.Confab), Info.plist and Help Book
identity, all user-facing UI text, internal Log subsystem and color
identifiers, localization catalogs (6 languages, including a proper
reworded/retranslated Intel-deprecation notice), Help Book HTML
content, and docs (README/DEVELOPMENT/PRIVACY/SECURITY).

Deliberately cosmetic-only: the on-disk data folder
(~/Library/Application Support/oAI/), database/backup filenames,
Keychain service identifiers, and EncryptionService's key-derivation
inputs are all left untouched so existing conversations, settings,
and stored API keys survive the update with zero migration and no
re-entering credentials. Verified live: a real signed build
successfully decrypted a stored API key and loaded an existing
conversation database after the bundle ID change.

Also includes a small already-completed, previously uncommitted
model-release-date feature (ModelInfo/OpenRouterModels/
OpenRouterProvider/ModelInfoView) that happened to share several
files with this rename.

Gitignored on this branch and updated on disk but not part of this
commit: CLAUDE.md, RELEASE_NOTES.md, and the build*.sh scripts.
2026-08-02 14:58:14 +02:00

171 lines
4.6 KiB
Swift

//
// EmailLogService.swift
// Confab
//
// Service for managing email handler activity logs
//
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (C) 2026 Rune Olsen
//
// This file is part of Confab.
//
// Confab is licensed under the PolyForm Noncommercial License 1.0.0.
// You may use, study, modify, and share it for any noncommercial
// purpose. Commercial use — including selling Confab or any part of
// it, standalone or bundled into another product or service —
// requires a separate commercial license from the copyright holder.
//
// See the LICENSE file or
// <https://polyformproject.org/licenses/noncommercial/1.0.0> for
// the full license text. For commercial licensing, contact Rune
// Olsen via <https://oai.pm>.
import Foundation
import os
@Observable
final class EmailLogService {
static let shared = EmailLogService()
private let db = DatabaseService.shared
private let log = Logger(subsystem: Log.subsystem, category: "email-log")
private init() {}
// MARK: - Log Operations
/// Save a successful email processing log
func logSuccess(
sender: String,
subject: String,
emailContent: String,
aiResponse: String,
tokens: Int?,
cost: Double?,
responseTime: TimeInterval?,
modelId: String?
) {
let entry = EmailLog(
sender: sender,
subject: subject,
emailContent: emailContent,
aiResponse: aiResponse,
status: .success,
tokens: tokens,
cost: cost,
responseTime: responseTime,
modelId: modelId
)
db.saveEmailLog(entry)
log.info("Email log saved: \(sender) - \(subject) [SUCCESS]")
}
/// Save a failed email processing log
func logError(
sender: String,
subject: String,
emailContent: String,
errorMessage: String,
modelId: String?
) {
let entry = EmailLog(
sender: sender,
subject: subject,
emailContent: emailContent,
aiResponse: nil,
status: .error,
errorMessage: errorMessage,
modelId: modelId
)
db.saveEmailLog(entry)
log.error("Email log saved: \(sender) - \(subject) [ERROR: \(errorMessage)]")
}
/// Load recent email logs (default: 100 most recent)
func loadLogs(limit: Int = 100) -> [EmailLog] {
do {
return try db.loadEmailLogs(limit: limit)
} catch {
log.error("Failed to load email logs: \(error.localizedDescription)")
return []
}
}
/// Delete a specific log entry
func deleteLog(id: UUID) {
db.deleteEmailLog(id: id)
log.info("Email log deleted: \(id.uuidString)")
}
/// Clear all email logs
func clearAllLogs() {
db.clearEmailLogs()
log.info("All email logs cleared")
}
/// Get total count of email logs
func getLogCount() -> Int {
do {
return try db.getEmailLogCount()
} catch {
log.error("Failed to get email log count: \(error.localizedDescription)")
return 0
}
}
// MARK: - Statistics
/// Get email processing statistics
func getStatistics() -> EmailStatistics {
let logs = loadLogs(limit: 1000) // Last 1000 for stats
let total = logs.count
let successful = logs.filter { $0.status == .success }.count
let errors = logs.filter { $0.status == .error }.count
let totalTokens = logs.compactMap { $0.tokens }.reduce(0, +)
let totalCost = logs.compactMap { $0.cost }.reduce(0.0, +)
let avgResponseTime: TimeInterval?
let responseTimes = logs.compactMap { $0.responseTime }
if !responseTimes.isEmpty {
avgResponseTime = responseTimes.reduce(0, +) / Double(responseTimes.count)
} else {
avgResponseTime = nil
}
return EmailStatistics(
total: total,
successful: successful,
errors: errors,
totalTokens: totalTokens,
totalCost: totalCost,
averageResponseTime: avgResponseTime
)
}
}
// MARK: - Statistics Model
struct EmailStatistics {
let total: Int
let successful: Int
let errors: Int
let totalTokens: Int
let totalCost: Double
let averageResponseTime: TimeInterval?
var successRate: Double {
guard total > 0 else { return 0.0 }
return Double(successful) / Double(total)
}
var errorRate: Double {
guard total > 0 else { return 0.0 }
return Double(errors) / Double(total)
}
}