import Foundation import SwiftUI enum PreflightStatus { case pass case warning(String) case fail(String) } struct PreflightCheck: Identifiable { let id = UUID() let name: String let detail: String let status: PreflightStatus var icon: String { switch status { case .pass: return "checkmark.circle.fill" case .warning: return "exclamationmark.triangle.fill" case .fail: return "xmark.circle.fill" } } var color: Color { switch status { case .pass: return .green case .warning: return .orange case .fail: return .red } } } struct PreflightReport { let checks: [PreflightCheck] var canProceed: Bool { !checks.contains { if case .fail = $0.status { return true }; return false } } var hasWarnings: Bool { checks.contains { if case .warning = $0.status { return true }; return false } } } @MainActor final class BuildPreflightChecker { private let config: DaemonConfiguration private let buildService: BuildService init(config: DaemonConfiguration, buildService: BuildService) { self.config = config; self.buildService = buildService } func run(project: SavedProject?, device: ConnectedDevice?, action: String) async -> PreflightReport { var checks: [PreflightCheck] = [] checks.append(await checkDaemon()) checks.append(checkProject(project)) checks.append(checkTeamID()) checks.append(checkDevice(device, action: action)) if let p = project { checks.append(await checkPath(p)) } checks.append(checkNoBuildRunning()) return PreflightReport(checks: checks) } private func checkDaemon() async -> PreflightCheck { guard let url = URL(string: "\(config.baseURL)/health") else { return PreflightCheck(name: "Daemon", detail: "Invalid URL", status: .fail("Daemon URL malformed. Check Settings.")) } var req = URLRequest(url: url); req.timeoutInterval = 3 do { let (_, resp) = try await URLSession.shared.data(for: req) guard (resp as? HTTPURLResponse)?.statusCode == 200 else { return PreflightCheck(name: "Daemon", detail: "Bad response", status: .fail("Daemon not responding correctly.")) } return PreflightCheck(name: "Daemon", detail: "Connected to \(config.baseURL)", status: .pass) } catch { return PreflightCheck(name: "Daemon", detail: error.localizedDescription, status: .fail("Cannot reach \(config.baseURL). Check Tailscale.")) } } private func checkProject(_ project: SavedProject?) -> PreflightCheck { guard let p = project else { return PreflightCheck(name: "Project", detail: "None selected", status: .fail("Select a project before building.")) } return PreflightCheck(name: "Project", detail: "\(p.name) โ€” \(p.scheme)", status: .pass) } private func checkTeamID() -> PreflightCheck { let t = config.developmentTeam.trimmingCharacters(in: .whitespaces) if t.isEmpty { return PreflightCheck(name: "Team ID", detail: "Not set", status: .fail("Enter your Team ID in Settings โ†’ Build.")) } let valid = t.count == 10 && t.allSatisfy({ $0.isLetter || $0.isNumber }) && t == t.uppercased() return PreflightCheck(name: "Team ID", detail: t, status: valid ? .pass : .warning("Format looks wrong โ€” should be 10 uppercase alphanumerics.")) } private func checkDevice(_ device: ConnectedDevice?, action: String) -> PreflightCheck { if action == "build" { return PreflightCheck(name: "Device", detail: "Build only", status: .pass) } guard let d = device else { return PreflightCheck(name: "Device", detail: "None selected", status: .fail("Select a paired device to deploy.")) } return PreflightCheck(name: "Device", detail: "\(d.name) ยท iOS \(d.osVersion)", status: d.isNetworkConnected ? .pass : .warning("Device paired but may not be on network.")) } private func checkPath(_ project: SavedProject) async -> PreflightCheck { let encoded = project.rootPath.urlEncoded guard let url = URL(string: "\(config.baseURL)/files/tree?path=\(encoded)") else { return PreflightCheck(name: "Project Path", detail: project.rootPath, status: .pass) } var req = URLRequest(url: url); req.timeoutInterval = 5 do { let (_, resp) = try await URLSession.shared.data(for: req) let code = (resp as? HTTPURLResponse)?.statusCode ?? 0 return PreflightCheck(name: "Project Path", detail: project.rootPath, status: code == 200 ? .pass : .fail("Path not found on Mac.")) } catch { return PreflightCheck(name: "Project Path", detail: "Check timed out", status: .warning("Could not verify path.")) } } private func checkNoBuildRunning() -> PreflightCheck { buildService.isBuilding ? PreflightCheck(name: "Build State", detail: "Already running", status: .fail("Wait for current build to finish.")) : PreflightCheck(name: "Build State", detail: "Ready", status: .pass) } }