solid_queue_tui 0.1.3 → 0.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ea9a6cc3e42df6d7c5f50f91ed2bbf360bbd3e65c9fabb6f53bcf649b69826f1
4
- data.tar.gz: 3cd1c06f2c4ea19f2c59e0c5dc79962211d8f18b8200b4f46955910c2fcd78e6
3
+ metadata.gz: edcec2bda0cc128879fda2b066fcbd560c7879c4a2554d53d3ff01c50c4d5040
4
+ data.tar.gz: 2457d9eb670fddb7a851f64dbe37c1f2b69fdcd9608d69e2feb5b0af4b406358
5
5
  SHA512:
6
- metadata.gz: 2364ad1bd990fca2ec78ad80a744c3d820e07b3add19dcbfcfeb88cebb5c07b47dc9b29acefafc27dac06a24b333cb11cd894ffba070435d4fbdcbb051d79941
7
- data.tar.gz: af34ecfb3046c67d8aac92f227d37740130946ee5f3abb919b55c2d8edd86b40ba242970dc9bda05e2191c397e42548f200295f8259ca05653a7992fa5d3731a
6
+ metadata.gz: a6f0174997d605c9e0d83c22c8a53973b017cd2e4cdd9cc260ace6f249d1a029c0c475a78124d5fd9537b9e5d1ca361c4b64adbd4a47c5842d0d05ede3ae153d
7
+ data.tar.gz: af07fa966c63b5f63b4f2e8d034e0d44b023294d443403b9130a631a5ecd3c52913dc0d71ef42ae85de8d81d37fc9e3c5e8def0f983cff6e372526bdf8e710bd
data/exe/qtop ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
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 "qtop 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
+ # Logs must never hit STDOUT/STDERR — that would corrupt the TUI.
20
+ # If a log/ directory exists (traditional Rails), write there.
21
+ # Otherwise (Rails 8 defaults), silence logging.
22
+ #TODO: figure out logging for rails 8, docker
23
+
24
+ log_dir = File.join(Dir.pwd, "log")
25
+ if Dir.exist?(log_dir)
26
+ log_file = File.join(log_dir, "#{Rails.env}.log")
27
+ tui_logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(log_file))
28
+ tui_logger.push_tags("SQTUI")
29
+ else
30
+ tui_logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(File::NULL))
31
+ end
32
+ ActiveRecord::Base.logger = tui_logger
33
+ Rails.logger = tui_logger
34
+
35
+ SolidQueueTui::CLI.run(ARGV)
@@ -492,6 +492,7 @@ module SolidQueueTui
492
492
  help_section("Actions"),
493
493
  help_line("r", "Refresh data"),
494
494
  help_line("/", "Filter by class name"),
495
+ help_line("c", "Clear active filter"),
495
496
  help_line("R", "Retry failed job (in Failed view)"),
496
497
  help_line("D", "Discard failed job (in Failed view)"),
497
498
  help_line("A", "Retry all failed jobs"),
@@ -15,7 +15,7 @@ module SolidQueueTui
15
15
  options = {}
16
16
 
17
17
  OptionParser.new do |opts|
18
- opts.banner = "Usage: sqtui [options]"
18
+ opts.banner = "Usage: qtop [options]"
19
19
  opts.separator ""
20
20
  opts.separator "Options:"
21
21
 
@@ -36,7 +36,7 @@ module SolidQueueTui
36
36
  end
37
37
 
38
38
  opts.on("-v", "--version", "Show version") do
39
- puts "sqtui v#{SolidQueueTui::VERSION}"
39
+ puts "qtop v#{SolidQueueTui::VERSION}"
40
40
  exit
41
41
  end
