import Foundation import Vapor import Observation enum ServerState: Equatable { case stopped case starting case running(port: Int, startedAt: Date) case error(String) var isRunning: Bool { if case .running = self { return true }; return false } var statusLabel: String { switch self { case .stopped: return "Stopped" case .starting: return "Starting…" case .running: return "Running" case .error(let m): return "Error: \(m)" } } var uptime: String? { guard case .running(_, let start) = self else { return nil } let e = Date().timeIntervalSince(start) if e < 60 { return String(format: "%.0fs", e) } if e < 3600 { return String(format: "%dm %ds", Int(e/60), Int(e)%60) } return String(format: "%dh %dm", Int(e/3600), Int(e/60)%60) } } struct ConnectedClient: Identifiable { let id = UUID(); let sessionId: String; let connectedAt: Date; let type: ClientType enum ClientType: String { case build = "Build Stream"; case lsp = "LSP Stream" } } struct DaemonLogLine: Identifiable { let id = UUID(); let date = Date(); let message: String; let level: Level enum Level { case info, success, warning, error var prefix: String { switch self { case .info: return "ℹ️"; case .success: return "✅" case .warning: return "⚠️"; case .error: return "❌" } } } var formattedTime: String { let f = DateFormatter(); f.dateFormat = "HH:mm:ss"; return f.string(from: date) } } @Observable final class DaemonController { var serverState: ServerState = .stopped var connectedClients: [ConnectedClient] = [] var logLines: [DaemonLogLine] = [] var buildCount: Int = 0 var preferences: AppPreferences private var vaporApp: Application? private let maxLogLines = 500 init(preferences: AppPreferences) { self.preferences = preferences } func start() async { guard !serverState.isRunning else { return } serverState = .starting appendLog("Starting PadXcode daemon on port \(preferences.port)…", level: .info) do { var env = try Environment.detect() try LoggingSystem.bootstrap(from: &env) let app = Application(env) app.http.server.configuration.hostname = "0.0.0.0" app.http.server.configuration.port = preferences.port try configure(app, controller: self) try await app.startup() vaporApp = app serverState = .running(port: preferences.port, startedAt: Date()) appendLog("✅ Daemon running on 0.0.0.0:\(preferences.port)", level: .success) appendLog("📡 Tailscale IP: \(preferences.tailscaleIP)", level: .info) } catch { serverState = .error(error.localizedDescription) appendLog("❌ Failed to start: \(error.localizedDescription)", level: .error) } } func stop() async { guard serverState.isRunning else { return } appendLog("Stopping daemon…", level: .info) await vaporApp?.asyncShutdown() vaporApp = nil; serverState = .stopped; connectedClients = [] appendLog("Daemon stopped.", level: .info) } func restart() async { await stop(); try? await Task.sleep(for: .milliseconds(500)); await start() } func clientConnected(sessionId: String, type: ConnectedClient.ClientType) { connectedClients.append(ConnectedClient(sessionId: sessionId, connectedAt: Date(), type: type)) appendLog("Client connected: \(type.rawValue) [\(sessionId.prefix(8))]", level: .info) } func clientDisconnected(sessionId: String) { connectedClients.removeAll { $0.sessionId == sessionId } appendLog("Client disconnected [\(sessionId.prefix(8))]", level: .info) } func buildStarted(scheme: String) { buildCount += 1; appendLog("Build #\(buildCount): \(scheme)", level: .info) } func appendLog(_ message: String, level: DaemonLogLine.Level) { logLines.append(DaemonLogLine(message: message, level: level)) if logLines.count > maxLogLines { logLines.removeFirst(logLines.count - maxLogLines) } } func clearLog() { logLines = [] } func applyPortChange() async { if serverState.isRunning { await restart() } } }