import SwiftUI struct SettingsView: View { @ObservedObject var monitor: ServerMonitor @State private var draft = ServerProfile() @State private var editingID: UUID? @State private var validation = "" @State private var interval = 5 @State private var notifications = true @State private var newRequiredService = "" @State private var showUninstalled = false var body: some View { HStack(spacing: 0) { VStack(spacing: 8) { Text("Servers").font(.headline).frame(maxWidth: .infinity, alignment: .leading) List(selection: $editingID) { ForEach(monitor.servers) { server in HStack { Image(systemName: "server.rack") Text(server.name) if !server.enabled { Image(systemName: "pause.circle") } }.tag(server.id) } .onMove { monitor.move(from: $0, to: $1) } } HStack { Button { newProfile() } label: { Image(systemName: "plus") } Button { if let id = editingID { monitor.remove(id); editingID = monitor.servers.first?.id } } label: { Image(systemName: "minus") } .disabled(editingID == nil) Spacer() } .buttonStyle(.borderless) } .frame(width: 205) .padding() Divider() VStack(alignment: .leading, spacing: 14) { Text(editingID == nil ? "Add a server" : "Connection details") .font(.title3.bold()) Form { TextField("Display name", text: $draft.name) TextField("Host / SSH alias", text: $draft.host) TextField("SSH username", text: $draft.user) TextField("SSH port", text: $draft.port) TextField("SSH key path (optional)", text: $draft.keyFile) TextField("Remote health command", text: $draft.remoteCommand) Toggle("Monitoring enabled", isOn: $draft.enabled) Toggle("Automatically discover services", isOn: $draft.discoverServices) } serviceConfiguration Text("Use the Pegasus adapter for Pegasus, or install server/remote-health-json on other Linux servers. SSH must work without interactive prompts. SSH known_hosts is enforced.") .font(.caption).foregroundStyle(.secondary) if !validation.isEmpty { Text(validation).foregroundStyle(.red).font(.caption) } HStack { Button("New Server") { newProfile() } Spacer() Button("Save and Refresh") { save() } .buttonStyle(.borderedProminent) } Divider() HStack { Picker("Refresh every", selection: $interval) { ForEach([1, 5, 10, 15, 30], id: \.self) { value in Text("\(value) min").tag(value) } } Toggle("Notifications", isOn: $notifications) } } .padding(20) .frame(minWidth: 460) } .onAppear { interval = monitor.intervalMinutes notifications = monitor.notifications editingID = monitor.selectedID if let selected = monitor.selected { draft = selected } } .onChange(of: editingID) { _, newValue in if let id = newValue, let server = monitor.servers.first(where: { $0.id == id }) { draft = server } validation = "" newRequiredService = "" } .onChange(of: interval) { _, value in monitor.intervalMinutes = value } .onChange(of: notifications) { _, value in monitor.notifications = value } } private var serviceConfiguration: some View { VStack(alignment: .leading, spacing: 10) { HStack { Text("Service monitoring").font(.headline) Spacer() Button("Refresh services") { if let id = editingID { Task { await monitor.refresh(id: id) } } }.disabled(editingID == nil) } Text("Mark critical services as Required. Missing or non-running required services count as failures. Optional services not installed do not affect health.") .font(.caption).foregroundStyle(.secondary) Toggle("Show not installed services", isOn: $showUninstalled) .font(.caption) let discovered = editingID.map { monitor.discoveredServices(for: $0) } ?? [] let display = discovered.filter { showUninstalled || $0.isInstalled || draft.requiredServices.contains($0.name) } if display.isEmpty { Text("No service discovery data yet. Refresh the server or add a required service below.") .font(.caption).foregroundStyle(.secondary) } else { ScrollView { LazyVStack(spacing: 6) { ForEach(display) { service in HStack(spacing: 8) { Image(systemName: service.symbol).foregroundStyle(service.color).frame(width: 20) Text(service.name).frame(maxWidth: .infinity, alignment: .leading) Text(service.title).font(.caption).foregroundStyle(.secondary) Toggle("Required", isOn: Binding( get: { draft.requiredServices.contains(service.name) }, set: { required in setRequired(service.name, required: required) } )).labelsHidden().help("Require \(service.name) to be running") } .font(.system(size: 12)) } } }.frame(maxHeight: 190) } HStack { TextField("Add required service (e.g. mysql)", text: $newRequiredService) .onSubmit { addRequiredService() } Button("Add Required") { addRequiredService() } .disabled(newRequiredService.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) } if !draft.requiredServices.isEmpty { Text("Required: \(draft.requiredServices.sorted().joined(separator: ", "))") .font(.caption).foregroundStyle(.secondary) } } } private func setRequired(_ name: String, required: Bool) { if required && !draft.requiredServices.contains(name) { draft.requiredServices.append(name) } if !required { draft.requiredServices.removeAll { $0 == name } } } private func addRequiredService() { let name = newRequiredService.trimmingCharacters(in: .whitespacesAndNewlines) guard !name.isEmpty, name.range(of: #"^[a-zA-Z0-9@_.-]+$"#, options: .regularExpression) != nil else { validation = "Use a valid systemd service name (letters, digits, @, _, -, or .)." return } setRequired(name, required: true) newRequiredService = "" validation = "" } private func newProfile() { editingID = nil draft = ServerProfile() validation = "" newRequiredService = "" } private func save() { guard !draft.name.trimmingCharacters(in: .whitespaces).isEmpty, !draft.host.trimmingCharacters(in: .whitespaces).isEmpty, !draft.user.trimmingCharacters(in: .whitespaces).isEmpty, let port = Int(draft.port), (1...65535).contains(port), !draft.remoteCommand.trimmingCharacters(in: .whitespaces).isEmpty else { validation = "Enter a name, host, SSH username, valid port and remote command." return } monitor.save(draft) editingID = draft.id validation = "" } }