solid_queue_tui 0.1.1 → 0.1.2

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/exe/sqtui +20 -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 +73 -17
  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 +14 -2
  14. data/lib/solid_queue_tui/data/failed_query.rb +37 -91
  15. data/lib/solid_queue_tui/data/jobs_query.rb +90 -123
  16. data/lib/solid_queue_tui/data/processes_query.rb +10 -34
  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/railtie.rb +9 -0
  21. data/lib/solid_queue_tui/version.rb +1 -1
  22. data/lib/solid_queue_tui/views/blocked_view.rb +126 -46
  23. data/lib/solid_queue_tui/views/concerns/filterable.rb +128 -0
  24. data/lib/solid_queue_tui/views/dashboard_view.rb +2 -1
  25. data/lib/solid_queue_tui/views/failed_view.rb +62 -72
  26. data/lib/solid_queue_tui/views/finished_view.rb +54 -67
  27. data/lib/solid_queue_tui/views/in_progress_view.rb +124 -44
  28. data/lib/solid_queue_tui/views/queues_view.rb +119 -35
  29. data/lib/solid_queue_tui/views/recurring_tasks_view.rb +202 -0
  30. data/lib/solid_queue_tui/views/scheduled_view.rb +74 -8
  31. data/lib/solid_queue_tui.rb +15 -4
  32. data/lib/tasks/solid_queue_tui.rake +8 -0
  33. metadata +16 -24
  34. 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: 362a8f6c4d4bd257ed5bdc4a7eaf93fb986b978313a5847bffd3dfe8e9f963ed
4
+ data.tar.gz: 9aabeac6f1fa7404ff7a485b2a47669ed1313244348fc29dc4bc34a751ab9bd8
5
5
  SHA512:
6
- metadata.gz: 5c5484690f95c5b7602399255d40c32d398de508c6e0d9240d221e90d04d5efff4b78e4370e8df8bd8b2d71b17f92832746639e2a53323b1c9678bcf71a617d5
7
- data.tar.gz: 6aa48617f392b62c597ad925746e008ef8f66ceb525f169fbbbfc904c7abd3551b77bd60b39af735b987bbd7a17fe14093859caf7eb9e55afdc9beab8be7d490
6
+ metadata.gz: 7de0862123508768172c46a17458c55651b6bda487d79e76f7e61933d223e29e848ba0614b4fc116acc8222dc5a245ac590fc92cff595b3c65c67367537589c1
7
+ data.tar.gz: b8d145ba61da8f1de1fe3a4c1b3678e06ca36e19e6e4622acc67942adf194228eb6c96e09cfa114168cc8c3b3b28140ffd2d53b05d653081394c89406fdf6648
data/exe/sqtui CHANGED
@@ -1,6 +1,25 @@
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
+ ActiveRecord::Base.logger = Rails.logger
23
+ Rails.logger.push_tags("SQTUI") if Rails.logger.respond_to?(:push_tags)
5
24
 
6
25
  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)
@@ -191,11 +193,17 @@ module SolidQueueTui
191
193
  switch_view(VIEW_FINISHED)
192
194
  return false
193
195
  in { type: :key, code: "8" }
196
+ switch_view(VIEW_RECURRING)
197
+ return false
198
+ in { type: :key, code: "9" }
194
199
  switch_view(VIEW_WORKERS)
195
200
  return false
196
201
  in { type: :key, code: "tab" }
197
202
  switch_view((@current_view + 1) % VIEW_COUNT)
198
203
  return false
204
+ in { type: :key, code: "back_tab" }
205
+ switch_view((@current_view - 1) % VIEW_COUNT)
206
+ return false
199
207
  in { type: :key, code: "enter" }
200
208
  open_detail
201
209
  return false
@@ -210,7 +218,11 @@ module SolidQueueTui
210
218
 
211
219
  # Pass to current view
212
220
  result = current_view.handle_input(event)
213
- refresh_data! if result == :refresh
221
+ if result == :refresh
222
+ refresh_data!
223
+ elsif result == :load_more
224
+ load_more_data!
225
+ end
214
226
 
215
227
  false
216
228
  end
@@ -243,38 +255,79 @@ module SolidQueueTui
243
255
  end
244
256
 
