solid_queue_tui 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/exe/sqtui +37 -1
  3. data/lib/solid_queue_tui/actions/discard_job.rb +4 -21
  4. data/lib/solid_queue_tui/actions/discard_scheduled_job.rb +5 -21
  5. data/lib/solid_queue_tui/actions/dispatch_scheduled_job.rb +4 -23
  6. data/lib/solid_queue_tui/actions/enqueue_recurring_task.rb +12 -0
  7. data/lib/solid_queue_tui/actions/retry_job.rb +14 -59
  8. data/lib/solid_queue_tui/actions/toggle_queue_pause.rb +19 -0
  9. data/lib/solid_queue_tui/application.rb +109 -21
  10. data/lib/solid_queue_tui/cli.rb +15 -8
  11. data/lib/solid_queue_tui/components/header.rb +26 -27
  12. data/lib/solid_queue_tui/components/help_bar.rb +1 -0
  13. data/lib/solid_queue_tui/components/job_table.rb +13 -2
  14. data/lib/solid_queue_tui/data/failed_query.rb +37 -91
  15. data/lib/solid_queue_tui/data/jobs_query.rb +119 -121
  16. data/lib/solid_queue_tui/data/processes_query.rb +32 -33
  17. data/lib/solid_queue_tui/data/queues_query.rb +6 -15
  18. data/lib/solid_queue_tui/data/recurring_tasks_query.rb +36 -0
  19. data/lib/solid_queue_tui/data/stats.rb +9 -27
  20. data/lib/solid_queue_tui/formatting_helpers.rb +63 -0
  21. data/lib/solid_queue_tui/railtie.rb +9 -0
  22. data/lib/solid_queue_tui/version.rb +1 -1
  23. data/lib/solid_queue_tui/views/blocked_view.rb +85 -74
  24. data/lib/solid_queue_tui/views/concerns/confirmable.rb +53 -0
  25. data/lib/solid_queue_tui/views/concerns/filterable.rb +128 -0
  26. data/lib/solid_queue_tui/views/concerns/paginatable.rb +79 -0
  27. data/lib/solid_queue_tui/views/dashboard_view.rb +4 -5
  28. data/lib/solid_queue_tui/views/failed_view.rb +65 -179
  29. data/lib/solid_queue_tui/views/finished_view.rb +33 -114
  30. data/lib/solid_queue_tui/views/in_progress_view.rb +85 -69
  31. data/lib/solid_queue_tui/views/job_detail_view.rb +179 -31
  32. data/lib/solid_queue_tui/views/processes_view.rb +2 -24
  33. data/lib/solid_queue_tui/views/queues_view.rb +250 -30
  34. data/lib/solid_queue_tui/views/recurring_tasks_view.rb +155 -0
  35. data/lib/solid_queue_tui/views/scheduled_view.rb +69 -107
  36. data/lib/solid_queue_tui.rb +18 -4
  37. data/lib/tasks/solid_queue_tui.rake +8 -0
  38. metadata +20 -25
  39. data/lib/solid_queue_tui/connection.rb +0 -58
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 969d0f0bb659a0568e476a7b6f9292a5f772217fc688cab1f55c23b505db4952
4
- data.tar.gz: ec7edd7aca4a711ac044f3bdd2ea00dbf791980535341806b271256bc44ae21b
3
+ metadata.gz: ea9a6cc3e42df6d7c5f50f91ed2bbf360bbd3e65c9fabb6f53bcf649b69826f1
4
+ data.tar.gz: 3cd1c06f2c4ea19f2c59e0c5dc79962211d8f18b8200b4f46955910c2fcd78e6
5
5
  SHA512:
