import Foundation import Observation struct ProjectRow: Decodable { let id: String let name: String } struct TaskRow: Decodable, Hashable { let id: String let title: String let status: String } struct BlobRow: Decodable { let id: String } struct SpikeError: Error, CustomStringConvertible { let description: String } @MainActor @Observable final class Server { var origin = UserDefaults.standard.string(forKey: "byre.server") ?? "" var tasks: [TaskRow] = [] var taskId = "" var status = "" var lines: [String] = [] func log(_ line: String) { lines.append(line) } private var base: URLComponents? func url(_ path: String, _ query: [URLQueryItem] = []) throws -> URL { guard var c = base else { throw SpikeError(description: "not connected") } c.path = path if !query.isEmpty { c.queryItems = query } guard let u = c.url else { throw SpikeError(description: "bad URL for \(path)") } return u } func data(_ request: URLRequest) async throws -> Data { let (data, response) = try await URLSession.shared.data(for: request) let code = (response as! HTTPURLResponse).statusCode if !(200..<300).contains(code) { throw SpikeError(description: "\(code) \(String(decoding: data, as: UTF8.self))") } return data } func get(_ type: T.Type, _ path: String, _ query: [URLQueryItem] = []) async throws -> T { try JSONDecoder().decode(T.self, from: try await data(URLRequest(url: try url(path, query)))) } func connect() async { guard let c = URLComponents(string: origin), c.scheme == "https", let host = c.host, !host.isEmpty, c.path.isEmpty || c.path == "/", c.url != nil else { base = nil status = "Server URL must be an https origin with no path, such as https://host.ts.net:8270" return } base = c UserDefaults.standard.set(origin, forKey: "byre.server") do { var all: [TaskRow] = [] for p in try await get([ProjectRow].self, "/api/projects") { all += try await get([TaskRow].self, "/api/projects/\(p.id)/tasks") } tasks = all.filter { $0.status == "active" } + all.filter { $0.status != "active" } if !tasks.contains(where: { $0.id == taskId }) { taskId = tasks.first?.id ?? "" } status = "\(tasks.count) tasks" } catch { status = "\(error)" } } func upload(_ image: Data) async throws -> String { var req = URLRequest(url: try url("/api/blobs")) req.httpMethod = "POST" req.httpBody = image return try JSONDecoder().decode(BlobRow.self, from: try await data(req)).id } }