42
42
 
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueueTui
4
+ module Data
5
+ class HourlyStatsQuery
6
+ Result = Struct.new(:data, :total, :peak, :avg, keyword_init: true)
7
+
8
+ def self.enqueued_per_hour
9
+ raw = SolidQueue::Job
10
+ .where(created_at: 24.hours.ago..)
11
+ .group(hour_sql(:created_at))
12
+ .count
13
+ build_result(raw)
14
+ rescue => e
15
+ empty_result
16
+ end
17
+
18
+ def self.processed_per_hour
19
+ raw = SolidQueue::Job
20
+ .where.not(finished_at: nil)
21
+ .where(finished_at: 24.hours.ago..)
22
+ .group(hour_sql(:finished_at))
23
+ .count
24
+ build_result(raw)
25
+ rescue => e
26
+ empty_result
27
+ end
28
+
29
+ def self.failed_per_hour
30
+ raw = SolidQueue::FailedExecution
31
+ .where(created_at: 24.hours.ago..)
32
+ .group(hour_sql(:created_at))
33
+ .count
34
+ build_result(raw)
35
+ rescue => e
36
+ empty_result
37
+ end
38
+
39
+ def self.empty_result
40
+ now = Time.now.utc
41
+ data = (0..23).map { |i| (now - (23 - i) * 3600).strftime("%H").to_i }
42
+ Result.new(data: data.map { 0 }, total: 0, peak: 0, avg: 0)
43
+ end
44
+
45
+ class << self
46
+ private
47
+
48
+ def hour_sql(column)
49
+ if sqlite?
50
+ Arel.sql("strftime('%Y-%m-%d %H:00:00', #{column})")
51
+ else
52
+ Arel.sql("DATE_TRUNC('hour', #{column})")
53
+ end
54
+ end
55
+
56
+ def build_result(raw_hash)
57
+ now = Time.now.utc
58
+
59
+ lookup = {}
60
+ raw_hash.each do |key, count|
61
+ time = key.is_a?(String) ? Time.parse("#{key} UTC") : key
62
+ lookup[time.strftime("%Y-%m-%d %H")] = count
63
+ end
64
+
65
+ # 24-slot array, oldest to newest — values only (for sparkline)
66
+ data = (0..23).map do |i|
67
+ hour_time = now - (23 - i) * 3600
68
+ key = hour_time.strftime("%Y-%m-%d %H")
69
+ lookup[key] || 0
70
+ end
71
+
72
+ total = data.sum
73
+ peak = data.max || 0
74
+ avg = total / 24
75
+
76
+ Result.new(data: data, total: total, peak: peak, avg: avg)
77
+ end
78
+
79
+ def sqlite?
80
+ SolidQueue::Job.connection.adapter_name.downcase.include?("sqlite")
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -5,7 +5,9 @@ module SolidQueueTui
5
5
  class Stats
6
6
  attr_reader :ready, :claimed, :failed, :scheduled, :blocked,
7
7
  :total_jobs, :completed_jobs, :process_count,
8
- :processes_by_kind
8
+ :processes_by_kind,
9
+ :enqueued_per_hour, :processed_per_hour, :failed_per_hour,
10
+ :queue_depths
9
11
 
10
12
  def initialize(data)
11
13
  @ready = data[:ready]
@@ -17,6 +19,10 @@ module SolidQueueTui
17
19
  @completed_jobs = data[:completed_jobs]
18
20
  @process_count = data[:process_count]
19
21
  @processes_by_kind = data[:processes_by_kind]
22
+ @enqueued_per_hour = data[:enqueued_per_hour]
23
+ @processed_per_hour = data[:processed_per_hour]
24
+ @failed_per_hour = data[:failed_per_hour]
25
+ @queue_depths = data[:queue_depths]
20
26
  end
21
27
 
22
28
  def self.fetch
@@ -29,7 +35,11 @@ module SolidQueueTui
29
35
  total_jobs: SolidQueue::Job.count,
30
36
  completed_jobs: SolidQueue::Job.finished.count,
31
37
  process_count: SolidQueue::Process.count,
32
- processes_by_kind: SolidQueue::Process.group(:kind).count
38
+ processes_by_kind: SolidQueue::Process.group(:kind).count,
39
+ enqueued_per_hour: HourlyStatsQuery.enqueued_per_hour,
40
+ processed_per_hour: HourlyStatsQuery.processed_per_hour,
41
+ failed_per_hour: HourlyStatsQuery.failed_per_hour,
42
+ queue_depths: SolidQueue::ReadyExecution.group(:queue_name).count
33
43
  )