6
- metadata.gz: 5c5484690f95c5b7602399255d40c32d398de508c6e0d9240d221e90d04d5efff4b78e4370e8df8bd8b2d71b17f92832746639e2a53323b1c9678bcf71a617d5
7
- data.tar.gz: 6aa48617f392b62c597ad925746e008ef8f66ceb525f169fbbbfc904c7abd3551b77bd60b39af735b987bbd7a17fe14093859caf7eb9e55afdc9beab8be7d490
6
+ metadata.gz: 2364ad1bd990fca2ec78ad80a744c3d820e07b3add19dcbfcfeb88cebb5c07b47dc9b29acefafc27dac06a24b333cb11cd894ffba070435d4fbdcbb051d79941
7
+ data.tar.gz: af34ecfb3046c67d8aac92f227d37740130946ee5f3abb919b55c2d8edd86b40ba242970dc9bda05e2191c397e42548f200295f8259ca05653a7992fa5d3731a
data/exe/sqtui CHANGED
@@ -1,6 +1,42 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "solid_queue_tui"
4
+ # Boot the host Rails application's environment.
5
+ # This ensures ActiveRecord, Solid Queue models, and the database
6
+ # connection pool are fully loaded before the TUI starts.
7
+ env_file = File.join(Dir.pwd, "config", "environment.rb")
8
+
9
+ unless File.exist?(env_file)
10
+ $stderr.puts "Error: config/environment.rb not found in #{Dir.pwd}"
11
+ $stderr.puts ""
12
+ $stderr.puts "sqtui must be run from your Rails application's root directory."
13
+ $stderr.puts "Make sure solid_queue_tui is in your Gemfile and Solid Queue is configured."
14
+ exit 1
15
+ end
16
+
17
+ require env_file
18
+
19
+ # Log ActiveRecord queries to the Rails log file so TUI operations
20
+ # are visible in log/development.log (or whichever environment is active).
21
+ # Tag all TUI logs with [SQTUI] so they're easy to distinguish from app logs.
22
+ # CORRUPTING TERMINAL
23
+ # ActiveRecord::Base.logger = Rails.logger
24
+ # Rails.logger.push_tags("SQTUI") if Rails.logger.respond_to?(:push_tags)
25
+
26
+ # Logs must never hit STDOUT/STDERR — that would corrupt the TUI.
27
+ # If a log/ directory exists (traditional Rails), write there.
28
+ # Otherwise (Rails 8 defaults), silence logging.
29
+ #TODO: figure out logging for rails 8, docker
30
+
31
+ log_dir = File.join(Dir.pwd, "log")
32
+ if Dir.exist?(log_dir)
33
+ log_file = File.join(log_dir, "#{Rails.env}.log")
34
+ tui_logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(log_file))
35
+ tui_logger.push_tags("SQTUI")
36
+ else
37
+ tui_logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(File::NULL))
38
+ end
39
+ ActiveRecord::Base.logger = tui_logger
40
+ Rails.logger = tui_logger
5
41
 
6
42
  SolidQueueTui::CLI.run(ARGV)
@@ -4,28 +4,11 @@ module SolidQueueTui
4
4
  module Actions
5
5
  class DiscardJob
6
6
  def self.call(failed_execution_id)
7
- conn = ActiveRecord::Base.connection
8
-
9
- row = conn.select_one(
10
- "SELECT fe.id, fe.job_id FROM solid_queue_failed_executions fe " \
11
- "WHERE fe.id = #{conn.quote(failed_execution_id.to_i)}"
12
- )
13
- return false unless row
14
-
15
- conn.transaction do
16
- # Remove the failed execution
17
- conn.execute(
18
- "DELETE FROM solid_queue_failed_executions WHERE id = #{conn.quote(failed_execution_id.to_i)}"
19
- )
20
-
21
- # Mark the job as finished (discarded)
22
- conn.execute(
23
- "UPDATE solid_queue_jobs SET finished_at = #{conn.quote(Time.now.utc.iso8601)} " \
24
- "WHERE id = #{conn.quote(row['job_id'])}"
25
- )
26
- end
27
-
7
+ fe = SolidQueue::FailedExecution.find(failed_execution_id)
8
+ fe.discard
28
9
  true
10
+ rescue ActiveRecord::RecordNotFound
11
+ false
29
12
  rescue => e
30
13
  false
31
14
  end
@@ -4,30 +4,14 @@ module SolidQueueTui
4
4
  module Actions
5
5
  class DiscardScheduledJob
6
6
  def self.call(job_id)
7
- conn = ActiveRecord::Base.connection
8
-
9
- row = conn.select_one(
10
- "SELECT se.id, se.job_id " \
11
- "FROM solid_queue_scheduled_executions se " \
12
- "WHERE se.job_id = #{conn.quote(job_id.to_i)}"
13
- )
14
- return false unless row
15
-
16
- conn.transaction do
17
- conn.execute(
18
- "DELETE FROM solid_queue_scheduled_executions WHERE id = #{conn.quote(row['id'])}"
19
- )
20
-
21
- conn.execute(
22
- "UPDATE solid_queue_jobs SET finished_at = #{conn.quote(Time.now.utc.iso8601)} " \
23
- "WHERE id = #{conn.quote(row['job_id'])}"
24
- )
25
- end
26
-
7
+ se = SolidQueue::ScheduledExecution.find_by!(job_id: job_id)
8
+ se.discard
27
9
  true
