diff --git a/apps/macos/Sources/Clawdis/CronJobsStore.swift b/apps/macos/Sources/Clawdis/CronJobsStore.swift index 737960183..b0163552f 100644 --- a/apps/macos/Sources/Clawdis/CronJobsStore.swift +++ b/apps/macos/Sources/Clawdis/CronJobsStore.swift @@ -10,6 +10,10 @@ final class CronJobsStore: ObservableObject { @Published var selectedJobId: String? @Published var runEntries: [CronRunLogEntry] = [] + @Published var schedulerEnabled: Bool? + @Published var schedulerStorePath: String? + @Published var schedulerNextWakeAtMs: Int? + @Published var isLoadingJobs = false @Published var isLoadingRuns = false @Published var lastError: String? @@ -61,6 +65,11 @@ final class CronJobsStore: ObservableObject { defer { self.isLoadingJobs = false } do { + if let status = try? await self.fetchCronStatus() { + self.schedulerEnabled = status.enabled + self.schedulerStorePath = status.storePath + self.schedulerNextWakeAtMs = status.nextWakeAtMs + } let data = try await self.request( method: "cron.list", params: ["includeDisabled": true]) @@ -205,5 +214,16 @@ final class CronJobsStore: ObservableObject { let rawParams = params?.reduce(into: [String: AnyCodable]()) { $0[$1.key] = AnyCodable($1.value) } return try await GatewayConnection.shared.request(method: method, params: rawParams, timeoutMs: timeoutMs) } + + private func fetchCronStatus() async throws -> CronStatusResponse { + let data = try await self.request(method: "cron.status", params: nil) + return try JSONDecoder().decode(CronStatusResponse.self, from: data) + } } +private struct CronStatusResponse: Decodable { + let enabled: Bool + let storePath: String + let jobs: Int + let nextWakeAtMs: Int? +} diff --git a/apps/macos/Sources/Clawdis/CronSettings.swift b/apps/macos/Sources/Clawdis/CronSettings.swift index c87dc0684..c310adb28 100644 --- a/apps/macos/Sources/Clawdis/CronSettings.swift +++ b/apps/macos/Sources/Clawdis/CronSettings.swift @@ -15,6 +15,7 @@ struct CronSettings: View { var body: some View { VStack(alignment: .leading, spacing: 12) { self.header + self.schedulerBanner self.content Spacer(minLength: 0) } @@ -57,6 +58,38 @@ struct CronSettings: View { } } + private 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) + } + } + } + private var header: some View { HStack(alignment: .top) { VStack(alignment: .leading, spacing: 4) {