34
44
  rescue => e
35
45
  empty(error: e.message)
@@ -39,7 +49,9 @@ module SolidQueueTui
39
49
  new(
40
50
  ready: 0, claimed: 0, failed: 0, scheduled: 0, blocked: 0,
41
51
  total_jobs: 0, completed_jobs: 0, process_count: 0,
42
- processes_by_kind: {}
52
+ processes_by_kind: {},
53
+ enqueued_per_hour: nil, processed_per_hour: nil, failed_per_hour: nil,
54
+ queue_depths: {}
43
55
  )
44
56
  end
45
57
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidQueueTui
4
- VERSION = "0.1.3"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -54,8 +54,9 @@ module SolidQueueTui
54
54
  { key: "j/k", action: "Navigate" },
55
55
  { key: "Enter", action: "Detail" },
56
56
  { key: "/", action: "Filter" },
57
+ clear_filter_binding,
57
58
  { key: "G/g", action: "Bottom/Top" }
58
- ]
59
+ ].compact
59
60
  end
60
61
  end
61
62
 
@@ -84,7 +85,7 @@ module SolidQueueTui
84
85
  in { type: :key, code: "/" }
85
86
  enter_filter_mode
86
87
  nil
87
- in { type: :key, code: "esc" }
88
+ in { type: :key, code: "c" }
88
89
  clear_filter
89
90
  else
90
91
  nil
@@ -116,6 +116,10 @@ module SolidQueueTui
116
116
  "#{base_title} (#{parts.join(', ')})"
117
117
  end
118
118
 
119
+ def clear_filter_binding
120
+ @filters.empty? ? nil : { key: "c", action: "Clear Filter" }
121
+ end
122
+
119
123
  def filter_bindings
120
124
  [
121
125
  { key: "Tab", action: "Next Field" },
@@ -7,7 +7,6 @@ module SolidQueueTui
7
7
 
8
8
  def initialize(tui)
9
9
  @tui = tui
10
- @selected_row = 0
11
10
  end
12
11
 
13
12
  def update(stats:)
@@ -25,7 +24,7 @@ module SolidQueueTui
25
24
  )
26
25
 
27
26
  render_overview_panels(frame, top)
28
- render_completion(frame, bottom)
27
+ render_metrics(frame, bottom)
29
28
  end
30
29
 
31
30
  def handle_input(event)
@@ -47,6 +46,8 @@ module SolidQueueTui
47
46
 
48
47
  private
49
48
 
49
+ # --- Top panels (sticky) ---
50
+
50
51
  def render_overview_panels(frame, area)
51
52
  return unless @stats
52
53
 
@@ -129,28 +130,228 @@ module SolidQueueTui
129
130
  )
130
131
  end
131
132
 
132
- def render_completion(frame, area)
133
+ # --- Bottom section: chart + queue summary ---
134
+
135
+ def render_metrics(frame, area)
133
136
  return unless @stats
134
137
 