245
257
  def refresh_data!
246
- @stats = Data::Stats.fetch
247
258
  @last_refresh = Time.now
248
259
 
249
260
  case @current_view
250
261
  when VIEW_DASHBOARD
262
+ @stats = Data::Stats.fetch
251
263
  current_view.update(stats: @stats)
252
264
  when VIEW_QUEUES
253
265
  queues = Data::QueuesQuery.fetch
254
266
  current_view.update(queues: queues)
255
267
  when VIEW_FAILED
256
- filter = current_view.filter
257
- failed_jobs = Data::FailedQuery.fetch(filter: filter)
268
+ f = current_view.filters
269
+ current_view.total_count = Data::FailedQuery.count(filter: f[:class_name], queue: f[:queue])
270
+ failed_jobs = Data::FailedQuery.fetch(filter: f[:class_name], queue: f[:queue], limit: SolidQueueTui.page_size, offset: 0)
258
271
  current_view.update(failed_jobs: failed_jobs)
259
272
  when VIEW_IN_PROGRESS
260
- jobs = Data::JobsQuery.fetch(status: "claimed")
273
+ f = current_view.filters
274
+ current_view.total_count = Data::JobsQuery.count(status: "claimed", filter: f[:class_name], queue: f[:queue])
275
+ jobs = Data::JobsQuery.fetch(status: "claimed", filter: f[:class_name], queue: f[:queue], limit: SolidQueueTui.page_size, offset: 0)
261
276
  current_view.update(jobs: jobs)
262
277
  when VIEW_BLOCKED
263
- jobs = Data::JobsQuery.fetch(status: "blocked")
278
+ f = current_view.filters
279
+ current_view.total_count = Data::JobsQuery.count(status: "blocked", filter: f[:class_name], queue: f[:queue])
280
+ jobs = Data::JobsQuery.fetch(status: "blocked", filter: f[:class_name], queue: f[:queue], limit: SolidQueueTui.page_size, offset: 0)
264
281
  current_view.update(jobs: jobs)
265
282
  when VIEW_SCHEDULED
266
- jobs = Data::JobsQuery.fetch(status: "scheduled")
283
+ f = current_view.filters
284
+ current_view.total_count = Data::JobsQuery.count(status: "scheduled", filter: f[:class_name], queue: f[:queue])
285
+ jobs = Data::JobsQuery.fetch(status: "scheduled", filter: f[:class_name], queue: f[:queue], limit: SolidQueueTui.page_size, offset: 0)
267
286
  current_view.update(jobs: jobs)
268
287
  when VIEW_FINISHED
269
- filter = current_view.respond_to?(:filter) ? current_view.filter : nil
270
- jobs = Data::JobsQuery.fetch(status: "completed", filter: filter)
288
+ f = current_view.filters
289
+ current_view.total_count = Data::JobsQuery.count(status: "completed", filter: f[:class_name], queue: f[:queue])
290
+ jobs = Data::JobsQuery.fetch(status: "completed", filter: f[:class_name], queue: f[:queue], limit: SolidQueueTui.page_size, offset: 0)
271
291
  current_view.update(jobs: jobs)
292
+ when VIEW_RECURRING
293
+ tasks = Data::RecurringTasksQuery.fetch
294
+ current_view.update(tasks: tasks)
272
295
  when VIEW_WORKERS
273
296
  processes = Data::ProcessesQuery.fetch
274
297
  current_view.update(processes: processes)
275
298
  end
276
299
  rescue => e
