import SwiftUI extension CronSettings { var body: some View { VStack(alignment: .leading, spacing: 12) { self.header self.schedulerBanner self.content Spacer(minLength: 0) } .onAppear { self.store.start() } .onDisappear { self.store.stop() } .sheet(isPresented: self.$showEditor) { CronJobEditor( job: self.editingJob, isSaving: self.$isSaving, error: self.$editorError, onCancel: { self.showEditor = false self.editingJob = nil }, onSave: { payload in Task { await self.save(payload: payload) } }) } .alert("Delete cron job?", isPresented: Binding( get: { self.confirmDelete != nil }, set: { if !$0 { self.confirmDelete = nil } })) { Button("Cancel", role: .cancel) { self.confirmDelete = nil } Button("Delete", role: .destructive) { if let job = self.confirmDelete { Task { await self.store.removeJob(id: job.id) } } self.confirmDelete = nil } } message: { if let job = self.confirmDelete { Text(job.displayName) } } .onChange(of: self.store.selectedJobId) { _, newValue in guard let newValue else { return } Task { await self.store.refreshRuns(jobId: newValue) } } } var schedulerBanner: some View { Group { if self.store.schedulerEnabled == false { VStack(alignment: .leading, spacing: 6) { HStack(spacing: 8) { Image(systemName: "exclamationmark.triangle.fill") .foregroundStyle(.orange) Text("Cron scheduler is disabled") .font(.headline) Spacer() } Text( "Jobs are saved, but they will not run automatically until `cron.enabled` is set to `true` " + "and the Gateway restarts.") .font(.footnote) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) if let storePath = self.store.schedulerStorePath, !storePath.isEmpty { Text(storePath) .font(.caption.monospaced()) .foregroundStyle(.secondary) .textSelection(.enabled) .lineLimit(1) .truncationMode(.middle) } } .frame(maxWidth: .infinity, alignment: .leading) .padding(10) .background(Color.orange.opacity(0.10)) .cornerRadius(8) } } } var header: some View { HStack(alignment: .top) { VStack(alignment: .leading, spacing: 4) { Text("Cron") .font(.headline) Text("Manage Gateway cron jobs (main session vs isolated runs) and inspect run history.") .font(.footnote) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) } Spacer() HStack(spacing: 8) { Button { Task { await self.store.refreshJobs() } } label: { Label("Refresh", systemImage: "arrow.clockwise") } .buttonStyle(.bordered) .disabled(self.store.isLoadingJobs) Button { self.editorError = nil self.editingJob = nil self.showEditor = true } label: { Label("New Job", systemImage: "plus") } .buttonStyle(.borderedProminent) } } } var content: some View { HStack(spacing: 12) { VStack(alignment: .leading, spacing: 8) { if let err = self.store.lastError { Text("Error: \(err)") .font(.footnote) .foregroundStyle(.red) } else if let msg = self.store.statusMessage { Text(msg) .font(.footnote) .foregroundStyle(.secondary) } List(selection: self.$store.selectedJobId) { ForEach(self.store.jobs) { job in self.jobRow(job) .tag(job.id) .contextMenu { self.jobContextMenu(job) } } } .listStyle(.inset) } .frame(width: 250) Divider() self.detail .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } } @ViewBuilder var detail: some View { if let selected = self.selectedJob { ScrollView(.vertical) { VStack(alignment: .leading, spacing: 12) { self.detailHeader(selected) self.detailCard(selected) self.runHistoryCard(selected) } .frame(maxWidth: .infinity, alignment: .leading) .padding(.top, 2) } } else { VStack(alignment: .leading, spacing: 8) { Text("Select a job to inspect details and run history.") .font(.callout) .foregroundStyle(.secondary) Text("Tip: use ‘New Job’ to add one, or enable cron in your gateway config.") .font(.caption) .foregroundStyle(.tertiary) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .padding(.top, 8) } } }