PadXcode-iPad/Build/BuildPreflightChecker.swift

137 lines
5.6 KiB
Swift
Raw Permalink Normal View History

2026-04-12 00:46:30 -07:00
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)
}
}