277
- # Silently handle refresh errors to keep TUI responsive
300
+ 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
301
+ end
302
+
303
+ def load_more_data!
304
+ view = current_view
305
+ offset = view.current_offset
306
+
307
+ case @current_view
308
+ when VIEW_FAILED
309
+ f = view.filters
310
+ more = Data::FailedQuery.fetch(filter: f[:class_name], queue: f[:queue], limit: SolidQueueTui.page_size, offset: offset)
311
+ view.append(failed_jobs: more)
312
+ when VIEW_IN_PROGRESS
313
+ f = view.filters
314
+ more = Data::JobsQuery.fetch(status: "claimed", filter: f[:class_name], queue: f[:queue], limit: SolidQueueTui.page_size, offset: offset)
315
+ view.append(jobs: more)
316
+ when VIEW_BLOCKED
317
+ f = view.filters
318
+ more = Data::JobsQuery.fetch(status: "blocked", filter: f[:class_name], queue: f[:queue], limit: SolidQueueTui.page_size, offset: offset)
319
+ view.append(jobs: more)
320
+ when VIEW_SCHEDULED
321
+ f = view.filters
322
+ more = Data::JobsQuery.fetch(status: "scheduled", filter: f[:class_name], queue: f[:queue], limit: SolidQueueTui.page_size, offset: offset)
323
+ view.append(jobs: more)
324
+ when VIEW_FINISHED
325
+ f = view.filters
326
+ more = Data::JobsQuery.fetch(status: "completed", filter: f[:class_name], queue: f[:queue], limit: SolidQueueTui.page_size, offset: offset)
327
+ view.append(jobs: more)
328
+ end
329
+ rescue => e
330
+ Rails.logger.tagged("SQTUI") { Rails.logger.error("load_more_data! error: #{e.class}: #{e.message}") } if defined?(Rails) && Rails.logger
278
331
  end
279
332
 
280
333
  def setup_dev_reloader!
@@ -393,8 +446,9 @@ module SolidQueueTui
393
446
  ]),
394
447
  empty_line,
395
448
  help_section("Navigation"),
396
- help_line("1-8", "Switch between views"),
449
+ help_line("1-9", "Switch between views"),
397
450
  help_line("Tab", "Next view"),
451
+ help_line("Shift + Tab", "Previous View"),
398
452
  help_line(":", "Command mode (:queues, :failed, ...)"),
399
453
  help_line("Esc", "Back to Dashboard"),
400
454
  help_line("j / Up", "Move selection up"),
@@ -418,12 +472,14 @@ module SolidQueueTui
418
472
  help_line("5", "Blocked — Concurrency-blocked jobs"),
419
473
  help_line("6", "Scheduled — Future scheduled jobs"),
420
474
  help_line("7", "Finished — Completed jobs"),
421
- help_line("8", "WorkersActive processes"),
475
+ help_line("8", "RecurringRecurring tasks"),
476
+ help_line("9", "Workers — Active processes"),
422
477
  empty_line,
423
478
  help_section("General"),
424
479
  help_line("?", "Toggle this help"),
425
480
  help_line("q", "Quit"),
426
- help_line("Ctrl+C", "Force quit")
481
+ help_line("Ctrl+C", "Force quit"),
482
+ help_line("fn + select", "Select Text")
427
483
  ]
428
484
 
429
485
  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" }
@@ -12,15 +12,18 @@ module SolidQueueTui
12
12
  "completed" => :dark_gray,
13
13
  "active" => :green,
14
14
  "paused" => :red,
15
+ "delayed" => :red,
16
+ "pending" => :dark_gray,
15
17
  "unknown" => :white
16
18
  }.freeze
17
19
 
18
- def initialize(tui, title:, columns:, rows:, selected_row: nil, empty_message: "No data")
20
+ def initialize(tui, title:, columns:, rows:, selected_row: nil, total_count: nil, empty_message: "No data")
19
21
  @tui = tui
20
22
  @title = title
21
23
  @columns = columns
22
24
  @rows = rows
23
25
  @selected_row = selected_row
26
+ @total_count = total_count
24
27
  @empty_message = empty_message
25
28
  end
26
29
 
@@ -36,11 +39,20 @@ module SolidQueueTui
36
39
  private
37
40
 
38
41
  def title_text
39
- text = " #{@title} [#{@rows.size}]"
42
+ count_text = if @total_count && @total_count > @rows.size
43
+ "#{@rows.size}/#{format_number(@total_count)}"
44
+ else
45
+ @rows.size.to_s
46
+ end
47
+ text = " #{@title} [#{count_text}]"
40
48
  text += " #{@selected_row + 1}/#{@rows.size}" if @selected_row && @rows.size > 0
41
49
  text + " "
42
50
  end
43
51
 
52
+ def format_number(n)
53
+ n.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
54
+ end
55
+
44
56
  def render_table(frame, area, table_state)
45
57
  widths = @columns.map do |col|
46
58
  case col[:width]