135
- lines = [
136
- @tui.text_line(spans: [
137
- @tui.text_span(content: " Total: ", style: @tui.style(fg: :dark_gray)),
138
- @tui.text_span(content: format_number(@stats.total_jobs), style: @tui.style(fg: :white, modifiers: [:bold])),
139
- @tui.text_span(content: " Completed: ", style: @tui.style(fg: :dark_gray)),
140
- @tui.text_span(content: format_number(@stats.completed_jobs), style: @tui.style(fg: :green))
138
+ chart_area, bottom_area = @tui.layout_split(
139
+ area,
140
+ direction: :vertical,
141
+ constraints: [
142
+ @tui.constraint_percentage(70),
143
+ @tui.constraint_percentage(30)
144
+ ]
145
+ )
146
+
147
+ queue_area, summary_area = @tui.layout_split(
148
+ bottom_area,
149
+ direction: :horizontal,
150
+ constraints: [
151
+ @tui.constraint_percentage(50),
152
+ @tui.constraint_percentage(50)
153
+ ]
154
+ )
155
+
156
+ render_throughput_chart(frame, chart_area)
157
+ render_queue_depth(frame, queue_area)
158
+ render_summary(frame, summary_area)
159
+ end
160
+
161
+ def render_throughput_chart(frame, area)
162
+ enqueued = @stats.enqueued_per_hour
163
+ processed = @stats.processed_per_hour
164
+ failed = @stats.failed_per_hour
165
+
166
+ # Convert 24-element arrays to [x, y] coordinate pairs
167
+ enqueued_data = to_chart_data(enqueued)
168
+ processed_data = to_chart_data(processed)
169
+ failed_data = to_chart_data(failed)
170
+
171
+ # Y-axis bounds
172
+ all_values = [enqueued&.data, processed&.data, failed&.data].compact.flatten
173
+ y_max = (all_values.max || 10).to_f
174
+ y_max = 10.0 if y_max == 0
175
+
176
+ # X-axis labels at key positions
177
+ now = Time.now.utc
178
+ x_labels = [0, 6, 12, 18, 23].map do |i|
179
+ (now - (23 - i) * 3600).strftime("%H:%M")
180
+ end
181
+
182
+ # Y-axis labels
183
+ y_labels = ["0", format_number((y_max / 2).round), format_number(y_max.round)]
184
+
185
+ datasets = []
186
+ if enqueued_data.any?
187
+ datasets << RatatuiRuby::Widgets::Dataset.new(
188
+ name: "",
189
+ data: enqueued_data,
190
+ style: @tui.style(fg: :cyan),
191
+ marker: :braille,
192
+ graph_type: :line
193
+ )
194
+ end
195
+
196
+ if processed_data.any?
197
+ datasets << RatatuiRuby::Widgets::Dataset.new(
198
+ name: "",
199
+ data: processed_data,
200
+ style: @tui.style(fg: :green),
201
+ marker: :braille,
202
+ graph_type: :line
203
+ )
204
+ end
205
+
206
+ if failed_data.any?
207
+ datasets << RatatuiRuby::Widgets::Dataset.new(
208
+ name: "",
209
+ data: failed_data,
210
+ style: @tui.style(fg: :red),
211
+ marker: :braille,
212
+ graph_type: :line
213
+ )
214
+ end
215
+
216
+ x_axis = RatatuiRuby::Widgets::Axis.new(
217
+ bounds: [0.0, 23.0],
218
+ labels: x_labels,
219
+ style: @tui.style(fg: :dark_gray)
220
+ )
221
+
222
+ y_axis = RatatuiRuby::Widgets::Axis.new(
223
+ bounds: [0.0, y_max],
224
+ labels: y_labels,
225
+ style: @tui.style(fg: :dark_gray)
226
+ )
227
+
228
+ chart = @tui.chart(
229
+ datasets: datasets,
230
+ x_axis: x_axis,
231
+ y_axis: y_axis,
232
+ block: @tui.block(
233
+ title: " Throughput (24h) ",
234
+ title_style: @tui.style(fg: :cyan, modifiers: [:bold]),
235
+ titles: [
236
+ { content: " ● Enqueued ", position: :top, alignment: :right, style: @tui.style(fg: :cyan) },
237
+ { content: "● Processed ", position: :top, alignment: :right, style: @tui.style(fg: :green) },
238
+ { content: "● Failed ", position: :top, alignment: :right, style: @tui.style(fg: :red) }
239
+ ],
240
+ borders: [:all],
241
+ border_type: :rounded,
242
+ border_style: @tui.style(fg: :dark_gray)
243
+ )
244
+ )
245
+
246
+ frame.render_widget(chart, area)
247
+ end
248
+
249
+ def render_queue_depth(frame, area)
250
+ lines = []
251
+
252
+ queue_depths = @stats.queue_depths
253
+ if queue_depths.any?
254
+ total_depth = queue_depths.values.sum
255
+ sorted = queue_depths.sort_by { |_, v| -v }
256
+ top_queues = sorted.first(5)
257
+ remaining = sorted.drop(5)
258
+ max_depth = top_queues.first&.last || 1
259
+
260
+ top_queues.each_with_index do |(name, count), idx|
261
+ pct = total_depth > 0 ? (count.to_f / total_depth * 100).round(1) : 0
262
+ bar_width = 20
263
+ filled = max_depth > 0 ? (count.to_f / max_depth * bar_width).round : 0
264
+ empty_bar = bar_width - filled
265
+
266
+ lines << @tui.text_line(spans: [
267
+ @tui.text_span(content: " #{name.ljust(14)}", style: @tui.style(fg: :white)),
268
+ @tui.text_span(content: "#{"█" * filled}", style: @tui.style(fg: :cyan)),
269
+ @tui.text_span(content: "#{"░" * empty_bar}", style: @tui.style(fg: :dark_gray)),
270
+ @tui.text_span(content: " #{format_number(count).rjust(6)} (#{pct}%)", style: @tui.style(fg: :dark_gray))
271
+ ])
272
+
273
+ if idx < top_queues.size - 1
274
+ lines << @tui.text_line(spans: [
275
+ @tui.text_span(content: "", style: @tui.style(fg: :dark_gray))
276
+ ])
277
+ end
278
+ end
279
+
280
+ if remaining.any?
281
+ others_count = remaining.sum { |_, v| v }
282
+ pct = total_depth > 0 ? (others_count.to_f / total_depth * 100).round(1) : 0
283
+ lines << @tui.text_line(spans: [
284
+ @tui.text_span(content: " +#{remaining.size} more", style: @tui.style(fg: :dark_gray)),
285
+ @tui.text_span(content: " #{format_number(others_count).rjust(26)} (#{pct}%)", style: @tui.style(fg: :dark_gray))
286
+ ])
287
+ end
288
+ else
289
+ lines << @tui.text_line(spans: [
290
+ @tui.text_span(content: " No queued jobs", style: @tui.style(fg: :dark_gray))
141
291
  ])
142
- ]
292
+ end
293
+
294
+ frame.render_widget(
295
+ @tui.paragraph(
296
+ text: lines,
297
+ block: @tui.block(
298
+ title: " Queue Depth ",
299
+ title_style: @tui.style(fg: :cyan, modifiers: [:bold]),
300
+ borders: [:all],
301
+ border_type: :rounded,
302
+ border_style: @tui.style(fg: :dark_gray)
303
+ )
304
+ ),
305
+ area
306
+ )
307
+ end
308
+
309
+ def render_summary(frame, area)
310
+ lines = []
311
+
312
+ # Throughput totals (24h)
313
+ enq_total = @stats.enqueued_per_hour&.total || 0
314
+ proc_total = @stats.processed_per_hour&.total || 0
315
+ fail_total = @stats.failed_per_hour&.total || 0
316
+
317
+ [
318
+ ["Enqueued", enq_total, :cyan],
319
+ ["Processed", proc_total, :green],
320
+ ["Failed", fail_total, :red]
321
+ ].each do |label, value, color|
322
+ lines << @tui.text_line(spans: [
323
+ @tui.text_span(content: " #{label.ljust(12)}", style: @tui.style(fg: color)),
324
+ @tui.text_span(content: format_number(value).rjust(10), style: @tui.style(fg: :white, modifiers: [:bold]))
325
+ ])
326
+ end
327
+
328
+ # Separator
329
+ lines << @tui.text_line(spans: [
330
+ @tui.text_span(content: "", style: @tui.style(fg: :dark_gray))
331
+ ])
332
+
333
+ # Overall totals + completion bar
334
+ [
335
+ ["Total", @stats.total_jobs, :white],
336
+ ["Completed", @stats.completed_jobs, :green]
337
+ ].each do |label, value, color|
338
+ lines << @tui.text_line(spans: [
339
+ @tui.text_span(content: " #{label.ljust(12)}", style: @tui.style(fg: :dark_gray)),
340
+ @tui.text_span(content: format_number(value).rjust(10), style: @tui.style(fg: color, modifiers: [:bold]))
341
+ ])
342
+ end
143
343
 
144
344
  if @stats.total_jobs > 0
145
- completed_ratio = @stats.completed_jobs.to_f / @stats.total_jobs
146
- bar_width = 40
147
- filled = (completed_ratio * bar_width).round
148
- empty = bar_width - filled
345
+ ratio = @stats.completed_jobs.to_f / @stats.total_jobs
346
+ bar_w = 30
347
+ filled = (ratio * bar_w).round
348
+ empty_bar = bar_w - filled
149
349
 
150
350
  lines << @tui.text_line(spans: [
151
- @tui.text_span(content: " Completion: ", style: @tui.style(fg: :dark_gray)),
152
- @tui.text_span(content: "#{'' * filled}#{'░' * empty}", style: @tui.style(fg: :green)),
153
- @tui.text_span(content: " #{(completed_ratio * 100).round(1)}%", style: @tui.style(fg: :white))
351
+ @tui.text_span(content: " ", style: @tui.style(fg: :dark_gray)),
352
+ @tui.text_span(content: "#{"" * filled}", style: @tui.style(fg: :green)),
353
+ @tui.text_span(content: "#{"░" * empty_bar}", style: @tui.style(fg: :dark_gray)),
354
+ @tui.text_span(content: " #{(ratio * 100).round(1)}%", style: @tui.style(fg: :white))
154
355
  ])
155
356
  end
156
357
 
@@ -158,7 +359,7 @@ module SolidQueueTui
158
359
  @tui.paragraph(
159
360
  text: lines,
160
361
  block: @tui.block(
161
- title: " Overview ",
362
+ title: " Summary ",
162
363
  title_style: @tui.style(fg: :cyan, modifiers: [:bold]),
163
364
  borders: [:all],
164
365
  border_type: :rounded,
@@ -169,6 +370,13 @@ module SolidQueueTui
169
370
  )
170
371
  end
171
372
 
373
+ # --- Helpers ---
374
+
375
+ def to_chart_data(result)
376
+ return [] unless result
377
+ result.data.each_with_index.map { |v, i| [i.to_f, v.to_f] }
378
+ end
379
+
172
380
  def status_line(label, value, color)
173
381
  bar_char = value.to_i > 0 ? "●" : "○"
174
382
  @tui.text_line(spans: [
@@ -65,8 +65,9 @@ module SolidQueueTui
65
65
  { key: "R", action: "Retry" },
66
66
  { key: "D", action: "Discard" },
67
67
  { key: "A", action: "Retry All" },
68
- { key: "/", action: "Filter" }
69
- ]
68
+ { key: "/", action: "Filter" },
69
+ clear_filter_binding
70
+ ].compact
70
71
  end
71
72
  end
72
73
 
@@ -104,7 +105,7 @@ module SolidQueueTui
104
105
  in { type: :key, code: "/" }
105
106
  enter_filter_mode
106
107
  nil
107
- in { type: :key, code: "esc" }
108
+ in { type: :key, code: "c" }
108
109
  clear_filter
109
110
  else
110
111
  nil
@@ -54,9 +54,9 @@ module SolidQueueTui
54
54
  { key: "j/k", action: "Navigate" },
55
55
  { key: "Enter", action: "Detail" },
56
56
  { key: "/", action: "Filter" },
57
- { key: "Esc", action: "Clear Filter" },
57
+ clear_filter_binding,
58
58
  { key: "G/g", action: "Bottom/Top" }
59
- ]
59
+ ].compact
60
60
  end
61
61
  end
62
62
 
@@ -85,7 +85,7 @@ module SolidQueueTui
85
85
  in { type: :key, code: "/" }
86
86
  enter_filter_mode
87
87
  nil
88
- in { type: :key, code: "esc" }
88
+ in { type: :key, code: "c" }
89
89
  clear_filter
90
90
  else
91
91
  nil
@@ -54,8 +54,9 @@ module SolidQueueTui
54
54
  { key: "j/k", action: "Navigate" },
55
55
  { key: "Enter", action: "Detail" },
56
56
  { key: "/", action: "Filter" },
57
+ clear_filter_binding,
57
58
  { key: "G/g", action: "Bottom/Top" }
58
- ]
59
+ ].compact
59
60
  end