10
+ rescue ActiveRecord::RecordNotFound
11
+ false
28
12
  rescue => e
29
13
  false
30
14
  end
31
15
  end
32
16
  end
33
- end
17
+ end
@@ -4,32 +4,13 @@ module SolidQueueTui
4
4
  module Actions
5
5
  class DispatchScheduledJob
6
6
  def self.call(job_id)
7
- conn = ActiveRecord::Base.connection
8
-
9
- row = conn.select_one(
10
- "SELECT se.id, se.job_id, j.queue_name, j.priority " \
11
- "FROM solid_queue_scheduled_executions se " \
12
- "JOIN solid_queue_jobs j ON j.id = se.job_id " \
13
- "WHERE j.id = #{conn.quote(job_id.to_i)}"
14
- )
15
- return false unless row
16
-
17
- conn.transaction do
18
- conn.execute(
19
- "INSERT INTO solid_queue_ready_executions (job_id, queue_name, priority, created_at) " \
20
- "VALUES (#{conn.quote(row['job_id'])}, #{conn.quote(row['queue_name'])}, " \
21
- "#{conn.quote(row['priority'])}, #{conn.quote(Time.now.utc.iso8601)})"
22
- )
23
-
24
- conn.execute(
25
- "DELETE FROM solid_queue_scheduled_executions WHERE id = #{conn.quote(row['id'])}"
26
- )
27
- end
28
-
7
+ SolidQueue::ScheduledExecution.dispatch_jobs([job_id])
29
8
  true
9
+ rescue ActiveRecord::RecordNotFound
10
+ false
30
11
  rescue => e
31
12
  false
32
13
  end
33
14
  end
34
15
  end
35
- end
16
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueueTui
4
+ module Actions
5
+ class EnqueueRecurringTask
6
+ def self.call(task_key)
7
+ task = SolidQueue::RecurringTask.find_by!(key: task_key)
8
+ task.enqueue(at: Time.now)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -4,72 +4,27 @@ module SolidQueueTui
4
4
  module Actions
5
5
  class RetryJob
6
6
  def self.call(failed_execution_id)
7
- conn = ActiveRecord::Base.connection
8
-
9
- # Get the failed execution and its job
10
- row = conn.select_one(
11
- "SELECT fe.id, fe.job_id, j.queue_name, j.priority " \
12
- "FROM solid_queue_failed_executions fe " \
13
- "JOIN solid_queue_jobs j ON j.id = fe.job_id " \
14
- "WHERE fe.id = #{conn.quote(failed_execution_id.to_i)}"
15
- )
16
- return false unless row
17
-
18
- conn.transaction do
19
- # Create a ready execution for the job
20
- conn.execute(
21
- "INSERT INTO solid_queue_ready_executions (job_id, queue_name, priority, created_at) " \
22
- "VALUES (#{conn.quote(row['job_id'])}, #{conn.quote(row['queue_name'])}, " \
23
- "#{conn.quote(row['priority'])}, #{conn.quote(Time.now.utc.iso8601)})"
24
- )
25
-
26
- # Remove the failed execution
27
- conn.execute(
28
- "DELETE FROM solid_queue_failed_executions WHERE id = #{conn.quote(failed_execution_id.to_i)}"
29
- )
30
-
31
- # Clear finished_at on the job
32
- conn.execute(
33
- "UPDATE solid_queue_jobs SET finished_at = NULL " \
34
- "WHERE id = #{conn.quote(row['job_id'])}"
35
- )
36
- end
37
-
7
+ fe = SolidQueue::FailedExecution.find(failed_execution_id)
8
+ fe.retry
38
9
  true
10
+ rescue ActiveRecord::RecordNotFound
11
+ false
39
12
  rescue => e
40
13
  false
41
14
  end
42
15
 
