Phase 0: menu-bar scaffold, permissions, backend health check
Native SwiftUI menu-bar app (LSUIElement, macOS 13+), generated from project.yml via XcodeGen. Includes: - PermissionsManager (Microphone / Screen Recording / Accessibility) + UI - SparkControlHealth: GET /api/status over self-signed TLS (InsecureTrustDelegate) - AppSettings persistence (host, TLS-skip, output folder, adapter toggles) - Menu-bar panel + Settings, app sandbox & hardened runtime off (LAN tool)
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
import Foundation
|
||||
|
||||
/// URLSession delegate that trusts the server certificate without validation.
|
||||
///
|
||||
/// SparkControl sits behind a Start9 self-signed Root CA on the LAN, so default
|
||||
/// trust evaluation rejects it. This delegate is used **only** when the
|
||||
/// "Skip TLS verification" setting is on. It trusts any server certificate —
|
||||
/// acceptable for a personal tool on a trusted local network and nothing else.
|
||||
final class InsecureTrustDelegate: NSObject, URLSessionDelegate {
|
||||
func urlSession(
|
||||
_ session: URLSession,
|
||||
didReceive challenge: URLAuthenticationChallenge,
|
||||
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
|
||||
) {
|
||||
guard
|
||||
challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
|
||||
let serverTrust = challenge.protectionSpace.serverTrust
|
||||
else {
|
||||
completionHandler(.performDefaultHandling, nil)
|
||||
return
|
||||
}
|
||||
completionHandler(.useCredential, URLCredential(trust: serverTrust))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
/// Performs the Phase 0 backend reachability check: `GET {baseURL}/api/status`.
|
||||
///
|
||||
/// This is a thin slice — the full `SparkControlClient` (label-merge, multipart,
|
||||
/// sequential queueing, retries) arrives in Phase 5.
|
||||
@MainActor
|
||||
final class SparkControlHealth: ObservableObject {
|
||||
|
||||
enum Status: Equatable {
|
||||
case unknown
|
||||
case checking
|
||||
case online(String)
|
||||
case offline(String)
|
||||
}
|
||||
|
||||
@Published private(set) var status: Status = .unknown
|
||||
@Published private(set) var lastChecked: Date?
|
||||
|
||||
func check(baseURL: String, skipTLS: Bool) async {
|
||||
status = .checking
|
||||
|
||||
let trimmed = baseURL.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let base = trimmed.hasSuffix("/") ? String(trimmed.dropLast()) : trimmed
|
||||
guard !base.isEmpty, let url = URL(string: base + "/api/status") else {
|
||||
status = .offline("Invalid host URL")
|
||||
return
|
||||
}
|
||||
|
||||
let config = URLSessionConfiguration.ephemeral
|
||||
config.timeoutIntervalForRequest = 8
|
||||
config.waitsForConnectivity = false
|
||||
|
||||
let delegate: URLSessionDelegate? = skipTLS ? InsecureTrustDelegate() : nil
|
||||
let session = URLSession(configuration: config, delegate: delegate, delegateQueue: nil)
|
||||
defer { session.finishTasksAndInvalidate() }
|
||||
|
||||
do {
|
||||
let (data, response) = try await session.data(from: url)
|
||||
lastChecked = Date()
|
||||
guard let http = response as? HTTPURLResponse else {
|
||||
status = .offline("No HTTP response")
|
||||
return
|
||||
}
|
||||
if (200..<300).contains(http.statusCode) {
|
||||
status = .online(Self.summarize(data) ?? "Reachable")
|
||||
} else {
|
||||
status = .offline("HTTP \(http.statusCode)")
|
||||
}
|
||||
} catch {
|
||||
lastChecked = Date()
|
||||
status = .offline(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
/// Best-effort one-line summary of the `/api/status` body, if it's JSON.
|
||||
private static func summarize(_ data: Data) -> String? {
|
||||
guard let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||
return nil
|
||||
}
|
||||
if let s = object["status"] as? String { return s }
|
||||
if let s = object["state"] as? String { return s }
|
||||
return "Reachable"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user