import Foundation struct AppConfig: Codable, Equatable { var appToken: String = "" var tableId: String = "" var assigneeName: String = "" var fieldMappings: FieldMappings = FieldMappings() var pollIntervalSeconds: Int = 300 /// Daily refresh times in "HH:mm" format, e.g. ["08:00", "14:00"]. /// Only used when pollIntervalSeconds == -1. var dailyRefreshTimes: [String] = [] var showFloatingWidget: Bool = false var launchAtLogin: Bool = true var feishuBaseDomain: String = "xorbitlab.feishu.cn" /// When false (the default), Bugger waits for the next scheduled poll /// instead of fetching immediately on launch. Avoids a crash-on-start /// loop if the table fetch is failing. var refreshOnStart: Bool = false struct FieldMappings: Codable, Equatable { var titleField: String = "Title" var priorityField: String = "Priority" var statusField: String = "Status" var assigneeField: String = "Assignee" var reporterField: String = "Reporter" var customerField: String = "CustomerName" var createdAtField: String = "Created At" var updatedAtField: String = "Updated At" /// Map Feishu display values to standard BugStatus cases. /// Keys are the Feishu select-option text; values are the standard status. /// Unknown values default to .unknown (shown as active). var statusMappings: [String: BugStatus] = [ "Bug Triage": .open, "Open": .open, "New": .open, "待处理": .open, "开发进行中": .inProgress, "In Progress": .inProgress, "验收中": .inReview, "In Review": .inReview, "验收完毕": .resolved, "Resolved": .resolved, "Fixed": .resolved, "取消": .closed, "Closed": .closed, "Won't fix": .closed, "转需求": .closed, "验收不通过": .closed, ] } var isConfigured: Bool { !appToken.isEmpty && !tableId.isEmpty } // Explicit keys + tolerant decoding so a config persisted before a new // field was added (missing key) doesn't fail to decode and wipe settings. enum CodingKeys: String, CodingKey { case appToken, tableId, assigneeName, fieldMappings case pollIntervalSeconds, dailyRefreshTimes case showFloatingWidget, launchAtLogin, feishuBaseDomain, refreshOnStart } init() {} init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) appToken = try c.decodeIfPresent(String.self, forKey: .appToken) ?? "" tableId = try c.decodeIfPresent(String.self, forKey: .tableId) ?? "" assigneeName = try c.decodeIfPresent(String.self, forKey: .assigneeName) ?? "" fieldMappings = try c.decodeIfPresent(FieldMappings.self, forKey: .fieldMappings) ?? FieldMappings() pollIntervalSeconds = try c.decodeIfPresent(Int.self, forKey: .pollIntervalSeconds) ?? 300 dailyRefreshTimes = try c.decodeIfPresent([String].self, forKey: .dailyRefreshTimes) ?? [] showFloatingWidget = try c.decodeIfPresent(Bool.self, forKey: .showFloatingWidget) ?? false launchAtLogin = try c.decodeIfPresent(Bool.self, forKey: .launchAtLogin) ?? true feishuBaseDomain = try c.decodeIfPresent(String.self, forKey: .feishuBaseDomain) ?? "xorbitlab.feishu.cn" refreshOnStart = try c.decodeIfPresent(Bool.self, forKey: .refreshOnStart) ?? false } }