Added a lot of functionality. Bugfixes and changes

This commit is contained in:
2026-02-15 16:46:06 +01:00
parent 2434e554f8
commit 04c9b8da1e
31 changed files with 6653 additions and 239 deletions

View File

@@ -18,6 +18,7 @@ struct ConversationRecord: Codable, FetchableRecord, PersistableRecord, Sendable
var name: String
var createdAt: String
var updatedAt: String
var primaryModel: String?
}
struct MessageRecord: Codable, FetchableRecord, PersistableRecord, Sendable {
@@ -31,6 +32,7 @@ struct MessageRecord: Codable, FetchableRecord, PersistableRecord, Sendable {
var cost: Double?
var timestamp: String
var sortOrder: Int
var modelId: String?
}
struct SettingRecord: Codable, FetchableRecord, PersistableRecord, Sendable {
@@ -48,6 +50,23 @@ struct HistoryRecord: Codable, FetchableRecord, PersistableRecord, Sendable {
var timestamp: String
}
struct EmailLogRecord: Codable, FetchableRecord, PersistableRecord, Sendable {
static let databaseTableName = "email_logs"
var id: String
var timestamp: String
var sender: String
var subject: String
var emailContent: String
var aiResponse: String?
var status: String // "success" or "error"
var errorMessage: String?
var tokens: Int?
var cost: Double?
var responseTime: Double?
var modelId: String?
}
// MARK: - DatabaseService
final class DatabaseService: Sendable {
@@ -127,6 +146,42 @@ final class DatabaseService: Sendable {
)
}
migrator.registerMigration("v4") { db in
// Add modelId to messages table (nullable for existing messages)
try db.alter(table: "messages") { t in
t.add(column: "modelId", .text)
}
// Add primaryModel to conversations table (nullable)
try db.alter(table: "conversations") { t in
t.add(column: "primaryModel", .text)
}
}
migrator.registerMigration("v5") { db in
// Email handler logs
try db.create(table: "email_logs") { t in
t.primaryKey("id", .text)
t.column("timestamp", .text).notNull()
t.column("sender", .text).notNull()
t.column("subject", .text).notNull()
t.column("emailContent", .text).notNull()
t.column("aiResponse", .text)
t.column("status", .text).notNull() // "success" or "error"
t.column("errorMessage", .text)
t.column("tokens", .integer)
t.column("cost", .double)
t.column("responseTime", .double)
t.column("modelId", .text)
}
try db.create(
index: "email_logs_on_timestamp",
on: "email_logs",
columns: ["timestamp"]
)
}
return migrator
}
@@ -152,32 +207,68 @@ final class DatabaseService: Sendable {
}
}
// MARK: - Encrypted Settings Operations
/// Store an encrypted setting (for sensitive data like API keys)
nonisolated func setEncryptedSetting(key: String, value: String) throws {
let encryptedValue = try EncryptionService.shared.encrypt(value)
try dbQueue.write { db in
let record = SettingRecord(key: "encrypted_\(key)", value: encryptedValue)
try record.save(db)
}
}
/// Retrieve and decrypt an encrypted setting
nonisolated func getEncryptedSetting(key: String) throws -> String? {
let encryptedValue = try dbQueue.read { db in
try SettingRecord.fetchOne(db, key: "encrypted_\(key)")?.value
}
guard let encryptedValue = encryptedValue else {
return nil
}
return try EncryptionService.shared.decrypt(encryptedValue)
}
/// Delete an encrypted setting
nonisolated func deleteEncryptedSetting(key: String) {
try? dbQueue.write { db in
_ = try SettingRecord.deleteOne(db, key: "encrypted_\(key)")
}
}
// MARK: - Conversation Operations
nonisolated func saveConversation(name: String, messages: [Message]) throws -> Conversation {
Log.db.info("Saving conversation '\(name)' with \(messages.count) messages")
let convId = UUID()
return try saveConversation(id: UUID(), name: name, messages: messages, primaryModel: nil)
}
nonisolated func saveConversation(id: UUID, name: String, messages: [Message], primaryModel: String?) throws -> Conversation {
Log.db.info("Saving conversation '\(name)' with \(messages.count) messages (primaryModel: \(primaryModel ?? "none"))")
let now = Date()
let nowString = isoFormatter.string(from: now)
let convRecord = ConversationRecord(
id: convId.uuidString,
id: id.uuidString,
name: name,
createdAt: nowString,
updatedAt: nowString
updatedAt: nowString,
primaryModel: primaryModel
)
let messageRecords = messages.enumerated().compactMap { index, msg -> MessageRecord? in
guard msg.role != .system else { return nil }
return MessageRecord(
id: UUID().uuidString,
conversationId: convId.uuidString,
conversationId: id.uuidString,
role: msg.role.rawValue,
content: msg.content,
tokens: msg.tokens,
cost: msg.cost,
timestamp: isoFormatter.string(from: msg.timestamp),
sortOrder: index
sortOrder: index,
modelId: msg.modelId
)
}
@@ -190,11 +281,12 @@ final class DatabaseService: Sendable {
let savedMessages = messages.filter { $0.role != .system }
return Conversation(
id: convId,
id: id,
name: name,
messages: savedMessages,
createdAt: now,
updatedAt: now
updatedAt: now,
primaryModel: primaryModel
)
}
@@ -221,7 +313,8 @@ final class DatabaseService: Sendable {
content: record.content,
tokens: record.tokens,
cost: record.cost,
timestamp: timestamp
timestamp: timestamp,
modelId: record.modelId
)
}
@@ -235,7 +328,8 @@ final class DatabaseService: Sendable {
name: convRecord.name,
messages: messages,
createdAt: createdAt,
updatedAt: updatedAt
updatedAt: updatedAt,
primaryModel: convRecord.primaryModel
)
return (conversation, messages)
@@ -404,4 +498,80 @@ final class DatabaseService: Sendable {
}
}
}
// MARK: - Email Log Operations
nonisolated func saveEmailLog(_ log: EmailLog) {
let record = EmailLogRecord(
id: log.id.uuidString,
timestamp: isoFormatter.string(from: log.timestamp),
sender: log.sender,
subject: log.subject,
emailContent: log.emailContent,
aiResponse: log.aiResponse,
status: log.status.rawValue,
errorMessage: log.errorMessage,
tokens: log.tokens,
cost: log.cost,
responseTime: log.responseTime,
modelId: log.modelId
)
try? dbQueue.write { db in
try record.insert(db)
}
}
nonisolated func loadEmailLogs(limit: Int = 100) throws -> [EmailLog] {
try dbQueue.read { db in
let records = try EmailLogRecord
.order(Column("timestamp").desc)
.limit(limit)
.fetchAll(db)
return records.compactMap { record in
guard let timestamp = isoFormatter.date(from: record.timestamp),
let status = EmailLogStatus(rawValue: record.status),
let id = UUID(uuidString: record.id) else {
return nil
}
return EmailLog(
id: id,
timestamp: timestamp,
sender: record.sender,
subject: record.subject,
emailContent: record.emailContent,
aiResponse: record.aiResponse,
status: status,
errorMessage: record.errorMessage,
tokens: record.tokens,
cost: record.cost,
responseTime: record.responseTime,
modelId: record.modelId
)
}
}
}
nonisolated func deleteEmailLog(id: UUID) {
try? dbQueue.write { db in
try db.execute(
sql: "DELETE FROM email_logs WHERE id = ?",
arguments: [id.uuidString]
)
}
}
nonisolated func clearEmailLogs() {
try? dbQueue.write { db in
try db.execute(sql: "DELETE FROM email_logs")
}
}
nonisolated func getEmailLogCount() throws -> Int {
try dbQueue.read { db in
try EmailLogRecord.fetchCount(db)
}
}
}