60
61
  end
61
62
 
@@ -84,7 +85,7 @@ module SolidQueueTui
84
85
  in { type: :key, code: "/" }
85
86
  enter_filter_mode
86
87
  nil
87
- in { type: :key, code: "esc" }
88
+ in { type: :key, code: "c" }
88
89
  clear_filter
89
90
  else
90
91
  nil
@@ -101,9 +101,10 @@ module SolidQueueTui
101
101
  { key: "j/k", action: "Navigate" },
102
102
  { key: "Enter", action: "Detail" },
103
103
  { key: "/", action: "Filter" },
104
+ clear_filter_binding,
104
105
  { key: "Esc", action: "Back" },
105
106
  { key: "G/g", action: "Bottom/Top" }
106
- ]
107
+ ].compact
107
108
  end
108
109
  end
109
110
  end
@@ -233,6 +234,8 @@ module SolidQueueTui
233
234
  in { type: :key, code: "/" }
234
235
  enter_filter_mode
235
236
  nil
237
+ in { type: :key, code: "c" }
238
+ clear_filter
236
239
  in { type: :key, code: "esc" }
237
240
  exit_detail_mode
238
241
  else
@@ -67,10 +67,11 @@ module SolidQueueTui
67
67
  { key: "j/k", action: "Navigate" },