43
- def self.retry_all
44
- conn = ActiveRecord::Base.connection
45
-
46
- rows = conn.select_all(
47
- "SELECT fe.id, fe.job_id, j.queue_name, j.priority " \
48
- "FROM solid_queue_failed_executions fe " \
49
- "JOIN solid_queue_jobs j ON j.id = fe.job_id"
50
- )
51
-
52
- count = 0
53
- rows.each do |row|
54
- conn.transaction do
55
- conn.execute(
56
- "INSERT INTO solid_queue_ready_executions (job_id, queue_name, priority, created_at) " \
57
- "VALUES (#{conn.quote(row['job_id'])}, #{conn.quote(row['queue_name'])}, " \
58
- "#{conn.quote(row['priority'])}, #{conn.quote(Time.now.utc.iso8601)})"
59
- )
60
- conn.execute(
61
- "DELETE FROM solid_queue_failed_executions WHERE id = #{conn.quote(row['id'])}"
62
- )
63
- conn.execute(
64
- "UPDATE solid_queue_jobs SET finished_at = NULL WHERE id = #{conn.quote(row['job_id'])}"
65
- )
66
- count += 1
67
- end
68
- rescue
69
- next
70
- end
16
+ def self.retry_all(filter: nil, queue: nil)
17
+ scope = SolidQueue::FailedExecution.joins(:job)
18
+ scope = scope.merge(SolidQueue::Job.where("class_name LIKE ?", "%#{filter}%")) if filter.present?
19
+ scope = scope.merge(SolidQueue::Job.where(queue_name: queue)) if queue.present?
20
+ count = scope.count
21
+ return 0 if count == 0
71
22
 
23
+ jobs = SolidQueue::Job.where(id: scope.select(:job_id))
24
+ SolidQueue::FailedExecution.retry_all(jobs)
72
25
  count
26
+ rescue => e
27
+ 0
73
28
  end
74
29
  end
75
30
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueueTui
4
+ module Actions
5
+ class ToggleQueuePause
6
+ def self.call(queue_name)
7
+ queue = SolidQueue::Queue.find_by_name(queue_name)
8
+ if queue.paused?
9
+ queue.resume
10
+ else
11
+ queue.pause
12
+ end
13
+ true
14
+ rescue => e
15
+ false
16
+ end
17
+ end
18
+ end
19
+ end
@@ -12,9 +12,10 @@ module SolidQueueTui
12
12
  VIEW_BLOCKED = 4
13
13
  VIEW_SCHEDULED = 5
14
14
  VIEW_FINISHED = 6
15
- VIEW_WORKERS = 7
15
+ VIEW_RECURRING = 7
16
+ VIEW_WORKERS = 8
16
17
 
17
- VIEW_COUNT = 8
18
+ VIEW_COUNT = 9
18
19
 
19
20
  COMMAND_MAP = {
20
21
  "dashboard" => VIEW_DASHBOARD,
@@ -24,6 +25,7 @@ module SolidQueueTui
24
25
  "blocked" => VIEW_BLOCKED,
25
26
  "scheduled" => VIEW_SCHEDULED,
26
27
  "finished" => VIEW_FINISHED,
28
+ "recurring" => VIEW_RECURRING,
27
29
  "workers" => VIEW_WORKERS
28
30
  }.freeze
29
31
 
@@ -39,8 +41,7 @@ module SolidQueueTui
39
41
  end
40
42
 
41
43
  def run
42
- config = Connection.establish!
43
- @refresh_interval = config.fetch("refresh", 2).to_i
44
+ @refresh_interval = SolidQueueTui.refresh_interval
44
45
  setup_dev_reloader! if @dev
45
46
 
46
47
  RatatuiRuby.run do |tui|
@@ -69,6 +70,7 @@ module SolidQueueTui
69
70
  VIEW_BLOCKED => Views::BlockedView.new(@tui),
70
71
  VIEW_SCHEDULED => Views::ScheduledView.new(@tui),
71
72
  VIEW_FINISHED => Views::FinishedView.new(@tui),
73
+ VIEW_RECURRING => Views::RecurringTasksView.new(@tui),
72
74
  VIEW_WORKERS => Views::ProcessesView.new(@tui)
73
75
  }
74
76
  @job_detail = Views::JobDetailView.new(@tui)
@@ -141,10 +143,17 @@ module SolidQueueTui
141
143
  return false
142
144
  end
143
145
 
144
- # If view is in a modal state (filter, confirm), it gets all input
146
+ # If view is in a modal state (filter, confirm, detail sub-view), it gets all input
145
147
  if current_view.respond_to?(:capturing_input?) && current_view.capturing_input?
146
148
  result = current_view.handle_input(event)
147
- refresh_data! if result == :refresh
149
+ case result
150
+ when :refresh, :enter_queue, :exit_queue
151
+ refresh_data!
152
+ when :load_more
153
+ load_more_data!
154
+ when :open_detail
155
+ open_detail
156
+ end
148
157
  return false
