Merge Conflicts (#41)
Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local> Reviewed-on: #41
This commit was merged in pull request #41.
This commit is contained in:
302
iOS/velocity-ipad/velocity/Core/Config/AppConfig.swift
Normal file
302
iOS/velocity-ipad/velocity/Core/Config/AppConfig.swift
Normal file
@@ -0,0 +1,302 @@
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
/// Central app configuration.
|
||||
/// Build settings remain the fallback, but production installs should prefer
|
||||
/// runtime configuration stored on-device.
|
||||
enum AppConfig {
|
||||
private static let runtimeBaseURLKey = "velocity.runtime.base_url"
|
||||
private static let runtimeDreamWeaverBaseURLKey = "velocity.runtime.dream_weaver_base_url"
|
||||
private static let runtimeDreamWeaverAPIKeyKey = "velocity.runtime.dream_weaver_api_key"
|
||||
private static let runtimeEmailKey = "velocity.runtime.email"
|
||||
private static let runtimePasswordKey = "velocity.runtime.password"
|
||||
private static let runtimeBearerTokenKey = "velocity.runtime.bearer_token"
|
||||
private static let runtimeAccessTokenKey = "velocity.runtime.access_token"
|
||||
private static let runtimeAccessTokenExpiresAtKey = "velocity.runtime.access_token_expires_at"
|
||||
private static let keychainService = "com.desineuron.velocity.ipad.session"
|
||||
|
||||
static func parsedValue(from infoDictionary: [String: Any]?, key: String) -> String? {
|
||||
let raw = infoDictionary?[key] as? String
|
||||
return sanitizedValue(raw, key: key)
|
||||
}
|
||||
|
||||
static func isLiveConfigured(
|
||||
bearerToken: String?,
|
||||
email: String?,
|
||||
password: String?
|
||||
) -> Bool {
|
||||
bearerToken != nil || (email != nil && password != nil)
|
||||
}
|
||||
|
||||
static func authModeDescription(
|
||||
bearerToken: String?,
|
||||
email: String?,
|
||||
password: String?
|
||||
) -> String {
|
||||
if bearerToken != nil {
|
||||
return "Bearer token"
|
||||
}
|
||||
if email != nil && password != nil {
|
||||
return "Email/password"
|
||||
}
|
||||
return "Credentials required"
|
||||
}
|
||||
|
||||
private static func value(for key: String) -> String? {
|
||||
parsedValue(from: Bundle.main.infoDictionary, key: key)
|
||||
}
|
||||
|
||||
private static func sanitizedValue(_ raw: String?, key: String) -> String? {
|
||||
guard let raw else {
|
||||
return nil
|
||||
}
|
||||
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if trimmed.isEmpty || trimmed == "$(\(key))" {
|
||||
return nil
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
/// Base URL for the Velocity backend / gateway.
|
||||
static var baseURL: String {
|
||||
runtimeBaseURL ?? value(for: "BASE_URL") ?? "https://velocity.desineuron.in/api"
|
||||
}
|
||||
|
||||
/// Dedicated Dream Weaver gateway endpoint when configured; otherwise
|
||||
/// generation falls back to the main backend endpoint.
|
||||
static var dreamWeaverBaseURL: String {
|
||||
configuredDreamWeaverBaseURL ?? originBaseURL(from: baseURL)
|
||||
}
|
||||
|
||||
static var usesDedicatedDreamWeaverBaseURL: Bool {
|
||||
guard let configuredDreamWeaverBaseURL else {
|
||||
return false
|
||||
}
|
||||
return configuredDreamWeaverBaseURL != baseURL
|
||||
}
|
||||
|
||||
static var dreamWeaverAPIKey: String? {
|
||||
runtimeDreamWeaverAPIKey ?? value(for: "DREAM_WEAVER_API_KEY")
|
||||
}
|
||||
|
||||
static var apiEmail: String? {
|
||||
runtimeEmail ?? value(for: "API_EMAIL")
|
||||
}
|
||||
|
||||
static var apiPassword: String? {
|
||||
runtimePassword ?? value(for: "API_PASSWORD")
|
||||
}
|
||||
|
||||
static var apiBearerToken: String? {
|
||||
runtimeBearerToken ?? value(for: "API_BEARER_TOKEN")
|
||||
}
|
||||
|
||||
static var apiAccessToken: String? {
|
||||
guard let expiresAt = runtimeAccessTokenExpiresAt, expiresAt > Date().addingTimeInterval(60) else {
|
||||
try? clearStoredAccessToken()
|
||||
return nil
|
||||
}
|
||||
return secret(account: runtimeAccessTokenKey)
|
||||
}
|
||||
|
||||
static var isLiveConfigured: Bool {
|
||||
isLiveConfigured(
|
||||
bearerToken: apiBearerToken,
|
||||
email: apiEmail,
|
||||
password: apiPassword
|
||||
)
|
||||
}
|
||||
|
||||
static var authModeDescription: String {
|
||||
authModeDescription(
|
||||
bearerToken: apiBearerToken,
|
||||
email: apiEmail,
|
||||
password: apiPassword
|
||||
)
|
||||
}
|
||||
|
||||
static var hasStoredRuntimeConfiguration: Bool {
|
||||
runtimeBaseURL != nil ||
|
||||
runtimeDreamWeaverBaseURL != nil ||
|
||||
runtimeEmail != nil ||
|
||||
runtimePassword != nil ||
|
||||
runtimeBearerToken != nil
|
||||
}
|
||||
|
||||
static func currentSessionConfiguration() -> AppSessionConfiguration {
|
||||
AppSessionConfiguration(
|
||||
baseURL: baseURL,
|
||||
dreamWeaverBaseURL: dreamWeaverBaseURL,
|
||||
usesDedicatedDreamWeaverBaseURL: usesDedicatedDreamWeaverBaseURL,
|
||||
hasDreamWeaverAPIKey: dreamWeaverAPIKey != nil,
|
||||
email: apiEmail,
|
||||
hasPassword: apiPassword != nil,
|
||||
hasBearerToken: apiBearerToken != nil,
|
||||
source: hasStoredRuntimeConfiguration ? .secureDeviceStorage : .buildConfiguration
|
||||
)
|
||||
}
|
||||
|
||||
static func saveRuntimeConfiguration(
|
||||
baseURL: String,
|
||||
dreamWeaverBaseURL: String?,
|
||||
dreamWeaverAPIKey: String?,
|
||||
email: String?,
|
||||
password: String?,
|
||||
bearerToken: String?
|
||||
) throws {
|
||||
UserDefaults.standard.set(baseURL, forKey: runtimeBaseURLKey)
|
||||
|
||||
if let dreamWeaverBaseURL {
|
||||
UserDefaults.standard.set(dreamWeaverBaseURL, forKey: runtimeDreamWeaverBaseURLKey)
|
||||
} else {
|
||||
UserDefaults.standard.removeObject(forKey: runtimeDreamWeaverBaseURLKey)
|
||||
}
|
||||
|
||||
if let email {
|
||||
UserDefaults.standard.set(email, forKey: runtimeEmailKey)
|
||||
} else {
|
||||
UserDefaults.standard.removeObject(forKey: runtimeEmailKey)
|
||||
}
|
||||
|
||||
try storeSecret(dreamWeaverAPIKey, account: runtimeDreamWeaverAPIKeyKey)
|
||||
try storeSecret(password, account: runtimePasswordKey)
|
||||
try storeSecret(bearerToken, account: runtimeBearerTokenKey)
|
||||
try clearStoredAccessToken()
|
||||
}
|
||||
|
||||
static func clearStoredRuntimeConfiguration() throws {
|
||||
UserDefaults.standard.removeObject(forKey: runtimeBaseURLKey)
|
||||
UserDefaults.standard.removeObject(forKey: runtimeDreamWeaverBaseURLKey)
|
||||
UserDefaults.standard.removeObject(forKey: runtimeEmailKey)
|
||||
try deleteSecret(account: runtimeDreamWeaverAPIKeyKey)
|
||||
try deleteSecret(account: runtimePasswordKey)
|
||||
try deleteSecret(account: runtimeBearerTokenKey)
|
||||
try clearStoredAccessToken()
|
||||
}
|
||||
|
||||
static func saveAccessToken(_ token: String, expiresIn: Int?) throws {
|
||||
try storeSecret(token, account: runtimeAccessTokenKey)
|
||||
let lifetime = TimeInterval(max(expiresIn ?? 28_800, 60))
|
||||
UserDefaults.standard.set(Date().addingTimeInterval(lifetime).timeIntervalSince1970, forKey: runtimeAccessTokenExpiresAtKey)
|
||||
}
|
||||
|
||||
static func clearStoredAccessToken() throws {
|
||||
UserDefaults.standard.removeObject(forKey: runtimeAccessTokenExpiresAtKey)
|
||||
try deleteSecret(account: runtimeAccessTokenKey)
|
||||
}
|
||||
|
||||
private static var runtimeBaseURL: String? {
|
||||
sanitizedValue(UserDefaults.standard.string(forKey: runtimeBaseURLKey), key: runtimeBaseURLKey)
|
||||
}
|
||||
|
||||
private static var configuredDreamWeaverBaseURL: String? {
|
||||
runtimeDreamWeaverBaseURL ?? value(for: "DREAM_WEAVER_BASE_URL")
|
||||
}
|
||||
|
||||
private static var runtimeDreamWeaverBaseURL: String? {
|
||||
sanitizedValue(
|
||||
UserDefaults.standard.string(forKey: runtimeDreamWeaverBaseURLKey),
|
||||
key: runtimeDreamWeaverBaseURLKey
|
||||
)
|
||||
}
|
||||
|
||||
private static var runtimeDreamWeaverAPIKey: String? {
|
||||
secret(account: runtimeDreamWeaverAPIKeyKey)
|
||||
}
|
||||
|
||||
private static var runtimeEmail: String? {
|
||||
sanitizedValue(UserDefaults.standard.string(forKey: runtimeEmailKey), key: runtimeEmailKey)
|
||||
}
|
||||
|
||||
private static var runtimePassword: String? {
|
||||
secret(account: runtimePasswordKey)
|
||||
}
|
||||
|
||||
private static var runtimeBearerToken: String? {
|
||||
secret(account: runtimeBearerTokenKey)
|
||||
}
|
||||
|
||||
private static var runtimeAccessTokenExpiresAt: Date? {
|
||||
let rawValue = UserDefaults.standard.double(forKey: runtimeAccessTokenExpiresAtKey)
|
||||
guard rawValue > 0 else {
|
||||
return nil
|
||||
}
|
||||
return Date(timeIntervalSince1970: rawValue)
|
||||
}
|
||||
|
||||
private static func originBaseURL(from rawValue: String) -> String {
|
||||
guard var components = URLComponents(string: rawValue) else {
|
||||
return rawValue
|
||||
}
|
||||
components.path = ""
|
||||
components.query = nil
|
||||
components.fragment = nil
|
||||
return components.string ?? rawValue
|
||||
}
|
||||
|
||||
private static func secret(account: String) -> String? {
|
||||
let query: [CFString: Any] = [
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrService: keychainService,
|
||||
kSecAttrAccount: account,
|
||||
kSecReturnData: true,
|
||||
kSecMatchLimit: kSecMatchLimitOne
|
||||
]
|
||||
|
||||
var result: AnyObject?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
guard status == errSecSuccess else {
|
||||
return nil
|
||||
}
|
||||
guard let data = result as? Data else {
|
||||
return nil
|
||||
}
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
private static func storeSecret(_ value: String?, account: String) throws {
|
||||
if let value, let data = value.data(using: .utf8) {
|
||||
let query: [CFString: Any] = [
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrService: keychainService,
|
||||
kSecAttrAccount: account
|
||||
]
|
||||
|
||||
let attributes: [CFString: Any] = [
|
||||
kSecValueData: data
|
||||
]
|
||||
|
||||
let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
|
||||
if status == errSecSuccess {
|
||||
return
|
||||
}
|
||||
|
||||
if status == errSecItemNotFound {
|
||||
var addQuery = query
|
||||
addQuery[kSecValueData] = data
|
||||
let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
|
||||
guard addStatus == errSecSuccess else {
|
||||
throw SessionPersistenceError.keychainWriteFailed(addStatus)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
throw SessionPersistenceError.keychainWriteFailed(status)
|
||||
}
|
||||
|
||||
try deleteSecret(account: account)
|
||||
}
|
||||
|
||||
private static func deleteSecret(account: String) throws {
|
||||
let query: [CFString: Any] = [
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrService: keychainService,
|
||||
kSecAttrAccount: account
|
||||
]
|
||||
|
||||
let status = SecItemDelete(query as CFDictionary)
|
||||
guard status == errSecSuccess || status == errSecItemNotFound else {
|
||||
throw SessionPersistenceError.keychainDeleteFailed(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
import Foundation
|
||||
|
||||
enum SessionAuthMode: String, CaseIterable, Identifiable {
|
||||
case emailPassword = "Email/password"
|
||||
case bearerToken = "Bearer token"
|
||||
|
||||
var id: String { rawValue }
|
||||
}
|
||||
|
||||
enum SessionConfigurationSource: String {
|
||||
case buildConfiguration = "Build configuration"
|
||||
case secureDeviceStorage = "Secure device storage"
|
||||
}
|
||||
|
||||
struct AppSessionConfiguration: Equatable {
|
||||
let baseURL: String
|
||||
let dreamWeaverBaseURL: String
|
||||
let usesDedicatedDreamWeaverBaseURL: Bool
|
||||
let hasDreamWeaverAPIKey: Bool
|
||||
let email: String?
|
||||
let hasPassword: Bool
|
||||
let hasBearerToken: Bool
|
||||
let source: SessionConfigurationSource
|
||||
|
||||
var authMode: SessionAuthMode {
|
||||
hasBearerToken ? .bearerToken : .emailPassword
|
||||
}
|
||||
|
||||
var isConfigured: Bool {
|
||||
hasBearerToken || (email != nil && hasPassword)
|
||||
}
|
||||
|
||||
var authModeDescription: String {
|
||||
if hasBearerToken {
|
||||
return "Bearer token"
|
||||
}
|
||||
if email != nil && hasPassword {
|
||||
return "Email/password"
|
||||
}
|
||||
return "Credentials required"
|
||||
}
|
||||
|
||||
var operatorIdentity: String {
|
||||
if let email, !email.isEmpty {
|
||||
return email
|
||||
}
|
||||
if hasBearerToken {
|
||||
return "Token authenticated operator"
|
||||
}
|
||||
return "Unconfigured operator"
|
||||
}
|
||||
|
||||
var dreamWeaverEndpointModeDescription: String {
|
||||
usesDedicatedDreamWeaverBaseURL ? "Dedicated gateway" : "Shared with backend"
|
||||
}
|
||||
|
||||
var dreamWeaverAuthenticationDescription: String {
|
||||
hasDreamWeaverAPIKey ? "API key configured" : "No gateway key configured"
|
||||
}
|
||||
}
|
||||
|
||||
struct SessionConfigurationDraft: Equatable {
|
||||
var baseURL: String
|
||||
var dreamWeaverBaseURL: String
|
||||
var dreamWeaverAPIKey: String
|
||||
var authMode: SessionAuthMode
|
||||
var email: String
|
||||
var password: String
|
||||
var bearerToken: String
|
||||
var existingDreamWeaverAPIKeyAvailable: Bool
|
||||
var existingPasswordAvailable: Bool
|
||||
var existingBearerTokenAvailable: Bool
|
||||
var baselineEmail: String?
|
||||
|
||||
var trimmedBaseURL: String? {
|
||||
Self.trimmedValue(baseURL)
|
||||
}
|
||||
|
||||
var trimmedEmail: String? {
|
||||
Self.trimmedValue(email)
|
||||
}
|
||||
|
||||
var trimmedPassword: String? {
|
||||
Self.trimmedValue(password)
|
||||
}
|
||||
|
||||
var trimmedBearerToken: String? {
|
||||
Self.trimmedValue(bearerToken)
|
||||
}
|
||||
|
||||
var normalizedBaseURL: String? {
|
||||
Self.normalizedHTTPSOrigin(from: trimmedBaseURL)
|
||||
}
|
||||
|
||||
var trimmedDreamWeaverBaseURL: String? {
|
||||
Self.trimmedValue(dreamWeaverBaseURL)
|
||||
}
|
||||
|
||||
var normalizedDreamWeaverBaseURL: String? {
|
||||
Self.normalizedHTTPSOrigin(from: trimmedDreamWeaverBaseURL)
|
||||
}
|
||||
|
||||
var trimmedDreamWeaverAPIKey: String? {
|
||||
Self.trimmedValue(dreamWeaverAPIKey)
|
||||
}
|
||||
|
||||
func validationErrors() -> [String] {
|
||||
var errors: [String] = []
|
||||
|
||||
guard let trimmedBaseURL else {
|
||||
errors.append("Backend endpoint is required.")
|
||||
return errors
|
||||
}
|
||||
|
||||
guard URLComponents(string: trimmedBaseURL) != nil else {
|
||||
errors.append("Backend endpoint must be a valid URL.")
|
||||
return errors
|
||||
}
|
||||
|
||||
guard normalizedBaseURL != nil else {
|
||||
errors.append("Backend endpoint must be an HTTPS API base like https://velocity.desineuron.in/api.")
|
||||
return errors
|
||||
}
|
||||
|
||||
if let trimmedDreamWeaverBaseURL {
|
||||
guard URLComponents(string: trimmedDreamWeaverBaseURL) != nil else {
|
||||
errors.append("Dream Weaver endpoint must be a valid URL.")
|
||||
return errors
|
||||
}
|
||||
|
||||
guard normalizedDreamWeaverBaseURL != nil else {
|
||||
errors.append("Dream Weaver endpoint must be an HTTPS origin like https://dreamweaver.desineuron.in.")
|
||||
return errors
|
||||
}
|
||||
}
|
||||
|
||||
switch authMode {
|
||||
case .emailPassword:
|
||||
guard let trimmedEmail else {
|
||||
errors.append("Operator email is required for email/password login.")
|
||||
break
|
||||
}
|
||||
|
||||
guard trimmedEmail.contains("@"), trimmedEmail.contains(".") else {
|
||||
errors.append("Operator email must look like a valid email address.")
|
||||
break
|
||||
}
|
||||
|
||||
if trimmedPassword == nil &&
|
||||
!(existingPasswordAvailable && trimmedEmail.caseInsensitiveCompare(baselineEmail ?? "") == .orderedSame) {
|
||||
errors.append("Password is required for email/password login.")
|
||||
}
|
||||
|
||||
case .bearerToken:
|
||||
if trimmedBearerToken == nil && !existingBearerTokenAvailable {
|
||||
errors.append("Bearer token is required when token auth is selected.")
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
func resolvedEmail(existingEmail: String?) -> String? {
|
||||
guard authMode == .emailPassword else { return nil }
|
||||
return trimmedEmail ?? existingEmail
|
||||
}
|
||||
|
||||
func resolvedPassword(existingPassword: String?) -> String? {
|
||||
guard authMode == .emailPassword else { return nil }
|
||||
if let trimmedPassword {
|
||||
return trimmedPassword
|
||||
}
|
||||
guard existingPasswordAvailable else {
|
||||
return nil
|
||||
}
|
||||
guard trimmedEmail?.caseInsensitiveCompare(baselineEmail ?? "") == .orderedSame else {
|
||||
return nil
|
||||
}
|
||||
return existingPassword
|
||||
}
|
||||
|
||||
func resolvedBearerToken(existingToken: String?) -> String? {
|
||||
guard authMode == .bearerToken else { return nil }
|
||||
return trimmedBearerToken ?? (existingBearerTokenAvailable ? existingToken : nil)
|
||||
}
|
||||
|
||||
func resolvedDreamWeaverBaseURL(normalizedBaseURL: String) -> String? {
|
||||
normalizedDreamWeaverBaseURL
|
||||
}
|
||||
|
||||
func resolvedDreamWeaverAPIKey(existingKey: String?) -> String? {
|
||||
trimmedDreamWeaverAPIKey ?? (existingDreamWeaverAPIKeyAvailable ? existingKey : nil)
|
||||
}
|
||||
|
||||
private static func trimmedValue(_ value: String?) -> String? {
|
||||
guard let value else {
|
||||
return nil
|
||||
}
|
||||
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return trimmed.isEmpty ? nil : trimmed
|
||||
}
|
||||
|
||||
private static func normalizedHTTPSOrigin(from raw: String?) -> String? {
|
||||
guard let raw else {
|
||||
return nil
|
||||
}
|
||||
guard var components = URLComponents(string: raw) else {
|
||||
return nil
|
||||
}
|
||||
guard let scheme = components.scheme?.lowercased(),
|
||||
let host = components.host?.lowercased() else {
|
||||
return nil
|
||||
}
|
||||
guard scheme == "https" else {
|
||||
return nil
|
||||
}
|
||||
guard components.query == nil, components.fragment == nil else {
|
||||
return nil
|
||||
}
|
||||
let path = components.path.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let normalizedPath = path == "/api/" ? "/api" : path
|
||||
guard normalizedPath.isEmpty || normalizedPath == "/" || normalizedPath == "/api" else {
|
||||
return nil
|
||||
}
|
||||
|
||||
components.scheme = scheme
|
||||
components.host = host
|
||||
components.path = normalizedPath == "/api" ? "/api" : ""
|
||||
components.query = nil
|
||||
components.fragment = nil
|
||||
return components.string
|
||||
}
|
||||
}
|
||||
|
||||
enum SessionPersistenceError: LocalizedError {
|
||||
case keychainWriteFailed(OSStatus)
|
||||
case keychainDeleteFailed(OSStatus)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .keychainWriteFailed(let status):
|
||||
return "Velocity could not save credentials securely on this iPad. Keychain status: \(status)."
|
||||
case .keychainDeleteFailed(let status):
|
||||
return "Velocity could not clear stored credentials from this iPad. Keychain status: \(status)."
|
||||
}
|
||||
}
|
||||
}
|
||||
204
iOS/velocity-ipad/velocity/Core/Config/SessionStore.swift
Normal file
204
iOS/velocity-ipad/velocity/Core/Config/SessionStore.swift
Normal file
@@ -0,0 +1,204 @@
|
||||
import Foundation
|
||||
import Observation
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
final class SessionStore {
|
||||
static let shared = SessionStore()
|
||||
|
||||
private init() {
|
||||
reloadFromPersistedConfiguration()
|
||||
}
|
||||
|
||||
var currentConfiguration = AppConfig.currentSessionConfiguration()
|
||||
var draftBaseURL = ""
|
||||
var draftDreamWeaverBaseURL = ""
|
||||
var draftDreamWeaverAPIKey = ""
|
||||
var draftAuthMode: SessionAuthMode = .emailPassword
|
||||
var draftEmail = ""
|
||||
var draftPassword = ""
|
||||
var draftBearerToken = ""
|
||||
var isSaving = false
|
||||
var statusMessage: String?
|
||||
var errorMessage: String?
|
||||
|
||||
private var existingPasswordAvailable = false
|
||||
private var existingBearerTokenAvailable = false
|
||||
private var existingDreamWeaverAPIKeyAvailable = false
|
||||
private var baselineEmail: String?
|
||||
|
||||
var isConfigured: Bool {
|
||||
currentConfiguration.isConfigured
|
||||
}
|
||||
|
||||
var authModeDescription: String {
|
||||
currentConfiguration.authModeDescription
|
||||
}
|
||||
|
||||
var operatorIdentity: String {
|
||||
currentConfiguration.operatorIdentity
|
||||
}
|
||||
|
||||
var endpointDisplay: String {
|
||||
currentConfiguration.baseURL
|
||||
}
|
||||
|
||||
var dreamWeaverEndpointDisplay: String {
|
||||
currentConfiguration.dreamWeaverBaseURL
|
||||
}
|
||||
|
||||
var dreamWeaverEndpointModeDescription: String {
|
||||
currentConfiguration.dreamWeaverEndpointModeDescription
|
||||
}
|
||||
|
||||
var dreamWeaverAuthenticationDescription: String {
|
||||
currentConfiguration.dreamWeaverAuthenticationDescription
|
||||
}
|
||||
|
||||
var configurationSourceDescription: String {
|
||||
currentConfiguration.source.rawValue
|
||||
}
|
||||
|
||||
var isUsingStoredRuntimeConfiguration: Bool {
|
||||
currentConfiguration.source == .secureDeviceStorage
|
||||
}
|
||||
|
||||
var hasUnsavedChanges: Bool {
|
||||
draftBaseURL != currentConfiguration.baseURL ||
|
||||
draftDreamWeaverBaseURL != persistedDreamWeaverDraftValue ||
|
||||
!draftDreamWeaverAPIKey.isEmpty ||
|
||||
draftAuthMode != currentConfiguration.authMode ||
|
||||
draftEmail != (currentConfiguration.email ?? "") ||
|
||||
!draftPassword.isEmpty ||
|
||||
!draftBearerToken.isEmpty
|
||||
}
|
||||
|
||||
func reloadFromPersistedConfiguration() {
|
||||
currentConfiguration = AppConfig.currentSessionConfiguration()
|
||||
draftBaseURL = currentConfiguration.baseURL
|
||||
draftDreamWeaverBaseURL = persistedDreamWeaverDraftValue
|
||||
draftDreamWeaverAPIKey = ""
|
||||
draftAuthMode = currentConfiguration.authMode
|
||||
draftEmail = currentConfiguration.email ?? ""
|
||||
draftPassword = ""
|
||||
draftBearerToken = ""
|
||||
existingDreamWeaverAPIKeyAvailable = currentConfiguration.hasDreamWeaverAPIKey
|
||||
existingPasswordAvailable = currentConfiguration.hasPassword
|
||||
existingBearerTokenAvailable = currentConfiguration.hasBearerToken
|
||||
baselineEmail = currentConfiguration.email
|
||||
}
|
||||
|
||||
func discardDraftChanges() {
|
||||
errorMessage = nil
|
||||
statusMessage = nil
|
||||
reloadFromPersistedConfiguration()
|
||||
}
|
||||
|
||||
func saveDraft() async {
|
||||
errorMessage = nil
|
||||
statusMessage = nil
|
||||
|
||||
let draft = SessionConfigurationDraft(
|
||||
baseURL: draftBaseURL,
|
||||
dreamWeaverBaseURL: draftDreamWeaverBaseURL,
|
||||
dreamWeaverAPIKey: draftDreamWeaverAPIKey,
|
||||
authMode: draftAuthMode,
|
||||
email: draftEmail,
|
||||
password: draftPassword,
|
||||
bearerToken: draftBearerToken,
|
||||
existingDreamWeaverAPIKeyAvailable: existingDreamWeaverAPIKeyAvailable,
|
||||
existingPasswordAvailable: existingPasswordAvailable,
|
||||
existingBearerTokenAvailable: existingBearerTokenAvailable,
|
||||
baselineEmail: baselineEmail
|
||||
)
|
||||
|
||||
let errors = draft.validationErrors()
|
||||
guard errors.isEmpty else {
|
||||
errorMessage = errors.joined(separator: " ")
|
||||
return
|
||||
}
|
||||
|
||||
guard let normalizedBaseURL = draft.normalizedBaseURL else {
|
||||
errorMessage = "Backend endpoint must be a valid HTTPS origin."
|
||||
return
|
||||
}
|
||||
|
||||
isSaving = true
|
||||
|
||||
do {
|
||||
try AppConfig.saveRuntimeConfiguration(
|
||||
baseURL: normalizedBaseURL,
|
||||
dreamWeaverBaseURL: draft.resolvedDreamWeaverBaseURL(normalizedBaseURL: normalizedBaseURL),
|
||||
dreamWeaverAPIKey: draft.resolvedDreamWeaverAPIKey(existingKey: AppConfig.dreamWeaverAPIKey),
|
||||
email: draft.resolvedEmail(existingEmail: currentConfiguration.email),
|
||||
password: draft.resolvedPassword(existingPassword: AppConfig.apiPassword),
|
||||
bearerToken: draft.resolvedBearerToken(existingToken: AppConfig.apiBearerToken)
|
||||
)
|
||||
|
||||
await VelocityAPIClient.shared.resetSession()
|
||||
AppStore.shared.resetLiveData()
|
||||
reloadFromPersistedConfiguration()
|
||||
await AppStore.shared.refresh()
|
||||
let dreamWeaverHealthy = await ComfyClient.shared.checkHealth()
|
||||
statusMessage = verificationStatusMessage(
|
||||
successPrefix: "Configuration saved.",
|
||||
backendRefreshError: AppStore.shared.errorMessage,
|
||||
dreamWeaverHealthy: dreamWeaverHealthy
|
||||
)
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
|
||||
isSaving = false
|
||||
}
|
||||
|
||||
func clearStoredConfiguration() async {
|
||||
errorMessage = nil
|
||||
statusMessage = nil
|
||||
isSaving = true
|
||||
|
||||
do {
|
||||
try AppConfig.clearStoredRuntimeConfiguration()
|
||||
await VelocityAPIClient.shared.resetSession()
|
||||
AppStore.shared.resetLiveData()
|
||||
reloadFromPersistedConfiguration()
|
||||
|
||||
if currentConfiguration.isConfigured {
|
||||
await AppStore.shared.refresh()
|
||||
let dreamWeaverHealthy = await ComfyClient.shared.checkHealth()
|
||||
statusMessage = verificationStatusMessage(
|
||||
successPrefix: "Stored override cleared. Velocity is now using the build configuration.",
|
||||
backendRefreshError: AppStore.shared.errorMessage,
|
||||
dreamWeaverHealthy: dreamWeaverHealthy
|
||||
)
|
||||
} else {
|
||||
statusMessage = "Stored session cleared. This iPad now requires runtime configuration before live data can load."
|
||||
}
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
|
||||
isSaving = false
|
||||
}
|
||||
|
||||
private var persistedDreamWeaverDraftValue: String {
|
||||
currentConfiguration.usesDedicatedDreamWeaverBaseURL ? currentConfiguration.dreamWeaverBaseURL : ""
|
||||
}
|
||||
|
||||
private func verificationStatusMessage(
|
||||
successPrefix: String,
|
||||
backendRefreshError: String?,
|
||||
dreamWeaverHealthy: Bool
|
||||
) -> String {
|
||||
switch (backendRefreshError, dreamWeaverHealthy) {
|
||||
case (nil, true):
|
||||
return "\(successPrefix) Core backend refresh and Dream Weaver gateway probe both succeeded."
|
||||
case (let backendRefreshError?, true):
|
||||
return "\(successPrefix) Core backend refresh failed: \(backendRefreshError) Dream Weaver gateway probe succeeded."
|
||||
case (nil, false):
|
||||
return "\(successPrefix) Core backend refresh succeeded, but the Dream Weaver gateway probe failed. Verify the dedicated generation endpoint and routing."
|
||||
case (let backendRefreshError?, false):
|
||||
return "\(successPrefix) Core backend refresh failed: \(backendRefreshError) Dream Weaver gateway probe also failed."
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user