68
68
  { key: "Enter", action: "Detail" },
69
69
  { key: "/", action: "Filter" },
70
+ clear_filter_binding,
70
71
  { key: "N", action: "Run Now" },
71
72
  { key: "D", action: "Discard" },
72
73
  { key: "G/g", action: "Bottom/Top" }
73
- ]
74
+ ].compact
74
75
  end
75
76
  end
76
77
 
@@ -101,7 +102,7 @@ module SolidQueueTui
101
102
  in { type: :key, code: "/" }
102
103
  enter_filter_mode
103
104
  nil
104
- in { type: :key, code: "esc" }
105
+ in { type: :key, code: "c" }
105
106
  clear_filter
106
107
  else
107
108
  nil
@@ -5,6 +5,7 @@ require_relative "solid_queue_tui/formatting_helpers"
5
5
 
6
6
  # Data layer
7
7
  require_relative "solid_queue_tui/data/stats"
8
+ require_relative "solid_queue_tui/data/hourly_stats_query"
8
9
  require_relative "solid_queue_tui/data/jobs_query"
9
10
  require_relative "solid_queue_tui/data/queues_query"
10
11
  require_relative "solid_queue_tui/data/processes_query"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_queue_tui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shiva Reddy
@@ -84,11 +84,13 @@ description: Real-time terminal dashboard to monitor and manage Solid Queue jobs
84
84
  Built with ratatui_ruby for native Rust rendering performance.
85
85
  email:
86
86
  executables:
87
+ - qtop
87
88
  - sqtui
88
89
  extensions: []
89
90
  extra_rdoc_files: []
90
91
  files:
91
92
  - LICENSE.txt
93
+ - exe/qtop
92
94
  - exe/sqtui
93
95
  - lib/solid_queue_tui.rb
94
96
  - lib/solid_queue_tui/actions/discard_job.rb
@@ -103,6 +105,7 @@ files:
103
105
  - lib/solid_queue_tui/components/help_bar.rb
104
106
  - lib/solid_queue_tui/components/job_table.rb
105
107
  - lib/solid_queue_tui/data/failed_query.rb
108
+ - lib/solid_queue_tui/data/hourly_stats_query.rb
106
109
  - lib/solid_queue_tui/data/jobs_query.rb
107
110
  - lib/solid_queue_tui/data/processes_query.rb
108
111
  - lib/solid_queue_tui/data/queues_query.rb