149
158
  end
150
159
 
@@ -191,11 +200,17 @@ module SolidQueueTui
191
200
  switch_view(VIEW_FINISHED)
192
201
  return false
193
202
  in { type: :key, code: "8" }
203
+ switch_view(VIEW_RECURRING)
204
+ return false
205
+ in { type: :key, code: "9" }
194
206
  switch_view(VIEW_WORKERS)
195
207
  return false
196
208
  in { type: :key, code: "tab" }
197
209
  switch_view((@current_view + 1) % VIEW_COUNT)
198
210
  return false
211
+ in { type: :key, code: "back_tab" }
212
+ switch_view((@current_view - 1) % VIEW_COUNT)
213
+ return false
199
214
  in { type: :key, code: "enter" }
200
215
  open_detail
201
216
  return false
@@ -210,7 +225,11 @@ module SolidQueueTui
210
225
 
211
226
  # Pass to current view
212
227
  result = current_view.handle_input(event)
213
- refresh_data! if result == :refresh
228
+ if result == :refresh
229
+ refresh_data!
230
+ elsif result == :load_more
231
+ load_more_data!
232
+ end
214
233
 
215
234
  false
216
235
  end
@@ -229,11 +248,21 @@ module SolidQueueTui
229
248
  return unless item
230
249
 
231
250
  case @current_view
251
+ when VIEW_QUEUES
252
+ if current_view.detail_mode?
253
+ @job_detail.show(job: item) if item.respond_to?(:id)
254
+ else
255
+ result = current_view.handle_input({ type: :key, code: "enter" })
256
+ refresh_data! if result == :enter_queue
257
+ end
232
258
  when VIEW_FAILED
233
259
  failed_job = Data::FailedQuery.fetch_one(item.id) if item.respond_to?(:id)
234
260
  @job_detail.show(failed_job: failed_job || item)
235
261
  when VIEW_IN_PROGRESS, VIEW_BLOCKED, VIEW_SCHEDULED, VIEW_FINISHED
236
262
  @job_detail.show(job: item) if item.respond_to?(:id)
263
+ when VIEW_WORKERS
264
+ running_jobs = Data::ProcessesQuery.fetch_running_jobs(process_id: item.id)
265
+ @job_detail.show(process: item, running_jobs: running_jobs)
237
266
  end
238
267
  end
239
268
 
@@ -243,38 +272,94 @@ module SolidQueueTui
243
272
  end
244
273
 
245
274
  def refresh_data!
246
- @stats = Data::Stats.fetch
247
275
  @last_refresh = Time.now
248
276
 
249
277
  case @current_view
250
278
  when VIEW_DASHBOARD
279
+ @stats = Data::Stats.fetch
251
280
  current_view.update(stats: @stats)
252
281
  when VIEW_QUEUES
253
- queues = Data::QueuesQuery.fetch
254
- current_view.update(queues: queues)
282
+ if current_view.detail_mode?
283
+ q = current_view.selected_queue_name
284
+ f = current_view.filters
285
+ current_view.total_count = Data::JobsQuery.count_pending(queue: q, filter: f[:class_name])
286
+ jobs = Data::JobsQuery.fetch_pending(queue: q, filter: f[:class_name], limit: SolidQueueTui.page_size, offset: 0)
287
+ current_view.update_detail(jobs: jobs)
288
+ else
289
+ queues = Data::QueuesQuery.fetch
290
+ current_view.update(queues: queues)
291
+ end
255
292
  when VIEW_FAILED
256
- filter = current_view.filter
257
- failed_jobs = Data::FailedQuery.fetch(filter: filter)
293
+ f = current_view.filters
294
+ current_view.total_count = Data::FailedQuery.count(filter: f[:class_name], queue: f[:queue])
295
+ failed_jobs = Data::FailedQuery.fetch(filter: f[:class_name], queue: f[:queue], limit: SolidQueueTui.page_size, offset: 0)
258
296
  current_view.update(failed_jobs: failed_jobs)
259
297
  when VIEW_IN_PROGRESS
260
- jobs = Data::JobsQuery.fetch(status: "claimed")
298
+ f = current_view.filters
299
+ current_view.total_count = Data::JobsQuery.count(status: "claimed", filter: f[:class_name], queue: f[:queue])
300
+ jobs = Data::JobsQuery.fetch(status: "claimed", filter: f[:class_name], queue: f[:queue], limit: SolidQueueTui.page_size, offset: 0)
261
301
  current_view.update(jobs: jobs)
