import SwiftUI /// Connection configuration. Doubles as initial setup (no config yet) and the /// in-app Settings tab. Credentials are entered on the Login screen, not here. struct SettingsView: View { let isInitialSetup: Bool @Environment(AppEnvironment.self) private var env @Environment(\.dismiss) private var dismiss @State private var urlString = "https://" @State private var apiVersion = "v1" @State private var databaseId = "" @State private var testPhase: LoadPhase = .idle @State private var testResult: String? @State private var saveError: String? var body: some View { Form { Section { TextField("https://host[/api]", text: $urlString) .textInputAutocapitalization(.never) .autocorrectionDisabled() .keyboardType(.URL) TextField("API version", text: $apiVersion) .textInputAutocapitalization(.never) .autocorrectionDisabled() } header: { Text("Server") } footer: { Text("Host or origin, e.g. https://mcms.lab.svorka.net — “/api” is appended automatically unless you enter a path. Endpoints are v1.") } Section("Database (optional)") { TextField("Database ID", text: $databaseId) .textInputAutocapitalization(.never) .autocorrectionDisabled() } Section { Button { Task { await runTest() } } label: { HStack { Text("Test connection") Spacer() switch testPhase { case .loading: ProgressView() case .loaded: Image(systemName: "checkmark.circle.fill").foregroundStyle(.green) case .failed: Image(systemName: "xmark.circle.fill").foregroundStyle(.red) case .idle: EmptyView() } } } .disabled(!canSave) if let testResult { Text(testResult).font(.footnote).foregroundStyle(.secondary) } } footer: { if case .failed(let msg) = testPhase { Text(msg).foregroundStyle(.red) } } if !isInitialSetup { Section { Button("Sign out", role: .destructive) { Task { await env.signOut() } } } } if let saveError { Section { Text(saveError).foregroundStyle(.red) } } } .navigationTitle(isInitialSetup ? "Set up connection" : "Settings") .toolbar { ToolbarItem(placement: .confirmationAction) { Button("Save") { save() }.disabled(!canSave) } } .onAppear(perform: prefill) } private var canSave: Bool { Self.normalizedBaseURL(urlString) != nil && !apiVersion.trimmingCharacters(in: .whitespaces).isEmpty } private func prefill() { guard let config = env.configuration else { return } urlString = config.baseURL.absoluteString apiVersion = config.apiVersion databaseId = config.databaseId ?? "" } private func makeConfig() -> APIConfiguration? { // TLS uses system trust only (no self-signed bypass); trustPolicy defaults to .system. guard let url = Self.normalizedBaseURL(urlString), url.host != nil else { return nil } return APIConfiguration( baseURL: url, apiVersion: apiVersion.trimmingCharacters(in: .whitespaces), databaseId: databaseId.isEmpty ? nil : databaseId ) } /// Normalizes the entered server URL: requires scheme + host, trims any /// trailing slash, and defaults the path to `/api` when none is given — every /// MCMS REST path lives under `/api` (dev guide). A reverse proxy mounted /// elsewhere can still be entered with an explicit path. static func normalizedBaseURL(_ raw: String) -> URL? { let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) guard var comps = URLComponents(string: trimmed), comps.scheme != nil, let host = comps.host, !host.isEmpty else { return nil } var path = comps.path while path.hasSuffix("/") { path.removeLast() } if path.isEmpty { path = "/api" } comps.path = path return comps.url } private func save() { guard let config = makeConfig() else { saveError = "Enter a valid URL including scheme (https://)." return } saveError = nil env.configure(config) if !isInitialSetup { dismiss() } } private func runTest() async { guard let config = makeConfig() else { return } testPhase = .loading testResult = nil do { let result = try await env.probe(config) testResult = "Connected · \(result)" testPhase = .loaded } catch { testPhase = .failed((error as? APIError)?.localizedDescription ?? error.localizedDescription) } } }