import Foundation /// A configured Makima server. /// Stored as a list in UserDefaults; the API key lives in Keychain keyed by /// `keychainID`. Allows future multi-profile support without migration. struct ServerProfile: Codable, Identifiable, Hashable { let id: UUID var label: String // "makima.jp" or user-named var baseURLString: String // e.g. "https://makima.jp" var lastConnectedAt: Date? init(id: UUID = UUID(), label: String, baseURLString: String, lastConnectedAt: Date? = nil) { self.id = id self.label = label self.baseURLString = baseURLString self.lastConnectedAt = lastConnectedAt } /// Validated URL, or nil if malformed. Strips trailing slash. var baseURL: URL? { let trimmed = baseURLString.trimmingCharacters(in: .whitespacesAndNewlines) .trimmingCharacters(in: CharacterSet(charactersIn: "/")) guard let url = URL(string: trimmed) else { return nil } guard let scheme = url.scheme?.lowercased(), scheme == "https" || scheme == "http" else { return nil } return url } /// API root: `/api/v1` var apiBaseURL: URL? { baseURL?.appendingPathComponent("api/v1") } /// WebSocket scheme version of `apiBaseURL`. var apiWebSocketBaseURL: URL? { guard let url = apiBaseURL, var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return nil } switch components.scheme { case "http": components.scheme = "ws" case "https": components.scheme = "wss" default: return nil } return components.url } /// Stable Keychain identifier — never changes after the profile is minted. var keychainID: String { "profile.\(id.uuidString)" } static let defaultLabel = "makima.jp" static let defaultBaseURL = "https://makima.jp" }