262
302
  when VIEW_BLOCKED
263
- jobs = Data::JobsQuery.fetch(status: "blocked")
303
+ f = current_view.filters
304
+ current_view.total_count = Data::JobsQuery.count(status: "blocked", filter: f[:class_name], queue: f[:queue])
305
+ jobs = Data::JobsQuery.fetch(status: "blocked", filter: f[:class_name], queue: f[:queue], limit: SolidQueueTui.page_size, offset: 0)
264
306
  current_view.update(jobs: jobs)
265
307
  when VIEW_SCHEDULED
266
- jobs = Data::JobsQuery.fetch(status: "scheduled")
308
+ f = current_view.filters
309
+ current_view.total_count = Data::JobsQuery.count(status: "scheduled", filter: f[:class_name], queue: f[:queue])
310
+ jobs = Data::JobsQuery.fetch(status: "scheduled", filter: f[:class_name], queue: f[:queue], limit: SolidQueueTui.page_size, offset: 0)
267
311
  current_view.update(jobs: jobs)
268
312
  when VIEW_FINISHED
269
- filter = current_view.respond_to?(:filter) ? current_view.filter : nil
270
- jobs = Data::JobsQuery.fetch(status: "completed", filter: filter)
313
+ f = current_view.filters
314
+ current_view.total_count = Data::JobsQuery.count(status: "completed", filter: f[:class_name], queue: f[:queue])
315
+ jobs = Data::JobsQuery.fetch(status: "completed", filter: f[:class_name], queue: f[:queue], limit: SolidQueueTui.page_size, offset: 0)
271
316
  current_view.update(jobs: jobs)
317
+ when VIEW_RECURRING
318
+ tasks = Data::RecurringTasksQuery.fetch
319
+ current_view.update(tasks: tasks)
272
320
  when VIEW_WORKERS
273
321
  processes = Data::ProcessesQuery.fetch
274
322
  current_view.update(processes: processes)
275
323
  end
276
324
  rescue => e
277
- # Silently handle refresh errors to keep TUI responsive
325
+ Rails.logger.tagged("SQTUI") { Rails.logger.error("refresh_data! error: #{e.class}: #{e.message}\n#{e.backtrace&.first(5)&.join("\n")}") } if defined?(Rails) && Rails.logger
326
+ end
327
+
328
+ def load_more_data!
329
+ view = current_view
330
+ offset = view.current_offset
331
+
332
+ case @current_view
333
+ when VIEW_QUEUES
334
+ if view.detail_mode?
335
+ q = view.selected_queue_name
336
+ f = view.filters
337
+ more = Data::JobsQuery.fetch_pending(queue: q, filter: f[:class_name], limit: SolidQueueTui.page_size, offset: offset)
338
+ view.append(jobs: more)
339
+ end
340
+ when VIEW_FAILED
341
+ f = view.filters
342
+ more = Data::FailedQuery.fetch(filter: f[:class_name], queue: f[:queue], limit: SolidQueueTui.page_size, offset: offset)
343
+ view.append(failed_jobs: more)
344
+ when VIEW_IN_PROGRESS
345
+ f = view.filters
346
+ more = Data::JobsQuery.fetch(status: "claimed", filter: f[:class_name], queue: f[:queue], limit: SolidQueueTui.page_size, offset: offset)
347
+ view.append(jobs: more)
348
+ when VIEW_BLOCKED
349
+ f = view.filters
350
+ more = Data::JobsQuery.fetch(status: "blocked", filter: f[:class_name], queue: f[:queue], limit: SolidQueueTui.page_size, offset: offset)
351
+ view.append(jobs: more)
352
+ when VIEW_SCHEDULED
353
+ f = view.filters
354
+ more = Data::JobsQuery.fetch(status: "scheduled", filter: f[:class_name], queue: f[:queue], limit: SolidQueueTui.page_size, offset: offset)
355
+ view.append(jobs: more)
356
+ when VIEW_FINISHED
357
+ f = view.filters
358
+ more = Data::JobsQuery.fetch(status: "completed", filter: f[:class_name], queue: f[:queue], limit: SolidQueueTui.page_size, offset: offset)
359
+ view.append(jobs: more)
360
+ end
361
+ rescue => e
362
+ Rails.logger.tagged("SQTUI") { Rails.logger.error("load_more_data! error: #{e.class}: #{e.message}") } if defined?(Rails) && Rails.logger
278
363
  end
