This commit is contained in:
Mark Gregory
2026-10-21 14:50:05 +01:00
parent 90987bd898
commit 9e501ca176
44 changed files with 461 additions and 222 deletions
+70
View File
@@ -7,6 +7,8 @@ struct SettingsView: View {
@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) {
@@ -44,7 +46,9 @@ struct SettingsView: View {
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) }
@@ -65,6 +69,7 @@ struct SettingsView: View {
}
}
.padding(20)
.frame(minWidth: 460)
}
.onAppear {
interval = monitor.intervalMinutes
@@ -75,15 +80,80 @@ struct SettingsView: View {
.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,