279
364
 
280
365
  def setup_dev_reloader!
@@ -393,8 +478,9 @@ module SolidQueueTui
393
478
  ]),
394
479
  empty_line,
395
480
  help_section("Navigation"),
396
- help_line("1-8", "Switch between views"),
481
+ help_line("1-9", "Switch between views"),
397
482
  help_line("Tab", "Next view"),
483
+ help_line("Shift + Tab", "Previous View"),
398
484
  help_line(":", "Command mode (:queues, :failed, ...)"),
399
485
  help_line("Esc", "Back to Dashboard"),
400
486
  help_line("j / Up", "Move selection up"),
@@ -418,12 +504,14 @@ module SolidQueueTui
418
504
  help_line("5", "Blocked — Concurrency-blocked jobs"),
419
505
  help_line("6", "Scheduled — Future scheduled jobs"),
420
506
  help_line("7", "Finished — Completed jobs"),
421
- help_line("8", "WorkersActive processes"),
507
+ help_line("8", "RecurringRecurring tasks"),
508
+ help_line("9", "Workers — Active processes"),
422
509
  empty_line,
423
510
  help_section("General"),
424
511
  help_line("?", "Toggle this help"),
425
512
  help_line("q", "Quit"),
426
- help_line("Ctrl+C", "Force quit")
513
+ help_line("Ctrl+C", "Force quit"),
514
+ help_line("fn + select", "Select Text")
427
515
  ]
428
516
 
429
517
  frame.render_widget(
@@ -7,9 +7,6 @@ module SolidQueueTui
7
7
  def self.run(args)
8
8
  options = parse_options(args)
9
9
  Application.new(**options).run
10
- rescue Connection::ConnectionError => e
11
- $stderr.puts "Connection error: #{e.message}"
12
- exit 1
13
10
  rescue Interrupt
14
11
  exit 0
15
12
  end
@@ -22,10 +19,22 @@ module SolidQueueTui
22
19
  opts.separator ""
23
20
  opts.separator "Options:"
24
21
 
25
- opts.on("--dev", "Enable hot-reload (watches lib/ for changes)") do
22
+ opts.on("--dev", "Enable hot-reload (development only)") do
23
+ unless defined?(Rails) && Rails.env.development?
24
+ $stderr.puts "Error: --dev is only allowed in the development environment."
25
+ exit 1
26
+ end
26
27
  options[:dev] = true
27
28
  end
28
29
 
30
+ opts.on("--page-size N", Integer, "Number of rows per page (default: #{SolidQueueTui.page_size})") do |n|
31
+ SolidQueueTui.page_size = n
32
+ end
33
+
34
+ opts.on("--refresh-interval N", Integer, "Refresh interval in seconds (default: #{SolidQueueTui.refresh_interval})") do |n|
35
+ SolidQueueTui.refresh_interval = n
36
+ end
37
+
29
38
  opts.on("-v", "--version", "Show version") do
30
39
  puts "sqtui v#{SolidQueueTui::VERSION}"
31
40
  exit
@@ -34,10 +43,8 @@ module SolidQueueTui
34
43
  opts.on("-h", "--help", "Show this help") do
35
44
  puts opts
36
45
  puts ""
37
- puts "Configuration:"
38
- puts " Create config/solid_tui.yml with:"
39
- puts " database_url: sqlite3:storage/queue.sqlite3"
40
- puts " refresh: 2"
46
+ puts "Run from your Rails app root directory."
47
+ puts "Requires solid_queue_tui in your Gemfile and Solid Queue configured."
41
48
  exit
42
49
  end
43
50
  end.parse!(args)
@@ -4,11 +4,11 @@ module SolidQueueTui
4
4
  module Components
5
5
  class Header
6
6
  LOGO = [
7
- " ____ _ _ _ ___ ",
8
- "/ ___| ___ | (_) __| | / _ \\ _ _ ___ _ _ ___ ",
9
- "\\___ \\ / _ \\| | |/ _` | | | | | | | |/ _ \\ | | |/ _ \\",
10
- " ___) | (_) | | | (_| | | |_| | |_| | __/ |_| | __/",
11
- "|____/ \\___/|_|_|\\__,_| \\___/\\__,_|\\___|\\__,_|\\___|"
7
+ " ____ _ _ _ ___ ",
8
+ "/ ___| ___ | (_) __| | / _ \\ _ _ ___ _ _ ___",
9
+ "\\___ \\ / _ \\| | |/ _` | | | | || | | |/ _ \\ | | |/ _ \\",
10
+ " ___) | (_) | | | (_| | | |_| || |_| | __/ |_| | __/",
11
+ "|____/ \\___/|_|_|\\__,_| \\__\\_\\ \\__,_|\\___|\\__,_|\\___|"
12
12
  ].freeze
13
13
 
14
14
  VIEWS = [
@@ -19,7 +19,8 @@ module SolidQueueTui
19
19
  { key: "5", label: "Blocked" },
20
20
  { key: "6", label: "Scheduled" },
21
21
  { key: "7", label: "Finished" },
22
- { key: "8", label: "Workers" }
22
+ { key: "8", label: "Recurring" },
23
+ { key: "9", label: "Workers" }
23
24
  ].freeze
24
25
 
25
26
  def initialize(tui, current_view:)
@@ -61,36 +62,34 @@ module SolidQueueTui
61
62
  end
62
63
 
63
64
  def render_nav(frame, area)
64
- spans = [
65
- @tui.text_span(content: " ", style: @tui.style(fg: :white))
66
- ]
65
+ tab_spans = []
67
66
 
68
67
  VIEWS.each_with_index do |view, idx|
69
68
  active = idx == @current_view
70
69
 
71
- spans << @tui.text_span(
72
- content: "<#{view[:key]}>",
73
- style: @tui.style(fg: :cyan, modifiers: active ? [:bold] : [])
74
- )
75
- spans << @tui.text_span(
76
- content: " #{view[:label]}",
77
- style: @tui.style(
78
- fg: active ? :yellow : :dark_gray,
79
- modifiers: active ? [:bold, :underlined] : []
70
+ if active
71
+ tab_spans << @tui.text_span(
72
+ content: " #{view[:key]}·#{view[:label]} ",
73
+ style: @tui.style(fg: :cyan, modifiers: [:bold, :underlined])
74
+ )
75
+ else
76
+ tab_spans << @tui.text_span(
77
+ content: " #{view[:key]}·#{view[:label]} ",
78
+ style: @tui.style(fg: :dark_gray)
80
79
  )
81
- )
82
- spans << @tui.text_span(content: " ", style: @tui.style(fg: :white))
80
+ end
81
+ tab_spans << @tui.text_span(content: " ", style: @tui.style(fg: :white))
83
82
  end
84
83
 
85
84
  lines = [
86
- @tui.text_line(spans: spans, alignment: :right),
85
+ @tui.text_line(spans: tab_spans, alignment: :right),
87
86
  @tui.text_line(spans: [
88
- @tui.text_span(content: "<?>", style: @tui.style(fg: :cyan)),
89
- @tui.text_span(content: " Help ", style: @tui.style(fg: :dark_gray)),
90
- @tui.text_span(content: "<q>", style: @tui.style(fg: :cyan)),
91
- @tui.text_span(content: " Quit ", style: @tui.style(fg: :dark_gray)),
92
- @tui.text_span(content: "<r>", style: @tui.style(fg: :cyan)),
93
- @tui.text_span(content: " Refresh", style: @tui.style(fg: :dark_gray))
87
+ @tui.text_span(content: "<?> ", style: @tui.style(fg: :cyan)),
88
+ @tui.text_span(content: "Help ", style: @tui.style(fg: :dark_gray)),
89
+ @tui.text_span(content: "<q> ", style: @tui.style(fg: :cyan)),
90
+ @tui.text_span(content: "Quit ", style: @tui.style(fg: :dark_gray)),
91
+ @tui.text_span(content: "<r> ", style: @tui.style(fg: :cyan)),
92
+ @tui.text_span(content: "Refresh", style: @tui.style(fg: :dark_gray))
94
93
  ], alignment: :right)
95
94
  ]
96
95
 
@@ -7,6 +7,7 @@ module SolidQueueTui
7
7
  { key: "q", action: "Quit" },
8
8
  { key: "r", action: "Refresh" },
9
9
  { key: "Tab", action: "Next View" },
10
+ { Key: "Shift + Tab", action: "Previous View"},
10
11
  { key: "j/k", action: "Navigate" },
11
12
  { key: "/", action: "Filter" },
12
13
  { key: "Esc", action: "Clear" }